Skip to main content
  1. Resources/
  2. Study Materials/
  3. Information & Communication Technology Engineering/
  4. ICT Semester 4/
  5. Java Programming (4343203)/

4 mins· ·
Milav Dabgar
Author
Milav Dabgar
Experienced lecturer in the electrical and electronic manufacturing industry. Skilled in Embedded Systems, Image Processing, Data Science, MATLAB, Python, STM32. Strong education professional with a Master’s degree in Communication Systems Engineering from L.D. College of Engineering - Ahmedabad.
Lecture 18: Generics and Type Safety | Java Programming (4343203)

Generics and Type Safety

Java Programming (4343203)

Lecture 18

Unit 4: Advanced Java - Type Safety
GTU Computer Engineering Semester 4

Learning Objectives

  • Understand the need for generics in Java programming
  • Create and use generic classes and interfaces
  • Implement generic methods and constructors
  • Master bounded type parameters and wildcards
  • Apply type erasure concepts and limitations
  • Utilize generic collections for type-safe programming
Focus: Writing type-safe, reusable code using Java's generics system for better compile-time error detection.

Why Do We Need Generics?

Problems Before Generics


// Pre-Java 5 code (without generics)
import java.util.*;

public class PreGenericsExample {
    public static void main(String[] args) {
        // Raw type ArrayList
        ArrayList list = new ArrayList();
        
        list.add("Hello");
        list.add("World");
        list.add(42); // Oops! Integer in String list
        
        // No compile-time type checking
        for (Object obj : list) {
            String str = (String) obj; // ClassCastException!
            System.out.println(str.toUpperCase());
        }
    }
}
                        
Problems:
  • No compile-time type checking
  • Runtime ClassCastException
  • Explicit casting required
  • Code is not self-documenting

Solution with Generics


// Java 5+ code (with generics)
import java.util.*;

public class WithGenericsExample {
    public static void main(String[] args) {
        // Generic ArrayList
        ArrayList list = new ArrayList();
        
        list.add("Hello");
        list.add("World");
        // list.add(42); // Compile-time error!
        
        // No explicit casting needed
        for (String str : list) {
            System.out.println(str.toUpperCase());
        }
        
        // Type-safe retrieval
        String first = list.get(0); // No casting
    }
}
                        
Benefits:
  • Compile-time type safety
  • No ClassCastException
  • No explicit casting
  • Self-documenting code
  • Better performance (no boxing/unboxing)

Generic Classes

Creating a Generic Class


// Generic Box class
public class Box {
    private T content;
    
    // Constructor
    public Box(T content) {
        this.content = content;
    }
    
    // Generic methods
    public T getContent() {
        return content;
    }
    
    public void setContent(T content) {
        this.content = content;
    }
    
    // Method with generic parameter
    public boolean isSameType(Box other) {
        return this.content.getClass().equals(other.content.getClass());
    }
    
    @Override
    public String toString() {
        return "Box{content=" + content + ", type=" + content.getClass().getSimpleName() + "}";
    }
}

// Usage examples
public class GenericClassDemo {
    public static void main(String[] args) {
        // Creating generic instances
        Box stringBox = new Box<>("Hello Generics!");
        Box integerBox = new Box<>(42);
        Box doubleBox = new Box<>(3.14);
        
        System.out.println("String box: " + stringBox);
        System.out.println("Integer box: " + integerBox);
        System.out.println("Double box: " + doubleBox);
        
        // Type-safe operations
        String str = stringBox.getContent(); // No casting needed
        Integer num = integerBox.getContent();
        
        // stringBox.setContent(123); // Compile-time error!
        stringBox.setContent("Updated content"); // OK
        
        // Comparing types
        Box anotherStringBox = new Box<>("Another string");
        System.out.println("Same type? " + stringBox.isSameType(anotherStringBox));
        
        // Diamond operator (Java 7+)
        Box diamondBox = new Box<>("Diamond operator");
        System.out.println("Diamond box: " + diamondBox);
    }
}
                

Multiple Type Parameters


// Generic class with multiple type parameters
public class Pair {
    private T first;
    private U second;
    
    public Pair(T first, U second) {
        this.first = first;
        this.second = second;
    }
    
    // Getters and setters
    public T getFirst() { return first; }
    public void setFirst(T first) { this.first = first; }
    
    public U getSecond() { return second; }
    public void setSecond(U second) { this.second = second; }
    
    // Utility method
    public void swap() {
        // This won't work because T and U might be different types
        // We need bounded types or careful design
    }
    
    @Override
    public String toString() {
        return "Pair{first=" + first + ", second=" + second + "}";
    }
    
    // Static generic method
    public static  Pair of(T first, U second) {
        return new Pair<>(first, second);
    }
}

// Generic Triple class
public class Triple {
    private T first;
    private U second;
    private V third;
    
    public Triple(T first, U second, V third) {
        this.first = first;
        this.second = second;
        this.third = third;
    }
    
    // Getters
    public T getFirst() { return first; }
    public U getSecond() { return second; }
    public V getThird() { return third; }
    
    @Override
    public String toString() {
        return String.format("Triple{%s, %s, %s}", first, second, third);
    }
}

// Usage demonstration
public class MultipleTypeParametersDemo {
    public static void main(String[] args) {
        // Different type combinations
        Pair nameAge = new Pair<>("Alice", 25);
        Pair idName = new Pair<>(101, "Bob");
        Pair scorePass = new Pair<>(85.5, true);
        
        System.out.println("Name-Age: " + nameAge);
        System.out.println("ID-Name: " + idName);
        System.out.println("Score-Pass: " + scorePass);
        
        // Using static factory method
        Pair coordinates = Pair.of("X", "Y");
        System.out.println("Coordinates: " + coordinates);
        
        // Triple example
        Triple studentRecord = 
            new Triple<>("Charlie", 22, 3.8);
        System.out.println("Student: " + studentRecord);
        
        // Nested generics
        Pair> complexPair = 
            new Pair<>("Student", new Pair<>(20, 3.5));
        System.out.println("Complex pair: " + complexPair);
    }
}
                

Generic Methods


public class GenericMethods {
    
    // Generic static method
    public static  void swap(T[] array, int i, int j) {
        if (i >= 0 && j >= 0 && i < array.length && j < array.length) {
            T temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }
    
    // Generic method with return type
    public static  T getMiddleElement(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[array.length / 2];
    }
    
    // Generic method with multiple type parameters
    public static  boolean areEqual(T obj1, U obj2) {
        return obj1 != null && obj1.equals(obj2);
    }
    
    // Generic method in non-generic class
    public  Box createBox(T content) {
        return new Box<>(content);
    }
    
    // Generic method with bounded type parameter
    public static  double getAverage(T[] numbers) {
        double sum = 0.0;
        for (T num : numbers) {
            sum += num.doubleValue();
        }
        return sum / numbers.length;
    }
    
    // Generic method with multiple bounds
    public static > T findMax(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i].compareTo(max) > 0) {
                max = array[i];
            }
        }
        return max;
    }
    
    // Generic method for printing arrays
    public static  void printArray(String name, T[] array) {
        System.out.print(name + ": [");
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i]);
            if (i < array.length - 1) System.out.print(", ");
        }
        System.out.println("]");
    }
    
    public static void main(String[] args) {
        // Test generic methods
        
        // String array operations
        String[] names = {"Alice", "Bob", "Charlie", "Diana"};
        printArray("Original names", names);
        
        swap(names, 0, 2);
        printArray("After swapping 0 and 2", names);
        
        String middleName = getMiddleElement(names);
        System.out.println("Middle name: " + middleName);
        
        // Integer array operations
        Integer[] numbers = {10, 20, 30, 40, 50};
        printArray("Numbers", numbers);
        
        Integer middleNumber = getMiddleElement(numbers);
        System.out.println("Middle number: " + middleNumber);
        
        double average = getAverage(numbers);
        System.out.println("Average: " + average);
        
        Integer maxNumber = findMax(numbers);
        System.out.println("Max number: " + maxNumber);
        
        // Double array
        Double[] decimals = {3.14, 2.71, 1.41, 1.73};
        Double maxDecimal = findMax(decimals);
        System.out.println("Max decimal: " + maxDecimal);
        
        // Equality testing
        System.out.println("Are 'Hello' and 'Hello' equal? " + areEqual("Hello", "Hello"));
        System.out.println("Are 42 and 42.0 equal? " + areEqual(42, 42.0));
        
        // Creating boxes using generic method
        GenericMethods gm = new GenericMethods();
        Box stringBox = gm.createBox("Generic Method Created");
        Box intBox = gm.createBox(100);
        
        System.out.println("Created boxes:");
        System.out.println("  " + stringBox);
        System.out.println("  " + intBox);
    }
}
                

Bounded Type Parameters

Upper Bounded Types


// Upper bounded with extends
class NumberBox {
    private T value;
    
    public NumberBox(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
    
    // Can call Number methods
    public double getDoubleValue() {
        return value.doubleValue();
    }
    
    // Can call Number methods
    public int getIntValue() {
        return value.intValue();
    }
    
    // Comparison method
    public boolean isGreaterThan(NumberBox other) {
        return this.value.doubleValue() > 
               other.value.doubleValue();
    }
}

// Usage
public class BoundedDemo {
    public static void main(String[] args) {
        NumberBox intBox = 
            new NumberBox<>(42);
        NumberBox doubleBox = 
            new NumberBox<>(3.14);
        
        System.out.println("Int value: " + 
                          intBox.getDoubleValue());
        System.out.println("Double value: " + 
                          doubleBox.getIntValue());
        
        // NumberBox stringBox = 
        //     new NumberBox<>("Hello"); // Error!
    }
}
                        

Multiple Bounds


// Interface for comparison
interface Printable {
    void print();
}

// Class implementing the interface
class PrintableNumber extends Number 
        implements Printable, Comparable {
    private double value;
    
    public PrintableNumber(double value) {
        this.value = value;
    }
    
    @Override
    public void print() {
        System.out.println("Value: " + value);
    }
    
    @Override
    public int compareTo(PrintableNumber other) {
        return Double.compare(this.value, other.value);
    }
    
    @Override
    public int intValue() { return (int) value; }
    
    @Override
    public long longValue() { return (long) value; }
    
    @Override
    public float floatValue() { return (float) value; }
    
    @Override
    public double doubleValue() { return value; }
    
    @Override
    public String toString() { 
        return String.valueOf(value); 
    }
}

// Generic class with multiple bounds
class AdvancedBox> {
    private T content;
    
    public AdvancedBox(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
    
    public void printContent() {
        content.print(); // From Printable
    }
    
    public double getNumericValue() {
        return content.doubleValue(); // From Number
    }
    
    public boolean isGreaterThan(AdvancedBox other) {
        return content.compareTo(other.content) > 0; // From Comparable
    }
}
                        

Wildcards in Generics

Unbounded Wildcards


import java.util.*;

public class WildcardsDemo {
    
    // Unbounded wildcard - can accept any type
    public static void printList(List list) {
        for (Object item : list) {
            System.out.print(item + " ");
        }
        System.out.println();
    }
    
    // Upper bounded wildcard - accepts Number and its subtypes
    public static double sumNumbers(List numbers) {
        double sum = 0.0;
        for (Number num : numbers) {
            sum += num.doubleValue();
        }
        return sum;
    }
    
    // Lower bounded wildcard - accepts Number and its supertypes
    public static void addNumbers(List numbers) {
        numbers.add(42);
        numbers.add(3.14);
        numbers.add(100L);
        // numbers.add("Hello"); // Compile error - String is not Number or supertype
    }
    
    // Method demonstrating PECS principle
    // Producer Extends, Consumer Super
    public static  void copy(List source, List destination) {
        for (T item : source) {
            destination.add(item);
        }
    }
    
    public static void main(String[] args) {
        System.out.println("=== Wildcards Demonstration ===");
        
        // Different types of lists
        List stringList = Arrays.asList("Hello", "World", "Generics");
        List intList = Arrays.asList(1, 2, 3, 4, 5);
        List doubleList = Arrays.asList(1.1, 2.2, 3.3);
        
        // Unbounded wildcard usage
        System.out.println("Printing different types of lists:");
        printList(stringList);
        printList(intList);
        printList(doubleList);
        
        // Upper bounded wildcard
        System.out.println("Sum of integers: " + sumNumbers(intList));
        System.out.println("Sum of doubles: " + sumNumbers(doubleList));
        // System.out.println("Sum of strings: " + sumNumbers(stringList)); // Error!
        
        // Lower bounded wildcard
        List numberList = new ArrayList<>();
        List objectList = new ArrayList<>();
        
        addNumbers(numberList);
        addNumbers(objectList);
        // addNumbers(intList); // Error! Integer is not supertype of Number
        
        System.out.println("Number list after adding: " + numberList);
        System.out.println("Object list after adding: " + objectList);
        
        // PECS principle demonstration
        List source = Arrays.asList(10, 20, 30);
        List destination = new ArrayList<>();
        
        copy(source, destination);
        System.out.println("Copied from Integer list to Number list: " + destination);
        
        // Wildcard capture demonstration
        demonstrateWildcardCapture();
    }
    
    // Wildcard capture helper
    private static void demonstrateWildcardCapture() {
        System.out.println("\n--- Wildcard Capture ---");
        
        List stringList = Arrays.asList("A", "B", "C");
        swapElements(stringList, 0, 2);
        System.out.println("After swap: " + stringList);
        
        List intList = new ArrayList<>(Arrays.asList(1, 2, 3));
        swapElements(intList, 0, 2);
        System.out.println("After swap: " + intList);
    }
    
    // Method using wildcard capture
    public static void swapElements(List list, int i, int j) {
        swapElementsHelper(list, i, j);
    }
    
    // Helper method that captures the wildcard type
    private static  void swapElementsHelper(List list, int i, int j) {
        T temp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, temp);
    }
}
                

Type Erasure and Limitations

What is Type Erasure?

  • Generics are compile-time feature only
  • Type information removed at runtime
  • Ensures backward compatibility
  • Raw types exist at runtime

// Before compilation
List stringList = new ArrayList();
List intList = new ArrayList();

// After type erasure (at runtime)
List stringList = new ArrayList();
List intList = new ArrayList();

// This is why this doesn't work:
public class ErasureExample {
    // public void method(List list) { }
    // public void method(List list) { } 
    // Compile error: same erasure!
    
    // Workaround using bounded types
    public void method(List list, String dummy) { }
    public void method(List list, Integer dummy) { }
}
                        

Limitations Due to Type Erasure


public class GenericLimitations {
    
    // 1. Cannot create instance of type parameter
    // T instance = new T(); // Error!
    
    // 2. Cannot create array of generic type
    // T[] array = new T[10]; // Error!
    
    // 3. Cannot use primitive types
    // List intList; // Error! Use List
    
    // 4. Cannot use instanceof with parameterized types
    public boolean checkType(Object obj) {
        // return obj instanceof List; // Error!
        return obj instanceof List; // OK, but loses type info
    }
    
    // 5. Cannot create generic exception classes
    // class MyException extends Exception { } // Error!
    
    // 6. Cannot have static fields of type T
    // static T staticField; // Error!
    
    // Workarounds
    private Class clazz;
    
    public GenericLimitations(Class clazz) {
        this.clazz = clazz;
    }
    
    // Workaround for creating instances
    public T createInstance() throws InstantiationException, 
                                   IllegalAccessException {
        return clazz.newInstance();
    }
    
    // Workaround for arrays
    @SuppressWarnings("unchecked")
    public T[] createArray(int size) {
        return (T[]) java.lang.reflect.Array.newInstance(clazz, size);
    }
}

// Bridge methods example
class Parent {
    public T getValue() { return null; }
    public void setValue(T value) { }
}

class Child extends Parent {
    @Override
    public String getValue() { return "child"; }
    
    @Override
    public void setValue(String value) { }
    
    // Compiler generates bridge methods:
    // public Object getValue() { return getValue(); }
    // public void setValue(Object value) { setValue((String) value); }
}
                        

Generic Collections Best Practices


import java.util.*;

public class GenericCollectionsBestPractices {
    
    public static void main(String[] args) {
        demonstrateListGenerics();
        demonstrateMapGenerics();
        demonstrateSetGenerics();
        demonstrateCollectionUtilities();
    }
    
    private static void demonstrateListGenerics() {
        System.out.println("=== List Generics ===");
        
        // Prefer interface types
        List list = new ArrayList<>(); // Diamond operator
        
        // Type-safe operations
        list.add("Java");
        list.add("Generics");
        list.add("Collections");
        
        // No casting needed
        for (String item : list) {
            System.out.println(item.toUpperCase());
        }
        
        // Generic methods with lists
        Collections.sort(list);
        String first = Collections.min(list);
        System.out.println("First alphabetically: " + first);
    }
    
    private static void demonstrateMapGenerics() {
        System.out.println("\n=== Map Generics ===");
        
        Map> studentGrades = new HashMap<>();
        
        // Adding data
        studentGrades.put("Alice", Arrays.asList(85, 90, 88));
        studentGrades.put("Bob", Arrays.asList(78, 82, 85));
        studentGrades.put("Charlie", Arrays.asList(92, 95, 90));
        
        // Type-safe iteration
        for (Map.Entry> entry : studentGrades.entrySet()) {
            String student = entry.getKey();
            List grades = entry.getValue();
            
            double average = grades.stream().mapToInt(Integer::intValue).average().orElse(0.0);
            System.out.printf("%s: %s (avg: %.2f)%n", student, grades, average);
        }
        
        // Generic method calls
        Set students = studentGrades.keySet();
        Collection> allGrades = studentGrades.values();
        
        System.out.println("Students: " + students);
        System.out.println("Total grade lists: " + allGrades.size());
    }
    
    private static void demonstrateSetGenerics() {
        System.out.println("\n=== Set Generics ===");
        
        Set hashSet = new HashSet<>();
        Set treeSet = new TreeSet<>();
        
        String[] words = {"banana", "apple", "cherry", "date", "apple"};
        
        Collections.addAll(hashSet, words);
        Collections.addAll(treeSet, words);
        
        System.out.println("HashSet (no order): " + hashSet);
        System.out.println("TreeSet (sorted): " + treeSet);
        
        // Set operations
        Set vowelWords = new HashSet<>();
        vowelWords.add("apple");
        vowelWords.add("orange");
        
        Set intersection = new HashSet<>(hashSet);
        intersection.retainAll(vowelWords);
        System.out.println("Intersection: " + intersection);
    }
    
    private static void demonstrateCollectionUtilities() {
        System.out.println("\n=== Collection Utilities ===");
        
        // Generic utility methods
        List numbers = Arrays.asList(5, 2, 8, 1, 9, 3);
        
        // Sorting
        List sortedNumbers = new ArrayList<>(numbers);
        Collections.sort(sortedNumbers);
        System.out.println("Sorted: " + sortedNumbers);
        
        // Reverse sorting
        Collections.sort(sortedNumbers, Collections.reverseOrder());
        System.out.println("Reverse sorted: " + sortedNumbers);
        
        // Binary search
        Collections.sort(sortedNumbers);
        int index = Collections.binarySearch(sortedNumbers, 5);
        System.out.println("Index of 5: " + index);
        
        // Custom comparator with generics
        List names = Arrays.asList("Alice", "Bob", "Charlie", "David");
        Collections.sort(names, Comparator.comparing(String::length));
        System.out.println("Sorted by length: " + names);
        
        // Frequency counting
        List letters = Arrays.asList("a", "b", "a", "c", "b", "a");
        int frequency = Collections.frequency(letters, "a");
        System.out.println("Frequency of 'a': " + frequency);
        
        // Type-safe empty collections
        List emptyList = Collections.emptyList();
        Set emptySet = Collections.emptySet();
        Map emptyMap = Collections.emptyMap();
        
        // Immutable collections (Java 9+) or using Collections.unmodifiableXxx
        List immutableList = Collections.unmodifiableList(names);
        System.out.println("Immutable list: " + immutableList);
    }
}
                

GTU Previous Year Question (Summer 2022)

Q: Write a Java program to demonstrate the use of generics. Create a generic class 'Container' that can store any type of object. Implement methods for adding, removing, and retrieving objects. Also demonstrate bounded type parameters with an example.

Solution:


import java.util.*;

// Generic Container class
class Container {
    private List items;
    private int maxSize;
    
    // Constructor
    public Container(int maxSize) {
        this.maxSize = maxSize;
        this.items = new ArrayList<>();
    }
    
    // Default constructor with unlimited size
    public Container() {
        this(-1); // -1 indicates no size limit
    }
    
    // Add item to container
    public boolean add(T item) {
        if (maxSize > 0 && items.size() >= maxSize) {
            System.out.println("Container is full! Cannot add: " + item);
            return false;
        }
        items.add(item);
        System.out.println("Added: " + item);
        return true;
    }
    
    // Add item at specific index
    public boolean add(int index, T item) {
        if (maxSize > 0 && items.size() >= maxSize) {
            System.out.println("Container is full! Cannot add: " + item);
            return false;
        }
        if (index < 0 || index > items.size()) {
            System.out.println("Invalid index: " + index);
            return false;
        }
        items.add(index, item);
        System.out.println("Added at index " + index + ": " + item);
        return true;
    }
    
    // Remove item by object
    public boolean remove(T item) {
        boolean removed = items.remove(item);
        if (removed) {
            System.out.println("Removed: " + item);
        } else {
            System.out.println("Item not found: " + item);
        }
        return removed;
    }
    
    // Remove item by index
    public T remove(int index) {
        if (index < 0 || index >= items.size()) {
            System.out.println("Invalid index: " + index);
            return null;
        }
        T removedItem = items.remove(index);
        System.out.println("Removed from index " + index + ": " + removedItem);
        return removedItem;
    }
    
    // Retrieve item by index
    public T get(int index) {
        if (index < 0 || index >= items.size()) {
            System.out.println("Invalid index: " + index);
            return null;
        }
        return items.get(index);
    }
    
    // Check if container contains item
    public boolean contains(T item) {
        return items.contains(item);
    }
    
    // Get size
    public int size() {
        return items.size();
    }
    
    // Check if empty
    public boolean isEmpty() {
        return items.isEmpty();
    }
    
    // Clear all items
    public void clear() {
        items.clear();
        System.out.println("Container cleared");
    }
    
    // Get all items
    public List getAllItems() {
        return new ArrayList<>(items); // Return copy for safety
    }
    
    // Display all items
    public void display() {
        if (items.isEmpty()) {
            System.out.println("Container is empty");
        } else {
            System.out.println("Container contents: " + items);
        }
    }
    
    // Generic method to find first item matching predicate
    public T findFirst(Predicate predicate) {
        for (T item : items) {
            if (predicate.test(item)) {
                return item;
            }
        }
        return null;
    }
    
    @Override
    public String toString() {
        return "Container{size=" + items.size() + 
               ", maxSize=" + (maxSize > 0 ? maxSize : "unlimited") + 
               ", items=" + items + "}";
    }
}

// Functional interface for demonstration
@FunctionalInterface
interface Predicate {
    boolean test(T item);
}
                

Bounded Type Parameters Example:


// Bounded container for numeric types only
class NumericContainer {
    private List numbers;
    
    public NumericContainer() {
        this.numbers = new ArrayList<>();
    }
    
    // Add number
    public void add(T number) {
        numbers.add(number);
        System.out.println("Added number: " + number);
    }
    
    // Calculate sum using Number methods
    public double getSum() {
        double sum = 0.0;
        for (T number : numbers) {
            sum += number.doubleValue(); // Can call Number methods
        }
        return sum;
    }
    
    // Calculate average
    public double getAverage() {
        if (numbers.isEmpty()) return 0.0;
        return getSum() / numbers.size();
    }
    
    // Find maximum value
    public T getMax() {
        if (numbers.isEmpty()) return null;
        
        T max = numbers.get(0);
        for (T number : numbers) {
            if (number.doubleValue() > max.doubleValue()) {
                max = number;
            }
        }
        return max;
    }
    
    // Find minimum value
    public T getMin() {
        if (numbers.isEmpty()) return null;
        
        T min = numbers.get(0);
        for (T number : numbers) {
            if (number.doubleValue() < min.doubleValue()) {
                min = number;
            }
        }
        return min;
    }
    
    // Get all numbers
    public List getNumbers() {
        return new ArrayList<>(numbers);
    }
    
    // Display statistics
    public void displayStatistics() {
        System.out.println("=== Numeric Container Statistics ===");
        System.out.println("Count: " + numbers.size());
        if (!numbers.isEmpty()) {
            System.out.printf("Sum: %.2f%n", getSum());
            System.out.printf("Average: %.2f%n", getAverage());
            System.out.println("Maximum: " + getMax());
            System.out.println("Minimum: " + getMin());
        }
        System.out.println("Numbers: " + numbers);
    }
    
    @Override
    public String toString() {
        return "NumericContainer{count=" + numbers.size() + ", numbers=" + numbers + "}";
    }
}

// Comparable container for sortable types
class SortableContainer> {
    private List items;
    
    public SortableContainer() {
        this.items = new ArrayList<>();
    }
    
    // Add item
    public void add(T item) {
        items.add(item);
        System.out.println("Added to sortable container: " + item);
    }
    
    // Sort items using natural ordering
    public void sort() {
        Collections.sort(items);
        System.out.println("Container sorted");
    }
    
    // Get sorted items without modifying original
    public List getSorted() {
        List sorted = new ArrayList<>(items);
        Collections.sort(sorted);
        return sorted;
    }
    
    // Find item using binary search (requires sorted container)
    public int binarySearch(T item) {
        sort(); // Ensure sorted first
        return Collections.binarySearch(items, item);
    }
    
    // Get items
    public List getItems() {
        return new ArrayList<>(items);
    }
    
    @Override
    public String toString() {
        return "SortableContainer{items=" + items + "}";
    }
}
                

Complete Demonstration:


public class GenericContainerDemo {
    public static void main(String[] args) {
        System.out.println("=== Generic Container Demonstration ===\n");
        
        // Demonstrate basic generic container
        demonstrateBasicContainer();
        
        // Demonstrate bounded numeric container
        demonstrateNumericContainer();
        
        // Demonstrate sortable container
        demonstrateSortableContainer();
    }
    
    private static void demonstrateBasicContainer() {
        System.out.println("--- Basic Generic Container ---");
        
        // String container
        Container stringContainer = new Container<>(5); // Max 5 items
        stringContainer.add("Hello");
        stringContainer.add("Generics");
        stringContainer.add("World");
        stringContainer.add(1, "Java"); // Insert at index 1
        stringContainer.display();
        
        System.out.println("Contains 'Java': " + stringContainer.contains("Java"));
        System.out.println("Item at index 2: " + stringContainer.get(2));
        
        stringContainer.remove("World");
        stringContainer.display();
        
        // Integer container
        Container intContainer = new Container<>(); // Unlimited size
        intContainer.add(10);
        intContainer.add(20);
        intContainer.add(30);
        intContainer.add(40);
        
        System.out.println("Integer container: " + intContainer);
        
        // Find first even number
        Integer firstEven = intContainer.findFirst(num -> num % 2 == 0);
        System.out.println("First even number: " + firstEven);
        
        // Student container (custom objects)
        Container studentContainer = new Container<>();
        studentContainer.add(new Student("Alice", 85));
        studentContainer.add(new Student("Bob", 92));
        studentContainer.add(new Student("Charlie", 78));
        
        System.out.println("Student container:");
        studentContainer.display();
        
        System.out.println();
    }
    
    private static void demonstrateNumericContainer() {
        System.out.println("--- Bounded Numeric Container ---");
        
        // Integer numeric container
        NumericContainer intNumbers = new NumericContainer<>();
        intNumbers.add(10);
        intNumbers.add(25);
        intNumbers.add(15);
        intNumbers.add(30);
        intNumbers.displayStatistics();
        
        // Double numeric container
        NumericContainer doubleNumbers = new NumericContainer<>();
        doubleNumbers.add(3.14);
        doubleNumbers.add(2.71);
        doubleNumbers.add(1.41);
        doubleNumbers.displayStatistics();
        
        // Cannot create NumericContainer - compile error!
        // NumericContainer stringNumbers = new NumericContainer<>(); // Error!
        
        System.out.println();
    }
    
    private static void demonstrateSortableContainer() {
        System.out.println("--- Sortable Container ---");
        
        // String sortable container
        SortableContainer words = new SortableContainer<>();
        words.add("Zebra");
        words.add("Apple");
        words.add("Mango");
        words.add("Banana");
        
        System.out.println("Before sorting: " + words.getItems());
        System.out.println("Sorted items: " + words.getSorted());
        
        words.sort();
        System.out.println("After sorting: " + words.getItems());
        
        int index = words.binarySearch("Mango");
        System.out.println("Index of 'Mango': " + index);
        
        // Integer sortable container
        SortableContainer numbers = new SortableContainer<>();
        numbers.add(42);
        numbers.add(17);
        numbers.add(35);
        numbers.add(8);
        
        System.out.println("Numbers before sorting: " + numbers.getItems());
        numbers.sort();
        System.out.println("Numbers after sorting: " + numbers.getItems());
        
        System.out.println();
    }
}

// Helper class for demonstration
class Student {
    private String name;
    private int grade;
    
    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }
    
    public String getName() { return name; }
    public int getGrade() { return grade; }
    
    @Override
    public String toString() {
        return "Student{name='" + name + "', grade=" + grade + "}";
    }
}
                
Solution Features:
  • Complete generic Container class with type safety
  • Bounded NumericContainer for Number subtypes only
  • SortableContainer for Comparable types
  • Comprehensive CRUD operations and utility methods
  • Real-world examples with different data types
  • Demonstrates compile-time type checking benefits

🧪 Hands-on Lab Exercise

Lab 18: Generic Data Processing Pipeline

Task: Create a generic data processing pipeline that can transform, filter, and aggregate data of any type using generics, bounded parameters, and wildcards.

Requirements:

  • Create a generic Pipeline<T> class with transformation methods
  • Implement generic Transformer<T, R> and Predicate<T> interfaces
  • Use bounded type parameters for numeric aggregations
  • Implement wildcard methods for pipeline composition
  • Create a generic cache system with type-safe operations
  • Demonstrate pipeline chaining with different data types
Challenge: Implement a generic repository pattern with CRUD operations that maintains type safety across different entity types.

📚 Lecture Summary

Key Concepts Covered

  • Generic classes and interfaces
  • Generic methods and constructors
  • Bounded type parameters (extends)
  • Wildcards (?, extends, super)
  • Type erasure and limitations
  • Generic collections best practices

Best Practices

  • Use generics for type safety
  • Prefer bounded types when constraints are needed
  • Apply PECS principle for wildcards
  • Use diamond operator for cleaner code
  • Avoid raw types in new code
  • Understand type erasure limitations

🎯 Next Lecture Preview

Lecture 19: Input/Output and File Handling

  • Stream classes and I/O hierarchy
  • File and directory operations
  • Character vs byte streams
  • Serialization and deserialization

© 2025 Milav Dabgar

Powered by Hugo & Blowfish