27 авг. 2015 г.

Внутренние классы. Часть 4 – локальные классы.

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

Видимость локального класса регулируется областью видимости того блока, в котором он объявлен. Но при этом локальный класс сохраняет доступ ко всем полям и методам внешнего класса, а также ко всем константам, объявленным в текущем блоке кода, то есть полям и аргументам метода объявленным как final. Но начиная с JDK 8 локальный класс может обращаться к любым полям и аргументам метода объявленным в текущем блоке кода, даже если они не объявлены как final, но только в том случае если их значение не изменяется после инициализации, по существу те же яйца только вид с боку. Такие переменные или аргументы, значение которых не изменяется после их инициализации называются (в терминологии JDK8) – effectively final.

Локальные классы никогда не объявляются с помощью модификаторов доступа (т.е. public, protected и т.п.), поскольку их область видимости всегда ограничивается блоком, в котором они объявлены.

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

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

Интерфейсы нельзя определить локально.

Подобно внутреннему классу, локальный класс не может называться так же, как и окружающий его класс.

Как было отмечено ранее, локальный класс может использовать локальные переменные, параметры методов, находящиеся в его области видимости и даже параметры-исключения оператора catch, но только те из них, которые объявлены как final. Это объясняется тем, что время жизни локального класса может значительно превышать время выполнения метода, в котором класс определен. По этой причине локальный класс должен иметь свои внутренние копии всех локальных переменных, которые он использует (эти копии автоматически создаются компилятором). Единственный способ обеспечить идентичность значений локальной переменной и ее копии – объявить локальную переменную как final. Опять же напомню, что это все было справедливо до JDK 7 включительно. В JDK 8 ситуация поменялась и можно обойтись без объявления переменной как final, но не менять ее значение в коде после инициализации. Хотя по большому счету лучше, для самоконтроля, все таки объявлять переменные как final.

Экземпляры локальных классов, как и экземпляры внутренних классов, имеют окружающий экземпляр, ссылка на который неявно передается всем конструкторам локальных классов. Локальные классы могут применять такой же синтаксис this, какой используют внутренние классы для явной ссылки на объект окружающего класса или члены. Так как локальные классы не видимы вне блока, в котором они определены, то нет необходимости использовать синтаксис new, применяемый к внутренним классами для явного указания окружающего экземпляра при создании экземпляра локального класса.

Локальные классы могут наследовать любые другие классы и естественно получать доступ к членам этих классов если они не объявлены как private, что в принципе естественно и не нарушает общих правил, поскольку унаследованные члены становятся членами класса наследника, а private члены не могут быть унаследованы, на то они и private.

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

Теперь немного попрактикуемся…

2015-09-03_165328

2015-09-03_165426

И сразу же приведу вывод данной программы, при условии что она была скомпилирована в JDK8:

2015-09-03_165439

Красноватым подсвечены две строки которые вызовут ошибку компиляции, если их компилировать в JDK7 или ниже, так как afStr и noFinal не имеют модификатора final.

Собственно тут мы имеем три класса верхнего уровня: External01, External02 и Outer. В классе Outer есть внутренний класс Inner содержащий метод printInner(), внутри которого определен локальный класс Local и там же создается его экземпляр и на нем вызывается метод printLocal().

В классе Main создаем экземпляр класса Inner и на нем вызываем метод printInner().

L0001

На отрывке кода слева приведен пример того, что если мы попробуем в коде метода printInner() в котором так же определен класс Local изменить аргумент afStr, то компилятор нам выдаст ошибку:

Local variable afStr defined in an enclosing scope must be final or effectively final

В которой сообщает что переменная afStr не является effectively final и соответственно класс не может быть скомпилирован.

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

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

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

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

Следующие два примера, я надеюсь, прояснят эту картину.

L0002

Вывод этой программы:

L0004

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

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

L0005

L0006

У этого примера такой вывод:

L0007

Во втором примере стоит обратить внимание что локальная переменная str является аргументом передаваемым в метод getLocal() класса External и в этом методе она ни где не сохраняется. Более того она даже не доступна из других мест класса External. Она доступна только в методе getLocal(). Кроме того, локальный класс Local так же ни где не сохраняет ее значение, а просто возвращает его, так как имеет доступ ко всем локальным переменным блока в котором был объявлен. Переменная str перестает существовать после завершения работы метода getLocal(). А класс Main, вообще ни чего не может знать о классе Local (впрочем так же как и класс External), так как он ему не доступен, но все таки может его использовать, а локальный класс может использовать переменную str. Почему так все происходит уже объяснялось выше. Если не понятно, то садимся и вдумчиво вкуриваем эту статейку и медитируем внимательно :)

Ну и на последок продемонстрируем пример с затенением полей и наследование в локальных классах.

L0008

L0009

Вывод у программы теперь такой:

L0010

Тут следует обратить внимание что в методах getLocal() и getLocalStr() мы заменили str на argstr, чтобы сохранить предыдущую функциональность, показывающую сохранение копий финальных переменных в экземпляре локального класса. Иначе бы она была затенена полем str локального класса.

В остальном тут все достаточно просто и обсуждалось уже много раз.

1 комментарий: