Programovací jazyk Java Novinky jazyka Java 8 Programovací jazyk Java
Java SE 8 Vydána 18. 3. 2014 Přináší mj.: funkční rozhraní, lambda výrazy, datové proudy, další možnosti.
Funkční rozhraní (Function interfaces) Rozhraní deklarující právě jednu abstraktní metodu (tj. vyžadující implementaci právě jedné metody). Ve standardní knihovně taková rozhraní již byla, ale doposud byla především jednoúčelová (Comparable, Runnable) Knihovna Javy 8 přináší balíček java.util.function, definující celou řadu obecných funkčních rozhraní.
Funkční rozhraní v balíčku java.util.function Metody rozhraní ze skupiny Consumer jsou čistí konzumenti. Zpracují své parametry, ale nic nevrací. Consumer<T> void accept(T value) DoubleConsumer void accept(double value) IntConsumer void accept(int value) LongConsumer void accept(long value) BiConsumer<T, U> void accept(T t, U value) ObjDoubleConsumer void accept(T t, double value) ObjIntConsumer void accept(T t, int value) ObjLongConsumer void accept(T t, long value)
Funkční rozhraní v balíčku java.util.function Metody rozhraní ze skupiny Supplier jsou naopak čistí producenti. Nemají parametry a vracejí hodnotu zadaného typu. Supplier<T> T get() BooleanSupplier boolean getAsBoolean() DoubleSupplier double getAsDouble() IntSupplier int getAsInt() LongSupplier long getAsLong() Metody rozhraní ze skupiny Function jsou klasické funkce: zpracují svůj parametr a vrátí funkční hodnotu. Function<T, R> R apply(T value) DoubleFunction<R> R apply(double value) IntFunction<R> R apply(int value) LongFunction<R> R apply(long value)
Funkční rozhraní v balíčku java.util.function Další rozhraní ze skupiny Function: ToIntFunction<T> int applyAsInt (T value) ToDoubleFunction<T> double applyAsDouble(T value) ToLongFunction<T> long applyAsLong (T value) DoubleToIntFunction int applyAsInt (double value) DoubleToLongFunction long applyAsLong (double value) IntToDoubleFunction double applyAsDouble(int value) IntToLongFunction long applyAsLong (int value) LongToDoubleFunction double applyAsDouble(long value) LongToIntFunction int applyAsInt (long value) BiFunction <T,U,R> R apply (T t, U u) ToDoubleBiFunction<T,U,R> double applyAsDouble(T t,U u) ToIntBiFunction <T,U,R> int applyAsInt (T t, U u) ToLongBiFunction <T,U,R> long applyAsLong (T t, U u)
Funkční rozhraní v balíčku java.util.function Metody rozhraní ze skupiny UnaryOperator představují unární operátory. Jejich návratová hodnota je stejného typu jako jejich parametr. UnaryOperator<T> T apply (T operand) DoubleUnaryOperator double applyAsDouble(double operand) IntUnaryOperator int applyAsInt (int operand) LongUnaryOperator long applyAsLong (long operand) Metody rozhraní ze skupiny BinaryOperator představují binární operátory. Mají dva parametry, přičemž typy obou parametrů i funkční hodnoty jsou shodné. BinaryOperator<T> T apply (T left, T right) DoubleBinaryOperator double applyAsDouble(double left, double right) IntBinaryOperator int applyAsInt (int left, int right) LongBinaryOperator long applyAsLong (long left, long right)
Funkční rozhraní v balíčku java.util.function Metody rozhraní ze skupiny Predicate slouží k získání logické hodnoty odvozené z hodnoty jejich parametru. Predicate<T> boolean test(T value) DoublePredicate boolean test(double value) IntPredicate boolean test(int value) LongPredicate boolean test(long value) BiPredicate<T,U> boolean test(T t, U u)
Anotace @FunctionalInterface Označíme-li rozhraní touto anotací, bude překladač kontrolovat, zda toto rozhraní obsahuje opravdu právě jednu abstraktní metodu. Neabstraktních metod může mít libovolné množství.
Lambda výrazy Představují způsob, jak definovat jako objekt nějakou část kódu, kterou bychom v jiné části programu rádi použili. Lambda výraz můžeme uložit do proměnné funkčního typu a předávat metodám jako hodnotu parametru funkčního typu. Překladač definuje lambda výraz jako instanci funkčního rozhraní, jehož metoda má odpovídající parametry a vrací hodnotu odpovídajícího typu (převod na příslušný typ zařídí překladač).
Lambda výrazy Lambda výraz zapisujeme některým z následujících způsobů: (parametry) -> { příkazy } // Obecný tvar parametr -> { příkazy } // Jediný parametr (parametry) -> výraz // Tělo tvoří pouze vyhodnocovaný výraz parametr -> výraz // Jediný parametr + pouze 1 výraz Je-li vlevo jediný parametr, nemusí se dávat do kulatých závorek. Není-li žádný, nebo je-li jich více, závorky jsou potřeba. Dokáže-li si překladač odvodit typ parametru, nemusí se uvádět. Pokud se vpravo pouze vyhodnocuje nějaký výraz, nemusí se dávat do složených závorek.
Lambda výrazy @FunctionalInterface public interface MojeFunkce { public int počítej(int vstup); } Funkci implementující rozhraní MojeFunkce můžeme vytvořit několika způsoby: MojeFunkce naDruhou = (int i) -> { return i * i; }; MojeFunkce naDruhou = (i) -> i * i; MojeFunkce naDruhou = i -> i * i;
Lambda výrazy K vytvoření funkce z předchozího snímku můžeme použít i připravené funkční rozhraní IntFunction: IntFunction<Integer> naDruhou = i -> i * i; Další příklad: IntFunction<Double> odmocnina = i -> Math.sqrt(i); Příklady použití: int mocnina = naDruhou.apply(5); // 25 double o = odmocnina.apply(9); // 3
Odkazy na metody a konstruktory Pomocí dvou dvojteček (::) se můžeme odkazovat na metody nebo konstruktory a konvertovat je na funkce, resp. funkční rozhraní. Namísto Supplier<Long> dodavatel = () -> System.currentTimeMillis(); napíšeme Supplier<Long> dodavatel = System::currentTimeMillis; a v obou případech použijeme např. takto: long čas = dodavatel.get();
Odkazy na metody a konstruktory Další příklady použití dvou dvojteček: Pro metody instance: Date datum = new Date(); Supplier<Long> dodavatel = datum::getTime; long čas = dodavatel.get(); Pro konstruktory: Function<Long, Date> generátorData = Date::new; Date datum = generátorData.apply(System.currentTimeMillis()); System.out.println(datum); Metoda nebo konstruktor musí mít samozřejmě vhodný počet a typ parametrů, které odpovídají požadovanému funkčnímu rozhraní. Výše byl zavolán konstruktor public Date(long date).
Výchozí metody rozhraní V Javě 8 můžou rozhraní obsahovat i neabstraktní metody, tedy metody, obsahující implementaci, ne jen hlavičku. Pro označení těchto metod se používá klíčové slovo default. Třída, která implementuje toto rozhraní, tak získá i tuto funkcionalitu – jedná se tak v podstatě o vícenásobnou dědičnost. Užitečné je to mj. v souvislosti s funkčními rozhraními a lambdami. Pomocí lambda výrazu definujeme určitou funkcionalitu, ale koncový uživatel daného rozhraní může volat jinou metodu (výchozí, definovanou v rozhraní), která tu metodu z lambdy obaluje, případně dělá něco úplně jiného (např. skládá více funkčních rozhraní dohromady – viz dále).
Funkční rozhraní Function z balíčku java.util.function Přehled metod: Modifikátor a typ Metoda a popis default <V> Function<V,R> compose(Function<? super V,? extends T> before) Vrátí složenou funkci, která na svůj vstup nejprve aplikuje funkci before a pak aplikuje na výsledek funkci předanou jako argument. default <V> Function<T,V> andThen(Function<? super R,? extends V> after) Vrátí složenou funkci. Nejprve se provede funkce předaná jako argument, na výsledek se aplikuje funkce after. Oproti compose volá tedy funkce v opačném pořadí. R apply(T t) Aplikuje funkci na zadaný argument.
Funkční rozhraní Function z balíčku java.util.function Ukázky výchozích metod: Function<Integer, Integer> přičtiJedna = a -> a + 1; Function<Integer, Integer> vynásobDvěma = a -> a * 2; int x = vynásobDvěma.compose(přičtiJedna).apply(5); // (5 + 1) × 2 = 12 int y = přičtiJedna.compose(vynásobDvěma).apply(5); // (5 × 2) + 1 = 11 int z = vynásobDvěma.andThen(přičtiJedna).apply(5); // (5 × 2) + 1 = 11
Predikáty Predikát je zde funkce, jejímž oborem hodnot je boolean. Definiční obor je daný generickým typem a může to být cokoli. Pomocí metody Predicate.test(T t) vyhodnocujeme pravdivost predikátu nad zadanou hodnotu. Vytvoření predikátů: Predicate<String> neníNull = Objects::nonNull; Predicate<String> správnáDélka = s -> s.matches(".{8,32}"); Predicate<String> obsahujeČíslici = s -> s.matches(".*[0-9].*"); Predicate<String> obsahujeVelkéPísmeno = s -> s.matches(".*[A-Z].*");
Predikáty Použití predikátů: neníNull.test(„abc"); // vrátí true neníNull.test(null); // vrátí false správnáDélka.test("abcdefghij"); // vrátí true správnáDélka.test("abc"); // vrátí false obsahujeČíslici.test("ab7c")); // vrátí true obsahujeČíslici.test("abc")); // vrátí false
Predikáty Stejně jako funkce – i predikáty můžeme díky výchozím metodám skládat: Predicate<String> složenýPredikát = neníNull.and(správnáDélka).and(obsahujeČíslici.or (obsahujeVelkéPísmeno)); a složený predikát vyhodnotit: boolean a = složenýPredikát.test("Abcd123"); // = false boolean b = složenýPredikát.test("abcdefgh"); // = false boolean c = složenýPredikát.test("abcdefgh123"); // = true boolean d = složenýPredikát.test("Abcdefgh"); // = true
Datové proudy Při práci s klasickými poli a kolekcemi se používají především externí (sekvenční) iterátory Program požádá kolekci o iterátor, který pak v cyklu žádá o další instanci, s níž provede požadovanou operaci. Proud se chová jako objekt využívající interní (dávkové) iterátory provádějící s iterovanými objekty operace definované lambda-výrazy. Program definuje, jaká data se do proudu zařadí, resp. jak proud ona data získá. Nezávisle na tom se definuje, jak se těmito daty bude pracovat.
Analogie: výrobní linka Na počátku je vstup V případě výrobního pásu je to sklad dílů, v případě proudu to bude zdroj dat – většinou nějaký zdrojový kontejner. Zdrojem ale může být i generátor, který vytváří data na požádání Pracoviště Podél výrobního pásu jsou připravená pracoviště, na nichž zaškolení dělníci provádějí jednotlivé operace. V případě proudu nahradíme dělníky metodami (přesněji lambda výrazy), které s příchozím objektem provedou požadovanou operaci Zpracovaný objekt pokračuje po proudu k dalšímu pracovišti Výsledky Na konci výrobního pásu „vypadne“ hotový výrobek. Na konci proudu obdržíme požadovaný výsledek.
Odchylky od kolekcí a polí Neblokují si žádnou paměť pro zpracovávaná data; data odněkud „přitékají“, „pracoviště“ je zpracuje a pošle dál. Ve většině případů neovlivňují zdrojová data. Naplánované operace se chovají obdobně jako dělníci u pásu: nehrnou se do skladu (kontejneru), aby v něm data zpracovaly, ale počkají si, až k nim zpracovávaný objekt po proudu „přiteče“ a pak se o něj postarají. Nezajímají se o to, zda je vstup dat konečný (klasický kontejner), nebo nekonečný.
Datové proudy Výhody datových proudů: Z programu zmizí pomocný kód, který měl na starosti řízení iterací, a zpřehlední se tak popis akcí, které se mají se zpracovávanými objekty udělat. Objeví-li se v budoucnu nějaké propracovanější techniky paralelizace prováděných činností, není třeba upravovat část programu řešící aplikační logiku, ale stačí pouze vylepšit knihovnu proudů a tím se automaticky zefektivní zpracování programů, které tyto proudy využívají.
Ukázka zpracování datového proudu List<String> cislaSlovne = Arrays.asList("200", "400", "100", "300"); Integer[] cisla = cislaSlovne .stream() .map(Integer::parseInt) .filter(num -> num < 300) .toArray(Integer[]::new); for (int i: cisla) { System.out.print(i + ", "); } Vypíše: 200, 100,
Práce s obsahem proudu Obsah proudu můžeme během práce filtrovat použitím metody Stream<T> filter(Predicate<? super T> predicate) Proudy můžeme také řadit: Stream<T> sorted() Stream<T> sorted(Comparator<? super T> comparator) Prvky proudu můžeme konvertovat na jiný typ: <R> Stream<R> map(Function<? super T,? extends R> mapper) DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) IntStream mapToInt (ToIntFunction <? super T> mapper) LongStream mapToLong (ToLongFunction <? super T> mapper)
Možnosti získání proudu Z kolekcí prostřednictvím metod stream() a parallelStream() Z polí prostřednictvím metody Arrays.stream(Object[]) Z továrních metod rozhraní Stream, např.: Stream of(Object[]) IntStream range(int, int) Stream iterate(Object, UnaryOperator) Řádky souboru lze získat metodou BufferedReader.lines() Proud náhodných čísel lze získat metodou Random.ints() Proud položek v ZIP-souboru lze získat metodou JarFile.stream()
Další novinky Nové API pro datum a čas (balíček java.time) – umožňuje např. práci s časovými zónami (lokální datum a čas, internacionalizace aplikace), poskytuje vláknově bezpečné (thread-safe) objekty. Nahrazuje „neohrabanou“ třídu java.util.Calendar. Více ZDE. Paralelní řazení polí – využití víceprocesorových počítačů a vícejádrových procesorů (metoda parallelSort ve třídě java.util.Arrays). Zabezpečení – nativní podpora TLS (Transport Layer Security neboli TLS jsou protokoly, které umožňují zabezpečenou komunikaci na internetu), nativní podpora NSA Suite B Cryptographic Algorithms. (Balíček šifrovacích algoritmů, které pomáhají uchránit data při transportu přes nezabezpečený internet. Tyto algoritmy jsou velmi silné a jejich prolomení je v podstatě nemožné.) Nástroj pro zpracování (engine, interpreter) jazyka JavaScript s názvem Nashorn.
Přehled všech novinek v Javě 8 http://www.oracle.com/technetwork/java/javase/8-whats-new- 2157071.html