Du kan finne en presentasjonen som hører til workshopen under docs.
Denne workshopen er delt inn i to deler: den første delen gir deg en generell introduksjon til noen viktige konsepter i Kotlin, før vi videre skal bli litt mer kjent med backend-utvikling og spring.
Og ikke glem, bruk coachene og kollegaene dine aktivt! Vi er her for å hjelpe 🚀
Kotlin har to nøkkelord for å definere variabler: val
og var
.
val
brukes for å definere en variabel som ikke kan endres etter at den er opprettet, altså en konstant.
var
brukes for å definere en variabel som kan endres.
Oppgaver:
Åpne filen i introduction som heter Variabler.kt.
- Prøv å kjøre
main
-funksjonen. Hva får du ut i konsollen? - Bytt ut
var
medval
på linje 4, og kjørmain
-funksjonen igjen. Hva skjer nå? Hvorfor er dette ikke lov? - Skriv om koden slik at du fortsatt får samme utskrift som i del 1, uten å bruke
var
på linje 4. (Hint: Det er lov å definere nyevalues
basert på tidligere values)
Løsningsforslag 🤠
- Utskriften i konsollen skriver ut :
Verdien til tall1 er : 1
Verdien til tall1 pluss 1 er : 2
Verdien til tall1 pluss 1 delt på to er: 1
-
Koden kompilerer ikke lengre og en får en feilmelding som sier at
val
ikke kan endres. Dette er fordival
kan ikke defineres på ny etter at den har blitt definert. -
Løsningsforslag:
val tall1 = 1
println("Verdien til tall1 er: $tall1")
val tall2 = tall1 + 1
println("Verdien til tall1 pluss 1 er: $tall2")
val tall3 = tall2 / 2
println("Verdien til tall1 pluss 1 delt på to er: $tall3")
Funksjoner i kotlin defineres med fun
-nøkkelordet, og kan ha parametere og returverdier.
fun add(a: Int, b: Int): Int {
return a + b
}
Man kan også gi et parameter en defaultverdi ved å skrive = <verdi>
etter typen som dette: a: Int = 0
.
Oppgaver:
Åpne filen i introduction som heter Funksjoner.kt
- Lag en funksjon som heter
add
som tar to heltall som parametere og returnerer summen av dem. - Lag en ny funksjon som heter
addWithDefault
som tar to heltall som parametere, men det andre parameteret skal ha en defaultverdi på 0. Denne funksjonen skal returnere summen av de to tallene. - Skriv om funksjonen
addWithDefault
slik at du ikke trenger å brukereturn
-nøkkelordet eller krøllparenteser. Dette kan du gjøre ved å bruke en single-expression function.
Løsningsforslag 🤠
Løsningsforslag til oppgave 1:
fun add(a: Int, b: Int): Int {
return a + b
}
Løsningsforslag til oppgave 2:
fun addWithDefault(a: Int, b: Int = 0): Int {
return a + b
}
Løsningsforslag til oppgave 3:
fun addWithDefault(a: Int, b: Int = 0) = a + b
I Kotlin er det en viktig forskjell på typer som kan være null
og typer som ikke kan være null
.
Typer som kan være null
har et ?
-tegn etter typen, f.eks. String?
er en type som kan være null
, mens String
er
en type som ikke kan være null
.
Dette er en viktig forskjell fra Java, hvor alle typer kan være null
med mindre de er primitive typer som Int
,
Boolean
, etc.
I kotlin så må en eksplisitt håndtere null
-verdier, og det finnes flere måter å gjøre dette på:
-
Null-sjekk: Du kan sjekke om en verdi er
null
ved å brukeif
-setninger.val navn: String? = null if (navn != null) { println(navn.length) }
-
Safe call operator: Du kan bruke
?.
for å kalle en metode på en verdi som kan værenull
. Hvis verdien ernull
, vil hele uttrykket returnerenull
uten å kaste en feil.val navn: String? = null println(navn?.length) // Dette vil skrive ut null uten å kaste en feil
-
Elvis-operator: Du kan bruke
?:
for å gi en defaultverdi hvis verdien ernull
.val navn: String? = null println(navn?.length ?: 0) // Dette vil skrive ut 0 hvis navn er null
-
!!-operator: Du kan bruke
!!
for å kaste enNullPointerException
hvis verdien ernull
. Dette bør brukes med forsiktighet, da det kan føre til krasj i programmet.val navn: String? = null println(navn!!.length) // Dette vil kaste en NullPointerException hvis navn er null
Oppgaver:
Åpne filen i introduction som heter Nullability.kt.
- Kjør
main
-funksjonen. Hva skjer og hvorfor? (PS: Det kan være du må trykke på den øverste røde "error"-sirkelen og scrolle opp i terminalen for å se feilen :) ) - Kommenter ut kodelinje 8 og bytt ut typen på parameteret
streng
fraString?
tilString
. Hva skjer når du kjørermain
-funksjonen nå? Hvorfor må vi kommentere ut kodelinje 8? - Skriv en funksjon som tar inn et parameter av typen
String?
og returnerer lengden dersom strengen ikke er null. Dersom den er null, skal funksjonen returnerenull
. - Skriv en funksjon som tar inn et parameter av typen
String?
og returnerer lengden dersom strengen ikke er null. Dersom den er null, skal funksjonen returnere tallet 0.
Løsningsforslag 🤠
Oppgave 1:
Når du kjører main
-funksjonen, vil den kaste en NullPointerException
fordi vi prøver å kalle length
på en null
-verdi.
Oppgave 2:
Når du kommenterer ut kodelinje 8 og endrer typen på parameteret streng
fra String?
til String
, vil koden
kompilere og skrive ut
Lengden på streng1 er : 23
Vi må kommentere ut kodelinje 8 fordi String
-typen ikke kan være null
, og vi prøver å kalle length
på en null
-verdi.
Oppgave 3:
fun skrivUtStringlengde(streng: String?): Int? {
return streng?.length
}
Oppgave 4:
fun skrivUtStringlengde(streng: String?): Int? {
return streng?.length ?: 0
}
I Kotlin defineres klasser med class
-nøkkelordet. En klasse kan ha egenskaper (properties) og metoder (functions).
Egenskaper defineres med val
eller var
, og metoder defineres med fun
. Her er et eksempel på en klasse:
class Person(val name: String, var age: Int) {
fun introduce() {
println(
"Hei, jeg heter $name og er $age år gammel"
)
}
}
Oppgaver:
Åpne filen i introduction som heter Klasser.kt.
- Lag en klasse som heter
Coach
som har følgende egenskaper:navn
: StringaarIBekk
: Intfavorittaktivitet
: String
- Lag en funksjon i
Coach
-klassen som heterskrivUtInfo
som skriver ut navnet, antall år i Bekk og hva deres favorittaktivitet er.
Løsningsforslag 🤠
Oppgave 1:
class Coach(
val navn: String,
val aarIBekk: Int,
val favorittaktivitet: String,
)
Oppgave 2:
class Coach(
val navn: String,
val aarIBekk: Int,
val favorittaktivitet: String,
) {
fun skrivUtInfo() {
println("Navn: $navn, År i Bekk: $aarIBekk, Favorittaktivitet: $favorittaktivitet")
}
}
En data class
er en klasse kun ment til å representere data og har en del funksjonalitet som er nyttig for å jobbe med
data.
Når du definerer en dataklasse får du en del funksjonalitet gratis, som f.eks. toString
, equals
, hashCode
og
copy
.
Dette gjør at sammenligning av objekter blir enklere, og at du kan lage kopier av objekter med endrede verdier uten å
måtte skrive mye kode selv.
Oppgaver:
Åpne filen i introduction som heter DataClass.kt. Her
ligger det en klasse som heter Konsulent
og en main funksjon.
- Kjør main funksjonen, og se hva som skjer.
- Gjør
Konsulent
om til endata class
og kjør main funksjonen igjen. Hvilke to forskjeller ser du ser du i konsollutskriften? Hvorfor er det slik?
Løsningsforslag 🤠
I konsollutskriften ser vi to endringer:- Vi får en fin utskrift av objektet vårt, i stedet for at det bare står
Konsulent@<hashkode>
. - Vi får
true
når vi sammenligner to konsulenter med samme navn, i stedet forfalse
.
Endringen i objektutskriften er fordi Konsulent
alle vanlige klasser (class
) har en default implementasjon av
toString
som skriver ut klassenavnet og en hashkode.
Dette fører til at utskriften av println(konsulent)
blir noe sånt som Konsulent@6d06d69c
. Instanser av data class
derimot,
har en implementasjon av toString
som skriver ut alle feltene i klassen, slik at utskriften blir noe sånt som
Konsulent(name=Patrick)
.
Endringen i sammenligningen er fordi vanlige klasser (class
) har en default implementasjon av ==
(eller equals
som
det heter)
som bare sammenligner referansene til objektene, altså om de er det samme objektet i minnet.
data class
derimot, har en implementasjon av equals
som sammenligner innholdet i objektene, altså om de har samme
verdi for alle feltene.
Dette fører til at når Konsulent er en data class
så får vi true
dersom vi sammenligner to objekter med samme navn.
I Kotlin er man ofte opptatt av mutability
og immutability
, eller "muterbarhet" og "ikke-muterbarhet".
Disse konseptene referer til hvorvidt dataen i et objekt kan endres etter at det objektet har blitt opprettet.
En stor fordel med å gjøre ting immutable
er at koden kan bli mer lesbar og lettere å debugge. Eksempler på hvordan
dette kommer frem er:
- Ett ikke-muterbart objekt du bruker kan du alltids være sikker på at ikke kommer til å endre seg et annet sted i koden. Dette reduserer utilsiktede utfall da det er vanskeligere å påvirke annen funksjonalitet ved "uhell" dersom den funksjonaliteten også bruker samme data.
- En kan alltids spore verdier tilbake til der de ble opprettet. Dette gjør det lettere å forstå hvor i koden en verdi ble opprettet, hvor den har blitt brukt og hvor den skal brukes videre.
I standardbiblioteket til Kotlin skiller man på datastrukturer som er muterbare og de som ikke er det. F.eks:
det finnes både List<T>
og MutableList<T>
. Begge disse typene er lister, men typen List
er ikke-muterbar og lar
deg dermed ikke legge til eller fjerrne
elementer fra lista. Typen MutableList<T>
lar deg bruke funksjonene add
og remove
for å legge til og fjerne
elementer fra lista.
Dersom en har et objekt som en ønsker å endre på, så er det vanlig å lage en kopi av objektet og deretter utføre endringene en ønsker å gjøre. F.eks.
val tall = listOf(1, 2, 3)
val flereTall =
tall + 4 // Dette lager en kopi av lista `tall` og legger til 4 i den kopierte lista, uten å endre på `tall`.
println(tall) // -> [1, 2, 3]
println(flereTall) // -> [1, 2, 3, 4]
Egendefinerte dataklasser har også støtte for dette gjennom copy
-funksjonen som genereres automatisk når du definerer
en data class
.
Denne gir deg muligheten til å lage en kopi av et objekt med noen av feltene endret, mens de andre feltene forblir
uendret.
val patrick = Person(name = "Patrick", age = 27)
val eldrePatrick =
patrick.copy(age = 28) // Dette lager en kopi av `patrick` som er 28 år gammel, men beholder navnet "Patrick".
Oppgaver:
Åpne filen i introduction som heter Mutability.kt
Oppgave 1:
- Kommenter inn linjen med
patrick.name
, og undersøk feilen du får. Hvorfor er ikke dette lov? - Hvordan kan du opprette et nytt person-objekt med samme verdi for
age
men med et annet navn?"
Oppgave 2:
Din oppgave er å få skrevet ut en liste med tallene fra lista viktigeTall
men med tallet 4 lagt på slutten.
Dette skal da gjøres uten å endre på hvordan funksjonen funkSjonalitetSomIkkeLikerTalletFire
fungerer.
- Kommenter inn linja
skrivUtTallListeOgTalletFire(viktigeTall)
og kjør koden. Hvorfor kræsjer koden? - Skriv om koden i funksjonen
skrivUtTallListeOgTalletFire
slik at du ikke endrer verdiene i listaviktigeTall
men fortsatt får skrevet ut lista med tallet 4 på slutten.
Løsningsforslag 🤠
Løsningsforslag til oppgave 1:
Linja patrick.name = "Sondre"
er ikke lov fordi name
er definert som en ikke-muterbar verdi med nøkkelordet val
og
kan dermed ikke endres etter at objektet er opprettet.
val patrick = Person("Patrick", 27)
val sondre = patrick.copy(name = "Sondre")
println(sondre) // -> Person(name=Sondre, age=27)
Koden kræsjer fordi vi legger til tallet 4 i den muterbare lista viktigeTall
i funksjonen
skrivUtTallListeOgTalletFire
.
Dette fører til at koden kræsjer i funksjonen funkSjonalitetSomIkkeLikerTalletFire
, da den funksjonen ikke krever at
lista ikke kan ikkeholde tallet 4.
Løsningsforslag til oppgave 2:
fun skrivUtTallListeOgTalletFire(viktigeTall: MutableList<Int>) {
val viktigeTallMedFire = viktigeTall + 4 // Lager en kopi av lista uten å endre på den originale lista.
println(viktigeTallMedFire) // -> [1, 2, 3, 4]
}
Kotlin har støtte og legger veldig til rette for bruk av funksjoner som kalles Higher Order Functions
.
Det høres kanskje litt stort ut, men en Higher Order Function
er bare en funksjon tar en eller flere funksjoner som
argumenter,
eller returnerer en funksjon som resultat. Ett eksempel på en slik funksjon er map
-funksjonen som finnes på lister i
Kotlin:
fun doble(tall: Int): Int {
return tall * 2
}
val tallListe = listOf(1, 2, 3)
val dobledeTall = tallListe.map({ tall -> doble(tall) }) // -> [2, 4, 6]
Her ser man hvordan map
-funksjonen tar inn funksjonen doble
, og bruker den til å transformere hvert element i
listen.
For lesbarhetens skyld pleier en ofte å kombinere slike higher order functions
med funksjoner uten navn, bedre kjent
somanonyme
- funksjoner eller lambda
-funksjoner.
Vi kan f.eks. skrive om koden over til å bruke en lambda
i stedet for å definere en egen funksjon:
val tallListe = listOf(1, 2, 3)
val dobledeTall = tallListe.map({ tall -> tall * 2 }) // -> [2, 4, 6]
Standardbiblioteket til Kotlin bruker lambda ganske mye, og språket har en del syntaktisk støtte for å gjøre det enklere
å bruke.
F.eks. En trenger ikke å navngi argumentene i en lambda og en kan referere til dem direkte ved å bruke it
som et
standardnavn istedenfor.
listOf(1, 2, 3).map({ tall -> tall * 2 }) // [2, 4, 6]
listOf(1, 2, 3).map({ it * 2 }) // [2, 4, 6]
Og hvis en lambda er det siste argumentet til en funksjon kan en også droppe bruken av paranteser.
listOf(1, 2, 3).map { it * 2 } // [2, 4, 6]
Store deler av det vi gjør som utviklere er å hente data, manipulere den og deretter bruke den videre i applikasjonene våre. I Kotlin finnes det mange "higher order functions" som gjør dette veldig mye lettere enn i Java. Andre eksempler inkluderer da
filter
, for filtrering:
listOf(1, 2, 3).filter { it != 2 } // [1, 3]
reduce
, for å slå sammen elementer i en liste basert på en funksjon:
listOf(
1,
2,
3
).reduce { tall1, tall2 -> tall1 + tall2 } // Plusser sammen alle tallene i lista, og returnerer 6 (Ekvivalent til sum)
partition
, for å dele en liste i to basert på en betingelse:
val (tallUnderTo, tallMedToEllerMer) = listOf(1, 2, 3).partition { it < 2 }
println(tallUnderTo) // [1]
println(tallMedToEllerMer) // [2, 3]
any
, for å sjekke om minst ett element i en liste oppfyller en betingelse:
val inneholderTalletTo = listOf(1, 2, 3).any { it == 2 }
println(inneHolderTalletTo) // true
Kraften i bruken av slike higher order functions
og lambda-funksjoner er at de lar deg skrive kode som bryter ned
oppgaver i mindre biter og dermed er lettere å lese.
F.eks. vi for å sjekke om en liste inneholder et positivt tall hvor dens kvadrat er større enn 20
listOf(-5, -2, 0, 2, 5)
.filter { it > 0 } // Filtrer ut negative tall -> [2, 5]
.map { it * it } // Kvadrer tallene -> [4, 25]}
.any { it > 20 } // Sjekk om noen av tallene er større enn 20 -> true
Oppgave:
Åpne filen som heter HighOrderFunction.kt:
- Bruk
coacher2025
-listen, og bruk lambdafunksjon(er) for å finne ut hvor mange år alle coachene i 2025 har jobbet i Bekk. - Bruk
coacher2025
-listen, og lag en liste for coachene som er i teknologi-avdelingen. - Bruk
coacher2025
-listen, og skriv kode for å lage en kopi av lista hvor Frikk er i BMC-avdelingen og Sivert er i Design-avdelingen.
Bruk main-funksjonen til å sjekke at du får riktig resultat.
Løsningsforslag 🤠
val antallAarIBekk = coacher2025.map { it.aarIBekk }.reduce { aarIBekk1, aarIBekk2 -> aarIBekk1 + aarIBekk2 }
val teknologiCoacher = coacher2025.filter { it.avdeling == Avdeling.TEKNOLOGI }
val endredeCoacher = coacher2025.map { coach ->
when (coach.name) {
"Frikk" -> coach.copy(avdeling = Avdeling.BMC)
"Sivert" -> coach.copy(avdeling = Avdeling.DESIGN)
else -> coach
}
}
Du kan lese mer om high order functions i Kotlin her
Her bruker vi when
-uttrykket, som ikke ble dekket i presentasjonen. Det kan du lese mer
om her.
Noen ganger har vi behov for spesialtilpasset funksjonalitet på en klasse som vi ikke har tilgang til å endre, f.eks. de
innebygde klassene Int
eller String
.
Da kan du skrive en spesiell type funksjon som heter "Extension Functions".
Funksjonen kan skrives på følgende måte:
fun <Klasse> .<funksjonsnavn>(<argumenter>): <return-type> {
// Gjør noe
}
For å refere til instansen av klassen bruker vi this
. Ett eksempel på en slik funksjon er:
fun List<Int>.doble(): List<Int> = this.map { it * 2 }
listOf(1, 2, 3).doble() // -> [2, 4, 6]
Dersom en kombinerer extension functions med higher order functions
og lambda-funksjoner kan en få veldig lesbar kode.
Ta f.eks. det tidligere eksempelet med å sjekke om en liste inneholder et positivt tall hvor kvadratet er større enn 20:
listOf(-5, -2, 0, 2, 5)
.filter { it > 0 } // Filtrer ut negative tall -> [2, 5]
.map { it * it } // Kvadrer tallene -> [4, 25]}
.any { it > 20 } // Sjekk om noen av tallene er større enn 20 -> true
Dette kan skrives om til:
val List<Int>.fjernNegativeVerdier(): List<Int> = this.filter { it > 0 }
val List<Int>.kvadrerVerdier(): List<Int> = this.map { it * it }
val List<Int>.sjekkOmListaHarEnVerdiOver(verdi: Int): List<Int> = this.any { it > verdi }
listOf(-5, -2, 0, 2, 5)
.fjernNegativeVerdier()
.kvadrerVerdier()
.sjekkOmListaHarEnVerdiOver(20) // -> true
Oppgave: Oppgavene ligger i fila ExtensionFunctions.kt.
- Lag en extension function for
List<BootcampCoach>
som returnerer bare Coacher fra en avdeling. - Lag en extension function for
List<BootcampCoach>
som skriver ut navn, antall år i Bekk og avdeling for alle bootcampcoachene. - Lag en extension function for
List<BootcampCoach>
som returnerer totalt antall år i Bekk for alle coachene i listen. - Lag en extension function for
List<BootcampCoach>
som øker antall år i Bekk for alle coachene i listen med et gitt antall år.
Løsningsforslag 🤠
// Oppgave 1
fun List<BootcampCoach>.fraAvdeling(avdeling: Avdeling): List<BootcampCoach> {
return this.filter { it.avdeling == avdeling }
}
// Oppgave 2
fun List<BootcampCoach>.skrivUtInfo() {
this.forEach { coach ->
println("${coach.name} er i avdeling ${coach.avdeling} og har jobbet ${coach.aarIBekk} år i Bekk")
}
}
// Oppgave 3
fun List<BootcampCoach>.totaltAntallAarIBekk(): Int {
return this.map { it.aarIBekk }.reduce { total, aar -> total + aar }
}
// Oppgave 4
fun List<BootcampCoach>.leggTilAar(aar: Int): List<BootcampCoach> {
return this.map { coach ->
coach.copy(
aarIBekk = coach.aarIBekk + aar
)
}
}
Du kan lese mer om extension functions i den offisielle Kotlin-dokumentasjonen.
Nå har du fått en introduksjon til noen av de viktigste konseptene i Kotlin, og du er klar for å begynne arbeidet med Kordle! En introduksjon til både Kordle og oppgavene finner du i her.