Polymorphism – A core OOP concept that refers to working with objects through their interfaces without knowledge about their specific types and internal structure
Liskov Substitution Principle (LSP) – If for each object o1 of type S, there is an object o2 of type T, such that for all programs P defined in terms of T the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T
class UselessClass
fun main() {
val uselessObject = UselessClass() // () here is constructor invocation
}
class Person()
{
var name: String //
Error: Property must be initialized or be abstract
}
class Person(name: String) // Primary ctor with parameter - name
{
var name: String = name // Mutable property initialized
}
val person = Person("John")
//...
//...
person.name = "" //
Allowed: 'var' can be reassigned
class Person(name: String) // Primary ctor with parameter - name
{
val name: String = name // Immutable property initialized
}
val person = Person2("John")
//...
//...
person.name = "" //
Error: 'val' cannot be reassigned
class Person(name: String) // Primary ctor with parameter - name
{
val name: String = name
}
Shorthand, idiomatic way to declare and initialize a property in one go:
class Person(val name: String) // property — declared + initialized automatically
class Person(val name: String, val age: Int) { // 1. primary constructor
val isAdult: Boolean
init { // 2. runs as part of the primary constructor
println("Person created: $name")
isAdult = age >= 18
}
constructor(name: String) : this(name, 0) { // 3. secondary constructor
println("Age not provided")
}
}
Person("Alice", 30) // Output: Person created: Alice
Person("Alice") // Output: Person created: Alice, Age not provided
open class Point(val x: Int, val y: Int) {
constructor(other: Point) : this(other.x, other.y) { ... }
constructor(circle: Circle) : this(circle.centre) { ... }
}
Constructors can be chained, but they should always call the primary constructor in the end
A secondary constructor’s body will be executed after the object is created with the primary constructor. If it calls other constructors, then it will be executed after the other constructors’ bodies are executed
Inheritor class must call parent’s constructor:
class ColoredPoint(val color: Color, x: Int, y: Int) : Point(x, y) { ... }
class Example(val value: Int, info: String) {
val anotherValue: Int
var info = "Description: $info"
init {
this.info += ", with value $value"
}
val thirdValue = compute() * 2
private fun compute() = value * 10
init {
anotherValue = compute()
}
}
There can be multiple init blocks
Init block can access only values declared above it
If value has the same name as constructor parameter, prefix it with this
interface RegularCat {
fun pet()
fun feed(food: Food)
}
interface SickCat {
fun checkStomach()
fun giveMedicine(pill: Pill)
}
Interfaces cannot have a state
VS
abstract class RegularCat {
abstract val name: String
abstract fun pet()
abstract fun feed(food: Food)
}
abstract class SickCat {
abstract val location: String
abstract fun checkStomach()
fun giveMedicine(pill: Pill) {}
}
Abstract classes cannot have an instance, but can have a state
class Poop {} class Food {}
abstract class RegularCat {
protected abstract val isHungry: Boolean
private fun poop(): Poop { /* do the thing */ }
abstract fun feed(food: Food)
}
class MyCat : RegularCat() {
override val isHungry: Boolean = false
override fun feed(food: Food) {
if (isHungry) { /* do the thing */ }
else { poop() } // MyCat cannot poop
}
}
class SickDomesticCat : RegularCat(), CatAtHospital {
override var isHungry: Boolean = false
get() = field
set(value) {...}
override fun pet() {...}
override fun feed(food: Food) {...}
override fun checkStomach() {...}
override fun giveMedicine(pill: Pill) {...}
}
To allow a class to be inherited by other classes, the class should be marked with the open keyword (Abstract classes are always open)
In Kotlin you can inherit only from one class, and from as many interfaces as you like
When you’re inheriting from a class, you have to call its constructor, just like how secondary constructors have to call the primary
interface DomesticAnimal {
fun pet()
}
class Dog: DomesticAnimal {
override fun pet() {...}
}
class Cat: DomesticAnimal {
override fun pet() {...}
}
fun main() {
val homeZoo = listOf<DomesticAnimal>(Dog(), Cat())
homeZoo.forEach { it.pet() }
}
import kotlin.math.max
class PositiveAttitude(startingAttitude: Int) {
var attitude = max(0, startingAttitude)
set(value) =
if (value >= 0) {
field = value
} else {
println("Only positive attitude!")
field = 0
}
var hiddenAttitude: Int = startingAttitude
private set
get() {
if (field < 0) {
println("Don't ask this!")
field += 10
}
return field
}
val isSecretlyNegative: Boolean
get() = hiddenAttitude < 0
}
Properties can optionally have an initializer, getter, and setter
Use the field keyword to access the values inside the getter or setter, otherwise you might encounter infinite recursion
Properties may have no backing field at all
open class OpenBase(open val value: Int)
interface AnotherExample {
/* abstract */ val anotherValue: OpenBase
}
open class OpenChild(value: Int) : OpenBase(value), AnotherExample {
override var value: Int = 1000
get() = field - 7
override val anotherValue: OpenBase = OpenBase(value)
}
open class AnotherChild(value: Int) : OpenChild(value) {
final override var value: Int = value
get() = super.value // default get() is used otherwise
set(value) { field = value * 2 }
final override val anotherValue: OpenChild = OpenChild(value) // Notice that we use OpenChild here, not OpenBase
}
Properties may be open or abstract, which means that their getters and setters might or must be overridden by inheritors, respectively. Interfaces can have properties, but they are always abstract. You can prohibit further overriding by marking a property final
class SomeType()
class Example {
operator fun plus(other: Example): Example = Example()
operator fun dec() = this // return type has to be a subtype
operator fun get(i: Int, j: Int): SomeType = SomeType()
operator fun get(x: Double?, y: String) = this
operator fun <T> invoke(l: List<T>): SomeType = SomeType()
}
operator fun Example.rangeTo(other: Example): Iterable<Example> = listOf(this, other)
fun main() {
var ex1 = Example()
val ex2 = ex1 + --ex1 // -- reassigned ex1, so it has to be var
for (ex in ex1..ex2) {
ex[23, 42]
ex[null, "Wow"](listOf(1,2,3))
}
}
Kotlin provides the ability to extend a class or an interface with new functionality
without need to inherit from the class or use forbidden magic (reflection)
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' is the given MutableList<T>
this[index1] = this[index2]
this[index2] = tmp
}
If the extended class already has the new method with the same name and signature, the original one will be used
Extended class does not change at all. Extensions is a new function that can be called like a method. It cannot access private members, for example.
Extensions have static dispatch, rather than virtual dispatch by receiver type. An extension function being called is determined by the type of the expression on which the function is invoked, not by the type of the result from evaluating that expression at runtime.
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(s: Shape) {
println(s.getName())
}
printClassName(Rectangle()) // "Shape", not Rectangle
data class Person(val name: String, val surname: String)
infix fun String.with(other: String) = Person(this, other)
fun main() {
val realHero = "Ryan" with "Gosling"
val (real, bean) = realHero
}
class SomeData(val list: List<Int>) {
operator fun component1() = list.first()
operator fun component2() = SomeData(list.subList(1, list.size))
operator fun component3() = "This is weird"
}
fun main() {
val sd = SomeData(listOf(1, 2, 3))
val (head, tail, msg) = sd
val (h, t) = sd
val (onlyComponent1) = sd
}
Class can overload any number of componentN methods for use in destructive declarations
Data classes have these methods by default
class Person(val name: String, val ssn: String)
val p1 = Person("John", "123-45-6789")
val p2 = Person("John", "123-45-6789")
println(p1 == p2) // false, because they are different objects
data class Person(val name: String, val ssn: String)
val p1 = Person("John", "123-45-6789")
val p2 = Person("John", "123-45-6789")
println(p1 == p2) // true, because they are structurally equal
data class User(val name: String, val age: Int)
The compiler automatically derives:
The standard library provides the Pair and Triple classes, but named data classes are a much better design choice
data class User(val name: String, val age: Int)
fun main() {
val user = User("John", 23)
val (name, age) = user // destructing declaration calls componentN()
val (onlyName) = user
val olderUser = user.copy(age = 42)
}
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
Each enum constant is an object
Each enum is an instance of the enum class, thus it can be initialized as:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
Enum classes can have methods or even implement interfaces
val g = Color.valueOf("green".uppercase())
when(g) {
Color.RED -> println("blood")
Color.GREEN -> println("grass")
Color.BLUE -> println("sky")
}
sealed class Shape
data class Circle(val radius: Double) : Shape()
data class Rectangle(val w: Double, val h: Double) : Shape()
fun main() {
val shapes: List<Shape> = listOf(Circle(5.0), Rectangle(3.0, 4.0))
for (shape in shapes) {
val description = when (shape) {
is Circle -> "Circle with radius ${shape.radius}" // Smart casting
is Rectangle -> "Rectangle ${shape.w} x ${shape.h}"
}
println(description)
}
}
Sealed = closed set - all inheritors known at compilation + defined in the same file/package
Exhaustiveness checking - the compiler forces you to handle every variant
Discriminated Union - a type that is exactly one of a fixed set of named variants, and you always know which one
Single Abstract Method (SAM) interface - has one abstract method
Kotlin allows us to use a lambda instead of a class definition to implement a SAM
fun interface IntPredicate {
fun accept(i: Int): Boolean
}
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}
VS
val isEven = IntPredicate { it % 2 == 0 }
fun main() {
println("Is 7 even? - ${isEven.accept(7)}")
}
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ...
}
val allDataProviders: Collection<DataProvider>
get() = // ...
}
DataProviderManager.registerDataProvider(...)
Singleton tied to the class
Accessed via MyClass.Companion or MyClass
Can implement interfaces - unlike static members
Holds state and behavior
Enables the class to be its own factory
also runs the incrementing side effect, then returns the newly created MyClass instance
interface Factory {
fun create(): MyClass
}
class MyClass {
companion object : Factory {
var counter: Int = 0
override fun create(): MyClass =
MyClass().also {
it.announce()
counter += 1
}
}
fun announce(): Unit = println("created!")
}
val f: Factory = MyClass//.Companion
val instance1 = f.create()
val instance2 = f.create()
fun main() {
println(MyClass/*.Companion*/.counter)
}
Any is unified sypertype
fun printAnything(value: Any) {
println(value)
}
fun main() {
printAnything(42) // accepts any type of argument
}
Nothing is an uninhabited subtype of every type - the evaluation never completes normally
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
val someNullable: String? = null
val name: String = someNullable ?: fail("name was null") // compiles
Thanx!
Polygon (pentagon)
Quadrangle (trapezoid)
Triangle
Arrows: level 1 → Polygon
Rectangle
Square
Rhombus (diamond)
Parallelogram
IsoscelesTriangle
RightTriangle
Arrows: level 2 → Quadrangle
Arrows: level 2 → Triangle
LEFT: paths behind boxes
LEFT: boxes
RIGHT: paths behind boxes
RIGHT: boxes (gray fill)