Site icon Blog programisty Java

Java 9: modularyzacja kodu i mechanizm jshell

java 9

W tej notce dowiesz się o większości istotnych zmianach jakie zaszły w Java 9. Potraktuj go jako przewodnik dla doświadczonego programisty. Większość z nich to poprawki jakie programiści JDK musieli zawrzeć po wydaniu Java 8. Aby w pełni zrozumieć ten wpis polecam wpierw zapoznać się z kursem odnośnie wszystkich zmian, jakie zaszły w JDK 8*.

Java Platform Module System (projekt Jigsaw)

Już we wcześniejszych wersjach Java wielu programistów zgłaszało twórcom języka problem związany z wydajnością coraz to szybciej rosnącego JDK. Niestety, ale z ubiegiem lat, JDK stało się kontenerem dla wielu prawie nieużywanych bibliotek. Dziś, w dobie powszechnego dostępu do Internetu, wiele osób stawiało sobie pytanie, czy naprawdę JDK musi zawierać tak wiele elementów. Dodatkowo, ostatnim czasem coraz bardziej popularna stało się tworzenie aplikacji zgodnie z architekturą mikroserwisową, co także wymuszało odchudzenie JDK. Dlatego też od wersji Java z numerem 9, zaczęto redukować wiele bibliotek, takich jak np. JAXB, JavaFX, itp.

Aby to wszystko wykonać, należało wymyśleć sposób, na sensowne i bezpieczne zmodularyzowanie JDK. Po wielu latach prac, udało się to osiągnąć właśnie za pomocą Java Platform Module System. Technikę tę możesz też używać w swoich komponentach, w celu kontroli nadmiarowych zależności.

W jaki sposób stworzyć własny moduł? Każdy z nich musi zawierać się w odpowiednich podprojektach (możesz je utożsamiać z osobnymi modułami mavenowymi, jeśli korzystasz z narzędzia maven). Każdy w tych podprojektów w katalogu startowym (np. src/java/main) powinien mieć definicję modułu w pliku module-info.java.

Rozkład katalogów przy modularyzacji w Java 9

Teraz moduł „helloworld” zawierać będzie dwa plik: HelloInterface i HelloWorld:

package jdk.java9.jigsaw.hello;

public interface HelloInterface {
    void sayHello();
}
package jdk.java9.jigsaw.hello;

public class HelloWorld implements HelloInterface{
    public static void sayHelloWorld() {
        System.out.println("Hello World!");
    }

    public void sayHello() {
        System.out.println("Hello!");
    }
}

Nie ma tu nic specjalnego. Po prostu zwykła implementacja, którą będziesz można wykorzystać w module zależnym. Najważniejsze, aby zawrzeć w pliku module-info definicję jak należy z niego korzystać. W tym przypadku będzie to po prostu moduł udostępniający kod zawarty w pakiecie jdk.java9.jigsaw.hello.

module jdk.java9.jigsaw.hello {
	exports jdk.java9.jigsaw.hello;
        provides jdk.java9.jigsaw.hello.HelloInterface with 
        jdk.java9.jigsaw.hello.HelloWorld;
}

Dyrektywa exports służy do udostępnienia wszystkich publicznych składowych pakietu. Ponieważ chciałbym aby mój kod był traktowany jako niezależny serwis, który będzie ładowany w drugim module, skorzystałem ze słów kluczowych provides with. Provides powinno wskazywać na interface, a słowo with na jego implementację.

Teraz w module zależnym hellomain użyję wcześniej zaprojektowany moduł.

package jdk.java9.jigsaw.main;

import java.util.ServiceLoader;

import jdk.java9.jigsaw.hello.HelloInterface;
import jdk.java9.jigsaw.hello.HelloWorld;

public class ModulesMain {

    public static void main(String[] args) {
        HelloWorld.sayHelloWorld();
        
        Iterable<HelloInterface> services = ServiceLoader.load(HelloInterface.class);
        HelloInterface service = services.iterator().next();
        service.sayHello();
    }

}

ModuleMain zawiera klasyczną metodę main, która poprzez skorzystanie z API wystawionego w HelloInterface wyświetli napis „Hello World”, a później „Hello!”. Klasę i interfejs z modułu helloworld importujesz, tak jakby zawarte były w tym samym projekcie.

Efekt na konsoli powinien być:

Hello, Modules!
Hello!

To był bardzo prosty przykład jak można skorzystać z modułów. Jednak sam mechanizm modularyzacji jest dużo bardziej skomplikowany i pozwala na tworzenie wielu przypadków. Nie będę się jednak aż tak mocno w niego zagłębiał w tej notce. Chcę Ci jedynie zasygnalizować, że taka zmiana się pojawiła w JDK 9.

Zmiany w Stream API

Nowe funkcje, o które rozszerzono Stream API:

  • Iterate

Ciekawa nowa funkcja umożliwiająca w łatwy sposób iterowanie za pomocą for-each w strumieniu. Wcześniej, aby wykonywać własny sposób iteracji, należało napisać swój iterator, co było bardzo czasochłonne. Teraz można korzystać z pętli for-each w taki sam sposób jak z pętli for.

Przykład skorzystania z operacji iterate w celu wypunktowania co drugiej wartości z ciągu liczb od 0 do 20.

Stream.iterate(0, i -> i <= 20, i -> i + 2)
	.forEach(System.out::println);

Operacja takeWhile przyjmuje predykat, który jest stosowany do elementów w celu określenia najdłuższego prefiksu tych elementów (jeśli strumień jest uporządkowany) lub podzbioru elementów strumienia (jeśli strumień jest nieuporządkowany). Najdłuższy prefiks (ang. longest prefix) to po prostu ciągła sekwencja elementów strumienia, które pasują do danego predykatu. Podzbiór strumienia (ang. stream’s subset) z kolei to zestaw niektórych (ale nie wszystkich) elementów strumienia pasujących do predykatu.

Prostym przypadkiem użycia może być narysowanie tzw. „choinki”. Klasycznym rozwiązaniem tego problemu jest użycie pętli for. Jeśli chcesz zrobić to zadanie, wykorzystując Stream API, wystarczy stworzyć nieskończoną pętlę za pomocą iterate, a następnie wyznaczyć warunek stopu za pomocą operacji takeWhile (w moim przypadku nastąpi on, gdy rozmiar stringa dojdzie do dziesięciu).

List<String> takeWhileExample = Stream.iterate("", s -> s + "*")
		  .takeWhile(s -> s.length() < 10)
		  .collect(Collectors.toList());
		
takeWhileExample.forEach(System.out::println);

Ta operacja działa podobnie jak takeWhile, z tą różnicą, że sprawdza, czy każdy element spełnia pewien warunek. Jeśli nie, to go odrzuca, ale nie przerywa swojego działania.

Spójrz na poniższy przykład:

List<Integer> dropWhileExample = Stream.of(1, 3, 5, 6, 8, 9, 10)
            .dropWhile(number -> (number % 2 == 1))
            .collect(Collectors.toList());
  
System.out.println(dropWhileExample);

Ten strumień będzie przetwarzał ciąg liczb i odrzucał je, aż nie natknie się na liczbę parzystą. Gdy to nastąpi, reszta elementów pozostanie w strumienie. W rezultacie będą to liczby: 6, 8, 9, 10. Działanie dropWhile jest odwrotne do takeWhile. Ten sam kod dla takeWhile zwróci ciąg: 1, 3, 5.

Bardzo przydatne rozwiązanie, które pomaga obsługiwać strumienie, które mogą zawierać null. Przydaje się to przede wszystkim, gdy chcesz zmapować dane z jednej kolekcji z drugą, ale nie jesteś pewien, czy każdy element klucza będzie posiadał odpowiednią wartość. W poprzedniej wersji Java należałoby, obsłużyć w wyrażeniu lambda odpowiedni warunek na wypadek, gdy element byłby null (np. element == null ? Stream.empty() : Stream.of(temp)😉

	List<String> collection = Arrays.asList("1", "2", "3");
        Map<String, String> map = new HashMap<>() {
            {
                put("1", "first");
                put("2", "second");
            }
        };
		
	List<String> collect = collection.stream()
		.flatMap(s -> Stream.ofNullable(map.get(s)))
		.collect(Collectors.toList());

JShell

JShell jest odpowiedzią na coraz większą popularyzację języków interpretowanych. Celem tego projektu jest udostępnienie podstawowych funkcjonalności języka Java za pomocą prostej linii komend. Przypomina to trochę programowanie w Pythonie czy Perlu za pomocą shella na systemach Unixowych. Szczerze mówiąc, nie do końca rozumiem ten zabieg twórców JDK. Jedyne sensowne użycie takiej linii komend, jakie jestem wstanie znaleźć, to próba za programowania prostego programu na serwerach typu mainframe (czyli posiadających jedynie linię komend). Czasami może to być przydatne w celu weryfikacji, czy np. jakaś funkcjonalność jest prawidłowo zaimplementowana, itp. Ewentualnie przejawia jeszcze do mnie argument, że można wykorzystać jshell jako możliwość nauki programowania, bez konieczności instalacji lokalnie JDK oraz odpowiedniego IDE*.

Przykład prostego wywołania System.out za pomocą konsoli jshell

Zmiany w try-with-resources

Jedną z bardziej irytujących rzeczy w poprzednich wersjach Java, było brak możliwości wydzielenia argumentów try-with-resources przed całą strukturą. W ten sposób kod, który zawierał wiele długich zmiennych stał się dużo bardziej czytelny.

Stary zapis:

try (BufferedReader bufferedReader = new BufferedReader(new FileReader(inputfilePath))){
			String readLine;
			while((readLine = bufferedReader.readLine()) != null) {
				System.out.println(readLine);
			}
		} catch (IOException e) {
			/* some logger */
		}

Nowy zapis:

		BufferedReader bufferedReader = new BufferedReader(new FileReader(inputfilePath));
		
		try (bufferedReader){
			String readLine;
			while((readLine = bufferedReader.readLine()) != null) {
				System.out.println(readLine);
			}
		} catch (IOException e) {
			/* some logger */
		}

Metody fabryczne dla kolekcji

W Java 9 zaimplementowano nowe metody fabryczne dla kolekcji (ang. Factory Methods for Collections), które powinny ułatwić tworzenie struktur danych z wypełnionymi już elementami.

List.of – Umożliwia tworzenie listy (immutable).

List.of("one", "two", "three");

Set.of – Pozwala utworzyć zbiór (pamiętaj, że kolejność wpisywanych elementów, nie ma tu znaczenia).

Set.of("one", "two", "three");

Map.of – Tworzy mapę. Niestety jest to fabryka mało intuicyjna. Klucze i wartości są tu wpisywane po kolei, co sprawia, że zapis jest mało czytelny.

Map.of("key1", "value1", "key2", "value2");

Map.ofEntries – Tworzenie mapy na podstawie obiektów Map.entry. Moim zdaniem bardziej czytelny sposób, z racji, że widać pary kluczy i wartości.

Map.ofEntries(Map.entry("key3", "value3"), Map.entry("key4", "value4"));

Prywatne metody w interfejsach

Kwestia oczywista. Od JDK w wersji 8 można było tworzyć metody bezstanowe w interfejsach. Niestety dotąd, gdy metoda była bardzo długo lub je kod powtarzał się w kilku innych miejscach, nie było możliwości wykonania refaktoryzacji kodu za pomocą metod prywatnych. To uchybienie poprawiono w kolejnej wersji Java.

public interface PrivateMethod {
	String getNameById(Long id);
	
	static String getPhoneWithPrefix(String prefix, String number) {
		return prefix + " " + number;
	}
	
	default String getOwner(String firstName, String secondName, String street, String city) {
		return getFirstNameWithSecondName(firstName, secondName) + " " + getAddress(street, city);
	}
	
	
	private String getFirstNameWithSecondName(String firstName, String secondName) {
		return firstName + " " + secondName;
	}
	
	private String getAddress(String street, String city) {
		return street + ", " + city;
	}
}

Nowa wersja HttpClient

Nowsza wersja protokołu HTTP została zaprojektowana w celu poprawy ogólnej wydajności wysyłania żądań przez klienta i odbierania odpowiedzi z serwera. Nowe API klienta HTTP można znaleźć w java.net.HTTP.* Zapewnia także natywną obsługę protokołu HTTP 1.1/2 WebSocket.

Podsumowanie zmian w Java 9

Jak widzisz w wersji 9 nie weszło wiele rewolucyjnych zmian. Moim zdaniem, z punktu widzenia dewelopera, najczęściej używanymi przez Ciebie zmianami będą pewnie: stosowanie modularyzacji oraz zmiany w Stream API.

*Kurs odnośnie zmian w poprzedniej wersji JDK: Java 8

*Konsola online dla osób, które chciałyby przetestować jej możliwości: https://tryjshell.org/

Kod do lekcji: https://github.com/developeronthego/java9-plus

Exit mobile version