Java Templates I: What You Need to Know

Omar MohamedOmar Mohamed
8 min read

Table of Contents

  1. Introduction to Java Generics

  2. Basic Generic Template <T>

  3. Double and Triple Templates <T, R>

  4. Making a Method Generic While the Class is Not

  5. Bounded Type Parameters

  6. Multiple Bounds in Generics

  7. Conclusion

  8. References


In Java, Generics allow you to create classes, methods, and interfaces that can operate on any data type, thereby reducing code duplication and increasing type safety. Generics enable you to write flexible and reusable code, and they allow you to work with unknown types, denoted by type parameters such as <T>. Java templates (also known as generics) are especially useful for collections and algorithms that should work with multiple types.

Let's dive into each concept with code examples.


Basic Generic Template: <T> with a Class

A simple use of generics involves defining a class with a type parameter <T>. Here's an example of a generic class called Box, which can hold any type of object:

Code Example: Generic Class with <T>

// A generic class that works with any type T
class Box<T> {
    private T value;

    // Constructor to set the value
    public Box(T value) {
        this.value = value;
    }

    // Method to get the value of the box
    public T getValue() {
        return value;
    }

    // Method to set the value
    public void setValue(T value) {
        this.value = value;
    }
}

public class Main {
    public static void main(String[] args) {
        // Creating a Box for Integer
        Box<Integer> intBox = new Box<>(123);
        System.out.println("Integer value: " + intBox.getValue());

        // Creating a Box for String
        Box<String> strBox = new Box<>("Hello");
        System.out.println("String value: " + strBox.getValue());
    }
}

Explanation:

  • The class Box<T> has a type parameter T that is replaced by a specific type (e.g., Integer or String) when an object is created.

  • The method getValue() and setValue() both use the generic type T.

Example of Non-Compiling Code:

Box<Integer> intBox = new Box<>(123);
intBox.setValue("This won't compile!"); // Error: incompatible types: String cannot be converted to Integer

This code does not compile because we're trying to set a String in a Box<Integer>. Generics ensure type safety at compile time.


Double and Triple Templates: <T, R>

Generics can have more than one type parameter. For example, let's create a Pair class that can hold two objects of different types.

Code Example: Double Generic Class <T, R>

// A generic class that works with two types T and R
class Pair<T, R> {
    private T first;
    private R second;

    public Pair(T first, R second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public R getSecond() {
        return second;
    }
}

public class Main {
    public static void main(String[] args) {
        // Creating a Pair with Integer and String
        Pair<Integer, String> pair = new Pair<>(1, "One");
        System.out.println("First: " + pair.getFirst());
        System.out.println("Second: " + pair.getSecond());
    }
}

Explanation:

  • Pair<T, R> uses two type parameters, T and R. It can store two values of different types.

  • In the main method, a Pair<Integer, String> is created, meaning the first element is of type Integer, and the second is of type String.

Example of Non-Compiling Code:

Pair<Integer, String> pair = new Pair<>(1, "One");
pair.setFirst("This won't compile!"); // Error: String cannot be converted to Integer

This code does not compile because we're trying to set a String in place of an Integer.

You can also add more type parameters, like in a Triple<T, U, V> class, but it's less common.


Making a Method Generic While the Class is Not

In Java, it's possible to define a generic method in a non-generic class. This is useful when only certain methods need to operate on different types, while the rest of the class is not type-specific.

Generic Method with Two Type Parameters

You can define a method with two type parameters, which allows the method to handle different types for its arguments. Let’s extend the Utility class with a method that compares two different types.

Code Example: Generic Method in Non-Generic Class (Two Types)

// A non-generic class
class Utility {

    // A generic method with two different type parameters
    public <T, U> void printTypes(T obj1, U obj2) {
        System.out.println("Type of obj1: " + obj1.getClass().getName());
        System.out.println("Type of obj2: " + obj2.getClass().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Utility util = new Utility();

        // Calling the method with a String and an Integer
        util.<String, Integer>printTypes("Hello", 123);

        // Type inference: no need to specify <String, Integer>
        util.printTypes("World", 456.78);  // Inferred types: String and Double
    }
}

Explanation:

  • The method printTypes has two type parameters <T, U>, meaning obj1 and obj2 can be of different types.

  • In the main method, we explicitly specify the types <String, Integer> when calling the method, but this is optional as Java can infer the types from the method arguments.

  • The method prints the class names of the passed objects.

Example of Non-Compiling Code:

javaCopy code// This will cause a compilation error due to type mismatch
Utility util = new Utility();
util.<String, Integer>printTypes(123, "Hello");  // Error: inferred type mismatch

This code will not compile because we explicitly stated that the first parameter should be a String and the second should be an Integer, but provided 123 (an Integer) as the first parameter and "Hello" (a String) as the second.


Bounded Type Parameters

Sometimes, you want to restrict the types that can be used with a generic class or method. Bounded type parameters allow you to specify that a type must be a subclass (or implementer) of a specific class or interface.

Code Example: Bounded Type Parameter

// A generic class that only works with Number or its subclasses
class Calculator<T extends Number> {
    private T number;

    public Calculator(T number) {
        this.number = number;
    }

    public double square() {
        return number.doubleValue() * number.doubleValue();
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator<Integer> intCalc = new Calculator<>(4);
        System.out.println("Square of 4: " + intCalc.square());

        Calculator<Double> doubleCalc = new Calculator<>(5.5);
        System.out.println("Square of 5.5: " + doubleCalc.square());
    }
}

Explanation:

  • The class Calculator<T> has a bounded type parameter T extends Number. This means that T must be a subclass of Number (e.g., Integer, Double, Float, etc.).

  • This restriction allows us to call methods like doubleValue() that are available in the Number class.

Example of Non-Compiling Code:

Calculator<String> strCalc = new Calculator<>("This won't compile!"); // Error: String is not a subtype of Number

This code does not compile because String is not a subclass of Number.

Do you think that T super Number is supported in Java? No, and I'll explain why. However, this concept can be addressed using wildcards, which we will discuss in the next article.


Why T super ClassName is Not Supported in Java

When you declare a generic type parameter, like <T>, you are saying that T represents a specific type or class. The type parameter is invariant, meaning that the type is fixed once it's determined at compile-time.

  • For example, if you declare a generic class as <T extends ClassName>, you are constraining T to be either ClassName or any subclass of ClassName. This allows you to ensure that T has the methods and properties of ClassName or its subclasses.

  • However, T super ClassName would imply that T could be any superclass of ClassName, which creates ambiguity when you use the type parameter in the class or method. This is because generics are meant to provide type safety, and allowing super would weaken this guarantee since T could now be a variety of types, possibly without the necessary properties or methods.


Multiple Bounds

You can specify that a type parameter must extend multiple classes or implement multiple interfaces. This is called multiple bounds.

Code Example: Multiple Bounds

// An interface
interface Printable {
    void print();
}

// A class that implements the Printable interface
class Report implements Printable {
    @Override
    public void print() {
        System.out.println("Printing report...");
    }
}

// A generic class with multiple bounds (must be Number and Printable)
class Document<T extends Number & Printable> {
    private T document;

    public Document(T document) {
        this.document = document;
    }

    public void process() {
        document.print();
        System.out.println("Document number: " + document.doubleValue());
    }
}

public class Main {
    public static void main(String[] args) {
        // Compilation error: Integer does not implement Printable
        // Document<Integer> doc = new Document<>(123);

        // Compilation error: Report does not extend Number
        // Document<Report> doc2 = new Document<>(new Report());

        // Correct usage would be a class that extends Number and implements Printable (if it existed)
    }
}

Explanation:

  • T extends Number & Printable specifies that T must extend Number and implement Printable.

  • The commented-out code examples will not compile:

    • Integer extends Number but does not implement Printable.

    • Report implements Printable but does not extend Number.

In practice, it's rare to find or need a class that fulfills multiple bounds like this.


Conclusion

Generics in Java are a powerful feature that enhances code flexibility and reusability while ensuring type safety. By allowing classes, methods, and interfaces to operate on any data type, generics minimize code duplication and reduce runtime errors. In this article, we have explored:

  1. Basic Generics: Using a single type parameter <T> in classes and methods to handle various types safely.

  2. Multiple Type Parameters: Implementing double and triple templates to manage objects of different types within a single class.

  3. Generic Methods in Non-Generic Classes: Defining methods with type parameters to perform operations on diverse types without modifying the entire class.

  4. Bounded Type Parameters: Restricting type parameters to subclasses or implementers of specific classes or interfaces to enforce constraints.

  5. Unsupported Type Parameters: Explaining why <T super ClassName> is not supported and how generics ensure type safety.

  6. Multiple Bounds: Combining multiple bounds to specify that a type must meet several requirements.

Generics are crucial in writing robust, reusable, and type-safe code in Java. Understanding and correctly applying generics concepts, such as type parameters, bounds, and wildcards, can greatly enhance the efficiency and maintainability of your code.


References

1
Subscribe to my newsletter

Read articles from Omar Mohamed directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Omar Mohamed
Omar Mohamed