Внутренние классы и замыкания Java

Java Go JVM переводчик

Автор: No Dishwashing Studio - Marklux

Источник:Marklux's Pub

Все права принадлежат автору, при перепечатке указывать источник

Внутренний класс Java

основное определение

Очень просто, не более чем определить класс внутри класса, который называется внутренним классом-членом:

public class OuterClass {
    private String name;
    private int age;
    class InnerClass {
        public InnerClass(){
            name = "mark";
            age = 20;
        }
        public void echo() {
            System.out.println(name + " " + age);
        }
    }
}

проблемное мышление

Приведенный выше очень простой пример также содержит множество вопросов, которые следует рассмотреть:

  • Как создаются экземпляры внутренних классов?
  • Может ли внутренний класс изменить свойства внешнего класса и какова связь между ними?
  • В чем смысл существования внутренних классов?

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

создание экземпляров и доступ к данным

Соединение формируется между внутренним классом и окружающим классом, так что внутренний класс может без ограничений обращаться к любому свойству внешнего класса. Как и в приведенном выше примере, InnerClass может свободно обращаться к частному свойству OuterClass.

Точно так же, поскольку внутренний класс зависит от существования внешнего класса, его нельзя использовать ввнешнийЧтобы создать его экземпляр напрямую, внешний класс должен быть создан до того, как можно будет создать экземпляр внутреннего класса (обратите внимание, что внутренний класс все еще может быть создан непосредственно в методе-члене внешнего класса):

public static void main(String[] args) {
        InnerClass inner = new OuterClass().new InnerClass();
        inner.echo();
}

используя внешний класс.newдля создания внешнего класса.

Мы также знаем, что связь между внутренним классом и внешним классом достигается через ссылку внешнего класса, хранимую внутренним классом.Чтобы получить эту ссылку, вы можете использовать ссылку внешнего класса..thisДля достижения вы можете обратиться к следующему тестовому примеру

public class OuterClass {
    private String name;
    private int age;
    class InnerClass {
        public InnerClass(){
            name = "mark";
            age = 20;
        }
        public void echo() {
            System.out.println(name + " " + age);
        }
        public OuterClass getOuter() {
            return OuterClass.this;
        }
    }

    @Test
    public void test() {
        OuterClass outer = new OuterClass();
        InnerClass inner = outer.new InnerClass();
        Assert.assertEquals(outer, inner.getOuter());
    }
}

Роль внутренних классов

Внутренние классы громоздки в создании и запутаны в использовании, так в чем же смысл внутренних классов?

Реализовать множественное наследование

Это может быть самым важным значением существования внутренних классов, обратитесь к объяснению в «Мышление на Java»:

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

Все мы знаем, что Java отменяет множественное наследование классов в C++ (но допускает несколько реализаций интерфейсов), но в реальном программировании неизбежно, что один и тот же класс должен наследоваться от двух классов одновременно.Это можно сделать с помощью внутренние классы.

Например, есть два абстрактных класса

public abstract class AbstractFather {
    protected int number;
    protected String fatherName;

    public abstract String sayHello();
}

public abstract class AbstractMother {
    protected int number;
    protected String motherName;

    public abstract String sayHello();
}

Если вы хотите наследовать эти два класса одновременно, это неизбежно вызоветnumberпеременные конфликты иsayHelloКонфликты методов, эти проблемы реализованы в сложной схеме на C++.Если вы используете внутренние классы, вы можете использовать два разных класса для наследования разных базовых классов, и вы можете организовать данные в соответствии с вашими потребностями.Доступ:

public class TestClass extends AbstractFather {
    @Override
    public String sayHello() {
        return fatherName;
    }

    class TestInnerClass extends AbstractMother {
        @Override
        public String sayHello() {
            return motherName;
        }
    }
}

разное

(Из «Думай на языке Java»)

  1. Внутренние классы могут использовать несколько экземпляров, каждый экземпляр имеет свою собственную информацию о состоянии и не зависит от информации других окружающих объектов.

  2. Внутри одного внешнего класса вы можете иметь несколько внутренних классов, реализующих один и тот же интерфейс по-разному, или наследовать один и тот же класс.

  3. Момент создания объекта внутреннего класса не зависит от создания объекта внешнего класса.

  4. Внутренний класс не имеет сбивающего с толку отношения «есть-а», это отдельная сущность.

  5. Внутренний класс обеспечивает лучшую инкапсуляцию, и никакие другие классы не могут получить к нему доступ, кроме внешнего класса.

Классификация внутренних классов

Внутренние классы, созданные в приведенном выше примере, принадлежатвнутренний класс-член, на самом деле в Java есть еще три внутренних класса:

  1. локальный внутренний класс

    Вложен в метод или в область. Обычно этот класс не должен быть общедоступным. По сравнению с внутренними классами-членами область действия локальных внутренних классов более узкая. Обычно используется для внутренней реализации некоторых частных вспомогательных функций.

    Определено в методе:

    public class Parcel5 {
    public Destionation destionation(String str){
        class PDestionation implements Destionation{
            private String label;
            private PDestionation(String whereTo){
                label = whereTo;
            }
            public String readLabel(){
                return label;
            }
        }
        return new PDestionation(str);
    }
    
    public static void main(String[] args) {
        Parcel5 parcel5 = new Parcel5();
        Destionation d = parcel5.destionation("chenssy");
    }
    }
    

    Определено в области:

    public class Parcel6 {
    private void internalTracking(boolean b){
        if(b){
            class TrackingSlip{
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip(){
                    return id;
                }
            }
            TrackingSlip ts = new TrackingSlip("chenssy");
            String string = ts.getSlip();
        }
    }
    
    public void track(){
        internalTracking(true);
    }
    
    public static void main(String[] args) {
        Parcel6 parcel6 = new Parcel6();
        parcel6.track();
    }
    

} ```

  1. статический внутренний класс

    Внутренний класс, измененный с помощью static, является статическим внутренним классом.Самое большое отличие от обычного внутреннего класса-члена заключается в том, что статический внутренний класс не имеет ссылки на окружающий класс. Следовательно, его создание не обязательно должно зависеть от включающего класса, но он также не может использовать какие-либо нестатические переменные-члены и методы включающего класса.

    Хорошим применением статических внутренних классов является создание потокобезопасного одноэлементного шаблона:

    public class Singleton {  
        private static class SingletonHolder {  
            private static final Singleton INSTANCE = new Singleton();  
        }  
        private Singleton (){}  
        public static final Singleton getInstance() {  
            return SingletonHolder.INSTANCE; 
        }  
    }
    

    Это использует преимущества характеристик JVM: статические внутренние классы реализуются при загрузке классов, поэтому они не будут затронуты многопоточностью, и, естественно, не будет нескольких экземпляров.

  2. анонимный внутренний класс

    Анонимные внутренние классы — это безымянные внутренние классы, которые часто используются, когда нам нужно быстро создать несколько потоков:

    new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }).start();
    

    Конечно, это также можно реализовать с помощью выражений λ. Давайте пока не будем упоминать об отношениях между ними. Давайте посмотрим, на что следует обратить внимание при использовании анонимных внутренних классов:

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

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

Внутренние классы и замыкания Java

Что такое закрытие?

в предыдущемГоворя о закрытияхЭто было объяснено в статье, и закрытие в одном предложении:

Замыкание — это сущность, состоящая из функции и связанной с ней эталонной среды (т. е. замыкание = функция + эталонная среда).

Все еще пишу очень простое замыкание в Go для последующего сравнения с анонимными внутренними классами Java:

func Add(y int) {
	return func(x int) int {
		return x + y
	}
}

a := Add(10)
a(5) // return 15

Мы знаем, что возвращаемое замыкание содержит переменную y, предоставленную в функции Add (то есть среду, сгенерированную замыканием), теперь давайте подумаем о том, как реализовано замыкание.

Согласно определению, замыкание = функция + эталонная среда. В приведенном выше примере эталонной средой является локальная переменная y, поэтому можно ли использовать структуру для определения замыкания? (Примечание: структура в Golang и класс в Java статус тот же), ответ — да (на самом деле нижний слой Golang определяет замыкания следующим образом):

type Closure struct {
	F func(x int) int
	y *int
}

То есть замыкание содержит саму функцию, а также ссылку на локальную переменную y.

Здесь следует особо отметить, что если y является переменной, определенной в стеке вызовов функции Add, то при вызове функции Add() y будет уничтожена, а затем для доступа к y будет использоваться исходный указатель. возникают, так вот принцип:

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

На самом деле, когда компилятор Go обрабатывает это замыкание, он будет использоватьescape analyzeЧтобы определить область действия переменной y, когда обнаруживается, что на переменную y ссылается замыкание, она переносит y в кучу (этот процесс называется переходом переменной).

Подводя итог, замыкание понимается с нижнего уровня, который является ссылкой на саму функцию и внешние переменные, которые ей нужны.В словах R процесс создания замыкания описывается как:

capture by reference

Внутренние классы и замыкания

Глядя на наше определение замыкания в конце, очень ли «ссылка на требуемую внешнюю переменную» похожа на ссылку внутреннего класса на внешний класс в Java?

Итак, все думают, что в Java нет замыканий. На самом деле замыкания есть везде в Java (объекты — это замыкания), так что мы не чувствуем, что используем замыкания. Внутренний класс-член — типичный пример, потому что он ссылка на объемлющий класс, см. следующий пример:

public class OuterClass {
    private int y = 10;
    private class Inner {
        public int innerAdd(int x) {
            return x + y;
        }
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        Inner inner = outer.new Inner();
        System.out.println(inner.innerAdd(5)); //result: 15
    }
}

Это использование на самом деле такое же, как замыкание, продемонстрированное ранее в языке Go, но писать на языке Go гораздо проще (в конце концов, функциональное программирование, естественно, поддерживается).

Являются ли анонимные внутренние классы замыканием?

Говоря только что об анонимных внутренних классах, я также упомянул замыкания, так являются ли анонимные внутренние классы замыканием?

Давайте сначала попробуем создать структуру, подобную замыканию, с анонимным внутренним классом:

interface AnnoInner {
    int innerAdd(int x);
}

public class OuterClass {
    private int y = 100;
    public AnnoInner getAnnoInner() {
        int z = 10;
        return new AnnoInner() {
            @Override
            public int innerAdd(int x) {
                // z = 20; 报错
                return x + y + z;
            }
        };
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        AnnoInner inner = outer.getAnnoInner();
        System.out.println(inner.innerAdd(5)); //result: 115
    }
}

Мы видим, что анонимный внутренний класс также может ссылаться на переменную-член y внешнего класса в обычном режиме, но для локальной переменной z, если вы попытаетесь изменить ее значение в анонимном внутреннем классе (без проблем для чтения), компилятор сообщит об ошибке.

Это также соответствует правилам, введенным выше при введении анонимных внутренних классов:

Если анонимный внутренний класс хочет получить доступ к локальной переменной, эта переменная должна быть final

Причина такого результата в том, что когда Java реализует анонимные внутренние классы для доступа к внешним локальным переменным, она не продвигает локальные переменные в кучу, как компилятор Go, а затем передает ссылки для достижения; создает копию значения, которое затем используется анонимным внутренним классом.

Таким образом, реализацию приведенного выше примера в компиляторе Java можно просто понять так:

return new AnnoInner() {
            @Override
            public int innerAdd(int x) {
                int copyZ = z; // 创建z的值拷贝
                return x + y + copyZ;
            }
        };

Итак, анонимный внутренний класс в Java — это неполное замыкание, говоря словами R:

захват по значению, а не по ссылке

Справочное чтение: