WL
Java Full Stack Developer
Wassim Lagnaoui

Lesson 05: Collections & Generics

Work with groups of data using Lists, Sets, and Maps, iterate safely with for-each, and add type safety with generics.

Introduction

So far you've worked with single objects, but real programs need to handle groups of data. Collections are Java's way of storing multiple items together, like a shopping cart holding multiple products or a contact list storing many phone numbers. Java provides three main types: Lists (ordered collections that allow duplicates), Sets (collections that ensure uniqueness), and Maps (key-value pairs for quick lookups). Generics add type safety by letting you specify what type of objects a collection can hold, preventing errors and eliminating the need for casting. The for-each loop makes it easy to process every item in a collection without worrying about indexes or boundaries. These tools are essential for building real applications that manage multiple pieces of data efficiently.

Lists

Definition

A List is an ordered collection that maintains the sequence of elements as you add them and allows duplicate values. You can access elements by their position (index) and the list will remember the exact order you put things in. Lists are perfect when the order matters or when you need to allow the same item to appear multiple times.

Analogy

Think of a List like a shopping list you write on paper. You jot down items in the order you think of them: "milk, bread, eggs, milk" (yes, you might write milk twice if you really need it). Each item has a position - milk is first, bread is second, and so on. You can look at the third item on your list, add new items to the end, or even insert something in the middle. The list keeps everything in the exact order you wrote it, and if you write the same item twice, it appears twice on the list.

Example

import java.util.ArrayList;
import java.util.List;

// Creating a shopping list using ArrayList
public class ShoppingListDemo {
  public static void main(String[] args) {
    // Create a list to store shopping items
    List<String> shoppingList = new ArrayList<>(); // ordered collection, allows duplicates

    // Add items to the list (they maintain order)
    shoppingList.add("milk");         // index 0 - first item
    shoppingList.add("bread");        // index 1 - second item
    shoppingList.add("eggs");         // index 2 - third item
    shoppingList.add("milk");         // index 3 - fourth item (duplicate allowed)

    // Access items by their position
    String firstItem = shoppingList.get(0);    // gets "milk"
    String lastItem = shoppingList.get(shoppingList.size() - 1); // gets "milk"

    // Check what's in the list
    System.out.println("Shopping list has " + shoppingList.size() + " items");
    System.out.println("First item: " + firstItem);

    // Add item at specific position
    shoppingList.add(1, "butter");     // insert "butter" at index 1

    // Loop through all items in order
    System.out.println("Complete shopping list:");
    for (String item : shoppingList) { // for-each loop visits each element
      System.out.println("- " + item); // print each item with a bullet point
    }

    // Check if list contains an item
    if (shoppingList.contains("eggs")) {        // search for "eggs"
      System.out.println("Don't forget to buy eggs!");
    }
  }
}

Sets

Definition

A Set is a collection that automatically ensures all elements are unique - it won't allow duplicates. When you try to add an item that's already in the set, it simply ignores the duplicate. Sets are perfect when you need to track unique items, like a list of unique website visitors or a collection of distinct product categories.

Analogy

A Set is like your collection of shoes in your closet. You might have many pairs, but you never keep two identical pairs of the exact same shoe. If someone gives you a pair of sneakers identical to one you already own, you don't put both in your closet - you keep just one. Your shoe collection naturally contains only unique items. When friends ask what shoes you have, you can quickly tell them if you own a specific type, but you can't point to "the first shoe" or "the third shoe" because they're not arranged in any particular order - they're just your unique collection.

Example

import java.util.HashSet;
import java.util.Set;

// Managing unique website visitors
public class VisitorTrackingDemo {
  public static void main(String[] args) {
    // Create a set to track unique visitors
    Set<String> uniqueVisitors = new HashSet<>(); // only unique elements

    // Add visitor IDs (simulating website visits)
    uniqueVisitors.add("user123");     // first visit - added
    uniqueVisitors.add("user456");     // new visitor - added
    uniqueVisitors.add("user789");     // another new visitor - added
    uniqueVisitors.add("user123");     // return visitor - ignored (duplicate)
    uniqueVisitors.add("user456");     // return visitor - ignored (duplicate)

    // Check how many unique visitors we have
    System.out.println("Unique visitors today: " + uniqueVisitors.size()); // prints 3

    // Check if a specific user visited
    if (uniqueVisitors.contains("user123")) {    // fast lookup
      System.out.println("User123 visited today");
    }

    // Loop through all unique visitors
    System.out.println("List of unique visitors:");
    for (String visitor : uniqueVisitors) {     // for-each over unique elements
      System.out.println("- " + visitor);       // print each unique visitor ID
    }

    // Try to add a duplicate and see what happens
    boolean wasAdded = uniqueVisitors.add("user123"); // try to add existing user
    System.out.println("Was user123 added again? " + wasAdded); // prints false

    // Remove a visitor
    uniqueVisitors.remove("user789");           // remove specific visitor
    System.out.println("Visitors after removal: " + uniqueVisitors.size());
  }
}

Maps

Definition

A Map stores pairs of keys and values, where each key is unique and maps to exactly one value. It's like a lookup table where you can quickly find a value by providing its key. Maps are perfect for associations like username to email address, product ID to price, or country to capital city.

Analogy

A Map is exactly like a phone book or dictionary. In a phone book, you look up a person's name (the key) to find their phone number (the value). Each person's name appears only once, but you can quickly flip to any name and get their number. In a dictionary, you look up a word (key) to find its definition (value). The phone book doesn't store names in the order you added them - it organizes them for fast lookup. When you need to find someone's number, you don't flip through every page; you jump directly to their name section and find them instantly.

Example

import java.util.HashMap;
import java.util.Map;

// Product catalog with prices
public class ProductCatalogDemo {
  public static void main(String[] args) {
    // Create a map to store product prices
    Map<String, Double> productPrices = new HashMap<>(); // key -> value pairs

    // Add products and their prices
    productPrices.put("laptop", 899.99);        // key: "laptop", value: 899.99
    productPrices.put("mouse", 25.50);          // key: "mouse", value: 25.50
    productPrices.put("keyboard", 75.00);       // key: "keyboard", value: 75.00
    productPrices.put("monitor", 299.99);       // key: "monitor", value: 299.99

    // Look up a product's price
    Double laptopPrice = productPrices.get("laptop");  // fast lookup by key
    System.out.println("Laptop costs: $" + laptopPrice);

    // Check if a product exists
    if (productPrices.containsKey("tablet")) {         // check if key exists
      System.out.println("We sell tablets");
    } else {
      System.out.println("Tablets not in catalog");
    }

    // Update a price (overwrite existing value)
    productPrices.put("mouse", 22.99);                 // update mouse price
    System.out.println("Updated mouse price: $" + productPrices.get("mouse"));

    // Loop through all products and prices
    System.out.println("Complete product catalog:");
    for (Map.Entry<String, Double> entry : productPrices.entrySet()) {
      String product = entry.getKey();          // get the product name (key)
      Double price = entry.getValue();          // get the price (value)
      System.out.println(product + ": $" + price);  // print product and price
    }

    // Get all product names
    System.out.println("Available products:");
    for (String product : productPrices.keySet()) {    // loop through keys only
      System.out.println("- " + product);
    }

    // Calculate total value of inventory
    double totalValue = 0;
    for (Double price : productPrices.values()) {      // loop through values only
      totalValue += price;                             // sum all prices
    }
    System.out.println("Total catalog value: $" + totalValue);
  }
}

Generics

Definition

Generics allow you to specify what type of objects a collection can hold by using angle brackets with the type name, like List<String> or Map<String, Integer>. This provides compile-time type safety, meaning the compiler will catch type mismatches before your program runs. Generics eliminate the need for casting and make your code more readable and reliable.

Analogy

Generics are like labeled storage containers in a warehouse. Instead of having generic boxes where you might accidentally put books in a box meant for tools, you have clearly labeled containers: "Books Only," "Tools Only," "Electronics Only." When you label a container upfront, everyone knows what belongs inside, and the warehouse manager (compiler) can catch mistakes before items get stored in the wrong place. You never have to guess what's in a container or worry about finding a wrench when you expected a book - the label guarantees the contents.

Example

import java.util.*;

// Demonstrating type safety with generics
public class GenericsDemo {
  public static void main(String[] args) {
    // WITHOUT generics (old way - not recommended)
    List oldList = new ArrayList();          // no type specified
    oldList.add("Hello");                    // can add any type
    oldList.add(42);                         // mixing types - dangerous!
    // String text = (String) oldList.get(1); // runtime error! 42 is not a String

    // WITH generics (modern way - recommended)
    List<String> names = new ArrayList<>();   // only Strings allowed
    names.add("Alice");                      // String - allowed
    names.add("Bob");                        // String - allowed
    // names.add(42);                        // compile error! 42 is not a String

    // Safe retrieval - no casting needed
    String firstName = names.get(0);         // guaranteed to be a String
    System.out.println("First name: " + firstName);

    // Generic Maps - key and value types specified
    Map<String, Integer> ageByName = new HashMap<>(); // String keys, Integer values
    ageByName.put("Alice", 25);              // String key, Integer value - OK
    ageByName.put("Bob", 30);                // String key, Integer value - OK
    // ageByName.put(42, "Alice");           // compile error! wrong types

    // Safe retrieval from map
    Integer aliceAge = ageByName.get("Alice"); // guaranteed Integer, no cast needed
    System.out.println("Alice is " + aliceAge + " years old");

    // Generic Sets
    Set<String> uniqueCities = new HashSet<>(); // only Strings
    uniqueCities.add("New York");
    uniqueCities.add("London");
    uniqueCities.add("Tokyo");
    // uniqueCities.add(123);                 // compile error!

    // Type-safe iteration
    for (String city : uniqueCities) {       // no casting needed
      System.out.println("City: " + city);   // city is guaranteed to be String
    }

    // Generics catch errors at compile time
    processStringList(names);                // works - names is List<String>
    // processStringList(oldList);           // compile error - oldList is raw type
  }

  // Method that requires a specific generic type
  public static void processStringList(List<String> stringList) {
    for (String str : stringList) {          // safe - all elements are Strings
      System.out.println("Processing: " + str.toUpperCase());
    }
  }
}

For-each Iteration

Definition

The for-each loop (also called enhanced for loop) provides a simple way to iterate through all elements in a collection without dealing with indexes or iterator objects. It automatically handles the details of traversing the collection and gives you each element one by one. Use it when you need to process every element but don't need to know the position or modify the collection during iteration.

Analogy

For-each iteration is like having items delivered to you one at a time from a conveyor belt. You don't need to worry about reaching for the next item, counting positions, or figuring out when the belt is empty. The belt automatically brings each item to you, and when you're done processing one item, the next one appears. You focus on handling each item as it arrives, and the system takes care of the delivery mechanism. It's much simpler than walking along the belt yourself and keeping track of where you are.

Example

import java.util.*;

// Different ways to iterate through collections
public class IterationDemo {
  public static void main(String[] args) {
    // Create sample collections
    List<String> fruits = Arrays.asList("apple", "banana", "orange");
    Set<String> colors = Set.of("red", "green", "blue");
    Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87, "Carol", 92);

    // For-each with Lists
    System.out.println("Fruits in our basket:");
    for (String fruit : fruits) {           // for-each automatically gets next element
      System.out.println("- " + fruit);     // process current fruit
    }

    // For-each with Sets
    System.out.println("Available colors:");
    for (String color : colors) {           // visits each unique color
      System.out.println("- " + color);     // process current color
    }

    // For-each with Map entries (key-value pairs)
    System.out.println("Student scores:");
    for (Map.Entry<String, Integer> entry : scores.entrySet()) {
      String student = entry.getKey();      // get student name
      Integer score = entry.getValue();     // get their score
      System.out.println(student + ": " + score + "%");
    }

    // For-each with just Map keys
    System.out.println("All students:");
    for (String student : scores.keySet()) { // iterate through keys only
      System.out.println("- " + student);
    }

    // For-each with just Map values
    System.out.println("All scores:");
    for (Integer score : scores.values()) {  // iterate through values only
      System.out.println("- " + score + "%");
    }

    // Compare with traditional for loop (more complex)
    System.out.println("Fruits using traditional for loop:");
    for (int i = 0; i < fruits.size(); i++) {  // manual index management
      String fruit = fruits.get(i);           // manual element access
      System.out.println("- " + fruit);       // same result, more code
    }

    // For-each is cleaner and less error-prone
    // No index out of bounds errors
    // No need to track collection size
    // Works with any collection type
  }
}

Summary

Collections are essential tools for managing groups of data in Java applications. Lists maintain order and allow duplicates, perfect for sequences like shopping carts or task lists. Sets ensure uniqueness, ideal for tracking distinct items like user IDs or categories. Maps provide fast key-value lookups, excellent for associations like user profiles or product catalogs. Generics add type safety, preventing errors and making code more readable by specifying exactly what types your collections hold. The for-each loop simplifies iteration, letting you process each element without worrying about indexes or boundaries. Together, these concepts form the foundation for handling multiple pieces of data efficiently and safely in your programs.