Kotlin grammar manual

Kotlin grammar manual (III)

When using kotlin, because the mastery is not reliable enough, it is still a habit of Java programming, which wastes the language features, convenience and indirectness provided by kotlin. When reading some Android open source libraries, it is difficult to read because many of them are written by kotlin syntax, and it is inconvenient to consult kotlin syntax, so I have to record kotlin syntax, It is convenient to review the past and consolidate their basic knowledge.

class

In kotlin, the keyword "class" is used to declare a class. The class declaration is composed of class name, class header (specifying its type parameters, main constructor, etc.) and class body surrounded by curly braces. Both class header and class body are optional. If a class has no class body, curly braces can be omitted; Classes in kotlin are public and final by default. As follows:

//No class head and class body
class Empty

Constructor

A class in Kotlin can have a primary constructor and one or more secondary constructors.

primary constructor

The primary constructor is part of the class header: it follows the class name (and optional type parameters).

class Person constructor(firstName: String) { }

If the main constructor does not have any annotations or visibility modifiers, you can omit the} constructor keyword

class Person(firstName: String) { }

//If there is no function body, you can cancel {}
class Person(firstName: String)

If the constructor has annotations or visibility modifiers, the constructor keyword is required and these modifiers precede it

class Customer public @Inject constructor(name: String) { /*......*/ }

The main constructor cannot contain any code. The initialization code can be placed in the initializer blocks prefixed with the init keyword. The initialization block contains the code executed when the class is created. The parameters of the main constructor can be used in the initialization block.

If necessary, you can also declare multiple initialization statement blocks in a class. It should be noted that if the constructor parameter is modified with val/var, it is equivalent to declaring a global attribute with the same name inside the class. If it is not modified by val/var, the parameters of the constructor can only be referenced when init function block and global attribute are initialized.

class Person(val firstName: String, val lastName: String, var age: Int) { 
    init { 
    println("initializer blocks , firstName is: $firstName , lastName is: $lastName,age is:$age,") 
    }
}

Secondary constructor

Class can also declare a secondary constructor prefixed with {constructor.

If the class has a primary constructor, each secondary constructor needs to be delegated to the primary constructor, which can be delegated directly or indirectly through other secondary constructors. Delegate to another constructor of the same class with the keyword "this":

class Point(val x: Int, val y: Int) {

    init {
        println("initializer blocks , x value is: $x , y value is: $y")
        
    }
    //Delegate the primary constructor directly through this
    constructor(base: Int) : this(base + 1, base + 1) {
        println("constructor(base: Int)")
    }

    //Indirect delegation through constructor(base: Int) secondary constructor
    constructor(base: Long) : this(base.toInt()) {
        println("constructor(base: Long)")
    }

}

Note that the code in the initialization block actually becomes part of the main constructor. Delegating to the primary constructor will be the first statement of the secondary constructor, so all the code in the initialization block and attribute initializer will be executed before the body of the secondary constructor.

Properties and fields

In kotlin, declaring an attribute in a class is the same as declaring a variable, using the val and var keywords. The val variable has only one getter, and the var variable has both getters and setter s

class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
    var state: String? = null
    var zip: String = "123456"
}

fun copyAddress(address: Address): Address {
    val result = Address() // There is no 'Kotlin' keyword in
    result.name = address.name // The accessor will be called
    result.street = address.street
    // ......
    return result
}

The complete syntax for declaring an attribute is:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

property_initializer, getter and setter are optional. Property type (PropertyType)_ Inferred from initializer or getter can also be omitted.

//There are default getter s and setter s, which can be omitted
var name = "Holmes, Sherlock"

Custom accessor

In addition to the default getters and setters, we can define custom accessors for properties. If we define a custom getter, it will be called every time we access the property; If we define a custom setter, it will be called every time we assign a value to a property.

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // Parse the string and assign it to other properties
    }

If you need to change the visibility or annotation of an accessor, but do not need to change the default implementation, you can define an accessor without defining its implementation:

var setterVisibility: String = "abc"
    private set // This setter is private and has a default implementation

var setterWithAnnotation: Any? = null
    @Inject set // Annotate this setter with Inject

Delay initialization properties and variables

In general, a property declared as a non null type must be initialized in the constructor, however, this is often inconvenient. For example, attributes can be initialized by dependency injection or in the setup method of unit test. To deal with this situation, you can mark the attribute with the lateinit modifier to tell the compiler that the attribute will be initialized at a later time.

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // Direct dereference
    }
}

This modifier can only be used for attributes in the class body (not the var attribute declared in the main constructor, and only if the attribute has no custom getter or setter). Since Kotlin 1.2, it is also used for top-level attributes and local variables. The property or variable must be a non empty type and cannot be a native type.
Accessing a lateinit attribute before initialization will throw a specific exception, which clearly identifies the fact that the attribute is accessed and it is not initialized.

spread function

The extension function is similar to the static tool method implemented in Java, which is used to add a new behavior to a class.

//Declare an extension function lastChar() for the String class to return the last character of the String
//get method is the internal method of String class, and length is the internal member variable of String class, which can be called directly here
fun String.lastChar() = get(length - 1)

//Declare an extension function doubleValue() for the Int class to return twice its value
//The this keyword represents the Int value itself
fun Int.doubleValue() = this * 2

After that, we can call the extension function directly like calling the method declared inside the class itself

fun main() {
    val name = "leavesC"
    println("$name lastChar is: " + name.lastChar())

    val age = 24
    println("$age doubleValue is: " + age.doubleValue())
}

If you need to declare a static extension function, you must define it on the associated object, so that you can call its extension function without Namer instance, just like calling a static function of Java

class Namer {

    companion object {

        val defaultName = "mike"

    }

}

fun Namer.Companion.getName(): String {
    return defaultName
}

fun main() {
    Namer.getName()
}

It should be noted that if the extension function is declared inside the class, the extension function can only be called inside the class and its subclasses. At this time, it is equivalent to declaring a non static function, which cannot be referenced outside. Therefore, extension functions are generally declared as global functions.

For extension functions, if the base class and subclass define an extension function with the same name respectively, which extension function to call is determined by the static type of the variable rather than the runtime type of the variable.

fun main() {
    val view: View = Button()
    
    //Type of runtime
    view.click()//Button clicked
    
    //Static type
    view.longClick() //View longClicked
}

open class View {
    open fun click() = println("View clicked")
}

class Button : View() {
    override fun click() = println("Button clicked")
}

fun View.longClick() = println("View longClicked")

fun Button.longClick() = println("Button longClicked")

If the member function and extension function of a class have the same signature, the member function will be used first

The extension function does not really modify the original class, but its bottom layer is actually implemented in the way of static import. Extension functions can be declared in any file, so a general practice is to put a series of related functions in a new file

It should be noted that the extension function will not automatically take effect in the whole project. If you need to use the extension function, you need to import it

Extended properties

Extension functions can also be used for attributes

//Extension functions can also be used for attributes
//Add a new attribute value customLen for the String class
var String.customLen: Int
    get() = length
    set(value) {
        println("set")
    }

fun main() {
    val name = "leavesC"
    println(name.customLen)//Can use get method
    name.customLen = 10//Will use the set method
    println(name.customLen)
    //7
    //set
    //7
}

Modifier

There are four visibility modifiers in Kotlin: private, protected, internal, and public. If no modifier is explicitly specified, the default visibility is public.

1,public

public modifier is the modifier with the lowest restriction level. It is available for all and is the default modifier

2,protected

The protected modifier can only be used on members in a class or interface. Protected members are only visible in the class and its subclasses.

3,internal

A package member defined as internal is visible to the whole module, but not to other modules. For example, suppose we want to publish an open source library that contains a class. We want this class to be globally visible to the library itself, but it cannot be referenced by external users. At this time, we can choose to declare it internal to achieve this purpose.

4,private

This means that it is only visible inside the class (including all its members)

5. final and open

Classes and methods in kotlin are final by default, that is, they are not inheritable. If you want to allow the creation of subclasses of a class, you need to use the open modifier to identify the class. In addition, you also need to add the open modifier for each property and method you want to override

open class View {
    open fun click() {

    }
	//Cannot be overridden in subclasses
    fun longClick() {

    }
}

class Button : View() {
    override fun click() {
        super.click()
    }
}

If a member of a base class or interface is overridden, the overridden member is also open by default. For example, in the above code, if the Button class is open, its subclass can also override its click() method. If you want to prohibit overwriting again, use {final} to close.

open class Button : View() {
    final override fun click() {
        super.click()
    }
}

Classification of classes

abstract class

Class and some of its members can be declared abstract. Abstract members may not be implemented in this class. It should be noted that we do not need to label an abstract class or function with open because it is the default.

abstract class BaseClass {
    abstract fun fun1()
}

Data class

We often create classes that only hold data. In these classes, some standard functions are often derived from data machinery. In Kotlin, this is called "data class" and marked as "data".
Defining a new data class is very simple:

data class Point(val x: Int, val y: Int)

By default, the data class generates the following methods for all attributes declared in the main constructor:

  • getter, setter (var is required)
  • componentN(). Corresponding to the attribute declaration order of the main constructor
  • copy()
  • toString()
  • hashCode()
  • equals()

In order to ensure the consistency and meaningful behavior of the generated code, the data class must meet the following requirements:

  • The main constructor needs to contain a parameter
  • All parameters of the main constructor need to be marked as val or var
  • Data classes cannot be abstract, open, sealed, or internal

Decompile User into Java classes as follows:

public final class Point {
   private final int x;
   private final int y;

   public final int getX() {
      return this.x;
   }

   public final int getY() {
      return this.y;
   }

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }

   public final int component1() {
      return this.x;
   }

   public final int component2() {
      return this.y;
   }

   @NotNull
   public final Point copy(int x, int y) {
      return new Point(x, y);
   }

   // $FF: synthetic method
   // $FF: bridge method
   @NotNull
   public static Point copy$default(Point var0, int var1, int var2, int var3, Object var4) {
      if ((var3 & 1) != 0) {
         var1 = var0.x;
      }

      if ((var3 & 2) != 0) {
         var2 = var0.y;
      }

      return var0.copy(var1, var2);
   }

   public String toString() {
      return "Point(x=" + this.x + ", y=" + this.y + ")";
   }

   public int hashCode() {
      return this.x * 31 + this.y;
   }

   public boolean equals(Object var1) {
      if (this != var1) {
         if (var1 instanceof Point) {
            Point var2 = (Point)var1;
            if (this.x == var2.x && this.y == var2.y) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

In fact, it is the template code of java POJO.

Many general operations can be simplified through data classes, which can be carried out conveniently: formatting output variable values, mapping objects to variables, comparing the equality between variables, copying variables and so on.

fun main() {
    val point1 = Point(10, 20)
    val point2 = Point(10, 20)
    println("point1 toString() : $point1") //point1 toString() : Point(x=10, y=20)
    println("point2 toString() : $point2") //point2 toString() : Point(x=10, y=20)

    val (x, y) = point1
    println("point1 x is $x,point1 y is $y") //point1 x is 10,point1 y is 20

    //In kotlin, "= =" is equivalent to Java's equals method
    //And "= =" is equivalent to "= =" method of Java
    println("point1 == point2 : ${point1 == point2}") //point1 == point2 : true
    println("point1 === point2 : ${point1 === point2}") //point1 === point2 : false

    val point3 = point1.copy(y = 30)
    println("point3 toString() : $point3") //point3 toString() : Point(x=10, y=30)
}

It should be noted that the toString(), equals(), hashCode(), copy() and other methods of the data class only consider the attributes declared in the main constructor.

Sealing class

Sealed class (sealed class) is used to restrict the possible subclasses created by the class. The direct subclasses of the class decorated with sealed can only be defined in the file where the sealed class is located (the indirect inheritors of sealed classes can be defined in other files). This helps developers master the changing relationship between parent and child classes, avoid potential bug s caused by code changes, and the constructor of sealed classes can only be private. The class modified by the sealed modifier also implicitly indicates that the class is an open class, so there is no need to explicitly add the open modifier.

sealed class View {

    fun click() {

    }

}
//For View class, its subclasses (Button, TextView) can only be defined in the same file
class Button : View() {

}

class TextView : View() {

}

Because the subclasses of the Sealed class are controllable for the compiler, if all subclasses of the Sealed class are processed in the when expression, there is no need to provide the else default branch. Even if the View subclass is added due to business changes in the future, the compiler will detect that the check method lacks branches and report an error after checking, so the check method is type safe

fun check(view: View): Boolean {
    when (view) {
        is Button -> {
            println("is Button")
            return true
        }
        is TextView -> {
            println("is TextView")
            return true
        }
    }
}

Enumeration class

The basic usage of enumeration class is to realize type safe enumeration:

enum class Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

Enumeration can declare some parameters

enum class Day(val index: Int) {
    SUNDAY(0), MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6)
}

Enumerations can also implement interfaces

interface OnChangedListener {

    fun onChanged()

}

enum class Day(val index: Int) : OnChangedListener {
    SUNDAY(0) {
        override fun onChanged() {

        }
    },
    MONDAY(1) {
        override fun onChanged() {
            
        }
    }
}

Enumeration also contains some common functions

fun main() {
    val day = Day.FRIDAY
    //Get value
    val value = day.index  //5
    //Get the corresponding enumeration value through String
    val value1 = Day.valueOf("SUNDAY") //SUNDAY
    //Gets an array containing all enumerated values
    val value2 = Day.values()
    //Get enumeration name
    val value3 = Day.SUNDAY.name //SUNDAY
    //Gets the location of the enumeration declaration
    val value4 = Day.TUESDAY.ordinal //2
}

Nested class

In kotlin, the class redefined in the class is a nested class by default. At this time, the nested class will not contain an implicit reference to the external class.

class Outer {

    private val bar = 1

    class Nested {
        fun foo1() = 2
    }
}

A nested class translated into java language is an internal static class, as shown below:

public final class Outer {
   private final int bar = 1;

   public static final class Nested {
      public final int foo1() {
         return 2;
      }
   }
}

You can see that Nested is actually a static class, so you can't access the non static members of the external class. For example, you can't access the bar variable in the foo1 () method

Inner class

If you need to access the members of the external class, you need to use the inner modifier to mark the nested class, which is called the internal class. Inner classes implicitly hold references to outer classes.

class Outer {

    private val bar = 1

    inner class Nested {
        fun foo1() = 2
        fun foo2() = bar//At this time, you can access the external bar variable
    }
}

Translated into java language, as follows:

public final class Outer {
   private final int bar = 1;

   public final class Nested {
      public final int foo1() {
         return 2;
      }

      public final int foo2() {
         return Outer.this.bar;
      }
   }
}

To sum up, in kotlin, nested classes are equivalent to static class A in java; The internal class is equivalent to class A in java. In kotlin, you need to add inner before the class keyword, which is the internal class.

Anonymous Inner Class

You can use object expressions to create anonymous inner class instances

window.addMouseListener(object : MouseAdapter() {

    override fun mouseClicked(e: MouseEvent) { ...... }

    override fun mouseEntered(e: MouseEvent) { ...... }
})

Inline class

First look at the following code:

fun sendEmail(delay: Long) {
    println(delay)
}

For the input parameters of the # sendEmail # method, we cannot strictly limit the meaning type of the input parameters. Some developers may understand delay as in milliseconds, and some developers may understand it as in minutes.
In order to improve the robustness of the program, we can declare a wrapper class as the parameter type:

fun sendEmail(delay: Time) {
    println(delay.second)
}

class Time(val second: Long)

class Minute(private val count: Int) {

    fun toTime(): Time {
        return Time(count * 60L)
    }

}

fun main() {
    sendEmail(Minute(10).toTime())
}

In this way, the code source limits the parameter types that developers can pass in, and developers can directly express their desired time size through the class name. However, due to the additional heap memory allocation problem, this method increases the performance overhead of the runtime, and the performance consumption of the new wrapper class is much higher than that of the native type.
InlineClass can solve this problem. Using the introverted class, the above code can be rewritten as follows:

fun sendEmail(delay: Time) {
    println(delay.second)
}

inline class Time(val second: Long)

inline class Minute(private val count: Int) {

    fun toTime(): Time {
        return Time(count * 60L)
    }

}

fun main() {
    sendEmail(Minute(10).toTime())
}

The class decorated with inline is called inline class. The inline class must contain a unique attribute, which is initialized in the main constructor. This unique attribute will be used to represent the instance of the inline class at run time, so as to avoid the additional overhead of wrapping the class at run time. After using the inline class, the sendEmail method will be interpreted as a function with the long type as the input parameter type, which does not contain any objects and will not consume performance.

inherit

In kotlin, all classes have a common superclass {any, and all classes of java have a superclass object; Any has three methods: equals(), hashCode(), and toString(). Therefore, all kotlin classes define these methods. As mentioned earlier when introducing keywords, by default, kotlin class is final and cannot be inherited. To make a class inheritable, you need to mark it with the open keyword.

open class Base(p: Int)

class Derived(p: Int) : Base(p)

If a derived class has a primary constructor, its base type must call the primary constructor of the base class directly or indirectly

open class Base(val str: String)

class SubClass(val strValue: String) : Base(strValue)

class SubClass2 : Base {

    constructor(strValue: String) : super(strValue)

    constructor(intValue: Int) : super(intValue.toString())

    constructor(doubValue: Double) : this(doubValue.toString())

}

Coverage method

kotlin needs to explicitly label the overwritable members and the overwritten members:

open class Base() {
    open fun fun1() {

    }

    fun fun2() {
        
    }
}

class SubClass() : Base() {
    override fun fun1() {
        super.fun1()
    }
}

Only the function marked with open can be overloaded by the subclass. The subclass uses override to indicate that the function is to override the same signature function of the parent class. Members marked override are also open and can be overridden by subclasses. If you want to prevent overwriting again, you can use the final keyword tag. If the parent class does not label the function with open, the subclass is not allowed to define the function with the same signature. For a final class (a class that is not marked with open), it is meaningless to use open to mark properties and methods.

Override attribute

Attribute coverage is similar to method coverage; Properties declared in the parent class and then redeclared in the derived class must begin with 'override', and they must have compatible types. Each declared property can be overridden by a property with an initializer or a property with a get method.

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

You can also override a val attribute with a VAR attribute, but not vice versa. Because a "val" attribute essentially declares a "get" method, and overriding it to "var" is just an additional "set" method in the subclass.
Note that you can use the 'override' keyword in the main constructor as part of the property declaration.

interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // There are always four vertices

class Polygon : Shape {
    override var vertexCount: Int = 0  // It can be set to any number later
}

Call superclass implementation

A derived class can call the function of its superclass and the implementation of its attribute accessor through the super keyword

open class BaseClass {
    open fun fun1() {
        println("BaseClass fun1")
    }
}

class SubClass : BaseClass() {

    override fun fun1() {
        super.fun1()
    }

}

For the inner class, it can directly call the function of the outer class

open class BaseClass2 {
    private fun fun1() {
        println("BaseClass fun1")
    }

    inner class InnerClass {
        fun fun2() {
            fun1()
        }
    }

}

However, if you want to access the parent class of the external class in an internal class, you need to implement it through the super keyword qualified by the external class name

open class BaseClass {
    open fun fun1() {
        println("BaseClass fun1")
    }
}

class SubClass : BaseClass() {

    override fun fun1() {
        println("SubClass fun1")
    }

    inner class InnerClass {

        fun fun2() {
            super@SubClass.fun1()
        }

    }

}

fun main() {
    val subClass = SubClass()
    val innerClass = subClass.InnerClass()
    //BaseClass fun1
    innerClass.fun2()
}

If a class inherits multiple implementations of the same member from its direct superclass and implemented interface, it must override the member and provide its own implementation to eliminate ambiguity

To indicate which supertype to inherit from, use super qualified by the supertype name in angle brackets, such as super < baseClass >

open class BaseClass {
    open fun fun1() {
        println("BaseClass fun1")
    }
}

interface BaseInterface {
    //Interface members are open by default
    fun fun1() {
        println("BaseInterface fun1")
    }
}

class SubClass() : BaseClass(), BaseInterface {
    override fun fun1() {
        //Call func1() function of SubClass
        super<BaseClass>.fun1()
        //Call fun1() function of BaseInterface
        super<BaseInterface>.fun1()
    }
}

Interface

The interface in kotlin is similar to that in Java 8. It can include the definition of abstract methods and the implementation of non abstract methods.

class View : Clickable {
    
    override fun click() {
        println("clicked")
    }

}

interface Clickable {
    fun click()
    fun longClick() = println("longClicked")
}

If a class implements several interfaces, and the interface contains methods with the default implementation and the same name, the compiler will require the developer to implement the method explicitly, and can choose the corresponding implementation of different interfaces in this method.

class View : Clickable, Clickable2 {

    override fun click() {
        println("clicked")
    }

    override fun longClick() {
        super<Clickable>.longClick()
        super<Clickable2>.longClick()
    }
}

interface Clickable {
    fun click()
    fun longClick() = println("longClicked")
}

interface Clickable2 {
    fun click()
    fun longClick() = println("longClicked2")
}

Properties in interface

Attributes can be defined in the interface. The properties declared in the interface are either abstract or provide an implementation of the accessor.

interface MyInterface {
    val prop: Int // Abstract

    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
}

SAM interface

An interface with only one abstract method is called a functional interface or SAM (Single Abstract Method) interface. Functional interfaces can have multiple non abstract members, but only one abstract member.

You can declare a functional interface in Kotlin with the {fun} modifier.

fun interface KRunnable {
   fun invoke()
}

SAM conversion

For functional interfaces, SAM conversion can be realized through {lambda expression, so as to make the code more concise and readable.

For example, there is a Kotlin functional interface:

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

If you don't use SAM conversion, you need to write code like this:

// Create an instance of the class
val isEven = object : IntPredicate {
   override fun accept(i: Int): Boolean {
       return i % 2 == 0
   }
}

By using Kotlin's SAM conversion, it can be changed to the following equivalent code:

// Create an instance through a lambda expression
val isEven = IntPredicate { it % 2 == 0 }

Keywords: Java Android kotlin

Added by alsaffar on Tue, 15 Feb 2022 03:36:42 +0200