Ten new features of Java 8

1, Default method for interface

Java 8 allows us to add a non abstract method implementation to the interface. We only need to use the default keyword. This feature is also called the extension method. The example is as follows:

interface Formula {
    double calculate(int a);

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

The Formula interface defines the sqrt method in addition to the calculation method. The subclass implementing the Formula interface only needs to implement one calculation method. The default method sqrt can be used directly on the subclass.

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

formula.calculate(100);     // 100.0
formula.sqrt(16);           // 4.0

The formula in this paper is implemented as an instance of an anonymous class, which is very easy to understand. Six lines of code realize the calculation of sqrt(a * 100). In the next section, we'll see a simpler way to implement a single method interface.

Translator's note: in Java, there is only one inheritance. If a class is to be given new features, it is usually implemented by using interfaces. In C + +, multiple inheritance is supported. A subclass is allowed to have interfaces and functions of multiple parent classes at the same time. In other languages, the method for a class to have its reusable code at the same time is called mixin. The new Java 8 feature is closer to Scala's trait in terms of compiler implementation. The concept of extension method is also known in C ා, which allows extension methods to existing types. It is different from Java 8 in semantics.

2, Lambda expression

First, let's see how strings are arranged in older versions of Java:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});

Just pass in a List object and a comparer to the static method Collections.sort to sort in the specified order. The common practice is to create an anonymous comparer object and pass it to the sort method.

In Java 8, you don't need to use the traditional anonymous object method. Java 8 provides a more concise syntax, lambda expression:

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

See, the code becomes more segmented and readable, but it can actually be written shorter:

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

If there is only one line of code in the function body, you can remove the braces {} and return keywords, but you can also write shorter:

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

The Java compiler can automatically derive parameter types, so you don't have to write them again. Now let's see what else lambda expressions can do to make it more convenient

3, Functional interface

How are lambda expressions represented in java's type system? Each lambda expression corresponds to a type, usually an interface type. The "functional interface" refers to the interface containing only one abstract method, and every lambda expression of this type will be matched to this abstract method. Because default methods are not abstract methods, you can also add default methods to your functional interfaces.

We can treat lambda expression as any interface type containing only one abstract method to ensure that your interface must meet this requirement. You only need to add @ functional interface annotation to your interface. If the compiler finds that there is more than one abstract method in the interface marked with this annotation, it will report an error.

@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted);    // 123

Note that if @ FunctionalInterface is not specified, the above code is also correct.

Translator's note: Map lambda expression to a single method interface, which was implemented in other languages before Java 8, such as Rhino JavaScript interpreter. If a function parameter receives a single method interface and you pass a function, Rhino interpreter will automatically make a single interface instance to function adapter. Typical application scenarios include The second parameter EventListener of addEventListener of org.w3c.dom.events.EventTarget.

4, Method and constructor references

The code in the previous section can also be represented by static method references:

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

Java 8 allows you to use the:: keyword to pass method or constructor references. The above code shows how to reference a static method. We can also reference an object's method:

converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"

Next let's see how the constructor is referenced by the:: keyword. First, we define a simple class with multiple constructors:

class Person {
    String firstName;
    String lastName;

    Person() {}

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

Next, we specify an object factory interface to create the Person object:

interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}

Instead of implementing a complete factory, we use constructor references to associate them:

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

We only need to use Person::new to get the reference of the Person class constructor. The Java compiler will automatically select the appropriate constructor according to the signature of the PersonFactory.create method.

5, Lambda scope

Accessing the outer scope in a lambda expression is similar to the way in older anonymous objects. You can directly access the external local variables marked final, or the fields and static variables of the instance.

6, Accessing local variables

We can access the outer local variables directly in the lambda expression:

final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

However, unlike anonymous objects, the variable num here can not be declared final, and the code is also correct:

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

stringConverter.convert(2);     // 3

However, the num here must not be modified by subsequent code (i.e. implicit with final semantics). For example, the following cannot be compiled:

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

Trying to modify num in a lambda expression is also not allowed.

7, Accessing object fields and static variables

Different from the local variables, the fields and static variables of the instance in the lambda are readable and writable. This behavior is consistent with anonymous objects:

class Lambda4 {
    static int outerStaticNum;
    int outerNum;

    void testScopes() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = 23;
            return String.valueOf(from);
        };

        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            return String.valueOf(from);
        };
    }
}

8, Default method for provider

Remember the formula example in the first section. The interface formula defines a default method, sqrt, that can be accessed directly by the formula instance, including anonymous objects, but not in lambda expressions.
The default method is not accessible in Lambda expressions, and the following code will not compile:

Formula formula = (a) -> sqrt( a * 100);
Built-in Functional Interfaces

The JDK 1.8 API contains many built-in functional interfaces, such as the Comparator or Runnable interfaces commonly used in old Java. These interfaces are annotated with @ functional interface in order to be used on lambda.
Java 8 API also provides many new functional interfaces to make the work more convenient. Some interfaces are from Google Guava library. Even if you are familiar with these interfaces, it is necessary to see how these interfaces are extended to lambda.

Predict interface

The Predicate interface has only one parameter and returns a boolean type. This interface contains several default methods to combine Predicate into other complex logic (such as and, or, non):

Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo");              // true
predicate.negate().test("foo");     // false

Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Function interface

The Function interface has a parameter and returns a result, with some default methods (compose, andThen) that can be combined with other functions:

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123");     // "123"

Supplier interface

The Supplier interface returns a value of any template. Unlike the Function interface, the interface does not have any parameters

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

Consumer interface

The Consumer interface represents the execution of an operation on a single parameter.

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

Comparator interface

Comparator is a classic interface in old Java. Java 8 adds a variety of default methods on top of it:

Comparator<Person> 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 interface

Optional is not a function but an interface. It's an auxiliary type to prevent NullPointerException exception. This is an important concept to be used in the next session. Now let's see what this interface can do

Optional is defined as a simple container whose value may or may not be null. Before Java 8, a function should return a non empty object but occasionally it may return null. In Java 8, it is not recommended to return null but optional.

Optional<String> optional = Optional.of("bam");

optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"

optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"

Stream interface

java.util.Stream represents the sequence of operations that can be applied to a group of elements at a time. Stream operations can be divided into intermediate operations or final operations. The final operation returns a specific type of calculation result, while the intermediate operation returns the stream itself, so you can string multiple operations in turn. Stream creation needs to specify a data source, such as the subclass of java.util.Collection, List or Set, which is not supported by Map. The operation of Stream can be performed in serial or parallel.

First, let's see how Stream is used. First, create a List of data used by the instance code:

List<String> 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");
stringCollection.add("ddd1");

Java 8 extends the collection class to create a stream through Collection.stream() or Collection.parallelStream(). The following sections explain the common stream operations in detail:

Filter filter

Filtering uses a predicate interface to filter and retain only qualified elements. This operation is an intermediate operation, so we can apply other Stream operations (such as forEach) to the filtered results. forEach needs a function to execute the filtered elements in turn. forEach is a final operation, so we cannot perform other Stream operations after forEach

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

// "aaa2", "aaa1"

Sort sort

Sorting is an intermediate operation, which returns the sorted Stream. If you do not specify a custom Comparator, the default sort will be used.

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

// "aaa1", "aaa2"

It should be noted that sorting only creates a Stream after sorting without affecting the original data source. After sorting, the original data stringCollection will not be modified.

System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map map
The intermediate operation map will convert the elements to other objects according to the specified Function interface. The following example shows the conversion of strings to uppercase strings. You can also convert objects to other types through map. The Stream type returned by map is determined by the return value of the Function passed in by map.
stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Match match

Stream provides a variety of matching operations, allowing you to detect whether the specified Predicate matches the entire stream. All matching operations are final operations and return a value 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 count
Count is a final operation that returns the number of elements in the Stream and the return value type is long.

long startsWithB = 
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

System.out.println(startsWithB);    // 3

Reduce protocol

This is a final operation. It allows multiple elements in a stream to be specified as one element through a specified function. The result after the specification is expressed through the Optional interface:

ptional<String> 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 earlier, there are two kinds of streams: serial Stream and parallel Stream. The operations on serial Stream are completed successively in one thread, while parallel Stream is executed simultaneously on multiple threads.

The following example shows how to improve performance through parallel streams:

First we create a large table without repeating elements

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

Then we calculate how long it will take to sort the Stream,
Serial sort:

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

/Serial time: 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 sorting time: 472 ms
The above two codes are almost the same, but the parallel version is 50% faster. The only change that needs to be made is to change stream() to parallelStream().

Map

As mentioned earlier, the Map type does not support stream, but Map provides some new and useful methods to handle some daily tasks.


Map<Integer, String> map = new HashMap<>();

for (int i = 0; i < 10; i++) {
    map.putIfAbsent(i, "val" + i);
}

map.forEach((id, val) -> System.out.println(val));

The above code is easy to understand. putIfAbsent does not need us to do additional existence checks, while forEach receives a Consumer interface to operate on each key value pair in the map.

The following example shows other useful functions on the map:

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

Next, I'll show you how to delete a key value matching item in the Map

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

It's also easy to merge Map elements:

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

What Merge does is insert if the key name does not exist, otherwise, Merge the value corresponding to the original key and insert it back into the map.

9, Date API

Java 8 includes a new set of time and date APIs under the package java.time. The new date API is similar to the open-source joda time library, but not exactly the same. The following examples show some of the most important parts of the new API:

Clock

The clock class provides methods to access the current date and time. Clock is time zone sensitive and can be used instead of System.currentTimeMillis() to get the current microseconds. A specific point in time can also be represented by the Instant class, which can also be used to create old java.util.Date objects.

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();

Instant instant = clock.instant();
Date legacyDate = Date.from(instant);   // legacy java.util.Date

Timezones time zone

The time zone is represented by ZoneId in the new API. The time zone can be easily obtained by using the static method of. Time zone defines the time difference to UTS time, which is extremely important when converting Instant time point object to local date object.

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 local time

LocalTime defines a time when there is no time zone information, such as 10 p.m. or 17:30:15. The following example creates two local times using the time zone created in the previous code. Then compare the time and calculate the time difference in hours and minutes:

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

LocalTime provides several factory methods to simplify the creation of objects, including parsing time 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 local date

LocalDate represents an exact Date, such as 2014-03-11. The value of this object is immutable, which is basically consistent with LocalTime. The following example shows how to add or subtract days / months / years to a Date object. Also note that these objects are immutable, and the operation always 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

Parsing a LocalDate type from a string is as simple as parsing LocalTime:

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 local datetime

LocalDateTime represents both time and date, which is equivalent to merging the previous two sections into one object. LocalDateTime, LocalTime and LocalDate are immutable. LocalDateTime provides some ways to access specific fields.

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

As long as time zone information is attached, it can be converted into a point in time Instant object, which can be easily converted into the old java.util.Date.

Instant instant = sylvester
        .atZone(ZoneId.systemDefault())
        .toInstant();

Date legacyDate = Date.from(instant);
System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014

Formatting LocalDateTime is the same as formatting time and date. In addition to using predefined formats, we can also define our own formats:

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 DateTimeFormatter is immutable, so it is thread safe.
For more information about the time date format: http://download.java.net/jdk8/docs/api/java/time/format/DateTimeFormatter.html

10, Annotation annotation

Multiple annotations are supported in Java 8. Let's take a look at an example to understand what it means.
First, define a wrapper class Hints annotation to place a specific set of Hint annotations:

@interface Hints {
    Hint[] value();
}

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

Java 8 allows us to use annotations of the same type multiple times, just mark @ Repeatable for the annotation.

Example 1: using a wrapper class as a container to store multiple annotations (old method)

@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}

Example 2: using multiple annotations (new method)

@Hint("hint1")
@Hint("hint2")
class Person {}

In the second example, the java compiler will implicitly define @ Hints annotation for you. Understanding this will help you to use reflection to obtain these information:

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
Even if we don't define @ Hints annotation on the Person class, we can get @ Hints annotation through getAnnotation(Hints.class). The more convenient way is to use getannotations bytype to get all @ Hint annotations directly.
In addition, Java 8 annotation is added to two new target s:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
I've written about the new features of Java 8. There must be more to discover. There are many useful things in JDK 1.8, such as arrays.parallelsort, stampedelock, completable future, etc.

Tags: Java Lambda JDK Scala

Posted on Thu, 07 May 2020 08:43:28 -0400 by michaelh613