1. Обзор
Разница между Map
и HashMap
в том, что первый — это интерфейс, а второй — реализация . Однако в этой статье мы копнем немного глубже и объясним, чем полезны интерфейсы. Кроме того, мы узнаем, как сделать код более гибким с помощью интерфейсов и почему у нас есть разные реализации для одного и того же интерфейса.
2. Назначение интерфейсов
Интерфейс — это контракт, определяющий только поведение. Каждый класс, реализующий определенный интерфейс, должен выполнять этот контракт. Чтобы лучше понять это, мы можем взять пример из реальной жизни. Представьте себе машину. У каждого человека в голове будет свой образ. Термин автомобиль подразумевает некоторые качества и поведение. Любой предмет, обладающий этими качествами, можно назвать автомобилем. Именно поэтому каждый из нас представлял себе разную машину.
Интерфейсы работают одинаково. Карта
— это абстракция, определяющая определенные качества и поведение. Картой
может быть только класс, обладающий всеми этими качествами . ``
3. Различные реализации
У нас разные реализации интерфейса карты
по той же причине, по которой у нас разные модели автомобилей. Все реализации служат разным целям. Невозможно найти лучшую реализацию в целом. Есть только лучшая реализация для какой-то цели. Несмотря на то, что спортивная машина быстра и круто выглядит, она не лучший выбор для семейного пикника или похода в мебельный магазин.
HashMap
— это простейшая реализация интерфейса Map
, обеспечивающая основные функции. В основном, эта реализация покрывает все потребности. Двумя другими широко используемыми реализациями являются TreeMap
и LinkedHashMap
, предоставляющие дополнительные функции.
Вот более подробная, но не полная иерархия:
4. Программирование для реализации
Представьте, что мы хотим вывести ключи и значения HashMap в
консоль:
public class HashMapPrinter {
public void printMap(final HashMap<?, ?> map) {
for (final 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); Compile time error
// hashMapPrinter.printMap(map); Compile time error
}
}
Попробуем понять, почему это происходит. В обоих этих случаях компилятор не может быть уверен, что внутри этого метода не будет никаких вызовов конкретных методов HashMap .
``
TreeMap
находится в другой ветви реализации Map
(не каламбур), поэтому в нем могут отсутствовать некоторые методы, определенные в HashMap .
Во втором случае, несмотря на реальный базовый объект типа HashMap ,
на него ссылается интерфейс Map .
Следовательно, этот объект сможет предоставлять только методы, определенные в Map
, а не в HashMap .
Таким образом, несмотря на то, что наш HashMapPrinter
довольно простой класс, он слишком специфичен . При таком подходе нам потребуется создать отдельный принтер
для каждой реализации карты
.
5. Программирование для интерфейсов
Часто новички путаются в значении выражений «программа для интерфейсов» или «код против интерфейсов». Рассмотрим следующий пример, который немного прояснит ситуацию. Мы изменим тип аргумента на наиболее общий из возможных, то есть на Map:
public class MapPrinter {
public void printMap(final Map<?, ?> map) {
for (final Entry<?, ?> entry : map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
Как мы видим, фактическая реализация осталась прежней, а единственное изменение — это тип аргумента. Это показывает, что метод не использовал никаких конкретных методов 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 .
Такой подход может устранить дублирование кода и обеспечить четкое назначение наших классов и методов.
6. Где использовать интерфейсы
В целом, аргументы должны быть максимально общего типа. В предыдущем примере мы видели, как простое изменение сигнатуры метода может улучшить наш код. Еще одно место, где у нас должен быть такой же подход, — это конструктор:
public class MapReporter {
private final Map<?, ?> map;
public MapReporter(final Map<?, ?> map) {
this.map = map;
}
public void printMap() {
for (final Entry<?, ?> entry : this.map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
Этот класс может работать с любой реализацией Map,
просто потому, что мы использовали правильный тип в конструкторе.
7. Заключение
Подводя итог, в этом уроке мы обсудили, почему интерфейсы — отличное средство для абстракции и определения контракта. Использование максимально общего типа сделает код простым для повторного использования и легким для чтения. В то же время такой подход уменьшает объем кода, что всегда является хорошим способом упростить кодовую базу.
Как всегда, код доступен на GitHub .