New in Java 8

Original author: Benjamin Winterberg
  • Transfer
Java has not died yet - and people are beginning to understand this.

Welcome to Java 8 tutorial . This material will introduce you step by step to all the new features of the language. You will learn how to use default interface methods, lambda expressions, method references, and repeatable annotations. All this will be followed by short and simple code examples. At the end of the article, you will get acquainted with the most recent changes in the API regarding streams, functional interfaces, extensions for associative arrays, as well as changes in the API for working with dates.

Default Interface Methods


Java 8 allows you to add non-abstract method implementations to an interface using the keyword default. This feature is also known as extension methods. Here is our first example:

interface Formula {
    double calculate(int a);
    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

In addition to the abstract method, the calculateinterface Formulaalso defines the default method sqrt. Classes implementing this interface should override only the abstract method calculate. The default method sqrtwill be available without overriding.

Formula formula = new Formula() {
    @Override
    public double calculate(int a) {
        return sqrt(a * 100);
    }
};
formula.calculate(100);     // 100.0
formula.sqrt(16);                // 4.0

formulaimplemented as an anonymous object. This code is pretty redundant: as many as 6 lines for such a simple calculation as sqrt(a * 100). In the next section, we will see that in Java 8 there is a much more elegant way to implement objects with a single method.

Lambda expressions


Let's start with a simple example: sorting an array of strings in previous versions of the language.

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);
    }
});

The static method Collections.sortaccepts a list and a comparator, which is used to sort the list. Surely you often had to create anonymous comparators in order to pass them to a method.

Java 8 provides a much shorter syntax, lambda expressions, so you don't have to waste time creating anonymous objects:

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});

As you can see, the code is much shorter and much more readable. And it can be made even shorter:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

For single-line methods, you can omit the brackets {} and the keyword return. So even shorter:

Collections.sort(names, (a, b) -> b.compareTo(a));

The compiler knows the types of parameters, so they can also be omitted. Let's see how else lambda expressions can be used.

Functional Interfaces


How do lambda expressions match the Java language type system? Each lambda corresponds to a type represented by an interface. The so-called functional interface must contain exactly one abstract method . Each lambda expression of this type will be associated with a declared method. Also, since default methods are not abstract, you can add as many methods as you like to the functional interface.

We can use any interfaces for lambda expressions that contain exactly one abstract method. An annotation is used to ensure that your interface meets this requirement.@FunctionalInterface. The compiler is aware of this annotation and will throw a compilation error if you add a second abstract method to the functional interface.

Example:

@FunctionalInterface
interface Converter {
    T convert(F from);
}

Converter converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted);    // 123

Keep in mind that this code will remain valid even if you remove the annotation @FunctionalInterface.

References to Methods and Constructors


The previous example can be simplified by using static method references:

Converter converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted);   // 123

Java 8 allows you to pass references to methods or constructors. To do this, use the keyword ::. The previous example illustrates passing a reference to a static method. However, we can also refer to the instance method:

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"

Let's see how to pass references to constructors. First, define a bean with several constructors:

class Person {
    String firstName;
    String lastName;
    Person() {}
    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

Then we define the interface of the factory, which will be used to create new persons:

interface PersonFactory

{ P create(String firstName, String lastName); }


Now, instead of implementing the interface, we connect everything together with a reference to the constructor:

PersonFactory personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

We create a link to the constructor using Person::new. The compiler automatically selects the appropriate constructor whose signature matches the signature PersonFactory.create.

Lambda Scopes


Access to external scope variables from a lambda expression is very similar to access from anonymous objects. You can refer to variables declared as finalinstance class fields and static variables.

final int num = 1;
Converter stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2);     // 3

But unlike anonymous objects, a variable numdoes not have to be declared as final. Such code will work too:

int num = 1;
Converter stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2);     // 3

However, the variable nummust still remain unchanged. The following code does not compile:

int num = 1;
Converter stringConverter = (from) -> String.valueOf(from + num);
num = 3;

Writing to a variable numwithin a lambda expression is also prohibited.

Access to fields and static variables

Unlike local variables, we can write values ​​to instance fields of the class and static variables inside lambda expressions. This behavior is well known by 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 interface methods

Remember the example with the formula from the first section? The interface Formuladefines a default method sqrt, which is accessible from each implementation of the interface, including anonymous objects. However, this will not work in lambda expressions.

Inside lambda expressions, it is forbidden to access the default methods. The following code does not compile:

Formula formula = (a) -> sqrt( a * 100);

Integrated Functional Interfaces


JDK 1.8 has many built-in functional interfaces. Some of them are well known from previous versions of the language, for example, Comparatoror Runnable. All of these interfaces were supported in lambdas by adding annotations @FunctionalInterface.

However, Java 8 also introduced many new functional interfaces that are designed to make your life easier. Some interfaces are well known in the Google Guava library. Even if you are unfamiliar with this library, you should take a look at how these interfaces have been supplemented with some useful extension methods.

Predicates

Predicates are functions that take a single argument and return a value of type boolean. The interface contains a variety of methods by default, allow to build complex conditions ( 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 some result. Default methods can be used to build call chains ( compose, andThen).

Function toInteger = Integer::valueOf;
Function backToString = toInteger.andThen(String::valueOf);
backToString.apply("123");     // "123"

Suppliers

Suppliers provide a result of a given type. Unlike functions, providers do not accept arguments.

Supplier personSupplier = Person::new;
personSupplier.get();   // new Person

Consumers

Consumers (consumers) are operations that are performed on a single input argument.

Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Comparators

Comparators are well known in previous versions of Java. Java 8 adds various default methods to the interface.

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

Optional values

Optionals are not functional interfaces, but are a convenient way to prevent a NullPointerException. This is an important concept that we will need in the next section, so let's take a look at how the optional values ​​work.

An optional value is essentially a container for a value that can be null. For example, you need a method that returns some value, but sometimes it must return an empty value. Instead of returning null, in Java 8 you can return an optional value.

Optional optional = Optional.of("bam");
optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"

Streams


A type java.util.Streamis a sequence of elements over which various operations can be performed. Operations on threads are either intermediate (intermediate) or final (terminal). Final operations return a result of a certain type, and intermediate operations return the same stream. Thus, you can build chains of several operations on the same stream. A stream is created based on sources, such as types that implement java.util.Collection, such as lists or sets (associative arrays are not supported). Operations on threads can be performed both sequentially and in parallel.

First, let's see how to work with a stream sequentially. First, create a source as a list of lines:

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");

In Java 8, you can quickly create threads using calls Collection.stream()or Collection.parallelStream(). The following sections explain the most common thread operations.

Filter

The Filter operation accepts a predicate that filters all elements of the stream. This operation is intermediate , i.e. allows us to call another operation (for example forEach) on the result. ForEach accepts a function that is called for each element in the (already filtered) stream. ForEach is the ultimate operation. It does not return any value, so further call to stream operations is impossible.

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);
// "aaa2", "aaa1"

Sorted

The Sorted operation is an intermediate operation that returns a sorted representation of a stream. Elements are sorted in the usual way if you have not provided your comparator:

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);
// "aaa1", "aaa2"

Remember that it sortedonly creates a sorted view and does not affect the order of elements in the original collection. The order of the lines in stringCollectionremains untouched:

System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map

An intermediate operation mapconverts each element to another object using the function passed. The following example converts each string to a uppercase string. However, you can also use mapto convert each object to an object of a different type. The type of resultant stream depends on the type of function that you pass in the call map.

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Match

To check whether a stream satisfies a given predicate, various match operations are used. All matching operations are finite and return a result of type boolean.

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

The operation Countis the final operation and returns the number of elements in the stream. The return type is long.

long startsWithB = 
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();
System.out.println(startsWithB);    // 3

Reduce

This final operation convolves the flow elements according to a given function. The result is an optional 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 threads


As mentioned above, threads can be sequential and parallel. Operations on serial threads are performed in one processor thread, on parallel threads - using several processor threads.

The following example demonstrates how you can easily increase speed using parallel threads.

First, create a large list of unique elements:

int max = 1000000;
List values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
    UUID uuid = UUID.randomUUID();
    values.add(uuid.toString());
}

Now we’ll measure the sorting time of this list.

Sequential sorting

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 sorting

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 pieces of code are almost identical, however parallel sorting is almost twice as fast. All you have to do is replace the call stream()with parallelStream().

Associative arrays


As already mentioned, associative arrays (maps) do not support streams. Instead, associative arrays now support various useful methods that solve common tasks.

Map map = new HashMap<>();
for (int i = 0; i < 10; i++) {
    map.putIfAbsent(i, "val" + i);
}
map.forEach((id, val) -> System.out.println(val));

This code does not need special comments: putIfAbsentit allows us not to write additional checks for null; forEachtakes a consumer who performs an operation on each element of the array.

This code shows how to use code for calculations using various functions:

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

Then we learn how to delete an object by key only if this object is associated with a key:

map.remove(3, "val3");
map.get(3);             // val33
map.remove(3, "val33");
map.get(3);             // null

Another useful method:

map.getOrDefault(42, "not found");  // not found

Merge records of two arrays? Easy:

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

In the absence of a key, Mergecreates a new key-value pair. Otherwise, it calls the join function for the existing value.

Date API


Java 8 contains a completely new API for working with dates and times, located in the package java.time. The new API is comparable to the Joda-Time library , but there are differences . The following sections describe the most important parts of the new API.

Clock

The type Clockprovides access to the current date and time. This type knows about time zones and can be used instead of a call System.currentTimeMillis()to return milliseconds. Such an exact date can also be represented by a class Instant. Objects of this class can be used to create objects of an obsolete type
java.util.Date.

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);   // legacy java.util.Date

Time Zones

Time zones are represented by type ZoneId. They can be accessed using static factory methods. Time zones contain offsets that are important for converting dates and times to local ones.

System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids
ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]

Localtime

The type LocalTimerepresents time, taking into account the time zone, for example, 10pm or 17:30:15. The following example creates two local times for the time zones defined above. Then both times are compared, and the difference between them in hours and minutes is calculated.

LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2));  // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween);       // -3
System.out.println(minutesBetween);     // -239

The LocalTime type contains various factory methods that make it easy to create new instances, as well as parsing strings.

LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late);       // 23:59:59
DateTimeFormatter germanFormatter =
    DateTimeFormatter
        .ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime);   // 13:37

Localdate

A type LocalDaterepresents a specific date, for example, 2014-03-11. Objects are LocalDateimmutable and are analogous LocalTime. The example demonstrates calculating a new date by adding or subtracting days, months, or years. Remember that each operation returns a new instance.

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek);    // FRIDAY

Creating an instance LocalDateby parsing a string:

DateTimeFormatter germanFormatter =
    DateTimeFormatter
        .ofLocalizedDate(FormatStyle.MEDIUM)
        .withLocale(Locale.GERMAN);
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas);   // 2014-12-24

Localdatetime

A type LocalDateTimeis a combination of date and time. Objects are LocalDateTimeimmutable and work similarly to LocalTimeand LocalDate. We can use various methods to extract specific values ​​from a date-time:

LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek);      // WEDNESDAY
Month month = sylvester.getMonth();
System.out.println(month);          // DECEMBER
long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay);    // 1439

By adding time zone information we can get Instant.

Instant instant = sylvester
        .atZone(ZoneId.systemDefault())
        .toInstant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014

Date-time formatting works the same as date or time formatting. We can use library or our own templates.

DateTimeFormatter formatter =
    DateTimeFormatter
        .ofPattern("MMM dd, yyyy - HH:mm");
LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string);     // Nov 03, 2014 - 07:13

Unlike java.text.NumberFormat, the new one DateTimeFormatteris immutable and thread safe.

Details about the syntax of the templates can be found here .

Annotations


Annotations in Java 8 are repeatable. Let's see an example right away to understand what it is.

First, we define a wrapper annotation that contains an array of annotations:

@interface Hints {
    Hint[] value();
}
@Repeatable(Hints.class)
@interface Hint {
    String value();
}

Java 8 allows us to use many annotations of the same type by specifying annotations @Repeatable.

Option 1: use container annotation (old way)
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}

Option 2: use repeatable annotation (new way)
@Hint("hint1")
@Hint("hint2")
class Person {}

When using option 2, the compiler automatically inserts the annotation @Hints. This is important when reading annotation information through reflection.

Hint hint = Person.class.getAnnotation(Hint.class);
System.out.println(hint);                   // null
Hints hints1 = Person.class.getAnnotation(Hints.class);
System.out.println(hints1.value().length);  // 2
Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
System.out.println(hints2.length);          // 2

Although we never declared the @Hints annotation in the class Person, it is available to us when called getAnnotation(Hints.class). However, a method getAnnotationsByTypethat directly provides access to all annotations is more convenient @Hint.

Moreover, annotations in Java 8 can be used on two more elements:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}

That's all


This completes the introduction to Java 8 programming. You have to yourself to explore other new JDK 1.8, for example, Arrays.parallelSort, StampedLock, CompletableFutureand others.

The full source code of the article is available on GitHub .

Also popular now: