Привет! Сегодня мы затронем важную новую тему:
паттерны, или по-другому шаблоны проектирования. Что же такое паттерны?
Если нужен короткий ответ: Адаптер это структурный паттерн, который позволяет подружить два класса с несовместимыми интерфейсами и при этом не переписывать ни один из них. Адаптер реализует тот интерфейс, которого ждет вызывающий код, принимает адаптируемый объект в конструктор и переадресует ему вызовы. В самой Java такие адаптеры уже есть:
InputStreamReader превращает байтовый
InputStream в символьный
Reader.
Кратко
- Адаптер это структурный паттерн: он делает совместимыми два класса с разными интерфейсами, не заставляя менять ни тот, ни другой.
- Участников четыре: клиент, целевой интерфейс, адаптируемый класс и сам адаптер.
- Адаптер реализует целевой интерфейс, а адаптируемый объект держит у себя в поле и получает через конструктор.
- Своей логики у адаптера нет: он только переводит вызов и делегирует работу адаптируемому объекту.
- Житейская аналогия: карту памяти в USB-разъем не вставить, а кардридер вставляется, и карта работает через него.
- В стандартной библиотеке Java адаптеры уже написаны:
InputStreamReader адаптирует InputStream к Reader, а OutputStreamWriter адаптирует OutputStream к Writer.
Паттерн проектирования простыми словами
Думаю, тебе известно выражение «не надо изобретать велосипед». В программировании, как и во многих других сферах, есть большое количество типовых ситуаций. Для каждой из них в процессе развития программирования создавались готовые работающие решения. Это и есть шаблоны проектирования.
Условно говоря, паттерн это некий пример, который предлагает решение ситуации вида: «если в вашей программе нужно сделать то-то, как это лучше всего сделать».
Паттернов очень много, им посвящена отличная книга «Head First. Паттерны проектирования» Эрика и Элизабет Фримен, с которой обязательно нужно ознакомиться. Про один из самых известных паттернов,
Singleton, у нас есть отдельная статья.
![Обложка русского издания книги «Паттерны проектирования» Эрика и Элизабет Фримен из серии Head First]()
Если говорить максимально кратко, паттерн состоит из распространенной проблемы и ее решения, которое уже можно считать неким стандартом.
В сегодняшней лекции мы познакомимся с одним из таких паттернов под названием «Адаптер».
Адаптер из реальной жизни: кардридер
Название у него говорящее, и ты не раз встречался с адаптерами в реальной жизни. Один из самых распространенных адаптеров это кардридеры, которыми снабжены множество компьютеров и ноутбуков.
![Две карты памяти SanDisk SD лежат на корпусе белого принтера со встроенным кардридером]()
Представь, что у нас есть какая-то карта памяти. В чем состоит проблема?
В том, что она не умеет взаимодействовать с компьютером. У них нет общего интерфейса.
У компьютера есть разъем USB, но карту памяти в него не вставить.
Карту невозможно вставить в компьютер, из-за чего мы не сможем сохранить наши фотографии, видео и другие данные.
Кардридер является адаптером, решающим данную проблему. Ведь у него есть USB-кабель! В отличие от самой карты, кардридер можно вставить в компьютер. У них с компьютером есть общий интерфейс, USB.
Адаптер в коде: интерфейс, адаптируемый класс, адаптер
Давай посмотрим, как это будет выглядеть на примере:
public interface USB {
void connectWithUsbCable();
}
Это наш интерфейс USB с единственным методом, вставить USB-кабель:
public class MemoryCard {
public void insert() {
System.out.println("Карта памяти успешно вставлена!");
}
public void copyData() {
System.out.println("Данные скопированы на компьютер!");
}
}
Это наш класс, реализующий карту памяти. В нем уже есть 2 нужных нам метода, но вот беда: интерфейс USB он не реализует. Карту нельзя вставить в USB-разъем.
public class CardReader implements USB {
private MemoryCard memoryCard;
public CardReader(MemoryCard memoryCard) {
this.memoryCard = memoryCard;
}
@Override
public void connectWithUsbCable() {
this.memoryCard.insert();
this.memoryCard.copyData();
}
}
А вот и наш адаптер!
Что же делает
класс CardReader и почему, собственно, он является адаптером?
Все просто. Адаптируемый класс (карта памяти) становится одним из полей адаптера. Это логично, ведь в реальной жизни мы тоже вставляем карту внутрь кардридера, и она тоже становится его частью.
В отличие от карты памяти, у адаптера есть общий интерфейс с компьютером. У него есть USB-кабель, то есть он может соединяться с другими устройствами по USB.
Поэтому в программе наш класс
CardReader реализует интерфейс USB. Но что же происходит внутри этого метода?
А там происходит ровно то, что нам нужно! Адаптер делегирует выполнение работы нашей карте памяти. Ведь сам-то адаптер ничего не делает, какого-то самостоятельного функционала у кардридера нет. Его задача только связать компьютер и карту памяти, чтобы карта могла сделать свою работу и скопировать файлы!
Наш адаптер позволяет ей сделать это, предоставив свой интерфейс (метод
connectWithUsbCable()) для «нужд» карты памяти.
Давай создадим какую-то программу-клиент, которая будет имитировать человека, желающего скопировать данные с карты памяти:
public class Main {
public static void main(String[] args) {
USB cardReader = new CardReader(new MemoryCard());
cardReader.connectWithUsbCable();
}
}
Что же у нас в результате получилось?
Вывод в консоль:
Карта памяти успешно вставлена!
Данные скопированы на компьютер!
Отлично, наша задача успешно выполнена!
Вот несколько дополнительных ссылок с информацией о паттерне Адаптер:
Абстрактные классы Reader и Writer
Теперь мы вернемся к нашему любимому занятию: выучим парочку новых классов для работы со вводом и выводом :) Сколько мы их уже выучили, интересно?
Сегодня речь пойдет о классах
Reader и
Writer.
Почему именно о них? Потому что это будет в тему нашему предыдущему разделу, адаптерам.
Давай рассмотрим их подробнее. Начнем с
Reader’a.
Reader это абстрактный класс, поэтому явно создавать его объекты у нас не получится.
Но на самом деле ты с ним уже знаком! Ведь хорошо знакомые тебе классы
BufferedReader и
InputStreamReader являются его наследниками :)
public class BufferedReader extends Reader {
…
}
public class InputStreamReader extends Reader {
…
}
InputStreamReader: адаптер внутри JDK
Так вот,
класс InputStreamReader это классический адаптер.
Как ты, наверное, помнишь, мы можем передать в его конструктор объект
InputStream. Чаще всего мы для этого используем переменную
System.in:
public static void main(String[] args) {
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
Что же делает
InputStreamReader?
Как и всякий адаптер, он преобразует один интерфейс к другому. В данном случае интерфейс
InputStream’a к интерфейсу
Reader’a.
Изначально у нас был класс
InputStream. Он неплохо работает, но с его помощью можно читать только отдельные байты.
Кроме того, у нас есть абстрактный класс
Reader. У него есть отличный и очень нужный нам функционал: он умеет читать символы! Нам такая возможность, конечно, очень нужна.
Но здесь мы сталкиваемся с классической проблемой, которую обычно решают адаптеры: несовместимость интерфейсов. В чем же она проявляется?
Давай заглянем прямо в документацию Oracle. Вот методы класса
InputStream.
![Скриншот документации Oracle: таблица методов класса InputStream, оранжевой рамкой выделены три варианта метода read, все они читают байты и возвращают int]()
Совокупность методов это и есть интерфейс.
Как видишь, метод
read() у этого класса есть (даже в нескольких вариантах), но читать он может только байты: или отдельные байты, или несколько байт с использованием буфера. Нам такой вариант не подходит, мы хотим читать символы.
Нужный нам функционал
уже реализован в абстрактном классе Reader. Это тоже можно увидеть в документации.
![Скриншот документации Oracle: таблица методов класса Reader, оранжевой рамкой выделены четыре варианта метода read, они читают символы в массив char или в CharBuffer]()
Однако интерфейсы
InputStream’a и
Reader’a несовместимы! Как видишь, во всех реализациях метода
read() у них отличаются типы данных, с которыми они работают:
InputStream читает в массив
byte[], а
Reader в массив
char[]. Возвращают эти методы
int и там, и там, но в одном случае за ним стоит байт, а в другом символ.
И именно здесь нам понадобится
InputStreamReader! Он выступит
Адаптером между нашими классами.
Как и в примере с кардридером, который мы рассмотрели выше, мы передаем объект «адаптируемого» класса «внутрь», то есть в конструктор класса-адаптера.
В прошлом примере мы передавали объект
MemoryCard внутрь
CardReader. А теперь передаем объект
InputStream в конструктор
InputStreamReader!
В качестве
InputStream мы используем уже ставшую привычной переменную
System.in:
public static void main(String[] args) {
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
И действительно: заглянув в документацию
InputStreamReader’a мы увидим, что «адаптация» прошла успешно :) Теперь в нашем распоряжении есть методы, которые позволяют нам читать символы.
![Скриншот документации Oracle: таблица методов класса InputStreamReader, оранжевой рамкой выделены методы read, которые читают уже символы]()
И хотя изначально наш объект
System.in (поток, привязанный к клавиатуре) не позволял этого делать, создав паттерн
Адаптер, создатели языка решили эту проблему.
OutputStreamWriter: то же самое для записи
У абстрактного класса
Reader, как и у большинства I/O-классов, есть брат-близнец,
Writer. Он имеет тот же большой плюс, что и
Reader: предоставляет удобный интерфейс для работы с символами.
С выходными потоками проблема и ее решение выглядят так же, как и в случае со входными.
Есть класс
OutputStream, который умеет записывать только байты; есть абстрактный класс
Writer, который умеет работать с символами, и есть два несовместимых интерфейса.
Эту проблему вновь успешно решает паттерн Адаптер. При помощи класса
OutputStreamWriter мы легко «адаптируем» два интерфейса классов
Writer и
OutputStream друг другу. И, получив байтовый поток
OutputStream в конструктор, с помощью
OutputStreamWriter мы, тем не менее, можем записывать символы, а не байты!
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
OutputStreamWriter streamWriter = new OutputStreamWriter(new FileOutputStream("C:\\Users\\Username\\Desktop\\test.txt"));
streamWriter.write(32144);
streamWriter.close();
}
}
Мы записали в наш файл символ с кодом 32144, это 綐, таким образом избавившись от необходимости работать с байтами :)
Одна оговорка про этот пример. Мы создали
OutputStreamWriter без указания кодировки, а значит он берет кодировку по умолчанию. До Java 18 она зависела от системы, и на Windows с русской локалью вместо иероглифа в файл ушел бы вопросительный знак. Начиная с Java 18 кодировкой по умолчанию везде стал UTF-8 (JEP 400), так что теперь пример отработает одинаково на любой машине. Если не хочешь зависеть от версии, передавай кодировку явно:
new OutputStreamWriter(stream, StandardCharsets.UTF_8).
Роли в паттерне Адаптер
Оба разобранных примера, кардридер и классы из
java.io, устроены совершенно одинаково. Вот кто есть кто.
| Роль в паттерне | Пример с кардридером | Чтение в java.io | Запись в java.io |
|---|
| Клиент | класс Main | код, которому нужны символы | код, которому нужны символы |
| Целевой интерфейс | USB | Reader | Writer |
| Адаптируемый класс | MemoryCard | InputStream, например System.in | OutputStream, например FileOutputStream |
| Адаптер | CardReader | InputStreamReader | OutputStreamWriter |
| Что уходит в конструктор адаптера | объект MemoryCard | объект InputStream | объект OutputStream |
Вопросы и ответы
Что такое паттерн Адаптер простыми словами?
Это способ подружить два класса, которые не понимают друг друга. Адаптер реализует интерфейс, которого ждет вызывающий код, а внутри хранит объект неподходящего класса и переадресует ему вызовы. Менять сами классы при этом не нужно.
Из каких участников состоит паттерн Адаптер?
Их четыре: клиент (код, который делает вызов), целевой интерфейс (то, что клиент умеет вызывать), адаптируемый класс (то, что нужно задействовать) и адаптер, который реализует целевой интерфейс и держит внутри адаптируемый объект.
Какие адаптеры есть в стандартной библиотеке Java?
Самые известные это
InputStreamReader и
OutputStreamWriter из пакета
java.io. Первый адаптирует байтовый
InputStream к символьному
Reader, второй
OutputStream к
Writer. Оба принимают адаптируемый поток в конструктор.
Чем Адаптер отличается от Декоратора?
Адаптер меняет интерфейс объекта, чтобы им смог пользоваться код, который его не понимает. Декоратор интерфейс не меняет, он добавляет объекту новое поведение поверх старого.
InputStreamReader это адаптер, а
BufferedReader, который оборачивает другой
Reader и добавляет буферизацию, это декоратор.
Зачем нужен адаптер, если можно просто переписать класс?
Часто переписать нельзя: класс лежит в чужой библиотеке, в стандартной библиотеке языка или им уже пользуется другой код, который сломается от правок. Адаптер решает задачу, не трогая ни адаптируемый класс, ни клиента.
Чем адаптер объекта отличается от адаптера класса?
Адаптер объекта хранит адаптируемый объект в поле и делегирует ему вызовы, именно так устроены
CardReader из этой статьи и
InputStreamReader. Адаптер класса вместо этого наследуется от адаптируемого класса. В Java чаще берут первый вариант, потому что множественное наследование классов запрещено.
Источник:
Oracle Java SE API: InputStreamReader, где этот класс прямо назван мостом от байтовых потоков к символьным.
Читайте также
На этом на сегодня все, до встречи на следующих лекциях! :)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
переменнойпотока (что логично)" А разве он что то ещё делает? Ну да он не совсем напрямую использует поток,а через переменную класса StreamDecoder, который собсна как я понял и расшифровывает байты как символы. Но это лишь и доказывает что InputStreamReader не что иное как адаптер, ибо никакую собственную логику он не реализует, а лишь является переходником. "для чтения символов класс BufferedReader и методы, напрашивается вопрос на кой черт нужен InputStreamReader" BufferReader это класс оболочка и данные он читает исключительно из других потоков реализующих интерфейс Reader. Тебя наверное смутил метод read() в этом классе, когда ты не встретил там ни одного упоминания переменной in, в которой и хранится входящий поток. Но это всё потому, что read() в BufferReader читает не из потока ,а из своего буфера и когда буфер полностью прочитан, тогда вызывается метод fill(), который создаёт новый буфер и заполняет его из потока в переменной in(например из того же InputStreamReader`а).