The motivation for generics comes from the pain of writing the same code multiple times, just to make it work for a wider range of types.
The underlying idea is to make the data type a usage parameter & not an implementation detail. By doing so, we can write code that is independent from a specific type, and at the same time accepts any type at use. As it sounds, it is generic !
So, for short, generics make our codebase lighter & more modular. ( Write less & use more)
Now that we've seen what generics are about, a question might be: why not use a super class for all the wanted types ?
For a better picture, in java we tend to say that generics are not needed, since the Object
class can be used to achieve the same goals. Indeed, the Object
class is generic since everything is an object in java, but generics offer much more advantages.
These advantages are:
- Type safety.
- No cast needed for data access.
- Compiler performance optimization.
Type safety is the most important one, since it makes our code much more robust & won't compile if it does not respect the type specified at use. Also, by sticking to the same type, our IDE & compiler can optimize its behavior to better match our workflow/program. Besides that, having a set type for all the generic entities makes debugging much easier & predictable.
No casting is also appreciated since it makes our code more straight forward & lighter. Besides that, since the type is already set, our IDE can also pick it up to help us write code faster.
Compiler performance is also a major one, since using the Object
class does not allow the compiler to manage memory at its best efficiency.
A last another advantage might be convention & standards. Indeed, generics in java are the convention rather than using the Object
class, and you are more likely to find code following this principle.
Now that we've seen why to use generics, it is time to learn how to do so. We will divide this section into three subsections & provide code snippets to follow along.
The first & most important notion of using generics are classes. The reason for that is because all generics are about is types, and these are implemented through classes.
Generic classes helps us write type independent class declarations, and we can do so as follows.
class Node<T>{
public T val;
public Node<T> next;
public Node(T val, Node<T> next){
this.val = val;
this.next = next;
}
}
As we can see, generics are distinguished with the <>
operators, and generic classes also follow this schema. To build a generic class, we declare a type placeholder between those chevrons, and inside the class, we'll use it as if it were a type.
The placeholder type can be named as you like, but the convention is the letters T, U, V, R
. Likely, you will find R
as a generic type for the return value, more on that later.
To use a generic class, all you need is to specify the type to follow. The code snippet below showcases an example.
...
// for ints
Node<int> head = new Node(10, null);
// for strings
Node<String> key = new Node("java", null);
...
That is the basic idea behind generic classes, now you can use that class for whatever type you wish.
Alongside generic classes, we also find generic interfaces. An interface is a contract that can be implemented by a class. Therefor, a generic interface is a type independent contract that can be implemented as well. Thus, by implementing a generic interface your class is going to be generic too.
To write a generic interface it is rather similar to a generic class. The code snippet below showcases how to do so.
interface Printer<T>{
void print(T arg);
}
To implement this generic class, we can do as follows.
class User implements Printer<User>{
...
// here we need to actually implement the code of the print method
void print(User user){
System.out.printf("@User %i\t .username=%s\n", user.id, user.username);
}
}
class Post implements Printer<Post>{
...
// here we need to actually implement the code of the print method
void print(Post post){
System.out.printf("@Post %i\t .title=%s\n", post.id, post.title);
}
}
As you can see, having generic interfaces is quite powerful, and we can expand our interfaces reach by doing so.
Now that we've seen how to declare generic classes & interfaces, we need to learn how to build generic methods since these are the ones that actually do something.
To implement a generic method we can do as follows.
class Node<T>{
private T val;
private Node<T> next;
public Node(T val, Node<T> next){
this.val = val;
this.next = next;
}
// generic setters
public <T> void setVal(T val){
this.val = val;
}
public <T> void setNext(Node<T> next){
this.next = next;
}
// generic getter
public <T> T getVal(){return this.val;}
public <T> Node<T> getNext(){return this.next;}
/*
The basic schema is :
Access_Modifier <...Types> Return_Type method_name(...args)
So both the <...Types> & the Return_Type can be generic, in fact the return type
is almost always one of the <...Types>
*/
}
As you see, to declare a generic method we need to tell java ahead about the used generic types. Then continue the method declaration.
To control the scope of your generic entities, you can either constrain or expand your generic types. To constrain we use the bounding strategy, and for expanding we use the wildcard strategy.
Generic bounding is used to constrain your generic types in a specific range. To do so, we use class inheritance or interface implementation. The code snippet below showcases how to do so.
/*
imagine that we need to compare our nodes with each other
to do so, we can bound our generic type to only contain
those which implement the comparable interface
*/
class Node<T implements Comparable<T>>{
/*
Our nodes can only be of types that implement comparable
*/
}
/*
imagine that we need to use our nodes for numeric data
to do so, we can bound our generic type to only contain those which extends the Number class
*/
class Node<T extends Number>{
/*
Our nodes can only be of types that extends Number
*/
}
With this approach, we are now bounding/constraining our generic types to a smaller set/range of types.
Generic wildcards are used to expand your range of types. It can be seen as the opposite of bounding but there are some differences. The idea is to allow the programmer to not set a type, but set a wildcard that can be replaced by any type.
Wildcards are basically used when you do not know what type should be considered. To understand this better, let's take an example.
Imagine that you are writing a generic class, and within it you want to write a generic method that is also static. Since it is static,it should not be bound to an instance of that class. Since, it is not bound to an instance, it should also not be bound/constrained to a type. That is where you need to use a wildcard.
The code example below showcases this notion.
class ListController<T>{
...
/*
Here, the method below takes a generic list,
but since the type T is also the type of the class instances
it won't work as intended. The reason is because a static method
should not be constrained to a single instance.
To fix this, we will use a wildcard
*/
public static void printList(List<T> arg){
...
};
}
class ListController<T>{
...
/*
Here, the method below takes a generic list,
and the type is not bounded/constrained to the current
instance. It matches anything that is called upon.
*/
public static void printList(List<?> arg){
...
};
}
By the same token, we can use wildcards with a bounding cover. The following code snippet showcases how to do so.
class ListController<T>{
...
/*
Match any type that extends the Number class.
*/
public static void printList(List<? extends Number> arg){
...
};
}
This explains all the crazy stuff that we see on java tutorials. If you do not agree with me, take a look to the following code snippet XD
// this is actually valid code !
ArrayList<?> al=new ArrayList<String>();
ArrayList<?> al2=new ArrayList();
ArrayList<? extends Runnable> al3=new ArrayList();
ArrayList<? super Runnable> al4=new ArrayList();
ArrayList<? super Runnable> al5=new ArrayList<Object>();
ArrayList<? super Runnable> al6=new ArrayList<Runnable>();
ArrayList<? extends Runnable> al7=new ArrayList<Runnable>();
ArrayList<? extends Runnable> al8=new ArrayList<Thread>();
ArrayList<? extends Object> al9=new ArrayList<Double>();
credit: Stackoverflow