Kotlin Classes - mariamaged/Java-Android-Kotlin GitHub Wiki
Kotlin - Basic Classes
Basic Classes
Classes in Kotlin are very similar to Java and Ruby counterparts:
- They primarily contain:
- Properties.
- Functions that operate on those properties.
- A class does not have to contain any of that.
- This is a valid Kotlin class definition.
class Foo {
}
- So is this.
class Foo
- Having a class with no body seems odd.
- However, in Kotlin, you will occasionally see that sort of thing, particularly with
data classesandsealed classes.
Creating Instances of Classes
- Java and Ruby both use
newto create instances of classes, whether as a keyword:
Java
Foo foo = new Foo();
Ruby
foo = Foo.new
- JavaScript has a few flavors of creating objects, including one using the
newkeyword, much as you would with Java. - In Kotlin, though, you treat the class name as a function name, and just call it:
class Foo
val foo = Foo()
Packages
- Kotlin makes use of packages the same way that Java does:
- As a namespace for classes.
- And, as with Java,
by default a class is not in a package.- Most Kotlin files are in an actual project - as opposed to little REPL experiments - will be in a package.
- What differs, compared to Java, is just the lack of the semicolon that the equivalent Java
packagestatement would require.
Common Contents
Functions
- Declaring an ordinary function is a matter of having the fun be inside the body of a class:
class Foo {
fun something() {
println("Hello, world!")
}
}
- Calling an object's function works pretty much like Java, by using a dot (.) notation.
class Foo {
fun something() {
println("Hello, world!")
}
}
val foo = Foo()
foo.something()
Properties
- Just as classes can have functions, they can have properties:
class Foo {
var count = 0;
fun something() {
count += 1
println("something() was called $count times")
}
}
val foo = Foo()
foo.something()
foo.something()
foo.something()
- Here, we have a
countproperty, initially set to 0, that we increment on each call tosomething(). - That
countis then used in the printed output, courtesy of a bit of string interpolation.
something() was called 1 times
something() was called 2 times
something() was called 3 times
this
- As with Java, Kotlin uses as a
pseudo propertyto refer to the instance of the object in whose function you happen to be running. - You can use that as a prefix for:
- Property references.
- Or function calls.
- Such as
this.count.
class Foo {
var count = 0
fun something() {
this.count += 1
println("something() was called $count times")
}
}
val foo = Foo()
foo.something()
foo.something()
foo.something()
- Or you could use it on its own to refer to the current instance, such as for supplying that object as a parameter to a function:
class Foo {
var count = 0
fun printMe() {
println(this)
}
}
val foo = Foo()
foo.printMe()
Constructors
- As with Java and Ruby, a Kotlin class does not necessarily need an explicitly-declared constructor.
- By default, you get a zero-parameter constructor, as Foo did in the preceding example.
- Many of your Kotlin classes will not need any custom constructors...but a few will need one, and on occasion they might need more than one.
What You Normally See
- In Java and Ruby, declaring constructors is a matter of having a specific method (or thing that looks like a method) declared on a the class.
- In Java, that
pseudo-methodhas thename of the class.
Java
class Foo {
final private int count;
Foo(int count) {
this.count = count;
}
}
Ruby
- We use the
initializemethod.
class Foo
def initialize(count)
@count = count
end
end
JavaScript
- Similarly, JavaScript uses a function named after the class, at least in some approaches of implementing objects in JavaScript.
Kotlin
- The way you will normally see Kotlin classes have a constructor is by making the actual
class nameitself take theconstructor parameters. - Here, we have one constructor parameter, count.
- The var keyword in front of it means that we can use it like a var property, in this case changing the value via the increment (+=) operator.
class Foo(var count: Int) {
fun something() {
count += 1
println("something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
something was called 1 times
something was called 2 times
something was called 3 times
The Formal Approach
- The constructor syntax shown above is really a shorthand for using the constructor keyword.
- For most classes, with zero or one constructors,
you will not seethe constructor keyword used. - It becomes more important when you have
multiple constructors, includingprivate constructors.
class Foo constructor(var count: Int) {
fun something() {
count += 1
println("something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
Init Blocks
Compared with Java and Ruby constructors, though, our Kotlin constructors are missing something: a body.
All we are doing is declaring a list of parameters that can be supplied to the instance of the class.
- Kotlin instead uses an init block.
- Syntax-wise, an init block resembles a Java
static block: a set of braces-wrapped statements preceded by a keyword.
- In
Java, thestatic blockis executed once for the class and is used to initialize complex static fields. - In
Kotlin, an init block is executed once for each instance and serves as a baseline constructor body.
- Here, inexplicably, we are passing in a String representation of an initial value to use for the count, rather than passing in the
var countas we did before. - The init block is used to initialize count from sillyCount as part of setting up our instance of instance of Foo.
class Foo(val sillyCount: String) {
var count = 0
init {
count = sillyCount.toInt()
}
fun something() {
count += 1
println("something() was called $count times")
}
}
val foo = Foo("7")
foo.something()
foo.something()
foo.something()
Parameters Sans var Or val
Sometimes, you will see constructor parameters that do not have the
varorvalkeyword.
- They just have the parameter name and type.
- They are still constructor parameters, but they are more limited in scope.
- You cannot refer to them from the functions of your class.
- You can refer to them from init blocks, though.
class Foo(sillyCount: String) {
var count = 0
init {
count = sillyCount.toInt()
}
fun something() {
count += 1
println("something was called $count times")
}
}
val foo = Foo("7")
foo.something()
foo.something()
foo.something()
Having Multiple Constructors
Some Kotlin classes have no constructor, using only the default zero-parameters constructor.
Some Kotlin have one constructor.
And a handful of classes will have more than one constructor.
- The
primary constructorappears in theclass declarationitself, as we have seen. - Other constructors, if they exist, are called
"secondary constructors", and they somewhat resembleordinary functions.
class Foo(var count: Int) {
constructor(sillyCount: String): this(sillyCount.toInt()) {
// could have code here if needed
}
fun something() {
count += 1
println("something() was called $count times")
}
}
val foo = Foo("7")
foo.something()
foo.something()
foo.something()
- A
primary constructorhasno function body- any such code goes to init block. Secondary constructors, though,can have bodies, the way that functions do, but only if needed.
Secondary constructorsMUST arrange to call the primary constructor.- This is handled by having a this() call after the constructor parameter list, separated from the list via a colon.
- The this() call needs to match the parameter list of the primary constructor.
- In the example shown above, the secondary constructor needs to provide an
Intto the primary constructor.
Default Parameter Values
Having multiple constructors is comparatively common in a language like Java, as through Java 8, there was no support for
default parameter values.
- Hence, if you wanted to allow for both simple and complex object instantiation:
- You needed to declare all of the necessary constructors.
- Perhaps chaining from one other.
- For example,
- Android Developers who have created
customViewclasses might be familiar with thefourViewconstructors. - Here, each of the constructors simply adds on another possible parameter to be supplied.
- And the constructors chain to other constructors to
eliminate duplication of initialization logic.
- Android Developers who have created
public View(Context context) {
// A ton of code.
}
public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0)
}
public View(Context context, @Nullable AttributeSet attra, int defStyleAttr, int defStyleRes) {
this(context);
// Another ton of code.
}
- Kotlin, like Ruby, dispenses with this, by allowing you to define
default valuesfor your constructor parameters...just as you can on ordinary functions.
class View (
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) {
init {
// Tons of code.
}
}
- Here, the latter three parameters all have default values defined, giving us the ability to call the constructor:
- With one parameter.
- All four parameters.
- Combinations in between.
- The above code snippet contains newlines in between the parameters .
- This is merely a matter of
code formatting. - Typical Kotlin will split long parameter lists up this way,
with one parameter per line.
Inheritence
As with many object-oriented classes, Kotlin supports class inheritance.
A subclass inherits from a superclass, and what it "inherits" from are all of the:
- Properties.
- Functions.
- Constructors.
What Happens By Default
- The
default base classfor a Kotlin class is Any.
Extending a Class
Java
- We use the
extendskeyword.
class Foo extends Bar{
}
Ruby
- We use the
<symbol.
class Foo < Bar
end
Kotlin
- We use the
:symbol.
class Foo(var count: Int) : Any() {
fun something() {
count += 1
println("something() was printed $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
Chaining to the Superclass Constructors
- The () at the end of Any() is actually a
callto thesuperclass constructor. - You will need to supply parameters to that call, if the superclass requires them.
- Base has a constructor requiring a tag parameter.
- Foo needs to supply that parameter when it calls the Base() constructor.
- Bu then Foo can reference the tag property that it
inherits, including it as part of the printed output.
open class Base(val tag: String)
class Foo(var count: Int) : Base("Foo example") {
fun something() {
count += 1
println("$tag: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
Open classes
You will notice that the declaration of Base has something beyond a constructor parameter: it is
prefixed with the open keyword.
- In Java, by default, any class can be extended by a subclass.
- To prevent a class from being extended, you have to mark it as
final.
- Kotlin goes the opposite route.
- Kotlin classes cannot be extended by default.
- You have to mark classes that can be extended using
open. - Failure to do that results in an error.
So, if we remove the open from the previous example.
class Base(val tag: String)
class Foo(var count: int) : Base("Foo Example") {
fun something() {
count += 1
println("something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
We get a compilation error.
This has an important side effect:
- You cannot create subclasses of arbitrary classes that you do not control.
- Only if the implementer of that other class is willing to support subclasses will the implementer use the open keyword, and only those classes you can extend.
- This approach is uncommon in major programming languages.
- Basically, Kotlin recognizes that inheritance is a
potentially fragile relationshipbetween two classes. - Ideally, the classes adhere to the Liskov substitution principle.
- This basically states that instances of a
subclassshould be usable everywhere that instances of thesuperclasscan be used.
- This basically states that instances of a
Overriding Functions
As you might expect, a subclass can call functions that it inherits from its superclass:
open class Base(val tag: String) {
fun gimmeTheTag() = tag
}
class Foo(var count: int) : Base("Foo Example") {
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
- And, as you might expect, a subclass can override functions of its superclasses.
- At least, if those too are marked as open.
By default, a Kotlin function is- Not open.
- Cannot be overriden.
- Not only must the superclass function be marked as open, but the
overriding functionmust be marked withoverride.
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
class Foo(var count: Int) : Base("Foo expression") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
Not the Original Tag: something() was called 1 times
Not the Original Tag: something() was called 2 times
Not the Original Tag: something() was called 3 times
- By default, open is transitive.
- Once a function is marked as open, it is open for
all subclasses down the hierarchy, regardless of whether intervening classesoverridethe function or not.
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(val thing: String) : Base(thing)
class Foo(var count: Int) : Intermezzo("Foo example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
Not the Original Tag: something() was called 1 times
Not the Original Tag: something() was called 2 times
Not the Original Tag: something() was called 3 times
- However, if Intermezzo makes its version of that function as
final.
open class Base(val tag: string) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(val thing: String) : Base(thing) {
final override fun gimmeTheTag() = "-$thing-"
}
class Foo(var count: Int) : Intermezzo("Foo expression") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
- This results in an error.
Overriding Properites
- It is also possible for a class to declare a property as
open, allowing classes to override it. - Mostly, this is needed for properties declared as constructor parameters.
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(val thing: String) : Base(thing)
class Foo(var count: Int) : Intermezzo("Foo example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
- Notice that the constructor parameter for
Baseis called tag.- While the constructor parameter for
Intermezzois called thing.- They happen to be the same value.
- Suppose, though, that we wanted to name these the same, such as having them both as tag:
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(val tag: String) : Base(tag)
class Foo(var count: Int) : Intermezzo("Foo example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.somthing()
- This results in a compile error.
- This is because both are DECLARED AS PROPERTIES, and by default you cannot re-declare a property by reusing the name.
- One way to address this is to realize that we do not need the constructor parameter in Intermezzo to be a property.
- It could be a PLAIN constructor parameter.
open class Base(val tag: string) {
fun gimmeTheTag() = tag
}
open class Intermezzo(tag: String) : Base(tag)
class Foo(var count: Int) : Intermezzo("Foo example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
- What the compiler suggests, though, is that we can make tag in Base be
openthen override it in Intermezzo:
open class Base(open val tag: String) {
open fun gimmeTheTag() = tag
}
open class Intermezzo(override val tag: String) : Base(tag)
class Foo(var count: Int) : Intermezzo("Foo example") {
override fun gimmeTheTag() = "Not the Original Tag"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
Chaining to Superclass Functions
- Like Java, Kotlin uses a
super . prefixto allow code in a subclass to specifically call implementations in a superclass. - Typically, you do this in a function that you overrode.
- To execute the superclass implementation.
- As part of the overriden function's implementation.
open class Base(val tag: String) {
open fun gimmeTheTag() = tag
}
class Foo(var count: Int) : Base("Foo Example") {
override fun gimmeTheTag() = super.gimmeTheTag() + "-Extended"
fun something() {
count += 1
println("${gimmeTheTag()}: something() was called $count times")
}
}
val foo = Foo(0)
foo.something()
foo.something()
foo.something()
Foo Example-Extended: something() was called 1 times
Foo Example-Extended: something() was called 2 times
Foo Example-Extended: something() was called 3 times
Inheritance Tests
Sometimes, we need to know whether a reference to some supertype is an instance of some subtype.
- In Java, usually we do this via
instanceof. - So if we have a class hierarchy like this:
class Animal {
}
class Frog extends Animal {
}
class Axotl extends Animal {
}
... we can use instanceof to see if some Animal is really a Frog:
void something(Animal criter) {
if(critter instanceof Frog) {
// Do froggy things.
}
}
- In Ruby,
kind_of?, oris_a?are the typical ways of accomplishing the same thing.
def something(critter)
if critter.is_a? Frog
# Do froggy things.
end
end
- In Kotlin, the same syntax as Java is used, with a shorter keyword:
is.
open class Animal
class Frog : Animal()
class Axotl : Animal()
val critter : Animal = Frog()
if(critter is Frog) println("Ribbit!")
else println("Ummm... whatever noise an axolotl makes!")
Inheritance and when
- In Kotlin, we can use is not only as part of an expression, but we can use it as a test for a when clause.
- This is helpful if you have a series of subtypes to check and handle separately.
- Though you may be better served by using
function overloading. - And putting the logic in the classes themselves.
- (e.g., have a
makeSound()function that all Animal classes implement, so you can just callmakeSound()on any Animal and get the desired result.
- Though you may be better served by using
open class Animal
class Frog: Animal()
class Axolotl: Animal()
val critter: Animal = Frog()
when(critter) {
is Frog -> println("Ribbit!")
else -> println("Umm...whatever noise an axolotl makes!")
}
Casting
Java programmers have a long history with casting objects.
- This turns a reference to a supertype into a subtype.
- In Java, we use the subtype name in parentheses to perform the
cast.
void something(Animal critter) {
if(critter instanceof Frog) {
Frog frog = (Frog) critter;
// Do something with the frog.
}
else {
Axolotl axolotl = (Axolotl) critter;
// Do something with the Axolotl.
}
}
- In Kotlin, casting gets a bit more interesting, because the compiler tries to help the
reducethenumber of caststhat you need. - In fact, well-written Kotlin code rarely involves casting.
Manual
- In Kotlin, you can use the
askeyword.
val critter: Animal = Frog()
val kermit = critter as Frog
Smart Casts
- In the Java code, in the if block, we cast critter to be a Frog.
- We know this is
safe, because we checked thetype validitywithinstanceofin theif test.
SO, IF WE KNOW THE CRITTER IS A FROG...WHY DOESN'T JAVA?
Presumably, the Kotlin developers asked themselves that sort of question.
Inside of if and when blocks, if we know that a variable is really of some subtype, the compiler lets us skip the casts and allows us to refer to the variable as the subtype,
EVEN THOUGH IT WAS DECLARED AS A SUPERTYPE.
open class Animal
class Frog: Animal() {
fun hop() = println("Hop!")
}
class Axolotl: Animal() {
fun swim() = println("Swish!")
}
val critter: Animal = Frog()
when(critter) {
is Frog -> critter.hop()
is Axolotl -> critter.swim()
}
- In a Java style world, this would not compile.
critteris anAnimal.Animalhas neither hop or swim.
However, Kotlin realizes that the is Frog code will only be executed if critter is a Frog, so it allows us to call hop() on critter, as if critter were declared as a Frog instead of an Animal.
- However, this only works when the compiler is certain of the type.
- As a result, this subtly-different edition of that code fails with a compile error.
Here, we replace is Axolotl with else.
- The Kotlin compiler knows that critter is not a Frog, as otherwise we would not be executing the else.
- However, critter might not be an Axolotl, as it could be an instance of Animal.
- As a result, Kotlin
cannot assume that is safeto call swim() on a critter, as while an Axolotl can swim(), an Animal cannot.
open class Animal
class Frog : Animal() {
fun hop() = println("Hop!")
}
class Axolotl : Animal() {
fun swim() = println("Swish!")
}
val critter : Animal = Frog()
when(critter) {
is Frog -> critter.hop()
else -> critter.swim()
}
- To get it to compile, we need to manually cast the critter to be an Axolotl:
open class Animal
class Frog: Animal() {
fun hop() = println("Hop!")
}
class Axolotl: Animal() {
fun swim() = println("Swish!")
}
val critter: Animal = Frog()
when(critter) {
is Frog -> critter.hop()
else -> (critter as Axolotl).swim()
}
- While this compiles, it might crash at runtime giving this error message from Kotlin/JVM.
Kotlin's "smart cast" - as this automatic casting is called - not only saves you from typing, but it also helps you avoid improper manual casts.