当一个没有引用其封闭类的匿名类从实例方法返回时,它具有对“this”的引用。为什么?

15

当从实例方法返回一个没有引用其封闭类的匿名类时,它将具有对this的引用。为什么?

考虑以下代码:

package so;

import java.lang.reflect.Field;

public class SOExample {

    private static Object getAnonymousClassFromStaticContext() {
        return new Object() {
        };
    }

    private Object getAnonymousClassFromInstanceContext() {
        return new Object() {
        };
    }

    public static void main(String[] args) throws NoSuchFieldException, SecurityException {

        Object anonymousClassFromStaticContext = getAnonymousClassFromStaticContext();
        Object anonymousClassFromInstanceContext = new SOExample().getAnonymousClassFromInstanceContext();

        Field[] fieldsFromAnonymousClassFromStaticContext = anonymousClassFromStaticContext.getClass().getDeclaredFields();
        Field[] fieldsFromAnonymousClassFromInstanceContext = anonymousClassFromInstanceContext.getClass().getDeclaredFields();

        System.out.println("Number of fields static context: " + fieldsFromAnonymousClassFromStaticContext.length);
        System.out.println("Number of fields instance context: " + fieldsFromAnonymousClassFromInstanceContext.length);
        System.out.println("Field from instance context: " + fieldsFromAnonymousClassFromInstanceContext[0]);

    }

}

这是输出结果:

Number of fields static context: 0
Number of fields instance context: 1
Field from instance context: final so.SOExample so.SOExample$2.this$0

每种方法虽然似乎调用相同的代码,但却在做不同的事情。在我看来,实例方法返回一个嵌套类,而静态方法返回一个静态嵌套类(作为静态成员,它显然不能引用 this)。

考虑到没有对封闭类的引用,我无法看到其中的好处。

背后发生了什么?


1
这就是为什么闭包不会这样做的原因。我认为这简化了实现,以避免在实际上不需要时优化this或外部类。 - Peter Lawrey
1
“鉴于没有提及封闭类,您的意思是什么?” - wero
https://dev59.com/v3M_5IYBdhLWcg3wgjS2#1353326 - Dave S
@wero 我的意思是,在实例方法中,我们在返回的匿名类中并没有像这样说:SOExample.this.someField = "foo" - Robert Bain
2
@RobertBain 引用仍然存在,它与你的匿名类非常相似,其中有一个private SOExample foobar成员变量,被初始化为外部实例 - 即使你没有使用它的代码,它仍然存在。这个对外部实例的引用是因为Java规范规定了如此 - 即使没有使用它的代码,也没有特殊情况会省略掉这个引用。 - nos
1
这种情况适用于所有本地类,不仅限于匿名类。老实说,我不明白为什么让在非静态内容中创建的本地类可以访问外部类的其他成员(为了实现这一点,需要引用来保存特定外部类的实例)会有什么奇怪之处。在静态上下文中创建的本地类无法知道任何关于实例的信息,因此它们不需要字段来保存这样的实例。 - Pshemo
3个回答

8
匿名/内部类背后有一个设计原则:每个内部类的实例都属于外部类的一个实例。 省略对内部类的引用会改变垃圾回收的行为:实际上,只要内部类还活着,外部类就无法被垃圾回收。这支持了内部类不能没有外部类存在的想法。
应用程序可能依赖于这种行为,例如创建临时文件并在析构函数中删除它。这样,只有当所有内部类都不存在时,文件才会被删除。
这也意味着当前的行为不能更改,因为更改它可能会破坏现有的应用程序。
所以当您不需要引用时,应始终将内部类标记为静态,因为否则可能会导致一些很好的内存泄漏。
编辑: 我试图表达的示例(抱歉代码质量很差):
class Ideone
{
    static Object[] objects = new Object[2];

    public static void main (String[] args) throws java.lang.Exception
    {
        M1();
        M2();
        System.gc();
    }

    static void M1() {
        objects[0] = new Foo().Bar();
    }
    static void M2() {
        objects[1] = new Foo().Baz();
    }
}

class Foo {
    static int i = 0;
    int j = i++;

    public Foo() {
        System.out.println("Constructed: " + j);
    }

    Object Bar() {
        return new Object() {

        };
    }
    static Object Baz() {
        return new Object() {

        };
    }

    protected void finalize() throws Throwable {
        System.out.println("Garbage collected " + j);
    }
}

输出:

构造: 0
构造: 1
已回收 1

正如您所看到的,第一个Foo没有被垃圾回收,因为仍然存在一个“内部实例”。为了使这种行为起作用,内部类需要一个引用。

当然,它也可以以不同的方式实现。但我想说的是,保留引用是有意做出的设计决策,以便“内部实例”不会比其父级存活更久。

顺便说一下:Java语言参考对此进行了相当晦涩的陈述(没有未访问外部类的内部类的例外):

A类或接口O的直接内部类C的实例i与O的实例相关联,称为i的直接封闭实例。如果对象创建时有任何立即封闭实例,则在创建对象时确定它(§15.9.2)。


我一定漏掉了什么,但从我的理解来看,你所说的问题与所提出的问题是相同的。如果没有提到封闭类,那么实例方法最好返回一个没有this引用的Object,即静态嵌套类。 - Robert Bain
不会。在这种情况下(返回静态嵌套类),保留返回的实例(在其他地方)不会保留外部类的引用。如果返回内部类,则会保留。我将尝试添加一个示例。 - Matthias
1
“当然,它也可以用不同的方式实现。但我认为,保留引用是一个有意的设计决策,这样“内部实例”就不会比其父级存在更长时间。”在特定情况下寻找小的微观优化似乎并不是很有效,因为有一个更大的设计模式在发挥作用。 - Dave S
你不能从这个 finalize 方法中得出任何结论。这个方法可能会被调用,也可能不会被调用,你永远无法知道... - Marco13
1
问题是:一旦以这种方式实现,您就无法更改它,因为需要向后兼容。因此,也许优化实际上可能有益于99%的应用程序,但由于会破坏1%的应用程序,我们被困在这种行为中。依赖这种行为是编写Java代码的可怕方式,我认为。 - Matthias
显示剩余2条评论

2
我觉得可以这样说:它引用了this,因为可能需要它。
想象一下程序稍作修改:
public class SOExample
{
    private static Object getAnonymousClassFromStaticContext()
    {
        return new Object()
        {
            @Override
            public String toString()
            {
                // ERROR: 
                // "No enclosing instance of the type SOExample is accessible in scope"
                return SOExample.this.toString(); 
            }
        };
    }

    private Object getAnonymousClassFromInstanceContext()
    {
        return new Object()
        {
            @Override
            public String toString()
            {
                // Fine
                return SOExample.this.toString(); 
            }
        };
    }
}

显然,在实例上下文中创建的对象需要对this的引用,因为它必须有可能访问封闭实例的方法(如果存在,则为字段)。

你原始示例中未以任何方式访问封闭实例并不意味着这个this引用默认不存在。

在什么时候应该做出不同的决定?编译器应该检查this引用是否真正需要,并在不需要的情况下将其丢弃吗?如果你不想要一个this,那么就创建一个静态内部类(或从静态上下文中创建此实例)。拥有对封闭实例的引用是内部类的实现方式。


顺便说一下:即使您没有相应地实现返回的对象的equals方法,与同一“上下文”中创建的两个对象进行比较也将返回false


“关于equals的观点很好,圣诞节假期让我受打击了! :) 已经移除。” - Robert Bain

0
即使我们看不到任何可见的引用,它仍然存在。请参见下面的代码。
package jetty;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Arrays;

public class SOExample2 {

    private static Object staticField = new Object () { };
    private Object nonStaticField = new Object () { };

    private static Object getAnonStatic() {
        return new Object() { };
    }

    private Object getAnonNonStatic() {
        return new Object() { };
    }

    public static void main(String[] args) throws NoSuchFieldException, SecurityException {
        System.out.println("Started");

        class StaticMethodLocal {

        }

        System.out.println("############## Fields ##############");
        printClassInfo(staticField.getClass());
        printClassInfo(new SOExample2().nonStaticField.getClass());

        System.out.println("############## Methods ##############");
        printClassInfo(getAnonStatic().getClass());
        printClassInfo(new SOExample2().getAnonNonStatic().getClass());

        System.out.println("############## Method Local ##############");
        printClassInfo(new StaticMethodLocal().getClass());
        printClassInfo(new SOExample2().getNonStaticMethodLocal().getClass());
    }

    public static <T>void printClassInfo(Class<T> klass) {
        System.out.println("Class : " + klass);
        String prefix = "\t";

        System.out.println(prefix + "Number fields : " + klass.getDeclaredFields().length);
        if(klass.getDeclaredFields().length > 0) {
            System.out.println(prefix + "fields : " + Arrays.toString(klass.getDeclaredFields()));
        } else {
            System.out.println(prefix + "no fields");
        }
        System.out.println(prefix + "modifiers : " + Modifier.toString(klass.getModifiers()));

        //Constructors
        Constructor<?>[] constructors = klass.getDeclaredConstructors();
        for(Constructor<?> constructor : constructors) {
            System.out.println(prefix + "constructor modifiers : " + Modifier.toString(constructor.getModifiers()));
            System.out.println(prefix + "constructor parameters : " + Arrays.toString(constructor.getParameterTypes()));
        }
        System.out.println("");
    }

    private Object getNonStaticMethodLocal () {
        class NonStaticMethodLocal {
        }
        return new NonStaticMethodLocal();
    }
}

输出:

Started
############## Fields ##############
Class : class jetty.SOExample2$1
    Number fields : 0
    no fields
    modifiers : 
    constructor modifiers : 
    constructor parameters : []

Class : class jetty.SOExample2$2
    Number fields : 1
    fields : [final jetty.SOExample2 jetty.SOExample2$2.this$0]
    modifiers : 
    constructor modifiers : 
    constructor parameters : [class jetty.SOExample2]

############## Methods ##############
Class : class jetty.SOExample2$3
    Number fields : 0
    no fields
    modifiers : 
    constructor modifiers : 
    constructor parameters : []

Class : class jetty.SOExample2$4
    Number fields : 1
    fields : [final jetty.SOExample2 jetty.SOExample2$4.this$0]
    modifiers : 
    constructor modifiers : 
    constructor parameters : [class jetty.SOExample2]

############## Method Local ##############
Class : class jetty.SOExample2$1StaticMethodLocal
    Number fields : 0
    no fields
    modifiers : 
    constructor modifiers : 
    constructor parameters : []

Class : class jetty.SOExample2$1NonStaticMethodLocal
    Number fields : 1
    fields : [final jetty.SOExample2 jetty.SOExample2$1NonStaticMethodLocal.this$0]
    modifiers : 
    constructor modifiers : 
    constructor parameters : [class jetty.SOExample2]

我已经添加了两种匿名类作为字段值和两个方法局部类。

从上面的输出可以清楚地看出,在非静态上下文中生成的类只有一个构造函数,而这个构造函数具有封闭类类型的参数。

正如输出所建议的那样,JVM在创建匿名/方法局部类时添加了一些额外的代码来保存封闭类实例引用。

这也可以在反编译器输出中看到。

    // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   SOExample2.java

//Static field anonynmouse class
class SOExample2$1
{
    SOExample2$1()
    {
    }
}

//Non static field anonynmouse class
class SOExample2$2
{
    final SOExample2 this$0;
    SOExample2$2()
    {
        this$0 = SOExample2.this;
        super();
    }
}

//static method anonynmouse class
class SOExample2$3
{
    SOExample2$3()
    {
    }
}

//Non static method anonynmouse class
class SOExample2$4
{
    final SOExample2 this$0;
    SOExample2$4()
    {
        this$0 = SOExample2.this;
        super();
    }
}

//Static method local class
class SOExample2$1StaticMethodLocal
{
    SOExample2$1StaticMethodLocal()
    {
    }
}

//Non static method local class
class SOExample2$1NonStaticMethodLocal
{
    final SOExample2 this$0;
    SOExample2$1NonStaticMethodLocal()
    {
        this$0 = SOExample2.this;
        super();
    }
}

结论:

  1. 编译器在生成类文件时会执行一些我们看不到的操作。例如,向类中添加一个默认构造函数或向构造函数中添加一个默认的超类构造函数调用super(),该构造函数没有显式调用任何this()自身构造函数或super()构造函数。同样,添加封闭类型的引用也是如此,其中没有任何魔法。我们可以轻松地定义这样的类,编译器只是为我们做了这件事,使我们的生活更加轻松。
  2. 这与编写自己的字节码相同。因此,通过绕过编译器本身,我们可以做到实际语言无法做到的事情。
  3. 如果匿名类中没有modifier输出,则可以得出结论它们只是方法局部类(所有类修饰符public、protected、private、abstract、static在方法内失去意义)。它们只是被称为匿名类,而实际上它们是方法局部类。

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