Skip to content

Programming by Design

If you're not prepared to be wrong, you'll never come up with anything original. – Sir Ken Robinson

  • About
  • Java-PbD
  • C-PbD
  • ASM-PbD
  • Algorithms
  • Other

Chapter 17 – Generic Methods and Classes

Posted on June 2, 2019April 24, 2025 By William Jojo
Java Book

(Updated January 16, 2025)

Table of contents

    Overview
    The Cloneable Interface
    The Comparable Interface
    Generic Methods
    Generic Classes
    Generic Data Structures
    Exercises

Overview

Much of the code we have dealt with up to this point has been geared toward a specific data type. For example, a particular method or class is often centered around strings or integers.

Now, this may be useful for some instances, but there comes a time when we have to write a piece of code generically. That is, we need the method or class to accept any data type we pass to it and still perform the actions accurately.

Both methods and classes support handling generic data type. We will start our discussion with two interfaces, Cloneable and Comparable, since these interfaces allow us to override some standard methods of the Object class. By doing so, we gain better support in handling data generically.


The Cloneable Interface

Recall from our discussion of arrays that they can be cloned using the clone() method. This is because the array class implements the Cloneable interface. The clone() method is a protected method of the Object superclass. Recall that all objects are descendants of the Object superclass. This method is protected and does not work for just any class we create or existing in some package. Instead, we must elect to implement the interface and override the clone() method with a public one.

Any attempt to use the clone() method without implementing the interface and overriding the clone() method with a public one will result in the following error:

clone() has protected access in java.lang.Object

Example 1 shows a basic implementation of cloning with the essential details highlighted.

CloneEx1.java
public class CloneEx1 {

    public static class MyClass implements Cloneable {
        String s; // immutable String
        int a, b; // primitive types

        public MyClass(String s, int a, int b) {
            this.s = s;
            this.a = a;
            this.b = b;
        }

        public Object clone() {
            MyClass dup;

            try {
                dup = (MyClass) super.clone();
            } catch (CloneNotSupportedException e) {
                return null;
            }

            return dup;
        }
    }

    public static void main(String[] args) {
        MyClass orig, copy;

        orig = new MyClass("Computer", 6, 24);
        copy = (MyClass) orig.clone();
    }
}

Example 1: Program to demonstrate the use of clone() method.

The act of cloning returns a field-for-field copy of the data in the object. The clone intends to be a complete duplicate of the information. We need to recognize the difference between a clone and simply using the statement:

copy = orig;

That statement only makes copy point to the same object, whereas we wanted an exact duplicate.

In Example 1, since there exists only one String object with the value “Computer” we do not have to worry about an explicit clone of the instance variable s. What if it was not a string but another object like an array? Consider the code in Example 2.

CloneEx2.java
public class CloneEx2 {

    public static class MyClass implements Cloneable {
        String s; // immutable String
        int ia[]; // array class object

        public MyClass(String s, int len) {
            this.s = s;
            ia = new int[len];
        }

        public Object clone() {
            MyClass dup;

            try {
                dup = (MyClass) super.clone();
                dup.ia = (int[]) ia.clone();
            } catch (CloneNotSupportedException e) {
                return null;
            }

            return dup;
        }
    }

    public static void main(String[] args) {
        MyClass orig, copy;

        orig = new MyClass("Grades", 50);
        copy = (MyClass) orig.clone();
    }
}

Example 2: Program to demonstrate the need for deep copying when cloning an object.

If we did not explicitly clone the ia instance variable, we would have ended up with a copy of the reference to the original array object. We must always be mindful of our objects when implementing clone(); otherwise, we may alter data that is referenced by another object. These can be very time-consuming bugs to resolve.


The Comparable Interface

This Comparable interface allows us to create a compareTo() method to compare two objects properly. Recall that the String class uses a similar method to compare strings and returns a positive, negative or zero value to indicate if a string is greater than, less than, or equal to the other, respectively like so:

String a = "abc", b = "abb";

if (a.compareTo(b) > 0)
    System.out.println(“a is greater than b.”);
else if (a.compareTo(b) < 0)
    System.out.println(“a is less than b.”);
else
    System.out.println(“a and b are equal”);

We will now consider a class that implements both interfaces Cloneable and Comparable. Example 3 demonstrates this with a Time class that represents clock time.

Time.class
public class Time implements Cloneable, Comparable {

  int hours, minutes, seconds;

  public Time (int hr, int min, int sec) {
    hours = hr;
    minutes = min;
    seconds = sec;
  }

  public Object clone() {
    Time dup;
    try {
      dup = (Time) super.clone();
    }
    catch (CloneNotSupportedException e) {
      return null;
    }

    return dup;
  }

  public int compareTo(Object otherTime) {
    int delta;
    Time t = (Time) otherTime;

    delta = hours - t.hours;
    if (delta != 0)
      return delta;

    delta = minutes - t.minutes;
    if ( delta != 0)
      return delta;

    return seconds - t.seconds;
  }

  public boolean equals(Object otherTime) {
    Time t = (Time) otherTime;
    return hours == t.hours && minutes == t.minutes && seconds == t.seconds;
  }

  public String toString() {
    return String.format("%02d:%02d:%02d", hours, minutes, seconds);
  }
}

Example 3: Implementation of the Time class that implements both Cloneable and Comparable interfaces.

Within the Time class, we have stripped much of the checking that would ordinarily be in place. All of the checks for accurate time, including making sure minutes and seconds are in the range of 0-59, and 24-hour representation is in the range of 0-23, would ordinarily be present. This was done to focus on the two interfaces implemented within the class.

The code in Example 3 includes the clone() method and the new compareTo() method. We have also included an equals() method that overrides the equals() method of the Object superclass so that it accurately verifies that two times are equivalent.

The code in Example 3 also has a main() method that exercises the Time class to make certain that clone(), compareTo() and equals() work as expected.

Generic (Parameterized) Methods

Imagine that we need to display a variable list of data. This data could be any primitive type or class. Imagine further that we have several methods to do the same work but specialized to a particular data type. Consider the following display() methods:

public static void display(int ... items) {
    for (int x : items)
        System.out.print(x + " ");
    System.out.println();
}

public static void display(double ... items) {
    for (int x : items)
        System.out.print(x + " ");
    System.out.println();
}

public static void display(String ... items) {
    for (int x : items)
        System.out.print(x + " ");
    System.out.println();
}

The ellipsis (...) indicates we want a variable argument list or varargs. It is used to designate that we may pass one or more items as individual, comma-separated values or as an array of that type like so:

int x, y, z, ia[] = new int[50];

// Sometime later
display(x);
display(z);
display(x, y, z);
display(ia);

Although overloading allows us to create as many methods as we need to deal with all of the possible types we might want to display, it is simply inconvenient to duplicate code in this manner. Using generic methods with type parameter or type variables allows us to simplify this arrangement into a single method as seen here:

public static <T> void display(T ... items) {
    for ( T x : items )
        System.out.print(x + " ");
    System.out.println();
}

The portions of the new display() method with the capital T indicate how the type is to be applied to the method. This is also known as a parameterized method.

  • First, the <T> is the comma-delimited list of type variables used in this method. This is placed just before specifying the return type for the method.
  • Second, we state that items is a list of references of type T. Type parameters can only represent references. There is no problem using primitive types since they will be autoboxed for us in JDK5 and later. The exception is an array of a primitive type since each array element is not autoboxed when passed.
  • Third, the variable x is of type T to complete the foreach loop.

It is not uncommon to specify the return type, formal parameters, or local variables using the type parameter. Any combination is allowed. In our display() example, the only item that was not specified by the type variable T is the return type - which is void.

It is essential to realize that we cannot instantiate objects specified by the type parameter. So, although we can declare variables:

T refName;

we cannot instantiate the objects:

refName = new T();

This is due to type erasure. Type erasure essentially removes all type knowledge at compile time so that there are only items of type Object to manipulate at runtime. This changes slightly if the type is bounded (see below), at which point the type becomes the type specified by the bound.

The program in Example 5 demonstrates using our generic display() method.

GenMethod.java
public class GenMethod {

    @SafeVarargs //For JDK 7 and higher
    public static  void display(T... items) {
        for (T x : items)
            System.out.print(x + " ");
        System.out.println();
    }

    public static void main(String[] args) {
        Time clock1, clock2;
        int x = 10, y = 15;
        Integer[] ia = {1, 2, 3, 4, 5};

        clock1 = new Time(11, 57, 0); // 11:57:00am
        clock2 = new Time(14, 0, 0); // 2:00:00pm

        System.out.print("x = ");
        display(x);

        System.out.print("y = ");
        display(y);

        System.out.print("x & y = ");
        display(x, y);

        System.out.print("ia = ");
        display(ia);

        System.out.print("clock1 & clock2 = ");
        display(clock1, clock2);
    }
}

Example 4: Program demonstrating a generic method with the Time class from a previous example.

The code in Example 4 also contains an annotation called @SafeVarargs. A warning will be given if parameterized values are used with varargs, which can lead to heap corruption. The details of this are many and can be found here.

We can also restrict the data type we will process in a method by using a bounded type parameter. This is shown in the following:

public static <T extends Comparable<T>> T min(T a, T b) {
    if (a.compareTo(b) < 0)
        return a;
    else
        return b;
}

public static <T extends Comparable<T>> T max(T a, T b) {
    if (a.compareTo(b) >= 0)
        return a;
    else
        return b;
}

The expression <T extends Comparable<T>> simply sets a different upper bound on what can be used as the type parameter. Normally, this is Object. This states that only those objects that implement the Comparable interface. This is necessary because we are using the compareTo() method, and not all classes implement that method.

Note that we are using extends instead of implements. Ordinarily, we would use implements when specifying an interface like Comparable. Whenever we are setting bounds on the type parameter we always use extends.

Bounded.java
public class Bounded {

    public static <T extends Comparable<T>> T min(T a, T b) {
        if (a.compareTo(b) < 0)
            return a;
        else
            return b;
    }

    public static <T extends Comparable<T>> T max(T a, T b) {
        if (a.compareTo(b) >= 0)
            return a;
        else
            return b;
    }

    public static void main(String[] args) {
        int a = 10, b = 15;
        double f = 4.01, g = 3.14;
        String s = "ZZZ", t = "abd";

        System.out.println(min(a, b) + " is smaller than " + max(a, b));
        System.out.println(min(f, g) + " is smaller than " + max(f, g));
        System.out.println(min(s, t) + " is smaller than " + max(s, t));
    }
}

Example 5: Program to demonstrate a generic method with a bounded type parameter.

The program in Example 5 shows how we can use the new min() and max() bounded generic methods.

In this program, note that the int and double type variables in the main() method are autoboxed before being sent to the min() and max() methods. The output is as follows:

10 is smaller than 15
3.14 is smaller than 4.01
ZZZ is smaller than abd

Generic Classes

Recall our use of checked types when using the Vector class. That is, we place the class type to be stored in the object into <> immediately after the name of the class, such as Vector<String>. We can create generic classes by the same means.

We will now take our Linked_List from a previous discussion and make it generic. Example 7 shows how this can be done. In this example, it is as simple as placing <T> after the class name and then using T for those variables that represent the data type being stored.

GenList.java
public class GenList<T> {

    private Node head;

    private class Node {
        private T data;
        private Node next;

        private Node(T item) {
            data = item;
            next = null;
        }
    }

    public GenList() {
        head = null;
    }

    public void insert(T item) {
        Node n = new Node(item);

        n.next = head;
        head = n;
    }

    public void delete(T item) {
        Node cur = head, back = null;

        while (cur != null) {
            if (cur.data.equals(item)) {
                if (back == null)
                    head = cur.next;
                else
                    back.next = cur.next;

                break; // leave the loop
            } else {
                back = cur;
                cur = cur.next; // move to the next node
            }
        }
    }

    @Override
    public String toString() {
        String o = "[";
        Node p = head;

        // first data value is alone.
        if (p != null) {
            o += p.data;
            p = p.next;
        }
        // remaining data values have ", value"
        while (p != null) {
            o += ", " + p.data;
            p = p.next;
        }
        return o + "]";
    }
}

Example 6: Class GenList demonstrating a generic class for a linked list.

The programs in Example 7, Example 8 and Example 9 exercise the generic class with the Integer, String and Time classes.

First, we test GenList with Integer as shown in Example 7.

TestGenListInt.java
public class TestGenListInt {

    public static void main(String[] args) {

        GenList<Integer> ll = new GenList<>();

        System.out.println("\nAdding 35, 78, 45 and 0 to the list");
        ll.insert(35);
        ll.insert(78);
        ll.insert(45);
        ll.insert(0);
        System.out.println(ll);

        System.out.println("\nRemoving 0 from head of list");
        ll.delete(0); // beginning
        System.out.println(ll);

        System.out.println("\nRemoving 78 from middle of list");
        ll.delete(78); // middle
        System.out.println(ll);

        System.out.println("\nRemoving 35 from end of list");
        ll.delete(35); // end
        System.out.println(ll);
    }
}

Example 7: Program to test GenList with Integer class objects.

The output of Example 7 is show below:

Adding 35, 78, 45 and 0 to the list
[0, 45, 78, 35]

Removing 0 from head of list
[45, 78, 35]

Removing 78 from middle of list
[45, 35]

Removing 35 from end of list
[45]

Next, Example 8 uses String to test the GenList class.

TestGenListString.java
public class TestGenListString {

    public static void main(String[] args) {

        GenList<String> ll = new GenList<String>();

        System.out.println("\nAdding Blue, Red, Orange and Green to the list");
        ll.insert("Blue");
        ll.insert("Red");
        ll.insert("Orange");
        ll.insert("Green");
        System.out.println(ll);

        System.out.println("\nRemoving Green from head of list");
        ll.delete("Green"); // beginning
        System.out.println(ll);

        System.out.println("\nRemoving Red from middle of list");
        ll.delete("Red"); // middle
        System.out.println(ll);

        System.out.println("\nRemoving Blue from end of list");
        ll.delete("Blue"); // end
        System.out.println(ll);
    }
}

Example 8: Program to test GenList with String objects.

The output of Example 9 is show below:

Adding Blue, Red, Orange and Green to the list
[Green, Orange, Red, Blue]

Removing Green from head of list
[Orange, Red, Blue]

Removing Red from middle of list
[Orange, Blue]

Removing Blue from end of list
[Orange]
TestGenListTime.java
public class TestGenListTime {

    public static void main(String[] args) {

        GenList<Time> ll = new GenList<>();
        Time clock1 = new Time(11, 35, 0);
        Time clock2 = new Time(0, 0, 0);
        Time clock3 = new Time(16, 0, 0);
        Time clock4 = new Time(23, 59, 59);

        System.out.printf("Adding %s, %s, %s & %s to the list.%n", clock1, clock2, clock3, clock4);
        ll.insert(clock1);
        ll.insert(clock2);
        ll.insert(clock3);
        ll.insert(clock4);
        System.out.println(ll);

        System.out.printf("\nRemoving %s from head of list%n", clock4);
        ll.delete(clock4); // beginning
        System.out.println(ll);

        System.out.printf("\nRemoving %s from middle of list%n", clock2);
        ll.delete(clock2); // middle
        System.out.println(ll);

        System.out.printf("\nRemoving %s from end of list%n", clock1);
        ll.delete(clock1); // end
        System.out.println(ll);
    }
}

Example 9: Program to test GenList with Time objects.

The output of Example 9 is shown below:

Adding 11:35:00, 00:00:00, 16:00:00 & 23:59:59 to the list.
[23:59:59, 16:00:00, 00:00:00, 11:35:00]

Removing 23:59:59 from head of list
[16:00:00, 00:00:00, 11:35:00]

Removing 00:00:00 from middle of list
[16:00:00, 11:35:00]

Removing 11:35:00 from end of list
[16:00:00]

Generic Data Structures

We discussed Stacks, Queues, and Deques in the previous topic. Below are all those with slight modifications to make them basic generic classes.

Stack

GenStack.java
import java.util.EmptyStackException;

public class GenStack<T> {
    // the top of the stack
    private Node top;
    private int size;

    private class Node {
        private T data;
        private Node next;

        private Node(T item) {
            data = item;
            next = null;
        }
    }

    public GenStack() {
        top = null;
        size = 0;
    }

    // returns the top value or exception when empty.
    public T peek() {
        if (isEmpty())
            throw new EmptyStackException();
        else
            return top.data;
    }

    // push new item on the top
    public void push(T item) {
        Node n = new Node(item);
        n.next = top;
        top = n;
        size++;
    }

    // remove item from the top and return the removed value.
    public T pop() {
        T v;
        if (isEmpty())
            throw new EmptyStackException();
        v = top.data;
        top = top.next;
        size--;
        return v;
    }

    // debug method to dump the contents of the stack to the screen
    public String toString() {
        Node p = top;
        String s = "";

        while (p != null) {
            s = s + p.data + "\n";
            p = p.next;
        }
        return s;
    }

    // method to determine if the stack is empty.
    public boolean isEmpty() {
        return top == null;
    }

    // method to return the number of frames
    public int size() {
        return size;
    }

    // main method to test the class
    public static void main(String[] args) {
        GenStack<Integer> s = new GenStack<>();

        s.push(7);
        s.push(5);
        s.push(3);
        System.out.println("Stack length is " + s.size());
        System.out.println("Stack contains:\n" + s);

        while (!s.isEmpty()) {
            System.out.println("Popped " + s.pop());
        }

        // intentially try to pop an empty stack
        try {
            System.out.println("Popped " + s.pop());
        } catch (EmptyStackException e) {
            System.out.println("That could have been bad!");
        }
    }
}

Queue

GenQueue.java
import java.util.NoSuchElementException;

public class GenQueue<T> {
    // the top of the queue
    private Node first, last;
    private int size;

    private class Node {
        private T data;
        private Node next;

        private Node(T item) {
            data = item;
            next = null;
        }
    }

    public GenQueue() {
        first = null;
        last = null;
        size = 0;
    }

    // returns the first value or exception when empty.
    public T peek() {
        if (isEmpty())
            throw new NoSuchElementException();
        else
            return first.data;
    }

    // enqueue (add) new item on the last
    public void enq(T item) {
        Node oldlast = last;

        last = new Node(item);
        if (isEmpty())
            first = last;
        else
            oldlast.next = last;
        size++;
    }

    // dequeue (remove) item from the first of the queue and return the removed value.
    public T deq() {
        T v;

        if (isEmpty())
            throw new NoSuchElementException();
        v = first.data;
        first = first.next;
        if (first == null)
            last = null;
        size--;
        return v;
    }

    // debug method to dump the contents of the queue to the screen
    public String toString() {
        Node p = first;
        String s;

        s = "FIRST ->";
        while (p != null) {
            s = s + " " + p.data + " ";
            p = p.next;
        }
        s = s + "<- LAST\n";
        return s;
    }

    // method to determine if the queue is empty.
    public boolean isEmpty() {
        return first == null;
    }

    // method to return the number of queue entries
    public int size() {
        return size;
    }

    // main method to test the class
    public static void main(String[] args) {
        GenQueue<Integer> q = new GenQueue<>();

        q.enq(7);
        q.enq(5);
        q.enq(3);
        System.out.print("Queue contains:\n" + q);
        System.out.println("Queue length is " + q.size());

        while (!q.isEmpty()) {
            System.out.println("Dequeued " + q.deq());
        }

        // intentionally try to deq an empty queue
        try {
            System.out.println("Dequeued " + q.deq());
        } catch (NoSuchElementException e) {
            System.out.println("That could have been bad!");
        }
    }
}

Deque

GenDeque.java
import java.util.NoSuchElementException;

public class GenDeque<T> {
    // the top of the queue
    private Node first, last;
    private int size;

    private class Node {
        private T data;
        private Node next, prev;

        private Node(T item) {
            data = item;
            next = null;
            prev = null;
        }
    }

    public GenDeque() {
        first = null;
        last = null;
        size = 0;
    }

    // returns the first value or exception when empty.
    public T peekFirst() {
        if (isEmpty())
            throw new NoSuchElementException();
        else
            return first.data;
    }

    // returns the last value or exception when empty.
    public T peekLast() {
        if (isEmpty())
            throw new NoSuchElementException();
        else
            return last.data;
    }

    // enqueue (add) new item on the last
    public void addLast(T item) {
        Node oldlast = last;

        last = new Node(item);
        if (isEmpty())
            first = last;
        else {
            // link up both directions
            oldlast.next = last;
            last.prev = oldlast;
        }
        size++;
    }

    // enqueue (add) new item on the first
    public void addFirst(T item) {
        Node oldfirst = first;

        first = new Node(item);
        if (isEmpty())
            last = first;
        else {
            // link up both directions
            oldfirst.prev = first;
            first.next = oldfirst;
        }
        size++;
    }

    // dequeue (remove) item from the first of the queue and return the removed value.
    public T removeFirst() {
        T v;

        if (isEmpty())
            throw new NoSuchElementException();
        v = first.data;
        // advance and null the prev link.
        first = first.next;
        if (first != null)
            first.prev = null;
        else
            last = null;
        size--;
        return v;
    }

    // dequeue (remove) item from the first of the queue and return the removed value.
    public T removeLast() {
        T v;

        if (isEmpty())
            throw new NoSuchElementException();
        v = last.data;
        // advance and null the prev link.
        last = last.prev;
        if (last != null)
            last.next = null;
        else
            first = null;
        size--;
        return v;
    }

    // debug method to dump the contents of the queue to the screen
    public String toString() {
        Node p = first;
        String s;

        s = "FIRST ->";
        while (p != null) {
            s = s + " " + p.data + " ";
            p = p.next;
        }
        s = s + "<- LAST\n";

        s = s + "LAST ->";
        p = last;
        while (p != null) {
            s = s + " " + p.data + " ";
            p = p.prev;
        }
        s = s + "<- FIRST\n";

        return s;
    }

    // method to determine if the queue is empty.
    public boolean isEmpty() {
        return first == null;
    }

    // method to return the number of queue entries
    public int size() {
        return size;
    }

    // main method to test the class
    public static void main(String[] args) {
        GenDeque<Integer> d = new GenDeque<>();
        int x;

        d.addLast(7);
        d.addLast(5);
        d.addLast(3);
        d.addFirst(13);
        d.addFirst(15);
        d.addFirst(17);
        System.out.print("Deque contains:\n" + d);
        System.out.println("Deque length is " + d.size());

        x = 0;
        while (x < 3) {
            System.out.println("Dequeued from front " + d.removeFirst());
            x++;
        }

        System.out.print("Deque contains:\n" + d);

        x = 0;
        while (x < 3) {
            System.out.println("Dequeued from end " + d.removeLast());
            x++;
        }

        System.out.print("Deque contains:\n" + d);

        // intentionally try to deq an empty queue
        try {
            System.out.println("Dequeued from front " + d.removeFirst());
        } catch (NoSuchElementException e) {
            System.out.println("That could have been bad!");
        }

        // intentionally try to deq an empty queue
        try {
            System.out.println("Dequeued fron end " + d.removeLast());
        } catch (NoSuchElementException e) {
            System.out.println("That could have been bad!");
        }
    }
}

Exercises

    (Beginner) Rewrite Example 5 using ternary operators.

Post navigation

❮ Previous Post: Chapter 16 – Stack, Queues and Deques
Next Post: Chapter 18 – Trees ❯

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Copyright © 2018 – 2025 Programming by Design.