10.1. Introduction to Generics

Before we get into the details of Java generics, let’s think about the type of problem they can help us solve.

Thought Exercise

Imagine you’ve been hired by a company called “Amazeon” that ships different types of products all around the world. To keep up with their inventory, they model each product in a separate Java class declared in the products.amazeon package. Below, you can see UML Class Diagrams for classes that model two of their products: drones and cameras. We left out many potential instance variables and methods to keep the example short. However, Amazeon offers thousands of products each with dozens to hundreds of instance variables/methods, so the real code would be much more complex than the example below.

hide circle
set namespaceSeparator none
skinparam classAttributeIconSize 0

package "products.amazeon" {
    class Camera {
       -resolution: double

       +<<new>> Camera(resolution: double)
       +getResolution(): double
       +setResolution(resolution: double): void
    }
    class Drone {
       -maxHeight: double
       +<<new>> Drone(maxHeight: double)
       +getHeight(): double
       +setHeight(maxHeight: double): void
    }
}

When the company ships an item, they need a separate class to store shipping information about the item. Again, these classes could contain information related to the recipient of the package, cost of shipping, etc. However, we have simplified the classes for the example. The Java classes modeling different types of shipping containers (holding different types of items) might look like this:

hide circle
set namespaceSeparator none
skinparam classAttributeIconSize 0

package "shipping.amazeon" {
   class DroneShippingContainer {
      -contents: Drone
      -weight: double

      +<<new>> DroneShippingContainer(contents: Drone,
        \t\t\t\t\t weight: double)
      +setWeight(weight: double): void
      +getWeight(): double
      +setContents(contents: Drone): void
      +getContents(): Drone
   }
   class CameraShippingContainer {
      -contents: Camera
      -weight: double

      +<<new>> CameraShippingContainer(contents: Camera,
      \t\t\t\t\t weight: double)
      +setWeight(weight: double): void
      +getWeight(): double
      +setContents(contents: Camera): void
      +getContents(): Camera
   }
}

Under the current system, Amazeon can only store shipping information related to drones and cameras. Your boss has asked you to create new shipping container classes for each product in inventory (remember, there are thousands of these!). Realizing that you would need to copy/paste existing code thousands of times to make this work, you quickly start to think “there must be better way!”. Take a few moments to think about the following ideas:

  1. Which methods likely contain identical code?

  2. Which methods likely contain extremely similar code that is not exactly the same (but close!).

  3. Could we add an interface to help us solve this problem?

  4. Could we create an inheritance hierarchy to help us solve this problem?

  5. What are the drawbacks and limitations of each of these approaches?

Thought Exercise Follow-Up
  1. The getWeight and setWeight methods would likely contain identical code.

  2. The setContents and getContents methods would contain very similar code. The only difference is the data type of the methods. The formal parameter for setContents is Drone in the DroneShippingContainer and Camera in CameraShippingContainer. Also, the return type of getContents is different between both classes.

  3. We could create an interface that has a getWeight and setWeight method in it. This would help us to write code that would be compatible with all classes that have those methods. However, it would not reduce the redundancy of creating thousands of shipping container classes.

  4. We could create a parent class called ShippingContainer (UML diagram below). However, we could only promote (move up) weight and the methods related to weight. We couldn’t promote contents or methods related to contents because the data type is different across classes.

    hide circle
set namespaceSeparator none
skinparam classAttributeIconSize 0

package "shipping.amazeon" {
   ShippingContainer <|-- DroneShippingContainer: extends
   ShippingContainer <|-- CameraShippingContainer: extends

   class ShippingContainer {
      -weight: double
      +setWeight(weight: double): void
      +getWeight(): double
   }
   class DroneShippingContainer {
     -contents: Drone

     +<<new>> DroneShippingContainer(contents: Drone,
      \t\t\t\t\t weight: double)
     +setContents(contents: Drone): void
     +getContents(): Drone
   }
   class CameraShippingContainer {
     -contents: Camera

     +<<new>> CameraShippingContainer(contents: Camera,
      \t\t\t\t\t weight: double)
     +setContents(contents: Camera): void
     +getContents(): Camera
   }
}

  5. Interfaces allow us to write code that works will all of the classes (through polymorphism). However, they do not cut down the amount of code required to write each of the shipping container classes.

    Adding the parent class reduces the redundancy of our classes while also allowing polymorphism. However, we would still have to write thousands of classes - each of which is extremely similar to the existing classes - to make our boss happy.

There has to be a better way…this is where Generics comes in!

Generics allow us to write classes and methods where the data type of the variables is parameterized! They enable us to create a generic ShippingContainer class and add the data type of the contents as a parameter. In other words, we no longer need to write thousands of different shipping container classes. Instead, we can just write one! The only class we will need in this scenario can be seen in the UML Class Diagram below:

hide circle
set namespaceSeparator none
skinparam classAttributeIconSize 0
skinparam genericDisplay old

package "shipping.amazeon" {

   class ShippingContainer <DATATYPE> {
       -contents: DATATYPE
       -weight: double
       +<<new>> ShippingContainer(contents: DATATYPE,
       \t\t\t\t\t weight: double)

       +setContents(contents: DATATYPE): void
       +getContents(): DATATYPE
       +setWeight(weight: double): void
       +getWeight(): double
   }
}

We will write the code for this class (and show you how to use it in more detail) later in the chapter. For now, just take a moment to appreciate the upside of being able to write a single class instead of thousands of others!

DATATYPE is called the type parameter for the generic class. When we instantiate the class, we provide a type argument to specify the type for that particular object. This is comparable to what you have already done with method parameters (formal and actual). When you write a method, the formal parameter is a variable (placeholder) for the value that will be passed in (the actual parameter). With generics, the type parameter is the placeholder and the type argument is what gets passed in.

Note

When we create an object of a generic class, a type argument must be supplied for the type parameter. If a type parameter like T is included in a declaration using <T>, then the type argument supplied for T can be any reference type that is compatible with the Object class. Since all reference types in Java are compatible with Object, we say that T is unbounded in this scenariio because any refeference type can be used when supplying its type argument. In a later section, a way to limit what type arguments can be supplied for a type parameter will be introduced; however, for now, let us continue to explore how things work when a type parameter is unbounded.

Consider the ShippingContainer class introduced earlier. Its class declaration includes one type parameter named DATATYPE. Since DATATYPE is included in the class declaration immediately after the class name as <DATATYPE>, it is unbounded and any reference type can used when supplying its type argument. That means that we can make ShippingContainer objects to store any type of content!

Here are a few examples of instantiating our new ShippingContainer class. We will do more in the next few sections:

// Create a Drone object and add it to a shipping container
Drone highFlyingDrone = new Drone(175.0);
ShippingContainer<Drone> droneContainer;
droneContainer = new ShippingContainer<Drone>(highFlyingDrone, 25.0);

// Create a Camera object and add it to a different shipping container
Camera highResCamera = new Camera(128.0);
ShippingContainer<Camera> cameraContainer;
cameraContainer = new ShippingContainer<Camera>(highResCamera, 10.0);

Notice how the provided type argument allows us to change the type of object being stored in the shipping container with no need to make another class!

Overall, there are three main benefits to generic code [GENERICS] (we will discuss each of these more throughout the chapter):

  1. Enable programmers to implement generic classes/methods. These only have to be written once and can work with any reference type.

  2. Eliminate type casting.

  3. Stronger type checks at compile time allowing programmers to know when we made a mistake related to data types at compile time (instead of run time).

Rapid Fire Review
  1. What problem do Java generics help solve?

    1. Allow creating methods that accept multiple parameters.

    2. Enable creating thousands of identical classes for different types of products.

    3. Allow writing classes and methods where the data type is parameterized.

    4. Allow storing multiple objects in an array.

  2. What is a type parameter in Java generics?

    1. A variable that holds the value passed to the method.

    2. A placeholder for the type that will be passed into the class or method.

    3. The object that is stored inside a generic class.

    4. The memory location where the data type is stored.

  3. Why are Java generics beneficial according to the text?

    1. They allow you to store primitive types directly in collections.

    2. They eliminate the need for writing multiple classes for different data types.

    3. They make runtime errors less likely, but still require casting.

    4. They allow changing data types during runtime.

  4. In the context of generics, why is the type argument required when creating an object using a generic class?

    1. It specifies the type of data that object will operate on.

    2. It ensures the generic class inherits the appropriate classes/methods for the object.

    3. It ensures the generic class can only handle primitive types.

    4. It ensures that only default types are used in the class.

  5. Which of the following is NOT a benefit of generics mentioned in the text?

    1. Enable implementing classes and methods that work with any reference type.

    2. Eliminate the need for type casting.

    3. Allow programmers to catch type errors at compile time.

    4. Allow for faster execution of the program by using generics.