Java 8 Features
- stream processing
- functional programming
- default methods in interface
- method reference: :: syntax
- anonymous functions, lambda expression
- functional interface: an interface that specifies exactly one abstract method.
Default Methods for Interafces
Default methods are a new feature introduced in Java 8 that allows interfaces to have implementation for methods. Prior to Java 8, interfaces could only define method signatures, but not provide implementation for them.
Default methods provide a way to add new methods to an interface without breaking the existing code that implements the interface. When a new method is added to an interface, all the classes that implement the interface need to be updated to provide an implementation for the new method.
However, with default methods, you can provide a default implementation that will be used if the implementing class does not provide its own implementation.
Here are some characteristics of default methods in Java 8:-
Default methods can be declared with the default keyword, and they must have a body.
-
Default methods can be overridden by classes that implement the interface.
-
Default methods can be inherited like other interface methods.
-
Default methods cannot be marked as abstract or final.
-
Default methods can access other default methods of the same interface.
-
Default methods cannot access instance variables or methods of the implementing class.
-
Default methods can be used to provide a default implementation for existing interfaces without breaking the existing code.
-
Default methods are useful for evolving existing interfaces to support new functionality without breaking the existing implementations. They enable backward compatibility and provide a way to write more modular and maintainable code in Java 8 and beyond.
Lambda Expressions
Lambda expressions are a new feature introduced in Java 8 that allows you to write concise and powerful functional code in Java. A lambda expression is a way to create an anonymous function that can be passed around as a parameter or returned as a result.
The syntax of a lambda expression consists of three parts:
-
A comma-separated list of parameters enclosed in parentheses (if the function takes no parameters, the parentheses can be omitted).
-
The arrow symbol ->, which separates the parameter list from the body of the function.
-
The body of the function, which can be a single expression or a block of statements enclosed in curly braces.
For example, here is a lambda expression that takes two integer parameters and returns their sum:
(int x, int y) -> x + y
Lambda expressions can be used in a variety of contexts, such as functional interfaces, streams, and parallel processing. They provide a way to write functional code that is concise and easy to read, while also promoting better performance and scalability.
Lambda expressions are an important addition to the Java language, and they enable developers to write more concise and expressive code that is better suited to modern software development practices.
Functional Interfaces
Functional interfaces are a new feature introduced in Java 8 that represent a single abstract method (SAM) interfaces, which are used to define contracts for functional programming (In functional programming, functions are first-class objects that can be passed around as arguments, returned as results, and composed into higher-order functions.) in Java. Functional interfaces provide a way to treat functions as first-class objects in Java, which can be passed around as arguments, returned as results, and composed into higher-order functions.
Here are some characteristics of functional interfaces in Java:-
Functional interfaces have exactly one abstract method.
-
Functional interfaces can have any number of default or static methods.
-
Functional interfaces can be annotated with the @- FunctionalInterface annotation to ensure that they only have one abstract method. This annotation is optional, but it can help catch errors at compile time.
-
Functional interfaces can be used with lambda expressions and method references to create - functional objects that can be passed around as arguments, returned as results, or composed into higher-order functions.
Some examples of - functional interfaces in Java include Runnable, Callable, Consumer, Predicate, Function, and Supplier. These interfaces define a single abstract method and can be used with lambda expressions to create - functional objects.
- Functional interfaces are an important feature in Java 8 and beyond, as they enable functional programming paradigms in Java and provide a way to write more concise and expressive code that is better suited to modern software development practices.
Functional Interface Example:
@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
Keep in mind that the code is also valid if the @FunctionalInterface annotation would be omitted.
Built-in Functional Interfaces
- Predicates
Predicates are boolean-valued functions of one argument. The interface contains various default methods for composing predicates to complex logical terms (and, or, negate)
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();
- Functions
Functions accept one argument and produce a result. Default methods can be used to chain multiple functions together (compose, andThen).
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
- Suppliers
Suppliers produce a result of a given generic type. Unlike Functions, Suppliers don't accept arguments.
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
- Consumers
Consumers represent operations to be performed on a single input argument.
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
- Comparators
Comparators are well known from older versions of Java. Java 8 adds various default methods to the interface.
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
- Optionals
Optionals are not functional interfaces, but nifty utilities to prevent NullPointerException. It's an important concept for the next section, so let's have a quick look at how Optionals work.
Optional is a simple container for a value which may be null or non-null. Think of a method which may return a non-null result but sometimes return nothing. Instead of returning null you return an Optional in Java 8.
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"
- Streams
The Java Stream API provides a functional approach to processing collections of objects. The Java Stream API was added in Java 8 along with several other functional programming features.
The Java Stream API is not related to the Java InputStream and Java OutputStream of Java IO. The InputStream and OutputStream are related to streams of bytes. The Java Stream API is for processing streams of objects - not bytes.
A Java Stream is a component that is capable of internal iteration of its elements, meaning it can iterate its elements itself. In contrast, when you are using the Java Collections iteration features (e.g a Java Iterator or the Java for-each loop used with a Java Iterable) you have to implement the iteration of the elements yourself.
Stream Processing
You can attach listeners to a Stream. These listeners are called when the Stream iterates the elements internally. The listeners are called once for each element in the stream. That way each listener gets to process each element in the stream. This is referred to as stream processing.
The listeners of a stream form a chain. The first listener in the chain can process the element in the stream, and then return a new element for the next listener in the chain to process. A listener can either return the same element or a new, depending on what the purpose of that listener (processor) is.
Obtain a Stream
There are many ways to obtain a Java Stream. One of the most common ways to obtain a Stream is from a Java Collection. Here is an example of obtaining a Stream from a Java List:
List<String> items = new ArrayList<String>();
items.add("one");
items.add("two");
items.add("three");
Stream<String> stream = items.stream();
This example first creates a Java List, then adds three Java Strings to it. Finally, the example calls the stream() method to obtain a Stream instance.
Terminal and Non-Terminal Operations
The Stream interface has a selection of terminal and non-terminal operations. A non-terminal stream operation is an operation that adds a listener to the stream without doing anything else. A terminal stream operation is an operation that starts the internal iteration of the elements, calls all the listeners, and returns a result.
Here is a Java Stream example which contains both a non-terminal and a terminal operation:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamExamples {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
Stream<String> stream = stringList.stream();
long count = stream
.map((value) -> { return value.toLowerCase(); })
.count();
System.out.println("count = " + count);
}
}
The call to the map() method of the Stream interface is a non-terminal operation. It merely sets a lambda expression on the stream which converts each element to lowercase. The map() method will be covered in more detail later on.
The call to the count() method is a terminal operation. This call starts the iteration internally, which will result in each element being converted to lowercase and then counted.
The conversion of the elements to lowercase does not actually affect the count of elements. The conversion part is just there as an example of a non-terminal operation.
Non-Terminal Operations
The non-terminal stream operations of the Java Stream API are operations that transform or filter the elements in the stream. When you add a non-terminal operation to a stream, you get a new stream back as result. The new stream represents the stream of elements resulting from the original stream with the non-terminal operation applied. Here is an example of a non-terminal operation added to a stream - which results in a new stream:
List<String> stringList = new ArrayList<String>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
Stream<String> stream = stringList.stream();
Stream<String> stringStream =
stream.map((value) -> { return value.toLowerCase(); });
Notice the call to stream.map(). This call actually returns a new Stream instance representing the original stream of strings with the map operation applied.
You can only add a single operation to a given Stream instance. If you need to chain multiple operations after each other, you will need to apply the second operation to the Stream operation resulting from the first operation. Here is how that looks:
Stream<String> stringStream1 =
stream.map((value) -> { return value.toLowerCase(); });
Stream<½String> stringStream2 =
stringStream1.map((value) -> { return value.toUpperCase(); });
Notice how the second call to Stream map() is called on the Stream returned by the first map() call.
It is quite common to chain the calls to non-terminal operations on a Java Stream. Here is an example of chaining the non-terminal operation calls on Java streams:
Stream<String> stream1 = stream
.map((value) -> { return value.toLowerCase(); })
.map((value) -> { return value.toUpperCase(); })
.map((value) -> { return value.substring(0,3); });
Many non-terminal Stream operations can take a Java Lambda Expression as parameter. This lambda expression implements a Java functional interface that fits the given non-terminal operation. For instance, the Function or Predicate interface. The parameter of the non-terminal operation method parameter is typically a functional interface - which is why it can also be implemented by a Java lambda expression.
[!StreamAPI-Methods] https://jojozhuang.github.io/programming/java-8-stream-api/
Method and Constructor References
Java 8 enables you to pass references of methods or constructors via the :: keyword. The above example shows how to reference a static method. But we can also reference object methods:
class Something {
String startsWith(String s) {
return String.valueOf(s.charAt(0));
}
}
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"
Let's see how the :: keyword works for constructors. First we define an example class with different constructors:
class Person {
String firstName;
String lastName;
Person() {}
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Next we specify a person factory interface to be used for creating new persons:
interface PersonFactory<P extends Person> {
P create(String firstName, String lastName);
}
Instead of implementing the factory manually, we glue everything together via constructor references:
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
We create a reference to the Person constructor via Person::new. The Java compiler automatically chooses the right constructor by matching the signature of PersonFactory.create.
Annotations
Annotations in Java 8 are repeatable. Let's dive directly into an example to figure that out.
First, we define a wrapper annotation which holds an array of the actual annotations:
@interface Hints {
Hint[] value();
}
@Repeatable(Hints.class)
@interface Hint {
String value();
}
Java 8 enables us to use multiple annotations of the same type by declaring the annotation @Repeatable.
Variant 1: Using the container annotation (old school)
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}
Variant 2: Using repeatable annotations (new school)
@Hint("hint1")
@Hint("hint2")
class Person {}
Using variant 2 the java compiler implicitly sets up the @Hints annotation under the hood. That's important for reading annotation information via 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 on the Person class, it's still readable via getAnnotation(Hints.class). However, the more convenient method is getAnnotationsByType which grants direct access to all annotated @Hint annotations.
Furthermore the usage of annotations in Java 8 is expanded to two new targets:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
Functional Programming
Functional programming contains some key concepts:
- Functions as first class objects
- Pure functions
- Higher order functions
Functions as First Class Objects
In the functional programming paradigm, functions are first class objects in the language. That means that you can create an “instance” of a function, as have a variable reference that function instance, just like a reference to a String, Map or any other object. Functions can also be passed as parameters to other functions.
Pure Functions
A function is a pure function if:
- The execution of the function has no side effects.
- The return value of the function depends only on the input parameters passed to the function.
Here is an example of a pure function (method) in Java:
public class ObjectWithPureFunction{
public int sum(int a, int b) {
return a + b;
}
}
Notice how the return value of the sum() function only depends on the input parameters. Notice also that the sum() has no side effects, meaning it does not modify any state (variables) outside the function anywhere.
Contrarily, here is an example of a non-pure function:
public class ObjectWithNonPureFunction{
private int value = 0;
public int add(int nextValue) {
this.value += nextValue;
return this.value;
}
}
Notice how the method add() uses a member variable to calculate its return value, and it also modifies the state of the value member variable, so it has a side effect.
Higher Order Functions
A function is a higher order function if at least one of the following conditions are met:
- The function takes one or more functions as parameters.
- The function returns another function as result.
In Java, the closest we can get to a higher order function is a function (method) that takes one or more lambda expressions as parameters, and returns another lambda expression. Here is an example of a higher order function in Java:
public class HigherOrderFunctionClass {
public <T> IFactory<T> createFactory(IProducer<T> producer, IConfigurator<T> configurator) {
return () -> {
T instance = producer.produce();
configurator.configure(instance);
return instance;
}
}
}
Notice how the createFactory() method returns a lambda expression as result. This is the first condition of a higher order function.
Notice also that the createFactory() method takes two instances as parameters which are both implementations of interfaces (IProducer and IConfigurator). Java lambda expressions have to implement a functional interface, remember?
Imagine the interfaces looks like this:
public interface IFactory<T> {
T create();
}
public interface IProducer<T> {
T produce();
}
public interface IConfigurator<T> {
void configure(T t);
}
As you can see, all of these interfaces are functional interfaces. Therefore they can be implemented by Java lambda expressions - and therefore the createFactory() method is a higher order function.