JavaRush /Blog Java /Random-PL /Przewodnik po Javie 8. 1 część.
ramhead
Poziom 13

Przewodnik po Javie 8. 1 część.

Opublikowano w grupie Random-PL

„Java wciąż żyje i ludzie zaczynają ją rozumieć”.

Witam w moim wprowadzeniu do Java 8. Ten przewodnik poprowadzi Cię krok po kroku przez wszystkie nowe funkcje tego języka. Dzięki krótkim i prostym przykładom kodu dowiesz się, jak używać domyślnych metod interfejsu , wyrażeń lambda , metod referencyjnych i powtarzalnych adnotacji . Pod koniec artykułu będziesz zaznajomiony z najnowszymi zmianami w interfejsach API, takimi jak strumienie, interfejsy funkcji, rozszerzenia skojarzeń i nowy interfejs Date API. Żadnych ścian nudnego tekstu – tylko kilka skomentowanych fragmentów kodu. Cieszyć się!

Domyślne metody interfejsów

Java 8 umożliwia dodawanie nieabstrakcyjnych metod zaimplementowanych w interfejsie poprzez użycie słowa kluczowego default . Ta funkcja jest również nazywana metodami rozszerzającymi . Oto nasz pierwszy przykład: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } Oprócz metody abstrakcyjnej oblicz interfejs Formula definiuje także domyślną metodę sqrt . Klasy implementujące interfejs Formula implementują jedynie metodę abstrakcyjnego obliczenia . Domyślnej metody sqrt można używać od razu po wyjęciu z pudełka. Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 Obiekt formuły jest zaimplementowany jako obiekt anonimowy. Kod jest imponujący: 6 linii kodu do prostego obliczenia sqrt(a * 100) . Jak zobaczymy w następnej sekcji, istnieje bardziej atrakcyjny sposób implementacji obiektów z pojedynczą metodą w Javie 8.

Wyrażenia lambda

Zacznijmy od prostego przykładu sortowania tablicy ciągów we wczesnych wersjach Javy: Pomocnicza metoda statystyczna Collections.sort pobiera listę i komparator do sortowania elementów danej listy. Często zdarza się, że tworzysz anonimowe komparatory i przekazujesz je do metod sortowania. Zamiast cały czas tworzyć anonimowe obiekty, Java 8 daje możliwość użycia znacznie mniejszej składni, wyrażeń lambda : Jak widać, kod jest znacznie krótszy i łatwiejszy do odczytania. Ale tutaj jest jeszcze krócej: w przypadku metody jednowierszowej możesz pozbyć się nawiasów klamrowych {} i słowa kluczowego return . Ale w tym miejscu kod staje się jeszcze krótszy: kompilator Java zna typy parametrów, więc możesz je również pominąć. Przyjrzyjmy się teraz bliżej sposobom wykorzystania wyrażeń lambda w prawdziwym życiu. List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

Interfejsy funkcjonalne

Jak wyrażenia lambda pasują do systemu typów Java? Każda lambda odpowiada danemu typowi zdefiniowanemu przez interfejs. A tak zwany interfejs funkcjonalny musi zawierać dokładnie jedną zadeklarowaną metodę abstrakcyjną. Każde wyrażenie lambda danego typu będzie odpowiadać tej abstrakcyjnej metodzie.Ponieważ metody domyślne nie są metodami abstrakcyjnymi, możesz dowolnie dodawać metody domyślne do swojego interfejsu funkcjonalnego. Jako wyrażenie lambda możemy użyć dowolnego interfejsu, pod warunkiem, że interfejs zawiera tylko jedną metodę abstrakcyjną. Aby mieć pewność, że Twój interfejs spełnia te warunki, musisz dodać adnotację @FunctionalInterface . Kompilator zostanie poinformowany tą adnotacją, że interfejs musi zawierać tylko jedną metodę, a jeśli napotka w tym interfejsie drugą metodę abstrakcyjną, zgłosi błąd. Przykład: Należy pamiętać, że ten kod będzie ważny także wtedy, gdy nie zostanie zadeklarowana adnotacja @FunctionalInterface . @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

Odniesienia do metod i konstruktorów

Powyższy przykład można jeszcze bardziej uprościć, stosując odwołanie do metody statystycznej: Java 8 umożliwia przekazywanie odwołań do metod i konstruktorów za pomocą symboli słowa kluczowego :: . Powyższy przykład pokazuje, jak można zastosować metody statystyczne. Ale możemy również odwoływać się do metod na obiektach: Przyjrzyjmy się, jak using :: działa w przypadku konstruktorów. Najpierw zdefiniujmy przykład z różnymi konstruktorami: Następnie zdefiniujmy interfejs fabryczny PersonFactory do tworzenia nowych obiektów osób : Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } Zamiast ręcznie implementować fabrykę, łączymy wszystko razem za pomocą referencji do konstruktora: Tworzymy referencję do konstruktora klasy Person poprzez Person::new . Kompilator Java automatycznie wywoła odpowiedni konstruktor, porównując sygnaturę konstruktorów z sygnaturą metody PersonFactory.create . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

Region Lambdy

Organizowanie dostępu do zmiennych o zasięgu zewnętrznym z wyrażeń lambda jest podobne do uzyskiwania dostępu z obiektu anonimowego. Dostęp do zmiennych końcowych można uzyskać z zakresu lokalnego, a także pól instancji i zmiennych zagregowanych.
Dostęp do zmiennych lokalnych
Z zakresu wyrażenia lambda możemy odczytać zmienną lokalną z modyfikatorem final : Jednak w przeciwieństwie do obiektów anonimowych, zmienne nie muszą być deklarowane jako final , aby były dostępne z poziomu wyrażenia lambda . Ten kod jest również poprawny: jednak zmienna num musi pozostać niezmienna, tj. być ukrytym finalem dla kompilacji kodu. Poniższy kod nie zostanie skompilowany: Zmiany wartości num w wyrażeniu lambda również nie są dozwolone. final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
Dostęp do pól instancji i zmiennych statystycznych
W przeciwieństwie do zmiennych lokalnych, możemy czytać i modyfikować pola instancji i zmienne statystyczne wewnątrz wyrażeń lambda. Znamy to zachowanie z anonimowych obiektów. class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
Dostęp do domyślnych metod interfejsów
Pamiętasz przykład z instancją formuły z pierwszej sekcji? Interfejs Formula definiuje domyślną metodę sqrt , do której można uzyskać dostęp z każdej instancji formuły , włączając w to obiekty anonimowe. To nie działa z wyrażeniami lambda. W wyrażeniach lambda nie można uzyskać dostępu do metod domyślnych. Poniższy kod nie kompiluje się: Formula formula = (a) -> sqrt( a * 100);

Wbudowane interfejsy funkcjonalne

API JDK 1.8 zawiera wiele wbudowanych interfejsów funkcjonalnych. Część z nich jest dobrze znana z poprzednich wersji Javy. Na przykład Comparator lub Runnable . Interfejsy te zostały rozszerzone o obsługę lambda przy użyciu adnotacji @FunctionalInterface . Ale API Java 8 jest również pełne nowych funkcjonalnych interfejsów, które ułatwią Ci życie. Niektóre z tych interfejsów są dobrze znane z biblioteki Google Guava . Nawet jeśli znasz tę bibliotekę, powinieneś przyjrzeć się bliżej, w jaki sposób te interfejsy są rozszerzane, za pomocą kilku przydatnych metod rozszerzania.
Predykaty
Predykaty to funkcje logiczne z jednym argumentem. Interfejs zawiera różne domyślne metody tworzenia złożonych wyrażeń logicznych (i, lub, negacja) przy użyciu predykatów Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
Funkcje
Funkcje przyjmują jeden argument i zwracają wynik. Do łączenia kilku funkcji w jeden łańcuch można zastosować metody domyślne (compose iThen). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Dostawcy
Dostawcy zwracają wynik (instancję) tego czy innego typu. W przeciwieństwie do funkcji dostawcy nie przyjmują argumentów. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Konsumenci
Konsumenci reprezentują metody interfejsu za pomocą jednego argumentu. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Komparatory
Komparatory znane są nam z poprzednich wersji Javy. Java 8 umożliwia dodawanie różnych domyślnych metod do interfejsów. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
Opcjonalne
Interfejs Opcjonalny nie działa, ale jest doskonałym narzędziem do zapobiegania wyjątkom NullPointerException . Jest to ważny punkt w następnej sekcji, więc rzućmy okiem na to, jak działa ten interfejs. Interfejs Opcjonalny to prosty kontener na wartości, które mogą mieć wartość null lub inną niż null. Wyobraź sobie, że metoda może zwrócić wartość lub nic. W Javie 8 zamiast zwracać wartość null , zwracasz instancję Opcjonalną . Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

Strumień

java.util.Stream to sekwencja elementów, na których wykonywana jest jedna lub wiele operacji. Każda operacja Stream jest pośrednia lub końcowa. Operacje terminalowe zwracają wynik określonego typu, natomiast operacje pośrednie zwracają sam obiekt strumienia, umożliwiając utworzenie łańcucha wywołań metod. Stream to interfejs podobny do java.util.Collection dla list i zestawów (mapy nie są obsługiwane).Każda operacja na Streamie może być wykonywana sekwencyjnie lub równolegle. Przyjrzyjmy się, jak działa strumień. Najpierw utworzymy przykładowy kod w postaci listy ciągów znaków: Kolekcje w Javie 8 zostały ulepszone, dzięki czemu można w prosty sposób tworzyć strumienie, wywołując Collection.stream() lub Collection.parallelStream() . W następnej sekcji wyjaśnione zostaną najważniejsze, proste operacje na strumieniach. List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
Filtr
Filtr akceptuje predykaty do filtrowania wszystkich elementów strumienia. Jest to operacja pośrednia, co pozwala na wywołanie innych operacji na strumieniu (na przykład forEach) na wynikowym (przefiltrowanym) wyniku. ForEach akceptuje operację, która zostanie wykonana na każdym elemencie już przefiltrowanego strumienia. ForEach jest operacją terminalową. Ponadto wywoływanie innych operacji jest niemożliwe. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Posortowane
Sorted to operacja pośrednia, która zwraca posortowaną reprezentację strumienia. Elementy są sortowane we właściwej kolejności, chyba że określisz swój Comparator . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" Należy pamiętać, że sorted tworzy posortowaną reprezentację strumienia bez wpływu na samą kolekcję. Kolejność elementów stringCollection pozostaje niezmieniona: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Mapa
Pośrednia operacja mapy konwertuje każdy element na inny obiekt za pomocą wynikowej funkcji. Poniższy przykład konwertuje każdy ciąg na ciąg wielkich liter. Ale możesz także użyć mapy, aby przekonwertować każdy obiekt na inny typ. Typ powstałych obiektów strumieniowych zależy od typu funkcji przekazywanej do mapy. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Mecz
Do sprawdzenia prawdziwości konkretnego predykatu w relacji strumienia można zastosować różne operacje dopasowywania. Wszystkie operacje dopasowania są terminalowe i zwracają wynik logiczny. boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
Liczyć
Count to operacja terminalowa, która zwraca liczbę elementów strumienia w postaci long . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Zmniejszyć
Jest to operacja terminalowa, która skraca elementy strumienia za pomocą przekazanej funkcji. Wynikiem będzie opcja zawierająca skróconą wartość. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Strumienie równoległe

Jak wspomniano powyżej, strumienie mogą być sekwencyjne lub równoległe. Operacje na strumieniu sekwencyjnym są wykonywane w wątku szeregowym, natomiast operacje na strumieniu równoległym są wykonywane na wielu równoległych wątkach. Poniższy przykład ilustruje, jak łatwo zwiększyć wydajność przy użyciu strumienia równoległego. Najpierw utwórzmy dużą listę unikalnych elementów: Teraz określimy czas spędzony na sortowaniu strumienia tej kolekcji. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
Strumień szeregowy
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
Strumień równoległy
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms Jak widać oba fragmenty są prawie identyczne, ale sortowanie równoległe jest o 50% szybsze. Wszystko, czego potrzebujesz, to zmienić stream() na równoległyStream() .

Mapa

Jak już wspomniano, mapy nie obsługują strumieni. Zamiast tego mapa zaczęła wspierać nowe i przydatne metody rozwiązywania typowych problemów. Powyższy kod powinien być intuicyjny: putIfAbsent ostrzega nas przed wpisywaniem dodatkowych kontroli zerowych. forEach akceptuje funkcję do wykonania dla każdej wartości mapy. Ten przykład pokazuje, jak za pomocą funkcji wykonywane są operacje na wartościach mapy: Następnie dowiemy się, jak usunąć wpis dla danego klucza tylko wtedy, gdy jest on odwzorowany na daną wartość: Inna dobra metoda: Łączenie wpisów na mapie jest całkiem proste: Łączenie albo wstawi klucz/wartość do mapy, jeśli nie ma wpisu dla danego klucza, albo zostanie wywołana funkcja scalania, która zmieni wartość istniejącego wpisu. Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION