Java Streams

Lambda expressions & Functional Interfaces

Lambda expressions & Functional Interfaces are an integral part of writing code using streams. In a nutshell, functional interfaces (in the java.util.function package) are Java’s way of adapting to the functional programming paradigm. They provide reference types for lambda expressions. Easiest way to think about lambda expressions is to think of inline code snippets. They reduce the overall amount of code you need to write, especially implementing anonymous inner classes.

List<String> strings = Arrays.asList("A","B","C");

// Inline implementation (via Method Reference) for Functional Interface called Consumer
strings.forEach(System.out::println);

// Inline implementation (via Lambda Expression) for Functional Interface called Consumer
strings.forEach((s) -> System.out.println(s))
// Inline implementation (via Lambda Expression) for Functional Interface called Predicate
strings.removeIf((s) -> { return s.equals("C");});

Streams

  • Streams are computed on-demand and do not really store anything (unlike collections)
  • Streams are lazy! Nothing gets computed until a terminal operation is called
  • Streams cannot be reused. Reinitialise after calling a terminal operation to reuse.
  • Streams support both sequential and parallel processing
  1. Build: To create a new stream
  2. Operate: Intermediate operations like filtering
  3. Terminate: Actions which indicate the stream to start executing operations and provide an output

Building streams

Streams can be built from Collections, from files or using StreamBuilder. The java.io APIs have been modified to return streams while reading from files (instead of byte arrays say) as in the example below. Assuming there exists a file called “fruits.txt” on the class path —

private static Stream<Fruit> getFruitStream() {
// Create the fruit stream from file
Stream<Fruit> fruitStream = null;
try(InputStreamReader inputStreamReader = new InputStreamReader(StreamStress.class.getClassLoader().getResourceAsStream("fruits.txt"));
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)){

Stream.Builder<Fruit> fruitStreamBuilder = Stream.builder();

bufferedReader.lines().forEach((line) -> {
String[] tokens = line.split(",");
fruitStreamBuilder.add(new Fruit(tokens[0],tokens[1],new Boolean(tokens[2])));
});
fruitStream = fruitStreamBuilder.build();
}catch(IOException ioe){
System.out.println(ioe.getMessage());
}
return fruitStream;
}
//Create streams from collections
List<String> strings = Arrays.asList("A","B","C");
strings.forEach((s) -> System.out.println(s));
//Create stream for range of numbers 0 to 9
IntStream.range(0,10).forEach(System.out::println);

Operating on streams

Whenever you think of streams, think Filter;Map;Collect (or Filter;Map;Reduce if you like). Operations are run on streams after they are created. Operations can be chained and get executed in the order. We can perform two types of operations on streams — intermediate operations & terminal operations. Intermediate operations, like filter() or map(), help during processing (e.g. massaging or enriching the elements of a stream). All intermediate operations are lazy. They get executed only when the terminal operations are called. Terminal operations, like forEach() or collect(), help providing an output (e.g. prints or i/o).

System.out.println("//1. Create fruit stream");
getFruitStream().forEach(FruitColorPrinter::printWithColor);

System.out.println("//2. Filter: Print all small fruits");
getFruitStream().filter((f) -> f.isSmall())
.forEach(FruitColorPrinter::printWithColor);


System.out.println("//3. Map & Collect: Apply function to each element of the stream");
List<String> fruitList1 = getFruitStream().map((f) -> f.getName().toUpperCase())
.collect(Collectors.toList());
System.out.println("All fruits: "+fruitList1);

List<String> fruitList = getFruitStream()
.flatMap((f) -> Arrays.asList(f.getName().toUpperCase(), "\uD83C\uDF7A").stream()) //<-- \uD83C\uDF7A is Beer Emoji!!
.collect(Collectors.toList());
System.out.println("All fruits w/ Beer! : "+fruitList);
  • filter() accepts a Predicate, as in the Functional Interfaces section above and must return a boolean.
  • map() & flatMap() accept a Function
  • flatMap() appends a 🍺 (because fruit beer is a thing!) to the output list.
  • The difference between map() and flatMap() is map cannot change the output stream format. The output stream’s size and type stays the same as the input. FlatMap, on the other hand can change the size and type of the output. Conceptually, think of map as ‘list of lists’ and flatMap as flattened list of all the elements in the ‘list of lists’.
  • forEach() used a ‘Method Reference’ to print to the console, using the ‘::’ operator
  • Java provides utilities to create Collectors used in conjunction with collect(). Collectors transform the stream into java.util.Collection objects.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Purnima Kamath

Purnima Kamath

PhD Student NUS, Techie, Painter in Oils 🎨