Theory:Generics and Object
As you know, generics enable us to parameterize types when defining classes (or interfaces) and methods. Parameterized types make it possible to re-use the same code while processing different concrete types.
# Reusing code with generics
Let's consider a generic class named GenericType
that stores a value of "some type".
class GenericType<T> {
private T t;
public GenericType(T t) {
this.t = t;
}
public T get() {
return t;
}
}
2
3
4
5
6
7
8
9
10
11
12
It is possible to create an object with a concrete type (e.g., String
):
GenericType<String> instance1 = new GenericType<>("abc");
String str = instance1.get();
2
We can also create instances with other types (Integer
, Character
) and then invoke the get
method to access the internal field. In this manner, generics allow us to use the same class and methods for processing different types.
# Reusing code with Object
But there is another way to reuse code. If we declare a field of type Object
, we can assign a value of any reference type to it. This approach was widely used before the introduction of generics in Java 5.
The following class demonstrates this concept:
class NonGenericClass {
private Object val;
public NonGenericClass(Object val) {
this.val = val;
}
public Object get() {
return val;
}
}
2
3
4
5
6
7
8
9
10
11
12
Now we can create an instance of this class with the same string as in the previous example (see GenericType
).
NonGenericClass instance2 = new NonGenericClass("abc");
It is also possible to create an instance by passing in a value of type Integer
, Character
, or any other reference type.
Using the Object
class this way allows us to reuse the same class to store different data types.
# The advantage of generics: from run-time to compile-time
After an invocation of the get()
method we obtain an Object
, not a String
or an Integer
. We cannot get a string directly from the method.
NonGenericClass instance2 = new NonGenericClass("abc");
String str = instance2.get(); // Compile-time error: Incompatible types
2
To get the string back, we must perform an explicit type-cast to the String
class.
String str = (String) instance2.get(); // "abc"
This works because a string was passed into instance2
. But what if the instance does not store a string? If this is the case, the code throws an exception. Here is an example:
NonGenericClass instance3 = new NonGenericClass(123);
String str = (String) instance3.get(); // throws java.lang.ClassCastException
2
Now we can see the main advantage of generics over the Object
class. Because there is no need to perform an explicit type-cast, we never get a runtime exception. If we do something wrong, we can see it at compile-time.
GenericType<String> instance4 = new GenericType<>("abc");
String str = instance4.get(); // There is no type-casting here
Integer num = instance4.get(); // It does not compile
2
3
4
A compile-time error will be detected by the programmer, not a user of the program. Because generics let the compiler take care of type casting, generics are both safer and more flexible compared to the Object
class.
# Generics without specifying a type argument
When you create an instance of a generic class, you have the option to not specify an argument type at all.
GenericType instance5 = new GenericType("my-string");
In this case, the field of the class is Object
, and the get
method returns an Object
as well.
The above code is equivalent to the following line:
GenericType<Object> instance5 = new GenericType<>("abc"); // it is parameterized with Object
Usually, you will not use generics parameterized by Object
due to the same problems as presented above. Just remember that this possibility exists.
# Conclusion
Both generics and Object allow you to write generalized code. Using Object
, however, may require explicit type-casting, which can lead to error-prone code. Generics provide type-safety by shifting type checking responsibilities to the Java compiler.