Kotlin Core Syntax: Operator overload and other conventions

Blog Home Page

Java has some language features associated with specific classes in the standard library, such as objects that implement the java.lang.Iterable interface can be used in for loops, and objects that implement the java.lang.AutoCloseable interface can be used in try-with-resources statements.

However, in kotlin, some functionality is related to a specific function name, not to a specific type.Kotlin uses convention principles, unlike java dependency types.Kotlin can add new methods to existing classes through the extension function mechanism, and any convention method can be defined as an extension function.

1. Overloaded Arithmetic Operators

In java, arithmetic operators can only be used with basic data types, and the + operator can be used with String values.If you add elements to a set, you want to be able to use the +=operator.In kotlin, this is possible.

1. Overloaded binary arithmetic operations

Let's start with an example: Define a Point class (representing a point) that adds (X, Y) coordinates of points together.

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

    // Define a method called "plus"
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

val p1 = Point(1, 2)
val p2 = Point(3, 4)
println(p1 + p2) // Call the "plus" method by using the + sign
//Output >> Point (x=4, y=6)

The operator keyword declares the plus function.All overloaded operator functions require this keyword token to indicate that the function is implemented as a convention.

Once the plus function is declared with the operator modifier, the + sign can be used to sum it directly.This is actually a call to the plus function.

In addition to declaring an operator as a member function, it can also be defined as an extension function

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

kotlin defines which operators can be overloaded and the corresponding name function defined in the class.The following table is an overloadable binary operator:

Expression Function name
a * b times
a / b div
a % b mod
a + b plus
a - b minus

When defining an operator, the two operands can be of different types

operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}

val p1 = Point(10, 20)
println(p1 * 1.5) // No custom support for interchangeability, not 1.5 * p1
//Output >> Point (x=15, y=30)

The kotlin operator does not support interchangeability by itself and cannot be 1.5 * p1.If you want to, you need to define a separate operator

operator fun Double.times(p: Point): Point {...}

The return type of an operator function can also be any operand type.
This operator takes a Char as the left value, an Int as the right value, and returns a String type.

operator fun Char.times(count: Int) : String {
    return toString().repeat(count)
}

println('b' * 3)
//Output Results >> BBB

2. Overloaded Composite Assignment Operators

These operators, such as +=, -=, are called compound assignment operators.

var p = Point(1, 2)
p += Point(3, 4) // Equivalent to p = p + Point(3, 4) notation
println(p)

//Output >> Point (x=4, y=6)

The +=operator modifies the object referenced by the variable, but does not reassign the reference, such as adding an element to a variable set

val numbers = ArrayList<Int>()
numbers += 12
println(numbers[0])

//Output Results >> 12

If you define a return value of Unit, called the plusAssign function, kotlin calls it where the +=operator is used.Binary operator corresponding functions, such as minusAssign, timesAssign

The kotlin standard library defines the plusAssign function for a variable set:

operator fun <T> MutableCollection<T>.plusAssign(element: T) {
    this.add(element)
}

When += is used in code, plus and plusAssign are theoretically both likely to be called, so try not to add plus and plusAssign operations to a class at the same time.

For example, if the Point class in the example is immutable, only the plus operation returning a new value should be provided. If a class is mutable, only the plusAssign and similar operations need to be provided.

The kotlin standard library supports two methods of collections, the + and - operators always return a new set, and the += and - = operators always modify them in one place when used for variable sets.When used with read-only collections, a modified copy is returned, meaning that+=and-= can only be used if the variable referencing the read-only collection is declared var.

val list = arrayListOf(1, 2)
list += 3 // +=Modify list
val newList = list + listOf(4, 5)  // Returns a new list containing all elements

println(list)
//Output >> [1, 2, 3]

println(newList)
//Output >> [1, 2, 3, 4, 5]

3. Overloading unary operators

Predefine a name to declare a function (a member function or an extension function) and mark it with the modifier operator.

// Unary operator has no parameters
operator fun Point.unaryMinus(): Point {
    return Point(-x, -y) // Reverse coordinates and return
}

Operators for overloadable unary algorithms:

Expression Function name
+a unaryPlus
-a unaryMinus
!a not
++a,a++ inc
--a, a-- dec

Self-Incrementing Operator Case:

operator fun BigDecimal.inc() = this + BigDecimal.ONE

var bd = BigDecimal.ZERO
println(bd++) //Suffix operation: increment after execution (returns the current value of the bd variable before executing ++)
//Output Results >> 0

println(++bd) //Prefix operation: Add before execution (as opposed to suffix operation)
//Output Results >> 2

2. Overloaded comparison operators

In kotlin, you can use comparison operators (==,!=, >, <etc.) for any object, not only for basic data types, but also directly for comparison operators.Unlike java, you need to call the equals or compareTo functions.

1. Equal sign operator:'equals'

If the == operator is used in kotlin, it will be converted to a call to the equals method.

==and!=can be used with nullable operators because they actually check to see if the operator is null.Comparing a == b checks if a is not empty. If not, call a.equals(b), otherwise, only if both parameters are empty references will the result be true

In the case of a Point class, marked as a data class, the implementation of equals is automatically generated by the compiler.If you need to do this manually, do the following:

class Point(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean {
        // Optimize: Check if the parameter is the same object as this
        if (this === other) return true
        // Check parameter types
        if (other !is Point) return false
        // other intelligently converts to Point to access x,y properties
        return other.x == x && other.y == y
    }
}

println(Point(1, 2) == Point(1, 2)) //Output Results >> true
println(Point(2, 3) != Point(3, 4)) //Output Results >> true
println(null == Point(1, 2)) //Output Results >> false

The identity operator (===) checks whether the two parameters are references to the same object (and the same value if they are basic data types).After implementing the equals function, this (==) operator is often used to optimize the calling code, but the === operator cannot be overloaded.

The equals method is defined in the Any class, so the equals method does not need to be marked as an operator because the basic method in the Any class is already marked.However, equals cannot be implemented as an extension method because implementations that inherit from the Any class always take precedence over extension functions.

public open class Any { 
    // ...
    public open operator fun equals(other: Any?): Boolean
}

!= The operator will also be converted to an equals method call, and the compiler will automatically reverse the return value.

2. Sort operator: compareTo

In java, classes implement the Comparable interface, and the compareTo method defined in the interface is used to determine whether one object is larger than another.In java, however, only the basic data types can be compared using <and>and all other types require element1.compareTo(element2).

In kotlin, you can use comparison operators (<, >, <=, >=), which are converted to compareTo, and the return type of compareTo must be Int.

Define the Person class to implement the compareTo method: first compare firstName, if the same, then compare lastName

class Person(
    val firstName: String, val lastName: String
) : Comparable<Person> {
    override fun compareTo(other: Person): Int {
        return compareValuesBy( // Call the given methods sequentially and compare their values
            this, other,
            Person::firstName, Person::lastName
        )
    }
}

val p1 = Person("a", "b");
val p2 = Person("a", "c");
println(p1 < p2)
//Output Results >> true

The compareValuesBy function in the kotlin standard library can be used to implement the compareTo method concisely.All classes that implement the Comparable interface in java can use concise operator syntax in kotlin without adding extension functions.For example:

println("abc" < "cba")
//Output Results >> true

3. Contracts between sets and intervals

Collections usually operate through subscripts.All of these operations in kotlin support operator syntax: to get or set an element with a subscript, you can use the syntax a[b] (called the subscript operator); you can use the in operator to check whether an element is within a set interval; or you can iterate over a set.

1. Access elements by subscripts:'get'and'set'

In kotlin, you can access elements in a map in square brackets:

val value = map[key]

You can also use the same operator to change an element of a variable map

mutable[key] = newValue

How does it work?
Subscript operators are a convention in kotlin.Reading elements using subscript operators is converted to calls to get operator methods, and writing elements calls set.These methods are already defined by the interfaces between Map and Mutable Map.

How do I add a similar method to a custom class?

Implement the get convention: Or take the custom Point class as an example, use square brackets to reference the coordinates of points, p[0] to access X coordinates, p[1] to access Y coordinates

operator fun Point.get(index: Int): Int {
    return when(index) {
        0 -> x
        1 -> y
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

val  p = Point(10, 20)
println(p[1])
//Output Results >> 20

Just define a get function and mark the operator, p[1] will be converted to a call to the get method.

Note: get parameters can be of any type, not just Int.You can also define get methods with multiple parameters.If you need to access the collection using different key types, you can also use different parameter types to define multiple overloaded get methods.

Implement the set convention: In the above example, the Point class is immutable (the variable is a val modification), so it makes no sense to implement the set convention.
Next, define a variable point MutablePoint

data class MutablePoint(var x: Int, var y: Int)

operator fun MutablePoint.set(index: Int, value: Int) {
    when(index) {
        0 -> x = value
        1 -> y = value
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

val p = MutablePoint(10, 23)
p[1] = 24
println(p)
//Output >> MutablePoint (x=10, y=24)

Just define a set function and mark the operator, p[1]=24 will be converted to a call to the set method.

2.'in'Convention

Another operator supported by a set is the in operator: to check whether an object belongs to a set, and for the function contains.

Implement in's convention: whether a checkpoint belongs to a rectangle

data class Rectangle(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
    // Using the until function to construct an interval
    return p.x in upperLeft.x until lowerRight.x &&
            p.y in upperLeft.y until lowerRight.y
}

val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect)
//Output Results >> true

The object to the right of in will call the contains function, and the object to the left of in will participate as a function.

3. RanTo's conventions

Create an interval using the..Syntax.For example: 1..10 represents numbers from 1 to 10.The..Operator is a simple way to call the rangeTo function.

The rangeTo function returns an interval.This operator can be defined for a custom class, but it is not needed if the class implements the Comparable interface.An interval of any comparable element can be created from the kotlin standard library:

operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>

For example:

val now = LocalDate.now();
val vacation = now..now.plusDays(10) // Create a 10-day interval starting today
println(now.plusWeeks(1) in vacation) // Detect whether a particular date belongs to this interval
//Output Results >> true

now..now.plusDays(10) is converted by the compiler to now.rangeTo(now.plusDays(10)).RanTo is not a member function of LocalDate, but an extension function of Comparable.

The rangeTo operator has lower precedence than the arithmetic operator and it is best to expand the parameters to avoid confusion:

val n = 9
println(1..(n + 1)) // It can be written as 1..n + 1, but it's a little clearer to enclose
//Output Results >> 1..10

The expression 1..N.forEach {print(it)} will not be compiled and must be enclosed to call the forEach method

val n = 9
(1..n).forEach { print(it) }
//Output Results >> 123456789

4. The convention of using "iterator" in a "for" loop

In kotlin, the in operator can also be used in for loops, just like interval checking.However, in this case it has a different meaning: it is used to perform iterations.For example: for(x in list) {...} will be converted to a call to list.iterator().

In kotlin, the iterator method can be defined as an extension function, so you can traverse a regular java string, and the standard library already defines an extension function iterator for CharSequence

operator fun CharSequence.iterator(): CharIterator

for(c in "abc"){}

You can define iterator methods for custom classes: iterators that implement date intervals

operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
    // This object implements Iterator that traverses the LocalDate element
    object : Iterator<LocalDate> {
        var current = start

        // Date uses compareTo Convention
        override fun hasNext() =
            current <= endInclusive

        // Return the current date as a result before modification
        override fun next() = current.apply {
            // Increase the current date by one day
            current = plusDays(1)
        }
    }

val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear

for (dayOff in daysOff) { println(dayOff) }
//Output Results >> 2016-12-31
//          2017-01-01

4. Deconstruction declarations and component functions

I believe you are already familiar with data classes.

Next, deconstruct the statement, how does it work?

// Data Class
data class Point(val x: Int, val y: Int) 

val p = Point(10, 20)
val (x, y) = p  // Declare the variables x, y, and then initialize with the components of p
println(x)
//Output Results >> 10

println(y)
//Output Results >> 20

A deconstruction declaration is like a normal variable declaration, but it has multiple variables in parentheses.

Deconstruction declarations also use convention principles.To initialize each variable in a deconstruction declaration, a function named component N is called, where N is the location of the variable in the declaration.

For data classes, the compiler generates a componentN function for each property declared in the main construction method.

We can also declare these functions manually for non-data types:

class Point(val x: Int, val y: Int) {
    operator fun component1() = x;
    operator fun component2() = y;
}

So much, what are the scenarios in which the deconstruction statement is used?
One of the main scenarios for deconstruction declarations is when multiple values are returned from a function, and a data class can be defined to hold the value required for the return and use it as the return type of the function.Then, by deconstructing the declaration, you can easily expand it and use its values.

For example, split a file name into file names and extensions

// Declare a data class to hold values
data class NameComponents(
    val name: String,
    val extension: String
)

fun splitFilename(fullName: String): NameComponents {
    val result = fullName.split(".", limit = 2)
    // Returns an instance of a data type
    return NameComponents(result[0], result[1])
}

val (name, ext) = splitFilename("example.kt")
println(name)
//Output Results >> example

println(ext)
//Output Results >> KT

The componentN function is also defined in arrays and collections.Deconstruction declarations can be used to handle collections of known size.
Modify the splitFilename function:

fun splitFilename(fullName: String): NameComponents {
    val (name, ext) = fullName.split(".", limit = 2)
    return NameComponents(name, ext)
}

CompoonentN only allows access to the first five elements of an object using this syntax in standard libraries.

Receives a function that returns multiple values, using the Pair and Triple classes in the standard library.

1. Deconstruction declarations and loops

Deconstruction declarations can be used not only as top-level statements in functions, but also in other places where variables can be declared, such as in loops

fun printEntries(map: Map<String, String>) {
    // Deconstruction declaration in in loop
    for ((key, value) in map) {
        println("$key -> $value")
    }
}

val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
//Output Results >> Oracle -> Java
//          JetBrains -> Kotlin

Where the extension functions component1 and component2 on Map.Entry return their key sum values, respectively

for (entry in map.entries) {
    val key = entry.component1()
    val value = entry.component2()
    // ...
}

5. Logic for reusing attribute access: delegate attributes

1. Basic operation of delegate attribute

Basic syntax for delegate properties:

class Foo {
  var p: Type by Delegate()
}

Property p delegates its accessor logic to another object, a new instance of the Delegate class.Get this object by evaluating the following expression with the keyword by.

The compiler creates a hidden auxiliary property and initializes it with an instance of the delegate object. The initialization Property p is delegated to the instance

class Foo {
   // The compiler automatically generates an auxiliary property
   private val delegate = Detegate()
   // Access to p calls the corresponding delegate's getValue and setValue
   var p: Type
      set(value: Type) = delegate.setValue(...,value)
      get() = delegate.getValue(...)
}

The Detegate class must have setValue and getValue methods, either member functions or extension functions.

class Detegate {
   // getValue contains logic to implement Getters
   operator fun getValue(...) {...}
   // setValue contains logic to implement setter
   operator fun setValue(..., value: Type) {...}
}

class Foo {
  // Keyword by associates attributes with delegate objects
  var p: Type by Delegate()
}

val foo = Foo()
val oldValue = foo.p // Modify properties by calling delegate.getValue(...)
foo.p = newValue // Implement property modifications by calling delegate.setValue(..., newValue)

2. Use delegate attributes: lazy initialization and "by lazy()"

Lazy initialization: When this property is first accessed, a part of the object is created as needed.

For example: a Person class that accesses a mailing list written by a person.Mail is stored in a database and time consuming to access.However, you only want to load messages on the first visit and execute them once

class Person {
    // The _emails property is used to save data, associate delegates
    private var _emails: List<String>? = null

    val emails: List<String>
        get() {
            if (_emails == null) {
                // Load mail on access
                _emails = loadEmails();
            }
            // If already loaded, return directly
            return _emails!!
        }

    private fun loadEmails(): List<String>? {
        // time consuming
        return listOf("1", "2");
    }
}

val p = Person()
println(p.emails)
//Output Results >> [1, 2]

What if there are several attributes?And this implementation is not thread safe.kotlin offers a better solution:
Using delegate attributes simplifies your code by encapsulating supporting attributes that store values and logic that ensures that values are initialized only once.
You can use the standard library function lazy to return delegates

Use delegate attributes for lazy initialization:

class Person {
    val emails by lazy { loadEmails() }
}

The lazy function returns an object that has a method named getValue with the correct signature, so it can be used with the by keyword to create a delegate property.By default, lazy functions are thread-safe.

3. Implement delegate properties

Notifying listeners when an object's properties change in java has a standard mechanism for such notifications: the PropertyChangeSupport s and the ProertyChangeEvent classes.But how does it work without using attribute delegation in kotlin?

The PropertyChangeSupport class maintains a list of listeners and sends them PropertyChangeEvent events.To use it, you usually need to store an instance of this class as a field of the bean class and delegate the processing of property changes to it.

To avoid creating this field in each class, create a tool class and inherit it from the bean class.

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this);

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

Write a Person class, specify a read-only property name and a writable property age, and notify its listener when the age changes

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    var age: Int = age
        set(newValue) {
            // field identifiers allow access to supported fields behind attributes
            val oldValue = field
            field = newValue
            // Notify listeners when properties change
            changeSupport.firePropertyChange("age", oldValue, newValue)
        }
}

val p = Person("kerwin", 30)
p.addPropertyChangeListener(PropertyChangeListener { event ->
    println("Property ${event.propertyName} change from ${event.oldValue} to ${event.newValue}")
})

p.age = 31;
//Output Results >> Property age change from 30 to 31

Next, notify property changes through auxiliary classes

class ObservableProperty(
    val propertyName: String,
    var propertyValue: Int,
    val changeSupport: PropertyChangeSupport
) {
    fun getValue() = propertyValue
    fun setValue(newValue: Int) {
        val oldValue = propertyValue
        propertyValue = newValue
        changeSupport.firePropertyChange(propertyName, oldValue, newValue)
    }
}

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    val _age = ObservableProperty("age", age, changeSupport)
    var age: Int
        get() = _age.getValue()
        set(newValue) = _age.setValue(newValue)
}

So we still need to create an ObservableProperty instance for each property and delegate the setter and getter to it.The delegate function in kotlin does not need to be written this way, but the signature of the ObservableProperty method needs to be changed to match the method required by the kotlin Convention

class ObservableProperty(
    var propertyValue: Int,
    val changeSupport: PropertyChangeSupport
) {
    operator fun getValue(p: Person, prop: KProperty<*>) = propertyValue
    operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
        val oldValue = propertyValue
        propertyValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

Where the ObservableProperty class has changed:

  • Both getValue and setValue functions are marked operator
  • These functions add two parameters: one to receive an instance of the property to set or read the property, and the other to represent the property itself, which is of type KProperty
  • Remove the propertyName property from the main construct

Then use the delegate property to bind to update the notification:

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    var age: Int by ObservableProperty(age, changeSupport)
}

With the by keyword, the kotlin compiler automatically executes code written manually before.The object on the right is called a delegate.Kotlin automatically stores the delegate in a hidden property and calls the delegate's getValue and setValue when accessing or modifying the property.

You don't have to implement observable attribute logic manually.The kotlin standard library already contains classes like ObservableProperty.The standard library is not coupled to the PropertyChangeSupport class.

Use Delegates.observable to notify property modifications:

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    private val observer = { property: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(property.name, oldValue, newValue)
    }
    var age: Int by Delegates.observable(age, observer)
}

The expression to the right of by is not necessarily a newly created instance.It can also be a function call, another property, or any other expression.As long as the value of this expression is an object that the compiler can call getValue and setValue with the correct parameter type.

4. Rules for changing delegate attributes

Next, let's summarize how delegate attributes work.
Suppose you have a class with a delegate attribute:

class C {
  val prop: Type by MyDelegate()
}

The MyDelegate instance is saved to a hidden property called <delegate>.The compiler will also represent this property with an object of type KProperty, which is called <property>

class C {
  private val <delegate> = MyDelegate()

  val prop: Type
     get() = <delegate>.getValue(this, <property>)
     set(value: Type) = <delegate>.setValue(this, <property>, value)
}

5. Save attribute values in map

Another common use of delegate attributes is in objects with dynamically defined sets of attributes, which are sometimes referred to as custom objects.

For example, define an attribute to store values in a map

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, arrtValue: String) {
        _attributes[attrName] = arrtValue
    }

    val name: String
        get() = _attributes["name"]!!  // Manually retrieve properties from map
}

Modifying it to a delegate property is easy, so you can place the map directly after the by keyword

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, arrtValue: String) {
        _attributes[attrName] = arrtValue
    }

    //  Use map as delegate property
    val name: String by _attributes
}

Because the standard library has defined getValue and setValue extension functions on the standard Map and Mutable Map interfaces.

If my article is helpful to you, you might as well give me a pat on the back (^^)

Tags: Android Java Attribute Oracle Database

Posted on Mon, 02 Dec 2019 12:29:26 -0800 by loquela