[Java] 정렬

gobeul·2023년 10월 19일
0

CS

목록 보기
3/4
post-thumbnail

파이썬에 익숙해져 있다보니 Java에서 정렬은 어렵게 느껴진다.
그렇지 않도록 이번 기회에 정리하자!

Comparable과 Comparator

자바에서 정렬을 사용하기 위해 우선 Comparable과 Comparator을 알아한다.

Interface

Comparable과 Comparator 모두 인터페이스(interface) 이다.
따라서 이 둘을 사용하기 위해서는 인터페이스 안의 선언된 추상 메소드를 사용하고자 하는 곳에서 재정의(Override)해야 한다.

Comparable을 보면 선언된 메서드가 하나 있다.

public int compareTo(T o);

Comparable을 사용하기 위해선 이 compareTo 메소드의 오버라이딩이 필요하다.


Comparator을 보면 이 친구는 메서드가 여러개 존재한다.
하지만 모든 메서드를 다 오버라이딩할 필요없이

int compare(T o1, T o2);

이 메서드만 오버라이딩 해주면 된다.

Comparator 인터페이스에서 compare 메소드만 오버라이딩하는 이유

java8 이후 부터는 인터페이스에 추상메서스 외에 일반 메소드를 구현할 수 있도록 변경되었다.

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);

    boolean equals(Object obj);

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        Objects.requireNonNull(other);
        return (Comparator<T> & Serializable) (c1, c2) -> {
            int res = compare(c1, c2);
            return (res != 0) ? res : other.compare(c1, c2);
        };
    }

    default <U> Comparator<T> thenComparing(
            Function<? super T, ? extends U> keyExtractor,
            Comparator<? super U> keyComparator)
    {
        return thenComparing(comparing(keyExtractor, keyComparator));
    }

    default <U extends Comparable<? super U>> Comparator<T> thenComparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        return thenComparing(comparing(keyExtractor));
    }

    default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor) {
        return thenComparing(comparingInt(keyExtractor));
    }

    default Comparator<T> thenComparingLong(ToLongFunction<? super T> keyExtractor) {
        return thenComparing(comparingLong(keyExtractor));
    }

    default Comparator<T> thenComparingDouble(ToDoubleFunction<? super T> keyExtractor) {
        return thenComparing(comparingDouble(keyExtractor));
    }

    public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
        return Collections.reverseOrder();
    }

    @SuppressWarnings("unchecked")
    public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
        return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
    }

    public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
        return new Comparators.NullComparator<>(true, comparator);
    }

    public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) {
        return new Comparators.NullComparator<>(false, comparator);
    }


    public static <T, U> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor,
            Comparator<? super U> keyComparator)
    {
        Objects.requireNonNull(keyExtractor);
        Objects.requireNonNull(keyComparator);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
                                              keyExtractor.apply(c2));
    }


    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }

    public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
    }

    public static <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor) {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> Long.compare(keyExtractor.applyAsLong(c1), keyExtractor.applyAsLong(c2));
    }

    public static<T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor) {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> Double.compare(keyExtractor.applyAsDouble(c1), keyExtractor.applyAsDouble(c2));
    }
}

실제 Comparator 인터페이스에 선언된 메소드들이다.
이를 보면 compareequals만 추상메서드 이고 나머지는 모두 구현 부분을 가지고 있는 일반 메소드 들이다.

그럼 equals는 왜 오버라이딩 하지 않을까?

equals 메소드는 Object 클래스에서 구현이 되어 있다.
그리고 Comparator 인터페이스를 사용하는 어떤 클래스는 모두 Object의 자식 클래스이다.
따라서 굳이 equals 메소드를 오버라이딩할 필요가 없는 것이다.

물론, 필요하다면 오버라이딩 해줄 수 있다.


역할

Comparable과 Comparator의 역할은 정렬을 위한 객체 비교시에 기준을 정해준다는 것이다.
예를 들어, 동물 객체를 정렬할때 어느것을 기준으로 할지 정해줘야 한다. 무게, 키, 나이 등 비교하는 기준이 필요하고 이걸 Comparable나 Comparator를 통해 정해줄 것이다.


int compareTo(T o) vs int compare(T o1, T o2)

두 인터페이스의 역할은 같다. 비교의 기준을 정해 주는 것.
그렇다면 두 인터페이스를 사용하기 위해 오버라이딩이 필요한 두 메소드에는 어떤 차이가 있을까?

우선 메소드의 파라미터 개수에 차이가 보인다.
Comparable의 compareTo의 경우 파라미터가 한 개로 자기 자신과 파라미터로 들어오는 객체를 비교한다.
Comparator compare의 경우 두 개의 파라미터로 자기 자신은 상관없고 파라미터 두개를 비교하게 된다.

또 하나의 차이는 Comparable은 lang 패키지로 별도의 import가 필요하지 않지만 Comparator은 util 패키지에 있기에 import가 필요하다.



Comparable

class Animal implements Comparable<Animal> {

    int weight;
    int age;

    Animal(int weight, int age) {
        this.weight = weight;
        this.age = age;
    }

    @Override
    public int compareTo(Animal a) {
        if (this.age > a.age) {
            return 1;
        } else if (this.age == a.age) {
            return 0;
        } else {
            return -1;
        }
    }
}

Animal 객체를 만들어 Comparable인터페이스를 사용했다.
나이 순서대로 비교하기 위해 만든 compareTo 메소드를 보면 Animal 객체 a와 자기 자신을 비교하게 되는데 내가 더 크면 1, 나랑 같으면 0, 내가 더 작으면 -1을 반환한다.

그런데 꼭 이럴 필요는 없다. 내가 더 크면 양수, 내가 더 작으면 음수를 반환해도 된다.

@Override
public int compareTo(Animal a) {
    return this.age - a.age;
}

이를 이용해 간단하게 변경가능하다. 내가 더 크면 양수가 나올것이고 같으면 0, 작으면 음수를 반환할 것이다.


주의할점

return this.age - a.age; 이 방법에는 주의할점이 있다.
반환값이 int 이기때문에 Overflow가 발생할 수 있다.

위 사진처럼 a 와 b를 비교시 a가 더 크기 때문에 양수값을 기대했지만 결과는 음수값이다.

따라서 Overflow가 발생할 수 있는 상황이라던지, 발생할지 모르는 상황이라면 코드가 좀 길어지더라도 조건문을 이용한 방법이 권장된다.



Comparator

class Animal implements Comparator<Animal> {

    int weight;
    int age;

    Animal(int weight, int age) {
        this.weight = weight;
        this.age = age;
    }


    @Override
    public int compare(Animal a1, Animal a2) {
        return a1.age - a2.age;
    }
}

코드를 보면 알겠지만 Comparator도 사실Comparable과 큰 차이가 없다.
compare 메서드의 경우 파라미터가 2개이고, 기준이 자기 자신에서 첫번째 파라미터로 변경된다는 점만 기억하면 되겠다.

Animal a = new Animal(1, 3);
Animal b = new Animal(1, 4);
Animal c = new Animal(1, 6);
        
c.compare(a, b);

compare 메소드의 비교는 파라미터의 객체간의 비교이므로 a와 b를 비교하는 상황에서 전혀상관 없는 c를 통해 비교가 가능하다.


주의할점

Comparable과 마찬가지로 return a1.age - a2.age 형태로 사용시에 Overflow를 주의해야 한다.


익명 객체를 이용한 응용

위에서 나이를 기준으로 객체를 비교했는데 어떨땐 나이로, 어떨땐 무게로 비교하고 싶을 때는 어떻게 할까?

익명 객체를 이용하여 이부분을 해결할 수 있다.

public class Main {
    public static void main(String[] args) {
        
        Animal a = new Animal(10, 100);
        Animal b = new Animal(50, 80);

        System.out.println(compAge.compare(a, b));
        System.out.println(compWeight.compare(a, b));
        
    }
    
    public static Comparator<Animal> compAge = new Comparator<Animal>() {
        @Override
        public int compare(Animal a1, Animal a2) {
            return a1.age - a2.age;
        }
    };
    
    public static Comparator<Animal> compWeight = new Comparator<Animal>() {
        @Override
        public int compare(Animal a1, Animal a2) {
            return a1.weight - a2.weight;
        }
    };

}

익명객체를 활용하여 나이, 무게 필요한 기준에 따라 사용할 수 있게 되었다.
이렇게 사용한다면 Animal 클래스에서는 Comparator 인터페이스를 사용할 필요가 없다.

Comparable의 경우는??

Comparable도 익명 객체를 사용한다면 할 수는 있지만.. 권하지는 않는다. 이유는 Comparable의 compareTo 메소드의 경우 파라미터의 개수가 1개로 자기 자신과 파라미터를 비교하게 된다.

즉, 익명 객체를 사용하면 익명 객체와 파라미터를 비교한 것이기 때문에 비교할 자신과 같은 익명객체를 만들어야되는데 복잡하다.



Java의 정렬

우선 자바에서는 오름차순을 디폴트로 잡는다.
즉, compare나 compareTo의 반환값이 양수이면 두 원소의 위치를 바꾸고 음수라면 바꾸지 않는다.


내림차순 정렬

자바에서 오름차순이 기본값이기에 compare나 compareTo의 반환값이 양수면 두 비교원소의 위치를 바꾼다고 했다. 그러면 내림차순은 어떻게 할까?

그냥 반대로 생각하면된다. 양수를 리턴할때 자리를 바꾼하고 했으니 원래 양수를 리턴하는 로직이 음수를 리턴하도록 하면 끝나는 일이다.
예를 들어
return a1.age - a2.age; 기존에 이런값을 return -(a1.age - a2.age); 이렇게 바꿔주면 된다. 그럼 a1이 더 큰 상황에서 음수를 리턴하게 되고 위치를 교환하는 일이 없어진다.

좀 더 깔끔하게 사용하고 싶다면
return a2.age - a1.age; 이렇게 사용하자.

다시 한번 말하지만 이는 int 자료형의 Overflow를 고려하지 않은 상태이다.


문자열 정렬

지금까지 비교를 보면 연산이 가능한 int 나 long 타입등을 통해 비교가 이뤄졌다.
그러면 사전순으로 정렬하고 싶을때는 어떻게 할까?

compareTo 메소드를 사용할 수 있는데 이 메소드는 Comparable의 compareTo하고는 다르다.
여기서 compareTo는 String 클래스에서 구현된 메소드 이다.

// Comparable
@Override
public int compareTo(Animal a1) {
    return this.name.compareTo(a1.name);
}

// Comparator
@Override
public int compareTo(Animal a1, Animal a2) {
    return a1.name.compareTo(a2.name);
}

이 역시 오름차순 기준이며 내림차순을 원할경우
return a1.name.compareTo(this.name);
return a2.name.compareTo(a1.name);
이런식으로 바꿔주자!


Arrays.sort()

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

        Animal[] animals = new Animal[3];
        animals[0] = new Animal(10, 100, "사자");
        animals[1] = new Animal(2, 300, "코끼리");
        animals[2] = new Animal(20, 15, "개미핥기");

        Arrays.sort(animals); // Comparable 사용 -> 코끼리 사자 개미핥기 
        Arrays.sort(animals, compWeight); // Comparator 사용 -> 개미핥기 사자 코끼리

        for (Animal animal : animals) {
            System.out.print(animal.name + " ");
        }
        System.out.println();

    }

    public static Comparator<Animal> compAge = new Comparator<Animal>() {
        @Override
        public int compare(Animal a1, Animal a2) {
            return a1.age - a2.age;
        }
    };

    public static Comparator<Animal> compWeight = new Comparator<Animal>() {
        @Override
        public int compare(Animal a1, Animal a2) {
            return a1.weight - a2.weight;
        }
    };

}

class Animal implements Comparable<Animal> {
    String name;
    int weight;
    int age;

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


    @Override
    public int compareTo(Animal a1) {
        return this.age - a1.age;
    }
}

Arrays.sort()를 사용하여 정렬해 보았다.
Comparable을 사용할 때는 클래스에 Comparable을 구현한 상태로 배열을 넘겨주면 되고,
Comparator을 사용할때는 익명 객체로 Comparator을 생성후 Arrays.sort() 파라미터에 배열과 함께 익명객체를 넘겨주면 된다.

익명 객체를 바로 파라미터에서 구현하는 방법도 있다.

Arrays.sort(animals, new Comparator<Animal>() {
    @Override
    public int compare(Animal o1, Animal o2) {
        return o1.name.length() - o2.name.length();
    }
});

아직 람다 표현식이 익숙하지 않지만 람다식을 이용하면 더 줄일 수 있다.

Arrays.sort(animals, (o1, o2) -> o1.name.length() - o2.name.length());

참고

profile
뚝딱뚝딱

0개의 댓글

관련 채용 정보