Now that we have set up our development environment and our IDE of choice, it is time to explore Kotlin. We will start by diving into the basics of the language and progress into more advanced topics, such as object-oriented programming (OOP).
The fundamentals of the Kotlin programming language
Kotlin basics
In this section, we will explore the basics of Kotlin—the building blocks, if you will. We will start by discussing variables.
Variables
A variable is an identifier for a memory location that holds a value. A simpler way to describe a variable is an identifier that holds a value. Consider the following program:
fun main(args: Array<String>) {
var x: Int = 1
}
The preceding x is a variable and the value it holds is 1. More specifically, x is an integer variable. The x is referred to as an integer variable because x has been defined to have the Int data type. As such, the x variable can only hold an integer value. To be more accurate, we say that x is an instance of the Int class. At this point, you must be wondering what the words instance and class mean in this context. All will be revealed in due time. For now, let's focus on the topic of variables.
When defining a variable in Kotlin, we make use of the var keyword. This keyword specifies that the variable is mutable in nature. Thus, it can be changed. The data type of the declared variable comes after a semicolon that follows the variable's identifier. It is important to note that the data type of a variable need not be explicitly defined. This is because Kotlin supports type inference—the ability to infer types of objects upon definition. We might as well have written the definition of our x variable as:
var x = 1
The outcome of the definition would be the same. A semicolon can be added to the end of the line of our variable definition but, similar to languages like JavaScript, it is not required:
var x = 1 // I am a variable identified by x and I hold a value of 1
var y = 2 // I am a variable identified by y and I hold a value of 2
var z: Int = x + y // I am a variable identified by z and I hold a value of 3
If we don't want the values of our variables to change over the course of the execution of our program, we can do so by making them immutable. Immutable variables are defined with the val keyword, as follows:
val x = 200
Variable scope
The scope of a variable is the region of a program where the variable is of consequence. In other words, the scope of a variable is the region of a program in which the variable can be used. Kotlin variables have block scope. Therefore, the variables can be utilized in all regions that the block they were defined in covers:
fun main(args: Array<String>) {
// block A begins
var a = 10
var i = 1
while (i < 10) {
// block B begins
val b = a / i
print(b)
i++
}
print(b) // Error occurs: variable b is out of scope
}
In the preceding program, we can directly observe the effects of block scope by taking a look at the two blocks. The definition of a function opened a new block. We have labeled to this block as B in our example. Within A, the a and i variables were declared. As such, the scope of the a and i variables exists within A.
A while loop was created within A, and as such, a new B block was opened. Loop declarations mark the beginning of new blocks. Within B, a b value is declared. The b value exists in the B scope and can't be used outside its scope. As such, when we attempt to print the value held by b outside the B block, an error will occur.
One thing worth noting is that the a and i variables can still be utilized within the B block. This is because B exists within the scope of A.
Local variables
These are variables that are local to a scope. The a, i, and b variables in our previous example are all local variables.
Operands and operators
An operator is the part of an instruction that specifies the value to be operated on. An operator also carries out a specific operation on its operands. Examples of operators are +, -, *, /, and %. Operators can be categorized based on the type of operations carried out and the number of operands acted upon by the operator.
Based on the type of operations carried out by the operator, we can classify operators into:
- Relational operators
- Assignment operators
- Logical operators
- Arithmetic operators
- Bitwise operators
Operator type |
Examples |
Relational operators |
>, <, >=, <=, == |
Assignment operators |
+=, -=, *=, /=, = |
Logical operators |
&&, ||, ! |
Arithmetic operators |
+, -, *, / |
Bitwise operators |
and(bits), or(bits), xor(bits), inv(), shl(bits), shr(bits), ushr(bits) |
Based on the number of operands acted upon, we have two main types of operators in Kotlin:
- Unary operators
- Binary operators
Operator type |
Description |
Examples |
Unary operator |
Requires only one operand |
!, ++, - - |
Binary operator |
Requires two operands |
+, -, *, /, %, &&, || |
Types
The type of a variable, with respect to its value space, is the set of possible values that the variable can possess. In many cases, it is useful to be able to explicitly specify the type of value you want to be held by a variable being declared. This can be done using a data type.
Some important types in Kotlin are:
- Int
- Float
- Double
- Boolean
- String
- Char
- Array
Int
This type represents a 32-bit signed integer. When a variable is declared with this type, the value space of the variable is the set of integers, that is, the variable can only hold integer values. We have seen the use of this type several times in our examples so far. The Int type can hold integer values within the range of -2,147,483,648 to 2,147,483,647.
Float
This type represents a single precision 32-bit floating-point number. When used with a variable, this type specifies that the variable can only hold floating-point values. Its range is approximately ±3.40282347E+38F (6-7 significant decimal digits):
var pi: Float = 3.142
Double
This type represents a double precision 64-bit floating-point number. Similar to the Float type, this type specifies that the variable being declared holds floating-point values. An important difference between the Double and Float types is that Double can hold numbers across a much larger range without overflow. Its range is approximately ±1.79769313486231570E+308 (15 significant decimal digits):
var d: Double = 3.142
Boolean
The true and false logical truth values are represented by the Boolean type:
var t: Boolean = true
var f: Boolean = false
Boolean values are operated upon by the &&, ||, and ! logical operators:
Operator name |
Operator |
Description |
Operator type |
Conjunction |
&& |
Evaluates to true when two of its operands are true, otherwise evaluates to false. |
Binary |
Disjunction |
|| |
Evaluates to true when at least one operand is true, otherwise evaluates to false. |
Binary |
Negation |
! |
Inverts the value of its Boolean operand. |
Unary |
String
A string is a sequence of characters. In Kotlin, strings are represented by the string class. Strings can be easily written by typing out a sequence of characters and surrounding it with double quotation marks:
val martinLutherQuote: String = "Free at last, Free at last, Thank God almighty we are free at last."
Char
This type is used to represent characters. A character is a unit of information that roughly corresponds to a grapheme, or a grapheme-like unit or symbol. In Kotlin, characters are of the Char type. Characters in single quotes in Kotlin, such as a, $, %, and &, are all examples of characters:
val c: Char = 'i' // I am a character
Recall we mentioned earlier that a string is a sequence of characters:
var c: Char
val sentence: String = "I am made up of characters."
for (character in sentence) {
c = character // Value of character assigned to c without error
println(c)
}
Array
An array is a data structure consisting of a set of elements or values with each element possessing at least one index or key. Arrays are very useful in storing collections of elements you wish to utilize later on in a program.
In Kotlin, arrays are created using the arrayOf() library method. The values you wish to store in the array are passed in a comma-separated sequence:
val names = arrayOf("Tobi", "Tonia", "Timi")
Each array value has a unique index that both specifies its position in the array and can be used to retrieve the value later on. The set of indices in an array starts with the index, 0, and progresses with an increment of 1.
The value held in any given index position of an array can be retrieved by either calling the Array#get() method or by using the [] operation:
val numbers = arrayOf(1, 2, 3, 4)
println(numbers[0]) // Prints 1
println(numbers.get(1)) // Prints 2
At any point in time, the value at a position of an array can be changed:
val numbers = arrayOf(1, 2, 3, 4)
println(numbers[0]) // Prints 1
numbers[0] = 23
println(numbers[0]) // Prints 23
You can check the size of an array at any time with its length property:
val numbers = arrayOf(1, 2, 3, 4)
println(numbers.length) // Prints 4
Functions
A function is a block of code that can be defined once and reused any number of times. When writing programs, it is best practice to break up complex programmatic processes into smaller units that perform specific tasks. Doing this has many advantages, some of which are:
- Improving code readability: It is much easier to read programs that have been broken down into functional units. This is because the scope of the code to be understood at any given point in time is reduced when functions are utilized. The majority of the time, a programmer needs to only write or adjust a section of a large code base. When functions are utilized, the context of the program that needs to be read to ameliorate program logic is restricted to the body of the function in which the logic is written.
- Improving the maintainability of a code base: The use of functions in a code base makes it easy to maintain programs. If a change needs to be made to a particular program feature, many times it is as easy as adjusting a function in which the feature has been created.
Declaring functions
Functions are declared with the fun keyword. The following is a simple function definition:
fun printSum(a: Int, b: Int) {
print(a + b)
}
The function simply prints the sum of two values that have been passed as arguments to it. Function definitions can be broken down into the following components:
- A function identifier: The identifier of a function is the name given to it. An identifier is required to refer to the function if we wish to invoke it later on in a program. In the preceding function declaration, printSum is the identifier of the function.
- A pair of parentheses containing a comma-separated list of the arguments being passed as values to the function: Values passed to a function are called arguments of the function. All arguments passed to the function must have a type. The type definition of an argument follows a semicolon placed after the argument name.
- A return type specification: Return types of functions are specified similarly to the way the types of variables and properties are. The return type specification follows the last parenthesis and is done by writing the type after a semicolon.
- A block containing the body of the function.
Observing the preceding function, it may appear that it has no return type. This is not true, the function has a return type of Unit. A unit return type need not be explicitly specified. The function might as well be declared as follows:
fun printSum(a: Int, b: Int): Unit {
print(a + b)
}
Invoking functions
Functions are not executed once they are defined. In order for the code within a function to be executed, the function must be invoked. Functions can be invoked as functions, as methods, and indirectly by utilizing the invoke() and call() methods. The following shows the direct functional invocation using the function itself:
fun repeat(word: String, times: Int) {
var i = 0
while (i < times) {
println(word)
i++
}
}
fun main(args: Array<String>) {
repeat("Hello!", 5)
}
Compile and run the preceding code. The word Hello is printed on the screen five times. Hello was passed as our first value in the function and 5 as our second. As a result of this, the word and times arguments are set to hold the Hello and 5 values in our repeat function. Our while loop runs and prints our word as long as i is less than the number of times specified. i++ is used to increase the value of i by 1. i is increased by one upon each iteration of the loop. The loop stops once i is equal to 5. Hence, our word Hello will be printed five times. Compiling and running the program will give us the following output:
The other methods of function invocation will be demonstrated over the course of this book.
Return values
A return value—as the name implies—is the value that a method returns. Functions in Kotlin can return values upon execution. The type of the value returned by a function is defined by the function's return type. This is demonstrated in the following code snippet:
fun returnFullName(firstName: String, surname: String): String {
return "${firstName} ${surname}"
}
fun main(args: Array<String>) {
val fullName: String = returnFullName("James", "Cameron")
println(fullName) // prints: James Cameron
}
In the preceding code, the returnFullName function takes two distinct strings as its input parameters and returns a string value when called. The return type has been defined in the function header. The string returned is created via string templates:
"${firstName} ${surname}"
The values for first name and last name are interpolated into the string of characters.
The function naming convention
The conventions for naming functions in Kotlin are similar to that of Java. When naming methods, camel case is utilized. In camel case, names are written such that each word in the name begins with a capital letter, with no intervening spaces or punctuation:
//Good function name
fun sayHello() {
println("Hello")
}
//Bad function name
fun say_hello() {
println("Hello")
}
Comments
When writing code, you may need to jot down important information pertaining to the code being written. This is done through the use of comments. There are three types of comments in Kotlin:
- Single-line comments
- Multiline comments
- Doc comments
Single-line comments
As the name implies, these comments span a single line. Single-line comments are started with two backslashes (//). Upon compilation of your program, all characters coming after these slashes are ignored. Consider the following code:
val b: Int = 957 // This is a single line comment
// println(b)
The value held by b is never printed to the console because the function that performs the printing operation has been commented out.
Multiline comments
Multiline comments span multiple lines. They are started with a backslash followed by an asterisk (/*) and ended by an asterisk followed by a backslash (*/):
/*
* I am a multiline comment.
* Everything within me is commented out.
*/
Doc comments
This type of comment is similar to a multiline comment. The major difference is that it is used to document code within a program. A doc comment starts with a backslash followed by two asterisk characters (/**) and ends with an asterisk followed by a backslash (*/):
/**
* Adds an [item] to the queue.
* @return the new size of the queue.
*/
fun enqueue(item: Object): Int { ... }
Controlling program flow
When writing programs, a scenario that often occurs is one in which we want to control how our program executes. This is necessary if we want to write programs that can make decisions based on conditions and program state. Kotlin possesses a number of structures for doing this, which will be familiar to people who have worked with programming languages in the past, such as if, while, and for constructs. There are also others that may not be familiar to individuals, such as the when construct. In this section, we will take a look at the structures at our disposal for controlling the flow of our program.
Conditional expressions
Conditional expressions are used for branching program flow. They execute or skip program statements based on the outcome of a conditional test. Conditional statements are the decision-making points of a program.
Kotlin has two main structures for handling branching. These are if expressions and when expressions.
The if expression
The if expression is used to make a logical decision based on the fulfillment of a condition. We make use of the if keyword to write if expressions:
val a = 1
if (a == 1) {
print("a is one")
}
The preceding if expression tests whether the a == 1 (read: a is equal to 1) condition holds true. If the condition is true, the a is one string is printed on the screen, otherwise nothing is printed.
An if expressions often has one or more accompanying else or else if keywords. These accompanying keywords can be used to further control the flow of a program. Take the following if expression for example:
val a = 4
if (a == 1) {
print("a is equal to one.")
} else if (a == 2) {
print("a is equal to two.")
} else {
print("a is neither one nor two.")
}
The preceding expression first tests whether a is equal to 1. This test evaluates to false so, the following condition has been tested. Surely a is not equal to 2. Hence the second condition evaluates to false. As a result of all previous conditions evaluating to false, the final statement is executed. Hence a is neither one nor two. is printed on the screen.
The when expression
The when expression is another means of controlling program flow. Let's observe how it works with a simple example:
fun printEvenNumbers(numbers: Array<Int>) {
numbers.forEach {
when (it % 2) {
0 -> println(it)
}
}
}
fun main (args: Array<String>) {
val numberList: Array<Int> = arrayOf(1, 2, 3, 4, 5, 6)
printEvenNumbers(numberList)
}
The preceding printEvenSum function takes an integer array as its only argument. We will cover arrays later on in this chapter, but for now think of them as a sequential collection of values existing in a value space. In this case, the array passed contains values that exist in the value space of integers. Each element of the array is iterated upon using the forEach method and each number is tested in the when expression.
Here, the it refers to the current value being iterated upon by the forEach method. The % operator is a binary operator that acts on two operands. It divides the first operand by the second and returns the remainder of the division. Thus, the when expression tests if/when the current value iterated upon (the value held within it) is divided by 2 and has a remainder of 0. If it does, the value is even and hence the value is printed.
To observe how the program works, copy and paste the preceding code into a file, then compile and run the program:
The Elvis operator
The Elvis operator is a terse structure that is present in Kotlin. It takes the following form:
(expression) ?: value2
Its usage in a Kotlin program is demonstrated in the following code block:
val nullName: String? = null
val firstName = nullName ?: "John"
If the value held by nullName is not null, the Elvis operator returns it, otherwise the "John" string is returned. Thus, firstName is assigned the value returned by the Elvis operator.
Loops
Looping statements are used to ensure that a collection of statements within a block of code repeats in execution. That is, a loop ensures that a number of statements within a program executes for a number of times. The looping constructs provided by Kotlin are the for loop, the while loop, and the do…while loop.
The for loops
The for loop in Kotlin iterates over any object that provides an iterator. It is similar to the for..in loop in Ruby. The loop has this syntax:
for (obj in collection) { … }
The block in the for loop is not necessary if only a single statement exists in the loop. A collection is a type of structure that provides an iterator. Consider the following program:
val numSet = arrayOf(1, 563, 23)
for (number in numSet) {
println(number)
}
Each value in the numSet array is iterated upon by the loop and assigned to the variable number. The number is then printed to the standard system output.
If instead of printing the numeric values of the number iterated upon, we wish to print the indices of each number, we can do that as follows:
for (index in numSet.indices) {
println(index)
}
You can specify a type for your iterator variable as well:
for (number: Int in numSet) {
println(number)
}
The while loops
A while loop executes instructions within a block as long as a specified condition is met. The while loops are created using the while keyword. It takes the following form:
while (condition) { … }
As in the case of the for loop, the block is optional in the case where only one sentence is within the scope of the loop. In a while loop, the statements in the block execute repeatedly while the condition specified still holds. Consider the following code:
val names = arrayOf("Jeffrey", "William", "Golding", "Segun", "Bob")
var i = 0
while (!names[i].equals("Segun")) {
println("I am not Segun.")
i++
}
In the preceding program, the block of code within the while loop executes and prints I am not Segun until the name Segun is encountered. Once Segun is encountered, the loop terminates and nothing else is printed out, as shown in the following screenshot:
The break and continue keywords
Often when declaring loops, there is a need to either break out of the loop if a condition fulfills, or start the next iteration at any point in time within the loop. This can be done with the break and continue keywords. Let's take an example to explain this further. Open a new Kotlin script file and copy the following code into it:
data class Student(val name: String, val age: Int, val school: String)
val prospectiveStudents: ArrayList<Student> = ArrayList()
val admittedStudents: ArrayList<Student> = ArrayList()
prospectiveStudents.add(Student("Daniel Martinez", 12, "Hogwarts"))
prospectiveStudents.add(Student("Jane Systrom", 22, "Harvard"))
prospectiveStudents.add(Student("Matthew Johnson", 22, "University of Maryland"))
prospectiveStudents.add(Student("Jide Sowade", 18, "University of Ibadan"))
prospectiveStudents.add(Student("Tom Hanks", 25, "Howard University"))
for (student in prospectiveStudents) {
if (student.age < 16) {
continue
}
admittedStudents.add(student)
if (admittedStudents.size >= 3) {
break
}
}
println(admittedStudents)
The preceding program is simplistic software for selecting admitted students out of a list of prospective students. We create a data class at the start of our program to model the data of each student, then two array lists are created. One array list holds the information of the prospective students, those that have applied for admission, and the other list holds the information of the students that have been admitted.
The next five lines of code add prospective students to the prospective student list. We then declare a loop that iterates over all students present in the prospective student list. If the age of the current student in the loop is less than 16 years old, the loop skips to the next iteration. This models the scenario in where a student is too young to be admitted (thus not added to the admitted students list).
If the student is 16 or older, the student is added to the admitted list. An if expression is then used to check whether the number of admitted students is greater than or equal to three. If the condition is true, the program breaks out of the loop and no further iterations are done. The last line of the program prints out the students present in the list.
Run the program to see the output:
The do...while loops
The do…while loop is similar to the while loop, with the exception that in the conditional test for the reiteration of the loop, it is carried out after the first iteration. It takes the following form:
do {
...
} while (condition)
The statements within the block are executed while the condition tested holds true:
var i = 0
do {
println("I’m in here!")
i++
} while (i < 10)
println("I’m out here!")
Nullable values: The NullPointerException is one thing that individuals who have first-hand experience writing Java code are certain to have encountered. The Kotlin type system is null-safe—it attempts to eliminate the occurrence of null references within code. As a result, Kotlin possesses nullable types and non-nullable types (types that can hold a null value and those that can't).
To properly explain the NullPointerException, we will consider the following Java program:
class NullPointerExample {
public static void main(String[] args) {
String name = "James Gates";
System.out.println(name.length()); // Prints 11
name = null; // assigning a value of null to name
System.out.println(name.length()); // throws NullPointerException
}
}
The preceding program performs the simple task of printing the length of a string variable to the standard system output. There is only one problem with our program. When we compile and run it, it throws a null pointer exception and terminates midway through execution, as we can see the following screenshot:
Can you spot the cause of the NullPointerException? The exception arises as a result of the String#length method being used on a null reference. As such, the program stops executing and throws the exception. Clearly, this is not something we want to occur in our programs.
We can prevent this in Kotlin by preventing the assignment of a null value to the name object:
var name: String = "James Gates"
println(name.length)
name = null // null value assignment not permitted
println(name.length)
As can be seen in the following screenshot, Kotlin's type system detects that a null value has been inappropriately assigned to the name variable and swiftly alerts the programmer of this blunder so it can be corrected:
At this point, you may be wondering what happens if a scenario arises in which the programmer intends to permit the passing of null values. In that scenario, the programmer simply declares the value as nullable by appending ? to the type of the variable:
var name: String? = "James"
println(name.length)
name = null // null value assignment permitted
println(name.length)
Regardless of the fact that we have declared the variable name to be nullable, we'll still get an error upon running the program. This is because we must access the length property of the variable in a safe way. This can be done with ?.:
var name: String? = "James"
println(name?.length)
name = null // null value assignment permitted
println(name?.length)
Now that we have used the ?. safe operator, the program will run as intended. Instead of a null pointer exception being thrown, the type system recognizes that a null pointer has been referenced and prevents the invocation of length() on the null object. The following screenshot shows the type-safe output:
An alternative to using the ?. safe operator would be to use the !! operator. The !! operator allows the program to continue execution and throws a KotlinNullPointerException once a function invocation is attempted on a null reference.
We can see the effects by replacing ?. with !!. in our written program. The following screenshot shows the output of the program when run. A KotlinNullPointerException is thrown as a result of the utilization of the !! operator:
Packages
A package is a logical grouping of related classes, interfaces, enumerations, annotations, and functions. As source files grow larger, it is necessary to group these files into meaningful and distinct collections for various reasons, such as to enhance the maintainability of applications, name conflict prevention, and access control.
A package is created with the package keyword followed by a package name:
package foo
There can be only one package statement per program file. If a package for a program file is not specified, the contents of the file are placed into the default package.
The import keyword
Often, classes and types need to make use of other classes and types existing outside the package in which they are declared. This can be done by importing package resources. If two classes belong in the same package, no import is necessary:
package animals
data class Buffalo(val mass: Int, val maxSpeed: Int, var isDead: Boolean = false)
In the following code snippet, the Buffalo class does not need to be imported into the program because it exists in the same package (animals) as the Lion class:
package animals
class Lion(val mass: Int, val maxSpeed: Int) {
fun kill(animal: Buffalo) { // Buffalo type used with our import
if (!animal.isDead) {
println("Lion attacking animal.")
animal.isDead = true
println("Lion kill successful.")
}
}
}
In order to import classes, functions, interfaces, and types in separate packages, we use the import keyword followed by the package name. For example, the following main function exists in the default package. As such, if we want to make use of the Lion and Buffalo classes in the main function, we must import it with the import keyword. Consider the following code:
import animals.Buffalo
import animals.Lion
fun main(args: Array<String>) {
val lion = Lion(190, 80)
val buffalo = Buffalo(620, 60)
println("Buffalo is dead: ${buffalo.isDead}")
lion.kill(buffalo)
println("Buffalo is dead: ${buffalo.isDead}")
}
Object-oriented programming concepts
Up till now, we have made used of classes in a number of examples but have not explored the concept in depth. This section will introduce you to the basics of classes as well as other object-oriented constructs available in Kotlin.
Introduction
In the beginning of high-level programming languages, programs were written procedurally. The programming languages available were mainly procedural in nature. A procedural programming language is a language that utilizes a series of structured, well-defined steps to compose programs.
As the software industry grew larger and programs grew bulkier, it became necessary to devise a better approach to designing software. This led to the advent of object-oriented programming languages.
The object-oriented programming model is a model organized around objects and data rather than actions and sequential logic. In object-oriented programming, objects, classes, and interfaces are composed, extended, and inherited toward the goal of building industrial-strength software.
A class is a modifiable and extensible program template for the creation of objects and the maintenance of state through the use of variables, constants, and properties. A class possesses characteristics and behaviors. Characteristics are exposed as variables and behaviors are implemented in the form of methods. Methods are functions that are specific to a class or a collection of classes. Classes have the ability to inherit characteristics and behaviors from other classes. This ability is called inheritance.
Kotlin is a fully object-oriented programming language and hence supports all features of object-oriented programming. In Kotlin, similar to Java and Ruby, only single inheritance is permitted. Some languages, such as C++, support multiple inheritance. A downside to multiple inheritance is that it brings up management issues, such as same name collisions. A class inheriting from another class is referred to as a subclass and the class it inherits from is its superclass.
An interface is a structure that enforces certain characteristics and behaviors in classes. Behavioral enforcements via interfaces can be done by implementing an interface in a class. Similar to classes, an interface can extend another interface.
An object is an instance of a class that may possesses its own unique state.
Working with classes
A class is declared using the class keyword followed by the class name:
class Person
As in the preceding example, a class in Kotlin need not have a body. Though this is charming, almost all the time you will want your class to have characteristics and behaviors placed within a body. This can be done with the use of opening and closing brackets:
class HelloPrinter {
fun printHello() {
println("Hello!")
}
}
In the preceding code snippet, we have a class named HelloPrinter with a single function declared in it. A function that is declared within a class is called a method. Methods can also be referred to as behaviors. Once a method is declared, it can be used by all instances of the class.
Creating objects
Declaring an instance of a class—or an object—is similar to declaring a variable. We can create an instance of the HelloPrinter class, as shown in the following code:
val printer = HelloPrinter()
The preceding printer is an instance of the HelloPrinter class. The opening and closing brackets after the HelloPrinter class name are used to invoke the primary constructor of the HelloPrinter class. A constructor is similar to a function. A constructor is a special function that initializes an object of a type.
The function declared within the HelloPrinter class can be invoked directly by the printer object at any point in time:
printer.printHello() // Prints hello
Occasionally, you may require a function to be directly invokable with the class without needing to create an object. This can be done with the use of a companion object.
Companion objects
Companion objects are declared within a class by utilizing the companion and object keywords. You can use functions that are static within a companion object:
class Printer {
companion object DocumentPrinter {
fun printDocument() = println("Document printing successful.")
}
}
fun main(args: Array<String>) {
Printer.printDocument() // printDocument() invoked via companion object
Printer.Companion.printDocument() // also invokes printDocument() via
// a companion object
}
Sometimes, you may want to give an identifier to a companion object. This can be done by placing the name after the object keyword. Consider the following example:
class Printer {
companion object DocumentPrinter { // Companion object identified by DocumentPrinter
fun printDocument() = println("Document printing successful.")
}
}
fun main(args: Array<String>) {
Printer.DocumentPrinter.printDocument() // printDocument() invoked via
// a named companion object
}
Properties
Classes can have properties that may be declared using the var and val keywords. For example, in the following code snippet, the Person class has three properties, age, firstName, and surname:
class Person { var age = 0
var firstName = ""
var surname = ""
}
Properties can be accessed through an instance of the class holding the property. This is done by appending a single dot character (.) to an instance identifier followed by the property name. For example, in the following code snippet, an instance—named person—of the Person class is created and its firstName, surname, and age properties are assigned by accessing the properties:
val person = Person()
person.firstName = "Raven"
person.surname = "Spacey"
person.age = 35