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
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
  1. DZone
  2. Data Engineering
  3. Databases
  4. Kotlin Collections' API Performance Anti-Patterns

Kotlin Collections' API Performance Anti-Patterns

Want to learn more about Kotlin Collections? Check out this post to learn more about Kotlin Collections' API performance and anti-patterns.

Grzegorz Piwowarek user avatar by
Grzegorz Piwowarek
·
Oct. 02, 18 · Presentation
Like (5)
Save
Tweet
Share
7.29K Views

Join the DZone community and get the full member experience.

Join For Free

Kotlin’s collections API is expressive and rich — but with great power comes great responsibility. There are certain practices that can cause unnecessary time-complexity and object allocation overhead.

To fully understand the context, make sure to check the previous article, first:

1. Chaining map() Calls

Two consecutive  map() calls might look innocent and justified from the readability point of view, but they might not be acceptable because of the performance impact.

Let’s have a look at a simple example:

list
  .map { it.toString() }
  .map { it.toUpperCase() }


Everything becomes clear once we break it apart and take a sneak peek at the map() implementation:

val map = list.map { it.toString() }
val anotherMap = map.map { it.toUpperCase() }


public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
    for (item in this) destination.add(transform(item))
    return destination
}


Every map() call triggers a new O(n)-running for loop and the creation of a new list object that gets garbage-collected after processing is finished. Is it a lot? It depends — but, it’s definitely better to be aware of that trade-off.

It turns out that the solution is as trivial as collapsing consecutive calls:

list.map { it.toString().toUpperCase() }


2. Chaining map() and flatMap() Calls

As in the example above, whenever you see a combination of  map() and flatMap(), the precious CPU cycles can be spared by incorporating them into a single flatMap() call.

So:

list
  .map { it.toList() }
  .flatMap { it }


becomes:

list.flatMap { it.toList() }


3. Chaining map() and filterNotNull()

If a collection contains nullable elements, it might be tempting to filter out all nulls first, and only then, you can perform mapping:

val list = listOf(1, 2, 3, null)

list
  .filterNotNull()
  .map { it.toString() }


Unfortunately, due to the eager nature of Kotlin collections, again, this generates a lot of unnecessary overhead:

public fun <C : MutableCollection<in T>, T : Any> Iterable<T?>.filterNotNullTo(destination: C): C {
    for (element in this) if (element != null) destination.add(element)
    return destination
}


  1.  filterNotNull() will create a new collection, iterate over the existing one, and repackage non-null elements into the new one, then return it
  2.  map() will create a new collection, iterate over the existing one, apply mapping, and store mapped elements in a new collection, then return it

Those two operations could be merged and optimized by leveraging the mapNotNull() method:

list.mapNotNull { it.toString() }


That’s not only convenient but avoids creating a redundant, short-lived collection object and performing redundant iterations.

4. Chaining map()/flatMap() With last()/first()

Whenever we want to obtain a single element, there’s no need to process all elements in a collection first, only to pick the last/first one:

list
  .map { it * 42 }
  .last()


Instead, it’s a better idea to obtain the first/last element, and then apply the transformation once:

list
  .last() * 42


Or, if we want to keep a chained calls structure, we can leverage  let():

list
  .last()
  .let { it * 42 }


5. Chaining filter() and first()/last()

Whenever we want to filter out certain elements and then pick first/last of them, there’s no need to process all elements first:

val list = listOf(1, 2, 3)

list
  .filter { it % 2 == 1 }
  .last() // first()


We can do much better just by leveraging a dedicated parameterized last(predicate: (T) -> Boolean) method:

list.last { it % 2 == 1 }


It turns out that this one actually starts the backward iteration from the end of a collection:

public inline fun <T> List<T>.last(predicate: (T) -> Boolean): T {
    val iterator = this.listIterator(size)
    while (iterator.hasPrevious()) {
        val element = iterator.previous()
        if (predicate(element)) return element
    }
    throw NoSuchElementException("List contains no element matching the predicate.")
}


6. Chaining filter() and count()

Whenever there’s a need to calculate all occurrences of items matching a given predicate, there’s no need to create a filtered collection using filter() first, and only then applycount():

list
  .filter { it % 2 == 1 }
  .count()


The above can be simply achieved with a dedicated count() overload that will spare us the creation of a new object:

list.count { it % 2 == 1 }


Conclusion

The Kotlin Collection API is a powerful and better way to be aware of the trade-offs associated with its eagerness.

Code snippets, as always, can be found on GitHub.

API Kotlin (programming language) Anti-pattern Element

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

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Top Five Tools for AI-based Test Automation
  • Too Many Tools? Streamline Your Stack With AIOps
  • Spring Cloud: How To Deal With Microservice Configuration (Part 1)
  • The Real Democratization of AI, and Why It Has to Be Closely Monitored

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
  • +1 (919) 678-0300

Let's be friends: