Concepts / Generic Types and Type Parameters

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.

  • Programming

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.

Code walkthrough
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"));
  1. 1
    The first line declares lengthOf. Its type arguments say the input is a String and the result is an Integer. The lambda body s.length() computes a number.
  2. 2
    The second line declares shout. Its type arguments say the input and the result are both String. The lambda body s.toUpperCase() returns text.
  3. 3
    Calling lengthOf.apply("Stream") sends the text Stream into the lambda. The lambda returns 6, so the first println shows 6.
  4. 4
    Calling shout.apply("stream") sends the text stream into the lambda. The lambda returns STREAM, so the second println shows STREAM.
  • lengthOf is 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
Output
6
STREAM

What 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
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>

Definition

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.

Functionfunctional interfaceStringT: input typeIntegerR: output typefirst slotsecond slot
Which position in Function<T,R> is the input type and which is the output type, and how does each slot get filled when I write Function<String,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.

t of type TforFunction<String,Integer>: aStringapply(t)the one operationresult of type RforFunction<String,Integer>:an Integerpassed inreturned out
When apply(t) is called, how does a value of type T flow into the function and come out as type R, and how are those two types fixed by the declaration?

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.

FeatureFunction<T,R>UnaryOperator<T>
Type parameters you supplyTwo: T and ROne: T
Input type of apply(t)TT
Return type of apply(t)R, which may differ from TT, always the same as the input
Example declarationFunction<String,Integer>UnaryOperator<String>
Behavior in wordsMaps a T to an RMaps a T to another T

Function<String,Integer> and UnaryOperator<String> differ in whether the two slots can hold different types.

java
Output
java
Notes on the code
  • 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.

Function<T,R>two independent slotsUnaryOperator<T>Function<T,T>DoubleUnaryOperatorUnaryOperator<Double>specializationIntUnaryOperatorsame naming pattern for intR merged into TT fixed to doubleint version
How do DoubleUnaryOperator and IntUnaryOperator relate to UnaryOperator and Function, and which parts of the general type-parameter idea do they fix to primitive types?

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 like

    Function<String,Integer> takes a String and returns an Integer

    Why it fails

    T and R are independent slots, so they may hold different types.

    Do this instead

    Read each slot separately and fill it with the type you actually need.

  • Common mistake

    Thinking UnaryOperator<T> takes two type arguments

    What it looks like

    Writing UnaryOperator<String,String> instead of UnaryOperator<String>

    Why it fails

    UnaryOperator<T> is Function<T,T> with the slots merged, so only T is supplied.

    Do this instead

    Supply 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 like

    Passing an Integer to a Function<String,Integer> and expecting it to work

    Why it fails

    apply(t) takes a value of type T, which the declaration fixed in advance.

    Do this instead

    Match the input to T and use the result as R.

Practice Tracing Type Arguments

EASY

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.
MEDIUM

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.