top of page

Understanding Java Stream API: A Comprehensive Guide for Beginners

Introduction


The Java Stream API is a powerful feature introduced in Java 8 that simplifies data processing tasks. It enables developers to write clean, concise, and efficient code by processing data in a functional programming style. This blog will walk you through the basics of the Java Stream API, with detailed explanations and easy-to-understand examples to help you get started.


Table of Contents


  1. What is a Stream?

  2. How to Create Streams

  3. Stream Operations: Intermediate vs. Terminal

  4. Common Stream Operations with Examples

    • Filtering

    • Mapping

    • Sorting

    • Reducing

  5. Working with Parallel Streams

  6. Best Practices for Using Streams

  7. Conclusion


1. What is a Stream?


A Stream in Java is a sequence of elements that can be processed in parallel or sequentially. Unlike collections, a stream does not store elements; instead, it carries elements from a source (like a collection or array) through a pipeline of operations. The Stream API allows you to perform operations like filtering, mapping, and reducing in a clean and declarative way.

Think of a stream as a conveyor belt in a factory: you place raw materials on one end, they go through various processing stages, and you get the finished product at the other end.


2. How to Create Streams


Streams can be created from various data sources, such as collections, arrays, or even from values directly. Here are a few ways to create streams:


From a Collection

You can create a stream from any Java Collection (like List, Set, etc.) using the stream() method.


List<String> names = Arrays.asList("Pankaj", "Amit", "Rahul");
Stream<String> stream = names.stream();

From an Array

Similarly, you can create a stream from an array using the Arrays.stream() method.


String[] nameArray = {"Pankaj", "Amit", "Rahul"};
Stream<String> stream = Arrays.stream(nameArray);

Using Stream.of()


The Stream.of() method allows you to create a stream directly from a set of values.

Stream<String> stream = Stream.of("Pankaj", "Amit", "Rahul");

3. Stream Operations: Intermediate vs. Terminal


Stream operations are categorized into two types:

  • Intermediate Operations: These operations return a new stream and are lazy, meaning they are not executed until a terminal operation is invoked. Examples include filter(), map(), and sorted().

  • Terminal Operations: These operations produce a result or a side effect and terminate the stream. Examples include forEach(), collect(), and reduce().


4. Common Stream Operations with Examples


Let’s dive into some of the most common stream operations with examples that are easy to follow.


4.1 Filtering


Filtering is one of the most commonly used operations, allowing you to select elements that meet a certain condition. The filter() method takes a predicate (a function that returns true or false) as an argument.


Example: Filtering Even Numbers


List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0) 
// Keep only even number
.collect(Collectors.toList());
System.out.println(evenNumbers); // Output: [2, 4, 6]
System.out.println(evenNumbers); // Output: [2, 4, 6]

Explanation:

  • The stream is created from the numbers list.

  • filter(n -> n % 2 == 0) selects only the even numbers.

  • The result is collected into a new list using collect(Collectors.toList()).


4.2 Mapping


Mapping transforms each element of the stream into another form using the map() method.


Example: Converting Names to Uppercase


List<String> names = Arrays.asList("Pankaj", "Amit", "Rahul");
List<String> upperCaseNames = names.stream()
.map(String::toUpperCase) // Convert each name to uppercase
.collect(Collectors.toList());
System.out.println(upperCaseNames); // Output: [PANKAJ, AMIT, RAHUL]

Explanation :


  • The stream is created from the names list.

  • map(String::toUpperCase) converts each string to uppercase.

  • The result is collected into a new list.


4.3 Sorting


Sorting can be done using the sorted() method, which returns a new stream with sorted elements.


Example: Sorting Names Alphabetically


List<String> names = Arrays.asList("Pankaj", "Amit", "Rahul");
List<String> sortedNames = names.stream()
.sorted() // Sort names alphabetically
.collect(Collectors.toList());
System.out.println(sortedNames); // Output: [Amit, Pankaj, Rahul]

Explanation :


  • The stream is created from the names list.

  • sorted() sorts the elements in natural order (alphabetical for strings).

  • The result is collected into a new list.


4.4 Reducing


The reduce() method combines elements of a stream to produce a single result. This is often used to sum elements, find a maximum, or concatenate strings.

Example : Summing Numbers


List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum); // Sum all numbers
System.out.println(sum); // Output: 15

Explanation :


  • The stream is created from the numbers list.

  • reduce(0, Integer::sum) starts with an initial value of 0 and sums all elements in the stream.


5. Working with Parallel Streams


Parallel streams allow you to process data in parallel, utilizing multiple CPU cores. You can create a parallel stream by using the parallelStream() method.


Example: Parallel Stream for Summing Numbers

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
int sum = numbers.parallelStream()
.reduce(0, Integer::sum);
System.out.println(sum); // Output: 36

Explanation:

  • The parallelStream() method creates a parallel stream.

  • The reduce() method then sums the numbers in parallel.


6. Best Practices for Using Streams


  • Use parallel streams with caution: While parallel streams can enhance performance, they may also introduce concurrency issues and reduce performance if not used correctly.

  • Avoid stateful lambda expressions: Stateless operations are preferred because they avoid potential issues with concurrent modifications.

  • Minimize side-effects: Streams are designed for functional programming, so side-effects should be avoided to ensure clean and predictable code.


  • Complete Example: Processing a List of People


    Imagine you have a list of people, and you want to:

    1. Filter out those who are under 18 years old.

    2. Convert their names to uppercase.

    3. Sort them by name.

    4. Collect the final list and print it.

    Here’s how you can achieve this using the Java Stream API.


import java.util.*;

import java.util.stream.Collectors;

class Person {

    String name;

    int age;

    Person(String name, int age) {

        this.name = name;

        this.age = age;

    }

    public String getName() {

        return name;

    }

    public int getAge() {

        return age;

    }

    @Override

    public String toString() {

        return name + " (" + age + ")";

    }

}

public class StreamExample {

    public static void main(String[] args) {

        // Create a list of people

        List<Person> people = Arrays.asList(

            new Person("Pankaj", 30),

            new Person("Amit", 25),

            new Person("Rahul", 17),

            new Person("Ravi", 20),

            new Person("Sneha", 15)

        );

        // Process the list using Stream API

        List<String> result = people.stream()

            .filter(person -> person.getAge() >= 18) 
// 1. Filter: Keep only adults

            .map(Person::getName)                            
// 2. Map: Get the name of each person

            .map(String::toUpperCase)                        
// 3. Map: Convert names to uppercase

            .sorted()                                        
// 4. Sort: Sort names alphabetically

            .collect(Collectors.toList());                   
// 5. Collect: Collect the result into a list

        // Print the result

        System.out.println(result); 
// Output: [AMIT, PANKAJ, RAVI]

    }

}

Explanation of the Example


  1. Person Class:

    • We define a Person class with name and age attributes. It has a constructor, getters for the attributes, and a toString() method for easy printing.

  2. Creating a List of People:

    • We create a list of Person objects, each representing a person with a name and age.

  3. Using the Stream API:

    • We convert the list to a stream using people.stream().

    • Filter: We use filter(person -> person.getAge() >= 18) to keep only those who are 18 or older.

    • Map (Name Extraction): We extract the names of the remaining people using map(Person::getName).

    • Map (Uppercase Conversion): We convert the names to uppercase using map(String::toUpperCase).

    • Sort: We sort the names alphabetically using sorted().

    • Collect: Finally, we collect the results into a new list using collect(Collectors.toList()).

  4. Printing the Result:

    • The result, which is a list of uppercase names of adults sorted alphabetically, is printed to the console.


7. Conclusion


The Java Stream API is a powerful tool that allows you to process data in a declarative and functional style. By understanding the basic concepts and operations, you can write more readable, maintainable, and efficient code. Whether you are filtering, mapping, or reducing data, the Stream API simplifies your tasks and enables you to focus on what your code should do rather than how it should do it.

Related Posts

See All

Java Date and Time API Tutorial

Welcome to Code with Pankaj! In this tutorial, we'll explore the Java Date and Time API, introduced in Java 8. This API provides a...

Comentarios


bottom of page