Iterables Java 8 Streams

Kotlin Iterables vs Java 8 Streams

Posted on by Peter Tokaji

Processing collections of items is a very common use case in nearly every program. Usually, these operations are chained (the input of the next one uses the output of the one before), and can be heavily parallelized. Traditionally, this is involves a lot of boilerplate and focus on synchronization, making these problems both a challenge, and a huge effort.

This is where functional programming comes to the rescue, making data operations more fluent, and accounting for easy-to-use synchronization options, plus better code readability. So supporting functional style operations on collections is a key feature of every advanced programming language - no wonder both Java 8 and Kotlin have built-in solutions for working with iterable streams (Stream API and Iterables). The aim of this post is to give a quick overview of what's common in these languages, and what are the key differences in usage.

Common functionality

Let's start with taking a look at the same set of features you can find in both languages! The Streams API and the Kotlin Iterable interface both provide options to perform basic operations on collections, such as: filtering elements, finding items, etc. Here are the most common functions, defined both in Java and Kotlin.

Java Kotlin
allMatch all
flatMap flatMap
map map
sorted sorted
skip drop

These 5 examples highlight how similar (or in some cases identical) the interfaces are.

Differences in defining transformations

Even though there are similarities (as seen above), the rest of the APIs are largely different. Let's take a look at a simple example about how we can create a list of numbers, apply a filter function, and print the results!

  List<Integer> numbers = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
  List<Integer> filteredList = numbers
                                    .stream()
                                    .filter(number -> number > 5)
                                    .peek(System.out::println)
                                    .collect(Collectors.toList());

This example operates with a List, which needs to be converted into a stream, because List does not implement the Stream<T> interface, where the transform functions are defined. Alternatively, a stream can be initialized with the of operator, for example: Stream.of(1,2,3). We collect every item satisfying a condition in a filter transformation, while the peek operator stands for executing commands not modifying the dataset, in this case logging. The last step is a conversion from stream to list, to be able to reassign the numbers variable.

fun main(vararg args: String) {
  val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
    .filter { it > 5 }
    .onEach { println(it) }
}

In Kotlin we don't need to convert a list into a different type, because the transformation function is defined in the Iterable<T> interface, and every type of Collection (like the List) implements it. The other key difference is the usage of the lambda parameter - by default, the it keyword refers to the parameter value, if you don't have to define a custom name. But of course you can, and it is a good habit to roll with a custom name when the expression is more complex, you have a custom object, or to avoid name shadowing in nested lambdas. But in our case, the shorthand syntax doesn't effect the readability.

Teaser: operators in Kotlin

Although the operators mentioned in the sections above can possibly cover most use cases, there are special problems that require a custom approach. This is where Kotlin shines, with an armada of built-in operators - this section contains some that caught our attention.

One of these is zipWithNext. Kotlin is getting traction among data scientists as well, who may appritiate these extra features familiar from R or Matlab. Additionally, multiplication or applying custom calculation logic on adjacent elements is a core and frequently used transformation during processing the input data in machine learning.

windowed is an other favourite, very useful in certain situations where you want to perform operations on subsections (aka windows, hence "windowed") of a collection. The following example shows how this works:

fun main(vararg args: String) {
  val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
    .windowed(2, 3, false)
    .onEach {
      it.onEach { print("$it ") }
      println()
    }

Output:
  1 2 
  4 5 
  7 8 
  10 11 

The first size parameter sets the numbers of elements inside one single window. step declares the distance between two windows, and partialWindow controls whether to keep partial windows in the end or not. Solving this problem by hand requires instantiating a sublist, checking conditions, and performing various computations, which can make the code more complex, thus making it harder to maintain. Instead, ysing windowed means you don't have to implement it by hand, saving you a lot of time - and you can trust JetBrains that they came up with an efficient implementation.

This is just a small teaser about what we get out of the box with Kotlin. It is worth mentioning that in version 1.2, the number of stream operators expanded even further. I would suggest to the reader to take a look at all the operators available here.

Final words

This article just scratches the surface of functional programming, giving you a peek into it. Using either Java Streams or Kotlin Iterable operators gives you the ability to transform collections really easily. Kotlin has the of being able to generate Java 6 bytecode, so if requirements don't allow using Java 8, but you would like to use modern, advanced coding techniques, you're going to love it.