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

Java 8 Guide. 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. Through short, simple code examples, you'll learn how to use interface default methods , lambda expressions , reference methods , and repeatable annotations . By the end of the article, you will be familiar with the latest changes to APIs such as streams, function 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 the interface through the use of the default keyword . This feature is also known as extension methods . Here's our first example: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } In addition to the abstract method calculate , the Formula interface also defines a default method sqrt . Classes that implement the Formula interface implement only the abstract calculate method . The default sqrt method can be used straight out of the box. The formula Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 object is implemented as an anonymous object. The code is quite impressive: 6 lines of code to simply calculate sqrt(a * 100) . As we'll see further in the next section, there is a more attractive way to implement single method objects 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 statistical helper method Collections.sort takes a list and a Comparator to sort the elements of the given list. What often happens is 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 much less 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 . But here's where the code gets even shorter: the Java compiler is aware of the types 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. 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));

Functional Interfaces

How do lambda expressions fit into the Java type system? Each lambda corresponds to a given type defined by an interface. And the so-called functional interface must contain exactly one declared abstract method. Every lambda expression of a given type will correspond to this abstract method. Since 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, provided that the interface contains only one abstract method. To ensure that your interface satisfies these conditions, you must add the @FunctionalInterface annotation . The compiler will be informed by this annotation that the interface must contain only one method, and if it encounters a second abstract method in this interface, it will throw an error. Example: Keep in mind that this code would also be valid even if the @FunctionalInterface annotation had 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 by using a statistical method reference: Java 8 allows you to pass references to methods and constructors using the :: keyword symbols . The above example shows how statistical methods can be used. But we can also reference methods on objects: Let's take a look at how using :: works for constructors. First, let's define an example with different constructors: Next, we define the PersonFactory factory interface for creating 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 implementing the factory manually, we tie everything together using a constructor reference: We create a reference to the constructor of the Person class via Person::new . The Java compiler will automatically call 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 outer 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 a lambda expression: But unlike anonymous objects, variables do not need to be declared final to be accessible from a lambda expression . This code is also correct: However, the num variable must remain immutable, i.e. be implicit final for code compilation. 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 Statistical Variables
Unlike local variables, we can read and modify instance fields and statistical variables inside lambda expressions. We know this behavior 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); }; } }
Access to default methods of interfaces
Remember the example with the formula instance 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. Default methods 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 using the @FunctionalInterface annotation . But the Java 8 API is also full of new functional interfaces that will make your life easier. Some of these interfaces are well known from Google's Guava library . Even if you are familiar with this library, you should take a closer 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 (and, or, negate) using predicates 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 produce a result. Default methods can be used to combine several functions together into one 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 do not take arguments. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Consumers
Consumers represent 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 many 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 a chain of method calls to be created. 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 sample code in the form of a list of strings: Collections in Java 8 are enhanced so that you can create streams quite simply by calling Collection.stream() or Collection.parallelStream() . The next section will explain the most important, simple stream operations. 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");
Filter
Filter accepts predicates to filter all elements of stream. This operation is intermediate, which allows us to call other stream operations (for example forEach) on the resulting (filtered) result. ForEach accepts an operation that will be performed on each element of the already filtered stream. ForEach is a terminal operation. Further, calling other operations is impossible. 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 specify 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 stringCollection elements remains untouched: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map
The intermediate map operation 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 matching operations can be used to test the truth of a particular predicate in the stream relation. 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 shortens 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, streams can be sequential or parallel. Sequential stream operations are performed on a serial thread, while parallel stream operations are performed on multiple parallel threads. The following example demonstrates how to easily increase performance using a parallel stream. First, let's create a large list of unique elements: Now we will determine the time spent sorting the 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 fragments are almost identical, but parallel sorting 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 problems. The code above should be intuitive: putIfAbsent warns us against writing additional null checks. forEach accepts a function to execute for each of the map values. This example shows how operations are performed on map values ​​using functions: Next, we will learn how to remove an entry for a given key only if it maps to a given value: Another good method: Merging map entries is quite easy: Merging 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, which will change the value of the existing entry. 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
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION