Ten new features of JAVA8

Posted by I WanT To Code PHP on Wed, 10 Nov 2021 15:46:33 +0100

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 extension method. An example is as follows:

interface Formula {
    double calculate(int a);

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

In addition to the calculate method, the Formula interface also defines the sqrt method. Subclasses that implement the Formula interface only need to implement one calculate method. The default method sqrt will be used directly on subclasses.

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. The code 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.

Lambda expression

First, let's look at how strings are arranged in the old version 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 comparator to the static method Collections.sort to sort in the specified order. The usual approach is to create an anonymous comparator 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);
});

You see, the code becomes more segmented and readable, but it can actually be 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 the return keyword, but you can also write it shorter:

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

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

Functional interface

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

We can treat lambda expression as any interface type that contains only one abstract method to ensure that your interface must meet this requirement. You only need to add @ functional interface annotation to your interface. The compiler will report an error if it finds that there are more than one abstract method on the interface with this annotation.

@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.

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 look at how constructors are referenced using the:: keyword. First, we define a simple class containing 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);
}

Here we use constructor references to associate them instead of implementing a complete factory:

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

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

Lambda scope

Accessing outer scopes in lambda expressions is similar to accessing anonymous objects in older versions. You can directly access external local variables marked final, or instance fields and static variables.

Accessing local variables

We can directly access the local variables of the outer layer 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 does not need to be declared final. 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 the following 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;

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

Accessing object fields and static variables

Different from local variables, lambda can read and write the fields of instances and static variables. 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);
        };
    }
}

Default method for provider

Remember the formula example in the first section? The interface formula defines a default method sqrt, which can be accessed directly by formula instances, including anonymous objects, but not in Lambda expressions. The default method cannot be accessed in Lambda expression, and the following code cannot be compiled:

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

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

Predict interface

The predict interface has only one parameter and returns boolean type. The interface contains a variety of default methods to combine predict 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, and then) 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 paradigm. Unlike the Function interface, this interface does not have any parameters

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

Consumer interface

The Consumer interface represents operations performed 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 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 is an auxiliary type used to prevent NullPointerException exceptions. This is an important concept to be used in the next session. Now let's take a brief look at what this interface can do:

Optional is defined as a simple container whose value may or may not be null. Before Java 8, generally a function should return a non empty object, but occasionally it may return null. In Java 8, it is not recommended that you 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 a sequence of operations that can be applied to a Set of elements at one time. Stream operations are 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. To create a stream, you need to specify a data source, such as a subclass of java.util.Collection, List or Set, which is not supported by Map. Stream operations can be performed serially or in parallel.

First, let's see how Stream is used. First, create the data List used in 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");

Java 8 extends the collection class. You can create a stream through Collection.stream() or Collection.parallelStream(). The following sections explain 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 requires a function to execute the filtered elements in turn. forEach is the 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 that 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 sorted Stream 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

The map mapping intermediate operation map will convert the elements into other objects in turn according to the specified Function interface. The following example shows how to convert a string to an uppercase string. You can also convert objects into 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 that allow you to detect whether a specified Predicate matches the entire stream. All matching operations are final and return a boolean value.

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 is a final operation that returns the number of elements in the Stream. 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, which allows multiple elements in the stream to be reduced to one element through the specified function. The results after exceeding the specification are represented through the Optional interface:

Optional<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 and parallel. The operations on the serial Stream are completed successively in one thread, while the parallel Stream is executed simultaneously on multiple threads.

The following example shows how parallel streams can improve performance:

First, we create a large table without duplicate 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 takes to sort this Stream. Serial 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));

//Serial time: 899 ms

Parallel sort:

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 takes 472 ms. the above two codes are almost the same, but the parallel version is as fast as 50%. The only change 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 deal with 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, and forEach receives a Consumer interface to operate 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, we show how to delete an item whose key values all match 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 is also easy to merge the elements of the Map:

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 values corresponding to the original key and re insert them into the map.

Date API

Java 8 contains 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 clock

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

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

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

Timezones time zone

In the new API, the time zone is represented by ZoneId. The time zone can be easily obtained by using the static method of. The time zone defines the time difference to UTS time, which is extremely important when converting from 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 without time zone information, such as 10 p.m. or 17:30:15. The following example creates two local times using the time zone created by the previous code. Then compare the time and calculate the time difference between the two times 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 a variety of factory methods to simplify object creation, 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

Local Date indicates an exact Date, such as March 11, 2014. The object value is immutable and is basically consistent with LocalTime. The following example shows how to add or subtract day / month / year 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 contents of the first two sections into one object. Like LocalTime and LocalDate, LocalDateTime is immutable. LocalDateTime provides some methods 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 the time zone information is attached, it can be converted into a point in time Instant object, which can be easily converted into an 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 the predefined format, we can also define the format ourselves:

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 time and date formats: http://download.java.net/jdk8/docs/api/java/time/format/DateTimeFormatter.html

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 group of specific Hints annotations:

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

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

Java 8 allows us to use the same type of annotation multiple times. We only need to mark @ Repeatable for the annotation.

Example 1: use wrapper class as 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 help you define the @ Hints annotation. Understanding this will help you use reflection to obtain this 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 do not define @ Hints annotation on the Person class, we can still obtain @ Hints annotation through getAnnotation(Hints.class). A more convenient way is to directly obtain all @ Hint annotations by using getAnnotationsByType. In addition, Java 8 annotations are added to two new target s:

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

Author: a deep sea like link into the code pit: https://www.jianshu.com/p/0bf8fe0f153b

Source: Jianshu

Topics: Java Back-end