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