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
От чего случается UnsupportedClassVersionError?

Одна из отличительных особенностей языка Java – обратная совместимость версий. Это значит что код, который был написан на Java 5, будет прекрасно работать и на Java 11.

К сожалению, это не работает в обратную сторону. Невозможно поддержать фичи, которые еще не были придуманы.

Скомпилированный .class файл хранит в себе версию байткода x.y – версию формата файла. Минорная часть y версии отражает совместимые технические изменения в рамках той же самой версии Java. Мажорная часть x увеличивается в каждом следующем релизе. Версии формата для разных релизов:

Java SE 14 = 58
Java SE 13 = 57
Java SE 12 = 56
Java SE 11 = 55
Java SE 10 = 54
Java SE 9 = 53
Java SE 8 = 52
Java SE 7 = 51
Java SE 6.0 = 50
Java SE 5.0 = 49
JDK 1.4 = 48
JDK 1.3 = 47
JDK 1.2 = 46
JDK 1.1 = 45

Ошибка UnsupportedClassVersionError возникает, когда JVM пытается загрузить слишком новую версию класса. Чтобы получить байткод, совместимый со старыми JVM, при компиляции необходимо передать версию в параметре --release (до Java 8 в параметрах -source и -target).

#JVM
Приведите примеры использования fork/join framework

Как следует из названий связанных классов, ForkJoinPool используется для рекурсивных задач. Это такие задачи, которые можно делить на порции, подзадачи. Отделение подзадачи – это операция fork, финальная агрегация результатов подзадач – join.

Реализация fork/join для самых популярных общих случаев уже есть в стандартной библиотеке, работать непосредственно с классом ForkJoinPool не потребуется. Метод parallelSetAll из класса Arrays применяет fork/join для генерации элементов массива; parallelPrefix для модификации; parallelSort для сортировки.

Фреймворк неявно работает и в параллельных стримах. В этом случае логику fork определяет его сплитератор, а join выполняют потоковые операции. Классический пример:

Arrays.stream(new int[]{1, 2, 3, 4}).parallel().sum();


Существуют целые категории частных задач, решения которых хорошо параллелизуются: векторные операции, работа с графами, поиск данных. Для специфичных задач придется реализовывать собственные RecursiveTask, RecursiveAction, или Spliterator.

#Многопоточность
Что такое Hibernate?

Hibernate – популярная библиотека, которая реализует технологию JPA, обеспечивает объектно-реляционный маппинг (ORM).

По задумке она избавляет разработчика от проблем хранения объектно-ориентированных (инкапсуляция-полиморфизм-наследование) данных в реляционных таблицах.

Среди особенностей можно выделить такие, как HQL (объектно-ориентированный SQL), автоматическую генерацию таблиц, два уровня кэширования, независимость от конкретной базы данных.

На сегодняшний день Hibernate считается многими разработчиками отчасти устаревшей. На больших проектах она требует объемный набор XML-конфигураций, на практике зачастую приходится спускаться на уровень нативного SQL конкретной базы, а маппинг порой ведет себя неочевидно. Для более близкого знакомства с проблемами этой библиотеки рекомендуется доклад Николая Алименкова.

Несмотря на свои проблемы, Hibernate остается лучшей альтернативой самописным DAO. Её использует под капотом Spring Data.

Список возможных альтернатив можно найти на википедии.

#SQL
От чего может случиться NoSuchMethodError?

Как очевидно из названия, программа пытается вызвать метод, которого не существует. В большинстве случаев это приводит к ошибке еще на этапе компиляции. Но в некоторых ситуациях о проблеме становится известно только во время выполнения, и тогда выбрасывается это исключение.

Вариантов таких ситуаций два. Либо проект был изменен и частично перекомпилирован; либо программа несовместима с внешней зависимостью: например неправильная версия jar-библиотеки в classpath.

Исключение NoSuchMethodError наследуется от LinkageError. Все такие ошибки – признаки несовместимых изменений после компиляции класса.

Не следует путать эту ошибку с NoSuchMethodException – исключением, которое случается при попытке вызвать несуществующий метод с помощью Reflection API. Важное отличие в том, что это не Error, его можно ловить и обрабатывать.

#JVM
Как реализовать двусторонний обмен данными между потоками?

Вопрос, который зачастую дается в виде практической задачи. Конечно, результата можно добиться разными способами: парой атомарных переменных, критическими секциями, потокобезопасными коллекциями. Но полезно знать, что специально для этого случая в стандартной библиотеке java.util.concurrent есть простой класс Exchanger.

Класс содержит единственный метод V exchange(V x). Один поток передает в него данные, и встает в ожидание. Ожидание завершается, когда второй поток также приходит в метод exchange со своей порцией информации. В качестве результата вызова потоки получают данные друг друга.

На основе класса Exchanger удобно создавать пайплайны обработки данных. Первый поток выполняет свою часть обработки, и складывает результаты в буфер. В качестве буфера может работать любой многоразовый объект-контейнер. Когда он заполняется, следующий поток обменивает его на второй, пустой буфер. Таким образом два буфера используются поочередно, не выделяется лишний раз память и не нагружается GC. Далее из попарно обменивающихся буферами потоков может строиться длинная многопоточная цепочка обработки.

#Многопоточность
В чём разница между интерпретатором, AOT и JIT-компилятором?

Интерпретация – простое последовательное воспроизведение кода программы, команда за командой.

AOT-компиляция (ahead-of-time, статическая) – процесс превращения текста на языке программирования в нативный код на машинном языке. Так работают языки вроде C++. В современных JDK можно получить настоящий ahead-of-time скомпилированный машинный код с помощью утилиты jaotc.

JIT-компиляция (just-in-time, динамическая) – «умная» интерпретация. Среда выполнения анализирует исполняемый код, оптимизируя часто вызываемые участки. Таким способом программа работает значительно быстрее, и сохраняет при этом преимущества платформо-независимости оригинального кода. Именно с JIT-компиляцией связана необходимость «прогрева» программ перед тестированием производительности.

Эти термины относятся не только к JVM, но и ко множеству других языков программирования. Конкретно в Java байткод – интерпретируемый. Но в JVM по умолчанию работает JIT-компилятор. А процесс компиляции Java-кода в байткод можно назвать AOT-компиляцией.

Для взаимодействия с JIT-компилятором из кода в JDK поставляется класс java.lang.Compiler. Его методом disable() можно отключить JIT и перевести программу в режим простой интерпретации. Сейчас этот класс объявлен устаревшим и готовится к удалению.

Более красивый способ влиять на компилятор – передавать его настройки параметрами JVM. Параметр -Djava.compiler=NONE также переключит программу с JIT на интерпретатор. В теории, через этот же параметр можно подключить другой JIT-компилятор, альтернативный встроенному в JVM.

#JVM
TestNG или JUnit – что выбрать?

Ответ на такого рода вопросы всегда зависит от дополнительных деталей контекста. Не просто так TestNG и JUnit – два самых популярных фреймворка для unit-тестирования, у каждого есть свои плюсы.

JUnit – золотой стандарт. Это библиотека из семейства xUnit, которое во многом сформировало unit-тестирование таким, каким мы знаем его сегодня. И до сих пор, оставаясь самой популярной библиотекой для тестирования, она продолжает активно развиваться.

Фреймворк TestNG был вдохновлен JUnit, но добавляет небольшой набор дополнительных фич. Некоторые из указанных на сайте возможностей на самом деле доступны и в JUnit: например data-driven тесты, или параллельное выполнение. Подробное сравнение JUnit 4 и TestNG доступно в статье от mkyong.

Стоит отметить, что современный JUnit 5 имеет достаточно заметные отличия от JUnit 4, их можно рассматривать как разные фреймворки. Подробнее об отличиях читайте на хабре.

#Инструменты
Чем IllegalArgumentException лучше чем NullPointerException?

Речь здесь идет о выборе подхода к обработке ошибки, когда пользователь передал в non-nullable параметр значение null.

Технически, оба этих исключения unchecked, оба из стандартной библиотеки, и особой разницы нет.

Однако семантически эти исключения отличаются. NullPointerException говорит пользователю о попытке обратиться к членам класса через null-ссылку. Это лишь техническое описание, без информации для пользователя о логике программы. IllegalArgumentException, напротив, явно говорит о недопустимом значении аргумента – это понятная для пользователя информация.

Семантическая разница иногда проявляется и технически. Например, в обработчиках исключений некоторых фреймворков именно IllegalArgumentException превращается в HTTP-ответ с кодом 400 Bad Request, в то время как NPE остается общим кодом «неизвестной ошибки» 500 Internal Server Error.

Кроме того, чтобы выбросить NullPointerException не требуется явного кода обработки null. Остается неизвестным, ожидаемо ли в этом случае исключение, или разработчик попросту забыл добавить корректную обработку.

Явная обработка null и выброс IllegalArgumentException для пользователя кода будет служить документацией, а сообщение об ошибке в аргументе его конструктора внесет еще больше ясности.

#Классы
Что такое phase, goal и lifecycle в Maven? (1/2)

Phase – виртуальные шаги из, которых состоит lifecycle в Maven. Вообще, существует три жизненных цикла:

• Clean – фазы pre-clean, clean, post-clean;
• Default – validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy;
• Site – pre-site, site, post-site, site-deploy.

Goal – это конкретное выполняемое плагином действие. Плагин привязывает свои голы к фазам. Например, когда мы вызываем mvn clean, работу по удалению файлов сборки делает не сама фаза clean, а привязанная к ней цель clean:clean из встроенного maven-clean-plugin.

#Инструменты
Что такое phase, goal и lifecycle в Maven? (2/2)

Когда аргументом команды mvn передается фаза, кроме нее самой выполнятся все предшествующие ей в том же лайфсайкле. Вместо фазы возможно явно передать цель (mvn clean:clean вместо mvn clean), но тогда никакие другие цели вызваны не будут (в данном примере цели фазы pre-clean).

Фазы всегда выполняются в том порядке, в котором они следуют в жизненном цикле. Если к одной фазе привязано несколько целей, они отработают в порядке объявления в pom.xml.

Некоторые плагины могут предоставлять цели, не привязанные ни к каким фазам. Их можно вызвать только явно.

Если в команду mvn передается несколько фаз/целей, они выполнятся последовательно. Каждая цель в процессе выполняется только однажды.

Так, вызов mvn test package – то же самое что mvn package, потому что в первом случае все цели из фазы test (и предыдущих) уже будут исполнены и пропущены в package. mvn clean install так заменить не получится, потому что это фазы из разных жизненных циклов.

#Инструменты
Отличаются ли сокращенные и обычные операторы?

Java предлагает программисту сокращенную запись для применения операции с сохранением ответа в операнд. Это например +=, &=, и другие. Их правильное название – операторы сложного присваивания (compound assignment). Сокращенные версии есть для всех арифметических и битовых операторов.

У таких сокращений есть одно неочевидное отличие от полных версий. Если прочитать спецификацию, там сказано, что x += y – это на самом деле сокращение от x = (XType)(x + y). То есть, кроме самой операции происходит приведение результата к типу левого операнда.

Незнание этой особенности может привести к ошибочно успешной компиляции, и неожиданным результатам работы кода (как в примере на изображении).

#Язык
4 и 5 декабря пройдет бесплатная Java конференция jLove – мероприятие будет интересно Java-программистам любого уровня от новичков до экспертов.

Конференция собрала лучших звезд Java мира из компаний Red Hat, IBM, Microsoft, jFrog, JetBrains, Oracle, Confluent, VMware и многих других.

Конференция пройдет онлайн, и мы хотим пригласить всех желающих принять участие – регистрация бесплатная!
Для полного погружения в конференцию будет пространство для общения со спикерами после выступлений, нетворкинг с участниками в Spatial Chat, общение со спонсорами, афтепати, и даже возможность выиграть книги по Reactive Spring или годовую подписку от JetBrains.

Все, что вам нужно знать на сайте: https://jlove.konfy.care.
Увидимся 4 и 5 декабря!
Сгенерируйте случайное число в интервале

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

Самые стандартные классы-генераторы случайных чисел создают равномерно распределенные значения. Любое число возникает с одинаковой вероятностью – это ожидаемое поведение для большинства задач. Например, метод Random.nextInt(1) будет генерировать примерно одинаковое количество значений 0 и 1.

Однако, программист легко может "испортить" равномерность значений последующими операциями. Возьмем вместо предыдущего примера Random.nextInt(2)%2. Такая конструкция тоже будет возвращать 0 или 1. Однако, третье возможное значение из генератора, 2, будет тоже превращено в 0. Значит, ответ 0 будет возникать в два раза чаще чем 1.

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

Если задача подразумевает более плотную работу с распределениями, стоит воспользоваться специализированной библиотекой вроде Apache Commons Math.

#Классы
Лишает ли var строгой типизации?

Спасибо подписчикам, которые недавно подняли этот вопрос в чате-обсуждении.

Ключевое слово var появилось в Java 10. Указание var вместо типа локальной переменной применяет к ней механизм вывода типов (type inference). Тип будет вычислен на этапе компиляции из того, чем переменная инициализируется.

Отсюда несколько выводов. Во-первых, нельзя использовать var в полях класса, параметрах метода, и где-либо еще кроме локальных переменных. Во-вторых, обязана быть инициализация с понятным типом – варианты var x; или var x = null; не скомпилируются.

И главное следствие – к концу компиляции у таких переменных фиксированный и известный тип, который не может быть изменен позднее. А это и есть определение строгой типизации.

Ответ: нет, выводимый тип – строгий. Более того, типизация остается статической.

Главное упущение – в инициализации разрешено использовать diamond operator. В обычных обстоятельствах в нём выведется правильный generic-тип, но в случае var информации недостаточно, и типом-параметром будет Object.

#Язык
Когда стоит выбрать char[] вместо String?

Первая, очевидная причина – оптимизация. Если вам заранее известен размер строки, и он фиксирован, может быть полезно выбрать массив. Если программа работает с неизменяемыми подстроками, удобно представить их в виде offset-ов общего массива (как это было сделано раньше в самом String).

Следует помнить, что оптимизировать нужно осознанно и своевременно. JVM тоже прикладывает усилия по оптимизации строк (вроде интернирования), которые могут оказаться эффективнее ваших.

Вторая, менее очевидная причина – безопасность. Строки в Java иммутабельны. Это значит, что когда вы сохраняете пароль в объекте типа String, физически уничтожить его из памяти может только сборщик мусора.

Существует способ алгоритмической атаки на систему, когда хакер своими действиями вызывает переполнение памяти, и конфиденциальная информация попадает в heap dump.

Если пароль хранится в массиве, программист может самостоятельно «занулить» значение после использования.

#Классы
#Безопасность
Опишите синтаксис javadoc-комментария (1/2)

Javadoc-комментарии к классам и их членам заключаются между /** и */. С точки зрения синтаксиса Java это обычные многострочные комментарии, но вторая * позволяет различным инструментам воспринимать их как документацию API. Изначально для этого использовалась стандартная утилита javadoc, которая генерировала HTML-документацию, сейчас джавадок активно используется прямо в IDE.

До Java 1.4 каждая строка комментария обязана была начинаться со *. Сейчас это требование необязательное, но следовать ему всё ещё принято.

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

В javadoc разрешено использовать HTML-теги. Фрагменты кода рекомендуется обрамлять тегом <code>, для списка с буллетами применяется <ul>, параграфы отделяются <p>. В документации библиотеки Reactor активно используются <img> с диаграммами.

#Инструменты
Опишите синтаксис javadoc-комментария (2/2)

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

Один тег можно использовать в блоке описания – @link. Он не обязан быть на новой строке, обрамляется фигурными скобками, и при рендеринге превращается в <a> со ссылкой на другую страницу документации.

Среди всех тегов обязательными считаются только @param для каждого параметра метода, и @return для не-void методов. Они применимы только для методов. А теги @author и @version наоборот, используются только в документации классов. Остальные блочные теги можно использовать везде:

@deprecated
@exception (то же что @throws)
@see
@since
@serial (то же что @serialField или @serialData)

Теги @author, @param, @throws и @see могут входить в один комментарий в нескольких экземплярах.

#Инструменты
Под каким типом хранить период времени?

В стандартной библиотеке современных версий Java для этих целей есть два класса:

Period – календарный период. Количество дней, месяцев и лет. Одним днем здесь считается день в терминах ZonedDateTime.

Duration – длительность времени. Количество наносекунд, секунд, минут, часов, и тоже дней. Здесь один день – ровно 24 часа.

Оба класса реализуют общий интерфейс TemporalAmount – период времени вообще. Оба иммутабельны, и как следствие, потокобезопасны. Любая модифицирующая операция вроде plusX() возвращает новый экземпляр с измененным значением.

Экземпляры обоих классов могут быть созданы из значений отдельных компонентов, из двух моментов времени методом between, или из строки. Строковое представление Duration: "P2DT3H4M", Period: "P1Y2M3D".

До Java 8 основным способом хранения периода были числовые примитивы. В этом подходе есть много недостатков, среди которых в первую очередь неограниченность значений и ненаглядность. Чтобы в Java 8+ получить период числом, используется метод between() нужного элемента енама ChronoUnit.

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

Раньше мы уже рассматривали различия скоупов singleton и prototype в Spring Framework. Допустим ситуацию, когда в singleton-компонент внедряется зависимость со скоупом prototype – когда будет создан её объект?

Если просто добавить к определению бина аннотацию @Scope(SCOPE_PROTOTYPE), и использовать этот бин в синглтоне через аннотацию @Autowired – будет создан только один объект. Потому что синглтон создается только однажды, и обращение к прототипу случится тоже однажды при его создании (при внедрении зависимости).

Примитивный способ получать новый объект при каждом обращении – отказаться от @Autowired, и доставать его из контекста вручную. Для этого нужно вызывать context.getBean(MyPrototype.class).

Воспользоваться автоматическим внедрением зависимостей можно через внедрение метода (паттерн «Команда»). Автовайрится не сам объект, а производящий его метод.

Более красивый декларативный способ – правильно настроить определение бина. В аннотации @Scope кроме самого scopeName доступен второй параметр – proxyMode. По умолчанию его значение NO – прокси не создается. Но если указать INTERFACES или TARGET_CLASS, то под @Autowired будет внедряться не сам объект, а сгенерированный фреймворком прокси. И когда проксируемый бин имеет скоуп prototype, то объект внутри прокси будет пересоздаваться при каждом обращении.

Лучший способ разобраться со скоупами – прочитать официальный гайд с иллюстрациями, и поэкспериментировать на практике. Для начала попробуйте пример с изображения ниже.

#Spring
Как сделать код с арифметикой кроссплатформенным?

Степень платформонезависимости Java явно определяется её спецификацией. Согласно спецификации, когда мы работаем с числами с плавающей точкой, на продакшне результат арифметических вычислений может отличаться от локального. Это не всегда проблема, но об этом стоит помнить.

Во-первых, разберемся насколько сильно результатам позволено расходиться. В документации разных методов из Math эта величина выражается в единицах ulp (unit in the last place). Это то, насколько увеличится число, если его битовое выражение увеличить на 1 бит. Для разных чисел значение ulp будет различаться. Получить его можно методом Math.ulp(x).

Если объявленной точности не хватает, на помощь придет класс StrictMath. В нём находится примерно такой же набор инструментов как в Math, но с повторяемыми результатами, которые можно получить стандартизованными алгоритмами на C.

Для обычных языковых выражений вроде инициализаторов и операторов введено понятие свойства FP-strict. Выражения с этим свойством будут кроссплатформенным. Чтобы добавить свойство всем операторам, на методе используется модификатор strictfp (о котором мы уже упоминали).

#Язык
Что такое Java-модули?

Java Platform Module System (JPMS) – система модулей, появившаяся в Java версии 9. Она разрабатывалась под названием Project Jigsaw, и ожидалась еще с Java 7.

Модуль – группа связанных пакетов и ресурсов. Механизм модулей определяет видимость содержимого одних модулей для других. Скрытые модули недоступны ни для обычного использования в коде, ни даже для рефлекшна: Class.forName() выбросит ClassNotFoundException.

Вдобавок, модули инкапсулируют собственные ресурсы – конфиги и медиафайлы больше не хранятся в общей куче.

Модуль определяется файлом объявления module-info.java. Поговорим о его содержимом и о применении модулей вообще в будущих постах постах.

JVM может работать относительно модулей в трех режимах:

• Legacy mode – применяется, когда код компилируется в режиме совместимости с версиями Java 8 и меньше. Весь код работает как безымянный модуль, флаги для настройки модулярности не работают.

• Single module mode – когда не используется флаг --module-source-path. Структура проекта остается старой, файл module-info.java лежит в директории /src. Поэтому иметь несколько модулей в одном проекте не получится.

• Multi-module mode – каждый модуль находится в собственной директории. Общая корневая директория передается в --module-source-path. Полноценное использование JPMS.

#Модули