Lambda išraiškos – Java į naują lygį  

Taip pat siūlome susipažinti: Anotacijos Java kalboje  
Java 8: Optional prieš null  

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 pList = Arrays.asList(new Point(1,2), new Point(2,3));
// Iteracijos 
for (Iterator pItr = pList.iterator(); pItr.hasNext();) {
     p = ((Point)pItr.next());
     p.translate(1, 1);
     System.out.println ("Taskas: " + p.x + " " + p.y);
}
   
// Arba elegantiškiau
Iterator pItr = pList.iterator();
while (pItr.hasNext()) {
     p = ((Point)pItr.next());
     p.translate(-2, -2);
     System.out.println ("A Taskas: " + p.x + " " + p.y);
}

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 () {
      public void accept (Point p) {
          p.translate(10,10);
          System.out.println ("Consumer Taskas: " + p.x + " " + p.y);
       }
 });

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).
Kitas dalykas – perdengiamo metodo accept pavadinimas. Mat interfeisas teturi tik vieną metodą, o tai jau tapo įprasta praktika supaprastinti (tai vadinama funkciniu interfeisu arba SAM).
Ir pagaliau Consumer tipą taip pat įmanoma išgauti iš konteksto – juk forEach kviečiamas kintamajam pList, aprašytam kaip List<Point>, tai leidžia nuspėti, kad Consumer tipas yra Point, praleidžiant ir accept tipą.
Tad pažymėkime tas dalis, kurias galima ištraukti iš konteksto:

  pList.forEach(new Consumer () {
    public void accept (Point p) {
        p.translate(10,10);
        System.out.println ("Consumer Taskas: " + p.x + " " + p.y);
    }
     });

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:

  • Daugelis verslo logikos apima duomenų bazių tipo operacijas tokias kaip grupavimas pagal kategorijas (pvz., vegetariški patiekalai) ar brangiausio patiekalo paieška. SQL kalboje nereikia realizuoti filtravimo operacijos – ji jau yra joje (kaip WHERE sąlyga) – imi ir naudoji. Ar to nereiktų rinkiniams?
  • Kaip apdoroti didelės apimties rinkinius? Greitaveikos užtikrinimui reikia procesų lygiagretinimo, tačiau tai sunku realizuoti naudojant iteratorius! O dar sunkiau ieškoti klaidų...

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 intList = Arrays.asList(1, 2, 3, 4, 5, 6);
  List pList = new ArrayList<>(); 
  
  for (Integer k : intList) {
      pList.add (new Point(k % 3, k/2) );
  }
  
  Double maxDistance = Double.MIN_VALUE;
  
  for (Point p : pList) {
      maxDistance = Math.max(p.distance(0, 0), maxDistance);
  }
  
  System.out.println ("Maksimumas: " + maxDistance);

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 intList = Arrays.asList(1, 2, 3, 4, 5, 6);
  List pList = new ArrayList<>(); 
  
  for (Integer k : intList) {
     pList.add (new Point(k % 3, k/2) );
  }
  
Double maxDistance = Double.MIN_VALUE;
  
  for (Point p : pList) {
   maxDistance = Math.max(p.distance(0, 0), maxDistance);
  }

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.
Visos tos transformacijos sraute gali būti atliekamos izoliuotai – o tai nuostabiai tinka išlygiagretinimui.

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:
a) Srautas gali „ištekėti“ iš daugelio šaltinių: rinkinių, masyvų ar generuojančių funkcijų. Taip pavyzdyje iš mūsų pradinio sveikų skaičių rinkinių sugeneruojamas iniStream;
b) Toliau atliekami srauto veiksmai;
c) srautas turi baigtis terminatoriumi, kuris „suvirškina“ srautą ir gražina vieną reikšmę (mūsų atveju – max) arba nieką, perteikiamą tipu Optional arba jo specializacijomis (mūsų atveju – OptionalDouble);

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 comparer = (d1, d2) -> Double.compare(d1, d2);
    
 Comparator<Point> cmpbyY = (p1,  p2) -> Double.compare(yExtractor.apply(p1), yExtractor.apply(p2));

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.
Patikrinti pavyzdžius galite tik su Java 8 aplinka!

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 pList = Arrays.asList(new Point(1,2), new Point(2,3));
			
    //   Iteracijos	pvz.		
    //   iki versijos 1.5
    for (Iterator pItr = pList.iterator(); pItr.hasNext();) {
           p = ((Point)pItr.next());
           p.translate(1, 1);
          System.out.println ("v1: Koord. (x,y): " + p.x + ", " + p.y);
    }
			
    // Arba
    Iterator pItr = pList.iterator();
    while (pItr.hasNext()) {
	p = ((Point)pItr.next());
	p.translate(-2, -2);
	System.out.println ("v2: Koord. (x,y): " + p.x + ", " + p.y);
    }

    // su for... each – va taip:		
    for (Point pp : pList) {
	pp.translate(3, 3);
	System.out.println ("v3: Koord. (x,y): " + pp.x + ", " + pp.y);
    }											
			
    // Kai panaudojamas vidinis iteratorius:
    LabasLambda thisInstance = new LabasLambda();
	
    PointArrayList pAL = thisInstance.new PointArrayList();		 
    pAL.add(0, new Point(1,7));
    pAL.add(1, new Point(7,1));
    pAL.add(1, new Point(10,10));
		  
    System.out.println ("Pirmojo x: " + pAL.get(0).x);


    // su vidine anonimine klase
    pList.forEach(new Consumer () {
         public void accept (Point p) {
	 p.translate(10,10);
                System.out.println ("Consumer versija: Koord. (x,y): " + p.x + ", " + p.y);
         }
    });
		  
    // Kaip ir pAL:			
    pAL.forEach(new Consumer () {
	public void accept (Point p) {
	     p.translate(10,10);
 	     System.out.println ("Consumer versija kitam masyvui: Koord. (x,y):: " + p.x + ", " + p.y);
	 }
    });	  
		  
    // Galiausiai – Lambda versija 		  
    pList.forEach(pPoint -> PerformAction(pPoint));
		  
     System.out.println ("Sudie, Lambda");
 }
		
   static void PerformAction (Point p) {
	p.translate(100, 100);
	 System.out.println ("Lambda versija: Koord: " + p.x + ", " + p.y);
   }

   // demonstruoja papildomas galimybes
   class PointTranslate implements PointAction {
	 	 
	public void doForPoint(Point p, int x, int y) {
		p.translate(x, y);
		System.out.println ("FE Taskas: " + p.x + " " + p.y);
	}
    }

	 public class PointArrayList extends ArrayList {
		 
		 public PointArrayList() {
		    super();
		 }
		 
		 public void forEach() {
		     System.out.println ("Eina kiekvienam");
		 }
		 
		 public void forEach(PointTranslate t) {
		    for (Point p : this) {
			t.doForPoint(p, 0, 0);
		    }			
		 }
		 
		 public void forEach(PointTranslate t, int x, int y) {
		    for (Point p : this) {
			t.doForPoint(p, x, y);
		    }			
		 }		 
	 }
}

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 intList = Arrays.asList(6, 2, 3, 1, 5, 4);
       List pList = new ArrayList<>();	
		
       for (Integer k : intList) {
             pList.add (new Point(k % 2, k/2) );
       }
		
      Double maxDistance = Double.MIN_VALUE;
		
      for (Point p : pList) {
	   maxDistance = Math.max(p.distance(0, 0), maxDistance);
      }
		
      System.out.println ("Maksimumas: " + maxDistance);
      // -- Tiriamo fragmento pabaiga
		
      System.out.println ("Pateikimas naudojant srautus:");			
      Stream	iniStream = intList.stream();
      Stream ps = iniStream.map(k -> new Point(k % 2, k/2));
      DoubleStream dist = ps.mapToDouble(p -> p.distance(0, 0));

      OptionalDouble mdd = dist.max();
	 
      System.out.println ("Srauto maksimumas: " + mdd.getAsDouble());

      System.out.println ("Pateikimas srautais su Lambda");
	 
      OptionalDouble md = intList.stream()
 	 .map(k ->  new Point(k % 3, k/2))
	 .mapToDouble(p -> p.distance(0, 0))	
	 .max();
			
      System.out.println ("Srauto maksimumas naudojant Lambda: " + md.getAsDouble());	

      // -- Lygiagretus srautas
      md = intList.parallelStream()
	 .map(k ->  new Point(k % 3, k/2))
	 .mapToDouble(p -> p.distance(0, 0))	
	 .max();
			
      System.out.println ("Srauto maksimumas lygiagretinus: " + md.getAsDouble());

      // ------------------- Kompozicija		
      Comparator byX = new Comparator () {
	 public int compare (Point p1, Point p2) {
		return Double.compare(p1.x,  p2.x);
	 }
      };
	
      // pateikiam pradines x 	
      for (Point pp : pList) { System.out.println(pp.x); }
		
      pList.sort(byX);

      // pateikiam rezultato x
      for (Point pp : pList) { System.out.println(" sorted by x: " +pp.x); }
		
      // Function interfeiso panaudojimas (sekite straipsniu):		
      Comparator byXX = (p1,  p2) -> Double.compare(p1.x,  p2.x);
			
      Function xExtractor = p -> p.getX();
      Function yExtractor = p -> p.getY();
		
      // Comparator comparer = (d1, d2) -> Double.compare(d1, d2);				
      // Comparator cmpbyY = (p1,  p2) -> Double.compare(xExtractor.apply(p1),  xExtractor.apply(p2));
		
      Comparator cmpbyX = (p1,  p2) -> xExtractor.apply(p1).compareTo(xExtractor.apply(p2));
				
      Comparator comparebyX =  Comparator.comparing (p -> p.x);
		
      // pList.sort(cmpbyY);   for (Point pp : pList) { System.out.println(" sorted by y:" + pp.y); 
		
      pList.sort(comparebyX);
 		
      for (Point pp : pList) { System.out.println(" sorted by x:" + pp.x); }
		
      Comparator cmpDistance =  Comparator.comparing (p -> p.distance(0, 0));
				
      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 +")"));
				
      System.out.println ("Sudie, Lambda srautai");
   }
}

(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
Ruby on Rails
'Java' ir ne tik ji!
Didžiųjų duomenų mitas
Skriptai - ateities kalbos?
JavaScript pradžiamokslis
Anotacijos Java kalboje
Java 8: Optional prieš null
Programavimas Unix aplinkoje
Programavimo kalbų klegesys
Pitonas, kandantis sau uodegą!
Pirmasis „Java“ įskiepis Lietuvoje
AWK kalba - sena ir nuolat aktuali
Įvadas į Perl kalbą: Kas naudoja Perl?
Truputis magijos: perrašome parseInt funkciją
Vaizdi rašysena - VB Script
Programavimo kalbų istorija
Iš kur Javos tas lėtumas?
Dygios JavaScript eilutės
Unix komandinė eilutė
ASP patarimų liūnas
AdvancedHTML
Vartiklis