Маппинг java что это
Ответы на самые популярные вопросы об интерфейсе Map
0. Как перебрать все значения Map
1. Как конвертировать Map в List
2. Как отсортировать ключи мапы
Поместить Map.Entry в список и отсортировать его, используя Comparator.
В компараторе будем сравнивать исключительно ключи пар:
Если разобрался с лямбдами, эту запись можно существенно сократить:
И, конечно, все можно переписать, используя лямбды:
В отличие от первого способа, используя SortedMap, мы всегда будем хранить данные в отсортированном виде.
3. Как отсортировать значения мапы
4. В чем разница между HashMap, TreeMap, и Hashtable
Порядок элементов. HashMap и Hashtable не гарантируют, что элементы будут храниться в порядке добавления. Кроме того, они не гарантируют, что порядок элементов не будет меняться со временем. В свою очередь, TreeMap гарантирует хранение элементов в порядке добавления или же в соответствии с заданным компаратором.
Допустимые значения. HashMap позволяет иметь ключ и значение null, HashTable — нет. TreeMap может использовать значения null только если это позволяет компаратор. Без использования компаратора (при хранении пар в порядке добавления) значение null не допускается.
Синхронизация. Только HashTable синхронизирована, остальные — нет. Если к мапе не будут обращаться разные потоки, рекомендуется использовать HashMap вместо HashTable.
И общее сравнение реализаций:
HashMap | HashTable | TreeMap | |
---|---|---|---|
Упорядоченность элементов | нет | нет | да |
null в качестве значения | да | нет | да/нет |
Потокобезопасность | нет | да | нет |
Алгоритмическая сложность поиска элементов | O(1) | O(1) | O(log n) |
Структура данных под капотом | хэш-таблица | хэш-таблица | красно-чёрное дерево |
5. Как создать двунаправленную мапу
6. Как создать пустую Map
Обычная инициализация объекта:
Создание неизменяемой (immutable) пустой мапы:
Interesting Tasks
воскресенье, 20 октября 2013 г.
Маппинг объектов с помощью java-object-merger
Для чего нужны мапперы объектов?
Простой ответ: чтобы копировать данные автоматически из одного объекта в другой. Но тогда вы можете спросить: зачем нужно это копирование? Можно усомниться, что это нужно очень часто. Значит следует дать более развернутый ответ.
В мире энтерпрайз приложений принято бить внутреннюю структуру на слои: слой доступа к базе, бизнес и представление/веб сервиса. В слое доступа к базе как правило обитают объекты мапящиеся на таблицы в базе. Условимся называть их DTO (от Data transfer object). По хорошему, они только переносят данные из таблиц и не содержат бизнес логики. На слое представления/веб сервисов, находятся объекты, доставляющие данные клиенту (браузер / клиенты веб сервисов). Назовем их VO (от View object). VO могут требовать только часть тех данных, которые есть в DTO, или же агрегировать данные из нескольких DTO. Они могут дополнительно заниматься локализацией или преобразованием данных в удобный для представления вид. Так что передавать DTO сразу на представление не совсем правильно. Так же иногда в бизнес слое выделяют бизнес объекты BO (Business object). Они являются обертками над DTO и содержат бизнес логику работы с ними: сохранение, модифицирование, бизнес операции. На фоне этого возникает задача по переносу данных между объектами из разных слоев. Скажем, замапить часть данных из DTO на VO. Или из VO на BO и потом сохранить то что получилось.
Если решать задачу в лоб, то получается примерно такой “тупой” код:
Мапперы объектов
Чем плох дозер
Какими качествами должен обладать маппер?
Реализация
Перед началом разработки был соблазн написать библиотеку на scala. Т.к. уже был положительный опыт ее использования.
Почему merger а не mapper?
Использование
Программа “Hello world” выглядит примерно так:
Во-первых, видим, что для маппинга необходимо, чтобы у свойства был геттер на обоих объектах. Это нужно для сравнения значений. И сеттер у целевого объекта, чтобы записывать новое значение. Сами свойства должны именоваться одинаково.
Посмотрим же как реализован метод map. Это поможет понять многие вещи о библиотеке.
Если исходный снапшот это бин, и если у него есть identifier, тогда пытаемся найти целевой бин для класса destinationClass используя IBeanFinder-ы [тут createSnapshot(destinationClass, identifier);]. Мы такие не регистрировали, да и identifier-а нет, значит идем дальше. В противном случает бин создается используя подходящий IObjectCreator [тут createSnapshot(destinationClass)]. Мы таких тоже не регистрировали, однако в стандартной поставке имеется создатель объектов конструктором по умолчанию — он и используется. Далее у целевого снапшота берется дифф от снапшота источника и применяется к целевому объекту. Все.
Кстати, дифф, для этого простого случая, будет выглядеть так:
Основные аннотации:
Преобразования типов
В IMergingContext можно регистрировать пользовательские преобразователи типов, из одного типа в другой (интерфейс TypeConverter). Стандартный набор преобразователей включает преобразования:
Map в Java с примерами
Интерфейс Java Map, java.util.Map, представляет отображение между ключом и значением. В частности, может хранить пары ключей и значений. Каждый ключ связан с определенным значением. После сохранения на карте вы можете позже найти значение, используя только ключ.
Интерфейс не является подтипом интерфейса Collection. Следовательно, он немного отличается от остальных типов коллекций.
Реализация
Поскольку Map является интерфейсом, вам необходимо создать конкретную реализацию интерфейса для его использования. API коллекций содержит следующие:
Наиболее часто используемые реализации – это HashMap и TreeMap. Каждая из них ведет себя немного по-разному в отношении порядка элементов при итерации карты и времени (большая запись 0), необходимого для вставки и доступа к элементам в картах.
Вот несколько примеров того, как создать экземпляр:
Вставка элементов
Чтобы добавить элементы, вы вызываете ее метод put(). Вот несколько примеров:
Три вызова put() отображают строковое значение на строковый ключ. Затем вы можете получить значение, используя этот ключ.
Только объекты могут быть вставлены
Только объекты могут быть использованы в качестве ключей и значений. Если вы передаете примитивные значения (например, int, double и т. Д.) в качестве ключа или значения, они будут автоматически упакованы перед передачей в качестве параметров. Вот пример параметров примитива auto-boxing, передаваемых методу put():
Значение, переданное методу put() в приведенном выше примере, является примитивом int. Java автоматически упаковывает его внутри экземпляра Integer, поскольку для put() в качестве ключа и значения требуется экземпляр Oject. Автобокс также может произойти, если вы передадите примитив в качестве ключа.
Последующие вставки с тем же ключом
Заданный ключ может появляться на карте только один раз. Это означает, что только одна пара ключ + значение для каждого из них может существовать одновременно. Другими словами, для ключа «key1» в одном экземпляре может храниться только одно значение. Конечно, вы можете хранить значения одного и того же ключа в разных экземплярах карты.
Если вы вызываете put() более одного раза с одним и тем же ключом, последнее заменяет существующее значение для данного ключа.
Нулевые ключи не допускаются
Обратите внимание, что ключ не может быть нулевым!
Карта использует методы ключа hashCode() и equals() для внутреннего хранения пары ключ-значение, поэтому, если ключ имеет значение null, карта не может правильно разместить пару внутри.
Допустимы нулевые значения
Значение пары ключ + значение, хранящееся на карте, может быть нулевым, так что это совершенно правильно:
Просто имейте в виду, что вы получите null, когда позже вызовете get() с этим ключом:
Переменная value будет иметь значение null после выполнения этого кода, если нулевое значение было вставлено для этого ключа ранее (как в предыдущем примере).
Вставка всех элементов с другой карты
После выполнения этого кода карта, на которую ссылается переменная mapB, будет содержать обе записи ключ + значение, вставленные в mapA в начале примера кода.
Копирование записей идет только в одну сторону. Вызов mapB.putAll(mapA) будет копировать только записи из mapA в mapB, а не из mapB в mapA. Чтобы скопировать записи другим способом, вам нужно будет выполнить код mapA.putAll(mapB).
Как получить элементы
Чтобы получить определенный элемент вы вызываете его метод get(), передавая ключ для этого элемента в качестве параметра:
Обратите внимание, что метод get() возвращает Java-объект, поэтому мы должны привести его к String(поскольку мы знаем, что значение является String). Позже в этом руководстве по Java Map вы увидите, как использовать Java Generics для ввода Map, чтобы она знала, какие конкретные типы ключей и значений она содержит. Это делает ненужным приведение типов и усложняет случайное добавление неправильных значений в карту.
Возвращение значения по умолчанию
Интерфейс имеет метод getOrDefault(), который может возвращать значение по умолчанию, предоставленное вами – в случае, если никакое значение не сохранено с помощью данного ключа:
В этом примере создается карта и в ней хранятся три значения с использованием ключей A, B и C. Затем вызывается метод Map getOrDefault(), передавая в качестве ключа строку String E вместе со значением по умолчанию – значением String по умолчанию. Поскольку карта не содержит объектов, хранящихся в ключе E, будет возвращено заданное значение по умолчанию.
Проверка содержится ли ключ
Используется метод containsKey():
После выполнения этого кода переменная hasKey будет иметь значение true, если пара ключ + значение была вставлена ранее с помощью строкового ключа 123, и false, если такая пара ключ + значение не была вставлена.
Проверка содержится ли значение
Используется метод containsValue():
После выполнения этого кода переменная hasValue будет содержать значение true, если пара ключ-значение была вставлена раньше, со строковым значением «значение 1», и false, если нет.
Перебор ключей
Существует несколько способов итерации ключей, хранящихся на карте. Наиболее часто используемые методы:
Все методы будут рассмотрены в следующих разделах.
Использование ключевого итератора
С помощью метода keySet():
Как вы можете видеть, ключ Iterator возвращает каждый ключ, сохраненный в Map, один за другим (по одному для каждого вызова next()). Получив ключ, вы можете получить элемент, сохраненный для этого ключа, с помощью метода get().
Использование цикл for-each
В Java 5 вы также можете использовать цикл for-each для итерации ключей, хранящихся на карте:
Эффект приведенного выше кода очень похож на код, показанный в предыдущем разделе.
Использование ключевого потока
Интерфейс Stream является частью Java Stream API, который был добавлен в Java 8. Сначала вы получаете ключ Set из карты, и из него вы можете получить Stream:
Итерация значений
Также возможно просто перебрать значения, хранящиеся в Map, с помощью метода values(). Варианты:
Все эти параметры описаны в следующих разделах.
Использование Value Iterator
Первый способ – это получить экземпляр итератора значения из значения Set и выполнить итерацию:
Поскольку набор неупорядочен, у вас нет никаких гарантий относительно порядка, в котором значения повторяются.
Использование for-each
Второй метод – это цикл Java for-each:
В этом примере будут распечатаны все значения, хранящиеся в переменной mapA MapA.
Использование потока
Подразумевает использование потока значений с помощью API-интерфейса Stream. Сначала вы получаете значение Set из карты, а из значения Set вы можете получить поток:
Итерация записей
Под записями подразумеваются пары ключ + значение. Существует два способа:
Обе эти опции будут объяснены в следующих разделах.
Использование итератора ввода
Обратите внимание, как ключ и значение могут быть получены из каждого экземпляра Map.Entry.
Имейте в виду, что приведенный выше код можно сделать немного лучше, используя Map, типизированную с помощью Generics, как показано далее в этом руководстве.
Использование For-Each
Обратите внимание, что этот пример тоже можно сделать немного красивее, используя универсальную карту.
Удаление записей
Вы удаляете записи, вызывая метод remove(Object key). Таким образом, вы удаляете пару (ключ, значение), соответствующую ключу:
После выполнения этой инструкции карта, на которую ссылается mapA, больше не будет содержать запись (пара ключ + значение) для ключа key1.
Удаление всех записей
Используется метод clear():
Замена записи
Можно заменить элемент, используя метод replace(). Он будет вставлять новое значение только в том случае, если к ключу сопоставимо существующее значение. В ином случае никакое значение не вставлено. Это отличается от того, как работает метод put(), который всегда вставляет значение, несмотря ни на что.
После выполнения этого кода экземпляр Map будет содержать более новое значение String для ключа String.
Количество записей
Вы можете узнать количество записей, используя метод size(). Количество записей в Java-карте также называется размером карты – отсюда и имя метода size(). Вот пример:
Проверка, пуста ли карта
Интерфейс имеет специальный метод для проверки isEmpty() и возвращает:
Общие карты
По умолчанию вы можете поместить любой объект в карту, но Generics из Java 5 позволяет ограничить типы объектов, которые вы можете использовать как для ключей, так и для значений в карте:
Эта карта теперь может принимать только объекты String для ключей и экземпляры MyObject для значений. Затем вы можете получить доступ к итерированным ключам и значениям без их приведения. Вот как это выглядит:
Функциональные операции
Интерфейс имеет несколько функциональных операций, добавленных из Java 8. Они позволяют взаимодействовать с Map в более функциональном стиле. Например, вы передаете лямбда-выражение в качестве параметра этим методам. Функциональные методы работы:
compute()
Метод принимает ключевой объект и лямбда-выражение в качестве параметров. Лямбда-выражение должно реализовывать интерфейс java.util.function.BiFunction. Вот пример:
В приведенном выше примере вы можете видеть, что лямбда-выражение проверяет, является ли значение, сопоставленное данному ключу, нулевым или нет, перед вызовом toString(). ToUpperCase() для него.
computeIfAbsent()
Метод Map computeIfAbsent() работает аналогично методу compute():
Этот пример на самом деле просто возвращает постоянное значение – строку 123. Однако лямбда-выражение могло вычислить значение любым необходимым способом – например, извлечь значение из другого объекта или объединить его с другими значениями и т. д.
computeIfPresent()
Метод работает противоположно computeIfAbsent(). Он вызывает только лямбда-выражение, переданное ему в качестве параметра, если в Map уже существует запись для этого ключа:
merge()
Метод принимает в качестве параметров ключ, значение и лямбда-выражение, реализующее интерфейс BiFunction.
java-object-merger — больше чем просто маппер объектов
Всем привет! Хотел бы представить вам новую библиотеку на java для маппинга/мержинга объектов, которую я “скромно” позиционирую как возможную альтернативу dozer-у. Если вы разрабатываете enterprise приложения на java, вам не безразлична эффективность вашей работы, и хочется писать меньше скучного кода, то приглашаю почитать дальше!
UPD. Выложено в центральный репозиторий мавена
Для чего нужны мапперы объектов?
Простой ответ: чтобы копировать данные автоматически из одного объекта в другой. Но тогда вы можете спросить: зачем нужно это копирование? Можно усомниться, что это нужно очень часто. Значит следует дать более развернутый ответ.
В мире энтерпрайз приложений принято бить внутреннюю структуру на слои: слой доступа к базе, бизнес и представление/веб сервиса. В слое доступа к базе как правило обитают объекты мапящиеся на таблицы в базе. Условимся называть их DTO (от Data transfer object). По хорошему, они только переносят данные из таблиц и не содержат бизнес логики. На слое представления/веб сервисов, находятся объекты, доставляющие данные клиенту (браузер / клиенты веб сервисов). Назовем их VO (от View object). VO могут требовать только часть тех данных, которые есть в DTO, или же агрегировать данные из нескольких DTO. Они могут дополнительно заниматься локализацией или преобразованием данных в удобный для представления вид. Так что передавать DTO сразу на представление не совсем правильно. Так же иногда в бизнес слое выделяют бизнес объекты BO (Business object). Они являются обертками над DTO и содержат бизнес логику работы с ними: сохранение, модифицирование, бизнес операции. На фоне этого возникает задача по переносу данных между объектами из разных слоев. Скажем, замапить часть данных из DTO на VO. Или из VO на BO и потом сохранить то что получилось.
Если решать задачу в лоб, то получается примерно такой “тупой” код:
Знакомо? 🙂 Если да, то могу вас обрадовать. Для этой проблемы уже придумали решение.
Мапперы объектов
Придуманы конечно-же не мной. Реализаций на java много. Вы можете ознакомится, к примеру тут.
Вкратце, задача маппера — скопировать все свойства одного объекта в другой, а так же проделать все то же рекурсивно для всех чайлдовых объектов, по ходу делая необходимые преобразование типов, если это требуется.
Мапперы из списка выше — все разные, более или менее примитивные. Самый мощный пожалуй dozer, с ним я работал около 2 лет, и некоторые вещи в нем перестали устраивать. А вялый темп дальнейшей разработки дозера побудили написать свой “велосипед” (да, я знакомился с другими мапперами — для наших требовний они еще хуже).
Чем плох dozer
Какими качествами должен обладать маппер?
Почему merger а не mapper?
java-object-merger отличает от других мапперов одна особенность. Основополагающая идея была в том, чтобы дать возможность создавать снимки объектов (Snapshot) на некоторый момент времени, и потом, сравнивая их, находить различия (Diff) подобно тому, как находим diff между двумя текстами. Причем должна быть возможность просмотра снапшотов и диффов в понятном для человека текстовом виде. Так, чтобы раз взглянув на дифф сразу стали ясны все отличия, а так же как будет изменен целевой объект после применения диффа. Таким образом добиваемся полной прозрачности процесса. Никакой магии и черных ящиков! Создание снапшотов открывает еще один интересный сценарий. Можно сделать снапшот объекта, потом как-то изменив его, сделать новый снапшот — проверить что изменилось, и, при желании, откатить изменения. Кстати дифф можно обойти специальным visitor-ом, и пометить только те изменения, которые хочется применить, а остальные проигнорировать.
Так что можно сказать, что merger — это нечто большее чем просто mapper.
Использование
Программа “Hello world” выглядит примерно так:
Во-первых, видим, что для маппинга необходимо, чтобы у свойства был геттер на обоих объектах. Это нужно для сравнения значений. И сеттер у целевого объекта, чтобы записывать новое значение. Сами свойства должны именоваться одинаково.
Посмотрим же как реализован метод map. Это поможет понять многие вещи о библиотеке.
Если исходный снапшот это бин, и если у него есть identifier, тогда пытаемся найти целевой бин для класса destinationClass используя IBeanFinder-ы [тут createSnapshot(destinationClass, identifier); ]. Мы такие не регистрировали, да и identifier-а нет, значит идем дальше. В противном случает бин создается используя подходящий IObjectCreator [тут createSnapshot(destinationClass) ]. Мы таких тоже не регистрировали, однако в стандартной поставке имеется создатель объектов конструктором по умолчанию — он и используется. Далее у целевого снапшота берется дифф от снапшота источника и применяется к целевому объекту. Все.
Кстати, дифф, для этого простого случая, будет выглядеть так:
Основные аннотации
Преобразования типов
Категории объектов
Производительность
Честно говоря, пока писал библиотеку — о производительности особо не задумывался. Да и изначально в целях высокой производительности не было. Однако, решил замерять время маппинга N раз на один тестовый объект. Исходный код теста. Объект довольно сложный, с полями значениями, дочерними бинами, коллекциями и мапами. Для сравнения взял dozer последней на текущий момент версии 5.4.0. Ожидал, что дозер не оставит никаких шансов. Но получилось совсем наоборот! dozer замапил 5000 тестовых объектов за 32 секунды, а java-object-merger 50000 объектов за 8 секунд. Разница какая-то дикая — в 40 раз…
Применение
java-object-merger был опробован на текущем проекте с моей основной работы (osgi, spring, hibernate, сотни мапящихся классов). Чтобы заменить им дозер полностью ушло менее 1 дня. По ходу находились некоторые явные косяки, но, после исправления, все основные сценарии работали нормально.
Ленивые снапшоты
ModelMapper: путешествие туда и обратно
По известным причинам, бэкенд не может отдавать данные из репозитория как есть. Самая известная — сущностные зависимости берутся из базы не в таком виде, в котором их может понять фронт. Сюда же можно добавить и сложности с парсингом enum (если поля enum содержат дополнительные параметры), и многие другие сложности, возникающие при автоматическом приведении типов (или невозможности автоматического их приведения). Отсюда вытекает необходимость в использовании Data Transfer Object — DTO, понятном и для бэка, и для фронта.
Конвертацию сущности в DTO можно решить по-разному. Можно применить библиотеку, можно (если проект маленький) наколхозить что-то вроде такого:
Такие самописные мапперы имеют явные недостатки:
Сразу хочу сказать, что если вам что-то непонятно, Вы можете скачать готовый проект с рабочим тестом, ссылка в конце статьи.
Первый шаг — это, конечно, добавление зависимости. Я использую gradle, но вам не составит труда добавить зависимость в maven-проект.
Этого достаточно, чтобы маппер заработал. Далее, нам необходимо создать бин.
Обычно достаточно просто вернуть new ModelMapper, но не лишним будет настроить маппер для наших нужд. Я задал строгую стратегию соответствия, включил сопоставление полей, пропуск нулловых полей и задал приватный уровень доступа к полям.
Далее, создаём следующую структуру сущностей. У нас будет единорог (Unicorn), у которого в подчинении будет какое-то количество дроидов (Droid), и у каждого дроида будет какое-то количество капкейков (Cupcake).
Эти сущности мы будем конвертировать в DTO. Существует как минимум два подхода к конвертации зависимостей из сущности в DTO. Один подразумевает сохранение только ID вместо сущности, но тогда каждую сущность из зависимости при нужде мы будем дёргать по ID дополнительно. Второй подход подразумевает сохранение DTO в зависимости. Так, при первом подходе мы бы конвертировали List droids в List droids (в новом списке храним только ID), а при втором подходе мы будем сохранять в List droids.
Для тонкой настройки маппера под наши нужды нам будет необходимо создать собственный класс-обёртку и переопределить логику для маппинга коллекций. Для этого мы создаём класс-компонент UnicornMapper, автовайрим туда наш маппер и переопределяем нужные нам методы.
Самый простой вариант класса-обёртки выглядит так:
Теперь нам достаточно заавтовайрить наш маппер в какой-нибудь сервис и дёргать по методам toDto и toEntity. Найденные в объекте сущности маппер будет превращать в DTO, DTO — в сущности.
Но если мы попробуем таким образом законвертировать что-нибудь, а потом вызвать, к примеру, toString, то мы получим StackOverflowException, и вот почему: в UnicornDto находится список DroidDto, в котором находится UnicornDto, в котором находятся DroidDto, и так до того момента, пока не закончится стековая память. Поэтому для обратных зависимостей я обычно использую не UnicornDto unicorn, а Long unicornId. Мы, таким образом, сохраняем связь с Unicorn, но обрубаем циклическую зависимость. Поправим наши DTO таким образом, чтобы вместо обратных DTO они хранили ID своих зависимостей.
Но теперь, если мы вызовём DroidMapper, мы получим unicornId == null. Это происходит потому, что ModelMapper не может определить точно, что такое Long. И просто не сетит его. И нам придётся заняться тонкой настройкой необходимых мапперов, чтобы научить их мапить сущности в ID.
Вспоминаем, что с каждым бином после его инициализации можно поработать вручную.
В @PostConstruct мы зададим правила, в которых укажем, какие поля маппер трогать не должен, потому что для них мы определим логику самостоятельно. В нашем случае, это как определение unicornId в DTO, так и определение Unicorn в сущности (поскольку что делать с Long unicornId, маппер так же не знает).
TypeMap — это и есть правило, в котором мы указываем все нюансы маппинга, а также, задаём конвертер. Мы указали, что для конвертирования из Droid в DroidDto мы пропускаем setUnicornId, а при обратной конвертации — setUnicorn. Конвертировать мы всё будем в конвертере toDtoConverter() для UnicornDto и в toEntityConverter() для Unicorn. Эти конвертеры мы должны описать в нашем компоненте.
Самый простой постконвертер выглядит так:
Нам необходимо расширить его функциональность:
То же самое делаем и с обратным конвертером:
По сути, мы просто вставляем в каждый постконвертер дополнительный метод, в котором пропишем собственную логику для пропущенных полей.
При мапинге в DTO мы сетим ID сущности. При мапинге в DTO достаём сущность из репозитория по ID.
Я показал необходимый минимум для начала работы с modelmapper и особо не рефакторил код. Если у тебя, читатель, есть что добавить к моей статье, я буду рад услышать конструктивную критику.
Проект можно посмотреть тут:
Проект на GitHub.
Любители чистого кода наверняка усмотрели уже возможность загнать многие компоненты кода в абстракции. Если Вы из их числа, предлагаю под кат.
Для начала, определим интерфейс для основных методов класса-обёртки.
Унаследуем от него абстрактный класс.
Постконвертеры и методы заполнения специфичных полей смело отправляем туда. Также, создаём два объекта типа Class и конструктор для их инициализации:
Теперь количество кода в DroidMapper сокращается до следующего:
Маппер без специфичных полей выглядит вообще просто: