Comparable 인터페이스의 메서드인 compareTo는 단순 동치성 비교에 더해 순서까지 비교하는 메서드이다.
자바 플랫폼 라이브러리 모든 값 클래스와 열거타입은 Comparable을 구현했다.
알파벳, 숫자, 연대 같은 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현해라
comparable, Comparator는 왜 사용 할까? primitive 타입은 부등호로 쉽게 값을 비교하지만 우리가 만든 클래스는 무엇을 기준으로 객체를 비교할지 알 수없기 때문에 사용자가 기준을 정해주지 않는 이상 어떤 객체가 더 높은 우선순위를 갖는지 판단할 수 없다. 이름을 비교할 때 성을 우선순위로 생각하고 같은 객체인지 이름 순으로 할 지 알 수없다.
compareTo 메서드의 일반규약
- Object.equals에 더해서 순서까지 비교할 수 있으며 Generic을 지원한다.
- 자기 자신이(this)이 compareTo에 전달된 객체보다 작으면 음수, 같으면 0, 크다면 양수를 리턴한다. 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
- 반사성, 대칭성, 추이성을 만족해야한다.
- 반드시 따라야하는 것은 아니지만 x.compareTo(y) == 0 이라면 x.equals(y)가 true 여야 한다.
각자 구현한 곳에서 오버라이딩시 리턴되는 값이 다를 수 있다.
BigDecimal은 음수면 -1, 같으면 0, 양수면 1을 리턴한다.
분기처리할 때
if( i == -1) {
...
} else if( i == 0 ) {
...
} else if( i == 1 ) {
...
}
이 것보다
if( i <= -1) {
...
} else if( i == 0 ) {
...
} else if( i >= 1 {
...
}
이게 나은걸까~
package item14;
import java.math.BigDecimal;
public class compareToConvention {
public static void main(String[] args) {
BigDecimal n1 = BigDecimal.valueOf(2312342);
BigDecimal n2 = BigDecimal.valueOf(1234345);
BigDecimal n3 = BigDecimal.valueOf(6757654);
BigDecimal n4 = BigDecimal.valueOf(1234345);
// 반사성
// x.compareTo(x) == 0
System.out.println(n1.compareTo(n1));
// 대칭성
// x.compareTo(y) == y.compareTo(x)
System.out.println(n2.compareTo(n1));
System.out.println(n1.compareTo(n2));
// 추이성
// x.compareTo(y) > 0 && y.compareTo(z) > 0 이면 x.compareTo(z) > 0
System.out.println(n3.compareTo(n1) > 0);
System.out.println(n1.compareTo(n2) > 0);
System.out.println(n3.compareTo(n2) > 0);
// 일관성
// x.compareTo(y) == 0 이면 (x.compareTo(z) == y.compareTo(z))
System.out.println(n4.compareTo(n2));
System.out.println(n2.compareTo(n1));
System.out.println(n4.compareTo(n1));
// compareTo가 0 이라면 equals는 true여야 한다. 아닐수도 있다
// (x.compareTo(y) == 0) == (x.equals(y))
BigDecimal oneZero = new BigDecimal("1.0");
BigDecimal oneZeroZero = new BigDecimal("1.00");
// Tree나 TreeMap에서 사용시 같다고 오버라이딩 돼서 둘 중 하나밖에 안들어간다.
System.out.println(oneZero.compareTo(oneZeroZero));
// 순서가 없는 컬렉션일 경우 다른 것으로 오버라이딩 돼서 false가 나온다.
// 소수점 뒷자리까지 비교 검사한다.
System.out.println(oneZero.equals(oneZeroZero));
} // end of main
} // end of class
Comparable 구현방법
구현방법1
- 자연적인 순서를 제공할 클래스에 implements Comparatable<T> 를 선언한다.
- compareTo 메서드를 재정의 한다.
- compareTo 메서드 안에서 기본 타입은 박싱된 기본 타입의 compare을 사용해 비교한다.
- 핵심 필드가 여러개라면 비교 순서가 중요하다. 순서를 결정하는데 있어서 가장 중요한 필드를 비교하고 그 값이 0 이라면 다음 필드를 비교한다.
- 기존 클래스를 확장하고 필드를 추가하는 경우 compareTo 규약을 지킬 수 없다.
- Composition을 활용할 것.
package item14;
import java.util.HashMap;
import java.util.Map;
public final class PhoneNumber implements Cloneable, Comparable<PhoneNumber>{
private final int areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
super();
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999,"프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
}
private static int rangeCheck(int val, int max, String arg) {
if(val < 0 || val > max) {
throw new IllegalArgumentException(arg + " : " + val);
}
return val;
}
/**
* 전화번호의 문자열 형식은 "XXX-YYY-ZZZZ" 형태의 12글자
* XXX는 지역코드, YYY는 프리픽스, ZZZZ는 가입자 번호
* 각 대문자는 10진수 숫자를 나타낸다.
* 각 부분의 자릿수를 채울 수 없다면 앞에서부터 0으로 채운다.
*/
@Override
public String toString() {
return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}
// PhoneNumber 클래스의 정보를 제공하는 팩터리 메서드
public static PhoneNumber of(String phoneNumberString) {
String[] split = phoneNumberString.split("-");
PhoneNumber pn = new PhoneNumber(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2]));
return pn;
}
@Override
public boolean equals(Object obj) {
if(obj == this) return true;
if(!(obj instanceof PhoneNumber)) return false;
PhoneNumber pn = (PhoneNumber) obj;
return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
}
@Override
public int hashCode() {
int result = Integer.hashCode(areaCode);
result = 31 * result + Integer.hashCode(prefix);
result = 31 * result + Integer.hashCode(lineNum);
return result;
}
// 메서드 overriding시 접근 제어자는 원본 메서드 보다 범위를 넓게 가질 수 있다.
// private, default 제어자는 protected보다 범위가 더 좁기 때문에 선언 불가
// 외부 객체에서 사용할 수 있게 public으로 선언
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public static void main(String[] args) {
PhoneNumber jenny = new PhoneNumber(707, 789, 1304);
Map<PhoneNumber, String> m = new HashMap<>();
m.put(jenny, "jenny");
PhoneNumber clone = jenny.clone();
System.out.println(m.get(clone)); // jenny
System.out.println(clone != jenny); // true
System.out.println(clone.getClass() == jenny.getClass()); // true
System.out.println(clone.equals(jenny)); // true
} // end of main
// 컴파일 시점에서부터 어떤 타입의 파라미터를 받아와야 할지 명확하다.
// 어떤 타입으로 받아야 하는지 고민할 필요 없음
// 잘못된 파라미터를 선언 했을 경우 컴파일 타임에서 에러가 난다.
@Override
public int compareTo(PhoneNumber pn) {
int result = Integer.compare(areaCode, pn.areaCode);
if(result == 0) {
result = Integer.compare(prefix, pn.prefix);
if(result == 0) {
result = Integer.compare(lineNum, pn.lineNum);
}
}
return result;
}
}// end of class
package item14.composition;
public class Point implements Comparable<Point> {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
Point p = (Point) obj;
return p.x == x && p.y == y;
}
@Override
public int compareTo(Point point) {
int result = Integer.compare(this.x, point.x);
if(result == 0) {
result = Integer.compare(this.y, point.y);
}
return result;
}
@Override
public int hashCode() {
return 31 * x + y;
}
}
package item14.composition;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class NamedPoint implements Comparable<NamedPoint>{
// 상속을 사용해서 필드값이 추가될 경우 compareTo 규약을 지킬 수 없으니
// Composition을 사용하라
private final Point point;
private final String name;
public NamedPoint(Point point, String name) {
this.point = point;
this.name = name;
}
// 뷰 제공
public Point getPoint() {
return this.point;
}
@Override
public int compareTo(NamedPoint namedPoint) {
// 가져온 Point 먼저 비교 하고
int result = this.point.compareTo(namedPoint.point);
// 후에 추가한 필드 비교
if(result == 0) {
result = this.name.compareTo(namedPoint.name);
}
return result;
}
}
Comparable을 구현한 클래스를 상속받아 쓴다면 하위 객체는 Comparable을 다시 구현할 수 없음. 필드값이 추가됐을때
equals 규약을 지킬 수 없었던 것처럼 Comparable도 is-a 관계 맺어서 값 비교하는게 나음
구현방법 2
Comparator 인터페이스를 통한 구현 compare(T o1, T o2) 오버라이딩
Comparable과 Comparator의 차이는
Comparable은 자기 자신과 매개변수 객체를 비교하고
Comparator는 파라미터로 들어오는 두 객체를 비교한다.
https://st-lab.tistory.com/243
정수 Overflow, Underflow
숫자 타입의 표현 범위가 넘어갈 경우 예상하는 값이 안나올 경우가 있다.
'Effective Java > 정리' 카테고리의 다른 글
Item16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2023.02.23 |
---|---|
Item15. 클래스와 멤버의 접근 권한을 최소화하라. (0) | 2023.02.23 |
Item.13 clone 재정의는 주의해서 진행하라. (0) | 2023.02.05 |
Item12. toString을 항상 재정의하라 (0) | 2023.02.04 |
Item11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2023.01.31 |