1. Comparable vs Comparator
언제 뭘 쓸까?
Comparable 사용:
- 학생을 학번순으로 정렬하는 것처럼, 객체의 "자연스러운" 기본 정렬 방식이 있을 때
- 한 가지 정렬 방식만 필요할 때
- 클래스를 직접 수정할 수 있을 때
Comparator 사용:
- 나이순, 이름순, 점수순 등 여러 가지 정렬 방식이 필요할 때
- String, Integer 같은 기존 클래스를 다르게 정렬하고 싶을 때
- 클래스를 수정할 수 없을 때
2. Comparable 상세 설명
Comparable 구현하기
class Student implements Comparable<Student> {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student other) {
// ✅ 안전한 방법 (오버플로우 방지)
return Integer.compare(this.age, other.age);
// ❌ 위험한 방법 (큰 수 차이나면 오버플로우 가능)
// return this.age - other.age;
}
@Override
public String toString() {
return name + "(" + age + "세)";
}
}
compareTo() 리턴값의 의미
음수를 리턴 → 내(this)가 더 작다 → 내가 앞에 위치해야 함
0을 리턴 → 둘이 같다 → 순서 유지
양수를 리턴 → 내(this)가 더 크다 → 내가 뒤에 위치해야 함
정렬의 기본동작이 오름차순이라 음수면 그대로 유지
0도 그대로 유지
양수면 자리바꾸기
Comparable 구현 후 사용 예시
// list.sort() 메서드 시그니처:
void sort(Comparator<? super E> c)
null을 넘기면
해당 클래스가 구현한 Comparable의 compareTo()를 사용
(요소가 Comparable을 구현하지 않았으면 런타임 예외 발생)
런타임 예외가 발생하므로 안전성이 낮다. 사용하지않는게 좋다.
Comparator.naturalOrder()를 넘기면
해당 클래스가 구현한 Comparable의 compareTo()를 사용한다.
구현하지않았다면 컴파일 에러가 발생하므로 안전성이 높다. 이걸 사용하는게 좋다
밑에는 Comparator.naturalOrder()의 코드중 일부인데
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder()
"자기 자신 또는 부모 타입과 비교 가능한(Comparable을 구현한) 타입 T에 대해,
T를 비교할 수 있는 Comparator를 반환하는 정적 메서드"
즉 T 부모클래스가 Comparable을 구현해도 T에서 Comparator.naturalOrder()를 사용할수있다는 뜻
모든 Wrapper타입 ( Integer,Long,Double,String ..)은 이미 Comparable을 구현하고 있다.
( 기본적으로 오름차순, 사전순 )
Student와 같은 직접 만든 클래스는 Comparator.naturalOrder()사용시
구현한 Comparalbe.compareTo()를 사용한다.
Comparator.reverseOrder()를 사용시에는 해당 결과를 반대로 적용한다. 그래서 내림차순이 적용된다.
오름차순 예시
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
List<Student> students = Arrays.asList(
new Student("영희", 25),
new Student("철수", 20)
);
// Integer.compare(this.age, other.age) 사용
students.sort(null); // null = "Comparable 사용" -> Comparator가 주어지지않으면 Comparable 사용
// 정렬 과정:
// 1. 영희(25).compareTo(철수(20)) 호출
// 2. Integer.compare(25, 20) 실행
// 3. 25 > 20이므로 양수 반환 (예: 1)
// 4. 양수 = "내(영희)가 더 크다" = "내가 뒤로 가야 한다"
// 5. 영희와 철수 위치 교환
// 6. 결과: [철수(20), 영희(25)]
내림차순 예시
@Override
public int compareTo(Student other) {
// 순서를 바꿔서 비교
return Integer.compare(other.age, this.age);
}
// 정렬 과정:
// 1. 철수(20).compareTo(영희(25)) 호출
// 2. Integer.compare(25, 20) 실행 (순서 주의!)
// 3. 25 > 20이므로 양수 반환
// 4. 양수 = "내(철수)가 더 크다"로 판단됨
// 5. 하지만 실제로는 철수 나이가 더 적으므로 뒤로 감
// 6. 결과: [영희(25), 철수(20)] ✅ 내림차순!
3. Comparator 상세 설명
왜 Comparator가 필요한가?
class Student {
String name;
int age;
int score;
// Comparable을 구현하면 한 가지 정렬만 가능
// 하지만 우리는 나이순, 이름순, 점수순 등 여러 방식으로 정렬하고 싶다!
}
Comparator는 함수형 인터페이스이다. ( 구현할 인터페이스 함수가 1개이다. 그래서 람다식으로 가능 )
Comparator 기본 사용법
class Student {
String name;
int age;
int score;
public Student(String name, int age, int score) {
this.name = name;
this.age = age;
this.score = score;
}
public String getName() { return name; }
public int getAge() { return age; }
public int getScore() { return score; }
@Override
public String toString() {
return name + "(나이:" + age + ", 점수:" + score + ")";
}
}
List<Student> students = new ArrayList<>();
students.add(new Student("철수", 20, 85));
students.add(new Student("영희", 18, 95));
students.add(new Student("민수", 22, 75));
이런 클래스와 리스트가 있다고했을때
여러 가지 정렬 방법
// 1. 나이 오름차순 (어린 순)
students.sort(Comparator.comparingInt(Student::getAge));
// 결과: [영희(18), 철수(20), 민수(22)]
// 2. 나이 내림차순 (나이 많은 순)
students.sort(Comparator.comparingInt(Student::getAge).reversed());
// 결과: [민수(22), 철수(20), 영희(18)]
// 3. 이름순 (가나다순)
students.sort(Comparator.comparing(Student::getName));
// 결과: [민수, 영희, 철수]
// 4. 점수 내림차순 (높은 점수순)
students.sort(Comparator.comparingInt(Student::getScore).reversed());
// 결과: [영희(95), 철수(85), 민수(75)]
Comparator.comparing() 이해하기
// 이 코드의 의미는?
students.sort(Comparator.comparing(s -> s.getName()));
// ^^^^^^^^^^^^
// "학생에서 무엇을 뽑아내서 정렬할지 ( 어떤 기준으로 정렬을 할까? 그런데 어디서 그 값을 꺼내야해? ) "
// 쉽게 말하면:
Comparator.comparing(s -> s.getName())
= "학생들을 이름 기준으로 비교해서 정렬해줘!"
sort()의 매개변수로 Comparator를 넘겨줘야하는데
Student는 Comparable을 상속받지않았으므로 어떤식으로 정렬할지 알려줘야한다.
students.sort(Comparator.comparing( s -> s.getName()));
Comparator.comparing()의 전달인자로 어떤값을 정렬에 사용해야할지 알려줘야한다.
그래서 정렬에 사용할 값을 가져오는 함수를 전달해줘야한다.
저런 람다식을 메서드 레퍼런스로 변경하면 다음과 같이된다.
students.sort(Comparator.comparing(Student::getName));
타입별 Comparator 메서드와 박싱/언박싱
// Object 타입 (wrapper타입인 String 등등, 객체) → comparing() 사용
Comparator.comparing(Student::getName)
// int 타입 → comparingInt() 사용 (박싱 없음!)
Comparator.comparingInt(Student::getAge)
// long 타입 → comparingLong() 사용
Comparator.comparingLong(Student::getSalaryInWon)
// double 타입 → comparingDouble() 사용
Comparator.comparingDouble(Student::getGpa)
여기서 Long Doubled은 Wrapper타입이 아닌 long, double인 primitive 타입을 말하는것이다.
class Student {
private int age; // primitive int
public int getAge() { return age; }
}
// ❌ 비효율적: comparing() 사용
Comparator.comparing(Student::getAge)
// 무슨 일이 일어나나?
// 1. getAge() 호출 → int 반환 (예: 20)
// 2. comparing()은 Comparable<?>을 기대함
// 3. int → Integer로 자동 변환 (박싱)
// 4. 비교할 때 Integer → int로 변환 (언박싱)
// 5. 매 비교마다 박싱/언박싱 발생! (느림)
// ✅ 효율적: comparingInt() 사용
Comparator.comparingInt(Student::getAge)
// 무슨 일이 일어나나?
// 1. getAge() 호출 → int 반환 (예: 20)
// 2. comparingInt()는 int를 직접 받음
// 3. int끼리 바로 비교
// 4. 박싱/언박싱 없음! (빠름)
list.sort() vs Collections.sort()
List<Student> students = new ArrayList<>();
// ✅ 현대 방식 (Java 8+) - 추천!
students.sort(Comparator.comparing(Student::getName));
// ❌ 옛날 방식 (Java 1.2~) - 피하기
Collections.sort(students, Comparator.comparing(Student::getName));
왜 Collections.sort()를 피해야 하나?
- 일관성: list.sort()가 더 객체지향적 (리스트 자체가 정렬 기능을 가짐)
students.sort(...) // 학생 리스트야, 정렬해!
Collections.sort(students, ...) // Collections한테 부탁?
- 간결성: students.sort()가 Collections.sort(students)보다 짧고 읽기 쉬움
- 최신 트렌드: Java 8 이후로는 list.sort()가 표준
- 실제 구현: Collections.sort()도 내부적으로 list.sort()를 호출함
// Collections.sort()의 실제 구현
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c); // 결국 list.sort()를 호출!
}
메서드 레퍼런스 ( :: )
메서드 레퍼런스란?
람다식을 더 간결하게 쓰는 문법 (Java 8+)
// 람다식
students.sort(Comparator.comparing(s -> s.getName()));
// 메서드 레퍼런스 (똑같은 의미, 더 간결!)
students.sort(Comparator.comparing(Student::getName));
Student::getName의 의미
- "Student 클래스의 getName 메서드를 호출해!"
- "각 Student 객체에 대해 getName()을 실행해!"
sort()의 전달인자로 Comparator를 줘야하고 Comparator.comparing()의 전달인자로는
정렬을 할 기준을 가져오는 익명함수를 제공해줘야한다.
그때 그 익명함수는 매개변수가 1개이다.
그게 s이고 s는 곧 리스트의 요소이며 그 리스트의 요소로 정렬할 기준을 반환해주면 된다.
이때 메서드 레퍼런스를 사용하면 편하다
람다식과 메서드 레퍼런스는 익명함수를 구현하는것이고
람다식은 익명함수를 만들때 직접 함수 본문을 작성하는것이고
메서드 레퍼런스는 기존 메서드를 참조해서 만드는것이다.
람다식에서 기존 메서드를 참조하고있다면 좀더 간단한식인 메서드 레퍼런스로 변경 가능하다.
메서드 레퍼런스는 전달인자가 0개이든 n개이든 상관없다.
인자 0개일때
// 람다식
() -> System.currentTimeMillis()
// 메서드 레퍼런스
System::currentTimeMillis
인자 1개일때
// 람다식
s -> s.getName()
s -> System.out.println(s)
// 메서드 레퍼런스
Student::getName
System.out::println
인자 2개일때
// 람다식
(a, b) -> Integer.compare(a, b)
(s1, s2) -> s1.compareTo(s2)
// 메서드 레퍼런스
Integer::compare
String::compareTo
인자 2개 이상도 가능
// 람다식
(a, b, c) -> someMethod(a, b, c)
// 메서드 레퍼런스
ClassName::someMethod
그냥 전달인자가 있다면 그걸 그대로 참조하는 메서드에 전달해주는 느낌이다.
복합정렬
여러 기준으로 정렬하기
// 점수 내림차순 → 같으면 나이 오름차순
students.sort(Comparator
.comparingInt(Student::getScore, Comparator.reverseOrder())
.thenComparingInt(Student::getAge));
// 나이 오름차순 → 이름순 → 점수 내림차순
students.sort(Comparator
.comparingInt(Student::getAge)
.thenComparing(Student::getName)
.thenComparingInt(Student::getScore, Comparator.reverseOrder()));
// null 처리 포함
students.sort(Comparator
.nullsLast(Comparator.comparing(Student::getName)));
---- 아래와 같이 사용 x
students.sort(Comparator
.comparingInt(Student::getScore).reverse()
.thenComparingInt(Student::getAge));
이렇게하면 score기준 내림차순, age기준 오름차순이 될거같지만
reverse()가 전체에 영향을 주어서 전체 내림차순이 되어버린다.
.reversed()를 어디에 쓰든:
├─ 그 이전 모든 기준 → 뒤집힘
├─ 그 이후 모든 기준 → 뒤집힘
└─ 결과: 전체가 다 반대로!
Primitive Type 정렬하기
기본적인 오름차순 정렬 방법
import java.util.Arrays;
int[] arr = {5, 2, 8, 1, 9};
Arrays.sort(arr); // 오름차순 정렬
// 결과: [1, 2, 5, 8, 9]
내림차순 정렬 방법
primitive 타입은 Comparator를 직접 사용할 수 없어서 약간 번거롭습니다
방법 1: 정렬 후 뒤집기
int[] arr = {5, 2, 8, 1, 9};
Arrays.sort(arr);
// 배열 뒤집기
for (int i = 0; i < arr.length / 2; i++) {
int temp = arr[i];
arr[i] = arr[arr.length - 1 - i];
arr[arr.length - 1 - i] = temp;
}
방법 2: Integer 배열로 변환 후 정렬
int[] arr = {5, 2, 8, 1, 9};
Integer[] arrBoxed = Arrays.stream(arr).boxed().toArray(Integer[]::new);
Arrays.sort(arrBoxed, Collections.reverseOrder());
// 다시 int[]로 변환
int[] result = Arrays.stream(arrBoxed).mapToInt(Integer::intValue).toArray();
방법 3: Stream 사용 (Java 8+, 간결함)
int[] arr = {5, 2, 8, 1, 9};
int[] sorted = Arrays.stream(arr)
.boxed()
.sorted(Collections.reverseOrder())
.mapToInt(Integer::intValue)
.toArray();
'자바 > ++' 카테고리의 다른 글
Stream ( with claude ) (1) | 2025.09.30 |
---|---|
자바 n진법 <-> 10진법 바꾸는법 (0) | 2024.01.18 |
repeat로 문자열 반복해서 이어붙이기 (0) | 2023.05.17 |
[Java] 두 배열 비교하기 (0) | 2022.12.27 |
[Java] BigInteger (큰 숫자 다루기) (0) | 2022.12.14 |
댓글