Comparator
Sissejuhatus
Kogumite sorteerimisel on vaja selgeks teha, milline objekt tuleb teise järel. Javas on selle jaoks kaks mehhanismi:
Comparableliides, mille kaudu seda rakendav klass määrab ära, kuidas järjestamist läbi viia.Comparatorliides, mis seab reeglid järjestamiseks ilma klassi muutmata.
| Omadus | Comparable | Comparator |
|---|---|---|
| Kus asub loogika | Klassis endas | Eraldi objektis |
| Meetod | compareTo(T other) | compare(T a, T b) |
| Mitu järjestust | Ainult üks | Nii palju kui vaja |
| Klassi muutmine | Vajalik | Pole vajalik |
| Sobib | Loomuliku järjestuse määramiseks | Alternatiivseks ja paindlikuks järjestamiseks |
Antud peatükk keskendub Comparator liidesele.
Sorteerimine ilma Comparator-ita
Oletame, et meil on klass Product, millel on id, nimi ja hind:
class Product {
private int id;
private String name;
private double price;
// constructor, getters: getId(), getName(), getPrice()
}
Me hoiame neid tooteid järjendis ning soovime neid sorteerida hinna järgi:
List<Product> products = new ArrayList<>();
products.add(new Product(1, "Widget", 5.99));
products.add(new Product(2, "Gadget", 2.49));
products.add(new Product(3, "Doohickey", 8.00));
List.sort() meetod peab teadma, kuidas kahte Product objekti võrrelda omavahel.
Kuna klassis endas seda loogikat pole, peab selle eraldi kuidagi kaasa andma.
Selleks on olemas Comparator liides.
list.sort(comparator) ja Collections.sort(list, comparator) on sünonüümid.
Comparator loomine
Comparator<T> on funktsionaalne liides, millel on üks meetod: compare(T a, T b).
See meetod tagastab:
- Negatiivse arvu, kui
apeaks tulema enneb - Arvu null (
0), kui objektid on võrdsed - Positiivse arvu, kui
apeaks tulema pealeb
Levinum ja lihtsam viis Comparator loomiseks on lambda-avaldisena (anonüümse funktsioonina):
products.sort((a, b) -> Double.compare(a.getPrice(), b.getPrice()));
Lambda-avaldis saab sisendina ((a, b)) kaks väärtust ning tagastab Double.compare() tulemuse.
Ära kunagi arvuta võrdlustulemust kahe arvu lahutamise teel (nt a.getId() - b.getId()).
See võib suurte täisarvude korral põhjustada ületäitumist ja anda vale tulemuse.
Kasuta alati spetsiaalseid võrdlusmeetodeid: Integer.compare(), Double.compare(), String.compareTo() jne.
List<User> users = getUsers();
// Bad - subtraction can overflow
users.sort((a, b) -> a.getId() - b.getId());
// Good - safe comparison
users.sort((a, b) -> Integer.compare(a.getId(), b.getId()));
Lühendatud tehasemeetodid
Levinud juhtudel, kui sorteeritakse ühe välja alusel, pakub Comparator ka staatilisi tehasemeetodeid, mis on lühemad ja selgemad, kui lambda käsitsi kirjutamine.
Näiteks kogumiku sorteerimiseks täisarvulise välja järgi tasub kasutada Comparator.comparingInt() tehasemeetodit:
products.sort(Comparator.comparingInt(Product::getId));
Product::getId on viide meetodile, lühendatud viis product -> product.getId() kirjutamise asemel.
Võrdlemisel kutsutakse iga objekti peal välja getId() meetodit ning võrreldakse tulemusi ja järjestatakse objektid korrektselt.
double ja long tüüpi väljade jaoks on vastavalt comparingDouble() ja comparingLong() meetodid:
products.sort(Comparator.comparingDouble(Product::getPrice));
Ning kõikide muude tüüpide jaoks, mida on mingil määral võimalik võrrelda, on olemas comparing() meetod:
products.sort(Comparator.comparing(Product::getName));
See töötab juhul, kui tagastatud väärtuse tüüp implementeerib Comparable liidest.
Teisejärguline sorteerimine
Kui kaks elementi on võrdsed, siis on võimalik rakendada ka teist Comparator-i, et viik lahendada.
Selleks tuleb esimesele võrdlusele aheldada juurde thenComparing-uga algav meetod,
näiteks:
// Sort by price; if prices are equal, sort by id
products.sort(
Comparator.comparingDouble(Product::getPrice)
.thenComparingInt(Product::getId)
);
Piirangut sellel ei ole, võib tekitada nii palju tasemeid kui vaja läheb:
products.sort(
Comparator.comparing(Product::getName)
.thenComparingDouble(Product::getPrice)
.thenComparingInt(Product::getId)
);
Järjestuse ümberpööramine
reversed() meetodiga on võimalik tekkinud järjestust ümber pöörata:
// Most expensive first
products.sort(Comparator.comparingDouble(Product::getPrice).reversed());
Kui reversed() kutsutakse välja pärast võrdlust, siis pööratakse ümber selle konkreetse võrdluse tulemus.
Järgnevad thenComparing() võrdlused toimivad tavapärases (mitte ümberpööratud) järjekorras.
// Price descending, then id ascending
products.sort(
Comparator.comparingDouble(Product::getPrice).reversed()
.thenComparingInt(Product::getId)
);
Loomulik järjestus
Comparator.naturalOrder() tagastab Comparator-i, mis järjestab elemendid nende loomulikku järjestust kasutades ehk vastavalt Comparable liidesele.
Comparator.reverseOrder() teeb sama, kuid tagurpidi.
Need on kasulikud siis, kui elemendid ise implementeerivad Comparable liidest (nt String, Integer), kuid on vaja anda kaasa Comparator objekt:
List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));
names.sort(Comparator.naturalOrder()); // [Alice, Bob, Charlie]
names.sort(Comparator.reverseOrder()); // [Charlie, Bob, Alice]
Samuti sobivad need kombineerimiseks thenComparing()-uga.
Näiteks sorteerimaks nimesid pikkuse järgi, ning sama pikkusega nimed tähestikulises vastupidises järjestuses:
names.sort(
Comparator.comparingInt(String::length)
.thenComparing(Comparator.reverseOrder())
);
Muutujasse salvestamine
Comparator objekte on võimalik salvestada ka muutujatesse, et neid hõlpsasti ka mujal uuesti kasutada:
Comparator<Product> byPriceThenId =
Comparator.comparingDouble(Product::getPrice)
.thenComparingInt(Product::getId);
products.sort(byPriceThenId);