“Java is still not dead―and people are starting to figure that out.”
This tutorial will use simple annotated code to describe new features, and you will not see blockbuster text.
1. The default method of the interface
Java 8 allows us to add a non-abstract method implementation to the interface, just use the default keyword. This feature is also called an extension method. The example is as follows:
The code copy is as follows:
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
In addition to having a calculated method, the Formula interface also defines the sqrt method. Subclasses that implement the Formula interface only need to implement a calculated method, and the default method sqrt will be used directly on the subclass.
The code copy is as follows:
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 the article is implemented as an instance of an anonymous class. The code is very easy to understand. 6 lines of code implement the calculation of sqrt(a * 100). In the next section, we will see a simpler approach to implementing a single-method interface.
Translator's note: There is only single inheritance in Java. If you want to assign new features to a class, you usually use interfaces to implement it. Multiple inheritance is supported in C++, allowing a subclass to have multiple parent classes at the same time. In other languages, the method of having a class have other reusable code at the same time is called mixin. This new Java 8 special is closer to Scala's trait from the perspective of compiler implementation. There is also a concept called extension method in C#, which allows existing types to be extended, which is semantically different from this in Java 8.
2. Lambda expressions
First, let's take a look at how strings are arranged in the old version of Java:
The code copy is as follows:
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 a List object and a comparator to the static method Collections.sort to arrange it in the specified order. It is usually done to create an anonymous comparator object and pass it to the sort method.
In Java 8, you don't need to use this traditional method of anonymous objects. Java 8 provides a more concise syntax, lambda expression:
The code copy is as follows:
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:
The code copy is as follows:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
For function body with only one line of code, you can remove the braces {} and return keywords, but you can write it shorter:
The code copy is as follows:
Collections.sort(names, (a, b) -> b.compareTo(a));
The Java compiler can automatically deduce parameter types, so you don't have to write the type again. Next, let's see what more convenient lambda expressions can do:
3. Functional interface
How do Lambda expressions be represented in Java's type system? Each lambda expression corresponds to a type, usually an interface type. "Functional Interface" refers to an interface that only contains an abstract method, and each 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 default methods to your functional interface.
We can treat lambda expressions as any interface type that contains only one abstract method to ensure that your interface must meet this requirement. You only need to add the @FunctionalInterface annotation to your interface. If the compiler finds that you have marked the interface with this annotation, the compiler will find that the interface you marked with is annotated by this annotation. There will be an error when there is more than one abstract method.
Examples are as follows:
The code copy is as follows:
@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
It should be noted that if @FunctionalInterface is not specified, the above code is also correct.
Translator's note maps lambda expressions to a single-method interface. This practice has been implemented in other languages before Java 8, such as the Rhino JavaScript interpreter. If a function parameter receives a single-method interface and you pass it It is a function. The Rhino interpreter will automatically make an instance of a single interface to a function adapter. Typical application scenarios include the addEventListener of org.w3c.dom.events.EventTarget. The second parameter EventListener.
4. Method and constructor reference
The code in the previous section can also be represented by static method references:
The code copy is as follows:
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 a method or constructor reference. The above code shows how to reference a static method, and we can also refer to an object's method:
The code copy is as follows:
converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"
Let’s take a look at how constructors are referenced using the :: keyword. First, we define a simple class with multiple constructors:
The code copy is as follows:
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 Person objects:
The code copy is as follows:
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:
The code copy is as follows:
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
We only need to use Person::new to get a reference to the Person class constructor. The Java compiler will automatically select the appropriate constructor based on the signature of the PersonFactory.create method.
5. Lambda scope
The way to access outer scopes in lambda expressions is similar to that in older versions of anonymous objects. You can directly access external local variables marked final, or fields and static variables of the instance.
6. Access local variables
We can access external local variables directly in lambda expressions:
The code copy is as follows:
final int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
But unlike anonymous objects, the variable num here can be declared as final without declaring it as final, and the code is also correct:
The code copy is as follows:
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
However, num here must not be modified by the subsequent code (that is, implicitly has final semantics), for example, the following cannot be compiled:
The code copy is as follows:
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
num = 3;
Trying to modify num in lambda expressions is also not allowed.
7. Access object fields and static variables
Unlike local variables, the fields and static variables within the lambda can be read and written. This behavior is consistent with anonymous objects:
Copy the code as follows: 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. The default method to access the interface
Remember the formula example in the first section. The interface Formula defines a default method sqrt that can be accessed directly by anonymous objects in formula instances including anonymous objects, but this does not work in lambda expressions.
The default method cannot be accessed in the Lambda expression, and the following code will not be compiled:
The code copy is as follows:
Formula formula = (a) -> sqrt( a * 100);
Built-in Functional Interfaces
The JDK 1.8 API contains many built-in functional interfaces, such as Comparator or Runnable interfaces commonly used in old Java, and these interfaces have added @FunctionalInterface annotation to be used on lambdas.
The Java 8 API also provides many new functional interfaces to make work more convenient. Some interfaces are from Google Guava library. Even if you are familiar with these, it is still necessary to see how these are extended to lambdas. Used.
Predicate interface
The Predicate interface has only one parameter, which returns the boolean type. This interface contains a number of default methods to combine Predicate into other complex logics (such as: versus, or, non):
The code copy is as follows:
Predicate<String> predicate = (s) -> s.length() > 0;
predict.test("foo"); // true
predict.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, and comes with some default methods (compose, andThen) that can be combined with other functions:
The code copy is as follows:
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 type. Unlike the Function interface, the interface does not have any parameters.
The code copy is as follows:
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
Consumer interface
The Consumer interface represents the operation performed on a single parameter.
The code copy is as follows:
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, and Java 8 has added a variety of default methods on it:
The code copy is as follows:
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. This 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 be null or not. Before Java 8, a function should generally return a non-empty object, but occasionally it may return null. In Java 8, it is not recommended that you return null but Optional.
The code copy is as follows:
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 two types: intermediate operations or final operations. The final operations return a specific type of calculation result, while the intermediate operations return the Stream itself, so that you can string multiple operations together in sequence. The creation of Stream requires specifying a data source, such as a subclass of java.util.Collection, List or Set, which is not supported by Map. Stream operations can be executed serially or in parallel.
First, let’s see how Stream is used. First, create the data list used to create the instance code:
The code copy is as follows:
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, and can create a Stream through Collection.stream() or Collection.parallelStream(). The following sections will explain the commonly used Stream operations in detail:
Filter Filter
Filtering is filtered through a predicate interface and only elements that meet the criteria are kept. This operation is an intermediate operation, so we can apply other Stream operations (such as forEach) in the filtered results. forEach requires a function to execute filtered elements in sequence. forEach is a final operation, so we cannot perform other Stream operations after forEach.
The code copy is as follows:
stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa2", "aaa1"
Sort Sort
Sort is an intermediate operation, and the returned Stream after sorting is returned. If you do not specify a custom Comparator, the default sort will be used.
The code copy is as follows:
stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa1", "aaa2"
It should be noted that sorting only creates a arranged Stream, and will not affect the original data source. After sorting, the original data stringCollection will not be modified:
The code copy is as follows:
System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map Mapping
The intermediate operation map converts elements into additional objects in sequence according to the specified Function interface. The following example shows converting a string to a capital string. You can also use map to convert objects to other types. The Stream type returned by map is determined based on the return value of the function passed in by your map.
The code copy is as follows:
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Match
Stream provides a variety of matching operations, allowing detection of whether a specified Predicate matches the entire Stream. All matching operations are final operations and return a value of type boolean.
The code copy is as follows:
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 nonStartsWithZ =
stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ); // true
Count
Counting is a final operation, which returns the number of elements in the Stream, and the return value type is long.
The code copy is as follows:
long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();
System.out.println(startsWithB); // 3
Reduce Regulations
This is a final operation, allowing multiple elements in the stream to be defined as one element through the specified function, and the result of the eligibility is represented by the Optional interface:
The code copy is as follows:
Optional<String> reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bb1#bb2#bb3#ccc#ddd1#ddd2"
Parallel Streams
As mentioned earlier, Stream has two types: serial and parallel. The operations on a serial Stream are completed in sequence in one thread, while parallel Stream is executed simultaneously on multiple threads.
The following example shows how to improve performance through parallel Stream:
First we create a large table without duplicate elements:
The code copy is as follows:
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 Sort:
The code copy is as follows:
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:
The code copy is as follows:
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 time: 472 ms
The above two codes are almost the same, but the parallel version is as fast as 50%. 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.
The code copy is as follows:
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 require 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:
The code copy is as follows:
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, show how to delete an item in the Map that matches all keys:
The code copy is as follows:
map.remove(3, "val3");
map.get(3); // val33
map.remove(3, "val33");
map.get(3); // null
Another useful method:
The code copy is as follows:
map.getOrDefault(42, "not found"); // not found
It's also easy to merge the elements of the Map:
The code copy is as follows:
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 reinsert it into the map.
9. Date API
Java 8 contains a brand 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 example shows some of the most important parts of this new API:
Clock clock
The Clock class provides a method to access the current date and time. Clock is time zone sensitive and can be used instead of System.currentTimeMillis() to get the current number of 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.
The code copy is as follows:
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 using the static method of. The time zone defines the time difference to UTS time, which is extremely important when converting between the Instant time point object and the local date object.
The code copy is as follows:
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 pm, or 17:30:15. The following example creates two local times using the time zone created by the previous code. The time is then compared and the time difference between the two times is calculated in hours and minutes:
The code copy is as follows:
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.
The code copy is as follows:
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 and is basically the same as that of LocalTime. The following example shows how to add/moon/year to a Date object. Also note that these objects are immutable, and the operation returns a new instance.
The code copy is as follows:
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:
The code copy is as follows:
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 the merge of the contents of the first two sections into one object. LocalDateTime, like LocalTime and LocalDate, are both immutable. LocalDateTime provides some methods to access specific fields.
The code copy is as follows:
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
Just attach the time zone information and it can be converted into a point-of-time Instant object. The Instant point-of-time object can be easily converted to the old-fashioned java.util.Date.
The code copy is as follows:
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 the format ourselves:
The code copy is as follows:
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 version of DateTimeFormatter is immutable, so it is thread-safe.
Detailed information about time and date format: http://download.java.net/jdk8/docs/api/java/time/format/DateTimeFormatter.html
10. Annotation Notes
Multiple annotations are supported in Java 8. Let’s first look at an example to understand what it means.
First, define a packaged Hints annotation to place a specific set of Hint annotations:
The code copy is as follows:
@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 label the annotation @Repeatable.
Example 1: Use wrapper class as a container to store multiple annotations (old method)
The code copy is as follows:
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}
Example 2: Using multiple annotations (new method)
The code copy is as follows:
@Hint("hint1")
@Hint("hint2")
class Person {}
In the second example, the java compiler will implicitly define the @Hints annotation for you. Understanding this will help you use reflection to obtain this information:
The code copy is as follows:
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 get @Hints annotation through getAnnotation(Hints.class). The more convenient way is to use getAnnotationsByType to directly get all @Hint annotations.
In addition, Java 8 annotations have been added to two new targets:
The code copy is as follows:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
That’s all about the new features of Java 8, and there are definitely more features waiting to be discovered. There are many useful things in JDK 1.8, such as Arrays.parallelSort, StampedLock and CompletableFuture.