Как стать автором
Обновить

Java: отличия Map и HashMap

Время на прочтение4 мин
Количество просмотров22K
Автор оригинала: Eugene Kovko

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

Теги:
Хабы:
Всего голосов 24: ↑4 и ↓20-14
Комментарии14

Публикации

Истории

Работа

Java разработчик
394 вакансии

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн