Back to blog
Backend Systemsbeginner

Java Collections, Generics & Streams API

Master Java's collection framework: List, Map, Set, Queue. Write type-safe generics. Use Streams for functional-style data transformations.

Asma HafeezApril 17, 20266 min read
javacollectionsgenericsstreamslambda
Share:𝕏

Java Collections, Generics & Streams API

Java's collection framework and Streams API turn complex data manipulation into readable, composable pipelines.


The Collections Framework

All collections live in java.util. Import them as needed.

List — Ordered, allows duplicates

JAVA
import java.util.ArrayList;
import java.util.List;

// Create
List<String> names = new ArrayList<>();
List<String> fixed = List.of("Alice", "Bob", "Carol");  // immutable

// Add
names.add("Alice");
names.add("Bob");
names.add(0, "Zara");    // insert at index 0

// Access
String first = names.get(0);       // "Zara"
int size = names.size();           // 3
boolean has = names.contains("Bob"); // true

// Remove
names.remove("Alice");             // by value
names.remove(0);                   // by index

// Iterate
for (String name : names) {
    System.out.println(name);
}

// Sort
names.sort(null);                  // natural order
names.sort((a, b) -> b.compareTo(a)); // reverse alphabetical

Map — Key-value pairs, unique keys

JAVA
import java.util.HashMap;
import java.util.Map;

Map<String, Integer> scores = new HashMap<>();

// Put
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Alice", 98);  // overwrites existing

// Get
int aliceScore = scores.get("Alice");          // 98
int defaultScore = scores.getOrDefault("Zara", 0); // 0

// Check
boolean hasKey = scores.containsKey("Bob");    // true

// Remove
scores.remove("Bob");

// Iterate
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

// Functional update
scores.merge("Alice", 5, Integer::sum);  // 98 + 5 = 103
scores.computeIfAbsent("Carol", k -> 0); // add Carol with 0 if missing

Set — Unique elements, no duplicates

JAVA
import java.util.HashSet;
import java.util.Set;

Set<String> visited = new HashSet<>();
visited.add("page1");
visited.add("page2");
visited.add("page1");  // no-op — already exists

System.out.println(visited.size());          // 2
System.out.println(visited.contains("page1")); // true

// Set operations
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> b = new HashSet<>(Set.of(2, 3, 4));

a.retainAll(b);  // intersection: {2, 3}
a.addAll(b);     // union: {2, 3, 4}
a.removeAll(b);  // difference: {1}

Queue and Deque

JAVA
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.Deque;

Queue<String> queue = new ArrayDeque<>();
queue.offer("first");
queue.offer("second");
String head = queue.poll();   // "first" — removes
String peek = queue.peek();   // "second" — doesn't remove

Deque<String> stack = new ArrayDeque<>();
stack.push("first");
stack.push("second");
String top = stack.pop();    // "second" (LIFO)

Generics

Generics let you write type-safe, reusable code.

JAVA
// A generic container
public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T get() { return value; }
    public void set(T value) { this.value = value; }

    @Override
    public String toString() { return "Box[" + value + "]"; }
}

Box<String> stringBox = new Box<>("hello");
Box<Integer> intBox = new Box<>(42);

System.out.println(stringBox.get().toUpperCase()); // HELLO
System.out.println(intBox.get() * 2);              // 84

Generic Methods

JAVA
// Works with any Comparable type
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

System.out.println(max(3, 7));         // 7
System.out.println(max("apple", "banana")); // banana

Bounded Type Parameters

JAVA
// Only accept Number subtypes (Integer, Double, etc.)
public static <T extends Number> double sum(List<T> numbers) {
    double total = 0;
    for (T n : numbers) {
        total += n.doubleValue();
    }
    return total;
}

List<Integer> ints = List.of(1, 2, 3, 4, 5);
System.out.println(sum(ints));  // 15.0

Streams API

Streams transform collections using a pipeline of operations. They are lazy — operations execute only when a terminal operation is called.

JAVA
import java.util.stream.Collectors;
import java.util.stream.Stream;

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// filter → map → collect
List<Integer> evenSquares = numbers.stream()
    .filter(n -> n % 2 == 0)    // keep even numbers
    .map(n -> n * n)             // square them
    .collect(Collectors.toList()); // [4, 16, 36, 64, 100]

Common Intermediate Operations

JAVA
List<String> names = List.of("Alice", "Bob", "Carol", "Alice", "Dave");

// filter — keep matching elements
List<String> longNames = names.stream()
    .filter(n -> n.length() > 4)
    .toList(); // ["Alice", "Carol", "Alice"]

// map — transform each element
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList(); // ["ALICE", "BOB", ...]

// distinct — remove duplicates
List<String> unique = names.stream()
    .distinct()
    .toList(); // ["Alice", "Bob", "Carol", "Dave"]

// sorted — natural or custom order
List<String> sorted = names.stream()
    .sorted()
    .toList();

// limit / skip
List<Integer> first3 = numbers.stream().limit(3).toList(); // [1, 2, 3]
List<Integer> after3 = numbers.stream().skip(3).toList();  // [4, 5, ...]

// flatMap — flatten nested collections
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .toList(); // [1, 2, 3, 4]

Common Terminal Operations

JAVA
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6);

// count
long count = nums.stream().filter(n -> n > 3).count(); // 4

// sum, min, max, average
int sum = nums.stream().mapToInt(Integer::intValue).sum(); // 31
OptionalInt max = nums.stream().mapToInt(Integer::intValue).max(); // 9
double avg = nums.stream().mapToInt(Integer::intValue).average().orElse(0); // 3.875

// findFirst, findAny
Optional<Integer> first = nums.stream().filter(n -> n > 5).findFirst(); // 9

// anyMatch, allMatch, noneMatch
boolean anyBig = nums.stream().anyMatch(n -> n > 8);  // true
boolean allPos = nums.stream().allMatch(n -> n > 0);  // true
boolean noneNeg = nums.stream().noneMatch(n -> n < 0); // true

// reduce — fold to a single value
int product = nums.stream().reduce(1, (a, b) -> a * b); // 3*1*4*1*5*9*2*6

// joining strings
String joined = Stream.of("Alice", "Bob", "Carol")
    .collect(Collectors.joining(", ")); // "Alice, Bob, Carol"

Collecting to Maps and Groups

JAVA
List<String> words = List.of("apple", "banana", "cherry", "avocado", "blueberry");

// Group by first letter
Map<Character, List<String>> grouped = words.stream()
    .collect(Collectors.groupingBy(w -> w.charAt(0)));
// {'a': ["apple", "avocado"], 'b': ["banana", "blueberry"], 'c': ["cherry"]}

// Count by first letter
Map<Character, Long> counts = words.stream()
    .collect(Collectors.groupingBy(w -> w.charAt(0), Collectors.counting()));
// {'a': 2, 'b': 2, 'c': 1}

// To Map
Map<String, Integer> wordLengths = words.stream()
    .collect(Collectors.toMap(w -> w, String::length));
// {"apple": 5, "banana": 6, ...}

Lambda Expressions

Lambdas are anonymous functions used with functional interfaces.

JAVA
// Functional interface (one abstract method)
@FunctionalInterface
interface Transformer<T, R> {
    R transform(T input);
}

Transformer<String, Integer> lengthFn = s -> s.length();
System.out.println(lengthFn.transform("hello"));  // 5

// Common functional interfaces from java.util.function
import java.util.function.*;

Function<String, Integer>  toLength  = String::length;    // T → R
Predicate<String>          isLong    = s -> s.length() > 5; // T → boolean
Consumer<String>           printer   = System.out::println;  // T → void
Supplier<String>           greeting  = () -> "Hello!";      // () → T
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

Key Takeaways

  1. Use List.of(), Set.of(), Map.of() for immutable collections — safer by default
  2. HashMap is unordered — use LinkedHashMap to preserve insertion order or TreeMap for sorted keys
  3. Stream pipelines are lazy — nothing runs until you call a terminal operation
  4. map transforms one type to another; flatMap flattens nested streams
  5. Method references (String::length, System.out::println) make streams more readable

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.