Filtering

  • Stream interface supports a filter method which takes a predicate (a function returning boolean) as argument.
  • filter method will return a stream including all elements that match the predicate
List<Dish> vegMenu = menu.stream()
        .filter(Dish::isVeg)	// Dish::isVeg is a predicate which returns true for dishes that are veg
        .collect(toList());
  • Stream interface also supports a distinct method which returns a stream with unique elements (this will depend on the implementation of hashcode and equals methods of the objects of the stream)
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
	.distinct()
    	.forEach(System.out::println);

 

Slicing

  • filter method above will need to iterate through the whole stream. 
  • If the stream is already sorted, we can just stop iterating once the predicate condition is satisfied
  • takeWhile method will slice any stream using predicate by stopping iteration once an element not satisfying the predicate is found.
List<Dish> slicedMenu = specialMenu.stream()
	.takeWhile(dish -> dish.getCalories() < 320)
    	// will stop iterating if dish with calories above 320 is found
    	.collect(toList());
  • dropWhile method is the complement of takeWhile - it drops any element satisfying the predicate, and will return all the remaining elements once the predicate is not satisfied.
List<Dish> slicedMenu = specialMenu.stream()
	.dropWhile(dish -> dish.getCalories() < 320)
    	// will drop dishes that has calories lower than 320,
    	// and return the remaining elements if calories is greater or equal than 320
    	.collect(toList());
  • limit(n) will select only the first n elements from the stream and return those immediately.
  • skip(n) will return a stream skipping the first n elements

 

Mapping

  • Stream API's map method takes a function as argument.
  • The function is applied to each element, which will map each element into a new element.
  • Depending on the function, it will change the data type of the elements in the stream.
List<Integer> dishNameLengths = menu.stream()	// a stream of Dish
	.map(Dish::getName)			// now a stream of String
    	.map(String::length)			// now a stream of Integer
    	.collect(toList());
  • We can flatten a stream using flatMap method.
  • In below, all the separate streams are flattened into a single stream.
List<String> uniqueCharacters = words.stream()
    .map(word -> word.split(""))	// each word is converted into an array
    	.flatMap(Arrays::stream)	// flattens each stream of array into a single stream of characters
        .distinct()
        .collect(toList());

 

Modern Java in Action

 

 

Finding and Matching

  • Another common use case of data processing with streams is finding elements that match a certain condition.
  • anyMatch method will return true if at least one element in the stream satisfies the given predicate - therefore anyMatch is a terminal operation.
if(students.stream().anyMatch(Student::isMale)) {
	System.out.println("We have a male student");
}
  • allMatch method will return true if all elements in the stream satisfy the given predicate
boolean isUnhappy = students.stream()
	.allMatch(student -> student.isPostGrad());
  • noneMatch method will return true if all elements in the stream do not satisfy the given predicate
boolean isHappy = students.stream()
	.allMatch(student -> student.isPostGrad());
  • findAny method will return an arbitrary element from the stream
  • This can be used with other methods such as filter.
Optional<Student> postGrad = students.stream()
	.filter(Student::isPostGrad)
        .findAny();
  • Unlike above findAny method, findFirst method will return the first element from the stream
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = numbers.stream()
	.map(n -> n*n)			// square each number
    	.filter(n -> n%3 == 0)		// leave only those that are divisible by 3
    	.findFirst();			// 9

 

Reducing

  • Used to reduce the stream into Optional<T>
  • 3 Main components: Identity, Accumulator, Combiner
    • Identity: the initial value of the reduction operation
    • Accumulator: a function that takes 2 arguments - the result is the partial result of the reduction, and will be the next element in the stream
    • Combiner: a function used to combine the 2 partial results - not necessarily needed if sequential streams are used (not parallel) and the types of the accumulator arguments and the types of its implementation match
  • Here the initial value is 0, and the accumulator is (a,b) -> a+b
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
  • If we do not have an initial value, the reduction operation cannot return a sum with an empty stream, so Optional<Integer> will be returned
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
  • Below reduction operation will reduce a list (stream) of strings into a single string
List<String> letters = Arrays.asList("a", "b", "c");
String result = letters.stream()
	.reduce("", (partialString, element) -> partialString + element);
  • For parallel streams, we need a function (combiner) to combine the partial results of the substreams into a single one
int sum = numbers.parallelStream()
	.reduce(0, (a, b) -> a + b, Integer::sum);	// Integer::sum is the combiner
  • We also need a combiner when the types of stream objects and accumulator parameters mismatch.
int result = users.stream()
	.reduce(0, (partialAge, user) -> partialAge + user.getAge(), Integer::sum);
    // partialAge is int, while user is User
    // Integer::sum is used as combiner to resolve this mismatch

 

 

'Java > Modern Java In Action' 카테고리의 다른 글

Collecting Data with Streams - 1  (0) 2023.11.10
Introducing Streams  (1) 2023.10.30

+ Recent posts