Перейти к основному содержимому

Разница между Map и HashMap в Java

· 5 мин. чтения

1. Обзор

Разница между Map и HashMap в том, что первый — это интерфейс, а второй — реализация . Однако в этой статье мы копнем немного глубже и объясним, чем полезны интерфейсы. Кроме того, мы узнаем, как сделать код более гибким с помощью интерфейсов и почему у нас есть разные реализации для одного и того же интерфейса.

2. Назначение интерфейсов

Интерфейс — это контракт, определяющий только поведение. Каждый класс, реализующий определенный интерфейс, должен выполнять этот контракт. Чтобы лучше понять это, мы можем взять пример из реальной жизни. Представьте себе машину. У каждого человека в голове будет свой образ. Термин автомобиль подразумевает некоторые качества и поведение. Любой предмет, обладающий этими качествами, можно назвать автомобилем. Именно поэтому каждый из нас представлял себе разную машину.

Интерфейсы работают одинаково. Карта — это абстракция, определяющая определенные качества и поведение. Картой может быть только класс, обладающий всеми этими качествами . ``

3. Различные реализации

У нас разные реализации интерфейса карты по той же причине, по которой у нас разные модели автомобилей. Все реализации служат разным целям. Невозможно найти лучшую реализацию в целом. Есть только лучшая реализация для какой-то цели. Несмотря на то, что спортивная машина быстра и круто выглядит, она не лучший выбор для семейного пикника или похода в мебельный магазин.

HashMap — это простейшая реализация интерфейса Map , обеспечивающая основные функции. В основном, эта реализация покрывает все потребности. Двумя другими широко используемыми реализациями являются TreeMap и LinkedHashMap , предоставляющие дополнительные функции.

Вот более подробная, но не полная иерархия:

./5b3b6de2f11e6fb9349875ae991b4574.png

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 .