Kotlin from Scratch geavanceerde eigenschappen en klassen

Kotlin is een moderne programmeertaal die compileert met Java bytecode. Het is gratis en open source en belooft om het coderen voor Android nog leuker te maken.  

In het vorige artikel leerde je over klassen en objecten in Kotlin. In deze zelfstudie blijven we meer over eigenschappen leren en bekijken we ook geavanceerde typen klassen in Kotlin door het volgende te verkennen:

  • laat-geïnitialiseerde eigenschappen
  • inline eigenschappen 
  • uitbreidingseigenschappen
  • data, enum, geneste en verzegelde klassen

1. Laat-geïnitialiseerde eigenschappen

We kunnen een niet-nul-eigenschap in Kotlin als verklaren late-geïnitialiseerd. Dit betekent dat een niet-nul-eigenschap niet zal worden geïnitialiseerd op het tijdstip van aangifte, met een waarde-daadwerkelijke initialisatie zal niet gebeuren via een constructor, maar in plaats daarvan zal het te laat worden geïnitialiseerd door een methode of afhankelijkheidsinjectie.

Laten we een voorbeeld bekijken om deze unieke eigenschapmodifier te begrijpen. 

class Presenter private var repository: Repository? = null fun initRepository (repo: Repository): Unit this.repository = repo class Repository fun saveAmount (amount: Double)  

In de bovenstaande code hebben we een veranderlijk nullabel opgegeven bewaarplaats eigenschap die van het type is bewaarplaats-in de klas Presentator-en we hebben deze eigenschap vervolgens tijdens de aangifte op nul gezet. We hebben een methode initRepository () in de Presentator klas die deze eigenschap later opnieuw initialiseert met een echte bewaarplaats aanleg. Merk op dat deze eigenschap ook een waarde kan worden toegewezen met behulp van een afhankelijkheidsinjector zoals Dagger.     

Nu kunnen we hier methoden of eigenschappen aanroepen bewaarplaats property, moeten we een nulcontrole uitvoeren of de operator voor veilige oproepen gebruiken. Waarom? Omdat het bewaarplaats eigendom is van nullable type (bewaarplaats?). (Als je een opfriscursus over nullability in Kotlin nodig hebt, bezoek dan Nullability, Loops en voorwaarden).

// Inside Presenter class fun save (aantal: Double) repository? .SaveAmount (amount)

Om te voorkomen dat elke keer dat we de methode van een accommodatie moeten gebruiken nulcontroles moeten uitvoeren, kunnen we die eigenschap markeren met de lateinit modifier - dit betekent dat we die eigenschap (die een instantie van een andere klasse is) hebben gedeclareerd als late-geïnitialiseerd (dit betekent dat de eigenschap later wordt geïnitialiseerd).  

class Presenter private lateinit var repository: Repository // ...

Nu, zolang we wachten totdat het eigendom een ​​waarde heeft gekregen, zijn we veilig om toegang te krijgen tot de methoden van de accommodatie zonder enige nulcontrole uit te voeren. De initialisatie van de eigenschap kan plaatsvinden in een settermethode of door injectie van afhankelijkheid. 

repository.saveAmount (hoeveelheid)

Houd er rekening mee dat als we toegang proberen te krijgen tot de methoden van de property voordat deze is geïnitialiseerd, we een kotlin.UninitializedPropertyAccessException inplaats van een NullPointerException. In dit geval is het uitzonderingsbericht "lateinit property repository is niet geïnitialiseerd". 

Houd ook rekening met de volgende beperkingen bij het vertragen van een eigenschap-initialisatie met lateinit:

  • Het moet muteerbaar zijn (aangegeven met var).
  • Het eigenschapstype kan geen primitief type zijn, bijvoorbeeld, Int, Dubbele, Vlotter, enzovoorts. 
  • De eigenschap kan geen aangepaste getter of setter hebben.

2. Inline eigenschappen

In Advanced Functions heb ik het in lijn modifier voor hogere-orde functies - dit helpt bij het optimaliseren van alle hogere-orde functies die een lambda als een parameter accepteren. 

In Kotlin kunnen we dit ook gebruiken in lijn modifier op eigenschappen. Als u deze modifier gebruikt, wordt de toegang tot de property geoptimaliseerd.

Laten we een praktisch voorbeeld bekijken. 

class Student val nickName: String get () println ("Nick name retrieved") geeft "koloCoder" terug fun main (args: Array) val student = Student () print (student.nickName)

In de bovenstaande code hebben we een normale eigenschap, bijnaam, dat heeft niet de in lijn modifier. Als we het codefragment decompileren, gebruiken we het Toon Kotlin Bytecode functie gebruiken (als u in IntelliJ IDEA of Android Studio bent Hulpmiddelen > Kotlin > Toon Kotlin Bytecode), zullen we de volgende Java-code zien:

openbare laatste klas Student @NotNull publieke finale String getNickName () String var1 = "Nick name retrieved"; System.out.println (Var1); terug "koloCoder";  openbare eindklasse InlineFunctionKt public static final void main (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); Student student = nieuwe student (); String var2 = student.getNickName (); System.out.print (var2); 

In de gegenereerde Java-code hierboven (sommige elementen van de gegenereerde code zijn omwille van de eenvoud verwijderd), kunt u dat binnen de hoofd() methode die de compiler heeft gemaakt Student object, genaamd de getNickName () methode en drukte vervolgens de retourwaarde ervan af.  

Laten we nu de eigenschap als specificeren in lijn in plaats daarvan en vergelijk de gegenereerde bytecode.

// ... inline val nickName: String // ... 

We voegen gewoon de in lijn modifier voor de variabele modifier: var of val. Hier is de bytecode gegenereerd voor deze inline-eigenschap:

// ... public static final void main (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); Student student = nieuwe student (); String var3 = "Bijnaam opgehaald"; System.out.println (var3); String var2 = "koloCoder"; System.out.print (var2);  // ... 

Nogmaals, sommige code is verwijderd, maar het belangrijkste om op te merken is het hoofd() methode. De compiler heeft de eigenschap gekopieerd krijgen() function body en plakte het in de call-site (dit mechanisme is vergelijkbaar met inline-functies). 

Onze code is geoptimaliseerd omdat het niet nodig is om een ​​object te maken en de methode van de eigenschap getter te bellen. Maar zoals besproken in de inline functions-post, zouden we een grotere bytecode hebben dan voorheen - dus wees voorzichtig. 

Merk ook op dat dit mechanisme zal werken voor eigenschappen die geen achtergrondveld hebben (onthoud dat een achtergrondveld slechts een veld is dat wordt gebruikt door eigenschappen wanneer u die veldgegevens wilt wijzigen of gebruiken). 

3. Extensie-eigenschappen 

In Advanced Functions heb ik ook de uitbreidingsfuncties besproken. Deze bieden ons de mogelijkheid om een ​​klasse uit te breiden met nieuwe functionaliteit zonder van die klasse te hoeven erven. Kotlin biedt ook een vergelijkbaar mechanisme voor eigenschappen, genaamd uitbreidingseigenschappen

val String.upperCaseFirstLetter: String get () = this.substring (0, 1) .toUpperCase (). plus (this.substring (1))

In de post Geavanceerde functies hebben we een gedefinieerd uppercaseFirstLetter () uitbreidingsfunctie met receiver-type Draad. Hier hebben we het in plaats daarvan omgezet naar een extensie-eigenschap op het hoogste niveau. Merk op dat u een gettermethode op uw eigendom moet definiëren om dit te laten werken. 

Met deze nieuwe kennis over extensie-eigenschappen weet u dus dat als u ooit wilt dat een klasse een eigenschap zou hebben die niet beschikbaar was, u vrij bent om een ​​extensie-eigenschap van die klasse te maken. 

4. Dataklassen

Laten we beginnen met een typische Java-klasse of POJO (Plain Old Java Object). 

public class BlogPost private final String-titel; privé uiteindelijke URI-URL; private finale Stringbeschrijving; privé slot Datum publicatiedatum; // ... constructor niet omwille van de beknoptheid opgenomen @Override openbare booleaanse gelijken (Object o) if (this == o) return true; if (o == null || getClass ()! = o.getClass ()) return false; BlogPost blogPost = (BlogPost) o; if (title! = null?! title.equals (blogPost.title): blogPost.title! = null) return false; if (url! = null?! url.equals (blogPost.url): blogPost.url! = null) return false; if (description! = null?! description.equals (blogPost.description): blogPost.description! = null) return false; return publishDate! = null? publishDate.equals (blogPost.publishDate): blogPost.publishDate == null;  @Override public int hashCode () int result = title! = Null? title.hashCode (): 0; resultaat = 31 * resultaat + (url! = null? url.hashCode (): 0); resultaat = 31 * resultaat + (description! = null? description.hashCode (): 0); resultaat = 31 * resultaat + (publicationDate! = null? publishDate.hashCode (): 0); terugkeer resultaat;  @Override public String toString () return "BlogPost " + "title = '" + title +' \ "+", url = "+ url +", + description + "\" + ", publicationDate =" + publishDate + '';  // ... setters en getters worden ook omwille van de eenvoud genegeerd

Zoals je kunt zien, moeten we de accessoire-eigenschappen van de klassen expliciet coderen: de getter en setter, evenals hashcodeis gelijk aan, en toString methoden (hoewel IntelliJ IDEA, Android Studio of de AutoValue-bibliotheek ons ​​kan helpen deze te genereren). We zien dit soort boilerplate-code meestal in de datalaag van een typisch Java-project. (Ik heb de field accessors en de constructor omwille van de beknoptheid verwijderd). 

Het leuke is dat het Kotlin-team ons heeft voorzien van de gegevens modifier voor klassen om het schrijven van deze boilerplate te elimineren.

Laten we nu de voorgaande code in plaats daarvan in Kotlin schrijven.

gegevensklasse BlogPost (var title: String, var url: URI, var description: String, var publishDate: Date)

Geweldig! We specificeren alleen de gegevens modifier voor de klasse zoekwoord om een ​​gegevensklasse te maken, net als wat we in onze hebben gedaan Blogpost Kotlin-klasse hierboven. Nu de is gelijk aanhashcodetoStringkopiëren, en er worden voor ons meerdere methoden voor meerdere componenten gecreëerd. Merk op dat een dataklasse andere klassen kan uitbreiden (dit is een nieuw kenmerk van Kotlin 1.1). 

De is gelijk aan Methode

Deze methode vergelijkt twee objecten voor gelijkheid en retourneert true als ze anderszins gelijk of onjuist zijn. Met andere woorden, het vergelijkt of de twee klasseninstanties dezelfde gegevens bevatten. 

student.equals (student3) // met de == in Kotlin student == student3 // hetzelfde als gebruiken van gelijken ()

In Kotlin, met behulp van de gelijkheidsoperator == zal de is gelijk aan methode achter de schermen.

De hashCode Methode 

Deze methode retourneert een geheel getal dat wordt gebruikt voor het snel opslaan en ophalen van gegevens die zijn opgeslagen in een op hash gebaseerde gegevensstructuur voor verzamelingen, bijvoorbeeld in de Hash kaart en HashSet collectie types.  

De toString Methode

Deze methode retourneert een Draad weergave van een object. 

gegevensklasse Persoon (var firstName: String, var lastName: String) val person = Person ("Chike", "Mgbemena") println (persoon) // prints "Person (firstName = Chike, lastName = Mgbemena)"

Door alleen het klasse-exemplaar aan te roepen, krijgen we een stringobject teruggestuurd - Kotlin roept het object toString () onder de motorkap voor ons. Maar als we het niet doen gegevens sleutelwoord in, zie wat onze objectstringrepresentatie zou zijn: 

com.chike.kotlin.classes.Person@2f0e140b

Veel minder informatief!

De kopiëren Methode

Met deze methode kunnen we een nieuw exemplaar van een object maken met dezelfde eigenschapswaarden. Met andere woorden, het maakt een kopie van het object. 

val person1 = Persoon ("Chike", "Mgbemena") println (persoon1) // Persoon (firstName = Chike, lastName = Mgbemena) val person2 = person1.copy () println (persoon2) // Persoon (firstName = Chike, lastName = Mgbemena)

Een cool ding over de kopiëren methode in Kotlin is het vermogen om eigenschappen tijdens het kopiëren te veranderen. 

val person3 = person1.copy (lastName = "Onu") println (person3) // Person3 (firstName = Chike, lastName = Onu)

Als u een Java-coder bent, is deze methode vergelijkbaar met de clone () methode die u al kent. Maar de Kotlin kopiëren methode heeft meer krachtige functies. 

Destructieve verklaring

In de Persoon klasse, we hebben ook twee methoden automatisch gegenereerd voor ons door de compiler vanwege de gegevens sleutelwoord geplaatst in de klas. Deze twee methoden worden voorafgegaan door "component", gevolgd door een achtervoegsel: component1 ()component2 (). Elk van deze methoden vertegenwoordigt de individuele eigenschappen van het type. Merk op dat het achtervoegsel overeenkomt met de volgorde van de eigenschappen die zijn gedeclareerd in de primaire constructor.

Dus in ons voorbeeld bellen component1 () zal de voornaam teruggeven en bellen component2 () zal de achternaam teruggeven.

println (person3.component1 ()) // Chike println (person3.component2 ()) // Onu

Het is moeilijk om de eigenschappen die deze stijl gebruiken te begrijpen en te lezen, dus het expliciet aanroepen van de eigenschap is veel beter. Deze impliciet gemaakte eigenschappen hebben echter een zeer nuttig doel: ze laten ons een destructureringsverklaring uitvoeren, waarin we elk onderdeel kunnen toewijzen aan een lokale variabele.

val (firstName, lastName) = Persoon ("Angelina", "Jolie") println (firstName + "" + lastName) // Angelina Jolie

Wat we hier hebben gedaan, is om de eerste en tweede eigenschappen (Voornaam en achternaam) van de Persoon type naar de variabelen Voornaam en achternaam respectievelijk. Ik besprak ook dit mechanisme dat bekend staat als destructureringsverklaring in het laatste gedeelte van de post Pakketten en basisfuncties. 

5. Geneste klassen

In het bericht Meer plezier met functies heb ik je verteld dat Kotlin ondersteuning biedt voor lokale of geneste functies, een functie die is gedeclareerd in een andere functie. Nou, Kotlin ondersteunt ook geneste klassen - een klasse die in een andere klas is gemaakt. 

class OuterClass class NestedClass fun nestedClassFunc () 

We noemen zelfs de openbare functies van de geneste klasse zoals hieronder te zien - een geneste klasse in Kotlin is gelijk aan a statisch geneste klasse in Java. Merk op dat geneste klassen geen verwijzing naar hun buitenklasse kunnen opslaan. 

val nestedClass = OuterClass.NestedClass () nestedClass.nestedClassFunc ()

We zijn ook vrij om de geneste klasse in te stellen als privé. Dit betekent dat we alleen een instantie van de klasse kunnen maken NestedClass in het kader van de OuterClass

Innerlijke klasse

Innerlijke klassen, aan de andere kant, kunnen verwijzen naar de buitenste klasse waarin het werd verklaard. Om een ​​innerlijke klasse te creëren, plaatsen we de binnenste sleutelwoord vóór de klasse sleutelwoord in een geneste klasse. 

class OuterClass () val oCPropt: String = "Yo" innerlijke klasse InnerClass fun innerClassFunc () val outerClass = this @ OuterClass print (outerClass.oCPropt) // prints "Yo"

Hier verwijzen we naar de OuterClass van de InnerClass door het gebruiken van  Dit @ OuterClass.

6. Enum-klassen

Een type enum declareert een set constanten die wordt vertegenwoordigd door ID's. Deze speciale soort klasse wordt gemaakt door het sleutelwoord enum dat is opgegeven vóór de klasse trefwoord. 

enum klasse Land NIGERIA, GHANA, CANADA

Om een ​​enum-waarde op te halen op basis van de naam (net als in Java), doen we dit:

Country.valueOf ( "NIGERIA")

Of we kunnen de Kotlin gebruiken enumValueOf() hulpmethode om constanten op een generieke manier te benaderen:

enumValueOf( "NIGERIA")

We kunnen ook alle waarden (zoals voor een Java-opsomming) als volgt krijgen:

Country.values ​​()

Eindelijk kunnen we de Kotlin gebruiken enumValues() hulpmethode om alle enum-items op een generieke manier te krijgen:

enumValues()

Dit retourneert een array met de enum-vermeldingen.  

Enum Constructors

Net als een normale klas, de enum type kan een eigen constructor hebben met eigenschappen die aan elke enum-constante zijn gekoppeld. 

enumklasse Land (val callingCode: Int) NIGERIA (234), VS (1), GHANA (233)

In de land enum type primaire constructor, hebben we de onveranderlijke eigenschap gedefinieerd callingCodes voor elke enum-constante. In elk van de constanten hebben we een argument doorgegeven aan de constructor. 

We kunnen dan als volgt toegang krijgen tot het eigenschap constants:

val country = Country.NIGERIA print (country.callingCode) // 234

7. Verzegelde klassen

Een verzegelde klasse in Kotlin is een abstracte klasse (je hebt nooit de intentie om er objecten van te maken) die andere klassen kunnen uitbreiden. Deze subklassen worden gedefinieerd in de verzegelde klasse-instantie - in hetzelfde bestand. Omdat al deze subklassen zijn gedefinieerd in de verzegelde klasse-instantie, kunnen we alle mogelijke subklassen kennen door het bestand eenvoudig te bekijken. 

Laten we een praktisch voorbeeld bekijken. 

// shape.kt sealed class Shape class Circle: Shape () class Triangle: Shape () class Rectangle: Shape ()

Om een ​​klasse als verzegeld aan te geven, voegen we de verzegeld modifier voor de klasse modifier in de klassedeclaratie-header - in ons geval hebben we de Vorm klasse als verzegeld. Een verzegelde klasse is onvolledig zonder zijn subklassen, net als een typische abstracte klasse, dus we moeten de afzonderlijke subklassen binnen hetzelfde bestand declareren (shape.kt in dit geval). Merk op dat u geen subklasse van een verzegelde klasse uit een ander bestand kunt definiëren. 

In onze code hierboven hebben we aangegeven dat de Vorm klasse kan alleen door de klassen worden uitgebreid CirkelDriehoek, en Rechthoek.

Verzegelde klassen in Kotlin hebben de volgende aanvullende regels:

  • We kunnen de modifier toevoegen abstract naar een verzegelde klasse, maar dit is overbodig omdat verzegelde klassen standaard abstract zijn.
  • Verzegelde klassen kunnen de Open of laatste wijziger. 
  • We zijn ook vrij om dataklassen en objecten als subklasse in een verzegelde klasse te declareren (ze moeten nog steeds in hetzelfde bestand worden gedeclareerd). 
  • Verzegelde klassen mogen geen openbare constructeurs hebben - hun constructeurs zijn standaard privé. 

Klassen die subklassen van een verzegelde klasse uitbreiden, kunnen in hetzelfde bestand of in een ander bestand worden geplaatst. De verzegelde klassenubklasse moet worden gemarkeerd met de Open modifier (je leert meer over inheritance in Kotlin in de volgende post). 

// employee.kt sealed class Medewerker open klasse Artiest: Medewerker () // musician.kt class Musicus: Artist ()

Een verzegelde klasse en zijn subklassen zijn echt handig in a wanneer uitdrukking. Bijvoorbeeld:

fun whatIsIt (shape: Shape) = when (shape) is Circle -> println ("A circle") is Triangle -> println ("A triangle") is Rectangle -> println ("A rectangle")

Hier is de compiler slim om ervoor te zorgen dat we alles mogelijk maken wanneer gevallen. Dat betekent dat het niet nodig is om de anders clausule. 

Als we in plaats daarvan het volgende zouden doen:

fun whatIsIt (shape: Shape) = when (shape) is Circle -> println ("A circle") is Triangle -> println ("A triangle")

De code compileert niet, omdat we niet alle mogelijke gevallen hebben opgenomen. We zouden de volgende foutmelding hebben:

Kotlin: 'wanneer' expressie moet uitputtend zijn, voeg noodzakelijk 'is Rectangle' branch of 'else' branch toe.

Dus we kunnen de is rechthoek case of include de anders clausule om het te voltooien wanneer uitdrukking. 

Conclusie

In deze tutorial leer je meer over lessen in Kotlin. We hebben het volgende besproken over klasse-eigenschappen:

  • late initialisatie
  • inline eigenschappen 
  • uitbreidingseigenschappen

Ook leerde je over enkele coole en geavanceerde klassen zoals data, enum, geneste en verzegelde klassen. In de volgende tutorial in de Kotlin From Scratch-serie, maak je kennis met interfaces en overerving in Kotlin. Tot ziens!

Voor meer informatie over de Kotlin-taal, raad ik aan de Kotlin-documentatie te bezoeken. Of bekijk enkele van onze andere Android-apps voor app-ontwikkeling hier op Envato Tuts!