Java 101: The Next Generation

Java 101: The essential Java language features tour, Part 1

Java programming with assertions and generics

Java 101: The Next Generation

Show More
1 2 3 Page 2
Page 2 of 3

The Java language supports the following kinds of actual type arguments:

  • Concrete type: A class or other reference type name is passed to the type parameter. For example, in List<Animal>, Animal is passed to E.
  • Concrete parameterized type: A parameterized type name is passed to the type parameter. For example, in Set<List<Shape>>, List<Shape> is passed to E.
  • Array type: An array is passed to the type parameter. For example, in Map<String, String[]>, String is passed to K and String[] is passed to V.
  • Type parameter: A type parameter is passed to the type parameter. For example, in class Container<E> { Set<E> elements; }, E is passed to E.
  • Wildcard: The question mark (?) is passed to the type parameter. For example, in Class<?>, ? is passed to T.

Each generic type implies the existence of a raw type, which is a generic type without a formal type parameter list. For example, Class is the raw type for Class<T>. Unlike generic types, raw types can be used with any kind of object.

Declaring and using generic types

Declaring a generic type involves specifying a formal type parameter list and using these type parameters throughout its implementation. Using the generic type involves passing actual type arguments to its type parameters when instantiating the generic type. See Listing 5.

Listing 5. GenDemo.java (version 1)

class Container<E>
{
   private E[] elements;
   private int index;

   Container(int size)
   {
      elements = (E[]) new Object[size];
      index = 0;
   }

   void add(E element)
   {
      elements[index++] = element;
   }

   E get(int index)
   {
      return elements[index];
   }

   int size()
   {
      return index;
   }
}

public class GenDemo
{
   public static void main(String[] args)
   {
      Container<String> con = new Container<String>(5);
      con.add(“North”);
      con.add(“South”);
      con.add(“East”);
      con.add(“West”);
      for (int i = 0; i < con.size(); i++)
         System.out.println(con.get(i));
   }
}

Listing 5 demonstrates generic type declaration and usage in the context of a simple container type that stores objects of the appropriate argument type. To keep the code simple, I’ve omitted error checking.

The Container class declares itself to be a generic type by specifying the <E> formal type parameter list. Type parameter E is used to identify the type of stored elements, the element to be added to the internal array, and the return type when retrieving an element.

The Container(int size) constructor creates the array via elements = (E[]) new Object[size];. If you’re wondering why I didn’t specify elements = new E[size];, the reason is that it isn’t possible: doing so could lead to a ClassCastException.

Compile Listing 5 (javac GenDemo.java). The (E[]) cast causes the compiler to output a warning about the cast being unchecked. It flags the possibility that downcasting from Object[] to E[] might violate type safety because Object[] can store any type of object.

Note, however, that there’s no way to violate type safety in this example. It’s simply not possible to store a non-E object in the internal array. I’ll show you how to suppress this warning message in a future article.

Execute java GenDemo to run this application. You should observe the following output:

North
South
East
West

Bounding type parameters

The E in Set<E> is an example of an unbounded type parameter because you can pass any actual type argument to E. For example, you can specify Set<Marble>, Set<Employee>, or Set<String>.

Sometimes, you’ll want to restrict the types of actual type arguments that can be passed to a type parameter. For example, perhaps you want to restrict a type parameter to accept only Employee and its subclasses.

You can limit a type parameter by specifying an upper bound, which is a type that serves as the upper limit on the types that can be passed as actual type arguments. Specify the upper bound via reserved word extends followed by the upper bound’s type name.

For example, class Employees<E extends Employee> restricts the types that can be passed to Employees to Employee or a subclass (e.g., Accountant). Specifying new Employees<Accountant> would be legal, whereas new Employees<String> would be illegal.

You can assign more than one upper bound to a type parameter. However, the first bound must always be a class, and the additional bounds must always be interfaces. Each bound is separated from its predecessor by an ampersand (&). Check out Listing 6.

Listing 6. GenDemo.java (version 2)

import java.math.BigDecimal;

import java.util.Arrays;

abstract class Employee
{
   private BigDecimal hourlySalary;
   private String name;

   Employee(String name, BigDecimal hourlySalary)
   {
      this.name = name;
      this.hourlySalary = hourlySalary;
   }

   public BigDecimal getHourlySalary()
   {
      return hourlySalary;
   }

   public String getName()
   {
      return name;
   }

   public String toString()
   {
      return name+”: “+hourlySalary.toString();
   }
}

class Accountant extends Employee implements Comparable<Accountant>
{
   Accountant(String name, BigDecimal hourlySalary)
   {
      super(name, hourlySalary);
   }

   public int compareTo(Accountant acct)
   {
      return getHourlySalary().compareTo(acct.getHourlySalary());
   }
}

class SortedEmployees<E extends Employee & Comparable<E>>
{
   private E[] employees;
   private int index;

   SortedEmployees(int size)
   {
      employees = (E[]) new Employee[size];
      int index = 0;
   }

   void add(E emp)
   {
      employees[index++] = emp;
      Arrays.sort(employees, 0, index);
   }

   E get(int index)
   {
      return employees[index];
   }

   int size()
   {
      return index;
   }
}

public class GenDemo
{
   public static void main(String[] args)
   {
      SortedEmployees<Accountant> se = new
SortedEmployees<Accountant>(10);
      se.add(new Accountant(“John Doe”, new BigDecimal(“35.40”)));
      se.add(new Accountant(“George Smith”, new BigDecimal(“15.20”)));
      se.add(new Accountant(“Jane Jones”, new BigDecimal(“25.60”)));

      for (int i = 0; i < se.size(); i++)
         System.out.println(se.get(i));
   }
}

Listing 6’s Employee class abstracts the concept of an employee that receives an hourly wage. This class is subclassed by Accountant, which also implements Comparable<Accountant> to indicate that Accountants can be compared according to their natural order, which happens to be hourly wage in this example.

The java.lang.Comparable interface is declared as a generic type with a single type parameter named T. This class provides an int compareTo(T o) method that compares the current object with the argument (of type T), returning a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

The SortedEmployees class lets you store Employee subclass instances that implement Comparable in an internal array. This array is sorted (via the java.util.Arrays class’s void sort(Object[] a, int fromIndex, int toIndex) class method) in ascending order of the hourly wage after an Employee subclass instance is added.

Compile Listing 6 (javac GenDemo.java) and run the application (java GenDemo). You should observe the following output:

George Smith: 15.20
Jane Jones: 25.60
John Doe: 35.40

What about lower bounds?

You cannot specify a lower bound for a generic type parameter. To understand why I recommend reading Angelika Langer’s Java Generics FAQs on the topic of lower bounds, which she says “would be confusing and not particularly helpful.”

Considering wildcards

Let’s say you want to print out a list of objects, regardless of whether these objects are strings, employees, shapes, or some other type. Your first attempt might look like what’s shown in Listing 7.

Listing 7. GenDemo.java (version 3)

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenDemo
{
   public static void main(String[] args)
   {
      List<String> directions = new ArrayList<String>();
      directions.add(“north”);
      directions.add(“south”);
      directions.add(“east”);
      directions.add(“west”);
      printList(directions);

      List<Integer> grades = new ArrayList<Integer>();
      grades.add(new Integer(98));
      grades.add(new Integer(63));
      grades.add(new Integer(87));
      printList(grades);
   }

   static void printList(List<Object> list)
   {
      Iterator<Object> iter = list.iterator();
      while (iter.hasNext())
         System.out.println(iter.next());
   }
}

It seems logical that a list of strings or a list of integers is a subtype of a list of objects, yet the compiler complains when you attempt to compile this listing. Specifically, it tells you that a list-of-string cannot be converted to a list-of-object, and similarly for a list-of-integer.

The error message you’ve received is related to the fundamental rule of generics:

For a given subtype x of type y, and given G as a raw type declaration, G<x> is not a subtype of G<y>.

According to this rule, although String and java.lang.Integer are subtypes of java.lang.Object, it’s not true that List<String> and List<Integer> are subtypes of List<Object>.

Why do we have this rule? Remember that generics are designed to catch type-safety violations at compile time, which is helpful: without generics, you are much more likely to be called in to work at 2 a.m. because your Java program has thrown a ClassCastException and crashed!

As a demonstration, let’s assume that List<String> was a subtype of List<Object>. If this was true, you might end up with the following code:

List<String> directions = new ArrayList<String>();
List<Object> objects = directions;
objects.add(new Integer());
String s = objects.get(0);

This code fragment creates a list of strings based on an array list. It then upcasts this list to a list of objects (which isn’t legal, but for now just pretend it is). Next, it adds an integer to the list of objects, which violates type safety. The problem occurs in the final line, which throws ClassCastException because the stored integer cannot be cast to a string.

Without generics, your only option to prevent such a violation of type safety would be to pass an object of type List<Object> to the printList() method in Listing 7, which wouldn’t be very useful. With generics, however, you can use wildcards to solve the problem, as shown in Listing 8.

Listing 8. GenDemo.java (version 4)

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenDemo
{
   public static void main(String[] args)
   {
      List<String> directions = new ArrayList<String>();
      directions.add(“north”);
      directions.add(“south”);
      directions.add(“east”);
      directions.add(“west”);
      printList(directions);

      List<Integer> grades = new ArrayList<Integer>();
      grades.add(new Integer(98));
      grades.add(new Integer(63));
      grades.add(new Integer(87));
      printList(grades);
   }

   static void printList(List<?> list)
   {
      Iterator<?> iter = list.iterator();
      while (iter.hasNext())
         System.out.println(iter.next());
   }
}

In Listing 8 I use a wildcard (the ? symbol) in place of Object in printList()’s parameter list and body. Because this symbol stands for any type, it’s legal to pass List<String> and List<Integer> to this method.

Compile Listing 8 (javac GenDemo.java) and run the application (java GenDemo). You should observe the following output:

north
south
east
west
98
63
87

Discovering generic methods

Now say you want to copy a list of objects to another list subject to a filter. You might consider declaring a void copy(List<Object> src, List<Object> dst, Filter filter) method, but this method would only be able to copy Lists of Objects and nothing else.

To pass source and destination lists of arbitrary type, you need to use the wildcard for a type placeholder. For example, consider the following copy() method:

void copy(List<?> src, List<?> dest, Filter filter)
{
   for (int i = 0; i < src.size(); i++)
      if (filter.accept(src.get(i)))
         dest.add(src.get(i));
}

This method’s parameter list is correct, but there’s a problem. According to the compiler, dest.add(src.get(i)); violates type safety. The ? implies that any kind of object can be the list’s element type, and it’s possible that the source and destination element types are incompatible.

For example, if the source list was a List of Shape and the destination list was a List of String, and copy() was allowed to proceed, ClassCastException would be thrown when attempting to retrieve the destination list’s elements.

You could partially solve this problem by providing upper and lower bounds for the wildcards, as follows:

void copy(List<? extends String> src, List<? super String> dest,
Filter filter)
{
   for (int i = 0; i < src.size(); i++)
      if (filter.accept(src.get(i)))
         dest.add(src.get(i));
}
1 2 3 Page 2
Page 2 of 3