Generics

Generics provide flexible type safety to your code. They move many common errors from run time to compile-time and provide cleaner, easier-to-write code. Generics also reduce the need for casting with collections and are used heavily in the Java Collections API.

Simple Cache Class Without Generic

The two examples below show very simple caching classes. Even though each class is very simple, a separate class is required for any object type.

public class CacheString { 
  private String message;
    public void add(String message){
      this.message = message;
    }
    public String get(){
      return this.message;
    } 
}
public class CacheShirt {
  private Shirt shirt;

  public void add(Shirt shirt){
    this.shirt = shirt;
  }

  public Shirt get(){
    return this.shirt;
  }
}

Generic Cache Class

To create a generic version of the CacheAny class, a variable named T is added to the class definition surrounded by angle brackets. In this case, T stands for “type” and can represent any type. As the example below shows, the code has changed to use t instead of a specific type of information. This change allows the CacheAny class to store any type of object.

1 public class CacheAny {
2
3   private T t;
4
5   public void add(T t){
6     this.t = t;
7 }
8
9   public T get(){
10    return this.t;
11  } 
12 }

T was chosen not by accident but by convention. Specific letters are commonly used with generics.

Note: You can use any identifier you want. The following values are merely strongly suggested.

Here are the conventions:

  • T: Type
  • E: Element
  • K: Key
  • V: Value
  • S, U: Used if there are second types, third types, or more

Generics in Action

Compare the type-restricted objects to their generic alternatives.

1 public static void main(String args[]){
2    CacheString myMessage = new CacheString(); // Type
3    CacheShirt myShirt = new CacheShirt(); // Type
4
5    //Generics
6    CacheAny[String] myGenericMessage = new CacheAny[String](); 
7    CacheAny[Shirt] myGenericShirt = new CacheAny[Shirt](); 
 
8
9    myMessage.add("Save this for me"); // Type
10    myGenericMessage.add("Save this for me"); // Generic 
11
12 }

Note how the one generic version of the class can replace any number of type-specific caching classes. The add() and get() functions work exactly the same way. In fact, if the myMessage declaration is changed to generic, no changes need to be made to the remaining code.

The example code can be found in the Generics project in the TestCacheAny.java file.

Generics with Type Inference Diamond

The type inference diamond is a new feature in JDK 7. In the generic code, notice how the right-side type definition is always equivalent to the left-side type definition. In JDK 7, you can use the diamond to indicate that the right type definition is equivalent to the left. This helps to avoid typing redundant information over and over again.

Example: TestCacheAnyDiamond.java

Syntax: There is no need to repeat types on the right side of the statement. Angle brackets indicate that type parameters are mirrored.

It Simplifies generic declarations and saves typing.

//Generics
CacheAny[String] myMessage = new CacheAny[](); 
}

Note: In a way, it works in an opposite way from a “normal” Java type assignment. For
example, Employee emp = new Manager(); makes emp object an instance of Manager.

But in the case of generics:

ArrayList[Manager] managementTeam = new ArrayList[]();

The left side of the expression (rather than the right side) determines the type.

Collections

A collection is a single object that manages a group of objects. Objects in the collection are called elements. Various collection types implement standard data structures including stacks, queues, dynamic arrays, and hashes. All the collection objects have been optimized for use in Java applications. Primitives are not allowed in a collection. The Collections API relies heavily on generics for its implementation.

Note: The Collections classes are all stored in the java.util package. The import statements are not shown in the following examples, but the import statements are required for each collection type:

  • import java.util.List;
  • import java.util.ArrayList;
  • import java.util.Map;

Collection Types

Collection Types Java

The diagram above shows the Collection framework. The framework is made up of a set of interfaces for working with a group (collection) of objects.

Characteristics of the Collection Framework

List, Set, and Map are interfaces in Java and many concrete implementations of them are available in the Collections API.

Collection Interfaces and Implementation

Interface Implementation Implementation Implementation
List ArrayList LinkedList
Set TreeSet HashSet LinkedHashSet
Map HashMap HashTable TreeMap
Deque ArrayDeque

The table above shows the commonly used interfaces and their popular implementation.

List Interface

The list defines generic list behavior. It is an ordered collection of elements.

List behaviors include:

  • Adding elements at a specific index
  • Getting an element based on an index
  • Removing an element based on an index
  • Overwriting an element based on an index
  • Getting the size of the list

The list allows duplicate elements.

ArrayList

ArrayList Is an implementation of the List interface. The list automatically grows if elements exceed initial size. ArrayList Has a numeric index.

  • Elements are accessed by index.
  • Elements can be inserted based on index.
  • Elements can be overwritten.

ArrayList allows duplicate items.

List<Integer> partList = new ArrayList<>((3);
   partList.add(new Integer(1111));
   partList.add(new Integer(2222));
   partList.add(new Integer(3333));
   partList.add(new Integer(4444)); // ArrayList auto grows
   System.out.println("First Part: " + partList.get(0)); // 
First item
   partList.add(0, new Integer(5555)); // Insert an item by 
index

Autoboxing and Unboxing

Autoboxing and Unboxing simplifies the syntax. It produces cleaner, easier-to-read code.

1 public class AutoBox {
2     public static void main(String[] args){
3        Integer intObject = new Integer(1);
4        int intPrimitive = 2;
5
6        Integer tempInteger;
7        int tempPrimitive;
8
9        tempInteger = new Integer(intPrimitive);
10       tempPrimitive = intObject.intValue();
11
12       tempInteger = intPrimitive; // Auto box
13       tempPrimitive = intObject; // Auto unbox

Lines 9 and 10 show a traditional method for moving between objects and primitives. Lines 12 and 13 show boxing and unboxing.

Autoboxing and Unboxing

Autoboxing and unboxing are Java language features that enable you to make sensible assignments without formal casting syntax. Java provides the casts for you at compile time.

Note: Be careful when using autoboxing in a loop. There is a performance cost to using this feature.

ArrayList Without Generics

In the example below, a part number list is created by using an ArrayList. There is no type definition when using syntax prior to Java version 1.5. So any type can be added to the list as shown on line 8. It is up to the programmer to know what objects are in the list and in what order. If the list was only for Integer objects, a runtime error would occur on line 12.

1 public class OldStyleArrayList {
2   public static void main(String args[]){ 
3     List partList = new ArrayList(3);
4
5     partList.add(new Integer(1111));
6     partList.add(new Integer(2222));
7     partList.add(new Integer(3333));
8     partList.add("Oops a string!");
9
10    Iterator elements = partList.iterator(); 
11    while (elements.hasNext()) {
12      Integer partNumberObject = (Integer)(elements.next()); // error?
13      int partNumber = partNumberObject.intValue();
14
15      System.out.println("Part number: " + partNumber);
16     } 
17   } 
18 }

On lines 10–16, with a nongeneric collection, an Iterator is used to iterate through the list of items. Notice that a lot of casting is required to get the objects back out of the list so you can print the data.

In the end, there is a lot of needless “syntactic sugar” (extra code) working with collections in this way. If the line that adds the String to the ArrayList is commented out, the program produces the following output:

Part number: 1111
Part number: 2222
Part number: 3333

Generic ArrayList

With generics, things are much simpler. When the ArrayList is initialized on line 3, any attempt to add an invalid value (line 8) results in a compile-time error.

1 public class GenericArrayList { 
2   public static void main(String args<>) {
3   List<Integer> partList = new ArrayList(3);
4
5   partList.add(new Integer(1111));
6   partList.add(new Integer(2222));
7   partList.add(new Integer(3333));
8   partList.add("Bad Data"); // compiler error now 
9
10  Iterator<Integer> elements = partList.iterator();
11   while (elements.hasNext()) {
12     Integer partNumberObject = elements.next();
13     int partNumber = partNumberObject.intValue();
14
15   System.out.println("Part number: " + partNumber);
16   }
17 }
18}

Note: On line 3, the ArrayList is assigned to a List type. Using this style enables you to swap out the List implementation without changing other code

Generic ArrayList: Iteration and Boxing

Using the for-each loop is much easier and provides much cleaner code. No casts are done because of the autounboxing feature of Java.

for (Integer partNumberObj:partList){
    int partNumber = partNumberObj; // Demos auto unboxing
    System.out.println("Part number: " + partNumber); 
}

Set Interface

The main difference between List and Set in Java is that List is an ordered collection, which allows duplicates, whereas Set is an unordered collection, which does not allow duplicates.

  • A Set is an interface that contains only unique elements.
  • A Set has no index.
  • Duplicate elements are not allowed.
  • You can iterate through elements to access them.
  • TreeSet provides sorted implementation.

TreeSet: Implementation of Set

The example in below uses a TreeSet, which sorts the items in the set.

1 public class SetExample {
2   public static void main(String[] args){
3     Set set = new TreeSet<>();
4
5     set.add("one");
6     set.add("two");
7     set.add("three");
8     set.add("three"); // not added, only unique
9
10      for (String item:set){
11        System.out.println("Item: " + item);
12      } 
13    }
14 }

If the program is run, the output is as follows:

Item: one
Item: three
Item: two

Map Interface

A Map is good for tracking things such as part lists and their descriptions as shown in the table below.

  • A collection that stores multiple key-value pairs
    – Key: Unique identifier for each element in a collection
    – Value: A value stored in the element associated with the key
  • Called “associative arrays” in other languages
Key Value
101 Blue Shirt
102 Black Shirt
103 Gray Shirt

Map Types

The Map interface does not extend the Collection interface because it represents mappings and not a collection of objects. Some of the key implementation classes include:

  • TreeMap: A map where the keys are automatically sorted
  • Hashtable: A classic associative array implementation with keys and values. Hashtable is synchronized
  • HashMap: An implementation just like Hashtable except that it accepts null keys and values. Also, it is not synchronized.

TreeMap: Implementation of Map

The example in the slide shows how to create a Map and perform standard operations on it.

1 public class MapExample {
2   public static void main(String[] args){ 
3     Map  partList = new TreeMap();
4     partList.put(“S001", "Blue Polo Shirt");
5     partList.put(“S002", "Black Polo Shirt");
6     partList.put(“H001", "Duke Hat");
7
8     partList.put(“S002", "Black T-Shirt"); // Overwrite value
9     Set keys = partList.keySet();
10
11    System.out.println("=== Part List ===");
12    for (String key:keys){
13       System.out.println("Part#: " + key + " " + 
14                  partList.get(key));
15      }
16    }
17 }

The output from the program is:

=== Part List ===
Part#: H002 Duke Hat
Part#: S001 Blue Polo Shirt
Part#: S002 Black T-Shirt

Deque Interface

Deque is a child interface of Collection (just like Set and List). It is a collection that can be used as a stack or a queue.

A queue is often used to track asynchronous message requests so they can be processed in order. A stack can be very useful for traversing a directory tree or similar structures.

A Deque is a “doubled-ended queue.” Essentially this means that a Deque can be used as a queue (first in, first out [FIFO] operations) or as a stack (last in, first out [LIFO] operations).

Stack with Deque: Example

1 public class TestStack {
2    public static void main(String[] args){
3      Deque stack = new ArrayDeque();
4      stack.push("one");
5      stack.push("two");
6      stack.push("three");
7
8      int size = stack.size() - 1;
9      while (size >= 0 ) {
10       System.out.println(stack.pop());
11       size--;
12     } 
13   }
14 }

Ordering Collections

The Collections API provides two interfaces for ordering elements: Comparable and Comparator. Both are implemented by using generics.

  • Comparable: Is implemented in a class and provides a single sorting option for the class.
  • Comparator: Enables you to create multiple sorting options. You plug in the designed option whenever you want

Using the Comparable interface:
– Overrides the compareTo method
– Provides only one sort option

The Comparator interface:
– Is implemented by using the compare method
– Enables you to create multiple Comparator classes
– Enables you to create and use numerous sorting option

Both interfaces can be used with sorted collections, such as TreeSet and TreeMap.

Comparable: Example

The example below implements the Comparable interface and its compareTo method. Notice that because the interface is designed by using generics, the angle brackets define the class type that is passed into the compareTo method. The if statements are included to demonstrate the comparisons that take place. You can also merely return a result.

1 public class ComparableStudent implements Comparable [ComparableStudent]{ 
2   private String name; private long id = 0; private double gpa = 0.0;
3
4    public ComparableStudent(String name, long id, double gpa){
5      // Additional code here
6    }
7    public String getName(){ return this.name; }
8     // Additional code here
9
10   public int compareTo(ComparableStudent s){
11     int result = this.name.compareTo(s.getName());
12     if (result > 0) { return 1; } 
13     else if (result < 0){ return -1; } 
14     else { return 0; }
15   }
16 }

The returned numbers have the following meaning.

  • Negative number: s comes before the current element.
  • Positive number: s comes after the current element.
  • Zero: s is equal to the current element.

In cases where the collection contains equivalent values, replace the code that returns zero with additional code that returns a negative or positive number.

Comparable Test: Example

In the example below, an ArrayList of ComparableStudent elements is created. After the list is initialized, it is sorted by using the Comparable interface.

public class TestComparable {
  public static void main(String[] args){
    Set [ComparableStudent]studentList = new TreeSet();
   
    studentList.add(new ComparableStudent("Thomas Jefferson", 1111, 3.8));
    studentList.add(new ComparableStudent("John Adams", 2222, 3.9));
    studentList.add(new ComparableStudent("George Washington", 3333, 3.4));

    for(ComparableStudent student:studentList){
      System.out.println(student);
    }
  }
}

The output of the program is as follows:

Name: George Washington ID: 3333 GPA:3.4
Name: John Adams ID: 2222 GPA:3.9
Name: Thomas Jefferson ID: 1111 GPA:3.8

Note: The ComparableStudent class has overridden the toString() method.

Comparator Interface

  • Is implemented by using the compare method
  • Enables you to create multiple Comparator classes
  • Enables you to create and use numerous sorting options

The example in the next section shows how to use Comparator with an unsorted interface such as ArrayList by using the Collections utility class.

Comparator: Example

The example below shows the Comparator classes that are created to sort based on Name and GPA. For the name comparison, the if statements have been simplified.

public class StudentSortName implements Comparator[Student]{
   public int compare(Student s1, Student s2){
     int result = s1.getName().compareTo(s2.getName());
     if (result != 0) { return result; }
     else { 
         return 0; // Or do more comparing
     } 
  }
}
public class StudentSortGpa implements Comparator{
    public int compare(Student s1, Student s2){
      if (s1.getGpa() [ s2.getGpa()) { return 1; }
      else if (s1.getGpa() ] s2.getGpa()) { return -1; }
      else { return 0; } 
    }
}

Comparator Test: Example

The example below shows how the two Comparator objects are used with a collection. Note: Some code has been commented out to save space.

  
1 public class TestComparator {
2    public static void main(String[] args){
3       List[ studentList] = new ArrayList(3);
4       Comparator [student]sortName = new StudentSortName();
5       Comparator[student] sortGpa = new StudentSortGpa();
6
7      // Initialize list here 
8
9       Collections.sort(studentList, sortName); 
10      for(Student student:studentList){
11         System.out.println(student);
12      }
13
14    Collections.sort(studentList, sortGpa); 
15    for(Student student:studentList){
16       System.out.println(student);
17     }
18   }
19}

Notice how the Comparator objects are initialized on lines 4 and 5. After the sortName and sortGpa variables are created, they can be passed to the sort() method by name. Running the program produces the following output:

Name: George Washington ID: 3333 GPA:3.4
Name: John Adams ID: 2222 GPA:3.9
Name: Thomas Jefferson ID: 1111 GPA:3.8
Name: John Adams ID: 2222 GPA:3.9
Name: Thomas Jefferson ID: 1111 GPA:3.8
Name: George Washington ID: 3333 GPA:3.4

Notes:

  • The Collections utility class provides a number of useful methods for various collections. Methods include min(), max(), copy(), and sort().
  • The Student class has overridden the toString() method.