Skip to content

Iterators vs streams

An iterator chain such as orders.iter().filter(...).map(...).collect() is a Java stream pipeline: lazy, with collect() (or sum(), count(), a for loop) as the terminal operation.

Java

/** The IDs of paid orders over 100.00, largest total first. */
static List<Long> bigPaidOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.paid() && o.totalCents() > 10_000)
.sorted(Comparator.comparingLong(Order::totalCents).reversed())
.map(Order::id)
.toList();
}

Rust

/// The IDs of paid orders over 100.00, largest total first.
pub fn big_paid_orders(orders: &[Order]) -> Vec<u64> {
let mut big: Vec<&Order> = orders
.iter()
.filter(|o| o.paid && o.total_cents > 10_000)
.collect();
big.sort_by_key(|o| Reverse(o.total_cents)); // largest first
big.iter().map(|o| o.id).collect()
}

The shape is the same: a source, then adapters (filter, map), then a terminal operation. Two differences are visible here:

  • iter() instead of stream(). The name says how the items are given: here as &Order, borrowed.
  • The sort is not in the chain. Rust has no sorted() adapter in the standard library, because a sort needs all the items. The code collects into a Vec, sorts it, and starts a second chain.

This is the most important thing to read in an iterator chain. Each method gives the items in a different way:

pub fn three_ways(mut orders: Vec<Order>) -> (u64, Vec<String>) {
// `iter()`: borrow each item. `orders` is still usable after this line.
let total: u64 = orders.iter().map(|o| o.total_cents).sum();
// `iter_mut()`: borrow each item mutably, to change it in place.
// `for x in &mut orders` is the short form of `orders.iter_mut()`.
for order in &mut orders {
order.paid = true;
}
// `into_iter()`: take ownership of each item. `orders` is gone after this.
let customers = orders.into_iter().map(|o| o.customer).collect();
(total, customers)
}
  1. Line 3: iter() gives &Order: a shared borrow of each item. The Vec is unchanged and still usable.
  2. Line 7: &mut orders in a for loop calls iter_mut(). It gives &mut Order, so the loop can change each item in place.
  3. Line 12: into_iter() gives Order by value. It consumes the Vec: after this line, orders cannot be used. The closure can move o.customer out, with no clone.
You write Items are The collection after
v.iter() or for x in &v &T unchanged
v.iter_mut() or for x in &mut v &mut T changed in place
v.into_iter() or for x in v T gone (moved)

When you see .clone() inside a chain, look at which of the three the chain starts with. Often into_iter() removes the clone, or iter() and a borrow are enough (see Ownership and moves).

An adapter does nothing on its own. It builds a description of the work. The work runs when something asks for items: next(), collect(), sum(), or a for loop.

pub fn first_big_order(orders: &[Order], checked: &mut usize) -> Option<u64> {
let mut big = orders
.iter()
.inspect(|_| *checked += 1) // counts how many orders the chain looks at
.filter(|o| o.total_cents > 10_000)
.map(|o| o.id); // nothing has run yet: `big` is only a description
big.next() // runs the chain until it finds one match, then stops
}

The test chains_are_lazy proves that the chain looks at one order only: next() stops at the first match. A Java stream with findFirst() behaves the same way.

collect() can build many types: a Vec, a HashMap, a String, a HashSet. It needs to know which. The type comes from the variable (let v: Vec<u64> = ...collect();), the return type of the function, or a turbofish: .collect::<Vec<_>>(). The _ means “infer the item type”.

collect() can also build a Result. An iterator of Result<T, E> collects into Result<Vec<T>, E>: either all the values, or the first error:

/// Parse every line, or return the first error.
pub fn parse_quantities(lines: &[&str]) -> Result<Vec<u32>, ParseIntError> {
lines
.iter()
.map(|line| line.trim().parse::<u32>())
.collect()
}

Java streams have no equivalent. In Java, a checked exception inside a lambda is a well-known problem. In Rust, the error is a value, and collect stops at the first one. The same works with Option.

pub fn adapters(prices: &[u64], names: &[&str]) {
let numbered: Vec<(usize, &u64)> = prices.iter().enumerate().collect(); // (0, p0), (1, p1), ...
let labelled: Vec<String> = names
.iter()
.zip(prices) // pairs, stops at the shorter one
.map(|(n, p)| format!("{n}={p}"))
.collect();
let rising = prices.windows(2).all(|w| w[0] <= w[1]); // overlapping pairs
let pages: Vec<&[u64]> = prices.chunks(2).collect(); // groups of 2, last may be shorter
let even: Vec<u64> = prices.iter().copied().filter(|p| p % 2 == 0).collect();
let parsed: Vec<u64> = names.iter().filter_map(|n| n.parse().ok()).collect(); // drop the failures
let chars: Vec<char> = names.iter().flat_map(|n| n.chars()).collect(); // one level flatter
let first_expensive = prices.iter().position(|&p| p > 1_000); // Option<usize>
let any_expensive = prices.iter().any(|&p| p > 5_000);
let (count, sum) = prices.iter().fold((0, 0), |(n, s), p| (n + 1, s + p)); // like `reduce`
}
Rust Java stream Note
map, filter map, filter
filter_map map then filter(Objects::nonNull) the closure returns an Option
flat_map flatMap
enumerate (an index variable) gives (index, item)
zip (none) pairs two iterators
take, skip, take_while limit, skip, takeWhile
any, all, find, position anyMatch, allMatch, findFirst stop at the first answer
sum, count, min, max sum, count, min, max
fold reduce with an identity
copied, cloned (none) &u64 to u64, &T to T
windows(n), chunks(n) (none) on slices, not on iterators

A chain is not always better. When the body has several steps, early exits, or ?, a for loop is often clearer:

/// A `for` loop is clearer when the body has several steps and an early exit.
pub fn reserve_stock(stock: &mut [u32], wanted: &[(usize, u32)]) -> Result<(), usize> {
for &(item, count) in wanted {
let Some(available) = stock.get_mut(item) else {
return Err(item);
};
if *available < count {
return Err(item);
}
*available -= count;
}
Ok(())
}

In review, do not ask for chains everywhere. Ask for the version that is easiest to read. A for loop over the items (for x in &items) is fine. A for loop over indexes (for i in 0..items.len()) usually is not. See S10: Index-based loops instead of iterators.

  • The right start: iter() to read, iter_mut() to change, into_iter() when the collection is not needed after.
  • Short chains of three to five steps, each on its own line.
  • The terminal operation that says the intent: any instead of filter(...).count() > 0, sum instead of a manual fold.
  • No collect() in the middle unless the next step needs all the items (sorting, or two passes).
  • for loops when the body is long or has early exits.
  • Index loops: for i in 0..v.len() with v[i]. See S10: Index-based loops instead of iterators.
  • collect(), then iterate again, or collect() only to call .len() or .is_empty(). See S11: collect() then iterate again.
  • .iter().map(|x| x.clone()) instead of .cloned(), or .iter().cloned() where into_iter() gives owned values for free.
  • .count() > 0 or .len() > 0 to test for “any”. Use any() or is_empty().
  • Very long chains with nested closures, fold with tuples, or flat_map inside map. Ask for named steps or a loop.

The task: the names of customers with paid orders, and whether any order is unpaid.

  1. WorseAn index loop, a &Vec parameter, a Vec of cloned orders built only to call len().

    pub fn customer_report(orders: &Vec<Order>) -> (Vec<String>, bool) {
    let mut names = Vec::new();
    for i in 0..orders.len() {
    if orders[i].paid {
    names.push(orders[i].customer.clone());
    }
    }
    let unpaid: Vec<Order> = orders
    .iter()
    .filter(|o| !o.paid)
    .map(|o| o.clone())
    .collect();
    let has_unpaid = unpaid.len() > 0;
    (names, has_unpaid)
    }
    Run in Playground (opens the Rust Playground in a new tab)
  2. BetterA filter and map chain for the names, and any for the check.

    pub fn customer_report(orders: &[Order]) -> (Vec<String>, bool) {
    let names = orders
    .iter()
    .filter(|o| o.paid)
    .map(|o| o.customer.clone())
    .collect();
    let has_unpaid = orders.iter().any(|o| !o.paid);
    (names, has_unpaid)
    }
    Run in Playground (opens the Rust Playground in a new tab)

The “worse” version gives the same result, as the test both_reports_agree shows. But it clones every unpaid order, and it keeps them in a Vec, only to ask if the Vec is empty. any stops at the first unpaid order and copies nothing. Clippy’s default lints report four problems in the “worse” code: needless_range_loop, map_clone, len_zero, and ptr_arg. The nursery lint needless_collect reports the Vec that is only used for its length.

  1. What is the difference between orders.iter() and orders.into_iter() on a Vec<Order>?

    Show the answer

    iter() gives &Order and leaves the Vec usable. into_iter() gives Order by value and consumes the Vec, so it cannot be used afterwards.

  2. A chain has .map(...) but no collect, sum, for loop, or next. What does it do?

    Show the answer

    Nothing. Iterators are lazy. The compiler warns about it: an unused iterator adapter is a must_use warning.

  3. What does lines.iter().map(|l| l.parse::<u32>()).collect::<Result<Vec<_>, _>>() return for one bad line?

    Show the answer

    Err with the parse error of the first bad line. If all lines parse, it is Ok with all the numbers.

  4. An agent wrote items.iter().filter(|i| i.urgent).count() > 0. What do you suggest?

    Show the answer

    items.iter().any(|i| i.urgent). It says the intent, and it stops at the first urgent item.