Generics: An Extra Measure of Type Safety in Java
Notes and code snippets from the Hello Java Workgroup of the NYJavaSIG held on December 14, 2011.
Bad Old Days (Java 1.4 and earlier):
Collections did not have type safety.
Wrong type could be inserted.
Cast required when extracting an item from the collection.
Type errors are detected at runtime (ClassCastException).
We set Eclipse's Java compiler compliance level to 1.4 and noted how in the old style we could add an integer to an intended ArrayList of Strings without warning, and get a runtime error when attempting to use it (BadOldDays.java):
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class BadOldDays { public static void main(String[] args) { List al = new ArrayList(); al.add("hello"); al.add("goodbye"); //al.add(new Integer(7)); Iterator i = al.iterator(); while(i.hasNext()) { String s = (String) i.next(); System.out.println(s); } } }
Since Java 5 (generics were added):
Generics - fancy name: parametric polymorphism
Old style (raw types) still allowed but cause warnings.
Using generics prevents inserting the wrong type into a collection.
Type errors are caught at compile time.
Next, we set Eclipse's Java compiler compliance level to 1.7 and converted the previous code to the new style with generics. Now, attempting to add an integer causes a compile time error: "The method add(String) in the type List<String> is not applicable for the arguments (Integer)." BetterDays.java:
import java.util.ArrayList; import java.util.List; public class BetterDays { public static void main(String[] args) { List<String> al = new ArrayList<String>(); al.add("hello"); al.add("goodbye"); //al.add(new Integer(7)); for(String s : al) { System.out.println(s); } } }
New in Java 7
Type inference using the diamond operator
http://docs.oracle.com/javase/tutorial/java/generics/gentypeinference.html
Saves you from typing out potentially long generic specifications twice.
This:
ArrayList<Map<String,? extends Float>> alm = new ArrayList<Map<String,? extends Float>>();
becomes this:
ArrayList<Map<String,? extends Float>> alm = new ArrayList<>();
My pet peeve: this is backwards!
It would be much cleaner to do it the way C++, Scala, and C# do it: simplify on the left side of the equals.
This eliminates all retyping, not just the type arguments.
In C++, this:
vector<boost::shared_ptr<Fruit> >* pvf = dynamic_cast<vector<boost::shared_ptr<Fruit> >*>(&vf);
becomes this:
auto pvf = dynamic_cast<vector<boost::shared_ptr<Fruit> >*>(&vf);
They battle it out here:
http://blogs.oracle.com/darcy/entry/javaone_2009_project_coin_slides
Covariance (or lack of it)
Good practice:
List<Number> ln = new ArrayList<Number>();
But the type parameter is not covariant!
ArrayList<Number> aln = new ArrayList<Integer>(); // not allowed
But a wild card:
ArrayList<?> aln = new ArrayList<Integer>();
or a bounded wild card:
ArrayList<? extends Number> aln = new ArrayList<Integer>();
is OK.
See BoundedWildCard class in Snippets.java and TryItOut.java
Experiment class in Snippets.java is a generic type.
Generic methods:
See Experiment class in Snippets.java.
When calling them, the compiler can usually infer the types by what is passed in. But to be explicit:
var<String>.func("hello");
or for static:
TheClass<String>.func("hello");
Type erasure:
In order to keep binary compatibility with older code, Sun chose to implement generics using type erasure.
All type checking is done at compile time, and the type information is discarded when generating byte code.
Thus, the byte code is identical for raw (non-generic) types and all generic types.
We ran javap -c on both BadOldDays.class and BetterDays.class and saw that the byte code was nearly identical.
As a consequence of type of erasure:
You can't directly create (new) an object of a class or method type parameter.
Static methods cannot use a class type parameter.
A static member variable cannot be or refer to a class type parameter.
You cannot create an array of a generic type unless the type parameter is the unbounded wildcard (?).
Class literals and instanceof must use raw types:
Map<Integer,String>.class or m instanceof Map<Integer,String> are illegal
Reification:
Might be coming in Java 9: http://www.infoq.com/news/2011/10/javaone-tech-keynote
Oracle's suggested type naming conventions:
The most commonly used type parameter names are:- E - Element (used extensively by the Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
Generics tutorial by Gilad Bracha (best I found):
http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf
http://docs.oracle.com/javase/tutorial/extra/generics/index.html
IBM:
Generics gotchas by Brian Goetz:
http://www.ibm.com/developerworks/java/library/j-jtp01255.html
Tutorial by Brian Goetz:
http://www.ibm.com/developerworks/java/tutorials/j-generics/index.html
Academic discussion of wildcards by the implementers:
http://bracha.org/wildcards.pdf
Fruit.java:
abstract class Fruit { abstract void biteMe(); protected int myNumber; } class Apple extends Fruit { Apple() { myNumber = ++appleCount; } void biteMe() { System.out.println("Apple " + myNumber + " crunches!"); } private static int appleCount = 0; } class Orange extends Fruit { Orange() { myNumber = ++orangeCount; } void biteMe() { System.out.println("Orange " + myNumber + " squirts!"); } private static int orangeCount = 0; } class McIntosh extends Apple {}TryItOut.java:
import java.util.ArrayList; import java.util.List; public class TryItOut { static void showCollection(List<? extends Fruit> l) { System.out.println("Showing " + l.size() + " fruits:"); for(Fruit f : l) { f.biteMe(); } } static void addMac(List<? super McIntosh> l) { l.add(new McIntosh()); } public static void main(String[] args) { List<Fruit> lf = new ArrayList<Fruit>(); lf.add(new Apple()); lf.add(new Orange()); lf.add(new Apple()); lf.add(new Orange()); showCollection(lf); List<Apple> la = new ArrayList<Apple>(); la.add(new Apple()); la.add(new Apple()); showCollection(la); addMac(new ArrayList<McIntosh>()); addMac(la); showCollection(la); // Can't do this - Orange is not base class of Apple: //addMac(new ArrayList<Orange>()); // List<String>[] lsa = new List[10]; // List<?>[] lsa = new List<?>[10]; // ok, array of unbounded wildcard type // Object o = lsa; // Object[] oa = (Object[]) o; // List<Integer> li = new ArrayList<Integer>(); // li.add(new Integer(3)); // oa[1] = li; // correct // String s = (String) lsa[1].get(0); // run time error, but cast is explicit } }Snippets.java:
import java.util.ArrayList; import java.util.HashMap; class Experiment<T> { Experiment(Class<T> c) { try { value = c.newInstance(); } catch (InstantiationException | IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private T value ; public void setValue(T value) { this.value = value; } public T getValue() { return value; } static <U,S extends U> U x(S t, S[] sa) { U[] ua = sa;//new S[10]; return t; } // pre-Java 7 type inference (from Effective Java 2nd edition) static <K,V> HashMap<K,V> newHashMap() { return new HashMap<K,V>(); } } interface I1 {} interface I2 {} class C1{} class C2 extends C1 implements I1, I2 {} class C3 { // For type parameters, use "extends" when the class either implements an interface or extends a class static <T extends C1 & I2 & I1> void func(ArrayList<T> al) {} // But apparently you have to list the class first because this complains that C1 is not an interface: //static <T extends I2 & I1 & C1> void func(ArrayList<T> al) {} void func2() { ArrayList<C2> al = new ArrayList<>(); func(al); } } class BoundedWildCards { void func() { ArrayList<Integer> ali = new ArrayList<Integer>(); ali.add(7); // Using upper bounded wild card: ArrayList<? extends Number> aln = ali; Number n = aln.get(0); System.out.println(n.byteValue()); // Integer i = 7; // aln.add(i); // not allowed - could be an array list of Float for all we know // Using lower bounded wild cards: ArrayList<? super Integer> aln2 = new ArrayList<Number>(); Integer i = 7; aln2.add(i); //Number n2 = aln2.get(0); // not allowed - could be an ArrayList of Objects for all we know Object o2 = aln2.get(0); // OK because everything is derived from Object // mnemonic from Effective Java 2nd edition: // PECS stands for producer extends, consumer-super // Not sure that helps! } } public class Snippets { public static void main(String[] args) { Experiment<String> s = new Experiment<>(String.class); HashMap<Integer,String> hmis = Experiment.newHashMap(); hmis.put(7, "hello"); Apple a = Experiment.x(new McIntosh(),new Apple[2]); System.out.println(a instanceof McIntosh); } }