JavaGenerics

Generics and Object

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.

How did you like the theory?
Report a typo