This commit is contained in:
Ivan I. Ovchinnikov 2023-01-23 15:07:01 +03:00
parent 66c43d0451
commit 71bcdcffd2
1 changed files with 34 additions and 50 deletions

View File

@ -7,7 +7,7 @@
Экран & Слова \\ \hline Экран & Слова \\ \hline
\endhead \endhead
Титул & Здравствуйте, добро пожаловать на курс посвящённый инструментарию разработки джава \\ \hline Титул & Здравствуйте, добро пожаловать на курс посвящённый инструментарию разработчика на джава \\ \hline
Отбивка & и сегодня на повестке дня у нас интерфейсы, но не те интерфейсы, что графические, а те, что программные. \\ \hline Отбивка & и сегодня на повестке дня у нас интерфейсы, но не те интерфейсы, что графические, а те, что программные. \\ \hline
@ -49,14 +49,40 @@
Отбивка Понятие интерфейса & Механизм наследования очень удобен, но он имеет свои ограничения. В частности мы можем наследовать только от одного класса, в отличие, например, от языка С++, где имеется множественное наследование \\ \hline Отбивка Понятие интерфейса & Механизм наследования очень удобен, но он имеет свои ограничения. В частности мы можем наследовать только от одного класса, в отличие, например, от языка С++, где имеется множественное наследование \\ \hline
фото руля-педалей-коробки, фото юсб, фото клавиатуры-мыки-монитора & В языке Java эту проблему частично позволяют решить интерфейсы. Интерфейсы определяют некоторый функционал, не имеющий конкретной реализации, который затем реализуют классы, применяющие эти интерфейсы. И один класс может реализовать множество интерфейсов. Если сказать проще, интерфейс можно очень-очень грубо представить как очень-очень абстрактный класс. До седьмой джавы это был просто набор методов без реализации. Начиная с восьмой наделали много тонкостей, с ними будем разбираться. Итак интерфейс - это описание методов. Примером интерфейса в реальной жизни может быть интерфейс управления автомобилем, интерфейс взаимодействия с компьютером или даже интерфейс USB, так, компьютеру не важно, что именно находится по ту сторону провода, флешка, веб-камера или мобильный телефон, а важно, что компьютер умеет работать с интерфейсом USB, отправлять туда байты или получать. Потоки ввода-вывода, которые мы проходили чуть раньше - это тоже своего рода интерфейс, соединяющий не важно какой программный код и не важно какой, например, файл. Все методы во всех интерфейсах всегда публичные, и в классическом варианте не имеют реализации. Ну и поскольку все методы всегда паблик то этот модификатор принято просто не писать. Для новичка это неочевидно и сбивает с толку, может показаться что модификатор дефолтный, а на самом деле он публичный, поблагодарим разработчиков джавы и пойдём дальше. \\ \hline интерфейс - это описание способов взаимодействия с объектом. фото руля-педалей-коробки, фото юсб, фото клавиатуры-мышки-монитора & В языке Java эту проблему частично позволяют решить интерфейсы. Интерфейсы определяют некоторый функционал, не имеющий конкретной реализации, который затем реализуют классы, применяющие эти интерфейсы. И один класс может применить к себе множество интерфейсов. Правильно говорить реализовать интерфейс, будем сразу говорить правильно. Если сказать проще, интерфейс можно очень-очень грубо представить как очень-очень абстрактный класс. До седьмой джавы это был просто набор методов без реализации. Начиная с восьмой наделали много тонкостей, с ними и будем разбираться. Итак интерфейс - это описание методов. Примером интерфейса в реальной жизни может быть интерфейс управления автомобилем, интерфейс взаимодействия с компьютером или даже интерфейс USB, так, компьютеру не важно, что именно находится по ту сторону провода, флешка, веб-камера или мобильный телефон, а важно, что компьютер умеет работать с интерфейсом USB, отправлять туда байты или получать. Потоки ввода-вывода, которые мы проходили чуть раньше - это тоже своего рода интерфейс, соединяющий не важно какой программный код и не важно какой, например, файл. Все методы во всех интерфейсах всегда публичные, и в классическом варианте не имеют реализации. Ну и поскольку все методы всегда паблик то этот модификатор принято просто не писать. Для новичка это неочевидно и сбивает с толку, может показаться что модификатор дефолтный, а на самом деле он публичный, поблагодарим разработчиков джавы и пойдём дальше. \\ \hline
06-16 & Интерфейсы объявляются также, как классы, и вообще могут иметь очень похожую на класс структуру, то есть быть вложенным или внутренним, но чаще всего интерфейсы описывают в отдельном файле, также как класс, но используя ключевое слово интерфейс. Создадим пару интерфейсов, например, человек и бык, опишем в них методы, например, ходить и издавать звуки. \\ \hline
06-17 & Для чего мы так сделали? Продолжим пример, создадим пару классов, класс мужчина и класс был. Класс мужчины будет у нас реализовывать интерфейс человека. То есть множественного наследования нет, но мы можем реализовать сколько угодно интерфейсов. Для того, чтобы реализовать интерфейс - мы должны переопределить все его методы, либо сделать класс абстрактным. Вот, статический анализатор кода в идее нам об этом явно говорит. Выведем модное сообщение о том что это классы мужчины или быка.
И теперь самая соль - мы можем в мэйне объявлять не только классы и создавать объекты, но и создать переменную, которая реализовывает интерфейс. То есть тут могут лежать абсолютно никак не связанные между собой объекты, главное, чтобы они реализовывали интерфейсА. И мы можем работать с методами интерфейса, которые могут быть для разных классов вообще по-разному реализованы. Такой вот полиморфизм. Понимаете насколько сильно это отличается от наследования, когда мы с вами создавали общий абстрактный класс животное и от него наследовали наших котиков? \\ \hline
06-18 & Чтобы стало сильно понятнее, создадим класс минотавра, кто плохо помнит греческую мифологию, это такой товарищ, который с телом человека и головой быка скучно сидел в лабиринте и ждал заблудившихся путников. соответственно, реализовывал интерфейсы человека и быка своим способом, ходил на ногах человека, но не мычал, как бык, а загадки загадывал. Интересно то, что в программе мы можем к минотавру обратиться не только как к человеку, но и как к быку, то есть гипотетически, можно создать некоторого Тесея, погонщика минотавровых стад. Но это уже, так сказать, полёт фантазии, нас интересует только техническая часть вопроса - классы не связаны между собой наследованием, а обращение к ним единообразное. \\ \hline
06-19 & Также важно, что в интерфейсах разрешено наследование. То есть у нас интерфейс может наследоваться от другого интерфейса, соответственно при реализации интерфейса мы должны переопределить методы всех родителей нашего интерфейса, то есть тут картина очень похожа на наследование классов, но внимание не запутайтесь, в интерфейсах разрешено множественное наследование. \\ \hline
06-20 & На несколько минут вернёмся к бэкграунду, пока мы его не забыли, применим наши новые интерфейсные знания на практике. Фон наследуется от спрайта, но ему от спрайта вообще ничего не надо, кроме двух методов. в которых он полностью пишет собственную реализацию, которая никак не коррелирует с тем, как ведут себя остальные спрайты. И вот сложилась ситуация в которой нам надо хранить в одном массиве очень похожие объекты но наследовать их друг от друга не логично. Как поправить? \\ \hline
06-21 & Напишем некий интерфейс, назовём его GameObject и скажем, что у него есть методы апдейт и рендер, без реализации. То есть это будут некие объекты которые должны уметь рисоваться и обновляться. Идём в спрайт, и говорим, что мы реализуем интерфейс гейм объекта. Смотрим, что сломалось, кто навскидку скажет, почему? Не нужно судорожно хвататься за клавиатуру, но мысль правильная - модификатор должен быть паблик. И Фон тоже теперь у нас реализует интерфейс гейм объекта. При этом получается, то фон вообще никак не связан со спрайтом, у них даже набор полей разный. Но оба умеют рисоваться и апдейтиться, благодаря интерфейсу. Быстро поправив основное окно и логику, сменив массив спрайтов на массив геймОбъектов запускаем и видим как бодро летают наши шарики. \\ \hline
06-22 & И вот мы подошли к самому главному, той гибкости, которую даёт нам работа с интерфейсами. Если вы обратите внимание, как развивается наше повествование по курсу, вы заметите, что сначала мы выходили за пределы одного метода, потом за пределы одного класса, затем за пределы одного пакета, за пределы программного кода, а теперь вовсе хотим написать код, который возможно будет использовать несколькими программами. Посмотрим со стороны, что мы тут понаписали. У наших классов канвы и спрайта, а также у интерфейса нет никакой специфики, их можно применить где угодно. Универсальные получились штуковины. Создадим какую-то условную вторую игру: новый пакет, новый класс, скопипастим немного кода, чтобы всё запускалось. И не писать же нам по новой спрайты и интерфейсы. сделаем правильное дробление по пакетам. то есть получается, что у нас есть некий библиотечный пакет, и какие-то игры с конкретными реализациями. создадим пакет КОММОН и переносим туда канву, спрайт и геймобъект, и ща будем всё чинить. Структура же понятна, только что её всесторонне обговорили? \\ \hline
06-23 & Гейм объект это интерфейс, ему вообще на всё плевать, только импорты лишние выкинем. Спрайт тоже не сломался, модификаторы доступа поправим на публичные и защищённые, где это возможно. Второе главное окно также инитим конструктор, размеры, положение. И вот хотим воспользоваться нашей канвой. Но не даёт же. почему, мы ж так классно всё придумали, никакой специфики. Но нет, канва то может принимать в конструкторе mainCircles. и канва уже у этого класса вызывает метод onDrawFrame(). Как это решается? это решается интерфейсом. нам надо написать какой-то интерфейс вроде canvasPaintListener, который будет уметь ждать от канвы вызов метода и как-то по своему его реализовывать. А то у нас получается катастрофа - игры зависят от общего пакета, а по хорошему общий пакет вообще ничего не должен знать о том, какие игры он помогает создать. Создаём в общем пакете интерфейс с одним методом, который имеет сигнатуру из нашего mainCircles. И перепишем канву, чтобы она не какой-то класс с шариками на вход принимала, а canvasPaintListener, назовём переменную листнер. и везде используем слушателя через интерфейс. В главном окне мы говорим что классы вы конечно хорошие, с прекрасным наследованием от фреймворка, но только ещё будете реализовывать интерфейс слушателя канвы. а метод у нас уже правильно написан, с верной сигнатурой. Чтобы подчеркнуть, что это реализация интерфейса, напишем аннотацию оверрайд. \\ \hline
06-24 & Насладимся летающими шариками в одном приложении, и летающими квадратиками в другом. Теперь на основе нашего очень хорошо отделённого интерфейсами общего пакета можно штамповать такие приложения практически без усилий. Понимаете, как разработчики в игровых студиях делают по сотне очень похожих игр в год? \\ \hline
отбивка Анонимные классы & Программные интерфейсы открывают перед разработчиком широчайшие возможности по написанию более выразительного кода. Одна из наиболее часто используемых возможностей - анонимные классы. \\ \hline
& Итак все мы знаем
что классы это такой новый для нашей программы тип данных. Мы уже привычными движениями создаём публичные классы в отдельный файлах и пишем в них логику, но это не всё, что позволяет нам джава. Классы также бывают вложенными и внутренними. Внутренние классы - это классы, которые пишутся внутри класса, который описан в файле, мы его даже можем использовать и вызывать его методы. А также вложенные или локальные классы, которые мы можем объявлять прямо в методах, и работать с ними, как с обычными классами, но область видимости у них будет только внутри метода.
К чему я это веду? к анонимным классам. Давайте переименуем наш интерфейс во что то вроде MouseListener и опишем в нём пару методов, например mouseUp(), mouseDown(). Перейдём в наш основной класс и скажем, что наш локальный класс будет реализовывать интерфейс и значит должен переопределять все его методы. Видим. Теперь мы можем экземпляр этого класса создать и даже попользоваться его методами. \\ \hline
& Пока что ловите мысль?
Очень часто, какието элементы управления (кнопчки, события входящих датчиков (клавиатура, мышка) требуют на вход каких нибудь слушателей, которые будут отлавливать их события и вообще знать что делать и это всё интерфейсы. Значит если мы туда передадим что-то, что реализует интерфейс, то это нечто уже начнёт ловить события и как-то их обрабатывать. Соответственно весьма часто такие классы создаются без имени, прямо в аргументе методов. Ну и действительно, зачем ему имя, если он будет использован только один раз и только в этом методе? Вот допустим у нас есть переменная MouseListener listener и нам нужно создать туда какой-то экземпляр, который реализует этот интерфейс. для этого есть такой синтаксис:
new MouseListener дальше пишем интерфейс, который реализуем
{} и у нас по сути открывается объявление класса, где мы пишем реализацию. И получается что мы создаём один экземпляр анонимного класса, который реализует интерфейс MouseListener. И конечно потом мы этот анонимный класс уже кладём в идентификатор (или можем прям вот так передать в метод). Конечно мы можем избежать этого, сейчас поговорим как, но в чужом коде это сплошь и рядом, вот такой замороченный синтаксис, спасибо разработчикам джава.
Понятен-ли этот синтаксис? Это создание объекта анонимного класса, никак не называющегося, реализующего интерфейс.
Тут можно поговорить о лямбдах, для того чтобы сократить синтаксис мы можем просто убрать то что неизменно для этого анонимного класса - название интерфейса, название метода и класс аргумента. \\ \hline
& \\ \hline
& \\ \hline
& \\ \hline
& \\ \hline
& \\ \hline
& \\ \hline
& \\ \hline & \\ \hline
& \\ \hline & \\ \hline
& \\ \hline & \\ \hline
@ -64,47 +90,5 @@
\end{longtable} \end{longtable}
\end{document} \end{document}
****** Для чего же?
Например, создадим пару классов, классА и классБ. КлассА будет у нас реализовывать интерфейсА. То есть множественного наследования нет, но мы можем реализовать сколько угодно интерфейсов. Для того, чтобы реализовать интерфейс - мы должны переопределить все его методы, либо сделать класс абстрактным. Вот, статический анализатор кода в идее нам об этом явно говорит. Выведем модное сообщение о том что это класс1 или класс2.
И теперь самая соль - мы можем в мэйне объявлять не только классы и создавать объекты, но и создать переменную, которая реализовывает интерфейсА. То есть тут могут лежать абсолютно никак не связанные между собой сущности, главное, чтобы они реализовывали интерфейсА. И мы можем работать с методами интерфейса, которые могут быть для разных классов вообще по-разному реализованы. Такой вот полиморфизм. Понимаете насколько сильно это отличается от наследования, когда мы с вами создавали общий абстрактный класс животное и от него наследовали наших котиков?
****** Также важно
Что в интерфейсах также разрешено наследование. То есть у нас интерфейс может наследоваться от другого интерфейса, соответственно при реализации интерфейса мы должны переопределить методы всех родителей нашего интерфейса, то есть тут картина очень похожа на наследование классов
****** Вернёмся к бэкграунду
Он наследуется от спрайта, но ему от спрайта вообще ничего не надо, кроме двух методов. в которых он полностью пишет собственную реализацию. И вот сложилась ситуация в которой нам надо хранить в одном массиве очень похожие объекты но наследовать их друг от друга не логично. Как поправить?
****** Напишем некий интерфейс, назовём его GameObject
и скажем, что у него есть методы апдейт и рендер, без реализации. То есть это будут некие объекты которые должны уметь рисоваться и апдейтиться. Идём в спрайт, и говорим, что мы реализуем интерфейс гейм объекта. Смотрим, что сломалось, кто навскидку скажет, почему? (модификатор должен быть пабликом). И Фон тоже теперь у нас реализует интерфейс гейм объекта. При этом получается, то фон вообще никак не связан со спрайтом, у них даже набор полей разный. Но оба умеют рисоваться и апдейтиться, благодаря интерфейсу.
****** Теперь идём править основное окно и логику
Здесь всё просто, мы массив спрайтов меняем на массив геймОбъектов и пробуем запустить. Вопросы, оценка сложности.
***** Интерфейсы
****** Давайте посмотрим со стороны
что мы тут понаписали. У наших классов канвы и спрайта, а также у интерфейса нет никакой специфики, их можно применить где угодно. Универсальные получились штуковины.
****** создадим какую-то условную вторую игру
новый пакет, новый класс, скопипастим немного кода, чтобы всё запускалось. И не писать же нам по новой спрайты и интерфейсы. сделаем правильное дробление по пакетам. то есть получается, что у нас есть некий библиотечный пакет, и какие-то игры с конкретными реализациями. создадим паке КОММОН и переносим туда канву, спрайт и геймобъект, и ща будем всё чинить.
Структура понятна?
Что не нравится канве? ондрофрейм нужно паблик
Что не нравится мэйн виндоу? гейм канвас надо сделать паблик
Гейм объект это интерфейс, ему вообще на всё плевать, только импорты лишние выкинем
Спрайт тоже не сломался
Идём в шарик он не видит спрайта, сделаем спрайт пабликом
он же не видет поля и методы, сделаем их протектед
он же не видит методы канвы. сделаем их публичными
И идём в мэйн, что тут? он не видит конструктор канвы. делаем пабликом
****** второе окно также инитим
конструктор, размеры, положение. И вот хотим воспользоваться нашей канвой. Но не даёт же. почему, мы ж так классно всё придумали, никакой специфики. Но нет, канва то может принимать в конструкторе mainCircles. и канва уже у этого класса вызывает метод onDrawFrame(). Как это решается? это решается интерфейсом. нам нам надо написать какой-то интерфейс вроде canvasPaintListener, который будет уметь ждать от канвы вызов метода и как-то по своему его реализовывать, и собственно реализовать этот интерфейс. А то у нас получается катастрофа - игры зависят от общего пакета, а общий пакет вообще ничего не должен знать о том, какие игры он помогает создать.
Создаём в общем пакете интерфейс с одним методом, который имеет сигнатуру из нашего mainCircles. И перепишем канву, чтобы она не какой-то класс с шариками на вход принимала, а canvasPaintListener, назовём переменную листнер. и везде используем листнер.
И в главном окне мы говорим что ты хороший класс, с прекрасным наследованием, но только ещё будешь реализовывать интерфейс слушателя канвы. а метод у нас уже правильно написан, с верной сигнатурой. Чтобы подчеркнуть, что это реализация интерфейса, напишем аннотацию оверрайд.
Во второй игре мы тоже реализуем этот же интерфейс, и передаём канве себя, потому что мы умеем слушать канву
***** Анонимные классы
****** Итак все мы знаем
что классы это такой новый для нашей программы тип данных. Мы уже привычными движениями создаём публичные классы в отдельный файлах и пишем в них логику, но это не всё, что позволяет нам джава. Классы также бывают вложенными и внутренними. Внутренние классы - это классы, которые пишутся внутри класса, который описан в файле, мы его даже можем использовать и вызывать его методы. А также вложенные или локальные классы, которые мы можем объявлять прямо в методах, и работать с ними, как с обычными классами, но область видимости у них будет только внутри метода.
К чему я это веду? к анонимным классам. Давайте переименуем наш интерфейс во что то вроде MouseListener и опишем в нём пару методов, например mouseUp(), mouseDown(). Перейдём в наш основной класс и скажем, что наш локальный класс будет реализовывать интерфейс и значит должен переопределять все его методы. Видим. Теперь мы можем экземпляр этого класса создать и даже попользоваться его методами.
****** Пока что ловите мысль?
Очень часто, какието элементы управления (кнопчки, события входящих датчиков (клавиатура, мышка) требуют на вход каких нибудь слушателей, которые будут отлавливать их события и вообще знать что делать и это всё интерфейсы. Значит если мы туда передадим что-то, что реализует интерфейс, то это нечто уже начнёт ловить события и как-то их обрабатывать. Соответственно весьма часто такие классы создаются без имени, прямо в аргументе методов. Ну и действительно, зачем ему имя, если он будет использован только один раз и только в этом методе? Вот допустим у нас есть переменная MouseListener listener и нам нужно создать туда какой-то экземпляр, который реализует этот интерфейс. для этого есть такой синтаксис:
new MouseListener дальше пишем интерфейс, который реализуем
{} и у нас по сути открывается объявление класса, где мы пишем реализацию. И получается что мы создаём один экземпляр анонимного класса, который реализует интерфейс MouseListener. И конечно потом мы этот анонимный класс уже кладём в идентификатор (или можем прям вот так передать в метод). Конечно мы можем избежать этого, сейчас поговорим как, но в чужом коде это сплошь и рядом, вот такой замороченный синтаксис, спасибо разработчикам джава.
Понятен-ли этот синтаксис? Это создание объекта анонимного класса, никак не называющегося, реализующего интерфейс.
Тут можно поговорить о лямбдах, для того чтобы сократить синтаксис мы можем просто убрать то что неизменно для этого анонимного класса - название интерфейса, название метода и класс аргумента.
****** Вернёмся к нашим кружочкам ****** Вернёмся к нашим кружочкам
Смотрим прям первый метод, invokeLater() он принимает на вход какой-то Runnable. Идём в Runnable и видим, что это интерфейс, который реализует один единственный метод run(). Получается, что мы в invokeLater передаём новый экземпляр анонимного класса который РЕАЛИЗУЕТ ИНТЕРФЕЙС Runnable, вот описание этого класса, мы в нём переопределяем метод run. Вот так это читается. Ровно тоже самое мы сделали с mouseListener. Здесь немного сложнее. Есть интерфейс MouseListener в котором описаны вот эти все методы. И есть специальный класс MouseAdapter в котором все эти методы уже реализованы. Реализации пустые, но они есть, поэтому мы можем не все методы оверрайдить, а только те, которые посчитаем нужными. И когда мы этот MouseAdapter отдаём нашему методу, мы говорим: создай нам новый экземпляр анонимного класса, который НАСЛЕДУЕТСЯ ОТ КЛАССА MouseAdapter и переопредели вот этот метод. Остальные оставляй пустыми. Чувствуете разницу? Как можно избежать таких конструкций и при этом получить понятный код без лишних заморочек? Очень просто. Скажем, что наш класс - реализует тот или иной интерфейс, и переопределим все методы. В некоторые из них даже напишем реализацию, и туда, где требуется интерфейс - передадим себя. Мы же теперь MouseListener, да и Runnable. Смотрим прям первый метод, invokeLater() он принимает на вход какой-то Runnable. Идём в Runnable и видим, что это интерфейс, который реализует один единственный метод run(). Получается, что мы в invokeLater передаём новый экземпляр анонимного класса который РЕАЛИЗУЕТ ИНТЕРФЕЙС Runnable, вот описание этого класса, мы в нём переопределяем метод run. Вот так это читается. Ровно тоже самое мы сделали с mouseListener. Здесь немного сложнее. Есть интерфейс MouseListener в котором описаны вот эти все методы. И есть специальный класс MouseAdapter в котором все эти методы уже реализованы. Реализации пустые, но они есть, поэтому мы можем не все методы оверрайдить, а только те, которые посчитаем нужными. И когда мы этот MouseAdapter отдаём нашему методу, мы говорим: создай нам новый экземпляр анонимного класса, который НАСЛЕДУЕТСЯ ОТ КЛАССА MouseAdapter и переопредели вот этот метод. Остальные оставляй пустыми. Чувствуете разницу? Как можно избежать таких конструкций и при этом получить понятный код без лишних заморочек? Очень просто. Скажем, что наш класс - реализует тот или иной интерфейс, и переопределим все методы. В некоторые из них даже напишем реализацию, и туда, где требуется интерфейс - передадим себя. Мы же теперь MouseListener, да и Runnable.
****** в тему анонимных классов можно ещё коротко сказать о перечислениях
в самом частом случае - перечисления это список именованных констант. объявленные в перечислении идентификаторы - это публичные статические финальные поля класса. в более сложных случаях перечисления могут содержать также методы, например, поиска идентификатора по значению, или получения значения из идентификатора