Funktiorajapinnat ja lambdalausekkeet
osaamistavoitteet
- Ymmärrät funktionaalisen rajapinnan käsitteen
- Osaat käyttää lambdalausekkeita ja funktioviitteitä funktiorajapintojen toteuttamiseen
- Tunnet Javan yleisimmät valmiit funktiorajapinnat (esim.
Function,Consumer) - Osaat määrittää olioille vaihtoehtoisia järjestyksiä
Comparator-rajapinnan ja lambdalausekkeiden avulla
Funktionaalinen rajapinta (engl. functional interface) on rajapinta, joka sisältää vain yhden pakollisen metodin. Sen tarkoituksena on edustaa yksittäistä toimintoa tai kyvykkyyttä.
Esimerkiksi seuraava rajapinta on funktionaalinen:
/**
* Rajapinta, joka edustaa funktiota. Se ottaa parametrina luvun
* ja palauttaa toisen luvun.
*/
public interface NumeroFunktio {
int laske(int luku);
}
Myös osassa 4.1 esimerkkinä tehty
Saadettava-rajapinta on
funktionaalinen, sillä se sisältää vain yhden metodin: asetaArvo.
Java tarjoaa yksinkertaistetun tavan luoda olioita, jotka toteuttavat funktionaalisia rajapintoja. Tämä mahdollistaa koodin, jossa voimme käsitellä funktioita lähes samalla tavalla kuin käsittelemme dataa.
Olion alustaminen funktiorajapinnasta
Jos haluamme luoda olion, joka toteuttaa NumeroFunktio-rajapinnan, voimme
määritellä luokan, joka toteuttaa rajapinnan ja sitten ylikirjoittaa metodin
laske.
public class KerroKahdella implements NumeroFunktio {
@Override
public int laske(int luku) {
return luku * 2;
}
}
void main() {
NumeroFunktio kerroKahdella = new KerroKahdella();
IO.println(kerroKahdella.laske(3));
}
Tässä jouduimme kirjoittamaan melko paljon koodia (uusi luokka, metodin
ylikirjoitus) vain yhtä pientä oliota varten. Nyt on kuitenkin niin, että
NumeroFunktio on funktionaalinen rajapinta: sillä on vain yksi pakollinen
metodi. Javassa on mahdollista käyttää olemassa olevaa metodia ilman luokan
rakentelua ikään kuin tämä metodi olisi kyseisen rajapinnan toteuttava olio.
Tätä kutsutaan funktioviitteeksi (engl. method reference).
int kerroKahdella(int luku) {
return luku * 2;
}
void main() {
// Käytetään olemassa olevaa metodia rajapinnan toteutuksena
// HIGHLIGHT_GREEN_BEGIN
NumeroFunktio funktio = this::kerroKahdella;
// HIGHLIGHT_GREEN_END
IO.println(funktio.laske(3));
}
Huomaa erityisesti syntaksi this::kerroKahdella. Se ei kutsu metodia, vaan se
ikään kuin luo viitteen metodiin. Java osaa automaattisesti luoda rajapinnan
toteuttavan olion, koska kerroKahdella-metodin parametrit ja palautusarvo
täsmäävät rajapinnan ainoan metodin kanssa. Jos yritämme tulostaa
funktio-muuttujan arvon, kokonaisluvun sijaan tulostuukin olion tiedot:
public interface NumeroFunktio {
int laske(int luku);
}
int kerroKahdella(int luku) {
return luku * 2;
}
void main() {
NumeroFunktio funktio = this::kerroKahdella;
IO.println(funktio);
}
Toiseksi, funktioviitteen yhteydessä käytetään ::-merkintää viittaamaan joko
olion tai luokan metodiin. Toisin sanoen, this::kerroKahdella tarkoittaa, että
funktioviite koskee nykyisen olion kerroKahdella-metodia. this-viitteen
sijaan voidaan käyttää olioviitettä tai luokkametodien tapauksessa luokkaa:
class Ohjelma {
public static int kerroKahdellaStatic(int luku) {
IO.println("Olen luokkametodi!");
return luku * 2;
}
public int kerroKahdellaEiStatic(int luku) {
IO.println("Olen oliometodi!");
return luku * 2;
}
void main() {
// Nyt kerroKahdella on luokkametodi, joten käytetään luokan nimeä
// olioviitteen sijaan.
NumeroFunktio funktio = Ohjelma::kerroKahdellaStatic;
// Tavallisen metodin viite saadaan olioviitteen kautta
NumeroFunktio funktio2 = this::kerroKahdellaEiStatic;
IO.println(funktio.laske(2));
IO.println(funktio2.laske(2));
}
}
Bonus: Miten funktioviite toimii?
Saatat miettiä, miten funktio voi yhtäkkiä "muuttua" olioksi. Javassa kyseessä on oikeastaan tekninen temppu. Ennen funktioviitteitä sama asia Javassa tehtiin anonyymeillä luokilla.
Kääntäjä muuttaa funktioviitteen this::kerroKahdella suunnilleen tällaiseksi
rakenteeksi:
int kerroKahdella(int luku) {
return luku * 2;
}
NumeroFunktio funktio = new NumeroFunktio() {
@Override
public int laske(int luku) {
return kerroKahdella(luku);
}
};
Lambdalauseke on siis todellisuudessa olio, joka toteuttaa halutun rajapinnan. Tästä syystä niitä voidaan käyttää vain sellaisten rajapintojen kanssa, joissa on tasan yksi metodi (funktionaaliset rajapinnat).
Mainittakoon, että vaikka anonyymejä luokkia käytetään nykyään vähemmän, voivat olla silti hyödyllisiä tapauksissa, jossa toteutettava rajapinta ei ole funktionaalinen.
Lambdalausekkeet
Javassa on mahdollista kirjoittaa funktion toteutus ilman erillistä metodia, suoraan siinä kohdassa, missä funktio tarvitaan. Tällaista lausekemuodossa kirjoitettua funktiota kutsutaan lambdalausekkeeksi (engl. lambda expression).
Lambdalausekkeen perusrakenne on seuraava:
(tyyppi parametri) -> {
// funktion runko
return tulos;
}
Jos parametreja on useampi, ne erotetaan pilkulla:
(tyyppi1 parametri1, tyyppi2 parametri2) -> {
// funktion runko
return tulos;
}
Lambdalausekkeelle ei tarvitse antaa erikseen nimeä. Tästä syystä niitä kutsutaan myös anonyymeiksi funktioiksi (engl. anonymous function). Alla esimerkki, tulostetaan listan alkioita lambdalausekkeella.
void main() {
// Välitetään anonyymi funktio suoraan forEach-metodille
List<String> marjoja = List.of("mansikka", "mustikka", "puolukka", "mansikka");
// tulosta vain mansikat
marjoja.forEach(marja -> {
if (marja.equals("mansikka")) {
IO.println(marja);
}
});
}
Tämähän olisi voitu kirjoittaa myös perinteisellä aliohjelmakutsulla:
void main() {
List<String> marjoja = List.of("mansikka", "mustikka", "puolukka");
tulostaMansikat(marjoja);
}
void tulostaMansikat(List<String> marjoja) {
for (String marja : marjoja) {
if (marja.equals("mansikka")) {
IO.println(marja);
}
}
}
Oleellinen ajatus on tämä: lambdalauseke ei ole irrallinen koodinpätkä, vaan se
on olio, joka toteuttaa funktionaalisen rajapinnan. Kullekin parametrille tulee
määritellä tyyppi, joka vastaa funktionaalisen rajapinnan metodin parametreja.
Palataan esimerkkiimme NumeroFunktio-rajapinnasta. Toteutetaan nyt
lambdalausekkeena funktio, joka kertoo syötetyn luvun kahdella. Tällaisen
lausekkeen tulee ottaa parametrina yksi kokonaisluku ja palauttaa kokonaisluku.
Muoto on seuraava:
(int luku) -> {
return luku * 2;
}
Koska tällainen lambdalauseke toteuttaa NumeroFunktio-rajapinnan, voimme
sijoittaa sen NumeroFunktio-tyyppiseen muuttujaan:
NumeroFunktio funktio = (int luku) -> {
return luku * 2;
};
Nyt voimme kutsua funktio-muuttujan laske-metodia, koska olemme toteuttaneet
NumeroFunktio-rajapinnan.
void main() {
NumeroFunktio funktio = (int luku) -> {
return luku * 2;
};
IO.println(funktio.laske(1));
IO.println(funktio.laske(2));
IO.println(funktio.laske(3));
IO.println(funktio.laske(4));
}
Emme siis tarvinneet erillistä luokkaa, emmekä edes erillistä metodia!
Lambdalausekkeiden suurin etu on juuri niiden tiiviys. Java osaa päätellä monta
asiaa automaattisesti, jolloin koodia vielä tästäkin voidaan lyhentää.
Ensinnäkin parametrien tyypit voidaan päätellä funktiorajapinnan parametrien
tyypeistä, joten tyypit voidaan usein jättää pois. Yllä olevassa esimerkissämme
voimme jättää pois int-tyypin, koska NumeroFunktio.laske-metodi ottaa
parametrina kokonaisluvun, eikä tätä tarvitse erikseen mainita lambdalausekkeessa.
public interface NumeroFunktio {
int laske(int luku);
}
void main() {
NumeroFunktio funktio = (luku) -> {
return luku * 2;
};
IO.println(funktio.laske(1));
IO.println(funktio.laske(2));
IO.println(funktio.laske(3));
IO.println(funktio.laske(4));
}
Toiseksi, jos lambdalausekkeen runko sisältää vain yhden lauseen, aaltosulut ja
return-sanan voi jättää pois.
public interface NumeroFunktio {
int laske(int luku);
}
void main() {
NumeroFunktio funktio = (luku) -> luku * 2;
IO.println(funktio.laske(1));
IO.println(funktio.laske(2));
IO.println(funktio.laske(3));
IO.println(funktio.laske(4));
}
Lopuksi, jos lambdalausekkeessa on tasan yksi parametri, myös kaarisulut voi jättää pois.
public interface NumeroFunktio {
int laske(int luku);
}
void main() {
NumeroFunktio funktio = luku -> luku * 2;
IO.println(funktio.laske(1));
IO.println(funktio.laske(2));
IO.println(funktio.laske(3));
IO.println(funktio.laske(4));
}
Lambdalausekkeissa on lisäksi tapana käyttää tavallista lyhyempiä parametrien nimiä, sillä parametrien merkitys dokumentoidaan funktionaalisessa rajapinnassa.
public interface NumeroFunktio {
int laske(int luku);
}
void main() {
NumeroFunktio kerroKahdella = x -> x * 2;
IO.println(kerroKahdella.laske(1));
IO.println(kerroKahdella.laske(2));
IO.println(kerroKahdella.laske(3));
IO.println(kerroKahdella.laske(4));
}
Funktiot parametreina
Koska lambdalauseke on olio, voimme välittää sen aliohjelmalle parametrina. Tämä mahdollistaa korkeamman abstraktiotason funktioiden kirjoittamisen. Voimme tehdä vaikkapa aliohjelman, joka osaa laskea kahden eri funktion summan:
public interface NumeroFunktio {
int laske(int luku);
}
/**
* Laskee kahden funktion summan tietylle arvolle.
*/
int summaaFunktiot(NumeroFunktio f1, NumeroFunktio f2, int x) {
return f1.laske(x) + f2.laske(x);
}
void main() {
// Välitetään kaksi eri funktiota (x*2 ja x*x) summattavaksi
int tulos = summaaFunktiot(x -> x * 2, x -> x * x, 3);
IO.println("Tulos: " + tulos); // (3*2) + (3*3) = 6 + 9 = 15
}
Valmiita funktiorajapintoja
Javassa on joukko valmiita yleisiä funktiorajapintoja, jotka löytyvät
java.util.function-paketista (ks.
JavaDoc).
Function<T, R>
(JavaDoc)
esittää funktiota, joka ottaa yhden parametrin tyyppiä T ja palauttaa
parametrin tyyppiä R. Esimerkiksi yllä oleva esimerkki voidaan yksinkertaistaa
käyttämällä valmista Function-rajapintaa NumeroFunktio-rajapinnan sijaan:
void main() {
Function<Integer, Integer> kerroKahdella = x -> x * 2;
Function<Integer, Integer> potenssiinKaksi = x -> x * x;
IO.println(kerroKahdella.apply(1));
IO.println(potenssiinKaksi.apply(2));
}
Function-rajapinta sisältää myös apumetodit andThen ja compose, joiden
avulla funktioita voidaan ketjuttaa:
void main() {
Function<Integer, Integer> kerroKahdella = x -> x * 2;
Function<Integer, Integer> potenssiinKaksi = x -> x * x;
// Laskee: (x^2) * 2
Function<Integer, Integer> potenssiinJaKerro = kerroKahdella.compose(potenssiinKaksi);
// Laskee: (x * 2)^2
Function<Integer, Integer> kerroJaPotenssiin = kerroKahdella.andThen(potenssiinKaksi);
IO.println(potenssiinJaKerro.apply(2));
IO.println(kerroJaPotenssiin.apply(2));
}
Vastaavasti BiFunction<T, U, R>
(JavaDoc)
edustaa funktiota, joka ottaa kaksi parametria ja palauttaa yhden arvon.
Consumer<T>
(JavaDoc)
ja BiConsumer<T, U>
(JavaDoc)
puolestaan vastaavat funktioita, jotka ottavat parametreja mutta eivät palauta
mitään (palautustyyppi on void). Esimerkiksi useat kokoelmat sisältävät
forEach-metodin, jolle välitetään Consumer-olio:
void main() {
List<String> marjoja = List.of("mansikka", "mustikka", "puolukka");
// IO.println sopii Consumer<T>:hen, sillä
// se ottaa yhden parametrin eikä palauta mitään
marjoja.forEach(IO::println);
Map<String, Integer> arvosanat = new HashMap<>(
Map.of( "Denis", 1,
"Antti-Jussi", 3,
"Sami", 5,
"Karri", 5)
);
// BiConsumer ottaa kaksi parametria (avain ja arvo)
arvosanat.forEach((nimi, arvosana) -> IO.println(nimi + " => " + arvosana));
}
Tee ohjelma, joka kysyy käyttäjältä kaksi desimaalilukua sekä laskutoimituksen ja tulostaa lopputuloksen seuraavasti:
Luku 1 > 12.0
Luku 2 > 3.0
Laskutoimitus (+, -, *, /) > +
12.0 + 3.0 = 15.0
Tässä vaiheessa sinun ei tarvitse käsitellä virheellisiä syötteitä, vaan voit
olettaa, että luvut annetaan aina lukuina. Sallitut laskutoimitukset ovat summa
(+), erotus (-), tulo (*) ja osamäärä (/). Voit olettaa, että vain näitä
laskutoimituksia käytetään syötteenä.
Älä käytä silmukoita tai ehtorakenteita. Sen sijaan toteuta laskutoimitukset lambdalausekkeina ja tallenna ne hakurakenteeseen käyttäen laskutoimituksen merkkiä avaimena.
Ohjelman suoritus päättyy tuloksen näyttämisen jälkeen.
Vinkki 1
Voit käyttää lambdalausekkeiden tyyppinä BiFunction<Double, Double, Double>
(JavaDoc)
tai DoubleBinaryOperator
(JavaDoc).
Vinkki 2
Voit käyttää hakurakenteen tyyppinä Map<String, BiFunction<Double, Double, Double>> tai Map<String, DoubleBinaryOperator>.
Voit joko valita hakurakenteelle tietyn toteutuksen tai alustaa muuttumattoman
hakurakenteen Map.of-metodilla:
Map<String, BiFunction<Double, Double, Double>> laskutoimitukset = Map.of(
"+", ...,
"-", ...,
"*", ...,
"/", ...
);
...-kohdan tilalle riittää asettaa sopiva lambdalauseke.
Comparator-rajapinta
Palataan luvussa 4.2 esiteltyyn
Comparable<T>-rajapintaan. Sen avulla määritimme olioille luonnollisen
järjestyksen. Toisinaan haluamme kuitenkin järjestää samoja olioita eri
tilanteissa eri tavoin (esim. henkilöt nimen mukaan TAI iän mukaan).
Javan Comparator-rajapinta
(JavaDoc)
tarjoaa tavan määrittää vaihtoehtoisia järjestystapoja. Rajapinta sisältää
vain yhden pakollisen metodin (compare), eli se on funktionaalinen rajapinta.
Rajapinnan compare toimii samalla periaatteella kuin Comparable.compareTo:
| Tapaus | Merkitys | Tulkinta |
|---|---|---|
cmp.compare(a, b) < 0 | a < b | a on järjestyksessä ennen b:tä |
cmp.compare(a, b) == 0 | a == b | a ja b ovat samanarvoisia |
cmp.compare(a, b) > 0 | a > b | a on järjestyksessä b:n jälkeen |
Koska Comparator on funktionaalinen, voimme määrittää vertailun erittäin
tiiviisti lambdalausekkeena:
void main() {
List<String> nimet = Arrays.asList("Ville", "Aino", "Matti");
// Järjestetään nimet pituuden mukaan (lyhyin ensin)
nimet.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));
IO.println(nimet); // [Aino, Ville, Matti]
}
Palataan vielä osassa 4.2 olevaan
keräilykorttiesimerkkiin.
Laajennetaan hieman Kerailykortti-luokkaa lisäämällä attribuutti sarja, joka
kuvaa korttisarjaa (esim. eläimet, ajoneuvot, jne.):
class Kerailykortti implements Comparable<Kerailykortti> {
private String nimi;
// HIGHLIGHT_GREEN_BEGIN
private String sarja;
// HIGHLIGHT_GREEN_END
private int tunnistenumero;
// HIGHLIGHT_GREEN_BEGIN
public Kerailykortti(String nimi, String sarja, int tunnistenumero) {
// HIGHLIGHT_GREEN_END
this.nimi = nimi;
// HIGHLIGHT_GREEN_BEGIN
this.sarja = sarja;
// HIGHLIGHT_GREEN_END
this.tunnistenumero = tunnistenumero;
}
@Override
public int compareTo(Kerailykortti other) {
int sarjaVertailu = this.sarja.compareTo(other.sarja);
if (sarjaVertailu != 0) {
return sarjaVertailu;
}
return Integer.compare(this.tunnistenumero, other.tunnistenumero);
}
public String getNimi() {
return nimi;
}
public String getSarja() {
return sarja;
}
@Override
public String toString() {
return "Kortti: " + nimi + " (Sarja: " + sarja + ", #" + tunnistenumero + ")";
}
}
void main() {
List<Kerailykortti> kortit = Arrays.asList(
new Kerailykortti("Loistava Lohikäärme", "Eläimet", 3),
new Kerailykortti("Vauhdikas Vespajetti", "Ajoneuvot", 1),
new Kerailykortti("Aloittelijan Ameeba", "Eläimet", 1),
new Kerailykortti("Mieletön Merihevonen", "Eläimet", 2),
new Kerailykortti("Nopea Nopsa", "Ajoneuvot", 2)
);
IO.println("Ennen järjestämistä:");
for (Kerailykortti kortti : kortit) {
IO.println(kortti);
}
Collections.sort(kortit);
IO.println();
IO.println("Jälkeen järjestämisen:");
for (Kerailykortti kortti : kortit) {
IO.println(kortti);
}
}
Tällä hetkellä keräilykorteille on määritelty luonnollinen järjestys siten, että
ensin keräilykortit järjestetään nimen ja sitten tunnisteen mukaan. Haluaisimme
kuitenkin tarjota vaihtoehtoisen tavan järjestää keräilykortteja sarjan nimen
mukaan. Tätä varten voimme käyttää Javan valmista sort-metodin versiota, joka
ottaa parametrina Comparator-vertailijan:
void main() {
List<Kerailykortti> kortit = Arrays.asList(
new Kerailykortti("Loistava Lohikäärme", "Eläimet", 3),
new Kerailykortti("Vauhdikas Vespajetti", "Ajoneuvot", 1),
new Kerailykortti("Aloittelijan Ameeba", "Eläimet", 1),
new Kerailykortti("Mieletön Merihevonen", "Eläimet", 2),
new Kerailykortti("Nopea Nopsa", "Ajoneuvot", 2)
);
IO.println("Ennen järjestämistä:");
kortit.forEach(IO::println);
// Collections.sort tarjoaa version, jossa toiseksi parametrina voi antaa
// järjestyksen, jonka mukaan alkioita järjestetään.
Comparator<Kerailykortti> sarjanMukaan =
(kortti1, kortti2) -> kortti1.getSarja().compareTo(kortti2.getSarja());
Collections.sort(kortit, sarjanMukaan);
IO.println();
IO.println("Jälkeen järjestämisen:");
kortit.forEach(IO::println);
}
Huomaa erityisesti, että nyt järjestäminen tehdään sarjanMukaan-vertailijan
mukaan, joka on määritelty lambdalausekkeena. Koska vertailija on määritelty
Kerailykortti-luokan ulkopuolella, lisäsimme myös saantimetodin getSarja().
Javan Comparator-luokka tarjoaa myös valmiita apumetodeja vertailijoiden
yhdistämiseksi.
Comparator.comparing()-metodi ottaa parametrina lambdalausekkeen, joka
palauttaa oliosta lasketun arvon, jonka perusteella vertailu tehdään. Metodi
soveltuu tilanteisiin, kun olio halutaan vertailla olion metodin palauttaman
arvon luonnollisen järjestyksen mukaan. Esimerkiksi keräilykorttien
sarjanMukaan-vertailija voidaan toteuttaa suoraviivaisemmin:
void main() {
List<Kerailykortti> kortit = Arrays.asList(
new Kerailykortti("Loistava Lohikäärme", "Eläimet", 3),
new Kerailykortti("Vauhdikas Vespajetti", "Ajoneuvot", 1),
new Kerailykortti("Aloittelijan Ameeba", "Eläimet", 1),
new Kerailykortti("Mieletön Merihevonen", "Eläimet", 2),
new Kerailykortti("Nopea Nopsa", "Ajoneuvot", 2)
);
IO.println("Ennen järjestämistä:");
kortit.forEach(IO::println);
Comparator<Kerailykortti> sarjanMukaan =
Comparator.comparing(Kerailykortti::getSarja);
Collections.sort(kortit, sarjanMukaan);
IO.println();
IO.println("Jälkeen järjestämisen:");
kortit.forEach(IO::println);
}
class Kerailykortti implements Comparable<Kerailykortti> {
private String nimi;
private String sarja;
private int tunnistenumero;
public Kerailykortti(String nimi, String sarja, int tunnistenumero) {
this.nimi = nimi;
this.sarja = sarja;
this.tunnistenumero = tunnistenumero;
}
public String getSarja() {
return sarja;
}
@Override
public int compareTo(Kerailykortti other) {
int sarjaVertailu = this.sarja.compareTo(other.sarja);
if (sarjaVertailu != 0) {
return sarjaVertailu;
}
return Integer.compare(this.tunnistenumero, other.tunnistenumero);
}
@Override
public String toString() {
return "Kortti: " + nimi + " (Sarja: " + sarja + ", #" + tunnistenumero + ")";
}
}
Puolestaan Comparator.thenComparing()-metodi palauttaa vertailijan, joka
yhdistää kaksi vertailijaa yhteen: ensin verrataan olioita ensimmäisen
vertailijan mukaan ja jos ensimmäinen vertailija palauttaa 0, vertaillaan
toisen vertailijan mukaan. Esimerkiksi keräilykorttien omaa compareTo-metodia
voidaan toteuttaa oliomaisemmin seuraavasti:
@Override
public int compareTo(Kerailykortti other) {
// Vertailija, joka vertaa kortteja sarjan mukaan
Comparator<Kerailykortti> sarjanMukaan = Comparator.comparing(k -> k.sarja);
// Vertailija, joka vertaa kortteja tunnistenumeron mukaan
Comparator<Kerailykortti> tunnistenumeronMukaan = Comparator.comparing(k -> k.tunnistenumero);
// vertaillaan ensin sarjan mukaan ja sitten tunnistenumeron mukaan
Comparator<Kerailykortti> vertailu = sarjanMukaan.thenComparing(tunnistenumeronMukaan);
return vertailu.compare(this, other);
}
Comparator.naturalOrder() palauttaa Comparator-vertailijan, joka järjestää
oliot niiden luonnollisen järjestyksen mukaan. Toisin sanoen, tämä
mahdollistaa ns. eristää Comparable-rajapintaa toteuttavan olion
compareTo-metodin toteutuksen vertailuolioksi. Esimerkiksi merkkijonojen
aakkosjärjestystä vastaavan vertailijan saa tällä tavoin:
void main() {
List<String> jonoja = new ArrayList<>(List.of("Denis", "Antti-Jussi", "Karri", "Rauli", "Sami"));
Comparator<String> aakkosjarjestys = Comparator.naturalOrder();
Collections.sort(jonoja, aakkosjarjestys);
IO.println(jonoja);
}
Comparator.reversed() luo uuden vertailijan, joka kääntää
vertailujärjestyksen. Tämän avulla esimerkiksi pystyy helposti järjestämään
merkkijonot käänteiseen aakkosjärjestykseen:
void main() {
List<String> jonoja = new ArrayList<>(List.of("Denis", "Antti-Jussi", "Karri", "Rauli", "Sami"));
Comparator<String> aakkosjarjestys = Comparator.naturalOrder();
// HIGHLIGHT_GREEN_BEGIN
Comparator<String> kaanteinenAakkosjarjestys = aakkosjarjestys.reversed();
// HIGHLIGHT_GREEN_END
Collections.sort(jonoja, kaanteinenAakkosjarjestys);
IO.println(jonoja);
}
Oletuksena monet vertailijat eivät käsittele null-viitteitä, mikä voi johtaa
erilaisiin virheisiin ja odottamattomiin tilanteisiin. Esimerkiksi jopa Javassa
määritelty String-merkkijonojen luonnollinen järjestys ei käsittele tapausta,
jos jompikumpi verrattavista merkkijonoista on null:
void main() {
String[] jono = {"Ohjelmointi 1", null, "Ohjelmointi 2"};
Arrays.sort(jono);
IO.println(Arrays.toString(jono));
}
java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "a[runHi]" is null
Comparator.nullsFirst() ja Comparator.nullsLast() -metodit auttavat
tällaisissa tilanteissa: ne ottavat parametriksi vertailuolion ja palauttavat
uuden vertailijan, joka osaa käsitellä null-viitteitä. Nimensä mukaan
nullsFirst() asettaa null-viitteet pienemmäksi kuin muut arvot (ja siten
järjestyksessä ensimmäiseksi), kun taas nullsLast asettaa null-viitteet
suuremmaksi kuin muut arvot (eli järjestyksessä viimeiseksi):
void main() {
String[] jono = {"Ohjelmointi 1", null, "Ohjelmointi 2"};
Comparator<String> aakkosjarjestys = Comparator.naturalOrder();
Comparator<String> nullitEnsimmaiseksi = Comparator.nullsFirst(aakkosjarjestys);
Arrays.sort(jono, nullitEnsimmaiseksi);
IO.println(Arrays.toString(jono));
Comparator<String> nullitViimeiseksi = Comparator.nullsLast(aakkosjarjestys);
Arrays.sort(jono, nullitViimeiseksi);
IO.println(Arrays.toString(jono));
}
Yhdistämällä eri Comparator-vertailijoiden metodeja voidaan toteuttaa hyvin
monipuolisia vertailijoita ilman ehtorakenteita:
void main() {
List<String> nimet = Arrays.asList("Ville", "Aino", "Matti", null);
// Rakennetaan monimutkaisempi vertailija:
// 1. Huomioi null-arvot (laita ne loppuun)
// 2. Järjestä pituuden mukaan
// 3. Jos pituus on sama, käytä aakkosjärjestystä (naturalOrder)
Comparator<String> vertailija = Comparator.nullsLast(
Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder())
);
nimet.sort(vertailija);
IO.println(nimet);
}
Laajenna Kerailykortti-luokkaa
lisäämällä sille attribuutti String harvinaisuus. Keräilykortin harvinaisuus voi olla yksi seuraavista arvoista
(vähiten harvinaisesta harvinaisimpaan): C, U, R, RR, RRR, SR, AR,
SAR, UR.
Kirjoita vertailija, joka järjestää listassa olevat kortit niiden harvinaisuuden mukaan. Voit käyttää seuraavaa valmista korttikokoelmaa koodisi testaamiseen:
Mallilista erilaisista korteista
List<Kerailykortti> kortit = new ArrayList<>(List.of(
new Kerailykortti("Kadonnut Puolipiste", "Koodiviidakko", 101, "C"),
new Kerailykortti("Loputon Silmukka", "Koodiviidakko", 102, "U"),
new Kerailykortti("Bugimetsästäjä", "Koodiviidakko", 103, "R"),
new Kerailykortti("Spagettikoodi-Hirviö", "Koodiviidakko", 104, "RR"),
new Kerailykortti("Ylikellotettu Prosessori", "Koodiviidakko", 105, "SR"),
new Kerailykortti("Pyhä Stack Overflow", "Koodiviidakko", 106, "RRR"),
new Kerailykortti("Null Pointer -Ninja", "Koodiviidakko", 107, "U"),
new Kerailykortti("Sininen Kuolemanruutu", "Koodiviidakko", 108, "AR"),
new Kerailykortti("Opiskelijakortti", "Kampus-Saaga", 201, "C"),
new Kerailykortti("Unelias Luennoitsija", "Kampus-Saaga", 202, "C"),
new Kerailykortti("Haalaribileet", "Kampus-Saaga", 203, "U"),
new Kerailykortti("Ylisuorittaja", "Kampus-Saaga", 204, "R"),
new Kerailykortti("Ilmainen Ämpäri", "Kampus-Saaga", 205, "SAR"),
new Kerailykortti("Myöhästynyt Palautus", "Kampus-Saaga", 206, "RR"),
new Kerailykortti("Akateeminen Vartti", "Kampus-Saaga", 207, "SR"),
new Kerailykortti("Gradu-Ahdistus", "Kampus-Saaga", 208, "AR"),
new Kerailykortti("Semman Pannukakku", "Kampus-Saaga", 209, "UR"),
new Kerailykortti("Vihainen Hirvi", "Suomi-Myytit", 301, "C"),
new Kerailykortti("Ikuinen Marraskuu", "Suomi-Myytit", 302, "RR"),
new Kerailykortti("Saunaklonkku", "Suomi-Myytit", 303, "SR"),
new Kerailykortti("Salmiakkisade", "Suomi-Myytit", 304, "U"),
new Kerailykortti("Väinämöisen Kantele", "Suomi-Myytit", 305, "SAR"),
new Kerailykortti("Sisu", "Suomi-Myytit", 306, "RRR"),
new Kerailykortti("Laser-Löylykauha", "Suomi-Myytit", 307, "UR"),
new Kerailykortti("Toripoliisi", "Suomi-Myytit", 308, "R")
));
Kirjoita main()-ohjelma, joka järjestää ja tulostaa keräilykortit
harvinaisuuden mukaan (yleisimmät kortit ensin, harvinaisimmat viimeiseksi).
Kortit, joiden harvinaisuus on jokin muu kuin yllä mainitut tai null, tulee
sijoittaa listan alkuun.
Parametri, jota ei käytetä
Käyttöliittymäkoodissa tulee usein vastaan tilanteita, joissa aliohjelman määrittely vaatii parametrin, mutta toteutus ei tarvitse sitä. Tyypillinen esimerkki on tapahtumankuuntelija: rajapinta edellyttää tapahtumaolion vastaanottamista, vaikka itse tapahtumatietoa ei käytettäisi.
button.setOnAction(event -> {
IO.println("Nappia painettu!");
});
Tästä voi tulla ohjelmointiympäristössä varoitus käyttämättömästä
event-parametrista. Varoitus on yleensä hyödyllinen, mutta tässä tapauksessa
kyse ei ole virheestä.
Koska parametria ei voi jättää kokonaan pois, Javassa sille voidaan antaa
nimeämätön muoto kirjoittamalla nimen paikalle _. Java 22:sta alkaen tämä on
virallinen kielen ominaisuus, jota kutsutaan nimeämättömäksi muuttujaksi
(unnamed variable).
button.setOnAction(_ -> {
// Tämä tapahtumankuuntelija ei tarvitse tapahtumatietoja, joten
// parametri voidaan hylätä nimellä "_"
IO.println("Nappia painettu!");
});
Tällöin ilmaistaan suoraan, että parametrin arvoa ei tarvita. Alaviivalla määriteltyyn parametriin ei voi viitata myöhemmin koodissa, ja mahdollinen "parameter is never used" -varoitus jää pois.
Vastaavanlainen käytäntö on käytössä myös joissakin muissa kielissä, kuten C#:ssa. Javassa kyse ei kuitenkaan ole enää pelkästä konventiosta, vaan virallisesta kieliominaisuudesta.