Kotlin

From bibbleWiki
Jump to navigation Jump to search

Introduction

Kotlin is like java

  • JVM language
  • Object orientated
  • Functional language, high order functions, we can
  • store, pass and return functions
fun main(args: Array<String>) {
    println("Hello World")
}

Types

The following data type are available.

  • Numbers
  • Characters
  • Boolean
  • Strings
  • Arrays
  • Collections
  • Ranges

Numbers

The following Number type are available

  • Byte Size 8, min -128, max 127
  • Short Size 16, min -32768, max 32767
  • int Size 32, min -2,147,483,648 max 2,147,483,647
  • Long Size 64, min -9,223,372,036,854,775,808, max 9,223,372,036,854,775,807
val one = 1 // Int
val threeBillion = 3000000000 // Long
val oneLong = 1L // Long
val oneByte: Byte = 1

For Floating Point Numbers

  • Float Size 32, Significant bit 24, Exponent Bits 8, Decimal Digits 6-7
  • Double Size 64, Significant bit 53, Exponent Bits 11, Decimal Digits 15-16
val pi = 3.14 // Double
val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float, actual value is 2.7182817

Characters

Kotlin represents character using char. Character should be declared in a single quote like ‘c’. Please enter the following code in our coding ground and see how Kotlin interprets the character variable. Character variable cannot be declared like number variables. Kotlin variable can be declared in two ways - one using “var” and another using “val”.

fun main(args: Array<String>) {
   val letter: Char    // defining a variable 
   letter = 'A'        // Assigning a value to it 
   println("$letter")
}

Boolean

Two values true or false. (See Oracle for why two values was mentioned)

Strings

Strings are character arrays. Like Java, they are immutable in nature. We have two kinds of string available in Kotlin - one is called raw String and another is called escaped String. In the following example, we will make use of these strings.

fun main(args: Array<String>) {
   var rawString :String  = "I am Raw String!"
   val escapedString : String  = "I am escaped String!\n"
   
   println("Hello!"+escapedString)
   println("Hey!!"+rawString)   
}

Arrays

Arrays in Kotlin are represented by the Array class, that has get and set functions (that turn into [] by operator overloading conventions), and size property, along with a few other useful member functions:

class Array<T> private constructor() {
    val size: Int
    operator fun get(index: Int): T
    operator fun set(index: Int, value: T): Unit

    operator fun iterator(): Iterator<T>
    // ...
}

To create an array, we can use a library function arrayOf() and pass the item values to it, so that arrayOf(1, 2, 3) creates an array [1, 2, 3]. Alternatively, the arrayOfNulls() library function can be used to create an array of a given size filled with null elements.

fun main(args: Array<String>) {
   val numbers: IntArray = intArrayOf(1, 2, 3, 4, 5)
   println("Hey!! I am array Example"+numbers[2])
}

Ranges

Ranges is another unique characteristic of Kotlin. Like Haskell, it provides an operator that helps you iterate through a range. Internally, it is implemented using rangeTo() and its operator form is (..).

In the following example, we will see how Kotlin interprets this range operator.

fun main(args: Array<String>) {
   val i:Int  = 2
   for (j in 1..4) 
   print(j) // prints "1234"
   
   if (i in 1..10) { // equivalent of 1 < = i && i < = 10
      println("we found your number --"+i)
   }
}

// Output is 1234we found your number

Here is an example reading bytes

 val myTest = 212
 for(i in 0..7)
 {
   val myGetValue = IsByteSet(myTest,i)
   val myTest2 = myGetValue
 }

And a reading bits backwards

 val myTest = 212
 for(i in 7 downTo 0)
 {
   val myGetValue = IsByteSet(myTest,i)
   val myTest2 = myGetValue
 }

Collections

Filtering and Sorting

Kotlin provides filter, map and flat map

  • filter Similar to a where in SQL
  • map Similar to a select in SQL
  • flatmap See below (flattens maps)

Filter

No any surprises here

   val ints = listOf(1,2,3,4,5)

   val smallInts = ints.filter{it < 4}

// 1,2,3

Map

No any surprises here either

   val ints = listOf(1,2,3,4,5)

   val smallInts = ints.map{it * it}

// 1,4,9,16, 25

Flatmap

This functions flattens a list of lists into a single list

   val meetings = listOf(
        Meeting(1, "Board"), 
        Meeting(2, "Committee"))
   
   val people: List<Person> = meetings
       .flatMap(Meeting::people)

// You could add distinct() to ensure distinction

Combining

   val ints = listOf(1,2,3,4,5)

   val smallInts = ints
                     .filter(it < 5)
                     .map{it * it}

// 1,4,9,16

Predicates

Example predicates available in kotlin can be used on collections too.

  • all
  • any
  • count
  • find (returns first elements with match predicate or null)

Here are some examples again no surprises.

val ints = listOf(1,2,3,4,5)
var largeInts = ints.any{it > 3} 
var numberOfLargeInts = ints.count{it > 3}

We can declare predicates as variables too.

val greaterThanThree = {v:Int -> v > 3}

var numberOfLargeInts = ints.count{greaterThanThree}

Sequences

When we use sequences they are lazy evaluation. That means we can pass them around without great performance hits. I guess it is a bit like a yield keyword.

fun iter(seq: Sequence<String>) {
   for(t in seq) println(t)
}

fun main(args:Array<String>) {

    val titles: Sequence<String> = meetings
      .asSequence()
      .filter{...}
      .map{...}

    iter(titles)
}

Creating Collections

The key thing to remember is that there are mutable and non mutable lists

Flow Control

If

if can be used as expression and statements.

if(q.Answer == q.CorrectAnswer) {
 // Do something
}
else
{
 // Do something else
}

// Or we can do 
var message = if(q.Answer == q.CorrectAnswer) {
   "You were correct"
} else {
   "Try again"
}

When

When can be used like a switch statement.

when(number) {
    0 -> println("Invalid number")
    1, 2 -> println("Number too low")
    3 -> println("Number correct")
    4 -> println("Number too high, but acceptable")
    else -> println("Number too high")
}

But it can also be used like an expression too.

var result = when(number) {
    0 -> "Invalid number"
    1, 2 -> "Number too low"
    3 -> "Number correct"
    4 -> "Number too high, but acceptable"
    else -> "Number too high"
}
// it prints when returned "Number too low"
println("when returned \"$result\"")

Try

In kotlin you can use the try as an expression too.

val result:Int = try {
    Integer.parseInt("ABC")
} catch(e:NumberFormationException) {
    42
}

While

Same a java

while (x > 0) {
    x--
}

Do

Same a java

do {
    val y = retrieveData()
} while (y != null) // y is visible here!

For Loops

Basic

Kotlin does not support the for(var x=0; x<Total; x++). To do this in kotlin you need to use ranges

for(i in 1..10) {
    println("iain is ace")
}

// Or
for(i in 1..10 step 2) {
    println("iain is ace")
}

// Or
for(item in collection) {
    println("iain is ace {item.attribute1}")
}

Lists

For lists we can deconstruct the list into an element and index

var numbers = listOf(1,2,3,4,5)
// blahhh blahhh
for((index, element) in numbers.withIndex()) {
    print("$element at $index")
}

Maps

For maps we can deconstruct the map into a key and value

var ages = TreeMap<String, Int>() 
// blahhh blahhh
for((name, age) in ages) {
    print("$name is $age")
}

Functions

General

  • Kotlin Functions can be expressions
  • Functions can be called by Java
  • Can have default parameters
  • Can have named parameters
  • Can extend existing types
  • Infix functions
  • Tailrec functions
  • High-level functions

Here we go derrrrr

fun test(inString : String) : Boolean {
    println("No alarms and no surprise $inString")
    return true
}

Function Expressions

We can attach an expression to a function name.

package rsk

fun main(args: Array<String>) {
    println(max(1,2))
}

fun max(a: Int, b: Int): Int = if(a >b) a else b

Calling From Java

To call from java we just need to add the package name to call the function

fun main(args: Array<String>) {
    println(rsk.max(1,2))
}

We can override this name in the kotlin package with

@file:JvmName("DisplayFunctions")

Default Parameters

fun test(param1: String = "Test") {
    println("No alarms and no surprise $param1")
}

Named Parameters

Nothing special here

fun test(param1: String = "Test", param2: String = "Test") {
    println("No alarms and no surprise $param1")
}
test (param2 = "Test", param1 = "Test2")

Extension Functions

This is like the approach for C#. This results in a static function but cuts down the use of utility code. These are static functions but reduce the number of utility functions. So in the example below we extend the String class

fun String.myOwnFunction(param1: String, param2: String): String {

    // this represents the receiver
    println("Iain is ace $param1, $param1, this")
}_
var myString = "This is a test"
myString.myOwnFunction("Ten", "Two")


println

Infix Functions

This allows better looking code to achieve the same thing. To concatenate two fields we might do the following.

class Header(var Name: String {}

fun Header.plus(other: Header): Header {
    return Header(this.Name + other.Name)
}

var myH1 = new Header("H1")
var myH2 = new Header("H2")

val myH3 = myH1.plus(myH2)

By changing the prefix to have infix we can now write

...
infix fun Header.plus(other: Header): Header {
    return Header(this.Name + other.Name)
}
...
val myH3 = myH1 plus myH2

Tailrec Functions

I really liked this example. It demonstrates the fib series inside a recursive function. When looking for the 100,000 number a stack overflow occurs so you can see the point of marking recursive functions, where appropriate tailrec which turns it into a for loop under the covers.

fun main(args: Array<String>) {
   println(fib(100000, BigInteger("1"), BigInteger("0")))
}

tailrec fun fib(n: Int, a: BigInteger, b: BigInteger): BigInteger {
    return if (n == 0) b else fib(n-1, a + b, a)
}

High-level Functions

Old Approach

To print the fibonacci sequence we could do

fun main(args: Array<String>) {
    var program = Program()
    program.fibonacci(8)
}

class Program {
    fun fibonacci(limit: Int) {
        var prev = 0
        var prevprev = 0
        var current = 1
    
        // 1,1,2,3,5,8,13
        for(i: Int in 1..limit) {
            println(current)

            var temp = current
            prevprev = prev
            prev = temp
            current = prev + prevprev
        }
    }
}

Separate Strategy

What we want to do with the result can be separated from the calculation itself by passing in the approach using anonymous functions.

fun main(args: Array<String>) {
    // var program = Program()
    program.fibonacci(8, object : Process {
        override fun execute(value: Int) {
            println(value)
        }
    })
}

interface Process {
    fun execute(value: Int)
}

class Program {
    fun fibonacci(limit: Int, action: Process) {
        var prev = 0
        var prevprev = 0
        var current = 1
    
        // 1,1,2,3,5,8,13
        for(i: Int in 1..limit) {
            // println(current)
            action.execute(current);

            var temp = current
            prevprev = prev
            prev = temp
            current = prev + prevprev
        }
    }
}

Quick Break Lambda

Lambda are similar to the c# lambdas. Functions and be declared and called as below.

fun calculateResult(a: Int, b: Int, func: (Int) -> Unit) {
    // a and b = some result
...
    func(result)
}

// This can be called either by
// My favourite
calculateResult(1,2, { s -> print(s) } )
calculateResult(1,2, { print(it) } )

// Their favourite 
calculateResult(1,2) { s -> print(s) }
calculateResult(1,2) { print(it) }

// The super short and should be my favourite 
calculateResult(1,2, ::print)

Implementiing Lambda

So we can now use lambda with our code.

fun main(args: Array<String>) {

    program.fibonacci(8, {n ->println(n)})

    // Or as "it" is the variable name
    program.fibonacci(8) { println(it) }

    // Or
    program.fibonacci(8, ::println)
}

class Program {
    fun fibonacci(limit: Int, action: (Int) -> Unit) {
        var prev = 0
        var prevprev = 0
        var current = 1
    
        // 1,1,2,3,5,8,13
        for(i: Int in 1..limit) {
            // println(current)
            action(current);

            var temp = current
            prevprev = prev
            prev = temp
            current = prev + prevprev
        }
    }
}

Closures

Closures are functions that can access and modify properties defined outside the scope of the function. To do this in java is quite difficult but this is easy in kotlin and demonstrating the point of separating concerns. We could total up the values in a fib sequence by doing

fun main(args: Array<String>) {
    var total = 0;
    program.fibonacci(8) {it -> total += it) }
    println(total)
}

With and Apply

These are extension methods and allow the initialisation of objects. e.g.

   val m = SomeClass()

   with(m) {
       SomeProp1 = "prop1"
       SomeProp2 = "prop2"
       SomeProp3 = "prop3"
   }

// Apply returns an object to allow chaining 

   m.apply {
       SomeProp1 = "prop1"
       SomeProp2 = "prop2"
       SomeProp3 = "prop3"
   }.nextFunctionCall()

Classes

General

Defining classes is much like c++ and java

class Person() {
    var Name: String = ""

    fun display() {
        println("Display :$Name"
    }
}

They have the following charactistics

  • final by default.
  • abstract classes
  • Sealed classes
  • Constructors
  • Data Classes
  • Lambda Functions

Final by Default

The classes cannot be derived from by default. There functions final by default too. To override you need to specify the open keyword.

open class Person() {
    var Name: String = ""

    open fun display() {
        println("Display :$Name"
    }
}

Abstract Classes

This forces the user, as it does in java, c# and c++ to derive a class before it can be instantiated. Any functions which are abstract must also be implemented.

Sealed classes

Sealed classes allow you restrict class hierarchies.

sealed class Event {
    class SendEvent(id: Int, to: String) : Event()
    class ReceiveEvent(id: Int, to: String) : Event()
} 

// This can subsequently be used with a when
fun handEvent(e: Event) = 
   when(e) {
      is SendEvent -> print(e.to)
      is ReceiveEvent -> print(e.from)
   }

Constuctors

General

If you pass a var to a constructor the name is then associated with the class without declaring the value separately. You can also use val for readonly property.

class Person(var Name: String, val IAmReadonlyLastName: String) {

    fun display() {
        println("Display :$Name $IAmReadonlyLastName"
    }
}

Other Initialization

We have initialise class properties also with the init function

class Person() {

    val name: String

    fun init() {
        this.name = name
        this.last_name = last_name
    }
}

Or initialize using the incoming

class Person(_name: String) {
    val name = _name
}

Secondary Constructors can be used.

class Person(_name: String) {

    val name = _name
    val age = _age

    constructor(_name: String, _age: Int) {
        name = _name;
        age = _age;
    }
}

Data Classes

Using dataclass you get a hashcode method, a equals() method, a toString() method and a copy method. When a data class the objects are compared by value.

fun main(args: Array<String>) {
    var obj1 = Test(1,"Test")
    var obj2 = Test(1,"Test")

    if(obj1 == obj2) // Fails if not a data class
}

data class Test(val prop1: Int, val prop2: String) {}

Lambda Functions

Kotlin supports lambda functions

class Person(var Name: String) {

    fun display() {
        println("Display :$Name"
    }
  
    fun displayWithLambda(func: (s:String) -> Unit) {
        func(Name)
    }
}

Intefaces

Like java, kotlin has interfaces

General

You can have default arguments and functions. The default access if public by default.

interface Time {
    fun setTime(hours: Int, mins: Int = 0, secs: Int = 0)
    fun setTime(time: IainTime) = setTime(time.hours)
}

class MyTime : Time {
   override fun setTime(hours: Int, mins: Int = 0, secs: Int = 0) {
   }
}

Overriding

To override which interface to use if there are two, use the super keyword. This looks like something most unlikely but hey.

interface A { fun doIt() = {} }
interface B { fun doIt() = {} }

class foo : A, B {
    override fun doIt() = {
        super<A>.doIt()
    }
}

Companion Objects

Singletons

With kotlin we can create object classes for singletons. E.g.

object Meetings {
    var allMeetings = arrayListOf<Meeting>()
}
Meetings.allMeetings.add(Meeting(...))

These are difficult to test so should be avoided where possible.

Companion Objects for Classes

Kotlin does not have static methods. To achieve the same as these you used companion objects. For example maybe add two factory methods for a class

class Student {
    // Interface is optional
    companion object : SomeInterface {
       override fun SomeInterface(item: Student) {
       }
       fun createType1Student(name: String) : Type1 {}
       fun createType2Student(name: String) : Type2 {}
    }
}

val myObject = Student.createType1Student("Fred");

Exceptions

General

Kotlin, unlike old java does not require exceptions but you can use the standard approach.

   var reader = FileReader("Filename.txt")
   try {
      reader.read()
   } catch(e :IOException) {

   } finally {

   }

Other

Nullable Stuff

Let

Let allows you to process non null checks.

m?.Let {
   callFunctionWithNonNull)
}

Lateinit

Late init allows you to state the variable is lateinit. This let you override the compiler and initialize later in the code. i.e. you can have a non null declared but not initialized

Java Nulls

In java we can add @Nullable to ensure that nulls are not passed between the two.

public void addTitle(@NotNull String title) {
   this.title = title;
}

public @Nullable String meetingTitle() {
   return title;
}

Reading and Writing to Gson

Given the following

    var mySportsActivity = SportsActivity(
           0.0,
           0.0,
           0,
           0.0,
           Date())
   val gson = Gson()
   val json = gson.toJson(mySportsActivity)
   var filename = "D:\\IAIN\\Output.json";

You can write to a file with

    FileWriter(filename).use {
       writer ->
       val gson = GsonBuilder().setPrettyPrinting().create()
       gson.toJson(mySportsActivity, writer)
    }

And read it back with

    FileReader("D:\\IAIN\\Output.json").use {
       reader ->
       val gson = GsonBuilder().setPrettyPrinting().create()
       mySportsActivity2 = gson.fromJson(reader,SportsActivity::class.java)
    }