是否可以恢复父 class 的重写方法?

Is it possible to recover a parent class's overridden method?

我现在正在摆弄物体,当我超过骑车 toString() 时注意到了一些东西以提高可读性。观察这些 classes 和结果。

    class Point {
        int x;
        int y;
        Point(int x, int y) {
            this.x = x; this.y = y;
        }
        public String toString() {
            return String.format("(%d,%d)",x,y);
        }
    }
    class Line extends Point {
        int l;
        Line(int x, int y, int l) {
            super(x,y);
            this.l = l;
        }
        public String toString() {
            return String.format("Length is %d", this.l);
        }
    }
JShell REPL:
> new Point(0,0)
==> (0,0)

> new Line(0,1,4)
==> Length is 4

> Point p = new Line(0,1,3);
p ==> Length is 3

> p.x
==> 0

> p.y
==> 1

> p.l
| Error    //Expected, p is a Point reference

> ((Line) p).l
==> 3

> Line l = (Line) p
l ==> Length is 3

> l.x
==> 0

> l.y
==> 1    //referencing x and y this way were surprising, but it makes sense

> ((Line) p).x
==> 0

> l.l
==> 3

> ((Point) l).toString()
==> "Length is 3"

对象实例必须使用正确的引用才能获得所需 class 中的方法。那么,为什么 toString() 被区别对待呢?似乎 toString() 被调用用于构造它的 class,无论我使用的是哪种类型的引用。

编辑:由于 toString() 被重写了两次,我怎样才能通过类型转换调用 Point.toString()

你误会了。

java 中的所有 方法解析为构造它的方法。

Java 使用两次命中模型来确定要调用的实际方法:

  1. 绑定到正确的签名(静态)。
  2. 找到要调用的正确重载(动态调度)。

绑定到正确的签名

java 中的方法有签名。签名包括名称、参数类型和 return 类型(尽管最后一个通常是无关紧要的;javac 不会编译任何具有仅 return 输入类型,仅此而已)。请注意,此处不考虑泛型。

示例:

  • public void foo()[1]
  • public void foo(String[] x) [2]
  • public void foo(String... x) [2]
  • public void foo(List<String> x) [3]
  • public void foo(List<Integer> x)[3]
  • public void foo(List<String> x, boolean whatever) [4]

此处具有相同编号的每个条目都具有相同的签名:[3] 被认为是相同的签名,因为泛型首先被清除,[2] 被认为是相同的签名,因为可变参数被实现为一个数组。

Java 将使用您在 上调用方法的表达式的 类型来确定您试图调用哪个签名。这完全是编译时的事情!

示例:

class Parent {
    public void foo(Object arg) { System.out.println("Parent"); }
}

class Child extends Parent {
    public void foo(String arg) { System.out.println("Child"); }
    // note: not the same signature!
}

Parent p = new Child();
p.foo("Hello");

javac(编译器)查看行 p.foo("Hello"),然后执行 'which signature is this?' 舞蹈:p 是 Parent 类型(事实上它指向 Child 类型的对象是无关紧要的;编译器不知道这一点,也不会考虑它),查看类型 Parent 具有的所有方法,只看到一个 foo,并且因此,该调用使用签名:void foo(Object)。如果 运行.

,此代码将打印 Parent

动态调度

一旦编译器计算出签名,就会在 class 文件中进行编码。当执行 class 文件时,使用动态调度:检查实际对象的类型,并调用最具体的版本。示例:

class Parent {
    public void foo(Object arg) { System.out.println("Parent"); }
}

class Child extends Parent {
    public void foo(Object arg) { System.out.println("Child"); }
    // note: now it IS the same signature!
}

Parent p = new Child();
p.foo("Hello");

请注意这是同一个示例,只是现在子方法的签名与父方法的签名相匹配 - 现在是 'the same method'。因此,虽然 p.foo("Hello") 调用的编译根本没有改变(事实上,您可以只重新编译 Child.java 而不是重新编译调用 p.foo 的代码,您会观察到this: 现在它打印 Child。那部分完全是 运行time 事件。

我强烈建议您在打算 'override' 方法时使用 @Override 注释,因此 Child.java 文件中的 foo(Object) 应该具有该注释.该注解只做一件事:如果你把它放在一个没有覆盖任何东西的方法上(例如,没有父类型具有与此相同的方法(签名和所有!)),这是一个编译器错误,否则, 它没有效果。编译器检查的文档是一件好事。

如果你把 @Override 放在 public void foo(String arg) 上,你就会得到一个错误。虽然它与父方法中的方法同名,但签名不同。

TL;DR:在 java 中,'method names' 包括所有无泛型的参数类型,因此如果参数列表不匹配,则不是同一种方法。但是如果他们这样做,总是使用动态调度,你不能在 java 中选择退出:调用的方法将是你调用它的实际对象之一。

注意:如果方法是静态的,则根本没有动态调度。都是静态的(编译时确定)

您没有重载 toString() 方法,而是在子 class 中覆盖 它。

如果您仍有任何疑问,请执行 post 您在 public static void main(String args[]){} 方法中编写的代码。

It appears that toString() is called for the class that it's constructed with, regardless of what type of reference I'm using.

是的。这就是 Java 中动态调度的全部内容。检查 What is dynamic method dispatch and how does it relate to inheritance? 以了解更多信息。

演示:

public class Main {
    public static void main(String[] args) {
        Point p1 = new Point(10, 20);
        Object p2 = new Point(5, 15);

        Point l1 = new Line(3, 8, 7);
        Line l2 = new Line(10, 20, 20);
        Object l3 = new Line(5, 15, 25);

        System.out.println(p1);
        System.out.println(p2);
        System.out.println(l1);
        System.out.println(l2);
        System.out.println(l3);
    }
}

输出:

(10,20)
(5,15)
Length is 7
Length is 20
Length is 25