Guide To Java Generics: The problem
What are Generics and what are they used for?
Generics allow types (classes and interfaces) to be a parameter for classes, interfaces, and methods. But what does that mean? To start, we need to understand the problem generics are trying to solve.
Type Safety
What does it mean to be a statically typed language?
A language is statically typed if the type of a variable is known at compile time. Java is statically typed since we know either i) declare the type of a variable in code or ii) the compiler can infer the type based on context.
What does type safety mean?
It’s important to know that statically typed and type safety are often wrongly interchanged but are actually two are separate concepts. The definition of type safety is built of two parts. First, a type error is error caused when there is a discrepancy between types in a operation or assignment. Attempting to treat an Integer as a String is an example.
Safety means there are procedures and checks to ensure something does not happen.
Thus, languages that are type safe, have mechanisms to prevent operations type errors from occurring. When the type safe checks are invoked are not part of the definition and is language dependent. Languages like Java employ type checking at compile time, while others will employ it at runtime.
Java is statically typed and employs strict type checking at compile time. This means that during compilation, the compiler validates that assignments and other operations on data are allowed by the data’s type. For example, if a variable is declared to be a type String, the compiler will prevent objects that are not Strings to be assigned to it. The following code will cause a compile error (and flagged in your favorite IDE):
public void processTransaction(Integer rate) {
String str = rate; // error
}
In the above code, we’re trying to assign data declared as an Integer to a variable declared as a String. This isn’t allowed because the compiler knows the data is of type Integer and we’re trying to treat it as a String.
Heterogeneous Classes and Casting
There are classes that operate on any type of object without caring what type of object it is. Classes of the Java Collections Framework are examples. An instance of the same List class can be used to store Integers or Strings or any other object.
But how do we accomplish this with type safety checks mentioned above? Well, prior to generics, most classes of the collection framework had signatures that used the type Object…and this is where we run into our first problems.
Since all classes in Java extend Object, having the collection classes work with Object resolved our compile time errors but lead to two problems.
Casting
First, by declaring operations on List using type Object, the compiler doesn’t know what specific kind of object is stored in the List – even if we know what kind of objects are stored in there.
List onlyStrings = new ArrayList();
onlyStrings.add("abc");
Object whatTypeIsThis = onlyStrings.get(0);
// type error since the compiler doesn't
// that a String is there, even though, we know it.
String value = onlyStrings.get(0);
In the above code example, we have put a single String into the List. Even if we absolutely know it’s a String, the compiler doesn’t know since the method signature of List#get() declares the return type as Object.
To get around this problem, developers resorted to casting objects into the types they wanted.
List onlyStrings = new ArrayList();
onlyStrings.add("abc");
String value = (String)onlyStrings.get(0);
Losing Type Safety
Second, by declaring everything as type Object, we are circumventing any compile time type safety checks. This means our list, which should only hold Strings, could now potentially hold mixed types – causing runtime errors.
List onlyStrings = new ArrayList();
onlyStrings.add("abc");
onlyStrings.add(new Integer(1)); // oops
// runtime error since the object at
// index 1 is of type Integer
String value = (String)onlyStrings.get(1);
The problem Statement
In many cases, the casting required to avoid type errors provided no real benefit and just cluttered up. In addition, the use of type Object to by-pass compilation errors introduced the loss of type safety, one of the major benefits of Java.
If a developer knows what kind of types should be in a array, they should be able to provide it as a ‘parameter’ during instance creation. Since this instance is bound to hold only objects of those types, the compiler should be able to detect if a developer is attempting to add some other type and flag it as a type error.
Generics, enter stage left
Generics introduces a mechanism for developers to create parametrized types. In other words, it allows developers to make types into parameters that are passed in as “arguments” when a generic class is instantiated or generic method is invoked. Let’s take a look at a brief example:
List<String> onlyStrings = new ArrayList<>();
onlyStrings.add("abc");
onlyStrings.add(new Integer(1)); // compile time error
// no more casting required, since the
// compiler 'knows' the List only stores Strings.
String value = onlyStrings.get(1);
The List is now being declared to only hold Strings (via the <String> syntax) and since the compiler now knows only Strings should be be inserted, it can flag type errors when we attempt to insert a Integer. In addition, we no longer need the ‘meaningless casting’ used when retrieving a value from the List.
In the next chapter, we take brief look at generic syntax and usage.