Relatable 인터페이스 정의하기

인터페이스를 구현하는 클래스를 선언하려면 클래스 선언에 implements 절을 포함하면 됩니다. 클래스는 둘 이상의 인터페이스를 구현할 수 있으므로 implements 키워드 뒤에는 클래스가 구현하는 인터페이스의 쉼표로 구분된 목록이 이어집니다. 관례에 따라 implements 절은 extends 절이 있는 경우 그 뒤에 옵니다.

객체의 크기를 비교하는 방법을 정의하는 인터페이스를 생각해 봅시다.

public interface Relatable {
 
    // this (object calling isLargerThan())
    // and other must be instances of 
    // the same class returns 1, 0, -1 
    // if this is greater than, 
    // equal to, or less than other
    public int isLargerThan(Relatable other);
}

비슷한 객체의 크기를 비교할 수 있으려면 객체가 무엇이든 상관없이 객체를 인스턴스화하는 클래스가 Relatable을 구현해야 합니다.

클래스에서 인스턴스화된 객체의 상대적인 “크기”를 비교할 수 있는 방법이 있다면 어떤 클래스든 Relatable을 구현할 수 있습니다. 문자열의 경우 문자 수, 책의 경우 페이지 수, 학생의 경우 무게 등이 될 수 있습니다. 평면형 기하학적 객체의 경우 면적은 좋은 선택이 될 것이고(뒤에 나오는 RectanglePlus 클래스 참조), 입체형 기하학적 객체의 경우 부피가 적합할 것입니다. 이러한 모든 클래스는 isLargerThan() 메서드를 구현할 수 있습니다.

클래스가 Relatable을 구현한다는 것을 알고 있다면, 해당 클래스에서 인스턴스화된 객체의 크기를 비교할 수 있다는 것을 알 수 있습니다.

 

Relatable 인터페이스 구현하기

다음은 객체 생성하기 섹션에서 소개한 Rectangle 클래스를 Relatable을 구현하도록 재작성한 것입니다.

public class RectanglePlus 
    implements Relatable {
    public int width = 0;
    public int height = 0;
    public Point origin;
 
    // four constructors
    public RectanglePlus() {
        origin = new Point(0, 0);
    }
    public RectanglePlus(Point p) {
        origin = p;
    }
    public RectanglePlus(int w, int h) {
        origin = new Point(0, 0);
        width = w;
        height = h;
    }
    public RectanglePlus(Point p, int w, int h) {
        origin = p;
        width = w;
        height = h;
    }
 
    // a method for moving the rectangle
    public void move(int x, int y) {
        origin.x = x;
        origin.y = y;
    }
 
    // a method for computing
    // the area of the rectangle
    public int getArea() {
        return width * height;
    }
    
    // a method required to implement
    // the Relatable interface
    public int isLargerThan(Relatable other) {
        RectanglePlus otherRect 
            = (RectanglePlus)other;
        if (this.getArea() < otherRect.getArea())
            return -1;
        else if (this.getArea() > otherRect.getArea())
            return 1;
        else
            return 0;               
    }
}

RectanglePlusRelatable을 구현하므로 두 개의 RectanglePlus 객체의 크기를 비교할 수 있습니다.

Note: Relatable 인터페이스에 정의된 isLargerThan() 메서드는 Relatable 타입의 객체를 받습니다. 이 코드 줄은 다른 객체를 RectanglePlus 인스턴스로 형변환합니다. 타입 캐스팅은 컴파일러에게 객체가 실제로 무엇인지 알려줍니다. 다른 인스턴스(other.getArea())에서 직접 getArea()를 호출하면 컴파일러가 다른 인스턴스가 실제로 RectanglePlus의 인스턴스라는 것을 이해하지 못하므로 컴파일에 실패할 수 있습니다.

 

진화하는 인터페이스

DoIt이라는 인터페이스를 개발했다고 가정해 봅시다:

public interface DoIt {
   void doSomething(int i, double x);
   int doSomethingElse(String s);
}

나중에 DoIt에 세 번째 메서드를 추가하여 이제 인터페이스가 다음과 같이 되었다고 가정해 보겠습니다:

public interface DoIt {
 
   void doSomething(int i, double x);
   int doSomethingElse(String s);
   boolean didItWork(int i, double x, String s);
   
}

이렇게 변경하면 이전 DoIt 인터페이스를 구현하는 모든 클래스는 더 이상 이전 인터페이스를 구현하지 않기 때문에 중단됩니다. 이 인터페이스에 의존하는 프로그래머들은 큰 소리로 항의할 것입니다.

인터페이스의 모든 용도를 예상하고 처음부터 완벽하게 지정하세요. 인터페이스에 메서드를 추가하려는 경우 몇 가지 옵션이 있습니다. DoIt을 확장하는 DoItPlus 인터페이스를 만들 수 있습니다:

public interface DoItPlus extends DoIt {
 
   boolean didItWork(int i, double x, String s);
   
}

이제 코드 사용자는 이전 인터페이스를 계속 사용하거나 새 인터페이스로 업그레이드할지 선택할 수 있습니다.

또는 새 메서드를 기본 메서드로 정의할 수도 있습니다. 다음 예제에서는 didItWork()라는 기본 메서드를 정의합니다:

public interface DoIt {
 
   void doSomething(int i, double x);
   int doSomethingElse(String s);
   default boolean didItWork(int i, double x, String s) {
       // Method body 
   }
}

기본 메서드에 대한 구현을 제공해야 한다는 점에 유의하세요. 기존 인터페이스에 새로운 정적 메서드를 정의할 수도 있습니다. 새로운 기본 메서드 또는 정적 메서드로 향상된 인터페이스를 구현하는 클래스가 있는 사용자는 추가 메서드를 수용하기 위해 클래스를 수정하거나 다시 컴파일할 필요가 없습니다.

 

기본 메서드

인터페이스 섹션에서는 자동차를 작동하기 위해 호출할 수 있는 메서드를 설명하는 업계 표준 인터페이스를 게시하는 컴퓨터 제어 자동차 제조업체와 관련된 예를 설명합니다. 이러한 컴퓨터 제어 자동차 제조업체가 자동차에 비행과 같은 새로운 기능을 추가한다면 어떻게 될까요? 이러한 제조업체는 다른 회사(예: 전자 유도 계기 제조업체)가 자사의 소프트웨어를 비행 자동차에 적용할 수 있도록 새로운 방법을 지정해야 할 것입니다. 이러한 자동차 제조업체는 이러한 새로운 비행 관련 방법을 어디에 선언할까요? 원래 인터페이스에 추가하면 해당 인터페이스를 구현한 프로그래머는 구현을 다시 작성해야 합니다. 정적 메서드로 추가하면 프로그래머는 이를 필수적인 핵심 메서드가 아닌 유틸리티 메서드로 간주할 것입니다.

기본 메서드를 사용하면 라이브러리 인터페이스에 새로운 기능을 추가하고 해당 인터페이스의 이전 버전용으로 작성된 코드와의 바이너리 호환성을 보장할 수 있습니다.

다음 인터페이스인 TimeClient를 살펴봅시다:

import java.time.*; 
 
public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
}

다음 클래스인 SimpleTimeClientTimeClient를 구현합니다.:

public class SimpleTimeClient implements TimeClient {
    
    private LocalDateTime dateAndTime;
    
    public SimpleTimeClient() {
        dateAndTime = LocalDateTime.now();
    }
    
    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }
    
    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }
    
    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second); 
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }
    
    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }
    
    public String toString() {
        return dateAndTime.toString();
    }
    
    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println(myTimeClient.toString());
    }
}

시간대 정보를 저장한다는 점을 제외하면 ZonedDateTime 객체를 통해 시간대를 지정하는 기능과 같은 새로운 기능을 TimeClient 인터페이스에 추가하고 싶다고 가정합니다(LocalDateTime 객체와 비슷함):

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();                           
    ZonedDateTime getZonedDateTime(String zoneString);
}

TimeClient 인터페이스를 이렇게 수정한 후에는 SimpleTimeClient 클래스를 수정하고 getZonedDateTime() 메서드도 구현해야 합니다. 그러나 이전 예제에서처럼 getZonedDateTime()을 추상 메서드로 남겨두는 대신 기본 구현을 정의할 수 있습니다. (추상 메서드는 구현 없이 선언된 메서드라는 것을 기억하세요.)

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
    
    static ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }
        
    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}

인터페이스의 메서드 정의는 메서드 서식의 시작 부분에 default 키워드를 사용하여 기본 메서드임을 지정합니다. 기본 메서드를 포함한 인터페이스의 모든 메서드 선언은 암시적으로 공용이므로 공용 수정자를 생략할 수 있습니다.

이 인터페이스를 사용하면 SimpleTimeClient 클래스를 수정할 필요가 없으며, 이 클래스(및 TimeClient 인터페이스를 구현하는 모든 클래스)에는 getZonedDateTime() 메서드가 이미 정의되어 있을 것입니다. 다음 예제인 TestSimpleTimeClientSimpleTimeClient의 인스턴스에서 getZonedDateTime() 메서드를 호출합니다:

public class TestSimpleTimeClient {
    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println("Current time: " + myTimeClient.toString());
        System.out.println("Time in California: " +
            myTimeClient.getZonedDateTime("Blah blah").toString());
    }
}

 

기본 메서드가 포함된 인터페이스 확장하기

기본 메서드가 포함된 인터페이스를 확장할 때는 다음과 같이 할 수 있습니다:

  • 기본 메서드를 전혀 언급하지 않음으로써 확장된 인터페이스가 기본 메서드를 상속할 수 있도록 합니다.

  • 기본 메서드를 다시 선언하여 추상적으로 만듭니다.

  • 기본 메서드를 재정의하여 기본 메서드를 재정의합니다.

  • 다음과 같이 TimeClient 인터페이스를 확장한다고 가정해 보겠습니다:

public interface AnotherTimeClient extends TimeClient { }

AnotherTimeClient 인터페이스를 구현하는 모든 클래스는 기본 메서드 TimeClient.getZonedDateTime()에 의해 지정된 구현을 갖게 됩니다.

다음과 같이 TimeClient 인터페이스를 확장한다고 가정합니다:

public interface AbstractZoneTimeClient extends TimeClient {
    public ZonedDateTime getZonedDateTime(String zoneString);
}

이 메서드는 인터페이스의 다른 모든 non-default (및 non-static) 메서드와 마찬가지로 추상 메서드이며, AbstractZoneTimeClient 인터페이스를 구현하는 모든 클래스는 getZonedDateTime() 메서드를 구현해야 합니다.

다음과 같이 TimeClient 인터페이스를 확장한다고 가정해 보겠습니다:

public interface HandleInvalidTimeZoneClient extends TimeClient {
    default public ZonedDateTime getZonedDateTime(String zoneString) {
        try {
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.of(zoneString)); 
        } catch (DateTimeException e) {
            System.err.println("Invalid zone ID: " + zoneString +
                "; using the default time zone instead.");
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.systemDefault());
        }
    }
}

HandleInvalidTimeZoneClient 인터페이스를 구현하는 모든 클래스는 이 인터페이스가 지정한 getZonedDateTime() 구현을 TimeClient 인터페이스가 지정한 구현 대신 사용할 것입니다.

 

Static 메서드

기본 메서드 외에도 인터페이스에 정적 메서드를 정의할 수 있습니다. (정적 메서드는 어떤 객체가 아닌 그것이 정의된 클래스와 연관된 메서드입니다. 클래스의 모든 인스턴스는 정적 메서드를 공유합니다.) 이렇게 하면 라이브러리에서 헬퍼 메서드를 더 쉽게 정리할 수 있으며, 인터페이스에 특정한 정적 메서드를 별도의 클래스가 아닌 동일한 인터페이스에 보관할 수 있습니다. 다음 예제는 시간대 식별자에 해당하는 ZoneId 객체를 검색하는 정적 메서드를 정의하며, 지정된 식별자에 해당하는 ZoneId 객체가 없는 경우 시스템 기본 시간대를 사용합니다. (따라서 getZonedDateTime() 메서드를 단순화할 수 있습니다):

public interface TimeClient {
    // ...
    static public ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }
 
    default public ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }    
}

클래스의 정적 메서드와 마찬가지로 인터페이스의 메서드 정의는 메서드 서식 시작 부분에 static 키워드를 사용하여 정적 메서드임을 지정합니다. 정적 메서드를 포함한 인터페이스의 모든 메서드 선언은 암시적으로 public이므로 public 수정자를 생략할 수 있습니다.

Java SE 9부터는 인터페이스의 구현을 정의하면서 인터페이스의 정적 메서드에서 공통 코드 조각을 추상화하기 위해 인터페이스에 private 메서드를 정의할 수 있습니다. 이러한 메서드는 구현에 속하며, 정의할 때 기본 메서드도 추상 메서드도 될 수 없습니다. 예를 들어, 인터페이스 구현에 내부에 있는 코드 조각을 호스팅하므로 getZoneId 메서드를 private 메서드로 만들 수 있습니다.

 

기본 메서드를 기존 라이브러리에 통합하기

기본 메서드를 사용하면 기존 인터페이스에 새로운 기능을 추가하고 해당 인터페이스의 이전 버전용으로 작성된 코드와의 바이너리 호환성을 보장할 수 있습니다. 특히 기본 메서드를 사용하면 람다 표현식을 매개변수로 받아들이는 메서드를 기존 인터페이스에 추가할 수 있습니다. 이 섹션에서는 기본 메서드와 정적 메서드를 통해 Comparator 인터페이스가 어떻게 개선되었는지 보여드립니다.

CardDeck 클래스를 살펴봅시다. Card 인터페이스에는 두 개의 enum 타입(SuitRank)과 두 개의 추상 메서드(getSuit()getRank())가 포함되어 있습니다:

public interface Card extends Comparable<Card> {
    
    public enum Suit { 
        DIAMONDS (1, "Diamonds"), 
        CLUBS    (2, "Clubs"   ), 
        HEARTS   (3, "Hearts"  ), 
        SPADES   (4, "Spades"  );
        
        private final int value;
        private final String text;
        Suit(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }
    
    public enum Rank { 
        DEUCE  (2 , "Two"  ),
        THREE  (3 , "Three"), 
        FOUR   (4 , "Four" ), 
        FIVE   (5 , "Five" ), 
        SIX    (6 , "Six"  ), 
        SEVEN  (7 , "Seven"),
        EIGHT  (8 , "Eight"), 
        NINE   (9 , "Nine" ), 
        TEN    (10, "Ten"  ), 
        JACK   (11, "Jack" ),
        QUEEN  (12, "Queen"), 
        KING   (13, "King" ),
        ACE    (14, "Ace"  );
        private final int value;
        private final String text;
        Rank(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }
    
    public Card.Suit getSuit();
    public Card.Rank getRank();
}

Deck 인터페이스에는 덱의 카드를 조작하는 다양한 방법이 포함되어 있습니다:

public interface Deck {
    
    List<Card> getCards();
    Deck deckFactory();
    int size();
    void addCard(Card card);
    void addCards(List<Card> cards);
    void addDeck(Deck deck);
    void shuffle();
    void sort();
    void sort(Comparator<Card> c);
    String deckToString();
 
    Map<Integer, Deck> deal(int players, int numberOfCards)
        throws IllegalArgumentException;
 
}

PlayingCard 클래스는 Card 인터페이스를 구현하고 StandardDeck 클래스는 Deck 인터페이스를 구현합니다.

StandardDeck 클래스는 다음과 같이 추상 메서드 Deck.sort()를 구현합니다:

public class StandardDeck implements Deck {
    
    private List<Card> entireDeck;
    
    // ...
    
    public void sort() {
        Collections.sort(entireDeck);
    }
    
    // ...
}

Collections.sort() 메서드는 요소 타입이 Comparable 인터페이스를 구현하는 List의 인스턴스를 정렬합니다. 멤버 entireDeckComparable을 확장하는 Card 타입의 요소를 가진 List의 인스턴스입니다. PlayingCard 클래스는 다음과 같이 Comparable.compareTo() 메서드를 구현합니다:

public int hashCode() {
    return ((suit.value()-1)*13)+rank.value();
}
 
public int compareTo(Card o) {
    return this.hashCode() - o.hashCode();
}

compareTo() 메서드는 StandardDeck.sort() 메서드가 카드 덱을 먼저 수트별로 정렬한 다음 순위별로 정렬하도록 합니다.

덱을 먼저 랭크별로 정렬한 다음 수트별로 정렬하려면 어떻게 해야 할까요? 새로운 정렬 기준을 지정하기 위해 Comparator 인터페이스를 구현하고 sort(List<T> list, Comparator<? super T> c)(Comparator 파라미터를 포함하는 정렬 메서드의 버전)를 사용해야 할 것입니다. StandardDeck 클래스에서 다음 메서드를 정의할 수 있습니다:

public void sort(Comparator<Card> c) {
    Collections.sort(entireDeck, c);
} 

이 메서드를 사용하면 Collections.sort() 메서드가 Card 클래스의 인스턴스를 정렬하는 방법을 지정할 수 있습니다. 이를 수행하는 한 가지 방법은 Comparator 인터페이스를 구현하여 카드 정렬 방법을 지정하는 것입니다. 예제 SortByRankThenSuit가 이를 수행합니다:

public class SortByRankThenSuit implements Comparator<Card> {
    public int compare(Card firstCard, Card secondCard) {
        int compVal =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compVal != 0)
            return compVal;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value(); 
    }
}

다음 호출은 먼저 카드 덱을 등급별로 정렬한 다음, 카드 수트를 기준으로 정렬합니다:

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(new SortByRankThenSuit());

그러나 이 접근 방식은 너무 장황하므로 정렬 기준만 지정하고 여러 정렬 구현을 만들지 않는 것이 더 좋습니다. 여러분이 Comparator 인터페이스를 작성한 개발자라고 가정해 봅시다. 다른 개발자가 정렬 기준을 더 쉽게 지정할 수 있도록 Comparator 인터페이스에 어떤 기본 또는 정적 메서드를 추가할 수 있을까요?

먼저 카드 덱을 카드의 종류에 관계없이 등급별로 정렬하고 싶다고 가정해 봅시다. 다음과 같이 StandardDeck.sort() 메서드를 호출할 수 있습니다:

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) ->
        firstCard.getRank().value() - secondCard.getRank().value()
); 

Comparator 인터페이스는 함수형 인터페이스이므로 sort() 메서드의 인수로 람다 표현식을 사용할 수 있습니다. 이 예제에서 람다 표현식은 두 개의 정수 값을 비교합니다.

개발자가 Card.getRank() 메서드만 호출하여 Comparator 인스턴스를 만들 수 있다면 더 간단할 것입니다. 특히 개발자가 getValue() 또는 hashCode()와 같은 메서드에서 숫자 값을 반환할 수 있는 모든 객체를 비교하는 Comparator 인스턴스를 만들 수 있다면 유용할 것입니다. Comparator 인터페이스는 정적 메서드 비교 기능을 통해 이 기능이 향상되었습니다:

myDeck.sort(Comparator.comparing((card) -> card.getRank()));  

이 예제에서는 메서드 참조를 대신 사용할 수 있습니다:

myDeck.sort(Comparator.comparing(Card::getRank));  

이 호출은 다양한 정렬 기준을 지정하고 여러 정렬 구현을 만들지 않는 방법을 더 잘 보여줍니다.

Comparator 인터페이스는 다른 데이터 타입을 비교하는 comparingDouble()comparingLong() 등의 다른 버전의 정적 메서드 비교로 향상되어 다른 데이터 타입을 비교하는 Comparator 인스턴스를 만들 수 있게 되었습니다.

개발자가 둘 이상의 기준을 가진 객체를 비교할 수 있는 Comparator 인스턴스를 만들고 싶다고 가정해 보겠습니다. 예를 들어, 카드 덱을 먼저 등급별로 정렬한 다음 카드의 종류별로 정렬하려면 어떻게 해야 할까요? 이전과 마찬가지로 람다 표현식을 사용하여 이러한 정렬 기준을 지정할 수 있습니다:

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) -> {
        int compare =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compare != 0)
            return compare;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value();
    }      
); 

개발자가 일련의 Comparator 인스턴스로부터 Comparator 인스턴스를 구축할 수 있다면 더 간단해질 것입니다. Comparator 인터페이스는 기본 메서드인 thenComparing()를 통해 이 기능을 향상시켰습니다:

myDeck.sort(
    Comparator
        .comparing(Card::getRank)
        .thenComparing(Comparator.comparing(Card::getSuit)));

Comparator 인터페이스는 기본 메서드인 thenComparing()의 다른 버전인 thenComparingDouble()로 향상되었습니다. base/java/util/Comparator.html#thenComparingDouble(java.util.function.ToDoubleFunction))) 및 thenComparingLong()을 사용하면 다른 데이터 타입을 비교하는 Comparator 인스턴스를 빌드할 수 있습니다.

개발자가 객체 컬렉션을 역순으로 정렬할 수 있는 Comparator 인스턴스를 생성하고 싶다고 가정해 보겠습니다. 예를 들어, 카드 덱을 2장에서 에이스까지가 아니라 에이스에서 2장까지 내림차순으로 정렬하려면 어떻게 해야 할까요? 이전과 마찬가지로 다른 람다 표현식을 지정할 수 있습니다. 하지만 개발자가 메서드를 호출하여 기존 Comparator를 반전시킬 수 있다면 더 간단할 것입니다. Comparator 인터페이스는 기본 메서드인 reversed()와 함께 이 기능으로 개선되었습니다:

myDeck.sort(
    Comparator.comparing(Card::getRank)
        .reversed()
        .thenComparing(Comparator.comparing(Card::getSuit)));

이 예제는 기본 메서드, 정적 메서드, 람다 표현식 및 메서드 참조를 통해 Comparator 인터페이스가 어떻게 향상되어 프로그래머가 호출되는 방식을 보고 기능을 빠르게 추론할 수 있는 보다 표현력 있는 라이브러리 메서드를 만들 수 있는지 보여줍니다. 이러한 구성을 사용하여 라이브러리의 인터페이스를 개선하세요.