Learn how to create custom function interfaces and why you should use built-in interfaces whenever possible.

An overview of

What is the type of a lambda expression? Some languages use function values or function objects to represent lambda expressions, but the Java language does not. Java uses functional interfaces to represent lambda expression types. This is actually an effective way to ensure backward compatibility with older versions of the Java language.

Take a look at the following code:

Thread thread = new Thread(new Runnable() {
  public void run(a) {
    System.out.println("In another thread"); }}); thread.start(); System.out.println("In main");
Copy the code

The Thread class and its constructor were introduced in Java 1.0 more than 20 years ago. The constructor has not changed since then. Passing anonymous instances of Runnable to constructors has been a tradition. But starting with java8, you have the option of passing lambda expressions:

public static void main(String[] args) {
		// TODO Auto-generated method stub
		Thread thread = new Thread(() -> System.out.println("This is a thread."));
		thread.start();
	}
Copy the code

Execution Result:

This is a threadCopy the code

Here the Thread constructor wants an instance that implements Runnable. In this case we pass a lambda expression instead of an object. We can actually pass lambda expressions to a variety of methods and constructors, including those created before Java8. This works because lambda expressions are represented in Java as functional interfaces.

There are three important rules for functional interfaces:

  1. A function interface has only one abstract method.
  2. An abstract method that is a public method in an Object class is not treated as a single abstract method.
  3. Function interfaces can have default methods and static methods.

Any interface that satisfies the rules of a single abstract method is automatically treated as a functional interface. This includes traditional interfaces such as Runnable and Callable, as well as custom interfaces that you build yourself.

Built-in function interface

In addition to the single abstract method already mentioned, JDK 8 includes several new function interfaces. The most commonly used interfaces include Function

, Predicate

, and Consumer

, which are defined in the java.util. Function package. Stream’s map method accepts Function

as an argument. Similarly, filter uses Predicate

and forEach uses Consumer

. The package also has other function interfaces, such as Supplier

, BiConsumer

and BiFunction

.
,>
,>



,>


,>

We can use the built-in function interfaces as arguments to our own methods. For example, suppose we have a Device class that contains methods checkout and checkin to indicate whether a Device is being used. When a user requests a new device, the method getFromAvailable returns a device from the pool of available devices, or creates a new device if necessary.

We can implement a function to borrow devices:

public void borrowDevice(Consumer<Device> use) {
  Device device = getFromAvailable();

  device.checkout();

  try {
    use.accept(device);      
  } finally{ device.checkin(); }}Copy the code
  • acceptConsumer<Device>As a parameter.
  • Get a device from the pool (we are not concerned with thread safety in this example).
  • Call the checkout method to set the device state to Checked out.
  • Deliver the device to the user.

Upon returning to the Consumer’s Accept method after the device call, change the device state to Checked in by calling the Checkin method.

One way of using the borrowDevice method is given below:

new Sample().borrowDevice(device -> System.out.println("using " + device));
Copy the code

Because the method takes a function interface as an argument, it is acceptable to pass in a lambda expression as an argument.

Custom function interface

Although it is best to use built-in function interfaces whenever possible, custom function interfaces are sometimes required.

To create a custom function interface, you need to do two things:

  1. use@FunctionalInterfaceAnnotate the interface, which is java8’s convention for custom function interfaces.
  2. Ensure that the interface has only one abstract method.

This convention makes it clear that the interface accepts lambda expressions. When the compiler sees the annotation, it verifies that the interface has only one abstract method.

Using the @FunctionalInterface annotation ensures that you get an error message if you accidentally violate the abstract method count rule when you change the interface in the future. This is useful because you find the problem immediately, rather than leaving it to the next developer to deal with later. No one wants to get an error message when lambda expressions are passed to someone else’s custom interface.

Create custom function interfaces

The following example creates a Transformer function interface:

@FunctionalInterface
public interface Transformer<T> {
  T transform(T input);
}
Copy the code

This interface is marked with the @functionalinterface annotation to indicate that it is a FunctionalInterface. Because the annotation is included in the java.lang package, there is no need to import it. The interface has a method called transform, which takes an object of type T parameterized and returns a transformed object of the same type. The semantics of the transformation will be determined by the implementation of the interface.

This is the OrderItem class:

public class OrderItem {
	private final int id;
	private final int price;

	protected OrderItem(int id, int price) {
		super(a);this.id = id;
		this.price = price;
	}

	public int getId(a) {
		return id;
	}

	public int getPrice(a) {
		return price;
	}

	@Override
	public String toString(a) {
		// TODO Auto-generated method stub
		return String.format("id: %d price: %d", id, price); }}Copy the code

Now look at the Order class:

public class Order {
	List<OrderItem> items;

	protected Order(List<OrderItem> items) {
		super(a);this.items = items;
	}
	public void transformAndPrint(Transformer<Stream<OrderItem>> transformOrderItems) { transformOrderItems.transform(items.stream()) .forEach(System.out::println); }}Copy the code

The transformAndPrint method takes a Transform

as an argument, calls the Transform method to Transform the Order items that belong to the Order instance, and then outputs those Order items in the transformed Order.

Test this method:

public class Sample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Order order = new Order(Arrays.asList(
			      new OrderItem(1.1225),
			      new OrderItem(2.983),
			      new OrderItem(3.1554))); order.transformAndPrint(new Transformer<Stream<OrderItem>>() {

			@Override
			public Stream<OrderItem> transform(Stream<OrderItem> input) {
				// TODO Auto-generated method stub
				returninput.sorted(Comparator.comparing(OrderItem::getPrice)); }}); }}Copy the code

Test results:

id: 2 price: 983
id: 1 price: 1225
id: 3 price: 1554
Copy the code

We pass an anonymous inner class as an argument to the transformAndPrint method. Within the Transform method, the sorted method of the given stream is called, which sorts the order items.

The power of lambda expressions

Anywhere we need a functional interface, we have three options:

  1. Pass an anonymous inner class.
  2. Pass a lambda expression.
  3. In some cases, pass a method reference instead of a lambda expression.
order.transformAndPrint(input -> input.sorted(Comparator.comparing(OrderItem::getPrice)));
Copy the code

This is much cleaner and easier to read than the anonymous inner classes we originally provided.

Custom function interfaces and built-in function interfaces

We demonstrated custom function interfaces above, so what are the advantages and disadvantages of custom function interfaces?

First advantages:

  • You can provide a descriptive name for your custom interface to help other developers modify or reuse it.
  • You can provide any valid syntactic name for an abstract method if you like. Only the receiver of the interface gains this advantage, and only when passing abstract methods. Calls that pass lambda expressions or method references do not gain this advantage.
  • You can use parameterized types in your interface, or keep it simple and specific to certain types.
  • You can write custom default and static methods that can be used by other implementations of the interface.

Disadvantages of custom interfaces:

  • You want to create multiple interfaces that all have abstract methods with the same signature, such as taking String as an argument and returning Integer. Although the names of the methods may vary, they are mostly redundant and can be replaced with an interface with a common name.

  • Anyone who wants to use custom interfaces must put extra effort into learning, understanding, and remembering them.

Which interface is best

Now that we know the pros and cons of a custom function interface versus a built-in one, how do we adopt that interface? Let’s review the Transformer interface.

public void transformAndPrint(Transformer<Stream<OrderItem>> transformOrderItems) {
Copy the code

The method transformAndPrint receives a parameter responsible for performing the transformation. This transformation may reorder the elements in the orderItems collection. Alternatively, it might block part of the attack for each order item. Or do nothing. We’ll leave the implementation to the caller.

Importantly, callers know that they can supply the transformation implementation as a parameter to the transformAndPrint method. The name of the function interface and its documentation should provide these details. In this case, these details are also clear from the parameter name (transformOrderItems), and they should be included in the documentation of the transformAndPrint function. Although the name of a function interface is useful, it is not the only way to understand the purpose and usage of a function interface.

Looking closely at the Transformer interface and comparing its purpose to the JDK’s built-in functions interface, we see that Function

can replace Transformer.
,>

public void transformAndPrint(Function<Stream<OrderItem>, Stream<OrderItem>> transformOrderItems) {
		  transformOrderItems.apply(items.stream())
		                     .forEach(System.out::println);
	}
Copy the code

The call to transformAndPrint uses an anonymous inner class, which we also need to change. However, we have changed the call to use a lambda expression:

order.transformAndPrint(orderItems -> orderItems.sorted(comparing(OrderItem::getPrice)));
Copy the code

We see that we can also use method references or lambda expressions to call functions. The name of the function interface has nothing to do with the lambda expression, but only with the compiler, which associates lambda expression arguments with method arguments. Again, it doesn’t matter to the caller whether the method name is transform or apply.

Using the built-in function interface reduces our interface by one, and calling this method does the same. We didn’t compromise the readability of the code.

conclusion

The design strategy of designing lambda expressions as functional interface types helped make Java compatible with earlier versions. Lambda expressions can be passed to any older function that normally receives a single abstract method interface. To receive a lambda expression, the parameter type of the method should be the function interface.

In some cases, it makes sense to create your own functional interfaces, but you should be careful when doing so. Consider custom functional interfaces only when your application requires a highly specialized approach, or when existing interfaces do not meet your needs. Always check for this functionality in a JDK built-in function interface. Use built-in function interfaces whenever possible.

Article study address:

Thank you Dr. Venkat Subramaniam

Dr Venkat Subramaniam site: http://agiledeveloper.com/

My blog

Knowledge change fate, and strive to change life