Releases

First Look at Kotlin 1.4-M2: Standard Library Improvements

We keep working on Kotlin 1.4 and the next preview 1.4-M2 is just around the corner. Right now we’re ready to unveil some improvements from this preview; in this post, we’ll get you acquainted with the standard library changes.

Here are some key improvements in the standard library in 1.4-M2:

Even though Kotlin 1.4-M2 hasn’t been released yet, we’ve deployed its early version to the Kotlin playground so that you can try everything you find in this post. The code samples in this post run on the new version as well.

If you can’t wait to try the new version, subscribe to the Kotlin blog newsletter and you won’t miss the release day.

Extending the common library

You can use the standard library in “common” code – the code shared between different platforms, be they Android & iOS or JVM & JS. We’re gradually extending the common library and adding or moving missing functionality to it.

The previous implementation of appendln in Kotlin/JVM added a system-dependent line separator (\n on UNIX systems and \r\n on Windows). However, we feel it is important to guarantee the same behavior independently of the operating system, and independently of the underlying platform if we’re talking about common code. This is why we’re deprecating appendln in favor of the new appendLine function, which always terminates lines with a single \n character:

You can still use System.lineSeparator() in the JVM when you need a platform-specific separator.

The other functions that can now be used in common code allow you to use the string representation of the given stack trace. The Throwable.stackTraceToString() extension returns the detailed description of this throwable with its stack trace, and Throwable.printStackTrace() prints this description to the standard error output.

In common code, you can also use the Throwable.addSuppressed() function, which allows you to specify the exceptions that were suppressed in order to deliver the exception, and the Throwable.suppressedExceptions property, which returns a list of all the suppressed exceptions.

New array functions

To provide a consistent experience when working with different container types, we’ve added new extension functions for arrays:

  • shuffle() – puts the array elements in a random order.
  • onEach() – performs the given action on each array element and returns the array itself.

These functions should be familiar to those who use lists; they work for arrays the same way.

We’ve also added new functions for sorting array subranges. Previously, there was sort() with parameters fromIndex and toIndex, which was JVM-only. Now it is common, and so are two new related functions, which are subrange versions of reverse() and sortDescending(). Each of these takes two indices and reorders the elements between them (including fromIndex but excluding toIndex) in ways that are hopefully evident from their names:

  • reverse() for array subranges reverses the order of the elements in the subrange.
  • sortDescending() for array subranges sorts the elements in the subrange in descending order.

New functions in collections API

In 1.4-M2, we continue expanding the collections API in the standard library to cover more real-life cases:

  • A new set creation function setOfNotNull() makes a set consisting of all the non-null items among the provided arguments.
  • onEachIndexed() and reduceIndexedOrNull() have been added as counterparts for onEach() and reduceOrNull(), respectively. As you may already know, Indexed in the name of a collection-processing function means that the operation applied has the element index as a parameter.

  • runningFold() and runningReduce() are introduced as synonyms for scan() and scanReduce(). Such names are more consistent with the related functions fold() and reduce(). In the future, scan() will be available along with runningFold() since it’s a commonly known name for this operation. The experimental scanReduce(), however, will be deprecated and removed soon.

Improving the existing API

As Kotlin 1.4 is a major “feature” release, we can add new features to the language, and new functions or interfaces to the standard library. We only add new experimental declarations in incremental releases (like 1.3.70). If you’re writing code using Kotlin 1.3.70 and you don’t use any experimental declarations, your code will compile just fine for your teammates who use Kotlin 1.3.40.

Feature releases don’t need to obey the same strict rules as minor ones, meaning that if you use a new feature or API, the Kotlin compiler of the 1.3 version may not compile code written in Kotlin 1.4. This allows us to introduce some changes to the API in order to improve it. We do try to carefully observe backward compatibility so that your code written for the older version continues to compile and work as before.

In Kotlin 1.4, we’ve relaxed several functions to accept nulls:

Kotlin 1.3 won’t compile this code because it requires the receiver of String.toBoolean() to be non-nullable. Kotlin 1.4 changes the receiver to a nullable string: String?.toBoolean(). Still, all the code you’ve written before continues to compile and work in Kotlin 1.4.

The same logic applies to the Array receiver of the contentEquals, contentHashCode, and contentToString functions: now it’s nullable. Also, String.format() now allows null as a locale argument, in which case no localization is applied.

The following constants defined in Doubles and Floats are now “real” constants:

They are now defined as const variables, so you can use them as annotation arguments.

SIZE_BITS and SIZE_BYTES are new constants in Double and Float; they contain the number of bits and bytes accordingly used to represent an instance of the type in binary form.

Note that we’ve also changed the Float.MAX_VALUE and Float.MIN_VALUE values in Kotlin/JS. Before, they were equal to JavaScript Number.MAX/MIN_VALUE, or equivalently, Double.MAX/MIN_VALUE, because Float is essentially equivalent to Double in Kotlin/JS. Now, these Float range constants are the same for all platforms.

maxOf() and minOf() with varargs

The maxOf() and minOf() functions from the standard library find the greater and the smaller of two values. Starting with 1.4-M1, maxOf() and minOf() can accept a variable number of arguments (vararg), allowing you to use them on any sets of numbers or other comparable items.

Property delegation improvements

In Kotlin, delegated properties work via conventions, not interfaces: the type you want to use as a delegate must define an operator function rather than implement the required interface. This provides flexibility (as we aren’t bound by specific interfaces), but in many practical use cases, it still is helpful to use interfaces.

In Kotlin 1.4, we’ve made such complementary interfaces even better to work with: we’ve introduced a new PropertyDelegateProvider interface, and ReadWriteProperty now inherits ReadOnlyProperty. Please read on for more details.

Delegate expression

The ReadWriteProperty and ReadOnlyProperty interfaces are handy for defining property delegates as your custom class or anonymous object can implement them:

Starting with 1.4, ReadWriteProperty inherits ReadOnlyProperty. This gives you more flexibility for working with delegate expressions. In our example, now you can pass the myDelegate() call whenever a ReadOnlyProperty is expected.

Here we’d like to emphasize that “read-only” isn’t the same as “immutable” in Kotlin, similarly to how a read-only list is not an immutable list. “Read-only” means “this interface provides only read-only access to the object in question”.

Providing a delegate

By using the mechanism of providing a delegate, you can extend the logic of creating a “delegate” object – the object to which the property implementation is delegated. You can find the details of how it works in the documentation. In 1.4, to make this mechanism a bit more convenient, we’ve added a new PropertyDelegateProvider interface. You can use it when you don’t want to create an extra class and prefer using an anonymous object, similar to what we’ve seen in myDelegate() example above.

Delegating to another property

Starting with 1.4, a property can delegate its getter and setter directly to another property. This may be useful, for instance, when you want to rename a property in a backward-compatible way: you introduce a new property, annotate an old one with the @Deprecated annotation, and delegate its implementation.

The optimization for compiled delegated properties described before works for this case. Since the delegation operator implementation doesn’t use the information about the property being delegated (oldName), there’s no need for the compiler to generate the KProperty instance with that information. In the future, it will also be possible not to generate an additional KMutableProperty instance for the delegate (newName).

How to try

All the described changes will be part of the Kotlin 1.4-M2 preview, but you can already try them online at play.kotl.in; just make sure to select the version 1.4-M2 in the settings.

Pre-release notes

Note that backward compatibility guarantees do not cover pre-release versions. The features and API can change in subsequent releases based on your feedback.

Share your feedback

We’re thankful for all your bug reports in our issue tracker. We’ll do our best to fix all the important issues before the final release so you won’t have to wait until yet another Kotlin release for them to be addressed.

You are also welcome to join the #eap channel in our Kotlin Slack (get an invite here) to ask questions, participate in discussions, and get notifications of new preview builds.

Let’s Kotlin!

image description