
Back in May 2017, Google announced that Kotlin had become the official language for Android development. Someone then heard the name of this language for the first time, someone wrote on it for a long time, but from that moment it became clear that everyone who is close to Android-development is now simply obliged to get to know it. This was followed by both enthusiastic responses “Finally!” And terrible indignation “Why do we need a new language? Than Java did not please? ", Etc. etc.
Since then, enough time has passed, and although the debate about whether good or bad Kotlin has not yet subsided, more and more Android code is written on it. And even completely conservative developers are also switching to it. In addition, the network can stumble upon information that the speed of development after mastering this language is increased by 30% compared with Java.
')
Today, Kotlin has already recovered from several childhood diseases, has acquired a lot of questions and answers to the Stack Overflow. Both his advantages and weaknesses became visible to the naked eye.
And on this wave, I had the idea to analyze in detail the individual elements of a young but popular language. Pay attention to complex issues and compare them with Java for clarity and better understanding. Understand the question somewhat deeper than this can be done by reading the documentation. If this article is of interest, then, most likely, it will initiate a whole cycle of articles. In the meantime, I'll start with pretty basic things that, nevertheless, hide a lot of pitfalls. Talk about constructors and initializers in Kotlin.
As in Java, in Kotlin, the creation of new objects — entities of a certain type — takes place by calling the class constructor. Arguments can also be passed to the constructor, and there can be several constructors. If you look at this process as if from the outside, then here the only difference from Java is the absence of the keyword new when invoking the constructor. Now let's take a deeper look and see what happens inside the class.
A class can have primary (primary) and additional (secondary) constructors.
The constructor is declared using the constructor keyword. If the primary constructor does not have access modifiers and annotations, the keyword can be omitted.
A class may not have explicit constructors. In this case, after the class declaration there are no constructions, we immediately go to the body of the class. If we draw an analogy with Java, this is equivalent to the absence of an explicit declaration of constructors, with the result that the default constructor (without parameters) will be generated automatically at the compilation stage. It looks, expected, like this:
class MyClassA
This is equivalent to the following record:
class MyClassA constructor()
But if you write this way, then you will be politely asked to remove the primary constructor without parameters.
The primary constructor is the one that is always called when an object is created, if there is one. For the time being, we take note of this, and analyze it in more detail later, when we proceed to the secondary constructors. Accordingly, we remember that if there are no designers at all, then in fact there is one (primary) one, but we don’t see it.
If, for example, we want the primary constructor without parameters to not have public access, then, together with the
private
modification, it will already be necessary to declare it explicitly with the
constructor
keyword.
The main feature of the primary constructor is that it has no body, i.e. cannot contain executable code. It simply takes the parameters and passes them deep into the class for further use. At the syntax level, it looks like this:
class MyClassA constructor(param1: String, param2: Int, param3: Boolean)
Parameters passed in this way can be used for various initializations, but no more. In pure form, we cannot use these arguments in the working class code. However, we can initialize the class fields right here. It looks like this:
class MyClassA constructor(val param1: String, var param2: Int, param3: Boolean)
Here,
param1
and
param2
can be used in the code as class fields, which is equivalent to the following:
class MyClassA constructor(p1: String, p2: Int, param3: Boolean)
Well, if we compare it with Java, it would look like this (and by the way, using this example, we can estimate how much Kotlin can reduce the amount of code):
public class MyClassAJava { private final String param1; private Integer param2; public MyClassAJava(String p1, Integer p2, Boolean param3) { this.param1 = p1; this.param2 = p2; } public String getParam1() { return param1; } public Integer getParam2() { return param2; } public void setParam2(final Integer param2) { this.param2 = param2; }
Let's talk about additional constructors. They are more like regular constructors in Java: they take parameters, and they can have an executable block. When declaring additional constructors, the constructor keyword is required. As mentioned earlier, despite the possibility of creating an object through a call to an additional constructor, the primary constructor (if any) should also be called with the help of the
this
. At the syntax level, it is organized like this:
class MyClassA(val p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) {
Those. the additional constructor is, as it were, the successor of the primary
Now, if we create an object by calling an additional constructor, the following will occur:
call an additional constructor;
call the main constructor;
initialization of the
p1
class field in the main constructor;
code execution in the body of an additional constructor.
This is similar to this in Java:
class MyClassAJava { private final String param1; public MyClassAJava(String p1) { param1 = p1; } public MyClassAJava(String p1, Integer p2, Boolean param3) { this(p1);
Recall that in Java we can call one constructor from another using the
this
only at the beginning of the constructor body. In Kotlin, this question was decided cardinally - they made such a call a part of the constructor signature. Just in case, I note that it is forbidden to call any (primary or additional) constructor directly from the additional body.
An additional constructor should always refer to the main one (if available), but can do it indirectly, referring to another additional constructor. The bottom line is that at the end of the chain we still get to the main thing. The operation of the constructors will obviously occur in the reverse order of the constructors turning to each other:
class MyClassA(p1: String) constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3)
Now the sequence is:
- call additional constructor with 4 parameters;
- call an additional constructor with 3 parameters;
- call the primary constructor;
- initialization of the p1 class field in the primary constructor;
- code execution in the constructor body with 3 parameters;
- code execution in the body of the constructor with 4 parameters.
In any case, the compiler will never let us forget to get to the primary constructor.
It so happens that a class does not have a primary constructor, and it may have one or more additional ones. Then additional constructors are not required to refer to someone, but they can refer to other additional constructors of this class. Earlier, we found out that the main constructor, not explicitly specified, is automatically generated, but this applies to cases when there are no constructors in the class at all. If there is at least one additional constructor, the primary constructor without parameters is not created:
class MyClassA {
We can create a class object by calling:
val myClassA = MyClassA()
In this case:
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) {
We can create an object only with such a call:
val myClassA = MyClassA(“some string”, 10, True)
In this regard, in Kotlin, compared with Java, there is nothing new.
By the way, like the primary constructor, an additional one may not have a body if its task is only to transfer parameters to other constructors.
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "") constructor(p1: String, p2: Int, p3: Boolean, p4: String) {
You should also pay attention to the fact that, unlike the primary constructor, initialization of the class fields in the list of arguments of the additional constructor is prohibited.
Those. Such a record will be invalid:
class MyClassA { constructor(val p1: String, var p2: Int, p3: Boolean){
Separately, it is worth noting that the additional constructor, like the primary constructor, may well be without parameters:
class MyClassA { constructor(){
Speaking of constructors, one can not but mention one of the convenient features of Kotlin - the ability to assign default values ​​for the arguments.
Now suppose that we have a class with several constructors that have different numbers of arguments. I will give an example in Java:
public class MyClassAJava { private String param1; private Integer param2; private boolean param3; private int param4; public MyClassAJava(String p1) { this (p1, 5); } public MyClassAJava(String p1, Integer p2) { this (p1, p2, true); } public MyClassAJava(String p1, Integer p2, boolean p3) { this(p1, p2, p3, 20); } public MyClassAJava(String p1, Integer p2, boolean p3, int p4) { this.param1 = p1; this.param2 = p2; this.param3 = p3; this.param4 = p4; }
As practice shows, such designs are quite common. Let's see how you can write the same thing on Kotlin:
class MyClassA (var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){
Now let's unite Kotlin together for how much he cut the code. By the way, in addition to reducing the number of rows, we get more order. Remember, for sure you have seen something like this:
public MyClassAJava(String p1, Integer p2, boolean p3) { this(p3, p1, p2, 20); } public MyClassAJava(boolean p1, String p2, Integer p3, int p4) {
When you see this, you want to find the person who wrote it, take it by the button, lead to the screen and ask in a sad voice: “Why?”
Although you can repeat this feat on Kotlin, but not necessary.
There is, however, one detail that should be taken into account in the case of such an abbreviated record on Kotlin: if we want to call the constructor with default values ​​from Java, we must add the
@JvmOverloads
annotation to it:
class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20)
Otherwise, we get an error.
Now let's talk
about initializers .
The initializer is a block of code marked with the
init
keyword. In this block, you can perform some logic on the initialization of class elements, including using the values ​​of the arguments that came to the primary constructor. We can also call functions from this block.
Java also has initialization blocks, but this is not the same thing. In them we cannot, as in Kotlin, transfer the value from the outside (arguments of the primary constructor). The initializer is very similar to the body of the primary constructor, rendered in a separate block. But it is at a glance. In fact this is not true. Let's figure it out.
An initializer may also exist when there is no primary constructor. If this is the case, then its code, like all initialization processes, is executed before the code of the additional constructor. There may be more than one initializer. In this case, the order of their call will coincide with the order of their location in the code. Also note that initialization of class fields may occur outside the
init
blocks. In this case, the initialization also occurs in accordance with the arrangement of the elements in the code, and this must be taken into account when calling methods from the initializer block. If you take this carelessly, then there is the likelihood of running into a mistake.
I will give some interesting cases of work with initializers.
class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } var testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } }
This code is quite valid, although not completely obvious. If you look at it, you can see that assigning a value to the
testParam
field in the initializer block occurs before the parameter is declared. By the way, this only works if we have an additional constructor in the class, but we don’t have a primary constructor (if we raise the declaration of the
testParam
field above the
init
block, it will work without the constructor). If we decompile byte code of this class in Java, we get the following:
public class MyClassB { @NotNull private String testParam = "some string"; @NotNull public final String getTestParam() { return this.testParam; } public final void setTestParam(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.testParam = var1; } public final void showTestParam() { Log.i("wow", "in showTestParam testParam = " + this.testParam); } public MyClassB() { this.showTestParam(); this.testParam = "new string"; this.testParam = "after"; Log.i("wow", "in constructor testParam = " + this.testParam); } }
Here we see that the first call to the field during initialization (in the
init
block or out of it) is equivalent to its usual initialization in Java. All other actions related to the assignment of a value in the initialization process, except for the first one (the first assignment of the value is combined with the declaration of the field) are transferred to the constructor.
If you conduct experiments with decompilation, it turns out that if there is no constructor, then the primary constructor is generated, and all the magic happens in it. If there are several additional constructors that do not refer to each other, and there is no primary constructor, then in the Java code of this class, all subsequent assignments of the value to the
testParam
field
testParam
duplicated in all additional constructors. If the primary constructor is there, then only in the primary one. Phew ...
And the most interesting thing for a snack: change the
testParam
signature from
var
to
val
:
class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } val testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } }
And somewhere in the code we call:
MyClassB myClassB = new MyClassB();
Everything compiled without errors, started, and here we see the output of the logs:
in showTestParam testParam = some string
in constructor testParam = after
It turns out that the field declared as
val
changed the value in the code execution process. Why is that? I think that this is a shortcoming of the Kotlin compiler, and in the future, perhaps this will not compile, but today everything is as it is.
Drawing conclusions from the above cases, you can only advise not to produce initialization blocks and not scatter them in the class, avoid re-assignment of values ​​in the initialization process, call only pure functions from the init-blocks. All this is done in order to avoid possible confusion.
So.
Initializers are a kind of code block that is necessarily executed when an object is created, regardless of which constructor this object is created with.It seems to understand. Consider the interaction of constructors and initializers. Within one class, everything is quite simple, but you need to remember:
- call an additional constructor;
- call the primary constructor;
- initialization of class fields and initializer blocks in the order of their location in the code;
- code execution in the body of an additional constructor.
More interesting are the cases with inheritance.
It is worth noting that as Object is the base for all classes in Java, so Any is such in Kotlin. However, Any and Object are not the same thing.
For a start on how inheritance occurs. A descendant class, like the parent class, may or may not have a primary constructor, but must refer to a specific constructor of the parent class.
If a descendant class has a primary constructor, then this constructor must point to a specific constructor of the base class. In this case, all additional constructors of a class of successor should refer to the main constructor of its class.
class MyClassC(p1: String): MyClassA(p1) { constructor(p1: String, p2: Int): this(p1) { //some code } //some code }
If a descendant class does not have a primary constructor, each of the additional constructors must refer to the constructor of the parent class using the keyword
super
. At the same time, various additional constructors of the heir class can refer to different constructors of the parent class:
class MyClassC : MyClassA constructor(p1: String, p2: Int): super(p1, p2)
Also, do not forget about the possibility of indirectly calling the constructor of the parent class through other constructors of the heir class:
class MyClassC : MyClassA constructor(p1: String, p2: Int): this (p1)
If the heir class has no constructors, then simply add a call to the constructor of the parent class after the heir class name:
class MyClassC: MyClassA(“some string”) {
However, there is still a variant with inheritance, in which reference to the constructor of the parent class is not required. Such a record is valid:
class MyClassC : MyClassB constructor(p1: String)
But only if the parent class has a parameterless constructor, which is the default constructor (primary or optional - not important).
Now consider the order of invoking initializers and constructors during inheritance:
- call the additional constructor of the successor;
- call the primary constructor of the heir;
- call an additional parent constructor;
- calling the parent's primary constructor;
- execution of parent parent
init
blocks; - Executing the body code of an additional parent constructor
- execution of the heir's
init
block - code execution of the body of the additional constructor of the heir
Let's talk more about comparison with Java, in which, in fact, there is no analogue of the primary constructor from Kotlin. In Java, all designers are equal and may or may not be called from each other. In Java and in Kotlin there is a default constructor, it is also a constructor without parameters, but it acquires a special status only during inheritance. Here you should pay attention to the following: when inheriting in Kotlin, we must explicitly indicate to the heir class which parent class constructor to use - the compiler will not let us forget about it. In Java, we can not explicitly specify this. Be careful: in this case, the default constructor of the parent class (if any) will be called.
At this stage, we will assume that we studied the designers and initializers quite deeply and now we know almost everything about them. Let's take a little rest and dig in the other direction!