Generic Types and Type Parameters
Function<T,R> represents functions from values of type T to values of type R, and it defines apply(t) where t is of type T and the return type is R.
Why Transformation Rules Matter in Stream Code
When you work with the Stream API, you often pass small pieces of behavior into methods. One piece might be a rule for transforming each element, another a test for filtering, and another a way to combine values. Java represents these pieces of behavior as objects of functional interfaces, and lambda expressions are a short way to write them. Two of the most common interfaces for transforming values are Function and UnaryOperator.
This article asks one focused question: what does each of these interfaces describe, and how are they related? The answer comes down to a single idea about the types that go into a transformation and the types that come out of it. Keep that idea in mind as you read, because each section below adds one piece of it.
Tracing Two Functions Through apply
Before explaining the declaration, watch two functional objects run. The first maps a String to an Integer. The second maps a String to a String. Both are built from lambda expressions, and both are used through the apply method. Read the code once, then trace what happens to each value as it passes through apply.
Function<String, Integer> lengthOf = s -> s.length();
Function<String, String> shout = s -> s.toUpperCase();
System.out.println(lengthOf.apply("Stream"));
System.out.println(shout.apply("stream"));- 1The first line declares
lengthOf. Its type arguments say the input is a String and the result is an Integer. The lambda bodys.length()computes a number. - 2The second line declares shout. Its type arguments say the input and the result are both String. The lambda body
s.toUpperCase()returns text. - 3Calling
lengthOf.apply("Stream")sends the text Stream into the lambda. The lambda returns 6, so the first println shows 6. - 4Calling
shout.apply("stream")sends the text stream into the lambda. The lambda returns STREAM, so the second println shows STREAM.
lengthOfis declared with T = String and R = Integer- shout is declared with T = String and R = String
apply("Stream")passes a String into the lambda and returns the length as an Integer
6
STREAMWhat do you think happens?
Before reading the output, predict what the first println prints. Think about the input type, the output type, and what the lambda body computes.
Reveal answer
It prints 6, the length of the String Stream.
The input type is String and the output type is Integer, so the lambda must return a number. The body s.length() counts the characters in Stream, which are S, t, r, e, a, m, so the result is 6.
The Two Slots of Function<T,R>
Function<T,R> represents functions from values of type T to values of type R. T and R are type parameters: placeholders in the interface declaration that you replace with real types, called type arguments, when you use the interface.
Reading the declaration Function<String,Integer> from left to right gives the contract. The first slot, T, is filled by String, so the input is a String. The second slot, R, is filled by Integer, so the result is an Integer. You can read the whole thing as a rule that takes a String and gives back an Integer.
How apply(t) Fixes Its Input and Output Types
The interface defines one operation, apply(t), where t is a value of type T. Calling apply with an input of type T produces a result of type R. The declaration decides both types in advance. Once you write Function<String,Integer>, the parameter of apply must be a String and the value it returns must be an Integer.
The flow in the diagram is the same every time. The value you pass in must match T. The lambda or other behavior behind the interface computes the result. The value you get back is typed as R, so the compiler checks that the result you use matches R. In the earlier example, lengthOf.apply("Stream") accepted a String and produced an Integer because the declaration said so.
Merging Both Slots in UnaryOperator<T>
The source states that UnaryOperator<T> is essentially Function<T,T>. The only type parameter you supply is T, and it fills both slots. The input and the output of UnaryOperator<T> therefore always share one type. An operator in this sense takes a value and returns a value of the same kind, such as a String that becomes another String, or a number that becomes a number of the same type.
A useful way to remember the relationship is that UnaryOperator<T> is Function<T,T> with the two type slots merged into one. Everything about apply carries over. You still call apply(t), and both the parameter type and the return type are T.
| Feature | Function<T,R> | UnaryOperator<T> |
|---|---|---|
| Type parameters you supply | Two: T and R | One: T |
| Input type of apply(t) | T | T |
| Return type of apply(t) | R, which may differ from T | T, always the same as the input |
| Example declaration | Function<String,Integer> | UnaryOperator<String> |
| Behavior in words | Maps a T to an R | Maps a T to another T |
Function<String,Integer> and UnaryOperator<String> differ in whether the two slots can hold different types.
java- UnaryOperator<String> sets T to String, which fills both slots
- The input is a String and the output must also be a String
Primitive Specializations: DoubleUnaryOperator and IntUnaryOperator
The source notes that DoubleUnaryOperator is a specialized version of UnaryOperator<Double>. It keeps the same unary idea, one value in and one value of the same kind out, and fixes the type to double. The source also mentions that IntUnaryOperator exists and follows the same naming pattern for int values. The source does not go further into these specialized interfaces, so this article only recognizes their names and their connection to UnaryOperator.
Read the diagram from top to bottom. Function<T,R> is the general idea with two independent slots. UnaryOperator<T> merges the two slots into one, so input and output share a type. DoubleUnaryOperator and IntUnaryOperator go one step further by fixing that shared type to a primitive numeric type. The unary shape stays the same at every level; only the type choice becomes narrower.
Mistakes When Reading Type Parameters
Common mistake Assuming Function<T,R> needs the same type for input and output
What it looks likeFunction<String,Integer> takes a String and returns an Integer
Why it failsT and R are independent slots, so they may hold different types.
Do this insteadRead each slot separately and fill it with the type you actually need.
Common mistake Thinking UnaryOperator<T> takes two type arguments
What it looks likeWriting UnaryOperator<String,String> instead of UnaryOperator<String>
Why it failsUnaryOperator<T> is Function<T,T> with the slots merged, so only T is supplied.
Do this insteadSupply one type argument, and both the input and the output will be that type.
Common mistake Expecting apply to change its input type
What it looks likePassing an Integer to a Function<String,Integer> and expecting it to work
Why it failsapply(t)takes a value of type T, which the declaration fixed in advance.Do this insteadMatch the input to T and use the result as R.
Practice Tracing Type Arguments
Given the declaration Function<String,Integer> measure, what is the type of the value you must pass to measure.apply, and what type does the call return? Then explain whether UnaryOperator<Integer> would accept the same kind of input.
Hints
- Look at which type fills T and which fills R.
- A UnaryOperator<Integer> uses one type argument for both the input and the output.
Write a declaration for a value that takes a String and returns the same String with its first letter capitalized, using UnaryOperator. Then write one for a value that takes a String and returns its length, using Function. Explain in one sentence why the first needs only one type argument and the second needs two.
Hints
- The first declaration should fill both slots with String.
- The second declaration has different input and output types.
Key Takeaways
- Function<T,R> describes a function from values of type T to values of type R, with T as the input type and R as the output type.
- The
apply(t)method takes a value of type T and returns a value of type R, and the declaration fixes both types in advance. - The input and output types of Function<T,R> are independent, so they may differ.
- UnaryOperator<T> is essentially Function<T,T>, so its input and output always share one type.
- DoubleUnaryOperator is a specialized version of UnaryOperator<Double>, and IntUnaryOperator follows the same naming pattern for int values.