Comparatori (Comparable și Comparator) în Java

Generics, lambda și stream-uri Java 1.2 (comparing, thenComparing, reversed: Java 8)

Comparable dă ordinea naturală a unei clase (compareTo); Comparator descrie orice altă ordine și se combină cu comparing, thenComparing și reversed.

Sintaxă
class Elev implements Comparable<Elev> {           // ordinea naturală
    public int compareTo(Elev altul) { ... }
}

Comparator<Elev> c = Comparator.comparing(Elev::medie)  // altă ordine
    .reversed()
    .thenComparing(Elev::nume);
lista.sort(c);

Ce face Comparatori (Comparable și Comparator)

Ca să sortezi obiecte, Java trebuie să știe cum se compară două. Metoda de comparare returnează un număr negativ dacă primul vine înainte, 0 dacă sunt egale și pozitiv dacă vine după. Clasele care implementează Comparable<T> au o ordine naturală (String alfabetic după codul caracterelor, numerele crescător, datele cronologic), folosită de Collections.sort, TreeMap și TreeSet.

Un Comparator<T> este un obiect separat care descrie o ordine, oricâte vrei pentru aceeași clasă. Metodele statice și implicite construiesc comparatori fără cod lung: Comparator.comparing(Elev::nume), comparingDouble, thenComparing (criteriu secundar), reversed() (ordine inversă), Comparator.nullsFirst(...), Comparator.reverseOrder().

Pentru numere folosește Integer.compare(a, b) sau Double.compare(a, b), nu a - b (poate depăși capacitatea). Pentru texte în română cu diacritice, ordinea naturală pune „Ș” după „Z”; un Collator pentru limba română ordonează corect.

Exemple

Exemplul 1: Ordine naturală și comparatori combinați

Ordinea naturală a lui Elev este după nume. Al doilea criteriu sortează descrescător după medie, iar la medii egale, alfabetic. Al treilea sortează după clasă, iar în aceeași clasă, cu Comparator.reverseOrder(), invers față de ordinea naturală (de la Z la A).

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

record Elev(String nume, double medie, int clasa) implements Comparable<Elev> {
    @Override
    public int compareTo(Elev altul) {
        return nume.compareTo(altul.nume);
    }
}

public class Main {
    public static void main(String[] args) {
        List<Elev> elevi = new ArrayList<>(List.of(
            new Elev("Radu", 9.10, 10), new Elev("Ana", 9.75, 11),
            new Elev("Ioana", 9.10, 10), new Elev("Bogdan", 8.40, 11)));

        Collections.sort(elevi);
        elevi.forEach(e -> System.out.print(e.nume() + " "));
        System.out.println("(alfabetic)");

        elevi.sort(Comparator.comparingDouble(Elev::medie).reversed().thenComparing(Elev::nume));
        elevi.forEach(e -> System.out.println(e.medie() + " " + e.nume()));

        elevi.sort(Comparator.comparingInt(Elev::clasa).thenComparing(Comparator.reverseOrder()));
        System.out.println(elevi.stream().map(e -> e.clasa() + ":" + e.nume()).toList());
    }
}

Rezultat

Ana Bogdan Ioana Radu (alfabetic)
9.75 Ana
9.1 Ioana
9.1 Radu
8.4 Bogdan
[10:Radu, 10:Ioana, 11:Bogdan, 11:Ana]

Exemplul 2: Valori null și texte cu diacritice

nullsLast pune orașele lipsă la final. Ordinea naturală a lui String pune „Ștefan” și „Țara” după „Zalău”; Collator pentru română le așază corect.

import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        List<String> orase = new ArrayList<>(Arrays.asList("Sibiu", null, "Arad", "Bacău", null));
        orase.sort(Comparator.nullsLast(Comparator.naturalOrder()));
        System.out.println(orase);

        List<String> nume = new ArrayList<>(List.of("Zalău", "Ștefan", "Sorin", "Țara", "Tudor", "Ana"));
        nume.sort(Comparator.naturalOrder());
        System.out.println("natural: " + nume);

        Collator romana = Collator.getInstance(Locale.forLanguageTag("ro-RO"));
        nume.sort(romana);
        System.out.println("română:  " + nume);
    }
}

Rezultat

[Arad, Bacău, Sibiu, null, null]
natural: [Ana, Sorin, Tudor, Zalău, Ștefan, Țara]
română:  [Ana, Sorin, Ștefan, Țara, Tudor, Zalău]

Greșeli frecvente

  • Compari cu scădere: return a.varsta - b.varsta;. Pentru valori mari scăderea depășește int și ordinea iese greșită. Folosește Integer.compare.
  • Scrii un compareTo care nu e de acord cu equals: TreeSet consideră egale (și elimină) obiectele pentru care comparatorul dă 0, chiar dacă sunt diferite.
  • Pui reversed() la final, după thenComparing, și inversezi tot lanțul, nu doar primul criteriu. Ordinea apelurilor contează.
  • Sortezi cuvinte românești cu ordinea naturală și „Ș” ajunge după „Z”. Folosește Collator.getInstance(Locale.forLanguageTag("ro-RO")).

Comenzi înrudite

  • Interfețe (interface, implements): O interfață (interface) descrie ce metode trebuie să aibă o clasă; clasa o promite cu implements și poate implementa mai multe interfețe.
  • Referințe la metode (::): O referință la metodă (Clasa::metoda) este o lambda prescurtată care doar apelează o metodă existentă.
  • Expresii lambda: O lambda este o funcție scurtă, fără nume, scrisă ca parametri -> corp, folosită oriunde se așteaptă o interfață funcțională.
  • equals și hashCode: equals decide când două obiecte sunt „egale” ca valoare; hashCode trebuie rescris împreună cu el, altfel HashMap și HashSet nu funcționează corect.
  • Comparator: Interfața care spune cum se ordonează două obiecte: compare(a, b) returnează un număr negativ, zero sau pozitiv.
  • record: record declară o clasă imutabilă pentru date: constructorul, accesorii, equals, hashCode și toString sunt generați automat.