Szerző: Forgács Ádám

Java 9 - API újdonságok

Folytatva egy korábbi írást, ismerkedjünk meg a Java 9-ben bevezetésre kerülő főbb API újításokról, módosításokról. Az alábbi bemutató nem teljeskörű, azokra fogunk koncentrálni, amik gyakrabban használhatóak a mindennapokban.

Új metódusok kollekciók létrehozására

Már hosszú ideje probléma az, hogy ha van egy vagy néhány értékünk, amiket szeretnénk az egyszerűség kedvéért egy kollekció megvalósításon keresztül használni, abba belepakolni, akkor azt csak hosszas ceremónián keresztül tehetjük meg. A helyzet még rosszabb, ha tudjuk, hogy több érték nem is lehet, tehát ha egy nem módosítható konténert akarunk létrehozni, akkor az még több karakter, amit be kell gépelni. Vegyük az alábbi kódot:

public void createImmutableList() {
	final String a = "abc";
	final String b = "def";
	final String c = "ghi";

	final List<String> immutableList = Collections.unmodifiableList(Arrays.asList(a, b, c));
}

Arra, hogy listát készítsünk néhány adott értékből, létezik a beépített Arrays.asList(...), viszont az új kollekció módosítható (a méretén nem lehet változtatni, de a belső elemek lecserélhetőek). A helyzet még rosszabb, ha Set-et akarunk létrehozni:

public void createImmutableSet() {
	final String a = "abc";
	final String b = "def";
	final String c = "ghi";

	final Set<String> immutableSet = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(a, b, c)));
}

Egy extra hívás kell, hogy a listából halmazt készíthessünk. Sokkal jobb lenne, ha lenne valamilyen új nyelvi kifejezés erre, vagy valamilyen könnyen használható metódus. A blogbejegyzés témájából már kitalálható, hogy az utóbbi megoldást választották a nyelv karbantartói. A könnyű megvalósítás, nagy haszon, de "konzervatívabb módosítások" címszavak alatt a List, Set és a Map interfészek új statikus metódusokat kaptak, amikkel már sokkal tömörebben hozhatunk létre nem módosítható kollekciókat. A példák egyértelműen szemléltetik:

public void createImmutableCollections() {
	final String a = "abc";
	final String b = "def";
	final String c = "ghi";

	final List<String> immutableList = List.of(a, b, c);

	final Set<String> immutableSet = Set.of(a, b, c);

	final Map<Integer, String> immutableMap = Map.of(1, a, 2, b, 3, c);
	// VAGY
	final Map<Integer, String> immutableMapFromEntries = Map.ofEntries(Map.entry(1, a), Map.entry(2, b), Map.entry(3, c));
}

Mindhárom interfész kapott új of nevű statikus metódusokat, 0-tól 10 paraméterig elfogadva értékeket, plusz egy varargs változatot. A megvalósítás során fontos volt, hogy minél kevesebb extra művelet fusson, ne lassítsuk feleslegesen az alkalmazást. Ezért nem szimplán csak egy varargs hívással lettek megoldva, elkerülve az extra tömb allokálást. Sőt, az implementáció (mert új belső osztályok készültek erre, nem a meglévőket használja) is úgy került lefejlesztésre, hogy kis számig (0, 1, 2) belül se használ tömböt, hanem külön mezők vannak az értékeknek. Három és fölötte egyszerű tömb van használva. Ezek az új metódusok hasznosak lesznek, és a teljesítmény miatt se kell aggódni. Nem tekinthetőek annyira forradalminak, effektíve a kibővítése 2 vagy több paraméterre a Collections.emptySet(), Collections.singletonList(...) stb. metódusoknak. Az új kollekciók hasonló stílusban fognak működni, mint a meglévők is.

Stream és Optional újdonságok

Néhány új metódus bekerült ezen osztályokhoz. A Stream esetén igazán jelentős újítás a Stream.takeWhile(...) / Stream.dropWhile(...). Eddig nem volt lehetőség arra, hogy a Stream-ek esetén egy Predicate alapján döntsük el, hogy meddig kérjük el az elemeket. Alapból csak egy konstans számot adhattunk meg a limit metódusnak. Ez azért volt így, mert a Stream-ek úgy lettek tervezve, hogy minden működjön egységesen és gyorsan, akár szekvenciálisan, akár több szálra párhuzamosítva dolgoztuk fel az elemeket. Valószínűleg a nyelv karbantartói rájöttek, hogy az esetek nagy többségében a Stream-eket csak szekvenciálisan használjuk/használják, nem számít a párhuzamosíthatóság. Mostantól lehetőség lesz a Predicate alapján megtartani/kiszűrni az elemeket. A működésük:

public void streamAdditions() {

	final String takeTheFirstFewValues = IntStream.range(0, 10).takeWhile(i -> i < 5).mapToObj(String::valueOf).collect(Collectors.joining(","));
	System.out.println(takeTheFirstFewValues); // 0,1,2,3,4

	final String dropTheFirstFewValues = IntStream.range(0, 10).dropWhile(i -> i < 5).mapToObj(String::valueOf).collect(Collectors.joining(","));
	System.out.println(dropTheFirstFewValues); // 5,6,7,8,9
}

Párhuzamos Stream-ek esetén a működés nem definiált, a lehetőség megvan, de egyáltalán nem ajánlott.

Az Optional két új metódust kapott, mindkettő nagyon hasznos tud lenni a megfelelő helyzetben. Az első a stream(), egy új Stream-et készíthetünk az Optional példányunkból. Üres Optional esetén üres Stream kerül visszaadásra. Miért is jó ez? Ha alapból egy Optional-ünk van, akkor feleslegesnek érződik egy lehetséges értékkel Stream-et kezdeni. Valójában ez akkor jó, ha el akarjuk kerülni a monádok egymásba ágyazását (Stream<Optional<Object>> vagy Optional<Stream<Object>>, egyik rosszabb, mint a másik). Vegyük az alábbi kódot:

public class User {
		// ...
	}

	public Optional<User> getUserByid(Integer id) {
		// ...
	}

	public void optionalAdditions() {
		Stream.of(1, 2, 3)
			.map(this::getUserByid)
			.filter(Optional::isPresent)
			.map(Optional::get)
			.forEach(System.out::println);

		// Most már lehet flatMap-et használni
		Stream.of(1, 2, 3)
			.map(this::getUserByid)
			.flatMap(Optional::stream)
			.forEach(System.out::println);
}

Tehát ha van egy Optional-okból álló Stream-ünk, akkor most már elegánsabban szedhetjük össze a meglévő értékeket. Könnyedén elkerülhetjük az egymásba ágyazást.

A másik újítás egy új or(...) nevű metódus. Most már lekezelhetjük azt a helyzetet, hogy az esetlegesen üres Optional példányunkat lecseréljük egy másikra. Az or egy Supplier-t vár, ami az épp aktuális típusú Optional-t adja vissza meghíváskor. A többi Optional metódussal ellentétben, ha van érték, akkor szimplán visszatér önmagával, ha nincs, akkor a kapott Supplier-t meghívva annak eredményével tér vissza.

InputStream transzfer

Ugyan kicsi változás, mindössze egy rövid metódus, de végre nem kell minden új projekt esetén megírni vagy mellékelni egy külső könyvtárat, csak ezért a 3-4 soros funkcióért. Mostantól van beépített metódus, transferTo(...) névvel az InputStream-en, amivel átírhatjuk a paraméterként kapott OutputStream-re. A metódust direkt az osztályra helyezték el, hogy bizonyos esetekben, ha megéri, akkor felül lehessen definiálni. Tipikus példa erre a ByteArrayInputStream. A használata nem is lehetne egyszerűbb:

public void byteTransfer(InputStream input, OutputStream output) throws IOException {
	try (input; output) {
		input.transferTo(output);
	}
}

Stack-walking API

Eddig, ha szükségünk lett volna az épp aktuális stackframe-re, hogy tudjuk hogy hol áll éppen a kódunk, kik hívták meg, akkor arra nem volt megbízható megoldás. A Throwable.getStackTrace() és Thread.getStackTrace() metódusokat lehetett eddig használni, de azok nem adtak vissza minden stack szintet, plusz a konkrét Class példányokat se szerezhettük meg. Egy új osztály, a StackWalker kerül bevezetésre ezen problémák megoldására.

public static void stackWalker() {
	// Végigmegyünk a StackFrame-eken
	StackWalker.getInstance().forEach(stackFrame -> System.out.println(stackFrame));

	// Egy Function<Stream, T> alapján szabályozzuk a bejárást
	StackWalker.getInstance().walk(stream ->
		stream
			.map(StackWalker.StackFrame::getFileName)
			.collect(Collectors.toList()));

	// Ki hívott meg, a Class-t adja vissza
	System.out.println(StackWalker.getInstance(RETAIN_CLASS_REFERENCE).getCallerClass());
}

A StackWalker egy új belső interfészt, a StackFrame-et használja, ami nem összetévesztendő a Throwable vagy Thread által használt StackTraceElement osztállyal. Bár a legtöbb fejlesztésnél nem lesz szükség erre, így vagy úgy a legtöbb projektet érinti. Például a Log4J2 esetén a különféle loggereknek nem kell megadni az épp aktuális Class példányt. Jelenleg többek között a belső használatra szánt "sun.reflect.Reflection" osztály segítségével keresik ki, hogy mely osztályból hívták meg a loggert. A Java 9-re való áttéréssel most már lesz publikus API erre a célra.

Fejlettebb deprekáció

Az egyik legismertebb annotáció, a @Deprecated két új beállítható értéket kapott. Az első a String típusú since, amivel megadható szabad formátumban, hogy mely verzió óta érvényes, illetve a boolean forRemoval, jelezve, hogy a tervek között van-e a megjelölt osztály, metódus stb. végleges eltávolítása a jövőben. A JDK publikus osztályain ritkaság lesz a végleges törlés, de a saját projektjeinkben most már egyértelműen megjelölhetjük, ha valamit el akarunk távolítani.

Akik nem élték túl a fejlesztést

Néhány beharangozott API viszont nem készült el. Közöttük van az új HTTP2 kliens. Nem került teljesen eldobásra, a JDK-hoz mellékelve lesz az elkészült verzió, a "jdk.incubating" csomag alatt. A publikusnak szánt változat valószínűleg a Java 10-el fog jönni, ami már az új nyelvi elemeket is kihasznál(hat)ja. Amire abszolút nem érdemes várni, az a JSON író/olvasó. Az Oracle már a fejlesztés korai szakaszában visszavonta a támogatását, és jelenleg nem is látszódik nyomós indok arra, hogy szükség lenne rá. De természetesen ennél is megvan rá a lehetőség, hogy később újra meglátogassák a témát.

A legnagyobb újítás, a modularitás bevezetése egy sor új osztályt hoz magával, de a témával egy külön bejegyzésben fogunk majd foglalkozni.

Ha tetszett a cikk oszd meg másokkal is.