New in Java 8
- Transfer
Java has not died yet - and people are beginning to understand this.
Welcome to Java 8 tutorial . This material will introduce you step by step to all the new features of the language. You will learn how to use default interface methods, lambda expressions, method references, and repeatable annotations. All this will be followed by short and simple code examples. At the end of the article, you will get acquainted with the most recent changes in the API regarding streams, functional interfaces, extensions for associative arrays, as well as changes in the API for working with dates.
Default Interface Methods
Java 8 allows you to add non-abstract method implementations to an interface using the keyword
default
. This feature is also known as extension methods. Here is our first example:interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
In addition to the abstract method, the
calculate
interface Formula
also defines the default method sqrt
. Classes implementing this interface should override only the abstract method calculate
. The default method sqrt
will be available without overriding.Formula formula = new Formula() {
@Override
public double calculate(int a) {
return sqrt(a * 100);
}
};
formula.calculate(100); // 100.0
formula.sqrt(16); // 4.0
formula
implemented as an anonymous object. This code is pretty redundant: as many as 6 lines for such a simple calculation as sqrt(a * 100)
. In the next section, we will see that in Java 8 there is a much more elegant way to implement objects with a single method.Lambda expressions
Let's start with a simple example: sorting an array of strings in previous versions of the language.
List names = Arrays.asList("peter", "anna", "mike", "xenia");
Collections.sort(names, new Comparator() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});
The static method
Collections.sort
accepts a list and a comparator, which is used to sort the list. Surely you often had to create anonymous comparators in order to pass them to a method. Java 8 provides a much shorter syntax, lambda expressions, so you don't have to waste time creating anonymous objects:
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});
As you can see, the code is much shorter and much more readable. And it can be made even shorter:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
For single-line methods, you can omit the brackets {} and the keyword
return
. So even shorter:Collections.sort(names, (a, b) -> b.compareTo(a));
The compiler knows the types of parameters, so they can also be omitted. Let's see how else lambda expressions can be used.
Functional Interfaces
How do lambda expressions match the Java language type system? Each lambda corresponds to a type represented by an interface. The so-called functional interface must contain exactly one abstract method . Each lambda expression of this type will be associated with a declared method. Also, since default methods are not abstract, you can add as many methods as you like to the functional interface.
We can use any interfaces for lambda expressions that contain exactly one abstract method. An annotation is used to ensure that your interface meets this requirement.
@FunctionalInterface
. The compiler is aware of this annotation and will throw a compilation error if you add a second abstract method to the functional interface. Example:
@FunctionalInterface
interface Converter {
T convert(F from);
}
Converter converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted); // 123
Keep in mind that this code will remain valid even if you remove the annotation
@FunctionalInterface
.References to Methods and Constructors
The previous example can be simplified by using static method references:
Converter converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted); // 123
Java 8 allows you to pass references to methods or constructors. To do this, use the keyword
::
. The previous example illustrates passing a reference to a static method. However, we can also refer to the instance method:class Something {
String startsWith(String s) {
return String.valueOf(s.charAt(0));
}
}
Something something = new Something();
Converter converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"
Let's see how to pass references to constructors. First, define a bean with several constructors:
class Person {
String firstName;
String lastName;
Person() {}
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Then we define the interface of the factory, which will be used to create new persons:
interface PersonFactory
{ P create(String firstName, String lastName); }
Now, instead of implementing the interface, we connect everything together with a reference to the constructor:
PersonFactory personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
We create a link to the constructor using
Person::new
. The compiler automatically selects the appropriate constructor whose signature matches the signature PersonFactory.create
.Lambda Scopes
Access to external scope variables from a lambda expression is very similar to access from anonymous objects. You can refer to variables declared as
final
instance class fields and static variables.final int num = 1;
Converter stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
But unlike anonymous objects, a variable
num
does not have to be declared as final
. Such code will work too:int num = 1;
Converter stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
However, the variable
num
must still remain unchanged. The following code does not compile:int num = 1;
Converter stringConverter = (from) -> String.valueOf(from + num);
num = 3;
Writing to a variable
num
within a lambda expression is also prohibited.Access to fields and static variables
Unlike local variables, we can write values to instance fields of the class and static variables inside lambda expressions. This behavior is well known by anonymous objects.
class Lambda4 {
static int outerStaticNum;
int outerNum;
void testScopes() {
Converter stringConverter1 = (from) -> {
outerNum = 23;
return String.valueOf(from);
};
Converter stringConverter2 = (from) -> {
outerStaticNum = 72;
return String.valueOf(from);
};
}
}
Access to default interface methods
Remember the example with the formula from the first section? The interface
Formula
defines a default method sqrt
, which is accessible from each implementation of the interface, including anonymous objects. However, this will not work in lambda expressions. Inside lambda expressions, it is forbidden to access the default methods. The following code does not compile:
Formula formula = (a) -> sqrt( a * 100);
Integrated Functional Interfaces
JDK 1.8 has many built-in functional interfaces. Some of them are well known from previous versions of the language, for example,
Comparator
or Runnable
. All of these interfaces were supported in lambdas by adding annotations @FunctionalInterface
. However, Java 8 also introduced many new functional interfaces that are designed to make your life easier. Some interfaces are well known in the Google Guava library. Even if you are unfamiliar with this library, you should take a look at how these interfaces have been supplemented with some useful extension methods.
Predicates
Predicates are functions that take a single argument and return a value of type boolean. The interface contains a variety of methods by default, allow to build complex conditions (
and
, or
, negate
).Predicate predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate nonNull = Objects::nonNull;
Predicate isNull = Objects::isNull;
Predicate isEmpty = String::isEmpty;
Predicate isNotEmpty = isEmpty.negate();
Functions
Functions take one argument and return some result. Default methods can be used to build call chains (
compose
, andThen
).Function toInteger = Integer::valueOf;
Function backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
Suppliers
Suppliers provide a result of a given type. Unlike functions, providers do not accept arguments.
Supplier personSupplier = Person::new;
personSupplier.get(); // new Person
Consumers
Consumers (consumers) are operations that are performed on a single input argument.
Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
Comparators
Comparators are well known in previous versions of Java. Java 8 adds various default methods to the interface.
Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");
comparator.compare(p1, p2); // > 0
comparator.reversed().compare(p1, p2); // < 0
Optional values
Optionals are not functional interfaces, but are a convenient way to prevent a NullPointerException. This is an important concept that we will need in the next section, so let's take a look at how the optional values work.
An optional value is essentially a container for a value that can be null. For example, you need a method that returns some value, but sometimes it must return an empty value. Instead of returning null, in Java 8 you can return an optional value.
Optional optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
Streams
A type
java.util.Stream
is a sequence of elements over which various operations can be performed. Operations on threads are either intermediate (intermediate) or final (terminal). Final operations return a result of a certain type, and intermediate operations return the same stream. Thus, you can build chains of several operations on the same stream. A stream is created based on sources, such as types that implement java.util.Collection
, such as lists or sets (associative arrays are not supported). Operations on threads can be performed both sequentially and in parallel. First, let's see how to work with a stream sequentially. First, create a source as a list of lines:
List stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");
In Java 8, you can quickly create threads using calls
Collection.stream()
or Collection.parallelStream()
. The following sections explain the most common thread operations.Filter
The Filter operation accepts a predicate that filters all elements of the stream. This operation is intermediate , i.e. allows us to call another operation (for example
forEach
) on the result. ForEach accepts a function that is called for each element in the (already filtered) stream. ForEach is the ultimate operation. It does not return any value, so further call to stream operations is impossible.stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa2", "aaa1"
Sorted
The Sorted operation is an intermediate operation that returns a sorted representation of a stream. Elements are sorted in the usual way if you have not provided your comparator:
stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa1", "aaa2"
Remember that it
sorted
only creates a sorted view and does not affect the order of elements in the original collection. The order of the lines in stringCollection
remains untouched:System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map
An intermediate operation
map
converts each element to another object using the function passed. The following example converts each string to a uppercase string. However, you can also use map
to convert each object to an object of a different type. The type of resultant stream depends on the type of function that you pass in the call map
.stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Match
To check whether a stream satisfies a given predicate, various match operations are used. All matching operations are finite and return a result of type boolean.
boolean anyStartsWithA =
stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA); // true
boolean allStartsWithA =
stringCollection
.stream()
.allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA); // false
boolean noneStartsWithZ =
stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ); // true
Count
The operation
Count
is the final operation and returns the number of elements in the stream. The return type is long
.long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();
System.out.println(startsWithB); // 3
Reduce
This final operation convolves the flow elements according to a given function. The result is an optional value.
Optional reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
Parallel threads
As mentioned above, threads can be sequential and parallel. Operations on serial threads are performed in one processor thread, on parallel threads - using several processor threads.
The following example demonstrates how you can easily increase speed using parallel threads.
First, create a large list of unique elements:
int max = 1000000;
List values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
UUID uuid = UUID.randomUUID();
values.add(uuid.toString());
}
Now we’ll measure the sorting time of this list.
Sequential sorting
long t0 = System.nanoTime();
long count = values.stream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));
// sequential sort took: 899 ms
Parallel sorting
long t0 = System.nanoTime();
long count = values.parallelStream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));
// parallel sort took: 472 ms
As you can see, both pieces of code are almost identical, however parallel sorting is almost twice as fast. All you have to do is replace the call
stream()
with parallelStream()
.Associative arrays
As already mentioned, associative arrays (maps) do not support streams. Instead, associative arrays now support various useful methods that solve common tasks.
Map map = new HashMap<>();
for (int i = 0; i < 10; i++) {
map.putIfAbsent(i, "val" + i);
}
map.forEach((id, val) -> System.out.println(val));
This code does not need special comments:
putIfAbsent
it allows us not to write additional checks for null; forEach
takes a consumer who performs an operation on each element of the array. This code shows how to use code for calculations using various functions:
map.computeIfPresent(3, (num, val) -> val + num);
map.get(3); // val33
map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9); // false
map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23); // true
map.computeIfAbsent(3, num -> "bam");
map.get(3); // val33
Then we learn how to delete an object by key only if this object is associated with a key:
map.remove(3, "val3");
map.get(3); // val33
map.remove(3, "val33");
map.get(3); // null
Another useful method:
map.getOrDefault(42, "not found"); // not found
Merge records of two arrays? Easy:
map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9); // val9
map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9); // val9concat
In the absence of a key,
Merge
creates a new key-value pair. Otherwise, it calls the join function for the existing value.Date API
Java 8 contains a completely new API for working with dates and times, located in the package
java.time
. The new API is comparable to the Joda-Time library , but there are differences . The following sections describe the most important parts of the new API.Clock
The type
Clock
provides access to the current date and time. This type knows about time zones and can be used instead of a call System.currentTimeMillis()
to return milliseconds. Such an exact date can also be represented by a class Instant
. Objects of this class can be used to create objects of an obsolete type java.util.Date
.Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant); // legacy java.util.Date
Time Zones
Time zones are represented by type
ZoneId
. They can be accessed using static factory methods. Time zones contain offsets that are important for converting dates and times to local ones.System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids
ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
Localtime
The type
LocalTime
represents time, taking into account the time zone, for example, 10pm or 17:30:15. The following example creates two local times for the time zones defined above. Then both times are compared, and the difference between them in hours and minutes is calculated.LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2)); // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween); // -3
System.out.println(minutesBetween); // -239
The LocalTime type contains various factory methods that make it easy to create new instances, as well as parsing strings.
LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime); // 13:37
Localdate
A type
LocalDate
represents a specific date, for example, 2014-03-11. Objects are LocalDate
immutable and are analogous LocalTime
. The example demonstrates calculating a new date by adding or subtracting days, months, or years. Remember that each operation returns a new instance.LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY
Creating an instance
LocalDate
by parsing a string:DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.GERMAN);
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas); // 2014-12-24
Localdatetime
A type
LocalDateTime
is a combination of date and time. Objects are LocalDateTime
immutable and work similarly to LocalTime
and LocalDate
. We can use various methods to extract specific values from a date-time:LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek); // WEDNESDAY
Month month = sylvester.getMonth();
System.out.println(month); // DECEMBER
long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay); // 1439
By adding time zone information we can get
Instant
.Instant instant = sylvester
.atZone(ZoneId.systemDefault())
.toInstant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
Date-time formatting works the same as date or time formatting. We can use library or our own templates.
DateTimeFormatter formatter =
DateTimeFormatter
.ofPattern("MMM dd, yyyy - HH:mm");
LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string); // Nov 03, 2014 - 07:13
Unlike
java.text.NumberFormat
, the new one DateTimeFormatter
is immutable and thread safe. Details about the syntax of the templates can be found here .
Annotations
Annotations in Java 8 are repeatable. Let's see an example right away to understand what it is.
First, we define a wrapper annotation that contains an array of annotations:
@interface Hints {
Hint[] value();
}
@Repeatable(Hints.class)
@interface Hint {
String value();
}
Java 8 allows us to use many annotations of the same type by specifying annotations
@Repeatable
. Option 1: use container annotation (old way)
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}
Option 2: use repeatable annotation (new way)
@Hint("hint1")
@Hint("hint2")
class Person {}
When using option 2, the compiler automatically inserts the annotation
@Hints
. This is important when reading annotation information through reflection.Hint hint = Person.class.getAnnotation(Hint.class);
System.out.println(hint); // null
Hints hints1 = Person.class.getAnnotation(Hints.class);
System.out.println(hints1.value().length); // 2
Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
System.out.println(hints2.length); // 2
Although we never declared the @Hints annotation in the class
Person
, it is available to us when called getAnnotation(Hints.class)
. However, a method getAnnotationsByType
that directly provides access to all annotations is more convenient @Hint
. Moreover, annotations in Java 8 can be used on two more elements:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
That's all
This completes the introduction to Java 8 programming. You have to yourself to explore other new JDK 1.8, for example,
Arrays.parallelSort
, StampedLock
, CompletableFuture
and others. The full source code of the article is available on GitHub .