Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
65 views

Generics in Java

Generics in Java – Part I introduces Java generics and provides examples of how to write generic classes and methods. It discusses how generics improve type safety over non-generic collections by preventing the addition of incorrectly typed elements. The document also covers naming conventions for generic types and how generics honor the Liskov substitution principle.

Uploaded by

vinh_itbk
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
65 views

Generics in Java

Generics in Java – Part I introduces Java generics and provides examples of how to write generic classes and methods. It discusses how generics improve type safety over non-generic collections by preventing the addition of incorrectly typed elements. The document also covers naming conventions for generic types and how generics honor the Liskov substitution principle.

Uploaded by

vinh_itbk
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 25

Generics in Java Part I

Venkat Subramaniam
venkats@agiledeveloper.com
http://www.agiledeveloper.com/download.aspx

Abstract
Java 5 (JDK 1.5) introduced the concept of Generics or parameterized types. In this
article, I introduce the concepts of Generics and show you examples of how to use it. In
Part II, we will look at how Generics are actually implemented in Java and some issues
with the use of Generics.

Issue of Type-Safety
Java is a strongly typed language. When programming with Java, at compile time, you
expect to know if you pass a wrong type of parameter to a method. For instance, if you
define

Dog aDog = aBookReference; // ERROR

where, aBookReference is a reference of type Book, which is not related to Dog, you
would get a compilation error.

Unfortunately though, when Java was introduced, this was not carried through fully into
the Collections library. So, for instance, you can write

Vector vec = new Vector();
vec.add("hello");
vec.add(new Dog());


There is no control on what type of object you place into the Vector. Consider the
following example:

package com.agiledeveloper;

import java.util.ArrayList;
import java.util.Iterator;

public class Test
{
public static void main(String[] args)
{
ArrayList list = new ArrayList();
populateNumbers(list);

int total = 0;
Iterator iter = list.iterator();
while(iter.hasNext())
{
total += ((Integer) (iter.next())).intValue();
}

System.out.println(total);
}

private static void populateNumbers(ArrayList list)
{
list.add(new Integer(1));
list.add(new Integer(2));
}
}

In the above program I create an ArrayList, populate it with some Integer values, and
then total the values by extracting the Integer out of the ArrayList.

The output from the above program is a value of 3 as you would expect.

Now, what if I change populateNumbers() method as follows:

private static void populateNumbers(ArrayList list)
{
list.add(new Integer(1));
list.add(new Integer(2));
list.add("hello");
}

I will not get any compilation errors. However, the program will not execute correctly.
We will get the following runtime error:

Exception in thread "main" java.lang.ClassCastException:
java.lang.String at com.agiledeveloper.Test.main(Test.java:17)


We did not quite have the type-safety with collections pre Java 5.

What are Generics?
Back in the good old days when I used to program in C++, I enjoyed using a cool feature
in C++ templates. Templates give you type-safety while allowing you to write code that
is general, that is, it is not specific to any particular type. While C++ template is a very
powerful concept, there are a few disadvantages with it. First, not all compilers support it
well. Second, it is fairly complex that it takes quite an effort to get good at using it.
Lastly, there are a number of idiosyncrasies in how you can use it that it starts hurting the
head when you get fancy with it (this can be said generally about C++, but that is another
story). When Java came out, most features in C++ that was complex, like templates and
operator overloading, were avoided.

In Java 5, finally it was decided to introduce Generics. Though generics the ability to
write general or generic code which is independent of a particular type is similar to the
template in C++ in concept, there are a number of differences. For one, unlike C++ where
different classes are generated for each parameterized type, in Java, there is only one
class for each generic type, irrespective of how many different types you instantiated it
with. There are of course certain problems as well in Java Generics, but that we will talk
about in Part II. In this part I, we will focus on the good things.

The work of Generics in Java originated from a project called GJ
1
(Generic Java) which
started out as a language extension. This idea then went though the Java Community
Process (JCP) as Java Specification Request (JSR) 14
2
.

Generic Type-safety
Lets start with the non-generic example we looked at to see how we can benefit from
Generics. Lets convert the code above to use Generics. The modified code is shown
below:

package com.agiledeveloper;

import java.util.ArrayList;
import java.util.Iterator;

public class Test
{
public static void main(String[] args)
{
ArrayList<Integer> list = new ArrayList<Integer>();
populateNumbers(list);

int total = 0;
for(Integer val : list)
{
total = total + val;
}

System.out.println(total);
}

private static void populateNumbers(ArrayList<Integer> list)
{
list.add(new Integer(1));
list.add(new Integer(2));
list.add("hello");
}
}

I am using ArrayList<Integer> instead of the ArrayList. Now, if I compile the code, I
get a compilation error:

Test.java:26: cannot find symbol
symbol : method add(java.lang.String)
location: class java.util.ArrayList<java.lang.Integer>
list.add("hello");
^
1 error

The parameterized type of ArrayList provides the type-safety. Making Java easier to
type and easier to type, was the slogan of the generics contributors in Java.

Naming Conventions
In order to avoid confusion between the generic parameters and real types in your code,
you must follow a good naming convention. If you are following good Java convention
and software development practices, you would probably not be naming your classes
with single letters. You would also be using mixed case with class names starting with
upper case. Here are some conventions to use for generics:

Use the letter E for collection elements, like in the definition
public class PriorityQueue<E> {}
Use letters T, U, S, etc. for general types

Writing Generic Classes
The syntax for writing a generic class is pretty simple. Here is an example of a generic
class:

package com.agiledeveloper;

public class Pair<E>
{
private E obj1;
private E obj2;

public Pair(E element1, E element2)
{
obj1 = element1;
obj2 = element2;
}

public E getFirstObject() { return obj1; }
public E getSecondObject() { return obj2; }
}

This class represents a pair of values of some generic type E. Lets look at some examples
of usage of this class:

// Good usage
Pair<Double> aPair
= new Pair<Double>(new Double(1), new Double(2.2));

If we try to create an object with types that mismatch we will get a compilation error. For
instance, consider the following example:

// Wrong usage
Pair<Double> anotherPair
= new Pair<Double>(new Integer(1), new Double(2.2));

Here, I am trying to send an instance of Integer and an instance of Double to the
instance of Pair. However, this will result in a compilation error.
Generics and Substitutability
Generics honor the Liskovs Substitutability Principle
4
. Let me explain that with an
example. Say I have a Basket of Fruits. To it I can add Oranges, Bananas, Grapes, etc.
Now, lets create a Basket of Banana. To this, I should only be able to add Bananas. It
should disallow adding other types of fruits. Banana is a Fruit, i.e., Banana inherits from
Fruit. Should Basket of Banana inherit from Basket for Fruits as shown in Figure below?



If Basket of Banana were to inherit from Basket of Fruit, then you may get a reference of
type Basket of Fruit to refer to an instance of Basket of Banana. Then, using this
reference, you may add a Banana to the basket, but you may also add an Orange. While
adding a Banana to a Basket of Banana is OK, adding an Orange is not. At best, this will
result in a runtime exception. However, the code that uses Basket of Fruits may not know
how to handle this. The Basket of Banana is not substitutable where a Basket of Fruits is
used.

Generics honor this principle. Lets look at this example:

Pair<Object> objectPair
= new Pair<Integer>(new Integer(1), new Integer(2));

This code will produce a compile time error:

Error: line (9) incompatible types found :
com.agiledeveloper.Pair<java.lang.Integer>
required: com.agiledeveloper.Pair<java.lang.Object>

Now, what if you want to treat different type of Pair commonly as one type? We will
look at this later in the Wildcard section.

Before we leave this topic, lets look at one weird behavior though. While

Pair<Object> objectPair
= new Pair<Integer>(new Integer(1), new Integer(2));

is not allowed, the following is allowed, however:

Pair objectPair
= new Pair<Integer>(new Integer(1), new Integer(2));

The Pair without any parameterized type is the non-generic form of the Pair class. Each
generic class also has a non-generic form so it can be accessed from a non-generic code.
This allows for backward compatibility with existing code or code that has not been
ported to use generics. While this compatibility has a certain advantage, this feature can
lead to some confusion and also type-safety issues.

Generic Methods
In addition to classes, methods may also be parameterized.

Consider the following example:

public static <T> void filter(Collection<T> in, Collection<T> out)
{
boolean flag = true;
for(T obj : in)
{
if(flag)
{
out.add(obj);
}
flag = !flag;
}
}

The filter() method copies alternate elements from the in Collection to the out
Collection. The <T> in front of the void indicates that the method is a generic method
with <T> being the parameterized type. Lets look at a usage of this generic method:

ArrayList<Integer> lst1 = new ArrayList<Integer>();
lst1.add(1);
lst1.add(2);
lst1.add(3);

ArrayList<Integer> lst2 = new ArrayList<Integer>();
filter(lst1, lst2);
System.out.println(lst2.size());

We populate an ArrayList lst1 with three values and then filter (copy) its contents into
another ArrayList lst2. The size of the lst2 after the call to filter() method is 2.

Now, lets look at a slightly different call:

ArrayList<Double> dblLst = new ArrayList<Double>();
filter(lst1, dblLst);

Here I get a compilation error:

Error:
line (34) <T>filter(java.util.Collection<T>,java.util.Collection<T>)
in com.agiledeveloper.Test cannot be applied to
(java.util.ArrayList<java.lang.Integer>,
java.util.ArrayList<java.lang.Double>)

The error says that it cant send ArrayList of different types to this method. This is
good. However, lets try the following:

ArrayList<Integer> lst3 = new ArrayList<Integer>();
ArrayList lst = new ArrayList();
lst.add("hello");
filter(lst, lst3);
System.out.println(lst3.size());

Like it or not, this code compiles with no error and the call to lst3.size() returns a 1.
First, why did this compile and whats going on here? The compiler bends over its back
to accommodate calls to generic methods, if possible. In this case, by treating lst3 as a
simple ArrayList, without any parameterized type that is (refer to the last paragraph in
the Generics and Substitutability section above), it is able to call the filter method.

Now, this can lead to some problems. Lets add another statement to the example above.
As I start typing, the IDE (I am using IntelliJ IDEA) is helping me with code prompt as
shown below:



It says that the call to the get() method takes an index and returns an Integer. Here is
the completed code:

ArrayList<Integer> lst3 = new ArrayList<Integer>();
ArrayList lst = new ArrayList();
lst.add("hello");
filter(lst, lst3);
System.out.println(lst3.size());
System.out.println(lst3.get(0));

So, what do you think should happen when you run this code? May be runtime
exception? Well, surprise! We get the following output for this code segment:

1
hello

Why is that? The answer is in what actually gets compiled (we will discuss more about
this in Part II of this article). The short answer for now is, even though code completion
suggested that an Integer is being returned, in reality the return type is Object. So, the
String "hello" managed to get through without any error.

Now, what happens if we add the following code:

for(Integer val: lst3)
{
System.out.println(val);
}

Here, clearly, I am asking for an Integer from the collection. This code will raise a
ClassCastException. While Generics are supposed to make our code type-safe, this
example shows how we can easily, with intent or by mistake, bypass that, and at best, end
up with runtime exception, or at worst, have the code silently misbehave. Enough of
those issues for now. We will look at some of these gotchas further in Part II. Lets
progress further on what works well for now in this Part I.

Upper bounds
Lets say we want to write a simple generic method to determine the max of two
parameters. The method prototype would look like this:

public static <T> T max(T obj1, T obj2)

I would use it as shown below:

System.out.println(max(new Integer(1), new Integer(2)));

Now, the question is how do I complete the implementation of the max() method? Lets
take a stab at this:

public static <T> T max(T obj1, T obj2)
{
if (obj1 > obj2) // ERROR
{
return obj1;
}
return obj2;
}

This will not work. The > operator is not defined on references. Hum, how can I then
compare the two objects? The Comparable interface comes to mind. So, why not use the
comparable interface to get our work done:

public static <T> T max(T obj1, T obj2)
{
// Not elegant code
Comparable c1 = (Comparable) obj1;
Comparable c2 = (Comparable) obj2;

if (c1.compareTo(c2) > 0)
{
return obj1;
}
return obj2;
}

While this code may work, there are two problems. First, it is ugly. Second, we have to
consider the case where the cast to Comparable fails. Since we are so heavily dependent
on the type implementing this interface, why not ask the compiler to enforce this. That is
exactly what upper bounds do for us. Here is the code:

public static <T extends Comparable> T max(T obj1, T obj2)
{
if (obj1.compareTo(obj2) > 0)
{
return obj1;
}
return obj2;
}

The compiler will check to make sure that the parameterized type given when calling this
method implements the Comparable interface. If you try to call max() with instances of
some type that does not implement the Comparable interface, you will get a stern
compilation error.

Wildcard
We are progressing well so far and you are probably eager to dive into a few more
interesting concepts with Generics. Lets consider this example:

public abstract class Animal
{
public void playWith(Collection<Animal> playGroup)
{

}
}

public class Dog extends Animal
{
public void playWith(Collection<Animal> playGroup)
{
}
}

The Animal class has a playWith() method that accepts a Collection of Animals. The
Dog, which extends Animal, overrides this method. Lets try to use the Dog class in an
example:

Collection<Dog> dogs = new ArrayList<Dog>();

Dog aDog = new Dog();
aDog.playWith(dogs); //ERROR

Here I create an instance of Dog and send a Collection of Dog to its playWith()
method. We get a compilation error:

Error: line (29) cannot find symbol
method playWith(java.util.Collection<com.agiledeveloper.Dog>)

This is because a Collection of Dogs cant be treated as a Collection of Animals
which the playWith() method expects (see the section Generics and Substitutability
above). However, it would make sense to be able to send a Collection of Dogs to this
method, isnt it? How can we do that? This is where the wildcard or unknown type comes
in.

We modify both the playMethod() methods (in Animal and Dog) as follows:

public void playWith(Collection<?> playGroup)

The Collection is not of type Animal. Instead it is of unknown type (?). Unknown type
is not Object, it is just unknown or unspecified.

Now, the code
aDog.playWith(dogs);
compiles with no error.

There is a problem however. We can also write:

ArrayList<Integer> numbers = new ArrayList<Integer>();
aDog.playWith(numbers);

The change I made to allow a Collection of Dogs to be sent to the playWith() method
now permits a Collection of Integers to be sent as well. If we allow that, that will
become one weird dog. How can we say that the compiler should allow Collections of
Animal or Collections of any type that extends Animal, but not any Collections of
other types? This is made possible by the use of upper bounds as shown below:

public void playWith(Collection<? extends Animal> playGroup)

One restriction of using wildcards is that you are allowed to get elements from a
Collection<?>, but you cant add elements to such a collection the compiler has no
idea what type it is dealing with.

Lower bounds
Lets consider one final example. Assume we want to copy elements from one collection
to another. Here is my first attempt for a code to do that:

public static <T> void copy(Collection<T> from, Collection<T> to) {}

Lets try using this method:

ArrayList<Dog> dogList1 = new ArrayList<Dog>();
ArrayList<Dog> dogList2 = new ArrayList<Dog>();
//
copy(dogList1, dogList2);

In this code we are copying Dogs from one Dog ArrayList to another.

Since Dogs are Animals, a Dog may be in both a Dogs ArrayList and an Animals
ArrayList, isnt it? So, here is the code to copy from a Dogs ArrayList to an Animals
ArrayList.

ArrayList<Animal> animalList = new ArrayList<Animal>();
copy(dogList1, animalList);

This code, however, fails compilation with error:

Error:
line (36) <T>copy(java.util.Collection<T>,java.util.Collection<T>)
in com.agiledeveloper.Test cannot be applied
to (java.util.ArrayList<com.agiledeveloper.Dog>,
java.util.ArrayList<com.agiledeveloper.Animal>)

How can we make this work? This is where the lower bounds come in. Our intent for the
second argument of Copy is for it to be of either type T or any type that is a base type of
T. Here is the code:

public static <T> void copy(Collection<T> from,
Collection<? super T> to)

Here we are saying that the type accepted by the second collection is the same type as T
is, or its super type.

Where are we?
I have shown, using examples, the power of the Generics in Java. There are issues with
using Generics in Java, however. I will defer discussions on this to the Part II of this
article. In Part II we will discuss some restrictions of Generics, how generics are
implemented in Java, the effect of type erasure, changes to the Java class library to
accommodate Generics, issues with converting a non-generics code to generics, and
finally some of the pitfalls or drawbacks of Generics.

Conclusion
In this Part I we discussed about Generics in Java and how we can use it. Generics
provide type-safety. Generics are implemented in such a way that it provides backward
compatibility with non-generic code. These are simpler than templates in C++ and also
there is no code bloat when you compile. In Part II we discuss the issues with using
Generics.

References
1. GJ http://homepages.inf.ed.ac.uk/wadler/gj
2. JSR 14 http://jcp.org/en/jsr/detail?id=14
3. http://java.sun.com/j2se/1.5.0/download.jsp
4. http://c2.com/cgi/wiki?OoDesignPrinciples
Generics in Java Part II
Venkat Subramaniam
venkats@agiledeveloper.com
http://www.agiledeveloper.com/download.aspx

Abstract
In Part-I we showed the benefits and usage of Generics in Java 5. In this part (Part-II), we
discuss how it is implemented in Java, and we delve into a number of issues with it. In
Part-III we will discuss the problems with mixing generic and non-generic code, and the
issues with converting a non-generic legacy code to generics.

Unchecked Warning
The Java compiler will warn you if it cant verify type-safety. You would see this if you
mix generic and non-generic code (which is not a good idea). Developing applications,
while leaving these kinds of warnings unattended is a risk. It is better to treat warnings
as errors.

Consider the following example:

public class Test
{
public static void foo1(Collection c)
{
}

public static void foo2(Collection<Integer> c)
{
}

public static void main(String[] args)
{
Collection<Integer> coll = new ArrayList<Integer>();
foo1(coll);

ArrayList lst = new ArrayList();
foo2(lst);
}
}

You have a method foo1 which accepts a traditional Collection as parameter. Method
foo2, on the other hand, accepts a generics version of the Collection. You are sending
an object of traditional ArrayList to method foo2. Since the ArrayList may contain
objects of different types, within the foo2 method, the compiler is not able to guarantee
that the Collection<Integer> will contain only instances of Integer. The compiler in
this case issues a warning as shown below:

Warning: line (22) [unchecked] unchecked conversion
found : java.util.ArrayList
required:
java.util.Collection<java.lang.Integer>

While getting this warning is certainly better than not being alerted about the potential
problem, it would have been better if it had been an error instead of a warning. Use the
compilation flag Xlint to make sure you do not overlook this warning.

There is another problem. In the main method, you are sending generics Collection of
Integer to the method foo1. Even though the compiler does not complain about this,
this is dangerous. What if within the foo1 method you add objects of types other than
Integer to the collection? This will break the type-safety.

You may be wondering how in the first place the compiler even allowed you to treat a
generic type as traditional type. Simply put, the reason is, there is no concept of
generics at the byte code level. I will delve into the details of this in the Generics
Implementation section.

Restrictions
There are a number of restrictions when it comes to using generics. You are not allowed
to create an array of generic collections. Any array of collection of wildcard is allowed,
but is dangerous from the type-safety point of view. You cant create a generic of
primitive type. For example, ArrayList<int> is not allowed. You are not allowed to
create parameterized static fields within a generic class, or have static methods with
parameterized types as parameters. For instance, consider the following:

class MyClass<T>
{
private Collection<T> myCol1; // OK
private static Collection<T> myCol2; // ERROR
}

Within generic class, you cant instantiate an object or an array of object of
parameterized type. For instance, if you have a generic class MyClass<T>, within a
method of that class you cant write:

new T();

or

new T[10];

You may throw an exception of generic type, however, in the catch block, you have to
use a specific type instead of the generic.

You may inherit your class from another generic class; however, you cant inherit from a
parametric type. For instance, while

class MyClass2<T> extends MyClass<T>
{
}

is OK,

class MyClass2<T> extends T
{
}

is not.

You are not allowed to inherit from two instantiations of the same generic type. For
example, while

class MyList implements MyCollection<Integer>
{
//...
}

is OK,

class MyList implements MyCollection<Integer>, MyCollection<Double>
{
//...
}

is not.

What is the reason for these restrictions? These restrictions largely arise from the way
generics are implemented. By understanding the mechanism used to implement generics
in Java, you can see where these restrictions come from and why they exist.

Generics Implementation
Generics is a Java language level feature. One of the design goals of generics was to keep
binary compatibility at the byte code level. By requiring no change to JVM, and
maintaining the same format of the class files (byte code), you can easily mix generics
code and non-generics code. However, this comes at a price. You may end up loosing
what generics are intended to provide in the first place type-safety.

Does it matter that generics are at the language level and not really at the byte code level?
There are two reasons to be concerned. One, if this is only a language level feature, what
would happen if and when other languages are expected to run on the JVM? If the other
languages to run on JVM are dynamic languages (Groovy, Ruby, Python, ), then it may
not be a big deal. However, if you attempt to run a strongly typed language on JVM, this
may be an issue. Second, if this is simply a language level features (one heck of a macro
essentially), then it would be possible to pass in correct types at runtime, using reflection,
for instance.

Unfortunately, generics in Java does not provide adequate type-safety. It does not fully
serve what it was created for.

Erasure
So, if generics is a language level feature, what happens when you compile your generics
code? Your code is striped out of all parametric types and each reference to parametric
type is replaced with a class (typically Object or something more specific). This process
is given a fancy name type erasure.

According to the documentation The main advantage of this approach is that it provides
total interoperability between generic code and legacy code that uses non-parameterized
types (which are technically known as raw types). The main disadvantages are that
parameter type information is not available at run time, and that automatically generated
casts may fail when interoperating with ill-behaved legacy code. There is, however, a
way to achieve guaranteed run-time type safety for generic collections even when
interoperating with ill-behaved legacy code.

While this provides interoperability with generic and non-generic code, it unfortunately
compromises type-safety. Lets look at the effect of erasure on your code.

Consider the example code:

class MyList<T>
{
public T ref;
}

By running javap c, you can look at whats in the byte code as shown below:

javap -c MyList
Compiled from "Test.java"
class com.agiledeveloper.MyList extends java.lang.Object{
public java.lang.Object ref;

com.agiledeveloper.MyList();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return

The type T of the ref member of the class has been erased to (replaced by) type Object.

Not all types are always erased to or replaced by Object. Take a look at this example:

class MyList<T extends Vehicle>
{
public T ref;
}

In this case, the type T is replace by Vehicle as shown below:

javap -c MyList
Compiled from "Test.java"
class com.agiledeveloper.MyList extends java.lang.Object{
public com.agiledeveloper.Vehicle ref;

com.agiledeveloper.MyList();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return

Now consider the example:

class MyList<T extends Comparable>
{
public T ref;
}

Here the type T is replace by Comparable interface.

Finally, if you use the multi-bound constraint, as in:

class MyList<T extends Vehicle & Comparable>
{
public T ref;
}

then the type T is replaced by Vehicle. The first type in the multi-bound constraint is
used as the type in erasure.

Effect of Erasure
Lets look at the effect of erasure on a code that uses a generic type. Consider the
example:

ArrayList<Integer> lst = new ArrayList<Integer>();
lst.add(new Integer(1));
Integer val = lst.get(0);

This is translated into:

ArrayList lst = new ArrayList();
lst.add(new Integer(1));
Integer val = (Integer) lst.get(0);

When you assign lst.get(0) to val, type casting is performed in the translated code. If
you were to write the code without using generics, you would have done the same.
Generics in Java, in this regards, simply acts as a syntax sugar.

Where are we?
We have discussed how Generics are treated in Java. We looked at the extent to which
type-safety is provided. We will discuss some more issues related to generics in the next
Part (Part III).

Conclusion
Generics in Java were created to provide type-safety. They are implemented only at the
language level. The concept is not carried down to the byte code level. It was designed to
provide compatibility with legacy code. As a result, generics lack what they were
intended for type-safety.

References
1. Generics in Java, Part-I at http://www.agiledeveloper.com/download.aspx (look at
references in Part-I)
Generics in Java Part III
Venkat Subramaniam
venkats@agiledeveloper.com
http://www.agiledeveloper.com/download.aspx

Abstract
In Part-I and II we discussed the benefits and usage of Java Generics, and how it is
implemented under the hood. In this Part-III, we conclude with discussions on issues with
mixing generic and non-generic (raw-type) code, and the issues of converting a non-
generic legacy code to generics.

Mixing Generic and non-generic code
Lets consider the following example:

import java.util.ArrayList;

public class Test
{
public static void addElements(Collection list)
{
list.add(3);
//list.add(1.2);
}

public static void main(String[] args)
{
ArrayList<Integer> lst = new ArrayList<Integer>();

addElements(lst);

//lst.add(3.2);

int total = 0;
for(int val : lst)
{
total += val;
}

System.out.println("Total is : " + total);

}
}

In the above example, lst refers to an instance of generic ArrayList. I am passing
that instance to the addElements() method. Within that method, I add 3 to the
ArrayList. Back in the main() main, I iterate though the ArrayList extracting one
integer value at a time from it, and total it. The output from the above program is:

Total is : 3

Now, in main(), if I uncomment the statement, lst.add(3.2);, I get a compilation
error as shown below:

Error: line (18) cannot find symbol method add(double)

On the other hand, if I leave that statement commented, but uncomment the statement
list.add(1.2); in the method addElements(), I dont get any compilation
errors. When I run the program, however, I get a runtime exception as shown below:

Exception in thread "main" java.lang.ClassCastException:
java.lang.Double
at Test.main(Test.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.ja
va:39)
at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccesso
rImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at
com.intellij.rt.execution.application.AppMain.main(AppMain.java:78)

What went wrong? In main() I am assuming that the ArrayList<Integer>
contains integer values. At runtime though, that assumption is proved wrong by the
addition of the value 1.2 in the addElements() method.

You may agree that getting a compile time error is better than getting a runtime error.
However, Generics dont fully provide the type-safety they were intended to provide. If
we are going to get runtime exception, it would be better to get that within the
addElements() method, where we are adding the value 1.2 to the ArrayList,
instead of in the main() when we are trying to fetch the elements out of the list. This
can be realized by using Collections classs checkedList() method as shown
below:

//addElements(lst);
addElements(Collections.checkedList(lst, Integer.class));

The checkedList() method wraps the given ArrayList in an object that will
ascertain that the elements added through it are of the specified type, in this case,
Integer type.

When I execute this program, I get the following runtime exception:

Exception in thread "main" java.lang.ClassCastException: Attempt to
insert class java.lang.Double element into collection with element type
class java.lang.Integer
at
java.util.Collections$CheckedCollection.typeCheck(Collections.java:2206
)
at
java.util.Collections$CheckedCollection.add(Collections.java:2240)
at Test.addElements(Test.java:11)
at Test.main(Test.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.ja
va:39)
at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccesso
rImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at
com.intellij.rt.execution.application.AppMain.main(AppMain.java:78)

Compare this exception message with the previous one. The exception is reported in this
case in line number 11 within the addElements() method instead of the previously
reported line number 21 within the main() method.

If you have to pass generic types to methods that accept non-generic types, consider
wrapping the objects as shown in the above example.

Converting non-generic code to generics
In Part II we discussed the type erasure technique and saw how the parameterized types
are converted to Object type or one of the types specified in the bound. If we have to
convert from non-generic type to generic type, is it simply the question of adding the
parameterized type E or replacing Object with E? Unfortunately, lifes not that simple.

Consider the following example:

import java.util.ArrayList;
import java.util.Collection;

public class MyList
{
private ArrayList list = new ArrayList();

public void add(Object anObject)
{
list.add(anObject);
}

public boolean contains(Object anObject)
{
if (list.contains(anObject))
return true;
return false;
}

public boolean containsAny(Collection objects)
{
for(Object anObject : objects)
{
if (contains(anObject))
return true;
}

return false;
}

public void addMany(Collection objects)
{
for(Object anObject : objects)
{
add(anObject);
}
}

public void copyTo(MyList destination)
{
for(Object anObject : list)
{
destination.list.add(anObject);
}
}
}

MyList is a class that represents my own collection. Lets not get too technical about
whether the addMany() method or the containsAny() method should actually
belong to the class MyList. From the design point of view, if you think these should not
belong here, they may belong elsewhere in a faade and the problems we will discuss
will then extend to that class. Now, lets look at a sample code that uses this class:

class Animal {}
class Dog extends Animal { }
class Cat extends Animal { }

public class Test
{
public static void main(String[] args)
{
MyList lst = new MyList();

Dog snow = new Dog();
lst.add(snow);

System.out.println("Does list contain my snow? "
+ lst.contains(snow));

Cat tom = new Cat();
lst.add(tom);

System.out.println("Does list contain tom? "
+ lst.contains(tom));
}
}

The above program produces the desired result as shown below:

Does list contain my snow? true
Does list contain tom? true

Now, lets set out the change the MyList to use generics. The simplest solution
modify Object with parameterized type E. Here is the result of that code change:


import java.util.ArrayList;
import java.util.Collection;

public class MyList<E>
{
private ArrayList<E> list = new ArrayList<E>();

public void add(E anObject)
{
list.add(anObject);
}

public boolean contains(E anObject)
{
if (list.contains(anObject))
return true;
return false;
}

public boolean containsAny(Collection<E> objects)
{
for(E anObject : objects)
{
if (contains(anObject))
return true;
}

return false;
}

public void addMany(Collection<E> objects)
{
for(E anObject : objects)
{
add(anObject);
}
}

public void copyTo(MyList<E> destination)
{
for(E anObject : list)
{
destination.list.add(anObject);
}
}
}

We modify the main() method to use the generic type (actually no change is needed to
main() if you will continue to use the non-generic style).

The only statement modified is shown below:

MyList<Animal> lst = new MyList<Animal>();

The program compiles with no error and produces the same result as before. So, the
conversion from raw-type to generics went very well right? Lets ship it?

Well, this hits right on the head with the issue of testing the code. Without good test, we
would end up shipping this code, only to get calls from clients who write code like the
following:

Dog rover = new Dog();
ArrayList<Dog> dogs = new ArrayList<Dog>();
dogs.add(snow);
dogs.add(rover);
System.out.println(
"Does list contain snow or rover? " +
lst.containsAny(dogs));

We get a compilation error:

Error: line (29)
containsAny(java.util.Collection<Animal>)
in MyList<Animal> cannot be applied to java.util.ArrayList<Dog>)

Whats the fix? We need to tweak the containsAny() method a little to accommodate
this reasonable call. The change is shown below:

public boolean containsAny(Collection<? extends E> objects)

(Refer to Part I and II of this article for details about lower-bounds, upper-bounds, and
wildcard).

Now, the program works fine again. However, if the main() is modified as follows, we
get a compilation error yet again:

lst.addMany(dogs);

Once again, this requires tweaking the code, this time the addMany() method.

public void addMany(Collection<? extends E> objects)

Now, lets take a look at the copyTo() method. Here is an example to use this method:

MyList<Dog> myDogs = new MyList<Dog>();
myDogs.add(new Dog());

myDogs.copyTo(new MyList<Dog>());

In the above code, we are copying Dogs from one MyList<Dog> to another
MyList<Dog>. Seems reasonable? Yep and it works. It is also legitimate to copy Dogs
from a MyList<Dog> to a MyList<Animal> isnt it? After all, a list of Animals
can hold Dogs. So, lets give that a shot:

MyList<Dog> myDogs = new MyList<Dog>();
myDogs.add(new Dog());

myDogs.copyTo(new MyList<Animal>());

This code, however, results in a compilation error as shown below:

Error: line (36) copyTo(MyList<Dog>) in MyList<Dog> cannot be
applied to (MyList<Animal>)

In this case, however, we do want a collection of base to be sent to this method. We have
to tweak again, this time the copyTo() method. We want this method to accept a
MyList of Dogs or MyList of Dogs base class. In general terms, we want it to accept
MyList of the parameterized type or MyList of the parameterized types base type. So,
here is the code for that:

public void copyTo(MyList<? super E> destination)

Depending on the situation, you may have to use the parameterized type E, a lower-
bound, an upper-bound or a wildcard. Unfortunately, this requires quite some thinking.
You may easily miss out on these details (like we saw in the above example) and the
problem may not surface until someone actually writes a piece of code that exercises your
code in a way so as to bring out the problem.

Conclusion
To summarized, Generics were developed to provide type-safety. They accomplish that
goal to a certain degree. However, they dont provide type-safety to the full extent. This
is largely due to the design goals and implementation constraints. First learn Generics in
Java. Then have the wisdom to decide when and how (much) to use it.

References
1. Generics in Java, Part-I at http://www.agiledeveloper.com/download.aspx (also
look at references in Part-I)
2. Generics in Java, Part-II at http://www.agiledeveloper.com/download.aspx

You might also like