만족은 하되 안주하지는 말자

기록해야 기억한다

프로그래밍/JAVA

[Java] 내가 만든 클래스 객체 배열 정렬하기 (comparable, comparator, comparing)

D36choi 2021. 2. 14. 16:55
728x90

www.baeldung.com/java-sorting

 

Sorting in Java | Baeldung

Practical introduction to sorting in Java.

www.baeldung.com

 

내가 직접 추상화해 만든 클래스의 객체들로 이루어진 배열을 정렬하고자 할 때 어떤 방법들이 있을까?
아래와 같은 방법들을 이용해 정렬이 가능하다.

 

자바에서, 정렬 메소드에 대한 이해

public class Coffee {
  private String name;
  private int price;

  public Coffee(String name, int price) {
    this.name = name;
    this.price = price;
  }
  public String getName() {
    return this.name;
  }
  public int getPrice() {
    return this.price;
  } 
}

 

위와 같이, 나만의 클래스인 Coffee 를 만들어 내가 팔고자 하는 커피 메뉴들의 배열을 정렬해보도록 하자.

 

class Main {

  public static void main(String[] args) {
        Coffee[] coffees = new Coffee[]{
                new Coffee("Americano",3500),
                new Coffee("Green tea Latte",5500),
                new Coffee("Vanilla Latte",4500),
                new Coffee("Espresso",3000)
        };

        Arrays.sort(coffees); // ERROR
        for (Coffee coffee : coffees) {
            System.out.println(coffee.toString());
        }
    }
  
}

sorting  의 가장 간단한 방법은 Collections.sort 와 Arrays.sort 가 있다. 두 메소드 전부 O(n log n)의 속도를 보장한다. 

(선택하는 정렬의 방법이 조금 다르다는 차이가 존재한다.)

 

위 코드는 동작하지 않는다. 왜냐면, sort 의 인자가 우리가 만든 custom object 가 될수 없기 때문이다. 적어도 해당 객체 내에서 어떤 멤버 변수를 키로써 정렬할지를 지정해야 한다. 

이 때 배열 내 객체는 자연적인 순서 natural order 로 정렬되도록 한다.

If the List consists of String elements, it will be sorted into alphabetical order. If it consists of Date elements, it will be sorted into chronological order. How does this happen? String and Date both implement the Comparable interface. Comparable implementations provide a natural ordering for a class, which allows objects of that class to be sorted automatically. The following table summarizes some of the more important Java platform classes that implement Comparable. -- Oracle Java Doc

위 내용을 내 나름 해석해보니 이런 내용이었다. 

 

리스트가 문자열을 포함한다면, 이 것은 알파벳순으로 정렬된다. 시간이라면? 시간순으로 정렬된다. 이는 자연스럽다. 자바 플랫폼에서 존재하는 Comparable 인터페이스에 이 "자연스러운 순서"대로 정렬이 되도록 명시되어있다. 우리는 이를 활용하면 되겠다.

 

해당 인터페이스를 통해 Price 순서대로 커피 리스트가 정렬되게 해보자.

 

Comparable interface 를 이용한 정렬

public class Coffee implements Comparable {
    private final String name;
    private final int price;

    public Coffee(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public Integer getPriceAsInteger() {
        return this.price;
    }
    
    @Override
    public boolean equals(Object obj) {
        return ((Coffee) obj).getPriceAsInteger().equals(getPriceAsInteger());
    }

    @Override
    public int compareTo(Object o) {
        Coffee e = (Coffee) o;
        return getPriceAsInteger().compareTo(e.getPriceAsInteger());
    }

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

 

comparable interface 를 구현토록 한다. equals 와 compareTo 메서드의 오버라이딩을 통해 위와 같이 두 객체간의 순서 비교를 price 멤버 변수를 가져와 정하도록 한다. 이과정에서, 코드가 좀 지저분해 보이는데 ..

 

왜 price 를 Integer wrapper 를 통해 가져오는가?

 

-> 원시값은 equals 를 호출할 수 없기 때문이다. 아마 비교연산자를 통해 하면 가능할 것이지만 여기선 Integer 클래스로 포장을 진행했다.

 

결과

Coffee{name='Espresso', price=3000}
Coffee{name='Americano', price=3500}
Coffee{name='Vanilla Latte', price=4500}
Coffee{name='Green tea Latte', price=5500}

위의 메인함수를 실행하면 위처럼 가격에 따라 순차적으로 커피가 출력되게 될 것이다.

equals, compareTo 의 오버라이딩을 통해 개발자가 만든 객체의 정렬이 가능해졌다!

 

comparator 를 통한 정렬

 

 

만약, 커피 객체 안의 멤버 변수가 위처럼 간단하지않고 토핑 / 색깔 / 주문자이름 / 메뉴도입시기 등 매우 매우 다양한 방법으로 객체 내 데이터가 존재한다면? 그리고 매번 정렬하고 싶은 놈이 달라진다면? 

 

sort 를 호출할 때마다 무엇으로 정렬할지를 명시할 수 있는 것이 이 방법이다.

먼저 위의 Coffee 클래스의 인터페이스 및 오버라이딩 함수를 제거한다.

메인을 아래와 같이 고친다.

 

public class CoffeeMain {
    public static void main(String[] args) {
        Coffee[] coffees = new Coffee[]{
                new Coffee("Americano",3500),
                new Coffee("Green tea Latte",5500),
                new Coffee("Vanilla Latte",4500),
                new Coffee("Espresso",3000)
        };

        Arrays.sort(coffees, new Comparator<Coffee>() {
            @Override
            public int compare(Coffee c1, Coffee c2) {
                return Integer.compare(c1.getPrice(),c2.getPrice());
            }
        });
        for (Coffee coffee : coffees) {
            System.out.println(coffee.toString());
        }
    }
}

 

sort API 의 두번째 파라미터로 이너클래스 객체를 전달하게 된다. 해당 객체는 비교할 두 객체를 인자로 받아

각 커피의 가격을 꺼내와 Integer 클래스에 정의된 compare 함수로 두 가격 정수 원시값을 비교해 

 

coffee1 가격 == coffee2 가격 일 경우 0 리턴

coffee1 가격 < coffee2   일 경우 음수 리턴

coffee1 가격 > coffee2  일 경우 양수 리턴

 

을 진행하게 된다!

 

람다식을 이용해 Comparator 코드를 줄여보자

 

 

이렇게 코드로써 한번만 쓰이고 버려지는 함수 또는 객체에 대해서는 람다식을 활용하면 로직도 축약되고 가시성이 높아질 수 있다.

저 복잡한 이너클래스 생성 코드를 람다식(자바8 도입)을 통해 간단하게 바꿀 수 있다.

 

// ...
Arrays.sort(coffees, (c1, c2) -> Integer.compare(c1.getPrice(),c2.getPrice()));
// ...

 

5줄의 코드가 1줄로 줄어드는 깔끔함을 보여준다...! 역시 람다란... 매력적이다. 위 코드는 아래와 같다. 극한의 축약이 가능해진다.

 

Arrays.sort(coffees, (c1, c2) -> {
                return Integer.compare(c1.getPrice(),c2.getPrice());
        });

 

위 수정사항대로 메인함수를 실행하면 똑같이 정렬된 결과를 보여준다.

 

여러 조건으로 비교를 하고싶다면 Comparing, thenComparing 을 활용하자

 

 

만약 가격이 동일한 커피 제품이 수십개라면 어떨까? 동일 가격의 커피들에 대해서도 가독성 좋게 이름순으로 정렬하고 싶을 수 있다.

그경우에는 Comparing 을 활용할 수 있다.

 

public static void main(String[] args) {
        Coffee[] coffees = new Coffee[]{
                new Coffee("Green tea",5500),
                new Coffee("StrawBerry Latte",5500),
                new Coffee("Americano",3500),
                new Coffee("Green tea Latte",5500),
                new Coffee("Vanilla Latte",4500),
                new Coffee("Espresso",3000)
        };

        List<Coffee> coffeesToSort = Arrays.asList(coffees);
        coffeesToSort.sort(Comparator.comparing(Coffee::getPrice)
                .thenComparing(Coffee::getName)
        );
        for (Coffee coffee : coffees) {
            System.out.println(coffee.toString());
        }
    }

가격이 동일한 5500원대의 3개 커피에 대해서, 이름 순으로 정렬이 되어야 할 것이다.

 

원하는 순서는 녹차 -> 녹차 라떼 -> 딸기 라떼의 순서이다.

 

Accepts a function that extracts a sort key from a type T, and returns a Comparator<T> that compares by that sort key using the specified Comparator.

Comparator.comparing() 메서드의 파라미터로는 정렬을 하기 위해 필요한 객체 데이터의 "키" 를 추출하는 메서드를 전달해주면 된다.

이 경우에는 1차적으로 가격순이기 때문에 getPrice() 를 전달한다.

 

그 뒤에는 가격이 동일한 경우에 대해 thenComparing() 메서드를 체이닝해 다음에 실행되도록 하게 한다. 

2번째 키로써 동작해야하는 커피의 이름을 전달한다.

 

우리가 원하는대로 커피가 가격 -> 이름 순으로 정렬되어 출력되는 걸 볼 수 있다...!

 

개발자가 만든 커스텀 객체에 대해 정렬하는 여러가지 방법을 공부해 보았다. 자바는 파이썬에 비해 알아야 할 게 많지만 그만큼 배우는 재미가 많은 것 같다.