Classes with methods |
Objects are the basis of object-oriented programming. In Scala, every piece of data is an object. The type of the object determines what you can do with the object. Classes can contain data (the state of the object) and methods (what you can do with the object). You can think about a class as a blueprint for objects. Once you define a class, you can create objects from the blueprint using the keyword new.
Consider an object as an atomic unit. Clients (that's the term for program code that uses the object) do not care about the implementation of the object, they only use the exposed methods and fields. "Exposed" means here that they are made available by the class for use by clients.
A date consists of three attributes: year, month, and day, all of them integers. We could thus store a date as a triple Triple<Int, Int, Int>, but then we may be confused if they indicate year-month-day, or day-month-year. It's better to create a data class with three attributes:
data class Date(val year: Int, val month: Int, val day: Int)
Both methods, however, do not guarantee that our date objects will be consistent. Consistency means that the attributes take on only legal, meaningful values, and that the values of each attribute are consistent with each other. For instance, a day value of 31 is consistent with a month value of 3, but not with a month value of 4. A day value of 29 and a month value of 2 are consistent only if the year value indicates a leap year. With the data class defined above, there is nothing stopping us from making mistakes like this:
>>> data class Date(val year: Int, val month: Int, val day: Int) >>> val d = Date(2017, 13, -5) >>> d Date(year=2017, month=13, day=-5)
To ensure that our Date objects are always consistent, we can add four require statements in the constructor of the object. It is indicated by the keyword init, followed by statements that are executed every time an object of this type is created (days1.kt):
val monthLength = intArrayOf(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) data class Date(val year: Int, val month: Int, val day: Int) { init { require(1901 <= year && year <= 2099) require(1 <= month && month <= 12) require(1 <= day && day <= monthLength[month - 1]) require(month != 2 || day <= 28 || (year % 4) == 0) } }
If the values are okay, nothing happens. Otherwise require throws an exception, and we know immediately that something is wrong. Since the Date object is immutable, the consistent state that is guaranteed when the object is constructed can never be broken and made inconsistent.
It is helpful if objects guarantee that their state is consistent. It makes it possible for functions working with the objects to proceed without error checking, and simplifies debugging since we will notice quickly when something went wrong.
To use my new Date object, I have to compile the definition, and can then test it interactively:
>>> val d1 = Date(2017, 3, 17) >>> d1 Date(year=2017, month=3, day=17) >>> d1.year 2017 >>> d1.day 17 >>> var d2 = Date(2017, 2, 29) java.lang.IllegalArgumentException: Failed requirement at Date.<init>(days1.kt:24)
In the past, we had functions that took arguments of a particular type, such as a function date2num that converted a date, represented as year, month, and day, into a day index starting on January 1, 1901.
In object-oriented programming, we prefer to define functions that work on a specific type as a method of that type. One advantage is that it clearly documents the available functions for a given type (for instance, to find out what you can do with a String, you would look at the list of methods of the String class). The main advantage, however, will be the possibility of hiding or protecting information inside the object, as we will see later.
For the moment, let us add methods to convert a Date object into a day index, and to return the day of the week of a Date (days3.kt):
val monthLength = intArrayOf(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) val weekday = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") data class Date(val year: Int, val month: Int, val day: Int) { init { require(1901 <= year && year <= 2099) require(1 <= month && month <= 12) require(1 <= day && day <= monthLength[month - 1]) require(month != 2 || day <= 28 || (year % 4) == 0) } // returns the number of days since 1901/01/01 (day 0) fun dayIndex(): Int { val fourY = (365 + 365 + 365 + 366) val yearn = year - 1901 var total = 0 total += fourY * (yearn / 4) total += 365 * (yearn % 4) for (m in 0 until month - 1) total += monthLength[m] total += day - 1 if (year%4 != 0 && month > 2) total -= 1 return total } fun dayOfWeek(): String = weekday[(dayIndex() + 1) % 7] }
A method looks like a function definition, but is placed inside the body of the class. The body of the method can refer to the names of the object's fields: Note the use of the fields year, month, and day in the method dayIndex.
A method can only be called if we have a reference to an object of that type available, and so whenever a method is executed, there is always a "current" object. The field names refer to the fields of this "current" object:
$ ktc days3.kt $ ktc Welcome to Kotlin version 1.0.1-2 (JRE 1.8.0_74-b02) Type :help for help, :quit for quit >>> var d1 = Date(2017, 4, 16) >>> var d2 = Date(2000, 1, 1) >>> d1.year 2017 >>> d2.month 1 >>> d1.dayIndex() 42474 >>> d2.dayIndex() 36159 >>> d1.dayOfWeek() Sunday >>> d2.dayOfWeek() Saturday
Every object in Kotlin or Java can be converted to a string, and that is what happens when you look at an object in the interactive mode, or using println: the object is first converted to a string using its toString method:
>>> d1 Date(year=2017, month=4, day=16) >>> d1.toString() Date(year=2017, month=4, day=16) >>> println(d1) Date(year=2017, month=4, day=16)Since we defined Date as a data class, the compiler is providing a reasonable toString method that shows the name of the class and the value of each field.
In production quality code, we may want a nice representation of dates. We can achieve this by overriding the default definition of the toString method, like this (days4.kt):
override fun toString(): String = "%s, %s %d, %d".format(dayOfWeek(), monthname[month-1], day, year)Note the keyword override. It is necessary because every object already has a toString method. So we are not adding a new method, we are overriding the previous definition.
With this method, our object looks much prettier:
$ ktc days4.kt $ ktc Welcome to Kotlin version 1.0.1-2 (JRE 1.8.0_74-b02) Type :help for help, :quit for quit >>> val d1 = Date(2017, 4, 16) >>> d1 Sunday, April 16, 2017 >>> d1.toString() Sunday, April 16, 2017 >>> println(d1) Sunday, April 16, 2017
Let's add a method to compute the number of days between two dates. Since we already have dayIndex(), this is very easy to implement (days5.kt):
fun diff(rhs: Date): Int = dayIndex() - rhs.dayIndex()
And it works well:
$ ktc days5.kt $ ktc Welcome to Kotlin version 1.0.1-2 (JRE 1.8.0_74-b02) Type :help for help, :quit for quit >>> var d1 = Date(1993, 7, 9) >>> var d2 = Date(2017, 4, 9) >>> d2.diff(d1) 8675
But wouldn't it be nice if we could write d2 - d1 for the difference between two dates instead of the not-so-pretty d2.diff(d1)?
In Kotlin, unlike in Java, this is possible. In fact, in Kotlin, there is no difference at all between operators like + and methods like take—they are all implemented as methods.
To define the - operator, all we have to do is to rename our diff method into minus, and to add the keyword operator (days6.kt):
operator fun minus(rhs: Date): Int = dayIndex() - rhs.dayIndex()We can now calculate with dates:
$ ktc days6.kt $ ktc Welcome to Kotlin version 1.0.1-2 (JRE 1.8.0_74-b02) Type :help for help, :quit for quit >>> val birth = Date(1993, 7, 9) >>> val today = Date(2017, 4, 17) >>> today - birth 8683
It can be fun to define your own operators, but don't go overboard—it is only useful when it makes the code more readable!
Kotlin, like Java and C++ but unlike C, allows the overloading of method names: It is allowed to have different methods that have the same name, and only differ in the type of arguments they accept. Here is a simple example (overloading.kts):
fun f(n: Int) { println("Int " + n) } fun f(s: String) { println("String " + s) } f(17) f("CS109")The compiler correctly determines that f(17) is a call to the first function, while f("CS109") is a call to the second function.
We can make use of overloading to add more operators to our Date class. We will allow adding or subtracting a number of days to a date to obtain a new date (days7.kt):
val monthLength = intArrayOf(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) val weekday = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") val monthname = arrayOf("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December") data class Date(val year: Int, val month: Int, val day: Int) { init { require(1901 <= year && year <= 2099) require(1 <= month && month <= 12) require(1 <= day && day <= monthLength[month - 1]) require(month != 2 || day <= 28 || (year % 4) == 0) } // returns the number of days since 1901/01/01 (day 0) fun dayIndex(): Int { val fourY = (365 + 365 + 365 + 366) val yearn = year - 1901 var total = 0 total += fourY * (yearn / 4) total += 365 * (yearn % 4) for (m in 0 until month - 1) total += monthLength[m] total += day - 1 if (year%4 != 0 && month > 2) total -= 1 return total } fun num2date(n: Int): Date { val fourY = (365 + 365 + 365 + 366) var year = 1901 + (n / fourY) * 4 var day = n % fourY if (day >= 365 + 365 + 365 + 59) { year += 3 day -= 365 + 365 + 365 } else { year += (day / 365) day = day % 365 if (day >= 59) day += 1 } var month = 1 while (day >= monthLength[month-1]) { day -= monthLength[month-1] month += 1 } return Date(year, month, day+1) } fun dayOfWeek(): String = weekday[(dayIndex() + 1) % 7] override fun toString(): String = "%s, %s %d, %d".format(dayOfWeek(), monthname[month-1], day, year) operator fun minus(rhs: Date): Int = dayIndex() - rhs.dayIndex() operator fun plus(n: Int): Date = num2date(dayIndex() + n) operator fun minus(n: Int): Date = num2date(dayIndex() - n) }
Note that there are two minus operators defined for the Date class. The compiler correctly selects the one we need depending on the type of the right-hand side:
$ ktc days7.kt $ ktc Welcome to Kotlin version 1.0.1-2 (JRE 1.8.0_74-b02) Type :help for help, :quit for quit >>> val birth = Date(1992, 8, 21) >>> val baekil = birth + 100 >>> baekil Sunday, November 29, 1992 >>> val today = Date(2017, 4, 19) >>> today - birth 9007 >>> today - 9007 Friday, August 21, 1992 >>> birth + 9007 Wednesday, April 19, 2017
Since we now have a nice date class, let's write a program to use it (days.kt):
val digits = "0123456789" // if s is not a legal date, or is not in range, // then throws IllegalArgumentException fun getDate(s: String): Date { if (s.length != 10 || s[4] != '/' || s[7] != '/') throw IllegalArgumentException() for ((i, ch) in s.withIndex()) { if (i != 4 && i != 7 && ch !in digits) throw IllegalArgumentException() } val year = s.substring(0, 4).toInt() val month = s.substring(5, 7).toInt() val day = s.substring(8).toInt() return Date(year, month, day) } fun main(args: Array<String>) { try { if (args.size == 1) { val d = getDate(args[0]) println("$d is a ${d.dayOfWeek()}") } else if (args.size == 2) { val d1 = getDate(args[0]) val d2 = getDate(args[1]) println("There are ${d2 - d1} days between $d1 and $d2") } else if (args.size == 3) { val d1 = getDate(args[0]) val sign = if (args[1] == "-") -1 else +1 val dist = args[2].toInt() val d2 = d1 + sign * dist println("$d1 ${args[1]} $dist days = $d2") } else { System.err.println("Must have one, two, or three arguments") } } catch (e: NumberFormatException) { System.err.println("Illegal number") } catch (e: IllegalArgumentException) { System.err.println("Illegal date") } }
As explained in the previous section, the program starts through the main function, which receives the command line arguments in the form of parameter args.
We compile the program:
$ ktc days.ktThe compiler produces a class DaysKt for the source file containing the main function, so this is the class we need to call to run the program:
$ kt DaysKt 2015/a3/04 Illegal date $ kt DaysKt 2016/04/26 Tuesday, April 26, 2016 is a Tuesday $ kt DaysKt 2016/04/26 2017/01/01 There are 250 days between Tuesday, April 26, 2016 and Sunday, January 1, 2017 $ kt DaysKt 2016/04/26 + 250 Tuesday, April 26, 2016 + 250 days = Sunday, January 1, 2017 $ kt DaysKt 2016/04/26 - 100 Tuesday, April 26, 2016 - 100 days = Sunday, January 17, 2016
Classes with methods |