꾸준한 스터디
article thumbnail

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

 

자바 [JAVA] - Comparable 과 Comparator의 이해

아마 이 글을 찾아 오신 분들 대개는 Comparable과 Comparator의 차이가 무엇인지 모르거나 궁금해서 찾아오셨을 것이다. 사실 알고보면 두 개는 그렇게 어렵지 않으나 아무래도 자바를 학습하면서 객

st-lab.tistory.com

 

정수 Overflow, Underflow

숫자 타입의 표현 범위가 넘어갈 경우 예상하는 값이 안나올 경우가 있다.

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!