Introducció

4388

All of us know that a number and a piece of text are pretty different. How do we know this? Well, you can perform arithmetic operations (such as multiplication) on numbers but not on texts.

Kotlin also knows it. That's why every variable has a type that determines what possible operations you can perform on this variable and which values you can store in it.

Variable types

A variable's type is set when the variable is declared:

val text = "Hello, I am studying Kotlin now."
val n = 1

In this case, Kotlin knows that text is a string and n is a number.

Kotlin determines the types of both variables automatically. This mechanism is called type inference.

You can also specify the type of a variable when declaring it.

Let's declare the same variables as in the previous example and specify their types:

val text: String = "Hello, I am studying Kotlin now."
val n: Int = 1

The Int type means that the variable stores an integer number (0, 1, 2, ..., 100_000_000, ...).

The String type means that the variable stores a string ("Hello", "John Smith"). Later, you will learn more about these and other data types.

You will see that people use both these forms of variable declaration in practice.

When you use type inference, you make your code more concise and readable, but in some cases, it may be better to specify the type.

For example, if we need to declare a variable and initialize it later, type inference won't work at all.

val greeting // error
greeting = "hello"

The example above is incorrect because Kotlin cannot infer the type of the variable when it is merely declared, while every variable must have a type.

On the contrary, the example below does work because the type is specified by the programmer:

val greeting: String // ok
greeting = "hello"

Type mismatch

One of the most important functions of data types is to protect you from assigning an unsuitable value to a variable.

Take a look at an example of code that doesn't work:

val n: Int = "abc" // Type mismatch: inferred type is String but Int was expected

So, if you see a type mismatch error, it means you've assigned something unsuitable to a variable. The same problem occurs when you try to assign an unsuitable value to a mutable variable declared with type inference.

var age = 30 // the type is inferred as Int
age = "31 years old" // Type mismatch

Note, you cannot change the type of a variable.

Properties of basic types

4455

Basic types can be separated into several groups according to their meaning.

The types from the same group operate similarly, but they have different sizes and, as a consequence, represent different ranges of values.

Numbers

Els processadors tenen dos circuits diferents per processar números enters i decimals.

Per aquest motiu tenim dos mesures de rendiment en un processador: els x i els FLOPS.

I el circuit d'enters només pot operar amb enters, i el de decimals amb decimals.

A més tenim processadors de 64 bits i de 32 bits (la majoria de mòbils).

Números enters

Per representar números enters (0,1,2, ...) tenim 4 tipus: Long (64 bits), Int (32 bits), Short (16 bits), Byte (8 bits) .

Com que cada tipus té una mida de bits diferent, cada tipus pot representar tants números com bits té: −(2n−1) to (2n−1)−1, where n is the number of bits. The range includes 0, that's why we subtract 1 from the upper bound.

  • Byte: 8 bits (1 byte), range varies from -128 to 127;
  • Short: 16 bits (2 bytes), range varies from -32768 to 32767;
  • Int: 32 bits (4 bytes), range varies from −(231) to (231)−1;
  • Long: 64 bits (8 bytes), range varies from −(263) to (263)−1.

The size cannot be changed. It does not depend on the operating system or hardware.

Per defecte, els números es representen amb Int ja que funcionen amb processadors de 32 i 64 bits i ocupen la mitat d'espai en memòria que un Long.

Però hi ha casos en que necessites un Long perquè 32 bits no donen per més: un processador de 32 bits només pot direccionar 4GB de RAM, i les IP són de 32 bits i fa temps que no hi ha prou, per això IPv6 que són IPs de 64 bits.

El tipus Byte normalment s'utilitza per representar dades mitjançant una array (per ejemple una película) i els bits no tenen cap significat númeric.

Quan parlem del tamany d'un arxiu parlem que té tants bytes (no tants bits).

I el tipus Short queda bé per complitut avui en dia, però només es fa servir en casos específics, per exemple per guardar el port d'un socket ja que és codifica en 16 bits.

Com hem dit al principi, Kotlin utilitza Int per representar números ja que normalment és el tipus més adient:

val zero = 0 // Int
val one = 1  // Int
val oneMillion = 1_000_000  // Int

De totes maneres, si el número és massa gran per un enter, Kotlin infereix de manera automàtica que el tipus de la variable ha de ser Long:

val bigNumber = 1_000_000_000_000_000 // Long, Kotlin automatically chooses it (Int is too small)

Si vols pots marcar de manera explícita el tipus en el literal:

val twoMillion = 2_000_000L // Long because it is tagged with L

O directament no utilitzar la inferència de tipus:

val ten: Long = 10  // Long because the type is specified
val shortNumber: Short = 15 // Short because the type is specified
val byteNumber: Byte = 15   // Byte because the type is specified

Activitats

1.- Que passa si declaro una variable amb un tipus de número enter que no pot representar el número literal?

val number: Byte = 2000

2.- I si declaro la variable, i després ... TODO

var number: Int = Int.MAX_VALUE
number += 1

3.- I si declaro la variable com Int i li passo un literal Long que cap en un enter?

val number: Int = 2L

Decimals

Floating-point types represent numbers with fractional parts.

Kotlin té dos tipus:

  1. Float (32 bits), un nom molt adient de l'época en que només havien processadors de 32 bits.

  2. Double (64 bits), que vol dir el doble, o sigui 64 bits per processadors de 64 bits.

These types can store only a limited number of decimal digits (~6-7 for Float and ~14-16 for Double).

Per defecte, Kotlin utilitza Double:

val pi = 3.1415 // Double

Tal com passa amb els enters, pots marcar el literal per inferir un altre tipus:

val e = 2.71828f             // Float because it is tagged with f
val fraction: Float = 1.51f  // Float because the type is specified

To display the maximum and minimum value of a numeric type (including Double and Float), you need to write the type name followed by a dot . and then either MIN_VALUE or MAX_VALUE:

println(Int.MIN_VALUE)  // -2147483648
println(Int.MAX_VALUE)  // 2147483647
println(Long.MIN_VALUE) // -9223372036854775808
println(Long.MAX_VALUE) // 9223372036854775807

It is also possible to get the size of an integer type in bytes or bits (1 byte = 8 bits):

println(Int.SIZE_BYTES) // 4
println(Int.SIZE_BITS)  // 32

Characters

Kotlin has a Char type to represent various letter characters (uppercase and lowercase), digits, and other symbols.

Each character is a letter character in single quotes.

The size is similar to the Short type (2 bytes = 16 bits):

val lowerCaseLetter = 'a'
val upperCaseLetter = 'Q'
val number = '1'
val space = ' '
val dollar = '$'

Characters can represent symbols of many alphabets, including hieroglyphs and some special symbols, which we will consider later.

Booleans

Kotlin provides a type called Boolean. It can store only two values: true and false. It represents only one bit of information, but its size is not precisely defined.

val enabled = true
val bugFound = false

We will often use this type in conditionals.

Strings

The String type represents a sequence of characters in double quotes. It is one of the most popular types.

val creditCardNumber = "1234 5678 9012 3456"
val message = "Learn Kotlin instead of Java."

Type conversion

4762

Type conversion, or type casting, involves switching a value from one data type to another.

This is notably critical in Kotlin because it is a statically typed language, which signifies that types are identified and strictly applied at compile time.

Implicit Conversion

Kotlin does not permit implicit conversions to avoid precision loss or unforeseen outcomes.

For instance, a Long cannot automatically convert to an Int:

val longValue: Long = 100L
// val intValue: Int = longValue // Error: Type mismatch

Explicit Type Conversion (Type Casts) in Kotlin

When you need to switch a variable from one type to another in Kotlin, explicit type conversion is necessary.

Kotlin is a statically-typed language, meaning types are checked at compile time and implicit type conversion is not permitted.

Below are some methods that Kotlin provides for explicit type conversion:

toInt(): Use this when you need to transform the value into an Int, or an integer format of a value.

val doubleValue = 2.5
val intValue = doubleValue.toInt() // intValue will be 2

toFloat(): Converts the value to a floating-point representation, or Float.

val intValue = 10
val floatValue = intValue.toFloat() // floatValue will be 10.0

toLong(): Use this when working with larger integer values; it transforms the value into a Long.

val intValue = 100
val longValue = intValue.toLong() // longValue will be 100L

toDouble(): Use this when dealing with high-precision arithmetic, it transforms the value into a Double.

val floatValue = 10.5f
val doubleValue = floatValue.toDouble() // doubleValue will be 10.5

toByte(): Transforms the value into a Byte; it's often used when dealing with low-level byte manipulation.

val intValue = 1
val byteValue = intValue.toByte() // byteValue will be 1

toShort(): Transforms the value into a Short. This method is lesser-known but can be helpful for specific scenarios.

val intValue = 5
val shortValue = intValue.toShort() // shortValue will be 5

toString(): This is often used for converting the value into a text representation, String, or for concatenation.

val intValue = 10
val stringValue = intValue.toString() // stringValue will be "10"

Any of these methods can be invoked on a variable to convert it to the desired type.

Keep in mind that, if the value being transformed is beyond the range of the target type, data loss or truncation may result. Make sure the value can safely change to prevent unexpected behavior.

Type Conversion Best Practices in Kotlin

You must execute type conversion with caution in Kotlin to avoid problems like precision loss or ClassCastException. Here are some guidelines to remember:

Use Explicit Conversions. Kotlin doesn't permit implicit type conversion, so utilize explicit methods like toInt(), toDouble(), and so on.

val i: Int = "123".toInt()

Check for null. If you are working with nullable types, make sure to use the safe call operator ?. before conversion.

val s: String? = null
val i: Int? = s?.toInt()

Handle NumberFormatException. Potential NumberFormatException should be handled when converting from String to a numeric type.

val s: String = "abc"
val i: Int? = try { s.toInt() } catch (e: NumberFormatException) { null }

Avoid Loss of Precision. Be careful when converting between numeric types to avoid losing precision.

val l: Long = 1_000_000_000L
val i: Int = l.toInt() // Potential loss of precision

Smart Casts with is. Use is checks for safe casting when dealing with inheritance.

if (obj is String) {
    println(obj.length)
}

Use the as? Operator for Safe Casting. Use as? to safely cast to a type and to avoid ClassCastException. This will return null if the operation fails.

val x: Any = "Kotlin"
val s: String? = x as? String

By following these practices, you can ensure that your data remains intact and prevents runtime exceptions during type conversions in Kotlin.

Type coercion

You already know how to perform type conversion.

There are more advanced aspects of it: for example, you know that we cannot assign a variable of Int type to a Long variable.

But what happens if we calculate the sum of Int and Long variables? In this case, the type is inferred from the context.

In such cases, the compiler automatically sets all components (it's called type coercion) and the result type to the widest type in the expression.

The picture below illustrates the direction of this casting:

TODO (graph)

Byte -> Int -> Long -> Float -> Double Short ->

The direction of transfer of variable types on the left to the right from Byte and Short to Double

Since the type of the result is wider than the previous type, there is no loss of information.

Type coercion is rare in Kotlin. It works only with numbers and strings.

Examples

The theory looks pretty clear, so let's take a look at some examples of type coercion.

from Int to Long:

val num: Int = 100
val longNum: Long = 1000
val result = num + longNum // 1100, Long

Although result is just 1100, it is the sum of Long and Int variables, so the type is automatically cast to Long.

If you try to declare a result as Int, you get an error because you cannot assign the value of Long type to an Int variable.

You can assign only an Int value or an integer number to a variable of Int type.

from Long to Double:

val bigNum: Long = 100000
val doubleNum: Double = 0.0
val bigFraction = bigNum - doubleNum // 100000.0, Double

Short and Byte types

Ja saps que els tipus Byte i Short normalment no s'utilitzen per representar quantitats númeriques, per això està Int.

També que un processador de 32 bits opera amb valors de 32 bits.

Per tant, quan sumes un Byte amb un Byte primer s'han de convertir en un Int per ser sumats pel processador, i el resultat és un Int.

A més és molt fàcil que quan sumes un Byte amb un Byte el resultat no es pugui representar amb un Byte

Per tant el resultat de sumar Bytes i Short entre ells sempre és un Int

If you need to do some calculations with these types, the result of the calculation is Int:

Byte and Byte

val one: Byte = 1
val two: Byte = 2
val three = one + two // 3, Int

Short and Short

val fourteen: Short = 14
val ten: Short = 10
val four = fourteen - ten // 4, Int

Short and Byte

val hundred: Short = 100
val five: Byte = 5
val zero = hundred % five // 0, Int

So what should we do if we want to sum two Byte variables and get a Byte result? Well, in this case, you must manually perform type conversion:

val one: Byte = 1
val five: Byte = 5
val six = (one + five).toByte() // 6, Byte

Remember that Byte can store data in the range -128.. 127.

Look at the example below of how type overflow works:

fun main() {
    val a: Byte = 120
    println((a + a).toByte()) // prints -16 because 120+120 > 127
}

Conclusion

To sum up, if you have an expression with different numeric types, use these rules to know the type of the result:

  1. If either operand is of type Double, the result is Double.

  2. Otherwise, if either operand is of type Float, the result is Float.

  3. Otherwise, if either operand is of type Long, the result is Long.

  4. Otherwise, the result is Int.

Type coercion does not occur when a value is put into the variable. For example, val longValue: Long = 10.toInt() is incorrect, because 10 is Int and longValue requires the Long type.

The compiler automatically deduces the type of expression. It helps you omit type conversion in simple cases, but you need to understand how it works to prevent confusion and errors.

Nullable types

7613

If you're familiar with Java, you've probably heard something like NullPointerException (NPE). If you are reading about it for the first time, you're a lucky person because NPE is the most frequent exception, which will make you so unhappy. Moreover, there isn't any convenient way to prevent such an exception in Java. Lucky you, Kotlin has a real remedy for NPE. Though first, we need to learn about a special type of reference.

Nullability

There are just a few ways how NPE may occur in Kotlin:

  1. explicit call of throw NullPointerException()
  2. !! syntax
  3. bad initializations, such as constructors and superclass constructors.

If you don't know anything about these things, it's just fine. You will learn about them later. For now, just remember that you don't have to pay as much attention to NPE as in Java and that you can concentrate on real tasks. Don't forget that Kotlin is a pragmatic language. So, what do we have?

First of all, every reference in Kotlin can be either nullable or not. Let's say we want to define a String variable, but we are not sure what it might be initially:

var name: String = null

So, what is null above? It just means that the name variable doesn't have a certain value.

This code won't compile because we declared a non-null variable.

How can we fix it? Pretty easy:

var name: String? = null

As you can see, we just added a ? sign right after the type of our variable. We marked our name variable as nullable.

We can also do the same with other types, like Int or Long:

var age: Int? = null

So, without a ? sign in the type you can't assign null to a variable.

Accessing nullable variables

Now try to guess what happens if you try to access this variable property:

var name: String? = null
print(name.length)

If you think there will be an error, you're right! This code won't even compile. What can we do, then?

Of course, we can add a common check for null like this:

if (name != null) {
    print(name.length)
}

If the name is null, the print won't be called.

Or we can access the length this way:

print(name?.length)

Here null is printed.

This ?. pair of symbols is called a safe call in Kotlin. We will dig into this concept in a special topic(TODO link topic).

Right now, there is enough information for you to practice.

Billion-dollar mistake

So, Kotlin introduces nullable types that differ from non-nullable ones. In old languages like Java, there is no difference because every type is nullable. Therefore, in many languages, it's not required to check a nullable variable against null before accessing it. This can cause lots of program crashes, and in 2009, Tony Hoare, a British Computer Scientist who invented the concept of null reference, described it as a "billion-dollar mistake":

"I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object-oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years."

Type system

9689

Kotlin is a statically-typed language, which means it enforces type checking during the compilation phase. This means that errors are detected before the program runs, resulting in fewer runtime errors and more trustworthy code.

Static Typing

Kotlin's static typing assures that the types of variables are known at compile time, optimising performance and improving clarity.

For instance:

val message: String = "Hello, Kotlin!"

In the above code snippet, message is clearly declared as a String. This clarity helps to prevent type-related errors.

Advantages of Kotlin's Type System

Null Safety. Kotlin's type system is tailored to get rid of the dreaded NullPointerException common in many Java programs. It differentiates between nullable and non-nullable types.

    var a: String = "abc"
    //a = null // Compile error

    var b: String? = "abc"
    b = null // OK

Smart Casts. Kotlin smartly manages type casting, reducing the need for explicit casts.

if (obj is String) {
    // obj is automatically cast to String in this scope
    println(obj.length)
}

Type Inference. Kotlin has efficient type inference capabilities, indicating you don't always need to state the type clearly.

val language = "Kotlin" // Type inferred as String

Extension Functions. You can extend existing types with new functionality without inheriting from the type.

TODO moure a la seva activitat

fun String.addExclamation() = this + "!"

println("Hello".addExclamation()) // Output: Hello!

Basic Types in Kotlin

(moure info al principi)

Kotlin has an extensive type system that includes various basic types. These function as the building blocks for data manipulation within the language.

Here's a short introduction to some fundamental types:

Int: Represents a 32-bit signed integer. It can range from a minimum value of -2,147,483,648 to a maximum value of 2,147,483,647.

val age: Int = 30

Double: A 64-bit double-precision floating-point number. It's perfect for representing numbers with fractional parts.

val pi: Double = 3.141592653589793

Char: Represents a single 16-bit Unicode character and is enclosed in single quotes.

val firstLetter: Char = 'A'

Boolean: Represents a truth value, which can either be true or false.

val isKotlinFun: Boolean = true

In Kotlin, these fundamental types are represented as objects, unlike Java's primitive types. However, Kotlin's compiler optimizes managing these types to be as efficient as Java primitives whenever possible. This optimisation process is called 'autoboxing'.

Kotlin also supports explicit type conversions as it does not perform implicit type conversions (widening conversions) for numbers. This means you need to manually convert types if you wish to assign a value of one type to a variable of another:

val i: Int = 42 val d: Double = i.toDouble() // explicit conversion

Understanding these basic types and how they're represented is key to effective Kotlin programming, as they form the foundation for more advanced data structures and operations.

Type cast

TODO molt avançat moure on toca

26543

Type checks and casts are essential in any programming language.

  1. Type checks allow developers to verify if an object belongs to a particular data type

  2. Type casts enable programmers to convert an object from one type to another.

Kotlin, being a statically-typed language, has several features that make type checks and casts easy and safe to use.

is and !is operators

The is and !is operators in Kotlin are used for type checks.

They allow developers to check if an object belongs to a particular data type.

The is operator returns true if an object belongs to the specified type and false if it doesn't.

Conversely, the !is operator returns true if an object doesn't belong to the specified type and false if it does.

For example:

val obj: Any = "Hello, Kotlin"
if (obj is String) {
   println(obj.uppercase())
} else {
   println("obj is not a String")
}

In the above code, we use the is operator to check if the obj variable is a String. If it is a String, we convert it to uppercase and print it. Otherwise, we print a message saying that obj is not a String.

This is a good example for the is operator, but let's remember the idioms in Kotlin, one of the advantages of this programming language.

One of the often used idioms in Kotlin is:

when (x) {
    is Foo -> ...
    is Bar -> ...
    else   -> ...
}

Look at an example of how we can use that:

fun processInput(input: Any) {
    when (input) {
        is Int -> println("Input is an integer")
        is String -> println("Input is a string")
        is Double -> println("Input is a double")
        else -> println("Unknown input")
    }
}

In this example, the function processInput takes an argument of type Any, which means it can accept any type of object. Within the function, we use when with is to check the type of the input object. Depending on the type, we print a message indicating what type of input it is. If the input object is not one of the expected types, we print the "Unknown input" message.

Smart casts

Kotlin also has a feature known as smart casts. Smart casts are used to simplify code when working with nullable types. When a nullable type is checked with the is operator, Kotlin automatically casts the object to a non-nullable type.

For example:

fun printLength(obj: Any) {
   if (obj is String) {
      println(obj.length)
   }
}

In the above code, we check if the obj variable is a String by using the is operator. If it is a String, we print its length. Since Kotlin automatically casts the obj variable to a non-nullable type, we don't need to use any type cast operator.

"Unsafe" cast operator

Kotlin has an unsafe cast operator, which is represented by the as keyword.

The as keyword is used to cast an object to a non-nullable type.

If the object cannot be cast to the specified type, the as operator throws a ClassCastException.

For example:

val obj: Any = "Hello, Kotlin"
val str: String = obj as String // Unsafe cast operator
println(str.uppercase())

In the above code, we use the as operator to cast the obj variable to a String. If obj is not a String, the as operator throws a ClassCastException.

"Safe" (nullable) cast operator

Kotlin also has a safe cast operator, which is represented by the as? keyword. The as? operator is used to cast an object to a nullable type.

If the object cannot be cast to the specified type, the as? operator returns null.

For example:

fun main() {
    val obj: Any = 123
    val str: String? = obj as? String // Safe (nullable) cast operator
    if (str != null) {
        println(str.uppercase())
    }
}

In the above code:

  1. We use the as? operator to cast the obj variable to a String.2. Since obj is not a String, the as? operator returns null.
  2. Therefore, the println statement doesn't print anything.

Generics type checks and casts

When working with generics, we may need to check whether an object is an instance of a specific type parameter or cast it to a type parameter.

To check whether an object is an instance of a specific type parameter, we can use the is operator with the type parameter in angle brackets.

For example:

fun <T> exampleFunction(obj: Any) {
    if (obj is T) {
        // obj is an instance of type parameter T
    } else {
        // obj is not an instance of type parameter T
    }
}

Similarly, we can cast an object to a type parameter using the as operator with the type parameter in angle brackets. However, if the object is not an instance of the type parameter, ClassCastException will be thrown.

To avoid this, we can use the safe cast operator as?, which returns null if the cast is not possible.

fun <T> exampleFunction(obj: Any) {
    val tObj: T? = obj as? T
    if (tObj != null) {
        // obj can be safely cast to type parameter T
    } else {
        // obj cannot be cast to type parameter T
    }
}

It's important to note that type erasure occurs with generics in Kotlin, meaning that the actual type of a generic object is not known at runtime. Therefore, certain operations, like creating a new instance of a type parameter or checking if a type parameter is a subtype of another class, are not possible.

Ranges

4633

Imagine a situation where you need to check whether the integer variable c is greater than or equal to a and less than or equal to b. To do that you may write something like this:

val within = a <= c && c <= b

This code works well. However,

Kotlin provides a more convenient way to do the same thing using ranges:

val within = c in a..b

Here,

  1. a..b is a closed-ended range of numbers from a to b (including both border values),

  2. in is a special keyword that is used to check whether a value is within a range. Later you will see that this keyword can be used with other types as well.

Also, we have an open-ended range:a..<b is a range of numbers from a until b (excluding the border value, b).

The value of within is true if c belongs to the range inclusively; otherwise, it is false.

Here are some examples:

println(5 in 5..15)  // true
println(12 in 5..15) // true
println(15 in 5..15) // true
println(20 in 5..15) // false
println(5 in 5..<15)  // true
println(15 in 5..<15) // false

If you need to exclude the right border, you may subtract 1 from it or use ..< to get the open-ended range (the recommended way).

val withinExclRight = c in a..b - 1 // a <= c && c < b
val withinExclRight = c in a..<b // a <= c && c < b (the recommended way)

If you need to check that a value is not within a range, just add ! (not) before in.

val notWithin = 100 !in 10..99 // true

You may combine ranges using standard logical operators.

The code below checks if c is within one of three ranges.

val within = c in 5..10 || c in 20..30 || c in 40..50 // true if c is within at least one range

You can assign a range to a variable and use it later.

val range = 100..200
println(300 in range) // false

In addition to integer ranges, you can also use ranges of characters and even strings (according to dictionary order).

println('b' in 'a'..'c') // true
println('k' in 'a'..'e') // false

println("hello" in "he".."hi") // true
println("abc" in "aab".."aac") // false

This is enough to understand ranges for integer numbers and characters. We won't cover other type ranges here. (TODO other topic ranges)