1. Overview
Sometimes we might need to represent only positive numbers in a domain model. As of Kotlin 1.3, Kotlin supports unsigned integers to accommodate this requirement.
In this short tutorial, we’re going to get familiar with declaring and using unsigned integers in Kotlin.
2. Unsigned Integers
Kotlin 1.3 introduced unsigned integers as an experimental feature. Currently, Kotlin only supports the following unsigned types:
- The kotlin.UByte is an unsigned 8-bit integer (0 – 255)
- The kotlin.UShort is an unsigned 16-bit integer (0 – 65535)
- The kotlin.UInt is an unsigned 32-bit integer (0 to 2^32 – 1)
- The kotlin.ULong is an unsigned 64-bit integer (0 to 2^64 -1)
*To assign a numeric literal to these unsigned types, Kotlin provides a new u/U suffix similar to what we had for floats*. For instance, here we’re assigning a few literals to unsigned data types:
val uByte: UByte = 42u
val uShort: UShort = 42u
val uInt: UInt = 42U
val uLong: ULong = 42u
As shown above, we used u or U suffices to tag the literal as an unsigned integer. The declared type will determine the exact variable type. For instance, in the first example, the “42u” literal is an unsigned UInt, but the declared type is UByte. So the literal value will be converted to UByte.
Of course, if we omit the type, the compiler will infer the UInt or ULong based on the size of the literal value:
val inferredUInt = 42U
val inferredULong = 0xFFFF_FFFF_FFFFu
The compiler should infer both types as they’re omitted. For the first one, since 42 fits inside a UInt, the inferred type will be UInt. On the contrary, the second value is larger than the UInt capacity, so the inferred type is ULong.
Also, it’s even possible to explicitly tag a numeric literal as ULong with the uL suffix:
val explicitULong = 42uL
Moreover, it’s worth mentioning that unsigned integers are implemented using another experimental feature in Kotlin 1.3 called inline classes.
3. Experimental Feature
As of this writing, this new unsigned integer feature is at the experimental stage. Therefore if we use them in our code, the compiler will issue a warning about the possibility of future incompatible changes:
This declaration is experimental and its usage should be marked with '@kotlin.ExperimentalUnsignedTypes' or '@OptIn(kotlin.ExperimentalUnsignedTypes::class)'
Fortunately, the warning itself is very self-descriptive. So if we’re sure about using this experimental feature, we can annotate the enclosing class or function with the ExperimentalUnsignedTypes or OptIn(kotlin.ExperimentalUnsignedTypes::class):
@ExperimentalUnsignedTypes
fun main() {
// use unsigned integers here without warning
}
When the Kotlin compiler sees these annotations, it will skip the warning.
As of Kotlin 1.5, the UInt, ULong, UByte, and UShort unsigned integer types are stable. The same goes for operations on these types, as well as on ranges and progressions of them. Therefore, they’re available without opt-in and safe to use in real-life projects. On the other hand, the arrays of unsigned types are still in the beta phase.
4. Unsigned Arrays
In addition to singular unsigned integers, it’s possible to create arrays with unsigned components. As a matter of fact, for each unsigned integer, there is a corresponding array type. To be more specific, these are UByteArray, UShortArray, UIntArray, and ULongArray.
To create an array with unsigned integer components, we can use their constructors:
val ba = UByteArray(42)
Here we’re creating an array of UBytes with 42 as the length. Similarly, other unsigned arrays provide a constructor with the same signature.
In addition to constructors, we can use the ubyteArrayOf() factory method to create an array with initial elements:
val ba2 = ubyteArrayOf(42u, 43u)
Here we’re creating an array of UBytes with two elements. Similarly, Kotlin provides a factory method with u*ArrayOf() syntax for other unsigned arrays, too.
5. Manipulating Unsigned Types
Unsigned integers support the same set of operations as the signed ones. For instance, we can add two unsigned types together, perform a left shift on them, and many other common arithmetic operations:
assertEquals(3u, 2u + 1u)
assertEquals(16u, 2u shl 3)
Similarly, unsigned arrays provide the same API as signed arrays:
uintArrayOf(42u, 43u).map { it * it }.forEach { println(it) }
Moreover, it’s possible to convert a signed integer to an unsigned one and vice versa:
val anInt = 42
val converted = anInt.toUInt()
Obviously, for each unsigned data type, Kotlin provides a toU*() method.
Please note that the most significant bit in signed integers is the sign bit. On the contrary, that bit is just a regular bit in unsigned integers. Therefore, converting a negative signed integer to an unsigned one can be tricky:
assertEquals((255).toUByte(), (-1).toUByte())
assertEquals((65535).toUShort(), (-1).toUShort())
assertEquals(4294967295u, (-1).toUInt())
assertEquals(18446744073709551615uL, (-1L).toULong())
The binary representation of -1 integer is “1111 1111 1111 1111 1111 1111 1111 1111”. Therefore, it gets converted to the maximum possible number in UByte, UShort, UInt, and ULong. So when we convert, say, an Int to its corresponding UInt, we can’t expect always to get the same number. Similarly, the same is true when we’re converting a UInt to an Int:
assertEquals(-1, (4294967295u).toInt())
It’s also possible to convert a signed array to an unsigned one:
val toUIntArray = intArrayOf(-1, -2).toUIntArray()
6. Conclusion
In this tutorial, we got familiar with unsigned integers in Kotlin. We saw a few different ways to declare such data types, manipulate them, and of course, create them from their corresponding signed types. We also saw how to create an array out of such data types.
As usual, all the examples are available over on GitHub.