Java Generics - 1. Introduction
This is part 1 of the 9 part series on Java Generics
If someone asks me - what are the best features of Java programming since the release of 1.5? I will vouch for two of them - Generics and Annotations. What makes Generics so special is the compile time checking of a type, the restrictions on the usage of a class it provides and simply its elegance. What amazes me however, is that the programmers rarely use the full power of Generics that Java programming language provides. So I thought of writing down a series of blogs that can be used by the programmers to understand and use them. There are a series of 9 blogs on the topic. They were too large to be adjusted into just one. So, just like in programming, I decided to 'divide and conquer'.
Topics
1. Introduction
2. Bounding
8. Target Types
|
|
Java Generics - Introduction
Generics are type checks during compilation. They add to the
stability and clarity of your Java code. As a developer you must strive to
write typed classes in case of need and always use generics for a typed class
(such as the classes extending collections and maps). Before we proceed further
let us look at a class called OBean which holds just an element
with getter and setter values, and explore why this is a potential problem
during runtime and if handled better with compiler checks can save us future
pangs.
public class OBean {
private Object e;
public void set(Object e){
this.e = e;
}
public Object get(){
return e;
}
}
|
Using this will involve memory of the input object type,
when retrieving the value using the get method of the class. Thus its
usage would be something like this.
OBean b = new OBean();
b.set(new Integer(5));
Integer i = (Integer)b.get();
System.out.println("Value=" + i);
|
It tells us that we can put any object in the class, but
when using we need to apply a proper cast. So the programmer must remember or reference the code for the type used in the set method, when using
the get
method. Incorrect cast can throw a runtime exception. What if the programmer
did this?
// can I do this?
String s = (String)b.get(); // Yes
System.out.println("Value=" + s); // Throws a ClassCastException
|
Well we would get a ClassCastException during runtime.
The compiler would give no signal of its potential threat during compilation.
Wouldn’t it be nice to have the compiler check for such potential threats and
let the programmer know about its dangers? Or better still, wouldn’t it be
better to disallow compilation of the above code? Well that’s what generics
are, and much much more as we shall se as we progress further. To allow the
compiler detect these problems the OBean class should be made generic to allow
us to insert only one type and retrieve only that type. Let us see how such a
class can be written. Declare a generic class Bean. Note that a letter E is
enclosed between the symbols < and >. <E> actually means any
element.
public class Bean<E> {
private E e;
public void set(E e){
this.e = e;
}
public E get(){
return e;
}
}
|
Not much of a difference. The object e of type Object
is now replaced by the letter E. Using the object of this class is
even simpler.
Bean<Integer>
bi = new Bean<Integer>();
//Bean<Integer>
bi = new Bean<>(); // Java 1.7 declaration
bi.set(new Integer(6));
Integer i2 = bi.get(); // no cast needed
System.out.println("Value=" + i2);
|
Note that you needn’t cast the object when you are
retrieving the values back. This solves the problem of casting. The java
compiler also checks for improper reference to another type. Thus the code in
the section below would give a compiler error. This means a programmer cannot
go wrong when accessing a typed object.
String s2 = bi.get(); // gives an error during compilation
bi.set("Hello"); // so will this
|
A quick word about the declaration – the declaration on the
left is matching the declaration at the right (<Integer>). There are
other ways of declaring too, and we will check those as we move forward. But
this is by far the most common way of declaring. So java 1.7 came up with a
shortcut. It allowed the user to forgo the type at the right side, allowing the
compiler to do that thinking on its own. Thus we can simply use the diamond (i.e.
the symbol < >) on the right side.
Bean<Integer> bi = new Bean<>(); // Java 1.7
declaration
|
Java could have eliminated the diamond completely on the
right side, but it chose instead to retain it, maybe to let the programmer know
that generics are being used.
Coming back to the Bean class’s declaration, we typed
the class with the letter E within the diamond < >, as <E>. Could we
use <T> instead? Yes why not, after all they mean the same thing. How
about lowercase E or T – like <e>, <t>? The answer is again yes.
But you should not use lowercase as a type declaration to avoid confusion, as
much as you must avoid declaring classes beginning with lowercase. Can you use
more than one letter to mean a type, say <Element> instead of <E>?
The answer is again, yes. But remember, that this will cause confusion when you
are using it as Element might represent a class as well. As a good programmer
you will not usually declare a class with a single uppercase letter and this
might help in keeping the distinction. In fact the letters should signify
something meaningful. As a guideline you can use the table below when declaring
the type element of a generic class. Though not very strict, java suggests
these naming conventions.
Generics Naming
Convention
|
|
Type
|
Meaning
|
E
|
Element (as in Collections)
|
K
|
Key (as in Maps)
|
V
|
Value (as in Maps)
|
N
|
Number
|
T
|
Type
|
S, U, etc
|
Any other type
|
Remember anything declared within the diamonds is a type,
and not a class. Say you only wanted to define a class which takes in only the Integer
type, can you declare the class as, to use it only for Integer?
public class Bean<Integer>
{ … }
The answer is yes and no. Yes you can declare but then it
would not mean that the type can only be of the type Integer, but would mean
the type should be referred with the name Integer (instead of E). It will take any
type while initialising. Besides, if you only want the class to take in values
of type Integer, you need not define your class as a typed one, rather as a
strict one without a type. Let’s have a funny example, and the reason why I
said earlier that you should avoid Strings as type names and stick only with
single uppercase letter. Let us declare a class called Improper.
public class Improper<Integer> {
public Integer i;
public void put(Integer i){
this.i = i;
}
public Integer get(){
return i;
}
}
|
Is this a valid declaration? Yes of course it is. Does it
mean it will take in only Integer? No! Of course, not. In fact it is no
different than the declaration of class Bean<E>. Here the string Integer
means any type, represented by the string “Integer”. Let’s check the use.
Improper<String>
im = new Improper<>();
im.put("Hello");
String s = im.get();
System.out.println("Value=" + s);
|
Is this usage proper? Yes it is again. Instead of
simplifying the problem, it in fact added to confusion. In fact the compiler
gives you a waring when you declare this class – “The type parameter Integer is hiding the type Integer”. So remember
the thumb rule for type declaration in a class – always use the uppercase
single letter, which makes sense. The letters such as <O> and <I>
don’t express themselves better as they appear to be the number zero and one to
the untrained eye. Of course you cannot use numbers within the diamonds like
<3>.
You might wonder what if I have a class that supports
multiple types? Should I use strings to define a type when I exhaust the
reasonable uppercase letters? The answer is still, no. If you have so many
parameter types then you are adding confusion to the declaration as well as its
usage. Do not declare a class having more than 3 types. Ok, maybe 4 just in
case of dire need. But that’s where you should stop. If more types are needed,
then you must inspect the structure of your class and split it properly. Let us
see a quick example of multiple type declaration. In fact you would have used
it when using the java Map class, which is defined by 2
types. Here is a class Pair which takes in 2 types –
represented by L (left) and R (right)
public class Pair<L, R> {
private L left;
private R right;
public void put(L left, R right){
this.left = left;
this.right = right;
}
public L getLeft(){
return left;
}
public R getRight(){
return right;
}
}
|
The usage is predictable. The rule is simple; whatever type
is used for L and R only those types are allowed in the place holders.
Pair<String,
Integer> siPair = new Pair<>();
siPair.put("Hello", new Integer(7));
String left
= siPair.getLeft();
Integer right = siPair.getRight();
System.out.println("Left=" + left + ",
Right=" + right);
|
If the type is defined for a class (or Interface), then it
can accept all the types that type extends (or implements). Since all classes
in java are derived from Object, any class which is defined
with the type Object will take in any object. But the catch is that when such
classes have methods which return the type values, then you’d get them back
only as an instance of Object. Here is a demonstration of
that code with the Bean<E> class by initializing it with Number and adding
objects which are of type Number (or extended from Number). Remember that the
classes Integer, BigInteger & Double
all extend the abstract class Number.
Bean<Number>
bn = new Bean<>();
// I can add anything which is of type Number (extends Number)
bn.set(new Integer(5));
bn.set(new BigInteger("4466"));
bn.set(new Double(7.75d));
// This is type safe as you can read it as a Number
Number n = bn.get();
Integer i = bn.get(); // compiler error
|
You can set any Number, but when you are reading it back you
can only read it as a Number. This preserves the type safety of the returned
value. In the above example, taking the return value of the get(
) method as an integer would give compiler error. Of couse you have can
cast the retrun value to the number, but then you have just defeated the cause
why generics exist in the first place. No prizes for guessing that the class
Pair<L, R> would work similarly.
By now, you would have understood the basic concept of the
creation and usage of generics. But this is just the tip of the iceberg. Java’s
generics can do a lot many things and has a lot of other features which will be
useful for the programmers. Let’s explore deeper. What if you want only a
particular type of elements to be used by the generic class? What if you want
to initialize the class only with say a Number type, i.e. the classes which
extend the class Number?
Comments
Post a Comment