After reading Chapter 2 about classes and objects, it is now time shine more light on properties and their types, and also on the options we have to declare functions and what can be done from inside functions. This chapter talks about property and function declarations, but also about an important feature of object-oriented languages, inheritance, through which properties and functions of some class can be altered and redefined by other classes. We also learn about visibility and encapsulation, which help us to improve program structure.
Properties and Their Types
Apart from this, any property from a class’s constructor prepended by val or var directly and automatically goes into a corresponding hidden property using the same name. In the following paragraphs we discuss all the possible options for properties given inside the class body.
Simple Properties
If inside the class or singleton object or companion object a value gets assigned inside the init{ } block, the = initial_value can be omitted.
If Kotlin can infer the type by the initial value given, the :PropertyType can be omitted.
Such simple properties can be accessed from outside via instanceName.propertyName for classes and ObjectName.propertyName for singleton objects. Inside the class or singleton object, just use the propertyName to access it.
Can you guess the output? This short program prints Peter Smith, although we changed the first name to Linda. The answer to the question of how this can be is that the full name gets calculated inside init{ }, and init{ } does not get invoked again after we alter the first name, so we’d have to take care of that.
Note
For example, you would introduce a new function like setFirstName() and update the first name, the full name, and the initials accordingly. A possibly cleaner variant is a function that calculates the full name on the fly, without using a separate property for it: fun fullName() = firstName + " " + lastName
This is one of the reasons you should prefer vals over vars wherever possible; it is just easier to avoid corrupted states.
Exercise 1
Property Types
String: This is a character string. Each character from the Basic Multilingual Plane (the original Unicode specification) is of type Char (see later). Supplementary characters use two Char elements. For most practical purposes and in the majority of languages, assuming each string element to be a single Char is an acceptable approach.
Int: This is an integer number. Values range from −2,147,483,648 to 2,147,483,647.
Double: This is a floating-point number between 4.94065645841246544 · 10-324 and 1.79769313486231570 · 10+308 for both positive and negative signs. Formally it is a 64-bit floating-point value from the IEEE 754 specification.
Boolean: This is a boolean value that can be true or false.
Any class: Properties can hold instances of any class or singleton object. This includes built-in classes, classes from libraries (software built by others you use), and your own classes.
Char: This is a single character. Characters in Kotlin use the UTF-16 encoding format (characters from the original Unicode specification) to store them.
Long: This is an extended integer number with values between −9,223,372,036,854,775,808 and 9,223,372,036,854,775,807.
Short: This is an integer number with a reduced value range. Values are from –32,768 to 32,767. You won’t see this often, because for most practical use cases an Int is the better choice.
Byte: This is an integer number from the very small range from –128 to 127. This type is frequently used for low-level operating system function calls. You will probably not use this type often, with the exception that it frequently gets used if you perform input/output (I/O) operations with files.
Float: This is a lower precision floating-point number. Ranges are from 1.40129846432481707 · 10−45 to 3.40282346638528860 · 10+38 for both positive and negative signs. Formally it is a 32-bit floating-point value from the IEEE 754 specification. Unless storage space or performance is a major issue, you would usually prefer Double over Float.
[Any class]: You can use any class or interface as a type, including those built in as provided by Kotlin, from other programs you use, and from your own programs.
[Enumeration]: Enumerations are data objects with possible values from a set of unordered textual values. See Chapter 4 for further details.
Property Value Assignment
where initial_value is any expression or literal that can be converted to the expected property type. We will talk about literals and type conversion later in this chapter.
This is only possible if the property was previously declared, either in the class, singleton object, or companion object, or as a var in the primary constructor declaration.
This is obviously possible only for mutable vars.
Exercise 2
Create a class A with one property var a:Int. Perform assignments: (a) set it to 1 inside the declaration, (b) set it to 2 inside an init{ } block, (c) set it to 3 inside function fun b(){ … }, and (d) set it to 4 inside a main function.
Literals
Literals
Literal Type | Description | Enter |
---|---|---|
Decimal Integer | An integer 0, ± 1, ± 2, … | 0, 1, 2, …, 2147483647, –1, –2, …, –2147483648 If you like you can use underscores as the thousands separator, as in 2_012 |
Double Precision Float | A double precision floating- point number between 4.94065645841247.10-324 and 1.79769313486232.10+308 with a positive or negative sign | Dot notation: [s]III.FFF where [s] is nothing or a + for positive values, – for negative values; III is the integer part (any number of digits), and FFF is the fractional part (any number of digits) Scientific notation: [s]CCC.FFFe[t]DDD where [s] is nothing or a + for positive values, – for negative values, CCC.FFF is the mantissa (one or more digits; the .FFF can be omitted if not needed), [t] is nothing or a + for positive exponents, – for negative exponents, and DDD is the (decimal) exponent (one or more digits) |
Char | A single character | Use single quotation marks, as in val someChar = 'A'. There are a number of special characters: write for a Tab, for a Backspace, for a newline, for a carriage Return, ' for a single quote, \ for a backslash, and $ for a dollar sign. In addition, you can write uXXXX for any unicode character XXXX (hex values); for example, u03B1 is an α |
String | A string of characters | Use double quotation marks, as in val someString = "Hello World". For the characters inside, the same rules as for Chars apply, except that for a single quotation mark you don’t use a preceding backslash, but for a double quotation mark you use one: "Don't say "Hello"". In Kotlin there are also multiline raw string literals: Use triple double quotation marks as in """ Here goes multiline contents""". Here the escaping rules for the characters inside no longer apply (that is where the name raw comes from). |
Hexadecimal Integer | An integer 0, ± 1, ± 2, … using the hexadecimal basis | 0x0, 0x1, 0x2, …, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10, …, 0x7FFFFFFF, –0x1, –0x2, …, –0x80000000 |
Long Decimal Integer | A long integer 0, ± 1, ± 2, … with extended limits | 0, 1, 2, …, 9223372036854775807, –1, –2, …, –9223372036854775808 If you like you can use underscores as thousands separator, as in 2_012L |
Long Hexadecimal Integer | An integer 0, ± 1, ± 2, … with extended limits, using the hexadecimal basis | 0x0, 0x1, 0x2, …, 0x9, 0xA, 0xB, …, 0xF, 0x10, …, 0x7FFFFFFFFFFFFFFF, –0x1, –0x2, …, –0x8000000000000000 |
Float | A single precision float | Same as double precision float, but add an f at the end; e.g., val f = 3.1415f |
Note
Remember that in the decimal system 214 means 2 · 102 + 1 · 101 + 4 · 100. In the hexadecimal system we accordingly have 0x13D mean 2 · 162 + 3 · 161 + 13 · 160. The letters A, B, …, F correspond to 10, 11, …, 15.
As for type compatibility, you can assign normal integers to long integer properties, but not the other way around. You can also assign reduced precision floats to double properties, but not the other way around. Disallowed assignments require you use a conversion (see Chapter 5).
To assign literals to Short and Byte properties, use integers, but make sure the limits are not exceeded.
Both single and triple double quotation mark String literal representations exhibit a feature called string templates . That means that an expression starting with a dollar sign and followed by an expression surrounded by curly brackets gets executed and its result is passed to the string. Therefore "4 + 1 = ${4+1}" evaluates to the string "4 + 1 = 5". For simple expressions built from just a single property name, the curly braces can be omitted, as in "The value of a is $a".
Exercise 3
avoiding the string concatenation "…" + "…"
Property Visibility
Visibility is about which parts of your program can access which functions and properties from other classes, interfaces, objects, or companion objects. We talk about visibility in depth in the section “Visibility of Classes and Class Members” later in this chapter.
Null Values
Other languages like Java allow nullability for any object type, which frequently leads to problems because null has neither any property nor function. Consider, for example, someInstance.someFunction(), which behaves nicely if someInstance points to a real object. If, however, you set someInstance = null, a subsequent someInstance.someFunction() is not possible and thus leads to an exceptional state. Because Kotlin draws a distinction between normal properties and nullable properties, such state inconsistencies can more easily be avoided by the Kotlin compiler.
Exercise 4
- 1.
You can perform an assignment val a:Int = null.
- 2.
It is possible to write val a:Int? = null; val b:Long = a.toLong().
- 3.
It is possible to write val a:Int? = null; val b:Long? = a.toLong().
- 4.
It is possible to write val a:Int? = null; val b:Long? = a?.toLong().
Property Declaration Modifiers
const: Add const as in
lateinit: If you add lateinit as in
This leads to a runtime error but not a compile time error, and thus thwarts the Kotlin nullability check system. Using lateinit makes sense if variables get initialized in a way the Kotlin compiler cannot detect (e.g., by reflection). Do not use lateinit unless you really know what you want do. By the way, it is possible to check whether a lateinit var has been initialized or not by using ::name.isInitialized.
Member Functions
Depending on the terminology used, functions are also sometimes called operations or methods .
Functions Not Returning Values
Inside the function body, you can have any number of return statements exiting the function. A return at the end of the body is allowed as well, but not needed.
Note
In Kotlin, function arguments cannot be reassigned inside the function body. This is not a disadvantage, as reassigning function parameters inside the function is considered bad practice anyway.
Functions also can have variable argument lists. This feature is called varargs and we discuss it later. Another feature we cover later is default arguments . Such arguments allow for the specification of a default value that will be used if a parameter is not specified in the function invocation.
private, protected, internal, and public: These are visibility modifiers. Visibility is explained in the section “Visibility of Classes and Class Members” later in this chapter.
open: Use this to mark a function in a class as overridable by subclasses. See the section “Inheritance” later in this chapter for details.
override: Use this to mark a function in a class as overriding a function from an interface or from a superclass. See the section “Inheritance” later in this chapter for details.
final override: This is the same as override, but indicates that further overwriting by subclasses is prohibited.
abstract: Abstract functions cannot have a body, and classes with abstract functions cannot be instantiated. You must override such functions in a subclass to make them concrete (which means “unabstract” them). See the section “Inheritance” later in this chapter for details.
You cannot freely mix modifiers. Particularly for the visibility modifiers, only one is allowed. You can, however, combine any of the visibility modifiers with any combination of the other modifiers listed here. If you need more than one modifier, the separator to use is a space character.
Note that declarations in interfaces usually don’t have and don’t need modifiers. Visibility values other than public, for example, are not allowed here. Functions in interfaces are public by default, and because they have no implementations in the interfaces themselves, you can consider them abstract by default, so explicitly adding abstract is unnecessary.
Exercise 5
Exercise 6
Functions Returning Values
It is possible to have more than one return statement inside the body, but they all must return a value of the anticipated type.
Note
Experience says that for improved code quality it is better to always use just one return statement at the end.
that the function return type is Int.
Note
Actually Kotlin internally lets all functions return a value. If a returned value is not needed, Kotlin assumes a special void type that it calls Unit. If you omit : ReturnType and the function does not return a value, or if the function body does not have a return statement at all, Unit is assumed. If, for whatever reason, it helps to improve the readability of your program, you can even write fun name(…) : Unit { … } to express that a function does not return any interesting value.
Exercise 7
Exercise 8
Exercise 9
Create an interface AInterface describing all of class A from Exercise 8.
Accessing Masked Properties
The this refers to this current object, so this.xyz means property xyz from this current object, not xyz as made visible by the function specification.
Note
Some people use the term shadowed instead of masked for such properties. Both mean the same thing.
Exercise 10
Function Invocation
To call the function of a companion object from inside the class you also just use functionName([parameters]). From outside the class, you’d use ClassName.functionName([parameters]) here.
Exercise 11
describe how to access function x() with 42 as a parameter from outside the class in a println() function.
Function Named Parameters
from inside the class or object. Here the parN are the exact function parameter names as in the function’s declaration. As an additional benefit of using named parameters, you can use any parameter sort order you like, because Kotlin knows how to properly distribute the parameters provided. You can also mix unnamed and named parameters, but it is then necessary to have all named parameters at the end of the parameter list.
Exercise 12
create an instance and use named parameters to set the name to John Doe.
Caution
Using named parameters in function calls greatly improves code readability. However, be careful if you use code from other programs, as with new program versions the parameter names might change.
Function Default Parameters
To let the defaults apply you then just omit them in the invocation. If you omit x parameters at the end of the list, the x rightmost parameters take their default values. This sorting order dependency makes using default parameters a little cumbersome. If you mix named parameters and default parameters, though, using defaults adds versatility to functions.
Exercise 13
add as defaults lastName = "", firstName = "", birthDay = null, ssn = null. Then invoke the function using named parameters, specifying just lastName = "Smith" and ssn = "1234567890".
Function Vararg Parameters
We learned functions exist to take input data and alter an object’s state from that, possibly producing some output data. So far we have learned about fixed parameter lists, covering a big subset of all possible use cases. What about lists of unknown, potentially unlimited size, though? Such lists are called arrays or collections, and any modern computer language needs to provide a way to handle such data in addition to types holding single data elements. We cover arrays and collections in greater detail in Chapter 9, but for now you should know that arrays and collections are fully fledged types and you can use them for single constructor and function parameters, as in …, someArray:Array<String>, ….
Note
Kotlin actually allows vararg parameters to appear anywhere earlier in the parameter list. Kotlin then, however, can distribute passed-in parameters during function invocation only if the next parameter after the vararg has a different type. Because this complicates call structures, it is better to avoid such vararg constructs.
You can extend the name list at will.
Exercise 14
to print the parameter. Create a main(args:Array<String>) function outside the class, instantiate a Club, and invoke its addMembers() function with three names “Hughes, John”, “Smith, Alina”, and “Curtis, Solange”.
Abstract Functions
Abstract classes are something between interfaces and normal classes: They provide implementations for some functions and leave other functions abstract (unimplemented) to allow for some variety. Abstract classes thus frequently serve for some kind of “basis” implementation, leaving the details to one or more classes implementing the abstract functions.
Here TheClass can be instantiated, as it implements the abstract function. For more details about subclassing see the section “Inheritance” later in this chapter.
Polymorphism
Inside a class, a singleton object, a companion object, or an interface, you can have several functions using the same name with different parameters. There is no magic to that, but this feature in object orientation theory has its own name: polymorphism.
If you have several functions with the same name, Kotlin decides by looking at the parameters. An invoking code specifies which function to actually use. This dispatching procedure usually works and you won’t see any problems, but with complicated classes and lots of possibilities for a certain class, perhaps including complex parameter lists with default arguments, interfaces, and varargs, the decision of which function to call is not unambiguous. In such cases the compiler issues an error message and you have to redesign the function call or your class for everything to work correctly.
If you now call calc.add(…) with some argument, Kotlin takes the argument’s type to find out which of the functions to call.
Caution
Be careful with function naming: Polymorphism (i.e., several functions with the same name) should not happen by accident or for just technical reasons. Instead, all functions using one particular name should serve the same purpose from a functional perspective.
Local Functions
Inheritance
In real life, inheritance means leaving one’s belongings to someone else. In object-oriented computer languages like Kotlin, the idea is similar. Given a class A, writing class B : A indicates that we give all the assets from class A to class B. What is this good for, beyond having some kind of renamed copy of A? The magic is that class B can overrule or override parts of the assets it inherits from class A. This can be used to alter some aspects of the class it inherits from to introduce new behavior.
Although this overriding of functions and properties somewhat deviates from the real-life analogy of inheritance, inheriting classes and overriding particular functions and properties is one of the central aspects of any object-oriented language.
Classes Inheriting from Other Classes
The open in the class declaration is a Kotlin specialty. Only classes marked with open can be used for inheritance.
Note
This is a somewhat odd design decision from the makers of Kotlin. It basically disables inheritance unless you add open to all possible classes that could be used for inheritance. In real life, developers will likely forget to add open to all their classes or even refuse to add open everywhere because it feels like a nuisance, so inheritance most likely is broken if your program uses classes from other programs or libraries. Unfortunately, there is no way out, so we have to live with that. You can, of course, add open wherever needed in your own classes.
In relation to each other, the class used as a basis for inheritance is also called a superclass and the class inheriting from it is a subclass. In the preceding code, therefore, A is the superclass of B, and B is the subclass of A.
In our NumberGuess example you can see, for instance, that our MainActivity class inherits from AppCompatActivity. This subclassing of built-in activity classes is important for any app to work with Android.
Constructor Inheritance
Because we know that secondary constructors always also invoke the primary constructor, inheritance by design in any case always invokes the superclass’s primary constructor and init{ } block. This is also true if the subclass provides its own init{ } block, which then gets called second in line. Beginners tend to forget this fact, but if you keep this in mind you could avoid some difficulty.
Such an overridden property will then be addressed by any code from the superclass that formerly used its own original version of the property.
Exercise 15
Note that Log.d("TAG", …) prints the second argument to the console.
Overriding Functions
in the subclass.
Overriding Properties
Using this notation, any usage of the property from inside class B and A is then covered by the property as declared in class B. The property behaves as if the declaration in class A didn’t exist any longer, and functions in A formerly using “their” version of this property will use the property from class B instead.
Exercise 16
Note that Log is provided by the Android libraries automatically included in your project. If you first get an error, place the cursor over it and then press Alt+Enter for a resolution. Can you guess why property g in class A has to be declared private, meaning no other class can see it or use it?
Exercise 17
In Exercise 16, remove the private from the property declaration and make class B override property g from class A. What will be the output?
Accessing Superclass Assets
Local Variables
Such local variables are valid from the place of their declaration to the end of the function; that is why they are called local. They are, of course, allowed to calculate any expression that is necessary to return something from the function, as they will not be destroyed before the return happens.
Local variables should not mask function parameters for code quality reasons. If you have a function parameter xyz of any type, you should not declare a local variable inside the function using the name xyz. The compiler allows that, but it will issue a warning about that shadowing.
Exercise 18
Visibility of Classes and Class Members
For conciseness, however, you would usually omit that, because public is the default visibility in Kotlin.
The client doesn’t need to know details of the internal functioning of a class or an object. It can just ignore anything that is marked private, reducing distractions and making it easier to understand and use the class or object.
Because the client only needs to know about public properties and functions, the implementation of the private functions together with all private properties is freely changeable at any time, provided the public properties and functions keep functioning in the expected way. It is thus easier to improve classes or fix deficiencies.
Here you also clearly see that we need onCreate() and onSaveInstanceState() to be public, because the Android runtime needs to access them from outside for life cycle handling. Furthermore, start() and guess() need to be public as well, because they get accessed from outside as a result of button presses. The remaining three functions are accessed from only inside the class, hence the private visibility for those.
Visibility
Visibility | Asset | Description |
---|---|---|
public | Function or property | (Default) The function or property is visible from everywhere inside and outside the structure unit. |
private | Function or property | The function or property is visible only from inside the same structure unit. |
protected | Function or property | The function or property is visible from inside the same structure unit, and from inside any direct subclass. Subclasses get declared via class TheSubclass-Name : TheClassName { … } and they inherit all the public and protected properties and functions of the class from which they inherit. |
internal | Function or property | Functions and properties are public only for structure units from the same program. For programs from other compilations, especially for programs from others you include in your software, internal gets treated like private. |
public | Class, singleton object, or companion object | (Default) The structure unit is visible from everywhere inside and outside the program. |
private | Class, singleton object, or companion object | The structure unit is visible only from inside the same file. For inner classes the structure unit is only visible from an enclosing structure unit. For example class A { private class B { ... } fun function() { val b = B() } } |
protected | Class, singleton object, or companion object | The structure unit is visible only from an enclosing structure unit or a subclass of it. For example class A { protected class B { ... } fun function() { val b = B() } } class AA : A { // subclass of A fun function() { val b = B() } } |
Note
For small projects, you wouldn’t care about any visibility modifiers apart from the default public one. For larger projects, adding visibility restrictions helps to improve software quality.
Self-Reference: This
If a function’s argument uses the same name as a property of the same class, we already know that the parameter masks the property. We also know that we still could access the property if we prepend this. In fact, this is the primary use case for using this. Under some circumstances it could also help to improve the readability if you prepend this. to function or property names. For example, in functions that set instance properties, using this helps to express that setting properties is the primary purpose of the function.
It technically also works without the three this. instances, but in that case it is less expressive.
Converting Classes to Strings
In Kotlin, any class automatically and implicitly inherits from the built-in class Any. You don’t have to explicitly state it, and there is no way to prevent it. This super-superclass already provides a couple of functions, one of which has the name and return type toString():String. This function is kind of a multipurpose diagnostic function that frequently gets used to let an instance tell about its state in a textual representation. The function is open, so any class can override this function to let your classes indicate instance state in an informal way.
Remember the super. addresses unoverridden properties and functions.
Exercise 19
will print something like A@232204a1, which, depending on the circumstances, is not very informative. Therefore for diagnostic output, providing a toString() implementation is a good idea.