2022. 2. 23. 16:17ㆍJava
알고리즘 문제를 풀며, 간간이 비교가 필요한 순간이 있다.
primitive type같은 경우는 해당 과정이 간단하다.
package com.jimkwon;
public class Main {
public static void main(String[] args) {
int a = 3, b = 5;
String str1 = "abc", str2 = "bbc";
System.out.println(a > b);
System.out.println(a < b);
System.out.println(str1.equals(str2));
// 설마 문자열인데 ==로 비교하진 않겠지?!
}
}
하지만 만약
1. 비교 대상이 객체라면?
2. 비교 대상이 Map과 같은 자료구조라면?
먼저 1번의 경우 간단한 Class를 예로 들어보자.
Dog 객체의 인스턴스 mix, maltix를 생성했다.
두 인스턴스를 비교하고 싶을 때, 어떤 것 기준으로 비교할지 컴파일러는 알 수 없다.
Map도 마찬가지다. key로 비교할 것인가? value로 비교할 것인가?
이러한 객체의 비교 기준을 사용자가 직접 정의할수 있는 것이 Comparable, Comparator 인터페이스다.
그럼 두 인터페이스의 차이는 무엇일까?
Comparable | Comparator | |
메소드의 매개변수 | 나 자신과 매개변수 1개를 비교한다. | 매개변수 2개의 값을 비교한다. |
정렬 기준 | 기본적인 정렬(오름차순, 내림차순) | 사용자가 정한 규칙을 기준으로 정렬 |
위의 표대로 크게 두 가지의 차이점을 알 수 있다.
구체적인 내용을 살펴보기 위해 먼저 Comparable부터 보도록 하자
1. Comparable 인터페이스 - compareTo 메소드 사용해보기
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Comparable.html
공식문서에 정의되어있는 Comparable 인터페이스이다.
첫 정의 부분을 살펴보면
이 인터페이스는 해당 인터페이스를 구현하는 각 클래스의 개체에 전체 순서를 적용합니다.
이 순서를 클래스의 고유 순서(natural ordering)라고 하며
클래스의 compareTo 메소드를 자연 비교(natural comparison) 메소드 라고 합니다.
Comparable은 인터페이스 이기 때문에 '구현체'가 필요하다.
해당 인터페이스에서 구현해야 하는 int compareTo(T o) 메소드에 대해 알아보자.
int compareTo(T o)
Parameters:
o - 비교될 오브젝트(객체)이다.
Returns:
비교되는 o 오브젝트보다 작으면 -1(음수), 같으면 0, 크면 1(양수)를 반환한다.
Throws:
NullPointerException - 지정한 개체 o가 null인 경우
ClassCastException - o가 비교할 다른 class 타입일 경우
정의를 살펴보았으니 실제로 사용되는 예시를 간단히 작성해 확인해보자!
결과
String 같은 경우는 ASCII값을 기준으로 결과를 반환한다.
"abc"와 "aba"같은 경우, 마지막 글자(앞 두글자는 동일함) c - a 의 아스키값 2를 반환한 것이다.
밑의 num 같은 경우는 숫자의 대소 비교에 따라 반환값을 리턴한다.
잠깐! 멈춰!
int compareTo(T o) 메소드는 매개변수로 오브젝트, 즉 객체를 받아야 하기 때문에 int가 아닌 Integer가 필요하다!
엥? 둘의 차이가 뭔데? 하면 primitive type과 object type에 대한 차이를 공부하고 오자..
그렇다면 본격적으로 이전에 만든 Dog 클래스 객체를 활용해서 비교를 가능하게 해보자!
Dog 클래스에 Comparable 인터페이스를 구현하여 직접 compareTo 메소드를 재정의하였다.
이렇듯 직접 정의한 객체에도 내가 지정한 기준으로 충분히 비교가 가능하다.
2. Comparable 인터페이스 - compareTo 메소드와 정렬(Sort)
Comparable 인터페이스의 첫 정의문을 다시 살펴보면
이 인터페이스는 해당 인터페이스를 구현하는 각 클래스의 개체에
전체 순서를 적용합니다.
라는 문구를 볼 수 있다. 즉, compareTo 메소드를 통해 객체의 "순서"를 적용할 수 있다는 것이다!
예시를 통해 같이 확인해보자
결과를 보면 compareTo에 선언한 대로
age의 값에 따라 오름차순으로 순번이 정해지는 것을 알 수 있다.
compareTo 내부의 값을 return this.age - o.age; <-로 변경해도 가능
Collections.sort(Object)를 실행하면, 해당 객체의 compareTo 메소드를 이용하여 정렬하기 때문이다.
Collections.sort() VS Arrays.sort()
일반적으로 Collections.sort는 List를 정렬할 때, Arrays.sort는 배열을 정렬할때 쓰인다.
또한 정렬의 방식에도 차이가 있는데
Collections.sort는 TimeSort (삽입정렬과 합병정렬을 결합한 정렬)이라 평균, 최악 둘다 nlogn인 반면
Arrays.sort는 DualPivotQuicksort 정렬이라 평균은 nlogn인데 최악은 n^2이므로
속도에도 차이가 있음을 알 수 있다.
따라서 Comparable 인터페이스를 정리한다면
- 자기 자신과 매개변수 1개를 비교한다
- 오름차순, 내림차순과 같은 기본적인 정렬을 사용하는데 쓰인다
Comparable에 대한 정리가 끝이 났다. 이 정도면 사용자가 충분히 객체에 대한 정렬 기준을 알아서 구현할 수 있지 않은가? 그런데 왜 Comparator가 필요할까???
필자가 이런 생각들을 해본다고 가정해보자
1. 나는 몸무게로도 정렬하고 싶고, 이름 길이순으로도 정렬하고 싶은데..
그때마다 계속 compareTo의 내용을 바꿔서 써야 할까?
2. 내가 접근할 수 없는 Class네?
Class에 직접 메소드를 구현할 수 없잖아?
Comparator 인터페이스를 사용하면 이러한 상황을 타개할 수 있다!
3. Comparator 인터페이스 - compare 메소드 사용
comparable과 마찬가지로 구현에 필요한 compare 메소드를 간단히 들여다보자
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Comparator.html#compare(T,T)
int compare(T o1, T o2)
Parameters:
o1 - 비교될 첫 번째 오브젝트다
o2 - 비교될 두 번째 오브젝트다
Returns:
o1이 o2 오브젝트보다 작으면 -1(음수), 같으면 0, 크면 1(양수)를 반환한다.
Throws:
NullPointerException - 지정한 개체 o가 null인 경우
ClassCastException - o가 비교할 다른 class 타입일 경우
마찬가지로 코드를 통해 구현해보자.
메소드는 comparable과 코드 내용이 별 차이가 없어보인다.
해당 코드를 보며 석연치 않은 부분들을 정리해보자.
1. 현재 재정의된 compare는 age를 기준으로 하고있다.
만약 main 함수 내에서 age 말고 weigth를 기준으로 비교하는걸 보고 싶다면
compare함수 속 내용을 다시 바꿔야 하는건가?
2. d2와 d3을 비교하는데 아무 상관없는 d 객체를 가져온다.
(d가 아니더라도 d2의 compare 함수를 가져오는 것도 뭔가 코드가 좋아보이지 않는다..)
Comparator 인터페이스와 익명객체를 활용하면 위의 고민점을 한방에 해결할 수 있다!!
4. Comparator 인터페이스 - 익명객체 사용
먼저, 익명객체란 무엇인가?
기본적으로 객체를 다루기 위해서는 Class를 선언하여 인스턴스를 생성하는게 기본이다.
헌데 만약 쓰임새가 '일회용'인 객체들이 여러개 필요하다면?
일일히 Class를 다 만들어서 인스턴스를 생성하는건 낭비일 것이다.
또한 특정 인터페이스의 메소드를 여러번 재정의해야 한다면?
이번 본문에서의 경우, 정렬을 다양한 기준으로 하고 싶은 경우가 해당된다.
이런 경우, Comparator를 구현하는 Class를 여러개 생성해서 각각 compare을 다르게 구현하거나
한 Class안의 compare 코드를 계속해서 바꿀 수밖에 없다. 너무 비효율적이지 않은가?
이러한 점을 보완하여 나온 것이 익명객체(익명 클래스)이다.
'재사용하지 않고 일시적으로 사용하고 버려질 수 있는 객체'를 의미하며 이름 그대로 '이름 없는 객체'를 의미한다.
사용법은 코드를 통해 살펴보자.
먼저 Dog 클래스를 보면
Comparator 인터페이스를 구현할 필요가 없으므로
다시 깔끔하게 돌아왔다.
익명객체에서 구현하기 떄문!
익명객체를 구현하는 방식은 크게 두 가지가 있는데
1. main 함수 안에 지역변수 처럼 사용한다 (방식 1)
2. main 함수 밖에 static타입으로 선언한다. (방식 2)
이건 본인의 취향 껏 사용하시길 바란다. (필자의 경우 알고리즘의 영향으로 1번이 편하다..)
비교하고 싶은 기준이 생길때마다 익명 객체를 선언해 즉시 사용할 수 있다.
그렇다면 이를 응용하여 정렬도 구현해보자.
5. Comparator 인터페이스 - 익명객체를 사용한 정렬
이렇듯 익명객체를 사용하면 Class에 직접 구현하지 않고도 손쉽게 여러가지 정렬 방법을 구현할 수 있다.
생김새는 처음엔 낯설수도 있지만, 무궁무진한 쓰임새를 가지고 있으니 알아두면 정말 좋을 것이다.
필자의 경우 알고리즘을 풀이할 때, 문제가 요구하는 정렬 방법에 맞춰 익명 객체를 생성하여 보다 쉽고 효율적으로 문제를 풀이할 수 있었다.
자, 다왔다! 이쯤되면 흐릿해진 이 글의 목적을 다시 한 번 되새기며 글을 마무리해보겠다.
1. Comparable vs Comparator?
Comparable | Comparator | |
메소드의 매개변수 | 나 자신과 매개변수 1개를 비교한다. | 매개변수 2개의 값을 비교한다. |
정렬 기준 | 기본적인 정렬(오름차순, 내림차순) | 사용자가 정한 규칙을 기준으로 정렬 |
2. 익명 객체?
재사용하지 않고 일시적으로 사용하고 버려질 수 있는 객체를 의미하며 이름 그대로 '이름 없는 객체'를 의미한다.
3. 익명 객체 & Comparator 사용 이유
1. 한 main함수 안에 compare가 다양한 기준으로 정렬하는 것을 원하는 경우
ex) 무게 순, 나이 순 등...
2. 해당 클래스에 접근할 수 없는 상황인 경우
클래스에 직접 compare 메소드나 compreTo 메소드를 구현할 수 없는 상황
필자는 이 글을 작성하며 많은 고통과 인내 끝에 해당 인터페이스들을 조금이나마 이해할 수 있었다..
이 글을 읽게된다면 독자들도 좋은 해답을 얻어갔으면 좋겠다 :)
아 헬스하러가야징
'Java' 카테고리의 다른 글
자바의 로봇청소기, 가비지 컬렉션 (0) | 2024.07.09 |
---|---|
[JAVA] 그래프 구현하기 (인접 행렬, 인접 리스트) (0) | 2022.03.19 |