JavaRush /Java Blog /Random EN /Functional Interfaces in Java

Functional Interfaces in Java

Published in the Random EN group
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. Functional Interfaces in Java - 1The 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 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 @FunctionalInterfaceis 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 @FunctionalInterfacethis 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
The concept of interfaces implies that a given unit of code cannot have any methods implemented. But starting with Java 8, it became possible to use static and default methods in interfaces. Static methods are bound directly to a class and do not require a specific object of that class to call such a method. That is, these methods fit harmoniously into the concept of interfaces. As an example, let's add a static method for checking an object for null to the previous class:
@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
Before Java 8, if we needed to create a method in an interface that was inherited by other classes, we could only create an abstract method that was implemented in each specific class. But what if this method is the same for all classes? In this case , abstract classes were most often used . But starting with Java 8, there is an option to use interfaces with implemented methods - the default methods. When inheriting an interface, you can override these methods or leave everything as is (leave the default logic). When creating a default method, we must add the keyword - 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
In the lecture Comparing Objects , we talked about the fact that all classes inherit from the class 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 Converteris 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, Converterit 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.Functional Interfaces in Java - 2

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 Predicatethat 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 ( ): FunctionStringInteger
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 applyto 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? Functional Interfaces in Java - 3And such that many methods Streamwork 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 method Stream- filterwhich takes as an argument Predicateand returns Streamonly 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 truewhen used in an testinterface 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 evenNumberswill consist of elements {2, 4, 6, 8}. And, as we remember, collectit will collect all elements into a certain collection: in our case, into List.

Method with Consumer

One of the methods in Stream, which uses the functional interface Consumer, is the peek. This is what our example for Consumerin 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 peekworks with Consumer, modification of the strings in Streamwill not occur, but peekwill return Streamwith the original elements: the same as they came to it. Therefore, the list peopleGreetingswill 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 in Streamthat uses the functional interface Supplieris generate, which generates an infinite sequence based on the functional interface passed to it. Let's use our example Supplierto 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 with Streaman argument Functionis a method mapthat 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 Functionin 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 uses UnaryOperatoras 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.
This is what our example will look like 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. Functional Interfaces in Java - 4That'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!
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION