Streams were introduced in Java 8 as a significant enhancement to the Java API, enabling the functional-style manipulation of collections and arrays. This Stream API in Java offers a sophisticated level of abstraction for the processing of data in a declarative and parallelizable fashion, resulting in more concise and expressive code. Join the Best JAVA Training Institutes in Chennai and learn one of the best programming languages from experienced professionals.
Characteristics of Streams in Java
Functional-Style Operations: Streams facilitate the use of functional-style operations like map, filter, and reduce, enabling developers to express data processing logic in a more declarative and easily comprehensible manner.
Pipelining: Streams enable the creation of pipelines by chaining multiple operations together, where an operation’s output serves as its subsequent operation’s input This promotes efficient and concise code without the need for intermediate collections.
Lazy Evaluation: Streams employ lazy evaluation, which means that intermediate operations are deferred until a terminal operation is invoked. This enhances efficiency by preventing unnecessary computations.
Parallel Processing: Streams can undergo parallel processing, leveraging multi-core architectures to enhance performance when dealing with substantial datasets. Parallel streams automatically divide data into multiple chunks and process them concurrently.
Streams in Java offer an effective and resource-efficient approach for data processing through a combination of sequential and parallel aggregate operations. The introduction of streams and stream classes in Java 8’s Stream API has provided specialized functionality for data manipulation and transformation. Developers can harness the Stream API in Java to create code that is not only more concise and expressive but also optimized for efficiency.
The Stream API in Java presents a comprehensive set of operations that can be applied to streams, encompassing filtering, mapping, and reducing. It furnishes a high-level abstraction for stream manipulation, offering a diverse array of operations for data processing.
Different Operations on Streams
Streams in Java offer a variety of operations that can be linked together to produce results. These operations are categorized into two main types:
- Intermediate Operations
- Terminal Operations
Intermediate Operations
Intermediate operations yield a stream as their output, and they are not executed until a terminal operation is called upon the stream. This deferred execution is known as lazy evaluation, which will be elaborated upon later.
filter()
The filter() method returns a stream containing elements from the original stream that satisfy a given predicate. A predicate is a functional interface in A Java function that returns a boolean value given a single input.
Example
public class Main {
public static void main(String[] args) {
final List list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
final List ans = list.stream()
.filter(value -> value % 2 == 0)
.collect(Collectors.toList());
System.out.println(Arrays.toString(ans.toArray()));
}
}
Output
[2, 4]
In this example, the filter() operation is used to create a new stream containing only the even numbers from the original list. The collect() terminal operation is then used to collect the filtered elements into a new list, resulting in the output [2, 4].
This demonstrates how intermediate operations, like filter(), allow for data transformation and selection before executing terminal operations, ensuring efficiency and flexibility in stream processing.
Explanation
In this explanation, we’ll discuss the map() method in the context of Java streams and provide an example of its usage.
map() Method
When working with Java streams, the map() method can be used to apply a given function to each element in the stream and create a new stream that contains the changed components. It applies the given function to each element and generates a one-to-one mapping between the original elements and the transformed elements.
Example
public class Main {
public static void main(String[] args) {
final List list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
final List ans = list.stream()
.map(value -> value * 10)
.collect(Collectors.toList());
System.out.println(Arrays.toString(ans.toArray()));
}
}
Output:
[10, 20, 30, 40, 50]
Explanation of the Example
In this example, we have a list of integers from 1 to 5. We use the stream() method to convert the list into a stream. Then, we apply the map() intermediate operation to transform each element of the stream. The transformation is done by multiplying each element by 10.
Here’s a breakdown of the steps
- The map() operation applies the lambda function value -> value * 10 to each element in the stream, resulting in a new stream with the elements [10, 20, 30, 40, 50].
- The collect() terminal operation is used to collect these transformed elements into a List<Integer> called ans.
- Finally, we print the contents of ans, which contains the transformed values [10, 20, 30, 40, 50].
The map() operation is a powerful tool for transforming data within a stream, allowing you to apply custom logic to each element. It is commonly used for data manipulation and conversion when working with streams.
Explanation
In this explanation, we’ll discuss the sorted() method in the context of Java streams and provide an example of its usage.
sorted() Method
The sorted() method is an intermediate operation in Java streams used to sort the elements of the stream either in natural order (ascending order) or according to a provided Comparator (custom order).
Example
public class Main {
public static void main(String[] args) {
final List list = new ArrayList<>(Arrays.asList(5, 1, 3, 4, 2));
System.out.println("Ascending Order");
list.stream().sorted()
.forEach(System.out::println);
System.out.println("\nDescending Order");
list.stream().sorted(Comparator.reverseOrder())
.forEach(System.out::println);
}
}
Output
Ascending Order
1
2
3
4
5
Descending Order
5
4
3
2
1
Explanation of the Example
In this example, we have a list of integers that is initially unsorted. We use the stream() method to convert the list into a stream.
- Ascending Order: We apply the sorted() operation to sort the elements in ascending (natural) order. The output stream contains the elements sorted in ascending order, and we use forEach() to print them in this order.
- Descending Order: We apply the sorted(Comparator.reverseOrder()) operation to sort the elements in descending order. Here, we provide a Comparator that reverses the natural order. The output stream contains the elements sorted in descending order, and we use forEach() to print them in this order.
The sorted() operation is useful for arranging the elements of a stream in a desired order, whether it be in ascending, descending, or a custom order defined by a Comparator. It provides flexibility in sorting elements within a stream. Enrol for the Java Course in Bangalore and learn Java from the fundamentals with our professionals
2. Terminal Operations
Terminal operations are the concluding operations in a stream pipeline. They generate the final results of the stream after all the intermediate operations have been executed, and once a terminal operation is invoked, the stream can no longer be used. One such terminal operation is forEach().
forEach()
The forEach() method iterates over each element in the stream and performs a specified action for each element. In the case of a parallel stream, it does not guarantee maintaining the order of the stream.
Example
public class Main {
public static void main(String[] args) {
final List list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
list.stream().forEach(System.out::println);
}
}
Output
1
2
3
4
5
In this example, the forEach() terminal operation is used to iterate through the elements of the list and print each element. It demonstrates how terminal operations are responsible for producing results and performing actions on the elements of a stream.
Keep in mind that once a terminal operation like forEach() is applied, the stream becomes consumed and cannot be reused for further operations.
Explanation
In this explanation, we’ll look at the forEach() and forEachOrdered() methods in the context of parallel streams in Java.
forEach() Method
The forEach() method is used to iterate through all the elements in a stream and perform a specified action for each element. When applied to a parallel stream, it does not guarantee the order of element processing. Elements may be processed in parallel, and their order in the output might not match the original order in the stream.
forEachOrdered() Method
The forEachOrdered() method is quite similar to forEach(), as it is used to iterate through and perform actions on each element of a stream. However, the key difference is that forEachOrdered() ensures that the order of element processing in a parallel stream matches the original order of the stream.
Example
public class Main {
public static void main(String[] args) {
Stream.of("A", "B", "C")
.parallel()
.forEach(x -> System.out.println("forEach: " + x));
Stream.of("A", "B", "C")
.parallel()
.forEachOrdered(x -> System.out.println("forEachOrdered: " + x));
}
}
Output
forEach: B
forEach: C
forEach: A
forEachOrdered: A
forEachOrdered: B
forEachOrdered: C
In this example, we have a parallel stream of strings “A,” “B,” and “C.” When using forEach() on the parallel stream, the order of output (“B,” “C,” “A”) does not match the original order of elements in the stream. This demonstrates the non-deterministic nature of parallel processing.
On the other hand, when using forEachOrdered(), the output order (“A,” “B,” “C”) matches the original order of elements in the stream. This is particularly useful when you want to maintain order in parallel stream processing.
In summary, forEach() is suitable for parallel streams when order doesn’t matter, while forEachOrdered() is used to ensure that the order of processing matches the original order of elements in the stream when working with parallel streams.
Explanation
In this explanation, we’ll discuss the collect() method in the context of Java streams and provide an example of its usage.
Collect() Method
The collect() method in Java is used to perform a mutable reduction operation on the elements of a stream using a Collector. Mutable reduction means that the result of the reduction operation is a mutable result container, such as a List in this case.
Collector
A Collector is a class in Java that provides various reduction operations, including accumulating elements into collections, summarizing elements, and more. It encapsulates the logic for collecting elements from a stream into a mutable container.
Example:
public class Main {
public static void main(String[] args) {
List evenList = Stream.of(1, 2, 3, 4, 5)
.filter(x -> x % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenList);
}
}
Output
[2, 4]
Explanation of the Example
In this example, we have a stream of integers from 1 to 5. We use the filter() intermediate operation to select only the even numbers from the stream. Then, we apply the collect() terminal operation to collect the filtered elements into a List.
Here’s a breakdown of the steps:
- The filter() operation filters the stream, keeping only the even numbers (2 and 4).
- The collect() operation takes the filtered elements and collects them into a List<Integer> called evenList.
- Finally, we print the contents of evenList, which contains the even numbers [2, 4].
The collect() method, in this case, is used to transform the stream into a more structured and mutable form, allowing you to work with the filtered elements in a convenient manner. It’s a common operation when working with streams to collect and organize data for further processing or reporting. Start learning Java from FITA Academyโs Java Training in Delhi to be the best in the field of java development
Different Ways to Create Streams in Java
Stream.empty()
Java provides the Stream.empty() method to create an empty stream. An empty stream is a stream that contains no elements. This is useful in situations where you want to avoid null pointer exceptions when calling methods that expect a stream as a parameter, and you plan to add objects to the stream later in your program.
Syntax
Stream stream = Stream.empty();
Here, T represents the type of elements that the stream will contain.
Example
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream stream = Stream.empty();
System.out.println("Size: " + stream.count());
}
}
Output:
makefile
Copy code
Size: 0
Explanation of the Example
In this example, we create an empty stream of type Integer using Stream.empty(). Since this stream is empty, it does not contain any elements. We then use the count() terminal operation to count the number of elements in the stream. As expected, the count is 0 because the stream is empty.
Creating empty streams is useful when you want to initialise a stream variable but do not have any initial data to put into it. Later in your program, you can add elements to the stream using various stream operations and methods.
Explanation
In this explanation, we’ll discuss how to create a stream using the Stream.builder() method in Java and provide an example of its usage.
Stream.builder()
Java provides the Stream.builder() method to create a stream using a builder pattern. This approach allows you to construct a stream step-by-step, adding objects to it as needed. The builder is initially empty,and you may use the add() method to add elements to it. Once you’ve added all the elements you want, you can call the build() method on the builder to create an instance of the Stream.
Syntax
Stream.Builder builder = Stream.builder();
Stream stream = builder.build();
Here, T represents the type of elements that the stream will contain.
Example
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream.Builder builder = Stream.builder();
builder.add("Tony Stark")
.add("Steve Rogers")
.add("Thor Odinson");
Stream stream = builder.build();
stream.forEach(System.out::println);
}
}
Output
Tony Stark
Steve Rogers
Thor Odinson
Explanation of the Example
In this example, we use Stream.builder() to create a builder for a stream of strings.Then, we add three strings to the builder using the add() method: “Tony Stark,” “Steve Rogers,” and “Thor Odinson.”
After adding the desired elements, we call builder.build() to create an instance of the Stream<String>. Finally, we use forEach() to iterate over the elements in the stream and print them to the console.
The Stream.builder() approach is useful when you need to construct a stream dynamically by adding elements to it as your program progresses, making it a flexible way to work with streams.
Comparison-Based Stream Operations
- min and max
- The min and max terminal operations are used to find the minimum and maximum elements of a stream, respectively, based on a specified comparator.
- In the example provided, a list of integers is used, and min and max are calculated using the Integer::compare comparator.
- The min and max operations return Optional values because the stream might be empty, and there may not be a minimum or maximum element.
- The orElse() method is used to provide a default value if the Optional is empty.
- distinct
- The distinct terminal operation filters out duplicate elements from a stream based on their natural order or a provided comparator.
- In the example, a list of integers contains duplicates, and distinct() is used to remove them.
- The result is collected into a new list using collect(Collectors.toList()).
- allMatc, anyMatch, and noneMatch
These terminal operations are used to check conditions on elements in a stream:
- allMatch checks if a given condition is true for all elements.
- anyMatch checks if the condition is true for any element.
- noneMatch checks if the condition is false for all elements.
In the example, a list of integers is used to demonstrate these operations with conditions related to even numbers and negativity.
Here’s a summary of the outputs in the provided examples:
- min finds the minimum element (1) and prints it.
- max finds the maximum element (8) and prints it.
- distinct removes duplicates, resulting in a list of distinct numbers.
- allMatch checks if all numbers are even (false).
- anyMatch checks if any number is even (true).
- noneMatch checks if none of the numbers are negative (true).
These terminal operations are valuable for performing specific tasks and checks on stream elements, making stream processing in Java flexible and powerful.They return a Boolean value indicating the result. For instance:
Example
List numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean allEven = numbers.stream().allMatch(num -> num % 2 == 0);
boolean anyEven = numbers.stream().anyMatch(num -> num % 2 == 0);
boolean noneNegative = numbers.stream().noneMatch(num -> num < 0);
Output
System.out.println(allEven); // Output: false
System.println(anyEven); // Output: true
System.println(noneNegative); // Output: true
```
In this example, `allMatch` checks if all numbers are even (false), `anyMatch` checks if any number is even (true), and `noneMatch` checks if none of the numbers are negative (true).
Stream Short-circuit Operations
In this explanation, we’ll discuss short-circuit operations in Java streams, both intermediate and terminal, and also briefly touch on parallel streams.
Short-circuit operations in Java streams are those that can terminate without processing all the elements in a stream. They are inspired by Java’s logical && and || operators, where the second expression is not evaluated if the outcome is already determined by the first expression (e.g., false && anything is always false).
Short-circuit operations in streams can be classified into two types:
Intermediate Short-Circuit Operations
- These operations produce a finite stream from an infinite stream. The primary intermediate short-circuit operation in Java streams is limit().
- The limit() operation restricts the number of elements in the stream, making it finite.
Example
public class Main {
public static void main(String[] args) {
Stream stream = Stream.of(1, 2, 3, 4, 5);
stream.limit(3).forEach(System.out::println);
}
}
Output
1
2
3
Explanation
The limit(3) operation reduces the stream size from 5 to 3.
Terminal Short-Circuit Operations
- Terminal short-circuit operations are those that can produce a result before processing all the elements of the stream. Examples include findFirst(), findAny(), allMatch(), anyMatch(), and noneMatch().
- These operations can stop processing once the result is determined.
Example
public class Main {
public static void main(String[] args) {
Stream stream = Stream.of(1, 2, 3, 4, 5);
boolean ans = stream.peek(x -> System.out.println("Checking " + x))
.anyMatch(x -> x == 3);
System.out.println("Is 3 present: " + ans);
}
}
Output
Checking 1
Checking 2
Checking 3
Is 3 present: true
Explanation
The anyMatch(x -> x == 3) operation halts once it finds the element 3 and doesn’t process the remaining stream elements.
Parallel Streams
By default, all stream operations in Java are sequential, meaning they are processed one after the other. However, you can explicitly specify that a stream should be processed in parallel by using the parallel() method on a sequential stream or by calling parallelStream() on a collection.
Parallel streams are useful when you have many independent tasks that can be processed simultaneously, reducing the running time of the program.
Example
public class Main {
public static void main(String[] args) {
Stream stream = Stream.of(1, 2, 3, 4, 5);
stream.parallel().forEach(System.out::println);
}
}
Output:
3
5
4
1
2
In this example, the parallel() method is called on the sequential stream stream to make it parallel. As a result, the elements are processed in parallel, potentially improving performance for tasks that can be parallelized.
Parallel streams are especially valuable when dealing with large datasets and tasks that can be split into smaller, independent units of work. Join Java Training in Pune and pick up one of the top programming languages from knowledgeable experts.
Method Types and Pipelines
In this explanation, we’ll delve into the concepts of intermediate and terminal operations, short-circuiting operations, pipelines, and lazy evaluation in Java streams, using your provided example.
Intermediate Operations
- Intermediate operations in Java streams transform or filter elements in a stream, producing a new stream as the output.
- Examples include filter, map, distinct, sorted, and limit.
- These operations modify the stream but do not produce a final result.
Terminal Operations
- Terminal operations in Java streams produce a result or side effect, marking the end of the stream processing.
- Examples include forEach, collect, reduce, count, min, max, anyMatch, allMatch, and noneMatch.
- These operations trigger the processing of the stream and produce a final result.
Short-Circuiting Operations
- Short-circuiting operations in Java streams can terminate stream processing early based on a condition.
- Examples include findFirst, findAny, and limit.
- These operations can optimize stream processing by stopping when a certain condition is met.
Pipelines
- Pipelines in Java streams chain together intermediate and terminal operations to process data in a fluent and expressive manner.
- Each operation takes input from the previous operation and produces output for the next.
- Pipelines allow for concise and declarative coding, enabling complex data transformations with ease.
Lazy Evaluation
- Lazy evaluation (also known as call-by-need evaluation) is an evaluation strategy that delays the evaluation of an expression until its value is needed.
- In Java streams, lazy evaluation means that intermediate operations are performed on the stream only when a terminal operation is invoked on it.
- Lazy evaluation allows for significant optimizations because it avoids unnecessary computation.
Example of Using Method Types and Pipeline in Streams
List numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.reduce(0, Integer::sum);
System.out.println(sum); // Output: 12
Explanation of the Example
- The stream pipeline starts with the stream() method on the numbers list.
- It applies the filter intermediate operation to keep only the even numbers.
- Then, it uses the map operation to double each number.
- Finally, the reduce terminal operation sums up all the elements in the stream.
- The output is the sum of the doubled even numbers, which is 12.
Java streams a combination of intermediate and terminal operations, along with lazy evaluation, allows for efficient and expressive data processing in a declarative manner.