Iterator Pattern
The Iterator pattern provides a way to access elements of a collection sequentially without exposing the underlying representation of the collection. It's one of the most fundamental behavioral patterns and is deeply integrated into many modern programming languages.
Core Concept
The Iterator pattern defines a standard interface for traversing collections, allowing you to:
- Access elements one by one without knowing the internal structure
- Support multiple simultaneous traversals of the same collection
- Provide a uniform interface for different collection types
Java Implementation (Classical OOP)
In Java, the Iterator pattern is typically implemented using the Iterator interface:
import java.util.*;
// Custom collection implementing Iterable
class BookCollection implements Iterable<String> {
private List<String> books = new ArrayList<>();
public void addBook(String book) {
books.add(book);
}
@Override
public Iterator<String> iterator() {
return new BookIterator();
}
// Inner class implementing Iterator
private class BookIterator implements Iterator<String> {
private int currentIndex = 0;
@Override
public boolean hasNext() {
return currentIndex < books.size();
}
@Override
public String next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return books.get(currentIndex++);
}
}
}
// Usage example
public class IteratorExample {
public static void main(String[] args) {
BookCollection library = new BookCollection();
library.addBook("Design Patterns");
library.addBook("Clean Code");
library.addBook("Refactoring");
// Using iterator explicitly
Iterator<String> iter = library.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
// Using enhanced for-loop (syntactic sugar over iterator)
for (String book : library) {
System.out.println(book);
}
}
}
Rust Implementation
Rust takes a different approach with its iterator system, making it more functional and zero-cost through compile-time optimizations. Rust has two main iterator traits: Iterator and IntoIterator.
use std::vec::IntoIter; // Custom collection #[derive(Debug)] struct BookCollection { books: Vec<String>, } impl BookCollection { fn new() -> Self { Self { books: Vec::new() } } fn add_book(&mut self, book: String) { self.books.push(book); } } // Implementing IntoIterator for owned values impl IntoIterator for BookCollection { type Item = String; type IntoIter = IntoIter<String>; fn into_iter(self) -> Self::IntoIter { self.books.into_iter() } } // Implementing IntoIterator for references (borrowing) impl<'a> IntoIterator for &'a BookCollection { type Item = &'a String; type IntoIter = std::slice::Iter<'a, String>; fn into_iter(self) -> Self::IntoIter { self.books.iter() } } // Custom iterator implementation struct BookIterator<'a> { books: &'a [String], index: usize, } impl<'a> BookIterator<'a> { fn new(books: &'a [String]) -> Self { Self { books, index: 0 } } } impl<'a> Iterator for BookIterator<'a> { type Item = &'a String; fn next(&mut self) -> Option<Self::Item> { if self.index < self.books.len() { let book = &self.books[self.index]; self.index += 1; Some(book) } else { None } } } impl BookCollection { // Method returning custom iterator fn iter(&self) -> BookIterator<'_> { BookIterator::new(&self.books) } } fn main() { let mut library = BookCollection::new(); library.add_book("Design Patterns".to_string()); library.add_book("Clean Code".to_string()); library.add_book("Refactoring".to_string()); // Using custom iterator println!("Using custom iterator:"); for book in library.iter() { println!("{}", book); } // Using IntoIterator for references println!("\nUsing IntoIterator for references:"); for book in &library { println!("{}", book); } // Using iterator combinators (functional style) println!("\nUsing iterator combinators:"); let uppercase_books: Vec<String> = library.iter() .map(|book| book.to_uppercase()) .collect(); for book in &uppercase_books { println!("{}", book); } // Consuming the collection with IntoIterator println!("\nConsuming iteration:"); for book in library { // This moves library println!("Owned: {}", book); } // library is no longer accessible here due to move } // Advanced example: Lazy iterator with state struct FibonacciIterator { current: u64, next: u64, } impl FibonacciIterator { fn new() -> Self { Self { current: 0, next: 1 } } } impl Iterator for FibonacciIterator { type Item = u64; fn next(&mut self) -> Option<Self::Item> { let current = self.current; self.current = self.next; self.next = current + self.next; // Prevent overflow by stopping at large numbers if current > 1_000_000 { None } else { Some(current) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_fibonacci_iterator() { let fib: Vec<u64> = FibonacciIterator::new() .take(10) .collect(); assert_eq!(fib, vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]); } #[test] fn test_book_collection_iterator() { let mut library = BookCollection::new(); library.add_book("Book 1".to_string()); library.add_book("Book 2".to_string()); let books: Vec<&String> = library.iter().collect(); assert_eq!(books.len(), 2); } }
Key Differences Between Java and Rust
Java Approach:
- Object-oriented with explicit iterator objects
- Manual memory management through garbage collection
- Exception-based error handling (NoSuchElementException)
- Mutable iterator state through instance variables
- Enhanced for-loop as syntactic sugar
Rust Approach:
- Functional programming style with zero-cost abstractions
- Ownership system eliminates need for garbage collection
- Option-based error handling (None instead of exceptions)
- Immutable by default with explicit mutability
- Rich set of iterator combinators (map, filter, fold, etc.)
- Lazy evaluation - iterators do nothing until consumed
Benefits of Rust's Iterator Design
- Zero-cost abstractions: Rust's iterators compile down to the same code as hand-written loops
- Memory safety: The ownership system prevents iterator invalidation bugs
- Functional composition: Chain operations together naturally
- Lazy evaluation: Only compute values when needed
- Explicit ownership: Clear distinction between borrowing and consuming iteration
The Iterator pattern in Rust demonstrates how the language takes classical design patterns and reimagines them through the lens of ownership, safety, and zero-cost abstractions, often resulting in more expressive and efficient code than traditional OOP implementations.
Core Rust concepts introduced
Building on the NewType and Builder patterns, the Iterator pattern introduces several new concepts:
1. Lifetimes and Lifetime Parameters
#![allow(unused)] fn main() { impl<'a> IntoIterator for &'a BookCollection { type Item = &'a String; type IntoIter = std::slice::Iter<'a, String>; } struct BookIterator<'a> { books: &'a [String], index: usize, } }
- Lifetime parameters (
'a): Tell the compiler how long references are valid - Lifetime annotations: Connect the lifetime of input and output references
- Borrow checker validation: Ensures references don't outlive the data they point to
- Generic over lifetimes: Structs and functions can be parameterized by lifetimes
2. Mutable References and Borrowing Rules
#![allow(unused)] fn main() { fn next(&mut self) -> Option<Self::Item> { self.index += 1; // Can modify because of &mut self // ... } fn iter(&self) -> BookIterator { // Cannot modify self, only read BookIterator::new(&self.books) } }
&mut self: Exclusive mutable access to modify the iterator state- Borrowing rules: Only one mutable reference OR multiple immutable references
- Interior mutability: Some types allow mutation through shared references (not shown here)
3. Closure Syntax and Capture
#![allow(unused)] fn main() { .map(|book| book.to_uppercase()) .filter(|&book| book.len() > 5) }
- Closure syntax:
|param| expressionfor anonymous functions - Capture by reference: Closures can borrow values from their environment
- Capture by value: Use
movekeyword to take ownership of captured variables - Closure traits:
Fn,FnMut, andFnOncedefine how closures interact with captured data
4. Iterator Combinators and Lazy Evaluation
#![allow(unused)] fn main() { let uppercase_books: Vec<String> = library.iter() .map(|book| book.to_uppercase()) // Transform each element .filter(|book| book.contains("RUST")) // Keep only matching elements .collect(); // Consume iterator into collection }
- Lazy evaluation: Iterators do nothing until consumed by methods like
collect() - Method chaining: Combine multiple operations in a fluent interface
- Adapters vs consumers:
map/filterare adapters,collect/foldare consumers - Zero-cost: The entire chain compiles to an efficient loop
5. Type Inference and the Turbofish
#![allow(unused)] fn main() { let books: Vec<&String> = library.iter().collect(); // Or with explicit type specification: let books = library.iter().collect::<Vec<&String>>(); }
- Type inference: Rust deduces types from context when possible
- Turbofish syntax (
::<Type>): Explicitly specify generic type parameters - When inference fails: Need explicit types when compiler can't determine the target type
6. Slice Types and Fat Pointers
#![allow(unused)] fn main() { struct BookIterator<'a> { books: &'a [String], // Slice reference index: usize, } }
- Slice types (
&[T]): References to a contiguous sequence of elements - Fat pointers: Slices contain both a pointer and a length
- No bounds checking overhead: Slice length is known at runtime
- Memory efficient: Just a reference to existing data, no allocation
7. Consuming vs Borrowing Iteration
#![allow(unused)] fn main() { // Borrowing iteration - collection remains usable for book in &library { println!("{}", book); } // Consuming iteration - collection is moved for book in library { // library is no longer accessible after this println!("Owned: {}", book); } }
- Different iteration modes:
&collectionvscollectionvs&mut collection - Move semantics in loops:
for book in librarymoves the entire collection - Preventing use-after-move: Compiler error if you try to use
libraryafter consuming iteration
8. Associated Types in Traits
#![allow(unused)] fn main() { impl Iterator for BookIterator<'a> { type Item = &'a String; // Associated type fn next(&mut self) -> Option<Self::Item> { // ... } } }
- Associated types: Types that are "associated" with a trait implementation
Self::Item: Refers to the associated type within the implementation- Cleaner than generics: Often more readable than generic type parameters
9. Infinite Iterators and State
#![allow(unused)] fn main() { struct FibonacciIterator { current: u64, next: u64, } impl Iterator for FibonacciIterator { type Item = u64; fn next(&mut self) -> Option<Self::Item> { let current = self.current; self.current = self.next; self.next = current + self.next; if current > 1_000_000 { None // Stop at large numbers } else { Some(current) } } } }
- Stateful iterators: Iterators can maintain internal state between calls
- Infinite sequences: Iterators don't need to have a predetermined end
- Conditional termination: Return
Noneto signal the end of iteration
These concepts demonstrate Rust's approach to safe, efficient iteration that eliminates common bugs like iterator invalidation while providing zero-cost abstractions and powerful functional programming capabilities.