Introduction to Generics - Wrox Press Java Programming 24-Hour Trainer 2nd (2015)

Wrox Press Java Programming 24-Hour Trainer 2nd (2015)

Lesson 12. Introduction to Generics

In the previous lesson you saw an example of a collection that stores objects of different types (see Listing 11-2). During the run time, that program would test the actual type of each object and cast it to an appropriate type—Customer or Order. If some code adds an element of another (unexpected) data type, this will result in a casting error, ClassCastException. Instead of leaving it until run time, it would be nice if during the compilation the compiler would prevent using unexpected types with collection, objects, or even method arguments and return types.

Java supports generics, which enable you to use parameterized data types—you can declare an object, collection, or method without specifying a concrete data type, shifting the definition of concrete types to the code that will use these objects, collections, or methods. In other words, ageneric type is one that can work with parameterized data types.

Parameterized Classes

Not only Java methods can accept parameters (also known as arguments), but classes can have them as well. I’ll show you how to do it in the section on custom parameterized classes.

By using generic notation, you get help from Java compiler, which does not allow you to use objects of the “wrong” types that don’t match the declaration. In other words, you can catch improper data types earlier, during the compilation phase. This concept is easier to explain by examples, and so we’ll get right into it.

Generics with Classes

Consider the ArrayList from Listing 11-2, which is a kitchen sink–like storage that can hold pretty much any object. But if you add the parameterized type Customer in angle brackets (ArrayList<Customer>) to the declaration of the customers collection (see Listing 12-1), any attempt to place an Order object there generates the following compiler error:

The method add(Customer) in the type ArrayList<Customer> is not

applicable for the arguments (Order).

Think of it this way: ArrayList can be used to store any objects, and using generics enables you to put a constraint on the types of objects allowed in a specific instance of ArrayList. This is an example of a parameterized class, which is just one use for generics.

Listing 12-1: Using generics in the collection

import java.util.List;

import java.util.ArrayList;

public class TestGenericCollection {

public static void main(String[] args) {

List<Customer> customers = new ArrayList<>();

Customer customer1 = new Customer("David","Lee");

customers.add(customer1);

Customer customer2 = new Customer("Ringo","Starr");

customers.add(customer2);

Order order = new Order();

customers.add(order); // Compiler error

}

}

Getting an error during compilation is better than getting runtime cast exceptions. Note the empty angle brackets in the preceding example. Those are called the diamond operator—you don’t need to repeat <Customer> on the right because this type has been specified already on the left. As a refresher, I’ve been using the List interface to declare the variable customers as explained in the previous lesson in the section "Programming to Interfaces."

What makes the ArrayList class capable of rejecting the unwanted data types? Open the source code of the ArrayList itself (pressing F3 in Eclipse shows the source code of any class or interface, if available). It starts as follows:

public class ArrayList<E> extends AbstractList<E>

implements List<E>, RandomAccess, Cloneable, Serializable

This magic <E> after the class name tells the Java compiler that this class can use some types of elements, but which ones remains unknown until a concrete instance of ArrayList is created. In Listing 12-1, the type parameter <Customer> replaces <E> during compilation, and the compiler ensures that the code stores only instances of Customer objects in the collection customers .

Type Erasure

I’d like to stress that this <E> notation is used only during the declaration of the type. The code in Listing 12-1 does not include <E>. The compiler replaces <E> with Customer and erases the parameterized data type in the byte code. This process is known as type erasure; it’s primarily done for compatibility with code written in older versions of Java that didn’t have generics. The generated byte code is the same with parameterized and with raw data types.

Now you can simplify the code from Listing 11-2 by removing casting (see Listing 12-2). Why? Because with generics, when the compiler sees a specific type, it automatically generates the bytecode, which performs casting internally! That’s why you don’t even need to cast the data returned by the method get(i) from Object to Customer any longer. Besides, you’re guaranteed that the collection customers will have only Customer instances. Java compiler has the ability to look at the method invocation and properly guess the type of the argument. It’s called type inference. It’s used not only with generics, but with lambda expressions as well, which will be covered in Lesson 13.

Listing 12-2: Iterating through customers without casting

List<Customer> customers = new ArrayList<>();

// The code to populate customers is omitted for brevity

// Iterate through the list customers and do something with each

// element of this collection. No casting required.

for (Customer customer: customers){

customer.doSomething();

}

Raw Types

Using the parameterized ArrayList in this example is not a must. You can still write the following:

List customers = new ArrayList();

The compiler gives you a warning that ArrayList is a raw type and should be parameterized. This basically means that compiler won’t help you if you add to this collection an object of a type that might blow up in some other place in the program. While using raw types is not an error, it should be avoided.

Declaring Generics

If you’ll be creating your own class for storing objects, you can use any letter(s) in angle brackets to declare that your class will use parameterized types. You can use any letters to represent parameterized types, but traditionally developers use <E> for element, <T> for type, <K> for keys,<V> for value, and so on. The letter is replaced by a concrete type during concrete variable declaration. Open the source code of the Java class Hashtable, and you see <K,V>, which stands for key and value:

public class Hashtable<K,V> extends Dictionary<K,V>

implements Map<K,V>, Cloneable, Serializable

Again, what types are used for storing keys and values is decided when the Hashtable is being declared. You can use a parameterized type for declaring variables wherever you can use regular data types. Listing 12-3 shows a fragment from the source code of the interface java.util.List. This interface declaration uses <E> as a data type.

Listing 12-3: Fragment from java.util.List interface

package java.util;

public interface List<E> extends Collection<E> {

Iterator<E> iterator();

<T> T[] toArray(T[] a);

boolean add(E e);

boolean containsAll(Collection<?> c);

boolean addAll(Collection<? extends E> c);

boolean addAll(int index, Collection<? extends E> c);

boolean removeAll(Collection<?> c);

E set(int index, E element);

void add(int index, E element);

ListIterator<E> listIterator();

ListIterator<E> listIterator(int index);

List<E> subList(int fromIndex, int toIndex);

}

Wildcards

Listing 12-3 contains question marks that represent unknown types. It’s easier to explain them with an example. Let’s turn the for loop from Listing 12-2 into a method. In Eclipse, highlight the code of the for loop, right-click, and select Refactor → Extract Method. In the pop-up window enter the method name processCustomers and click OK.

Listing 12-4: Refactored class TestGenericCollection

import java.util.ArrayList;

import java.util.Hashtable;

import java.util.List;

public class TestGenericCollection {

public static void main(String[] args) {

List<Customer> customers = new ArrayList<Customer>();

Customer customer1 = new Customer("David","Lee");

customers.add(customer1);

Customer customer2 = new Customer("Ringo","Starr");

customers.add(customer2);

Order order = new Order();

//customers.add(order); // Compiler error

// Iterate through the list customers and do something

// with each element of this collection.

// No casting required.

processData(customers);

}

private static void processData(List<Customer> customers){

for (Customer customer: customers){

customer.doSomething();

}

}

}

What if you want to make the method processData() more generic and useful not only for a collection of Customer objects but for others, too? Without generics you’d be using instanceof and writing something similar to Listing 12-5.

Listing 12-5: Back to casting

private static void processData(ArrayList data) {

for (Object object: data){

if(object instanceof Customer){

((Customer) object).doSomething();

}

}

}

But now, armed with the new knowledge, you can try to change the signature of the method processData() to the following:

private static void processData(List<Object> data){

// do something with data

}

Unfortunately, this solution won’t work, because there is no such thing as inheritance of parameterized types. In other words, even though the class Customer is a subclass of Object, such inheritance does not apply to parameters <Customer> and <Object>. This is when the question mark that represents an unknown type becomes handy. The next step in making processData() more generic is to change the method signature like so:

private static void processData(List<?> data){

// do something with data

}

Using such a method signature is different from simply declaring the method argument data of type List, which would require casting, as in Listing 12-5. With the wildcard notation you state, “At this point the type of data is not known, but whenever some code uses the method processData()it’ll be known and properly compiled so casting won’t be needed.”

The next challenge you face is to compile the code calling the method doSomething() on the objects of unknown types.

Creating Custom Parameterized Classes

Let’s consider an example with the bike store from the “Try It” section for Lesson 10. That online store has a truck that’s used to deliver bikes and, say, spare wheels for customers. We want to allow only bikes and wheels to be loaded into the truck; people are not allowed there. Consider the class hierarchy in Figure 12-1:

An object of type Truck can contain instances of Product. The object Ferry can contain Truck instances. The classes Product, Bike, Wheel, and Person don’t implement any business logic in this small application as it’s irrelevant for understanding generic types. But the fact that Product is a superclass of Bike and Model is important. The parameterized class Truck can look as follows:

public class Truck<T> {

private List<T> products = new ArrayList<>();

// load the product on the truck

public void add (T t){

products.add(t);

}

// Return products loaded on the truck

public List<T> getProducts(){

return products;

}

}

image

Figure 12-1: Figure 12-1. The bike store class hierarchy

This declaration uses a parameter of type T that’s unknown at this point yet. You specify a concrete type when you create a program that instantiates Truck; for example:

public class TestGenericType {

public static void main(String[] args) {

Truck<Product> shipment = new Truck<>();

Bike bike = new Bike();

Wheel wheel = new Wheel();

Person person = new Person();

shipment.add(bike);

shipment.add(wheel);

// shipment.add(person); // Compiler error

}

}

The variable shipment points at the instance of the Truck that allows adding only the objects of type Product or its subclasses. Because Person is not a Product, the compiler won’t let you add it to the Truck load.

Bounded Type Parameters

Bounded type parameters enable you to specify generic types with restrictions related to class inheritance hierarchies. Let’s continue using the same example of a bike store. Although Product is a superclass of a Bike, ArrayList<Product> is not a superclass of ArrayList<Bike>. Hence, if there is a method that expects ArrayList<Product> as an argument, you can’t provide ArrayList<Bike> instead. With generics, you should use the keyword extends to specify the upper bound of allowed types in the inheritance hierarchy. In this case it would be ArrayList<? extends Product>. This means that only ArrayList containing object of Product type and its subclasses are allowed. The question mark is a wildcard here.

To illustrate this, let’s change the rules in our bike store: You can’t mix bikes and wheels in the same truck. Also, you want to be able to load trucks on a ferry. The class Ferry looks like the following:

public class Ferry {

public void loadTruck(Truck<? extends Product> truck){ }

public void unloadToDock(List<? extends Product> ferryTrucks,

List<? super Product> dockTrucks){

for (Product product: ferryTrucks){

dockTrucks.add(product);

}

}

}

The method loadTruck() declares the argument with the upper bounded wildcard—only the trucks with Product and its subclasses can be loaded to the ferry. The class TestGenericBounded creates one truck loaded with two bikes, and another one loaded with three wheels. Then it loads both trucks on the ferry.

public class TestGenericBounded {

public static void main(String[] args) {

Ferry ferry = new Ferry();

// Load a truck with two bikes

Truck<Bike> bikes = new Truck<>();

bikes.add(new Bike());

bikes.add(new Bike());

// Load a truck with three wheels

Truck<Wheel> wheels = new Truck<>();

wheels.add(new Wheel());

wheels.add(new Wheel());

wheels.add(new Wheel());

// Load two trucks on the ferry

ferry.loadTruck(bikes);

ferry.loadTruck(wheels);

}

}

If the ferry’s loadTruck() method would be declared as loadTruck(Truck<Product> truck), you wouldn’t be able to load either the truck with bikes or the one with wheels.

The class Ferry also has the method unloadToDock() that illustrates lower bounded wildcards by using the keyword super.

public void unloadToDock(List<? extends Product> ferryTrucks,

List<? super Product> dockTrucks){

for (Product product : ferryTrucks){

dockTrucks.add(product);

}

}

Note the super keyword here. You are copying the data from a collection that can contain any subclasses of Product into another one. Revisit the class hierarchy diagram and think of a standard Java upcasting. The destination collection can be of a type of any class that the Product extends from (for example, of type Object). Moreover, you may introduce yet another class located between Object and Product in the inheritance hierarchy—this won’t break the code. The keyword super means exactly this; the destination collection can hold any types as long as they are superclasses of Product.

There is a simple in-out rule that may help you to figure out if you need to use extends or super keywords. If you’re creating a parameterized class to read data from it, use extends. If you are planning to put or copy data into a parameterized class, use super.

Generic Methods

While declaring a method you can either predefine data types for its arguments and return values or use generics. For example, the method toArray() from Listing 12-3 starts with a declaration of a new parameterized type (<T> in that case), which has to be placed in angle brackets right before the return type in the method signature. The very fact that a method declares a new type makes it generic. The following declaration of the toArray() method takes an array of objects of type T and returns an array of T objects:

<T> T[] toArray(T[] a);

Figure 12-2 explains the above line in greater details.

image

Figure 12-2: Figure 12-2. A signature of a generic method

If you have an ArrayList of integers, you can declare and convert it to an array as follows:

List<Integer> myNumericList = new ArrayList<>();

...

Integer myNumericArray[] = new Integer[myNumericList.size()];

myNumericArray = myNumericList.toArray();

If you need to use the same method toArray() with a list of customers, the data type <T> magically transforms (by compiler) into the Customer type:

List<Customer> myCustomerList = new ArrayList<Customer>();

...

Customer myCustomerArray[] = new Customer[myCustomerList.size()];

myCustomerArray = myCustomerList.toArray();

As in examples from the ??? section, you are allowed to put constraints on the type. For example, you can restrict the toArray() method to work only with types that implement the Comparable interface:

<T extends Comparable> T[] toArray(T[] a);

Try It

Create a simple program that uses generics with the class RetiredEmployee (which extends the class Employee) from Listing 7-2. Write a generic method that accepts a collection of RetiredEmployee objects and copies it into a collection of Employee objects. Use the methodunloadToDock() from class Ferry as an example.

Lesson Requirements

You should have Java installed.

NOTE You can download the code and resources for this “Try It” from the book’s web page at www.wrox.com/go/javaprog24hr2e. You can find them in Lesson12.zip.

Step-by-Step

1. Create a new Eclipse project called Lesson12.

2. Create classes Employee and then RetiredEmployee that extends Employee.

3. Create an executable Java class, TestGenericMethod, that accepts a List of RetiredEmployee objects and copies it into a List of Employee objects. This method should print on the system console the name of each Employee from the resulting collection.

4. Run the TestGenericMethod program and observe the printed names.

TIP Please select the videos for Lesson 12 online at www.wrox.com/go/javaprog24hr2e. You will also be able to download the code and resources for this lesson from the website.