"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 calculateinterface 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 keywordList
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
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() orList
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.
GO TO FULL VERSION