JavaRush /Java Blog /Random EN /Java 8. Manual. 1 part.
ramhead
Level 13

Java 8. Manual. 1 part.

Published in the Random EN group

"Java is still alive - and people are starting to understand it."

Welcome to my introduction to Java 8. This guide will take you step by step through all the new features of the language. Based on short and simple code examples, you'll learn how to use interface default methods , lambda expressions , reference methods , and repeatable annotations . By the end of this article, you will be familiar with the latest API changes such as streams, functional interfaces, association extensions, and the new Date API. No walls of boring text - just a bunch of commented code snippets. Enjoy!

Default methods for interfaces

Java 8 allows us to add non-abstract methods implemented in an interface through the use of the default keyword . This capability is also known as extension methods . Here is our first example: In addition to the abstract calculate interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } method , the Formula interface also defines a sqrt default method . Classes that implement the Formula interface only implement the abstract calculate method . The sqrt default method can be used right out of the box. The formula object is implemented as an anonymous object. The code is quite impressive: 6 lines of code for a simple calculation Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 sqrt(a * 100) . As we will see later in the next section, there is a more attractive way to implement objects with a single method in Java 8.

Lambda Expressions

Let's start with a simple example of how to sort an array of strings in early versions of Java: The static helper method Collections.sort takes a list and a Comparator to sort the elements of the given list. It often happens that you create anonymous comparators and pass them to sort methods. Instead of creating anonymous objects all the time, Java 8 gives you the ability to use a much smaller amount of syntax, lambda expressions : As you can see, the code is much shorter and easier to read. But here it gets even shorter: For a one-line method, you can get rid of the curly braces {} and the return keyword 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)); . But here's where the code gets even shorter: Collections.sort(names, (a, b) -> b.compareTo(a)); the Java compiler is type-aware of the parameters, so you can leave them out as well. Now let's dive deeper into how lambda expressions can be used in real life.

Functional interfaces

How do lambda expressions fit into the Java type system? Each lambda corresponds to a given type defined using an interface. And the so-called functional interface must contain exactly one declared abstract method. Every lambda expression of a given type will match that abstract method. Because default methods are not abstract methods, you are free to add default methods to your functional interface. We can use an arbitrary interface as a lambda expression, as long as that interface contains only one abstract method. To ensure that your interface satisfies such conditions, you must add the @FunctionalInterface annotation. The compiler will be aware by this annotation that the interface must contain only one method and if a second abstract method is found in this interface, it will throw an error. Example: Note that this code is also valid even if the @FunctionalInterface annotation has not been declared. @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

References to methods and constructors

The example above can be further simplified with a reference to a static method: Java 8 allows you to pass method and constructor references using the :: key symbols . The above example shows how statistical methods can be accessed. But we can also refer to methods on objects: Let's take a look at how using :: works for constructors. First, let's define an example with various constructors: Next, we define a PersonFactory interface to create new person objects : 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); } Instead of manually implementing the factory, we bundle everything together with a constructor reference: We create a reference to the constructor of the Person class via Person::new . The Java compiler will automatically invoke the appropriate constructor by comparing the signature of the constructors with the signature of the PersonFactory.create method . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

lambda region

Organizing access to external scope variables from lambda expressions is similar to accessing from an anonymous object. You can access final variables from local scope, as well as instance fields and aggregate variables.
Accessing Local Variables
We can read a local variable with the final modifier from the scope of the lambda expression: But unlike anonymous objects, variables do not have to be declared final in order to provide access from the lambda expression to variables . This code is also correct: However, the variable num must remain unchanged, i.e. be implicit final , for code to compile. The following code will not compile: Changes to num within a lambda expression are also not allowed. 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;
Accessing Instance Fields and Aggregate Variables
Unlike local variables, instance fields and aggregate variables can be read and modified inside lambda expressions. This behavior is known to us from anonymous objects. 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); }; } }
Accessing the default methods of interfaces
Remember the formula instance example from the first section? The Formula interface defines a default sqrt method that can be accessed from every instance of formula including anonymous objects. This does not work with lambda expressions. The default method cannot be accessed inside lambda expressions. The following code does not compile: Formula formula = (a) -> sqrt( a * 100);

Built-in functional interfaces

The JDK 1.8 API contains many built-in functional interfaces. Some of them are well known from previous versions of Java. For example Comparator or Runnable . These interfaces are extended to include "lambda" support with the @FunctionalInterface annotation . But the Java 8 API is also full of new functional interfaces to make your life easier. Some of these interfaces are well known from the Google Guava library . Even if you are familiar with this library, you should take a close look at how these interfaces are extended, with some useful extension methods.
predicates
Predicates are boolean functions with one argument. The interface contains various default methods for creating complex logical expressions using predicates (and, or, negate) 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();
Functions
Functions take one argument and return a result. Default methods can be used to chain multiple functions together in a single chain (compose, andThen). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Suppliers
Suppliers return a result (instance) of one type or another. Unlike functions, providers take no arguments. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Consumers
Consumers impersonate interface methods with a single argument. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Comparators
Comparators are known to us from previous versions of Java. Java 8 allows you to add various default methods to interfaces. 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
Optionals
The Optionals interface is not functional, but it is a great utility for preventing NullPointerException . This is an important point for the next section, so let's take a quick look at how this interface works. The Optional interface is a simple container for values ​​that can be null or non-null. Imagine that a method can return a value or nothing. In Java 8, instead of returning null , you return an Optional instance . 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

Stream

java.util.Stream is a sequence of elements on which one or more operations are performed. Each Stream operation is either intermediate or terminal. Terminal operations return a result of a specific type, while intermediate operations return the stream object itself, allowing you to chain method calls. Stream is an interface, like java.util.Collection for lists and sets (maps are not supported). Each Stream operation can be executed either sequentially or in parallel. Let's take a look at how stream works. First, we'll create a sample code in the form of a list of strings: Collections in Java 8 are extended so that you can simply create streams by calling Collection.stream() or 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"); Collection.parallelStream() . The next section will explain the most important, simple stream operations.
filter
Filter accepts predicates to filter all elements of stream. This operation is intermediate, which allows us to call other stream operations (eg forEach) on the result (filtered). ForEach accepts an operation to be performed on each element of the already filtered stream. ForEach is a terminal operation. Further, calling other operations is not possible. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Sorted
Sorted is an intermediate operation that returns a sorted representation of stream. The elements are sorted in the correct order unless you provide your Comparator . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" Keep in mind that sorted creates a sorted representation of stream without affecting the collection itself. The order of the elements of stringCollection remains intact: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map
An intermediate operation, map, converts each element into another object using the resulting function. The following example converts each string to an uppercase string. But you can also use map to convert each object to a different type. The type of the resulting stream objects depends on the type of function you pass to map. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Match
Various match operations can be used to test the truth of a particular predicate on stream. All match operations are terminal and return a boolean result. 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
Count
Count is a terminal operation that returns the number of elements of stream as a long . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Reduce
This is a terminal operation that reduces the elements of stream using the passed function. The result will be an Optional containing the shortened value. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Parallel Streams

As mentioned above, stream are serial and parallel. Serial stream operations are performed on a serial stream, while parallel stream operations are performed on multiple parallel streams. The following example demonstrates how easy it is to improve performance by using a parallel stream. To begin with, let's create a large list of unique elements: Now we will determine the time spent on sorting stream of this collection. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
serial stream
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
Parallel stream
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 As you can see, both snippets are almost identical, but the parallel sort is 50% faster. All you need is to change stream() to parallelStream() .

Map

As already mentioned, maps do not support streams. Instead, map began to support new and useful methods for solving common tasks. The code above should be intuitive: putIfAbsent warns us against writing extra checks for null. forEach accepts a function to execute for each of the map values. This example shows how to perform operations on map values ​​using functions: Next, we'll learn how to remove an entry for a given key only if it matches a given value: Another good method: Merging map entries is quite easy: 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 The merge will either insert the key/value into the map if there is no entry for the given key, or the merge function will be called to change the value of the existing entry.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION