What is stream?
Stream is an abstraction of data operations. It takes input from the collections, Arrays of I/O channels.
From imperative to declarative
For example, given a list of people, find out the first 10 people with age less or equals to 18.
The following solution is the imperative approach:
public void imperativeApproach() throws IOException {
List<Person> people = MockData.getPeople();
List<Person> peopleAbove18 = new ArrayList<>();
for (Person person : people) {
if (person.getAge() <= 18) {
peopleAbove18.add(person);
}
}
for (Person person: peopleAbove18) {
System.out.print(person);
}
}
The following is the declare approach style:
public void declareApproach() throws IOException {
List<Person> people = MockData.getPeople();
people.stream()
// a lambda function as a filter
.filter(person -> person.getAge() <= 18)
.limit(10)
.collect(Collectors.toList())
.forEach(System.out::print);
}
Abstraction
We mentioned that stream is an abstraction to the data manipulation. It abstract them in the following way:
- Concrete: can be the Sets, Lists, Maps, etc
- Stream: can be filter, map, etc.
- Concrete: collect the data to make it concrete again.
Intermediate and Terminate Operation
Java stream has different operation units:
- Intermediate operators: map, filter, sorted
- Terminators: collect, foreach, reduce
Each intermediate operation is lazily executed and return a stream, until a terminal operation is met.
Range
With IntStream.range(), you can create a stream with fixed set of elements, for example:
public void rangeIteratingLists() throws Exception {
List<Person> people = MockData.getPeople();
// Use int stream to loop through the list and print the object.
IntStream.range(0, 10).forEach(i -> System.out.println(people.get(i)));
// If you want to use for the first elements
people.stream().limit(10).forEach(System.out::println);
}
You can also iterate the function for the given number times:
public void intStreamIterate() throws Exception {
// This is very much like the fold function on Kotlin,
// that it keep iterating based on the iterator you provided.
IntStream.iterate(0, operand -> operand + 1).limit(10).forEach(System.out::println);
}
Max, Min and Comparators
Java stream provides built in Min/Max function that support customized comparators. For example:
public void min() throws Exception {
final List<Integer> numbers = ImmutableList.of(1, 2, 3, 100, 23, 93, 99);
int min = numbers.stream().min(Comparator.naturalOrder()).get();
System.out.println(min);
}
Distinct
Sometimes, we would like to get the distinct elements from the stream, then we could use the distinct api of the stream
public void distinct() throws Exception {
final List<Integer> numbers = ImmutableList.of(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9, 9);
List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
}
Filtering and Transformation
Stream filter api enables you to filter the content of the element, for example:
public void understandingFilter() throws Exception {
ImmutableList<Car> cars = MockData.getCars();
// Predicate is an assertion that returns true or false
final Predicate<Car> carPredicate = car -> car.getPrice() < 20000;
List<Car> carsFiltered = cars.stream()
.filter(carPredicate)
.collect(Collectors.toList());
And map API enable you to transform the format of the element, for example, we could define a another object and transform the given stream to the targeted stream:
public void ourFirstMapping() throws Exception {
// transform from one data type to another
List<Person> people = MockData.getPeople();
people.stream().map(p -> {
return new PersonDTO(p.getId(), p.getFirstName(), p.getAge());
}).collect(Collectors.toList());
}
Group Data
One common function in SQL queries are data grouping, for example:
SELECT COUNT(*), TYPE FROM JOB WHERE USER_ID = 123 GROUP BY TYPE
Java stream provides similar functionalities:
public void groupingAndCounting() throws Exception {
ArrayList<String> names = Lists
.newArrayList(
"John",
"John",
"Mariam",
"Alex",
"Mohammado",
"Mohammado",
"Vincent",
"Alex",
"Alex"
);
Map<String, Long> counting = names.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
counting.forEach((name, count) -> System.out.println(name + " > " + count));
}
Reduce and Flatmap
Very similar to the Hadoop Map/Reduce job, where map take care of transformation of the data, while the reduce job collect the data and do the final computation. For example:
public void reduce() throws Exception {
Integer[] integers = {1, 2, 3, 4, 99, 100, 121, 1302, 199};
// Compute the same of the elements, with the initial element as a
int sum = Arrays.stream(integers).reduce(0, (a, b) -> a + b);
System.out.println(sum);
// use the function reference
int sum2 = Arrays.stream(integers).reduce(0, Integer::sum);
System.out.println(sum2);
}
Flat map is different from the map function that it could flat the internal structure first.
For example:
List<List<String>> list = Arrays.asList(
Arrays.asList("a"),
Arrays.asList("b"));
System.out.println(list);
System.out.println(list
.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList()));
The result of the stream is a String List.