【问题标题】:why property overriding in kotlin makes primary constructor property zero为什么 kotlin 中的属性覆盖使主构造函数属性为零
【发布时间】:2020-04-09 12:13:19
【问题描述】:

我正在尝试将值传递给构造函数并打印值。

open class Car(c: Int){
    open var cost: Int = c
    init {
        println("This comes First $cost")
    }
}

open class Vehicle(cc: Int) : Car(cc) {
    override var cost: Int = 20000
    init {
        println("This comes Second $cost")
    }

    fun show(){
        println("cost = $cost")
    }
}

fun main() {
    var vehicle = Vehicle(1000)
    vehicle.show()
}

输出

This comes First 0
This comes Second 20000
cost = 20000

如果我只是评论这一行 override var cost: Int = 20000

输出是

This comes First 1000
This comes Second 1000
cost = 1000
  • 为什么重写子类中的属性时超级构造函数成本为零?
  • 我需要将其与 java 概念进行比较,以便在此处进行更好的解释

【问题讨论】:

  • @Tenfour04 很有趣,但你能详细说明你的答案吗?我没有为此找到任何资源。所以,请给出更多详细解释的答案,当然用简单的话。
  • 我修复了代码格式。在声明变量的类型时我们不需要加空格,应该是cost: Int而不是cost : Int

标签: java kotlin inheritance constructor overriding


【解决方案1】:

在Java中创建一个可变属性cost,你需要定义一个字段成本和一个getter和setter:

public class Car {

    private int cost;

    public Car(int c) {
        this.cost = c;
        System.out.println("This comes First " + getCost());
    }

    public int getCost() { return cost; }

    public void setCost(int cost) { this.cost = cost; }
}

Kotlin 在语言中嵌入了 property 的概念,因此您只需创建一个 var property 即可实现相同的目标做了:

open class Car(c : Int){
    open var cost : Int = c
    init {
        println("This comes First $cost")
    }
}

从开发人员的角度来看,这要简洁得多,但实现是相同的。 Kotlin 编译器在后台为我们生成 field 成本、get 方法set 方法。 现在有趣的部分。当您打开父类中的成本属性并在子类中覆盖它时,您实际上是在覆盖 get 方法。在 Kotlin 和 Java 中都不能覆盖字段。

正如@Pawel 在他的回答中提到的,这是 Vehicle 子类的 java 代码:

public class Vehicle extends Car {
   private int cost = 20000;

   @Override
   public int getCost() {
      return this.cost;
   }

   @Override
   public void setCost(int var1) {
      this.cost = var1;
   }

   public final void show() {
      System.out.println("cost = " + getCost());
   }

   public Vehicle(int cc) {
      super(cc);
      System.out.println("This comes Second " + getCost());
   }
}

当您在父类中执行println("This comes First $cost") 时,您实际上是在执行System.out.println("This comes First " + getCost());,而实际调用的getCost() 是子类中的Vehicle。由于子类cost字段还没有初始化,因为我们还在执行super(cc)调用,所以它的值是0。

【讨论】:

  • 你说,init 将首先运行,然后属性将被初始化。
  • init 相当于这个public Car(int c) { this.cost = c; System.out.println("This comes First " + getCost()); }。 Car 类中的属性首先被初始化,但 System.out.println 行中实际调用的 getCost() 是 Vehicle 类中的。
  • 在 Coursera 上的“Java 开发人员的 Kotlin”课程的第 4 周有一个名为“构造函数,继承语法”的视频,其中详细解释了这种行为。
  • 是的,它总是寻找覆盖方法和属性。有人分步回答了下面的执行流程。我现在明白了。
  • 这就是为什么建议构造函数永远不要调用他们自己的任何可能被覆盖的方法。 (而属性getter就是这样一种方法。)
【解决方案2】:

您是否查看过生成的字节码并尝试将其反向反编译回 java?如果您经常对 Kotlin 的底层工作原理感到困惑,它可以帮助您理解。

在这种情况下,您在 Java 中的类将如下所示(我反编译了您的代码并对其进行了一些清理):

public class Car {
   private int cost;

   public int getCost() {
      return this.cost;
   }

   public void setCost(int var1) {
      this.cost = var1;
   }

   public Car(int c) {
      this.cost = c;
      System.out.println("This comes First " + getCost());
   }
}

public class Vehicle extends Car {
   private int cost = 20000;

   public int getCost() {
      return this.cost;
   }

   public void setCost(int var1) {
      this.cost = var1;
   }

   public final void show() {
      System.out.println("cost = " + getCost());
   }

   public Vehicle(int cc) {
      super(cc);
      System.out.println("This comes Second " + getCost());
   }
}

发生的事情是open var 只是Vehicle 覆盖的setter 和getter 声明。

记住超类的初始化总是发生在子类之前,所以当Car构造函数被执行时Vehicle仍然没有被初始化(Vehicle.cost仍然是0)。

这意味着在第一种情况下:

This comes First 0   // Car constructor prints 0 because it returns Vehicle.cost which is unitialized
This comes Second 20000  // Vehicle constructor returns initialized value
cost = 20000

在第二种情况下,您不覆盖成本,CarVehicle 都返回 Car.cost

This comes First 1000  // Car constructor assigns constructor parameter to Car.cost and returns it
This comes Second 1000  // Vehicle constructor returns Car.cost as well
cost = 1000

另请注意,在第一种情况下,您的构造函数参数是无意义的:它被分配给 Car.cost 但该字段不可访问,因为它被 Vehicle.cost 遮蔽。

【讨论】:

  • 我没听懂你,我认为你需要详细说明。为什么因为,如果我只评论override var cost : Int = 20000,答案完全符合我的预期。但在取消注释时不会。所以我需要对此进行更多澄清
【解决方案3】:

简单来说。

创建对象时Vehicle(1000)
- 第一个 init 将被调用 - 然后变量被初始化

第 1 步:- 它将尝试调用 Vehicle 的构造函数
第 2 步:- 因为它继承了 Car,它将尝试首先调用 Car 的构造函数
第 3 步:- 它总是寻找覆盖方法而不是覆盖方法或属性因此,$cost 指向覆盖属性。
第 4 步:-override var cost : Int = 20000 在打印时未初始化 println("This comes First $cost") init 首先运行,然后属性将被初始化。
第 5 步:- 所以 $cost 默认为零。

试试这个,下面open var cost : Int = c
把,var costTest : Int = c。 “没有开放关键字”
然后,在 Vehicle init put 上,

println("This comes Second $cost $costTest")

在这里你会得到 costTest = 1000。

因为你没有覆盖 costTest

【讨论】:

    【解决方案4】:

    这种行为可能不直观,但它是属性如何与 JVM 一起工作的结果。

    当您继承 Car 时,Car 初始化发生在子类 Vehicle 初始化之前。所以 Car 的 init 块中的 println 调用会在 Vehicle 初始化之前访问该属性。由于这种访问相当于调用 Java 中的 getter 方法,因此它访问的是子类的 getter,而不是它自己的 getter,因为它已被覆盖。由于子类在这个阶段还没有初始化,它的getter返回支持字段的默认值,0。

    如果您使用非原始的、不可为空的属性执行此操作,您可以“欺骗” Kotlin 为您提供 Java NullPointerException,因为通常可以假定为非空。

    在任何一种情况下,默认代码检查都会警告您在初始化期间尝试访问开放属性,因为这种意外行为。

    解决方法是使用私有支持属性:

    open class Car(c : Int){
        private var _cost: Int = c
        open var cost : Int
            get() = _cost
            set(value) { _cost = value }
    
        init {
            println("This comes First $_cost")
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2013-03-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多