Upload
others
View
10
Download
0
Embed Size (px)
Citation preview
Haladó Java programozás
Haladó Java programozás
• Java 5 • Generikusok • (Enumok)
• Java 7 • Sztringek a switchben • try-with-resources
• Java 8 • Alapértelmezett interfészmetódusok • Lambda-kifejezések • Funkcionális interfészek • Streamek • Egyebek
Generikusok a Java nyelvben Bővebb információ:
http://javarevisited.blogspot.sg/2011/09/generics-java-example-tutorial.html
http://docs.oracle.com/javase/tutorial/java/generics/
http://thegreyblog.blogspot.hu/2011/03/java-generics-tutorial-part-i-basics.html
Generikusok
• Célja: fordítási idejű típusbiztonság • Futásidejű ClassCastException-ök megelőzése
• Típuserózió (type erasure): • A típusinformáció csak fordítási időben érhető el, a bájtkódba nem kerül be
• Értelemszerűen futásidőben sem érhető el • Pl.: List<String> esetén a bájtkódba már csak a List nyers típus (raw type)
kerül • Szükség esetén típuskényszerítést is belefordít a kódba
• Típuskövetkeztetés/típuslevezetés (type inference, Java 7 óta): • A Java fordító azon képessége, hogy a metódushívások és a hozzájuk tartozó
deklarációk alapján meghatározza az(oka)t a típusparaméter(eke)t, amelyek lehetővé teszik a hívást
• A legspecifikusabb ilyen típust határozza meg
Jelölésrendszer, elnevezési konvenciók Generikus fogalom Jelentés
Set<E> Generikus típus, E a formális típusparameter
Set<Integer> Parametrizált típus, Integer az aktuális típusparaméter
<T extends Comparable> Korlátozott típusparaméter (bounded type parameter)
<T super Comparable> Korlátozott típusparaméter (bounded type parameter)
Set<?> Korlátozatlan típushelyettesítő (unbounded wildcard)
<? extends T> Korlátozott típushelyettesítő (bounded wildcard type)
<? super T> Korlátozott típushelyettesítő (bounded wildcard type)
Set Nyers típus (raw type)
<T extends Comparable<T>> Rekurzív típuskorlát (recursive type bound)
T – típus E – elem K – kulcs V - érték N – szám
Típushelyettesítők (wildcards)
• Korlátozott (bounded wildcard): • <? extends T>
• A típushelyettesítők kovariánsak a felső korlátjukra nézve
• <? super T> • A típushelyettesítők kontravariánsak az alsó korlátjukra nézve
• Korlátozatlan (unbounded wildcard): • <?>
• Ekvivalens a <? extends Object>-tel
• A Set<T> parametrizált típus a Set nyers típus altípusa: Set setNyersTípusú = new HashSet<String>();
setNyersTípusú = new HashSet<Integer>();
• A Set<Object> bármilyen elemet tartalmazhat: Set<Object> setTetszőlegesTípusú = new HashSet<Object>();
setTetszőlegesTípusú.add("abc"); //legális
setTetszőlegesTípusú.add(new Float(3.0f)); //legális
setTetszőlegesTípusú = new HashSet<Integer>(); //illegális
• Set<?> ismeretlen típust jelöl, az alábbiak legálisak: Set<?> setIsmeretlenTípusú = new LinkedHashSet<String>();
setIsmeretlenTípusú = new LinkedHashSet<Integer>();
• A parametrizált típusok a típusok szintjén követik az öröklődési szabályokat:
Set<String> setOfString = new HashSet<String>(); //legális
setOfString = new LinkedHashSet<String>(); //legális
De ez nem igaz a típusparaméterekre! Pl. Set<Object>-nek nem adhatunk Set<String>-et!
• A Set<? extends Number> bármilyen elemet tartalmazhat:
Set<? extends Number> setTetszőlegesSzámTípusú = new
HashSet<Integer>(); //legális, mert Integer extends Number
setTetszőlegesSzámTípusú = new HashSet<Float>(); //legális
• Osztályliterálban nem használhatunk generikusokat
List.class //legális
List<Integer>.class //fordítási hiba
• A Set<? super TreeMap> TreeMap objektumokat, vagy a TreeMap bármely ősének objektumait tartalmazhatja:
Set<? super TreeMap> setTreeMapSzupertípusú = new LinkedHashSet<TreeMap>(); //legális
setTreeMapSzupertípusú = new HashSet<SortedMap>(); //legális, mivel SortedMap a TreeMap szuperosztálya
setTreeMapSzupertípusú = new LinkedHashSet<Map>(); //legális, mivel Map a TreeMap szupertípusa
• Generikus metódusok írásakor a típusparaméter deklarációjának helye a metódus szignatúrájában a módosítók és a visszatérési típus között van, pl.:
public static <T> T identical(T source){
return source;
}
Fogalmak
Egy programozási nyelv típusrendszerében egy szabály vagy típuskonstruktor
• kovariáns, ha megőrzi a típusok specifikusabbtól általánosabb felé történő rendezettségét;
• kontravariáns, ha megfordítja a sorrendet;
• invariáns, ha a fentiek egyike sem igaz.
• Forrás: Andrey Tyukin http://stackoverflow.com/a/19739576
Kovariancia • Javában a tömbök kovariánsak
• Egy T[] elemei T-be, vagy T valamely altípusába tartozóak lehetnek
Number[] numbers = new Number[3];
numbers[0] = new Integer(10);
numbers[1] = new Double(3.14);
numbers[2] = new Byte(0);
• S[] altípusa T[]-nek, ha S altípusa T-nek, vagyis legális a következő:
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
• Mi történik?
myNumber[0] = 3.14; //ArrayStoreException nem ellenőrzött kivétel
Kovariancia List<? extends Number> covariantList = new ArrayList<Integer>();
• Olyan elemek listája, amelyek típusa Number vagy annak valamely leszármazottja
• Olvashatunk • mivel bármi, ami a listában van, felkényszeríthető Number-ré
Number n = covariantList.get(0); //legális
• Az alábbi fordítási hibát ad:
Integer i = covariantList.get(0); //fordítási hiba
• De nem írhatunk! • mert a fordító nem tudja, hogy milyen tényleges objektum szerepel az általános szerkezetben
• ez a Number bármely leszármazottja – pl. Integer, Double, Long – lehet
covariantList.add(45L); //fordítási hiba
covariantList.add(123); //szintén
Kontravariancia List<? super Number> contravariantList = new ArrayList<Object>();
• Olyan elemek listája, amelyek típusa Number vagy valamely őse
• Írhatunk • az általánosabb struktúrába bármit tehetünk contravariantList.add(100); //legális
contravariantList.add(43L); //legális
contravariantList.add(3.14); //legális
contravariantList.add(o); //fordítási hiba
• De nem olvashatunk bármit! • Biztonságosan csak Object-et! Integer i = contravariantList.get(0); //fordítási hiba
Number n = contravariantList.get(0); //fordítási hiba
Object o = contravariantList.get(0); //legális
Típushelyettesítési „kocka”
"Java wildcard subtyping" by Vilhelm.s - Own work. Licensed under CC BY-SA 3.0 via Wikimedia Commons - http://commons.wikimedia.org/wiki/File:Java_wildcard_subtyping.svg#mediaviewer/File:Java_wildcard_subtyping.svg
PECS: Producer extends, consumer super
• Másnéven get and put principle
• Használjunk extends típushelyettesítőt, ha csak kiveszünk értékeket egy struktúrából
• Használjunk super típushelyettesítőt, ha csak beteszünk értékeket egy struktúrába
• Ne használjunk típushelyettesítőt, ha mindkettőt végezzük
• Példa: T típusú elemek kollekciója esetén (Collection<T>) • Ha végigmegyünk egy kollekción, és valamit akarunk tenni az elemeivel
• Ekkor a kollekció a termelő (producer), ezért célszerű Collection<? extends T>-t használni, hiszen így T bármely altípusának objektumát is beletehetjük
• Ha pakolni szeretnénk a kollekcióba • Ekkor a kollekció a fogyasztó (consumer), ezért célszerű Collection<? super T>-t
használni, hiszen nem érdekel, mi van a kollekcióban, mindaddig, amíg T típusú elemeket hozzáadhatok
Példa extends típushelyettesítő használatára
public static double sum(Collection<? extends Number> nums) {
double s = 0.0;
for (Number num : nums)
s += num.doubleValue();
return s;
}
List<Integer> ints = Arrays.asList(1,2,3); //sum(ints) == 6.0;
List<Double> doubles = Arrays.asList(2.78,3.14); //sum(doubles) == 5.92;
List<Number> nums = Arrays.<Number>asList(1,2,2.78,3.14); //sum(nums) == 8.92;
Példa super típushelyettesítő használatára
public static void count(Collection<? super Integer> ints, int n) {
for (int i = 0; i < n; i++)
ints.add(i);
}
List<Integer> ints = new ArrayList<Integer>();
count(ints, 5); //[0, 1, 2, 3, 4]
List<Number> nums = new ArrayList<Number>();
count(nums, 5); //[0, 1, 2, 3, 4]
nums.add(5.0); //[0, 1, 2, 3, 4, 5.0]
List<Object> objs = new ArrayList<Object>();
count(objs, 5); //[0, 1, 2, 3, 4]
objs.add("öt"); //[0, 1, 2, 3, 4, öt]
Mikor ne használjuk egyiket sem?
public static double sumCount(Collection<Number> nums, int n) {
count(nums, n);
return sum(nums);
}
Legjobb gyakorlatok generikusok használatához
• Kerüljük a nyerstípusokat! • Használjunk generikus típusokat, parametrizált osztályokat és metódusokat!
• Részesítsük előnyben a kollekcióosztályokat a tömbökkel szemben!
• Használjunk korlátozott típusparamétereket! • Így tudunk rugalmas API-t készíteni
• A @SuppressWarnings("unchecked") annotációt a lehető legszűkebb körben használjuk (ha egyáltalán)!
• Pl. egy egész metódus annotálása helyett annotáljunk csupán egy sort
• Konvertáljuk régi kódjaink nyerstípusú osztályait generikussá! • Robusztusabbá válik a kódunk
Sztringek a switch-ben Bővebb információ:
http://www.theserverside.com/tutorial/The-Switch-to-Java-7-Whats-New-with-Conditional-Switches
http://docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html
public static String getNapTípusa(String hétNapja) { String napTípusa; if (hétNapja == null) throw new IllegalArgumentException("Érvénytelen nap: " + hétNapja); switch (hétNapja) { case "Hétfő": napTípusa = "Munkahét eleje"; break; case "Kedd": case "Szerda": case "Csütörtök": napTípusa = "Hét közepe"; break; case "Péntek": napTípusa = "Munkahét vége"; break; case "Szombat": case "Vasárnap": napTípusa = "Hétvége"; break; default: throw new IllegalArgumentException("Érvénytelen nap: " + hétNapja); } return napTípusa; }
Sztringek a switch-ben
• Könnyebben olvasható, mint az egymásba ágyazott if–else–if-ek • A generált bájtkód is hatékonyabb
• A sztringek természetesen érzékenyek a kis- és nagybetűk közötti különbségre!
• Az értékvizsgálat az equals metódus segítségével történik • Biztosítsuk, hogy ne keletkezzen NullPointerException!
• A case címkék konstans kifejezések lehetnek
• A szelektor statikus típusa String kell, hogy legyen • Nem elég dinamikusan annak lenni!
try–with–resources Bővebb információ:
http://tutorials.jenkov.com/java-exception-handling/try-with-resources.html
http://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
Erőforráskezelés régen…
private static void printFile() throws IOException { InputStream input = null; try { input = new FileInputStream("file.txt"); int data = input.read(); while (data != -1) { System.out.print((char) data); data = input.read(); } } finally { if (input != null) input.close(); } }
Hol következhet be kivétel?
Ezt külön try-ba kellene tenni
…és most (Java 7 óta)
private static void printFileJava7() throws IOException { try (FileInputStream input = new FileInputStream("file.txt")) { int data = input.read(); while (data != -1) { System.out.print((char) data); data = input.read(); } } }
try–with–resources
• Biztosítja, hogy az utasításban deklarált erőforrások lezárása megtörténik
• Ehhez az erőforrásnak implementálnia kell a java.lang.AutoCloseable interfészt
• Több erőforrás is kezelhető:
private static void printFileJava7() throws IOException { try (FileInputStream input = new FileInputStream("file.txt"); BufferedInputStream bufferedInput = new BufferedInputStream(input)) { int data = bufferedInput.read(); while (data != -1) { System.out.print((char) data); data = bufferedInput.read(); } } }
Alapértelmezett interfészmetódusok
Problémák interfészekkel
• Létező interfészek evolúciója • Mi történik, ha egy interfészt egy új metódussal kellene bővíteni?
• Pl. a kollekcióinterfészeket kiegészítenék egy-egy stream(), parallelStream() ill. forEach() metódussal
• Minden olyan osztályban, amely implementálja az interfészt, implementálnunk kell a metódust! • Ha nem tesszük, osztályaink le sem fordulnak
• Különösen problémás 3rd party alkalmazások esetén
• Nem elég rugalmas tervezés • Konkrét osztályok közötti funkcionalitás-megosztás absztrakt osztályokkal lehetséges
• Az egyszeres öröklődés korlátokat szabott
• Számos esetben ún. adapter osztályt kellett létrehozni azért, hogy ne kelljen minden osztályban az interfész összes metódusát implementálni, példák (részletesebben ld. később):
• SAX-feldolgozás: ContentHandler stb. interfészek vs. DefaultHandler osztály
• Swing: MouseListener stb. interfészek vs. MouseAdapter osztály
• Ezen osztályok nagy technikai segítséget adtak, de nehezebben olvashatóvá tették a kódot azáltal, hogy elrejtették, hogy mely interfészek megvalósítása történik
Megoldás: alapértelmezett metódusok!
• Alternatív nevek: defender methods, virtual extension methods
• Alapértelmezett metódus nem lehet olyan szignatúrájú, amely az Object osztály valamely nem final módosítójú metódusáéval megegyezik
• Fordítási hiba jár érte, ha megpróbáljuk
• Az alapértelmezett metódust az interfészben adjuk meg, de az implementáló osztályok felüldefiniálhatják
• Nincs probléma az implementáló osztályokban evolúció esetén • Nincs szükség felesleges osztályokra
• Bináris kompatibilitás a régi interfésztől függő osztályokkal
• Szükséges elemek: • default kulcsszó • Alapértelmezett implementációt tartalmazó blokk
• Hátrány: az interfész szennyeződik oda nem illő dolgokkal (implementáció)
Példa
public interface Addressable { String getStreet(); String getCity(); default String getFullAddress() { return getStreet() + ", " + getCity(); } }
public class Letter implements Addressable { private String street, city; public Letter(String street, String city) { this.street = street; this.city = city; } @Override public String getCity() { return city; } @Override public String getStreet() { return street; } @Override public String getFullAddress() { return city() + ", " + street; } public static void main(String[] args) { Letter l = new Letter("123 AnyStreet", "AnyCity"); System.out.println(l.getFullAddress()); } }
123 AnyStreet ,
AnyCity
Lehetőségek alapértelmezett metódussal rendelkező interfészből származtatáskor • Nem teszünk említést az alapértelmezett metódusról
• Ekkor a leszármazott örökli azt
• Újradeklaráljuk az alapértelmezett metódust • Ez abstract-tá teszi
• Felüldefiniáljuk az alapértelmezett metódust • Vagyis új törzset rendelünk hozzá
Lambda-kifejezések Bővebb információ:
http://javarevisited.blogspot.hu/2014/02/10-example-of-lambda-expressions-in-java8.html
http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
http://www.slideshare.net/langer4711/lambdas-funct-progjdays2013
http://www.slideshare.net/martyhall/lambda-expressions-and-streams-in-java-8-presented-by-marty-hall-to-google-inc
http://www.drdobbs.com/jvm/lambda-expressions-in-java-8/240166764
Előzmények – Beágyazott osztály (nested class)
• Statikus beágyazott osztály (static nested class)
• Belső osztály (inner class) • Lokális osztály (local class)
• Névtelen osztály (anonymous class)
Beágyazott osztály
• Egy osztályon belül deklarált másik osztály • Mind a négyféle láthatóság jelen lehet (public, protected, csomag szintű, private)
• Külső szinten szereplő osztályok csak public vagy csomag szintű láthatósággal bírhatnak!
• Befoglaló/tartalmazó osztályának (enclosing class) tagja
• Statikus • Terminológia: statikus beágyazott osztály (static nested class) • Nem férnek hozzá befoglaló osztályuk példány szintű tagjaihoz!
• Nem statikus • Terminológia: belső osztály (inner class) • A befoglaló osztály példány szintű tagjait még akkor is eléri, ha azok private
láthatóságúak!
Beágyazott osztályok haszna
• Logikailag csoportosíthatjuk a csak egyetlen helyen használt osztályokat • Ha egy osztály csak egyetlen más osztály számára hasznos, akkor logikus, hogy
együtt tartsuk a kettőt.
• Növeli a bezárást • A és B legfelső szintű osztályok esetén, ha B-nek el kell érnie A-beli tagokat,
akkor A-ban ezek nem lehetnek privátok. Ha B-t beágyazzuk A-ba, akkor A tagjai úgy lehetnek privátak, hogy B eléri őket. Ráadásul B-t is elrejthetjük a világ elől.
• A kód könnyebben olvasható és karbantartható lesz
Lambda-kifejezések
• Egy funkcionális interfészt megvalósító névtelen osztály egy objektumát adhatjuk meg
new ValamilyenInterfész() {
@Override
public ValamilyenTípus valamilyenMetódus(paraméterek) {
törzs
}
}
Helyette ezt írjuk:
(paraméterek) -> {törzs}
{
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
Lambda-kifejezések
• Kerek zárójelek között megadott, egymástól vesszővel elválasztott formális paraméterek
• A paraméter típusa elmaradhat • Ha csak egy paraméter van, akkor a zárójelpár is elmaradhat • Ha egy sincs, akkor viszont kötelező kitenni az üres zárójelpárt!
• A nyíl token
• Egy törzs, amely egyetlen kifejezést vagy egy blokkot tartalmaz • Kifejezés megadása esetén az kiértékelődik • Használhatunk return utasítást is
• Mivel ez nem kifejezés, ezért blokkba helyezzük
• A void metódusok hívását nem kötelező blokkba tenni, az alábbi szabályos: email -> System.out.println(email)
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
Példa több formális paraméterű lambda-kifejezésre public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.println("40 + 2 = " + myApp.operateBinary(40, 2, addition));
System.out.println("20 - 10 = " + myApp.operateBinary(20, 10, subtraction));
}
}
Láthatóság
• Láthatóság szempontjából a lambda-kifejezések nem vezetnek be újabb szintet („lexically scoped”)
• Épp ezért nincs lyuk sem a hatáskörben
• A benne lévő esetleges deklarációkat is úgy értjük, mintha azok a befoglaló környezetben lennének
• Nem változtathatják meg egy lokális változó értékét (de egy példányváltozójét igen!)
• final vagy effektív final
Funkcionális interfészek Bővebb információ:
http://winterbe.com/posts/2014/03/16/java-8-tutorial/
Funkcionális interfész
• Másnéven: SAM (Single Abstract Method) interfész
• Minden lambda megfelel valamely típusnak, amit egy interfész ad meg
• Egy funkcionális interfész pontosan egy absztraktmetódus-deklarációt tartalmaz
• Mindaddig, amíg ezt biztosítjuk, tetszőleges interfészeket használhatunk lambda-kifejezésként
• Ennek biztosítására vezették be a @FunctionalInterface annotációtípust
• Ennek hatására a fordító fordítási hibát dob, ha egytől eltérő számú absztraktmetódus-deklaráció
Példa
@FunctionalInterface public interface Converter<F, T> { T convert(F from); }
Converter<String, Integer> converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123
Metódus- és konstruktorreferenciák
• Anélkül hivatkozhatunk egy metódusra, hogy ténylegesen meghívnánk • A példányosítás és a tömblétrehozás is ide tartozik
• ::
• Példák:
String::length // példánymetódus
System::currentTimeMillis // statikus metódus
List<String>::size // explicit generikustípus-paraméter
List::size // kikövetkeztetett generikustípus-paraméter
System.out::println
"abc"::length
super::toString
ArrayList::new
int[]::new
Példa metódusreferenciára public class Something { public String startsWith(String s) { return String.valueOf(s.charAt(0)); } public static void main(String[] args) { Something something = new Something(); Converter<String, String> converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" } }
Példa konstruktorreferenciára
• Tekintsük az alábbi osztályt, két konstruktorral: class Person { String firstName, lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
• Készítsünk egy factory interfészt: interface PersonFactory<P extends Person> { P create(String firstName, String lastName); }
• A fordító a create szignatúrája alapján automatikusan kiválasztja a megfelelő konstruktort: PersonFactory<Person> personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");
Beépített funkcionális interfészek
• Számos régi interfész is megkapta a @FunctionalInterface annotációt, pl. Runnable, Comparator
• Újak is megjelentek (java.util.function csomag): • Predicate
• Function
• Supplier
• Consumer
Predicate • A predikátumok egyparaméteres logikai függvények
• Funkcionális metódus: test(…) – kiértékeli a predikátumot az adott paraméterrel
• Alapértelmezett metódusai: and(…), or(…), negate()
Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
Function
• Egyparaméteres függvények, amelyek meghatároznak egy értéket
• Funkcionális metódus: apply(…) – alkalmazza a függvényt a paraméterére
• Alapértelmezett metódusai: • compose(before): előbb a paramétereként kapott (before) függvényt, majd ezt
alkalmazza
• andThen(after): előbb ezt a függvényt, majd a paramétereként kapottat (after) alkalmazza
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString =
toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
Supplier
• Egy valamilyen eredményeket biztosító (szolgáltató) objektumot ír le
• Funkcionális metódus: get() – megadja az eredményt
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
Consumer
• Egyparaméteres, eredmény nélküli művelet
• A többi funkcionális interfésztől eltérően mellékhatással rendelkezik
• Funkcionális metódus: accept(…) – végrehajtja a műveletet paraméterére
• Alapértelmezett metódus: • andThen(after): egy összetett Consumer-t határoz meg, amelyet úgy kapunk,
hogy ezen Consumer után végrehajtjuk after-t is
Consumer<Person> greeter = (p) -> System.out.println("Hello, " +
p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
Streamek Bővebb információ:
http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/
http://www.oracle.com/technetwork/articles/java/ma14-java-se-8-streams-2177646.html
http://www.slideshare.net/martyhall/lambda-expressions-and-streams-in-java-8-presented-by-marty-hall-to-google-inc
http://java67.blogspot.hu/2014/04/java-8-stream-api-examples-filter-map.html
Mik a streamek?
• A streamek monádok • A funkcionális programozásban monád alatt egy olyan struktúrát értünk, amely
lépéssorozatként megadott számításokat reprezentál. Egy monád szerkezetű típus definiálja, hogy mit jelent a típus műveleteinek láncolása, vagy függvényeinek beágyazása.
• Egy stream egy elemsorozatot reprezentál, és különféle műveleteket biztosít az elemeken végzett számítások érdekében
• Nincs mögöttük tárterület, vagyis nem kollekciók!
• A műveletek lehetnek köztesek (intermediate) vagy terminálisak (terminal) • Fluent API: a stream műveletek streameket adnak vissza
• Legtöbbjük egy lambda-kifejezést kaphat paraméterül • Ez adja meg a művelet pontos viselkedését
Köztes műveletek • Streamet adnak vissza, így pontosvessző nélkül láncolhatjuk egymáshz a
köztes műveleteket
• Példa köztes műveletekre: • filter: egy szűrő alkalmazásával új streamet hoz létre a régi alapján
• map: egy új streamet hoz létre a régi elemeinek leképezése alapján
• sorted: létrehoz egy új streamet a régi elemeinek rendezésével
• Teljes lista: http://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
• Csővezetéket alkotnak (operation pipeline) List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1"); myList.stream() .filter(s -> s.startsWith("c")) .map(String::toUpperCase) .sorted() .forEach(System.out::println);
Terminális műveletek
• Vagy void, vagy nem streamet visszaadó
• Pl.: forEach vagy reduce
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1"); myList.stream() .filter(s -> s.startsWith("c")) .map(String::toUpperCase) .sorted() .forEach(System.out::println); Eredmény: C1 C2
Műveletek kívánatos jellemzői
• Mellékhatásmentesség (non-interfering operation): nem módosítja a stream adatforrását
• Pl.: egyetlen lambda-kifejezés sem változtatja meg myList-et
• Állapotmentesség (stateless operation): a műveletvégrehajtás determinisztikus
• Pl.: egyetlen lambda-kifejezés sem függ olyan változótól (állapottól), amely a végrehajtás során megváltozhat
Streamtípusok
• A streameket különféle adatforrások (különösen kollekciók) alapján hozhatjuk létre
• A java.util.Collection új metódusai: • stream(): szekvenciális stream létrehozására
• parallelStream(): párhuzamos (több szálon futni képes) stream létrehozására
Szekvenciális streamek létrehozása
• stream() • Objektumok sima streamjét adja vissza
• of() • Objektumreferenciákból állít elő streamet
• range() • Primitív típusok streamjeinek (IntStream, LongStream, FloatStream, …)
inicializálására
Stream.of("a1", "a2", "a3") .findFirst() .ifPresent(System.out::println); // a1
IntStream.range(1, 4) .forEach(System.out::println); // 1 // 2 // 3
Primitív streamek
• Hasonlóak a hagyományoshoz, néhány különbséggel: • Specializált lambda-kifejezéseket használnak
• Pl. IntFunction-t Function helyett, IntPredicate-et Predicate helyett, stb.
• További terminális aggregáló műveletei vannak • sum() és average()
• Konverzió oda-vissza van (mapToInt, mapToLong, mapToDouble)
Arrays.stream(new int[] {1, 2, 3}) .map(n -> 2 * n + 1) .average() .ifPresent(System.out::println); // 5.0
Stream.of("a1", "a2", "a3") .map(s -> s.substring(1)) .mapToInt(Integer::parseInt) .max() .ifPresent(System.out::println); // 3
IntStream.range(1, 4) .mapToObj(i -> "a" + i) .forEach(System.out::println); // a1 // a2 // a3
Stream.of(1.0, 2.0, 3.0) .mapToInt(Double::intValue) .mapToObj(i -> "a" + i) .forEach(System.out::println); // a1 // a2 // a3
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; }) .forEach(s -> System.out.println("forEach: " + s));
Feldolgozási sorrend
• A köztes műveletek lusta kiértékelésűek, vagyis csak akkor hajtódnak végre, ha van terminális művelet
• Példa, ami nem csinál semmit:
• Kiegészítve egy terminálissal:
• Eredmény:
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; });
filter: d2 forEach: d2 filter: a2 forEach: a2 filter: b1 forEach: b1 filter: b3 forEach: b3 filter: c forEach: c
A végrehajtás nem horizontálisan, hanem vertikálisan történik!
A végrehajtás nem horizontálisan, hanem vertikálisan történik! • Ez csökkentheti az elemeken végrehajtandó műveletek számát
Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .anyMatch(s -> { System.out.println("anyMatch: " + s); return s.startsWith("A"); }); // map: d2 // anyMatch: D2 // map: a2 // anyMatch: A2
Az anyMatch igazat ad vissza, mihelyst a predikátuma alkalmazható a megadott input elemre (2. elem). A vertikális végrehajtás miatt a map csak kétszer kerül hívásra.
A sorrend számít? Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("A"); }) .forEach(s -> System.out.println("forEach: "+s));
// map: d2 // filter: D2 // map: a2 // filter: A2 // forEach: A2 // map: b1 // filter: B1 // map: b3 // filter: B3 // map: c // filter: C
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s));
// filter: d2 // filter: a2 // map: a2 // forEach: A2 // filter: b1 // filter: b3 // filter: c
A sorrend számít!
sorted
• Állapotőrző (stateful) köztes művelet • Ahhoz, hogy rendezzen egy elemsorozatot, nyilván kell tartania az állapotot Stream.of("d2", "a2", "b1", "b3", "c") .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s));
// sort: a2; d2 // sort: b1; a2 // sort: b1; d2 // sort: b1; a2 // sort: b3; b1 // sort: b3; d2 // sort: c; b3 // sort: c; d2 // filter: a2 // map: a2 // forEach: A2 // filter: b1 // filter: b3 // filter: c // filter: d2
Itt a sorted horizontálisan (a teljes inputra) végrehajtódik!
Javított változat
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s));
// filter: d2 // filter: a2 // filter: b1 // filter: b3 // filter: c // map: a2 // forEach: A2
Itt a sorted sosem hívódik meg, mert a filter egyeleműre szűkít!
Streamek újrafelhasználása
• Alaphelyzetben nem lehetséges • Egy terminális művelet meghívásakor a stream bezárul
• Megoldás: minden végrehajtani kívánt terminális művelethez hozzunk létre egy új streamláncot
• Minden get() hívás létrehoz egy új streamet
Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); stream.anyMatch(s -> true); // ok stream.noneMatch(s -> true); // kivétel (IllegalStateException)
Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("a")); streamSupplier.get().anyMatch(s -> true); // ok streamSupplier.get().noneMatch(s -> true); // ok
class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name; } } List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
collect
• Terminális művelet, amellyel egy stream elemeit különféle formára hozhatjuk • Pl. List, Set, Map, stb.
• Kap egy Collector-t, amely négy műveletből áll: • Egy supplier
• Egy accumulator
• Egy combiner
• Egy finisher
• Van sok beépített Collector
collect példák List<Person> filtered = persons .stream() .filter(p -> p.name.startsWith("P")) .collect(Collectors.toList()); System.out.println(filtered); // [Peter, Pamela]
Map<Integer, List<Person>> personsByAge = persons .stream() .collect(Collectors.groupingBy(p -> p.age)); personsByAge .forEach((age, p) -> System.out.format("age %s: %s\n", age, p)); // age 18: [Max] // age 23: [Peter, Pamela] // age 12: [David]
IntSummaryStatistics ageSummary = persons.stream().collect( Collectors.summarizingInt(p -> p.age)); System.out.println(ageSummary); // IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}
Saját Collector
• Fűzzük össze az összes személy nevét egy | (pipe) karakterrel Collector<Person, StringJoiner, String> personNameCollector = Collector.of(() -> new StringJoiner(" | "), // supplier (j, p) -> j.add(p.name.toUpperCase()), // accumulator (j1, j2) -> j1.merge(j2), // combiner StringJoiner::toString); // finisher String names = persons.stream().collect(personNameCollector); System.out.println(names); // MAX | PETER | PAMELA | DAVID
reduce
• Egy stream összes elemét egyetlen elemre redukálja
• Három változata van • Elemek streamjét a stream pontosan egy elemére redukálja
• Pl. ki a legidősebb személy?
• Egy egységelem és egy asszociatív BinaryOperator accumulator alapján redukál
• Egy egységelem, egy BiFunction accumulator és egy BinaryOperator típusú combiner függvény alapján redukál
• Gyakran ennél egyszerűbb explicit módon kombinálni a map-et és a reduce-t
reduce példák
persons .stream() .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2) .ifPresent(System.out::println); // Pamela
Ki a legidősebb?
Person result = persons .stream() .reduce(new Person("", 0), (p1, p2) -> { p1.age += p2.age; p1.name += p2.name; return p1; }); System.out.format("name=%s; age=%s", result.name, result.age); // name=MaxPeterPamelaDavid; age=76}
Hozzunk létre egy olyan Person objektumot, amely a stream objektumainak neveit és életkorát aggregálja!
reduce példák Mennyi a személyek összéletkora?
Integer ageSum = persons .stream() .reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2); System.out.println(ageSum); // 76
Integer ageSum = persons.stream().reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s\n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2); return sum1 + sum2; }); // accumulator: sum=0; person=Max // accumulator: sum=18; person=Peter // accumulator: sum=41; person=Pamela // accumulator: sum=64; person=David
• Egy kis debugolás: • Mit látunk?
• A combiner nem futott le!
peek
System.out.println("összeg: " + IntStream.range(1, 11) .map(i -> i + 5) .peek(i -> System.out.print(i + " ")) .reduce(0, (i, j) -> i + j) ); // 6 7 8 9 10 11 12 13 14 15 összeg: 105
• Köztes művelet
Párhuzamosítsunk! Integer ageSum = persons .parallelStream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s\n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2); return sum1 + sum2; }); // accumulator: sum=0; person=Pamela // accumulator: sum=0; person=David // accumulator: sum=0; person=Max // accumulator: sum=0; person=Peter // combiner: sum1=18; sum2=23 // combiner: sum1=23; sum2=12 // combiner: sum1=41; sum2=35
Párhuzamos streamek
• Futásidejű teljesítmény növelése céljából
• Egy ún. ForkJoinPool-t használnak, amely legfeljebb 5 szálra bont
• A következő JVM paraméterrel változtatható: -Djava.util.concurrent.ForkJoinPool.common.parallelism=5
• Kétféleképpen juthatunk hozzá: • A kollekciók parallelStream() metódusával • A parallel() köztes metódussal egy szekvenciális streamet
párhuzamosíthatunk
ForkJoinPool commonPool = ForkJoinPool.commonPool(); System.out.println(commonPool.getParallelism()); // 3
Példa Arrays.asList("a1", "a2", "b1", "c2", "c1") .parallelStream() .filter(s -> { System.out.format("filter: %s [%s]\n", s, Thread.currentThread().getName()); return true; }) .map(s -> { System.out.format("map: %s [%s]\n", s, Thread.currentThread().getName()); return s.toUpperCase(); }) .forEach( s -> System.out.format("forEach: %s [%s]\n", s, Thread.currentThread().getName()));
filter: b1 [main] map: b1 [main] filter: a2 [ForkJoinPool.commonPool-worker-1] filter: a1 [ForkJoinPool.commonPool-worker-2] forEach: B1 [main] filter: c1 [ForkJoinPool.commonPool-worker-3] map: c1 [ForkJoinPool.commonPool-worker-3] forEach: C1 [ForkJoinPool.commonPool-worker-3] filter: c2 [main] map: c2 [main] forEach: C2 [main] map: a1 [ForkJoinPool.commonPool-worker-2] forEach: A1 [ForkJoinPool.commonPool-worker-2] map: a2 [ForkJoinPool.commonPool-worker-1] forEach: A2 [ForkJoinPool.commonPool-worker-1]
Aggregált műveletek és iterátorok
• Az aggregált műveletek (pl. forEach) hasonlónak tűnhetnek az iterátorokhoz
• Van azonban néhány alapvető különbség: • Belső iterációt használnak
• Nincs next-hez hasonló metódusuk
• Belső delegációval az alkalmazásunk mondja meg, melyik kollekción iterálunk, de a JDK mondja meg, hogyan
• Egy stream elemeit dolgozzák fel • Nem pedig közvetlenül a kollekcióét. Épp ezért hívjuk őket stream műveleteknek
• A viselkedést paraméterek formájában adhatjuk meg • A legtöbb aggregált művelet lambda-kifejezéssel paraméterezhető, ezáltal testre szabható
Egyéb újdonságok A teljesség igénye nélkül, természetesen
java.util.Optional
• Egy olyan konténerobjektum, amely vagy tartalmaz null értéket, vagy nem
• Java 8 óta
• Főbb metódusok: • static empty(): visszaad egy üres Optional példányt
• static of(value): visszaad egy megadott value értékkel rendelkező Optional-t
• static ofNullable(value): ha value nem null, ezt adja vissza Optional-ként, különben pedig egy üres Optional-t
• get(): ha az Optional-ben van érték, azt adja meg, különben NoSuchElementException
• ifPresent(): igazat ad, ha az Optional-ben van érték, különben hamisat
• orElse(other): ha van benne érték, azt adja vissza, különben other-t
Mire jó az Optional?
• Rákényszerítjük a hívót, hogy gondoskodjon arról az esetről, ha egy adott objektum nem létezik
• Ezáltal kevesebb NPE (NullPointerException) következik be
• Minden olyan függvénynek Optional visszatérési típussal kellene rendelkeznie, amelyről elképzelhető, hogy nem ad vissza értéket
Hogyan használjuk?
public Optional<Student> findStudentByNeptunId(String neptunId) {
…
}
Optional<Student> optional = findStudentByNeptunId(neptunId);
if (optional.isPresent()) {
Student st = optional.get();
/* használhatjuk a Student objektumot */
}
else {
/* törődjünk azzal az esettel, amikor nem található */
}
java.util.StringJoiner
• Egy-egy opcionális prefix és suffix között megadott elhatárolójel segítségével összeállított karaktersorozatot hoz létre
• Collectors.joining
int[] t = new int[] {1,2,3,4,5,6,7,8,9,10}; StringJoiner sj = new StringJoiner(", ", "[", "]"); for (int i : t) sj.add(i + ""); // String.valueOf(i); System.out.println(sj); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
System.out.println(Arrays.stream(t) .mapToObj(i -> i + "") // .mapToObj(String::valueOf) .collect(Collectors.joining(", ", "[", "]"))); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Streams és lambda gyakorlás
• https://github.com/JavaCodeKata/stream-lambda
• https://github.com/AdoptOpenJDK/lambda-tutorial
• http://technologyconversations.com/2014/10/16/java-tutorial-through-katas/
• https://github.com/vfarcic/java-8-exercises