Home CPSC 340

Generics


 

Overview

Now we will look at how to make our class parameterized across a type, so that we can use them with lots of different types of data. Java's built-in ArrayList class does this, as does many other classes that come with the standard library.

Right now, our DynamicList class is fixed to only use Strings, but w'ed like to make it so we can say what type of data is to be stored like this:


DynamicList<String> names;
DynamicList<Double> numbers;

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


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

 

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

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:


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);

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).

To have multiple type parameters like this, 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.");
    }
}

 

Generic Dynamic Lists

Now we can go about making our DynamicList generic. We can start by adding <Type> to the class declaration, and using Type to declare the array:


public class DynamicList<Type> {
    private Type [] array;
    private int size;
    
    // ...

Then it's just a matter of changing "String" to "Type" in the code. For instance, the add method will now take a "Type" item instead:


    public void add(Type item) {
        if (size == array.length) {
            resize();
        }

        array[size] = item;
        size++;
    }

And so on for the rest of the methods.


 

The Generic Array Workaround

Unfortunately, if we compile this code as is, we get the following error:

DynamicList.java:9: error: generic array creation
        Type [] temp = new Type[size * 2];
                       ^

Java's generic system was added after the language was already designed and has some limitations. One of them is that it doesn't allow the creation of an array of a generic type.

In order to get around this, we can make an array of Objects, and then cast it back to an array of our generic type:


array = (Type[]) new Object[10];

However, Java deems this a potentially unsafe operation, giving us this lovely message:

Note: DynamicList.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

This is because casting from generic objects to a more specific type in general is tricky because you can't guarantee what sort of Objects they are. Here however, we just made them, so we know they are all null and it doesn't cause any problem to cast them.

We could just leave this warning message, but it's always best in coding to strive for zero warning messages when you compile. That's because if you do something else potentially dangerous, you need to think about whether it's a real problem or not. If we leave this message in, we won't notice new problems.

So we can the following line to tell Java to ignore this problem:


@SuppressWarnings("unchecked")

However, this needs to be before the declaration of the variable, so we also need to declare the new Object array as a local variable, then assign it into the class variable. So now our constructor looks like this:


    public DynamicList() {
        @SuppressWarnings("unchecked")
        Type[] genericArray = (Type[]) new Object[10];
        array = genericArray;
        size = 0;
    }

This is not exactly elegant, but it works around this flaw in the Java type system.


 

Final DynamicList

With the addition of generic types, our DynamicList can now do almost everything that the ArrayList can do. There are a few more methods we didn't implement. It is also lacking some error checking (such as checking that the indices passed to add and remove are valid). But hopefully this gives you a clear idea of how ArrayList works in Java.

The final version, with comments, is available in DynamicList.java.

Copyright © 2024 Ian Finlayson | Licensed under a Creative Commons BY-NC-SA 4.0 License.