Java Generics Tutorial

In this Java tutorial we learn how to define type placeholders with generic classes.

We learn how to define and instantiate a generic and how to constrain its types.

Here's a table of contents of what you'll learn in this lesson:
(click on a link to skip to its section)

Let's jump right in.

What are Generics

Essentially, generics are class templates. They allow us to define placeholders for the types of our class members.

How to define a generic in Java

We define a generic class by using open and close angular brackets after the class name. In between the brackets, we specify the type placeholder.

The placeholder will be substituted with an actual type when we implement the generic.

Syntax:
modifier class class_name<T> {

    // property of type T
    modifier T property_name;

    // method that returns
    // type T
    modifier T method_name() {

        return value_of_type_T;
    }

    // method that takes
    // argument of type T
    modifier return_type method_name(T parameter) {

        // body
    }
}

T acts as a placeholder for the type we will specify when we instantiate a new object.

Example:
public class Program {
    public static void main(String[] args) {}
}

// generic class
class Logger<T> {

    // method takes whatever type is
    // specified in the class type as
    // a parameter type
    public void consoleLog(T toLog) {
        System.out.println(toLog);
    }
}

In the example above, we define a generic class called ‘Logger’. In the ‘consoleLog’ method, we use the generic T type from the class as a parameter type.

How to instantiate a generic in Java

We instantiate an generic object like we would a normal class object, except that we specify the generic type between angle brackets.

Syntax:
class_name<type_class> object_name = new class_name<type_class>();
// or
class_name<type_class> object_name = new class_name<>();

There are a few things to note in this statement.

  • The angle brackets comes after the class name but before the constructor parentheses.
  • The type must be the type class wrapper. For example, Integer instead of int , or Float instead of float .
  • We don’t have to explicitly specify the type in the second set of angle brackets.

The concept of generics will become clear with an example.

Example:
public class Program {
    public static void main(String[] args) {

        // logger with 'String' type
        Logger<String> textLog = new Logger<String>();
        textLog.consoleLog("Logger with string type");

        // logger with 'float' type
        Logger<Float> intLog = new Logger<Float>();
        intLog.consoleLog(3.14f);
    }
}

// generic class
class Logger<T> {

    // method takes whatever type is
    // specified in the class type as
    // a parameter type
    public void consoleLog(T toLog) {
        System.out.println(toLog);
    }
}

In the example above, we instantiate two generic objects. One of type String, and one of type Float.

Because the ‘consoleLog’ method accepts whatever type is defined in the class, we can only pass that type to the method when we invoke it.

How to bound (constrain) a generic to a type in Java

To further improve on type safety, we can constrain the generic to only certain types.

To bound a generic type we use the extends keyword inside the angle brackets, followed by the type we want to constrain it to.

Syntax:
modifier class class_name<T extends type_class> {

    // class body
}

The type will be constrained to the specified type and any that inherits from it.

For example, the classes ‘Byte’, ‘Short’, ‘Integer’, ‘Long’, ‘Float’ and ‘Double’ are all children of the ‘Number’ class. If we constrained our generic to Number, it would include all of them.

Example:
public class Program {
    public static void main(String[] args) {

        // logger with 'String' type
        Logger<String> textLog = new Logger<String>();
        textLog.consoleLog("Logger with string type");

        // logger with a child
        // of 'Number' type
        Logger<Float> floatLog = new Logger<Float>();
        floatLog.consoleLog(3.14f);
    }
}

// constrain T to 'Number'
// and its children
class Logger<T extends Number> {

    public void consoleLog(T toLog) {
        System.out.println(toLog);
    }
}

This time, when we try to create a String object, the compiler raises an error on it, but not on the Float type.

Output:
Exception in thread "main" java.lang.Error: Unresolved compilation problems:
    Bound mismatch: The type String is not a valid substitute for the bounded parameter <T extends Number> of the type Logger<T>

How to set multiple bounds on a generic in Java

We’re not limited to just one type. To constrain more than one type, we use the & operator.

The only catch is, only the first constraint may be a class. The rest must be interfaces.

Syntax:
modifier class class_name<T extends type_class & interface & ...> {

    // class body
}
Example:
// standard interface
interface Log {
    public void consoleLog();
}

// class implements interface
// and defines the method body
class Logger implements Log {

    public void consoleLog() {
        System.out.println("Inside parent class: Logger");
    }
}

// generic class that constrains the types
// to the 'Logger' class and 'Log' interface
class MultiBound<T extends Logger & Log> {

    private T type;

    // we construct the generic
    // to take an object of one
    // of the two constrained
    // types, 'Logger' or 'Log'
    MultiBound(T type) {
        this.type = type;
    }

    // invoke the 'consoleLog'
    // method
    public void test() {
        this.type.consoleLog();
    }
}

public class Program {
    public static void main(String[] args) {

        // create an object of the generic
        // with the 'Logger' class as a type
        //
        // create a new 'Logger' object as
        // argument for the constructor type
        MultiBound<Logger> mb = new MultiBound<>(new Logger());

        // invoke the generic method that
        // invokes 'consoleLog'
        mb.test();
    }
}

In the example above, create a standard interface called ‘Log’, and a class called ‘Logger’ that implements it.

Then we create a generic class and constrain its types to the ‘Logger’ class and the ‘Log’ interface.

In the generic’s constructor we require one of the two types, and assign whicever to an object, so that we can access the members.

Lastly, we create a ‘test’ method in the generic that simply invokes the ‘consoleLog’ method.

In the main program we chose ‘Logger’ as the type we want as the generic type and passed a new ‘Logger’ object to the generic’s constructor.

That allows it be able to access the ‘consoleLog’ method that we invoked inside the ‘test’ method.

Summary: Points to remember

  • Generics are class templates that allow us to define member type placeholders.
    • We can substitute the type placeholders for our own when instantiating a generic class.
  • A generic can be constrained to one or more types to increase type safety.
    • We use the extends keyword to constrain a type.
  • We can constrain a generic to multiple types, but there is a limitation.
    • Only the first type may be a class, the rest must be interfaces that the class implements.