Last Updated on August 20, 2023 by KnownSense
The Stream API, which was introduced with the release of Java 8, serves as a powerful tool for processing collections of objects. A stream in Java represents a sequence of objects and offers a range of methods that can be efficiently chained together to achieve specific outcomes.
Key aspects of the Java stream feature include:
- Data Source Flexibility: Rather than being a standalone data structure, a stream draws its input from diverse sources like Collections, Arrays, or I/O channels.
- Non-Destructive Transformation: Streams operate without altering the original data structure. Instead, they provide results based on the operations performed in the pipeline.
- Lazy Evaluation: Intermediate operations in a stream are executed lazily, only when necessary. These operations return new streams as output, enabling a seamless chaining of multiple intermediate operations. The terminal operations, positioned at the end of the pipeline, finalize the stream and yield the desired outcome.
You can use stream to filter, collect, print, and convert from one data structure to other etc. In the following examples, we have apply various operations with the help of stream.
Different Operations On Streams
1. Intermediate Operations:
These operations are performed on a stream and return a new stream as a result. They are typically used to transform or filter the data in the stream.
- map: This operation lets you transform each element in the stream into something else. For example, you could convert a list of names into a list of their lengths.
- filter: You can use this operation to pick specific elements from the stream based on a condition. For instance, selecting only numbers greater than 10 from a list.
- sorted: This operation arranges the elements in the stream according to a specified order.
- distinct: It filters out duplicate elements from the stream, giving you only unique items.
- flatMap: This is like a more advanced version of the
map
operation. It takes an element and produces multiple elements, which are then flattened into a single stream.
2. Terminal Operations:
These operations mark the end of the stream pipeline and provide a final result.
- forEach: This operation allows you to perform an action on each element of the stream.
- count: It returns the total number of elements in the stream.
- collect: This operation gathers the elements of the stream and puts them into a collection, like a list or a set.
- reduce: It combines the elements of the stream in a specific way to produce a single result. This is useful for finding sums, products, or any custom aggregation.
- anyMatch, allMatch, noneMatch: These operations check if any, all, or none of the elements in the stream satisfy a given condition.
- findFirst, findAny: They retrieve the first (or any) element from the stream.
3. Short-Circuiting Operations:
These operations are terminal and provide a result before processing all the elements in the stream. They are useful when you don’t need to process the entire stream to get an answer.
- anyMatch: This returns
true
if any element in the stream matches a condition. - allMatch: It returns
true
if all elements in the stream satisfy a condition. - noneMatch: This returns
true
if none of the elements in the stream meet a condition. - findAny: This returns an arbitrary element from the stream.
Remember, you can chain these operations together to create a pipeline. For example, you could filter out specific elements, map the remaining ones, sort them, and then perform a terminal operation like counting or collecting. Streams offer a flexible and powerful way to process data with minimal code.
Ways to create a Stream in Java
From Collection: The Java Collection framework provides two methods, stream()
and parallelStream()
, to create a sequential and parallel stream from any collection, respectively.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
// Program to create a stream in Java
class Main
{
public static void main(String[] args)
{
// input collection
List<String> collection = Arrays.asList("Coding", "Knownsense", ".com");
// create a sequential stream from the collection
Stream<String> stream = collection.stream();
stream.forEach(System.out::print);
// create a parallel stream from the collection
Stream<String> parallel_stream = collection.parallelStream();
System.out.println(Arrays.toString(parallel_stream.toArray()));
}
}
Output:
CodingKnownsense.com
[Coding, Knownsense, .com]
From specified values: We can create a stream from the specified values using the Stream.of()
method.
Stream<Integer> stream = Stream.of(1, 4, 2, 5, 8);
Stream<String> streams = Stream.of("I", "LOVE", "CODING");
From an array: We can use Arrays.stream() to create a sequential stream with the specified array as its source. stream() method is overloaded – the first version takes the whole array, and the other version takes a part of an array.
String[] arr = { "Coding", "Knownsense", ".com" ,"is", "love"};
Stream<String> stream = Arrays.stream(arr);
Stream<String> stream1 = Arrays.stream(arr,0,2); //take stream from index 0 to 2 (Coding, Knownsense, .com)
Empty stream: We can use Stream.empty()
or Stream.of()
method to create an empty stream in Java.
Stream<String> emptyStream = Stream.empty();
From Builder: We can use a mutable builder for creating an ordered Stream. A stream builder has a lifecycle, which starts in a building phase, during which we can add elements in the required order, and then transitions to a built phase when the build() method is called.
Stream<String> streamBuilder = Stream.<String>builder()
.add("Coding")
.add("Knownsense")
.build();
Infinite stream: We can use the iterate() method to make an endless list of numbers. We start with a first number called a seed and use a rule to get the next number from the previous one. If we want to stop this list at some point, we can use the limit() method.
Stream<Integer> IntegerStream = Stream.iterate(0, n -> n + 1)
.limit(10);
Stream<Double> DoubleStream = Stream.iterate(0.0, n -> n + 1.0)
.limit(5);
Another way of creating an infinite stream of any custom type elements is by passing a method of a Supplier interface to a generate() method on a stream.
// generate a stream of UUIDs
Stream<UUID> UUIDStream = Stream.generate(UUID::randomUUID)
.limit(2);
// generate a stream of random integers
Stream<Integer> randomIntStream =
Stream.generate(ThreadLocalRandom.current()::nextInt).limit(2);
// generates a stream of random doubles between 0 and 1
Stream<Double> randomDoubleStream = Stream.generate(Math::random)
.limit(4);
// Another way to generates a stream of random integers
Random random = new Random();
randomIntStream = Stream.generate(random::nextInt)
.limit(5);
From a sequence that matches a pattern: We can create a stream from an input sequence that matches a pattern using the Pattern.splitAsStream() method, as shown below:
String input = "Coding Knownsense";
Stream<String> stream = Pattern.compile("\\s")
.splitAsStream(input);
Conclusion:
In summary, the Stream API introduced in Java 8 offers a robust and efficient means to process collections of objects. Through its intermediate and terminal operations, it provides a flexible way to transform, filter, and aggregate data. This non-destructive and lazy evaluation approach ensures optimal resource utilization. Streams enable concise and readable code for complex data operations, contributing to better code maintainability. Understanding the fundamentals of Java Streams is essential as it empowers developers to streamline data processing tasks, and in the following section, we will delve into practical examples of implementing streams for a deeper comprehension.