Liigu peamise sisu juurde

Comparator

Sissejuhatus

Kogumite sorteerimisel on vaja selgeks teha, milline objekt tuleb teise järel. Javas on selle jaoks kaks mehhanismi:

  • Comparable liides, mille kaudu seda rakendav klass määrab ära, kuidas järjestamist läbi viia.
  • Comparator liides, mis seab reeglid järjestamiseks ilma klassi muutmata.
OmadusComparableComparator
Kus asub loogikaKlassis endasEraldi objektis
MeetodcompareTo(T other)compare(T a, T b)
Mitu järjestustAinult üksNii palju kui vaja
Klassi muutmineVajalikPole vajalik
SobibLoomuliku järjestuse määramiseksAlternatiivseks 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.

teade

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 a peaks tulema enne b
  • Arvu null (0), kui objektid on võrdsed
  • Positiivse arvu, kui a peaks tulema peale b

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.

hoiatus

Ä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));
info

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);