Java Generics provide us with a mechanism for writing "generic" code, in the sense that one piece of code can work with many different types.
Interfaces give us a way to write code that works very generally, meaning that it can be used for many types. Essentially, we write code (like our sorting method from last class) where we say "as long as the type you have implements the interface we require, you can use our code on it". However, there's a serious limitation to the use of interfaces or regular old base classes (which amounts to the same thing) to provide "generic" code like this. In fact, the limitation came up (though I blithely ignored it in class) even with our simple examples. The limitation is this: type information gets lost.
This problem is most easily seen with the help of a simple concrete example.
public class MyPair {
Object x, y;
public MyPair(Object o1, Object o2) {
x = o1;
y = o2;
}
public Object first() { return x; }
public Object second() { return y; }
}
Now let's imagine we're using this:
MyPair p = new MyPair("ying","yang");
What is the type of
p.first()
? Well it is an Object. That means the only methods that can be called
with it are the few common to every Object. Nothing else is OK. In our example, we know the
result is actually a String, but
p.first().length()
won't compile. Our only alternative is to cast,
like this
String s = (String)p.first();
int n = s.length();
Not the biggest deal in this small example, but it gets
painful in bigger examples. More importantly, each of these casts could fail, because we
may unwittingly put the wrong type of object into the MyPair. Nothing checks these types
for us as we compile, so if we make such a mistake, it's as a runtime error, which is bad.
(Imagine if this code was for a heart monitor?)
Java provides us with another way to handle these problems: generics. Generics allow
you to write code where types become parameters, i.e. a variable name refers to
a type — like int, String or Double[
] — rather than referring to a value of
a fixed type. Type parameters are indicated by
<name>
, where name is the name of your type parameter.
For example, below we rewrite MyPair using a typeparameter named T
, and after
the declaration, we literally search-and-replace "Object" with "T":
public class MyPair<T> {
T x, y;
public MyPair(T o1, T o2) {
x = o1;
y = o2;
}
public T first() { return x; }
public T second() { return y; }
}
To instantiate a MyPair that stores Strings, we give String as a
type argument for the type parameter T
.
MyPair<String> p = new MyPair<String>("ying","yang");
It's important to note that the type of p is
MyPair<String>
, i.e. the type argument is a part of the full typename.
Thus, the type of p is different from MyPair<Scanner>
, for example. Moreover,
MyPair
by itself is not a type, since no type for the typeparameter T is specified.
The big payoff in this is that the compiler enforces that only Strings may be added to the
pair p and, therefore, it knows that the type of p.first()
is String. Thus,
p.first().length()
compiles no problem.
import java.util.*;
public class Ex0a {
public static class MyPair {
Object x, y;
public MyPair(Object o1,
Object o2) {
x = o1;
y = o2;
}
public Object first() {
return x;
}
public Object second() {
return y;
}
}
public static void main(String[] args) {
MyPair p = new MyPair("ying", "yang");
String s = (String)p.first();
System.out.println(s.length());
}
}
import java.util.*;
public class Ex0b {
public static class MyPair<T>{
T x, y;
public MyPair(T o1,
T o2) {
x = o1;
y = o2;
}
public T first() {
return x;
}
public T second() {
return y;
}
}
public static void main(String[] args) {
MyPair<String> p = new MyPair<String>("ying", "yang");
int n = p.first().length();
System.out.println(n);
}
}
Generics are especially valuable for "containers", i.e. classes that store and retrieve objects for you, but which don't much care what the objects are: like MyPair above. Our running favorite example of a container is ... Queue. Presumably, it's bothered you along the way that we have to keep recreating our Queue class every time there's something different we want to store. With generics, we don't have to do that. Literally, all we have to do is, starting with our Queue of Strings, modify our declaration like this
public class Queue<T> implements Iterable<T>and systematically search for String and replace it with T. If we do that, we get a Queue class that works for any type.
import java.util.*;
public class Queue implements Iterable<String>{
public void enqueue(String s) {
if (head == null) {
head = tail = new Node(s, null);
} else {
tail.next = new Node(s, null);
tail = tail.next;
}
}
public String dequeue() {
Node t = head;
head = head.next;
if (head == null) {
tail = null;
}
return t.data;
}
public boolean empty() {
return head == null;
}
public Iter iterator() {
return new Iter(head);
}
public class Iter implements Iterator<String>{
private Node curr;
public Iter(Node start) {
curr = start;
}
public boolean hasNext() {
return curr != null;
}
public String next() {
String s = curr.data;
curr = curr.next;
return s;
}
public void remove() throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
}
private class Node {
public String data;
public Node next;
public Node(String d,
Node n) {
data = d;
next = n;
}
}
private Node head = null, tail = null;
}
import java.util.*;
public class Queue<T> implements Iterable<T>{
public void enqueue(T s) {
if (head == null) {
head = tail = new Node(s, null);
} else {
tail.next = new Node(s, null);
tail = tail.next;
}
}
public T dequeue() {
Node t = head;
head = head.next;
if (head == null) {
tail = null;
}
return t.data;
}
public boolean empty() {
return head == null;
}
public Iter iterator() {
return new Iter(head);
}
public class Iter implements Iterator<T>{
private Node curr;
public Iter(Node start) {
curr = start;
}
public boolean hasNext() {
return curr != null;
}
public T next() {
T s = curr.data;
curr = curr.next;
return s;
}
public void remove() throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
}
private class Node {
public T data;
public Node next;
public Node(T d,
Node n) {
data = d;
next = n;
}
}
private Node head = null, tail = null;
}
Now, if we want a Queue of Scanners it would be
Queue<Scanner> Q = new Queue<Scanner>();
. If we want a Queue of
Strings it would be
Queue<String> Q = new Queue<String>();
. Here's a simple program
that shows this fact off.
import java.util.*;
public class Ex2 {
public static void main(String[] args) {
Queue<String> Q1 = new Queue<String>();
Queue<Integer> Q2 = new Queue<Integer>();
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
if (sc.hasNextInt()) {
Q2.enqueue(sc.nextInt());
} else {
Q1.enqueue(sc.next());
}
}
while (!Q1.empty()) {
System.out.print(Q1.dequeue() + " ");
}
while (!Q2.empty()) {
System.out.print(Q2.dequeue() + " ");
}
System.out.println();
}
}
~/$ java Ex2 I am 23 years and 5 months old on 3 March 2015 I am years and months old on March 23 5 3 2015
With the help of generics, we can turn to the biggest cheater's feature of the Java API: the ArrayList! ( ArrayList API documentation) ArrayList is a generic container that gives us array-like convenience for accessing elements (the method .get(i) gives us access by index) with linked-list like convenience for adding new elements (the method .add(x) adds a new element to the end of an ArrayList. There are even methods for inserting or removing elements at arbitrary positions. In fact, I fear you will be less enthralled with our good friend Queue now that you've met ArrayList.
import java.util.*;
public class Ex3 {
public static void main(String[] args) {
ArrayList<String> A1 = new ArrayList<String>();
ArrayList<Integer> A2 = new ArrayList<Integer>();
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
if (sc.hasNextInt()) {
A2.add(sc.nextInt());
} else {
A1.add(sc.next());
}
}
for (int i = 0; i < A1.size(); i++) {
System.out.print(A1.get(i) + " ");
}
for (int i = 0; i < A2.size(); i++) {
System.out.print(A2.get(i) + " ");
}
System.out.println();
}
}
~/$ java Ex3 I am 23 years and 5 months old on 3 March 2015 I am years and months old on March 23 5 3 2015