
class LinkedList<T>( val head: T, val tail: LinkedList<T>? ) val list = LinkedList(1, LinkedList(2, null))
LinkedList from the above snippet. We might want to create it:- Based on a set of items passed with the
varargparameter. - From a collection of a different type, like
ListorSet. - From another instance of the same type.
linkedListOf, toLinkedList or copy), and here are a few reasons why:-
Unlike constructors, functions have names. Names explain how an object is created and what the arguments are. For example, let’s say that you see the following code:
ArrayList(3). Can you guess what the argument means? Is it supposed to be the first element in the newly created list, or is it the initial capacity of the list? It is definitely not self-explanatory. In such a situation, a name likeArrayList.withCapacity(3)would clear up any confusion. Names are really useful: they explain arguments or characteristic ways of object creation. Another reason to have a name is that it solves potential conflicts between constructors with the same parameter types. -
Unlike constructors, functions can return an object of any subtype of their return type. This is especially important when we want to hide actual object implementations behind an interface. Think of
listOffrom stdlib. Its declared return type isList, which is an interface. But what does this really return? The answer depends on the platform we use. It is different for Kotlin/JVM, Kotlin/JS, and Kotlin/Native because they each use different built-in collections. This is an important optimization that was implemented by the Kotlin team. It also gives Kotlin creators much more freedom. The actual type of a list might change over time, but as long as new objects still implement theListinterface and act the same way, everything will be fine. Another example islazythat declaresLazyinterface as its result type, and depending on thead safety mode, in JVM it returns eitherSynchronizedLazyImpl,SafePublicationLazyImplorUnsafeLazyImpl. Each of those classes is private, so their implementations are protected. -
Unlike constructors, functions are not required to create a new object each time they’re invoked. This can be helpful because when we create objects using functions, we can include a caching mechanism to optimize object creation or to ensure object reuse for some cases (like in the Singleton pattern). We can also define a static factory function that returns
nullif the object cannot be created, likeConnections.createOrNull(), which returnsnullwhenConnectioncannot be created for some reason. - Factory functions can provide objects that might not yet exist. This is intensively used by creators of libraries that are based on annotation processing. In this way, programmers can operate on objects that will be generated or used via a proxy without building the project.
-
When we define a factory function outside an object, we can control its visibility. For instance, we can make a top-level factory function accessible only in the same file (
privatemodifier) or in the same module (internalmodifier). - Factory functions can be inlined, so their type parameters can be reified[^33_5]. Libraries use this to provide a more convenient API.
- A constructor needs to immediately call a constructor of a superclass or a primary constructor. When we use factory functions, we can postpone constructor usage. That allows us to include a more complex algorithm in object creation.
listOf, toList, List, etc. Those are all factory functions. Let's learn about the most important kinds of factory functions and their conventions:- Companion object factory functions
- Top-level factory functions
- Builders
- Conversion methods
- Copying methods
- Fake constructors
- Methods in factory classes
Files). Since the majority of the Kotlin community originated in Java, it has become popular to mimic this practice by defining factory functions in companion objects:class LinkedList<T>( val head: T, val tail: LinkedList<T>? ) { companion object { fun <T> of(vararg elements: T): LinkedList<T> { /*...*/ } } } // Usage val list = LinkedList.of(1, 2)
class LinkedList<T>( val head: T, val tail: LinkedList<T>? ) : MyList<T> { // ... } interface MyList<T> { // ... companion object { fun <T> of(vararg elements: T): MyList<T> { // ... } } } // Usage val list = MyList.of(1, 2)
JvmStatic annotation before the function, and you can easily use such a function in Groovy or Java in the same way as you use it in Kotlin.List.of is longer than listOf because it requires applying a suggestion two times instead of one. A companion object factory function needs to be defined in a companion object, while a top-level function can be defined anywhere.interface Tool { companion object { /*...*/ } } fun Tool.Companion.createBigTool(/*...*/): Tool { //... } val tool = Tool.createBigTool()
-
from- A type-conversion function that expects a single argument and returns a corresponding instance of the same type, for example:
val date: Date = Date.from(instant) -
of- An aggregation function that takes multiple arguments and returns an instance of the same type that incorporates them, for example:
val faceCards: Set<Rank> = EnumSet.of(JACK, QUEEN, KING) -
valueOf- A more verbose alternative tofromandof, for example:
val prime: BigInteger = BigInteger.valueOf(Integer.MAX_VALUE) -
instanceorgetInstance- Used in singletons to get the object instance. When parameterized, it will return an instance parameterized by arguments. Often, we can expect the returned instance to always be the same when the arguments are the same, for example:
val luke: StackWalker = StackWalker.getInstance(options) -
createInstanceornewInstance- LikegetInstance, but this function guarantees that each call returns a new instance, for example:
val newArray = Array.newInstance(classObject, arrayLen) -
get{Type}- LikegetInstance, but used if the factory function is in a different class. Type is the type of the object returned by the factory function, for example:
val fs: FileStore = Files.getFileStore(path) -
new{Type}- LikenewInstance, but used if the factory function is in a different class. Type is the type of object returned by the factory function, for example:
val br: BufferedReader = Files.newBufferedReader(path)
listOf suggests that is creates a list from a set of elements, so it is an aggregation function. createViewModel suggests that it creates a new instance of a ViewModel, so it is a new{Type} function.abstract class ActivityFactory { abstract fun getIntent(context: Context): Intent fun start(context: Context) { val intent = getIntent(context) context.startActivity(intent) } fun startForResult( activity: Activity, requestCode: Int ) { val intent = getIntent(activity) activity.startActivityForResult( intent, requestCode ) } } class MainActivity : AppCompatActivity() { //... companion object : ActivityFactory() { override fun getIntent(context: Context): Intent = Intent(context, MainActivity::class.java) } } // Usage val intent = MainActivity.getIntent(context) MainActivity.start(context) MainActivity.startForResult(activity, requestCode)
CoroutineContext.Key interface, which serves as a key we use to identify this context[^33_2].listOf, setOf, mapOf, lazy, sequence, flow, etc.fun <T> lazy(mode: LazyThreadSafetyMode, init:() -> T):Lazy<T>= when (mode) { SYNCHRONIZED -> SynchronizedLazyImpl(init) PUBLICATION -> SafePublicationLazyImpl(init) NONE -> UnsafeLazyImpl(init) }
fun createRetrofitService(baseUrl: String): Retrofit { return Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .build() } fun <T> createService(clazz: Class<T>, baseUrl: String): T { val retrofit = createRetrofitService(baseUrl) return retrofit.create(clazz) }
List or Map because listOf(1,2,3) is simpler and more readable than List.of(1,2,3). However, public top-level functions need to be used judiciously. Public top-level functions have a disadvantage: they are available everywhere, therefore it is easy to clutter up the developer’s IDE tips. This problem becomes more serious when top-level functions have the same names as class methods and therefore get confused with them. This is why top-level functions should be named wisely.val list = buildList { add(1) add(2) add(3) } println(list) // [1, 2, 3] val s = sequence { yield("A") yield("B") yield("C") } println(s.toList()) // [A, B, C]
// Starting a coroutine scope.launch { val processes = repo.getActiveProcesses() for (process in processes) { launch { process.start() repo.markProcessAsDone(process.id) } } } // Defining a flow val flow = flow { var lastId: String = null do { val page = fetchPage(lastId) emit(page.data) lastId = page.lastId } while (!page.isLast) }
val user = UserBuilder() .withName("Marcin") .withSurname("Moskala") .withAge(30) .build()
List to Sequence, from Int to Double, from RxJava Observable to Flow, etc. For all these, the standard way is to use conversion methods. Conversion methods are methods used to convert from one type to another. They are typically named to{Type} or as{Type}. For example:val sequence: Sequence = list.asSequence() val double: Double = i.toDouble() val flow: Flow = observable.asFlow()
to prefix means that we are actually creating a new object of another type. For instance, if you call toList on a Sequence, you will get a new List object, which means that all elements of the new list are calculated and accumulated into a newly created list when this function is called. The as prefix means that the newly created object is a wrapper or an extracted part of the original object. For example, if you call asSequence on a List, the result object will be a wrapper around the original list. Using as conversion functions is more efficient but can lead to synchronization problems or unexpected behavior. For example, if you call asSequence on a MutableList, you will get a Sequence that references the original list.fun main() { val seq1 = sequence<Int> { repeat(10) { print(it) yield(10) } } seq1.asSequence() // Nothing printed seq1.toList() // Prints 0123456789 val l1 = mutableListOf(1, 2, 3, 4) val l2 = l1.toList() val seq2 = l1.asSequence() l1.add(5) println(l2) // Prints [1, 2, 3, 4] println(seq2.toList()) // Prints [1, 2, 3, 4, 5] }
UserJson and User in an example application. Such methods are often defined as extension functions.class User( val id: UserId, val name: String, val surname: String, val age: Int, val tokens: List<Token> ) class UserJson( val id: UserId, val name: String, val surname: String, val age: Int, val tokens: List<Token> ) fun User.toUserJson() = UserJson( id = this.id, name = this.name, surname = this.surname, age = this.age, tokens = this.tokens ) fun UserJson.toUser() = User( id = this.id, name = this.name, surname = this.surname, age = this.age, tokens = this.tokens )
copy. When you need to apply a change to this object, a good name starts with with and the name of the property that should be changed (like withSurname).val user2 = user.copy() val user3 = user.withSurname(newSurname)
copy method, which can modify any primary constructor property, as we will see in Item 37: Use the data modifier to represent a bundle of data.class A fun b() = A() val a1 = A() val a2 = b()
val reference: () -> A = ::A
List and MutableList are interfaces. They cannot have constructors, but Kotlin developers wanted to allow the following List construction:List(4) { "User$it" } // [User0, User1, User2, User3]
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 }
- Hiding the actual implementation behind an interface (see
Job,CoroutineScope,Mutexfrom kotlinx.coroutines). - Depending on arguments, a different implementation can be returned, optimized for the given case (see
Channelfrom kotlinx.coroutines). - An algorithm can be used to create an object, which is not possible with a constructor (see
ListandMutableList).
fun Job(parent: Job? = null): CompletableJob = JobImpl(parent) fun CoroutineScope(context: CoroutineContext): CoroutineScope= ContextScope( if (context[Job] != null) context else context + Job() )
invoke operator. Take a look at the following example:class Tree<T> { companion object { operator fun <T> invoke( size: Int, generator: (Int) -> T ): Tree<T> { //... } } } // Usage Tree(10) { "$it" }
Tree.invoke(10) { "$it" }
invoke operator in this way is inconsistent with its name. More importantly, this approach is more complicated than just a top-level function. Just compare what reflection looks like when we reference a constructor, a fake constructor, and the invoke function in a companion object:val f: ()->Tree = ::Tree
val f: ()->Tree = ::Tree
val f: ()->Tree = Tree.Companion::invoke
data class Student( val id: Int, val name: String, val surname: String ) class StudentsFactory { var nextId = 0 fun next(name: String, surname: String) = Student(nextId++, name, surname) } val factory = StudentsFactory() val s1 = factory.next("Marcin", "Moskala") println(s1) // Student(id=0, name=Marcin, Surname=Moskala) val s2 = factory.next("Igor", "Wojda") println(s2) // Student(id=1, name=Igor, Surname=Wojda)
class UserFactory( private val uuidProvider: UuidProvider, private val timeProvider: TimeProvider, private val tokenService: TokenService, ) { fun create(newUserData: NewUserData): User { val id = uuidProvider.next() return User( id = id, creationTime = timeProvider.now(), token = tokenService.generateToken(id), name = newUserData.name, surname = newUserData.surname, // ... ) } }
- Companion object factory functions
- Top-level factory functions (including fake constructors and builders)
- Conversion functions
- Methods on factory classes
[^33_2]: This mechanism is better explained in my Kotlin Coroutines book.
[^33_3]: This will be explained soon in Item 34: Consider defining a DSL for complex object creation.
[^33_5]: Reified type parameters are explained in Item 51: Use the inline modifier for functions with parameters of functional types.