자바/++

[Java] Comparator , Comparable , 익명객체(클래스)

backend dev 2022. 12. 4.

 

Comparable, Comparator는 모두 인터페이스이다.

인터페이스 이므로 Comparable, Comparator를 사용하고자 한다면 인터페이스내에 선언된 메소드를 구현해야한다!

 

Comparable

내부에는 compareTo(T o)라는 메소드가 있고, Comparable을 사용하고자 한다면

compareTo 메소드를 재정의(오버라이드/구현)을 해줘야한다.

 

Comparator

내부에는 compare(T o1,T o2)라는 메소드가 있다. Comparator을 사용하고자 한다면

compare 메소드를 재정의(오버라이드/구현)을 해줘야한다.

 

 

인터페이스 관련 의문해결

 

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

Comparator 인터페이스 내부를 보면 여러개의 메소드가 있는데

인터페이스에 있는 모든 메소드를 구현해야하는것 아닌가?

-> Java8에서는 인터페이스 내부에도 일반 메소드도 구현할수 있도록 변경되었다.

즉 구현체클래스에서 반드시 구현해야하는 추상메소드가 아닌 

Comparator쓸때 많이쓰는 

naturalOrder() 메소드와 reverseOrder()는 미리 구현된 메소드라는것이다 (앞에 static이 붙어있는걸 보면 알수 있다.)

참고로 default가 붙어있는 메소드는 일반함수로 구현되어있어도 재정의(오버라이드)를 할 수있고, static이 붙어있는것은 재정의 할 수 없다.

 

 

Comparable과 Comparator

두 인터페이스의 주요한 용도는 객체를 비교하는것이다.

 

Primitive타입 변수인 int,double과 같은 실수 변수는 부등호 가지고 쉽게 두 변수를 비교 가능하다.

 

int a = 1;
int b = 2;
		
if(a > b) {
    System.out.println("a가 b보다 큽니다.");
}

 

그렇다면 객체는 어떤가 살펴보자.

public class prac {

    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    public static void main(String[] args) throws IOException {

        Student a = new Student(17, 2); //17살 2반
        Student b = new Student(18, 1); //18살 1반

        //두 객체를 어떻게 비교할까?, 자동적으로 정렬을 하기에는 기준이 2개(나이,학급)가 있다
        //어떤 기준으로 어떻게 비교해서 정렬을 할지를 설정해야한다.


        bw.flush();
        bw.close();
    }
}

class Student {
    int age; // 나이
    int classNumber; //학급

    public Student(int age, int classNumber) { //생성자
        this.age = age;
        this.classNumber = classNumber;
    }
}

객체는 사용자가 기준을 정해주지 않는 이상 어떤 객체가 더 높은 우선순위를 갖는지 판단 할 수 가 없다.

이러한 문제점을 해결하기 위해 Comparable 또는 Comparator가 쓰인다는 것이다.

 

Comparable과 Comparator의 역할은 비슷한 것 같은데 무슨 차이인 것일까?

Comparable의 compareTo(T o) 메소드는 파라미터(매개변수)가 한 개이고,

Comparator의 compare(T o1, T o2) 메소드는 파라미터가 왜 두 개인 것일까?

 

Comparable은 자기 자신과 매개변수 객체를 비교하기 때문이고

Comparator는 두 매개변수 객체를 비교하기 때문이다.

 

Comparable은 자기 자신과 파라미터로 들어오는 객체를 비교

Comparator는 자기 자신의 상태가 어떻던 상관없이 파라미터로 들어오는 두 객체를 비교

 즉, 본질적으로 비교한다는 것 자체는 같지만, 비교 대상이 다르다는 것이다.

 

 

Comparable

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

Comparable 인터페이스 정의

<T>에 자신과 비교할 객체를 넣어주면된다는것

 

Comparable을 이용해서 클래스를 만들때 이런식으로 만든다.

public class 클래스이름 implements Comparable<Type> { 
 
/*
  ...
  클래스 내부 code
  ...
 */
 
	// 필수 구현 부분
	@Override // 오버라이드
	public int compareTo(Type o) {
		/*
		 비교 구현하는 부분
		 */
	}
}

필수 구현부분인 compareTo부분을 구현해주면된다.

Comparable을 자기자신과 매개변수 객체를 비교한다고 하였으니 

자기자신(객체)정의 안에다가 compareTo를 구현(오버라이드)해놔야

나중에 자기자신.compareTo(비교할객체)이런식으로 사용할 수 있을것이다.

 

아까 위에서 예시를 든 Student 클래스에 Comparable을 사용해보자.

class Student implements Comparable<Student>{
    int age; // 나이
    int classNumber; //학급

    public Student(int age, int classNumber) { //생성자
        this.age = age;
        this.classNumber = classNumber;
    }


    @Override
    public int compareTo(Student o) {
        //비교 구현
    }
}

Comparable<Student>를 implement해서 비교대상으로 들어올 객체는 Student 객체라는것을 명시해준다.

그리고 클래스내부에 compareTo를 구현해주면된다.

 

비교하는 부분 구현하는법

class Student implements Comparable<Student>{
    int age; // 나이
    int classNumber; //학급

    public Student(int age, int classNumber) { //생성자
        this.age = age;
        this.classNumber = classNumber;
    }


    @Override
    public int compareTo(Student o) {
        
        if(this.age >o.age) // 자기자신의 age가 o의 age보다 크다면 양수리턴
        {
            return 1;
        } 
        else if (this.age == o.age) { // 같다면 0 리턴
            return 0;
        } else { // 자기자신의 age가 o의 age보다 작다면 음수 리턴
            return -1;
        }
    }
}

구현하는 코드를 먼저 보게 되면 이런식으로 구현하게된다.

compareTo의 반환형을 보면 int 이기때문.

어떤 기준으로 양수 , 0 , 음수를 반환하는것일까?

 

Comparable은 다시 한번 말하지만, 자기자신 객체와 입력들어온 객체랑 비교하는것이다.

즉 자기 자신을 기준삼아 대소관계를 파악한다.

만약 내가 갖고있는값이 7이라고 가정하자. 그리고 상대방은 3이라고 가정하자.

내가 갖고있는값 7은 상대방값 3보다 4가 더 크다.

만약 상대방이 가지고있는값이 9라면 

내가 갖고있는값 7은 상대방값 9보다 2만큼 작다 -> -2만큼 크다라고 표현가능하다.

상대방값 9 보다 -2 만큼 크다는것

-> 9에서 -2를 더 더해줘야 내가 갖고있는값 7이 된다는 뜻 -> 9 + (-2) = 7

 

이미지로 보면 이렇게된다.

자기자신과 들어온값에 대한 값의 차이를 양수,0,음수로 표현가능한대

그것을 위에 코드에서 return 1(양수), return 0 , return -1(음수)로 표현한것이다.

@Override
	public int compareTo(Student o) {
    
		// 자기자신의 age가 o의 age보다 크다면 양수
		if(this.age > o.age) {
			return 142352;
		}
		// 자기 자신의 age와 o의 age가 같다면 0
		else if(this.age == o.age) {
			return 0;
		}
		// 자기 자신의 age가 o의 age보다 작다면 음수
		else {
			return -1324;
		}
	}

이렇게 1말고 , 양수 아무값이나 음수 -1 말고 음수 아무값으로 해도 동작은 한다.

하지만 깔끔하게 보기위해서 위에 코드처럼 1,0,-1를 사용하는것이다.

 

더 깔끔하게

생각해보면 1,0,-1와 부등호를 이용하는것보다는

결국 자기자신의 age값과 들어온 객체 age값의 차이를 리턴해주면 되는것이므로

 

this.age - o.age를 리턴해주는것이 더 깔끔할 것이다.

자기자신이 더 크다면 양수가 리턴될것이고

자기자신과 같다면 0이 리턴될것이고

자기자신이 더 작다면 음수가 리턴될것이므로

 

명령어 한줄로 위에 코드가 정리가 된다.

 

public class prac {

    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    public static void main(String[] args) throws IOException {

        Student a = new Student(17, 2); //17살 2반
        Student b = new Student(18, 1); //18살 1반

        bw.write("리턴값 : " + a.compareTo(b)+"\n");
        if (a.compareTo(b) < 0) {
            bw.write("a의 나이가 b의 나이보다 작습니다.");
        }

        bw.flush();
        bw.close();

    }
}

class Student implements Comparable<Student>{
    int age; // 나이
    int classNumber; //학급

    public Student(int age, int classNumber) { //생성자
        this.age = age;
        this.classNumber = classNumber;
    }


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

이렇게 정리가되고

실제로 동작하는지 체크해보면

잘동작하는것을 볼 수 있다.  (나이 기준으로 비교한 예제)

 

주의할점

자료형의 최대크기를 넘어가는 연산결과가 나오지않게끔 주의해야한다.

그럴 가능성이 있다면 위쪽에서 보여준 부등호를 사용해서 1,0,-1를 리턴하는 쪽으로 코드를 짜야한다.

 

 

 

 

 

[Comparable의 특징]


 
1. 자기 자신과 매개변수를 비교한다.

2. compareTo 메소드를 반드시 구현해야한다.

 


Comparator

Comparator는 두 매개변수 객체를 비교한다

 

자기자신이 아니라. 파라미터(매개변수)로 들어오는 두 객체를 비교한다. 그 점이 Comparable과 차이가 있다.

 

Comparable 인터페이스와 형식이 유사하게 생겼다.

<T>는 하나의 객체타입이 들어갈 자리라고 보면된다.

 

클래스를 만들때 Comparator의 사용법은 다음과 같다.

이부분은 가볍게 넘어간다, 결국 익명객체를 이용해서 구현하는방법을 사용할것이기 때문에

그전에 익명객체를 이용한 방식이 왜 더 좋은지 알기 위해 클래스를 만들때 Comparator를 구현하는 예시를 보자

import java.util.Comparator;	// import 필요
class Student implements Comparator<Student> {
 
	int age;			// 나이
	int classNumber;	// 학급
	
	Student(int age, int classNumber) {
		this.age = age;
		this.classNumber = classNumber;
	}
	
	@Override
	public int compare(Student o1, Student o2) {
    
		// o1의 학급이 o2의 학급보다 크다면 양수
		if(o1.classNumber > o2.classNumber) {
			return 1;
		}
		// o1의 학급이 o2의 학급과 같다면 0
		else if(o1.classNumber == o2.classNumber) {
			return 0;
		}
		// o1의 학급이 o2의 학급보다 작다면 음수
		else {
			return -1;
		}
	}
}

이런식으로 Comparable을 구현할때처럼 만들어 줄 수 있다.

하지만 비교구현하는부분이 좀 다른데

 

앞서 Comparable의 compareTo()와는 다르게, 두 객체를 비교하는 것이기 때문에

파라미터로 들어오는 o1과 o2의 classNumber을 비교해주는 것이다.

 

좀 더 구체적으로 말하자면 Comparable의 compareTo는 선행 원소가 자기 자신이 되고,

후행 원소가 매개 변수로 들어오는 o 가 되는 반면에,

Comparator의 compare는 선행 원소가 o1이 되고, 후행 원소가 o2가 된다.

 

이 말은, o1과 o2를 비교함에 있어 자기 자신(객체)은 두 객체(매개변수들) 비교에 영향이 없다는 것이다.

 

public class prac {

    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    public static void main(String[] args) throws IOException {

        Student a = new Student(17, 2); //17살 2반
        Student b = new Student(18, 1); //18살 1반
        Student c = new Student(19, 3);

        if (a.compare(b, c) < 0) {
            bw.write("b가 c보다 나이가 적다!");
        }

        bw.flush();
        bw.close();

    }
}

class Student implements Comparator<Student>{
    int age; // 나이
    int classNumber; //학급

    public Student(int age, int classNumber) { //생성자
        this.age = age;
        this.classNumber = classNumber;
    }


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

테스트 예제

테스트 결과이다.

 

테스트를 보면 a객체는 b와c의 비교에는 어떤 연관도 없지만 a객체안의 compare를 사용하기 위해 그저 사용된것이다.

물론 비교하는 부분을

if (b.compare(b, c) < 0) {
    bw.write("b가 c보다 나이가 적다!");
}

이걸로 바꿔서 b객체의 compare메소드를 사용해도 결과는 같다.

 

중요!

결론적으로 compare 메소드를 사용하기 위해서는 어떤객체든 필요하게 된다는것이다.

위에 예시처럼 아무객체나 가져와서 객체.compare 메소드를 사용하는것도 되고

Student comp = new Student(0, 0);	// 비교만을 위해 사용할 객체

그냥 비교만을 위한 더미객체를 만들어서 compare를 사용해도되지만

 

그것보다 더 좋은것은

 

Comparator의 비교기능만 떼어와서 사용하는것이다

 

 

익명 객체(클래스)를 활용해서 Comparator 응용하기

중요한 부분이다. 위에 예시처럼 객체안의 compare를 사용하기 보다는 익명객체를 이용해서 더 깔끔하게 사용해보자.

 

그전에 익명객체에 대해 알아야한다.

익명 객체는 쉽게 말해서 '이름이 정의되지 않은 객체'를 의미한다. 

 

자바는 객체지향 언어다.

그래서 어떠한 객체를 만든다고 한다면 class를 생성하여 이름을 정의한다.

위에서 예시로 들었던 Student 또한 Student라는 이름으로 정의된 객체다.

 

그럼 이름이 정의되지 않는다는 것은 무엇일까?

 

우리가 클래스를 생성할 때 class 키워드 다음에 이름을 정의했다.

하지만, 이름 없이 class를 정의하는것은 불가능하다.

 

하지만,

우리의 고민처럼 특정 구현 부분만 따로 사용한다거나, 부분적으로 기능을 일시적으로 바꿔야 할 경우생길 때가 있다. 이럴 때 사용할 수 있는 것이 바로 익명객체인데, 일단 코드를 먼저 보도록 하자.

public class Anonymous {
	public static void main(String[] args) {
	
    	// Rectagle 객체 a
		Rectangle a = new Rectangle();
		
		// 익명 객체 1 
		Rectangle anonymous1 = new Rectangle() {
		
			@Override
			int get() {
				return width;
			}
		};
		
		System.out.println(a.get());
		System.out.println(anonymous1.get());
		System.out.println(anonymous2.get());
	}
	
	// 익명 객체 2
	static Rectangle anonymous2 = new Rectangle() {
		
		int depth = 30;
		@Override
		int get() {
			return width * height * depth;
		}
	};
}
 
class Rectangle {
	
	int width = 10;
	int height = 20;
	
	int get() {	
		return height;
	}
}

일반적으로 객체를 생성할때

Rectangle a = new Rectangle();

이렇게 생성한다.

 

하지만 익명객체의 생성부분을 보면

Rectangle a = new Rectangle() { 
//...구현부...// 
};

이런식으로 구성되어있다. [일반적인 객체생성과 다르게 중괄호로 된 구현부가 존재한다]

 

왜 이름이 익명객체인것일까? 얼핏보면 일반적인 객체 생성방식처럼 보이지만

{} 블럭 안의 구현부를 주목해야한다.

 

일반적으로 우리가 객체를 생성하기위해서는 클래스를 먼저 정의한다.

클래스를 만들때 클래스의 이름을 정하고,

변수를 선언하고 , 메소드를 정의한다. 그 클래스의 이름을 이용해서 객체를 생성한다.

Rectangle의 클래스의 내용은

class Rectangle {
	
	int width = 10;
	int height = 20;
	
	int get() {	
		return height;
	}
}

다음과 같았고 

Rectangle a = new Rectangle();

Rectangle이라는 클래스이름을 가지고 a라는 Rectangle객체 (=Rectangle 클래스의 인스턴스)를 만든다. 일반적으로

 

또는 클래스를 정의할때 interface를 implements해서 인터페이스의 메소드를 재정의하거나

class를 상속(extends)해서 부모의 메소드,필드를 사용 또는 재정의하기도 한다.

class Rectangle extends Square implements Shape{

    int width = 10;
    int height = 20;

    int get() {
        return height;
    }
}

이렇게 클래스를 정의하면서 클래스의 이름은 필수적이다.

 

그러나 다시 위로가서

익명객체1,2의 생성부분의 코드를 보자.

// 익명 객체 1 
Rectangle anonymous1 = new Rectangle() {

    @Override
    int get() {
        return width;
    }
};
// 익명 객체 2
static Rectangle anonymous2 = new Rectangle() {

    int depth = 30;
    @Override
    int get() {
        return width * height * depth;
    }
};

구현부 ({  } 내부) 에서 변수를 선언하기도 하고 , Rectangle의 메소드 get()을 재정의(overide)하기도 하였다.

 

쉽게말해서

 

구현부 {} 에서 Rectangle을 상속받은 하나의 익명 클래스를 생성한것이고

그 익명클래스를 바로 이용해서 anonymous1,2 라는 객체를 생성한것이다.

 

위에서 Rectangle을 상속하여 익명객체를 생성한것처럼

익명 객체를 생성하려면 상속할 대상이 필요하다.

 

여기서 상속이라는것은 class의 extends 뿐만 아니라 interface의 implements도 말한다.

상속할 class나 구현할 interface가 필요하다는것(=익명 객체를 만들때 클래스나 인터페이스 둘다 된다는것)

 

예시코드

public class Anonymous {
	public static void main(String[] args) {
 
		Rectangle a = new Rectangle();
		
		Shape anonymous = new Shape() {
			int depth = 40;
			
			@Override
			public int get() {
				return width * height * depth;
			}
		};
 
		System.out.println(a.get());			// Shape 인터페이스를 구현한 Rectangle
		System.out.println(anonymous.get());	// Shape 인터페이스를 구현한 익명 객체
	}
 
}
 
class Rectangle implements Shape {
	int depth = 40;
	
	@Override
	public int get() {
		return width * height * depth;
	}
}
 
interface Shape {
 
	int width = 10;
	int height = 20;
 
	int get();
}
Shape anonymous = new Shape() {
    int depth = 40;

    @Override
    public int get() {
        return width * height * depth;
    }
};

이 부분을 보면

Shape라는 인터페이스를 구현하여  (구현부를 통해 해당 인터페이스를 구현한 익명 클래스를 정의하고)

익명객체 anonymous를 만든다.  ( 그 익명 클래스를 이용하여 바로 익명 객체 생성)

 

 

익명 객체 설명 끝!

 

다시돌아가서

원하는것은 Comparator 인터페이스의 compare 기능만 구현해서 사용하고 싶은것이였다.

 

방금 배운 익명객체를 적용해보자.

1. Comparator라는 인터페이스를 구현하는 익명 클래스를 구현부에서 정의하자.

2. 그걸 이용한 익명객체를 바로 만든다.

 

 

예시

public class prac {

    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    public static void main(String[] args) throws IOException {

        Student a = new Student(17, 2); //17살 2반
        Student b = new Student(18, 1); //18살 1반
        Student c = new Student(19, 3);
        
        //Comparator를 구현해서 익명객체를 생성해준다. -> 이렇게 main안에서 지역변수처럼 익명객체를 만들어줘도되고
        Comparator<Student> anonymous1 = new Comparator<Student>() {
            //원하는대로 재정의한다.
            @Override
            public int compare(Student o1, Student o2) {
                return o1.age-o2.age;
            }
        };
        
        bw.flush();
        bw.close();
    }

    //익명 객체 생성 방법 2 -> 위에 선언한 BufferedReader처럼 , main밖에서 정적타입 변수처럼 생성해도 된다. ( 생긴게 메소드 정의하는것 같디만 객체 생성이다)
    public static Comparator<Student> anonymous2 = new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.age - o2.age;
        }
    };
    
}
class Student{ //Comparator를 implements로 받지않는다.
    int age; // 나이
    int classNumber; //학급

    public Student(int age, int classNumber) { //생성자
        this.age = age;
        this.classNumber = classNumber;
    }
    
}

main 내부에서 지역변수처럼 익명객체를 생성하거나

main 밖에서 BufferedReader 객체 생성한것처럼 익명객체도 밖에서 생성해줘도된다. ( 메소드 정의하는것처럼 생겼기에 구분을 조심하자)

 

이렇게 익명객체를 생성했으므로

 

아까 Student클래스안에 compare를 재정의하고

다른 객체의 compare 메소드를 쓰는것보다,

익명클래스를 이용하여 Comparator를 재정의하고 , 익명객체를 만들면

익명객체의 compare를 이용해서 쉽게 비교가 가능하다.

 

응용 테스트 코드

public class prac {

    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    public static void main(String[] args) throws IOException {

        Student a = new Student(17, 2); //17살 2반
        Student b = new Student(18, 1); //18살 1반
        Student c = new Student(19, 3);

        //Comparator를 구현해서 익명객체를 생성해준다. -> 이렇게 main안에서 지역변수처럼 익명객체를 만들어줘도되고
        Comparator<Student> anonymous1 = new Comparator<Student>() {
            //원하는대로 재정의한다.
            @Override
            public int compare(Student o1, Student o2) {
                return o1.age-o2.age;
            }
        };

        if (anonymous1.compare(a, b) < 0) {
            bw.write("익명객체1 이용 : a가 b보다 어립니다.\n");
        }
        if (anonymous2.compare(a, b) < 0) {
            bw.write("익명객체2 이용 : a가 b보다 어립니다.");
        }


        bw.flush();
        bw.close();
    }

    //익명 객체 생성 방법 2 -> 위에 선언한 BufferedReader처럼 , main밖에서 지역변수처럼 생성해도 된다. ( 생긴게 메소드 정의하는것 같디만 객체 생성이다)
    public static Comparator<Student> anonymous2 = new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.age - o2.age;
        }
    };

}
class Student{ //Comparator를 implements로 받지않는다.
    int age; // 나이
    int classNumber; //학급

    public Student(int age, int classNumber) { //생성자
        this.age = age;
        this.classNumber = classNumber;
    }

}

익명객체를 이용한 테스트 결과 -> 잘 작동한다!

 

 

익명 객체를 사용하면 좋은 점이 하나 더 있다.

익명 객체는 이름이 정의되지 않은 하나의 새로운 클래스의 인스턴스와 같다고 보면 된다.

 

클래스를 상속(구현)할 때, 이름만 다르게 하면 몇 개던 여러개를 생성할 수 있듯이,

익명 객체를 가리키는 변수명만 달리하면

몇 개든 자유롭게 다른 클래스와 인터페이스를 상속,구현해서 익명객체를 생성가능하다.

 

이런식으로 원하는대로 재구현(overide)해서 다른 비교기준을 가지는 익명객체로 응용가능

// 학급 대소 비교 익명 객체
public static Comparator<Student> comp = new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.classNumber - o2.classNumber;
    }
};

// 나이 대소 비교 익명 객체
public static Comparator<Student> comp2 = new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
};

 

 

의문?

Comparable도 익명객체로 할수있지 않을까?

-> Comparable은 자기자신과 다른 객체를 비교하는것이다.

 

익명객체로 구현하게된다면

자기자신  == 익명객체 (익명클래스의 인스턴스)

 

다른객체 == 전달인자 (비교대상으로 들어오는 객체)

 

둘이 절대 같을 수가없다.

익명객체는 방금 익명클래스를 만들어서 생성한 유일무의한 객체이기 때문이다.

 

 

Comparable, Comparator을 이용한 정렬 방법 [중요]

지금까지는 Comparable과 Comparator을 이용하여 비교하는 방법까지 배웠다.

리턴되는 int값이 양수인지 0인지 음수인지 받아서 체크하는 방법으로 비교를 했었다.

 

Comparable, Comparator을 이용한 정렬 방법을 설명하기전에

일반적인 정렬을 설명해야한다.

 

Java에서의 정렬은 특별한 정의가 되어있지 않는 한 '오름차순'을 기준으로 한다.

 

오름차순으로 정렬이 된다는 것은 무엇일까?

 

예로들어 {1, 3, 2} 배열이 있다고 가정해보자.

그럼 우리가 최종적으로 얻어야 할 배열 {1, 2, 3} 을 얻기 위해 정렬 알고리즘을 사용하게 될 것이다.

이 때, 정렬을 하기 위해 두 원소를 비교 하게 될 것 아닌가?

정렬 메소드에서 두 수를 비교하기 위해 index 0 원소와 index 1 원소를 비교한다고 가정해보자.

그럼 선행 원소인 1과 후행 원소인 3의 경우 대소관계는 어떻게 되는가? 1이 3보다 작다. 

 

앞서 선행 원소와 후행 원소를 비교 할 때, 얼마큼 차이가 나는지를 반환한다고 헀다.

return o1 - o2; 를 한다면, 1-3 = -2로 '음수'가 나올 것이다. 

 

이 때, 자바에서는 오름차순을 디폴트 기준으로 삼고 있다고 했다. 이 말은 선행 원소가 후행 원소보다 '작다'는 뜻이다.

(위에서 1,3 을 비교한것처럼)

 

즉, compare 혹은 compareTo를 사용하여 객체를 비교 할 경우 음수가 나오면 두 원소의 위치를 바꾸지 않는다는 것이다.

[(정렬 기준이 오름차순이므로)   1,3만 본다면 {1,3}은 오름차순이니까!]

 

그 다음 정렬 알고리즘에 의해 index 1 원소와 index 2 원소를 비교한다고 해보자.

선행 원소인 3이 후행원소 2보다 크다.

 

compare 혹은 compareTo를 사용하여 index 1 원소와 index 2 원소를 비교한다면 '양수'가 나올 것이다. (3-2 = 1) 이는 곧 이러면 선행 원소가 후행 원소보다 크다는 뜻이라는 것이다.

 

즉, compare 혹은 compareTo를 사용하여 객체를 비교 할 경우 양수가 나오면 두 원소의 위치를 바꾼다는 것이다.

그러면 {1, 2, 3} 으로 오름차순으로 정렬 될 것이다.

 

 

위에서 살펴본 동작대로 규칙을 일반화할 수 있다.

 

 

[두 수의 비교 결과에 따른 작동 방식]

 

음수일 경우 : 두 원소의 위치를 교환 안함

양수일 경우 : 두 원소의 위치를 교환 함

 

 

Arrays.sort() 사용해보기

Arrays.sort()는 primitive 타입의 정렬만 가능하다! 

객체타입은 정렬 기준이 없기 때문이다.

 

하지만

Arrays.sort()의 내부동작을 보면

Comparator의 compare또는 Comparable의 compareTo 메소드를 활용하는 정렬도 가능하다.

 

객체가 Comparable을 구현해서 compare 재정의 했던가

또는

익명객체를 이용해서 compareTo를 재정의하고 Arrays.sort()의 매개변수로 Comparator를 전달해준다면

 

그 객체도 Arrays.sort를 이용해서 정렬이 가능하다!

 

Comparable 구현해서 객체 정렬하기

import java.util.Arrays;
 
public class Test {
	
	public static void main(String[] args) {
		
		MyInteger[] arr = new MyInteger[10];
		
		// 객체 배열 초기화 (랜덤 값으로) 
		for(int i = 0; i < 10; i++) {
			arr[i] = new MyInteger((int)(Math.random() * 100));
		}
 
		// 정렬 이전
		System.out.print("정렬 전 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
		
		Arrays.sort(arr);
        
		// 정렬 이후
		System.out.print("정렬 후 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
	}
	
}
 
class MyInteger implements Comparable<MyInteger> {
	int value;
	
	public MyInteger(int value) {
		this.value = value;
	}
	
	@Override
	public int compareTo(MyInteger o) {
		return this.value - o.value;
	}
	
}

MyInteger이라는 클래스에 Comparable을 implements하고 compareTo를 구현했기에

MyInteger객체 배열은 Array.sort()를 사용할 수 있다. 

(Array.sort()내부에서 MyInteger 클래스 내부의 재정의된 compareTo를 이용해서 정렬을 해줄거니까!)

 

(구현안해주면 Arrays.sort()쓸때오류발생 -> 정렬 기준이 없으니까)

 

 

Comparator 익명객체 구현해서 객체배열 정렬하기

import java.util.Arrays;
import java.util.Comparator;
 
public class Test {
	
	public static void main(String[] args) {
		
		MyInteger[] arr = new MyInteger[10];
		
		// 객체 배열 초기화 (랜덤 값으로) 
		for(int i = 0; i < 10; i++) {
			arr[i] = new MyInteger((int)(Math.random() * 100));
		}
 
		// 정렬 이전
		System.out.print("정렬 전 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
		
		Arrays.sort(arr, comp);		// MyInteger에 대한 Comparator을 구현한 익명객체를 넘겨줌
        
		// 정렬 이후
		System.out.print("정렬 후 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
	}
 
	
	static Comparator<MyInteger> comp = new Comparator<MyInteger>() {
		
		@Override
		public int compare(MyInteger o1, MyInteger o2) {
			return o1.value - o2.value;
		}
	};
}
 
 
class MyInteger {
	int value;
	
	public MyInteger(int value) {
		this.value = value;
	}
	
	
}

main내부가 아직 외부에 빼서

정적변수로  Comparator 익명객체를 생성해주었다.

생성한 익명객체를 Arrays.sort()로 객체배열과 함께 전달해주면 익명객체안에 정의한 비교 방법을 기준으로 정렬해준다.

 

 

Collections.sort()사용하기

 static class Position implements Comparable<Position> {
        int x;
        int y;

        public Position(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public int compareTo(Position o) {
            if (this.x == o.x) {
                return this.y - o.y;
            }
            return this.x - o.x;
        }

    }

Comparable의 compareTo를 구현했기때문에

Collection.sort()에 Coparator를 넣지않아도 잘 동작한다.

 

List.sort()는 반드시 매개변수로 Comparator를 넣도록 되어있어서 동작하지않는다.

List.sort()를 사용하려면 익명객체로 넣을때 사용하면 될듯하다.

 

하지만 두가지 경우 다 쓸수있는 Collections.sort를 주로 사용할것.

 

 

만약에 내림차순을 구현하고 싶을때는?

 

위에서 

[두 수의 비교 결과에 따른 작동 방식]

 

음수일 경우 : 두 원소의 위치를 교환 안함

양수일 경우 : 두 원소의 위치를 교환 함

 

이렇게 일반화 했었다.

 

그리고 compare,compareTo 메소드를 구현할때 다음과 같았다.

// Comparable
public int compareTo(MyClass o) {
	return this.value - o.value;
}
 
// Comparator
public int compare(Myclass o1, MyClass o2) {
	return o1.value - o2.value;
}

이는 선행 원소가 후행 원소보다 작으면 compare 혹은 compareTo 메소드의 반환값이 음수가 나오고,

정렬 알고리즘에서는 두 원소를 비교할 때 두 원소는 오름차순 상태라는 의미이므로 두 원소가 교환되지 않는다는 것이다.

 

반대로 선행 원소가 후행원소보다 크면  compare 혹은 compareTo 메소드의 반환값이 양수가 나오고,

정렬 알고리즘에서는 두 원소를 비교할 때 두 원소는 내림차순 상태라는 의미이므로 두 원소가 교환된다는 것이다.

 

즉, 정렬 알고리즘에서는 두 원소를 compare 혹은 compareTo 를 써서

양수값이 나오냐, 음수값이 나오냐에 따라 판단을 한다는 것이다.

 

위 방법이 오름차순을 구현하는 방법이고

내림차순으로 정렬하고 싶다면 두 원소를 비교한 반환값을 반대로 해주면 된다

 

쉽게 말해

두 값의 차가 양수가 된다면(선행원소가 후행원소보다 클때) 이를 음수로 전환해서 자리를 안바꾸게하고,

두 값의 차가 음수가 된다면(선행원소가 후행원소보다 작을때) 이를 양수로 전환해서 자리를 바꾸게끔해서

 

내림차순을 구현하는것이다.

[정렬은 오름차순이 기준이니까 , 그 기준의 규칙을 이용하는것]

 

양수와 음수를 전환하는 방법은 다음과 같이 반환값의 부호를 바꿔줘서 바꾼다.

// Comparable
public int compareTo(MyClass o) {
	return -(this.value - o.value);
}
 
// Comparator
public int compare(Myclass o1, MyClass o2) {
	return -(o1.value - o2.value);
}

-(this.value - o.value) 는 분배법칙으로 인해 -this.value + o.value가 되고 

정리하면

o.value - this.value가 된다.

 

-(o1.value - o2.value)는 분배법칙으로 인해 -o1.value + o2.value가 되고

정리하면

o2.value - o1.value가 된다.

 

이걸 이용해서 더 깔끔하게 코드를 적어보면

// Comparable
public int compareTo(MyClass o) {
	return o.value - this.value;	// == -(this.value - o.value);
}
 
// Comparator
public int compare(Myclass o1, MyClass o2) {
	return o2.value - o1.value;		// == -(o1.value - o2.value);
}

가 된다.

 

 

내림차순 Comparable 이용 정렬

import java.util.Arrays;
import java.util.Comparator;
 
public class Test {
	
	public static void main(String[] args) {
		
		MyInteger[] arr = new MyInteger[10];
		
		// 객체 배열 초기화 (랜덤 값으로) 
		for(int i = 0; i < 10; i++) {
			arr[i] = new MyInteger((int)(Math.random() * 100));
		}
 
		// 정렬 이전
		System.out.print("정렬 전 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
		
		Arrays.sort(arr);		// MyInteger에 대한 Comparable을 사용하여 정렬
        
		// 정렬 이후
		System.out.print("정렬 후 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
	}
}
 
 
class MyInteger implements Comparable<MyInteger> {
	int value;
	
	public MyInteger(int value) {
		this.value = value;
	}
	
	@Override
	public int compareTo(MyInteger o) {
		return o.value - this.value;
	}
	
}

 

내림차순 Comparator 이용 정렬

import java.util.Arrays;
import java.util.Comparator;
 
public class Test {
	
	public static void main(String[] args) {
		
		MyInteger[] arr = new MyInteger[10];
		
		// 객체 배열 초기화 (랜덤 값으로) 
		for(int i = 0; i < 10; i++) {
			arr[i] = new MyInteger((int)(Math.random() * 100));
		}
 
		// 정렬 이전
		System.out.print("정렬 전 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
		
		Arrays.sort(arr, comp);		// MyInteger에 대한 Comparator을 구현한 익명객체를 넘겨줌
        
		// 정렬 이후
		System.out.print("정렬 후 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
	}
 
	
	static Comparator<MyInteger> comp = new Comparator<MyInteger>() {
		
		@Override
		public int compare(MyInteger o1, MyInteger o2) {
			return o2.value-  o1.value;
		}
	};
}
 
 
class MyInteger {
	int value;
	
	public MyInteger(int value) {
		this.value = value;
	}
	
	
}

 

이해는 했지만

암기하기가 쉽지않아보인다.

 

암기하는법

모든 정렬은 오름차순이 기준임을 기억하고

/*
 [오름차순]
 작은 원소가 큰 원소보다 앞에 있으므로 오름차순이다.
 */
public int compareTo(MyClass o) {
	return this.value - o.value;
}
public int compare(Myclass o1, MyClass o2) {
	return o1.value - o2.value;	
}
 
 
/*
 [내림차순]
 큰 원소가 작은 원소보다 앞에 있으므로 내림차순이다.
 */
public int compareTo(MyClass o) {
	return o.value - this.value;
}
public int compare(Myclass o1, MyClass o2) {
	return o2.value - o1.value;
}

선행원소가 앞에오고 후행원소가 뒤에간 경우라면 (선행원소 - 후행원소) => 오름차순

+ 앞에서 뒤에꺼를 비교하는것이 정석 -> 정석 == 오름차순

 

후행원소가 앞에오고 선행원소가 뒤에간 경우라면 (후행원소 - 선행원소) => 내림차순

+ 뒤에서 앞에꺼를 비교하는것은 정석이 아님 -> 내림차순

 

 

 

보면 알겠지만, Comparator는 익명객체로 여러개를 생성할 수 있지만,

Comparable의 경우 compareTo 하나 밖에 구현할 수 없다.

그렇다보니, 보통은 Comparable은 여러분이 비교하고자 하는 가장 기본적인 설정(보통은 오름차순)으로 구현하는 경우가 많고, Comparator는 여러개를 생성할 수 있다보니 특별한 정렬을 원할 때 많이 쓰인다.

쉽게 말해 Comparable은 기본(default) 순서를 정의하는데 사용되며, Comparator은 특별한(specific) 기준의 순서를 정의할 때 사용된다는 것이다.

 

 

 


람다 이용해서 간단하게 사용하기

https://inpa.tistory.com/entry/%E2%98%95-%EB%9E%8C%EB%8B%A4%EC%8B%9D-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-Comparator

 

☕ 람다식 리팩토링 하기 (Comparator 축약 원리)

Comparator 람다식 리팩토링 해보기 다음은 실제로 자바 프로그래밍에서 배열을 정렬(sort) 할때 사용되는 Comparator 인터페이스 사용 예제이다. Apple 클래스가 있고 생성자 인자로 사과의 무게(weight)

inpa.tistory.com

 

https://keeeeeepgoing.tistory.com/66

 

 

결론

객체든 ,2차원배열이든, 뭐든

원하는 정렬방식을 구현해서 (Comparator인터페이스를 구현한 익명클래스의 익명객체) 

정렬할 수 있다.

 

 

 

Comparator 이용하는 문제 예제

 

백준/11650 좌표 정렬하기

좌표 정렬하기 시간 제한메모리 제한제출정답맞힌 사람정답 비율 1 초 256 MB 94418 44658 34535 47.988% 문제 2차원 평면 위의 점 N개가 주어진다. 좌표를 x좌표가 증가하는 순으로, x좌표가 같으면 y좌표

keeeeeepgoing.tistory.com

 

 

출처,참고,더자세한정보

 

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

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

st-lab.tistory.com

 

'자바 > ++' 카테고리의 다른 글

[Java] compareTo()  (0) 2022.12.05
[Java] 람다 [미완성]  (0) 2022.12.05
[Java] Collection (컬렉션) 정리  (0) 2022.12.04
[Java] 나눗셈  (0) 2022.12.04
[Java] Split 메소드 문자열 자르기  (0) 2022.12.03

댓글