Wprowadzenie do języka Kotlin - W01 PDF

Summary

Ten dokument to wprowadzenie do języka Kotlin. Zawarte są w nim cele przedmiotu, wymagania wstępne i tematyka wykładów. Zawiera również zalecaną literaturę. Treść opiera się na notatkach z zajęć akademickich, a nie na oficjalnych materiałach egzaminacyjnych.

Full Transcript

Wprowadzenie do języka Kotlin Cele przedmiotu i wymagania wstępne Cele przedmiotu: Poznanie języka Kotlin Poznanie współczesnych sposobów tworzenia interfejsu użytkownika Poznanie zalecanych zasad tworzenia architektury aplikacji mobilnej Wymagan...

Wprowadzenie do języka Kotlin Cele przedmiotu i wymagania wstępne Cele przedmiotu: Poznanie języka Kotlin Poznanie współczesnych sposobów tworzenia interfejsu użytkownika Poznanie zalecanych zasad tworzenia architektury aplikacji mobilnej Wymagania wstępne: Znajomość programowania obiektowego. Język angielski – stopień podstawowy Znajomość tematyki struktur danych oraz sieci komputerowych Zaliczenie pisemne w formie testu z pytaniami zamkniętymi Efekty uczenia się W zakresie wiedzy: Student zna najważniejsze elementy języka Kotlin Student potrafi scharakteryzować architekturę współczesnej aplikacji mobilnej Student zna współczesne metody tworzenia interfejsu użytkownika W zakresie umiejętności: Student potrafi zaprojektować i zaimplementować aplikację mobilną o architekturze pozwalającej na łatwe utrzymanie i rozbudowę aplikacji Student potrafi tworzyć interfejsy użytkownika wykorzystując różne metody W zakresie kompetencji społecznych: Student zdaje sobie sprawę z dynamicznego rozwoju technologii mobilnych i rozumie potrzebę rozwijania i aktualizowania swojej wiedzy Tematyka wykładów Wprowadzenie do języka Kotlin – składnia, system typów, wyrażenia, operatory, funkcje Klasy i interfejsy w języku Kotlin – składnia, dziedziczenie, metody rozszerzające Metody tworzenia interfejsu użytkownika w systemie Android Nawigacja w aplikacji – graf nawigacji, przechodzenie po grafie, przekazywanie parametrów Programowanie asynchroniczne w języku Kotlin z wykorzystaniem współprogramów Architektura aplikacji – wprowadzenie; zarządzanie stanem, obsługa cyklu życia; wiązanie widoków i danych; przechowywanie danych; zarządzanie wykonaniem zadań w tle Wstrzykiwanie zależności w systemie Android Tworzenie testów aplikacji mobilnych Zalecana literatura Strona internetowa: https://kotlinlang.org/ Strona internetowa: https://developer.android.com Efektywny Kotlin: najlepsze praktyki, Marcin Moskała, przekład: Tomasz Walczak, Gliwice, Helion, 2021 Kickstart Modern Android Development with Jetpack and Kotlin, Catalin Ghita, Packt Publishing, 2022 Kotlin language specification (https://kotlinlang.org/spec/) Java and Kotlin code performance in selected web frameworks, Grzegorz Bujnowski, Jakub Smołka, JCSI - Journal of Computer Sciences Institute.- 2020, vol. 16, s. 219-226 Analysis of the development Android’s runtime, Kostiantyn Honcharenko, Jakub Smołka, JCSI - Journal of Computer Sciences Institute.- 2019, vol. 12, s. 246-251 Wydajność języków C++ oraz Java na platformie Android, Paweł Wlazło, Jakub Smołka, JCSI - Journal of Computer Sciences Institute.- 2022, vol. 23, s. 135-139 Programowanie aplikacji dla systemu Android, Jakub Smołka, Lublin, Politechnika Lubelska, 2014 Język Kotlin Kotlin - wieloplatformowy, statycznie typowany, uniwersalny język programowania wysokiego poziomu z wnioskowaniem typów. Zaprojektowany tak, aby w pełni współdziałał z Javą; wersja standardowej biblioteki Kotlina w wersji JVM zależy od biblioteki klas Java Działa na JVM, ale także kompiluje się do JavaScript lub kodu natywnego poprzez LLVM Od Android Studio 3.0 (10/2017) Kotlin jest dołączany jako alternatywa dla standardowego kompilatora Java. Kompilator Android Kotlin domyślnie generuje kod bajtowy Java 8, ale pozwala programiście wybrać docelową wersję Java 9 do 20 w celu optymalizacji Historia Lipiec 2011 – firma JetBrains zaprezentowała Projekt Kotlin - nowy język dla JVM, nad którym pracowano od roku Luty 2012 - udostępniono projekt jako open source na licencji Apache 2 Nazwa pochodzi od wyspy Kotlin 15 lutego 2016 roku – wydanie Kotlin 1.0; firma JetBrains zobowiązała się do zapewnienia długoterminowej kompatybilności, począwszy od tej wersji Podczas Google I/O 2017 firma Google ogłosiła pierwszorzędne wsparcie dla Kotlina na Androida 28 listopada 2017 roku – wydanie Kotlina 1.2; dodano funkcję współdzielenia kodu pomiędzy platformami JVM i JavaScript 29 października 2018 roku – wydanie Kotlina 1.3 -dodano współprogramy do programowania asynchronicznego. 7 maja 2019 r. firma Google ogłosiła, że język programowania Kotlin jest obecnie preferowanym językiem dla twórców aplikacji na Androida Sierpień 2020 roku – wydanie Kotlina 1.4 - zawierał m.in. kilka drobnych zmian w obsłudze platform Apple (współpraca z Objective-C/Swift) Maj 2021 – wydanie Kotlina 1.5; listopad 2021 – wydanie Kotlina 1.6; czerwiec 2022 – wydanie Kotlina 1.7 (nowy kompilator K2 w wersji alfa); grudzień 2022 – Kotlin 1.8 , lipiec 2023 – Kotlin 1.9 Podstawowe typy danych Kategoria Typy Całkowite ze znakiem Byte, Short, Int, Long Całkowite bez znaku UByte, UShort, UInt, ULong Zmiennoprzecinkowe Float, Double Logiczne Boolean Znaki Char Ciągi znaków String Podstawowe typy danych Any – korzeń hierarchii klas w Kotlinie; wszystkie klasy dziedziczą po Any (podobnie jak w Javie po Object) Unit – typ z jedną wartością – obiektem Unit (odpowiednik void z Javy) Nothing – nie ma instancji; służy do reprezentowania wartości, która nigdy nie istnieje np. jeżeli funkcja ma typ Nothing oznacza to, że nigdy nie zwraca wartości (zawsze zgłasza wyjątek) Podstawy składni fun main(args: Array) { //zmienne var a: Int = 1 //typ podaje się po dwukropku; typowanie //statyczne var b = 1 //Int var c = 1.0 //Double println("$a $b $c") //1 1 1.0 a++; b++; c++ println("$a $b $c") //2 2 2.0 val d = 1 println(d) //1 //d=d+1 //d jest immutable b = c.toInt() //konwersja między typami (inne podobnie) println(b) Podstawy składni //szablony łańcuchów var e = 1 val s1 = "e is $e" println(s1) //e is 1 e = 2 //wyrażenia muszą być w {} po znaku $ val s2 = "${s1.replace("is", "was")}, but now is $e" println(s2) //e was 1, but now is 2 //instrukcje sterujące //if w zasadzie standardowy ale jest wyrażeniem //(nie ma "war ? wart1 : wart2) //a == 2, d ==1 println(if (a>d) a else d) //2 val f = if (a>d) { 10 20 } else 30 println(f) //20 - wartość wyrażenia to wartość ostatniej linii w {} Podstawy składni val text = "Hello" //when = "odpowiednik" switch when (text) { //prównuje wartości do text "1" -> println("One") "Hello" -> println("Greeting") else -> println("Unknown") //else nie zawsze jest potrzebne (np. w //przypadku typów wyliczeniowych) } //Greeting val result = when (text) { //też jest wyrażeniem "Hello" -> "Greeting" else -> "Unknown" } // Greeting val temp = 18 val description = when { temp < 15 -> "cold" //można też używać wyrażeń logicznych temp "warm" else -> "hot" } println(description) // warm Podstawy składni //tablice //funkcje do tworzenia tablic val arr1= arrayOf(1,2,3) //[1, 2, 3] println(Arrays.toString(arr1)) val arr2 = arrayOfNulls(4) //null null null null arr2.forEach { print("$it ") }; println() //użycie klasy i konstruktora val arr3 = Array(5) { i -> (i * i).toString() } println(Arrays.toString(arr3)) //[0, 1, 4, 9, 16] arr3="10" //są też tablice z typami prostymi ByteArray, ShortArray, IntArray... val x: IntArray = intArrayOf(1, 2, 3) x = x + x //[5, 2, 3] val arr4 = IntArray(5) //[0, 0, 0, 0, 0] val arr5 = IntArray(5) { 42 } //[42, 42, 42, 42, 42] var arr6 = IntArray(5) { it * 1 } //[0, 1, 2, 3, 4] Podstawy składni //zakresy //for - z zakresem for (number in 1..3) { print(number) } //123 println() //for po kolekcji val cakes = listOf("carrot", "cheese", "chocolate") for (cake in cakes) { print("$cake cake ") } //carrot cake cheese cake chocolate cake println() //while i do while typowe Podstawy składni //sprawdzanie typów i rzutowanie var obj:Any = "abc" if (obj is String) { println(obj.length) //3; //obj zostało automatycznie rzutowane na String } if (obj !is String) return println(obj.length) //3; tutaj obj też jest rzutowane na String //tutaj po prawej stronie || i && będzie podobnie if (obj !is String || obj.length == 0) return if (obj is String && obj.length > 0) { println(obj.length) //3 } Podstawy składni //"is" działa też z when when (obj) { is String -> println("obj is String") else -> println("obj is something else") } //obj is String //jawne rzutowanie val obj2:Any = 1 //val str1:String = obj2 as String //nie ok - wyjątek jeżeli rzutowanie nie jest prawidłowe val int1:Int = obj2 as Int //ok val str2:String? = obj2 as? String //nie ok - str2 == null } Zakresy fun main(args: Array) { //zakresy (ranges) println(4 in 1..4) //true - zakres obustronnie domknięty println(4 in 1.. string.uppercase() } println(upperCaseString("hello")) //HELLO Wyrażenia lambda //przekazywanie jako parametr val numbers = listOf(1, -2, 3, -4, 5, -6) // normalna składnia val positivesDoubled = numbers.filter({ x -> x > 0 }) // specjalna składnia.map { x -> x * 2 } println(positivesDoubled) // [2, 6, 10] val sum = positivesDoubled.fold(0, { acc, item -> acc + item }) println(sum) //18 // końcowa lambda val sum2 = positivesDoubled.fold(0) { acc, item -> acc + item } println(sum2) //18 Wyrażenia lambda //typy wyrażeń lambda np. (Int, Int) -> Int , () -> Unit val upperCaseString2: (String) -> String = { string -> string.uppercase() } println(upperCaseString2("hello")) //HELLO //returny i lambdy fun foo() { listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return // wychodzi z funkcji a nie z lambdy print(it) } println("this point is unreachable") } foo() // 12 println() Wyrażenia lambda fun foo2() { listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) { //zastąpienie lambdy //funkcją anonimową if (value == 3) return //wyjście z funkcji anonimowej print(value) }) println(" done with anonymous function") } foo2() //1245 done with anonymous function fun foo3() { listOf(1, 2, 3, 4, 5).forEach lit@{ //etykieta if (it == 3) return@lit //return do etykiety print(it) } println(" done with explicit label") } foo3() //1245 done with explicit label Wyrażenia lambda fun foo4() { listOf(1, 2, 3, 4, 5).forEach { // niejawna etykieta if (it == 3) return@forEach // powrót do wywołującego // lambdy – pętli forEach print(it) } println(" done with implicit label") } foo4() //1245 done with implicit label } Klasy – konstruktor i inicjalizacja class Customer //główny konstruktor class Customer2 constructor(firstName:String) { //... } //jeżeli konstruktor nie ma modyfikatorów np. private albo adnotacji to //można pominąć class Customer3(firstName: String,lastName: String) { //inicjalizacja wykona się w takiej kolejności jak w kodzie val firstNameProp:String = firstName.also(::println) init { println("init#1, firstName=$firstName") } Klasy – konstruktor i inicjalizacja val lastNameProp:String = lastName.also(::println) init { println("init#2, lastName=$lastNameProp") } } fun main(args: Array) { //tworzenie obiektu - nie ma new val cust=Customer3("Andrew","Android") //Andrew /n init#1, //firstName=Andrew /n Android /n init#2, lastName=Android } Klasy – podstawowy i dodatkowe konstruktory //podstawowy konstruktor class Person(val name: String) { val children: MutableList = mutableListOf() //dodatkowy konstruktor - musi być delegacja do podstawowego (słowo this) constructor(name: String, parent: Person) : this(name) { parent.children.add(this) } } //chyba że konstruktor podstawowy nie ma parametrów - wtedy delegacja niejawna class Constructors { init { println("Init block") } constructor(i: Int) { //użycie tego konstruktora i tak wykona blok init println("Constructor $i") } } Klasy – właściwości //właściwości - w nawiasach po nazwie klasy, mogą mieć domyślne wartości class Contact( val id: Int, var email: String = "[email protected]" ) { //lub w {} val category: String = "work" //mogą mieć domyślne wartości //gettery, settery i pola var name:String = "" //inicjalizator przypisuje wartość do pola get() = field //getter może mieć krótką postać set(value) { //do pola odwołujemy się za pomocą field field = value } } fun main(args: Array) { val contact = Contact(1, "[email protected]") val contact2 = Contact(2) println(contact.email) //[email protected] contact.name="Mary" println(contact.name) } Klasy - widoczność Funkcje, właściwości, klasy, obiekty i interfejsy zadeklarowane na „najwyższym poziomie” (bezpośrednio w pakiecie): public = deklaracje widoczne wszędzie (domyślny) private = deklaracja widoczna tylko w pliku zawierającym tę deklarację internal = deklaracja widoczna w wszędzie w tym samym module (=moduł IntelliJ, projekt Maven, Gradle source set) protected nie jest dostępny dla deklaracji najwyższego poziomu Klasy - widoczność Składowe klasy: private = składowa widoczna tylko wewnątrz tej klasy protected = jak prywatny, ale jest również widoczny w podklasach internal = każdy element wewnątrz tego samego modułu, który widzi deklarującą klasę, widzi jej wewnętrzne elementy public = każdy element który widzi klasę deklarującą, widzi jej publiczne składowe (domyślny) Widoczność konstruktora określa się przed słowem constructor (wtedy nie można go pominąć) Klasy – dziedziczenie //klasa musi być open aby można było po niej dziedziczyć open class Base(p: Int) //domyślnie dziedziczy po Any, która ma metody //equals(), hashCode() i toString() class Derived(p: Int) : Base(p) //jawne określenie klasy bazowej i //przekazanie parametrów konstruktora open class Shape { open val vertexCount: Int = 0 open fun draw() { } //tylko otwarte metody można nadpisać w //klasach pochodnych fun fill() { } //metody nie można nadpisać open fun setColor(color: Color) { } } Klasy – dziedziczenie class Circle() : Shape() { override fun draw() { } //napisywanie metody final override fun setColor(color: Color) { //final - w klasach //pochodnych nie można nadpisać //... } } //właściwość można nadpisać w konstruktorze podtawowym class Rectangle(override val vertexCount: Int = 4) : Shape() // zawsze //ma 4 wierzchołki class Polygon : Shape() { override var vertexCount: Int = 0 //właściwość val można nadpisać //za pomocą var (ale nie na odwrót) } Klasy danych //automatyczne toString, equals, ==, copy, hashCode(), component1()... //główny konstruktor musi mieć co najmniej 1 parametr //nie mogą być abstrakcyjne, otwarte, szczelne (sealed), wewnętrzne (inner) //mogą mieć ciało data class User(val name: String, val id: Int) fun main(args: Array) { val user = User("Alex", 1) println(user) //User(name=Alex, id=1) val secondUser = User("Alex", 1) val thirdUser = User("Max", 2) println("user == secondUser: ${user == secondUser}") //user == secondUser: // true println("user == thirdUser: ${user == thirdUser}") //user == thirdUser: // false //można łatwo stworzyć kopię i ewentualnie zmienić właściwość println(user.copy()) //User(name=Alex, id=1) println(user.copy("Max")) //User(name=Max, id=1) println(user.copy(id = 3)) //User(name=Alex, id=3) } Klasy zagnieżdżone i wewnętrzne class Outer { private val bar: Int = 1 class Nested { //fun foo() = bar //w zagnieżdżonych nie ma dostępu do //składowych klasy zawierającej fun foo() = 2 } inner class Inner { fun foo() = bar //w wewnętrznych jest } } fun main(args: Array) { println(Outer.Nested().foo()) //2 println(Outer().Inner().foo()) //1 //tutaj musimy mieć obiekt Outer } Funkcje rozszerzające //funkcja rozszerzająca może być ogólna (pierwsze ) //funkcja rozszerza klasę MutableList fun MutableList.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' to lista this[index1] = this[index2] this[index2] = tmp } fun main(args: Array) { val list = mutableListOf(1, 2, 3) list.swap(0, 2) // 'this' wewnątrz 'swap()' to referencja do listy println(list) } Funkcje rozszerzające open class Shape class Rectangle: Shape() { fun getName() = "in class Rectangle" } fun Shape.getName() = "Shape" fun Rectangle.getName() = "Rectangle" fun main(args: Array) { val shape:Shape = Rectangle() //wywołania funkcji rozszerzających są rozwiązywane statycznie //(wywołanie z typu zmiennej) println(shape.getName()) //Shape val rectangle:Rectangle = Rectangle() //w przypadku konfliktu składowa klasy zawsze wygrywa println(rectangle.getName()) //in class Rectangle } Interfejsy interface MyInterface { val prop: Int // abstrakcyjna właściwość val propertyWithImplementation: String get() = "foo" fun foo() { //metoda z implementacją domyślną print(prop) } fun bar() //metoda abstrakcyjna } Interfejsy //implementacja interfejsu class Child : MyInterface { override val prop: Int = 29 //właściwość bez implementacji override fun bar() { //i metoda abstrakcyjna muszą być zaimplementowane println(propertyWithImplementation) } } fun main(args: Array) { val anonymous = object : MyInterface { //podobnie jeżeli chcemy dziedziczyć //po klasie override val prop: Int get() = 42 override fun bar() { println("bar") } } println(anonymous.prop) //42 } Singleton object Singleton { //nie może mieć konstruktora var a=1 } data object DataSingleton { //wygenerowane toString(), equals(), //hashCode() ale nie ma copy() i //componentN() var a=3 } fun main(args: Array) { println(Singleton.a) //1 Singleton.a=2 println(Singleton.a) //2 println(DataSingleton) //DataSingleton - toString zwraca nazwę } Obiekty towarzyszące class MyClass { companion object Factory { //obiekt towarzyszący może być nazwany fun create(): MyClass = MyClass() } } class MyClass2 { companion object { //obiekt towarzyszący może być nienazwany fun create(): MyClass2 = MyClass2() } } fun main(args: Array) { //niezależnie od tego czy obiekt towarzyszący jest nazwany czy nie możemy //po prostu użyć nazwy klasy val instance = MyClass.create() val instance2 = MyClass2.create() //jeżeli jest nienazwany możemy sie odwołać za pomocą Companion val companion = MyClass.Factory val companion2 = MyClass2.Companion } Obiekty towarzyszące //mimo, że obiekty towarzyszące przypominają składowe statyczne to są //prawdziwymi obiektami i mogą np. implementować interfejsy interface Factory { fun create(): T } class MyClass3 { companion object : Factory { override fun create(): MyClass3 = MyClass3() } } Delegacja interfejsów interface Base { val y:Int; fun print() fun print2() } class BaseImpl(val x: Int) : Base { override val y = 15 override fun print() { println("$x $y") } override fun print2() { println("2") } } Delegacja interfejsów class Derived(b: Base) : Base by b { //delegacja imlpementacji interfejsu do składowych publicznych obiektu b override val y = 20 //implementacja print z delegata nie ma dostępu do tej właściwości override fun print2() { //funkcje z delegata można nadpisać println("2 override") } } fun main() { val b = BaseImpl(10) Derived(b).print() //10 15 Derived(b).print2() //2 override } Delegacja właściwości class Example { var p: String by Delegate() } //są gotowe klasy i funkcje, które umożliwiają łatwe tworzenie delegatów np. //lazy()/Lazy i w klasie Delegates (observable, veoable) class Delegate { //nie musi implementować interfejsu ale muszą mieć funkcje //operatorów getValue i setValue (dla var) i getValue (dla val) operator fun getValue(thisRef: Any?, property: KProperty): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty, value: String) { println("$value has been assigned to '${property.name}' in $thisRef.") } } Delegacja właściwości Lazy() - funkcja, która pobiera lambdę i zwraca instancję Lazy - implementację leniwej właściwości pierwsze wywołanie get() wykonuje lambdę przekazaną do funkcji lazy() i zapamiętuje wynik kolejne wywołania get() po prostu zwracają zapamiętany wynik Delegates.observable() - przyjmuje dwa argumenty: wartość początkową i funkcję (lambdę) obsługującą modyfikację funkcja obsługi jest wywoływana za każdym razem, gdy właściwość jest modyfikowana (po wykonaniu przypisania). Funkcja obsługi ma trzy parametry: właściwość, do której jest przypisana, starą wartość i nową wartość Delegates.vetoable() - zwraca delegata właściwości (do odczytu/zapisu), który po zmianie właściwości wywołuje funkcję obsługi, która ma możliwość zawetowanie modyfikacji Delegacja właściwości class User(val map: Map) { val name: String by map val age: Int by map } fun main(args: Array) { val e = Example() println(e.p) //Example@6e8cf4c6, thank you for delegating 'p' to me! e.p = "NEW" //NEW has been assigned to 'p' in Example@6e8cf4c6. val user = User(mapOf( "name" to "John Doe", "age" to 25 )) println(user.name) //John Doe println(user.age) //25 } Null safety fun main(args: Array) { var neverNull: String = "This can't be null" //neverNull = null //błąd var nullable: String? = "You can keep a null here" nullable = null var inferredNonNull = "The compiler assumes non-nullable" //inferredNonNull = null //domyślnie typy nie są nullowalne //nie akceptujemy wartości null; funkcje mogą być wewnątrz funkcji fun strLength(notNull: String): Int { return notNull.length } println(strLength(neverNull)) // 18 //println(strLength(nullable)) //String i String? to niezgodne //typy Null safety fun describeString(maybeString: String?): String { if (maybeString != null && maybeString.length > 0) { return "String of length ${maybeString.length}" } else { return "Empty or null string" } } var nullString: String? = null println(describeString(nullString)) //Empty or null string fun lengthString(maybeString: String?): Int? = maybeString?.length println(lengthString(nullString)) //null //bezpieczne wywołanie ?. zostanie wykonane tylko gdy referencja != null //w przeciwnym razie zwracany jest null; można je łączyć w ciągi a.b?.c?.d println(nullString?.uppercase()) //null nullable="Isn't null" println(nullable?.uppercase()) //ISN'T NULL //operator Elvisa println(nullString?.length ?: 0) // 0 println(nullable?.length ?: 0) // 10 } Operatory porównania W Kotlinie istnieją dwa rodzaje równości Równość strukturalna == (i zanegowany odpowiednik !=) Odpowiada wykonaniu equals() Tłumaczona jest na: a?.equals(b) ?: (b === null) Porównanie do null – a == null jest równoważne a === null Równość referencyjna === (i zanegowany odpowiednik !==) Polega na porównaniu referencji W przypadku wartości reprezentowanych przez typy proste w trakcie wykonania np. Int porównanie a === b jest równoważne a == b Kolekcje Biblioteka Standardowa Kotlina (Kotlin Standard Library) zapewnia implementacje podstawowych typów kolekcji: zbiorów, list i map. Dla każdego typu kolekcji jest para interfejsów definiujących operacje: Interfejs tylko do odczytu (read-only) - udostępnia operacje umożliwiające dostęp do elementów kolekcji Interfejs zmienny (mutable) - rozszerza odpowiedni interfejs tylko do odczytu o operacje zapisu pozwalające na: dodawanie, usuwanie i aktualizowanie elementów Kolekcja zmienna nie musi być przypisana do var Kolekcje tylko do odczytu są kowariantne tzn. jeśli np. klasa Rectangle dziedziczy po Shape to można użyć klasy List wszędzie tam, gdzie wymagana jest klasa List (typy kolekcji mają tę samą relację podtypów, co typy elementów). Mapy są kowariantne pod względem typu wartości, ale nie typu klucza Kolekcje zmienne nie są kowariantne Kolekcje – interfejsy kolekcji https://kotlinlang.org/docs/collections-overview.html#collection-types Listy – tworzenie, przeglądanie fun printAll(strings: Collection) { for(s in strings) print("$s ") println() } fun main(args: Array) { //tworzenie i przeglądanie list val numbers = listOf("one", "two", "three", "four") //funkcja do //tworzenia niezmiennych list printAll(numbers) //one two three four println("${numbers.size} ${numbers.get(2)} ${numbers} ${numbers.indexOf("two")}") //4 three four 1 val numbersIterator = numbers.iterator() //jest też listIterator() //pozwalający na przeglądanie w dwóch kierunkach while (numbersIterator.hasNext()) { print("${numbersIterator.next()} ") } //one two three four println() Listy – tworzenie, kopiowanie val doubled = List(3, { it * 2 }) //można też użyć funkcji //inicjalizującej List; aby lista była modyfikowalna trzeba użyć //funkcji MutableList println(doubled) //[0, 2, 4] //funkcje kopiujące kolekcje z biblioteki standardowej tworzą //płytkie kopie val listCopy=doubled.toMutableList() //i inne toList(), toSet() itd //- tworzą kopie, które można modyfikować niezależnie od oryginałów listCopy=3 println(listCopy) //[0, 3, 4] println(doubled) //[0, 2, 4] val linkedList = LinkedList(listOf("one", "two", "three")) //można używać konstruktorów konkretnych typów Listy – porównywanie, modyfikacja //porównywanie list val bob = Pair("Bob", 31) //Pair - para 2 elementów; tutaj imię i //wiek val people = listOf(Pair("Adam", 20), bob, bob) val people2 = listOf(Pair("Adam", 20), Pair("Bob", 31), bob) println(people == people2) //true - listy są równe jeżeli elementy //są strukturalnie równe //zmienna lista val numbersMutable = mutableListOf(1, 2, 3, 4) //funkcja do //tworzenia list zmiennych numbersMutable.add(5) numbersMutable.removeAt(1) numbersMutable = 0 numbersMutable.shuffle() println(numbersMutable) //[3, 0, 4, 5] Zbiory – tworzenie, użycie //zbiory i kolejność elementów val setOfnumbers = setOf(1, 2, 3, 4) // LinkedHashSet jest domyślną //implementacją (zachowuje kolejność) println("Number of elements: ${setOfnumbers.size}") //Number of elements: 4 if (setOfnumbers.contains(1)) println("1 is in the set") //1 is in the set val setOfNumbersBackwards = setOf(4, 3, 2, 1) println("The sets are equal: ${setOfnumbers == setOfNumbersBackwards}") //The sets are equal: true //kolejność się nie liczy) println(setOfnumbers.first() == setOfNumbersBackwards.first()) //false - tu //faktyczna kolejność ma znaczenie println(setOfnumbers.first() == setOfNumbersBackwards.last()) //true //tworzenie pustych kolekcji na przykładzie zbioru val emptySet = mutableSetOf() //trzeba określić typ Mapy – tworzenie, użycie //mapy val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1) //funkcja infiksowa "to" tworzy tymczasowe obiekty //Pair val numbersMap2 = mutableMapOf().apply { this["one"] = "1"; this["two"] = "2" } //bardziej // efektywne - bez tworzenia tymczasowych obiektów val map = buildMap { //funkcje budujące (są też buildSet() i put("a", 1) //buildList()) put("b", 0) put("c", 4) } println("${numbersMap.keys} ${numbersMap.values}") //[key1, key2, key3, key4] [1, 2, 3, 1] if ("key2" in numbersMap) println("Value by key \"key2\": ${numbersMap["key2"]}") //Value by key "key2": 2 Mapy – tworzenie, użycie if (1 in numbersMap.values) println("The value 1 is in the map") //The value 1 is in the map if (numbersMap.containsValue(1)) println("The value 1 is in the map") //The value 1 is in the map val anotherMap = mapOf("key2" to 2, "key1" to 1, "key4" to 1, "key3" to 3) println("The maps are equal: ${numbersMap == anotherMap}") //The maps are equal: true val mutableNumbersMap = mutableMapOf("one" to 1, "two" to 2) mutableNumbersMap.put("three", 3) mutableNumbersMap["one"] = 11 println(mutableNumbersMap) //{one=11, two=2, three=3} } Operacje na kolekcjach Typowe operacje dzielą się na następujące grupy: transformacje, filtrowanie, operatory plus i minus, grupowanie, pobieranie części kolekcji, pobieranie pojedynczych elementów, porządkowanie, operacje agregujące Mapowanie - tworzy kolekcję na podstawie wartości funkcji wykonanej na elementach innej kolekcji Zip - polega na budowaniu list par z elementów o tych samych pozycjach w obu kolekcjach Asocjacja - budowanie map z elementów kolekcji (kluczy) i określonych wartości z nimi związanych pochodzących z innej kolekcji (wartości) Spłaszczanie (flatten) – uruchomiona na kolekcji złożonej z kolekcji (np. liście list) zwraca pojedynczą kolekcję ze wszystkimi elementami Operacje na kolekcjach Sprawdzanie spełnienia warunków (any(), none(), all()) Podział (partition) – podział na elementy spełniające i niespełniające warunku Grupowanie (groupBy) – tworzy mapę, w której wartościami są listy elementów posiadające wspólną cechę a kluczami wartości tej cechy (np. klucz – pierwsza litera słowa, wartość – lista słów zaczynających się od tej litery) Wycinanie - (slice) wycinanie fragmentu, (take…) pozostawianie fragmentu, (drop…) odrzucanie fragmentu kolekcji Agregacja (minOrNull, maxOrNull, average, sum, count,...) Operacje na kolekcjach fun main(args: Array) { val numbers = mutableListOf("one", "two", "three", "four") val longerThan3 = numbers.filter { it.length > 3 } //tworzenie //nowej kolekcji z elementami spełniającymi warunek` println(longerThan3) //[three, four] val filterResults = mutableListOf() //kolekcja na wynik numbers.filterTo(filterResults) { it.length > 3 } //filtrowanie do //istniejącej kolekcji numbers.filterIndexedTo(filterResults) { index, _ -> index == 0 } //filtrowanie do istniejącej kolekcji z uwzględnieniem indeksu println(filterResults) //[three, four, one] //funkcje "To" też zwracają wynik val result = numbers.mapTo(HashSet()) { it.length } println("distinct item lengths are $result") } Scope functions - zestawienie Odniesienie do Funkcja Zwracana wartość Czy jest funkcją rozszerzającą? obiektu let it Wynik lambdy tak run this Wynik lambdy tak run - nie (nie ma obiektu Wynik lambdy kontekstowego) with this nie (obiekt kontekstowy jest Wynik lambdy argumentem) apply this Obiekt kontekstowy tak also it Obiekt kontekstowy tak Scope functions – kiedy stosować? let - wykonywanie lambdy na obiektach, które nie dopuszczają wartości null (non-nullable) let - wprowadzenie wyrażenia jako zmiennej o zasięgu lokalnym (wewnątrz lambdy) apply - konfiguracja obiektu run - konfiguracja obiektu i obliczenie wyniku nierozszerzające run - uruchamianie instrukcji, w których wymagane jest wyrażenie also - zrealizowanie dodatkowych efektów with - funkcja grupująca wywołania na obiekcie Funkcja let data class MutablePair(var first: Int, var second: Int) fun main(args: Array) { //let - obiekt kontekstowy dostępny jako it; zwraca wynik lambdy //używany do wywoływania jednej lub więcej funkcji na wynikach //ciągów wywołań val numbers = mutableListOf("one", "two", "three", "four", "five") numbers.map { it.length }.filter { it > 3 }.let { //możemy wykonać wiele operacji na wyniku filtrowania bez //przypisywania go do zmiennej println(it) //kolejne operacje } //[5, 4, 4] numbers.map { it.length }.filter { it > 3 }.let(::println) //jeżeli tylko jedna operacja to można użyć //referencji do funkcji Funkcja let //let jest często używane do wykonywania operacji na obiektach !=null fun processNonNullString(str: String) = println(str) val str: String? = "Hello" val length = str?.let { //tutaj it jest !=null processNonNullString(it) //Hello it.length } println(length) //5 //użycie let do wprowadzenia zmiennej w ograniczonym zakresie aby //poprawić czytelność kodu (definiujemy nazwę parametru lambdy zamiast //używać domyślnego it) val modifiedFirstItem = numbers.first().let { firstItem -> println("The first item of the list is '$firstItem'") //The first item of the list is 'one' if (firstItem.length >= 5) firstItem else "!" + firstItem + "!" }.uppercase() println("First item after modifications: '$modifiedFirstItem'") //First item after modifications: '!ONE!' Funkcja with //with - obiekt kontekstowy dostępny jako this; zwraca wynik lambdy //nie jest funkcją rozszerzającą - obiekt kontekstowy przekazywany //jest jako parametr używany do "zrobienia czegoś z obiektem" with(numbers) { println("'with' is called with argument $this") //'with' is called with argument [one, two, three, four, five] println("It contains $size elements") //It contains 5 elements } val firstAndLast = with(numbers) { "The first element is ${first()}," + " the last element is ${last()}" } println(firstAndLast) //The first element is one, the last element is five Funkcje run //run - obiekt kontekstowy dostępny jako this; zwraca wynik lambdy //podobny do with ale jest funkcją rozszerzającą val firstAndLast2 = numbers.run { "The first element is ${first()}," + " the last element is ${last()}" } //bardzo podobnie można użyć let (różnica it.first() i it.last()) //run jest również dostępny jak zwykła funkcja (nierozszerzająca) //pozwala wykonać blok instrukcji tam gdzie jest potrzebne jest wyrażenie val hexNumberRegex = run { val digits = "0-9" val hexDigits = "A-Fa-f" val sign = "+-" Regex("[$sign]?[$digits$hexDigits]+") } for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) { print("${match.value} ") } //+123 -FFFF 88 println() Funkcja apply //apply - obiekt kontekstowy dostępny jako this; zwraca obiekt, dla //którego było wywołane; zalecana do bloków kodu wykonujących //operacje na składowych obiektu; najbardziej typowo do //konfiguracji obiektu //data class MutablePair(var first: Int, var second: Int) val pair = MutablePair(1, 2).apply { first = 3 second = 4 } println(pair) //MutablePair(first=3, second=4) Funkcje also, takeIf, takeUnless //also - obiekt kontekstowy it; zwracana obiekt, dla którego wywołane //używane do działań wymagających referencji do obiektu (a nie jego //właściwości i metod) albo wtedy gdy potrzebna jest referencja this z //zewnętrznego zakresu (nie chcemy przesłonić) numbers.also { println("The list elements before adding new one: $it") }.add("six") println(numbers) //[one, two, three, four, five, six] //dodatkowe funkcje takeIf i takeUnless //przydatne do wstawiania warunków w łańcuchy wywołań val number = (0..99).random() println(number) //39 val evenOrNull = number.takeIf { it % 2 == 0 }.also(::println) //null val oddOrNull = number.takeUnless { it % 2 == 0 }.also(::println) //39 } Destrukturyzacja data class Result(val result: Int, val status: String) fun getResult(): Result { return Result(0,"OK") } fun main(args: Array) { //deklaracja destrukturyzująca (zamiast używania metod component1(), //component2() z klasy danych) val (result, status) = getResult() println("$result $status") //0 OK val (_, status2) = getResult() //"_" jeżeli jakiś val resultList = listOf(Result(0,"OK"), Result(1,"NOT OK")) for ((result,status) in resultList) { print("$result $status ") //0 OK 1 NOT OK } println() Destrukturyzacja //destrukturyzacja i mapy val map = mapOf(0 to "OK", 1 to "NOT OK") for ((key,value) in map) { print("$key $value ") //0 OK 1 NOT OK } println() //destrukturyzacja i lambdy //zamiast map.mapValues { // entry -> "${entry.value}" }.also(::println) map.mapValues { (key, value) -> "$value" }.also(::println) //{0=OK, 1=NOT OK} //{ a,b ->... } - lambda z 2 parametrami //{ (a,b) ->... } - lambda z 1 parametrem i destrukturyzacją //{ (a,b),c ->... } - lambda z 2 parametrami i destrukturyzacją } Domain Specific Language (DSL) Używając nazwanych funkcji jako budowniczych (builders) w połączeniu z literałami funkcyjnymi z odbiorcą (receiver), możliwe jest tworzenie w Kotlinie bezpiecznych ze względu na typ (type- safe), statycznie typowanych budowniczych Budowniczy umożliwiają tworzenie języków specyficznych dla domeny (DSL) opartych na Kotlinie, odpowiednich do budowania złożonych hierarchicznych struktur danych w sposób semi- deklaratywny HTML DSL //parametr bezparametrowa metoda klasy HTML; lambda przekazana do funkcji //zawiera wywołania metod klasy HTML (head, body) fun html(init: HTML.() -> Unit): HTML { val html = HTML() html.init() //tutaj wykonają się metody return html } // parametr to nazwa elementu w html class HTML : TagWithText("html") { //tworzymy obiekt Head i wykonujemy metody z klasy Head wywołane w //lambdzie (konfigurujemy obiekt Head()) //initTag to metoda tej klasy odziedziczona po klasie Tag //wywołanie tej funkcji oznacza, że do obiektu HTML dodajemy potomka //HEAD fun head(init: Head.() -> Unit) = initTag(Head(), init) fun body(init: Body.() -> Unit) = initTag(Body(), init) } HTML DSL class Head : TagWithText("head") { fun title(init: Title.() -> Unit) = initTag(Title(), init) } class Title : TagWithText("title") interface Element { fun render(builder: StringBuilder, indent: String) } class TextElement(val text: String) : Element { override fun render(builder: StringBuilder, indent: String) { builder.append("$indent$text\n") } } class Body : BodyTag("body") class B : BodyTag("b") class P : BodyTag("p") class H1 : BodyTag("h1") HTML DSL class A : BodyTag("a") { var href: String get() = attributes["href"]!! //mapa attributes zdefiniowana w klasie Tag set(value) { attributes["href"] = value } } //klasa BodyTag zawiera kilka funkcji do dodawania typowych elementów (potomków) abstract class BodyTag(name: String) : TagWithText(name) { fun b(init: B.() -> Unit) = initTag(B(), init) fun p(init: P.() -> Unit) = initTag(P(), init) fun h1(init: H1.() -> Unit) = initTag(H1(), init) fun a(href: String, init: A.() -> Unit) { val a = initTag(A(), init) a.href = href } } HTML DSL abstract class TagWithText(name: String) : Tag(name) { //pozwala dodać zwykłego tekstowego potomka wewnątrz elementu HTML operator fun String.unaryPlus() { children.add(TextElement(this)) //lista children zdefiniowana w klasie //Tag } } //tworzymy adnotację do oznaczania elementów DSL (chodzi o to który obiekt //(receiver) będzie otrzymywał wywołania metod przy zagnieżdżonych lambdach) @DslMarker annotation class HtmlTagMarker //klasa zawierająca wspólne elementy @HtmlTagMarker abstract class Tag(val name: String) : Element { val children = arrayListOf() val attributes = hashMapOf() HTML DSL //wywołanie tej metody powoduje dodanie do bieżącego elementu np. HTML //potomka przekazanego jako parametr np. BODY //np. dostajemy obiekt np. tag=Body() i lambdę z wywołaniami metod Body protected fun initTag(tag: T, init: T.() -> Unit): T { tag.init() //wykonujemy lambdę z np. wywołaniami metod Body (obiekt Body //jest skonfigurowany) children.add(tag) //dodajemy go jako potomka bieżącego elementu return tag } //metoda jest odpowiedzialna za dodanie tego elementu i wszystkich jego //potomków w postaci tekstowej do StringBuildera; z zadanym wcięciem override fun render(builder: StringBuilder, indent: String) { builder.append("$indent\n") for (c in children) { c.render(builder, indent + " ") } builder.append("$indent\n") } HTML DSL //przekształcenie atrybutów do postaci tekstowej private fun renderAttributes(): String { val builder = StringBuilder() for ((attr, value) in attributes) { builder.append(" $attr=\"$value\"") } return builder.toString() } override fun toString(): String { val builder = StringBuilder() render(builder, "") return builder.toString() } } HTML DSL fun main(args: Array) { Title val page=html { head { title {+"Title"} Header 1 } Paragraph body { bold h1 {+"Header 1"} p { link +"Paragraph " b {+"bold "} a(href = "https://kotlinlang.org") {+"link"} } } } println(page) }

Use Quizgecko on...
Browser
Browser