A generic type is a generic class or interface that is parameterized over types.
The following MyStack
class will be modified to demonstrate the concept.
public class MyStack {
List<Object> list = new LinkedList<>();
public void push(Object o) {
list.addFirst(o);
}
public Object pop() {
Object o = list.getFirst();
list.removeFirst();
return o;
}
@Override
public String toString() {
return list.toString();
}
}
Since MyStack
methods accept and return instances of Object
, you are free to pass in whatever you want, provided that it is not one of the primitive types.
There is no way to verify, at compile time, how the class is used.
One part of the code may place an Integer
in the stack and expect to get integers out of it, while another part of the code may mistakenly pass in a String
, resulting in a runtime error.
public static void main(String[] args) {
MyStack stack = new MyStack();
stack.push("Rick");
stack.push(10);
String element = (String) stack.pop(); // => This will throw an exception
System.out.println(element);
}
This is what the generic version of MyStack
looks like.
public class MyStack<T> {
List<T> list = new LinkedList<>();
public void push(T o) {
list.addFirst(o);
}
public T pop() {
T o = list.getFirst();
list.removeFirst();
return o;
}
@Override
public String toString() {
return list.toString();
}
}
The concept of a data structure can be understood independently of the element type it manipulates:
Generic classes allow implementing data structures in a type-independent manner, which is a big contribution for software reusability!
Once you have a generic class, you can use a simple, concise notation to indicate the type(s) that should be used in place of the class’s type parameter(s).
At compilation time, the compiler ensures the type safety of your code and uses the erasure techniques to enable your client code to interact with the generic class.
This is how we declare a generic class.
public class MyStack<T> {
List<T> list = new LinkedList<>();
public void push(T o) {
list.addFirst(o);
}
public T pop() {
T o = list.getFirst();
list.removeFirst();
return o;
}
}
The type-parameter section follows the class' name (<T>
in our example).
A generic class may have multiple type parameters.
Note that, whenever you reuse a type parameter inside your class, you are binding the type of the variable, parameter, or return type to that of the class.
When using a generic class, we need to instantiate the type parameter.
MyStack<String> stack = new MyStack<>();
Then, all of its parameterized methods will follow the new type.
stack.push("Rick");
stack.push("Morty");
String character = stack.pop();
You will only be able to pass strings to the stack.push()
method.
You will not need to cast objects returned from the stack.pop()
method.
The Pair
interface and the OrderedPair
class both have 2 type parameters.
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
The following statements create two instantiations of the OrderedPair
class.
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
A raw type is a generic type used without providing a type argument.
MyStack rawStack = new MyStack();
Raw types show up in legacy code because lots of API classes (such as the Collections classes) were not generic prior to JDK 5.0.
When using raw types, you essentially get pre-generics behavior (a MyStack
gives you objects).
For backward compatibility, assigning a parameterized type to its raw type is allowed.
MyStack<Integer> intStack = new MyStack<>();
MyStack rawStack = intStack;
But if you assign a raw type to a parameterized type, you get a warning.
MyStack rawStack = new MyStack();
MyStack<Integer> intStack = rawStack;
We can only provide upper-bound restrictions on generic types.
public class MyStack<T extends Person> {
// class body...
}
You cannot use wildcards at a class level!
Generics enables classes and interfaces to have "parameters".
Much like the more familiar parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs.
Code that uses generics has many benefits over non-generic code.
OrderedList
generic class: a list that is always ordered.public static void main(String[] args) {
OrderedList<String> orderedStringList = new OrderedList<>();
orderedStringList.add("Rick");
System.out.println(orderedStringList);
orderedStringList.add("Morty");
orderedStringList.add("Beth");
orderedStringList.add("Beth");
System.out.println(orderedStringList);
orderedStringList.remove("Beth");
System.out.println(orderedStringList);
}
[Rick]
[Beth, Beth, Morty, Rick]
You can find the solution to this exercise here.