Kotlin: digging deeper. Constructors and initializers



    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 managed to recover from several childhood diseases, has acquired a large number 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:

    classMyClassA

    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 designer without parameters not to have public access, then, together with the modification private, it will already be necessary to declare it explicitly with the keyword constructor.

    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){
      // some code
    }

    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){
      // some code
    }

    Here param1and param2you can use the code as a class field, which is equivalent to the following:

    class  MyClassA constructor(p1: String, p2: Int, param3: Boolean){
      val param1 = p1
      var param2 = p2
      // some code
    }

    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):

    publicclassMyClassAJava{
      privatefinal 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;
      }
      // some code
    }
    

    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 keyword is constructorrequired. 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 a keyword this. At the syntax level, it is organized like this:

    classMyClassA(val p1: String) {
      constructor(p1: String, p2: Int, p3: Boolean) : this(p1) {
         // some code
      }
      // some code
    }
    

    Those. the additional constructor is, as it were, the successor of the primary one.
    Now, if we create an object by calling an additional constructor, the following will occur: a

    call to an additional constructor;
    call the main constructor;
    initialization of the class field p1in the main constructor;
    code execution in the body of an additional constructor.

    This is similar to this in Java:

    classMyClassAJava{
      privatefinal String param1;
      publicMyClassAJava(String p1){
         param1 = p1;
      }
      publicMyClassAJava(String p1, Integer p2, Boolean param3){
         this(p1);
         // some code
      }
      // some code
    }
    

    Recall that in Java we can call one constructor from another using a keyword thisonly 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) : this(p1) {
         // some code
      }constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3) {
         // some code
      }// some code
    }
    

    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:

    classMyClassA{
    // some code
    } 
    

    We can create a class object by calling:

    val myClassA = MyClassA()

    In this case:

    classMyClassA{
      constructor(p1: String, p2: Int, p3: Boolean)  {
         // some code
      }
      // some code
    }
    

    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.

    classMyClassA{
      constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "")
      constructor(p1: String, p2: Int, p3: Boolean, p4: String) {
         // some code
      }
      // some code
    }
    

    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:

    classMyClassA{
      constructor(val p1: String, var p2: Int, p3: Boolean){
         // some code
      }
      // some code
    }
    

    Separately, it is worth noting that the additional constructor, like the primary constructor, may well be without parameters:

    classMyClassA{
      constructor(){
         // some code
      }
      // some code
    }
    

    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:

    publicclassMyClassAJava{
      private String param1;
      private Integer param2;
      privateboolean param3;
      privateint param4;
      publicMyClassAJava(String p1){
         this (p1, 5);
      }
      publicMyClassAJava(String p1, Integer p2){
         this (p1, p2, true);
      }
      publicMyClassAJava(String p1, Integer p2, boolean p3){
         this(p1, p2, p3, 20);
      }
      publicMyClassAJava(String p1, Integer p2, boolean p3, int p4){
         this.param1 = p1;
         this.param2 = p2;
         this.param3 = p3;
         this.param4 = p4;
      }
    // some code
    }
    

    As practice shows, such designs are quite common. Let's see how you can write the same thing on Kotlin:

    classMyClassA(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){
      // some code 
    }
    

    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:

    publicMyClassAJava(String p1, Integer p2, boolean p3){
         this(p3, p1, p2, 20);
      }
      publicMyClassAJava(boolean p1, String p2, Integer p3, int p4){
      // some code 
      }
    

    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, you don’t.

    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 a constructor with default values ​​from Java, then we need to add an annotation to it @JvmOverloads:

    class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){
      // some code
    }

    Otherwise, we get an error.

    Now let's talk about initializers .

    An initializer is a block of code marked with a keyword init. 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 blocks init. 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.

    classMyClassB{
      init {
         testParam = "some string"
         showTestParam()
      }
      init {
         testParam = "new string"
      }
      var testParam: String = "after"constructor(){
         Log.i("wow", "in constructor testParam = $testParam")
      }
      funshowTestParam(){
         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 the assignment of a value to a field testParamin 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 field declaration testParamabove the block init, it will work without the constructor). If we decompile byte code of this class in Java, we get the following:

    publicclassMyClassB{
      @NotNullprivate String testParam = "some string";
      @NotNullpublicfinal String getTestParam() {
         returnthis.testParam;
      }
      publicfinal void setTestParam(@NotNull String var1) {
         Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
         this.testParam = var1;
      }
      publicfinal 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 a field during initialization (in a block initor outside 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 field are testParamduplicated in all additional constructors. If the primary constructor is there, then only in the primary one. Phew ...

    And for a snack the most interesting: change the signature testParamfrom varto val:

    classMyClassB{
      init {
         testParam = "some string"
         showTestParam()
      }
      init {
         testParam = "new string"
      }
      val testParam: String = "after"constructor(){
         Log.i("wow", "in constructor testParam = $testParam")
      }
      funshowTestParam(){
         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 process of code execution. 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.

    classMyClassC(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 a 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): super(p1) {
         //some code
      }constructor(p1: String, p2: Int): super(p1, p2) {
         //some code
      }//some code
    }
    

    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): super(p1){
         //some code
      }constructor(p1: String, p2: Int): this (p1){
         //some code
      }//some code
    }
    

    If the heir class has no constructors, then simply add a call to the constructor of the parent class after the heir class name:

    classMyClassC: MyClassA(“some string”) {
      //some code
    }
    

    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(){
         //some code
      }constructor(p1: String){
      }//some code
    }
    

    But only if the parent class has a parameterless constructor, which is the default constructor (the primary or optional class is 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 initparent blocks ;
    • execution of the body code of the additional constructor of the parent;
    • execution of the initheir block ;
    • code execution of the body of the additional constructor of the heir.

    Let's talk more about the 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!

    Also popular now: