Home CPSC 340

Generics

 

Overview

Today we will look at how to write classes that are parameterized across a type, so that we can use them with lots of different types of data. This is done for some of Java's built in types like ArrayList, HashTable, etc.

For example, we can declare an ArrayList storing different types of data:


ArrayList<String> names;
ArrayList<Double> numbers;

Here there is just one class, called ArrayList, which can store objects. However, we can specify what sort of objects we want each individual list to store by putting it inside the angled brackets.

This affects the methods we will later call on these lists too. For example, the add method takes a different type depending on how we originally made the list:


names.add("Barry");
numbers.add(36.625);

Likewise, we can make different sorts of HashTable objects by filling in two type parameters:


HashTable<String, Integer> heights;

If you haven't used HashTable before, it basically makes a mapping from one thing to another (like a dictionary in Python). So we could use this to map from people's names to their phone numbers:


HashTable<String, Integer> phonebook;

phonebook.put("Alfie", 1234567);
phonebook.put("Bernard", 5550505);

In HashTable, there are two type parameters: one for what is the thing we are mapping from, and another for what we are mapping to.

There's no limit to how many type parameters a class could take, though in practice more than a few is very unusual to see (just like with array dimensions).


 

Generic Classes

To make our own generic classes, we simply add a type parameter to the class line, inside of angled brackets. For example, to declare a generic class called "Thing" with a type parameter called "Type", we would use the following line:


class Thing<Type> {

Then, inside the class body, we can use "Type" as a type. For example, we can declare instance variables with that type, or parameters or return types. It's basically a stand-in for whatever type will be supplied later.

The word "Type" in this line is just a variable name, it doesn't have to be called that. Some people use "T" instead.

The following class contains an instance variable of the parameterized type, as well as using it in parameter lists and returns. This class just contains one object of another type, and allows the user to access it:


class Thing<Type> {
    // declare an object of the parameterized type
    private Type object;

    // can be used in parameter lists and returns too
    public Thing(Type object) {
        this.object = object;
    }

    public Type get() {
        return object;
    } 

    public void set(Type object) {
        this.object = object;
    }
}

 

Using Generic Classes

Now that we have a generic class, we can use it to make objects. To do this, we need to supply the type parameter (just like you do for ArrayLists). For instance, we can make two "Thing" objects", one to store Integers, and one to store Strings:


Thing<Integer> number = new Thing<Integer>(7);
Thing<String> message = new Thing<String>("Hello!");

Notice that you have to supply the type parameter both when you declare the object, and again when you instantiate it.

Just like ArrayLists, we have to put a class-type in for the parameterized type. We can't use a primitive type like int, char, or double. To get around this, Java has "wrapper classes" such as Integer, Character and Double.

After we create these objects, we can then use them accordingly. The type is then filled in inside the class. For example, the "get" method for the "number" object returns an Integer, while the "get" method for the "message" object returns a String.

The rest of this example program uses these objects:


for (int i = 0; i < number.get(); i++) {
    System.out.println(message.get());
}

 

Multiple Type Parameters

To have multiple type parameters (like HashTable), we just list them all on the class declaration line. For example, we can make a generic class called "Pair" that stores two pieces of data, of any type.

That could be done with the following generic class:


class Pair<Type1, Type2> {
    private Type1 first;
    private Type2 second;

    public Pair(Type1 first, Type2 second) {
        this.first = first;
        this.second = second;
    }

    public Type1 getFirst() {
        return first;
    }

    public Type2 getSecond() {
        return second;
    }
}

Our two type parameters are called "Type1" and "Type2". Now when we make a Pair object, we must fill in both types. A type like this could be useful if we want to be able to return two different values from a method.

The following code, for example, returns a String and an Integer from a method by putting them in a Pair:


public class Multiple {
    public static Pair<String, Integer> getInfo() {
        Scanner in = new Scanner(System.in);

        System.out.println("What is your name? ");
        String name = in.next();

        System.out.println("What is your age? ");
        int age = in.nextInt();

        return new Pair<String, Integer>(name, age); 
    }

    public static void main(String args[]) {
        Pair<String, Integer> info = getInfo();
        System.out.println("Hello " + info.getFirst() + ". You are " + info.getSecond() + " years old.");
    }
}

 

Use of Generics

We will write our data structures as generic classes. This will allow us to use them to store any type of data in the structures we create.

Next class we will start by creating our own version of Java's ArrayList class.

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