Index

Iterating Over Collections

Java Collections

3.1 Iterator and ListIterator

When working with Java Collections, one of the most fundamental tasks is traversing through the elements. The Iterator interface is a core tool designed specifically to enable safe and efficient traversal of collections, while also allowing modification during iteration.

What is an Iterator?

An Iterator provides a standardized way to access elements sequentially without exposing the underlying structure of the collection. It supports three main operations:

Using an iterator avoids potential issues like ConcurrentModificationException that can occur if you modify a collection while iterating over it using a traditional for-loop.

Example: Using Iterator with a List

import java.util.*;

public class IteratorExample {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry", "Date"));

        Iterator<String> iterator = fruits.iterator();
        while (iterator.hasNext()) {
            String fruit = iterator.next();
            System.out.println(fruit);
            if ("Banana".equals(fruit)) {
                iterator.remove(); // Removes "Banana" safely during iteration
            }
        }

        System.out.println("After removal: " + fruits);
    }
}

Output:

Apple
Banana
Cherry
Date
After removal: [Apple, Cherry, Date]

Here, the remove() method of the iterator safely removes the element "Banana" during iteration without causing errors.

Introducing ListIterator: Bidirectional Traversal for Lists

While Iterator only supports forward traversal, the ListIterator interface extends it to provide bidirectional traversal and additional functionality, but it works only with List implementations like ArrayList or LinkedList.

Key features of ListIterator:

Example: Using ListIterator

import java.util.*;

public class ListIteratorExample {
    public static void main(String[] args) {
        // Initialize the list
        List<String> list = new LinkedList<>(Arrays.asList("Red", "Green", "Blue"));
        
        // Create ListIterator
        ListIterator<String> listIter = list.listIterator();

        // Forward iteration
        System.out.println("Forward iteration:");
        while (listIter.hasNext()) {
            String color = listIter.next();
            System.out.println("Next: " + color);
        }

        // Backward iteration
        System.out.println("\nBackward iteration:");
        while (listIter.hasPrevious()) {
            String color = listIter.previous();
            System.out.println("Previous: " + color);
        }

        // Modify element during iteration
        System.out.println("\nModifying elements:");
        while (listIter.hasNext()) {
            String color = listIter.next();
            if ("Green".equals(color)) {
                listIter.set("Yellow"); // Replaces "Green" with "Yellow"
                System.out.println("Replaced 'Green' with 'Yellow'");
            }
        }

        // Final list
        System.out.println("\nModified list: " + list);
    }
}

Expected Output

Forward iteration:
Next: Red
Next: Green
Next: Blue

Backward iteration:
Previous: Blue
Previous: Green
Previous: Red

Modifying elements:
Replaced 'Green' with 'Yellow'

Modified list: [Red, Yellow, Blue]

Practical Use Cases and Best Practices

By mastering Iterator and ListIterator, you gain powerful, safe tools to navigate and manipulate collections efficiently in your Java programs.

Index

3.2 For-Loop

The enhanced for-loop (also known as the for-each loop) is a convenient and concise way to iterate over elements in a collection or array. Introduced in Java 5, it simplifies iteration by hiding the complexity of the underlying iterator.

Syntax

for (ElementType element : collection) {
    // Use element
}

This syntax automatically retrieves each element from the collection one by one, making the code easier to read and write.

Example: Iterating Over a List

List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");

for (String fruit : fruits) {
    System.out.println(fruit);
}

Output:

Apple
Banana
Cherry

You can also use the enhanced for-loop with other collections like Set or arrays:

Set<Integer> numbers = new HashSet<>(Arrays.asList(10, 20, 30));

for (Integer number : numbers) {
    System.out.println(number);
}

int[] scores = {90, 85, 78};

for (int score : scores) {
    System.out.println(score);
}

Limitations of Enhanced For-Loop

While the enhanced for-loop is great for simple traversal, it has some limitations compared to using an explicit Iterator:

For example, this code will fail if you try to remove elements inside the for-each loop:

for (String fruit : fruits) {
    if ("Banana".equals(fruit)) {
        fruits.remove(fruit); // Throws ConcurrentModificationException
    }
}

Summary

The enhanced for-loop is ideal for simple, read-only iteration over collections and arrays. Its clean syntax reduces boilerplate and improves readability. However, for tasks requiring modification of collections during iteration or more control over traversal, the explicit use of Iterator or traditional loops is necessary.

Understanding when and how to use the enhanced for-loop effectively helps you write clearer and safer Java code when working with collections.

Index

3.3 API Basics (intro)

Java’s Stream API, introduced in Java 8, provides a modern, functional-style way to process collections. Unlike traditional iteration using loops or iterators, streams enable you to express complex data-processing queries clearly and concisely, often with better readability and parallelism support.

What is a Stream?

A stream is a sequence of elements supporting functional-style operations such as filtering, mapping, and reducing. Streams are not data structures themselves β€” instead, they operate on data sources like collections, arrays, or I/O channels, producing a pipeline of computations.

Key characteristics of streams:

Core Concepts

Simple Example: Filtering and Mapping

Suppose you have a list of fruits, and you want to get the names of all fruits starting with the letter β€œA” in uppercase.

import java.util.*;
import java.util.stream.*;

public class StreamIntro {
    public static void main(String[] args) {
        List<String> fruits = Arrays.asList("Apple", "Banana", "Avocado", "Cherry", "Apricot");

        List<String> filtered = fruits.stream()          // Create stream from list
            .filter(f -> f.startsWith("A"))              // Keep only fruits starting with 'A'
            .map(String::toUpperCase)                     // Convert to uppercase
            .collect(Collectors.toList());                // Collect results into a new list

        System.out.println(filtered); // Output: [APPLE, AVOCADO, APRICOT]
    }
}

Here’s what happens step-by-step:

  1. .stream() converts the list into a stream.
  2. .filter(...) selects elements starting with β€œA”.
  3. .map(...) transforms each selected fruit to uppercase.
  4. .collect(...) gathers the results into a new list.

Another Example: Summing Numbers with Streams

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

int sumOfEven = numbers.stream()
    .filter(n -> n % 2 == 0)  // Keep only even numbers
    .mapToInt(Integer::intValue) // Convert to IntStream for primitive operations
    .sum();                   // Sum all remaining numbers

System.out.println("Sum of even numbers: " + sumOfEven); // Output: 6

Benefits of Using Streams

Summary

The Stream API is a powerful tool that complements the Java Collections Framework by enabling functional-style data processing. It offers a declarative way to filter, transform, and aggregate data with easy-to-read syntax. While not a replacement for all collection operations, streams are an excellent choice for expressive and efficient data manipulation in modern Java programs.

Index

3.4 Examples: Different ways to iterate and modify collections

import java.util.*;

public class IterationExamples {
    public static void main(String[] args) {
        // ===== Using Iterator with safe removal =====
        List<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry", "Date"));
        System.out.println("Original list: " + fruits);

        Iterator<String> iterator = fruits.iterator();
        while (iterator.hasNext()) {
            String fruit = iterator.next();
            if ("Banana".equals(fruit)) {
                iterator.remove(); // Safe removal during iteration
            }
        }
        System.out.println("After Iterator removal of 'Banana': " + fruits);
        // Output: [Apple, Cherry, Date]

        // ===== Using ListIterator for bidirectional traversal and modification =====
        ListIterator<String> listIterator = fruits.listIterator();
        System.out.println("\nForward iteration:");
        while (listIterator.hasNext()) {
            System.out.println(listIterator.next());
        }
        System.out.println("Backward iteration:");
        while (listIterator.hasPrevious()) {
            System.out.println(listIterator.previous());
        }
        // Modify an element safely
        while (listIterator.hasNext()) {
            String fruit = listIterator.next();
            if ("Date".equals(fruit)) {
                listIterator.set("Dragonfruit"); // Replace element safely
            }
        }
        System.out.println("After modification with ListIterator: " + fruits);
        // Output: [Apple, Cherry, Dragonfruit]

        // ===== Using enhanced for-loop (for-each) =====
        System.out.println("\nEnhanced for-loop iteration (read-only):");
        for (String fruit : fruits) {
            System.out.println(fruit);
            // fruits.remove(fruit); // Uncommenting this line causes ConcurrentModificationException!
        }

        // ===== Using Stream API for iteration and filtering =====
        System.out.println("\nStream API - filter fruits starting with 'A':");
        fruits.stream()
              .filter(f -> f.startsWith("A"))
              .forEach(System.out::println);
        // Output: Apple

        // Collect filtered results into a new list
        List<String> filtered = fruits.stream()
                                      .filter(f -> f.length() > 5)
                                      .toList();
        System.out.println("Fruits with length > 5: " + filtered);
        // Output: [Dragonfruit]

        // ===== Common mistake: modifying collection inside enhanced for-loop =====
        try {
            for (String fruit : fruits) {
                if ("Apple".equals(fruit)) {
                    fruits.remove(fruit); // Causes ConcurrentModificationException
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("\nCaught exception: Cannot modify collection inside enhanced for-loop!");
        }
    }
}

Explanation:

These examples demonstrate practical ways to iterate and modify collections safely while avoiding common pitfalls, helping you write robust Java code.

Index