Home > Mobile >  Why is the Sequence in Kotlin named 'Sequence' as like Java Stream?
Why is the Sequence in Kotlin named 'Sequence' as like Java Stream?

Time:11-19

I've studied Kotlin nowadays and got a question of the reason why the Sequence in Kotlin is named 'Sequence'. I heard that the Java Stream is similar to Kotlin Sequence. Are there some critical differences to distinguish between Kotlin Sequence and Java Stream from the names?

If it exists such description or link, I'm appreciated you guys to reply below.

The description like Java and Kotlin Docs or the proper reasons of naming Sequence in Kotlin.

CodePudding user response:

You asked

Java Streams versus Kotlin Sequences

Deterministic point of view by 4 topic.

  • Null Safety

Using Java streams within Kotlin code results in platform types when using non-primitive values. For example, the following evaluates to List<Person!> instead of List so it becomes less strongly typed:

people.stream()
    .filter { it.age > 18 }
    .toList() // evaluates to List<Person!>

When dealing with values which might not be present, sequences return nullable types whereas streams wrap the result in an Optional:

// Sequence
val nameOfAdultWithLongName = people.asSequence()
    ...
    .find { it.name.length > 5 }
    ?.name
// Stream
val nameOfAdultWithLongName = people.stream()
    ...
    .filter { it.name.length > 5 }
    .findAny()
    .get() // unsafe unwrapping of Optional
    .name

Although we could use orElse(null) in the example above, the compiler doesn’t force us to use Optional wrappers correctly. Even if we did use orElse(null), the resulting value would be a platform type so the compiler doesn’t enforce safe usage. This pattern has bitten us several times as a runtime exception will be thrown with the stream version if no person is found. Sequences, however, use Kotlin nullable types so safe usage is enforced at compile time.Therefore sequences are safer from a null-safety perspective.

  • Primitive handling

Although Kotlin doesn’t expose primitive types in its type system, it uses primitives behind the scenes when possible. For example, a nullable Double (Double?) is stored as a java.lang.Double behind the scenes whereas a non-nullable Double is stored as a primitive double when possible.

Streams have primitive variants to avoid autoboxing but sequences do not:

// Sequence
people.asSequence()
    .map { it.weight } // Autobox non-nullable Double
    ...
// Stream
people.stream()
    .mapToDouble { it.weight } // DoubleStream from here onwards
    ...

However, if we capture them in a collection then they’ll be autoboxed anyway since generic collections store references. Additionally, if you’re already dealing with boxed values, unboxing and collecting them in another list is worse than passing along the boxed references so primitive streams can be detrimental when over-used:

// Stream
val testScores = people.stream()
   .filter { it.testScore != null }
   .mapToDouble { it.testScore!! } // Very bad! Use map { ... }
   .toList() // Unnecessary autoboxing because we unboxed them

Although sequences don’t have primitive variants, they avoid some autoboxing by including utilities to simplify common actions. For example, we can use sumByDouble instead of needing to map the value and then sum it as a separate step. These reduce autoboxing and also simplify the code.

When autoboxing happens as a result of sequences, this results in a very efficient heap-usage pattern. Sequences (& streams) pass each element along through all sequence actions until reaching the terminal operation before moving on to the next element. This results in having just a single reachable autoboxed object at any point in time. Garbage collectors are designed to be efficient with short-lived objects since only surviving objects get moved around so the autoboxing that results from sequences is the best possible / least expensive type of heap usage. The memory of these short-lived autoboxed objects won’t flood the survivor spaces so this will utilize the efficient path of the garbage collector rather than causing full collections.

All else being equal, avoiding autoboxing is preferred. Therefore streams can be more efficient when working with temporary primitive values in separate stream actions. However, this only applies when using the specialized versions and also as long as we don’t overuse the primitive variants as they can be detrimental sometimes.

  • Optional values

Streams create Optional wrappers when values might not be present (eg. with min, max, reduce, find, etc.) whereas sequences use nullable types:

// Sequence
people.asSequence()
   ...
   .find { it.name.length > 5 } // returns nullable Person
// Stream
people.stream()
   ...
   .filter { it.name.length > 5 }
   .findAny() // returns Optional<Person> wrapper

Therefore sequences are more efficient with optional values as they avoid creating the Optional wrapper object.

  • Lambda creation

Sequences support mapping and filtering non-null values in 1 step and thus reduce the number of lambdas instances:

// Sequence
people.asSequence()
   .mapNotNull { it.testScore } // create lambda instance
...
// Stream
people.stream()
   .map { it.testScore } // create lambda instance
   .filter { it != null } // create another lambda instance
   ...

Additionally, most terminal operations on sequences are inline functions which avoid the creation of the final lambda instance:

people.asSequence()
    .filter { it.age >= 18 }
    .forEach { println(it.name) } // forEach inlined at compile time

Therefore sequences create fewer lambda instances resulting in more efficient execution due to less indirection.

  • Related