Java Interview Review
6.71K subscribers
12 photos
262 links
Популярные вопросы и ответы с собеседований на Java-разработчика.

Канал для Android-разработчиков @AndroidSobes

Связаться с нами @SobesAdmin

http://itsobes.ru/
Download Telegram
to view and join the conversation
Опишите жизненный цикл сервлета (1/2)

Совсем недавно мы рассмотрели жизненный цикл компонентов Spring-приложения. Поговорим теперь об этапах работы центрального элемента JavaEE web-приложения – сервлета.

События жизненного цикла сервлета можно разделить на две группы. Первая группа – события, относящиеся непосредственно к самому сервлету. Все они объявляются методами из базового интерфейса Servlet. Примерно такие же фазы жизни и у фильтров.

1. Загрузка класса и создание экземпляра сервлета, конструктор. Может быть создано несколько экземпляров, здесь можно располагать только техническую часть инициализации (подробнее).

2. Инициализация – в общем случае метод init. Инициализация гарантированно происходит до первого запроса, один раз на один сервлет (даже если экземпляров несколько). По умолчанию она ленивая, то есть случается непосредственно перед обработкой этого первого запроса. Другой режим, пре-инициализация, включается свойством loadOnStartup.

Если в качестве инициализации нужно всего лишь параметризовать сервлет, вместо реализации метода достаточно передать параметры в свойстве initParams аннотации @WebServlet. Эти параметры, и другую информацию можно прочитать из объекта конфигурации типа ServletConfig. Дефолтная реализация init-метода принимает эту конфигурацию и сохраняется в поле.

3. Обработка запроса – service. В отличие от Spring Framework, в классической JavaEE все запросы вне зависимости от HTTP-метода, пути и набора параметров попадают в единый метод-обработчик. Вся информация о запросе приходит в параметре ServletRequest. У метода нет возвращаемого значения, вместо этого модифицируется второй параметр – ServletResponse. Для фильтров аналогичный метод doFilter, он кроме запроса и ответа принимает третий параметр, FilterChain.

4. Завершение работы – destroy. Здесь всё как обычно с финализациями: вызывается только однажды, только при корректном завершении, после ее вызова service/doFilter уже никогда не будет вызван.

#JavaEE
Опишите жизненный цикл сервлета (2/2)

Другая группа событий – то что происходит со связанными с сервлетом сущностями. Таких сущностей три: ServletContext, ServletRequest и HttpSession.

На каждую из этих сущностей есть два интерфейса-слушателя. Интерфейс XListener сообщает о создании и уничтожении сущности через два соответствующих метода. Так, чтобы совершить какое-то действие при открытии новой HTTP-сессии, действие нужно поместить в реализацию метода HttpSessionListener.sessionCreated().

Все три типа позволяют хранить пользовательские данные – атрибуты. Они хранятся по ключам-строкам, как в хэш-таблице. Методы второго интерфейса XAttributeListener дают возможность обработать добавление, изменение и удаление атрибутов. То есть, если где-то в программе в контекст сервлета добавляется значение (вызван ServletContext.setAttribute("myKey", someValue)), то сработает ServletContextAttributeListener.attributeAdded.

• ServletContext – прослойка между отдельными сервлетами и сервлет-контейнером. Есть только один контекст на всё web-приложение. Он в отличие от сервлетов не ленивый, и инициализируется на старте приложения, до инициализации отдельных сервлетов.

• ServletRequest – отдельный запрос. На каждый запрос создается новый такой объект, инициализируется перед основным обработчиком Servlet.service(), уничтожается после.

• HttpSession – HTTP-сессия, которая служит для поддержания состояния между запросами для одного пользователя. Обычно создается вручную внутри обработчика Servlet.service(). Уничтожаться может как тоже вручную в обработчике, так и по истечению срока жизни. Только для сессии есть третий интерфейс, который уведомляет об изменении идентификатора – HttpSessionIdListener.

Чтобы объект под этими интерфейсами начал получать уведомления, его нужно зарегистрировать в контексте. Это делается либо явным вызовом ServletContext.addListener, либо аннотацией @WebListener на класс.

#JavaEE
Как обойти коллекцию?

for/while. Классический способ: целочисленная переменная-индекс, которая увеличивается от 0 до size(). Можно использовать для неполного обхода, с нестандартным шагом. Плата за это – возможность ошибиться в индексах и менее читабельный код.

Iterator. ООП-способ: методом iterator() получить объект-итератор, и вызывать у него next() пока hasNext() возвращает true. В реализации может быть дополнительная логика, такая как потокобезопасность. Такой «объект-итерацию» коллекции можно передать в сторонний код, не отдавая саму коллекцию. Всё еще требует слишком много кода.

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

Стримы. Создать от коллекции стрим и работать с элементами в нём. Кроме простого forEach(), можно воспользоваться всей мощью Java Steam API – фильтровать, преобразовывать и агрегировать элементы. За это создаются лишние объекты, а синтаксис гораздо более развесистый.

Функции Java 8. С этой версии появились удобные средства для обхода не только строк. У коллекций и хэш-таблиц добавились методы forEach для обхода и replaceAll для модификации. Как со стримами, они дают функциональный стиль, но без избыточного создания стримов. Внутри используются простые итераторы и циклы for.

#Коллекции
#Язык
Может ли имя класса не совпадать с именем файла?

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

Protected и private классов верхнего уровня не бывает в принципе, а вот на package-protected это ограничение не распространяется. Это значит, что класс без модификатора доступа может иметь любое имя. Также это значит, что рядом с основным публичным классом файла (или вместо него) можно объявить любое количество других классов без модификатора доступа, с произвольными именами. Они будут доступны внутри всего пакета.

Так что ответ – может.

#Язык
Что такое Jakarta EE?

Сомнительно, что такое спросят на собеседовании. Этот пост – скорее экскурс в историю и способ разобраться в хитросплетениях названий.

Enterprise-версия Java дважды за свою историю была переименована.

Говоря о первом переименовании, стоит сказать пару слов о версиях Java вообще. Сначала JDK подчинялся правилам версионирования semver, продукт с названием Java 1 – это JDK версии 1.0.2 и позже 1.1.

В 1.2 вся платформа получил маркетинговое название Java 2, а версия JDK разделилась на отдельные части: J2SE, J2ME и J2EE. Эти правила, сохранялись до 1.4.

В 2004 году, подгоняемая молодым конкурентом в лице C#, Java получила множество обновлений. Среди них – появление современной нумерации. Из названия следующего релиза пропала цифра 2, а формат версии из инженерного стал маркетинговым. Вышла Java 5, J2EE 1.5 превратилась в Java EE 5.

В 2018-ом компания Oracle рассталась с enterprise-версией, передала её open-source организации Eclipse Foundation. Авторское право на слово «Java» осталось за компанией, и технология получила свое нынешнее официальное название: Jakarta EE. Не следует путать ее с другим проектом Apache Jakarta.

Еще один забавный факт: до Java 7 у версий были кодовые имена, семерка вышла под именем «Дельфин». Далее разработка перешла от Sun к Oracle, и веселье кончилось.

#JavaEE
Как использовать JavaEE сервлет в Spring Framework?

Web-приложение на Spring MVC технически само по себе работает на сервлетах: всю обработку запросов берет на себя единый DispatcherServlet. С его помощью реализуется паттерн Front Controller.

Если вам нужно определить в программе полностью независимый от Spring-контекста сервлет или фильтр, ничего особенного для этого делать не нужно. Как обычно в Servlet API, нужно объявить класс, добавить его в web.xml как сервлет, добавить для сервлета маппинг.

Сервлет живет вне Spring-контекста, внедрение зависимостей в нём просто так не заработает. Чтобы использовать autowiring, на этапе инициализации сервлета вызывается статический SpringBeanAutowiringSupport.processInjectionBasedOnServletContext, с текущим сервлетом и его контекстом в аргументах. В этом же утилитарном классе есть ряд других средств для работы с контекстом извне.

Если программа построена на Spring Boot, создание бина типа ServletRegistrationBean поможет добавить сервлеты в рантайме. А для декларативного добавления на этапе компиляции, к классу конфигурации применяется @ServletComponentScan. С этой аннотацией стартер приложения просканирует и добавит в контекст все web-компоненты в стиле Servlet 3.0: классы с аннотациями @WebFilter, @WebListener и @WebServlet.

#JavaEE
#Spring
Как передать runtime информацию о generic-типе?

Когда вы проектируете API-метод библиотеки, иногда логика его реализации может зависеть от указанного клиентом типа. Особенно часто с этой задачей встречаются при разработке парсеров. Например, библиотека Jackson превращает JSON в объект заданного класса. На интервью этот вопрос можно встретить в виде практической задачи.

Первое, что приходит в голову для решения – дженерик-параметр. Такой подход не сработает, потому что тип будет стёрт во время компиляции, а логика будет происходить позже, во время выполнения.

Решение, которое сработает для многих случаев – объявление в методе аргумента типа Class<T>. Пользователь будет передавать в него значение Foo.class или fooInstance.getClass(). Проблемы с ним начинаются, когда становится нужно передать generic-тип. Синтаксис .class не поддерживает дженерики, а .getClass() от экземпляров List<String> и List<Integer> вернет один и тот же объект-описание сырого типа List.

На помощь приходит техника, описанная в предыдущей публикации.

1. Объявляется generic класс-обертка над типом: TypeInformation<T>;. Наш метод будет принимать информацию о типе в виде экземпляра этой обертки.

2. В обертку добавляется конструктор с видимостью protected. Теперь можно создавать объекты только наследников, но не самого этого типа.

3. Пользователь будет передавать экземпляр анонимного наследника обертки: new TypeInformation<List<String>>() {}.

4. Внутри вызов getClass().getGenericSuperclass() вернет ParameterizedType. Это будет описание типа родителя анонима, то есть самой обертки. Из него с помощью getActualTypeArguments() можно достать рантайм-информацию о значении дженерика (о List<String>).

#Дженерики
Что такое fail-fast и fail-safe итераторы?

Это не какие-то отдельные типы, а характеристики разных реализаций интерфейса Iterator. Они определяют, как поведет себя итератор при изменении перебираемой последовательности.

Fail-fast – «быстрый» итератор. Когда после его создания коллекция как-либо изменилась, он падает с ошибкой без лишних разбирательств. Так работает итератор класса ArrayList, при изменении он выбрасывает ConcurrentModificationException. Рекомендуется не основывать логику программы на fail-fast отказах, и использовать их только как признак ошибки реализации.

Fail-safe – «умный» итератор. Обычно плата за отказоустойчивость – возможная неконсистентность данных («слабая консистентность»). Итератор класса ConcurrentHashMap работает с копией данных, он не выбросит исключение при изменении коллекции, но может не увидеть часть свежих изменений. Плата за отсутствие ошибок других fail-safe итераторов может отличаться, детали всегда можно найти в документации коллекций.

#Коллекции
Какой способ логирования выбрать?

Библиотеки логирования, ставшие стандартом де-факто, в хронологическом порядке их появления:

• java.util.Logging из стандартной библиотеки
• Log4j (deprecated)
• Logback
• Log4j 2

Все эти библиотеки поддерживают основные фичи: уровни логирования (что писать), различные хэндлеры (куда писать), форматы (как писать). Развитие библиотек происходило последовательно, в нюансах каждая из них учитывала ошибки предыдущей, сохраняя её преимущества. Обычно выбирают самые свежие варианты – Logback и Log4j 2.

Для совмещения нескольких решений в одном проекте существуют библиотеки-фасады, которые подставляют разные реализации под общий интерфейс:
• SLF4j
• Apache Commons Logging (JCL)

Кроме того, можно использовать bridge-библиотеки. Они маскируют одну реализацию непосредственно под интерфейс другой. С их помощью, например, можно уместить в одном проекте использование двух разных фасадов, или обойтись вовсе без фасадов.

Для более глубокого погружения в вопрос библиотек логирования рекомендуется к просмотру этот доклад.

#Инструменты
Как сравнивать элементы перечисления?

Элементы enum-а компилируются в статические константы-экземпляры его класса. Экземпляры гарантированно синглтоны. Это значит, для их сравнения безопасно использовать ==, даже после десериализации и в многопоточной среде.

Скомпилированный класс неявно наследуется от java.lang.Enum, в котором все методы из Object кроме toString объявлены финальными. В частности, невозможно изменить поведение метода equals – он сравнивает enum-ы с помощью ==. Так что equals тоже можно использовать без опаски.

Но помимо этого есть несколько отличий в пользу ==:

1. == не выбросит NullPointerException. Прежде чем вызывать equals у переменной, придется удостовериться что она не null.

2. == не позволит сравнить объекты разных типов. Оператор еще на этапе компиляции подскажет, что такое сравнение не имеет смысла. equals же будет принимать аргумент под типом Object, и всегда возвращать false уже в рантайме.

3. == быстрее. Скорее всего разница в производительности будет незаметной, но тем не менее, оператор не требует лишнего вызова метода.

#Язык
Как остановить поток?

В Java поток представлен классом Thread. В нём есть метод stop(), но пользоваться им нельзя, метод помечен как deprecated. Такая жесткая остановка моментально возвращает все захваченные потоком мониторы, и защищенные ими данные могут оказаться в неконсистентном состоянии.

Разработчики рекомендуют вместо этого использовать флаг, который будет показывать о намерении остановить поток. Флаг выставляется извне потока, а внутри проверяется в подходящий момент. Если нужно остановиться, поток просто выходит из метода run(). В качестве такого флага подойдет переменная типа AtomicBoolean.

Когда в потоке используются блокирующие операции, обычно для определенного типа операции существует свой способ её прервать. Например, можно закрыть сокет, на котором поток ожидает. Для большинства блокирующих операций сработает метод Thread.interrupt(). С его помощью можно прервать Object.wait() и операции из NIO.

Останется только правильно обработать такое прерывание. Прерванный wait() выбросит InterruptedException, Selector.select() вернет результат. Чтобы отличить осознанное прерывание с целью завершить тред от какого-либо другого, его обработку всё ещё необходимо снабдить проверкой флага.

#Многопоточность
Какой выбрать тип для даты/времени?

В пакете java.util расположены старые классы стандартной библиотеки Java: Date (дата+время), Calendar (конвертация и манипуляция), TimeZone (смещение часового пояса). Эти классы обладали рядом известных проблем. Экземпляры были изменяемыми, что делало их потоко-небезопасными. Работа с датами через календарь была неудобной, не было нормальной поддержки часовых поясов и интернационализации.

Постепенно стандартом де-факто стала сторонняя библиотека Joda-Time. Её разработчики решили все названные выше проблемы.

В Java 8 был добавлен пакет java.time, который взял решения из Joda-Time в стандарт, создатель библиотеки участвовал в разработке. Ключевые классы пакета:
LocalDate, LocalTime и LocalDateTime – локальные для пользователя дата/время.
ZonedDateTime – дата/время в определенной часовой зоне.
Period и Duration – периоды дат и времени соответственно.

Отдельно существуют классы Date и Time пакета java.sql. Это представление даты и времени для обмена данными через JDBC. Не стоит пользоваться ими вне уровня доступа к данным, хотя бы потому, что это классы-наследники старого java.util.Date.

Таким образом, в проектах на Java 8+ нужно использовать современные java.time.*, для более старых – подключать Joda-Time.

#Классы
Из каких основных сущностей состоит Spring-приложение? (1/2)

Bean – объект бизнес-логики в терминологии Spring Framework.

BeanDefinition – описание того, как создавать бин. Объект хранит его тип, метаинформацию, набор параметров для конструктора.

BeanFactory – главная точка входа в DI-контейнер. Хранит BeanDefinition-ы, умеет создавать по ним экземпляры бинов, или выдавать существующие, в зависимости от скоупа.

BeanPostProcessor – донастраивает только что созданные бины, перед тем как положить их в контейнер. Его методы уже упоминались в публикации про жизненный цикл. Типичное место, чтобы оборачивать бины в прокси. Также с помощью такого пост-процессора внедряются @Autowired-зависимости. Пост-процессоры бинов живут внутри экземпляра BeanFactory.

BeanFactoryPostProcessor – тоже пост-обработчик, но для определений бинов (BeanDefinition). Обычно используется для модификации параметров или класса, из которых будут строиться бины.

Для создания определений бинов в основном применяются классы и интерфейсы *BeanDefinitionReader. Некоторые из них вызываются прямо из контекста приложения, другие реализуют BeanFactoryPostProcessor. Один такой пост-процессор, например, отвечает за добавление определений бинов по аннотациям @Component и @Configuration.

#Spring
Из каких основных сущностей состоит Spring-приложение? (2/2)

Реализация интерфейса ApplicationContext – основное хранилище конфигурации Spring-приложения (или его части). Контекст неизменяем, но может быть целиком перезагружен. Xml-файл конфигурации на старте приложения превращается в объект *XmlApplicationContext. Для конфигурации на аннотациях создастся AnnotationConfigApplicationContext. Контекст выполняет четыре разных обязанности:

1. DI-контейнер. ApplicationContext функционирует как специальная реализация BeanFactory. Он также производит и хранит бины, но, в отличие от обычных фабрик, контексты в приложении составляют иерархию. Определения бинов из дочерних контекстов перекрывают родительские.

2. Загрузка ресурсов. Под интерфейсом ResourceLoader контекст занимается загрузкой в память приложения файлов, как из classpath, так и из остальной файловой системы.

3. Публикация событий приложения. Контекст распространяет в приложении «события» – наследники ApplicationEvent. Любой бин, которому нужно получать уведомления об этих событиях, просто реализует интерфейс ApplicationListener. Таким образом реализуется паттерн наблюдатель.

4. Интернационализация. По коду, набору аргументов и локали, через интерфейс контекста MessageSource можно получать локализованные текстовые сообщения для пользователей.

#Spring
Как прочитать InputStream в строку?

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

В общем виде все решения выглядят так. Заводится буфер – массив символов. Поток направляется в этот буфер. По заполнению данные из массива присоединяются в хвост строки-результата.

Простой способ – использовать трюк со сканером. Вообще класс Scanner читает из потока подстроки, разделенные указанным символом. Когда нужно прочитать всю строку сразу, в качестве разделителя устанавливается "\\A" – спецсимвол «начало строки». Это решение просто в реализации, но имеет проблемы. Размер внутреннего буфера фиксирован (1024 символа), а логика поиска разделителя плохо влияет на производительность.

Хорошее решение для продакшна – читать в собственный массив-буфер непосредственно методом InputStream.read, либо обернув поток в InputStreamReader. Данные из буфера затем переправляются в строку через StringBuilder или ByteArrayOutputStream. За готовой реализацией можно обратиться в библиотеки Apache Commons IO и Google Guava. Полный код реализации и сравнение производительности описаны на stackoverflow.

На интервью этот вопрос часто возникает как часть практической задачи, для консольного ввода-вывода. Поэтому, если вы идете на собеседование со своим компьютером, и неуверенно владеете классами работы с потоками, стоит заранее подготовить шпаргалку с кодом.

#Строки
Как написать на Java UDP-сервер?

Естественно, сначала необходимо разобраться, что такое UDP. Упрощая, User Datagram Protocol – это альтернатива TCP, когда информацию нужно слать быстро, много, и при этом допустимы потери и дублирование данных. Типичные примеры использования – потоковое видео и аудио, интернет-телефония, торренты.

В Java данные, которые планируется отправить клиентам по протоколу UDP, упаковываются в объект класса DatagramPacket. В виде массива байтов их передают в конструктор.

Для отправки и получения информации используется DatagramSocket. Он похож на ServerSocket, который применяют для создания TCP-сервера. Для приёма сообщений используется блокирующий метод receive, для отправки – send. Примечательно, что оба метода принимают DatagramPacket параметром. В случае receive его байтовый массив заполняется пришедшими данными.

Для реализации клиентской стороны используется тот же самый DatagramSocket. Просто он создается несвязанным (unbound) – в его конструкторе не указывается порт. Адрес и порт, на которые нужно отправить сообщение, устанавливаются через конструктор DatagramPacket.

В Java NIO доступна версия UDP-сокета в виде канала – DatagramChannel.

#Сеть
Как сгенерировать уникальный идентификатор?

UUID (universally unique identifier) – стандарт, описывающий способ создания правильных уникальных идентификаторов. Значения генерируются на основании таких источников информации, как системное время и MAC-адрес, за счет чего они остаются с достаточной вероятностью уникальными, даже будучи сгенерированными независимо. Можно с разных машин добавлять в базу данных записи с UUID-идентификаторами, и не бояться конфликта.

UUID бывает пяти разных версий, версия определяет способ создания. Формат остается одинаковым: это строковое шестнадцатеричное представление 128-битного целого числа (два long-а), разделенное дефисами на группы фиксированного размера:

25b32eaa-3017-4ad7-9224-383f6bfa5212

В Java уникальный идентификатор представляется иммутабельным классом UUID из пакета java.util. В нём нет сложной логики, только getter-ы для описанных в стандарте составных частей, конструктор и статические фабричные методы.

Единственный конструктор позволяет создать экземпляр по двум указанным половинам значения (старшие и младшие 64 бита в виде long параметров). nameUUIDFromBytes строит из заданного массива байтов UUID версии 3. randomUUID генерирует случайный уникальный идентификатор версии 4, с применением SecureRandom.

#Классы
Отличается ли List<?> от List<? extends Object>?

Все классы без исключения наследуются от Object. Поэтому неограниченный wildcard <?> всегда подразумевает его в качестве верхней границы. Оба этих типа в рантайме сотрутся в List<Object>, функциональных отличий нет.

Не смотря на одинаковое поведение, существует одно синтаксическое различие. Неограниченный дженерик – reifiable тип. Это значит, что он представлен в рантайме. Такой тип можно использовать в операторе instanceof, тогда как синтаксическая конструкция x instanceof List<? extends Object> приведет к ошибке компиляции.

Тип List без параметра имеет больше отличий, мы уже говорили о них ранее, в публикациях про raw types и проблему heap pollution.

#Дженерики
Как измерить производительность метода?

Если вы собираетесь оптимизировать код, или же выбрать лучшее с точки зрения производительности готовое решение, выбор необходимо подтвердить тестом производительности до и после. Такие тесты называются benchmark.

Первый вариант который приходит в голову – измерить время до, выполнить метод, посмотреть сколько времени прошло. У такого подхода есть ряд нюансов. Каждый последующий замер может случайным образом сильно отличаться от предыдущего. В реальном приложении результат будет совсем отличным от тестового, потому что виртуальная машина будет «прогретой»: заполнятся различные системные кэши, произойдут JIT-оптимизации горячего кода, память наполнится мусором.

Наивный подход может показать приблизительную производительность долгих операций: ввода-вывода, системных вызовов, или пользовательских сценариев на развернутом тестовом стенде. При тестировании кусков Java кода (в микробенчмарках) время выполнения очень мало, поэтому упомянутые нюансы вносят значительный вклад в результат.

Самое правильное решение – использовать готовый инструмент из JDK, JMH. Для этого его библиотеки добавляются в зависимости, а тестовый метод помечается аннотацией @Benchmark. Тесты можно запускать в виде исполняемого jar-файла, либо вызовом специальных программных методов библиотеки. Итоги тестирования будут выведены в виде таблицы в консоль.

Для дальнейшего погружения в особенности использования JMH рекомендуется к просмотру доклад автора инструмента.

#Инструменты
В чём преимущества и недостатки Spring Boot?

Основные сущности фреймворка Spring Boot – это стартеры. Зависимости с названиями вида spring-boot-starter-xxx выполняют две основных задачи. Во-первых, они добавляют набор типичных сторонних библиотек-зависимостей; во-вторых, регистрируют типичные бины и их конфигурации. Кроме того, со Spring Boot в проекте появляется ряд таких полезностей, как embedded-сервер, конфигурация web-приложения без web.xml, метрики, properties вынесенные из кода во внешние файлы.

Например, spring-boot-starter-data-jpa даст вам готовый комплект всего необходимого для использования JPA: драйвер, совместимую с ним версию Hibernate, библиотеки Persistence API и Spring Data. В контексте приложения появятся все нужные для JPA репозиториев бины.

Таким образом Spring Boot ускоряет и упрощает разработку, дает возможность избавиться от boilerplate-кода в проекте и сфокусироваться на бизнес-задачах. Это бывает особенно важно в микросервисной архитектуре, когда создается большое количество приложений.

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

#Spring
Какие бывают проблемы с арифметикой в Java?

Переполнения.
Числа примитивных типов в Java хранятся в дискретной оперативной памяти компьютера и занимают фиксированный объем. Из этого вытекает ограничение диапазона возможных значений. Когда результат арифметической операции выпадает из диапазона, значение идет по кругу – максимальное становится минимальным, либо наоборот. Такая ситуация называется переполнение (underflow/overflow).

Решение: если опасность переполнения значима, помогут методы с суффиксом *Exact из классе Math. Это безопасные аналоги арифметических операций, которые бросают исключение в случае переполнения.

Платформо-зависимые округления.
По умолчанию JVM производит арифметические вычисления насколько это возможно точно. Пределы точности могут зависеть от аппаратного обеспечения. Это неприемлемо для программ, к которым предъявляют строгие требования переносимости, когда результат вычислений должен быть одним и тем же на любом железе.

Решение: модификатор strictfp в объявлении класса или метода приводит точность вычислений к единой спецификации IEEE 754. За это может ухудшиться производительность и уменьшиться точность значений.

ArithmeticException.
Операторы могут выбрасывать исключение. Это происходит, например, при делении на ноль. Это же исключение бросают безопасные методы из Math.

Решение: неожиданное исключение обычно указывает на логическую ошибку. Лучший способ предотвратить логические ошибки – покрыть код Unit-тестами.

#Язык