Chapter 5 - Java for Beginners Course

Generic Classes

When designing classes, there will be cases when you want to provide a class that can be generic, as in, a class that can work with different types of objects. However, you also want to provide a way for users of your class to specify the type they want to use inside of your generic class.

The way to achieve this is by using type parameters. You can think of type parameters in a similar way to a method parameter, whereas a method parameter is a value used inside a method, a type parameter is a class or interface used in the definition of our class.

Type parameters are specified inside angle brackets (< and >), and its syntax is as follows:

class ClassName<TypeParameter1, ..., TypeParameterN> {
    //...
}

Let’s use an example to clarify this concept.

The Gift class

Assume you’re building an online shop and you want to provide developers in your team/company with a Gift class to indicate that a certain item is to be provided as a gift from the user that is paying to someone else.

The item that the gift contains can be anything and we don’t control what classes are accepted. In its basic form, the following could be our initial Gift class:

public class Gift {
    private Object item;

    private String from;

    private String to;

    public Gift(Object item, String from, String to) {
        this.item = item;
        this.from = from;
        this.to = to;
    }

    public Object getGiftItem() {
        return item;
    }

    public void printGiftFromTo() {
        System.out.println("Gift from " + from + " to " + to);
    }
}

In its current form, users of our class will face a problem when retrieving the item from inside the Gift. For example, a user of our class could define the following gift:

Toy toy = new Toy("RC Car");
Gift gift = new Gift(toy, "John", "Jane");

But if they later want to get the Toy that is inside the gift, for example, if they want to do this in some other place of the code:

Toy giftedToy = gift.getGiftItem();

The user of our class will get a compilation error. This happens because the item inside our Gift class is of type Object and our getGiftItem() method returns a reference of type Object as well. Java will indicate that it can’t convert from Object to Toy.

The user of our class could use explicit casting which we’ll cover in the next chapter and do
Toy giftedToy = (Toy) gift.getGiftItem();, however, where possible we should avoid this.

Generic Gift class

From our point of view, our Gift class can work with any type of Object we want, but we don’t want to restrict our users to the Object class in Java. We want to allow them to specify the type of object that goes into the Gift.

This will be our input type parameter to our class, which we define like this:

public class Gift<T> {
    //...
}

Here we are defining that Gift has a type parameter that we are calling T. Bear in mind that T is an input type parameter, we don’t know what it will be, but we can use this parameter T as a type in our class.

This means we can define fields/variables of type T and have methods with return type/parameters of that type T as well.

So let’s do that:

public class Gift<T> {
    private T item;

    private String from;

    private String to;

    public Gift(T item, String from, String to) {
        this.item = item;
        this.from = from;
        this.to = to;
    }

    public T getGiftItem() {
        return item;
    }

    public void printGiftFromTo() {
        System.out.println("Gift from " + from + " to " + to);
    }
}

You’ll notice that we changed 3 things in the class. The main one being our item field, which changed from being of type Object to a field of type T. This is exactly what we wanted, this will allow users of our class to define the type of item that will be stored in the Gift.

We’re also changing our constructor, such that our input object matches the type we expect, and the return type of our getGiftItem() method to match the type of the object we’re storing.

How to use a Generic class?

Now that we have a generic Gift<T> class, we can use it like we normally did. The only difference is that we need to define the type we want to use inside angle brackets. The type can be any class or interface.

For example, if we want to use the Gift class to store a Toy object we’d do this:

Toy toy = new Toy("RC Car");
Gift<Toy> gift = new Gift<Toy>(toy, "John", "Jane");

You’ll see that we’re repeating the type parameter <Toy> on the left and right hand side of the assignment. To avoid repeating yourself, you can use the diamond operator (<>) in Java which has the same result, so these 2 are equivalent:

Gift<Toy> gift = new Gift<>(toy, "John", "Jane");
Gift<Toy> gift = new Gift<Toy>(toy, "John", "Jane");

Advantages of using generics

One of the advantages of using generics is that we’re telling the compiler more information about what we expect. In the example above, we’re telling it that our Gift should store an object of type Toy, so if we try to do this:

String notAToy = "This is a string";
Gift<Toy> gift2 = new Gift<Toy>(notAToy, "John", "Jane");
                                -------

The compiler will show an error, informing us that the constructor being invoked doesn’t exist (we expect the first parameter to be a Toy and not a String).

A second advantage is readability of the code. It is clearer for a person reading the code that Gift<Toy> gift = …​ is meant to contain a Toy object, rather than reading Gift gift = …​ which doesn’t tell much to a person reading the code.

And lastly, going back to our initial example, users of our generic class can now get the Toy that was stored inside of the Gift object as expected using the getGiftItem() method, without the need to use explicit casting (which we’ll cover in the next chapter), for example:

In the example code run the JavaGenericClassApp
Gift<Toy> gift = new Gift<Toy>(new Toy("RC Car"), "John", "Jane");
Toy giftedToy = gift.getGiftItem();
gift.printGiftFromTo();
giftedToy.printDescription();

Output:

Gift from John to Jane
Toy: RC Car

Generic Types

As well as defining Generic Classes, you can also define Generic Interfaces. A generic type is used to refer to either a generic class or a generic interface.

Generic Methods

Inside of a class, you can define a method that defines its own separate list of type parameters.

In our Gift<T> class for example, we could define the following generic method:

public static <S> Gift<S> anonymousGift(S item, String to) {
    return new Gift<S>(item, "?", to);
}

This method defines a type parameter S at the beginning, and it can only be used in the context of that method.

To invoke it, we do the following:

Gift<Toy> anonymousGift = Gift.<Toy>anonymousGift(new Toy("Drone"), "Jack");
anonymousGift.printGiftFromTo();

Output:

Gift from ? to Jack

The first line in our example above can be simplified as the compiler can infer that the type of object we’re using is Toy, so we can simply write this instead:

Gift<Toy> anonymousGift = Gift.anonymousGift(new Toy("Drone"), "Jack");
There are more details to cover about generics that go beyond the scope of this chapter. The introduction given here is important for the next chapters once we start using more classes that come directly from Java. Most of these classes/interfaces are generic types.