Подтвердить что ты не робот

Когда анонимный класс, не имеющий ссылки на его охватывающий класс, возвращается из метода экземпляра, он ссылается на это. Зачем?

Когда анонимный класс, не имеющий ссылки на его класс включения, возвращается из метода экземпляра, он имеет ссылку на 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).

Учитывая тот факт, что нет ссылки на охватывающий класс, я не вижу преимущества в этом.

Что происходит за кулисами?

4b9b3361

Ответ 1

Существует принцип проектирования для анонимных/внутренних классов: Каждый экземпляр внутреннего класса принадлежит экземпляру внешнего класса.

Оставив ссылку на внутренний класс, вы измените поведение сборщика мусора: способ, которым он реализован, внешний класс не может быть собранным мусором, поскольку внутренний класс жив. Это подтверждает идею о том, что внутренний класс не может существовать без внешнего класса.

Приложения могут полагаться на это поведение, например, создавая временный файл и удаляя его в деструкторе. Таким образом, файл будет удален только при исчезновении всех внутренних классов.

Это также означает, что текущее поведение не может быть изменено, поскольку изменение может привести к поломке существующих приложений.

Поэтому вы всегда должны ставить внутренние классы как статические, если вам не нужна эталонная ссылка, потому что это может привести к некоторым утечкам памяти.

Изменить: Пример того, что я пытаюсь сказать (извините за ужасное качество кода):

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 - это не сбор мусора, потому что все еще существует "внутренний экземпляр". Чтобы это поведение работало, внутренний класс нуждается в ссылке.

Конечно, он также может быть реализован по-разному. Но я бы сказал, что сохранение ссылки - это конструктивное решение, сделанное специально, так что "внутренний экземпляр" не переживет своего родителя.

BTW: Ссылка на язык Java говорит об этом довольно загадочно (для внутренних классов, которые не имеют доступа к внешнему классу, нет исключения):

Экземпляр я прямого внутреннего класса C класса или интерфейса O связанный с экземпляром O, известный как сразу экземпляр i. Приближающийся экземпляр объекта, если any, определяется, когда объект создан (§15.9.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, тогда создайте статический внутренний класс (или создайте этот экземпляр из статического контекста). Наличие ссылки на прилагаемый экземпляр - это просто то, как реализуются внутренние классы.


Кстати: сравнение с equal вернет false даже для двух объектов, которые созданы из одного и того же "контекста", если вы не реализуете свой собственный метод equals в возвращаемых объектах соответственно

Ответ 3

Даже если мы не видим никакой видимой ссылки, она будет существовать. См. Код ниже.

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();
    }
}

Вывод:

  • Компилятор делает вещи при создании файлов классов, которые мы не видим. Например, добавив default constructor в класс или добавив конструктор суперкласса по умолчанию super() для конструктора, который явно не вызывает конструктор self this() или конструктор super(). То же самое верно и для добавления ссылки закрывающего типа, там нет волшебства. Мы можем легко определить такой класс, компилятор просто упростит нашу жизнь, сделав это для нас.
  • Это то же самое, что написать собственный байтовый код. Таким образом, в обход самого компилятора мы можем сделать это, чего не может быть на самом языке.
  • Учитывая отсутствие вывода modifier в анонимных классах, можно сделать вывод, что они являются только локальными классами методов (все модификаторы классов public, protected, private, abstract, static) теряют смысл внутри метода. Они просто называются анонимным классом под маской, они являются локальными классами методов.