"this"关键词:Java中的工作机制

"this" keyword: Working mechanism in Java

学习Java一段时间后,第一次使用this关键字,搞得我很困惑。

这就是我感到困惑的原因。我写了下面的代码:

class BasicInheritanceTest3Base{
    private int x = 0;
    public int y;

    public void a() {
        x++;
        this.x++;
        System.out.println("BasicInheritanceTest3Base.a()");
        b();
        this.b();
        System.out.println(x);
        System.out.println(y);
    }

    public void b(){
        System.out.println("BasicInheritanceTest3Base.b()");
    }
}

public class BasicInheritanceTest3 extends BasicInheritanceTest3Base {
    private int x = 3;
    public int y = 2;

    public void b() {
        System.out.println("BasicInheritanceTest3.b()");
    }

    public static void main(String[] args){
        BasicInheritanceTest3 bit2 = new BasicInheritanceTest3();
        bit2.a();
    }
}

我得到以下输出:

BasicInheritanceTest3Base.a()
BasicInheritanceTest3.b()
BasicInheritanceTest3.b()
2
0

现在这里的第一个问题是:为什么 xthis.x 指向基 class 的 x 而不是子 class?如果this.x指向基class的x,为什么this.b()调用子class的b()?字段和方法的行为是否不同?

然而,主要关注的是 this 关键字的机制。 我的意思是你知道,this 指向(引用)当前对象。仔细想想,这不是什么神奇的行为。某处必须有 this 字段。例如,class 的 .class 文字是不可见的,但存在于发出的字节码中。同样,这个引用应该出现在字节码中。

好吧,假设上面是真的,this 应该是一个 public final(一个空白的 final),它在每次构造对象及其字段被实例化时被实例化。这意味着它是一个实例变量而不是静态变量。

现在,如果 this 被实例化为当前对象的引用(这只是一个特定的对象),为什么上述 this 的使用对于字段和方法是不同的?那么总而言之,this背后的机制是什么?该机制是否也适用于 super 关键字?

编辑: 阅读问题和评论的每个人,我想问一下,编译器声明的 this 字段在哪里以及它的限定符是什么。结果行为是如何在幕后发生的?

为什么 x 和 this.x 指向基数 class 的 x 而不是 Child class?

因为 Java 中的字段不是多态的。字段绑定在编译时解决。如果你想使用递增作为多态性,你可以用一个方法来实现。要正确执行,您需要在 parent 和 child.

中定义它
public void increment(){
    x++; //this.x++; would do the same;
}

而如果this.x指向基class的x,为什么this.b()调用child的b()class?

因为另一方面方法是多态的,这意味着它们的绑定在 run-time 处解析,这就是 this.b() 从 child class 调用方法的原因,在您的情况下,这是 BasicInheritanceTest3 的实例并调用相应的方法。

字段和方法的行为是否不同?

如你所见。

Super 是对基础 class 的引用,因此您可以在例如需要调用重写方法 or/and 隐藏字段时访问它。

编辑回复: 这是一个引用,这意味着它只是 object 的地址以及它在 JVM 内存中的所有数据,JVM 如何处理这个关键字并不是真正已知或重要的,它可能在实例化时声明。但最后你需要知道的是,这是对 Object 他自己的实例的引用。

[编辑答案] 我做了一些研究并获得了以下信息以进一步回答您的问题。 我们确实可以通过使用逆向工程工具将字节码转换回 java 源来验证 this 是字节码的一部分。

Why would we find the this in the bytecode?

因为 java 是多通道编译器,并且字节码在任何其他平台和任何其他机器上都可以是 运行,所以所有信息都必须在字节码中,足够的信息能够将字节码反向工程到源代码中。此外,由于源代码必须与字节码的原始源代码相同,因此包括变量和字段的确切名称在内的所有内容都必须 "somehow" 与字节码中的所有信息保持良好的组织。 而 C++ 或 Pascal 与 java 不同,它们使用单​​通道编译器,它们大多不会保留字段的确切名称,并且由于此类语言输出最终的 "executable" 文件,该文件必须准备好成为 运行,可能不太关心维护确切的名称(指令不必通过另一个 "pass")。如果有人对可执行文件(C++ 或 Pascal)进行逆向工程,变量名将不是人类可读的。所以在字节码中 "this" 可能表示为非人类可读的格式,但同样可以反向工程回 "this"。对于单遍编译器,情况并非如此。

Multi Pass Compiler

Class 方法不能直接访问实例变量或实例方法——它们必须使用对象引用。 此外,class 方法不能使用 this 关键字,因为没有 this 的实例可供参考。

Now the first question here is: Why x and this.x point to the x of base class and not the Child class?

因为多态行为不适用于字段,所以结果来自基础class。

why this.b() calls the b() of child class? Is the behavior of this different for fields and methods?

这一行:BasicInheritanceTest3 bit2 = new BasicInheritanceTest3(); 堆中唯一的对象(就基和子 class 而言)是类型 BasicInheritanceTest3 的对象。因此,无论 this 是什么,调用都将应用于子 class 方法。 bit2 正在引用堆中它自己的层次结构(继承)。

现在 - 编译器处理它的方式与 jdk 处理任何其他 key/reserved 单词的方式相同。 class 方法的上下文中是不允许的 Class 方法不能直接访问实例变量或实例方法——它们必须使用对象引用。此外,class 方法不能使用 this 关键字,因为没有 this 可以引用的实例。确实是一个有趣的问题因此给了 OP 对这个问题的赞成票。

我读到的更有帮助的信息是: 标识符和保留关键字是标记,就像 + 这样的单个字符和像 != 这样的字符序列。

我想请求社区保留此主题。我没有探讨 jdk(编译器和 运行time)如何处理关键字和保留字。

Java Api Docs: this

其他答案和评论解释了字段如何不是多态的以及如何根据实例引用的编译时类型解析字段访问表达式。下面,我将解释字节码如何处理 this 引用。

在关于 接收参数 的章节中,Java Virtual Machine Specification 指出

If n arguments are passed to an instance method, they are received, by convention, in the local variables numbered 1 through n of the frame created for the new method invocation. The arguments are received in the order they were passed. For example:

int addTwo(int i, int j) {
    return i + j;
}

compiles to:

Method int addTwo(int,int)
0   iload_1        // Push value of local variable 1 (i)
1   iload_2        // Push value of local variable 2 (j)
2   iadd           // Add; leave int result on operand stack
3   ireturn        // Return int result

By convention, an instance method is passed a reference to its instance in local variable 0. In the Java programming language the instance is accessible via the this keyword.

Class (static) methods do not have an instance, so for them this use of local variable 0 is unnecessary. A class method starts using local variables at index 0. If the addTwo method were a class method, its arguments would be passed in a similar way to the first version:

static int addTwoStatic(int i, int j) {
    return i + j;
}

compiles to:

Method int addTwoStatic(int,int)
0   iload_0
1   iload_1
2   iadd
3   ireturn

The only difference is that the method arguments appear starting in local variable 0 rather than 1.

换句话说,您可以将 this 视为未在任何地方声明或被声明为每个实例方法的第一个参数。为每个实例方法创建一个局部变量 table 条目,并在每次调用时填充。

Invoking methods 上的章节指出

The normal method invocation for a instance method dispatches on the run-time type of the object. (They are virtual, in C++ terms.) Such an invocation is implemented using the invokevirtual instruction, which takes as its argument an index to a run-time constant pool entry giving the internal form of the binary name of the class type of the object, the name of the method to invoke, and that method's descriptor (§4.3.3). To invoke the addTwo method, defined earlier as an instance method, we might write:

int add12and13() {
    return addTwo(12, 13);
}

This compiles to:

Method int add12and13()
0   aload_0             // Push local variable 0 (this)
1   bipush 12           // Push int constant 12
3   bipush 13           // Push int constant 13
5   invokevirtual #4    // Method Example.addtwo(II)I
8   ireturn             // Return int on top of operand stack;
                        // it is the int result of addTwo()

The invocation is set up by first pushing a reference to the current instance, this, on to the operand stack. The method invocation's arguments, int values 12 and 13, are then pushed. When the frame for the addTwo method is created, the arguments passed to the method become the initial values of the new frame's local variables. That is, the reference for this and the two arguments, pushed onto the operand stack by the invoker, will become the initial values of local variables 0, 1, and 2 of the invoked method.

1.为什么 x 和 this.x 指向 base class 的 x 而不是 Child class?

我们可以看到这个例子:

class TestBase {
    private int x;
    public void a() {
        this.x++;
    }
    public int getX() {
        return x;
    }
}
public class Test extends TestBase{
    private int x;
    public int getX() {
        return this.x;
    }
}

和生成的字节码:

public class Test extends TestBase{
public Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method TestBase."<init>":()V
   4:   return

public int getX();
  Code:
   0:   aload_0
   1:   getfield        #2; //Field x:I
   4:   ireturn

public void a();
  Code:
   0:   aload_0
   1:   invokespecial   #3; //Method TestBase.a:()V
   4:   return

}

那里TestextendsTestBase和方法a被编译成Testclass,它会调用它的父亲 1: invokespecial #3; //Method TestBase.a:()V.

TestgetX 方法将从它自己的 constant pool tablehttp://en.wikipedia.org/wiki/Java_bytecode_instruction_listings

调用 1: getfield #2; //Field x:I

TestBaseclass字节码:

class TestBase extends java.lang.Object{
TestBase();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public void a();
  Code:
   0:   aload_0
   1:   dup
   2:   getfield        #2; //Field x:I
   5:   iconst_1
   6:   iadd
   7:   putfield        #2; //Field x:I
   10:  return

public int getX();
  Code:
   0:   aload_0
   1:   getfield        #2; //Field x:I
   4:   ireturn

}

方法 a() 也会通过 getfield #2; //Field x:I 从它自己的常量池中获取 x

所以还有一件事:Java的gettersetter是邪恶的。

事实上,JAVA编程语言中的多态性属性只能应用于方法具有足够资格成为多态成员的方法。您不应该将 Fields 视为具有上述 property.Thus 的成员,您不会再对此类问题感到困惑。