Что такое маппинг java
Перейти к содержимому

Что такое маппинг java

  • автор:

Mapstruct

Mapstruct — это библиотека для Java, которая генерирует код для передачи данных между разными сущностями в программе. Она помогает сопоставлять объекты из одной сущности с другой. Этот процесс называется маппингом данных, а инструмент для передачи — маппером, или Java mapper.

Освойте профессию «Java-разработчик»

Маппер в широком смысле — подсистема для преобразования и передачи данных. В случае с Mapstruct разработчик обычно пишет аннотацию маппера — короткий блок кода. Он описывает, какую информацию и в какую сущность нужно передать. А библиотека на основе этой аннотации генерирует код, который и занимается непосредственной передачей данных. Поэтому Mapstruct часто называют генератором кода, который базируется на аннотациях.

Благодаря Mapstruct маппинг можно автоматизировать: не понадобится вручную писать методы, которые обрабатывают данные и передают их в другую сущность, в другой слой программы. Такие методы — большое количество лишнего кода, который надо писать и поддерживать. Mapstruct избавляет от этой проблемы.

Кто и зачем пользуется Mapstruct

Библиотеку используют разработчики, чтобы упростить и ускорить написание кода. Для маппинга есть и другие методы, но конкретно Mapstruct имеет ряд преимуществ: он автоматизирован и потому не отнимает лишнего времени, типобезопасен, быстро работает.

  • упростить работу программистов, позволить им сконцентрироваться на бизнес-логике;
  • сократить время на написание кода;
  • избавиться от ошибок, связанных с человеческим фактором;
  • убрать необходимость поддерживать большое количество шаблонного кода мапперов;
  • ускорить работу самой программы, так как Mapstruct работает быстро и генерирует код прямо во время компиляции;
  • увеличить производительность — по этой же причине.

Приложения на Java обычно масштабные и многоуровневые, а передача данных между уровнями нужна часто. Использовать для нее Mapstruct удобнее, чем писать нужные методы вручную.

Профессия / 14 месяцев
Java-разработчик

Освойте востребованный язык

Group 1321314345 (4)

Как работает Mapstruct

По определению с официального сайта Mapstruct — процессор аннотаций, который подключается к компилятору языка Java. Его можно добавить к проекту в любой популярной среде для языка, он совместим с фреймворками для автоматизированной сборки Java-кода, такими как Maven или Gradle.

Во время компиляции кода, то есть его преобразования в исполняемый файл для системы, Mapstruct «читает» написанные разработчиком аннотации. Это блоки описательного кода: он ничего не делает, но описывает, что и куда надо передать. На их основе Mapstruct создает непосредственную реализацию этих описаний и запускает ее. Это работает только с аннотациями для мапперов: именно для них нужна библиотека.

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

Станьте Java-разработчиком
и создавайте сложные сервисы
на востребованном языке

Как пользоваться Mapstruct

Подключение к проекту. Мы не будем приводить точные инструкции: они различаются для разных сред и фреймворков. Если вы работаете с Maven, достаточно добавить одну зависимость в файл .pom — это специальный файл, которым фреймворк пользуется для подключения сторонних модулей. Затем понадобится добавить плагин для Mapstruct в раздел : эта часть файла описывает сборку.

В случае с другими инструментами порядок действий примерно такой же: различаются в основном форматы записи и синтаксис. Подробнее о подключении к разным фреймворкам можно почитать на официальном сайте проекта: там даны инструкции для всех популярных инструментов.

Написание аннотации. Инструкция для Mapstruct озаглавливается аннотацией @Mapper; на следующей строке после этого указания разработчик начинает писать интерфейс. По структуре интерфейс похож на класс, но в Java между этими понятиями есть различие: у интерфейса есть только методы, то есть действия, он не хранит никаких свойств.

Описание интерфейса начинается со слов public interface , причем имя может быть любым, но рекомендуется называть сущности понятными именами.

Синтаксис описания зависит от особенностей маппинга. В простейшем случае это строки, которые выглядят примерно так:

Методом называют функцию внутри интерфейса или объекта. Название метода придумывает сам разработчик; рекомендуется делать его понятным и наглядным. Сам код для метода в этом случае писать не нужно: это та самая практическая реализация, которую должен сгенерировать Mapstruct.

Дополнительные объяснения. Разработчик также может дать Mapstruct дополнительные инструкции с объяснениями, как преобразовывать объекты, если их типы или имена полей не совпадают: в изначальной сущности одно, а в целевой должно быть другое. Это делается с помощью аннотации @Mapping внутри интерфейса: после нее в скобочках указываются изначальные и целевые имена.

Выглядит это так:

По похожему принципу можно преобразовать типы.

Получение реализации. Этот шаг не всегда обязателен. Но если вы хотите получить созданный Mapstruct код, записать его куда-то и использовать позже — так можно сделать. В интерфейсе можно объявить переменную INSTANCE и записать туда сгенерированный маппер с помощью специального метода getMapper.

Из этой переменной клиент может считать сгенерированный код и воспользоваться им, если это понадобится.

Компиляция. Скомпилировать проект, в котором используется Mapstruct, понадобится вручную. Для этого в большинстве фреймворков для автоматизации достаточно набрать одну команду в консоли: она запустит компилятор.

После компиляции в папке проекта появятся новые файлы. Один из них — реализация маппера, обычно он располагается в подпапке с классами. Среды разработки для Java позволяют декомпилировать файлы, то есть преобразовать из исполняемых инструкций для системы обратно в исходный код: можно воспользоваться этой возможностью и посмотреть, что сгенерировал Mapstruct.

Тестирование. Чтобы убедиться, что сгенерированный код работает правильно, для него рекомендуют писать unit-тесты: так называются тесты, которые охватывают только один конечный компонент. Это перестраховка: она помогает убедиться, что всё перенеслось правильно. Тесты особенно важны на ранних этапах использования, например при обучении.

Как начать пользоваться Mapstruct

Скачать библиотеку можно с официального репозитория проекта на GitHub. Она бесплатная, с открытым исходным кодом, так что вы сможете просмотреть его в любой момент. Потом останется подключить Mapstruct к проекту — мы уже писали про это выше. Можно просто воспользоваться инструкцией с официального сайта. После этого библиотекой можно пользоваться.

Java-разработчик

Java уже 20 лет в мировом топе языков программирования. На нем создают сложные финансовые сервисы, стриминги и маркетплейсы. Освойте технологии, которые нужны для backend-разработки, за 14 месяцев.

java-object-merger — больше чем просто маппер объектов

Всем привет! Хотел бы представить вам новую библиотеку на java для маппинга/мержинга объектов, которую я “скромно” позиционирую как возможную альтернативу dozer-у. Если вы разрабатываете enterprise приложения на java, вам не безразлична эффективность вашей работы, и хочется писать меньше скучного кода, то приглашаю почитать дальше!

UPD. Выложено в центральный репозиторий мавена

 net.sf.brunneng.jom java-object-merger 0.8.5.1  

Для чего нужны мапперы объектов?

Простой ответ: чтобы копировать данные автоматически из одного объекта в другой. Но тогда вы можете спросить: зачем нужно это копирование? Можно усомниться, что это нужно очень часто. Значит следует дать более развернутый ответ.
В мире энтерпрайз приложений принято бить внутреннюю структуру на слои: слой доступа к базе, бизнес и представление/веб сервиса. В слое доступа к базе как правило обитают объекты мапящиеся на таблицы в базе. Условимся называть их DTO (от Data transfer object). По хорошему, они только переносят данные из таблиц и не содержат бизнес логики. На слое представления/веб сервисов, находятся объекты, доставляющие данные клиенту (браузер / клиенты веб сервисов). Назовем их VO (от View object). VO могут требовать только часть тех данных, которые есть в DTO, или же агрегировать данные из нескольких DTO. Они могут дополнительно заниматься локализацией или преобразованием данных в удобный для представления вид. Так что передавать DTO сразу на представление не совсем правильно. Так же иногда в бизнес слое выделяют бизнес объекты BO (Business object). Они являются обертками над DTO и содержат бизнес логику работы с ними: сохранение, модифицирование, бизнес операции. На фоне этого возникает задача по переносу данных между объектами из разных слоев. Скажем, замапить часть данных из DTO на VO. Или из VO на BO и потом сохранить то что получилось.

Если решать задачу в лоб, то получается примерно такой “тупой” код:

… employeeVO.setPositionName(employee.getPositionName()); employeeVO.setPerson(new PersonVO()); PersionVO personVO = employeeVO.getPerson(); PersonDTD person = employee.getPerson(); personVO.setFirstName(person.getFirstName()); personVO.setMiddleName(person.getMiddleName()); personVO.setLastName(person.getLastName()); . 

Знакомо? 🙂 Если да, то могу вас обрадовать. Для этой проблемы уже придумали решение.

Мапперы объектов

Придуманы конечно-же не мной. Реализаций на java много. Вы можете ознакомится, к примеру тут.
Вкратце, задача маппера — скопировать все свойства одного объекта в другой, а так же проделать все то же рекурсивно для всех чайлдовых объектов, по ходу делая необходимые преобразование типов, если это требуется.
Мапперы из списка выше — все разные, более или менее примитивные. Самый мощный пожалуй dozer, с ним я работал около 2 лет, и некоторые вещи в нем перестали устраивать. А вялый темп дальнейшей разработки дозера побудили написать свой “велосипед” (да, я знакомился с другими мапперами — для наших требовний они еще хуже).

Чем плох dozer
  1. Бедная поддержка конфигурирования через аннотации (есть только @Mapping ).
  2. Невозможно мапить из нескольких полей в одно (к примеру собрать полное имя из имени, фамилии и отчества).
  3. Проблемы с маппингом генерик свойств. Если в родительском абстрактном классе есть геттер возвращающий генерик тип T, где , а в чайлде T определен, то при маппинге чайлда будет учитываться только спецификация типа T. Будто бы он IEntity, а не тот, которым он определен в чайлдовом классе..
    Классы свойств хранятся как строки во внутреннем кэше дозера, и чтобы получить класс, используется специальный класс лоадер. Проблемы с этим возникают в osgi окружении, когда dozer находится в одном бандле, а нужный класс бина в другом, не доступным из первого. Проблему мы побороли хоть и стандартным способом — подсунув нужный класс лоадер, но сама реализация: хранить класс в виде строки — выглядит странно. Возможно это для того чтобы не создавать perm gen space мемори ликов. Но все равно не очень понятно.
    Если что-то вдруг не мапится, то разобраться в этом очень сложно. Если вы будете дебажить дозер, то поймете почему. Там какое-то… просто сумасшедшее нагромождение ООП паттернов — все запутанно и не явно. Впрочем, это только на мой вкус.

Какими качествами должен обладать маппер?
  1. Широкая поддержка конфигурации через аннотации.
  2. Полная поддержка генериков.
  3. Чистый, понятный код, который сможет подебажить любой не рискуя сломать мозг.
  4. По умолчанию, без каких либо дополнительных настроек, должно мапить так, как этого скорее всего будет ожидать разработчик.
  5. Должна быть возможность тонкой настройки (не хуже чем у дозера).
Почему merger а не mapper?

java-object-merger отличает от других мапперов одна особенность. Основополагающая идея была в том, чтобы дать возможность создавать снимки объектов (Snapshot) на некоторый момент времени, и потом, сравнивая их, находить различия (Diff) подобно тому, как находим diff между двумя текстами. Причем должна быть возможность просмотра снапшотов и диффов в понятном для человека текстовом виде. Так, чтобы раз взглянув на дифф сразу стали ясны все отличия, а так же как будет изменен целевой объект после применения диффа. Таким образом добиваемся полной прозрачности процесса. Никакой магии и черных ящиков! Создание снапшотов открывает еще один интересный сценарий. Можно сделать снапшот объекта, потом как-то изменив его, сделать новый снапшот — проверить что изменилось, и, при желании, откатить изменения. Кстати дифф можно обойти специальным visitor-ом, и пометить только те изменения, которые хочется применить, а остальные проигнорировать.
Так что можно сказать, что merger — это нечто большее чем просто mapper.

Использование

Программа “Hello world” выглядит примерно так:

import net.sf.brunneng.jom.IMergingContext; import net.sf.brunneng.jom.MergingContext; public class Main < public static class A1 < private String field1; public String getField1() < return field1; >public void setField1(String field1) < this.field1 = field1; >> public static class A2 < private String field1; public A2(String field1) < this.field1 = field1; >public String getField1() < return field1; >> public static void main(String[] args) < IMergingContext context = new MergingContext(); A2 a2 = new A2("Hello world!"); A1 a1 = context.map(a2, A1.class); System.out.println(a1.getField1()); >> 

Во-первых, видим, что для маппинга необходимо, чтобы у свойства был геттер на обоих объектах. Это нужно для сравнения значений. И сеттер у целевого объекта, чтобы записывать новое значение. Сами свойства должны именоваться одинаково.

Посмотрим же как реализован метод map. Это поможет понять многие вещи о библиотеке.

@Override public T map(Object source, Class destinationClass) < Snapshot sourceSnapshot = createSnapshot(source); Snapshot destSnapshot = null; if (sourceSnapshot.getRoot().getType().equals(DataNodeType.BEAN)) < Object identifier = ((BeanDataNode)sourceSnapshot.getRoot()).getIdentifier(); if (identifier != null) < destSnapshot = createSnapshot(destinationClass, identifier); >> if (destSnapshot == null) < destSnapshot = createSnapshot(destinationClass); >Diff diff = destSnapshot.findDiffFrom(sourceSnapshot); diff.apply(); return (T)destSnapshot.getRoot().getObject(); > 

Если исходный снапшот это бин, и если у него есть identifier, тогда пытаемся найти целевой бин для класса destinationClass используя IBeanFinder-ы [тут createSnapshot(destinationClass, identifier); ]. Мы такие не регистрировали, да и identifier-а нет, значит идем дальше. В противном случает бин создается используя подходящий IObjectCreator [тут createSnapshot(destinationClass) ]. Мы таких тоже не регистрировали, однако в стандартной поставке имеется создатель объектов конструктором по умолчанию — он и используется. Далее у целевого снапшота берется дифф от снапшота источника и применяется к целевому объекту. Все.

Кстати, дифф, для этого простого случая, будет выглядеть так:

MODIFY < dest object : Main$A1@28a38b58 src object : Main$A2@76f8d6a6 ADD < dest property : String field1 = null src property : String field1 = "Hello world!" >> 
Основные аннотации
  • @Mapping — задает путь к полю для маппинга на другом конце ассоциации (например “employee.person.firstName” ). Может быть указано на классе целевого объекта или объекта источника.
  • @Skip — поле не попадает в снапшот, не сравнивается и не мапится.
  • @Identifier — помечает поле которое считаеся идентификатором бина. Таким образом при сравнении коллекций мы будем знать какой объект с каким должен сравниваться. А именно будут сравниваться объекты с совпадающими идентификаторами. Так же, если в процессе применения диффа возникнет потребность создать бин, и при этом известен идентификатор, то будет попытка вначале найти этот бин при помощи зарегистрированных IBeanFinder -ов. Так, реализация IBeanFInder может искать бины к примеру в базе данных.
  • @MapFromMany — то же самое что и @Mapping только указывается на классе целевого объекта и позволяет указать массив свойств на объекте источнике которые будут мапится на поле в целевом объекте.
  • @Converter — позволяет задать на свойстве класс наследник PropertyConverter . — он выполнит преобразование между свойствами. Конвертер свойств обязателен при маппинге нескольких полей на одно, т.к. он как раз и должен будет собрать все значения из источника воедино и сформировать из них одно значение.
  • @OnPropertyChange, @OnBeanMappingStarted, @OnBeanMappingFinished — позволяют пометить методы прослушивающие соответствующие эвенты в жизненном цикле маппинга, которые происходят в данном бине.
  • И другие.
Преобразования типов
  • примитивных типов в обертки, и наоборот
  • преобразования дат
  • объектов в строку
  • энумы в энумы, и строки в энумы по имени константы энума
Категории объектов
  1. Объекты значения: примитивные типы, объекты в пакете java.lang , даты, массивы объектов значений. Список классов считающихся значениями можно расширять через IMergingConext .
  2. Коллекции — массивы, все наследующиеся от java.util.Collection .
  3. Мапы — все наследующиеся от java.util.Map .
  4. Бины — все остальные.
Производительность

Честно говоря, пока писал библиотеку — о производительности особо не задумывался. Да и изначально в целях высокой производительности не было. Однако, решил замерять время маппинга N раз на один тестовый объект. Исходный код теста. Объект довольно сложный, с полями значениями, дочерними бинами, коллекциями и мапами. Для сравнения взял dozer последней на текущий момент версии 5.4.0. Ожидал, что дозер не оставит никаких шансов. Но получилось совсем наоборот! dozer замапил 5000 тестовых объектов за 32 секунды, а java-object-merger 50000 объектов за 8 секунд. Разница какая-то дикая — в 40 раз…

Применение

java-object-merger был опробован на текущем проекте с моей основной работы (osgi, spring, hibernate, сотни мапящихся классов). Чтобы заменить им дозер полностью ушло менее 1 дня. По ходу находились некоторые явные косяки, но, после исправления, все основные сценарии работали нормально.

Ленивые снапшоты

Одна из явных проблем, найденных во время прикручивания маппера к реальному проекту было то, что если делать снапшот на DTO у которой есть ленивые списки других сущностей, а те другие ссылаются на третьи и т.д, то за создание одного снапшота можно, ненароком, выкачать пол базы. Поэтому было решено сделать все свойства в снапшоте ленивыми по умолчанию. Это означает, что они не будут вытаскиваться из объектов до момента сравнения с соответствующим свойством при взятии диффа. Или пока явно не вызовем на снапшоте метод loadLazyProperties() . А при вытаскивании свойства происходит автоматическое достраивание снапшота — опять же с ленивыми свойствами, которые ждут пока их догрузят.

Маппинг примитивных типов данных

Во время предыдущих трех уровней мы немного познакомились с Hibernate. Настало время зайти на второй круг. Теперь мы начнем изучать тоже самое, только глубже. И начнем мы с маппинга полей Entity-класса на колонки таблиц в базе данных.

Как ты уже знаешь, маппинг поля в Entity классе на колонку выполняется с помощью аннотации @Column . И теперь вопрос: а поля каких типов можно замапить такой аннотацией?

Все типы данных в Java можно разбить на три группы:

  • Тип достаточно простой и его легко сохранить в базу данных.
  • Тип сложный и для него нужно писать специальный конвертор.
  • Тип очень сложный и для хранения его значений нужна отдельная таблица.

К простым типам, про которые Hibernate знает, как их сохранять, относятся такие:

Типы в языке Java package Примеры классов
Примитивные типы языка Java boolean , int , double и т. п.
Обертки над примитивами java.lang Boolean , Integer , Double и т. п.
Строки java.lang String
«Продвинутые» числа java.math BigInteger и BigDecimal
Дата и время java.time LocalDate , LocalTime , LocalDateTime , OffsetTime , OffsetDateTime , Instant
Различные вариации даты и времени java.util Date и Calendar
Старые форматы даты и времени java.sql Date , Time , Timestamp
Массив байт или символов byte[] или Byte[] , char[] или Character[]
Перечисления (enum) Любой enum
Сериализуемые объекты Любая имплементация java.io.Serializable

Для всех этих типов есть их аналоги в языке SQL, поэтому Hibernate хорошо знает, как их сохранять и загружать из базы.

 @Entity @Table(name="user") class User

Задания типа вручную – аннотация @Type

Иногда ты можешь захотеть вмешаться в политику Hibernate и явно указать ему в каком типе нужно хранить данные в базе. Например, у тебя в Entity-классе поле имеет тип Integer, но в базе для него есть колонка с типом VARCHAR.

Для этого есть специальная аннотация – @Type . Выглядит она очень просто:

 @Type(type="имя-типа")

Давай, например, попросим Hibernate, чтобы поле createdDate нашего класса User хранилось в базе в виде строки:

 @Entity @Table(name="user") class User

Если Hibernate поймет, как конвертировать тип Date в ваш новый тип, то просто сделает это. Если не поймет, тогда тебе нужно будет указать специальной конвертор типов. Но об этом немного позднее.

Список Hibernate-типов для баз данных

Кстати, ты обратил внимание, что мы указали тип org.hibernate.type.StringType, а не String. Это потому, что мы выбрали один из типов, который поддерживает СУБД, а не язык Java. У них у каждого своя система типов. Просто разработчики Hibernate придумали удобные названия в стиле Java, вместо этих VARCHAR’ов.

Кстати, этот список не такой уж и маленький. Приведу тут его часть:

Hibernate type (org.hibernate.type package) JDBC type Java type BasicTypeRegistry key(s)
StringType VARCHAR java.lang.String string, java.lang.String
MaterializedClob CLOB java.lang.String materialized_clob
TextType LONGVARCHAR java.lang.String text
CharacterType CHAR char, java.lang.Character char, java.lang.Character
BooleanType BIT boolean, java.lang.Boolean boolean, java.lang.Boolean
NumericBooleanType INTEGER, 0 is false, 1 is true boolean, java.lang.Boolean numeric_boolean
YesNoType CHAR, ‘N’/’n’ is false, ‘Y’/’y’ is true. The uppercase value is written to the database. boolean, java.lang.Boolean yes_no
TrueFalseType CHAR, ‘F’/’f’ is false, ‘T’/’t’ is true. The uppercase value is written to the database. boolean, java.lang.Boolean true_false
ByteType TINYINT byte, java.lang.Byte byte, java.lang.Byte
ShortType SMALLINT short, java.lang.Short short, java.lang.Short
IntegerTypes INTEGER int, java.lang.Integer int, java.lang.Integer
LongType BIGINT long, java.lang.Long long, java.lang.Long
FloatType FLOAT float, java.lang.Float float, java.lang.Float
DoubleType DOUBLE double, java.lang.Double double, java.lang.Double
BigIntegerType NUMERIC java.math.BigInteger big_integer, java.math.BigInteger
BigDecimalType NUMERIC java.math.BigDecimal big_decimal, java.math.bigDecimal
TimestampType TIMESTAMP java.sql.Timestamp timestamp, java.sql.Timestamp
TimeType TIME java.sql.Time time, java.sql.Time
DateType DATE java.sql.Date date, java.sql.Date
CalendarType TIMESTAMP java.util.Calendar calendar, java.util.Calendar
CalendarDateType DATE java.util.Calendar calendar_date
CurrencyType java.util.Currency VARCHAR currency, java.util.Currency
LocaleType VARCHAR java.util.Locale locale, java.utility.locale
TimeZoneType VARCHAR, using the TimeZone ID java.util.TimeZone timezone, java.util.TimeZone
UrlType VARCHAR java.net.URL url, java.net.URL
ClassType VARCHAR (class FQN) java.lang.Class class, java.lang.Class

Таблица, конечно, большая, но очень полезная. Например, из нее понятно, что тип Boolean можно сохранить в базу как минимум шестью разными способами. Тебе столько не нужно? А кто сказал, что способ сохранения выбираешь ты?

В SQL нет типа Boolean и его часто хранят так:

  • 1 или 0
  • ‘F’ или ‘T’
  • ‘Y’ или ‘N’

Поэтому очень хорошо, когда Hibernate понимает все эти заморочки. Или, например, давай возьмем хранение массивов данных в базе. Есть куча разных вариантов и Hibernate умеет работать с ними со всеми:

Hibernate type (org.hibernate.type package) JDBC type Java type BasicTypeRegistr
BlobType BLOB java.sql.Blob blog, java.sql.Blob
ClobType CLOB java.sql.Clob clob, java.sql.Clob
BinaryType VARBINARY byte[] binary, byte[]
MaterializedBlobType BLOB byte[] materized_blob
ImageType LONGVARBINARY byte[] image
WrapperBinaryType VARBINARY java.lang.Byte[] wrapper-binary, Byte[], java.lang.Byte[]
CharArrayType VARCHAR char[] characters, char[]
CharacterArrayType VARCHAR java.lang.Character[] wrapper-characters, Character[], java.lang.Character[]
UUIDBinaryType BINARY java.util.UUID uuid-binary, java.util.UUID
UUIDCharType CHAR, can also read VARCHAR java.util.UUID uuid-char
PostgresUUIDType PostgreSQL UUID, through Types#OTHER, which complies to the PostgreSQL JDBC driver definition java.util.UUID pg-uuid

А после выпуска JDK 8 в Hibernate добавилось еще несколько типов, связанных со временем. И все с целью упростить тебе жизнь. Тебе больше не нужно думать, поддерживаются ли все эти новомодные типы. Создатели Hibernate уже добавили их поддержку для тебя:

Hibernate type (org.hibernate.type package) JDBC type Java type BasicTypeRegistr
DurationType BIGINT java.time.Duration Duration, java.time.Duration
InstantType TIMESTAMP java.time.Instant Instant, java.time.Instant
LocalDateTimeType TIMESTAMP java.time.LocalDateTime LocalDateTime, java.time.LocalDateTime
LocalDateType DATE java.time.LocalDate LocalDate, java.time.LocalDate
LocalTimeType TIME java.time.LocalTime LocalTime, java.time.LocalTime
OffsetDateTimeType TIMESTAMP java.time.OffsetDateTime OffsetDateTime, java.time.OffsetDateTime
OffsetTimeType TIME java.time.OffsetTime OffsetTime, java.time.OffsetTime
OffsetTimeType TIMESTAMP java.time.ZonedDateTime ZonedDateTime, java.time.ZonedDateTime

Что такое маппинг java

Проверьте, пожалуйста, код в разделе «3. Как отсортировать значения мапы», в нём надо переименовать переменную.

Dmytryi Shubchynskyi Уровень 108 Expert
22 февраля 2022
для чего нужно Создание неизменяемой (immutable) пустой мапы ?
Sergey Kornilov Уровень 39
5 июля 2021
В закладки.
Иван Уровень 41
8 января 2021

Так же есть ошибка в пункте 4, подпункт 3, где говорится про потокобезопасность HashTable. В тексте нужно поменять местами HashMap и HashTable и тогда таблица будет верно читаться.

Yuliia Boklah Уровень 41
6 сентября 2020

В свою очередь, TreeMap гарантирует хранение элементов в порядке добавления или же в соответствии с заданным компаратором. Указана совершенно неправильная информация. Речь идет об LinkedHashMap. В TreeMap элементы хранятся в есттественном порядке (natural ordering) или же в соответствии с заданным компаратором. Исправьте ошибку и не вводите учеников в заблуждение.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *