Hello! In the Java Syntax Pro quest, we studied lambda expressions and said that they are nothing more than an implementation of a functional method from a functional interface. In other words, this is the implementation of some anonymous (unknown) class, its unrealized method. And if in the lectures of the course we delved into manipulations with lambda expressions, now we will consider, so to speak, the other side: namely, these very interfaces. The eighth version of Java introduced the concept of functional interfaces . What is this? An interface with one unimplemented (abstract) method is considered functional. Many out-of-the-box interfaces fall under this definition, such as, for example, the previously discussed interface Predicate
Consumer
Supplier
Function
UnaryOperator
Comparator
. And also interfaces that we create ourselves, such as:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
We have an interface whose task is to convert objects of one type into objects of another (a kind of adapter). The annotation @FunctionalInterface
is not something super complex or important, since its purpose is to tell the compiler that this interface is functional and should contain no more than one method. If an interface with this annotation has more than one unimplemented (abstract) method, the compiler will not skip this interface, since it will perceive it as erroneous code. Interfaces without this annotation can be considered functional and will work, but @FunctionalInterface
this is nothing more than additional insurance. Let's go back to class Comparator
. If you look at its code (or documentation ), you can see that it has many more than one method. Then you ask: how, then, can it be considered a functional interface? Abstract interfaces can have methods that are not within the scope of a single method:
- static
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
Having received this method, the compiler did not complain, which means our interface is still functional.
- default methods
default
:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
}
Again, we see that the compiler did not start complaining, and we did not go beyond the limitations of the functional interface.
- Object class methods
Object
. This does not apply to interfaces. But if we have an abstract method in the interface that matches the signature with some method of the class Object
, such a method (or methods) will not break our functional interface restriction:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
boolean equals(Object obj);
}
And again, our compiler does not complain, so the interface Converter
is still considered functional. Now the question is: why do we need to limit ourselves to one unimplemented method in a functional interface? And then so that we can implement it using lambdas. Let's look at this with an example Converter
. To do this, let's create a class Dog
:
public class Dog {
String name;
int age;
int weight;
public Dog(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
And a similar one Raccoon
(raccoon):
public class Raccoon {
String name;
int age;
int weight;
public Raccoon(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
Suppose we have an object Dog
, and we need to create an object based on its fields Raccoon
. That is, Converter
it converts an object of one type to another. How will it look like:
public static void main(String[] args) {
Dog dog = new Dog("Bobbie", 5, 3);
Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);
Raccoon raccoon = converter.convert(dog);
System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
When we run it, we get the following output to the console:
Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
And this means that our method worked correctly.
Basic Java 8 Functional Interfaces
Well, now let’s look at several functional interfaces that Java 8 brought us and which are actively used in conjunction with the Stream API.Predicate
Predicate
— a functional interface for checking whether a certain condition is met. If the condition is met, returns true
, otherwise - false
:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
As an example, consider creating a Predicate
that will check for parity of a number of type Integer
:
public static void main(String[] args) {
Predicate<Integer> isEvenNumber = x -> x % 2==0;
System.out.println(isEvenNumber.test(4));
System.out.println(isEvenNumber.test(3));
}
Console output:
true
false
Consumer
Consumer
(from English - “consumer”) - a functional interface that takes an object of type T as an input argument, performs some actions, but returns nothing:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
As an example, consider , whose task is to output a greeting to the console with the passed string argument: Consumer
public static void main(String[] args) {
Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
greetings.accept("Elena");
}
Console output:
Hello Elena !!!
Supplier
Supplier
(from English - provider) - a functional interface that does not take any arguments, but returns an object of type T:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
As an example, consider Supplier
, which will produce random names from a list:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList .add("Elena");
nameList .add("John");
nameList .add("Alex");
nameList .add("Jim");
nameList .add("Sara");
Supplier<String> randomName = () -> {
int value = (int)(Math.random() * nameList.size());
return nameList.get(value);
};
System.out.println(randomName.get());
}
And if we run this, we will see random results from a list of names in the console.
Function
Function
— this functional interface takes an argument T and casts it to an object of type R, which is returned as a result:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
As an example, let's take , which converts numbers from string format ( ) to number format ( ): Function
String
Integer
public static void main(String[] args) {
Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
System.out.println(valueConverter.apply("678"));
}
When we run it, we get the following output to the console:
678
PS: if we pass not only numbers, but also other characters into the string, an exception will be thrown - NumberFormatException
.
UnaryOperator
UnaryOperator
— a functional interface that takes an object of type T as a parameter, performs some operations on it and returns the result of the operations in the form of an object of the same type T:
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
UnaryOperator
, which uses its method apply
to square a number:
public static void main(String[] args) {
UnaryOperator<Integer> squareValue = x -> x * x;
System.out.println(squareValue.apply(9));
}
Console output:
81
We looked at five functional interfaces. This is not all that is available to us starting with Java 8 - these are the main interfaces. The rest of the available ones are their complicated analogues. The complete list can be found in the official Oracle documentation .
Functional interfaces in Stream
As discussed above, these functional interfaces are tightly coupled with the Stream API. How, you ask? And such that many methodsStream
work specifically with these functional interfaces. Let's look at how functional interfaces can be used in Stream
.
Method with Predicate
For example, let's take the class methodStream
- filter
which takes as an argument Predicate
and returns Stream
only those elements that satisfy the condition Predicate
. In the context of Stream
-a, this means that it only passes through those elements that are returned true
when used in an test
interface method Predicate
. This is what our example would look like for Predicate
, but for a filter of elements in Stream
:
public static void main(String[] args) {
List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.filter(x -> x % 2==0)
.collect(Collectors.toList());
}
As a result, the list evenNumbers
will consist of elements {2, 4, 6, 8}. And, as we remember, collect
it will collect all elements into a certain collection: in our case, into List
.
Method with Consumer
One of the methods inStream
, which uses the functional interface Consumer
, is the peek
. This is what our example for Consumer
in will look like Stream
:
public static void main(String[] args) {
List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
.peek(x -> System.out.println("Hello " + x + " !!!"))
.collect(Collectors.toList());
}
Console output:
Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
But since the method peek
works with Consumer
, modification of the strings in Stream
will not occur, but peek
will return Stream
with the original elements: the same as they came to it. Therefore, the list peopleGreetings
will consist of the elements "Elena", "John", "Alex", "Jim", "Sara". There is also a commonly used method foreach
, which is similar to the method peek
, but the difference is that it is final - terminal.
Method with Supplier
An example of a method inStream
that uses the functional interface Supplier
is generate
, which generates an infinite sequence based on the functional interface passed to it. Let's use our example Supplier
to print five random names to the console:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList.add("Elena");
nameList.add("John");
nameList.add("Alex");
nameList.add("Jim");
nameList.add("Sara");
Stream.generate(() -> {
int value = (int) (Math.random() * nameList.size());
return nameList.get(value);
}).limit(5).forEach(System.out::println);
}
And this is the output we get in the console:
John
Elena
Elena
Elena
Jim
Here we used the method limit(5)
to set a limit on the method generate
, otherwise the program would print random names to the console indefinitely.
Method with Function
A typical example of a method withStream
an argument Function
is a method map
that takes elements of one type, does something with them and passes them on, but these can already be elements of a different type. What an example with Function
in might look like Stream
:
public static void main(String[] args) {
List<Integer> values = Stream.of("32", "43", "74", "54", "3")
.map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
As a result, we get a list of numbers, but in Integer
.
Method with UnaryOperator
As a method that usesUnaryOperator
as an argument, let's take a class method Stream
- iterate
. This method is similar to the method generate
: it also generates an infinite sequence but has two arguments:
- the first is the element from which the sequence generation begins;
- the second is
UnaryOperator
, which indicates the principle of generating new elements from the first element.
UnaryOperator
, but in the method iterate
:
public static void main(String[] args) {
Stream.iterate(9, x -> x * x)
.limit(4)
.forEach(System.out::println);
}
When we run it, we get the following output to the console:
9
81
6561
43046721
That is, each of our elements is multiplied by itself, and so on for the first four numbers. That's all! It would be great if after reading this article you are one step closer to understanding and mastering the Stream API in Java!