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.