Java

[Java] Comparable / Comparator에 대해서 알아보자! (feat. 백준알고리즘-1181)

coding-knowjam(코딩노잼) 2021. 1. 14.
728x90

안녕하세요 coding-knowjam입니다.

오늘은 Comparable, Comparator 인터페이스에 대해서 알아보겠습니다.

 

 

1. Comparable Interface

1.1 API 문서

Java 8 API 문서에서 Comparable Interface는 다음과 같이 설명하고 있습니다.

(API 문서 : https://docs.oracle.com/javase/8/docs/api/) 

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 objects that implement this interface can be sorted automatically by Collections.sort (and Arrays.sort). Objects that implement this interface can be used as keys in a sorted map or as elements in a sorted set, without the need to specify a comparator.
(... 중략 )

내용을 정리하면 Comparable Interface를 구현한 클래스는 compareTo() 메서드를 오버라이드 해서 구현 클래스 객체들 간의 정렬 기준을 정의할 수 있고, 이를 natural ordering이라 부릅니다.

natural ordering을 정의한 클래스들은 정렬 함수를 통해서 정렬을 할 수 있습니다.

리스트는 Collection.sort()를, 배열은 Arrays.sort()를 사용합니다.

 

1.2 구현 방법

구현을 위해 코드를 작성해보겠습니다.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

// 상품 클래스
class Product implements Comparable<Product>{
	
	//상품 이름
	String name;

	//상품 가격
	int price;
	
	Product(String name, int price) {
		this.name = name;
		this.price = price;
	}

	@Override
	public int compareTo(Product other) {
		// return값이 양수면 자리 교환, 0이거나 음수면 그대로
		return this.name.charAt(0) > other.name.charAt(0) ? 1 : -1;
	}

	@Override
	public String toString() {
		return "Product [name=" + name + ", price=" + price + "]";
	}
}

public class ComparableTest {
	
	public static void main(String[] args) {
		Product product1 = new Product("갤럭시폴드2", 2000000);
		Product product2 = new Product("LG롤러블", 2300000);
		Product product3 = new Product("아이폰12PRO", 1700000);
		
		// List 사용
		List<Product> list = new ArrayList<>();
		list.add(product1);
		list.add(product2);
		list.add(product3);
		
		System.out.println("List 정렬 전");
		list.forEach((s) -> System.out.println(s));

		Collections.sort(list);
		
		System.out.println("List 정렬 후");
		list.forEach((s) -> System.out.println(s));
		
		// 배열 사용
		Product[] array = {product1, product2, product3};
		
		System.out.println("배열 정렬 전");
		Arrays.stream(array).forEach(s -> System.out.println(s));
		
		Arrays.sort(array);
		
		System.out.println("배열 정렬 후");
		Arrays.stream(array).forEach(s -> System.out.println(s));
	}
}

Comparable Interface를 구현하기 위해 Product Class를 만들어주고 내부에 상품 이름과 가격 필드를 선언하였습니다.

Comparable Interface를 구현하면 compareTo() 메서드를 오버라이드 해야 하며 작성한 기준으로 정렬이 이루어집니다.

 

위의 예제는 상품 앞글자를 기준으로 가나다순으로 정렬되도록 작성하였습니다.

main메서드를 보면 리스트일 때와 배열일 때 사용하는 정렬 함수가 다른 것을 볼 수 있습니다.

해당 예제는 Java 8 버전 이상을 사용하므로 결과 출력에 스트림과 람다식을 사용하였습니다.

실행결과는 다음과 같습니다.

Comparable 구현 실행결과 1

결과를 보면 한글보다 영문이 앞으로 정렬되어있는데 영문의 유니코드 값이 한글의 유니코드 값보다 작기 때문에 영문이 맨 앞으로 정렬된 것입니다.

현재 정렬 기준의 반대로 정렬을 하고 싶으시면 삼항 연산자의 부등호를 바꿔주시면 됩니다.

(사실 문자열의 경우 앞글자만 비교하면 안 되고 문자열 모두 비교해서 정렬해야 하는데 이해하시는 데 있어서 혼란이 올 것 같아 앞글자 기준으로 정렬이 되도록 작성하였습니다.

전체비교 정렬을 위해서는 return문에 this.name.compareTo(other.name); 를 작성해주시면 됩니다.

String Class도 Comparable Interface를 구현하고 있기 때문에 직접 compareTo() 메서드를 사용하는 것이 가능합니다.)

추가 내용으로 자주 사용하는 primitive타입(int, double 등등)의 래퍼 클래스들은 Comparable Interface를 구현하고 있기 때문에 개발자가 직접 구현하지 않아도 해당 클래스 타입의 배열과 리스트에 정렬 함수를 사용할 수 있습니다.

 

추가 예제를 하나 더 보겠습니다.

만약 상품 앞 글자가 동일할 때 가격을 기준으로 오름차순 정렬을 하고 싶다면 어떻게 작성해야 할까요?

삼항 연산자는 가독성이 별로이므로, if문을 통해서 작성해보겠습니다.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

// 상품 클래스
class Product implements Comparable<Product>{
	
	//상품 이름
	String name;

	//상품 가격
	int price;
	
	Product(String name, int price) {
		this.name = name;
		this.price = price;
	}

	@Override
	public int compareTo(Product other) {
		// return값이 양수면 객체 교환, 0이거나 음수면 그대로
		if(this.name.charAt(0) > other.name.charAt(0)) {
			return 1;	
		}else if(this.name.charAt(0) == other.name.charAt(0)) {
			if(this.price > other.price) {
				return 1;
			}else {
				return -1;
			}
		}else {
			return -1;
		}
	}

	@Override
	public String toString() {
		return "Product [name=" + name + ", price=" + price + "]";
	}
}

public class ComparableTest {
	
	public static void main(String[] args) {
		Product product1 = new Product("갤럭시폴드2", 2000000);
		Product product2 = new Product("LG롤러블", 2300000);
		Product product3 = new Product("아이폰12PRO", 1700000);
		Product product4 = new Product("갤럭시폴드2(중고)", 1400000);
		Product product5 = new Product("LG롤러블(중고)", 1700000);
		Product product6 = new Product("아이폰12PRO(중고)", 1300000);
		
		
		// List 사용
		List<Product> list = new ArrayList<>();
		list.add(product1);
		list.add(product2);
		list.add(product3);
		list.add(product4);
		list.add(product5);
		list.add(product6);
		
		System.out.println("List 정렬 전");
		list.forEach((s) -> System.out.println(s));

		Collections.sort(list);
		
		System.out.println("List 정렬 후");
		list.forEach((s) -> System.out.println(s));
		
		
		// 배열 사용
		Product[] array = {product1, product2, product3, product4, product5, product6};
		
		System.out.println();
		System.out.println("배열 정렬 전");
		Arrays.stream(array).forEach(s -> System.out.println(s));
		
		Arrays.sort(array);
		
		System.out.println("배열 정렬 후");
		Arrays.stream(array).forEach(s -> System.out.println(s));
	}
	
}

compareTo() 메서드를 수정해주었고, 테스트를 위해 Product 객체를 추가해주었습니다.

return값이 양수일 때 객체 간의 교환이 이루어진다는 점만 기억하시면 이해하시는데 어렵지 않을 겁니다.

실행을 하면 다음과 같이 정렬이 이루어집니다.

Comparable 구현 실행결과 2

 

 

2. Comarator Interface

2.1 API 문서

@FunctionalInterface
A comparison function, which imposes a total ordering on some collection of objects. Comparators can be passed to a sort method (such as Collections.sort or Arrays.sort) to allow precise control over the sort order. Comparators can also be used to control the order of certain data structures (such as sorted sets or sorted maps), or to provide an ordering for collections of objects that don't have a natural ordering.The ordering imposed by a comparator c on a set of elements S is said to be consistent with equals if and only if c.compare(e1, e2)==0 has the same boolean value as e1.equals(e2) for every e1 and e2 in S.
(.. 중략)

Comparator API문서 설명이 조금 애매한 부분이 있습니다.

그래서 그냥 실제 구현해서 사용하는 방법을 토대로 이해한 문서 내용을 말씀드리면 다음과 같습니다.

Comparator Interface를 구현한 클래스는 compare() 메서드를 오버라이드 해서 정렬 기준을 정의할 수 있습니다.

구현 클래스를 Arrays.sort() 또는 Collections.sort() 메서드에 파라미터로 넘겨 compare() 메서드에 정의한 기준으로 정렬을 수행할 수 있습니다.

구현 클래스를 정렬 함수에 파라미터로 넘기면 정렬이 될 리스트나 배열은 Comparable Interface를 구현해도 되고, 구현하지 않아도 됩니다.

추가 내용으로 Comparable Interface에는 @FunctionallInterface이 붙어있는데 이를 함수형 인터페이스라고 부르며, 메서드에서 함수형 인터페이스를 파라미터로 받을 경우 람다식으로 사용이 가능합니다.

 

2.2 구현 방법

백준 알고리즘 문제를 가지고 구현해보겠습니다. (문제도 풀고 코드도 보고~)

(문제 링크 : https://www.acmicpc.net/problem/1181)

문제 내용을 간단히 적으면 영문자로 이루어진 단어를 정렬하는 문제입니다.

정렬 규칙은 1. 길이가 짧은 순으로 2. 길이가 같으면 사전 순서대로 입니다.

package com.nojam.coding.algorithm;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class BOJ_1181 {

	public static void main(String[] args) throws NumberFormatException, IOException {
		
		// 입력을 받기 위한 BufferedReader
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		// 출력에 사용할  BufferedWriter
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

		int n = Integer.parseInt(br.readLine());
		
		List<String> word_array = new ArrayList<String>();
		
		for(int i=0 ; i<n ; i++) {
			word_array.add(br.readLine());
		}
		
		// 파라미터로 넘길 Comparator타입객체를 익명객체로 생성해줍니다.
		Collections.sort(word_array, new Comparator<String>() {
			@Override
			public int compare(String o1, String o2) {
				// o1은 현재객체, o2는 비교객체
				// 길이가 짧은순으로 정렬을 위해 현재객체의 길이가 길면 교환합니다.
				if(o1.length() > o2.length()) {
					return 1;
				}else if(o1.length() == o2.length()){
					// 길이가 같을 경우 compareTo를 사용해서 사전순 정렬
					return o1.compareTo(o2);
				}else {
					return -1;
				}
			}
		});
		
		// stream을 이욯해서 중복제거 및 출력
		word_array.stream().distinct().forEach(s->{
			try {
				bw.write(s);
				bw.newLine();
			} catch (IOException e) {
				e.printStackTrace();
			}
		});
		
		bw.flush();
		bw.close();
		
	}
}

대부분의 코드들은 입력을 받고 출력을 하기 위해 작성한 것이므로 Collections.sort() 부분을 보겠습니다.

Collections.sort() 메서드에 파라미터로 넘겨줄 Comparator Interface는 익명 객체로 구현했습니다.

이 부분은 익명 객체로 하셔도 되고, 직접 클래스를 만들어서 implements후에 해당 클래스를 파라미터로 사용하셔도 되고, 람다식으로 작성하셔도 됩니다.

Comparator Interface에서 오버라이드 해야 하는 compare() 메서드는 앞서 봤던 compareTo() 메서드와 작성요령은 동일합니다.

return값이 양수이면 객체의 교환이 발생하고, 그 외에는 객체의 교환이 일어나지 않습니다.

길이가 같을 경우 compareTo() 메서드를 사용해서 사전 순 정렬을 하고 있는데, 해당 메서드는 String클래스가 기본적으로 Comparator Interface를 구현하고 있기 때문에 사용이 가능한 것입니다.

 

알고리즘 문제에서 예시로 준 입력값은 다음과 같습니다.

13 but i wont hesitate no more no more it cannot wait im yours

해당 입력값을 가지고 위의 작성한 코드에 넣어서 실행하면 다음과 같이 정렬이 됩니다.

입력 개수보다 출력 개수가 줄어든 것은 문제에서 중복 단어 제거가 있어서 그렇습니다.

Comparator 구현 실행결과


728x90

댓글