CS F213 Objected Oriented Programming Labsheet 11

Java - Generics

Java Generic methods and generic classes enable programmers to specify, with a single method declaration, a set of related methods, or with a single class declaration, a set of related types, respectively.

Generics also provide compile-time type safety that allows programmers to catch invalid types at compile time.

Using Java Generic concept, we might write a generic method for sorting an array of objects, then invoke the generic method with Integer arrays, Double arrays, String arrays and so on, to sort the array elements.

Types of Java Generics

  1. Generic Classes

A generic class declaration looks like a non-generic class declaration, except that the class name is followed by a type parameter section.

As with generic methods, the type parameter section of a generic class can have one or more type parameters separated by commas. These classes are known as parameterized classes or parameterized types because they accept one or more parameters.

Example

public class Box<T> {
   private T t;

   public void add(T t) {
      this.t = t;
   }

   public T get() {
      return t;
   }

   public static void main(String[] args) {
      Box<Integer> integerBox = new Box<Integer>();
      Box<String> stringBox = new Box<String>();
    
      integerBox.add(new Integer(10));
      stringBox.add(new String("Hello World"));

      System.out.printf("Integer Value :%d\n\n", integerBox.get());
      System.out.printf("String Value :%s\n", stringBox.get());
   }
}

Output

Integer Value :10
String Value :Hello World
  1. Generic Methods: You can write a single generic method declaration that can be called with arguments of different types. Based on the types of the arguments passed to the generic method, the compiler handles each method call appropriately.

Rules to Define Generic Methods

  • All generic method declarations have a type parameter section delimited by angle brackets (< and >) that precedes the method's return type ( < E >).

  • Each type parameter section contains one or more type parameters separated by commas. A type parameter, also known as a type variable, is an identifier that specifies a generic type name.

  • The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.

  • A generic method's body is declared like that of any other method. Note that type parameters can represent only reference types, not primitive types (like int, double and char)

Example

public class GenericMethodTest {
   // generic method printArray
   public static < E > void printArray( E[] inputArray ) {
      // Display array elements
      for(E element : inputArray) {
         System.out.printf("%s ", element);
      }
      System.out.println();
   }

   public static void main(String args[]) {
      // Create arrays of Integer, Double and Character
      Integer[] intArray = { 1, 2, 3, 4, 5 };
      Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
      Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };

      System.out.println("Array integerArray contains:");
      printArray(intArray);   // pass an Integer array

      System.out.println("\nArray doubleArray contains:");
      printArray(doubleArray);   // pass a Double array

      System.out.println("\nArray characterArray contains:");
      printArray(charArray);   // pass a Character array
   }
}

Output

Array integerArray contains:
1 2 3 4 5 

Array doubleArray contains:
1.1 2.2 3.3 4.4 

Array characterArray contains:
H E L L O

Bounded Type Parameters

There may be times when you'll want to restrict the kinds of types that are allowed to be passed to a type parameter. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.

To declare a bounded type parameter, list the type parameter's name, followed by the extends keyword, followed by its upper bound.

Following example illustrates how extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces). This example is Generic method to return the largest of three Comparable objects

Exaamle

public class MaximumTest {
   // determines the largest of three Comparable objects
   public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
      T max = x;   // assume x is initially the largest
      if(y.compareTo(max) > 0) {
         max = y;   // y is the largest so far
      }
      if(z.compareTo(max) > 0) {
         max = z;   // z is the largest now                 
      }
      return max;   // returns the largest object   
   }
   public static void main(String args[]) {
      System.out.printf("Max of %d, %d and %d is %d\n\n", 
         3, 4, 5, maximum( 3, 4, 5 ));

      System.out.printf("Max of %.1f,%.1f and %.1f is %.1f\n\n",
         6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ));

      System.out.printf("Max of %s, %s and %s is %s\n","pear",
         "apple", "orange", maximum("pear", "apple", "orange"));
   }
}

Output

Max of 3, 4 and 5 is 5

Max of 6.6,8.8 and 7.7 is 8.8

Max of pear, apple and orange is pear

Wildcard in Java Generics

The ? (question mark) symbol represents the wildcard element. It means any type. If we write <? extends Number>, it means any child class of Number, e.g., Integer, Float, and double. Now we can call the method of Number class through any child class object.

We can use a wildcard as a type of a parameter, field, return type, or local variable. However, it is not allowed to use a wildcard as a type argument for a generic method invocation, a generic class instance creation, or a supertype.

Example

import java.util.*;  
abstract class Shape{  
abstract void draw();  
}  
class Rectangle extends Shape{  
void draw(){System.out.println("drawing rectangle");}  
}  
class Circle extends Shape{  
void draw(){System.out.println("drawing circle");}  
}  
class GenericTest{  
//creating a method that accepts only child class of Shape  
public static void drawShapes(List<? extends Shape> lists){  
for(Shape s:lists){  
s.draw();//calling method of Shape class by child class instance  
}  
}  
public static void main(String args[]){  
List<Rectangle> list1=new ArrayList<Rectangle>();  
list1.add(new Rectangle());  
  
List<Circle> list2=new ArrayList<Circle>();  
list2.add(new Circle());  
list2.add(new Circle());  
  
drawShapes(list1);  
drawShapes(list2);  
}}  

Output

drawing rectangle
drawing circle
drawing circle

Upper Bounded Wildcards

The purpose of upper bounded wildcards is to decrease the restrictions on a variable. It restricts the unknown type to be a specific type or a subtype of that type. It is used by declaring wildcard character ("?") followed by the extends (in case of, class) or implements (in case of, interface) keyword, followed by its upper bound.

Suppose, we want to write the method for the list of Number and its subtypes (like Integer, Double). Using List<? extends Number> is suitable for a list of type Number or any of its subclasses whereas List< Number> works with the list of type Number only. So, List<? extends Number> is less restrictive than List< Number>

Example

import java.util.ArrayList;  
public class UpperBoundWildcard {     
    private static Double add(ArrayList<? extends Number> num) {  
        double sum=0.0;  
        for(Number n:num)  
        {  
            sum = sum+n.doubleValue();  
        }     
        return sum;  
    }  
    public static void main(String[] args) {   
        ArrayList<Integer> l1=new ArrayList<Integer>();  
        l1.add(10);  
        l1.add(20);  
        System.out.println("displaying the sum= "+add(l1));  
        ArrayList<Double> l2=new ArrayList<Double>();  
        l2.add(30.0);  
        l2.add(40.0);  
        System.out.println("displaying the sum= "+add(l2));     
    }     
}  

Output

displaying the sum= 30.0
displaying the sum= 70.0

Lower Bounded Wildcards

The purpose of lower bounded wildcards is to restrict the unknown type to be a specific type or a supertype of that type. It is used by declaring wildcard character ("?") followed by the super keyword, followed by its lower bound.

Suppose, we want to write the method for the list of Integer and its supertype (like Number, Object). Using List<? super Integer> is suitable for a list of type Integer or any of its superclasses whereas List< Integer> works with the list of type Integer only. So, List<? super Integer> is less restrictive than List< Integer>.

Example

import java.util.Arrays;  
import java.util.List;  
public class LowerBoundWildcard {  
    public static void addNumbers(List<? super Integer> list) {  
        for(Object n:list)  
        {  
              System.out.println(n);  
        }  
    }  
public static void main(String[] args) {  
    List<Integer> l1=Arrays.asList(1,2,3);  
      System.out.println("displaying the Integer values");  
    addNumbers(l1);    
    List<Number> l2=Arrays.asList(1.0,2.0,3.0);  
      System.out.println("displaying the Number values");  
    addNumbers(l2);  
}  
}  

Output

displaying the Integer values
1
2
3
displaying the Number values
1.0
2.0
3.0

Generic Interfaces

In addition to generic classes and methods, you can also have generic interfaces. Generic interfaces are specified just like generic classes. Here is an example. It creates an interface called MinMax that declares the methods min( ) and max( ), which are expected to return the minimum and maximum value of some set of objects.

Example

interface MinMax<T extends Comparable<T>> {
 T min();
 T max();
}
// Now, implement MinMax
class MyClass<T extends Comparable<T>> implements MinMax<T> {
 T[] vals;
 MyClass(T[] o) { vals = o; }
 // Return the minimum value in vals.
 public T min() {
 T v = vals[0];
 for(int i=1; i < vals.length; i++)
 if(vals[i].compareTo(v) < 0) v = vals[i];
 return v;
 }
 // Return the maximum value in vals.
 public T max() {
 T v = vals[0];
 for(int i=1; i < vals.length; i++)
 if(vals[i].compareTo(v) > 0) v = vals[i];
 return v;
 }
}
class GenIFDemo {
 public static void main(String[] args) {
 Integer[] inums = {3, 6, 2, 8, 6 };
 Character[] chs = {'b', 'r', 'p', 'w' };
 MyClass<Integer> iob = new MyClass<Integer>(inums);
 MyClass<Character> cob = new MyClass<Character>(chs);
 System.out.println("Max value in inums: " + iob.max());
 System.out.println("Min value in inums: " + iob.min());
 System.out.println("Max value in chs: " + cob.max());
 System.out.println("Min value in chs: " + cob.min());
 }
}

Output

Max value in inums: 8
Min value in inums: 2
Max value in chs: w
Min value in chs: b

The generic interface offers two benefits. First, it can be implemented for different types of data. Second, it allows you to put constraints (that is, bounds) on the types of data for which the interface can be implemented. In the MinMax example, only types that implement the Comparable interface can be passed to T.

Generic Class Hierarchies

Generic classes can be part of a class hierarchy in just the same way as a non-generic class. Thus, a generic class can act as a superclass or be a subclass. The key difference between generic and non-generic hierarchies is that in a generic hierarchy, any type arguments needed by a generic superclass must be passed up the hierarchy by all subclasses. This is similar to the way that constructor arguments must be passed up a hierarchy.

Using a Generic Superclass

class Gen<T> {
 T ob;
 Gen(T o) {
 ob = o;
 }
 // Return ob.
 T getOb() {
 return ob;
 }
}
// A subclass of Gen.
class Gen2<T> extends Gen<T> {
 Gen2(T o) {
 super(o);
 }
}

The type parameter T is specified by Gen2 and is also passed to Gen in the extends clause. This means that whatever type is passed to Gen2 will also be passed to Gen. For example, this declaration

It is perfectly acceptable for a non-generic class to be the superclass of a generic subclass. For example, consider this program:

class NonGen {
 int num;
 NonGen(int i) {
 num = i;
 }
 int getnum() {
 return num;
 }
}
// A generic subclass.
class Gen<T> extends NonGen {
 T ob; // declare an object of type T
 // Pass the constructor a reference to
 // an object of type T.
 Gen(T o, int i) {
 super(i);
 ob = o;
 }
 // Return ob.
 T getOb() {
 return ob;
 }
}
// Create a Gen object.
class HierDemo2 {
 public static void main(String[] args) {
 // Create a Gen object for String.
 Gen<String> w = new Gen<String>("Hello", 47);
 System.out.print(w.getOb() + " ");
 System.out.println(w.getnum());
 }
}

Run-Time Type Comparisons Within a Generic Hierarchy

Recall the run-time type information operator instanceof. As explained, instanceof determines if an object is an instance of a class. It returns true if an object is of the specified type or can be cast to the specified type. The instanceof operator can be applied to objects of generic classes.

Casting

You can cast one instance of a generic class into another only if the two are otherwise compatible and their type arguments are the same.

Type Inference with Generics

Beginning with JDK 7, it became possible to shorten the syntax used to create an instance of a generic type. So now,

MyClass<Integer, String> mcOb = new MyClass<Integer, String>(98, "A String");

can be replaced by

MyClass<Integer, String> mcOb = new MyClass<>(98, "A String");

Notice that the instance creation portion simply uses <>, which is an empty type argument list. This is referred to as the diamond operator. It tells the compiler to infer the type arguments needed by the constructor in the new expression. The principal advantage of this type-inference syntax is that it shortens what are sometimes quite long declaration statements.

A generic class cannot extend Throwable. This means that you cannot create generic exception classes.

Exercises

Exercise 1

Write a Java program that implements a generic method in Main class called swapElements. The Box class should have a single instance variable, value, of type T. The swapElements method should take an array of type T and swap the elements at the specified positions.

Sample Input (in code)

Integer[] intArray = {1, 2, 3, 4, 5};
int intPosition1 = 1;
int intPosition2 = 3;

Character[] charArray = {'a', 'b', 'c', 'd', 'e'};
int charPosition1 = 0;
int charPosition2 = 4;

Sample Output

Integer array before swapping elements: [1, 2, 3, 4, 5]
Integer array after swapping elements: [1, 4, 3, 2, 5]
Character array before swapping elements: [a, b, c, d, e]
Character array after swapping elements: [e, b, c, d, a]

Exercise 2

Write a Java program that includes a generic class called DataProcessor<T extends Comparable<? super T>>. The DataProcessor class should have the following methods:

  • findMax: Accepts a list of elements of type T or its subtypes and returns the maximum element in the list based on their natural ordering.

  • findMin: Accepts a list of elements of type T or its subtypes and returns the minimum element in the list based on their natural ordering.

  • computeAverage: Accepts a list of elements of type T, or its subtypes that extend Number, and returns the average value of the elements as a double.

In the main method, create a list of integers and a list of doubles. Use the DataProcessor class to find and print the maximum and minimum values in each list. Also, compute and print the average of the values in each list.

use wildcards and bounded parameters to achieve the subtypes task

Sample Input

DataProcessor<Integer> integerProcessor = new DataProcessor<>();
List<Integer> integerList = List.of(10, 20, 5, 15);
Integer maxInteger = integerProcessor.findMax(integerList);
Integer minInteger = integerProcessor.findMin(integerList);
double avgInteger = integerProcessor.computeAverage(integerList);

Sample Output

Max integer: 20
Min integer: 5
Average integer: 12.5

Exercise 3

You are tasked with building an Online Shopping System that allows users to manage their shopping carts and make purchases. The system should support adding products to the shopping cart, removing products from the cart, calculating the total price, and printing the items in the cart. Additionally, the system should provide functionality to print receipts for the purchased items.

Requirements:

The system should have a generic interface Product that represents a product. The Product interface should have the following methods:

getName(): Returns the name of the product as a string. getPrice(): Returns the price of the product as a double.

The system should have a generic class ShoppingCart< T extends Product> that represents a shopping cart. The ShoppingCart class should have the following methods:

addItem(T item): Adds a product to the shopping cart. removeItem(T item): Removes a product from the shopping cart. calculateTotalPrice(): Calculates and returns the total price of all items in the shopping cart as a double. printItems(Printer<? super T> printer): Prints the items in the shopping cart using the provided Printer object.

The system should have a class ConsolePrinter< T> that implements the Printer interface. The ConsolePrinter class should have the following method:

print(T item): Prints the provided item to the console.

The system should have a class ReceiptPrinter< T extends Product> that implements the Printer interface. The ReceiptPrinter class should have the following method:

print(T item): Prints the name and price of the provided product as a receipt.

The ShoppingCart class should have an internal data structure, such as a list or a map, to store the products.

The ShoppingCart class should enforce the following constraints:

A product cannot be added to the shopping cart if it already exists in the cart. A product cannot be removed from the shopping cart if it does not exist in the cart. The system should have a class Main that serves as an entry point for the application.

The Main class should provide a simple command-line interface to interact with the online shopping system. The user should be able to choose different operations, such as adding a product to the cart, removing a product from the cart, calculating the total price, and printing the items in the cart.

The main class is given below along with the expected output, implement other classes and methods to achieve the given output

interface Product {
    String getName();
    double getPrice();
}

interface Printer<T> {
    void print(T item);
}

class ShoppingCart<T extends Product> {
    //complete all methods
}
class Grocery implements Product {
    //complete
}
class Electronics implements Product {
    //complete
}
class ConsolePrinter<T> implements Printer<T> {
    //complete
}
class ReceiptPrinter<T extends Product> implements Printer<T>{
    //complete
}
public class Main {
    public static void main(String[] args) {
        ShoppingCart<Grocery> groceryCart = new ShoppingCart<>();
        ShoppingCart<Electronics> electronicsCart = new ShoppingCart<>();
        Grocery apple = new Grocery("Apple", 0.5);
        Grocery milk = new Grocery("Milk", 1.99);
        Electronics laptop = new Electronics("Laptop", 999.99);
        Electronics headphones = new Electronics("Headphones", 49.99);
        groceryCart.addItem(apple);
        groceryCart.addItem(milk);
        electronicsCart.addItem(laptop);
        electronicsCart.addItem(headphones);
        ConsolePrinter<Product> consolePrinter = new ConsolePrinter<>();
        ReceiptPrinter<Product> receiptPrinter = new ReceiptPrinter<>();
        System.out.println("Grocery Cart Items:");
        groceryCart.printItems(consolePrinter);
        System.out.println("Total Price: $" + groceryCart.calculateTotalPrice());
        System.out.println();
        System.out.println("Electronics Cart Items:");
        electronicsCart.printItems(receiptPrinter);
        System.out.println("Total Price: $" + electronicsCart.calculateTotalPrice());
    }
}

Sample Output

Grocery Cart Items:
Product: Apple, Price: $0.5
Product: Milk, Price: $1.99
Total Price: $2.49

Electronics Cart Items:
Product: Laptop, Price: $999.99
Product: Headphones, Price: $49.99
Total Price: $1049.98

Last updated