3. Signatures of generic classes and methods
![]() |
The RxJava library has many methods that accept instances of generic interfaces as parameters. Sometimes, their signatures are complex. Here are a few examples:
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
public final <R> Observable<R> flatMap(Func1<? super T,? extends Observable<? extends R>> func)
These two methods use two generic types, T and R. But what does the parameter definition of the map method mean, for example: Func1<? super T,? extends R> func ?
Information on generics can be found in the URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Some of the information below is sourced from this URL. Generics were introduced with Java 1.5.
Consider a web service that returns various types of information. The web service may sometimes fail to return this information and must then report an error to its client. We can standardize the web service’s response in the following form:
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
...
}
- line 1: the [Response] class is parameterized by type T;
- line 9: the type T is the type of the response body, which is what the client actually expects;
- line 5: an error code, 0 if no error;
- line 7: messages explaining the error, null if no error;
On the client side, we can then write:
Response<Product> product=getDataFromWebService(...) ;
or
Response<List<Product>>=getDataFromWebService(...) ;
The formal type T is replaced by an actual type, here [Product] or [List<Product>]. The generic class [Response<T>] is useful here because it allows us to work with a standard response.
To create a type [Response] using the new operator, we write:
Response<List<Product>> response=new Response<List<Product>> (...) ;
Starting with Java version 1.8, the previous statement can be written more simply:
Response<List<Product>> response=new Response<> (...) ;
There is no longer any need to specify the actual type of the generic parameters on the right side of the equals sign: the formal type specified on the left side of the equals sign will be used instead. This is called type inference: the compiler is able to determine the actual type of the generic parameters itself based on the context. This feature is widely used in lambda functions, where the actual type of a method’s generic parameters is often omitted.
Now, on the client side, the [getDataFromWebService] method could have the following signature:
<T1,T2> Response<T1> getDataFromWebService(String urlWebService, String httpMethod, T2 post)
Here we have a generic method parameterized by two types, T1 and T2:
- T1 is the type of the expected response body;
- T2 is the type of the value posted for a POST operation, null for a GET operation;
- urlWebService: is the URL of the web service;
- httpMethod: is the HTTP, GET, or POST method to be used to query this URL;
On the client side, the following call can be made:
Long id=... ;
Response<Product> product=this.<Product,Long>getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;
In this case, we will have T1=Product and T2=Long.
With Java 8, we can use type inference and write more simply:
Long id=... ;
Response<Product> product=getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;
Here is another possible call:
Long[] ids=... ;
Response<List<Product>> product=getDataFromWebService('http://localhost:8080/rest/products','POST',ids) ;
In this case, we will have T1=List<Product> and T2=Long[].
The generic class or method can place constraints on its generic parameters. Let’s revisit the definition of the [map] method from RxJava:
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
The method accepts two parameter types, T and R. The type [Func1] is a generic interface:
![]() |
Func1 defines a functional interface, i.e., an interface with a single method. Functional interfaces form the basis of Java 8 lambda expressions. Here, the interface’s method is defined as:
So T is the type of the parameter passed to [call] and R is the type of the result obtained. Let’s return to the definition of the [map] method:
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
- The [map] method expects a single parameter of type [Func1<? super T,? extends R>] and returns a type [Observable<R>];
- ? super T: the parameter passed to the [Func1.call] method must be of type T or a supertype of T;
- ? extends R: the type of the result returned by the [call] method must be of type R or a subtype of R if R is a class, or implement R if R is an interface;
To better understand the two previous constraints, let’s look at examples from the [https://docs.oracle.com/javase/tutorial/java/generics/bounded.html] reference:
package tests;
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u) {
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.set(new Integer(10));
integerBox.inspect(new Long(20));
integerBox.inspect(new Double(-1.78));
//integerBox.inspect("some text"); // error: this is still String!
}
}
- line 3: the class [Box] is parameterized by type T, which is the type of the field in line 5;
- line 15: the method [inspect] accepts a parameter of type U. A method can also have generic parameters. It is then declared as follows:
Here, U is declared as the method’s generic type using <U> but with the constraint <U extends Number>, meaning that U must extend the class [Number]. Because of this constraint, the compiler reports an error on line 25;
- lines 23–24: calls to the method [inspect] with types derived from [Number];
The following results are obtained:
Note: To execute the class in Intellij, proceed as follows [1, 2]:
![]() |
Now, consider the following example:
public void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i);
}
}
The method [addNumbers] accepts a List<T> type as a parameter, where T is a parent class of the [Integer] class. Because of this restriction, the integers int 1 through 3 can be added to the list (lines 2–4), and the method could be called as follows:
List<Number> numbers=new ArrayList<>();
// add double Number <-- Double
numbers.add(7.8);
// add Long Number <-- Long
numbers.add(1L);
// add List<Number> Number <-- Integer
addNumbers(numbers);
// display List<Number>
for(Number number : numbers){
System.out.println(number.getClass().getName());
}
This produces the following result:
To extend the Box<T> class, we would write:
class OtherBox<T> extends Box<T> {
}
The child class can itself introduce new generic parameters:
class AnotherBox<T, T1> extends Box<T> {
void consumer(T1 t1) {
System.out.printf("%s%n", t1);
}
}
An interface can also use generic parameters:
interface I<T> {
void doSomethingWith(T t);
}
To implement it, use the following syntax:
class A<T> implements I<T> {
@Override
public void doSomethingWith(T t) {
System.out.printf("%s%n", t);
}
}
We now know enough about generics to tackle lambda functions.


