继承:是否有一种方法可以发现调用方法的类?

7
使用以下代码:
class SuperTest {
    SuperTest() { whoAmI(); }
    void whoAmI() { System.out.println(getClass().getName()); }
}

class Test extends SuperTest {
    Test() { whoAmI(); }        
}
new Test()会打印两次"Test"。作为一个初学者,我原本期望输出结果是"SuperTest / Test"。现在我明白了为什么这不可能实现,以及为什么隐式的this只会引用子类型。

然而,我找不到应该如何编写whoAmI()才能实际打印出SuperTest / Test。 换句话说:如何让whoAmI()访问它所“调用的”类型的名称?

编辑:我正在更改问题的标题,以便更好地描述问题。 (旧标题为:继承:是否有一种“类似于this”的结构可以从派生类型引用超类型)。

3个回答

5
这是预期的行为,因为在这两种情况下都调用了类型为Test的同一对象上的getType()。这是因为JVM在调用基类构造函数时知道正在创建的对象的类型是派生类Test的类型,因此打印的就是该类型。
这种行为并不适用于所有编程语言,但在Java中是这样工作的。
如果一个类想要访问自己的Class对象,它可以使用SuperTest.class表达式来静态地实现。
要获得所需的行为,您可以像这样从构造函数将类传递给whoAmI
class SuperTest {
    SuperTest() { whoAmI(SuperTest.class); }
    void whoAmI(Class c) { System.out.println(c.getName()); }
}
class Test extends SuperTest {
    Test() { whoAmI(Test.class); }        
}

1
糟糕,我的意思是“调用该方法的类”。但我担心答案仍然有效。 - mins
1
@mins 您说得对,除非调用者告诉方法,否则无法确定一个方法从哪个类中调用。 - Sergey Kalinichenko
1
@mins,有一种方法可以让一个方法在运行时获取它的调用者的名称:创建一个新的Throwable,调用它的fillInStackTrace()方法,调用它的getStackTrace()方法,并查看数组中的第一个StackTraceElement。坏消息是,你可以得到调用方法的名称,但不能得到签名,你可以得到调用者类的名称,但不能得到包名;因此没有足够的信息来明确地识别调用者,但可能有足够的信息进行调试/日志记录。 - Solomon Slow
聪明!谢谢。根据上下文,这个替代方案值得更新到你的答案中,其中一个可能更合适。我喜欢第二个。 - mins
@mins 通过 fillInStackTrace() 获取调用者边界近乎于一个黑客技巧,而且非常低效。除了调试目的外,不应该被用于其他任何目的。 - Sergey Kalinichenko
显示剩余2条评论

0

为了您的娱乐,这是一个测试程序,演示了一个可以打印任何所需祖先深度的“whoAmI”:

public class Test {
  public static void main(String[] args) {
    Sub2 sub2 = new Sub2();
    System.out.println("Four levels");
    sub2.whoAmi(4);
    System.out.println("Two levels");
    sub2.whoAmi(2);
  }
  public void whoAmi(int levels){
    if(levels > 1){
      Class<? extends Object> ancestor = getClass();
      for(int i=1; i<levels && ancestor != null; i++){
        ancestor = ancestor.getSuperclass();
      }
      if(ancestor != null){
        System.out.println(ancestor.getName());
      }
      whoAmi(levels - 1);
    } else {
      System.out.println(getClass().getName());      
    }
  }
}

class Sub1 extends Test {
}

class Sub2 extends Sub1 {
}

它打印出对象所需的祖先数量,对于n=1,它与问题中的代码相同,对吧?无论如何都很有趣! - mins
@mins 正确。它本质上是原始代码和使用getSuperclass()查找祖先的思路的结合。 - Patricia Shanahan

0

如何让 whoAmI() 方法访问它所被调用的类型的名称?

您可以使用 Class.getSuperclass() 访问超类:

void whoAmI() {
    Class superclass = getClass().getSuperclass();
    String superclassText = "no superclass found";
    if (superclass != null) superclassText = superclass.getName();
    System.out.println(superclassText);
}

这个技术上回答了你的问题,但是只是因为我假设我们正在讨论的方法属于超类(在你的示例中确实如此)。

但也许你真正想知道的是“即使在子类的实例上调用方法,该方法如何识别它所定义的类?” 这将是一个更加复杂的问题。虽然java.lang.reflect.Method.getDeclaringClass()似乎可以做到你要求的事情,但你首先需要声明类的Class实例(即SuperTest.class),这就失去了意义——如果你已经有了这个,你根本不需要使用java.lang.reflect.Method。

然而,如果我能再改变一下前提,让我们假设你嵌入了每个类都有一个静态的Class实例,像这样:

private final static Class THIS_CLASS = SuperTest.class;

那么就可以编写一个不通过类名访问其声明类的方法,因为它可以假定将其复制粘贴到的任何类都定义了THIS_CLASS
void whoAmI() {
    final String myName = "whoAmI";
    final Class[] myParamTypes = {};
    String result;
    try {
        Method thisMethod = THIS_CLASS.getDeclaredMethod(myName, myParamTypes);
        result = "I am " + thisMethod.getDeclaringClass().getName() + "." +
                myName + Arrays.toString(myParamTypes);
        // Now swap the square brackets from Arrays.toString for parentheses
        result = result.replaceAll("\\[", "\\(").replaceAll("\\]", "\\)");
    } catch (NoSuchMethodException | SecurityException ex) {
        result = ex.toString();
    }
    System.out.println(result);
}

这将产生I am packagename.MinsSuperTest.whoAmI()作为输出。


谢谢。whoAmI 的第一个版本将打印 "SuperTest / SuperTest",这不是预期的输出。第二个版本确实回答了另一个问题(知道方法定义的位置,但不知道从哪里调用它)。 - mins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接