Hello everyone,

Let's continue Stream API with intermediate operations.Stream API supported for two major operations. They are,

1) Intermediate Operation.

2) Terminal Operation.

In this tutorial I will go to the depth in the Intermediate Operations. Before continuing this tutorial I recommend you to read my Stream API tutorial.

Intermediate Operation.

When you perform an action on a stream that creates another stream, it's called an intermediate operation. These operations don't give a final result but produce a new stream. You can link them together to create a sequence of operations, like a pipeline. The term "intermediate" means these actions transform one stream into another.

Ex :- map(), filter(), distinct(), stored(), limit(), skip()

How to use Intermediate Operation In stream.

filter() Operation

Syntax

Stream<T> filter(Predicate<? super T> predicate
  • T represents the type of elements in the stream.
  • Predicate is a stateless function that evaluates each element in the stream and determines whether it should be included or excluded.

This operation allows us to filter elements from a stream based on a given condition.Remember that filter() creates a new stream without modifying the original one.

Example 1)
Filtering the elements divided by two.
public class Main {
   public static void main(String[] args) {
       List<Integer> intList = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
       List<Integer> collect = intList.stream().filter(num -> num % 2 == 0).collect(Collectors.toList());
       collect.forEach(System.out::println);
   }
}

Output

2 4 6 8 10

Filtering the elements with an uppercase letter at index 1
public class Main {
    public static void main(String[] args) {
           Stream<String> nameStream = Stream.of("Nimal", "kMal", "SUNil", "Abishek");
           nameStream.filter(str -> Character.isUpperCase(str.charAt(1)))
                   .forEach(System.out::println);
    }
}

Output :

kMal SUNil

distinct() Operation

The distinct() function returns the unique values from the original stream. In other words, it removes the duplicates. This distinct method uses the hashcode() and equals() method to determine the distinctness.

Syntax

Stream<T> distinct()
  • Here, T represents the type of elements in the stream.
Example 1)
Remove duplicate numbers from list.
public class Main {
   public static void main(String[] args) {
       List<Integer> duplicateItemList = Arrays.asList(1, 5, 2, 3,4, 3, 1, 5);
       System.out.println("Unique elements:");
       duplicateItemList.stream().distinct().forEach(System.out::println);
   }
}

Output :

Unique elements: 1 5 2 3 4

Example 2)
Remove Duplicate Names from the list
public class Main {
   public static void main(String[] args) {
       List<String> list = Arrays.asList("Nimal", "Kamal", "Sunil", "Nimal", "Amal", "Kamal");
       System.out.println("Unique Names:");
       list.stream().distinct().forEach(System.out::println);
   }
}

Output :

Unique Names: Nimal Kamal Sunil Amal

map() and flatMap() Operations

map() transforms each input element into a single output value.It performs one-to-one mapping. Use map() when you need to apply a transformation to each element of a collection and return a stream containing the updated results.

flatmap() transforms each input element into zero or more output values.it performs one-to-many mapping. Use flatMap() when you need to flatten or transform a stream, especially when dealing with nested structures.

Example 1)

In this example, map() is used to convert list integers to their squares.

public class Main {
    public static void main(String[] args) {
           List<Integer> listOfNumbers = Arrays.asList(1, 2, 3, 4, 5);
           List<Integer> listOfSquares = listOfNumbers.stream()
                   .map(x -> x * x)
                   .collect(Collectors.toList());
           System.out.println("Original List: " + listOfNumbers);
           System.out.println("List of Squares: " + listOfSquares);
    }
}
Example 2)

In this example we have a list of words. Using these words we can create a list of all distinct letters.

public class Main {
   public static void main(String[] args) {
       List<String> listOfWords = Arrays.asList("Hello", "World");
       List<String> distinctLetters = listOfWords.stream()
               .map(word -> word.split(""))
               .flatMap(Arrays::stream)
               .distinct()
               .collect(Collectors.toList());
       System.out.println("Original List of Words: " + listOfWords);
       System.out.println("List of Distinct Letters: " + distinctLetters);
   }
}
Example 3)

For example, let's assume we have a list of books, and each book has a title and a list of authors. We’ll use map() function to extract book titles and flatMap() to extract distinct authors across all books.

public class Main {
   public static void main(String[] args) {
       List<Book> listOfBooks = Arrays.asList(
               new Book("Book1", Arrays.asList("Author1", "Author2")),
               new Book("Book2", Arrays.asList("Author2", "Author3")),
               new Book("Book3", Arrays.asList("Author3", "Author4"))
       );

       // Extracting book titles using map()
       listOfBooks.stream()
               .map(Book::getTitle)
               .collect(Collectors.toList())
               .forEach(System.out::println);
       System.out.println("\nExtracting distinct authors using flatMap():");
       
       // Extracting distinct authors using flatMap()
       listOfBooks.stream()
               .flatMap(book -> book.getAuthors().stream())
               .distinct()
               .forEach(System.out::println);
   }
}

Output :

Book1 Book2 Book3

Extracting distinct authors using flatMap():

Author1 Author2 Author3 Author4

In this example , map(Book:getTitle) is used to extract the titles of all books, and flatMap (book -> book.getAuthors().stream()) is used to extract distinct authors across all books. The use of distinct() ensures that each author is printed only once, even if they have authored multiple books in the list.

sorted() Operation

This function allows you to sort elements in a collection (such as a list) based on a specified ordering.

Syntax

Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)

We can sort the stream using the default order with the first method signature. However, the second method signature allows us to pass a Comparator and sort the stream according to our needs. For example, the code snippet below demonstrates the concept using the stream API in Java 8.

Example 1)

Below code snippet explains the first syntax

public class Main {
    public static void main(String[] args) {
           Stream<String> streamOfStrings = Stream.of("Sunil", "Mike", "Wonka", "Frank");
           streamOfStrings.sorted().forEach(System.out::println); 
    }
}

Output :

Frank Mike Sunil Wonka

Example 2)

Below code snippet explains the second syntax.

class Person {
    String name;
    int age;
    
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    
    @Override
    public String toString() {
        return "Person{" + "name='" + name + "', age=" + age + '}';
    }
}

public class Main {
   public static void main(String[] args) {
   
       List<Person> people = Arrays.asList(
               new Person("Alice", 30),
               new Person("Bob", 25),
               new Person("Charlie", 35)
       );

       // Sorting by age
       List<Person> sortedPeople = people.stream()
               .sorted((p1, p2) -> Integer.compare(p1.age, p2.age))
               .collect(Collectors.toList());

       System.out.println("Sorted People by Age: " + sortedPeople);
   }
}

In this example the sorted() method is applied with a comparator (p1, p2) -> Integer.compare(p1.age, p2.age). This comparator compares two Person objects based on their ages. Finally, the collect(Collectors.toList()) is used to collect the sorted elements into a new list.

Output :

Sorted People by Age: [Person{name='Bob', age=25}, Person{name='Alice', age=30}, Person{name='Charlie', age=35}]

peek() Operation

Method Signature

Stream<T> peek(Consumer<? super T> action)

This method allows us to observe elements as they flow through the stream pipeline. Unlike other intermediate operations, peek() does not modify the stream; it simply provides a way to inspect the elements.The primary purpose of peek() is for debugging and logging during stream processing.It takes a Consumer as an argument, where T represents the type of elements in the stream.Think sometimes we need to modify an element’s internal state (e.g., change a property), To this kind of this peek() can be handy.

Let’s consider a simple example. Suppose we have a list of integers, and we want to log each element before collecting them into a new list:

public class Main {
   public static void main(String[] args) {
   
       List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

       List<Integer> newList = numbers.stream()
               .peek(System.out::println) // Log each element
               .collect(Collectors.toList());

       System.out.println("New list: " + newList);
   }
}    

Remember that peek() is an intermediate operation, so it won’t trigger any processing until a terminal operation (like collect() or forEach()) is applied.

Now you have a better understand about the intermediate operation in Stream API, how to use it, and the how to use that function using real world examples. In the next Tutorial, I will talk about Terminal Operations in Stream API (Terminal Operation).