Kotlin is pragmatic, safe, concise, and interoperable, meaning it focuses on using proven
solutions for common tasks, preventing common errors such as NullPointerException
s, supporting compact and easy-to-read code, and unrestricted
ch 1 : Intro to kotlin basics
val x = 1
-
Kotlin automatically determines that its type is Int. The ability of the compiler to determine types from context is called type inference
-
Most of the code that would lead to a NullPointerException in Java fails to compile in Kotlin, ensuring that you fix the error before the application gets to your
ch 2 : Defining
-
val (from value)—Immutable reference. A variable declared with val can’t be reassigned after it’s initialized. It corresponds to a final variable in Java.
-
var (from variable)—Mutable reference. The value of such a variable can be changed. This declaration corresponds to a regular (non-final)
-
Using immutable references, immutable objects, and functions without side effects makes your code closer to the functional style.
class Person(
val name: String,
var isMarried: Boolean
)
name
Read-only property: generates a field and a trivial getterisMarried
Writable property: a field, a getter, and a set
* The concise syntax 1..5 creates a range. Ranges and progressions allow Kotlin to use a uniform syntax and set of abstractions in for loops and also work with the in and !in operators that check whether a value belongs to a range.
val percentage = if (number in 0..100) number
In Kotlin, lazy
is a function that is used to create a lazily initialized property.
A lazily initialized property is a property that is computed or initialized only when it is accessed for the first time, not when the object is created.
ch 3 : Defining and calling functions-:
-
Kotlin doesn’t have its own set of collection classes. All of your existing knowledge about Java collections
-
joinToString() :
/* Java */
collection.joinToString(/* separator */ " ", /* prefix */ " ", /* postfix */ ".");
collection.joinToString(separator = " ", prefix = " ", postfix = ".")
Note: in a call, you should also specify the names for all the arguments after that, to avoid confusion.
package strings
fun String.lastChar(): Char = get(length - 1)
import strings.lastChar
val c = "Kotlin".lastChar() // n
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
NOTE: The word to in this line of code isn’t a built-in construct, but rather a method invocation of a special kind, called an infix call
println("12.345-6.A".split("\\.|-".toRegex()))
[12, 345, 6, A]
For instance, in Kotlin you use an extension function toRegex to convert a string into a regular expression
class User(val id: Int, val name: String, val address: String)
fun saveUser(user: User) {
fun validate(value: String, fieldName: String){
if (value.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: " +
"$fieldName is empty")
}}
validate(user.name, "Name")
validate(user.address, "Address")
// Save user to the database
}
>>> saveUser(User(1, "", ""))
java.lang.IllegalArgumentException: Cannot save user 1: Name is empty
NOTE: Local functions help you structure your code more cleanly and eliminate duplication
ch 4 : Classes-:
-
All classes and methods that aren’t specifically intended to be overridden in subclasses need to be explicitly marked as final
-
If you want to allow the creation of subclasses of a class, you need to mark the class with the open modifier. In addition, you need to add the open modifier to everyproperty or method that can be overridden: //AU: I’ve added blank lines between the code lines to make room for the annotations. OK? TT
open class RichButton : Clickable { //1
fun disable() {} //2
open fun animate() {} // 3
override fun click() {} //4
}
1-This class is open: others can inherit from it.
2-This function is "final": you can’t override it in a subclass.
3-This function is open: you may override it in a subclass
4-This function overrides an open function and is open as well.
- Inner Class : the inner keyword is used to mark a nested class as an inner class. An inner class can access members of its outer class, including private members, and has a reference to an instance of its outer class.
class OuterClass {
private val outerProperty = "Outer property"
inner class InnerClass {
fun printOuterProperty() {
println(outerProperty) // output: Outer property
}
}
}
Usecases in Android: ViewHolder class in RecyclerView adapter
- Sealed Class :
You mark a superclass with the sealed modifier, and that restricts the possibility of creating subclasses. All the direct subclasses must be nested in the superclass
can only be subclassed within the same file where it is declared.
- companion:
. If you do that, you gain the ability to access the methods and properties of that object directly through the name of the containing class, without specifying the name of the object explicitly. The resulting syntax looks exactly like static method invocation in Java. Here’s a basic example showing the syntax:
class A {
companion object{
fun bar() {
println("Companion object called")
}
}
}
A.bar()
Companion object called
ch 5 : Lambdas-: more concise and readable
- This is program to find the oldest person in list using lambdas (maxBy)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy { it.age })
// output : Person(name=Bob, age=31)
val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2)) // 3
data class Person(val name: String, val age: Int)
val list = listOf(1, 2, 3, 4)
list.filter { it % 2 == 0 }
// output => [2, 4]
The filter function can remove unwanted elements from a collection, but it doesn’t change the elements. Transforming elements is where map comes into play
val list = listOf(1, 2, 3, 4)
list.map { it * it }
// output => [1, 4, 9, 16]
val people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.all(canBeInClub27))
/ output => false
If you need to check whether there’s at least one matching element, use any:
println(people.any(canBeInClub27))
// true
Note that !all (not-all), !any (not-any) can be replaced with any with a negated condition and vice versa
For example, you want to group people of the same age together. It’s convenient to pass this quality directly as a parameter. The groupBy function can do this for you
val people = listOf(Person("Alice", 31),
Person("Bob", 29), Person("Carol", 31))
println(people.groupBy { it.age })
// output => {29=[Person(name=Bob, age=29)],
// 31=[Person(name=Alice, age=31), Person(name=Carol, age=31)]
so the result type is Map<Int, List>
The flatMap function does two things: at first it transforms (or maps) each element to a collection according to the function given as an argument, and then it combines (or flattens) several lists into one
val strings = listOf("abc", "def")
println(strings.flatMap { it.toList() })
// output => [a, b, c, d, e, f]
The Kotlin standard library reference says that both filter and map return a list. That means this chain of calls will create two lists: one to hold the results of the filter function and another for the results of map. This isn’t a problem when the source list contains two elements, but it becomes much less efficient if you have a million. To make this more efficient => you can convert the operation so it uses sequences instead of using collections directly:
people.asSequence()
.map(Person::name)
.filter { it.startsWith("A") }
.toList()
use to perform multiple operations on the same object without repeating its name (make it short)
see the diff between two codes 1.
fun alphabet(): String {
val result = StringBuilder()
for (letter in 'A'..'Z') {
result.append(letter)
}
result.append("\nNow I know the alphabet!")
return result.toString()
}
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder){
for (letter in 'A'..'Z'){
this.append(letter)
}
append("\nNow I know the alphabet!")
this.toString()
}}
As you can see, apply is an extension function, works almost exactly the same as with; the only difference is that apply() always returns the object passed to it as a parameter
fun alphabet() = StringBuilder().apply{
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
// end of Ch 5
ch 6 : The Kotlin type system
Nullability is a feature of the Kotlin type system that helps you avoid NullPointerException
errors.
Kotlin convert this problem from runtime errors to compile errors
a type without a question mark ?
denotes that variables of this type can’t store null references.
?.
It allows you to combine a null
check and a method call into a single operation.
For example, the expression s?.toUpperCase()
is equivalent to the following, more cumbersome one:
if (s != null) s.toUpperCase() else null
fun foo(s: String?) {
val t: String = s ?: "" // If "s" is null, the result is an empty string
}
The as?
operator tries to cast a value to the specified type and returns null
if the
value doesn’t have the proper type
It’s represented by a double exclamation mark and converts any value to a non-null type
to deal with a nullable argument that should be passed to a function that expects a non-null parameter
email?.let { email -> sendEmailTo(email) } == if (email != null) sendEmailTo(email)
Kotlin doesn’t distinguish between primitive types and wrapper types (ex: Integer
)
You always use the same type (for example, Int
)
The Unit
type in Kotlin fulfills the same function as void
in Java.
It can be used as a return type of a function that has nothing interesting to return:
fun f(): Unit { ... } == fun f() { ... }
ch 7 : Operator overloading and other conventions
data class Point(val x: Int, val y: Int){
operator fun plus(other: Point): Point{
return Point(x + other.x, y + other.y)
}
}
** NOTE: the operator
keyword to declare the plus function. All functions used to overload operators need to be marked with that keyword**
operator fun Point.unaryMinus(): Point{
return Point(-x, -y)
}
>>> val p = Point(10, 20)
>>> println(-p)
Point(x=-10, y=-20)
Note how you use the identity equals operator (===) to check whether the parameter to equals is the same object as the one on which equals is called.
==> creating part of an object on demand, when it’s accessed for the first time
benefits of using the lazy keyword:
1.Efficient resource utilization ==> With lazy initialization, resources are only allocated when they are actually needed
2.Thread-safe initialization ==> The lazy delegate ensures that the initialization of the property is thread-safe.
3.Cleaner code ==> The lazy keyword helps to simplify code by encapsulating the lazy initialization logic in a concise and readable manner
4.Support for immutable properties ==> The lazy keyword can be used with val properties
so you can use it together with the by
keyword to create a delegated
property. The parameter of lazy is a lambda that it calls to initialize the value. The lazy
function is thread-safe by default
val emails by lazy { loadEmails(this)}
ch 8 : Higher-order functions
val action: () -> Unit = { println(42) }
The Unit
type is used to specify that a function returns no meaningful value
The inline
keyword in Kotlin is used to declare an inline
function or an inline
property. When a function or property is marked as inline, the compiler replaces the call sites of that function or property with the actual code defined in it during the compilation process. This results in the elimination of the function call overhead, as the code is directly inserted at the call site.
ch 9 : Generics
Using generics in Kotlin provides several benefits:
Type Safety: Generics allow you to define types that a class or function can operate on. This helps in catching type errors at compile-time rather than runtime. It ensures that the code operates on the correct types and helps avoid ClassCastException errors.
Code Reusability: Generics promote code reuse by allowing you to write generic algorithms and data structures that can work with different types. This eliminates the need to duplicate code for similar functionality with different types.
Abstraction: Generics provide a level of abstraction by allowing you to write code that is independent of specific types. This makes the code more flexible and adaptable to different data types.
Performance: Generics can help improve performance by avoiding unnecessary type conversions. With generics, you can write algorithms and data structures that work directly with the desired types, eliminating the need for casting or converting objects.
If you’re going to write a function that works with a list, and you want it to work with
any list (a generic one), not a list of elements of a specific type, you need to write a generic function
val <T> List<T>.penultimate: T
get() = this[size - 2]
>>> println(listOf(1, 2, 3, 4).penultimate)
// output ==> 3