DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
Securing Your Software Supply Chain with JFrog and Azure
Register Today

Trending

  • Building a Flask Web Application With Docker: A Step-by-Step Guide
  • Software Development: Best Practices and Methods
  • Why I Prefer Trunk-Based Development
  • Using Render Log Streams to Log to Papertrail

Trending

  • Building a Flask Web Application With Docker: A Step-by-Step Guide
  • Software Development: Best Practices and Methods
  • Why I Prefer Trunk-Based Development
  • Using Render Log Streams to Log to Papertrail
  1. DZone
  2. Coding
  3. Languages
  4. Kotlin: Beware of Java Stream API Habits

Kotlin: Beware of Java Stream API Habits

Kotlin's Collections should be familiar to anyone who has used Java's Stream API, but be careful, there are considerations to make for lazy initialization.

Grzegorz Piwowarek user avatar by
Grzegorz Piwowarek
·
May. 01, 18 · Tutorial
Like (19)
Save
Tweet
Share
19.52K Views

Join the DZone community and get the full member experience.

Join For Free

Kotlin’s Collections API is extremely versatile and expressive, but there’s one important thing to be aware of — especially when migrating from Java.

Aching Design of Java Collections

In Java, although we could easily implement our own immutable data structures or simply use provided immutable views, using them on an everyday basis was challenging because of the Collections API design.

For example, imagine yourself implementing a new immutable data structure for Java and trying to implement the following methods:

boolean add(E e);
boolean remove(Object o);
void clear();
boolean removeAll(Collection<?> c);
// ...


The only reasonable way is to simply forbid using them:

public E set(int index, E element) {
    throw new UnsupportedOperationException();
}
public void add(int index, E element) {
    throw new UnsupportedOperationException();
}
public E remove(int index) {
    throw new UnsupportedOperationException();
}
// and so on ...


And this shall remain like this forever as long as Java embraces backward compatibility (although the latest releases were not very good at this).

This is one of the reasons why the only reasonable way to enrich the API was to introduce a new abstraction – the Stream API. Streams aren’t collections, so the API could be designed from scratch.

Stream API Laziness

Besides the new fancy functional API, the major selling point of java.util.stream.Stream was laziness – which made them nearly as performant as standard imperative solutions and made it possible to work with potentially infinite sequences.

Consider the following example:

Optional<Integer> resultStream = list.stream()
.map(i -> i * 2)
.filter(i -> i > 1)
.findAny();


Initially, it might be tempting to think that the optimistic time complexity is O(2*N), while the imperative alternative provides O(1) while being less error-prone and less readable:

Optional<Integer> resultLoop = Optional.empty();
for (Integer i : list) {
    Integer integer = i * 2;
    if (integer > 1) {
        resultLoop = Optional.of(integer);
        break;
    }
}


Even the huge improvement in code maintainability/readability wouldn’t be enough to justify the performance impact.

Luckily, thanks to lazy evaluation, both examples provide the optimistic time complexity of O(1) (the first encountered element matches) and pessimistic of O(N) (no elements match) – Streams simply take elements one by one and push them through the whole operation chain, allowing short-circuiting as soon as the result is obtained.

A Trap of Kotlin’s Expressive Collections API

Kotlin’s Collections API is extremely versatile, expressive, and well-suited for working with immutable data structures – sounds great, right? But they surely can backfire if not used properly!

The great thing about collections in Kotlin is that they are immutable by default and feature a very rich functional API, so we no longer need to turn to the Stream API — because our collections provide all this functionality already.

So, if we tried to recreate the example above, we could simply write:

val list = listOf(1, 2, 3, 4, 5)
list
.map(expensiveOperation())
.map(anotherExpensiveOperation())
.first()


Nice and clean, but there’s a catch – let’s measure the execution time.

What do you think the result will be assuming that both expensiveOperation() and anotherExpensiveOperation() last exactly one second?

Kotlin’s collections are not lazy, hence the operation took around 10 seconds, which matches the optimistic time-complexity of O(2*N), where N = 5.

With great power comes great responsibility – an addition of every new map/flatMap/filter/… call, might negatively impact the time-complexity of your solution in certain scenarios because each of them simply creates a new collection instance simply by iterating over the previous one.

Bringing Laziness Back

Good news, everyone!

Naturally, Kotlin provided a native way of achieving laziness in situations like above by introducing lazy sequences. We can convert every Iterable to a lazy Sequence, which is a Kotlin’s Stream alternative, by using the asSequence() method:

val list = listOf(1, 2, 3, 4, 5).asSequence()
list
.map(expensiveOperation())
.map(anotherExpensiveOperation())
.first()


If we measure the execution time now, we can breathe a sigh of relief because the result is around 2 seconds regardless of the sequence size, which matches the time-complexity of O(1) (2 seconds because of two map() calls).

The above code snippets can be found on GitHub.

Key Takeaways

  • Kotlin Collections are not lazy in nature like the Java Stream API is.
  • If you want laziness, use asSequence().
API Kotlin (programming language) Stream (computing) Java (programming language)

Published at DZone with permission of Grzegorz Piwowarek, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Trending

  • Building a Flask Web Application With Docker: A Step-by-Step Guide
  • Software Development: Best Practices and Methods
  • Why I Prefer Trunk-Based Development
  • Using Render Log Streams to Log to Papertrail

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com

Let's be friends: