From Python to Java: Comprehensions and Streams
Tags: python, java, programming, tutorial
Published: 2022-04-08 17:36:25

For most of my career, I've worked primarily as a Python developer. There were occasional trips into the frontend for some JavaScript or a brief detour through a Go application, but the vast majority of my work was solidly Python.

That is, until I joined Square. Square might be better known for its Ruby code, but there is also a sizable chunk of Java within its codebase. So when I came to Square, I knew I'd be putting away my list comprehensions, generators, and `@decorators`, and instead I'd be learning some new programming language patterns and idioms.

But of course, my Python experience would not be for nothing. As it turns out, we can find parallel ideas throughout most programming languages. In this series, I'll be highlighting some of these parallels I've encountered switching from Python to Java in hopes that it might help someone else who also finds themselves crossing this language barrier!

---

## Introduction

It's impossible to avoid list (and dictionary!) comprehensions when working in Python. Whether you need to create a sublist by filtering out certain values, transform each element in an existing list, or rearrange keys and values into a new dictionary, comprehensions are a quintessential Python tool.

They're so useful in Python and so numerous that you might find yourself afraid that you can't do even the simplest tasks in Java. You might recall hearing other developers groan about Java's verbosity and heavy-handedness as you start writing...

```java
List<Integer> evenIntegers = new ArrayList<Integer>();
for (Integer integer : allIntegers) {
    if (integer % 2 == 0) {
        evenIntegers.add(integer);
    }
}
```

...at least that's how I felt, until I learned about Streams. With Streams, we don't have to rewire our brains or begrudgingly write lengthy boilerplate for simple tasks.


## `.stream()`, `.collect()`

However, this is Java and we are going to be writing _some_ boilerplate. Types are everything in Java and in order to leverage the Streams API, we'll have to have an object of type `Stream<T>`. Luckily this is often as easy as calling `.stream()` on our object (assuming our object is iterable: a list, set, array, etc).

Once we have our `Stream<T>` object, we can apply a series of filters and transformations, but in the end we're going to want a list again (or a set, map, etc). This is where `.collect()` comes in along with various `Collectors`. In this post, we'll only focus on two Collector methods, `Collectors.toList()` and `Collectors.toMap()`.

The basic structure of a stream is like so
```ascii
obj.stream()
   <intermediate actions>
   .collect(...)
```
More verbose than Python, certainly, but not too bad!

So without further ado, let's check out a few common operations and how we might do them with Python's comprehensions and then with Java's Streams.

## Filtering

Imagine we have a list of integers and we want only the even numbers. (The example is trivial, but I think it generalizes well enough.)

In Python we would solve this problem with a simple list comprehension[^1]:

```python
even_nums = [n for n in list_of_nums if n % 2 == 0]
```

With Java Streams, we could accomplish this like so: 

```java
List<Integer> evenNums = listOfNums.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
```

## Transforming

Sometimes we want to take one list of elements and transform them into something else. For a simple example, let's take our same list of integers and multiply each by two.

First in Python[^2],
```python
doubled_nums = [n * 2 for n in list_of_nums]
```

and then with Java Streams,
```java
List<Integer> doubledNums = listOfNums.stream()
    .map(n -> n * 2)
    .collect(Collectors.toList());
```


## Lists to Dictionaries

Not _everything_ can be accomplished with lists[^3], sometimes we need to put things into a dictionary (or a Map, as they're called in Java land).

Suppose we have a _list_ of `User` objects and we want a _dictionary_ of each `user.name` keyed by their `user.id`.

```python
names_by_id = {user.id: user.name for user in users}
```

It's a little more complicated in Java. In our previous examples, we used `Collectors.toList()`. Now that we want a Map, we'll have to use `Collectors.toMap()`. This collector asks for a lambda function to produce the key and another one to produce the value:

```java
Map<Integer, String> namesById = users.stream()
    .collect(Collectors.toMap(
        user -> user.getId(),
        user -> user.getName()));
```

## Conclusion

Java has a reputation for having a verbose syntax, and Streams are no exception. But they _are_ a marked improvement over excessive loops and temporary variables for otherwise simple transformations and collections.

While this post highlighted some ways that we can use Streams when we would otherwise write a comprehension in Python, it's important to know that Streams are _much more_ than this and are capable of some really cool things (parallelization, lazy evaluation, sorting, querying, etc, etc)!

To learn more about Streams, check out [this post by Eugen Paraschiv on Stackify](https://stackify.com/streams-guide-java-8/) and of course the [Java documentation](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/package-summary.html)




[^1]: Python _does_ have support for the more functional programming-style `filter` that more closely mirrors Java's implementation. Though list comprehensions are generally considered to be more Pythonic than `filter`.

[^2]: Like the previous note, Python has support for `map` but _in most cases_ the list comprehension is preferred.

[^3]: Lisp programmers might take issue with this statement.