As you know, generics enable types to be parameters when defining classes (or interfaces) and methods. It makes possible to re-use the same code to process different concrete types.
Reusing code with generics and Object
Let's consider a generic class named GenericType. It stores a value of "some type".
class GenericType<T> {
private T t;
public GenericType(T t) {
this.t = t;
}
public T get() {
return t;
}
}
It is possible to create an instance that stores a concrete type (String
, for example).
GenericType<String> instance1 = new GenericType<>("abc");
String str = instance1.get();
You can create instances with other types specified (Integer
, Character
) and then invoke the get method to take the internal field. So, generics allow you to use the same class and methods for processing different types.
But there is another way to reuse code in a similar way. If we declare the class field as Object
, we can assign a value of any reference type to it. This approach has been widely used before Java 5.
The class NonGenericType demonstrates it.
class NonGenericClass {
private Object val;
public NonGenericClass(Object val) {
this.val = val;
}
public Object get() {
return val;
}
}
Now we can create an instance of this type with the same string as in the previous example (see GenericType
).
NonGenericClass instance2 = new NonGenericClass("abc");
It is also possible to create an instance passing a value of type Integer
or Character
.
So, you can reuse the same class with an Object
field to store different types in the same way.
The advantage of generics: from run-time to compile-time
But there is one problem, after invocation the method get we obtain an Object, not a String
or Integer
. We cannot get a string directly from the method.
NonGenericClass instance2 = new NonGenericClass("abc");
String str = instance2.get(); // Compile-time error: Incompatible types
To get the string back, we should perform an explicit type-casting to the String
class.
String str = (String) instance2.get(); // "abc"
Of course, it works, but what if the instance does not store a string at all because the field val can keep any type (It is declared as Object
)? Then the code throws an exception. Here is an example:
NonGenericClass instance3 = new NonGenericClass(123);
String str = (String) instance3.get(); // throws java.lang.ClassCastException
Now we can see the main advantage of generics over the class Object
to re-use code. There is no need to perform an explicit type-casting and, as a consequence, we never get the runtime exception. If we do something wrong, we can see it at the 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
A compile-time error is detected by the programmer, not a user of the program. This makes generics both flexible and safe. At the same time, working with Object
requires type-casting that is error-prone. Let the compiler worry about it, not you.
Generics without specifying a type argument
When you create an instance of a generic class, you have a possibility 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 method get returns an Object
as well.
It is the same as the following line:
GenericType<Object> instance5 = new GenericType<>("abc"); // it is parameterized with Object
Usually, you will not use generics parameterized by Object
because it has the same problems as presented above. Just remember that this possibility exists.
Conclusion
Both generics and Object allows you to write a generalized code but using Object
may need explicit type-casting that is error-prone. Generics provide type-safety by shifting more type checking responsibilities to the Java compiler.