Let’s see how we can apply Joshua Bloch’s Effective Java in the Kotlin world, starting with Creating and Destroying Objects.
Item 1: Consider static factory methods instead of constructors
In Kotlin, we can e.g. use the companion object to achieve this:
class MyFragment {
companion object {
// add @JvmStatic annotation to turn them into static methods if needed
fun newInstance(arg1: Int, arg2: String) {
...
}
}
}
Or we can simply use a function, like those in the Kotlin standard libraries, e.g.:
fun <T> listOf(vararg elements: T): List<T> {
...
}
Item 2: Consider a builder when faced with many constructor parameters
For constructors or functions written in Kotlin, we can simply use named parameters. For example, if your Widget
class has a constructor that accepts four integer arguments:
class Widget(x: Int, y: Int, width: Int, height: Int) {
...
}
You can call it like below:
val widget = Widget(
x = 12,
y = 34,
width = 56,
height = 78
)
Or we can create a simple DSL like below:
data class Name(var firstName: String, var lastName: String = "")
data class UserInfo(val name: Name = Name(""), var password: String = "") {
fun name(block: Name.() -> Unit) = name.block()
}
fun userInfo(block: UserInfo.() -> Unit): UserInfo = UserInfo().apply(block)
fun main() {
val user = userInfo {
name {
firstName = "Xizhi"
lastName = "Zhu"
}
password = "super secure password"
}
}
Item 3: Enforce the singleton property with a private constructor or an enum type
Kotlin provides a built-in way to create a singleton by using the object
keyword:
object Singleton {
fun myFunction() {
...
}
}
Singleton.myFunction()
Item 4: Enforce noninstantiability with a private constructor
In Kotlin, you can just use top-level functions to achieve this. For example, the standard library has a bunch of utility functions to create lists:
// creates an empty read-only list
public fun <T> emptyList(): List<T> = EmptyList
// creates a read-only list containing the given elements
public fun <T> listOf(vararg elements: T): List<T> =
if (elements.size > 0) elements.asList() else emptyList()
// creates an empty mutable list
public inline fun <T> mutableListOf(): MutableList<T> = ArrayList()
Item 5: Prefer dependency injection to hardwiring resources
There isn’t anything special for Kotlin for this item, keep using Dagger or any other dependency injection tools you prefer.
Item 6: Avoid creating unnecessary objects
Here’re just some of the scenarios, far from an exhaustive list.
Primitive types
When using “primitive types”, try to avoid nullable types, because non-nullable types like Int
will be translated to primitive types like int
, but nullable types like Int?
will be translated to boxed types like Integer
.
Similarly, choose primitive type arrays over the generic one, because IntArray
will be translated to int[]
, but Array<Int>
will be translated to Integer[]
.
Iterations
The basic for-loops are optimized with zero overhead. For example:
for (i in 0..10) {
println(i)
}
// The above Kotlin code will be translated to:
int i = 0;
for(byte var1 = 10; i <= var1; ++i) {
boolean var2 = false;
System.out.println(i);
}
It’s the same for downTo
and until
. However, it gets ugly when you apply step
there:
for (i in 0..10 step 2) {
println(i)
}
// The above Kotlin code will be translated to:
byte var3 = 0;
IntProgression var10000 = RangesKt.step((IntProgression)(new IntRange(var3, 10)), 2);
int i = var10000.getFirst();
int var1 = var10000.getLast();
int var2 = var10000.getStep();
if (var2 >= 0) {
if (i > var1) {
return;
}
} else if (i < var1) {
return;
}
while(true) {
boolean var4 = false;
System.out.println(i);
if (i == var1) {
return;
}
i += var2;
}
On the other hand, if you use forEach
, you’ll see it’s will be translated into more complicated code than the basic for-loops:
(0..10).forEach {
println(it)
}
// The above Kotlin code will be translated to:
byte var0 = 0;
Iterable $this$forEach$iv = (Iterable)(new IntRange(var0, 10));
int $i$f$forEach = false;
Iterator var2 = $this$forEach$iv.iterator();
while(var2.hasNext()) {
int element$iv = ((IntIterator)var2).nextInt();
int var5 = false;
boolean var6 = false;
System.out.println(element$iv);
}
Lambda
As expected, lambda comes with a cost, for example:
fun higherOrderFunction(f: () -> Unit) {
f()
}
// And you use it like this:
val a = 0
higherOrderFunction {
println(a)
}
The code that calls the higher order function will be translated to something below:
final int a = 0;
higherOrderFunction((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
int var1 = a;
boolean var2 = false;
System.out.println(var1);
}
}));
To optimize this, you can inline the function, then the calling code will become this:
int a = 0;
int $i$f$higherOrderFunction = false;
int var2 = false;
boolean var4 = false;
System.out.println(a);
Item 7: Eliminate obsolete object references
There isn’t anything special for Kotlin for this item. Always remember to nullify references which are no longer needed.
Item 8: Avoid finalizers and cleaners
Actually, Kotlin’s Any
class does NOT provide a finalize()
method (though you can still override it). Please refer to Item 9 on how to release non-memory resources in Kotlin.
Item 9: Prefer try-with-resources to try-finally
There is no try-with-resources in Kotlin, but we can use the use()
extension function for this purpose.
To release non-memory resources in Kotlin, just have your class implement AutoCloseable
, and requires its client to use the use()
extension function:
class MyClass: AutoCloseable {
override fun close() {
...
}
}
// You can use it like this to ensure close() is called:
MyClass().use {
...
}