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.