Streams Basics
Streams were introduced in Java 8 to process collections in a
functional and declarative style.
Common operations: filter, map, collect, reduce
Key ideas:
- A stream is not a data storage (not a collection)
- It works with pipelines
- Supports lazy evaluation
- Encurage declarative programming
Streams are not about iteration - they are about transformation pipelines.
package streams_basics;
import java.util.Arrays;
import java.util.List;
public class StreamsBasics {
public static void main(String[] args) {
List<String> names =
Arrays.asList("John", "Jane", "Doe", "Julia");
System.out.println("Traditional way - imprerative");
for (String name : names) {
if (name.startsWith("J")) {
System.out.print(name + " ");
}
}
System.out.println("Stream way - declarative");
names.stream()
.filter(x -> x.startsWith("J"))
.forEach(System.out::print);
}
}
1. Iteration Styles
Streams allow us to write collection-processing code at a higher level of abstraction.
In programming there are TWO iterative approaches:
External iteration (imperative):
- The developer controls HOW iteration happens
- Uses loops or iterators
- Requires mutable state (counter, variables)
Internal iteration (declarative):
- The library controls HOW iteration hapens
- The developer specifies WHAT should be done
- No explicit loops or mutable counters
Iterator - external iterator
Stream - internal iterator
package streams_basics;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class IterationStyles {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3);
int count;
count = 0;
for (int n : numbers) {
if (n <= 2) count++;
}
System.out.println("For-each loop: " + count);
count = 0;
Iterator<Integer> it = numbers.iterator();
while(it.hasNext()) {
int n = it.next();
if (n <= 2) count++;
}
System.out.println("Iterator: " + count);
long total = numbers.stream()
.filter(n -> n <= 2)
.count();
System.out.println("Stream: " + total);
}
}
2. Common Operations
Streams most common operations:
- filter = Select elements based on a condition (Predicate)
- mapping = Transform each element into another form
- sorting = Sort elements (natural or custom comparator)
- collecting = Convert stream into a collection or other result
- reduce = Combine elements into a single result
package streams_basics;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class CommonOperations {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers =
numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("Filter: " + numbers);
List<String> names = Arrays.asList("john", "jane", "jack");
names =
names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Mapping: " + names);
List<Integer> nums = Arrays.asList(10, 20, 30);
int sum =
nums.stream()
.mapToInt(Integer::intValue)
.sum();
System.out.println("Sum: " + sum);
List<Integer> items = Arrays.asList(5, 2, 8, 1);
items =
items.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("Sorting: " + items);
List<String> employee = Arrays.asList("john", "jane", "mary", "jack");
employee =
employee.stream()
.filter(name -> name.startsWith("j"))
.collect(Collectors.toList());
System.out.println("Collect: " + employee);
List<Integer> quantities = Arrays.asList(10, 20, 30);
int total =
quantities.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Reduce: " + total);
}
}
3. Laziness
Streams are evaluated lazily.
Key ideas:
- A stream does NOT process data when it is created
- Intermediate operations (filter, map) are lazy
- Nothing happens until a TERMINAL operation is invoked
Terminal operations:
- count()
- forEach()
- collect()
- findFirst()
- anyMatch()
Without a terminal operation, a stream does nothing.
When we write numbers.strea().filter(...) we are only describing a pipeline.
package streams_basics;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Laziness {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.stream()
.filter(n -> {
return n <= 2;
});
System.out.println(numbers);
numbers = numbers.stream()
.filter(n -> {
return n <= 2;
})
.collect(Collectors.toList());
System.out.println(numbers);
}
}
4. Application Example
Real-life scenario.
Order processing application.
package streams_basics.app_example;
record Order(String id, double amount, boolean paid) {};
4.1 Traditional (for-loop implementation)
Why developers use for-loop traditionally:
- Easy to understand
- Explicit logic
- Debug-fiendly
Limitations (real project pain):
- Boilerplate (loop + if + accumulator)
- Harder to extend pipeline
- Mutability (total variable)
package streams.basics.traditional;
import java.util.Arrays;
import java.util.List;
public class OrdersApp {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("01", 100, true),
new Order("02", 200, false),
new Order("03", 300, true)
);
double total = 0;
for (Order order : orders) {
if (order.isPaid()) {
total += order.getAmount();
}
}
System.out.println("Total paid amount: " + total);
}
}
4.2 Bad Stream Implementation (common mistake)
What's wrong here:
- Using streams like a loop (forEach)
- This is just a loop replacement
- Mutable state hack
Big red flag:
- Breaks functional style
- Not thread-safe
- Upgly and error-prone
Real industry smell:
- If you have forEach + mutation, you are misusing streams.
package streams.basics.bad;
import java.util.Arrays;
import java.util.List;
public class OrdersApp {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("01", 100, true),
new Order("02", 200, false),
new Order("03", 300, true)
);
final double[] total = {0};
orders.stream()
.filter(order -> {
return order.isPaid();
})
.forEach(order -> {
total[0] += order.getAmount();
});
System.out.println("Total paid amount: " + total[0]);
}
}
4.3 Correct Stream Implementation
Why this is correct:
- Declarative (reads like business logic)
- No mutation (no shared state, no hacks)
- Performance-friendly (can be parallelized safely)
- Composable (easy to extend)
package streams.basics;
import java.util.Arrays;
import java.util.List;
public class OrdersApp {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("01", 100, true),
new Order("02", 200, false),
new Order("03", 300, true)
);
double total = orders.stream()
.filter(Order::isPaid)
.mapToDouble(Order::getAmount)
.sum();
System.out.println("Total paid amount: " + total);
}
}