JavaRush /Blog Java /Random-PL /Funkcje Java 8 — kompletny przewodnik (część 2)
0xFF
Poziom 9
Донецк

Funkcje Java 8 — kompletny przewodnik (część 2)

Opublikowano w grupie Random-PL
Druga część tłumaczenia artykułu Funkcje Java 8 – Przewodnik ULTIMATE . Pierwsza część jest tutaj (link może ulec zmianie). Funkcje Java 8 — kompletny przewodnik (część 2) — 1

5. Nowe funkcje w bibliotekach Java 8

W Java 8 dodano wiele nowych klas i rozszerzono istniejące, aby lepiej obsługiwać współczesną współbieżność, programowanie funkcjonalne, datę/godzinę i nie tylko.

5.1. Klasa Opcjonalna

Słynny wyjątek NullPointerException jest zdecydowanie najczęstszą przyczyną błędów aplikacji Java. Dawno temu doskonały projekt Google Guava został zaprezentowany Optionaljako rozwiązanie NullPointerException, zapobiegając w ten sposób zanieczyszczaniu kodu przez sprawdzanie wartości null, a w rezultacie zachęcając do pisania czystszego kodu. Zainspirowana przez Google klasa Guava Optionaljest teraz częścią Java 8. OptionalTo tylko kontener: może zawierać wartość lub jakiś typ Тalbo po prostu mieć wartość null. Zapewnia wiele przydatnych metod, dzięki którym jawne sprawdzanie wartości null nie jest już uzasadnione. Bardziej szczegółowe informacje można znaleźć w oficjalnej dokumentacji . Spójrzmy na dwa małe przykłady użycia Optional: z wartością null i bez niej.
Optional<String> fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
Metoda isPresent()zwraca wartość true , jeśli instancja Optionalzawiera wartość różną od null, w przeciwnym razie false . Metoda orElseGet()zawiera mechanizm rezerwowy dla wyniku, jeśli Optionalzawiera on wartość null, akceptując funkcje generujące wartość domyślną. Metoda map () przekształca bieżącą wartość Optionali zwraca nową instancję Optional. Metoda orElse()jest podobna do orElseGet(), ale zamiast funkcji przyjmuje wartość domyślną. Oto wynik tego programu:
Full Name is set? false
Full Name: [none]
Hey Stranger!
Rzućmy okiem na inny przykład:
Optional<String> firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) );
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();
Wynik będzie następujący:
First Name is set? true
First Name: Tom
Hey Tom!
Bardziej szczegółowe informacje można znaleźć w oficjalnej dokumentacji .

5.2. Strumienie

Nowo dodany Stream API ( java.util.stream) wprowadza programowanie w prawdziwym stylu funkcjonalnym w Javie. Jest to zdecydowanie najbardziej wszechstronny dodatek do biblioteki Java, który pozwala programistom Java na znacznie większą wydajność, a także umożliwia im tworzenie wydajnego, czystego i zwięzłego kodu. Stream API znacznie ułatwia przetwarzanie kolekcji (ale nie ograniczając się do nich, jak zobaczymy później). Weźmy jako przykład prostą klasę Task.
public class Streams  {
    private enum Status {
        OPEN, CLOSED
    };

    private static final class Task {
        private final Status status;
        private final Integer points;

        Task( final Status status, final Integer points ) {
            this.status = status;
            this.points = points;
        }

        public Integer getPoints() {
            return points;
        }

        public Status getStatus() {
            return status;
        }

        @Override
        public String toString() {
            return String.format( "[%s, %d]", status, points );
        }
    }
}
Zadanie ma pewne punkty (lub pseudotrudności) i może być OTWARTE lub ZAMKNIĘTE . Przedstawmy mały zbiór problemów do zabawy.
final Collection<Task> tasks = Arrays.asList(
    new Task( Status.OPEN, 5 ),
    new Task( Status.OPEN, 13 ),
    new Task( Status.CLOSED, 8 )
);
Pierwszym pytaniem, które zamierzamy poznać, jest to, ile punktów zawierają obecnie zadania OPEN ? Przed wersją Java 8 typowym rozwiązaniem było użycie iteratora foreach. Ale w Javie 8 odpowiedzią są strumienie: sekwencja elementów obsługujących sekwencyjne i równoległe operacje agregujące.
// Подсчет общего количества очков всех активных задач с использованием sum()
final long totalPointsOfOpenTasks = tasks
    .stream()
    .filter( task -> task.getStatus() == Status.OPEN )
    .mapToInt( Task::getPoints )
    .sum();

System.out.println( "Total points: " + totalPointsOfOpenTasks );
Dane wyjściowe konsoli będą wyglądać następująco:
Total points: 18
Przyjrzyjmy się, co się tutaj dzieje. Najpierw kolekcja zadań jest konwertowana na reprezentację strumieniową. Następnie operacja filterodfiltrowuje wszystkie zadania posiadające status ZAMKNIĘTE . W następnym kroku operacja mapToIntkonwertuje strumienie Taskna strumienie Integer, stosując metodę Task::getPointsdla każdej instancji Task. Na koniec wszystkie punkty sumuje się metodą sum, która daje wynik końcowy. Zanim przejdziemy do kolejnych przykładów, warto pamiętać o kilku uwagach dotyczących wątków (więcej szczegółów tutaj ). Operacje streamdzielą się na operacje pośrednie i końcowe . Operacje pośrednie zwracają nowy strumień. Zawsze są leniwi; wykonując operacje pośrednie, takie jak filter, w rzeczywistości nie dokonują filtrowania, ale zamiast tego tworzą nowy strumień, który po ukończeniu zawiera elementy pierwotnego strumienia pasujące do podanego predykatu. Operacje skończone , takie jak forEachi sum, można przepuszczać przez strumień, aby uzyskać wynik lub efekt uboczny. Po zakończeniu ostatniej operacji strumień uważa się za wykorzystany i nie można go ponownie użyć. W prawie wszystkich przypadkach operacje końcowe zwykle kończą przechodzenie przez podstawowe źródło danych. Kolejną cenną cechą wątków jest standardowa obsługa procesów równoległych. Spójrzmy na ten przykład, który oblicza sumę wyników wszystkich problemów.
// Calculate total points of all tasks
final double totalPoints = tasks
   .stream()
   .parallel()
   .map( task -> task.getPoints() ) // or map( Task::getPoints )
   .reduce( 0, Integer::sum );

System.out.println( "Total points (all tasks): " + totalPoints );
Jest to bardzo podobne do pierwszego przykładu, z tą różnicą, że staramy się przetwarzać wszystkie zadania równolegle i obliczać wynik końcowy metodą reduce. Oto wynik konsoli:
Total points (all tasks): 26.0
Często istnieje potrzeba grupowania elementów według określonego kryterium. Przykład pokazuje, jak wątki mogą w tym pomóc.
// Группировка задач по их статусу
final Map<Status, List<Task>> map = tasks
    .stream()
    .collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
Dane wyjściowe konsoli będą następujące:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
Aby zakończyć przykłady problemów, obliczmy ogólny procent (lub wagę) każdego problemu w kolekcji na podstawie łącznej liczby punktów:
// Подсчет веса каждой задачи (Jak процент от общего количества очков)
final Collection<String> result = tasks
    .stream()                                        // Stream<String>
    .mapToInt( Task::getPoints )                     // IntStream
    .asLongStream()                                  // LongStream
    .mapToDouble( points -> points / totalPoints )   // DoubleStream
    .boxed()                                         // Stream<Double>
    .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
    .mapToObj( percentage -> percentage + "%" )      // Stream<String>
    .collect( Collectors.toList() );                 // List<String>

System.out.println( result );
Dane wyjściowe konsoli będą wyglądać następująco:
[19%, 50%, 30%]
Wreszcie, jak zauważyliśmy wcześniej, Stream API nie jest przeznaczony tylko dla kolekcji Java. Typowa operacja we/wy, taka jak czytanie plików tekstowych wiersz po wierszu, jest bardzo dobrym kandydatem do wykorzystania przetwarzania strumieniowego. Oto mały przykład, który to potwierdza.
final Path path = new File( filename ).toPath();
try( Stream<String> lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
    lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}
Metoda onConsolewywoływana w wątku zwraca równoważny wątek z dodatkową prywatną procedurą obsługi. Prywatna procedura obsługi jest wywoływana, gdy metoda close()jest wywoływana w wątku. Stream API wraz z lambdami i metodami referencyjnymi oraz metodami domyślnymi i statycznymi w Javie 8 są odpowiedzią na współczesne paradygmaty tworzenia oprogramowania. Bardziej szczegółowe informacje można znaleźć w oficjalnej dokumentacji .

5.3. Interfejs API daty/godziny (JSR 310)

Java 8 zapewnia nowy wygląd zarządzania datą i czasem, udostępniając nowy interfejs API daty i godziny (JSR 310) . Manipulowanie datą i godziną jest jednym z największych problemów programistów Java. Standardowe java.util.Dateprzestrzeganie java.util.Calendarogólnie nie poprawiło sytuacji (może nawet uczyniło ją bardziej zagmatwaną). Tak narodził się Joda-Time : świetna alternatywa API daty/godziny dla Java . Nowy interfejs API daty/godziny w Javie 8 (JSR 310) jest pod silnym wpływem Joda-Time i czerpie z niego to, co najlepsze. Nowy pakiet java.timezawiera wszystkie klasy dotyczące daty, godziny, daty/godziny, stref czasowych, czasów trwania i manipulacji czasem . Projekt API bardzo poważnie potraktował niezmienność: zmiany nie są dozwolone (twarda lekcja wyciągnięta z java.util.Calendar). Jeśli wymagana jest modyfikacja, zwrócona zostanie nowa instancja odpowiedniej klasy. Przyjrzyjmy się głównym klasom i przykładom ich użycia. Pierwsza klasa Clock, która zapewnia dostęp do aktualnej chwili, daty i godziny przy użyciu strefy czasowej. Clockmożna użyć zamiast System.currentTimeMillis()i TimeZone.getDefault().
// Получить системное время Jak смещение UTC
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
Przykładowe wyjście konsoli:
2014-04-12T15:19:29.282Z
1397315969360
Inne nowe klasy, którym się przyjrzymy, to LocaleDatei LocalTime. LocaleDatezawiera tylko część daty bez strefy czasowej w systemie kalendarza ISO-8601. W związku z tym LocalTimezawiera tylko część kodu czasowego>.
// получить местную data и время время
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );

System.out.println( date );
System.out.println( dateFromClock );

// получить местную data и время время
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );

System.out.println( time );
System.out.println( timeFromClock );
Przykładowe wyjście konsoli:
2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
LocalDateTimełączy LocaleDatei LocalTimeoraz zawiera datę i godzinę, ale nie strefę czasową, w systemie kalendarzowym ISO-8601. Poniżej podano prosty przykład.
// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );

System.out.println( datetime );
System.out.println( datetimeFromClock );
Przykładowe wyjście konsoli:
2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
Jeśli potrzebujesz daty/godziny dla określonej strefy czasowej, ZonedDateTime. Zawiera datę i godzinę w systemie kalendarza ISO-8601. Oto kilka przykładów dla różnych stref czasowych.
// Получение даты/времени для временной зоны
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );

System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );
Przykładowe wyjście konsoli:
2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]
Na koniec przyjrzyjmy się klasie Duration: przedział czasu w sekundach i nanosekundach. Dzięki temu obliczenia pomiędzy dwiema datami są bardzo proste. Zobaczmy, jak to zrobić:
// Получаем разницу между двумя датами
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );

final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );
Powyższy przykład oblicza czas trwania (w dniach i godzinach) pomiędzy dwiema datami: 16 kwietnia 2014 r. i 16 kwietnia 2015 r . Oto przykład wyjścia konsoli:
Duration in days: 365
Duration in hours: 8783
Ogólne wrażenie nowej daty/godziny w Javie 8 jest bardzo, bardzo pozytywne. Po części dlatego, że zmiany bazują na sprawdzonym w boju fundamencie (Joda-Time), po części dlatego, że tym razem kwestia została poważnie przemyślana i wysłuchano głosów twórców. Szczegółowe informacje można znaleźć w oficjalnej dokumentacji .

5.4. Silnik JavaScript Nashorn

Java 8 jest dostarczana z nowym silnikiem JavaScript Nashorn , który umożliwia tworzenie i uruchamianie niektórych typów aplikacji JavaScript na maszynie JVM. Silnik JavaScript Nashorn to po prostu kolejna implementacja javax.script.ScriptEngine, która podlega temu samemu zestawowi reguł, aby umożliwić interakcję Java i JavaScript. Oto mały przykład.
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );

System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
Przykładowe wyjście konsoli:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2

5.5. Baza64

Wreszcie obsługa kodowania Base64 znalazła się w standardowej bibliotece Java wraz z wydaniem Java 8. Jest bardzo łatwe w użyciu, co pokazuje przykład.
package com.javacodegeeks.java8.base64;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Base64s {
    public static void main(String[] args) {
        final String text = "Base64 finally in Java 8!";

        final String encoded = Base64
            .getEncoder()
            .encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
        System.out.println( encoded );

        final String decoded = new String(
            Base64.getDecoder().decode( encoded ),
            StandardCharsets.UTF_8 );
        System.out.println( decoded );
    }
}
Dane wyjściowe programu w konsoli pokazują zarówno zakodowany, jak i zdekodowany tekst:
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
Istnieją również klasy dla koderów/dekoderów przyjaznych adresom URL, a także koderów/dekoderów przyjaznych MIME ( Base64.getUrlEncoder()/ Base64.getUrlDecoder(), Base64.getMimeEncoder()/ Base64.getMimeDecoder()).

5.6. Tablice równoległe

Wersja Java 8 dodaje wiele nowych metod równoległego przetwarzania tablic. Być może najważniejszym z nich jest parallelSort(), który może znacznie przyspieszyć sortowanie na maszynach wielordzeniowych. parallelXxxMały przykład poniżej ilustruje działanie nowej rodziny metod ( ).
package com.javacodegeeks.java8.parallel.arrays;

import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

public class ParallelArrays {
    public static void main( String[] args ) {
        long[] arrayOfLong = new long [ 20000 ];

        Arrays.parallelSetAll( arrayOfLong,
            index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
            i -> System.out.print( i + " " ) );
        System.out.println();

        Arrays.parallelSort( arrayOfLong );
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
            i -> System.out.print( i + " " ) );
        System.out.println();
    }
}
Ten mały fragment kodu wykorzystuje metodę parallelSetAll()wypełniania tablicy 20 000 losowych wartości. Po tym się go stosuje parallelSort(). Program wypisuje pierwsze 10 elementów przed i po sortowaniu, aby pokazać, że tablica jest rzeczywiście posortowana. Przykładowe wyjście programu może wyglądać następująco (należy pamiętać, że elementy tablicy są losowe).
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793

5.7. Równoległość

Do klasy dodano nowe metody java.util.concurrent.ConcurrentHashMapobsługujące operacje agregujące w oparciu o nowo dodane obiekty strumieniowe i wyrażenia lambda. Do klasy dodano także nowe metody java.util.concurrent.ForkJoinPoolwspierające wspólne tworzenie puli (zobacz także nasz bezpłatny kurs na temat współbieżności w Javie ). Dodano nową klasę java.util.concurrent.locks.StampedLockzapewniającą blokowanie oparte na możliwościach z trzema trybami dostępu do kontroli odczytu/zapisu (można ją uznać za lepszą alternatywę dla niezbyt dobrej java.util.concurrent.locks.ReadWriteLock). Nowe klasy, które zostały dodane do pakietu java.util.concurrent.atomic:
  • Podwójny akumulator
  • Podwójny dodatek
  • Długi akumulator
  • Długi dodatek

6. Nowe funkcje w środowisku wykonawczym Java (JVM)

Obszar ten PermGenzostał wycofany i zastąpiony przez Metaspace (JEP 122). Opcje JVM -XX:PermSizei -XX:MaxPermSizezostały zastąpione odpowiednio przez -XX:MetaSpaceSizei -XX:MaxMetaspaceSize.

7. Wnioski

Przyszłość już nadeszła: Java 8 posunęła swoją platformę do przodu, dostarczając funkcje, które pozwalają programistom zwiększyć produktywność. Jest jeszcze za wcześnie, aby przenieść systemy produkcyjne na Javę 8, ale w ciągu najbliższych kilku miesięcy adopcja powinna powoli zacząć rosnąć. Jednakże teraz jest czas, aby rozpocząć przygotowywanie bazy kodu pod kątem zgodności z Javą 8 i przygotować się na wprowadzenie zmian w Javie 8, gdy będzie ona wystarczająco bezpieczna i stabilna. Jako dowód akceptacji przez społeczność Java 8, firma Pivotal wydała niedawno Spring Framework ze wsparciem produkcyjnym dla Java 8 . W komentarzach możesz podzielić się swoją opinią na temat ekscytujących nowych funkcji w Javie 8.

8. Źródła

Kilka dodatkowych zasobów szczegółowo omawiających różne aspekty funkcji Java 8:
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION