Привет! Сегодня мы затронем важную новую тему — паттерны, или по-другому — шаблоны проектирования. Что же такое паттерны?
Думаю, тебе известно выражение «не надо изобретать велосипед». В программировании, как и во многих других сферах, есть большое количество типовых ситуаций. Для каждой из них в процессе развития программирования создавались готовые работающие решения. Это и есть шаблоны проектирования.
Условно говоря, паттерн — это некий пример, который предлагает решение ситуации вида: «если в вашей программе нужно сделать то-то, как это лучше всего сделать».
Паттернов очень много, им посвящена отличная книга «Изучаем шаблоны проектирования», с которой обязательно нужно ознакомиться.
Если говорить максимально кратко, паттерн состоит из распространенной проблемы и ее решения, которое уже можно считать неким стандартом.
В сегодняшней лекции мы познакомимся с одним из таких паттернов под названием «Адаптер».
Название у него говорящее, и ты не раз встречался с адаптерами в реальной жизни. Один из самых распространенных адаптеров — кардридеры, которыми снабжены множество компьютеров и ноутбуков.
Представь, что у нас есть какая-то карта памяти. В чем состоит проблема?
В том, что она не умеет взаимодействовать с компьютером. У них нет общего интерфейса.
У компьютера есть разъем 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();
}
}
Что же у нас в результате получилось?
Вывод в консоль:
Карта памяти успешно вставлена!
Данные скопированы на компьютер!
Отлично, наша задача успешно выполнена!
Вот несколько дополнительных ссылок с информацией о паттерне Адаптер:
- Видео Adapter Pattern – Design Patterns;
- Паттерн проектирования «Адаптер» / «Adapter»;
- Шаблоны проектирования простым языком.
Абстрактные классы Reader и Writer
Теперь мы вернемся к нашему любимому занятию: выучим парочку новых классов для работы со вводом и выводом :) Сколько мы их уже выучили, интересно? Сегодня речь пойдет о классахReader
и Writer
.
Почему именно о них? Потому что это будет в тему нашему предыдущему разделу — адаптерам.
Давай рассмотрим их подробнее. Начнем с Reader
’a.
Reader
— это абстрактный класс, поэтому явно создавать его объекты у нас не получится.
Но на самом деле ты с ним уже знаком! Ведь хорошо знакомые тебе классы BufferedReader
и InputStreamReader
являются его наследниками :)
public class BufferedReader extends Reader {
…
}
public class InputStreamReader extends Reader {
…
}
Так вот, класс 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
.

read()
у этого класса есть (даже в нескольких вариантах), но читать он может только байты: или отдельные байты, или несколько байт с использованием буфера. Нам такой вариант не подходит — мы хотим читать символы.
Нужный нам функционал уже реализован в абстрактном классе Reader
. Это тоже можно увидеть в документации.

InputStream
'a и Reader
'a несовместимы! Как видишь, во всех реализациях метода read()
у них отличаются и передаваемые параметры, и возвращаемые значения.
И именно здесь нам понадобится InputStreamReader
! Он выступит Адаптером между нашими классами.
Как и в примере с кардридером, который мы рассмотрели выше, мы передаем объект «адаптируемого» класса «внутрь», то есть в конструктор класса-адаптера.
В прошлом примере мы передавали объект MemoryCard
внутрь CardReader
. А теперь передаем объект InputStream
в конструктор InputStreamReader
!
В качестве InputStream
мы используем уже ставшую привычной переменную System.in
:
public static void main(String[] args) {
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
}
И действительно: заглянув в документацию InputStreamReader
'a мы увидим, что «адаптация» прошла успешно :) Теперь в нашем распоряжении есть методы, которые позволяют нам читать символы.

System.in
(поток, привязанный к клавиатуре) не позволял этого делать, создав паттерн Адаптер создатели языка решили эту проблему.
У абстрактного класса 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 — 綐, таким образом избавившись от необходимости работать с байтами :)
На этом на сегодня все, до встречи на следующих лекциях! :)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
переменнойпотока (что логично)" А разве он что то ещё делает? Ну да он не совсем напрямую использует поток,а через переменную класса StreamDecoder, который собсна как я понял и расшифровывает байты как символы. Но это лишь и доказывает что InputStreamReader не что иное как адаптер, ибо никакую собственную логику он не реализует, а лишь является переходником. "для чтения символов класс BufferedReader и методы, напрашивается вопрос на кой черт нужен InputStreamReader" BufferReader это класс оболочка и данные он читает исключительно из других потоков реализующих интерфейс Reader. Тебя наверное смутил метод read() в этом классе, когда ты не встретил там ни одного упоминания переменной in, в которой и хранится входящий поток. Но это всё потому, что read() в BufferReader читает не из потока ,а из своего буфера и когда буфер полностью прочитан, тогда вызывается метод fill(), который создаёт новый буфер и заполняет его из потока в переменной in(например из того же InputStreamReader`а).