Moment For Technology

Kotlin tutorial (7) Operator overloading and other conventions

Posted on Sept. 23, 2022, 8:54 p.m. by Tara Ramirez
Category: android Tag: java android The compiler kotlin

Write at the beginning: I plan to write a Kotlin series of tutorials, one is to make my own memory and understanding more profound, two is to share with the students who also want to learn Kotlin. The knowledge points in this series of articles will be written in order from the book "Kotlin In Action". While displaying the knowledge points in the book, I will also add corresponding Java code for comparative learning and better understanding.

Kotlin Tutorial (1) Basic Kotlin Tutorial (2) Functions Kotlin Tutorial (3) Classes, Objects and Interfaces Kotlin Tutorial (4) Nullability Kotlin Tutorial (5) Types Kotlin Tutorial (6) Lambda programming Kotlin Tutorial (7) Operator overloading and Other conventions Higher-order functions Kotlin tutorial (9) Generics


As you know, Java has some language features associated with specific classes in the standard library. For example, exclusivities that implement the java.lang.Iterable interface can be used in a for loop, and objects that implement the java.lang.AutoCloseable interface can be used in a try-with-resources statement. Kotlin also has many features that work in a very similar way, implementing language-specific constructs by calling functions defined in their own code. However, in Kotlin, these functions are associated with specific function names, not tied to specific types.

In this chapter we will use a common UI framework class Point to demonstrate the definition:

data class Ponit(val x: Int, val y: Int)
Copy the code

Overloading arithmetic operators

In Java, the full set of arithmetic operations can only be used with primitive data types, and the + operator can be used with String values. However, these operators are also handy in other situations. For example, using the + sign is more elegant than using the Add method when dealing with numbers using the BigInteger class: When adding elements to a collection, you might also wish you could use the += operator, which is what you can do in Kotlin.

Overloading binary arithmetic operations

Let's support the first operation by adding the two points together:

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}
 val p1 = Point(10, 20)
 val p2 = Point(30, 40)
 println(p1 + p2)
Point(x=40, y=60)
Copy the code

All functions used for overloaded operators need to be marked with the operator keyword, indicating that you are using the function as an implementation of the corresponding convention and do not happen to define a function with the same name. After declaring the plus function with the operator modifier, you can simply use the + sign to sum. The time plus function a + b - a.plus(b) is actually called. In addition to being declared as a member function, we can also define an extension function, which is equally valid:

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

An overloaded binary arithmetic operator in Kotlin

expression The function name
a * b times
a / b div
a % b mod
a + b plus
a - b minus

Operators of custom types have essentially the same precedence as operators of standard numeric types. For example, a + b * c, the multiplication will be performed before the plus sign. The *, /, and % operators have the same precedence over the + and - operators.

Operator functions and Java

Calling the Kotlin operator from Java is easy: because each overloaded operator is defined as a function, they can be called just like normal functions. When Java is called from Kotlin, it can be used in Kotlin as long as the Java code has a matching function name and number of arguments. If Java already has a similar method with a different name, you can modify the function name by extending the function to replace the existing Java method.

When you define an operator, you do not require the two operands to be of the same type. For example, let's define an operator that allows you to scale a point with a number and convert it between coordinate systems:

operator fun Point.times(scale: Double): Point {
    returnPoint ((x * scale). ToInt (), scale (y *) toInt ())}    val p1 = Point (10, 20)    println (p1 * 1.5) Point (x = 15, y = 30)Copy the code

Note that the Kotlin operator does not automatically support commutativity (the left and right sides of the commutative operator). If you want the user to be able to use 1.5 * p in addition to p * 1.5, you need to define a separate operator operator fun Double. Times (p: Point) : Point.

The return type of an operator function can be different from any operand type. For example, an operator can be defined to create a string by repeating a single character multiple times:

operator fun Char.times(count: Int): String {
    return toString().repeat(count)
}
 println('a' * 3)
aaa
Copy the code

This operator takes a Char as an lvalue, an Int as an rvalue, and returns a String.

As with normal functions, you can override the operator function: you can define multiple methods with the same name but with different argument types.

There are no special operators for bitwise operations

Kotlin does not define any bitwise operators for standard numeric types. Therefore, you are not allowed to define them for custom types either. Instead, it uses regular functions that support infix call syntax and can define similar functions for custom types. Here is the complete list of functions Kotlin provides to perform bit operations:

  • SHL -- moved left with sign, equivalent to in Java
  • SHR -- signed right shift, equivalent to Java
  • Ushr -- unsigned right shift, equivalent to in Java
  • And -- bitwise and, equivalent to in Java
  • Or, bitwise or equivalent | in Java
  • Xor -- bitwise xor, equivalent to ^ in Java
  • Inv -- reverse bit, equivalent to ~ in Java

Overload the compound assignment operator

In general, when defining operator functions like plus, Kotlin supports += as well as + sign. Operators like +=, -= and so on are called compound assignment operators. Look at this example:

 var p1 = Point(10, 20)
 p1 += Point(30, 40)
 println(p1)
Point(x=40, y=60)
Copy the code

Point = point + point (30, 40) Of course, this only works for mutable variables. In some cases, defining the += operator can modify the object referenced by the variable that uses it, but does not reassign the reference. Adding an element to a mutable collection is a good example:

 val numbers = ArrayListInt()
 numbers += 42
 println(numbers)
42
Copy the code

If you define a function called plusAssign that returns Unit, Kotlin will call it where the += operator is used. Other binary arithmetic operators have similarly named counterparts: minusAssign, timeAssign, etc. The Kotlin library defines the plusAssign function for mutable collections, so we can use += as in the example:

operator fun T MutableCollectionT plusAssgin(element: T) {
    this.add(element)
}
Copy the code

Both plus and plusAssign can theoretically be called when you use += in your code. If both functions are defined and used in this case, the compiler will report an error! One way to do this is to call the plusAssign function as normal. The other way is to use val instead of var so that the plusAssign operation does not apply. However, it is recommended to define only one operator. Plus usually returns a new object, while plusAssign returns the previous object. Use this principle to select an appropriate operator definition.

The Kotlin library supports both approaches to collections. The + and - operators always return a new collection. The += and -= operators always modify in place when they are used for mutable collections: when they are used for read-only collections, or return a modified copy (this means that you can only use += and -= if the variable referencing a read-only collection is declared as var). As their operands, a single element can be used, or other sets of the same element type can be used:

 val list = arrayListOf(1, 2)  List += 3  val newList = list + listOf(4, 5) // Returns a new collection  println(list) [1, 2, 3]  println(newList) [1, 2, 3, 4, 5]Copy the code

Overloading unary operators

The process for overloading unary operators is the same as you saw earlier: declare (a member function or extension function) with a predefined name and mark it with the modifier operator. Let's look at an example:

operator fun Point.unaryMinus(): Point = Point(-x, -y)

 val p = Point(10, 20)
println(-p)
Point(x=-10, y=-20)
Copy the code

A function used to overload unary operators without any arguments.

An overloaded unary algorithm operator

expression The function name
+a unaryPlus
-a unaryMinus
! a not
++a, a++ inc
--a, a-- dec

When you define the inc and dec functions to override the increment and decrement operators, the compiler automatically supports the same semantics as the prefix and postfix increment operators for ordinary numeric types. Consider this example of the ++ operator used to override a BigDecimal class:

operator fun BigDecimal.inc() = this + BigDecimal.ONE  var bd = BigDecimal.ZERO  println(bd++) 0  println(++bd)  2Copy the code

The suffix ++ first returns the current value of the bd variable and then performs ++, which is the opposite of the prefix operation. Printing multiple values is the same as you would see with a variable of type Int and doesn't require anything special to support it.

Overload the comparison operator

As with arithmetic operators, in Kotlin, you can use comparison operators (==,! =, , , etc.), not just the basic data types. Instead of calling equals or compareTo as Java does, you can use the comparison operator directly.

The equals operator

As we explained in Tutorial 3, the == operator used in Kotlin will be converted to a call to equals. Use! The = operator is also converted to an equals call. The obvious difference is that their results are reversed, unlike all other operators: == and! = can be used for airlift arithmetic because these operators actually check for null operands. Comparing a == b checks if a is non-empty, and if not, calls a.equals(b) otherwise, only two arguments are null references and the result is true.

a == b - a? .equals(b) ? : (b == null)

For the Point class, since it is already marked as a data class, the equals implementation will be automatically generated by the compiler. But if implemented manually, the name code could look like this:

data class Point(val x: Int, val y: Int) { override fun equals(other: Any?) : Boolean {if (other === this) return true
        if(other ! is Point)return false
        return other.x == x  other.y == y
    }
}
 println(Point(10, 20) == Point(10, 20))
true println(Point(10, 20) ! = Point(5, 5))true
  println(null == Point(10, 20))
false
Copy the code

The identity operator (===) is used to check whether the argument is the same as the object calling equals. The identity operator is exactly the same as the == operator in Java: check if two parameters are references to the same object (and if they are primitive data types, check if they have the same value). This operator is often used to optimize the calling code after the equals method is implemented. Note that the === operator cannot be overloaded. The equals function is marked override because, unlike other conventions, the implementation of this method is defined in the Any class, which explains why you don't need to mark it as an operator. The base method in Any is already marked. The operator modifier of a function also applies to all methods that implement or override it. Also note that equals cannot be implemented as an extension method because an implementation inherited from the Any class always takes precedence over the extension function. This example shows! The use of the = operator is also converted to a call to equals, and the compiler automatically inverts the return value, so you don't need to do anything else.

Sort operator: compareTo

In Java, classes can implement the Comparable interface for use in algorithms that compare values, such as when looking for maximum values or sorting. The compareTo method defined in the interface is used to determine whether one object is larger than another. But in Java, there is no concise syntax for this method call, only the basic data types can be compared using and , and all other types need to be explicitly written as element1.conpareTo(element2). Kotlin supports the same Comparable interface. But the compareTo methods defined in the formula can be called by convention, and the use of the comparison operators (,,=, and =) will be converted to compareTo, whose return type must be Int. The expression p1 p2 is equivalent to p1.compareTo(p2) 0. The other comparison operators operate in exactly the same way. We assume that the size of Point is determined by its position on the Y-axis, and the larger y is, the larger Point is:

data class Point(val x: Int, val y: Int) : ComparablePoint {
    override fun compareTo(other: Point): Int {
        return y.compareTo(other.y)
    }
}

 val p1 = Point(10, 20)
 val p2  = Point(30, 40)
 val p3  = Point(30, 10)
 println(p1  p2)
true
 println(p1  p3)
false
Copy the code

We override the compareTo method by implementing the Comparable interface, which can also be compared by Java functions such as the ability to sort collections. Like equals, the operator modifier is already used in the base class interface, so there is no need to repeat it when overriding the interface. All Classes in Java that implement the Comparable interface can use the concise operator syntax in Kotlin without having to add extension functions:

 println("abc"  "bac")
true
Copy the code

Finalization of sets and intervals

Elements are accessed by subscripts: get and set

We already know that Kotlin can access elements of a map in a similar way to arrays in Java:

val value = map[key]
Copy the code

We can also use the same operator to change the elements of a mutable map:

mutableMap[key] = newValue
Copy the code

Let's see how it works. In Kotlin, the subscript operator is a convention. Reading an element using the subscript operator is converted to a call to the GET operator method, and writing an element calls set. The Map and MutableMap interfaces already define these methods. Let's see how to add a similar method to a custom class. Square brackets can be used to refer to the coordinates of points, with p[0] accessing the x coordinate and p[1] accessing the y coordinate:

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])
20
Copy the code

All you need to do is define a function called get and mark operator. Expressions like p[1], where p has type Point, will be converted to a call to the GET method. x[a, b] - x.get(a ,b)

The arguments to get can be of any type, not just Int. For example, when you use the subscript operator on a map, the parameter type is the type of the key, which can be of any type. You can also define get methods with multiple parameters. For example, if you want to implement a class that represents a two-dimensional array or matrix, you can define a method such as operator Fun get(rowIndex: Int, colIndex: Int) and call it with matrix[row, col]. If you need to access a collection using different key types, you can also define multiple overloaded GET methods with different parameter types. We can similarly define a function that changes the value at a given subscript using square brackets syntax. The Point class is immutable, so this way of defining Point is meaningless. As an example, let's define another class to represent a mutable point:

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, 20)
 p[1] = 42
 println(p)
MutablePoint(x=10, y=42)
Copy the code

This example is as simple as defining a function called set to use the subscript operator in an assignment statement. The last argument to set receives the value to the right of the medium sign in the assignment statement, and the other arguments are subscripts inside square brackets. x[a ,b] = c - x.set(a, b, c)

In the terms of the

Another operator supported by collections is the IN operator, which checks whether an object belongs to a collection. The corresponding function is called contains. Let's implement the following, using the in operator to check whether a point belongs to a rectangle:

operator fun Rectangle.contains(p: Point): Boolean {
    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)
true
 println(Point(5, 5) in rect)
false
Copy the code

The object to the right of in will call the contains function, and the object to the left of IN will be used as an entry parameter. a in c - c.contains(a)

In our Rectangle. Contains implementation, we use the library's until function to build an open interval, and then use the in operator to check whether a point belongs to that interval. The open interval is the interval that does not contain the last point. For example, if you use 10.. 20 builds a normal interval (closed interval) that includes all numbers from 10 to 20, including 20. The interval 10 until 20 includes numbers from 10 to 19, but does not include 20. A rectangle class is usually defined in such a way that its bottom and right coordinates are not part of the rectangle, so it is appropriate to use open intervals here.

The provisions of the rangeTo

To create a range, use the.. Syntax. . The operator is a neat way to call the rangeTo function. start.. end - start.rangeTo(end)

The rangeTo function returns an interval. You can define this operator for your own class. However, if the class implements the Comparable interface, this is not required: you can create an interval of any Comparable element using the Kotlin standard library, which defines rangeTo functions that can be used on any Comparable element:

operator fun T: ComparableT T.rangeTo(that: T): ClosedRangeT
Copy the code

This function returns an interval that can be used to check whether some other element belongs to it.

The rangeTo operator has a lower precedence than the arithmetic operator, but it is best to enclose the arguments to avoid confusion:

 val n = 9  println(0.. (n + 1)) 0.. 10Copy the code

Also note that the expression 0.. N.foreach {} is not compiled and must enclose interval expressions to call its methods:

   (0.. n).forEach {print(it) }
0123456789
Copy the code

Use iterator's convention in the for loop

In Kotlin, the in operator can also be used in a for loop, just like interval checking. But in this case it means something different: it is used to perform iterations. This means that a call such as for(x in list) {} will be converted to list.iterator(), and hasNext and next methods will be repeatedly called on it just as in Java. In Kotlin, this is also a convention, meaning that iterator methods can be defined as extension functions. This explains why it is possible to iterate over a regular Java String: the library already defines an extension function iterator for CharSequence, which is the parent class of String:

public operator fun CharSequence.iterator(): CharIterator = object : CharIterator() {
    private var index = 0

    public override fun nextChar(): Char = get(index++)

    public override fun hasNext(): Boolean = index  length
}

 for (c in "abc") {}
Copy the code

Deconstruct declarations and component functions

Destruct declarations allow you to expand a single compound value and use it to initialize multiple individual variables. Here's how it works:

 val p = Point(10, 20)
 val (x, y) = p
 println(x)
10
 println(y)
20
Copy the code

A destruct declaration looks like a normal variable declaration, but it has multiple variables in parentheses. In fact, deconstructing statements again uses the principle of convention. To initialize each variable in a structure declaration, a function called componentN is called, where N is the location of the variable in the declaration. In other words, the previous example can be converted to: val (a, b) = p - val a = p.ponent1 (); Val b = p.ponent2 () For data classes, the compiler generates a componentN function for each property declared in the main constructor. The following example shows how to declare these functions manually for non-data classes:

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

One of the main scenarios used for destructuring declarations is to return multiple values from a function, which is very useful. If you want to do this, you can define a data class to hold the required value for the return and use it as the return type of the function. After calling a function, you can easily expand it by destructing the declaration, using its values. As an example, let's write a simple function that splits a filename into a name and an extension:

data class NameComponents(val name: String, val extension: String)

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

 val (name, ext) = splitFilename("example.kt")
 println(name)
example
 println(ext)
kt
Copy the code

Of course, it's impossible to define a wireless number of componentN functions so that the syntax can work with any number of sets, but that doesn't help either. The library only allows access to the first five elements of an object using this syntax. An easier way to make a function return multiple values is to use the Pair and Triple classes from the standard library, which are less semantically expressive because these classes also don't know what's in the object they return, but because they don't need to define their own classes, they can write less code.

Deconstruct declarations and loops

Destruct declarations can be used not only for top-level statements in functions, but also in other places where variables can be declared, such as in loops. A good example is enumerating items in a map. Here's a small example:

fun printEntries(mapL MapString, String) {
    for ((key, value) in map){
        println("$key - $value")
    }
}

 val map = mapOf("Oracle" to "Java"."JetBrans" to "Kotlin")   printEntries(map)
Oracle - Java
JetBrans - Kotlin
Copy the code

This simple example uses two of Kotlin's conventions: one for iterating over an object and the other for deconstructing declarations. The Kotlin library adds an extended iterator function to map that returns an iterator for an Entry. So, unlike Java, you can iterate over a Map directly. It also contains the extension functions Component1 and Component2 on Map.Entry, which return its key and value, respectively. In fact, the previous loop is converted to code like this:

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

Logic for reusing property access: delegate properties

Basic operations on delegate properties

The basic syntax for delegate attributes looks like this:

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

The property P delegates its accessor logic to another object: in this case, a new instance of the Delegate class. This object is obtained by evaluating the expression that follows it with the keyword BY, which can be used for any object that conforms to the rules of the attribute delegate convention. The compiler creates a hidden secondary property and initializes it with an instance of the delegate object to which the initial property P is delegated. For simplicity, we'll call it delegate:

Class Foo {private val delegate = delegate () // The compiler automatically generates var p: Type //p access to the delegateset(value: Type) = delegate.setValue(... , value) get() = delegate.getValue(...) }Copy the code

By convention, the Delegate class must have getValue and setValue methods (the latter only applies to mutable attributes). They can be member functions or extension functions. To make the example more concise, we omit the arguments here. The exact function signature will follow later. A simple implementation of the Delegate class would look something like this:

class Delegate{ operator fun getValue(...) {... } // Implement getter logic operator funsetValue(... , value: Type) {... } // implement setter logic} class Foo{var p: Type by Delegate() // Attribute associated Delegate object}   val foo = foo ()  val oldValue = foo.p  foo.p = newValueCopy the code

You can use foo.p as a normal property, and in fact, it will call the methods of the secondary property of type Delegate. To see how this mechanism can be used in practice, let's first look at an example of the power of delegate properties: library support for lazy initialization.

Using delegate attributes: lazy initialization and by lazy()

Lazy initialization is a common pattern, where parts of the object are created as needed until the property is first accessed. This is useful when the initialization process consumes a lot of resources and data is not always needed to use the object. A Person class, for example, can be used to access a mailing list written by a Person. Mail is stored in a database, which takes time to access. You want the mail to be loaded only on the first access and executed only once. Suppose you already have a function loadEmails, which is used to retrieve emails from a database:

class Email {/*... */} fun loadEmail(person: Person): ListEmail { println("Load emails for ${person.name}")
    returnlistOf(/*... * /)}Copy the code

Here's how to use the additional _emails property for lazy loading, null until not loaded, and then loaded as a mailing list:

class Person(val name: String) {
    private var _emails: ListEmail? = null
    val emails: ListEmail
        get() {
            if(_emails == null) {
                _emails = loadEmails(this)
            }
            return _emials!!
        }
}
 val p = Person("Alice")  p.mails // The first Load will access the email Load emailsfor Alice
 p.emails
Copy the code

This uses something called attribute support. You have one property, _emails, that stores this value, and another that provides reading access to the property. You need to use two properties because properties have different types: _emails can be null, while emails are non-null. This technique is often used and is worth mastering. But this code is a bit verbose: how long does it have to be if there are several lazy attributes? Also, it doesn't always work: the implementation is not thread-safe. Kotlin offers a better solution. Using delegate attributes makes the code much simpler, encapsulating the logic used to store the deserving attributes and ensure that the value is initialized only once. Here you can use the delegate put back by the standard library function lazy.

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}
Copy the code

The lazy function returns an object that has a properly signed method named getValue, so it can be used with the BY keyword to create a delegate property. The argument to lazy is a lambda that can be called to initialize this value. By default, the lazy function is thread-safe, and if needed, you can set other options to tell it which lock to use, or avoid synchronization altogether if the class is never used in multiple threads again.

Implementing delegate properties

To see how delegate properties are implemented, let's look at another example: notifying listeners when an object's properties change. This is useful in many different situations: for example, when an object is displayed on the UI, you want the UI to refresh automatically when the object changes. Java has standard mechanisms for such notifications: PropertyChangeSupport and PropertyChangeEvent classes. Let's look at how to use delegate properties without them in Kotlin, and then refactor the code to use delegate properties. The PropertyChangeSupport class maintains a list of listeners and sends them the PropertyChangeEvent event. To use it, you typically need to store an instance of the class as a field of the Bean class and delegate the handling of property changes to it. To avoid adding this field to every class, you need to create a small utility class that stores an instance of PropertyChangeSupport and listens for property changes. After that, your class inherits the utility class to access changeSupport.

open class PropertyChangeAware { protected val changeSupport = PropertyChangeSupport(this) fun addPropertyChangeListener(listener: PropertyChangeListener) { changeSupport.addPropertyChangeListener(listener) } fun removePropertyChangeListener(listener:  PropertyChangeListener) { changeSupport.removePropertyChangeListener(listener) } }Copy the code

Now let's write a Person class that defines a read-only property (as a Person's name, it generally doesn't change at any time) and two writable properties: age and salary. This class notifies its listeners when the person's age or salary changes.

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
    var age: Int = age
        set(newValue) {val oldValue = field / / field identifier field visit to support field = newValue changeSupport. FirePropertyChange ("age", oldValue, newValue) // Notifies listeners of property changes} var salary: Int = salaryset(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange("salary", oldValue, newValue)
        }
}

fun main(args: ArrayString) {
    val p = Person("Dmitry", 34, 2000) / / add listeners p.a ddPropertyChangeListener (PropertyChangeListener {event -  println ("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}"Property age changed from 34 to 35 Property salary changed from 2000 to 2100Copy the code

There's a lot of duplicate code in the setter, so let's try to extract a class that stores the value of this property and initiates the notification.

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

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
    val _age = ObservableProperty("age", age, changeSupport)

    var age: Int
        get() = _age.getValue()
        set(value) = _age.setValue(value)

    val _salary = ObservableProperty("salary", age, changeSupport)

    var salary: Int
        get() = _salary.getValue()
        set(value) = _salary.setValue(value)
}
Copy the code

You should now have a pretty good idea of how delegate properties work in Kotlin. You create a class that holds the value of an attribute and automatically triggers change notifications when the attribute is modified. You remove the repetitive logic code, but it takes quite a bit of boilerplate code to create an ObservableProperty instance for each property and delegate the getter and setter to it. Kotlin's delegate properties feature lets you get rid of boilerplate code. But before you do that, you need to change the signature of the ObservableProperty method to match the method required by the Kotlin convention.

class ObservableProperty(
        var propValue: Int, val changeSupport: PropertyChangeSupport
) {
    operator fun getValue(p: Person, prop: KProperty*): Int = propValue

    operator fun setValue(p: Person, prop: KProperty*, newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}
Copy the code

Compared to the previous version, this time the code has made some changes:

  • Now, to get back to that as well, the getValue and setValue functions are marked with operator
  • These functions take two parameters: one to receive an instance of the property, which is used to set or read the property, and one to represent the property itself. The property is of type KProperty (more on this later), and for now you just need to know the name of the property that can be accessed via kproperty.name.
  • The name attribute was removed from the main constructor because the attribute name is now accessible through KProperty.

Finally, you can see the magic of the Kotlin delegate property, how much shorter is the code?

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
    var age: Int by ObservableProperty(age, changeSupport)
    var salary: Int by ObservableProperty(salary, changeSupport)
}
Copy the code

With the keyword BY, the Kotlin compiler automatically does what was done manually in previous versions of the code. If you compare this code to the previous version of the Person class: the code generated when you use the delegate attribute is very similar, 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 the property is accessed or modified. Instead of manually implementing observable property logic, you can use the Kotlin standard library, which already includes classes like ObserverProperty. There is no coupling between the standard library and the PropertyChangeSupport class used here, so you need to pass a lambda to tell it how to tell a property that it is worth changing, like this:

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
    private val observer = {
        prop: KProperty*, oldValue: Int, newValue: Int -
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }

    var age: Int by Delegates.observable(age, observer)
    var salary: Int by Delegates.observable(salary, observer)
}
Copy the code

The expression to the right of by does not have to be a newly created instance. It can be a function call, another property, or any other expression, as long as the value of the expression is an object that the compiler can call getValue and setValue with the correct argument type. As with other conventions, getValue and setValue can be methods or extension functions of the object's own life. Note that to keep the example simple, we have only shown how to use a delegate attribute of type Int; the delegate attribute mechanism is really generic and applies to any other type.

Transformation rules for delegate attributes

Let's summarize how delegate properties work. Suppose you already have a class with delegate properties:

class C {
    var p: Type by MyDelegate()
}

val c = C()
Copy the code

The MyDelegate instance is saved to a hidden property called Delegate . The compiler will also represent this property with an object of type KProperty, called Property . The compiler generates the following code:

class C {
    private val delegate = MyDelegate()
    
    var prop: Type
        get() = delegate.getValue(this, property)
        set(value: Type) = delegate.setValue(this, property, value)
}
Copy the code

Therefore, in each property accessor, the compiler generates the corresponding getValue and setValue methods: val x = c.prop - val x = .getValue(c, ) c,prop = x - .setValue(c, , x)

This mechanism is very simple, but it can implement many interesting scenarios. You can customize where to store the property value (in a map, database table, or Cookie of the user session) and what to do when accessing the property (such as adding authentication, change notifications, and so on).

Save attribute values in map

Another common way delegate properties come into play is with objects that have dynamically defined sets of properties. Such objects are sometimes called expando objects. For example, consider a contact management system that can be used to store arbitrary information about contacts. Each person in the system has some attributes that require special treatment (such as a name) and an arbitrary number of additional attributes that are unique to each person (such as the birthday of the youngest child). One way to implement such a system is to store all of a person's attributes in a map, providing attributes indefinitely-to access information that requires special processing. Here's an example:

class Person {
    private val _attributes = hashMapOfString, String()
    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }
    
    val name: String
        get() = _attributes["name"]!!!!! } fun main(args: ArrayString) { val p = Person() val data = mapOf("name" to "Dimtry"."company" to "JetBrans")
    for ((attrName, value) inData) {p.setAttribute(attrName, value)} println(p.name)} // Output DimtryCopy the code

A generic API is used to load data into an object (in a real project, JSON deserialization or similar), and then use a specific API to access the value of a property. Changing it to a delegate attribute is simple enough to place map directly after the BY keyword.

class Person {
    private val _attributes = hashMapOfString, String()
    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    val name: String by _attributes
}
Copy the code

Since the library already defines getValue and setValue extension functions on the standard Map and MutableMap interfaces, this can be used directly here. The name of the attribute is automatically used as the key in the map and the attribute value as the value in the map. Attributes [prop. Name] has been changed to _attributes[prop. Name].

Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.