This chapter covers some extended object orientation features that are not necessary for a program to work, but nevertheless improve readability and expressiveness. The chapter presumes that you have read Chapter 2. We also use the NumberGuess sample app from Chapter 2.
Anonymous Classes
If you extend some superclass as in the latter listing, this one might also be abstract. It is then necessary, however, to implement all abstract functions, as is usually the case for an instantiation to be possible. Because the name of the interface implementation or subclass is neither specified nor needed, such classes are called anonymous classes. Inside the class body between the curly braces you can write anything that you could also write inside a named class’s body.
Note
The object : inside the declaration suggests that there is just a one-time instantiation. It is not possible to have several instances of anonymous classes.
the first this refers to the anonymous class, and this@A refers to the instance of class A.
Inner Classes
Functions and Properties Outside Classes
In your project you can have Kotlin files that do not contain a single class, interface, or object declaration but nevertheless show val and var properties and functions. Whereas at first sight we seem to work outside object orientation if we use such files, in fact the Kotlin compiler implicitly and secretly creates a singleton object based on the package name and puts such properties and functions into this object.
Note
For very small projects it is acceptable to not use explicit classes and singleton objects. If a project gets bigger, it is still possible to only use such nonclass files, but you’ll then risk having chaotic and unreadable code in the end.
It does not matter where in the file you declare val and var properties and functions; they will be usable from everywhere in the file.
Such properties and functions are visible to other classes or singleton objects you write in other files using import the.package.name.name where the last name refers to the property or function name.
You can have several files of that type inside a package. The Kotlin compiler then just sequentially parses all the files and gathers all the functions and properties that are neither from inside a class nor a singleton object. The file names play no role here.
If you have several such files in different packages (as defined by the package declarations at the top of the files), name clashes do not cause a problem. You could have properties and functions using the same name. Nevertheless, you should avoid this to keep your code readable.
It is possible to add classes, interfaces, and singleton objects to such files. You can use such structure units from the place of their declaration until the end of the file.
Additionally, it is possible to import properties and functions from all files of that kind inside a particular package using the wildcard notation import the.package.name.*. This comes in very handy to avoid a lengthy import list.
Exercise 1
Could you think of a way to rewrite the Util.kt file to not use the object { } declaration? What will the client code look like?
Importing Functions and Properties
at the top of the file, after the package declaration and together with the other import statements for importing classes and singleton objects. It is then possible to directly use the function or property by using just its name, without a prepending ObjectName.
Note
There is no wildcard for importing all properties and functions of a singleton object. You have to put each of them into its own import line.
Exercise 2
such that Math. is no longer needed.
Data Classes
The class automatically gets a specially tailored toString() function based on the properties; you don’t have to write your own.
The class automatically gets sensible equals() and hashCode() functions based only on the properties. We’ll talk about object equality later; for now, this is what you need to know: The equality check relation a == b for two instances of data classes yields true only if the instances belong to the same data class, and all their properties need to be equal as well.
You can see that with the single data class line at the top, we can make the function movePoint() return a structured datum.
Exercise 3
- 1.
Point2D(0, 1) == Point2D(1, 0)
- 2.
Point2D(1, 0) == Point3D(1, 0, 0)
- 3.
Point2D(1, 0).x == Point3D(1, 0, 0).x
- 4.
Point2D(1, 0) == Point2D(1.0, 0)
- 5.
Point2D(1, 0) == Point2D(1, 0)
Describe why or why not.
Exercise 4
Which classes of the NumberGuess game are considered data classes? Perform the conversion.
Enumerations
The enumeration type is basically a nonnumeric data type with values from a given set. Basically here means that internally the type gets handled by an integer by default, but in basic usage scenarios you don’t have to be concerned about that. The term set is used in a mathematical sense, which means that values must be unique and do not have a sort order.
where for EnumerationName you can use any camelCase name and VALUEx is any string from the character set A-Z0-9_ starting with a letter or _.
Note
For the values, technically more characters are available, but by convention you should use a combination of the characters shown here.
Use enumVal.name to get the value’s name as a string.
Use enumVal.ordinal to get the value’s index in the enumeration values list.
Exercise 5
Add a Gender enumeration to the GameUser class from the NumberGuess game app. Allow values M, F, and X. Add a corresponding constructor parameter gender to the GameUser constructor parameters with default value X.
Custom Property Accessors
We also know that to get the var we write object.propertyName and to set it we write object.propertyName =...
Inside the set body you can access all the functions and all the other properties of the object. In addition, you can use the special field identifier to refer to the datum corresponding to the property.
or any of the other visibility modifiers. To make the getter private, though, the property itself must be declared to be private as well. Making the setter private for a public property is a valid option instead.
Interestingly, it is possible to define properties that do not have corresponding data in the class or singleton object. If you define both the setter and getter of a property and specify neither an initial value nor use field inside the setter code, no data field will be generated for that property.
Exercise 6
Can you guess what can be done with val instead of var properties?
Exercise 7
Write an str property that does the same as the toString() function (so it is possible to write obj.str instead of obj.toString()).
Exercise 8
Find a way to avoid such corrupted states. Hint: Afterward an init{ } block is no longer needed. Update your code accordingly.
Kotlin Extensions
In Kotlin, it is possible to “dynamically” add extensions to classes. We need to put that dynamically in quotation marks because the usage of such extensions must be defined in your code prior to execution. It is not possible in Kotlin to decide during runtime whether or not, and if so, which extensions get used. Computer language designers usually refer to such features as static features.
Here is what we mean by extensions: Wouldn’t it be nice if we could add functions and custom properties to any class? This could be very useful, for example, if we want to add extra functionality to classes and functions provided by others. We know we can use inheritance for that purpose, but depending on the circumstances, this might not be possible or the implementation could feel clumsy.
Caution
The extension mechanism is extremely powerful. Be cautious not to overuse it. You can write very elegant code using extensions that no one understands without time-consuming research of the extension definitions.
Extension Functions
inside some file fileName.kt (the file name doesn’t play a role here, so use whatever you like) inside some package the.ext.pckg. Remember the == checks for equality.
The same process is possible for any other class, including your own classes, and companion objects. For the latter case write fun SomeClass.Companion.ext() { } to define a new extension function ext. The Companion here is a literal identifier used to address the companion object.
Note
If extension functions have the same name and function signature (parameter set) as already existing functions, the latter take priority.
Extension Properties
Extensions with Nullable Receivers
Note
Receiver refers to the class or singleton object that is being extended.
you can check whether this == null inside the body and appropriately react in such cases to do the right thing. You can then write instance.newFunction(...) even if instance is null and even then get into the extension function.
Encapsulating Extensions
The obvious advantage of encapsulated extensions is that we don’t have to import extension files. If we want to define extensions that are usable for many classes, the nonencapsulated variant would be the better choice.
Functions with Tail Recursion
Note that the if() expression returns the part before or after the else, depending on whether the argument evaluates to true or false (we’ll be talking about branching later in the book).
For proper application functioning, the runtime engine needs to keep track of function calls, so internally a call to factorial() will look like factorial( factorial( factorial (...) ) ) If this recursion depth is not too high, this is not a problem. If it gets really high, though, we’ll run into trouble concerning memory usage and performance. However, provided the recursion happens in the last statement of such a function, it can be converted to a tail recursion function and then internally an overuse of system resources won’t happen.
Infix Operators
The infix could be omitted here, because Kotlin knows that * belongs to an infix operation.
Using the standard operators for defining custom calculation is called operator overloading. In the following section we learn more about that, using a list and the textual representation of all the standard operators.
Operator Overloading
Handling one expression is referred to as a unary operation and the operator accordingly gets called a unary operator. Likewise, handling two expressions gives us binary operations and binary operators.
From math we know a lot of operators like –a, a + b, a * b, a / b, and so on. Kotlin, of course, has many such operators built in for its data types, so 7 + 3 and 5 * 4 and so on do the expected things. We’ll be talking about operator expressions in detail later in this book, but for now we want to pay some attention to operator overloading, the capability of Kotlin that lets you define your own operators using standard operator symbols for your own classes.
That is it. The val v:Vector = p2 - p1 now works, so whenever the compiler sees a - between two Point instances it calculates the vector combining them.
to the Vector class.
Operators
Symbol | Arity | Textual | Standard Meaning |
---|---|---|---|
+ | U | unaryPlus | Reproduces data (e.g., +3). |
− | U | unaryMinus | Negates data (e.g., −7). |
! | U | not | Logically negates data (e.g., !true == false). |
++ | U | inc | Increments data (e.g., var a = 6; a++; // -> a == 7). The operator must not change the object on which it gets invoked! The assignment of the incremented value happens under the hood. |
−− | U | dec | Decrements data (e.g., var a = 6; a−−; // -> a == 5). The operator must not change the object on which it gets invoked! The assignment of the decremented value happens under the hood. |
+ | B | plus | Adds two values. |
− | B | minus | Subtracts two values. |
∗ | B | times | Multiplies two values. |
/ | B | div | Divides two values. |
% | B | rem | Remainder of a division (e.g., 5 % 3 = 2). |
.. | B | rangeTo | Creates a range (e.g., 2..5 -> 2, 3, 4, 5) |
in !in | B | contains | Checks whether the right side is contained or not contained in the left side. |
[ ] | B+ | get / set | Indexed access. If on the left side of an assignment like q[5] = ... the set() function gets used with the last parameter designating the value to set. The get() and set() functions allow more than one parameter, which then corresponds to several comma-separated indexes inside []; for example, q[i] →q.get(i), q[i,j] →q.get(i, j), and q[i,j] = 7 →q.set(i, j, 7) |
( ) | B+ | invoke | Invocation. Allows more than one parameter, which then corresponds to several comma-separated parameters inside (); for example, q(a) →q.invoke(a) and q(a, b) →q.invoke(a, b). |
+ = | B | plusAssign | Same as plus(), but assigns the result to the instance at which the operator is invoked. |
− = | B | minusAssign | Same as minus(), but assigns the result to the instance at which the operator is invoked. |
∗ = | B | timesAssign | Same as times(), but assigns the result to the instance at which the operator is invoked. |
/ = | B | divAssign | Same as div(), but assigns the result to the instance at which the operator is invoked. |
% = | B | remAssign | Same as rem(), but assigns the result to the instance at which the operator is invoked. |
== | B | equals | Checks for equality. The ! = stands for unequals and corresponds to equals() returning false. |
< > <= >= | B | compareTo | Compares two values. The compareTo() function is supposed to return −1, 0, +1, depending on whether the argument is less than, equal to, or greater than the value to which the function is applied. |
Note
Because in the operator function body or expression you can calculate what you want, you can let the operator do strange stuff. Just bear in mind that your class users expect a certain behavior when operators are used, so be reasonable with what you calculate there.
Don’t forget to import the extension file, just like you would for any other extension.
Exercise 9
Add - and + operators to the Vector class. The calculation consists of adding or subtracting the dx and dy members: Vector(this.dx + v2.dx, this.dy + v2.dy) and Vector(this.dx - v2.dx, this.dy - v2.dy) if v2 is the operator function parameter.