Generics and Collections in OOPS -- Object Oriented Programming - Java


Index

  1. 1. Introduction to Generics
  2. 2. Java Collections

1. Introduction to Generics

Generics in Java allow you to write flexible, reusable code while ensuring type safety. With generics, you can define classes, interfaces, and methods where the type of data is specified at runtime, but with compile-time checks. This avoids issues like ClassCastException and removes the need for extensive casting.

For example, consider a simple generic class that holds any type of data:

public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

Here:

Box<String> stringBox = new Box<>();
stringBox.setItem("Hello, Generics!");
System.out.println(stringBox.getItem());

Box<Integer> intBox = new Box<>();
intBox.setItem(123);
System.out.println(intBox.getItem());

This ensures type safety: you can only put a String in a Box<String> and an Integer in a Box<Integer>.


2. Java Collections

Here's a link to my Github repository for more in-depth code examples: https://github.com/shasankp000/JavaCollections

The Collections Framework provides a set of classes and interfaces for working with collections of objects, such as lists, sets, and maps. Key interfaces include:

Each collection type supports different operations, and generics enhance their flexibility and safety.

Key Classes in Collections Framework

  1. ArrayList

    • Resizable array implementation of the List interface.
    • Allows duplicate elements and maintains insertion order.
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Apple"); // Allows duplicates
System.out.println(fruits); // [Apple, Banana, Apple]

  1. HashSet
Set<String> cities = new HashSet<>();
cities.add("New York");
cities.add("Los Angeles");
cities.add("New York"); // Duplicate, will not be added
System.out.println(cities); // [Los Angeles, New York]

  1. HashMap
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 35); // Overwrites the previous value for "Alice"
System.out.println(ages); // {Alice=35, Bob=25}

3. Using Generics with Collections

Generics help ensure that your collections only contain objects of a specified type. Here’s how you can use generics with the collections framework:

List<String> names = new ArrayList<>();
names.add("John");
names.add("Alice");

// Uncommenting the following line would cause a compile-time error
// names.add(123);

for (String name : names) {
    System.out.println(name);
}

Without generics, you would have to cast objects when retrieving them from a collection, leading to potential runtime errors. Generics help catch such errors at compile time.


4. Bounded Type Parameters

Sometimes, you may want to restrict the types that can be used with generics to a certain hierarchy. This is where bounded type parameters come in.

public class NumberBox<T extends Number> {  // T must be a subclass of Number
    private T number;

    public NumberBox(T number) {
        this.number = number;
    }

    public double square() {
        return number.doubleValue() * number.doubleValue();
    }
}

// Example usage:
NumberBox<Integer> intBox = new NumberBox<>(5);
System.out.println(intBox.square()); // Output: 25.0

Here, T is constrained to subclasses of Number, meaning NumberBox can be instantiated with Integer, Double, Float, etc., but not String or other types.


5. Wildcards

Wildcards in generics (?) let you create flexible methods that work with collections of different types. There are three main types of wildcards:

  1. Unbounded Wildcard (?): Allows any type.
public void printList(List<?> list) {
    for (Object element : list) {
        System.out.println(element);
    }
}
  1. Upper Bounded Wildcard (? extends Type): Restricts to Type or any subclass.
public double sumNumbers(List<? extends Number> numbers) {
    double sum = 0.0;
    for (Number num : numbers) {
        sum += num.doubleValue();
    }
    return sum;
}
  1. Lower Bounded Wildcard (? super Type): Restricts to Type or any superclass.
public void addIntegers(List<? super Integer> list) {
    list.add(1); // Can add Integer or subclasses of Integer
}

Putting It All Together

Here’s a complete example using a few different collection types with generics:

import java.util.*;

public class GenericCollectionsExample {
    public static void main(String[] args) {
        // ArrayList with generics
        List<String> names = new ArrayList<>();
        names.add("Alice");
        names.add("Bob");

        // HashSet with generics
        Set<Integer> numbers = new HashSet<>();
        numbers.add(10);
        numbers.add(20);

        // HashMap with generics
        Map<String, Double> productPrices = new HashMap<>();
        productPrices.put("Book", 12.99);
        productPrices.put("Pen", 1.49);

        // Displaying values
        printList(names);
        System.out.println("Sum of numbers: " + sumNumbers(new ArrayList<>(numbers)));
        System.out.println("Product prices: " + productPrices);
    }

    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.println(element);
        }
    }

    public static double sumNumbers(List<? extends Number> numbers) {
        double sum = 0.0;
        for (Number num : numbers) {
            sum += num.doubleValue();
        }
        return sum;
    }
}

This example: