1. List of methods of the Stream
class
The Stream
class was created to make it easy to construct chains of data streams. To achieve this, the Stream<T>
class has methods that return new Stream
objects.
Each of these data streams does one simple action, but if you combine them into chains and add interesting lambda functions, then you have a powerful mechanism for generating the output you want. Soon you will see for yourself.
Here are the methods of the Stream
class (only the most basic ones):
Methods | Description |
---|---|
|
Creates a stream from a set of objects |
|
Generates a stream according to the specified rule |
|
Concatenates two streams |
|
Filters the data, only passing along data that matches the specified rule |
|
Removes duplicates. Does not pass along data that has already been encountered |
|
Sorts the data |
|
Performs an action on each element in the stream |
|
Returns a stream that is truncated so that it is no longer than the specified limit |
|
Skips the first n elements |
|
Converts data from one type to another |
|
Converts data from one type to another |
|
Checks whether there is at least one element in the stream that matches the specified rule |
|
Checks whether all the elements in the stream match the specified rule |
|
Checks whether none of the elements in the stream match the specified rule |
|
Returns the first element found that matches the rule |
|
Returns any element in the stream that matches the rule |
|
Searches for the minimum element in the data stream |
|
Returns the maximum element in the data stream |
|
Returns the number of elements in the data stream |
|
Reads all the data from the stream and returns it as a collection |
2. Intermediate and terminal operations by the Stream
class
As you can see, not all methods in the table above return a Stream
. This is related to the fact that the methods of the Stream
class can be divided into intermediate (also known as non-terminal) methods and terminal methods.
Intermediate methods
Intermediate methods return an object that implements the Stream
interface, and they can be chained together.
Terminal methods
Terminal methods return a value other than a Stream
.
Method call pipeline
Thus, you can build a stream pipeline consisting of any number of intermediate methods and a single terminal method call at the end. This approach lets you implement rather complex logic, while increasing code readability.
The data inside a data stream does not change at all. A chain of intermediate methods is a slick (declarative) way of specifying a data processing pipeline that will be executed after the terminal method is called.
In other words, if the terminal method is not called, then the data in the data stream is not processed in any way. Only after the terminal method is called does the data begin to be processed according to the rules specified in the stream pipeline.
stream()
.intemediateOperation1()
.intemediateOperation2()
...
.intemediateOperationN()
.terminalOperation();
Comparison of intermediate and terminal methods:
intermediate | terminal | |
---|---|---|
Return type | Stream |
not a Stream |
Can be combined with multiple methods of the same type to form a pipeline | yes | no |
Number of methods in a single pipeline | any | no more than one |
Produces the end result | no | yes |
Starts processing the data in the stream | no | yes |
Let's look at an example.
Suppose we have a club for animal lovers. Tomorrow the club celebrates Ginger Cat Day. The club has pet owners, each of which has a list of pets. They are not limited to cats.
Task: you need to identify all the names of all the ginger cats in order to create personalized greeting cards for them for tomorrow's "professional holiday". The greeting cards should be sorted by the cat's age, from oldest to youngest.
First, we provide some classes to help solving this task:
public enum Color {
WHITE,
BLACK,
DARK_GREY,
LIGHT_GREY,
FOXY,
GREEN,
YELLOW,
BLUE,
MAGENTA
}
public abstract class Animal {
private String name;
private Color color;
private int age;
public Animal(String name, Color color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
public String getName() {
return name;
}
public Color getColor() {
return color;
}
public int getAge() {
return age;
}
}
public class Cat extends Animal {
public Cat(String name, Color color, int age) {
super(name, color, age);
}
}
public class Dog extends Animal {
public Dog(String name, Color color, int age) {
super(name, color, age);
}
}
public class Parrot extends Animal {
public Parrot(String name, Color color, int age) {
super(name, color, age);
}
}
public class Pig extends Animal {
public Pig(String name, Color color, int age) {
super(name, color, age);
}
}
public class Snake extends Animal {
public Snake(String name, Color color, int age) {
super(name, color, age);
}
}
public class Owner {
private String name;
private List<Animal> pets = new ArrayList<>();
public Owner(String name) {
this.name = name;
}
public List<Animal> getPets() {
return pets;
}
}
Now let's look at the Selector
class, where the selection will be made according to the specified criteria:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Selector {
private static List<Owner> owners;
private static void initData() {
final Owner owner1 = new Owner("Ronan Turner");
owner1.getPets().addAll(List.of(
new Cat("Baron", Color.BLACK, 3),
new Cat("Sultan", Color.DARK_GREY, 4),
new Dog("Elsa", Color.WHITE, 0)
));
final Owner owner2 = new Owner("Scarlet Murray");
owner2.getPets().addAll(List.of(
new Cat("Ginger", Color.FOXY, 7),
new Cat("Oscar", Color.FOXY, 5),
new Parrot("Admiral", Color.BLUE, 3)
));
final Owner owner3 = new Owner("Felicity Mason");
owner3.getPets().addAll(List.of(
new Dog("Arnold", Color.FOXY, 3),
new Pig("Vacuum Cleaner", Color.LIGHT_GREY, 8)
));
final Owner owner4 = new Owner("Mitchell Stone");
owner4.getPets().addAll(List.of(
new Snake("Mr. Boa", Color.DARK_GREY, 2)
));
final Owner owner5 = new Owner("Jonathan Snyder");
owner5.getPets().addAll(List.of(
new Cat("Fisher", Color.BLACK, 16),
new Cat("Zorro", Color.FOXY, 14),
new Cat("Margo", Color.WHITE, 3),
new Cat("Brawler", Color.DARK_GREY, 1)
));
owners = List.of(owner1, owner2, owner3, owner4, owner5);
}
}
What remains is to add code to the main
method. Currently, we first call the initData()
method, which populates the list of pet owners in the club. Then we select the names of ginger cats sorted by their age in descending order.
First, let's look code that does not use streams to solve this task:
public static void main(String[] args) {
initData();
List<String> findNames = new ArrayList<>();
List<Cat> findCats = new ArrayList<>();
for (Owner owner : owners) {
for (Animal pet : owner.getPets()) {
if (Cat.class.equals(pet.getClass()) && Color.FOXY == pet.getColor()) {
findCats.add((Cat) pet);
}
}
}
Collections.sort(findCats, new Comparator<Cat>() {
public int compare(Cat o1, Cat o2) {
return o2.getAge() - o1.getAge();
}
});
for (Cat cat : findCats) {
findNames.add(cat.getName());
}
findNames.forEach(System.out::println);
}
Now let's look at an alternative:
public static void main(String[] args) {
initData();
final List<String> findNames = owners.stream()
.flatMap(owner -> owner.getPets().stream())
.filter(pet -> Cat.class.equals(pet.getClass()))
.filter(cat -> Color.FOXY == cat.getColor())
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
.map(Animal::getName)
.collect(Collectors.toList());
findNames.forEach(System.out::println);
}
As you can see, the code is much more compact. In addition, each line of the stream pipeline is a single action, so they can be read like sentences in English:
|
Move from a Stream<Owner> to a Stream<Pet> |
|
Retain only cats in the data stream |
|
Retain only ginger cats in the data stream |
|
Sort by age in descending order |
|
Get the names |
|
Put the result into a list |
3. Creating streams
The Stream
class has three methods that we haven't covered yet. The purpose of these three methods is to create new threads.
Stream<T>.of(T obj)
method
The of()
method creates a stream that consists of a single element. This is usually needed when, say, a function takes a Stream<T>
object as an argument, but you only have a T
object. Then you can easily and simply use the of()
method to get a stream that consists of a single element.
Example:
Stream<Integer> stream = Stream.of(1);
Stream<T> Stream.of(T obj1, T obj2, T obj3, ...)
method
The of()
method creates a stream that consists of passed elements. Any number of elements is allowed. Example:
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<T> Stream.generate(Supplier<T> obj)
method
The generate()
method lets you set a rule that will be used to generate the next element of the stream when it is requested. For example, you can give out a random number each time.
Example:
Stream<Double> s = Stream.generate(Math::random);
Stream<T> Stream.concat(Stream<T> a, Stream<T> b)
method
The concat()
method concatenates the two passed streams into one. When the data is read, it is read first from the first stream, and then from the second. Example:
Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = Stream.of(10, 11, 12, 13, 14);
Stream<Integer> result = Stream.concat(stream1, stream2);
4. Filtering data
Another 6 methods create new data streams, letting you combine streams into chains (or pipelines) of varying complexity.
Stream<T> filter(Predicate<T>)
method
This method returns a new data stream that filters the source data stream according to the passed rule. The method must be called on an object whose type is Stream<T>
.
You can specify the filtering rule using a lambda function, which the compiler will then convert to a Predicate<T>
object.
Examples:
Chained streams | Explanation |
---|---|
|
Retain only numbers less than three |
|
Retain only numbers greater than zero |
Stream<T> sorted(Comparator<T>)
method
This method returns a new data stream that sorts the data in the source stream. You pass in a comparator, which sets the rules for comparing two elements of the data stream.
Stream<T> distinct()
method
This method returns a new data stream that contains only the unique elements in the source data stream. All duplicate data is discarded. Example:
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.distinct(); // 1, 2, 3, 4, 5
Stream<T> peek(Consumer<T>)
method
This method returns a new data stream, though the data in it is the same as in the source stream. But when the next element is requested from the stream, the function you passed to the peek()
method gets called with it.
If you pass the function System.out::println
to the peek()
method, then all the objects will be displayed when they will pass through the stream.
Stream<T> limit(int n)
method
This method returns a new data stream that contains only first n
elements in the source data stream. All other data is discarded. Example:
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.limit(3); // 1, 2, 3
Stream<T> skip(int n)
method
This method returns a new data stream that contains all the same elements as the source stream, but skips (ignores) the first n
elements. Example:
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.skip(3); // 4, 5, 2, 2, 2, 3, 4
GO TO FULL VERSION