Home CPSC 330

Generics

Overview

In Java, generics solve the same problem as templates do in C++. They allow for writing code that works with a variety of data types.

When creating an ArrayList, we use generics:


ArrayList<String> names = new ArrayList<String>();

Today we will see how to write generic classes and methods.

Generics look similar to C++ templates, but actually work quite differently.


Generic Classes

When defining a class, it can be made generic by inserting a type list after the name:


class ClassName <T> {

}

Here the class is generic and takes one type parameter T. If there are multiple type parameters, they can be separated by commas.

The class below can store a single value of any type and return it:


class Any<T> {
  public Any(T v) {
    value = v;
  }

  public T get() {
    return value;
  }

  private T value;
}

Then we can create objects of this type as follows:


    Any<String> s = new Any<String>("Hello");
    Any<Integer> i = new Any<Integer>(100);

We cannot actually use any type when filling in a generic. It must be an Object type.

Java has types such as Integer that provide object wrappers for the built in types.


Example: Stack

As a larger example, if we were to implement a Stack class, we would want it to be able to store any data type.

A class for storing a single node of the Stack might look like this:


class StackNode<T> {
  public StackNode(T data, StackNode<T> underneath) {
    this.data = data;
    this.underneath = underneath;
  }

  public T getData() {
    return data;
  }

  public StackNode<T> getUnderneath() {
    return underneath;
  }

  private StackNode<T> underneath;
  private T data;
}

Note that every place we refer to the StackNode must also have the type parameter.

Below is a Stack class that uses the StackNode class. It too is generic:


class Stack<T> {
  public Stack() {
    top = null;
  }

  public void push(T new_data) {
    StackNode<T> new_node = new StackNode<T>(new_data, top);
    top = new_node;
  }

  public T pop() {
    T top_data = top.getData();
    top = top.getUnderneath();
    return top_data;
  }

  public boolean empty() {
    return top == null;
  }

  private StackNode<T> top;
}

The complete program is listed in this file.


Generics vs. Polymorphism

With polymorphism, we can also treat a group of objects in the same way. We could write the Stack class this way instead:


class StackNode {
  public StackNode(Object data, StackNode underneath) {
    this.data = data;
    this.underneath = underneath;
  }

  public Object getData() {
    return data;
  }

  public StackNode getUnderneath() {
    return underneath;
  }

  private StackNode underneath;
  private Object data;
}

class Stack {
  public Stack() {
    top = null;
  }

  public void push(Object new_data) {
    StackNode new_node = new StackNode(new_data, top);
    top = new_node;
  }

  public Object pop() {
    Object top_data = top.getData();
    top = top.getUnderneath();
    return top_data;
  }

  public boolean empty() {
    return top == null;
  }

  private StackNode top;
}

There are a few differences between these classes:

Generics also allow for specifying a base class for the type parameter. If we have an inheritance hierarchy starting at a class called Base, then we could create a class:


class Example<T extends Base> {

}

Now we can create objects of the Example class and fill in anything derived from Base as the type parameter.


Generics & Inheritance

We can also inherit from a generic class. This is done in the Java Vector class. The Vector class derives from AbstractList and AbstractCollection with the same type parameter.

We could also create a class that inherits from a generic class, but is not itself generic. This example illustrates this as we make a Calculator class inherit from Stack<Double>:


// inherit the calculator class from a generic class Stack<Double>
class Calculator extends Stack<Double> {
  public Calculator(String e) {
    expression = e;
  }

  public double evaluate() {
    int j;
    double a, b;

    for(int i = 0; i < expression.length(); i++) {
      // if it's a digit
      if(Character.isDigit(expression.charAt(i))) {
        double number;
        String temp = ""; 
        for(j = 0; j < 100; j++, i++) {
          if(!Character.isDigit(expression.charAt(i)) && (expression.charAt(i) != '.')) {
            break;
          }   
          temp = temp + String.valueOf(expression.charAt(i));
        }

        number = Double.parseDouble(temp);
        push(number);
      } else if(expression.charAt(i) == '+') {
        b = pop();
        a = pop();
        push(a + b); 
      } else if(expression.charAt(i) == '-') {
        b = pop();
        a = pop();
        push(a - b); 
      } else if(expression.charAt(i) == '*') {
        b = pop();
        a = pop();
        push(a * b); 
      } else if(expression.charAt(i) == '/') {
        b = pop();
        a = pop();
        push(a / b); 
      }   
      else if(expression.charAt(i) == '^') {
        b = pop();
        a = pop();
        push(Math.pow(a, b));
      } 
    }

    return pop();
  }

  private String expression;
}

Java Collections

The Java collections framework is a set of classes and interfaces for dealing with data structures. It is similar to the STL in C++.

It contains the following data structure classes (in addition to many more):

Having common interfaces allows for us to write code using the interface without tying ourselves to a particular implementation. For example, it's very common for Java code to use the generic List interface. This can then refer to an ArrayList or a LinkedList depending on which is better for the task at hand.

Copyright © 2018 Ian Finlayson | Licensed under a Creative Commons Attribution 4.0 International License.