Внутренние и вложенные Java классы

В Java вложенные классы — это классы, которые определены внутри другого класса.

Цель — четко сгруппировать вложенный класс с окружающим его классом, сигнализируя о том, что эти два класса должны использоваться вместе. Или, возможно, что вложенный должен использоваться только внутри его вмещающего (владеющего) класса.

Java-разработчики часто используют внутренние классы (нестатические вложенные) — это только один из нескольких различных типов:

  • статические;
  • нестатические;
  • локальные;
  • анонимные.

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

Статические

Объявляются:

public class Outer {

  public static class Nested {

  }

}

Чтобы создать экземпляр класса Nested, вы должны сослаться на него, добавив к нему префикс имени внешнего класса, например:

Outer.Nested instance = new Outer.Nested();

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

Нестатические внутренние классы

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

public class Outer {

  public class Inner {
  }

}

Вот как вы создаете экземпляр класса Inner:

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

Обратите внимание, как вы ставите new после ссылки на внешний класс, чтобы создать экземпляр внутреннего.

Нестатические классы (внутренние классы) имеют доступ к полям включающего класса, даже если они объявлены закрытыми:

public class Outer {

    private String text = "I am private!";

    public class Inner {

        public void printText() {
            System.out.println(text);
        }
    }
}

Обратите внимание, как метод printText() класса Inner ссылается на поле частного текста класса Outer. Это вполне возможно. Вот как вы бы вызвали метод printText():

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.printText();

Shadowing

Если внутренний класс объявляет поля или методы с теми же именами, что и у поля или методов в своем включающем классе, внутренние поля или методы называются внешними полями или методами:

public class Outer {

    private String text = "I am Outer private!";

    public class Inner {

        private String text = "I am Inner private";

        public void printText() {
            System.out.println(text);
        }
    }
}

В приведенном выше примере класс Outer и Inner содержит поле с именем text. Когда класс Inner ссылается на текст, он ссылается на свое собственное поле. Когда Outer ссылается на текст, он также ссылается на свое собственное поле.

Java позволяет классу Inner ссылаться на текстовое поле класса Outer. Для этого ему нужно добавить префикс ссылки на текстовое поле к Outer.this. (имя внешнего класса + .this. + имя поля), например:

public class Outer {

    private String text = "I am Outer private!";

    public class Inner {

        private String text = "I am Inner private";

        public void printText() {
            System.out.println(text);
            System.out.println(Outer.this.text);
        }
    }
}

Теперь метод Inner.printText() будет печатать поля Inner.text и Outer.text.

Локальные

Локальные классы в Java похожи на внутренние (нестатические вложенные), которые определены внутри метода или блока контекста({…}) внутри метода:

class Outer {

    public void printText() {

        class Local {

        }

        Local local = new Local();
    }

}

Характеристика локальных классов:

  • Доступ возможен только из метода или блока контекста, в котором они определены.
  • Могут получать доступ к членам (полям и методам) своего окружающего класса, как и обычные внутренние.
  • Могут обращаться к локальным переменным внутри того же метода или блока области действия, если эти переменные объявлены как окончательные.
  • Из Java 8 могут обращаться к локальным переменным и параметрам метода, в котором объявлен локальный класс. Параметр должен быть объявлен как final или быть им. Фактически это означает, что переменная никогда не изменяется после инициализации. Параметры метода часто являются фактически окончательными.
  • Могут быть объявлены внутри статических методов. В этом случае локальный класс имеет доступ только к статическим частям окружающего класса.
  • Не могут содержать все виды статических объявлений (допустимы константы — переменные объявлены как static final), потому что не являются статичными по своей природе — даже если они объявлены внутри статического метода.
  • Для них применяются те же правила затенения, что и для внутренних классов.

Анонимные

Анонимные классы — это вложенные без имени класса. Они обычно объявляются либо как подклассы существующего класса, либо как реализации некоторого интерфейса.

Определяются, когда они создаются. Вот пример, который объявляет анонимный подкласс суперкласса с именем SuperClass:

public class SuperClass {

  public void doIt() {
    System.out.println("SuperClass doIt()");
  }

}
SuperClass instance = new SuperClass() {

    public void doIt() {
        System.out.println("Anonymous class doIt()");
    }
};

instance.doIt();

Запуск этого кода приведет к тому, что анонимный класс doIt() будет напечатан в System.out. Он расширяет SuperClass и переопределяет метод doIt().

Анонимный класс также может реализовывать интерфейс вместо расширения класса:

public interface MyInterface {

  public void doIt();

}
MyInterface instance = new MyInterface() {

    public void doIt() {
        System.out.println("Anonymous class doIt()");
    }
};

instance.doIt();

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

Этот тип класса может получить доступ к членам включающего класса. Также к локальным переменным, которые объявлены как final.

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

final Strint textToPrint = "Text...";

MyInterface instance = new MyInterface() {

    private String text;

    //static initializer
    {  this.text = textToPrint;  }

    public void doIt() {
        System.out.println(this.text);
    }
};

instance.doIt();

К этому типу класса применяются те же правила дублирования, что и к внутренним.

Преимущества

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

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

Примером может быть класс Cache. Внутри Cache вы можете объявить класс CacheEntry, который может содержать информацию о конкретной записи в кэше (кэшированное значение, время вставки, количество обращений и т. д.).

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

Вот два скелета реализации Cache, иллюстрирующие эти моменты:

public class Cache {

    private Map cacheMap = new HashMap();

    private class CacheEntry {
        public long   timeInserted = 0;
        public object value        = null;
    }

    public void store(String key, Object value){
        CacheEntry entry = new CacheEntry();
        entry.value = value;
        entry.timeInserted = System.currentTimeMillis();
        this.cacheMap.put(key, entry);
    }

    public Object get(String key) {
        CacheEntry entry = this.cacheMap.get(key);
        if(entry == null) return null;
        return entry.value;
    }

}
public class Cache {

    private Map cacheMap = new HashMap();

    public class CacheEntry {
        public long   timeInserted = 0;
        public object value        = null;
    }

    public void store(String key, Object value){
        CacheEntry entry = new CacheEntry();
        entry.value = value;
        entry.timeInserted = System.currentTimeMillis();
        this.cacheMap.put(key, entry);
    }

    public Object get(String key) {
        CacheEntry entry = this.cacheMap.get(key);
        if(entry == null) return null;
        return entry.value;
    }

    public CacheEntry getCacheEntry(String key) {
        return this.cacheMap.get(key);
        }

}

Первый класс Cache скрывает свой вложенный класс CacheEntry, в то время как второй класс Cache предоставляет его.

Оцените статью