Мързеливите не са единствените, които пишат за Comparators и сравнения в Java. Не съм мързелив, така че моля за още едно обяснение. Надявам се, че няма да е излишно. И да, тази статия е отговорът на въпроса: „ Можете ли да напишете компаратор по памет? “ Надявам се, че всеки ще може да напише компаратор по памет, след като прочете тази статия.

Въведение
Както знаете, Java е обектно-ориентиран език. В резултат на това е обичайно да се манипулират обекти в Java. Но рано or късно се сблъсквате със задачата да сравнявате обекти въз основа на няHowва характеристика. Например : Да предположим, че имаме няHowво съобщение, описано от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;
}
}
Поставете този клас в Java компилатора на Tutorialspoint . Не забравяйте да добавите и инструкциите за импортиране:
import java.util.Random;
import java.util.ArrayList;
import java.util.List;
В main
метода създайте няколко съобщения:
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);
}
Нека помислим Howво бихме направor, ако искаме да ги сравним? Например, искаме да сортираме по id. И за да създадем ред, трябва по няHowъв начин да сравним обектите, за да разберем кой обект трябва да е първи (т.е. по-малкият) и кой следва (т.е. по-големият). Нека започнем с клас като java.lang.Object . Знаем, че всички класове имплицитно наследяват Object
класа. И това има смисъл, защото отразява концепцията, че „всичко е обект“ и осигурява общо поведение за всички класове. Този клас диктува, че всеки клас има два метода: → hashCode
Методът hashCode
връща някои числови (int
) представяне на обекта. Какво означава това? Това означава, че ако създадете два различни екземпляра на клас, тогава те трябва да имат различни hashCode
s. Описанието на метода казва толкова много: „Доколкото е разумно практично, методът hashCode, дефиниран от клас Object, връща отделни цели числа за отделни обекти“. С други думи, за две различни instance
s, трябва да има различни hashCode
s. Тоест този метод не е подходящ за нашето сравнение. → equals
. Методът equals
отговаря на въпроса "равни ли са тези обекти?" и връща boolean
." По подразбиране този метод има следния code:
public boolean equals(Object obj) {
return (this == obj);
}
Тоест, ако този метод не е заменен, той по същество казва дали препратките към обекти съвпадат or не. Това не е, което искаме за нашите съобщения, защото се интересуваме от идентификатори на съобщения, а не от препратки към обекти. И дори да отменим equals
метода, най-многото, на което можем да се надяваме, е да научим дали са равни. И това не е достатъчно, за да определим реда. И така, от Howво се нуждаем тогава? Имаме нужда от нещо, което да се сравнява. Този, който сравнява, е Comparator
. Отворете Java API и намерете Comparator . Наистина има java.util.Comparator
интерфейс java.util.Comparator and java.util.Comparable
Както можете да видите, такъв интерфейс съществува. Клас, който го имплементира, казва: "Аз имплементирам метод, който сравнява обекти." Единственото нещо, което наистина трябва да запомните, е договорът за сравнение, който се изразява по следния начин:
Comparator returns an int according to the following rules:
- It returns a negative int if the first object is smaller
- It returns a positive int if the first object is larger
- It returns zero if the objects are equal
Сега нека напишем компаратор. Ще трябва да импортираме java.util.Comparator
. След инструкцията за импортиране добавете следното към main
метода: Comparator<Message> comparator = new Comparator<Message>();
Разбира се, това няма да работи, защото Comparator
е интерфейс. Така че добавяме фигурни скоби {}
след скобите. Напишете следния метод в скобите:
public int compare(Message o1, Message o2) {
return o1.getId().compareTo(o2.getId());
}
Дори не е нужно да помните правописа. Компараторът е този, който извършва сравнение, тоест сравнява. За да посочим относителния ред на обектите, връщаме int
. Това е общо взето. Хубаво и лесно. Както можете да видите от примера, освен Comparator, има още един интерфейс — java.lang.Comparable
, който изисква да внедрим compareTo
метода. Този интерфейс казва: "клас, който ме внедрява, прави възможно сравняването на екземпляри на класа." Например Integer
внедряването на compare
To е Howто следва:
(x < y) ? -1 : ((x == y) ? 0 : 1)
Java 8 въведе някои хубави промени. Ако погледнете по-отблизо интерфейса Comparator
, ще видите @FunctionalInterface
анотацията над него. Тази анотация е с информационна цел и ни казва, че този интерфейс е функционален. Това означава, че този интерфейс има само 1 абстрактен метод, който е метод без реализация. Какво ни дава това? Сега можем да напишем codeа на компаратора така:
Comparator<Message> comparator = (o1, o2) -> o1.getId().compareTo(o2.getId());
Наименуваме променливите в скоби. Java ще види, че тъй като има само един метод, необходимият брой и типове входни параметри са ясни. След това използваме оператора стрелка, за да ги предадем на тази част от codeа. Нещо повече, благодарение на Java 8 вече имаме методи по подразбиране в интерфейсите. Тези методи се появяват по подразбиране, когато внедрим интерфейс. Интерфейсът Comparator
има няколко. Например:
Comparator moreImportant = Comparator.reverseOrder();
Comparator lessImportant = Comparator.naturalOrder();
Има друг метод, който ще направи вашия code по-чист. Разгледайте примера по-горе, където дефинирахме нашия компаратор. Какво прави? Доста е примитивно. Той просто взема обект и извлича няHowва стойност, която е „сравнима“. Например Integer
implements comparable
, така че можем да извършим операция compareTo върху стойностите на полетата за идентификатор на съобщение. Тази проста функция за сравнение може да бъде написана така:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
С други думи, имаме a Comparator
, което сравнява по следния начин: взема обекти, използва getId()
метода, за да получи a Comparable
от тях, и след това използва, compareTo
за да сравни. И вече няма ужасни конструкции. И накрая, искам да отбележа още една особеност. Компараторите могат да бъдат верижни. Например:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
comparator = comparator.thenComparing(obj -> obj.getMessage().length());
Приложение
Обявяването на компаратор се оказва доста логично, не мислите ли? Сега трябва да видим How и къде да го използваме. →Collections.sort(java.util.Collections)
Можем, разбира се, да сортираме колекции по този начин. Но не всяка колекция, а само списъци. Тук няма нищо необичайно, защото списъците са вид колекции, в които имате достъп до елементи чрез техния индекс. Това позволява вторият елемент да бъде разменен с третия елемент. Ето защо следният метод за сортиране е само за списъци:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
Collections.sort(messages, comparator);
→ Arrays.sort(java.util.Arrays)
Масивите също са лесни за сортиране. Отново по същата причина — достъпът до техните елементи се осъществява чрез индекс. → Descendants of java.util.SortedSet and java.util.SortedMap
Ще си спомните това Set
и Map
не гарантирате реда, в който се съхраняват елементите. НО имаме специални реализации, които гарантират реда. И ако елементите на колекция не изпълняват java.util.Comparable
, тогава можем да предадем a Comparator
на нейния конструктор:
Set<Message> msgSet = new TreeSet(comparator);
→ Stream API
В Stream API, който се появи в Java 8, компараторите ви позволяват да опростите работата с елементи на потока. Да предположим например, че имаме нужда от поредица от произволни числа от 0 до 999 включително:
Supplier<Integer> randomizer = () -> new Random().nextInt(1000);
Stream.generate(randomizer)
.limit(10)
.sorted(Comparator.naturalOrder())
.forEach(e -> System.out.println(e));
Можем да спрем до тук, но има още по-интересни проблеми. Да предположим например, че трябва да подготвите Map
, където ключът е идентификатор на съобщение. Освен това искаме да сортираме тези ключове, така че ще започнем със следния code:
Map<Integer, Message> collected = Arrays.stream(messages)
.sorted(Comparator.comparing(msg -> msg.getId()))
.collect(Collectors.toMap(msg -> msg.getId(), msg -> msg));
Всъщност получаваме HashMap
тук. И Howто знаем, това не гарантира ниHowъв ред. В резултат на това нашите елементи, които бяха сортирани по id, просто губят реда си. Не е добре. Ще трябва да променим малко нашия колектор:
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));
Кодът започна да изглежда малко по-страшен, но сега проблемът е решен правилно. Прочетете повече за различните групи тук:
Можете да създадете свой собствен колектор. Прочетете повече тук: „Създаване на персонализиран колектор в Java 8“ . И ще ви е полезно да прочетете дискусията тук: „Списък на Java 8 за картографиране с поток“ .
Капан за падане
Comparator
и Comparable
са добри. Но има един нюанс, който трябва да запомните. Когато клас извършва сортиране, той очаква, че вашият клас може да бъде преобразуван в Comparable
. Ако това не е така, тогава ще получите грешка по време на изпълнение. Да разгледаме един пример:
SortedSet<Message> msg = new TreeSet<>();
msg.add(new Message(2, "Developer".getBytes()));
Изглежда, че тук нищо не е наред. Но всъщност в нашия пример ще се провали с грешка: java.lang.ClassCastException: Message cannot be cast to java.lang.Comparable
И всичко това, защото се опита да сортира елементите (в края на краищата е SortedSet
)... но не можа. Не забравяйте това, когато работите с SortedMap
и SortedSet
.
Още четене: |
---|
GO TO FULL VERSION