Programming/Java

[Java] 비교를 위한 인터페이스 : Comparable과 Comparator

세동세 2024. 7. 1. 16:54

들어가며

자바를 주언어로 변경하면서, 코딩테스트에 임할 때 내가 가장 어려워한 부분이다.

PriorityQueue에 객체를 넣었을 때 특정 기준에 대해 "비교"하고 싶을 때, Python을 람다식을 사용해 간단하게 해결했었는데

Java의 경우 Comparable? Comparator? 여러 코드들을 응용했고 비슷비슷하게 생긴 표현식에 IDE의 도움 없이는 구현하기 어려웠기에 시간이 있을 때 정리를 해두려고 한다. 

 

인터페이스

일종의 추상 클래스
클래스들이 필수로 구현해야 하는 추상 자료형
사용을 강제하거나, 인터페이스의 추상메서드를 사용하도록 하여, 각 클래스들의 변경을 용이하게 대처한다.

Comparable과 Comparator 모두 객체를 비교 목적의 인터페이스이다.

따라서, 인터페이스 내에 선언된 메서드를 Override 하여 반드시 구현해야 한다.

 

Comparable

객체 내부에 비교 기준을 부여하여 다른 객체와 비교를 하는 것

 

int compareTo(T o)

static class Student implements Comparable<Student> {

    String name;
    int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Student o) {

        if (this.age > o.age) return 1;
        else if (this.age == o.age) return 0;
        else return -1;
    }
}
  • 자기 자신의 값이 o의 값 보다 크다면 양수
  • 자기 자신의 값이 o의 값과 같다면 0
  • 자기 자신의 값이 o의 값보다 작다면 음수

따라서 return 시, `this.age - o.age`를 이용하면, 자신이 더 클 경우 양수, 자신이 작을 경우 음수를 반환할 것이다.

        @Override
        public int compareTo(Student o) {
            return this.age - o.age;
        }

 

하지만 이 경우, 뺄셈 과정에서 자료형의 범위를 넘어버리는 경우가 발생할 수 있다.

int 자료형 = 32비트 = -2,147,483,.648 ~ 2,147,483,647 이다.
만약 해당 범위 밖을 넘어가게 되면, int 자료형에서 표현할 수 없는 수로 Overflow가 발생한다.

만약 o1 = 1, o2 = -2,147,483,648 일 경우 1 - (-2,147,483,648) = 2,147,483,649가 되어야하지만,
Overflow가 되어 -2,147,483,648이 되어 음수값이 나온다.
1인 o1이 -2,147,483,648인 o2보다 작게 결과가 나오는 상황이 생긴다.

```
# 학생 1 = 1 / 학생 2 = Integer.MIN_VALUE
# 나와야 할 순서 = 학생_2 -> 학생_1
# 결과
Student{name='학생_1', age=1}
Student{name='학생_2', age=-2147483648}

```

따라서, "대소비교" 시 Overflow가 발생할 여지가 있는지에 대해 반드시 확인하고 사용해야 한다.

 

예시 코드

import java.util.Arrays;

public class Main {

    static class Student implements Comparable<Student> {

        String name;
        int age;

        Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public int compareTo(Student o) {

            if (this.age > o.age) return 1;
            else if (this.age == o.age) return 0;
            else return -1;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

    public static void main(String[] args) {

        int [] age = {5, 13, 2, 9, 10};
        Student[] studentList = new Student[age.length];

        for (int idx = 0; idx < age.length; idx ++) {
            studentList[idx] = new Student("학생_" + (idx + 1), age[idx]);
        }

        Arrays.sort(studentList);

        for (Student student : studentList) {
            System.out.println(student.toString());
        }
    }
}​
Student{name='학생_3', age=2}
Student{name='학생_1', age=5}
Student{name='학생_4', age=9}
Student{name='학생_5', age=10}
Student{name='학생_2', age=13}

 

https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html#method.summary

 

Comparable (Java Platform SE 8 )

This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's natural ordering, and the class's compareTo method is referred to as its natural comparison method. Lists (and arrays) of o

docs.oracle.com

 

 

 

 

Comparator

객체 외부에서 비교할 두 객체를 비교하여 비교 기준으로써의 역할

int compare(T o1, T o2)

static class Student implements Comparator<Student> {

    String name;
    int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compare(Student o1, Student o2) {
    	/**
        o1의 age가 o2의 age보다 크다면 양수 반환
        같다면 0
        작다면 음수 반환
        */
        return o1.age - o2.age;
    }
}

 

따라서, 하단 처럼 사용할 수 있다.

 

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

public class Main {

    static class Student implements Comparator<Student> {

        String name;
        int age;

        Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public int compare(Student o1, Student o2) {
            return o1.age - o2.age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

    public static void main(String[] args) {


        Student student_1 = new Student("학생1", 38);
        Student student_2 = new Student("학생2", 20);

        System.out.println(student_1.compare(student_1, student_2));
        
    }
}
  •  Comparator 처럼 A 기준으로 비교하고 싶으면 `a.compare(a,b)`

 

하지만, Comparator는 A 객체의 compare 메서드를 이용해 비교하지만, 그 내부의 두 매개변수인 o1 과 o2가 비교되는 것이기 때문에, A 객체와 관련 없이 두 객체의 비교값을 반환한다.

즉, 객체 자체와는 상관 없이 독립적으로 매개변수로 넘겨진 두 객체를 비교한다

 

따라서, Comparator를 통해 compare 메서드를 사용하려면 compare 메서드를 활용하기 위한 객체가 필요하다.

비교 대상의 객체만으로 정렬을 적용하기 어렵고, Comparable과 다르게 Class의 목적에 맞게 사용되지 않는다.

 

어떻게 변경할 수 있을까?

Comparator 기능만 따로 둔다.

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

public class Main {

    static class Student {

        String name;
        int age;

        Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

    static class StudentComparator implements Comparator<Student> {

        @Override
        public int compare(Student o1, Student o2) {
            return o1.age - o2.age;
        }
    }

    public static void main(String[] args) {

        // 비교 연산 객체 생성
        StudentComparator studentComparator = new StudentComparator();

        // 학생 생성
        int [] age = {5, 13, 2, 9, 10};
        Student[] studentList = new Student[age.length];

        for (int idx = 0; idx < age.length; idx ++) {
            studentList[idx] = new Student("학생_" + (idx + 1), age[idx]);
        }

        Arrays.sort(studentList, studentComparator);

        for (Student student : studentList) {
            System.out.println(student.toString());
        }
    }
}

 

다음 실행 결과는 Comparable과 일치한다.

1. Student 객체는 학생의 역할 기능만 존재한다.

2. Comparator 는 도구로서, 객체의 비교를 위해 존재한다.

 

정렬을 위해 사용되는 Comparator 구현체는 보통 한 번만 사용되는 경우가 많은데, Class로 정의를 하나?

익명객체(익명 클래스)를 사용하자

왜 사용할까?
프로그램 내에서 단발적으로 사용되어야 하는 객체일 경우
재사용성이 없고 확장성을 활용하는 것이 유지보수에 더 불리할 때

 

Comparator라는 interface 가 존재한다. 이는 구현(상속)할 대상이 존재한다는 것이므로, 익명객체를 만들 수 있다.

    Comparator<Student> comparator = new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.age - o2.age;
        }
    };
    
    Arrays.sort(studentList, comparator);

 

혹은

    Arrays.sort(studentList, new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.age - o2.age;
        }
    });

 

IDE를 활용할 때는 구현이 어렵지 않지만, 프로그래머스를 이용해 코딩테스트를 보고 있으면, 이런 구현부가 막막할 때가 있다.

어떻게 응용하고 쉽게 외울 수 있을까?

 

람다식을 사용하자

Arrays.sort(studentList, (o1, o2) -> o1.age - o2.age);

 

 

https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html#method.summary

 

Comparator (Java Platform SE 8 )

Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. In the foregoing description, the notation sgn(expression) designates the mathematical s

docs.oracle.com

 

 


 

 

자바로 코딩테스트를 준비하면서, 이런 자바의 기초 지식이 기초 코딩에도 영향이 크구나 느낀다.

나 역시 간과하고 문제를 풀다보니, 갑자기 비교연산이 필요할 경우 당황하기도 하고 갑자기 까먹어버린다 ㅜ

오늘부로 한번 정리가 된 것 같다 :)