Generics

From Suhrid.net Wiki
Jump to navigationJump to search

Introduction

  • Generics are a way of providing type information for Abstract Data Types
  • Generics is a way to enforce ONLY compile-time type safety. Generics are implemented in the compiler only, the JVM has no clue about the use of generics.
  • All the type information is not present at run-time. The compiler strips out type information from the bytecode using a process called type erasure.
  • WHY Type erasure ? To ensure backward compatibility with legacy code.
  • This compile-time safety is broken when generic and non-generic legacy code are mixed up.

See below:

       private void bar() {
		List<Integer> li = new ArrayList<Integer>();
		li.add(new Integer(1));
		li.add(new Integer(2));
		foo(li);
		for(Integer i : li) {
			System.out.println(i.intValue()); // This will fail with a ClassCastException.
		}
	}
	
	private static void foo(List l) {
		l.add(new Integer(3));
		l.add(new String("4")); //Compiler ALLOWS this !  However, warning will be generated.
	}
  • Watch out when autoboxing is involved with legacy code.
List l = new ArrayList();
l.add(123); //Auto-boxing happens.
int i = l.get(0); //Compile-time error. Autounboxing cant work because get() returns Object and not Integer.

Generics and Raw Types

  • Apart from the parameterized type(s), each generic type also defines a raw type, which is the name of the generic type without the parameters.
  • It is possible to use a generic class by its raw type only. e.g. instead of using List<String> using a plain List.
  • The compiler will issue an "unchecked" warning when it detects that there could be a problem at runtime when using the raw type.
  • Assignment of Generic type to raw type is OK.
  • Assignment of Raw type to Generic type will lead to an unchecked warning.
  
         	List ll = new ArrayList(); //OK
		
		List<String> sL = ll; //Unchecked warning - Raw to Generic

                ll = sL; //OK - Generic to Raw
	
		List ll2 = new ArrayList<Integer>(); //OK
                
                ll2.add(new Integer(42)); //Unchecked warning since ll2 is raw
  • Compiler provides more details when the -Xlint:unchecked option.
  • Note by default, the compiler will not warn you when raw types are being used, it warns only for "unchecked" operations.

Polymorphism and Generics

  • This is OK:
List<Number> nos = new ArrayList<Number>();

nos.add(new Integer(10));
nos.add(new Double(20.2));
List<String> list = new ArrayList<String>();

However : Polymorphism only applies to the base type i.e list can be declared as arraylist You CANNOT do this:

 List<Animal> obj = new ArrayList<Dog>(); //NOT POSSIBLE

WHY ?

To prevent scenarios where you cannot add say, a Cat object to a Dog List. If the above conversion were possible it will be possible to do so. See below:

//NOTE : This is not possible actually, because the compiler prevents it.
public void foo() {
   List<Dog> dList = new ArrayList<Dog>();
   addAnimal(dList); //Compiler flags an error here. a Dog list cannot be assigned to an Animal list
}

private void addAnimal(List<Animal> aList) {
  aList.add(new Cat());
}

However, the SAME thing is possible with Arrays

public void foo() {
   Dog[] dA = new Dog[]{};
   addAnimal(dList); 
}

private void addAnimal(Animal[] aa) {
  aa[0] = new Cat(); //This will cause a runtime ArrayStoreException
}
  • The reason why it such polymorphism is possible with Arrays but not with collections is because of Type Erasure - Since there is no type information at run-time, JVM cannot raise an exception.
  • List<Dog> is not a subtype of List<Animal> - they are treated as different types. In case of arrays Dog[] is a subtype of Animal[] - because the type information is preserved at runtime.
  • This will be exactly the same problem when type-safe collections are mixed with non-type safe ones.
  • So, the compiler will prevent such polymorphic assignments when we are dealing with type-safe collections.
  • Because of the fact that Arrays retain type and Generics do not, we CANNOT mix Generics with Arrays. It is ILLEGAL to create an array of a Generic Type!
ArrayList[] la = new ArrayList[10];	//OK 
ArrayList<String>[] lsa = new ArrayList<String>[10]; //NOT OK
  • This is a CLASSIC OCPJP question:
List<List<String>> l = new ArrayList<ArrayList<String>>(); //THIS IS ILLEGAL;

//Whilst above looks normal, its not.
// The Generic Type has to match, in this case the generic type is List<String>! so this can be changed to:

List<List<String>> l = new ArrayList<List<String>>();

//OR this is also OK, here the generic type is ArrayList<String>

List<ArrayList<String>> = new ArrayList<ArrayList<String>>();

Wildcards

  • Q : How can generic collections be used polymorphically ?
  • A : Wildcards !
  • See example below. This means all Lists of Dogs, Cats, Elephants etc can be passed to the move method.
  • However the compiler ensures that the list cannot be modified to avoid wrong animals being inserted into the wrong lists.
  • How does the compiler do that ? It disallows any method in the List interface which accepts a generic parameters like add(E) or bar(E).
       public static void foo() {
		List<Dog> ld = new ArrayList<Dog>();
		foo(ld);
	}
	
	private static void move(List<? extends Animal> wildL) {
		for(Animal a : wildL) {
			a.move();
		}
	}


  • If you do want to modify the list, the wildcard declaration must be made safe by using the keyword super.
  • In the below example, it means that all Lists containing objects of superclasses of Dog can be passed to the foo method.
  • So foo is safe to add Dog objects to the passed list, since the passed list is guaranteed to be a list of some objects which are above Dog. So type-safety is maintained.
  • NOTE: the keyword super can only be used with wildcards ! Not with generic type declarations.
       public static void bar() {
		List<Dog> ld = new ArrayList<Dog>();
		foo(ld);
	}
	
	private static void foo(List<? super Dog> dogList) {
              dogList.add(new Dog()); //OK
              dogList.add(new Alsatian()); //OK
              dogList.add(new Cat()); //WONT WORK
              dogList.add(new Animal()); //WONT WORK. 
              dogList.add(new Object()); //WONT WORK.
	}
  • Why does the compiler not allow the addition of an animal object or a plain old object here ? Because List<? super Dog> says Lists containing objects of super-types of Dogs can be passed to this method. which means that we could pass a List<Animal> to this method as well. So, if the compiler allowed a plain Object to be added to a List<Animal> it wont make sense. So any objects of the super type are not allowed to be added to the list.
  • In general <? extends Type> will allow us to perform get() operations that will return objects upperbounded by Type. This will not allow the addition of any objects to the ADT.
 e.g. List<? extends Dog> - get operations will return type Dog. set operations will not be allowed.
  • In general <? super Type> will allow us to perform set() operations by passing objects that are lowerbounded by Type (Objects of Type and its subtypes are allowed). Get operations will be return type Object.
 e.g. List<? super Dog> - set will allow types of Dog and below, get will return type Object.
  • Common idiom is to use <? extends Type> to get objects from a data structure and <? super Type> be used to set objects in a data structure.
  • NOTE VERY IMPORTANT: Wildcards specify what kind of collections the method can accept. NOT what can be added to the collection
  • So try to look at the wildcard syntax as what kind of parameterized types it allows and then see what objects will make sense to be added to the ADT.
  • Only using the wildcard - like below, means that any list can be passed to it. But of course nothing can be added to the list.
        void foo(List<?> anyList)
  • List<?> list is different from List<Object> objList ! The second will take only lists which contain Objects, whereas the former will take any List.
  • It follows that List<? extends Object> is the same as List<?>
  •  ? is called the unbounded wildcard. If we want to use a generic type but dont know or dont care about what the actual type is, we can use "?" - List<?> means a list of some type.
  • The compiler allows the assignment of raw lists to List<?> //without any warning.
  • Also following the logic of wildcards, the compiler will not allow the addition of any object to a List<?>.
  • Wildcards can also be used to declared bounded variables.
e.g. List<? extends Number> genNumList;
//The following are then legal:

genNumList = new ArrayList<Integer>();
genNumList = new ArrayList<Float>();

Wildcard Capture

  • A wildcard represents a family of types.
  • However, the compiler needs to have a better idea of the wildcard type to enforce type checking - the compiler represents the wildcard by an anonymous specific type.
  • This specific unknown type used by the compiler is called the capture of the wildcard.

Generic Declarations

  • Generic class. See example below:
  • The Generic type T's object (t) is treated only as a regular Object. (Obviously, since at compile-time, we are unaware of its type)
class Node<T> {
	private T t;
	
	public Node(T t) {
		this.t = t;
	}
	
	public T getNode() {
		return t;
	}
	
	public String toString() {
		return t.toString();
	}
	
}

public class GenericDeclarations {

	public static void main(String[] args) {
		Node<Integer> ni = new Node<Integer>(1); 
                Node<JButton> njb = new Node<JButton>(new JButton());
	}
	
}
  • An invocation of a generic type is called a parametrized type like Node<Integer>. The compiler treats a parametrized type as a new type.
  • Wildcard types cannot be used in the header of a generic class to restrict the type. This is ILLEGAL:
class ComparableThings<? extends Comparable> { //ILLEGAL
}
  • Instead the correct way to restrict the parameterized types to be Comparables is as below:
class ComparableThing<E extends Comparable<E>> { //OK

}
  • The compiler will now only permit ComparableThing<Integer> i = new ComparableThing<Integer>() and not ComparableThing<Thread> t = new ComparableThing<Thread>();
  • Note: "T" is a convention used to indicate a Generic type. However you can use any identifier like
class Node<Type> { }
//Note, you can legal Java classes as identifiers too ! This produces something ABSURD like:
class Node<String> { }
//This does not mean Node of Strings!! rather String identifer "hides" the class name - it can be used as a generic type, like 
Node<Thread> nT = new Node<Thread>();
  • Gotcha's:
    • A constructor cannot specify the generic type. e.g. Node<T>() {} is not legal.
    • A type parameter cannot be used to create a new object. e.g. T t = new T() is not legal.
    • A type parameter is non-static. It cannot be used in a static context. Why ? Because like instance variables the type parameter is associated with objects.

Generic Methods

  • The class need not be generic. Individuals method can accept generic types.
  • The generic type has to be specified BEFORE the return type of the method. See below
class Node {
	
	StringBuffer nodeName = new StringBuffer("node");
	
	public <T> void append(T t) {
		nodeName.append(": " + t.toString());
	}
}


public class GenericDeclarations {

	public static void main(String[] args) {
		Node n = new Node();
		n.append(new JButton());
		n.append(new Thread());
		
		System.out.println(n.nodeName);
	}
	
}
  • This version will accept only Numbers
class Utils {
public <T extends Number,U extends Number> void addNum(T t, U u) {
		Number n1 = (Number) t;
		Number n2 = (Number) u;
		System.out.println(n1.intValue() + n2.intValue());
}

}
  • Generic methods invocation:
  • The type can be passed when calling a generic method ! e.g the above addNum can be called like:
Utils u = new Utils();

u.<Integer, Integer>addNum(30,30); //Here we are explicitly telling the compiler to treat the parameters as Integers.

//In  normal method invocations, the compiler will "infer" the type

u.addNum(20.5, 30.2); //Here the compiler "infers" the type as Double
  • Note the following method declaration is NOT generic, it assumes there is a class called "T" that is available somewhere
     public void foo(T t) { }
  • See the below method in Collections class (simplified) which returns a reverse order comparator for a given collection.
     public static <T> Comparator<T> reverseOrder() {
        return (Comparator<T>) REVERSE_ORDER;
    }

    private static final Comparator REVERSE_ORDER = new ReverseComparator();

    private static class ReverseComparator<T> implements Comparator<Comparable<Object>> {

        public int compare(Comparable<Object> c1, Comparable<Object> c2) {
            return c2.compareTo(c1);
        }
    }
  • Note that type T will be the type of the objects contained in the collection. See below: Here T will be Integer.
  • Collections.sort takes (List<T>, Comparator<? super T>) as the two parameters. Now, Collections.reverseOrder() returns a Comparator<T> which will be used as Comparator<Integer>
  List<Integer> marks = new ArrayList<Integer>();
  Collections.sort(marks, Collections.reverseOrder());