[JAVA] Comparable / Comparator 정리

sangwoo·2022년 11월 12일

출처

st-lab님의 자바 [JAVA] - Comparable 과 Comparator의 이해를 보고 정리한 글입니다.

들어가기전

Comparable / Comparator는 객체를 비교할 수 있는 인터페이스

학생의 이름과 나이를 가지고 있는 클래스를 생성해본다고 가정.

public class Test {
	public static void main(String[] args) {
    	Student s1 = new Student("홍길동" 20); // 학생1
		Student s2 = new Student("김철수" 22); // 학생2
		
        if (s1 ? s2) // s1, s2의 대소 비교는 어떻게 할까?
    }
}

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

위의 코드에서, 학생1(s1), 학생2(s2)를 생성하였다. 이 코드를 보면 어떻게 학생들의 대소관계를 알아낼 수 있을까?

  1. 이름을 기준으로 한다.
  2. 나이를 기준으로 한다.

이렇게 여러가지 기준이 나올 수 있기 때문에, Comparable / Comparator 를 통해 특정 객체를 비교할 수 있는 기준을 정의하여 준다.

Comparable

📌 자기 자신과 매개변수 객체를 비교

// Student 클래스는 Comparable<T> 인터페이스를 구현 받는다.
class Student implements Comparable<Student>{
    String name;
    int age;

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

	// Comparable<T>에서 정의된 compareTo를 재정의
    @Override
    public int compareTo(Student o) {
       if (this.age > o.age)
       		return 1;
		else if (this.age == o.age)
        	return 0;
		else
        	return -1;
    }
}
  • 나이를 기준으로 Student 객체를 비교
  • 객체 자기 자신과 compareTo() 매개변수로 들어온 객체를 비교

비교 후 반환 값

  • 크면 양수, 같으면 0, 작으면 음수

무슨 기준으로 양수, 0, 음수를 반환할까?

  • 항상 객체 자기 자신을 기준으로 비교를 한다.
  • this.ageo.age보다 크다면 양수
  • this.ageo.age보다 같으면 0
  • this.ageo.age보다 작으면 음수

양수는 무엇을, 음수는 무엇을

  • 보통 양수는 1, 음수는 -1을 반환한다.

조건문을 쓰지 않고 더 간단하게 반환하는 방법

// Student 클래스는 Comparable<T> 인터페이스를 구현 받는다.
class Student implements Comparable<Student>{
    String name;
    int age;

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

	// Comparable<T>에서 정의된 compareTo를 재정의
    @Override
    public int compareTo(Student o) {
    	/*
        	아래 결과가 양수면 this.age가 o.age보다 크다.
            음수이면 this.age가 o.age보다 작다.
            if문을 사용하지 않고도 두 값의 차이를 통해 
            대소 관계를 반환할 수 있다.
        */
       return this.age - o.age;
    }
}
  • 각 객체의 기준 속성 값의 차이를 통해 양수, 음수, 0 반환.
  • this.age를 기준으로 o.age를 빼주면 양수값이 나올거고, 그 말은 this.ageo.age 보다 크다.

📌 반환을 1, 0, -1로 했을 경우 문제점

  • 뺄셈 과정에서 자료형의 범위를 넘어버리는 경우가 발생한다.

  • EX) o1 = 1, o2 = -2,147,483,648일 경우

  • 권장사항은 IF 문을 통한 <, >, == 로 대소비교

Full Code

import java.util.*;

public class Test {
    public static void main(String[] args) {
        Student hong = new Student("홍길동", 20);
        Student kim = new Student("김철수", 22);
		
        // 우리가 재정의 했던 compareTo() 메서드를 호출하고
        // 매개변수로 kim 객체를 전달해주면
        // compareTo() 의 내부에 작성한 비교 코드를 통해
        // 양수, 음수, 0 값중 하나를 반환해준다.
        // 1: 자기 자신, 즉 hong이 더 크다.
        // 0: hong과 kim의 크기는 같다.
        // -1: kim의 크기가 더 크다.
        int compareValue = hong.compareTo(kim);
        
        if (compareValue > 0)
            System.out.println("hong 객체가 kim 객체보다 크다.");
        else if (compareValue < 0)
            System.out.println("hong 객체가 kim 객체보다 작다.");
        else
            System.out.println("hong 객체 kim 객체의 크기는 같다.");
    }
}


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 o) {
        if (this.age > o.age)
            return 1;
        else if (this.age < o.age)
            return -1;
        else
            return 0;
    }
}

Compareble 정리

  1. 자기 자신과 compareTo()의 매개변수로 받은 객체를 비교
  2. compareTo() 를 반드시 구현

Comparator

📌 매개변수로 받은 두개의 객체를 서로 비교

// Student 클래스는 Comparator<T> 인터페이스를 구현 받는다.
class Student implements Comparator<Student>{
    String name;
    int age;

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

	// Comparator<T>에서 정의된 compare 재정의
    @Override
    public int compare(Student o1, Student o2) {
       if (o1.age > o2.age)
       		return 1;
		else if (o1.age == o2.age)
        	return 0;
		else
        	return -1;
	/*
		return o1.age - o2.age;
	*/
    }
}
  • Comparable 과 비교 방법도 같다. 차이점은 자기 자신과의 비교
  • compare() 또한 주석처리된 부분처럼 작성 가능

Full Code

import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        Student hong = new Student("홍길동", 20);
        Student kim = new Student("김철수", 22);

        int compareValue = hong.compare(hong, kim);
        if (compareValue > 0)
            System.out.println("hong 객체가 kim 객체보다 크다.");
        else if (compareValue < 0)
            System.out.println("hong 객체가 kim 객체보다 작다.");
        else
            System.out.println("hong 객체 kim 객체의 크기는 같다.");
    }
}


class Student implements Comparator<Student>{
    String name;
    int age;

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

    @Override
    public int compare(Student o1, Student o2) {
        if (o1.age > o2.age)
            return 1;
        else if (o1.age == o2.age)
            return 0;
        else 
            return -1;
    }
}
  • hong.compare(hong, kim) 을 보면 객체에 상관 없이 독립적은 두 객체를 매개변수로 전달에 비교한다.

comparator 개선

특정 클래스 내부에 Comparator인터페이스를 상속받아, compare() 를 구현 해준다면, 해당 compare()를 사용하기 위해선 해당 클래스 타입의 객체를 생성해 주어야한다.

좀 헷갈린다. 코드로 보자

public class Main {
    public static void main(String[] args) {
        Student hong = new Student("홍길동", 20);
        Student kim = new Student("김철수", 22);
        Student lee = new Student("이발수", 24);
        
        int compareValue = hong.compare(hong, lee);
		int compareValue = hong.compare(kim, kim);
        int compareValue = hong.compare(kim, lee);
	}
}

// 이하 생략

딱 봐도 코드가 좀 보기 불편하다. 독립된 객체 두개를 비교하는데, 굳이 객체의 메서드로 들어가 객체를 통해 호출하기에는 좋지 않아 보인다.

익명 클래스 사용

public class Main {
    public static void main(String[] args) {
        Student hong = new Student("홍길동", 20);
        Student kim = new Student("김철수", 22);
        Student lee = new Student("이발수", 24);
        
        // 익명 클래스를 통해 특정 객체에 귀속되지 않아도 됨.
        Comparator<Student> comp = new Comparator<Student>() {
        	@Override
            public int compare(Student o1, Student o2) {
            	/*
                	비교 조건 생략 . . .
                */
            }
        }
        
        int compareValue = hong.compare(hong, lee);
		int compareValue = hong.compare(kim, kim);
        int compareValue = hong.compare(kim, lee);
	}
    
    public static Comparator<Student> comp2 = new Comparator<>() {
    	@Override
		public int compare(Student o1, Student o2) {
            	/*
                	비교 조건 생략 . . .
                */
        }
    }
}
  • Comparator 타입의 클래스를 생성함과 동시에 내부적으로 정의 해주어 비교만을 위한 익명 클래스을 만든다.
  • static 으로도 정의 가능.

익명 클래스를 사용의 장점

  • 익명 객체의 변수명만 중복되지 않게 하면, 위에서는 나이 기준으로 대소관계를 판별하였지만 이름으로도 판별이 가능한 익명 객체를 만들 수 있다.

Comparable / Comparator 를 통한 정렬

자바의 정렬

  • 기본 값으로 항상 오름차순 정렬을 실행
  • 선행 원소가 후행 원소보다 작다. EX) 1, 2, 3 . . .

compare / compareTo 를 통한 정렬

  • 음수가 나오면 두 원소의 위치를 교환하지 않는다.
  • 양수가 나오면 두 원소의 위치를 교환한다.

Comparable 정렬

public class Main {
    public static void main(String[] args) {
        Student hong = new Student("홍길동", 20);
        Student kim = new Student("김철수", 22);
        Student lee = new Student("이문복", 28);
        Student choi = new Student("최치김", 35);
        Student jung = new Student("정순하", 50);

        // 리스트에 학생 객체들 저장.
        Student[] list = new Student[5];
        list[0] = hong;
        list[1] = kim;
        list[2] = lee;
        list[3] = choi;
        list[4] = jung;

        // 정렬 전
        System.out.println(Arrays.toString(list));

        // 정렬
        Arrays.sort(list);
        
        // 정렬 후
        System.out.println(Arrays.toString(list));
    }
}


class Student {
    String name;
    int age;

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

위 코드를 실행하면 예외가 발생

  • Arrays.sort() 의 기본 메서드는 내부에서 인자로 받은 배열의 타입 (여기서는 Student)에 정의된 compareTo의 반환 값을 통해 정렬을 수행.
  • 현재 Student 클래스에는 재정의된 compareTo() 가 존재하지 않아, 예외 발생
  • Comparable<T> 구현 받아, compareTo() 재정의 해준다면 정상적인 배열의 오름차순 정렬 결과를 받을 수 있다.

수정 코드

public class Main {
    public static void main(String[] args) {
        Student hong = new Student("홍길동", 20);
        Student kim = new Student("김철수", 22);
        Student lee = new Student("이문복", 28);
        Student choi = new Student("최치김", 35);
        Student jung = new Student("정순하", 50);

        // 리스트에 학생 객체들 저장.
        Student[] list = new Student[5];
        list[0] = hong;
        list[1] = jung;
        list[2] = choi;
        list[3] = lee;
        list[4] = kim;

        // 정렬 전
        System.out.println(Arrays.toString(list));

        // 정렬
        Arrays.sort(list);
        
        // 정렬 후
        System.out.println(Arrays.toString(list));
    }
}


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 o) {
        if (this.age > o.age)
            return 1;
        else if (this.age == o.age)
            return 0;
        else
            return -1;
    }

    @Override
    public String toString() {
        return this.name + " " + this.age;
    }
}

Comparator 정렬

public class Main {
    public static void main(String[] args) {
        Student hong = new Student("홍길동", 20);
        Student kim = new Student("김철수", 22);
        Student choi = new Student("최치김", 35);
        Student lee = new Student("이문복", 28);
        Student jung = new Student("정순하", 50);

        // 리스트에 학생 객체들 저장.
        Student[] list = new Student[5];
        list[0] = hong;
        list[1] = jung;
        list[2] = choi;
        list[3] = lee;
        list[4] = kim;

        // 정렬 전
        System.out.println(Arrays.toString(list));

        Comparator<Student> comp = new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                if (o1.age > o2.age)
                    return 1;
                else if (o1.age == o2.age)
                    return 0;
                else
                    return -1;
            }
        };

        // 정렬
        Arrays.sort(list, comp);

        // 정렬 후
        System.out.println(Arrays.toString(list));
    }
}


class Student {
    String name;
    int age;

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

    @Override
    public String toString() {
        return this.name + " " + this.age;
    }
}
  • main() 내부에 Comparator<Student>를 구현한 익명 클래스를 생성한뒤 Arrays.sort()의 두번째 매개변수로 전달
  • 오버로딩된 sort()를 통해 Student 클래스 내부에 정의된 Comparable<>의 추상 메서드 compareTo 존재 유무 상관 없이, Comparator 익명 클래스의 compare() 메서드를 통해 배열 요소의 정렬 수행.

내림차순

  • compareTo() / compare()에서 반환되는 양수, 음수, 0을 각 조건의 반대가 되도록해보자.
/*
	생략 ...
*/
if	(o1.age > o2.age)
	return 1;
else if (o1.age == o2.age)
	return 0;
else
	return -1;
/*
	생략
*/
  • return값이 1이면 자리 교환(이유: 오름차순 정렬이기때문에 더 큰 값의 값을 후행의 값으로 이동시켜야 함)
  • 반대로 생각하면 o1.age > o2.age 일때 자리교환을 하지 않고 o1.age < o2.age 일때 자리교환을 한다면 더 작은 값을 후행 값으로 설정함을 의미
  • 위의 return 값을 1, -1 서로 바꿔주면 해결
/*
	생략 ...
*/
if	(o1.age > o2.age)
	return -1;
else if (o1.age == o2.age)
	return 0;
else
	return 1;
/*
	생략
*/

더 간단한 방법

/*
	생략 ...
*/
compare(Student o1, Student o2) {
	return o2.age - o1.age
}

compareTo(Student o) {
	return o2.age - this.age;
}
/*
	생략
*/
  • 위에서 본 큰값에서 작은 값을 뺀 결과값을 반대로 작은 값에서 큰값을 빼도록 설정

❓ 어떤 상황에 두개를 구별해서 사용해야 할까?

Comparable<T>는 보통 클래스 내부에 한번만 재정의 한다. 그래서 보통 해당 클래스의 가장 기본(Default) 비교로 사용한다.

Comparator<T>는 특정 상황에 맞는 기준이 필요할때 익명클래스로 정의하여 비교할 수 있는 장점이 있다보니, 보통 Comparable을 사용하다가, 특별한 정렬이 필요하다면 그때 쓰이는 경우가 많다.

0개의 댓글