Lambda išraiškos Java į naują lygį Taip pat siūlome susipažinti: Anotacijos Java kalboje Java 8 yra vienas didesnių Java kalbos išvystymų. O vienu svarbių Java 8 išplėtimų buvo lambda išraiškų įtraukimas į kalbą. Šiame puslapyje pateiksime neformalų įvadą į šią koncepciją, parodydami, kaip jos leidžia padidinti spartą bei parašyti vaizdesnį kodą. Nuo išorinės prie vidinės iteracijos Paimkime paprastą kodą, operuojantį su java.awt.Point objektų rinkiniu. Jį prabėgti iki Java 5 galėjome tokiu būdu: List Kaip matote rinkinio element perinkimui naudojamas Iterator objektas. Toks būdas išlikęs iki šiol ir į jį kompiliatorius kompiliuoja for each ciklus. Iterator objektas yra atsakingas už elemento išrinkimo tvarką, pvz., jis išrenka ArrayList elementus nosekliai. Tad nieko nekeičia, jei dabar galime užrašyti taip: for (Point pp : pList) { pp.translate(3, 3); System.out.println ("T Taskas: " + pp.x + " " + pp.y); } Tai kur problema? Java Collections Framework įdiegus 1998 m. atrodė protinga tokiu būdu leisti paimti elementus. Kas nuo tada pasikeitė? Atsakymas yra aparatinės dalies vystymęsi. Kompiuteriai jau senokai turi po kelis procesorius, tačiau nuo 1998 m. iki keliais branduoliais (dual core) procesorių pasirodymo 2005 m. įvyko daug revoliucinių pokyčių mikroschemose. Dėl daugelio priežasčių sustojo 40 m. trukęs eksponentinis procesorių spartėjimas. Ir nors tapo neįmanoma pagalinti iki 6 GHz, tačiau galima pagaminti procesorių su dviem branduoliais po 3 GHz ir ši tendencija tebesitęsia: Java 8 pasirodymo 2014 m. kovą metu vyravo 4-ių branduolių procesoriai, gausėjo ir 8-ių branduolių procesorių. Programuotojams reikėjo patogių galimybių išnaudoti lygiagrečius skaičiavimus. Vidinės iteracijos ir lambda išraiškos Iteracijos ne tik yra reikalaujančios daug skaičiavimų, bet ir labai svarbios. Mums svarbu, kad rinkiniai turėtų metodą ką, t.y. kokius veiksmus atlikti su kiekvienu rinkinio elementu tokio metodo akivaizdus pavadinimas yra forEach, tačiau iki Java 8 java.util.List neturėjo tokio metodo. Naujasis Collection.forEach metodas yra vidinės iteracijos pavyzdys. Kai klasė nėra labai didelė, paprastesnis panaudojimo būdas yra apibrėžti anoniminę vidinę klasę, pvz., pList.forEach(new Consumer Atrodo pernelyg daug žodžių? Turim pabandyti nustatyti tas vietas, kur mes perduodame
informaciją kompiliatoriui, nors jisai tai galėtų išgauti iš konteksto. Vienas tokių dalykų yra perduodamas
interfeiso pavadinimas kompiliatoriui pakaktų žinoti, kad forEach parametro tipas yra
Consumer<T> (T kažkoks tipas). pList.forEach(new Consumer Tam, kad atskirtume parametrą nuo veiksmų, reikalingas sintaksės pakeitimas įvedant skirtuką ->, tad ankstesnį pavyzdį galime perrašyti taip: static void PerformAction (Point p) { p.translate(100, 100); System.out.println ("Lambda Taskas: " + p.x + " " + p.y); } pList.forEach(pLambda -> PerformAction(pLambda)); Nuo rinkinių prie srautų Rinkiniai yra labai naudojami Java programose beveik kiekviena jų naudoja ir abdoroja rinkinius, kurie yra daygelio uždavinių pagrindas, nes leidžia grupuoti ir apdoroti duomenis. Kaip pavyzdį paimkime patiekalų meniu, kuriame skaičiuosime jų kalorijas. Tačiau darbas su rinkiniais toli gražu nėra tobulas:
Geresniam darbui su rinkiniais buvo pasiūlytos papildomos bibliotekos. Pvz, Google sukurta Guava leido papildomas konteinerio klases (multižemėlapiai ir multiaibės). Panašias galimybes siūlė ir Apache Commons Collections. O galiausiai M. Fusco lambdaj suteikė nemažai priemonių rinkinių manipuliavimui deklaratyviu stiliumi, įkvėptu funkcionalinio programavimo būdo. Ir štai dabar Java 8 atėjo su nuosava deklaratyvaus pobūdžio biblioteka. Tad ir pažiūrėkime į naujas idiomas, kurias naudoti įgalina lambda išraiškos. Pradžioje kiek modifikuokime mūsų pavyzdį. Jame iš sveikų skaičių rinkinio pagal kai kurias taisykles sudaromas taškų rinkinys, o tada randamas jų atstumų nuo koordinačių pradžios maksimumas. List Atrodo normaliai, tačiau priekabiai pažiūrėjus pastebėsime nemalonių momentų. Pirma, tai daugžodžiavimas (9 eilutės operacijoms). Antra, pList tereikia saugoti tik laikinai. Trečia, turim prielaidą, kad tuščio sąrašo maksimalus atstumas lygus Double.MIN_VALUE. Tačiau didžiausia spraga yra tarp programuotojo ketinimo ir būdo, kaip tas ketinimas išreikštas kode. Kad suprastume šį fragmentą, turime išsiaiškinti, kaip jis veikia, tada bandyti atspėti programuotojo ketinimą (na, jei esate laimės kūdikis, gal programuotojas jūsų pasigailėjo ir parašė komentarus) ir tik tada galite patikrinti ketinimo realizacijos teisingumą. Visa tai lėtas ir palankus klaidų padarymui procesas o juk aukšto lygio kalbų tikslas buvo supaprastinti kodą ir geriau parodyti kūrėjo minties eigą! Tad vėl sužymėsime kodo dalis, nereikalingas mūsų tikslo pasiekimui: List Gausime naują, į duomenis orientuotą požiūrį, kuris atrodo įprastai, jei esate susipažinęs su Unix
konvejeriais ir filtrais: galime nagrinėti procesą, kaip pradinio rinkinio sveiko skaičiaus reikšmė virsta
tašku, o šis atstumo reikšme (double). Jį galima pavaizduoti diagrama. Nuo veiksmo prie
veiksmo perduodamos reikšmių sekos jos vadinamos srautais. Srautai skiriasi nuo rinkinių tuo, kad nenumato atminties tų reikšmių saugojimui (tai judantys duomenys). Java 8 perteikia srautus java.util.stream interfeisais: Stream bendroms reikšmėms, o IntStream, LongStream ir DoubleStream primityviems tipams. Veiksmai sraute perteikia operaciją, kuri vadinama map - ji transformuoja kiekvieną srauto elementą pagal savo taisykles. Tačiau srauto operacijos gali ir pertvarkyti, pašalinti ar įterpti reikšmes, t.y. kaip paties srauto transformaciją. Tad veiksmą galime įsivaizduoti kaip tarpinę operaciją, ne tik apibrėžtą sraute, bet ir gražinančią srautą kaip išėjimą. Tad mūsų pradinį pavyzdį perrašius srautų notacija, gausime: List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6); Stream<Integer> iniStream = intList.stream(); Stream<Point> ps = iniStream.map(k -> new Point(k % 3, k/2)); DoubleStream dist = ps.mapToDouble(p -> p.distance(0, 0)); OptionalDouble md = dist.max(); System.out.println ("Srauto maksimumas: " + md.getAsDouble()); Trumpi paaiškinimai: O dabar jį supaprastinam į galutinį pavidalą: List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6); OptionalDouble md = intList.stream() .map(k -> new Point(k % 3, k/2)) .mapToDouble(p -> p.distance(0, 0)) .max(); System.out.println ("Srauto maksimumas: " + md.getAsDouble()); Šis stilius dažnai vadinamas srauniu, nes kodas srūva. Jo struktūra atspindi pagrindines operacijas, o ne jas paslepia. Be to, pasiekiamas ir produktyvumas srautų kodas yra beveik dukart spartesnis už iteraciją. Vykdant su dideliais duomenų kiekiais ir išlygiagretinus pasiekiama puiki sparta. Nuo nuoseklaus prie lygiagretaus Čia liambda išraiškos taip pat vaidina lemiamą vaidmenį. Tarkim, dideliam duomenų kiekiui norime juos padalinti į dvi dalis ir atlikti veiksmus su kiekviena puse atskirai, o tada gautus rezultatus apjungti. Tą procesą galima tęsti rekursyviai tol, kol imtys taps pakankamai mažos. Tam mūsų ankstesniam pavyzdžiui tereikia tik vieno pakeitimo: List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6); OptionalDouble md = intList.parallelStream() .map(k -> new Point(k % 3, k/2)) .mapToDouble(p -> p.distance(0, 0)) .max(); System.out.println ("Srauto maksimumas: " + md.getAsDouble()); Kompozicinė elgsena Lambda išraiškos panašios į funkcijas, tad natūralu paklausti, ar jos gali elgtis kaip funkcijos. Toks perspektyvos pakeitimas drąsintų mus dirbti labiau su elgsena nei su objektais. Pvz., esminė funkcijų operacija yra kompozicija: iš dviejų funkcijų sudaryti trečią. Tarkim, norime surūšiuoti taškų sąrašą pagal x koordinatę. Standartinis Java būdas yra sukurti Comparator: Comparator<Point> byX = new Comparator<Point> () { public int compare (Point p1, Point p2) { return Double.compare(p1.x, p2.x); } }; pList.sort(byX); Pakeisdami anoniminės vidinės klasės aprašą lambda funkcija tai galim perrašyti: Comparator<Point> byX = (p1, p2) -> Double.compare(p1.x, p2.x); Tačiau tai nepadėjo su kita svarbia problema Comparator yra monolitinis. Jei norėsime rūšiuoti ne pagal x, o pagal y teks kopijuoti visą šį kodą keičiant x į y. reikia turėti galimybę sukurti parametrizuotą versiją. Ir štai java.util.function randame interfeisą Function: public interface Function<T,R> { public R apply T (T t); } Tai leidžia mums parašyti lambda išraiškas tiek rakto gavimui, tiek palyginimui ir naujoji versija būtų: Function<Point, Double> yExtractor = p -> p.getY(); Comparator Tačiau padidindami lankstumą praradome aiškumą. Bet pastebėjus, kad ekstraktorius perteikia natūralią lyginamųjų tvarką, kodą galime perrašyti į: Function<Point, Double> yExtractor = p -> p.getY(); Comparator<Point> cmpbyX = (p1, p2) -< xExtractor.apply(p1).compareTo(xExtractor.apply(p2)); Pastebėjus šio atvejo svarbą, Comparator buvo papildytas statiniu metodu comparing - pateikus ekstraktorių jis sukuria atitinkamą Comparator, panaudodamas natūralią lyginamųjų tvarką. Jis leidžia mums perrašyti kodą taip: Comparator<Point> comparebyX = Comparator.comparing (p -> p.x); pList.sort(comparebyX); Kad pamatytumėm šio pagerinimo naudą, tarkim, kad mūsų uždavinys kiek pasikeitė: vietoje suradimo toliausiai nuo pradžios nutolusio taško atstumą, turim atspausdinti visus taškus atstumo didėjimo tvarka. Tokiu atveju lyginimas aprašomas taip: Comparator<Point> cmpDistance = Comparator.comparing (p -> p.distance(0, 0)); Tad srauto konvejeris naujai užduočiai persirašytų taip: intList.stream() .map(k -> new Point(k % 3, k/2)) .sorted(Comparator.comparing (p -> p.distance(0, 0))) .forEach (p -> System.out.println("(x,y): (" + p.x + "," + p.y +")")); (c) 2016, Vartiklis. Visos teisės saugoma. Leidžiama naudoti tik asmeninės savišvietos tikslais. Bet koks platinimasbet kokiomis priemonemis, viso teksto arba atskiros jo dalies, draudžiamas! Priedai Pateiksime pilnus demonstacinius pradinio kodo tekstus, iliustruojančius aptartus klausimus. Labas, Lambda! import java.awt.Point; import java.util.Arrays; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.function.Consumer; public class LabasLambda { public interface PointAction { void doForPoint(Point p, int x, int z); } public static void main(String[] args) { Point p; System.out.println ("Labas, Lambda"); List Labas, Lambda srautai! import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.OptionalDouble; import java.util.function.Function; import java.util.stream.DoubleStream; import java.util.stream.IntStream; import java.util.stream.Stream; public class LambdaStream { public static void main(String[] args) { System.out.println ("Labas, Lambda srautai"); // -- Tiriamas pradinis pavyzdys List (c) 2016, Vartiklis. Visos teisės saugomos. Leidžiama naudoti tik asmeninės savišvietos tikslais. Bet koks platinimas bet kokiomis priemonemis, viso teksto arba atskiros jo dalies, draudžiamas!
Tiesiog - Java | |