Although Java8 has been around for a long time, and there are many features to make code more efficient and secure, most Java programmers still haven’t crossed the Java8 hurdle. Benjamin wrote this Java8 introduction in 2014, which I think is very good and may help you get over the Java8 hurdle.


This tutorial will walk you through the new features of Java8 step by step. In order, this article includes the following: default methods for interfaces, lambda expressions, method references, reusable annotations, and some API updates, Streams, functional interfaces, extensions to Map, and the new Date API.

There is no big text in this article, only annotated code snippets, hope you enjoy!

The default method of the interface

Java8 allows you to implement specific methods in an interface by prefixed the method with the default keyword. This feature is also known as the virtual extension method. Here’s the first example:

interface Formual {    
    double calculate(int a);
    default double sqrt(int a) {
        returnMath.sqrt(a); }}Copy the code

In the example above, the Formual interface defines a default method SQRT, and the interface’s implementation class implements the calculate method out of the box as long as it needs to.

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

The code above anonymously implements the Formual interface. The code is quite verbose, taking 6 lines to implement SQRT (a * 100). You can do this elegantly in the next section using Java8 features.

Lambda expressions

Let’s take a look at how sorting a List of strings was implemented in previous 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) {
        returnb.compareTo(a); }});Copy the code

The static method collection.sort takes a string List and a string Comparator to compare the string List passed in. The usual approach is to implement an anonymous Comparator and pass it into the sort method.

Compared to a lengthy implementation using anonymous methods, Java8 can be implemented in very short code using lambda expressions:

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

This code is already much shorter than the anonymous method, but it could be even shorter:

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

Sort (names, (a,b)->b.compareTo(a)); Can also be

The method is implemented in one line of code, omitting the {} and return keywords. But it could be even shorter:

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

The Java compiler can determine the type of the argument based on the context, so you can omit the type of the argument as well. Let’s explore further uses of lambda expressions.

Functional interface

How do lambda expressions and match Java’s type system? Every lambda expression is typed by the interface, so every functional interface declares at least one abstract method. The argument types of each lambda expression must match the arguments of this abstract method. Since the method identified by the default keyword is not an abstract method, you can add any number of default methods to the interface.

Note: Each lambda is a functional interface, so an interface using @functionInterface can only have one abstract method

You can treat any interface that contains only one abstract method as a lambda expression. To ensure that the interface meets the requirements, you need to annotate the @functionalInterface. If you annotate more than one virtual method on the interface, the compiler will report an error. Here’s an example:

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

But if you omit the @functionalInterface annotation, the code will work just fine.

Method references

The above example code can be further simplified by using static method references:

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

Java8 allows you to use :: to call references to static methods and constructors. The above code shows how to reference a static method. Object methods can also be referenced in the same way:

class Something {
    String startsWith(String s) {
        return String.valueOf(s.charAt(0)); }}Copy the code
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"
Copy the code

Note: The println referenced by system. out::println is not a static method because system. out is an object

Let’s take a look at how :: works on the constructor. First define a class Person with a different constructor:

class Person {
    String firstName;
    String lastName;

    Person() {}

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

Next, define a Person factory interface to create a new Person object:

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

Instead of implementing a factory manually, a new Person object is created using a reference to the constructor:

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

The constructor reference to the Person class is obtained via Person::new. The Java compiler then automatically selects the appropriate constructor based on the PersonFactory::create argument.

Note: Lambda, method references, and constructor references are all generated by instances of @functionalInterface. Only an abstract method’s interface defaults to an @functionalInterface. An @functionalInterface annotated interface can only have one abstract method.

Access scope for Lambda

Using lambda expressions to access external variables is much simpler than using objects implemented anonymously. Lambda expressions can access final, member, and static variables that are external to the local domain.

Accessing local variables

Lambda expressions can access external local final variables:

final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
stringConverter.convert(2);     / / 3
Copy the code

Unlike anonymous, the num variable can not be final, and the following code works:

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

stringConverter.convert(2);     / / 3
Copy the code

However, num is implicitly final during compilation, and the following code will have a compilation error:

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

You cannot change the value of num in a lambda expression.

Access member variables and static variables

In contrast to accessing local variables, member and static variables can be read and written in lambda expressions. This way of accessing variables is also implemented in anonymous variables:

class Lambda4 {
    static int outerStaticNum;
    int outerNum;

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

        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            returnString.valueOf(from); }; }}Copy the code

Note: External variables cannot be assigned within a lambda. If you need to get a value from a lambda, you can define a final array outside the lambda and bring the value out of the array.

Access the default interface method

Remember the Formula example? The Formula interface defines a default method SQRT that can be accessed in every instance of a Formula, including anonymously implemented objects. But the silent approach doesn’t work with lambda expressions.

The default method is not accessible through a lambda expression, and the following code does not compile:

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

Built-in functional interface

Java8 includes a number of built-in functional interfaces. There are some widely used interfaces such as Comparator and Runnable. These existing interfaces are all extended via @functionalInterface to support lambda expressions.

But Java8 also has some new functional interfaces that will make your code easier to write. Some of these are from the Google Guava library. Even if you are already familiar with the library, you should pay close attention to how the interfaces are extended by useful methods.

Predicates

Predicate is a Boolean function of one argument. This interface provides a number of default functions to combine into complex logical operations (and, and).

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();
Copy the code
Functions

Function takes a parameter and produces a result. The default method can be used in a chain of methods.

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

backToString.apply("123");     / / "123"
Copy the code
Suppliers

Supplier generates an object based on the given class attributes. Supplier does not support passing in parameters.

Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person
Copy the code
Consumers

The Consumer performs a series of predefined processes on the input parameters.

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

Java8 adds many default methods to the Comparator interface, which has been used in older versions of Java.

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
Copy the code
Optionals

Optional is not a functional interface, but a great way to eliminate NullPointerExceptions. That’s what the next section is going to focus on, but let’s look at how Optional works.

Optional is a container that contains a value that may or may not be null. Consider that a method may return a value other than null, or it may return nothing at all. In Java8, you can make it not return null, or return an Optional object.

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"
Copy the code

Note: Each of these built-in functional interfaces is annotated with @funcationalInterface, which is a kind of syntactically sweet, providing a convenient way for different types of functional methods to be defined without having to start over, and requiring different functional interfaces in subsequent stages of Stream programming. These built-in interfaces are also ready for Stream programming.

Streams

A java.util.Stream represents a list of elements that can perform one or more operations. Stream operations can be intermediate or terminal operations. Terminal operations return type-determined results. The intermediate operation returns the Stream object itself, which can continue to call other chains of methods within the same line of code.

Stream objects can be created from java.util.Collection objects, and streams can support concatenation and parallelism compared to lists and sets (maps are not currently supported).

Let’s first look at concatenation, creating a Stream from the List object:

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");
Copy the code

Java8 of Collections have been expanded, can pass Collections. Stream () or Collections. ParallelStream () to create the stream object, The following sections describe the most common Stream operations.

Filter

A Filter accepts a Predicate to Filter all the elements in the Stream. This operation is an intermediate operation that calls another Stream operation (for example, forEach) on the result of the filter. ForEach takes a Consumer parameter and executes it on each filtered Stream element. ForEach is a terminal operation, so no other Stream operation can be called after this operation.

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

Note: Each stream can no longer continue intermediate operations such as filter after performing terminal operations such as forEach.

Sorted

Sorted is an intermediate operation that returns a Sorted Stream. If no custom Comparator is passed in, the elements will be sorted in their natural order.

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

Need to be aware of is Sorted will only ordered by convection elements inside, rather than changing the order of the elements in the original collection, the execution Sorted after operation, the order of the elements in a stringCollection hasn’t changed:

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

Map is an intermediate operation that turns each element in the Stream into another object according to a given function. The following example shows how to convert each string to uppercase. You can also use a Map to convert each element into another type. The Stream type depends on the type returned by the method you pass into the Map.

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Copy the code
Match

Various Match operations can be used to determine whether a given Predicate matches an element in the Stream. The Match operation is a terminal operation that returns 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
Copy the code
Count

Count is a terminal operation that returns a long representing the number of elements in the Stream.

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

System.out.println(startsWithB);    / / 3
Copy the code
Reduce

Reduce is a terminal operation that operates on all elements of the Stream based on the given method and returns an Optional value.

Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
Copy the code

Note: ifPresent takes an object of type Consumer, system. out::println is a method reference, and println is a function that takes an argument and returns no value, which fits the definition of Consumer.

Parallel Streams

As mentioned above, streams can be concatenated or parallel. Serial operation of Stream is performed on a single thread, and parallel operation is performed concurrently on multiple threads.

The following example shows how to use parallel Stream to improve program performance.

First initialize a list with many elements, each of which is unique:

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

Next, test the time it takes to concatenate and parallel Stream the list, respectively.

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

// sequential sort took: 899 ms
Copy the code

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 sort took: 472 ms
Copy the code

As it turns out, running this almost identical code makes parallel sorting about 50% faster, and you just have to change stream() to parallelStream().

Map

As mentioned earlier, Map does not support Stream, but maps already support a number of new and useful ways to accomplish common 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));
Copy the code

As you can see from the above code, putIfAbsent doesn’t have to check for null, and forEach accepts a Consumer to iterate over each element in the map.

The following code shows how to make the map’s built-in methods compute:

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
Copy the code

Let’s learn how to delete the value of a key, only if the input value is equal to the value in the Map:

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

map.remove(3."val33");
map.get(3);             // null
Copy the code

The following method is also useful:

map.getOrDefault(42."not found");  // not found
Copy the code

Merging values in a Map is also fairly simple:

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
Copy the code

If the value for the current key does not exist, the input value is put directly into the Map, otherwise the Merge function is called to change the existing value.

Date API

Java8 has a new date and time API under the java.time package. These new date apis are comparable to Joda-time, but not quite the same. The following includes the most important parts of these new apis.

Clock

The Clock class can be used to access the current date and time. Clock can get the current time zone. Instead of System.currentTimemillis (), Clock can get the current number of milliseconds. Moments on the current timeline can be represented using the Instant class, which can also create the original 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
Copy the code
Timezones

The time zone is represented by the zoneId, which can be accessed through static factory methods. The time zone class also defines an offset to convert between the current time or time and the time in the destination time zone.

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]
Copy the code
LocalTime

LocalTime indicates a time without a time zone, for example, 10PM or 17:30:15. The following example creates two local times for the previously defined time zone. Then compare the two times and calculate the 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
Copy the code

Local time can be used to create instances using a number of factory methods, including converting strings to get instances:

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
Copy the code
LocalDate

LocalDate indicates a specific date, such as 2017-03-11. It’s immutable, it’s exactly the same as LocalTime. The following example shows how to add or subtract days, months, or years to a date. Note that each computation 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
Copy the code

Changing LocalDate from a string is as simple as changing 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
Copy the code
LocalDateTime

LocalDateTime represents a specific date and time, which combines the date and time in the example above. LocalDateTime is immutable and used in the same way as LocalDate and LocalTime. Methods can be used to get certain properties in the LocalDateTime instance.

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
Copy the code

Other information about a time zone can be converted from Instant objects. Instant instances can be easily converted to java.util.Date objects.

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

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

Formatting a LocalDateTime object is the same as formatting a LocalDate and LocalTime object; you can use a custom format instead of defining it in advance.

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
Copy the code

Unlike java.text.NumberFormat, the new DateTimeFormatter is immutable and thread-safe.

See here for more formatting syntax.

annotations

Annotations in Java8 are reusable, and here are a few examples to illustrate this feature.

First, define an annotation wrapper that wraps an array of annotations:

@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.TYPE})
@interface Hints {
    Hint[] value();
}

@Repeatable(Hints.class)
@interface Hint {
    String value(a);
}
Copy the code

Java8 allows multiple annotations on the same type with @REPEATable.

Old use: Annotate using containers

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

New usage: Use reusable annotations

@Hint("hint1")@Hint("hint2")
class Person {}
Copy the code

The Java compiler implicitly uses the @Hints annotation when using the new usage. This is important for reading annotations 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
Copy the code

Although the @hints annotation is not declared on the Person class, it is available through the getAnnotation(hints.class). However, it is more convenient to get all the @hints directly through getAnnotationByType.

In addition, using annotations in Java8 can be extended to two new targets

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

(after)

The original

Follow the wechat official account and talk about other things