Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Stream API | enum & Stream API
Java Data Structures
course content

Course Content

Java Data Structures

Java Data Structures

1. Basic Data Structures
2. Additional Data Structures
3. Map
4. enum & Stream API

bookStream API

There are various ways to process data in Java – loops, methods, and different algorithms. However, in Java 8, a very powerful tool was introduced – the Stream API.

In simple terms, the Stream API is a way to work quickly and easily with a stream of information. In our case, this stream of information is represented by collections. The Stream API has some concepts. Here are the main ones.

Main concepts

  • Stream: Represents a sequence of data elements that can be processed;
  • Intermediate Operations: Operations that create a new stream after their execution. Examples: filter, map, distinct, sorted;
  • Terminal Operations: Operations that complete the processing of the stream and return a result. Examples: collect, forEach, count, reduce;
  • Parallel Streams: Allow parallel processing of data. Methods parallel() and parallelStream() are used to create parallel streams.

Enough talking about theory, let's start coding!

Declaring a stream is done by using a method on the collection we want to turn into a stream:

java

main

copy
12
List<String> strings = Arrays.asList("a", "b", "c"); Stream<String> stream = strings.stream();

With the stream() method, we obtained a stream of strings. But to start working with the stream, we need to understand what lambda expressions are, as stream methods mainly work with them.

Lambda Expressions

Lambda expressions were introduced in Java 8, and they represent a simplified form of creating anonymous functions in Java. We haven't covered anonymous functions before as they were not highly necessary, but now we'll get acquainted with them through lambda expressions.

Lambda Expression Syntax:

The general syntax for lambda expressions in Java looks like this:

java

example

copy
123
(parameters) -> expression // or (parameters) -> { statements; }
  • Parameters: This is a parameter list that can be empty or contain one or more parameters;
  • Arrow: Represented by the symbol ->, which separates the parameters from the body of the lambda expression;
  • Expression or Statements: This is the body of the function, containing an expression or a block of statements;

Here's an example of a lambda expression representing a simple function that adds two numbers:

java

example

copy
12345678910
// Traditional way MathOperation addition = new MathOperation() { @Override public int operate(int a, int b) { return a + b; } }; // Using a lambda expression MathOperation addition = (int a, int b) -> a + b;

Let's take a closer look at what exactly is happening in the code above and how we use lambda expressions:

java

main

copy
1234567891011121314
package com.example; // Functional interface with a single abstract method interface MyMathOperation { int operate(int a, int b); } public class Main { public static void main(String[] args) { // Using a lambda expression to implement the interface MyMathOperation addition = (a, b) -> a + b; System.out.println("Sum: " + addition.operate(5, 3)); } }

Explanation:

  • Created a functional interface MyMathOperation with a single abstract method operate;
  • Used a lambda expression to implement this method, performing the addition of two numbers;
  • Printed the result of the addition.

I understand that it might be challenging to grasp what is happening in this code for now, but let's move on back to the Stream API, where lambda expressions are frequently used, and try to understand how to use them in practice.

As you remember, earlier, we created a stream of strings from a list of strings. Now, let's use stream methods to make each string in this stream uppercase:

java

main

copy
12345678910111213
package com.example; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class Main { public static void main(String[] args) { List<String> strings = Arrays.asList("a", "b", "c"); Stream<String> stream = strings.stream(); stream.map(e -> e.toUpperCase()).toList(); } }

In the code above, we used a lambda expression and two methods: map() and toList(). If it's clear what the toList() method does, the map() method changes each element in the stream according to the provided lambda expression.

Let's take a closer look at how the lambda expression works here:

The map() method applies the toUpperCase() method to each element of the stream. We defined the element of this stream as e and, using the lambda expression, instructed the program to apply this method to each element.

But this is not the end yet because we applied an intermediate operation. This means that operations on the stream are not yet completed. To complete the work on the stream, we need to apply a terminal operation, which will finish the operations on the stream and return a specific value. For example, we can use the toList() method, and the modified stream will be converted into a list.

For example:

java

main

copy
1234567891011121314
package com.example; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class Main { public static void main(String[] args) { List<String> strings = Arrays.asList("a", "b", "c"); Stream<String> stream = strings.stream(); List<String> list = stream.map(e -> e.toUpperCase()).toList(); System.out.println(list); } }

Note

Notice that after using the terminal operation, we can no longer use stream methods. In our case, after the terminal operation toList(), our stream was converted into a list, so we cannot use stream methods on the list.

Let's take a closer look at possible intermediate operations in the stream.

Intermediate operations

  • The map() method - you are already familiar with this method; it performs operations specified by the lambda expression on each element in the stream.

For example, let's use the substring() method on each element in the stream of strings:

java

main

copy
123456789101112131415
package com.example; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class Main { public static void main(String[] args) { List<String> strings = Arrays.asList("Unlock", "Infinity", "with", "Codefinity"); System.out.println("List of strings: " + strings); Stream<String> stream = strings.stream(); List<String> list = stream.map(e -> e.substring(1, 4)).toList(); System.out.println("Modified list: " + list); } }
  • The filter() method takes a lambda expression with a condition based on which the stream will be filtered. In other words, all elements that meet the condition will remain in the stream, and elements that do not meet the condition will be removed from the stream. Let's modify the stream to keep only the elements whose length is greater than 5:
java

main

copy
12345678910111213141516
package com.example; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class Main { public static void main(String[] args) { List<String> strings = Arrays.asList("Unlock", "Infinity", "with", "Codefinity"); System.out.println("List of strings: " + strings); Stream<String> stream = strings.stream(); stream = stream.filter(e -> e.length() > 5); List<String> list = stream.map(e -> e.substring(1, 4)).toList(); System.out.println("Modified list: " + list); } }

Using the filter() method, we remove the string "with" from the stream because this word is less than 5 characters.

You can also use intermediate operations multiple times in a row.

For example, we can slightly simplify the above code:

java

main

copy
12345678910111213141516
package com.example; import java.util.Arrays; import java.util.List; public class Main { public static void main(String[] args) { List<String> strings = Arrays.asList("Unlock", "Infinity", "with", "Codefinity"); System.out.println("List of strings: " + strings); List<String> list = strings.stream() .filter(e -> e.length() > 5) .map(e -> e.substring(1, 4)) .toList(); System.out.println("Modified list: " + list); } }

When chaining multiple stream methods together, it is recommended to put each method on a new line to significantly improve code readability.

  • The flatMap() method transforms each element of a stream into a new stream and combines the results into a single stream. In other words, with this method, we can split the stream into streams, and then they will be merged into one stream. For example, we have a list of strings where each string may contain more than one word, such as a list of first and last names. And we need to capitalize the first letter of each of these words:
java

main

copy
123456789101112131415161718192021222324
package com.example; import java.util.Arrays; import java.util.List; public class Main { public static void main(String[] args) { List<String> users = Arrays.asList("Ethan Johnson", "Olivia smith", "mason davis", "Ava taylor", "logan brown", "Emma Anderson", "jackson miller"); System.out.println("List of users: " + users); List<String> list = users.stream() .flatMap(e -> Arrays.stream(e.split(" "))) .map(e -> capitalizeFirstLetter(e)) .toList(); System.out.println("List with capitalized names and surnames: " + list); } private static String capitalizeFirstLetter(String word) { if (word == null || word.isEmpty()) { return word; } return Character.toUpperCase(word.charAt(0)) + word.substring(1); } }

In the code above, we wrote a separate private method that capitalizes the first letter in a word and used this method in the map() method along with a lambda expression.

Note that using the flatMap method, we split each element of the stream into different streams using the Arrays.stream(e.split(" ")) method. Because the split() method returns an array, we need to use the Arrays.stream() method to split this array into streams.

Then, all these streams are merged into one stream, after which we use the method we wrote. Voila, now we have all the names and surnames of users with the first letter capitalized.

You know what would be cool? If we put these names and surnames into a HashMap, where the key is the surname and the value is the first name.

Let's implement this in the code:

java

main

copy
12345678910111213141516171819202122232425262728293031323334
package com.example; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; public class Main { public static void main(String[] args) { List<String> users = Arrays.asList("Ethan Johnson", "Olivia smith", "mason davis", "Ava taylor", "logan brown", "Emma Anderson", "jackson miller"); System.out.println("List of users: " + users); List<String> list = users.stream() .flatMap(e -> Arrays.stream(e.split(" "))) .map(e -> capitalizeFirstLetter(e)) .toList(); System.out.println("List with capitalized names and surnames: " + list); Map<String, String> usersKeyValue = new HashMap<>(); for (int i = 0; i < list.size() - 1; i+=2) { String name = list.get(i); String surname = list.get(i + 1); usersKeyValue.put(surname, name); } System.out.println("Map with surnames as keys and names as values: " + usersKeyValue); } private static String capitalizeFirstLetter(String word) { if (word == null || word.isEmpty()) { return word; } return Character.toUpperCase(word.charAt(0)) + word.substring(1); } }

With a simple loop, we stored the first name and last name in variables and then in the map. Note how the loop works. We increment the variable i by 2 in each iteration because we need to skip the last name once we've already recorded it.

Note

This method of filtering data is quite risky because data can be recorded in the wrong order, but in our case, it doesn't play a significant role.

  • The distinct() method removes duplicates from the stream. In general, this can be useful if you need unique elements in the stream or if you want to quickly eliminate duplicates from a list. You can easily achieve this with the following construct:
  • The sorted method sorts all elements in the stream in natural order, from the smallest to the largest number or in alphabetical order. This can also be useful if you need a sorted stream or if you need to quickly sort a list;
  • The skip(n) method skips the first n elements of the stream. This can be useful when working with text files, where the first n lines might be, for example, metadata or a file description. It's also worth mentioning the limit(n) method, which generally limits the number of elements in the stream. Even if we create a stream with 1000 elements and then use limit(200), the stream will contain only the first 200 elements.
java

main

copy
123456789101112
package com.example; import java.util.Arrays; import java.util.List; public class Main { public static void main(String[] args) { List<Integer> example = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); example = example.stream().skip(3).limit(5).toList(); System.out.println("List: " + example); } }

These are the main intermediate methods you'll need to use. You can explore the rest of the methods by referring to the link to the official Java documentation. Let's move on to terminal methods.

Terminal Methods

  • The terminal method you're already familiar with is toList(). It converts the stream into a list and returns it. In other words, we can directly assign this stream with methods to a list. This method was introduced in Java 17 and serves as a replacement for the more complex construct collect(Collectors.toList());
  • The collect() method also converts the stream into a specific data structure. It uses, as a parameter, a method from the Collectors interface. This interface has methods such as toList(), toSet(), and toCollection(). For example:
java

main

copy
123456789101112131415
package com.example; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { List<Integer> example = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Set<Integer> integerSet = example.stream().collect(Collectors.toSet()); System.out.println("List: " + example); System.out.println("Set: " + integerSet); } }

The forEach() method takes a lambda expression and performs a specific action for each element in the stream.

For example:

java

main

copy
1234567891011
package com.example; import java.util.Arrays; import java.util.List; public class Main { public static void main(String[] args) { List<Integer> example = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); example.stream().forEach(e -> System.out.println(e + 1)); } }

The difference between this method and the map method is that this method is terminal, and after it, you cannot call other methods.

These are all the basic methods for working with streams. It's a complex topic, and you may not grasp it immediately. However, it's a subject that is mastered through practice. In the upcoming practice chapters with streams, you will have plenty of opportunities to work with them, as it's a very convenient and practical way to manipulate lists and arrays of data!

1. What is the primary purpose of the Stream API in Java?
2. Which of the following is a terminal operation in the Stream API?
3. What does the `map` operation do in the Stream API?
4. How is the `flatMap` operation different from `map` in the Stream API?
5. What does the `filter` operation do in the Stream API?
6. What is the purpose of the `forEach` operation in the Stream API?
7. Which of the following is an intermediate operation in the Stream API?
8. How is the `limit` operation used in the Stream API?
What is the primary purpose of the Stream API in Java?

What is the primary purpose of the Stream API in Java?

Select the correct answer

Which of the following is a terminal operation in the Stream API?

Which of the following is a terminal operation in the Stream API?

Select the correct answer

What does the `map` operation do in the Stream API?

What does the map operation do in the Stream API?

Select the correct answer

How is the `flatMap` operation different from `map` in the Stream API?

How is the flatMap operation different from map in the Stream API?

Select the correct answer

What does the `filter` operation do in the Stream API?

What does the filter operation do in the Stream API?

Select the correct answer

What is the purpose of the `forEach` operation in the Stream API?

What is the purpose of the forEach operation in the Stream API?

Select the correct answer

Which of the following is an intermediate operation in the Stream API?

Which of the following is an intermediate operation in the Stream API?

Select the correct answer

How is the `limit` operation used in the Stream API?

How is the limit operation used in the Stream API?

Select the correct answer

Everything was clear?

How can we improve it?

Thanks for your feedback!

Section 4. Chapter 3
some-alt