10.8. Old Quiz: Using Generics

10.8.1. Generics Practice Quiz

Generics Practice Quiz

Generics Quiz

Consider the generic Pair class below:

hide circle
hide empty members
set namespaceSeparator none
skinparam classAttributeIconSize 0
skinparam classAttributeIconSize 0
skinparam genericDisplay old
skinparam defaultFontName monospaced
skinparam defaultFontStyle bold

skinparam class {
  BackgroundColor LightYellow
  BackgroundColor<<interface>> AliceBlue
}

class Pair<K,V> {
    + <<new>> Pair(key: K, value: V)
    + getKey(): K
    + getValue(): V
    + <<override>> toString(): String
    + <<override>> hashCode(): int
    + <<override>> equals(o: Object): boolean
}

Which statements below are appropriate uses of generics? An appropriate use of generics will compile and avoid the use of raw types. Select all that apply. Assume each option is independent of the others.

  1. Pair<String, String> p = new Pair<>("a", "b");

  2. Pair<> p = new Pair<String, String>("Bred's", "Fern");

  3. Pair p<String, Boolean> = new Pair<String, Boolean>("Brad", true);

  4. Pair<String, Boolean> p = new Pair("Brad", true);

  5. Pair<Person, Boolean> p = new Pair<Person, Boolean>("Jack", true);

  6. Pair<Pair<Integer, Double>, String> p = new Pair<>(new Pair<Integer, Double>(4, 5.0), "yas");

  7. Pair<String, Boolean> p = new Pair<String, Boolean>("Brad", true);

  8. Pair p = new Pair(7.2, 4);

  9. Pair p = new Pair("Sally", "Joe");

  10. Pair<Shape, String> p = new Pair<Ellipse, String>(new Ellipse(4.2, 3.8), "hello");

Solution
  1. Yes. This is the second-best way to do this kind of statement. The variable type is parameterized and the constructor call makes use of the diamond operator to infer the types for the type parameters.

  2. No. The diamond operator is not allowed with type declarations. That is, you must always parameterize the generic type parameters in a variable declaration.

  3. No. You cannot parameterize the variable name. The programmer probably intended to parameterize the variable’s type.

  4. No. This will compile, but the compiler will generate a warning because the programmer is assigning raw type to a parameterized type.

  5. No. The first parameter (“Jack”) is a String which is not compatible with the expected Person type.

  6. Yes. Believe it or not, this actually compiles! It follows all the rules for parameterizing the variable type and makes proper use of the diamond operator in the constructor calls. It’s tricky because one of the type parameters itself is a parameterized generic type!

  7. Yes. This is the preferred way to do this kind of statement. The variable type and the constructor call are both parameterized.

  8. No. This declares and instantiates raw type. The type parameters T and U both default to Object. Although the class itself is generic, this use of a raw type will likely still require casting to be useful.

  9. No. This declares and instantiates raw type. The type parameters T and U both default to Object. Although the class itself is generic, this use of a raw type will likely still require casting to be useful.

  10. No. Pair<Ellipse, String> is not compatible with Pair<Shape, String> because Pair<Shape, String> is not a parent of Pair<Ellipse, String>. The fact that Ellipse is compatible with Shape has no impact on the compatibility between these types.

10.9. Additional Practice Exercises

Question 1

hide circle
set namespaceSeparator none
skinparam classAttributeIconSize 0
skinparam genericDisplay old

package "shipping.amazeon" {

   class ShippingContainer <DATATYPE> {
       -contents: DATATYPE
       -weight: double
       +<<new>> ShippingContainer(contents: DATATYPE,
       \t\t\t\t\t weight: double)

       +setContents(contents: DATATYPE): void
       +getContents(): DATATYPE
       +setWeight(weight: double): void
       +getWeight(): double
   }
}

Using the UML diagram above (from section 10.1), explain the following:

  1. Explain how the type parameter DATATYPE allows a single generic class to handle multiple product types without creating new classes.

    Solution

    The type parameter DATATYPE is a placeholder for the product type. When creating a ShippingContainer, you provide the actual type (e.g., Drone or Camera). This allows one class to store any type of product without creating separate classes. The compiler ensures that only objects of the specified type can be stored, preventing type errors.

  2. Consider the following code and explain why this code produces a compile-time error.

    1ShippingContainer<Drone> droneContainer;
    2droneContainer = new ShippingContainer<>(new Drone(175.0), 25.0);
    3droneContainer.setContents(new Camera(128.0));
    
    Solution

    Here, droneContainer can only store Drone objects, and cameraContainer can only store Camera objects.

    Trying to store a Camera in droneContainer would produce a compile-time error.

  3. Discuss why catching type errors at compile time is preferable to runtime errors in large software systems like Amazeon’s.

    Solution

    Catching type errors at compile time is preferable because it allows programmers to find and fix mistakes before the program runs, reducing the risk of crashes or unexpected behavior in production. Compile time errors are also easier to find as the compiler often points you directly to the cause of the issue.

    In large systems like Amazeon’s, runtime errors caused by storing the wrong type could affect thousands of shipments, be costly to debug, and compromise reliability. Compile-time type checking ensures safer, more predictable, and easier-to-maintain code.

Question 2
  1. Explain the difference between a type parameter and a type argument in the context of Java Generics.

    Solution

    A type parameter is a placeholder used in a generic class or method definition. It does not represent a real type until the class or method is used. For example, in ShippingContainer<DATATYPE>, DATATYPE is a type parameter.

    A type argument is the actual type supplied when creating an object from a generic class or calling a generic method. It replaces the type parameter. For example, in ShippingContainer<Integer>, Integer is the type argument that tells the compiler what type DATATYPE should be for this instance.

  2. In the following declaration, identify the type parameter and the type argument:

    ShippingContainer<Integer> intContainer = new ShippingContainer<Integer>(17);
    
    Solution
    • Type parameter: DATATYPE in the class definition ShippingContainer<DATATYPE>

    • Type argument: Integer in ShippingContainer<Integer>

  3. Why does the following code not compile?

    ShippingContainer<int> intContainer = new ShippingContainer<int>(17);
    
    Solution

    Java generics cannot use primitive types (such as int, char, double) as type arguments. Generics require reference types, not primitive types. Using a primitive type directly causes a compile-time error.

Question 3
 1/**
 2 * Utility class providing generic methods for arrays.
 3 */
 4 public class Utility {
 5
 6     /**
 7      * Prints all elements of a given array to standard output.
 8      *
 9      * @param <T>   the type of array elements
10      * @param array the array whose elements are to be printed
11      */
12     public static <T> void printArray(T[] array) {
13         for (T item : array) {
14             System.out.println(item);
15         } // for
16     } // printArray
17
18     /**
19      * Finds the maximum element in a given array of Comparable objects.
20      *
21      * @param <T>   the type of array elements, which must implement Comparable
22      * @param array the array to search for the maximum element
23      * @return the maximum element in the array
24      * @throws NullPointerException     if the array or its first element is null
25      * @throws IllegalArgumentException if the array is empty
26      */
27     public static <T extends Comparable<T>> T findMax(T[] array) {
28         if (array == null || array.length == 0) {
29             throw new IllegalArgumentException("Array must not be null or empty");
30         } // if
31
32         T max = array[0];
33         for (T item : array) {
34             if (item.compareTo(max) > 0) {
35                 max = item;
36             } // if
37         } // for
38         return max;
39     } // findMax
40 } // Utility
 1public class Main {
 2
 3    public static void main(String[] args) {
 4
 5        String[] names = {"Alice", "Bob", "Charlie"};
 6        Integer[] scores = {95, 82, 77};
 7        Double[] gpas = {3.8, 3.4, 3.9};
 8
 9        // Printing arrays of different types
10        Utility.printArray(names);
11        Utility.printArray(scores);
12        Utility.printArray(gpas);
13
14        // Finding maximum values
15        System.out.println("Highest score: " + Utility.findMax(scores));
16        System.out.println("Highest GPA: " + Utility.findMax(gpas));
17    } // main
18} // Main

Using the code above, answer the following questions:

  1. How does the <T> type parameter in printArray allow the same method to work for arrays of any type?

  2. Why is <T extends Comparable<T>> necessary in findMax, and what does it ensure about the data passed to the method?

  3. How do generic methods help reduce code duplication compared to writing separate methods for each type?

  4. How does the compiler help catch type errors at compile time when using generic methods?

  5. How does this approach improve code maintenance and prevent mistakes in large applications with many data types?

Solutions
  1. The type parameter <T> is a placeholder for a specific type that gets determined when the method is called. For example:

    String[] names = {"Alice", "Bob", "Charlie"};
    Integer[] scores = {95, 82, 77};
    
    printArray(names);   // Here T is String
    printArray(scores);  // Here T is Integer
    

    This means the method is not tied to any one data type. Instead, the compiler automatically substitutes the appropriate type when the method is used. As a result, one method definition works for all object types, while preserving type safety.

  2. <T extends Comparable<T>> restricts T so that the type parameter (the replacement for T) must implement the Comparable interface. This ensures that any element can be compared to another element of the same type using compareTo.

    Without this bound, the compiler would not allow the code to call item.compareTo(max) because it could not guarantee that T objects are comparable. Example: Integer and Double implement Comparable, so they work with findMax. A custom class would only work if it also implements Comparable.

  3. Without generics, you would have to write a separate method for every data type (e.g., one for String[], one for Integer[], one for Double[]). These methods would contain almost identical code, creating redundancy. Generics consolidate this into a single reusable method. Example: one printArray method works for all arrays, instead of three (or more) separate ones. This makes the code shorter, clearer, and easier to maintain.

  4. The compiler enforces type safety by ensuring that the method only works with the type specified when the method is called. For example:

    ShippingContainer<Drone> droneContainer = new ShippingContainer<>();
    // droneContainer.add(new Camera());  // Compile-time error!
    

    This error would be caught before running the program, preventing runtime crashes.

    In findMax, the compiler also ensures that only comparable types are passed, preventing logical errors during execution.

  5. This approach improves code maintenance by reducing duplication—one generic method can handle many data types instead of writing separate methods for each. It also leverages compile-time checking, which prevents invalid type usage before the program runs. In large applications with many data types, this means fewer bugs, easier updates, and more scalable, reliable code.

Question 4
  1. Write a static generic method called printCenter that prints the center element(s) of an array of any type.

    • If the array has an odd number of elements, it should print the single middle element.

    • If the array has an even number of elements, it should print the two central elements.

    /**
     * Prints the center element(s) of the given array.
     * <p>
     * If the array has an odd number of elements, it prints the single middle element.
     * If the array has an even number of elements, it prints the two central elements.
     *
     * @param <T> the type of elements in the array
     * @param array the array of elements
     */
     public static <T> void printCenter(T[] array) {
         // Your code here...
     } // printCenter
    
    Solution
    public static <T> void printCenter(T[] array) {
        if (array.length == 0) {
            System.out.println("Array is empty.");
        } else {
            int mid = array.length / 2;
    
            if (array.length % 2 == 0) {
                System.out.println(array[mid - 1] + ", " + array[mid]);
            } else {
                System.out.println(array[mid]);
            } // if
        } // if
    } // printCenter
    
  2. Assuming that we have a valid implementation of printCenter, which of the following calls will compile and run correctly?

    String[] names = {"Alice", "Bob", "Charlie"};
    Double[] gpas = {3.8, 3.4, 3.9, 4.0};
    
    Utility.printCenter(names);   // (a)
    Utility.printCenter(gpas);    // (b)
    Utility.printCenter(42);      // (c)
    
    Solution
    1. This call is valid. The type parameter T is inferred as String.

    2. This call is valid. The type parameter T is inferred as Double.

    3. This call does not compile. The argument is an int rather than an array, and the method requires T[].

  3. Write a static generic method called bestOfTwo that returns the larger of two values. Require that T implements Comparable<T>.

    /**
     * Returns the larger of two values.
     *
     * @param <T> the type of the values (must be comparable)
     * @param a the first value
     * @param b the second value
     * @return the larger value
     */
    public static <T extends Comparable<T>> T bestOfTwo(T a, T b) {
        // Your code here...
    } // bestOfTwo
    
    Solution
    /**
     * Returns the larger of two values.
     *
     * @param <T> the type of the values (must be comparable)
     * @param a the first value
     * @param b the second value
     * @return the larger value
     */
    public static <T extends Comparable<T>> T bestOfTwo(T a, T b) {
        if (a.compareTo(b) >= 0) {
            return a;
        } else {
            return b;
        } // if
    } // bestOfTwo
    
  4. Assuming that we have a valid implementation of bestOfTwo, which of the following calls will compile and run correctly?

    String s1 = "apple";
    String s2 = "zebra";
    Integer a = 17, b = 42;
    Object o1 = new Object();
    Object o2 = new Object();
    
    Utility.bestOfTwo(s1, s2);   // (a)
    Utility.bestOfTwo(a, b);     // (b)
    Utility.bestOfTwo(o1, o2);   // (c)
    
    Solution
    • This call is valid. The type parameter T is String, which implements Comparable<String>.

    • This call is valid. The type parameter T is Integer, which implements Comparable<Integer>.

    • This call does not compile. The type Object does not implement Comparable<Object>, and therefore does not satisfy the bound T extends Comparable<T>.

Question 5

Assume you have access to the following method:

public static <T> T foo(int a, int b)
  1. What is the return type of the following call?

    String s = Utility.<String>foo(10, 20);
    
    Solution

    In this call, the type parameter <T> is explicitly specified as String. This means that wherever T appears in the method signature, it is replaced by String. The compiler interprets the method as:

    public static String foo(int a, int b)
    

    Because the return type is now String, assigning the result to a variable of type String is valid. The compiler ensures that only a String value can be returned for this call. This demonstrates how generic methods allow the same method to produce different types depending on the type parameter, avoiding the need to write separate methods for each type.

  2. What is the return type of the following call?

    Double d = Utility.<Double>foo(5, 7);
    
    Solution

    Here, the type parameter <T> is specified as Double. The method signature is therefore treated as:

    public static Double foo(int a, int b)
    

    The compiler now expects a return value of type Double, and assigning it to Double d is valid. This shows that by specifying the type parameter, we can safely use the same generic method to work with completely different types, while the compiler enforces type safety at compile time.

  3. Will the following line compile? If so, what type will foo return?

    Camera c = Utility.<Camera>foo(1, 2);
    
    Solution

    In this example, the type parameter <T> is Camera. The compiler views the method as:

    public static Camera foo(int a, int b)
    

    Because the type parameter matches the variable type Camera c, the assignment is valid and the code compiles successfully. If we tried to assign the result to a variable of a different type, the compiler would produce an error. This illustrates one of the key advantages of generic methods: type errors can be caught at compile time, rather than causing runtime failures.

Question 6

Assume you have access to the following code:

public static <T> int foo(String a, T b)
  1. Will the following line compile? Why or why not?

    Utility.<String>foo("test", "data");
    
    Solution

    Yes, this line compiles. The type parameter <T> is specified as String, so the method signature becomes:

    public static int foo(String a, String b)
    

    Both arguments match the expected types. The compiler confirms type safety, and the code runs successfully.

  2. Will the following line compile? Why or why not?

    Utility.<Integer>foo("grade", 95);
    
    Solution

    Yes, this line compiles. The type parameter <T> is Integer, so the method signature becomes:

    public static int foo(String a, Integer b)
    

    The first argument is a String and the second is an Integer, which matches the expected types. The compiler allows this call and ensures type safety at compile time.

  3. Will the following line compile? Explain your reasoning.

    Utility.<Camera>foo("check", new Drone(150.0));
    
    Solution

    No, this line will not compile. The type parameter <T> is Camera, so the compiler expects the second argument to be of type Camera. Since a Drone object is provided instead, the types are incompatible. The compiler catches this mismatch at compile time, preventing a potential runtime error.

  4. Will this compile successfully?

    String s = Utility.<String>foo("hello", "world");
    
    Solution

    No, this assignment is invalid. Although the type parameter is String, the method foo is declared to return int. The compiler sees a mismatch: an int value cannot be assigned to a variable of type String. This demonstrates that generic type parameters only affect formal parameters, not the declared return type, unless the return type itself uses the type parameter.

Question 7

Assume you have access to the following method:

public static <T> T foo(String a, T b)
  1. What is the return type of the following call?

    String s = Utility.<String>foo("note", "text");
    
    Solution

    The type parameter <T> is specified as String, and the method signature is:

    public static <T> T foo(String a, T b)
    

    By substituting T with String, the method becomes:

    public static String foo(String a, String b)
    

    Therefore, the return type of this call is String, which matches the variable s. The code will compile successfully.

  2. Will the following line compile? Why or why not?

    Integer i = Utility.<Integer>foo("score", 42);
    
    Solution

    Yes, this line compiles. The type parameter <T> is Integer, so the method expects the second argument to be an Integer. The argument provided is 42, which is automatically boxed into an Integer by Java’s autoboxing feature. The return type is also Integer, which matches the variable i. The compiler ensures type safety.

  3. What will be the type of the returned value in this call?

    Shape sh = Utility.<Shape>foo("shape", new Circle(3.5));
    
    Solution

    Here, the type parameter T is replaced by Shape, so the method signature becomes:

    public static Shape foo(String a, Shape b)
    

    The second argument is new Circle(3.5), which is valid because Circle is a subclass of Shape. The return type is Shape, so the variable sh correctly holds the returned object. The compiler enforces this type relationship.

Question 8

Assume you have access to the following method:

Listing 10.5 in Utility.java
public <T> T foo(String a, T b)
  1. Will the following compile? If so, what is the return type?

    Utility util = new Utility();
    String s = util.<String>foo("A", "B");
    
    Solution

    Yes, this line compiles. The method foo is a non-static generic method with the signature:

    public <T> T foo(String a, T b)
    

    Here, the type parameter <T> is specified as String, so b must be a String. Both the argument B and the variable s are of type String, so the call is valid. The return type is String.

  2. Will this line compile? If so, what is the return type?

    Utility util = new Utility();
    Drone d = new Drone(200.0);
    Drone d2 = util.<Drone>foo("drone", d);
    
    Solution

    Yes, this line compiles. By specifying <Drone>, the method expects the second argument b to be a Drone. The argument provided is indeed a Drone, and the returned value matches the type Drone. Therefore, the return type is Drone, and the assignment to d2 is valid.

  3. What happens if we try the following? Explain.

    Utility util = new Utility();
    Camera c = new Camera("Nikon");
    Drone d = util.<Camera>foo("fail", c);
    
    Solution

    This will not compile. The method call specifies the type parameter as <Camera>, so the second argument b must be a Camera. However, the left-hand side variable is of type Drone. The compiler detects a mismatch between the specified type parameter and the assigned variable type. Java’s type checking prevents this code from compiling, avoiding a potential runtime error.

Question 9

Assume you have access to the following method:

public class SomeClass<T> {

    public <R> T foo(T a, R b) {
        // ...
    } // foo

} // someClass
  1. What is the return type of the following call?

    SomeClass<Double> sc = new SomeClass<Double>();
    Double d = sc.<String>foo(3.14, "pi");
    
    Solution

    The method foo has the signature:

    public <R> T foo(T a, R b)
    

    Here, the class-level type parameter T is Double, and the method-level type parameter R is specified as String. The first argument a is a Double, matching T, and the second argument b is a String, matching R. The return type is determined by the class-level type parameter T, which is Double. Therefore, the variable d correctly receives a Double.

  2. Will this compile? If so, what type is returned?

    SomeClass<String> sc = new SomeClass<String>();
    String s = sc.<Integer>foo("data", 99);
    
    Solution

    Yes, this compiles. T is String for the class, and R is specified as Integer for the method. The first argument data matches T and the second argument 99 matches R. The return type is T, which is String, so the assignment to s is valid.

  3. What is the return type here?

    SomeClass<Camera> sc = new SomeClass<Camera>();
    Camera c = sc.<Drone>foo(new Camera("Canon"), new Drone(180.0));
    
    Solution

    The class-level type parameter T is Camera and the method-level parameter R is Drone. Argument a is a Camera, matching T, and argument b is a Drone, matching R. The return type is T, which is Camera. Hence, the variable c correctly receives a Camera object.

  4. How does this example illustrate the interaction between the class-level type parameter T and the method-level type parameter R?

    Solution

    In this example, T is defined at the class level and determines the type returned by all non-static methods that use T. The method-level type parameter R is independent of T and only affects the method’s parameters or local logic. This separation allows the method to accept arguments of a different type than the class while still returning a value of the class-level type. It demonstrates how generic methods can coexist inside generic classes, providing flexibility in type handling without sacrificing type safety.

Question 10
  1. Will this compile? If so, what is the return type?

    SomeClass<Integer> sc = new SomeClass<Integer>();
    Integer i = sc.<String>foo(10, "ten");
    
    Solution

    Yes, this line will compile. The class-level type parameter T is Integer, so the first argument (10) matches T. The method-level type parameter R is String, so the second argument (“ten”) matches R. The return type of foo is T (Integer), so the assignment is valid.

  2. What happens in this scenario?

    SomeClass<Double> sc = new SomeClass<Double>();
    Double d = sc.<Double>foo(3.14, 2.718);
    
    Solution

    This will compile. The class-level type parameter T is Double, so the first argument (3.14) matches T. The method-level type parameter R is also specified as Double, matching the second argument (2.718). The method returns T, which is Double, so the assignment to d is valid.

  3. Predict the behavior of this code:

    SomeClass<String> sc = new SomeClass<String>();
    String s = sc.<Character>foo("hello", 'A');
    
    Solution

    This will compile. T is String (class-level), so the first argument “hello” matches. R is Character (method-level), so the second argument ‘A’ matches. The method returns T, which is String, so the assignment to s is valid.

  4. Will the following code compile? Why or why not?

    SomeClass<Camera> sc = new SomeClass<Camera>();
    Camera c = sc.<Drone>foo(new Drone(100.0), new Camera("Sony"));
    
    Solution

    This will not compile. The class-level type parameter T is Camera, so the first argument must be a Camera, but new Drone(100.0) is a Drone. Method-level type parameter R is Drone, which matches the second argument type, but the mismatch for T prevents compilation.

  5. Suppose you have the following code. Will it compile?

    SomeClass<Shape> sc = new SomeClass<Shape>();
    Shape sh = sc.<Rectangle>foo(new Circle(5.0), new Square(3.0));
    
    Solution

    This will not compile. The class-level type parameter T is Shape, so the first argument must be a Shape. new Circle(5.0) is a Circle, which is a subclass of Shape, so it is compatible. The method-level type parameter R is Rectangle, so the second argument must be a Rectangle. new Square(3.0) is a subclass of Rectangle (if assuming inheritance Square extends Rectangle), so it is compatible. The method returns T, which is Shape, so the return type Shape sh is valid. Therefore, this line will compile, assuming the stated inheritance hierarchy.