Не смотря на отсутствие статей за последние месяцы, хочу заявить что блог жив :) Множество статей за эти месяцы находится в стадии черновиков и редактирования. По мере завершения работы над ними они будут появлятся за соответствующие прошлые даты, когда они были созданы в виде черновиков. Так что лучше следить за обновлением оглавления блога.
24 дек. 2015 г.
30 авг. 2015 г.
Внутренние классы. Часть 7 – безопасность
Как уже говорилось вложенные классы имеют доступ ко всем членам внешнего класса включая даже private поля и методы. Может возникнуть вопрос, каким образом вложенные классы получают дополнительные привилегии доступа, ведь они преобразуются в обычные, а виртуальной машине вообще о них ничего не известно?
Все это изучим на одном примере, где мы попытаемся получить доступ к закрытому полю класса, используя подставные классы. Как уже говорилось внутренние классы имеют доступ ко все членам внешнего класса. Виртуальная же машина ни чего не знает о внутренних классах, так как компилятор преобразует все внутренние классы в классы верхнего уровня и именует их по правилам которые мы уже обсуждали несколько раз.
Идея в том чтобы создать клас который примитивно содержит в private поле пароль. И у него будет только один метод который проверяет строку введенную как параметр на предмет совпадает она или нет с паролем хранящемся в приватном поле. Но этот метод не возвращает значение этого поля, а лишь выводит сообщение правильный пароль или нет.
Затем в другом проекте мы попробуем создать внутренний класс, который постарается получить доступ к этому приватному полю.
И так! У нас есть класс PassCheck, у которого есть private поле PASSWORD содержащее пароль. Так же у него есть метод, который позволяет проверить переданное в него значение.
И затем выводит соответствующее сообщение.
Но прошу заметить этот метод не возвращает значение private поля, а лишь проверяет соответствует ли переданный аргумент паролю.
В классе Main просто происходит создание экземпляра класса PassCheck и передача в его метод значения полученного из командной строки.
Пример запуска программы приведен слева.
Как мы видим программа очень простая.
Но в ней нет метода выводящего пароль. Мы же хотим его получить.
И так представим ситуацию что у нас есть только класс файлы с этой программой, то есть файлы имеющие расширение .class, но нет исходных текстов. И нам надо как то получить заветный пароль.
Теперь начинаем исследование наших .class файлов при помощи утилиты javap. Сразу же хочу предупредить некоторых умников, что да, если запустить javap с ключом –c, то в дизассемблированном коде можно увидеть наш пароль в чистом виде, но это не то что мы хотим. Мы хотим написать свою программу которая выведет значение private поля на экран.
Если мы дадим команду javap -p PassCheck.class, то увидим следующий вывод на экран:
Мы видим что у класса PassChek есть private поле PASSWORD, а так же метод, который ни чего не возвращает (void), а просто получает значение типа String и как можно предположить, сравнивает его с паролем. Ну мы то знаем что сравнивает :) сами же писали. Но сейчас мы как будто потеряли исходники и забыли пароль.
В классе Main как видим ни чего интересного нет. Мы можем предположить что там просто создается экземпляр класса PassChek и в него передается аргумент командной строки.
То есть нам надо работать с классом PassCheck и получить значение поля PASSWORD.
Для этого постараемся использовать внутренние классы, так как они имеют доступ даже к private полям внешнего класса. Но тут есть одна заковырка. Внутренний класс в априори должен быть в том же пакете что и внешний. Для этого нам придется создать еще один проект и в нем создать пакет pro.java.inner07. И в этом пакете мы создадим свой класс Main, назвав его MainHack, и свой класс PassCheck, название его надо нам будет сохранить и внутри него создадим внутренний класс. Затем откомпилируем все это дело и в каталог где лежат наши атакуемые .class файлы перепишем MainHack.class и внутренний класс, внешний же оставим на месте, так как нам надо атаковать именно класс PassCheck в его изначальном виде.
Мы создали два класса. Наш класс PassCheck содержит внутренний класс, который к тому же наследуется от внешнего класса PassCheck, а значит наследует и поле PASSWORD.
Хотя, в принципе, наследоваться было и не особо нужно, но это задел на следующую тему, где мы будем более подробно рассматривать наследование.
Класс MainHack создает экземпляр класса PassCheck и вызывает на нем метода сравнения паролей. Затем создает экземпляр внутреннего класса Hack и вызывает на нем унаследованный им и переопределенный метод comparePassword().
Этот метод на самом деле ни чего не сравнивает, а должен просто вывести значение поля PASSWORD.
То что у нас получилось я зафиксировал в коммите Git под шагом 1. Это если кто то захочет повторить этот эксперимент.
Запуск и работа класса MainHack и модифицированного класса PassCheck показана слева. Теперь к сути. У нас получилось три файла:
И нас интересует файл PassCheck$Hack.class.
Что будет если его подставить в пакет где у нас находится настоящий атакуемый нами класс PassCheck?
И естественно для его запуска нам нужен будет класс MainHack. На скрине слева показаны те файлы которые мы скопировали в пакет с атакуемым классом PassCheck.class. Запустим MianHack и посмотрим что будет…
И мы наблюдаем очень интересное поведение. Мы видим что атакуемый класс PassCheck, отработал, но правда с ошибками с которыми мы чуть позже разберемся. А пока надо понять что в классе MainHack и в классе PassCheck$Hack нет кода который бы выводил сообщение "Пароль не правильный", он есть только в классе PassCheck, что и доказывает что наш класс MainHack запустил атакуемый нами класс PassCheck. Мы так же видим номера строк где вылезла ошибка. Про трассировку ошибок мы еще поговорим но гораздо позже. А пока обращаем внимание на строку под номером 18, которая находится во внутреннем классе PasccChek$Hack.
То есть как видно из отрывка кода, ошибку вызывает строка отмеченная стрелкой:
println
("PASSWORD = " + PASSWORD);Но есть и хорошая новость! Этот облом показывает что наш класс PassCheck$Hack выполняется, и что очень важно пытается получить доступ, но получает по шаловливым ручкам.
Давайте закомментируем эту строку. Откомпилируем наш класс PassCheck$Hack и попытаемся запустить все снова.
Когда мы закомментировали 18 строку выполнение нашего класса PassCheck$Hack прошло без ошибок! То есть экземпляр нашего класса PassCheck$Hack подцепляется к экземпляру атакуемого класса PassCheck. Сбой происходит только тогда, когда наш класс хочет получить доступ к private полю. Теперь осталось понять почему вылетает ошибка и как это обойти.
Для этого придется поговорить о более продвинутых вещах, чем начальное изучение Java :)
И так, поехали!
В Java Language Specification (section 13.1.11) говорится: "A construct emitted by a Java compiler must be marked as synthetic if it does not correspond to a construct declared explicitly or implicitly in source code, unless the emitted construct is a class initialization method".
Ну и кратко на русском. Любы конструкции добавленные компилятором в байт-код класса, не имеющие соответствующих конструкций в исходном коде, должны помечаться как синтетические. Исключение составляют конструкторы по умолчанию и инициализационные блоки.
Еще одно упоминание о синтетических методах можно найти в документации на метод Member.isSynthetic().
Компилятор Java, так же, должен создавать синтетические методы для вложенных классов в случаях когда происходит доступ к private членам из вложенного класса во внешнем или наоборот, из внешнего к вложенным.
Чтобы это продемонстроровать расскоментируем строку println("PASSWORD = " + PASSWORD); но не будем запускать программу, а просто откомпилируем и посмотрим при помощи javap.
Как мы видим в подчеркнутой красной линией строке, появился новый статический метод с именем access$0. Это и есть синтетический метод который был создан компиляротом. В нашем коде класса PassCheck такого метода нет. Этот метод был создан для того чтобы предоставить доступ к private члену внешнего класса, нашему внутреннему классу. Синтетические методы создаются для доступа к каждому private члену. Если бы у нас было несколько private членов и обращений к ним, то для каждого бы был создан отдельный синтетический метод. Надо заметить, что синтетические метды создаются при прямом доступе к ним, если доступ к ним предоставляют public методы, то синтетические методы создаваться не будут. На этом, пока с синтетическими методами закончим и пойдем дальше.
А дальше без хирургического вмешательства ни как. Тут еще надо понять, что в изначальном, атакуемом (откомпилированном) классе PassCheck содержится пароль, который мы хотим увидеть, но там нет метода который его ввыводит. И нет синтетического метода который бы обращался к private полю PASSWORD. Поэтому нам туда его надо внеднить. То есть пропатчить непосредственно файл PassCheck.class.
Для этого сравним откомпилированные классы (файлы .class) – атакуемый и атакующий.
Как видим разница есть. Ну это и естественно :) И поэтому нам надо пропатчить атакуемый файл CheckPass.class.
После того как он будет пропатчен изменениями из атакующего файла PassCheck.class, надо еще будет переписать файлы MainHack.class и PassCheck$Hack.class в каталог с атакуемым классом. Копирование этих классов уже было показано на одном из сркинов выше. Ну и теперь запускаем наш MainHack.class…
Тадааааммммм! Мы получили то что хотели. Правда с хирургическим вмешательством.
Конечно до приватного поля можно было добраться и через reflection, но эту тему мы пока не проходили поэтому оставим ее на потом :)
29 авг. 2015 г.
Внутренние классы. Часть 6 – практика.
Сперва поупражняемся по мотивам видео представленного ниже.
Очень хороший урок и объяснение внутренних классов и так же удачный пример.
Собственно это мое повторение примера приведенного в видео.
По большому счету здесь и объяснять то нечего, так как в видео все разжевано дальше не куда :)
Единственное на что стоит снова обратить внимание так это на то, что для внутренних классов доступны все поля и методы внешнего класса, даже те что объявлены как private.
Именно поэтому в методах getAmount() и toDeposit() класса Card можно напрямую обратиться к private полю amount.
И катати, да, :) я случайно засунул этот пример в пакет anonym05. Хочу заметить что к анонимным классам данный пример ни какого отношения не имеет :) просто лень его было уже переносить в другой пакет :)
Ну и вывод программы:
Из вывода программы видно, что изменения производимые через объект внутреннего класса изменяют поле amount объекта внешнего класса.
Ну и чтобы все стало еще более понятно приведу скриншот класса Main:
Надеюсь это уже все поставит на свои места.
Сперва создаем экземпляр класса Account – father.
Затем выводим на консоль его состояние.
После создаем экземпляр класса Card – mother.
Выводим его состояние на консоль.
Затем через mother снимаем со счета 18 у.е. :)
Выводим состояние счета через экземпляры классов Account и Card. Видим что оно одинаковое. Как и должно быть.
Пополняем счет через мамочку на 8 у.е., выводим на консоль. Пополняем через папочку на 10 у.е., выводим на консоль.
Все работает как и ожидалось, как и должно было работать.
В следующем примере мы повторим программу из этой статьи, но только уже с использованием анонимного класса.
В единственном методе start() класса Clock, анонимный класс имплементирует интерфейс ActionListener. В данном случае это просто вывод текущего времени на экран. Затем создается объект таймера куда как параметры передаются ссылка на нашу реализацию интерфейса и время через которое будет срабатывать его метод (в нашем случае 1сек.). В метод start так же передается boolean параметр, если он true, то будет еще выводится и линия после вывода времени.
Заметьте, что передаваемый параметр boolean сопровождается модификатором final, хотя если вы используете JDK8, то это не обязательно. Главное чтобы он не изменялся в методе start().
Теперь с новыми знаниями об анонимных классах перепишем пример фабричного метода, который мы уже писали тут. Весь код на скриншоте я приводить тут не буду. Разницу между этим и предыдущим можно посмотреть перейдя по линкам. Но сразу можно сказать что текущий пример стал куда короче.
На скриншоте приведет пример имплементации обоих интерфейсов. Причем имплементация ServiceFactory сделана при помощи анонимного класса, что очень уменьшило объем кода.
Вывод программы представлен слева.
Чтобы все стало более понятно приведу ниже еще и код создания наших "продуктов".
Тут приведен отрывок из кода, который создает наши "продукты" и вызывает на них методы.
Принцип тут тот же, что был описан тут. Это кстати тоже пример (немного переделанный мной) из книги "Философия Java". Хотя там все реализовано было через статическое поле, но я решил сделать чуть по другому :) Чтобы можно было задать имя сервиса и вывести его.
Вроде бы с этим примером все :)
Теперь еще повторим пример фабричного метода с игрой, но уже с анонимными классами. Тут уже не буду приводить отрывков кода, просто можно перейти по линкам и сравнить эти два примера.
И еще один пример итератора с использованием анонимных классов.
Это переделанный вариант кода который мы писали тут.
Вывод у программы не изменился:
Единственное что мы сделали при помощи анонимного класса, так это сражу же в объявлении метода selector() избавились от необходимости создавать отдельный класс SequenceSelector.
В результате этого код стал чуть поменьше и чуть попонятней. А может и менее понятней :), но интересней и занятней :)
Сейчас метод selector() возвращает ссылку на экземпляр анонимного класса приведенную к типу интерфейса ISelector.
И затем через методы анонимного класса происходит вывод значений хранящихся в items. То есть еще раз демонстрируется, что для внутренних классов доступны даже private члены внешнего класса.
28 авг. 2015 г.
Внутренние классы. Часть 5 – анонимные классы.
Анонимный класс – это локальный класс без имени. Можно объявить анонимный (безымянный) класс, который может расширить (extends) другой класс или реализовать (implements) интерфейс. Объявление такого класса выполняется одновременно с созданием его объекта посредством оператора new.
Анонимные классы эффективно используются, как правило, для реализации (переопределения) нескольких методов и создания собственных методов объекта. Так же когда локальный класс используется всего один раз, можно применить синтаксис анонимного класса, который позволяет совместить определение и использование класса.
Конструкторы в анонимных классах ни определять, ни переопределять нельзя. Анонимный класс не может иметь конструкторов, поскольку имя конструктора должно совпадать с именем класса, а в данном случае класс не имеет имени.
Так как у анонимного класса нет имени, то, как я уже говорил, в теле класса нельзя определить его конструктор. Это одно из основных ограничений анонимных классов. Любые аргументы, которые вы укажете в круглых скобках, стоящих за именем родительского класса в определении анонимного класса, неявно передаются конструктору родительского класса. Чаще всего анонимные классы применяются для расширения родительских классов простыми классами, которые не требуют аргументов конструктора, поэтому скобки в определении анонимного класса зачастую пусты.
Анонимные классы допускают вложенность друг в друга, что очень сильно запутывает код и делает эти конструкции непонятными, поэтому эти возможности обычно не используются.
Теперь рассмотрим на практике простой пример:
На примере слева красными рамками обозначены фрагменты кода где объявляются анонимные классы. Первый имплементирует интерфейс Iout, второй расширяет класс External. Обратите внимание на двоеточие после фигурных скобок закрывающих объявление класса, оно является обязательным, так как по существу объявление анонимного класса представляет собой выражение. Это означает, что его можно записать как часть большего выражения, например вызова метода. Я привел достаточно простые примеры, хотя синтаксис все равно может показаться сложным. И начал я с объявления анонимного класса реализующего интерфейс Iout, так как на этом примере более легко понять почему анонимные классы называются анонимными – так как у них нет имени. Как вы помните невозможно создать экземпляр интерфейса, поскольку он является полностью абстрактным классом, а тут мы создаем класс реализующий интерфейс, но у этого класса нет имени и ссылку на этот анонимный класс мы присваиваем интерфейсной переменной iout. Надеюсь все просто? Ни кто не запутался?
Во втором же примере не все так прозрачно, так как может показаться что у класса все же есть имя External, но это не так. Класс по прежнему анонимный, так как он расширяет класс External и не является экземпляром класса External, хотя ссылка на этот анонимный класс присвоена переменной класса External, что является нормальным и обычным. Надеюсь что опять ни кто не запутался :)
Анонимные классы, если они созданы как расширение супер класса могут ссылаться на члены своих супер классов. Код и вывод программы подтверждают это.
Первую строку выводит метод outPrint(), другие – extPrint().
Как видите, синтаксис определения анонимного класса включает так же и создание экземпляра этого класса используя для этого ключевое слово new, за которым следует имя супер класса или интерфейса и затем определение самого анонимного класса в фигурных скобках за которыми следует двоеточие. Если имя, следующее за ключевым словом new, это имя класса, то анонимный класс является подклассом этого класса. Если имя, следующее за ключевым словом new, представляет собой интерфейс, то анонимный класс реализует этот интерфейс и расширяет класс Object. Данный синтаксис не позволяет указать секции extends, implements или имя класса. В следствии этого анонимный класс может реализовать только один интерфейс.
Теперь небольшой пример на то, что объявление анонимного класса представляет собой выражение. То есть мы можем использовать его как часть другого выражения, что показано на примере слева подсвеченной строкой. Так же обратите внимание как мы получаем доступ к анонимному классу в данном примере.
Вывод у программы следующий:
Как видите, поскольку анонимный класс не имеет имени, то компилятор использует номера в качестве идентификаторов для таких классов. На скриншоте показано как выглядят в откомпилированном виде классы нашей программы.
Для анонимных классов, так же как и для локальных, компилятор передает в конструктор скрытую ссылку .this на окружающий класс. Поэтому к объекту окружающего класса можно обращаться так же, как и в локальном классе – через имя_венешнего_класса.this.
Так как анонимные классы представляют один из типов локальных классов, анонимные классы и локальные классы несут одинаковые ограничения.
Анонимные классы, так же как и локальные, имеют доступ к локальным переменным своего блока кода которые объявлены как final или они должны быть effectively final.
Анонимные классы имеют доступ ко всем членам своего вешнего класса.
Если в анонимном классе объявлена переменная с таким же именем как и в окружающем классе, то она затеняет переменную окружающего класса.
Анонимный класс не может определять статические поля, методы или классы, кроме констант static final. Интерфейс не может быть объявлен анонимно, потому что нет способа реализовать интерфейс без имени. Так же как и локальные классы, анонимные классы не могут быть public, private, protected или static.
В анонимном классе вы не можете объявить статические инициализационные блоки.
В анонимном классе вы не можете объявить интерфейс.
Поскольку у анонимного класса нет имени, то невозможно определить конструктор анонимного класса. Если вашему классу нужен конструктор, вы должны задействовать локальный класс. Впрочем, зачастую в качестве замены конструктора можно применять инициализатор экземпляра.
В анонимном классе вы можете объявить:
- Поля
- Дополнительные методы (даже если этих методов нет в классе родителе)
- Инициализационные блоки экземпляра
- Локальные классы
Ну и теперь рассмотрим примеры всех вышеприведенных утверждений.
Начнем пожалуй с этого :) то есть с .this. В Принципе это можно было еще на первом примере показать, но чет запамятовал. В общем первый пример мутировал в текущий. Я добавил строку str в класс Outer и затем обратился к ней из анонимного класса. Эти две строки подсвечены желтым.
Теперь у программы такой вывод:
В общем тут нет ни чего не обычного, так как анонимные классы это те же внутренние классы, но с некоторыми ограничениями которые мы уже озвучили чуть выше.
А теперь попробуем смоделировать конструктор анонимного класса :)
Чтобы смоделировать конструктор в анонимном классе нам будет необходим класс от которого будет наследоваться наш анонимный класс. С анонимным классом имплементирующем интерфейс такой номер не прокатит, так как у интерфейсов нет конструкторов.
Нам так же необходимо чтобы у родительского класса был конструктор. И тогда, как уже говорилось, все аргументы которые будут указаны в круглых скобках при создании анонимного класса будут передаваться конструктору родительского класса, но так же будут доступны и в анонимном. Причем даже если в родительском классе эти аргументы используются только в конструкторе и ни где более, то есть не сохраняются в полях родительского класса, эти аргументы все равно будут доступны в анонимном классе. Но они должны быть переданы в конструктор с модификатором final или быть effectively final.
В данном примере так же видно, что анонимный класс имеет доступ к private полям внешнего класса, а аргумент i переданный в конструктор супер класса доступен в инициализаторе и методах анонимного класса и этот аргумент можно использовать для инициализации полей анонимного класса, но саму переменную i изменять нельзя.
Вывод программы представлен ниже:
Как видно из вывода программы аргумент i переданный в конструктор суперкласса Base доступен как в конструкторе суперкласса, так и в инициализаторе и методах анонимного класса, который в данном случае является и наследником класса Base и так же внутренним его классом. Именно поэтому ему доступно private static поле i. Я умышленно дал одинаковые названия аргументу и полю, чтобы продемонстрировать области видимости переменных и полей.
И еще один пример на тему эмулирования конструкторов в анонимных классах:
В это программе не используются статические методы и поля для того чтобы продемонстрировать доступ к полям экземпляров в классах Base и External. По существу у нас есть два анонимных класса. Анонимный класс в классе Base является и вложенным в него и его же наследником, а в классе External анонимный класс является наследником класса Base и внутренним для класса External. Именно по этому в первом случае анонимный класс имеет доступ к private полю str класса Base, а во втором не имеет и использует для доступа к нему унаследованный метод getStr(). Собственно из вывода программы видно как она работает :)
Я немного изменил предыдущий пример, чтобы продемонстрировать наследование. Теперь класс External является наследником класса Base. И поэтому от туда был убран метод getThis() и добавлены конструкторы. Класс Main тоже претерпел небольшие изменения, туда была добавлена одна строка.
println
("e.getStr = " + e.getStr());Обратите внимание что анонимный класс находящийся внутри класса External так же является наследником класса Base.
С теорией по внутренним классам вроде пока все :) В следующем посте немного попрактикуемся по всей теме внутренних классов. Может по ходу практики еще всплывет что-то, что возможно было упущено в теории.
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. Исключение составляет случай когда наследование локального класса другим локальным классом происходит в том же блоке кода, но это вообще сумасшедшая ситуация, хотя и вполне возможная :)
Теперь немного попрактикуемся…
И сразу же приведу вывод данной программы, при условии что она была скомпилирована в JDK8:
Красноватым подсвечены две строки которые вызовут ошибку компиляции, если их компилировать в JDK7 или ниже, так как afStr и noFinal не имеют модификатора final.
Собственно тут мы имеем три класса верхнего уровня: External01, External02 и Outer. В классе Outer есть внутренний класс Inner содержащий метод printInner(), внутри которого определен локальный класс Local и там же создается его экземпляр и на нем вызывается метод printLocal().
В классе Main создаем экземпляр класса Inner и на нем вызываем метод printInner().
На отрывке кода слева приведен пример того, что если мы попробуем в коде метода printInner() в котором так же определен класс Local изменить аргумент afStr, то компилятор нам выдаст ошибку:
Local variable afStr defined in an enclosing scope must be final or effectively final
В которой сообщает что переменная afStr не является effectively final и соответственно класс не может быть скомпилирован.
Теперь давайте попробуем разобраться с этим эффектом чуть подробнее, чтобы понять что происходит и почему.
Локальная переменная определяется в блоке кода, который задает ее область видимости. Локальная переменная перестает существовать вне области видимости. Java – это язык с лексической областью видимости. Это означает, что область видимости зависит от того, как написан исходный код. Любой код внутри фигурных скобок, обрамляющих границы блока, может использовать локальные переменные, определенные в этом блоке.
Лексическая область видимости просто определяет сегмент исходного кода, внутри которого может применяться переменная. Обычно область видимости принято считать временной областью, то есть локальная переменная существует с момента, когда интерпретатор Java начал выполнение некоторого блока, и до момента, когда интерпретатор вышел из этого блока. Обычно именно так нужно представлять локальные переменные и область их видимости.
Введение в язык локальных классов портит картину, так как локальные классы могут использовать локальные переменные, а период жизни экземпляров локальных классов может превышать период, в течение которого интерпретатор выполняет блок кода. Другими словами, если вы создаете экземпляр локального класса, то он не пропадает автоматически, когда интерпретатор завершает выполнение блока, создавшего этот класс.
Следующие два примера, я надеюсь, прояснят эту картину.
Вывод этой программы:
Поведение этой программы может вызвать удивление. Помните, что лексическая область видимости методов локального класса никак не связана со временем входа и выхода интерпретатора из блока, определяющего локальный класс. Можно сказать и по-другому: каждый экземпляр локального класса содержит автоматически созданную копию каждой локальной переменной fi, которую он использует. Поэтому у него есть собственная приватная копия области видимости, которая существовала при создании класса.
Еще одна фишка этой программы в том, что на каждой итерации цикла переменная fi и локальный класс создаются заново.
У этого примера такой вывод:
Во втором примере стоит обратить внимание что локальная переменная str является аргументом передаваемым в метод getLocal() класса External и в этом методе она ни где не сохраняется. Более того она даже не доступна из других мест класса External. Она доступна только в методе getLocal(). Кроме того, локальный класс Local так же ни где не сохраняет ее значение, а просто возвращает его, так как имеет доступ ко всем локальным переменным блока в котором был объявлен. Переменная str перестает существовать после завершения работы метода getLocal(). А класс Main, вообще ни чего не может знать о классе Local (впрочем так же как и класс External), так как он ему не доступен, но все таки может его использовать, а локальный класс может использовать переменную str. Почему так все происходит уже объяснялось выше. Если не понятно, то садимся и вдумчиво вкуриваем эту статейку и медитируем внимательно :)
Ну и на последок продемонстрируем пример с затенением полей и наследование в локальных классах.
Вывод у программы теперь такой:
Тут следует обратить внимание что в методах getLocal() и getLocalStr() мы заменили str на argstr, чтобы сохранить предыдущую функциональность, показывающую сохранение копий финальных переменных в экземпляре локального класса. Иначе бы она была затенена полем str локального класса.
В остальном тут все достаточно просто и обсуждалось уже много раз.