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.
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:
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:
Which methods likely contain identical code?
Which methods likely contain extremely similar code that is not exactly the same (but close!).
Could we add an interface to help us solve this problem?
Could we create an inheritance hierarchy to help us solve this problem?
What are the drawbacks and limitations of each of these approaches?
Thought Exercise Follow-Up
The
getWeight
andsetWeight
methods would likely contain identical code.The
setContents
andgetContents
methods would contain very similar code. The only difference is the data type of the methods. The formal parameter forsetContents
isDrone
in theDroneShippingContainer
andCamera
inCameraShippingContainer
. Also, the return type ofgetContents
is different between both classes.We could create an interface that has a
getWeight
andsetWeight
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.We could create a parent class called
ShippingContainer
(UML diagram below). However, we could only promote (move up)weight
and the methods related toweight
. We couldn’t promotecontents
or methods related tocontents
because the data type is different across classes.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:
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):
Enable programmers to implement generic classes/methods. These only have to be written once and can work with any reference type.
Eliminate type casting.
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
What problem do Java generics help solve?
Allow creating methods that accept multiple parameters.
Enable creating thousands of identical classes for different types of products.
Allow writing classes and methods where the data type is parameterized.
Allow storing multiple objects in an array.
What is a type parameter in Java generics?
A variable that holds the value passed to the method.
A placeholder for the type that will be passed into the class or method.
The object that is stored inside a generic class.
The memory location where the data type is stored.
Why are Java generics beneficial according to the text?
They allow you to store primitive types directly in collections.
They eliminate the need for writing multiple classes for different data types.
They make runtime errors less likely, but still require casting.
They allow changing data types during runtime.
In the context of generics, why is the type argument required when creating an object using a generic class?
It specifies the type of data that object will operate on.
It ensures the generic class inherits the appropriate classes/methods for the object.
It ensures the generic class can only handle primitive types.
It ensures that only default types are used in the class.
Which of the following is NOT a benefit of generics mentioned in the text?
Enable implementing classes and methods that work with any reference type.
Eliminate the need for type casting.
Allow programmers to catch type errors at compile time.
Allow for faster execution of the program by using generics.