Javan funktionaaliset rajapinnat¶
Aiemmin todettiin, että “kun annamme lajittelufunktiolle parametriksi lambda-funktion, luo Java
sen pohjalta automaattisesti rajapinnan Comparator
toteuttavan olion, jolla on lambda-funktion
kuvaamat askeleet suorittava jäsenfunktio compare
”. Tässä on tarkemmin ottaen kyse siitä, että
Comparator
on ns. “funktionaalinen rajapinta”, ja Javassa sekä lambda-funktio että
funktioviittaus ovat syntaktisia oikoteitä funktionaalisen rajapinnan täyttävän olion luomiseksi.
Niitä voisi ajatella yksinkertaistetuiksi tavoiksi tehdä nimetön luokkamääritys.
Javassa funktionaalinen rajapinta tarkoittaa yksinkertaisesti rajapintaa, joka määrittää täsmälleen yhden abstraktin jäsenfunktion. Esimerkiksi kumpikin seuraavista rajapinnoista on Javan näkökulmasta funktionaalinen rajapinta, koska niistä kummassakin on täsmälleen yksi abstrakti jäsenfunktio.
public interface SayHello {
public void sayHello(); // Abstrakti jäsenfunktio.
}
public interface Comparator<T> {
int compare(T a, T b); // Abstrakti jäsenfunktio.
boolean equals(Object obj); // Ei ole abstrakti, koska oletustoteutus peritään Object-luokalta!
}
Kun lambda-funktio tai funktioviittaus esiintyy kontekstissa, jossa tarvitaan funktionaalisen rajapinnan toteuttava olio, Java-kääntäjä automaattisesti luo sellaisen niin, että lambda-funktio tai viitattu funktio antaa toteutuksen kyseisen funktionaalisen rajapinnan abstraktille jäsenfunktiolle. Käytämme jatkossa funktionaalisen rajapinnan toteuttavasta oliosta nimitystä funktio-olio.
Alla on esimerkkinä funktio helloTest
, joka saa parametrikseen SayHello
-rajapinnan
toteuttavan funktio-olion. Funktiota kutsuttaessa parametri voidaan antaa lambda-funktiona tai
funktioviittauksena, koska SayHello
on funktionaalinen rajapinta. Toki parametri voidaan antaa
myös eksplisiittisesti olionakin käyttäen joko tavallista luokkaa tai nimetöntä luokkamääritystä.
Alla onkin esimerkit jokaisesta edellä mainitusta neljästä tavasta välittää SayHello
-rajapinnan
täyttävä olio funktiolle helloTest
.
public class FunctionalExample {
public static interface SayHello {
public void sayHello();
}
public static void helloTest(SayHello sh) {
sh.sayHello();
}
public static class MyHello implements SayHello {
@Override
public void sayHello() {
System.out.println("Hello from class MyHello!");
}
}
public static void myHelloFunction() {
System.out.println("Hello from a referenced function!");
}
public static void main(String[] args) {
// Vaihtoehto #1: määritetään funktion helloTest parametri luokan MyHello-oliona.
helloTest(new MyHello());
// Vaihtoehto #2: määritetään funktion helloTest parametri nimettömänä luokkana.
helloTest(new SayHello() {
@Override
public void sayHello() {
System.out.println("Hello from an anonymous class!");
}
});
// Vaihtoehto #3: määritetään funktion helloTest parametri lambda-funktiona.
helloTest(() -> System.out.println("Hello from a lambda-function!"));
// Vaihtoehto #4: määritetään funktion helloTest parametri funktioviittauksella.
helloTest(FunctionalExample::myHelloFunction);
}
}
Edellisen testikoodin suoritus tulostaa:
Hello from class MyHello!
Hello from an anonymous class!
Hello from a lambda-function!
Hello from a referenced function!
Kun Java-kääntäjä prosessoi edellisen koodin kohdan
helloTest(() -> System.out.println("Hello from lambda-function!"))
sillä on käytettävissään
tieto, että funktio helloTest
ottaa parametrikseen rajapinnan SayHello
toteuttavan
funktio-olion. Kääntäjä lisäksi tietää, että kyseisellä rajapinnalla on yksi abtrakti jäsenfunktio,
sayHello
, eli kyseessä on funktionaalinen rajapinta. Tämän johdosta kääntäjä generoi jokseenkin
samanlaisen funktio-olion kuin jos olisimme käyttäneet rajapinnan SayHello
toteuttavaa
nimetöntä luokkamääritystä, jonka määrittämä funktio sayHello
vastaa antamaamme
lambda-funktiota. Kohta helloTest(FunctionalExample::myHelloFunction)
käsitellään muuten
samalla tavalla, mutta funktion sayHello
toteuttaa viitattu funktio myHelloFunction
.
Kuten nimettömien luokkamääritysten kohdalla, myös lambda-funktion (tai funktioviittauksenkin) luoman funktio-olion (eli viitteen siihen) voi tallettaa muuttujaan. Alla on sama lajitteluesimerkki kuin aiemmin nimettömien luokkamääritysten yhteydessä, mutta nyt lambda-funktiota käyttäen.
Point2D[] ps = {new Point2D(2.5, -1.5), new Point2D(-3.5, 4.5), new Point2D(0.5, 0.5)};
Comparator<Point2D> comparator = (Point2D a, Point2D b) -> { // Lambda-funktio.
int cmp = Double.compare(a.getX(), b.getX());
if(cmp == 0) {
cmp = Double.compare(a.getY(), b.getY());
}
return cmp;
};
Arrays.sort(ps, comparator); // Annetaan vertailuolio comparator parametrina.
for(Point2D p : ps) {
System.out.format(" (%.1f, %.1f)", p.getX(), p.getY());
}
Voimme siis käyttää lambda-funktiota tai funktioviittausta sellaisen funktio-olion luontiin, jonka tyypillä on täsmälleen yksi abstrakti jäsenfunktio. Lambda-funktio luonnollisesti pitää määrittää niin, että sen muoto on yhteensopiva toteutettavan abstraktin jäsenfunktion kanssa (esim. parametrien määrä ja palautettavan arvo tyyppi). Huomaa, että lambda-funktio tai funktioviittaus ei itsessään ilmaise, minkä rajapinnan täyttävä funktio-olio sen pohjalta luodaan: kääntäjä päättelee rajapinnan tyypin käyttökontekstin pohjalta eli yleensä siitä, minkätyyppiseen muuttujaan (mukaanlukien minkätyyppiseen funktion parametriin) lambda-funktio tai funktioviittaus sijoitetaan.
Javan luokkakirjastossa on määritetty jo tutuksi käyneen Comparator
-rajapinnan lisäksi useita
muitakin funktionaalisia rajapintoja. Alla on esitetty niistä muutaman määritykset niin, että
rajapinnasta esitetään ainoastaan sen abstrakti funktio. Rajapinnoilla voi olla lisäksi esimerkiksi
oletusfunktioita.
// Rajapinta Predicate määrittää "testin" eli funktion, joka ottaa
// parametrin ja palauttaa sen perusteella totuusarvon true tai false.
public interface Predicate<T> {
boolean test(T t); // Funktio test tutkii, täyttääkö parametrina saatu olio t jonkin ehdon.
}
// Rajapinta Function määrittää funktion, joka ottaa yhden
// tyyppiä T olevan parametrin ja palauttaa tyyppiä R olevan arvon.
public interface Function<T,R> {
R apply(T t); // Funktio apply toteuttaa funktion logiikan.
}
// Rajapinta UnaryOperator määrittää funktion, joka ottaa yhden tyyppiä
// T olevan parametrin ja palauttaa samaa tyyppiä T olevan arvon.
// UnaryOperator perii rajapinnan Function, ja alla esitetty abstrakti
// funktio apply on tosiasiassa määritetty rajapinnassa Function.
public interface UnaryOperator<T> extends Function<T,T> {
T apply(T t); // Funktio apply toteuttaa funktion logiikan.
}
// Rajapinta BiFunction määrittää funktion, joka ottaa yhden tyyppiä T ja
// yhden tyyppiä U olevan parametrin ja palauttaa tyyppiä R olevan arvon.
public interface BiFunction<T,U,R> {
R apply(T t, U u); // Funktio apply toteuttaa funktion logiikan.
}
// Rajapinta BinaryOperator määrittää funktion, joka ottaa kaksi tyyppiä
// T olevaa parametria ja palauttaa samaa tyyppiä T olevan arvon.
// BinaryOperator perii rajapinnan BiFunction, ja alla esitetty abstrakti
// funktio apply on tosiasiassa määritetty rajapinnassa BiFunction.
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
T apply(T t, T u); // Funktio apply toteuttaa funktion logiikan.
}
// Rajapinta Consumer määrittää funktion, joka ottaa yhden tyyppiä T olevan
// parametrin eikä palauta arvoa (tietyssä mielessä "kuluttaa" saamansa parametrin).
public interface Consumer<T> {
void accept(T t); // Funktio accept toteuttaa funktion logiikan.
}
// Rajapinta BiConsumer määrittää funktion, joka ottaa yhden tyyppiä T ja yhden tyyppiä
// U olevan parametrin eikä palauta arvoa (tietyssä mielessä "kuluttaa" saamansa parametrit).
public interface BiConsumer<T>
void accept(T t, U u); // Funktio accept toteuttaa funktion logiikan.
}
// Rajapinta Supplier määrittää funktion, joka ei ota parametria ja palauttaa
// tyyppiä R olevan arvon (tietyssä mielessä "tuottaa" uuden arvon).
public interface Supplier<R>
R get(); // Funktio get toteuttaa funktion logiikan.
}
Kun toteutat funktion, joka ottaa parametrinaan jonkinlaisen funktio-olion, ei sen ottavan funktioparametrin tyypin määrittämiseksi tarvitse välttämättä määrittää omaa funktionaalista rajapintaa, jos jokin Javan valmiista funktionaalisista rajapinnoista sopii käyttötarkoitukseen (abstraktin funktion parametrit ja palautusarvo täsmäävät).
Alla on esimerkkinä funktionaalisen rajapinnan käytöstä funktio filter
, joka ottaa
parametrinaan listasäiliön list
sekä rajapinnan Predicate
toteuttavan funktio-olion
predicate
ja palauttaa ArrayList
-listan, jossa on mukana vain sellaiset listan list
alkiot t
, joille kutsu predicate.test(t)
palauttaa true
. Teknisenä detaljina
parametrin predicate
jokerimääritys sallii sellaisen predikaatin, joka osaa testata tyyppiä
T
tai sen jotain ylityyppiä olevan alkion.
// Staattinen geneerinen tyyppiparametrin T ottava funktio.
public static <T> ArrayList<T> filter(List<T> list, Predicate<? super T> predicate) {
ArrayList<T> result = new ArrayList<>();
for(T t : list) {
if(predicate.test(t)) { // Läpäiseekö alkio t testin predicate.test(t)?
result.add(t); // Jos läpäisi, lisätään t tuloslistaan.
}
}
return result;
}
Alla on esimerkki, kuinka edellä määritettyä filter
-funktiota voisi käyttää vaikkapa karsimaan
sanalistasta kaikki muut paitsi tietynpituiset (alla 5) sanat.
// Testausta varten lista sanoja, johon alempana sovelletaan filter-funktiota.
List<String> words = List.of("one", "two", "three", "four", "five", "six", "seven");
System.out.format("Original list: %s%n", words);
ArrayList<String> len5words = filter(words, w -> w.length() == 5); // Testi: onko pituus 5?
System.out.format("Filtered list: %s%n", len5words);
Tämä koodinpätkä tulostaisi:
Original list: [one, two, three, four, five, six, seven]
Filtered list: [three, seven]
Edellistä esimerkkifunktiota voisi laajentaa antamaan kutsujan määrittää jonkinlaisen muunnoksen,
joka tulokseen jätettäviin alkioihin kohdistetaan: siinä missä ylempänä alkio lisätään listaan
lausekkeella result.add(t)
, voitaisiin se nyt lisätä lausekkeella result.add(mapper.apply(t))
, missä
mapper
olisi käyttäjän välittämä toinen funktio-olio, jonka jäsenfunktio apply
suorittaa
alkion muunnoksen. Jos silmäilemme ylempänä kuvattuja Javan funktionaalisia rajapintoja, näyttäisi
rajapinta Function
sopivan tällaisen funktio-olion tyypiksi: sen funktio apply
ottaa yhden
parametrin ja palauttaa arvon. Alla on esitetty funktio filterMap
, joka toimii edellä
hahmotellulla tavalla. Tyyppiparametri T
on alkuperäisen listan alkioiden tyyppi ja R
on
palautettavan listan alkioiden tyyppi. Parametrin mapper
jokerimääritys ? extends R
kuvastaa sitä, että palautettavaan listaan on sinänsä laillista laittaa joko tyypin R
tai sen
jonkin alityypin olioita.
public static <T,R> ArrayList<R> filterMap(List<T> list, Predicate<? super T> predicate,
Function<? super T,? extends R> mapper) {
ArrayList<R> result = new ArrayList<>();
for(T t : list) {
if(predicate.test(t)) {
result.add(mapper.apply(t)); // Lisätään mapperin muuntama t tuloslistaan.
}
}
return result;
}
Tämän funktion avulla voisimme esimerkiksi poimia sanalistasta kaikki kokonaisulukuja esittävät
sanat ja palauttaa ne Integer
-listassa:
// Testausta varten lista sanoja, joista osa esittää kokonaislukuja.
List<String> words = List.of("one", "2", "three", "4", "five", "six", "7");
System.out.format("Original list: %s%n", words);
ArrayList<Integer> ints = filterMap(words, w -> { // Predicate, joka tutkii onko w luku.
try{
Integer.parseInt(w);
}
catch(NumberFormatException e){
return false;
}
return true;
},
w -> Integer.parseInt(w)); // Function, joka muuntaa w:n luvuksi.
System.out.format("Filtered list: %s%n", ints);
Tämä koodinpätkä tulostaisi:
Original list: [one, 2, three, 4, five, six, 7]
Filtered list: [2, 4, 7]