Question

Java LINQ Equivalent: Stream API, Filters, Maps, and Collectors

javalinq

Question

What is the Java equivalent of LINQ for querying, filtering, transforming, and collecting data from collections?

Short Answer

Java does not include LINQ with the same syntax as C#, but the Java Stream API is the standard built-in approach for performing LINQ-like operations on collections. You will learn how to use streams with operations such as filter, map, sorted, and collect, and how they differ from ordinary loops.

Concept

The closest standard Java equivalent to C# LINQ is the Stream API, introduced in Java 8.

A stream represents a pipeline for processing a sequence of values. It lets you describe what result you want rather than manually controlling indexes, temporary lists, and loop flow.

For example, suppose you need the names of active users, sorted alphabetically. With a stream, you can:

  1. Start with a collection.
  2. Filter out inactive users.
  3. Transform each remaining user into a name.
  4. Sort the names.
  5. Collect the result into a list.
List<String> activeNames = users.stream()
    .filter(User::isActive)
    .map(User::getName)
    .sorted()
    .collect(Collectors.toList());

This is similar in purpose to a LINQ query such as Where, Select, OrderBy, and ToList.

Streams matter because collection processing is everywhere in real programs: API results, database records already loaded into memory, file lines, user input, reports, and event data. A stream pipeline can make this processing concise and easier to read when used appropriately.

A crucial distinction: Java streams process data in memory. They are not automatically database queries. Database query tools may offer LINQ-like APIs, but those are separate libraries or frameworks.

Mental Model

Think of a stream as an assembly line.

  • The source collection is a box of raw items arriving at the line.
  • filter is an inspector who removes items that do not meet a rule.
  • map is a worker who converts each item into a different form.
  • sorted arranges the items.
  • collect packs the final items into a new box, such as a List.

Each stage describes one transformation. The original collection stays unchanged unless you explicitly modify it elsewhere.

Take Quiz

Syntax and Examples

A stream pipeline usually has three parts:

source.stream()
    .intermediateOperation()
    .terminalOperation();
  • Source: usually a Collection, such as a List or Set.
  • Intermediate operations: build the pipeline, for example filter, map, sorted, and distinct.
  • Terminal operation: produces a result or performs work, for example collect, count, findFirst, forEach, or reduce.

Filter numbers

import java.util.List;
import java.util.stream.Collectors;

List<Integer> scores = List.of(45, 72, 90, 58, 81);

List<Integer> passingScores = scores.stream()
    .filter(score -> score >= 60)
    .collect(Collectors.toList());

System.out.println(passingScores); // [72, 90, 81]

filter keeps values for which its condition returns true.

Transform values with map

import java.util.List;
import java.util.stream.Collectors;

List<String> names = List.of("ana", "ben", "chris");

List<String> uppercaseNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

System.out.println(uppercaseNames); // [ANA, BEN, CHRIS]

map converts every input item into an output item. Here, every String becomes an uppercase String.

Filter, transform, and sort

import java.util.List;
import java.util.stream.Collectors;

List<String> products = List.of("Keyboard", "Pen", "Monitor", "Notebook");

List<String> longProductNames = products.stream()
    .filter(product -> product.length() >= 7)
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList());

System.out.println(longProductNames); // [KEYBOARD, MONITOR, NOTEBOOK]

Step by Step Execution

Consider this pipeline:

import java.util.List;
import java.util.stream.Collectors;

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

List<Integer> result = numbers.stream()
    .filter(number -> number % 2 == 0)
    .map(number -> number * 10)
    .collect(Collectors.toList());

System.out.println(result); // [20, 40]

Execution is easiest to understand item by item:

  1. numbers.stream() creates a stream over 1, 2, 3, 4, 5.
  2. filter(number -> number % 2 == 0) keeps only even values:
    • 1 is removed.
    • 2 continues.
    • 3 is removed.
    • 4 continues.
    • 5 is removed.
  3. map(number -> number * 10) transforms the remaining values:
    • 2 becomes 20.
    • 4 becomes 40.
  4. collect(Collectors.toList()) gathers 20 and 40 into a new List.
  5. The original numbers list is still [1, 2, 3, 4, 5].

Intermediate operations such as filter and map are generally lazy: they do not fully run until a terminal operation, such as collect, starts the pipeline.

Real World Use Cases

Streams are useful when you need to process a group of in-memory values.

  • API response processing: keep orders with a PAID status and return their order IDs.
  • Reporting: total the prices of items in a shopping cart.
  • Search: find products whose names contain a search term.
  • Data cleanup: trim text, remove blank entries, and eliminate duplicates.
  • Permissions: select actions allowed for the current user.
  • Import validation: identify invalid rows before saving valid rows.

Example: calculate the total cost of available products.

int total = products.stream()
    .filter(Product::isAvailable)
    .mapToInt(Product::getPriceInCents)
    .sum();

mapToInt creates an IntStream, which provides numeric operations such as sum, average, min, and max.

Take Quiz

Real Codebase Usage

In real Java projects, streams are commonly used for small, focused collection transformations.

Validate before processing

Use a guard clause before creating a pipeline when an input may be invalid.

public List<String> normalizeTags(List<String> tags) {
    if (tags == null) {
        return List.of();
    }

    return tags.stream()
        .filter(tag -> tag != null && !tag.isBlank())
        .map(String::trim)
        .map(String::toLowerCase)
        .distinct()
        .sorted()
        .collect(Collectors.toList());
}

Build lookup maps

A common task is turning a list into a map keyed by an ID.

import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

Map<Long, User> usersById = users.stream()
    .collect(Collectors.toMap(User::getId, Function.identity()));

This assumes IDs are unique. If duplicate keys are possible, provide a merge rule.

Map<Long, User> usersById = users.stream()
    .collect(Collectors.toMap(
        User::getId,
        Function.identity(),
        (first, second) -> first
    ));

Find one value safely

Use Optional-returning operations instead of assuming an item exists.

Optional<User> admin = users.stream()
    .filter(User::isAdmin)
    .findFirst();

Keep pipelines understandable

A pipeline is most maintainable when each stage has one clear job. If business rules become long or need detailed error handling, a named method or a normal loop may be clearer.

Common Mistakes

Forgetting a terminal operation

This pipeline does not produce a list because filter only creates another stream.

List<Integer> values = List.of(1, 2, 3);
values.stream().filter(value -> value > 1); // Result is ignored

Finish with a terminal operation:

List<Integer> result = values.stream()
    .filter(value -> value > 1)
    .collect(Collectors.toList());

Reusing a stream

A stream is consumed after a terminal operation.

Stream<String> stream = List.of("a", "b").stream();
long count = stream.count();
stream.forEach(System.out::println); // Throws IllegalStateException

Create a new stream from the collection when needed.

Expecting map to remove items

map transforms every item; it does not select items.

List<String> names = List.of("Ada", "", "Lin");

List<String> result = names.stream()
    .map(String::trim)
    .collect(Collectors.toList()); // Still contains ""

Use filter to remove unwanted values:

List<String> result = names.stream()
    .map(String::trim)
    .filter(name -> !name.isEmpty())
    .collect(Collectors.toList());

Modifying the source collection during streaming

Do not add or remove elements from a collection while streaming it. This can cause errors such as ConcurrentModificationException.

Instead, produce a new collection with filter or another stream operation.

Ignoring null

Calling a method on a null element causes NullPointerException.

names.stream()
    .map(String::toUpperCase) // Fails if an element is null
    .collect(Collectors.toList());

If nulls are allowed, remove them first:

names.stream()
    .filter(Objects::nonNull)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

Using parallel streams automatically

parallelStream() is not automatically faster. It can add overhead, complicate debugging, and be unsafe when operations have shared mutable state. Start with stream() and measure before choosing parallel processing.

Comparisons

C# LINQ ideaJava Stream API equivalentPurpose
Where(...)filter(...)Keep matching values
Select(...)map(...)Transform each value
OrderBy(...)sorted(...)Sort values
ToList()collect(Collectors.toList())Create a list
Count()count()Count values
FirstOrDefault()findFirst()Get the first value as Optional
Any(...)anyMatch(...)Test whether any value matches
Sum(...)mapToInt(...).sum()Sum numeric values

Streams versus loops

Streamsfor loops
Good for declarative filtering and transformationGood for complex control flow and step-by-step mutation
Often concise for pipelinesOften clearer when there are many branches or side effects
Encourages returning a resultCan directly update variables and collections
Uses operations such as filter and mapUses if, indexes, break, and continue

Neither approach is always better. Prefer the version that is easiest for your team to understand and maintain.

Streams versus database queries

A Java stream processes objects that are already available to the application. A database query filters data in the database. For large datasets, filtering in the database is often more efficient because the application does not need to load every row first.

Take Quiz

Cheat Sheet

// Start a stream from a collection
items.stream()

// Keep matching items
.filter(item -> condition)

// Transform each item
.map(item -> transformedItem)

// Remove duplicates and sort
.distinct()
.sorted()

// Create a List
.collect(Collectors.toList())

// Count matches
.filter(item -> condition)
.count()

// Check for matches
.anyMatch(item -> condition)

// Find one item
.findFirst() // returns Optional<T>

// Sum integer values
.mapToInt(Item::getQuantity)
.sum()

Key rules:

  • A stream pipeline needs a terminal operation such as collect, count, sum, or forEach to run.
  • filter selects values; map transforms values.
  • Streams generally do not alter the original collection.
  • A stream can be consumed only once.
  • Use Collectors.toList() for broad compatibility across Java versions.

FAQ

Is there an exact LINQ equivalent in Java?

No. Java does not have LINQ query syntax built into the language. The standard Stream API provides many equivalent operations for in-memory collection processing.

What Java version introduced streams?

The Stream API was introduced in Java 8.

Is stream() the same as a collection?

No. A collection stores values. A stream processes values from a source and is normally consumed once.

Does filter change the original list?

No. filter creates a processing stage. When collected, it usually creates a separate result collection.

When should I use a loop instead of a stream?

Use a loop when the logic needs complex branching, early exits across multiple steps, detailed debugging, or controlled mutation. Use a stream for clear transformations and queries over collections.

Can Java streams query a database?

Not by themselves. Streams process in-memory data. Database frameworks may offer query APIs, but those execute through the database layer rather than through ordinary Java streams.

Why does findFirst() return Optional?

The stream may be empty. Optional makes the possible absence of a result explicit.

Should I use parallelStream() for better performance?

Only after measuring. Parallel processing can be slower for small workloads and may create problems when the pipeline performs shared mutable operations.

Take Quiz

Mini Project

Description

Create a small order-reporting feature. An online shop has a list of orders, and the application needs a report containing the names of paid orders worth at least a chosen amount. This demonstrates a realistic stream pipeline that filters objects, transforms them, sorts results, and collects them into a list.

Goal

Produce an alphabetically sorted list of customer names for qualifying paid orders.

Requirements

Requirement 1

Take Quiz

Keep learning

Related questions

View more