Disclaimer
Всем привет! Я начинающий Java-разработчик. В рамках развития своей карьеры я решил сделать две вещи:
Завести канал в ТГ, где собираюсь рассказывать о себе, своем пути и проблемах, с которыми сталкиваюсь - https://t.me/java_wine
Завести блог на Хабре, куда буду выкладывать переводы материалов, которые использую для своего обучения и развития.
Надеюсь, буду полезен сообществу и новичкам, которые пойдут по моим или чьим-то еще стопам.
Run!
Введение
Map - это интерфейс, а HashMap - одна из его реализаций. Тем не менее, в этой статье мы постараемся разобраться, чем полезны интерфейсы, узнаем как сделать код гибче с помощью интерфейсов и почему существуют разные реализации одного и того же интерфейса.
Назначение интерфейсов
Интерфейс - контракт, определяющий поведение класса. Каждый класс, реализующий интерфейс, должен исполнять этот контракт (реализовать все его методы). Чтобы лучше с этим разобраться, давайте представим себе автомобиль: Лада. Ниссан, Мерседес, Jeep или даже Бэтмобиль. Термин "автомобиль" подразумевает некоторые качества и поведение. Любой объект, обладающий этими качествами, можно назвать автомобилем. Поэтому, каждый из нас представляет автомобиль по-своему.
Интерфейсы устроены похожим образом. Map - это абстракция, которая определяет определенное поведение. Только класс, обладающий этим поведением может быть типа Map.
Различные реализации
В Java есть различные реализации интерфейса Map по той же причине, по которой у нас существуют различные автомобили. Реализации служат различным целям. Поэтому в зависимости от цели вы и выбираете реализацию. Согласитесь, несмотря на все преимущества спортивного автомобиля, по бездорожью он вряд ли проедет.
Hashmap - наиболее простая реализация Map, которая обеспечивает базовую функциональность. Две другие реализации - TreeMap и LinkedHashMap - предоставляют дополнительные возможности.
Вот более подробная (но не полная) иерархия:

Программирование на уровне реализаций
Представьте, что нам нужно вывести в консоли ключи и значения Map:
public class HashMapPrinter {
public void printMap(final HashMap<?, ?> map) {
for (final Map.Entry<?, ?> entry : map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
Небольшой метод, который делает необходимую работу. Тем не менее, есть проблемка. Он будет работать только с объектом типа HashMap. Следовательно, каждый раз когда мы будем пытаться в него передать объект типа TreeMap или даже HashMap, на который ссылается переменная типа Map, будет возникать ошибка.
public class Main {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
HashMap<String, String> hashMap = new HashMap<>();
TreeMap<String, String> treeMap = new TreeMap<>();
HashMapPrinter hashMapPrinter = new HashMapPrinter();
hashMapPrinter.printMap(hashMap);
// hashMapPrinter.printMap(treeMap); (1) ошибка компиляции
// hashMapPrinter.printMap(map); (2) ошибка компиляции
}
}
Попробуем понять, почему так происходит. В обоих случаях компилятор не может быть уверенным, что внутри метода HashMapPrinter нет вызовов специфичных для HashMap методов.
TreeMap находится в отдельной ветке реализаций интерфейса Map (смотри иерархию), следовательно, в нем могут отсутствовать некоторые методы, определенные в HashMap (1).
В случае (2), несмотря на то что реальный объект это HashMap, тип его ссылки - Map. Следовательно, у объекта можно будет воспользоваться только методами, определенными в Map, но не в HashMap.
В итоге мы имеем очень простой класс HashMapPrinter, который является слишком специфичным. При таком подходе нам придется создавать Printer для каждой реализации Map.
Программирование на уровне интерфейсов
Часто новичков смущает и путает значение выражения "программирование на уровне интерфейсов". Давайте разберем следующий пример, который немного прояснит ситуацию. Изменим в нашем примере тип аргумента на более общий, которым является Map:
public class MapPrinter {
public void printMap(final Map<?, ?> map) {
for (final Map.Entry<?, ?> entry : map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
Как видно, фактическая реализация не изменилась, а единственное отличие это тип аргумента - теперь это "final Map". Тем самым мы показываем компилятору, что метод не использует никаких специфических для HashMap и других реализаций методов. Вся необходимая функциональность уже была определена в методе entrySet().
Маленькое изменение = большая выгода. Теперь этот класс может работать с любой реализацией Map:
public class Main {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
HashMap<String, String> hashMap = new HashMap<>();
TreeMap<String, String> treeMap = new TreeMap<>();
MapPrinter mapPrinter = new MapPrinter();
mapPrinter.printMap(hashMap);
mapPrinter.printMap(treeMap);
mapPrinter.printMap(map);
}
}
Программирование на интерфейсах помогло создать универсальный класс, который может работать с любой реализацией Map. Такой подход позволяет устранить дублирование кода и делает наши классы банально лучше.
Где еще использовать интерфейсы
В целом, аргументы в наших методах должны быть как можно более общего типа. В предыдущем примере мы видели, как простое изменение типа аргумента помогло улучшить код. Еще одно место, где это можно использовать - конструкторы:
public class MapReporter {
private final Map<?, ?> map;
public MapReporter(final Map<?, ?> map) {
this.map = map;
}
public void printMap() {
for (final Map.Entry<?, ?> entry : this.map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
Этот класс будет прекрасно работать с любой имплементацией интерфейса Map, потому что в конструкторе использован правильный тип.
Заключение
В этой небольшой статье мы увидели, как интерфейсы могут быть использованы для дополнительной абстракции в наших программах. Использования интерфейсов делает код компактнее, легче для повторного использования и удобным для чтения.
Автор - Борис https://t.me/java_wine