제육's 휘발성 코딩
article thumbnail
반응형

Comparable을 구현할지 고려하라

객체를 정렬하기 위한 인터페이스로 자바에선 Comparable 인터페이스와 Comparator 인터페이스 두 가지가 존재 한다. 정확히는 정렬하기 위한 인터페이스라기 보단, 객체를 비교할 수 있는 인터페이스라고 보는 것이 바람직하다.

public class PointTest {
  public static void main(String[] args) {
    Point a = new Point(1, 2);
    Point b = new Point(2, 3);
  }

  static class Point {
    int x, y;

    public Point(int x, int y) {
      this.x = x;
      this.y = y;
    }
  }
}
  • 다음과 같은 Point 객체는 부등호 비교뿐만 아니라 객체를 어떻게 비교할지에 대한 기준이 있어야한다. 기준 없이 sorting 하게 되면 ClassCastException과 같은 예외가 발생할 것이다. 따라서 이러한 객체를 비교할 수 있는 기능을 인터페이스를 통해 제공한다.

 

Comparable vs Comparator

두 인터페이스는 비교하는 것이 같지만, 비교 대상이 다르다. 파라미터만 봐도 Comparable은 compareTo(T o), Comparator의 compare(T o1, T o2)로 갯수가 다르다. 또한, Comparator는 lang 패키지에 있으므로 import를 해줄 필요가 없지만, Comparator는 util 패키지에 있으므로 import를 해줘야 한다.

 

Comparable은 자기 자신과 매개변수 객체를 비교하는 것이고, Comparator는 두 매개변수 객체를 비교하는 것이다.

 

package effective.item14;

import java.util.Arrays;
import java.util.Comparator;

public class PointTest {
  public static void main(String[] args) {
    Point[] points = new Point[5];
    for (int i=0; i<5; i++)
      points[i] = new Point(i+1, i+2);

    Point[] points2 = deepCopy(points);

        // x 기준 내림차순, Arrays.sort 의 두 번째 파라미터로 Comparator 객체를 넣을 수 있다.
    Arrays.sort(points, comp); 
    print(points);

    System.out.println("======="); 
    Arrays.sort(points2); // x 기준 오름차순
    print(points2);

  }

  static Comparator<Point> comp = new Comparator<Point>() {
    @Override
    public int compare(Point o1, Point o2) {
      return Integer.compare(o2.x, o1.x); // 내림차순 
    }
  };

  static class Point implements Comparable<Point> {
    int x, y;

    public Point(int x, int y) {
      this.x = x;
      this.y = y;
    }

    @Override
    public int compareTo(Point o) {
      return Integer.compare(this.x, o.x);
    }
  }

  static Point[] deepCopy(Point[] original) {
    Point[] copyArr = new Point[original.length];
    for (int i=0; i<original.length; i++)
      copyArr[i] = original[i];
    return copyArr;
  }

  static void print(Point[] arr) {
    for (Point x : arr)
      System.out.println(x.x +" " + x.y);
  }
}
  • 코드를 보고 눈치 챘을 수도 있다. Comparator 인터페이스는 외부에서 익명 객체로 규칙을 정의할 수 있기 때문에 여러 개를 생성할 수 있지만, Comparable 인터페이스는 객체 자신을 내부에서 정의하기 때문에 하나 밖에 구현할 수 없는 것이다.
  • 객체 규칙을 Comparable을 통해 정해두었지만, Compartor로 외부 규칙을 정하면 우선 순위가 Comparator가 높아진다. (코드 상 오름차순에서 내림차순으로)
  • 정렬 규칙은 Integer.compare() 를 사용했는데, 이 부분은 - 나 부등호 연산으로도 가능하지만, 오류를 유발할 수 있으므로 사용한다. (래퍼 클래스만 가능, String의 경우 compareTo 사용)
    • compare, compareTo 메서드는 음수, 0 , 양수 중의 값을 반환한다. 음수의 경우 두 객체의 위치를 바꾼다. 이 때 - 연산자를 사용한 경우 (Integer.MIN_VALUE - 1), (Integer.MAX_VALUE + 1)과 같은 경우에 언더플로우, 오버플로우 등의 잘못된 결과를 가질 수 있다.

 

equals와 compareTo 관계

equals 메서드와 compareTo는 일관성 되어야 한다. 다음 일관되지 않은 BigDecimal 클래스를 살펴보자.

import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class BigDecimalTest {
  public static void main(String[] args) {
    final BigDecimal bigDecimal1 = new BigDecimal("1.0");
    final BigDecimal bigDecimal2 = new BigDecimal("1.00");

    Set<BigDecimal> hashSet = new HashSet<>();
    hashSet.add(bigDecimal1);
    hashSet.add(bigDecimal2);

    Set<BigDecimal> treeSet = new TreeSet<>();
    treeSet.add(bigDecimal1);
    treeSet.add(bigDecimal2);

    System.out.println(hashSet.size()); // 2
    System.out.println(treeSet.size()); // 1
  }
}
  • 다음 결과를 보면 size의 결과가 다른 것을 확인할 수 있다. HashSetequals를 기반으로 비교를하기 때문에 다른 객체로 인식되어 크기가 2가 되고, TreeSetcompareTo를 기반으로 동치성을 비교하기 때문에 같은 값으로 인식되어 compareTo가 0을 반환하기 때문에 크기가 1이 된다.

 

Comparator

자바 8에서부터 Comparator 인터페이스가 일련의 비교자 생성 메서드와 메서드 연쇄방식으로 비교자를 생성할 수 있게 되었다.

Comparator는 comparingIntthenComparingInt 등의 숫자용 기본 타입을 커버하는 보조 생성 메서드들을 갖고 있다.

static final Comparator<Point> COMPARATOR =
  Comparator.comparingInt((Point point) -> point.y) // 1. y를 기준으로 오름차순
  .thenComparingInt(point -> point.x);                            // 2. x를 기준으로 오름차순
  • comparingInt는 int 타입 키에 매핑하는 키 추출 함수를 인수로 받아 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메소드이다.
  • thenComparingInt는 첫 번째 비교자를 적용한 후 새로운 키로 추가 비교하는 경우 수행한다. 여러 비교할 필드가 존재할 경우 계속해서 체이닝으로 사용가능하다.

 

static Comparator<Object> hashCoceOrder = new Comparator<>() {
  public int compare(Object o1, Object o2) {
    return o1.hashCode() - o2.hashCode();
  }
};
  • 해시코드 값의 차를 기준으로 Comparator를 사용하는 경우 정수 오버플로우나 부동소수점 계산 방식에 따른 오류를 발생할 수 있으므로 해당 방식으로 -는 사용하면 안된다.

 

static Comparator<Object> hashCodeOrder = new Comparator<>() {
  public int compare(Object o1, Object o2) {
    return Integer.compare(o1.hashCode(), o2.hashCode());
  }
};

static Comparator<Object> hashCoder = Comparator.comparingInt(o -> o.hashCode());
  • 다음과 같이 정적 compare 메서드 또는 비교자 생성 메서드를 활용하여 구현하는 것이 바람직하다.

 

실수 표현 방식

위의 예시 코드에서 부동소수점 계산 방식에 따른 오류에 대해 언급이 되었는데, 간단하게 살펴보자.

컴퓨터는 2진수로만 표현하는데 실수의 경우 정수에 비해 복잡하다. 현재에는 실수를 2진수로 변환하기 위한 방식으로 고정 소수점 방식과 부동 소수점 방식이 있다. 대부분 부동 소수점 방식을 사용하고 있다.

 

고정 소수점 방식

image

  • 실수는 정수부와 소수부로 나눌 수 있으며, 고정 소수점 방식은 고정된 자릿수의 소수를 표현하는 것이다. 예를 들어 위의 그림과 같이 32비트 실수를 고정 소수점 방식으로 표현한다면 다음과 같다. 해당 방식은 정수부와 소수부의 자릿수가 크지 않기 때문에 표현할 수 있는 범위가 적어진다.

 

부동 소수점 방식

실수를 정수부와 소수부가 아닌, 가수부와 지수부로 나누어 표현한 경우이다. 대부분 IEEE 754 표준을 따르고 있다.

IEEE float형 부동 소수점 방식

image

  • float형 타입은 소수 6자리 까지는 정확하게 표현할 수 있으나, 그 이상은 정확하게 표현하지 못한다.

IEEE double형 부동 소수점 방식

image

float num = 1.23456789f;
double num2 = 1.23456789123456789;
System.out.println(num);  // 1.2345679
System.out.println(num2); // 1.234567891234568

double v1 = 12.23;
double v2 = 34.45;
System.out.println(v1 + v2); // 46.68000000000001

BigDecimal bv1 = new BigDecimal(String.valueOf(v1));
//        BigDecimal bv1 = BigDecimal.valueOf(12.23); // 동일 방식
BigDecimal bv2 = new BigDecimal(String.valueOf(v2));
System.out.println(bv1.add(bv2)); // 46.68
  • 다음과 같은 실수 연산은 소숫점 단위 값을 정확히 표현하는 것이 아니라 근사값으로 처리하기 때문에 오차가 발생한다. 이 문제를 해결하기 위해 BigDecimal이라는 클래스를 통해 생성자와 파라미터로 문자열, 또는 정적 팩토리 메서드로 값을 넣어서 계산한다.

 

정리

순서를 고려해야 하는 객체를 다를 경우 Comparable 또는 Comparator 인터페이스를 구현하자.

이 때 객체 내부에서 공통된 처리를 할지, 생성된 인스턴스 마다 새로운 규칙을 적용할 지의 여부에 따라 선택하며, 필드의 값을 비교할 때 부등호가 아닌 래퍼 클래스에서 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.

 


REFERENCES

https://st-lab.tistory.com/243

http://www.tcpschool.com/java/java_datatype_floatingPointNumber

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^