- Johdanto
- lisätietoja:
- Iteroitavissa striimattavaksi Java-kielellä
- miten If/else-logiikkaa käytetään Java 8-Streameissa
- lambdas Java 8: ssa
- funktionaaliset rajapinnat
- funktiot
- primitiivisten funktioiden erikoisuudet
- Kaksiariteettifunktioiden erikoistuminen
- toimittajat
- kuluttajat
- predikaatit
- operaattorit
- Legacy Functional Interfaces
- johtopäätös
Johdanto
Tämä opetusohjelma on opas eri funktionaalisiin rajapintoihin, joita esiintyy Java 8: ssa, sekä niiden yleisiin käyttötapauksiin ja käyttöön JDK-standardikirjastossa.
lisätietoja:
Iteroitavissa striimattavaksi Java-kielellä
miten If/else-logiikkaa käytetään Java 8-Streameissa
lambdas Java 8: ssa
Java 8 toi mukanaan voimakkaan uuden syntaktisen parannuksen lambda-ilmaisujen muodossa. Lambda on nimetön funktio, jonka voimme hoitaa ensiluokkaisena kielikansalaisena. Voimme esimerkiksi siirtää sen tai palauttaa sen menetelmästä.
ennen Java 8: aa loimme yleensä jokaiselle tapaukselle luokan, jossa piti kapseloida yksi toiminnallisuus. Tämä merkitsi paljon tarpeetonta boilerplate-koodia, jolla määriteltiin jotain, joka toimi alkeellisena funktioesityksenä.
artikkelissa ”Lambda Expressions and Functional Interfaces: Tips and Best Practices” kuvataan tarkemmin lambdasin kanssa työskentelyn toiminnallisia rajapintoja ja parhaita käytäntöjä. Tämä opas keskittyy joitakin erityisiä toiminnallisia rajapintoja, jotka ovat läsnä java.util.funktiopaketti.
funktionaaliset rajapinnat
on suositeltavaa, että kaikissa funktionaalisissa rajapinnoissa on informatiivinen @FunctionalInterface-merkintä. Tämä viestii selvästi rajapinnan tarkoituksen ja mahdollistaa myös kääntäjän luoda virheen, jos merkitty rajapinta ei täytä ehtoja.
mikä tahansa rajapinta, jossa on Sam(Single Abstract Method), on funktionaalinen rajapinta, ja sen toteutusta voidaan käsitellä lambda-lausekkeina.
huomaa, että Java 8: n oletusmenetelmät eivät ole abstrakteja eikä niitä lasketa; funktionaalisessa käyttöliittymässä voi silti olla useita oletusmenetelmiä. Voimme havaita tämän tarkastelemalla Funktion dokumentaatiota.
funktiot
Lambdan yksinkertaisin ja yleisin tapaus on funktionaalinen rajapinta menetelmällä, joka saa yhden arvon ja palauttaa toisen. Tätä yksittäisen argumentin funktiota edustaa Funktion rajapinta, jota parametrisoidaan sen argumentin tyyppien ja palautusarvon mukaan:
public interface Function<T, R> { … }
yksi Funktiotyypin käyttötavoista standardikirjastossa on kartta.computeIfAbsent menetelmä. Tämä menetelmä palauttaa kartan arvon avaimella, mutta laskee arvon, jos avainta ei jo ole kartassa. Arvon laskemiseen käytetään läpäistyä Funktion toteutusta:
Map<String, Integer> nameMap = new HashMap<>();Integer value = nameMap.computeIfAbsent("John", s -> s.length());
tässä tapauksessa lasketaan arvo soveltamalla funktiota avaimeen, laitetaan kartan sisään ja palautetaan myös menetelmäkutsusta. Voimme korvata Lambdan menetelmäviitteellä, joka vastaa hyväksyttyjä ja palautettuja arvotyyppejä.
muista, että objekti me vedota menetelmä on, itse asiassa, implisiittinen ensimmäinen argumentti menetelmän. Näin voimme valaa instanssin metodin pituuden viittauksen Funktion rajapintaan:
Integer value = nameMap.computeIfAbsent("John", String::length);
Funktion rajapinnassa on myös oletuskokoomusmenetelmä, jonka avulla voidaan yhdistää useita funktioita yhdeksi ja suorittaa ne peräkkäin:
Function<Integer, String> intToString = Object::toString;Function<String, String> quote = s -> "'" + s + "'";Function<Integer, String> quoteIntToString = quote.compose(intToString);assertEquals("'5'", quoteIntToString.apply(5));
quoteIntToString-funktio on yhdistelmä lainausfunktiosta, jota sovelletaan intToString-funktion tulokseen.
primitiivisten funktioiden erikoisuudet
koska primitiivinen tyyppi ei voi olla yleinen tyyppiargumentti, on funktioiden rajapinnasta olemassa versioita käytetyimmille primitiivisille tyypeille double, int, long, ja niiden yhdistelmille argumentti-ja palautustyypeissä:
- IntFunction, LongFunction, DoubleFunction: arguments are of specified type, return type is parameterized
- ToIntFunction, ToLongFunction, ToDoubleFunction: return type is of specified type, arguments are parameterized
- DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction: kun sekä argumentti-että palautustyyppi on määritelty primitiivisiksi tyypeiksi, kuten niiden nimet
esimerkkinä, ei ole olemassa out-of-the-box-funktionaalista rajapintaa funktiolle, joka ottaa lyhyen ja palauttaa tavun, mutta mikään ei estä meitä kirjoittamasta omaa:
@FunctionalInterfacepublic interface ShortToByteFunction { byte applyAsByte(short s);}
nyt voimme kirjoittaa menetelmän, joka muuttaa lyhyen joukon tavun joukoksi käyttäen Shorttobytefunktion määrittelemää sääntöä:
public byte transformArray(short array, ShortToByteFunction function) { byte transformedArray = new byte; for (int i = 0; i < array.length; i++) { transformedArray = function.applyAsByte(array); } return transformedArray;}
näin voisimme käyttää sitä muuntamaan sortsien joukon tavujen riviksi kerrottuna 2: lla:
short array = {(short) 1, (short) 2, (short) 3};byte transformedArray = transformArray(array, s -> (byte) (s * 2));byte expectedArray = {(byte) 2, (byte) 4, (byte) 6};assertArrayEquals(expectedArray, transformedArray);
Kaksiariteettifunktioiden erikoistuminen
lambdojen määrittelemiseksi kahdella argumentilla on käytettävä lisäliittymiä, joiden nimissä on ”Bi”-avainsana: Bifunktio, Todoublebunction, ToIntBiFunction ja Tolongbifunktio.
Bifunktiolla on sekä argumentit että palautustyyppi yleistettynä, kun taas Todoublebifunktiolla ja muilla voidaan palauttaa alkeisarvo.
yksi tyypillinen esimerkki tämän rajapinnan käytöstä STANDARDIRAJAPINNASSA on kartassa.replaceAll-menetelmä, joka mahdollistaa kartan kaikkien arvojen korvaamisen jollakin laskennallisella arvolla.
käytetään bifunktiototeutusta, joka saa avaimen ja vanhan arvon laskemaan palkalle uusi arvo ja palauttamaan se.
Map<String, Integer> salaries = new HashMap<>();salaries.put("John", 40000);salaries.put("Freddy", 30000);salaries.put("Samuel", 50000);salaries.replaceAll((name, oldValue) -> name.equals("Freddy") ? oldValue : oldValue + 10000);
toimittajat
toimittajan funktionaalinen rajapinta on jälleen yksi funktionaalinen erikoistuminen, joka ei vaadi argumentteja. Käytämme sitä yleensä laiskan arvonmuodostukseen. Esimerkiksi, let ’ s määritellä funktio, joka neliöt kaksinkertainen arvo. Se ei saa itse arvoa, vaan tämän arvon tarjoajan:
public double squareLazy(Supplier<Double> lazyValue) { return Math.pow(lazyValue.get(), 2);}
Tämä antaa meille mahdollisuuden laiskasti luoda argumentin tämän funktion kutsumiselle käyttäen Toimittajatoteutusta. Tästä voi olla hyötyä, jos argumentin synty vie huomattavan paljon aikaa. Simuloimme, että käyttämällä Guavan sleepUninterruptibly method:
Supplier<Double> lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d;};Double valueSquared = squareLazy(lazyValue);
toinen käyttötapaus toimittajalle on sekvenssin generoinnin logiikan määrittely. Sen osoittamiseksi käytetään staattista virtaa.luo menetelmä Fibonaccin lukujen virran luomiseksi:
int fibs = {0, 1};Stream<Integer> fibonacci = Stream.generate(() -> { int result = fibs; int fib3 = fibs + fibs; fibs = fibs; fibs = fib3; return result;});
funktio, jonka siirrämme virtaan.generate-menetelmä toteuttaa toimittajan toiminnallisen rajapinnan. Huomaa, että ollakseen hyödyllinen generaattorina, toimittaja tarvitsee yleensä jonkinlaisen ulkoisen tilan. Tällöin sen tila käsittää kaksi viimeistä Fibonaccin järjestyslukua.
toteuttaaksemme tämän tilan, käytämme matriisia parin muuttujan sijaan, koska kaikkien Lambdan sisällä käytettyjen ulkoisten muuttujien on oltava käytännössä lopullisia.
muita Supplier-toiminnallisen rajapinnan erikoisaloja ovat Boolean Supplier, DoubleSupplier, LongSupplier ja IntSupplier, joiden palautustyypit ovat vastaavia primitiivejä.
kuluttajat
toisin kuin toimittaja kuluttaja hyväksyy yleistetyn väitteen eikä palauta mitään. Se on toiminto, joka edustaa sivuvaikutuksia.
esimerkiksi tervehditään kaikkia nimilistalla tulostamalla tervehdys konsoliin. Lambda siirtyi listalle.forEach-menetelmä toteuttaa kuluttajan funktionaalisen rajapinnan:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");names.forEach(name -> System.out.println("Hello, " + name));
kuluttajasta on olemassa myös erikoistuneita versioita — DoubleConsumer, IntConsumer ja LongConsumer — jotka saavat argumentteina alkeellisia arvoja. Mielenkiintoisempi on BiConsumer käyttöliittymä. Yksi sen käyttötapauksista iteroidaan kartan merkintöjen kautta:
Map<String, Integer> ages = new HashMap<>();ages.put("John", 25);ages.put("Freddy", 24);ages.put("Samuel", 30);ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));
toinen erikoistuneiden BiConsumer-versioiden joukko koostuu Objdoubleconsumerista, Objintconsumerista ja Obslongconsumerista, jotka saavat kaksi argumenttia; toinen argumenteista on yleistetty ja toinen on alkeellinen tyyppi.
predikaatit
matemaattisessa logiikassa predikaatti on funktio, joka saa arvon ja palauttaa Boolen arvon.
Predikaattifunktionaalinen rajapinta on sellaisen Funktion erikoistuminen, joka saa yleistetyn arvon ja palauttaa Boolen. Tyypillinen predikaatti Lambdan käyttötapaus on suodattaa arvokokoelma:
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");List<String> namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());
yllä olevassa koodissa suodatetaan lista Stream API: n avulla ja säilytetään vain Nimet, jotka alkavat kirjaimella ”A”. Predikaattitoteutus kiteyttää suodatuslogiikan.
kuten kaikissa edellisissä esimerkeissä, tästä funktiosta on olemassa intpredicate -, DoublePredicate-ja LongPredicate-versioita, jotka saavat alkeellisia arvoja.
operaattorit
Operaattoriliitännät ovat erikoistapauksia, joissa funktio saa ja palauttaa saman arvotyypin. UnaryOperator-rajapinta saa yhden argumentin. Yksi sen käyttötapauksista kokoelmien API: ssa on korvata kaikki luettelon arvot joillakin samantyyppisillä laskennallisilla arvoilla:
List<String> names = Arrays.asList("bob", "josh", "megan");names.replaceAll(name -> name.toUpperCase());
luettelo.replafeall-funktio palauttaa mitättömäksi, koska se korvaa käytössä olevat arvot. Jotta luettelon arvojen muuttamiseen käytetty lambda sopisi tarkoitukseen, sen on palautettava sama tulostyyppi kuin se saa. Siksi UnaryOperator on hyödyllinen tässä.
nimen sijasta -> nimi.toppercase (), Voimme käyttää metodiviittausta:
names.replaceAll(String::toUpperCase);
yksi mielenkiintoisimmista Binäärioperaattorin käyttötapauksista on pelkistysoperaatio. Oletetaan, että haluamme koota kokoelma kokonaislukuja summa kaikkien arvojen. Stream API: n avulla voisimme tehdä tämän kerääjän avulla, mutta yleisempi tapa olisi käyttää pelkistysmenetelmää:
List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2);
pelkistysmenetelmä saa alkuvaraajan arvon ja Binaryoperaattorifunktion. Tämän funktion argumentit ovat saman tyypin arvopari; funktio itsessään sisältää myös logiikan, jolla ne voidaan yhdistää yhdeksi saman tyyppiseksi arvoksi. Ohitettavan funktion tulee olla assosiatiivinen eli arvon aggregaation järjestyksellä ei ole merkitystä, eli seuraavan ehdon tulee pitää:
op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
Binäärioperaattorioperaattorifunktion assosiatiivinen ominaisuus mahdollistaa reduktioprosessin helpon paralleloinnin.
on tietenkin myös erikoisaloja UnaryOperator ja BinaryOperator, joita voidaan käyttää alkeellisilla arvoilla, eli DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator ja LongBinaryOperator.
Legacy Functional Interfaces
kaikki funktionaaliset rajapinnat eivät ilmestyneet Javan 8. Monet rajapinnat aiemmista versioista Java noudattaa rajoitteita FunctionalInterface, ja voimme käyttää niitä lambdas. Merkittäviä esimerkkejä ovat ajettavat ja soitettavat rajapinnat, joita käytetään samanaikaisissa sovellusliittymissä. Java 8: ssa nämä rajapinnat merkitään myös @FunctionalInterface-merkinnällä. Näin voidaan yksinkertaistaa huomattavasti rinnakkaiskoodia:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));thread.start();