Tämä kurssi on jo päättynyt.

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]

Javassa funktionaalinen rajapinta tarkoittaa

Funktio-olio on

Javan luokkakirjastossa on määritetty funktionaalisena rajapintana

Palautusta lähetetään...