The article is based on the Kotlin
version "1.1-04".
This article is devoted to the description of the automatic class generation mechanism by the Kotlin
compiler for anonymous block of user code. It is described in which cases automatic classes are created, where they are located, how they are implemented and used.
In Java
there is only one way to create a code element - it is a lambda expression that can only implement a functional interface (for more details, see the Java
documentation). In Kotlin
there are several different ways to describe an object containing custom code.
It is a well-known feature of Kotlin
that it is very easy to implement code that allows writing such constructs:
fun Action( cb:()->Unit ) { cb() } fun Test() { Action{ // } }
How is this implemented and what is the risk of using such a code in a program?
PS: The original version of the article contained a logical error and incorrect conclusions. I apologize to everyone for this fact. This article has been revised and supplemented with additional data.
The description of a separate code in the JVM
not possible, you can operate only with classes. In some situations, the Kotlin
compiler automatically creates classes in which the specified code will be executed, and in some cases calls it directly. In different cases, classes of different types will be created.
Kotlin
allows you to implement constructs with anonymous code using different syntaxes. The examples below are divided by their method of use in the test programs, and not by the way they are implemented by the compiler.
In fact, almost all of the listed syntax constructs are implemented in the same way.
Anonymous function
A block of code with arbitrary parameters and a return value, which can be called directly or passed as a parameter to a function.
fun Action(cb : () -> Int) // = println(cb()) class BB { fun F() = 1133 fun M() { Action(this::F) // } } fun statMethod() = 1111 fun Test() { val cb = { 1122 } // cb() // Action(cb) // Action { 1133 } // Action(::statMethod) // }
The constructions " ::statMethod
" and " this::F
" are used to facilitate the syntax and are completely equivalent to ::statMethod
function in anonymous code:
Action{ statMethod() } // Action(::statMethod) Action{ F() } // Action(this::F)
Anonymous object function
A block of code that runs in the context of an object instance, i.e. will have a field " this
", referring to the object for which this code is called.
In the implementation of this approach in Kotlin
this
object is emulated to simplify the syntax, but in fact an implicitly passed object instance is used. It should be noted that despite the presence of the this
and accessing the class elements without specifying an explicit object, the executed code is not part of the class and, accordingly, does not have access to its protected elements.
import java.awt.event.ActionEvent import java.awt.event.ActionListener fun <T> cAction(v : T, cb : T.() -> Int) // = println(v.cb()) class BB { fun F() = 1133 // fun M() { cAction(this, BB::F) // } } fun Test() { cAction("text", String::length) // }
Lambda function
A block implemented in an automatically created class, a successor of a functional interface, for a single method of this interface.
fun add(i : ActionListener) {} // fun Test() { add(ActionListener { }) // , }
Anonymous class
Automatically created anonymous class, with the possibility of inheritance from the specified class.
fun add(i : ActionListener) {} // fun Test() { add(object : ActionListener {// , override fun actionPerformed(e : ActionEvent?) {} }) }
Let's put together all the above described methods of declaring automatic objects into one source file and compile it.
import java.awt.event.ActionEvent import java.awt.event.ActionListener fun add(i : ActionListener) {} fun <T> cAction(v : T, cb : T.() -> Int) = println(v.cb()) fun Action(cb : () -> Int) = println(cb()) fun statMethod() = 1111 class BB { fun F() = 1133 fun M() { //jm/test/ktest/BB$M$1.INSTANCE cAction(this, BB::F) //jm/test/ktest/BB$M$2 Action(this::F) //jm/test/ktest/BB$M$3 Action{F()} } } fun Test() { //jm/test/ktest/KMainKt$Test$1 add(object : ActionListener { override fun actionPerformed(e : ActionEvent?) {} }) //jm/test/ktest/KMainKt$Test$2.INSTANCE add(ActionListener { e-> }) //jm/test/ktest/KMainKt$Test$cb$1.INSTANCE val cb = { 1122 } cb() Action(cb) //jm/test/ktest/KMainKt$Test$3.INSTANCE Action { 1133 } //jm/test/ktest/KMainKt$Test$4.INSTANCE Action(::statMethod) //jm/test/ktest/KMainKt$Test$5.INSTANCE cAction("text", String::length) }
If you look in the directory where the compiler saved the created classes, you can see the following files:
File name | Size, byte |
---|---|
BB.class | 1150 |
KMainKt.class | 3169 |
BB $ M $ 1.class | 1710 |
BB $ M $ 2.class | 1379 |
BB $ M $ 3.class | 1025 |
KMainKt $ Test $ 1.class | 973 |
KMainKt $ Test $ 2.class | 910 |
KMainKt $ Test $ 3.class | 1029 |
KMainKt $ Test $ 4.class | 1450 |
KMainKt $ Test $ 5.class | 1222 |
KMainKt $ Test $ cb $ 1.class | 1035 |
It is clear where the first two files came from - these are the classes that are described in our program:
BB.class
" is a class declared in the text.KMainKt.class
" is the main class of the Kotlin
program, which includes all data and methods that are not included in the classes. The name of this file consists of the file name of the source program (in our case it is β kMain.kt
β) and the suffix β Kt
β. In our example, all the methods used are placed in this class, such as β Action
β, β cAction
β, etc.Where did the rest of the files come from?
These files contain the code of classes that the compiler generated automatically. In the program text above, in the comments, the names of the generated classes are indicated, in accordance with the place of their declaration in the program.
ATTENTION: The compiler creates a new CLASS each time, rather than a new instance of some class. Those. each time the code is used, a new class is created in the program, which is used in the only place of the program!
If you look at the source text, you can pay attention to the fact that the text, it would seem, not related to the creation of temporary objects, also leads to the generation of classes.
Action(::statMethod) // jm/test/ktest/KMainKt$Test$4.INSTANCE cAction("text", String::length) // jm/test/ktest/KMainKt$Test$5.INSTANCE
This is explained by the fact that in order to ensure the syntax used in the program text, the compiler simply creates wrapper classes that call the specified methods.
The table above shows that the generated class files are quite large, large, to the size of the code that is executed in blocks of code for which they were created. Despite the fact that in the example the volume of the actual useful code is almost zero, the volume of the created classes is almost 4 times the size of the main file of the program " KMainKt.class
". The reason for this is that a lot of additional information is stored in the class files and, if the amount of class code is small, the amount of useful information in the class file will be a small part of it.
If the code that executes the automatic classes is large, then the volume of the resulting files can be ignored. If, in most cases, the code for the automatic class is very small, then it is rather difficult to call the creation of 1-1.5Kb files for each anonymous block described in the program effective or even acceptable.
Automatically generated classes directly affect the size of the program in assembled form. With the active use of anonymous code in the program, its size can grow rapidly. In many cases, program size can play a significant role. This may concern both mobile or WEB applications, as well as applications intended for execution on personal computers.
We will conduct another experiment: check the ability of Kotlin
to optimize automatic classes.
If we consider all the places where the compiler automatically creates classes in our example, then we can assume that there is an optimization related to the fact that classes will be created only for blocks with unique code, and in all other cases objects of already existing classes will be used.
To test the optimization possibilities, simply duplicate two lines of code. One, using custom code, and the second, using fully automatic code.
Action{} Action{} Action( ::statMethod ) Action( ::statMethod )
Ideally, Kotlin
optimizes the use of the automatic class for both the first group and the second. In a good version - only for the second. Checking the uniqueness of the user code can be very difficult to implement, so we will only rely on the elementary optimization of classes created by the compiler completely in automatic mode.
I will leave the modification of the example, its compilation and viewing of the class files as an independent exercise and proceed directly to the result.
A new unique class will be created for each code description location!
Unfortunately, in the current version of Kotlin
(version 1.1-M04 is used), no optimization related to the generation of automatic classes does not exist and any use of the code will create a new class.
In Kotlin
programs, the use of anonymous code is very widespread and, without doubt, the ease of its description and use is one of the principal advantages of this language. Due to the abundance of anonymous code applications, the problem of increasing the size of a program due to the generation of classes for each code fragment can become a very big problem.
There are two ways to deal with the storage of redundant information in a file with the collected application.
The first is to use various tools that are designed to remove service information from the compiled files for the JVM
.
One of the very interesting tools of this group is ProGuard
. This is a free tool that allows you to repack a JAR
archive with the application and remove all service information from it. In addition to the simple removal of proprietary information, it does a great job of optimizing the code for the classes. As a bonus, this tool can be used as an " Obfuscator
", i.e. a tool that makes it difficult to recover application logic from its code.
This is a powerful tool, but its review goes beyond the goals of this article.
The second way (which can be combined with the first) is to use the features of the Kotlin
compiler. This feature is discussed in the next section.
Let's make experiment with our example, having changed the description of two methods.
inline fun <T> cAction(v : T, cb : T.() -> Int) = println(v.cb()) inline fun Action(cb : () -> Int) = println(cb())
In this case, we added the inline
to the write function.
After compiling the program, the list of files created by the compiler for automatic classes was reduced to three: β KMainKt$Test$1.class
β, β KMainKt$Test$2.class
β and β KMainKt$Test$cb$1.class
β. Those. there are only those classes that are not passed as parameters to the Action and cAction functions. The size of the file " KMainKt.class
" increased by about 200 bytes.
Where are the other classes?
The code of all functions declared with the inline
modifier was inserted directly into the place where the functions were called. In addition, the compiler has created separate functions with the same name. The syntax of the language will not allow using the declared functions. when you try to call it, its code will be placed in the place of use, but its static copy is created to provide access to this function through reflection
.
The block of anonymous code in Kotlin, depending on where it is used, can be implemented either with the help of automatic objects, or placed directly at the call site. In the first case, an object of a certain class will be created, the description of which will be in this section, and in the second case such a block as an object does not exist at all.
When creating automatic classes, Kotlin can create two types of objects:
Custom Type Objects
The first method is used when creating anonymous classes, lambda functions and implementing interfaces using delegates. In this case, a class will be created, the successor of the specified (explicitly or implicitly) user class and the code block will be placed inside one of the methods of this class.
fun add(i : ActionListener) {} add(object : ActionListener { override fun actionPerformed(e : ActionEvent?) {} }) add(ActionListener { e -> })
If an automatic class is created for an anonymous class, then an object for it will be built in the place of the declaration and passed as a parameter to the destination. If a class is created to implement a functional interface, then if it does not capture local variables, the previously created singleton of this class will be used. If local variables are captured, a new object will be built, to which references to all captured variables will be passed to the constructor as parameters.
// : add(ActionListener { e -> }) class AutoClassForCase1 : ActionListener { override fun actionPerformed(e : ActionEvent?) { // - } companion object { @JvmField val INSTANCE = AutoClassForCase1() } } add( AutoClassForCase1.INSTANCE ) //
Capture Local Variables
Kotlin
, in contrast to Java
, allows anonymous code to capture local variables of any type (in Java
they must be " effectively final
"), so the transfer of captured variables to an automatic class occurs through the special class " Ref<T>
". As a result of this approach, if a local variable was captured by at least one block of code, then it automatically turns into a link at the place of its use, and all references to it are made using this link. This method introduces some additional overhead in comparison with the method used in Java
, but allows you to change the values ββof variables.
When blocking a local variable, the code is:
fun Test() { val local = 10 Action{ local+1 } local + 2 }
turns into this:
fun Test() { val local = Ref<Int>(10) // Action{ local.value + 1 } // local.value + 2 // local.value = null // }
At the same time, you need to pay attention to the following points:
Objects for code block
The example described just above for calling an anonymous block of code is implemented by the compiler in approximately the following way:
// class AutoClassForMethod : Function0<Int> { // private val local : Ref<Int> // constructor( l:Ref<Int> ) { local.value = l.value } // fun invoke() : Int { // } } // Action( AutoClassForMethod(local) )
In this example, for simplicity, I lowered the layer of intermediate parameter conversions from Any
(actually Object
because this part of the Kotlin
code is written in Java
) to specific types since it does not affect understanding.
All Kotlin code blocks always have the type FunctionX<Ret[,Param1[,...]]>
, where FunctionX
is a set of template classes that implement a method call with an arbitrary number of parameters. The last template type is the return type, the first is the type of the first parameter, and so on. The symbol in the class name I used to denote the number of method parameters. Those. the method described in Kotlin as
()->Unit
will have the actual type Function0<Unit>
, and the method described as (Int,String)->Double
type Function2<Int,String,Double>
, etc.
FunctionX
templates are described in the Kotlin
library " kotlin-runtime.jar
" and can be used in your program. The syntax for describing blocks of code using the "standard" syntax in the form ()->Unit
and using the template Function0<Unit>
absolutely equivalent. You can verify this by replacing the function declaration in the original example.
fun <T> cAction(v : T, cb : Function1<T,Int>) = println(cb.invoke(v)) fun Action(cb : Function0<Int>) = println(cb.invoke())
The program will be collected and executed as before.
Please note that the syntax " T.() -> Int
" used earlier was completely transparently replaced by the use of a template with passing an extra type corresponding to the type of object for which the method will be called. Those. the difference between calling the static function and the function of the object, only in the extra parameter " this
".
What is this useful?
From the point of view of a simple user, the available syntax of the language is a little useful here.
But, from the point of view of a developer of a library, or a person implementing a complex control algorithm using dynamic lists of methods, knowing how automated objects are organized and how to handle blocks of code can be extremely useful.
β .
inline
, . Those. " ++
", . inline- Kotlin
- inline- " ++
". , .
inline
, , . , . Those. " inline
" .
inline- inline-
, " inline
" , . , " inline
", ..
class Holder { var cb : (()->Unit)? = null inline fun setup( cb:()->Unit ) { this.cb = cb // : inline- } } fun Test() { Holder().setup{ } }
inline
- inline
. , , - . , , inline-, , " noinline
".
class Holder { var cb : (()->Unit)? = null inline fun setup( cb:()->Unit, noinline delayed:(()->Unit)?=null ) { this.cb = delayed cb() CallCodeWithDelay() } fun DelayPassed() = cb?.invoke() }
++
inline
. Kotlin
. , , . , . " inline
" .
, inline
, . , , Kotlin
, Lint
, , inline
. , , .
@Suppress("NOTHING_TO_INLINE") inline fun SetupControl( ctl: Label ) { ctl.text = "Label" ctl.setSize( 100,100 ) } fun Test() { SetupControl( Label() ) }
SetupControl
"" , .
" ++
", , , inline
.
, Kotlin
return
. , inline
, .
fun Test1() : Int { Action { return 3 } }
inline
, , return
, , . Those. , " Test
". return
β .
, inline
, , .
JVM
, , . , , ..
fun <T,P> templateTest(value : T) : P { if ( value is P ) // : return value } templateTest<String,String>("text")
inline- " reified
".
inline fun <T,reified P> templateTest(value : T) : P { if ( value is P ) // return value }
. , templateTest , . , Kotlin reified
,
, , , refrection
, , reified
, .
This function has a reified type parameter and thus can only be inlined at compilation time, not called directly
inline-
, inline
, .
Kotlin
. . .
.. , .
, , . , , , Β«KotlinΒ», Β«RefΒ». , . , . inline- , ..
, , .
, inline- . Kotlin
, β .
, .
inline-, :
, , , , . -, .
, (.. -inline- ). inline- , , , , , . Those. , inline , β ..
. , .
Source: https://habr.com/ru/post/319692/