Learn how to use type inference in lambda expressions and tips for improving parameter naming.

An overview of

Java8 is a version of Java that supports type inference, and it only supports it for lambda expressions. The use of type inference in lambda expressions is powerful and will help you prepare for future Java releases, which will use type inference for variables and more. The trick here is to name the parameters appropriately, trusting the Java compiler to infer the rest. Most of the time the compiler can fully infer the type. When it cannot be inferred, an error is reported. To understand how lambda expression inference works, look at at least one example where the type cannot be inferred.

Display type and redundancy

Suppose we ask a person what his or her name is, and it will answer: “MY name is XXX”. Examples like this happen all the time in life, but it’s more efficient to simply say “XXX.” All you need is a name, so the rest of the sentence is redundant.

We often encounter this kind of superfluous thing in code. Java developers can use forEach iterations to output a double of every value in a range. Look at the following example:

public static void main(String[] args) {
		// TODO Auto-generated method stub
		IntStream.rangeClosed(1.5)
		  .forEach((int number) -> System.out.println(number * 2));
	}
Copy the code

Test results:

2, 4, 6, 8, 10Copy the code

The rangeClosed method generates a stream of int values from 1 to 5. The only responsibility of a lambda expression is to take an int called number and print a double of that value using println of PrintStream. The lambda expression is syntactically correct, but the type details are redundant.

Type inference in java8

When you extract a value in a numeric range, the compiler knows that the value is of type int. Declarations that do not need to be displayed in code.

In Java8 we can discard types in lambda expressions:

IntStream.rangeClosed(1.5)
  .forEach((number) -> System.out.println(number * 2));
Copy the code

Because Java is a statically typed language, it needs to know the types of all objects and variables at compile time. Omitting types from the argument list of lambda expressions does not bring Java any closer to a dynamically typed language. However, adding the right type inference would bring Java closer to other statically typed languages, such as Haskel.

Trust compiler

If you omit a type from an argument to a lambda expression, Java needs to infer that type from context details. Returning to the previous example, when we called forEach on IntStream, the compiler looked for the method to determine which arguments it took. IntStream’s forEach method expects IntConsumer, whose abstract method Accept takes an int and returns void.

If the type is specified in the argument list, the compiler verifies that the type is as expected. If the type is omitted, the compiler will infer the expected type.

Java knows the type of lambda expression parameters at compile time, whether you supply the type or the compiler deduces the type. To test this, introduce an error into the lambda expression and omit the type of the argument:

The compiler will simply report an error. The compiler knows the type of the parameter named number. It reports an error because it cannot dereference a variable of type int using the dot operator. This operation cannot be performed on int variables.

Benefits of type inference

Omitting types from lambda expressions has two main benefits:

  • You type less. There is no need to enter type information because the compiler can easily determine the type itself.
  • Code has fewer impurities and is simpler.

Also, in general, if we have only one argument, the omitting type means we can also omit (), as follows:

IntStream.rangeClosed(1.5)
  .forEach(number -> System.out.println(number * 2));
Copy the code

Note that you will need to add parentheses for lambda expressions that take multiple arguments.

Type inference and readability

Type inference in lambda expressions goes against the common practice in Java, which specifies the type of each variable and parameter.

Take a look at this example:

List<String> result =
  cars.stream()
    .map((Car c) -> c.getRegistration())
    .map((String s) -> DMVRecords.getOwner(s))
    .map((Person o) -> o.getName())
    .map((String s) -> s.toUpperCase())
    .collect(toList());
Copy the code

Each lambda expression in this code specifies a type for its arguments, but we use single-letter variable names for the arguments. This is common in Java. But this approach is inappropriate because it discards domain-specific context.

We can do better than that. Let’s look at what happens when we rewrite the code with more powerful parameter names:

List<String> result =
  cars.stream()
    .map((Car car) -> car.getRegistration())
    .map((String registration) -> DMVRecords.getOwner(registration))
    .map((Person owner) -> owner.getName())
    .map((String name) -> name.toUpperCase())
    .collect(toList());
Copy the code

These parameter names contain domain-specific information. Instead of using s for String, we specify domain-specific details, such as registration and name. Similarly, instead of using p or O, we use owner to indicate that Person is not just a Person, but also the owner of the car.

Named parameters

Some languages, such as Scala and TypeScript, place more emphasis on parameter names than types. In Scala, we define parameters before defining types, for example by writing:

def getOwner(registration: String)
Copy the code

Instead of:

def getOwner(String registration)
Copy the code

Both types and parameter names are useful, but in Scala parameter names are more important. We can also consider this idea when writing lambda expressions in Java. Notice what happens when we discard type details and parentheses in our Vehicle registration example in Java:

List<String> result =
  cars.stream()
    .map(car -> car.getRegistration())
    .map(registration -> DMVRecords.getOwner(registration))
    .map(owner -> owner.getName())
    .map(name -> name.toUpperCase())
    .collect(toList());
Copy the code

Because we added descriptive parameter names, we didn’t lose much context, and the explicit (now redundant) type was quietly gone. The result is cleaner, more unpretentious code.

Limitations of type inference

Although using type inference can improve efficiency and readability, this technique is not suitable for all Settings. In some cases, type inference cannot be used at all. Fortunately, you can rely on the Java compiler to figure out when this happens.

Let’s look first at an example of compiler success and then at an example of test failure.

Extended type inference

In our first example, suppose we wanted to create a Comparator to compare Car instances. We first need a Car class:

class Car {
  public String getRegistration(a) { return null; }}Copy the code

Next, we’ll create a Comparator to compare them based on the registration information of the Car instance:

public static Comparator<Car> createComparator(a) {
  return comparing((Car car) -> car.getRegistration());
}
Copy the code

The lambda expression used as an argument to the Comparing method contains type information in its argument list. We know that Java compilers are very good at type inference, so let’s see what happens when parameter types are omitted, as follows:

public static Comparator<Car> createComparator(a) {
  return comparing(car -> car.getRegistration());
}
Copy the code

The Comparing method takes 1 parameter. It expects to use Function
and returns the Comparator

. Because Comparing is a static method on the Comparator

, the compiler currently has no clue as to what T or U might be.

To solve this problem, the compiler slightly extends the inference beyond the parameters passed to The Comparing method. It looks at how we process the results of calling Comparing. Based on this information, the compiler determines that we return only that result. Next, it sees the Comparator

returned by Comparing returned by createComparator as a Comparator

.

Pay attention! The compiler now understands our intent: it concludes that T should be bound to Car. From this information, it knows that the car parameter in the lambda expression should be of type CAR.

In this example, the compiler had to do some extra work to infer the type, but it worked. Next, let’s take a look at what happens when you raise the challenge to the limit of what the compiler can do.

Limitations of inference

First, we added a new call after the previous comparing call:

public static Comparator<Car> createComparator(a) {
  return comparing((Car car) -> car.getRegistration()).reversed();
}
Copy the code

This code has no compilation problems with explicit typing, but now let’s discard the type information and see what happens:

public static Comparator<Car> createComparator(a) {
  return comparing(car -> car.getRegistration()).reversed();
}
Copy the code

The Java compiler threw an error:

Sample.java:21: error: cannot find symbol
    return comparing(car -> car.getRegistration()).reversed(a);
                               ^
  symbol:   method getRegistration(a)
  location: variable car of type Object
Sample.java:21: error: incompatible types: Comparator<Object> cannot be converted to Comparator<Car>
    return comparing(car -> car.getRegistration()).reversed(a);
                                                           ^
2 errors
Copy the code

As in the previous scenario, before including.reversed(), the compiler will ask how we will process the result of the call comparing(car -> car.getregistration ()). In the previous example, we returned the result as Comparable

, so the compiler can infer that T is of type Car.

But in the modified version, we pass the comparable result as the target of the call reversed(). Comparable returns comparable

, reversed() without showing any additional information about the possible meaning of T. From this information, the compiler concludes that T must be of type Object. Unfortunately, this information is not enough for this code, because Object lacks the getRegistration() method we call in lambda expressions.

Type inference fails at this point. In this case, the compiler actually needs some information. Type inference analyzes parameters, return elements, or assignment elements to determine the type, but the compiler reaches its limits when the context provides insufficient detail.

Can method references be used as a remedy?

Before we abandon this special case, let’s try another approach: instead of using lambda expressions, try using method references:

public static Comparator<Car> createComparator(a) {
  return comparing(Car::getRegistration).reversed();
}
Copy the code

Because the Car type is stated directly, the compiler is satisfied.

conclusion

Java 8 introduces limited type inference capabilities for arguments to lambda expressions, which will be extended to local variables in future versions of Java. Now is the time to learn to omit type details and trust the compiler, which will help ease you into the Java environment of the future.

Rely on type inference and appropriately named parameters to write concise, more expressive, and less intrusive code. You can use type inference as long as you trust the compiler to infer the type for itself. Provide type details only if you are sure that the compiler really needs your help.

Article study address:

Thank you Dr. Venkat Subramaniam

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

Knowledge change fate, strive to change life