JavaRush /Blog Java /Random-PL /Komparator w Javie
Viacheslav
Poziom 3

Komparator w Javie

Opublikowano w grupie Random-PL
Tylko leniwi ludzie nie pisali o Komparatorze i porównaniu w Javie. Leniwym nie jestem - dlatego proszę Was o pokochanie i przypodobanie się jeszcze jednej wariacji. Mam nadzieję, że nie będzie to zbyteczne. I tak, ten artykuł jest odpowiedzią na pytanie: „Czy potrafisz napisać komparator z pamięci?” Mam nadzieję, że po przeczytaniu tego artykułu każdy będzie mógł napisać komparator z pamięci.
Komparator w Javie - 1
Wprowadzenie Java jest językiem zorientowanym obiektowo. W rezultacie w Javie powszechne jest operowanie obiektami. Ale prędzej czy później pojawia się zadanie porównywania obiektów według jakiejś zasady. Zatem biorąc pod uwagę: Mamy wiadomość opisaną przez klasę Message:
public static class Message {
    private String message;
    private int id;

    public Message(String message) {
        this.message = message;
        this.id = new Random().nextInt(1000);
    }
    public String getMessage() {
        return message;
    }
    public Integer getId() {
        return id;
    }
    public String toString() {
        return "[" + id + "] " + message;
    }
}
Dodajmy tę klasę do kompilatora Java Tutorialspoint . Pamiętajmy też o dodaniu importu:
import java.util.Random;
import java.util.ArrayList;
import java.util.List;
W metodzie głównej utworzymy kilka wiadomości:
public static void main(String[] args){
    List<Message> messages = new ArrayList();
    messages.add(new Message("Hello, World!"));
    messages.add(new Message("Hello, Sun!"));
    System.out.println(messages);
}
Zastanówmy się, co powinniśmy zrobić, jeśli chcemy je porównać? Na przykład chcemy sortować według identyfikatora. Aby stworzyć porządek, trzeba w jakiś sposób porównać obiekty, aby zrozumieć, który obiekt jest poprzedni (czyli mniejszy), a który następny (czyli większy). Zacznijmy od klasy takiej jak java.lang.Object . Jak wiemy, wszystkie klasy dziedziczą pośrednio po tej klasie Object. I to jest logiczne, ponieważ To zasadniczo wyraża koncepcję: „Wszystko jest przedmiotem” i zapewnia wspólne zachowanie dla wszystkich klas. Klasa ta definiuje, że każda klasa ma dwie metody: → hashCode Metoda hashCode zwraca pewną numeryczną (int) reprezentację obiektu jako instancję klasy. Co to znaczy? Oznacza to, że jeśli utworzyłeś dwie różne instancje klasy, to ponieważ instancje są różne, ich hashCode powinien być inny. Oto, co jest napisane w opisie metody: „O ile jest to rozsądnie praktyczne, metoda hashCode zdefiniowana przez klasę Object zwraca różne liczby całkowite dla różnych obiektów”. Oznacza to, że jeśli są to dwie różne instancje, to powinny mieć różne hashCody. Oznacza to, że ta metoda nie nadaje się do naszego porównania. → równa się Metoda równa odpowiada na pytanie „czy obiekty są równe” i zwraca wartość logiczną. Ta metoda ma domyślny kod:
public boolean equals(Object obj) {
    return (this == obj);
}
Oznacza to, że bez przesłaniania tej metody na obiekcie, metoda ta zasadniczo mówi, czy odniesienia do obiektu są zgodne, czy nie. To nie jest odpowiednie dla naszych wiadomości, ponieważ nie interesują nas linki do obiektu, interesuje nas identyfikator wiadomości. A nawet jeśli zastąpimy metodę równości, maksimum, jakie otrzymamy, to: „Są równi” lub „Nie są równi”. Ale to nie wystarczy, abyśmy ustalili kolejność.

Komparator i porównywalny w Javie

Co nam odpowiada? Jeśli przełożymy słowo „porównaj” na język angielski w tłumaczu, otrzymamy tłumaczenie „porównaj”. Świetnie, w takim razie potrzebujemy kogoś, kto dokona porównania. Jeśli porównasz to porównanie, wówczas tym, który porównuje, jest komparator. Otwórzmy Java Api i znajdźmy tam Comparator . I rzeczywiście istnieje taki interfejs - java.util.Comparator java.util.Comparator i java.lang.Comparable Jak widać, istnieje taki interfejs. Klasa, która to implementuje, mówi: „Implementuję funkcję porównywania obiektów”. Jedyną rzeczą, o której naprawdę warto pamiętać, jest umowa porównawcza, która jest wyrażona w następujący sposób:

Comparator возвращает int по следующей схеме: 
  • отрицательный int (первый obiekt отрицательный, то есть меньше)
  • положительный int (первый obiekt положительный, хороший, то есть больший)
  • ноль = obiektы равны
Napiszmy teraz komparator. Będziemy musieli zaimportować Java.util.Comparator . Po imporcie dodaj metodę do main: Comparator<Message> comparator = new Comparator<Message>(); Oczywiście to nie zadziała, ponieważ Komparator jest interfejsem. Dlatego po nawiasach dodamy kręcone { }. W tych nawiasach napiszemy metodę:
public int compare(Message o1, Message o2) {
    return o1.getId().compareTo(o2.getId());
}
Nie musisz nawet pamiętać, żeby to napisać. Komparator to taki, który dokonuje porównania, to znaczy porównuje. Aby odpowiedzieć na pytanie, w jakiej kolejności są porównywane obiekty, zwracamy int. Właściwie to wszystko. Prosto i łatwo. Jak widać na przykładzie oprócz Comparatora istnieje jeszcze jeden interfejs - java.lang.Comparable , przy implementacji którego musimy zdefiniować metodę CompareTo . Ten interfejs mówi, że „Klasa, która implementuje interfejs, umożliwia porównywanie instancji tej klasy”. Na przykład implementacja CompareTo w Integer wygląda następująco:
(x < y) ? -1 : ((x == y) ? 0 : 1)
Jak zapamiętać te wszystkie interfejsy? Po co? Wszystko pochodzi z języka angielskiego. Porównaj - aby porównać, ten, który porównuje, to Porównywarka (jako rejestrator np. Rejestrator), a przymiotnik „porównywany” to Porównywalny. Cóż, „porównaj z” jest tłumaczone nie tylko jako porównywać z, ale także jako porównywać. To proste. Język Java został napisany przez osoby anglojęzyczne i przy nazywaniu wszystkiego w Javie kierowali się po prostu językiem angielskim i w nazewnictwie była pewna logika. Metoda CompareTo opisuje, w jaki sposób instancję klasy należy porównywać z innymi instancjami. Na przykład ciągi znaków są porównywane leksygraficznie , a liczby — według wartości.
Komparator w Javie - 2
Java 8 przyniosła kilka fajnych zmian. Jeśli przyjrzymy się bliżej interfejsowi Komparatora, zobaczymy, że znajduje się nad nim adnotacja @FunctionalInterface. W rzeczywistości ta adnotacja ma charakter informacyjny i oznacza, że ​​ten interfejs jest funkcjonalny. Oznacza to, że ten interfejs ma tylko jedną metodę abstrakcyjną bez implementacji. Co nam to daje? Możemy teraz napisać kod komparatora w następujący sposób:
Comparator<Message> comparator = (o1, o2) -> o1.getId().compareTo(o2.getId());
W nawiasach podano, jak nazywamy zmienne. Sama Java to zobaczy, ponieważ... Jeśli istnieje tylko jedna metoda, jasne jest, jakie parametry wejściowe są potrzebne, ile i jakie typy. Następnie strzałką mówimy, że chcemy je przenieść do tej sekcji kodu. Dodatkowo dzięki Javie 8 w interfejsach pojawiły się metody domyślne - są to metody, które pojawiają się domyślnie (domyślnie) gdy implementujemy interfejs. W interfejsie komparatora dostępnych jest ich kilka, na przykład:
Comparator moreImportant = Comparator.reverseOrder();
Comparator lessImportant = Comparator.naturalOrder();
Istnieje inna metoda, która sprawi, że Twój kod będzie czystszy. Spójrzmy na powyższy przykład, w którym opisaliśmy nasz komparator. Co on robi? To dość prymitywne. Po prostu bierze obiekt i wydobywa z niego jakąś porównywalną wartość. Na przykład Integer implementuje funkcję porównywalną, więc mogliśmy wykonać funkcję CompareTo na wartościach identyfikatora wiadomości. Tę prostą funkcję komparatora można również zapisać w następujący sposób:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
Oznacza to dosłownie: „Mamy komparator, który porównuje w ten sposób: pobiera obiekty, pobiera z nich funkcję Comparable za pomocą metody getId() i porównuje za pomocą CompareTo”. I koniec z okropnymi projektami. Na koniec chciałbym zwrócić uwagę na jeszcze jedną funkcję. Komparatory można łączyć ze sobą. Na przykład:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
comparator = comparator.thenComparing(obj -> obj.getMessage().length());

Aplikacja

Deklaracja komparatora okazała się całkiem logiczna, prawda? Teraz musimy zobaczyć, jak go używać i w jakich miejscach. → Kolekcje.sort (java.util.Collections) Oczywiście w ten sposób możemy sortować kolekcje. Ale nie wszystko, tylko listy. I nie ma tu nic niezwykłego, bo... Jest to lista, która wymaga dostępu do elementu według indeksu. Umożliwia to zamianę elementu numer dwa na element numer trzy. Dlatego sortowanie w ten sposób jest możliwe tylko dla list:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
Collections.sort(messages, comparator);
Arrays.sort (java.util.Arrays) Tablice można także wygodnie sortować. Ponownie z tego samego powodu dostępu do elementów według indeksu. → Potomkowie java.util.SortedSet i java.util.SortedMap Jak pamiętamy, Set i Map nie gwarantują kolejności przechowywania rekordów. ALE mamy specjalne wdrożenia gwarantujące porządek. A jeśli elementy kolekcji nie implementują Java.lang.Comparable, to możemy przekazać Comparator konstruktorowi takich kolekcji:
Set<Message> msgSet = new TreeSet(comparator);
Stream API W Stream Api, które pojawiło się w Javie 8, komparator pozwala uprościć pracę na elementach strumieniowych. Na przykład potrzebujemy ciągu liczb losowych od 0 do 999 włącznie:
Supplier<Integer> randomizer = () -> new Random().nextInt(1000);
Stream.generate(randomizer)
    .limit(10)
    .sorted(Comparator.naturalOrder())
    .forEach(e -> System.out.println(e));
Moglibyśmy przestać, ale jest więcej interesujących problemów. Przykładowo trzeba przygotować Mapę, gdzie kluczem jest identyfikator wiadomości. Jednocześnie chcemy posortować te klucze tak, aby klucze były w kolejności od najmniejszego do największego. Zacznijmy od tego kodu:
Map<Integer, Message> collected = Arrays.stream(messages)
                .sorted(Comparator.comparing(msg -> msg.getId()))
                .collect(Collectors.toMap(msg -> msg.getId(), msg -> msg));
To, co tu wrócimy, to tak naprawdę HashMap. A jak wiemy, nie gwarantuje to żadnego porządku. Dlatego nasze rekordy posortowane według identyfikatorów po prostu się zepsuły. Niedobrze. Będziemy musieli trochę zmienić nasz kolektor:
Map<Integer, Message> collected = Arrays.stream(messages)
                .sorted(Comparator.comparing(msg -> msg.getId()))
                .collect(Collectors.toMap(msg -> msg.getId(), msg -> msg, (oldValue, newValue) -> oldValue, TreeMap::new));
Kod wyglądał nieco bardziej przerażająco, ale problem został teraz poprawnie rozwiązany dzięki jawnej implementacji TreeMap. Więcej o poszczególnych grupach można przeczytać tutaj: Możesz sam stworzyć kolektor. Więcej informacji możesz przeczytać tutaj: „Tworzenie niestandardowego modułu zbierającego w Javie 8” . Przydatne jest przeczytanie dyskusji tutaj: „Lista Java 8 do mapowania za pomocą strumienia” .
Komparator w Javie - 3
Grabie porównawcze i porównywalne są dobre. Ale jest z nimi jeden niuans, o którym warto pamiętać. Kiedy klasa wykonuje sortowanie, oblicza, że ​​może rzucić twoją klasę na Comparable. Jeśli tak nie jest, w momencie wykonania pojawi się błąd. Spójrzmy na przykład:
SortedSet<Message> msg = new TreeSet<>();
msg.add(new Message(2, "Developer".getBytes()));
Wydaje się, że nie ma tu nic złego. Ale tak naprawdę w naszym przykładzie zawiesi się z błędem: java.lang.ClassCastException: Message cannot be cast to java.lang.Comparable A wszystko dlatego, że próbował posortować elementy (w końcu jest to SortedSet). I nie mogłem. Powinieneś o tym pamiętać podczas pracy z SortedMap i SortedSet. Dodatkowo polecamy do obejrzenia: Yuri Tkach: HashSet i TreeSet - Kolekcje #1 - Zaawansowana Java
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION