Generics are a direct child of the world Generalize. In such our motive is to make a method, class, property etc able to generalize over a domain, rather than being “centralized” or specific. Generics were introduced first for an ever haunting issue resolution. From their generics other benefits were leveraged out of the topic and a new paradigm of programming was evolved, which was more concise, perfectly natural and easy to understand (difficult to program at times).
Here is the issue that existed before generics came into being
The issue
Generics prime reason of existence is the detection of code issues during compile time because the issues at runtime are always hard to trace. Compile time errors are reported easily alongwith their location and we can easily go and correct them. However often runtime errors force as to play darts in the dark.
Let’s begin writing a non generic piece of code that will highlight the issue.
public class MyWallet {
private Object object;
MyWallet()
{
}
public void add(Object object)
{
this.object=object;
}
public Object get()
{
return this.object;
}
}
The purpose of this code is simple. It simulates a person’s wallet, in which we pass it and object, then retrieve it. As we can see the methods work on Object, we are free to pass in any object as long as it’s not a primitive time (value types like int , floats etc).
Now the real part , supposedly in future it was decided that the Wallet should only hold Integer. There is no way you can restrict the client of the above code to do so. Except that you have to mention that in the comments. Often a developer doesn’t pay much attention to the comments section. If instead of Integer he passes in String , there would be no stopping ; the compiler wont protest and in a more critical real life example this could mean disaster.
public class MyWalletDemo
{
public static void main(String args[])
{
//Only add Integer objects into the box.
MyWallet integerWallet = new MyWallet();
integerWallet.add(new Integer(10));
Integer obtainedVal = (Integer)integerWallet.get();
System.out.println(obtainedVal)
}
}
Okay the above would work fine, without breaking up. We know that the cast we used on the red line would work because we have honored the comment and did exactly as told. We passed in an Integer to the wallet. However notice that the compiler knows nothing about the comment, and won’t stop you from using the add method with an argument of a different type
public class MyWalletDemo2 {
public static void main(String args[])
{
//Only add Integer objects into the box.
MyWallet integerWallet = new MyWallet();
// a careless programmer modifies the arguments as shown below
integerWallet.add(“10”);
Integer obtainedVal = (Integer)integerWallet.get();
System.out.println(obtainedVal)
}
}
Now the red line would burst. Why because we are trying to cast a String into and Integer and deserve to see a cast exception by the compiler. Note also that this is not a compile time error rather a runtime error, which as discussed is a hideous demon in a programmer’s life.
One of the issue resolution that generics were born to resolve was the above , to make the compiler generate a compile time error for the above case , so that a String CAN’T be passed into the add method. If a programmer does that, it won’t compile and would conveniently lead the developer to the red line in the code above, the place where the illegal cast was done.
Resolution
An obvious question now to an attentive reader would be that, if ONLY an integer would cause the program to normally function, and a String would cause it to collapse. Why couldn’t have we just converted the type of object from Object to Integer. Like this
public class MyWallet {
private Integer object;
MyWallet()
{
}
public void add(Integer object)
{
this.object=object;
}
public Integer get()
{
return this.object;
}
}
This makes sense because at the first place, only the Integer value was supposed to work. So why would we use a datatype of Object to complicate the issue and allow other types like String as well.
Well this is a question of code extensibility. If we use the above approach, now our MyWallet class would ONLY accept integers. Okay fine. Say tomorrow, we needed a Wallet which would accept String as well. We would have to open up our class MyWallet , and add functionality for String as well. Say tomorrow, we need our Wallets to work with Bool , then after a few while we need it to work with our custom made classes etc etc. Cleary we see , that the only thing that is messing around is the data type , rest everything remains same. Yet we have to keep on modifying our MyWallet class over and over again to accommodate new types.
A very attentive reader would have sensed an issue in the above paragraph already. None of that was true. At one instant MyWallet class can only exists for ONE type , because there can be only one “get” method that returns a type from the class. Look at the following
public class MyWallet {
Integer integerArgument;
String stringArgument;
public Integer get()
{
return this.integerArg;
}
public String get()
{
return this.stringArg;
}
}
This overloading would obviously not work since the arguments to both of them are same (void). We may introduce different method names for different types , but then a programmer must know beforehand whether MyWallet is being used for Strings or Integers. And there is no way to communicate that to the compiler. If a developer has filled the MyWallet class with Integers , nothing would stop me if I tried to retrieve strings from it. It would compile , and then since the MyWallet class weren’t use for strings , and I had performed operations on strings , the program would burst.
The last way that would possibly work would be to make separate class for separate types . This would be both visual, and would stop compile time errors.
public class MyWallet ForStrings{
private String object;
MyWallet()
{
}
public void add(String object)
{
this.object=object;
}
public String get()
{
return this.object;
}
}
Similarly we can create MyWalletForInteger etc etc. But you and I both can agree that there can’t be anything uglier then the above architecture on the planet. Not only this is a extreme case of code duplication, this nearly decapitates every OOP rule and messes up everything. We would have tons of senseless code littered across, and keeping track of them, and fishing them out would be a terribly bad thing to perform. Such code duplication is not apparent in the above examples , but in reality it is something to be fear. For example a List collection is a generic collection in Java. It takes in objects and stores them and retrieves them and compares them, amongst many other things. Now the List implementation of java is well above a thousand LOC (LinesOfCode). If we cancel generics out , and do what we suggest above , that is make separate classes for separate types like ListOfStrings , ListOfIntegers , we would have well over a million lines of code that have no reason to exist in our project . We realize that the only thing different is the type, and the operations on them etc are the same. Its time to extract the differences and generalize our classes. This is what generics is all about.
It reminds us of a very important OOP rule which states “Encapsulate the changes”. With generics this is what our MyWallet class would look like
MyWallet With Generics
public class MyWallet {
private T object;
MyWallet()
{
}
public void add(T object)
{
This.object=object;
}
public T get()
{
return this.object;
}
}
The code above is self explanatory and beautiful. We have passed in what we call a “type parameter” as we declared the class. Now whatever we pass for T , our class would start operating over it. This is how we would use our MyWallet now
MyWallet integerWallet = new MyWallet();
Once the above is initialized , we’re free to invoke its get method without any need for a cast
integerWallet.add(new Integer(10));
Integer retrievedInteger= integerWallet.get(); //no cast needed.
Also , if we try to add an incompatible type to MyWallet , it will throw us a compile time error which would be easily corrected.
integerWallet.add(“10”);
MyWalletTest.java:5: add(java.lang.Integer) in MyWallet
cannot be applied to (java.lang.String)
integerWallet.add("10");
Type Erasure
Also to note is that Java deploys a Scheme called Type Erasure. Because of this after a program is compiled all the information about the Type of a class is stripped off. As a result at runtime using reflection we would be unable to obtain the Type information of a class.
In contrast , C# generics doesn’t strip off the Type information of a class , and using reflection we would be able to obtain the Type information of a class at runtime.
The discussion about type erasure is best left as a different topic on a different day
Any questions , comments are really welcomed .
Thanks