原文: Effective Java in Kotlin, item 1: Consider static factory methods instead of constructors

Book reminder

The first rule of Effective Java: developers should consider static factory methods instead of constructors. Static factory methods use static methods to generate instances of classes. The following is an example of static factory method use in Java:

Boolean trueBoolean = Boolean.valueOf(true);
String number = String.valueOf(12);
List<Integer> list = Arrays.asList(1.2.4);
Copy the code

Static factory methods are powerful methods used to modify constructors. Here are some benefits of using static factory methods:

  • Unlike constructors, static factory methods are named. The method name explains how the instance is created and what the parameters are. For examplenew ArrayList(3)3It’s not clear what it means, you can think of it as the first element of the array or the size of the array. Let’s do the static factory method insteadArrayList.withSize(3)The meaning of the expression is very clear. One advantage of a named method is that it explains the meaning of parameters or, to some extent, reveals the mechanism by which instances are created. Another benefit is to resolve confusion between constructors of the same parameter type.
  • Unlike constructors, static factory methods do not create new instances every time they are called. The caching mechanism can be used to optimize instance creation when using the static factory approach, thus improving instance creation performance. Again, we can define something like thisConnectrions.createOrNull()Such a method makes it inConnectionReturns when it cannot be establishednull
  • Unlike constructors, static factory methods can return any subtype. This feature can be used to provide a more appropriate instance in different situations, helping us hide the real instance behind the interface. In Kotlin all collections are hidden behind interfaces. Such asListOf (1, 2, 3)When run on the Kotlin/JVM platformArrayList, returns JavaScript array when running on Kotlin/JS platforms (both types implement KotlinListInterface). Often it is the interface that we are working with, and the implementation hidden under the interface is not always important. Simply put, a static factory method can return any subtype of a supertype and even change some implementations of the type.
  • Reduces the verbosity of creating instances of parameterized types. Kotlin solves this problem to some extent, because Kotlin has better type inference.

Joshua Bloch points out some disadvantages of the static factory approach:

  • Static factory methods cannot be used to construct subclasses. We need to use the superclass constructor in the subclass construction, and we can’t use static factory methods instead
  • Static factory methods are not easily distinguishable from other static methods. Such asvalueOf.of.getInstance.newInstance.getTypenewTypeThese are common names for static factory methods.

Intuitive conclusion: constructors should be used when there is a strong correlation between the constructor and the instance structure itself; Instead, use a static factory approach

In Kotlin, Kotlin changes the way the static factory approach is implemented.

Companion factory method

Static methods are not allowed in Kotlin, and static factory methods in Java are often replaced in Kotlin by Companion Factory methods. Associated factory method refers to a factory method that is put into an associated object:

class MyList {
    / /...
    companion object {
        fun of(vararg i: Int) { / *... * /}}}Copy the code

Use the same method as the static factory method:

MyList.of(1.2.3.4)
Copy the code

The companion object is actually a singleton class, which leads to the fact that the companion object can inherit from other classes. This allows us to implement multiple generic factory methods and then provide them with different classes. The Provider class is a lightweight class for dependency injection:

abstract class Provider<T> {
    var original: T? = null
    var mocked: T? = null
    abstract fun create(a): T
    fun get(a): T = mocked ? : original ? : create().apply { original =this }
    fun lazyGet(a): Lazy<T> = lazy { get()}}Copy the code

For different classes, only specific constructors need to be provided:

interface UserRepository {
    fun getUser(a): User
    companion object: Provider<UserRepository> {
        override fun create(a) = UserRepositoryImpl()
    }
}
Copy the code

We can then use userreposiroty.get () to get the instance, or val user by userrepository.lazyget () for lazy loading. You can also declare specific implementations for testing or mocking

UserRepository.mocked = object: UserRepository { / *... * / }
Copy the code

This is a huge advantage over Java, where static factory methods must be implemented manually in each class. Another way to reuse factory methods is through Interface Delegation. We can use the example above as follows:

interface Dependency<T> {
    var mocked: T?
    fun get(a): T
    fun lazyGet(a): Lazy<T> = lazy { get()}}abstract class Provider<T>(val init: ()->T): Dependency<T> {
    var original: T? = null
    override var mocked: T? = null
     
    override fun get(a): T = mocked ? : original ? : init() .apply { original =this}}interface UserRepository {
    fun getUser(a): User
    companion object: Dependency<UserRepository> by Provider({
        UserRepositoryImpl() 
    }) 
}
Copy the code

Extension factory methods

Another benefit of putting factory methods into associated objects is that we can define extension methods for associated objects. So we can add a companion factory method to an external dependency (if the external dependency defines the companion object) :

interface Tool {
   companion object { … }
}
fun Tool.Companion.createBigTool(...).: BigTool {... }Copy the code

Or a named companion

interface Tool {
   companion objectThe Factory {... }}fun Tool.Factory.createBigTool(...).: BigTool {... }Copy the code

Top-level functions

It is also common in Kotlin to use top-level functions instead of companion factory methods, such as listOf,setOf, and mapOf. Library designers also often provide top-level methods to create instances. For example, in Android development, the traditional way to create an Activity Intent is static:

// Java
class MainActivity extends Activity {
    static Intent getIntent(Context context) {
        return newIntent(context, MainActivity.class); }}Copy the code

In the Kotlin Anko library, we use the top-level method intentFor

intentFor<MainActivity>()
Copy the code

The problem with using top-level methods is that public top-level methods are universally accessible and can easily “contaminate” the IDE’s intelligent hints.

Although public top-level methods need to be used with caution, they are a good choice for small, frequently created instances (such as List maps) because listOf(1,2,3) is simpler and more readable than list.of (1,2,3).

Fake constructors

Constructors are used in Kotlin in a similar way to top-level methods (Kotlin does not require the new keyword) :

class A(a)val a = A()
Copy the code

Constructors can be referenced in the same way as top-level methods:

val aReference = ::A
Copy the code

The only difference between a constructor and a method is that the first letter of the constructor needs to be capitalized. This fact is applied in many places even to the Kotlin standard library. List and MutableList are interfaces. They have no constructor, but users of Kotlin would like to be able to:

List(3) { "$it" } // same as listOf("0", "1", "2")
Copy the code

That’s why the following method appears in collections.kt:

public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)

public inline fun <T> MutableList(size: Int, init: (index: Int) -> T): MutableList<T> {
    val list = ArrayList<T>(size)
    repeat(size) { index -> list.add(init(index)) }
    return list
}
Copy the code

Fake Constructors look like constructors, and so do usage and presentation, yet many developers don’t realize that they are not constructors but top-level methods. Fake Constructors also have the advantage of static factory methods: they can return subtypes and do not need to generate new instances with each call; Without the limitations of the constructor. For example, the secondary constructor must immediately call the primary constructor or the constructor of the superclass. We can also postpone using fake constructors when we use them:

fun ListView(config: Config) : ListView {
    valThe items =...// Here we read items from config
    return ListView(items) // We call actual constructor
}
Copy the code

Primary constructor

Kotlin introduced the concept of primary constructors. Only one primary constructor can exist in a Kotlin class (Kotlin calls java-like constructors secondary constructors). Arguments in the primary constructor can be used throughout class creation:

class Student(name: String, surname: String) {
    val fullName = "$name $surname"
}
Copy the code

Parameters in the primary constructor can be defined directly as properties of the class:

class Student(val name: String, val surname: String) {
    val fullName 
        get() = "$name $surname"
}
Copy the code

When the primary Constructor contains default parameters, the overlapping Constructor is no longer needed.

Other ways to create an object

The factory method in Kotlin is not the only way Kotlin promotes instance creation. In the next article we will discuss how Kotlin improved the Builder pattern. For example, allowing DSLS to appear during object creation:

val dialog = alertDialog {
    title = "Hey, you!"
    message = "You want to read more about Kotlin?"
    setPositiveButton { makeMoreArticlesForReader() }
    setNegativeButton { startBeingSad() }
}
Copy the code

Conclusion

In Kotlin we can use these methods while retaining the benefits of the static factory approach:

  • Companion factory method
  • Top-level function
  • Fake constructor
  • Extension factory method

In most cases, the main constructor will suffice for object creation, but if you need to use other methods to create objects, consider the above methods.