Pair Generic Lab

Introduction

he goal of this lab is to explore key features of the Java collections framework. Collections are tailored to hold values of certain types through generic type parameters. Collection classes also provide iterators so that clients can loop through elements in a standard way.

Portions of this lab were developed by Dr. Michael Kirkpatrick with modifications by Dr. Chris Johnson.

Files

Download the following files:

Pair and PairTest

Your first task is to develop a generic `Pair`` class that represents a 2-tuple. A 2-tuple is a collection of two elements, a 3-tuple is a collection of three elements, and an n-tuple is a collection of n elements.

A Pair abstraction is useful when you need to store ordered pairs like Cartesian coordinates, names and quantities, latitudes and longitudes, keys and values, and so on. To make Pair versatile, you don’t want to overconstrain the types of the two components. You want the first component to be of any type, and you want the second component to be of any type. The two components may have the same or different types.

The downloaded Pair class is flexible: it can be used to store objects of any type. Unfortunately, it is both dangerous and inconvenient. It is dangerous because we may accidentally store an object of an unexpected type (as you saw above). It is inconvenient because we will usually need to cast objects retrieved from a Pair before they can be used.

We can address these issues by making our Pair class generic. Our improved class will accept two type parameters since we may want the type of the first element to be different from the type of the second. Complete the following steps. Follow these steps to build a generic Pair class and a set of unit tests:

  • Download files

  • Scan through Pair.java and PairTest.java. Run the unit tests and confirm that they pass—for the moment. You are about to change both files so that Pair uses generics instead of the meaningless Object class.

  • Change testPairCharacterInteger so the Pair is declared with type parameters Character and Integer. In other words, you don’t want this raw instance:

Pair pair = new Pair('B', 12);

You want this parameterized instance:

Pair<Character, Integer> pair = new Pair<>('B', 12);
  • The parameters are optional in the second <> because the compiler infers them from the variable declaration. The tests will no longer compile. That’s to be expected. A principle of test-driven development is that you write the test first, and the code to be tested second.

  • Edit Pair to use generics and pass the first test. Note that all occurrences of Object should be replaced with a generic parameter. The type Object should not appear anywhere in the finished class.

  • Eliminate the casts and value getters in the calls to assertEquals. Because of the generic parameter, the compiler knows the types and doesn’t need you to cast anything.

  • Implement testPairDoubleDouble and testPairStringInteger to perform appropriate tests for pairs of different component types. Make sure they pass. Note that assertEquals requires an extra tolerance parameter for doubles.

  • Modify the Pair class to use generics so that it passes these three tests. Note that all uses of Object should be replaced with a generic parameter. NOTE: The type of Object should NOT appear anywhere in the finished class.

  • Uncomment the remaining tests in PairTest and make sure they complete successfully.

Iterators

You have a versatile but typechecked Pair class. Now create another collection, an array of Pair. Follow the instructions inside the PairList.java file and complete the methods. The supporting files (PairListTest.java and PairListDriver.java) do not required any modification.

Why Not Generic Arrays

You cannot create an array of a generic type in Java. In the early days of the language, the Java designers decided that arrays could be subtypes of other array types, provided the elements followed a subtype relationship. Assigning an array of Integer`` to a reference of type Object[]``, for example, is perfectly legal:

Object[] somethings = new Integer[10];
somethings[0] = 17;

But assigning to a reference of type String[] is not because Integer is not a subtype of String:

tring[] strings = new Integer[10];  // fails to compile

Assigning the array to a supertype has a cost. The compiler can no longer detect that this code is dangerous because a String typechecks as an Object:

somethings[0] = "Hydrogen";

So, the Java designers decided to add checks at runtime. When an element is assigned, the Java virtual machine verifies that the new element is a subtype of the array’s element type. If not, it throws an ArrayStoreException. In this case, a String is not an Integer, and we see the exception. Now suppose you could make an array whose type depends on a generic parameter:

Object[] lists = new ArrayList<String>[10];  // only hypothetical

Because of the way Java implements generics, the generic parameter String is only known at compile time. It is effectively erased in the compiled bytecode (this is formally called type erasure). At runtime, then, the code behaves like this:

Object[] lists = new ArrayList[10];   // the generic is erased

The array’s element type is just plain ArrayList. Since the element type has been erased, assigning a different sort of ArrayList doesn’t generate an ArrayStoreException:

lists[0] = new ArrayList<Integer>();  // hypothetically legal

Being able to assign an ArrayList<Integer> to an array of ArrayList<String> would be bad news. Programmers wouldn’t be able to trust Java’s typechecking. So, the designers forbid you from creating an array of elements whose type depends on a generic. As a workaround, you may allocate a raw array but assign it to generic reference:

@SuppressWarnings("unchecked")
ArrayList<String>[] lists = new ArrayList[10];

The annotation is needed to disable the warning. Suppressing warnings is almost always a bad idea that you should avoid. Because the generics are present on lists, the compiler will catch any type mismatches at compile time.

Lets Use Generics

Take another look at the PairDriver.java file. Your IDE should now be showing multiple warnings indicating that you aren’t using the Pair class correctly: no type arguments are being provided.

Complete the following steps.

  1. Add type arguments so that the Pair instances all have String for the first object and Integer for the second.
  2. STOP and THINK After adding the generics, can you remove the explicit casting in largestStadium? Why or why not? If you can, then go ahead and do so.

Lab Submission

Submit the following files together in Gradescope:

  • Pair.java
  • PairTest.java
  • PairDriver.java
  • PairList.java
Last modified April 29, 2024: application lab download link update (3775b1d)