1 Constructor
class Person(var name: String, var age: Int)
This is a very classic way to declare a class in Kotlin, and this pair of parentheses is a main constructor for Person. Respectively, there is a concept called "subconstructor".
1.1 Main constructor
In the case above, the main constructor needs 2 args to construct a Person object. At the same time, it actually declares 2 properties named var name: String and var age: Int which are accessible inside the whole Person class and even accessible outside the class because by default it means public. They are properties because they are modified by var, without var or val, they will not be properties and can only be accessed in the main constructor.
As you can see, property name can be accessed inside the whole class, but arg age can only be accessed in the init block.
Init block
We can see init block as the body of main constructor even it is not so called officially. It is like the init block in Java but not exactly the same:
- In Java, init block can not access constructor's args;
- In Kotlin, init block can access constructor's args.
Property initialization
What if we declare a property but ignore its initialization?
After we erase firstChar's initialization line, IDE tells us "Property must be initialized or be abstract". So, this is Kotlin, always considers security of your code, it makes you to take good care of every property you declare. More details will be discussed in the following.
Inherit class and implement interface
When class Person is a child class of Animal, it is declared like this. This pair of parentheses after Animal is Animal's main constructor.
abstract class Animal
class Person(var name: String, age: Int): Animal()
When class Person is a class which implements a interface like Act, it is declared like this. Note that there is no parentheses after Act.
interface Act
class Person(var name: String, age: Int): Act
If class is a child class of Animal, at the same time implements a interface Act, it should be declared like this. Note that there is no order between Animal() and Act.
interface Act
abstract class Animal
class Person(var name: String, age: Int): Animal(), Act
1.2 Subconstructor
Subconstructor is declared inside the class and modified by constructor, like:
interface Act
abstract class Animal
class Person(var name: String, var age: Int): Animal(), Act {
constructor(name: String): this(name = name, age = 20)
constructor(age: Int): this(name = "")
}
Subconstructor provides different methods to construct a class, but every subconstructor has to invoke the main constructor or to invoke another subconstructor which has already invoked the main constructor. In another word, eventually it is the main constructor that constructs a class. It is like a chain but the main constructor is at the terminal end.
Well, Kotlin allows us to declare several subconstructors and even without main constructor, this is a compromise to Java, but this is seriously NOT RECOMMENDED.
Another thing, we know that Java does not have a main constructor, so if you want your Kotlin class with a main constructor to be used in Java code, you need to annotate it with@JvmOverloads like this:
class Person @JvmOverloads constructor(var name: String, var age: Int)
In this case, the key word constructor in the main constructor must not be left out or codes pop out an error.
1.3 Factory function
Here we are going to talk about factory functions with the same name with class, like this:
interface Act
abstract class Animal
class Person(var name: String, var age: Int) : Animal(), Act {
val persons = HashMap<String, Person>()
fun Person(name: String): Person {
return persons[name] ?: Person(name, 1).also { persons[name] = it }
}
}
The point in this case is that constructors in Kotlin do not use class's name, so it is ok to declare a function with class's name.
2 Accessibility
2.1 Modifiers
| Modifiers | Java | Kotlin |
|---|---|---|
| public | everywhere | (by default) everywhere |
| protected | inside the package and its child | inside the class and its child |
| default | (by default)inside the package | ✕ |
| private | inside the class | inside the class and the file |
| internal | ✕ | inside the module |
2.2 Modifiers in Kotlin
| Modifiers | Top level declaration | class | members |
|---|---|---|---|
| public | ✔ | ✔ | ✔ |
| internal | ✔(module) | ✔(module) | ✔(module) |
| protected | ✕ | ✕ | ✔ |
| private | ✔(file) | ✔(file) | ✔(class) |
internal
What is a "module"? In essence, a module is group of files / packages using the command
kotlincat the same time when compiling. Generally speaking, a .jar or .aar file is a module.
One more thing, now that internal is a specific modifier in Kotlin, so Java cannot recognize it, it won't be safe if Java code try to access Kotlin code modified by internal. Here's the solution:
internal class Person(var name: String, var age: Int) {
@JvmName("^*%")
fun sayHi() {
println("Hi! I am ${this.name}, and I am ${this.age} years old.")
}
}
we can specify function's name when it is used in Java codes by the annotation @JvmName("name"). If we want internal modifier works in Java codes, we can specify menbers' name as a illegal name, such as "^*%". This way, Java can not use our members.
constructor and property
When modifiers are set on constructors or properties, it looks like:
class Animal private constructor(private var category: String)
In this case, constructor must not be left out.
Modifiers can also modify properties' getter and setter, but there are some key options and considerations to take into account.
- getter's modifier must be kept as the same as the property (getter === property);
- setter's modifier must not be higher than the property (setter <= property).
top level declaration
Top level declaration includes properties, functions, classes etc declared directly in the file and outside a class. There are some considerations to take into account.
protectedis not allowed to modify a top level declaration;privatemeans accessibility in the file for a top level declaration.
3 Init later
There are many cases where we want to declare a val or a var without initializing it or we do not want to assign some random value to it. But Kotlin, for security consideration, always wants that coders take full responsible for what is declared.
Kotlin provides coders with some methods to initialize a val or a var.
We use the case in Android for example:
//Java
private TextView mTextView;
@Override
protected void onCreate(...) {
...
mTextView = findViewById(R.id.mTextView);
}
When we translate these codes directly into Kotlin:
3.1 null
We can assign a null to mTextView to avoid this error, but it has to be a type of TextView? which means whenever we want to use it, we have to tell whether it is null or not.
null IS NOT RECOMMENDED.❌
3.2 lateinit
Kotlin provides another feature for us to achieve "init later", that is lateinit.
By using this key word, the mTextView must be a var, or codes pops an error, which reads "'lateinit' modifier is allowed only on mutable properties"
For security consideration, we should tell whether mTextView is initialized or not before using it. Kotlin, since 1.2, provides a new feature to co-operate with lateinit to determine variables's initialization state.
But still,
lateinit IS NOT RECOMMENDED.❌
Due to these following reasons:
lateinitmakes Kt compiler to ignore properties' initialization state, and it's not accessiable for basic data type likeInt;- Coders must be complete aware of the life circle of property modified by
lateinit; - Do not use
lateinitin complex cases; - The feature since 1.2 for determining initialization state is not recommended.
3.3 by lazy
by lazy is used like this:
Property modified by by lazy will not be initialized until it is used for the first time, and it will be assigned by the output of lambda expression. So there is no security problem, especially NonPointerException(NPE) problem. There is no restriction. Last but no least, we can easily tell where properties are initialized because it is right behind its declaration.
by lazy IS HIGHLY RECOMMENDED.✅
4 Delegate
I represent YOU to DO SOMETHING.
4.1 Interface Delegate
Object x represents class A to implement method in interface B.
In the following case, there is an interface Api, and two classes implementing Api. When class ApiImpl implements Api, it overrides all methods as usual. When ApiWrapper implements Api, it overrides only method c(). Method a() and b() are represented by api, which is an implementation of interface Api. In this case, delegate api should be passed in via main constructor of ApiWrapper.
In function main(), when constructing ApiWrapper, we pass the object of ApiImpl into its main contructor.
So methods a() and b() in ApiWrapper perform the exact same function as they do in ApiImpl. Method c() in ApiWrapper is overrided so it is customized.
interface Api {
fun a()
fun b()
fun c()
}
class ApiWrapper(val api: Api) : Api by api {
override fun c() {
println("This is method c from ApiWrapper.")
}
}
class ApiImpl() : Api {
override fun a() {
println("This is method a from ApiImpl.")
}
override fun b() {
println("This is method b from ApiImpl.")
}
override fun c() {
println("This is method c from ApiImpl.")
}
}
fun main() {
val apiImpl = ApiImpl()
val apiWrapper = ApiWrapper(apiImpl)
apiImpl.a()
apiImpl.b()
apiImpl.c()
apiWrapper.a()
apiWrapper.b()
apiWrapper.c()
}
Console outputs:
SuperArray
Here we are going to declare a SuperArray using interface delegate.
class SuperArray<E>(
private val list: MutableList<E?>,
private val map: MutableMap<String, E?>
) : MutableList<E?> by list, MutableMap<String, E?> by map {
}
Because there are some methods from list and map with the same names, so we have to override these methods to tell compiler the specific logic of these mathods.
Eventually, SuperArray is declared as the following:
class SuperArray<E>(
// Main constructor with two args which both have a default value.
private val list: MutableList<E?> = ArrayList(),
private val map: MutableMap<String, E?> = HashMap()
) : MutableList<E?> by list, MutableMap<String, E?> by map {
/**
* To clear this SuperArray, collections both need to be cleared.
*/
override fun clear() {
list.clear()
map.clear()
}
/**
* To tell whether SuperArray is empty, both should be empty.
*/
override fun isEmpty(): Boolean = list.isEmpty() && map.isEmpty()
/**
* SuperArray's size equals the combination of both collections' size.
*/
override val size: Int
get() = list.size + map.size
/**
* If the index are out of bounds, SuperArray automatically expands itself to make it compact.
*
* @param index the index to be used.
* @param element the element to be put.
*/
override fun set(index: Int, element: E?): E? {
while (index >= list.size) {
list.add(null)
}
return list.set(index, element)
}
/**
* To customize print style.
*/
override fun toString(): String {
return "List:$list, Map:$map"
}
}
Using SuperArray in main function:
fun main() {
val superArray = SuperArray<String>()
superArray += "Hello"
superArray[1] = "World"
superArray[4] = "!"
superArray["I"] = "wo"
superArray["You"] = "ni"
println(superArray)
}
Console outputs:
4.2 Property Delegate
Object x represents property a to implement getter/setter.
by lazy
We have met by lazy before, it is actually a property delegate which is used to delegate property's getter.
by Delegates.observable
by lazy is for delegating getter, by Delegate.observable is for delegating setter.
import kotlin.properties.Delegates
class Demo {
val name: String by lazy {
"Jack"
}
var age: Int by Delegates.observable(0) {
property, oldValue, newValue ->
println("$property has changed from $oldValue to $newValue")
}
}
fun main() {
val demo = Demo()
println(demo.name)
demo.age = 12
}
Console outputs:
5 object & companion object
5.1 object
When a singleton is needed in Java, it can be declared as:
public class Singleton {
private Singleton(){}
public static final Singleton INSTANCE = new Singleton();
}
Well, in Kotlin, it is declared as:
object Singleton {
}
They are all hungry-style singleton.
object Singleton of course can have it own properties and functions.
object Singleton {
var x: Int = 2
fun y() = return x
}
fun main() {
println(Singleton.x)
println(Singleton.y())
}
The byte codes of Singleton.x and Singleton.y() are de-compiled to Java like:
Singleton.INSTANCE.getX()
Singleton.INSTANCE.y()
Why
getX()? Because there are backing field in Kotlin, so when a property is de-compiled to Java, a property equals a field plus a getter plus a setter. We can add annotation @JvmField to properties in Kotlin. In this case, these properties will have their getters and setters when de-compiled to Java.
But if we add annotation @JvmStatic to property x and function y(), like:
object Singleton {
@JvmStatic var x: Int = 2
@JvmStatic fun y() = return x
}
In Java they can be expressed as:
Singleton.getX()
Singleton.y()
We have to note that ** only members in named objects and companion objects can be annotated with
@JvmStatic**
One more thing, object CANNOT have a constructor, but it CAN have several init blocks like:
object Singleton {
init {
}
}
And, object CAN extend base class or implement interfaces like:
object Singleton: Runnable {
override fun run() {
}
}
5.2 companion object
What if we want to declare a static-style property or function inside a class? Here Kotlin provides companion object.
Also companion object can have a name like:
It is recommended to use A.x rather than A.Obj.x.
6 Inner class & Local class & Local function
6.1 Inner class
In Java, we can declare a class inside another class, it is called an inner class. Kotlin, of course, supports this.
class Outer {
class Inner {
}
static class StaticInner {
}
}
While in Kotlin, this is expressed as:
class Outer {
inner class Inner {
}
class StaticInner {
}
}
If a inner class is NOT modified by inner, it is a static inner class by default.
If we want to initiate a static inner class, we can make it without initiate its outter class; on the other hand, if we want to initiate a NOT static inner class, we have to initiate its outer class first or to get an object of its outer class.
val inner = Outer().Inner()
val staticInner = Outer.Inner()
Sense the difference.
6.2 Inner object
In Kotlin, we can also declare an object inside another object, like:
object OuterObject {
object StaticInnerObject
}
Because object is initiated once used, there is no concept of STATIC in object, so inner object CANNOT be modified by inner.
6.3 Anonymous inner class
new Runnable() {
@Override
public void run() {
}
}
object: Runnable {
override fun run() {
}
}
In Kotlin, anonymous inner class is actually an object without its name, so it can implement several interfaces and extend a class, like:
object: Cloneable, Runnable, BaseClass() {
override fun run() {
}
}
Java does NOT support this.
6.4 Local class
In both Java and Kotlin, we can declare a class inside a function, called "Loacl class", like:
public static void main(String[] args) {
class LocalClass implements Cloneable, Runnable {
@Override
public void run() {
}
}
}
fun main() {
class LocalClass: Cloneable, Runnable {
override fun run() {
}
}
}
6.5 Local function
Kotlin also supports local function, like:
7 Data class
data class Book(val author: String, val title: String)
A data class Book is declared above. What's the difference between a data class and a normal class?
Some people would say that "data class is just like JavaBean", but thay are actually not the same.
When modified by data, property declared in the main constructor is also called component. Compared to property, a component has more than a getter and a setter, it also has functions componentN(), equals(), hashCode(), toString(), and copy(), which are automatically generated by Kotlin compiler. So a component is more sophisticated than a property.
7.1 Deconstruction
Like we have met in type Pair, data class supports deconstruction.
val pair = "hello" to "world"
val (hello, world) = pair
val (author, title) = book1
As we can imagin, Pair is a data class.
When we do deconstruction, values / variables in parentheses are componetnts in data class respectively.
7.2 JavaBean vs data class
| JavaBean | data class | |
|---|---|---|
| Constructor | Constructor without args by default | Components as args |
| Field | Private field but expose getter or setter | Components |
| Extending | Able to extend and be extended | Able to extend normal class but CANNOT be extended |
| Component | No | Equality and deconstruction |
If you really really want to use data class as JavaBean, you should use two plugins called AllOpen and NoArg.
Why can not data class be extended? What if data class can be extended?
data class Person(val name: String)
data class Teacher(override val name: String, val subject: String): Person(name)
val person = Person("Lewei")
val teacher1 = Teacher("Lewei", "Maths")
val teacher2 = Teacher("Lewei", "Chinese")
person == teacher1 // true
teacher1 == person // fasle
person == teacher2 // true
teacher1 == teacher2 // false
Code above actually reveals that because data class has its own 'equals()' and 'hashCode()' function, once it is able to be extended, it cannot obey symmetry and transitivity of equality, in Chinese "无法遵守等式的对称性和传递性".
7.3 Use data class correctlly
- In most cases, it is ** NOT RECOMMENDED** that a data class comes with a class body.
-
Basic type of component like Number or String is RECOMMENDED.
-
DO NOT declare getter or setter for components, let they be default.
-
It's RECOMMENDED that components are declared as
val.
8 Enum class
Enum class in Kotlin is almost the same as in Java.
// Java
enum State {
Idle, Busy;
}
State.Idle.name()
State.Idle.ordinal()
// Kotlion
enum class State {
Idle, Busy;
}
State.Idle.name
State.Idle.ordinal
Enum class can have its own constructor.
//Java
enum State {
Idle(0),
Busy(1);
int id;
State(int id) {
this.id = id;
}
}
//Kotlin
enum class State(val id: Int) {
Idle(0),
Busy(1);
}
Enum class can implement interfaces.
//Java
enum State implements Runnable {
Idle, Busy;
@Override
public void run() {
}
}
//Kotlin
enum class State: Runnable {
Idle, Busy;
override fun run() {
}
}
Like in Java, elements in enum class in Kotlin can also implement methods by themselves.
//Java
enum State implements Runnable {
Idle {
@Override
public void run() {
}
},
Busy {
@Override
public void run() {
}
};
}
//Kotlin
enum class State: Runnable {
Idle {
override fun run() {
}
},
Busy {
override fun run() {
}
};
}
Kotlin has some special features for enum class compared to Java.
- Kotlin can declare extension methods for enum class.
//Kotlin
fun State.next(): State {
return State.value().let {
val nextOrdinal = (ordinal + 1) % it.size
it[nextOrdinal]
}
}
- Enums can be used in comparing cases.
//Kotlin
if (state <= State.Idle) {
}
- Enums can be used as a section.
//Kotlin
enum class Color {
White, Red, Green, Blue, Yellow, Black;
}
if (color in Color.White..Color.Green) {
}
- Use enum class in
when.
As we use when in normal cases, it HAVE TO have an else option, otherwise compiler throws an error.
While we use enum class in when, as long as we put all enums in when cases, else option should be ignored. Well this is easy to understand because enums are all here, there is no more else.
9 Sealed class
What is sealed class? Sealed class is a new concept in Kotlin, it has these following features.
- Sealed class is a special kind of abstract class;
- Subclasses of a sealed class SHOULD be declared in the same Kotlin file right with it;
- Because of feature 2, the count of its subclass is limited.
A sealed class is declared like:
sealed class PlayerState {
constructor() // private constructor NO.1
constructor(number: Int) // private constructor NO.2
}
For example, in one .kt file, we declared a sealed class called PlayerState and its subclasses Idle, Playing and Error, we assume that these are all states of player.
sealed class PlayerState
object Idle: PlayerState()
class Playing(val media: Media): PlayerState() {
}
class Error(val errorCode: Int): PlayerState() {
}
Because its subclass is limited, so like using enum class in when, as long as we list all possible options, there is no need for else.
class Player {
val state: PlayerState = Idle
fun play(media: Media) {
this.state = when (val state = this.state) {
Idle -> {
Playing(media).also(Playing::start)
}
is Playing -> {
state.stop()
Playing(media).also(Playing::start)
}
is Error -> {
state.recover()
Playing(media).also(Playing::start)
}
}
}
}
Sealed class versus enum class.
| sealed class | enum class | |
|---|---|---|
| states | extended by limited subclasses | limited enums |
| countability | subclasses are countable | enums are countable |
| difference between states | different types(classes) | different values |
When there is only objects extending a sealed class, then you may want an enum class.
10 Inline class
Kotlin starts to support inline class from version 1.3, and up to now, version 1.4.10-release, it is still an experimental feature. Let's take a quick peak.
Inline class is a class modifed by inline, like:
inline class BoxedInt(val int: Int) {
override fun toString(): String {
return "I\'m a BoxInt, my value is $int."
}
}
10.1 Things u should know
-
Inline class is a package for another type;
-
Inline class is like
Integerforintin Java; -
Kotlin compiler will try to unpack inline class and to use the packed type in the first place, unless confronting methods overrided by inline class or methods only declared in inline class. You might wonder that it is just like a normal subclass. Well, difference lies on how compiler deals with it. Think about inline function, it is an optimization for the performance of your codes.
-
In inline class, there should be one and only one value
valdeclared in its main constructor. Inline class can only pack one type. -
It is NOT allowed to declare a property with backing-field in inline class.
-
Inline class can implement interfaces but can NOT extend another class or be extended considering that Kotlin compiler want to optimize inline class when it's used.