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 at the URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Some of the information below is sourced from this URL. Generics were introduced in Java version 1.5.
Let’s imagine a web service that delivers information of various types. The web service may sometimes fail to deliver this information and must then report an error to its client. We can then 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 response
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 there is no error;
- line 7: messages explaining the error, null if there is 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 [Response] type using the new operator, we write:
Response<List<Product>> response = new Response<List<Product>>(...);
Since 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 use when querying this URL;
On the client side, we might have the following call:
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 impose constraints on its generic parameters. Let’s revisit the definition of the [map] method in RxJava:
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
The method takes 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 if R is a class, or implement R if R is an interface;
To better understand the two constraints above, let’s look at examples from the reference [https://docs.oracle.com/javase/tutorial/java/generics/bounded.html]:
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 a String!
}
}
- line 3: the [Box] class is parameterized by type T, which is the type of the field on line 5;
- line 15: the [inspect] method takes a parameter of type U. A method can also have generic parameters. It is then declared as follows:
Here, U is declared as the generic type of the method using <U> but with a constraint <U extends Number>, i.e., U must extend the [Number] class. Because of this constraint, the compiler reports an error on line 25;
- lines 23–24: calls to the [inspect] method with types derived from [Number];
The following results are obtained:
Note: To run the class in IntelliJ, follow these steps [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 [addNumbers] method takes a List<T> as a parameter, where T is a superclass of the [Integer] class. Because of this restriction, the integers 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 a 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:
java.lang.Double
java.lang.Long
java.lang.Integer
java.lang.Integer
java.lang.Integer
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.


