- Привет, Амиго!
- Привет, Билаабо!
- Я расскажу тебе сегодня про несколько ключевых слов в Java. Но начну с самого интересного – ключевого слова final. Если перевести его с английского, то получится что-то вроде финальный или окончательный.
Ключевое слово final можно добавлять при объявлении переменной, метода и класса.
- А зачем нужен этот final?
- Все довольно просто. Если мы пометили переменную словом final, то она становится неизменной:
final int i = 5; i++; //ошибка компиляции – нельзя менять значение переменной i |
- Ясно.
- Если мы пометили метод словом final, то этот метод запрещено переопределять в классах-наследниках:
class Cat { public final String getName() { return "cat"; } } class Tiger extends Cat { public String getName() //ошибка компиляции – нельзя переопределять метод getName() { return "tiger"; } } |
- Ясно. А зачем может понадобиться запрещать переопределение метода?
- Например, программист написал в этом методе много важного кода и хочет, чтобы все наследники его класса гарантированно имели заданное поведение.
И наконец – третье.
Если мы пометим словом final класс, то таким образом мы запретим наследоваться от него.
public final class Cat { public String getName() { return "cat"; } } class Tiger extends Cat //ошибка компиляции – нельзя наследоваться от класса Cat { public String getName() { return "tiger"; } } |
- А зачем запрещать наследование классов?
- Ты должен понять, что запрет наследования идет не из вредности, а ради безопасности и целостности кода. Если наследование класса не запрещено, то подразумевается, что оно разрешено. И код проектировщика класса будет нормально работать и с объектами его класса и с объектами класса-наследника.
А вот если разработчик видит, что даже при небольших изменениях в его классе, все перестанет работать, как задумано, тогда ему лучше запретить наследование.
- Класс String, например, объявлен как final, как и все примитивные типы: Integer, Boolean, Double, Character,…
- Ага, понимаю. Класс String сделан immutable и если бы вдруг появились изменяемые строки, то много чего перестало бы работать.
- Ну, почти. Скажем так, все работало бы почти по-старому, но иногда возникали бы ошибки, которые было бы очень сложно найти и понять. Поэтому, в некоторых случаях наследование классов или методов не грех и запретить – меньше потом ошибок вылавливать.
- А где еще можно писать final?
- final можно писать перед переменными-аргументами функции и перед переменными в методе. Вот пример:
public void setName(final String name) { final String displayName = "Mr."+name; … this.name = displayName; } |
- А какой в этом смысл?
- Ну, смыслов – два. Во-первых, мы объявляем переменную final – если хотим сообщить другим разработчикам, что это значение – определенная константа, а не просто переменная.
Например, мы хотим рассчитать НДС от цены:
public int getPriceNDS() { final int NDS = 20; return this.price * NDS / 200; } |
И во-вторых, если мы будем писать локальные или анонимные внутренние классы, то такие переменные нам понадобятся. Я расскажу о таких классах в ближайшее время. Но не сегодня.
- Ок, пока вроде ничего сложного.
- Обрати внимание, что неизменяемой становится только переменная, а не объект, на который она ссылается. Объект можно менять еще как.
- Как раз хотел спросить этот момент. А нет способа сделать объект неизменным?
- Нет, только если ты напишешь immutable класс.
Обрати внимание на такой момент - т.к. значение переменной менять нельзя, то ей сразу нужно присвоить начальное значение:
Этот код скомпилируется | Этот код не скомпилируется |
---|---|
class Home { private final int width = 200; private final int height = 100; public Home() { } } |
class Home { private final int width; private final int height; public Home() { } } |
Но, вместе с тем, Java разрешает перенести инициализацию final-переменных класса в конструктор.
Этот код скомпилируется | Этот код не скомпилируется |
---|---|
class Home { private final int width = 200; private final int height; public Home() { height = 100; } } |
class Home { private final int width; private final int height; public Home() { height = 100; } } |
Более того, в разных конструкторах final-переменные можно инициализировать разными значениями. Это очень удобно:
Этот код скомпилируется |
---|
class Home { private final int width; private final int height; public Home() { height = 100; width = 200; } public Home(int width) { this.height = 300; this.width = width; } public Home(int width, int height) { this.height = height; this.width = width; } } |
- Действительно интересная тема, и абсолютно все понятно, спасибо, Билаабо!
- Билаабо принес Амиго задачи:
Задачи |
---|
1. Запретите наследование Запретите наследование от класса Listener. |
2. Запретите переопределение Запретите переопределение метода onMouseDown. |
3. Запретите создание экземпляров класса Запретите создание экземпляров класса Listener. |
- Привет, Амиго!
- Привет, Элли!
- Сегодня будет очень интересная тема. Я сегодня расскажу о внутренних классах.
Если один класс объявить внутри другого, то такой класс называется внутренним.
Объекты внутренних классов при этом вложены в объекты внешних классов и могут обращаться к переменным внешних классов.
Пример |
---|
public class Car { int height = 160; ArrayList<Door> doors = new ArrayList<Door>(); public Car { doors.add(new Door()); doors.add(new Door()); doors.add(new Door()); doors.add(new Door()); } class Door() { public int getDoorHeight() { return (int)(height * 0.80); } } } |
Обрати внимание, у класса Door(дверь) есть метод, который возвращает высоту двери – getDoorHeight, этот метод использует переменную height объекта Car.
Объект типа Door не может существовать отдельно от объекта типа Car – ведь он использует его переменные. Компилятор незаметно добавляет в конструктор и в класс Door ссылку на объект внешнего класса Car, чтобы методы внутреннего класса Door могли обращаться к переменным и вызвать методы внешнего класса Car.
- Вложенные объекты. Мне все понятно. Судя по картинке тут все элементарно.
- Так и есть. Лишь пара нюансов.
Во внутреннем классе Door имеется ссылка на объект класса Car, поэтому:
1) Нельзя создать объект Door внутри статического метода в классе Car: негде взять ссылку на объект типа Car, который неявно передается в конструктор типа Door.
Правильно | Неправильно |
---|---|
public class Car { public static Door createDoor() { Car car = new Car(); return car.new Door(); } public class Door { int width, height; } } |
public class Car { public static Door createDoor() { return new Door(); } public class Door { int width, height; } } |
2) Класс Door не может содержать статические переменные и методы.
Правильно | Неправильно |
---|---|
public class Car { public int count; public int getCount() { return count; } public class Door { int width, height; } } |
public class Car { public class Door { public static int count; int width, height; public static int getCount() { return count; } } } |
- А если мне нужна общая переменная для всех объектов Door?
Ты всегда можешь объявить ее просто в классе Car – это и будет общая переменная для всех объектов Door, вложенных в объект Car.
3) Обрати внимание, что если внутренний класс объявлен как public, то его объекты можно создавать вне внешнего класса, но объект внешнего класса при этом обязан присутствовать:
Car car = new Car(); Car.Door door = car.new Door(); |
Car.Door door = new Car().newDoor(); |
4) И еще одно замечание – чуть не забыла.
Т.к. у нас два вложенных объекта, то в методах внутреннего объекта доступно две ссылки this:
public class Car { int width, height; public class Door { int width, height; public void setHeight(int height) { this.height = height; } public int getHeight() { if (height!=0) return this.height; else return (int)(Car.this.height * 0.8); } } |
Я специально объявила в классах одинаковые переменные.
Чтобы обратиться к переменной внешнего класса, когда она скрыта, или к this внешнего класса, достаточно написать «имя класса» точка this:
Как получить доступ к this внешнего (или любого) класса |
---|
Car.this |
Car.Door.this |
Car.Door.InnerClass2.InnerClass3.this |
- Т.е. если мы внутри метода внутреннего класса пишем this, то этот this относится к внутреннему классу?
- Да. Именно так.
Как тебе внутренние классы, Амиго?
- Очень интересно. Я бы не сказал, что очень сложно.
Есть много ограничений, но они вполне логичны, когда ты мне объяснила, откуда и зачем они берутся.
А я уже два месяца в практических заданиях постоянно пишу вложенные классы и только сейчас понял, что же я писал на самом деле.
Спасибо за отличный урок, Элли.
- Рада, что тебе понравилось, Амиго.
- Привет, Амиго! Готов заниматься?
- Да, давай уже сюда свои задачи по внутренним классам, Диего:
Задачи |
---|
1. Inner Реализовать метод getTwoSolutions, который должен возвращать массив из 2-х экземпляров класса Solution. Для каждого экземпляра класса Solution инициализировать поле innerClasses двумя значениями. Инициализация всех данных должна происходить только в методе getTwoSolutions. |
2. Inner 2 В классе SuperUser метод getDescription должен учитывать страну и город, т.е. возвращать результат аналогичный следующему: My name is George. I'm from the USA, Seattle. Используйте возможности иннер класса. |
2. Inner 3 Внутри класса Solution: 1) реализуйте private class TaskDataProvider используя Task и DbMock, цель которого - обновить поле tasks. 2) реализуйте private class NameDataProvider используя String и DbMock, цель которого - обновить поле names. |
- Итак, тема номер два – вложенные классы.
Перед объявлением внутреннего класса мы можем поставить ключевое слово – static и тогда внутренний класс станет вложенным.
Давай разберемся, что же значит слово static рядом с объявлением вложенного класса. Как ты думаешь?
- Если переменная объявлена статической, то она существует в единственном экземпляре, а если вложенный класс – статический, то можно создать всего один объект такого класса?
- Пусть слово static тут не вводит тебя в заблуждение. Если переменная объявлена статической – то она существуют в единственно экземпляре – это верно. Но статический вложенный класс больше похож на статический метод в этом плане. Слово static перед объявлением класса указывает, что этот класс не хранит в себе ссылок на объекты внешнего класса, внутри которого объявлен.
- Ага. Обычные методы втихаря хранят ссылку на объект, а статические – нет. То же и со статическими классами, я прав, Элли?
- Абсолютно. Твоя догадливость очень похвальна. Вложенные статические классы не имеют скрытых ссылок на объекты внешнего класса, в котором они объявлены.
class Zoo { private static int count = 7; private int mouseCount = 1; public static int getAnimalCount() { return count; } public int getMouseCount() { return mouseCount; } public static class Mouse { public Mouse() { } public int getTotalCount() { return count + mouseCount; //ошибка компиляции. } } } |
Давай посмотрим внимательно на этот пример.
К каким переменным может обращаться статический метод getAnimalCount?
- Только к статическим. Это же статический метод.
К каким переменным может обращаться метод getMouseCount?
- И к статическим, и к нестатическим. Он скрытно хранит ссылку(this) на объект типа Zoo.
- Верно. Так вот, статический вложенный класс Mouse, как и статический метод, может обращаться к статическим переменным класса Zoo, но не может обращаться к нестатическим.
Мы можем спокойно создавать объекты класса Mouse, даже когда нет ни одного созданного объекта класса Zoo. Вот как можно это сделать:
class Home { public static void main(String[] args) { Zoo.Mouse mouse = new Zoo.Mouse(); } } |
Фактически класс Mouse – это самый обычный класс. Из-за того, что он объявлен внутри класса Zoo, у него есть две особенности.
1) При создании объектов вложенного класса (как класс Mouse) вне внешнего класса-родителя, надо еще указывать через точку и имя внешнего класса.
Например так: Zoo.Mouse.
2) Класс Zoo.Mouse и его объекты имеют доступ к private static переменным и методам класса Zoo ( класс Mouse ведь тоже объявлен внутри класса Zoo).
На этом на сегодня все.
- Т.е. просто дополнительное имя и все?
Да.
- Это еще проще, чем казалось на первый взгляд.
- Привет, Амиго!
- Да, задачи, знаю, знаю. Кстати, Диего, у тебя получились отличные задачи.
- Спасибо, что наконец-то заметил. Держи еще:
Задачи |
---|
1. Как выбрать нужное? В методе main присвойте объекту Object obj экземпляр класса TEST Константу TEST и класс TEST менять нельзя. |
2. Рефакторинг Отрефакторите класс Solution: вынесите все константы в public вложенный(nested) класс Constants. Запретите наследоваться от Constants. |
- Привет, Амиго!
- Так здоровались уже, Элли!
- Так, не спорь с тетей. В 31 веке принято здороваться опять, если не видел человека более получаса. Так что не возникай!
Так вот, новая интересная тема – размножение роботов!
- О_О.
- Шучу, новая тема – анонимные вложенные классы.
Иногда в Java встречается ситуация, когда нужно унаследовать класс от нескольких классов. Т.к. множественное наследование классов в Java запрещено, эту проблему решают с помощью внутренних классов: в нужном нам классе мы объявляем внутренний класс и наследуем его от требуемого класса. Пример:
Пример внутреннего класса, унаследованного от Thread |
---|
class Tiger extends Cat { public void tigerRun() { ..... } public void startTiger() { TigerThread thread = new TigerThread(); thread.start(); } class TigerThread extends Thread { public void run() { tigerRun(); } } } |
Давай разберем этот пример:
Нам нужен класс, унаследованный от Thread, чтобы переопределить у него метод run.
Для этого внутри класса Tiger мы объявили внутренней класс TigerThread, который унаследовали от Thread и у которого переопределили метод run.
Для нашего удобства в классе Tiger мы объявили два метода (аналоги методов runи startкласса Thread) –методы tigerRun&startTiger.
В методе startTiger мы создаем объект типа TigerThread и вызываем у него метод start().
При этом Java-машина создаст новую нить, и эта нить начнет работу с вызова метода run, класса TigerThread.
А этот метод в свою очередь вызовет наш метод run – метод tigerRun.
- С нитями я уже дело имел, так что вроде не очень сложно.
А обязательно называть методы tigerRun и startTiger?
- Нет, можно было назвать run и start, но я хотела дополнительно показать, что мы не наследуемся от Thread, к тому же ты мог сильнее запутаться в моем пояснении.
- Ок. Тогда все вроде понятно. Только при вызове метода startTiger второй раз мы создадим еще один класс Thread и запустим его. Не получиться ли что у нас «один тигр будет бегать в двух различных нитях»?
- Ну ты и глазастый. Согласна, это не хорошо. Тогда давай перепишем код так:
Код |
---|
class Tiger extends Cat { public void tigerRun() { ..... } public void startTiger() { thread.start(); } private TigerThread thread = new TigerThread(); private class TigerThread extends Thread { public void run() { tigerRun(); } } } |
- Не то, чтобы отлично. Два раза все равно вызывать такой метод нельзя. Но в этот раз мы хотя бы не будем создавать вторую нить и делать вид, что все хорошо.
- Правильно, запустил второй раз, тигра – получи Exception.
- Я уже лучше тебя вижу все ошибки, Элли!
Да, ты молодец. Тогда перейдём к анонимным внутренним классам.
Обрати внимание на несколько аспектов вышеописанного кода:
1) Мы унаследовались от класса Thread, но фактически не дописали туда никакого кода. Нам скорее пришлось унаследоваться, а не «мы унаследовались с целью расширить класс Thread»
2) Будет создан всего один объект класса TigerThread.
Т.е. с целью переопределить один метод и создать один объект, мы написали целую кучу кода.
Помнишь, как я рассказывала про появление конструкторов?
До изобретения | После изобретения |
---|---|
TigerThread thread = new TigerThread(); private class TigerThread extends Thread { public void run() { tigerRun(); } } |
Thread thread = new Thread() { public void run() { tigerRun(); } }; |
- Вижу, что код стал компактнее, но не совсем понимаю, что произошло.
- Мы можем объединить в одном месте три вещи:
А) объявление класса-наследника
Б) переопределение метода
В) объявление переменной
Г) создание объекта класса-наследника.
Фактически мы объединяем вместе две операции – объявление класса-наследника и создание его объекта:
Без анонимного класса | С использованием анонимного класса |
---|---|
Cat tiger = new { } |
Cat tiger = new Сat() { }; |
Еще раз разбираем синтаксис:
Объявление переменной типа Thread |
---|
Thread thread = new Thread(); |
Объявление переменной типа «анонимный класс-наследник Thread» |
Thread thread = new Thread() { }; |
Обрати внимание – мы не просто объявляем новый класс – мы создаем переменную – в конце ставится точка с запятой!
- А если мы хотим переопределить метод run, то нужно писать так:
Объявление переменной типа Thread |
---|
Thread thread = new Thread() { public void run() { System.out.println("new run-method"); } }; |
- Быстро схватываешь – молодец!
- Спасибо. А если мне нужны еще методы, которых нет у класса Thread?
- Ты можешь их дописать.
Это же полноценный внутренний класс, хоть и анонимный:
Код | Описание |
---|---|
Thread thread = new Thread() { public void run() { printHi(); } public void printHi() { System.out.println("Hi!"); } }; |
Красным цветом отмечен код создания переменной. Зеленым – создания объекта. Синим – код анонимного класса-наследника. |
- Полноценный внутренний класс?
Т.е. я могу и переменные внешнего класса использовать?
- Да, конечно можешь.
- А в конструктор я могу что-то передавать?
- Да, но только параметры конструктора базового класса:
Класс | Объект анонимного внутреннего класса |
---|---|
class Cat { int x, y; Cat(int x, int y) { this.x = x; thix.y = y; } } |
Cat cat = new Cat(3,4) { public void print() { System.out.println(x+" "+y); } }; |
Мы не можем добавить свои параметры в чужой конструктор. Зато можем использовать переменные внешнего класса – это достаточно сильно компенсирует этот недостаток.
- А если мне все-таки очень нужно добавить в конструктор еще параметры?
- Тогда объяви обычный не анонимный внутренний класс и используй его.
- Действительно, я чуть не забыл об этом.
А если я объявлю статическую переменную, тогда анонимный класс будет вложенным, а не внутренним - т.е. без ссылки на внешний класс?
- Нет. Будет именно анонимный внутренний класс. Смотри примеры:
С анонимным классом | Без анонимного класса |
---|---|
Thread thread = new Thread() { public void run() { tigerRun(); } }; |
TigerThread thread = new TigerThread(); private class TigerThread extends Thread { public void run() { tigerRun(); } } |
static Thread thread = new Thread() { public void run() { tigerRun(); } }; |
static TigerThread thread = new TigerThread(); private class TigerThread extends Thread { public void run() { tigerRun(); } } |
- Ясно. Статической становится только переменная, но не класс.
- Ага.
На самом деле, в процессе компиляции Компилятор создает внутренние классы для всех анонимных внутренних классов. Такие классы обычно получают имена «1», «2», «3», и т.д.
- Кто хочет задачи по анонимным внутренним классам?
- Кто же еще, конечно я. Давай их уже. Хочу немного попрограммировать:
Задачи |
---|
1. Напряги извилины! Метод printName должен выводить свое собственное имя, т.е. "sout" Сделайте минимум изменений. |
2. Повторяем threads Сделать так, чтобы в методе someActions вызывались только методы класса Solution. Ожидаемый вывод в методе main: Amigo: Mmmmm, beef Amigo: knock knock Amigo: Zzzzzzz...1 sec |
3. Анонимность иногда так приятна! 1. В пакете vo создайте public классы User, Location, Server, Subject, Subscription, которые наследуются от NamedItem 2. В классе Solution для каждого класса создайте свой метод, который возвращает список экземпляров класса. Например, для класса User это будет - public List Для класса Location это будет - public List 3. Внутри каждого такого метода создайте анонимный класс от AbstractDbSelectExecutor и вызовите его нужный метод. Подсказка: тело метода должно начинаться так: returnnewAbstractDbSelectExecutor 4. Пример вывода для User и Location: Id=5, name='User-5', description=Got by executing 'select * from USER' Id=1, name='Location-1', description=Got by executing 'select * from LOCATION' 5. Проанализируйте пример вывода и сформируйте правильный query для всех классов. 6. Классы не должны содержать закоментированного кода. |
- Привет, Амиго!
Продолжаем наши уроки – учимся гуглить.
Вот тебе несколько заданий:
Вопрос | |
---|---|
1 | Как отсортировать массив чисел? |
2 | Как отсортировать список строк в обратном алфавитном порядке? |
3 | Как отправить email из программы Java? |
4 | Как определить, содержит ли переданный объект определенный метод? |
5 | В чем отличие TreeMap и HashMap? |
6 | Зачем нужно писать в таком коде ArrayList<?> |
7 | Как узнать максимальное значение int? |
8 | Как узнать минимальное значение byte? |
9 | Как преобразовать число в 16-тиричную строку (123->"7B")? |
10 | Как преобразовать число в двоичную строку (123->"1111011")? |
- Привет, Амиго!
Вот тебе дополнительный материал по теме.
- Привет, дружище!
Элли сегодня подходит и спрашивает: - Дорогой, как я тебе?
А, я что-то на своей волне был, вот и отвечаю:
Немножко надоела... А так - ничего!
- Ну ты даешь!
- Привет, Амиго!
Вопросы к собеседованиям | |
---|---|
1 | Какие бывают внутренние классы? |
2 | Во что компилируется анонимный внутренний класс? |
3 | Зачем использовать ключевое слово final при создании анонимных классов? |
4 | Как правильно создать объект внутреннего класса? |
5 | Как правильно создать объект вложенного класса? |
6 | Можно ли создавать статические методы/переменные во внутреннем классе? |
7 | Назовите три любых внутренних класса? |
8 | Как внутренние классы решают проблему множественного наследования в Java? |
9 | Чем отличаются анонимные классы, созданные на основе интерфейса и на основе класса? |
10 | Можно ли создать анонимный статический вложенный класс? |
- Здорово, Амиго!
- Здравия желаю, Капитан Бобров!
- Готов к новой полностью секретной миссии?
- Готов, сэр!
- Тогда вот тебе инструкции к первому секретному заданию:
«А теперь напишем игру "змейка"».
- Есть, написать игру «змейка»!
- Да не кричи ты. Говорят же тебе – полностью секретное задание.
Для его выполнения будешь работать с агентом IntellijIDEA. Он введет тебя в курс дела.
Все дальнейшие инструкции также через него.
- Разрешите приступить, сэр?
- Приступайте.