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

Собеседование Backend-Java-разработчика: вопросы и где искать ответы. Часть 1

Время на прочтение34 мин
Количество просмотров92K

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


Точно установить сложность всех вопросов не берусь — на разном уровне их потребуется раскрыть с различной степенью подробности. Я написал ответы где-то на плюс-минус middle, щедро приправив ссылками для дальнейших изысканий. На самые популярные вопросы сразу перенаправляю в источники с готовыми ответами. Заодно посмотрим по ссылкам в статье, насколько Хабр может помочь в подготовке к собесам.


Текста получилось много, поэтому пришлось разбить на две части. В первой поговорим про Java и Spring, а обо всём остальном — во второй. Вторая часть тут


TL;DR

GitHub-репозиторий с полной шпаргалкой тут, а Хабр всё ещё торт.


Вопросы


Java


1. Вопросы про Equals, hashcode и их связь с HashMap.

Опишите Контракт. Далее разговор переходит к устройству HashMap. Как устроена внутри? А происходит в случае возникновения коллизии? Назовите алгоритмические сложности поиска, чтения, удаления из элемента мапы. А что если ключ — это массив байтов? А может быть так, что мы положим элемент в мапу, а потом не найдем? Обсасывают бедную мапу со всех сторон. Самая популярная тема для обсуждения. Спрашивают все. Абсолютно все.


Контракт equals и hashcode:


  1. Для одного и того же объекта хэшкоды одинаковые.
  2. Если объекты равны по equals, то и хэшкоды одинаковые.
  3. Если же хэшкоды равны, то объекты могут быть не равны по equals (коллизия).
  4. Если хэшкоды разные, то и объекты разные.

В статье на Хабре это подробно разобрано, если кому-то покажется мало.


Про HashMap и вопросы по ним есть несколько отличных статей на Хабре (в картинках, с дополнениями из Java 8, а тут вопросы-ответы про коллекциям). Кроме того, можно посмотреть исходный код в вашей любимой IDE. Можете сделать себе конспект и повесить на стену :)


2. Вопросы про списки: какие есть, алгоритмическая сложность, какой брать для вставки в середину, в конец, в огурец.

По сути это вопрос про ArrayList vs LinkedList. Опять же, заезженная пластинка, разобранная на Хабре — вопросы-ответы про коллекциям, ArrayList в картинках, LinkedList в картинках, Что «под капотом» у LinkedList. Посмотреть исходники тоже полезно. Например, можно понтануться тем, что вставка в середину в ArrayList выполняется с помощью нативно реализованной функции System.arraycopy, поэтому не всё так плохо, как могло бы быть в этом случае.


3. Перечислите методы класса `Object`.

Этот вопрос далее перетекает либо в обсуждение HashMap, либо в основы многопоточного программирования на Java.


Чтобы вы вдруг внезапно не забыли каких-то методов (как это сделал я :D), привожу вам список и ссылку на JavaDoc:


  • clone
  • equals
  • finalize (Deprecated)
  • getClass
  • hashCode
  • toString
  • notify
  • notifyAll
  • wait

Также можно почитать, что там вообще есть в исходниках Object в статье на Хабре.


4. Расскажите про методы `wait`, `notify`, `notifyAll` и ключевое слово `synchronized`.

В принципе, статьи на Baeldung должно хватить. Лучше, конечно, пописать код с использованием wait, notify, notifyAll и synchronized руками. Также можно почитать официальный туториал от Oracle по Concurrency в Java.
Но если хотите пойти глубже, то хаброписатели опять спешат на помощь — тут. А также Java Language Specification, раздел 17.1 и 17.2.


5. JMM. Зачем нужно volatile. Популярный вопрос.

Не знаю как у вас, но у меня при упоминании JMM молниеносно всплывает в голове Алексей Шипилёв и его доклады — раз, два, три. Если вы больше чтец, чем смотрец, то Алексея можно и почитать — ать, два.


Кроме того, абсолютно не будет лишним посмотреть доклад Романа Елизарова по теоретическому минимуму JMM.


Если совсем нет времени, то можно пробежаться по небольшой статейке по JMM. Если есть время и интерес, тогда углубляемся в тему через статью на Хабре. А ещё на Хабре есть неплохой перевод статьи "Многопоточность. Java-модель памяти": часть 1 и часть 2.


Несомненным источником истины является Java Language Specification, раздел 17.4.


Также ответ на этот вопрос можно прочитать на itsobes.ru.
Не лишним будет ознакомиться с вопросом на JVM-уровне в статье How ‘volatile’ works on JVM level? на Medium.


6. Сборка мусора. Как работает? Какие сборщики знаете? Какие есть области памяти в JVM? Что будет с двумя или более объектами, которые ссылаются только друг на друга, но больше не на кого и никому не нужны - как с ними поступит сборщик и как именно это будет делать?

Память в Java делится на Stack и Heap.


Stack — это область памяти, доступ к которой организован в порядке LIFO. Сюда помещается frame — локальные переменные и параметры вызываемого метода. Здесь можно сразу уточнить, что примитивы хранятся на стеке, а вот у объектов тут хранится только ссылка, а сами объекты в Heap. НО, благодаря Escape Analysis и скаляризации из Java 6, объекты, которые являются исключительно локальными и не возвращаются за пределы выполняемого метода, также сохраняются в стеке. Про Escape Analysis и скаляризацию есть доклад (видео или текст) Руслана Черемина, или ещё тут.


Frame создаётся и кладётся на Stack при вызове метода. Frame уничтожается, когда завершается его вызов метода, как в случае нормального завершения, так и в результате выброса неперехваченного исключения. У каждого потока есть свой Stack и он имеет ограниченный размер. Подробности можно посмотреть в JVM Specification.


Теперь про Heap и сборку мусора. Тут большинство просто хочет услышать то, что написано в одном из сообщений telegram-канала Senior's Blog. Процитирую основную часть здесь:


Heap делится на два поколения:

  1. Young Generation
    1. Eden
    2. Survivor 0 и Survivor 1

  2. Old Generation
    1. Tenured



Young разделен на три части: Eden, Survivor 0 и Survivor 1. В Eden создаются все новые объекты. Один из Survivor регионов всегда пустой. При полном заполнении региона Eden запускается малая сборка мусора, и все живые объекты из Eden и Survivor перемещаются в пустой Survivor, а Eden и использующийся Survivor полностью очищается. Это делается для уменьшения фрагментации памяти. Объекты, которые несколько раз перемещаются между Survivor, затем помещаются в Tenured.

В случае, когда места для новых объектов не хватает уже в Tenured, в дело вступает полная сборка мусора, работающая с объектами из обоих поколений. При этом старшее поколение не делится на подрегионы по аналогии с младшим, а представляет собой один большой кусок памяти. Поэтому после удаления мертвых объектов из Tenured производится не перенос данных (переносить уже некуда), а их уплотнение, то есть размещение последовательно, без фрагментации. Такой механизм очистки называется Mark-Sweep-Compact по названию его шагов (пометить выжившие объекты, очистить память от мертвых объектов, уплотнить выжившие объекты).

Бывают еще объекты-акселераты, размер которых настолько велик, что создавать их в Eden, а потом таскать за собой по Survivor’ам слишком накладно. В этом случае они размещаются сразу в Tenured.

Младшее поколение занимает одну треть всей кучи, а старшее, соответственно, две трети. При этом каждый регион Survivor занимает одну десятую младшего поколения, то есть Eden занимает восемь десятых.

Существуют следующие реализации GC:



Если совсем кратко, то можно ознакомиться тут и вот тут.
Почитать на Хабре подробнее про сборку мусора в Java можно в серии статей "Дюк, вынеси мусор!" от alyginраз, два, три.
Послушать про работу с памятью и сборщиках мусора можно в выпуске 74 подкаста Podlodka с Алексеем Шипилёвом в гостях. Обязательно загляните в полезные ссылки к выпуску.


Ещё можно вспомнить про:


  • Method Area — область памяти с информацией о классах, включая статические поля. Одна на всю JVM.
  • Program Counter (PC) Register — отдельный на каждый поток регистр для хранения адреса текущей выполняемой инструкции.
  • Run-time Constant Pool — выделяется из Method Area для каждого класса или интерфейса. Грубо говоря, хранит литералы. Подробнее.
  • Native Method Stack — собственно Stack для работы нативных методов.

Дополнительно про gc и саму JVM (ох, бохатая и животрепещущая тема):


  • На богомерзком medium в картинках
  • Перевод статьи Алексея Шипилёва на Хабре — Самодельный сборщик мусора для OpenJDK
  • Отрывок из Java Garbage Collection Handbook про reachability algorithm
  • Статейка на Википедии про Tracing garbage collection
  • Доклад Simone Bordet про ZGC и Shenandoah
  • JVM Anatomy Quarks — серия постов от Алексея Шипилёва про устройство JVM. Это просто клад, за который будут воевать пришельцы на постапокалиптическую Землю, чтобы разгадать, как работает эта чёртва шайтан-виртуал-машина и промышленный код почивших человеков.

7. Что такое Executor и ExecutorService, Thread pool и зачем нужны?

Создавать и убивать потоки — дорого. Давайте создадим N потоков (Thread pool) и будем их переиспользовать. А давайте. Вот тут описано развёрнуто.


Executor (void execute​(Runnable command) — вот и весь интерфейс) и ExecutorService (уже покруче, может запускать Callable и не только) — грубо говоря, интерфейсы выполняторов параллельных задач. А реализуют их различные выполняторы на пулах потоков. Экземпляры готовых конкретных выполняторов можно получить с помощью класса Executors. Если смелый-умелый и зачем-то надо, то можно и самому реализовать, конечно.


Также подробнее можно почитать:



8. Могут ли быть в Java утечки памяти и когда? Как обнаружить причину? Как снять heap-dump?

Могут. Профилировать. Снимать heap-dump, например с помощью jmap, загружать в memory profiler (например в VisualVM)


Подробнее:



9. Что внутри параллельных стримов? На каком пуле работают параллельные стримы и в чем его особенность?

По умолчанию parallel stream использует ForkJoinPool.commonPool размером Runtime.getRuntime().availableProcessors() — 1. Common pool создаётся статически при первом обращении к ForkJoinPool и живёт до System::exit (игнорирует shutdown() или shutdownNow()). Когда некий поток отправляет задачу в common pool, то pool может использовать его же в качестве воркера. Common pool один на всё приложение. Можно запустить stream на отдельном ForkJoinPool — завернуть параллельный stream в Callable и передать на вход методу submit созданного ForkJoinPool. Этот трюк работает благодаря методу fork() из ForkJoinPool (тут подробности).


Сам по себе ForkJoinPool представляет реализацию ExecutorService, выполняющую ForkJoinTask (RecursiveAction и RecursiveTask). Данный pool создан для упрощения распараллеливания рекурсивных задач и утилизации породивших подзадачу потоков. ForkJoinPool использует подход work stealing — у каждого потока есть его локальная очередь задач, из хвоста которой другие потоки могут тырить себе задачи, если у них закончились свои. Украденная задача делится и заполняет очередь задач потока.


Подробнее:



10. Какие бывают операции в стримах? Напишите стрим?

Есть 2 вида операций в Java Stream:


  • Промежуточные (Intermediate) — filter, map, sorted, peek и т.д. Возвращают Stream.
  • Терминальные (Terminal) — collect, forEach, count, reduce, findFirst, anyMatch и т.д. Возвращают результат стрима и запускают его выполнение.

Кроме того, будет полезно ознакомиться с содержимым пакета java.util.stream и доступными коллекторами из Collectors.


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



Почитать подробнее про стримы лучше в Java Doc, но можно и в статьях:



Посмотреть:



11. Что можно положить и достать из List<? extends Number>, а что с List<? super Number>? Что такое ковариантность, контрвариантность, инвариантность?

Тут речь пойдёт про PECS — Producer extends, Consumer super (Joshua Bloch, Effective Java). А также вариантность — перенос наследования исходных типов на производные от них типы (контейнеры, делегаты, обобщения).


Ковариантность (covariance) — перенос наследования исходных типов на производные от них типы в прямом порядке.
Переменной типа List<? extends T> разрешено присвоить экземпляр списка, параметризованного T или его подклассом, но не родительским классом. В список типа List<? extends T> нельзя добавить никакой объект (можно только null) — нельзя гарантировать какого именно типа экземпляр списка будет присвоен переменной, поэтому нельзя гарантировать, что добавляемый объект разрешён в таком списке. Однако, из списка можно прочитать объект и он будет типа T и экземпляром либо T, либо одного из подклассов T.
Соответственно, List<? extends Number> можно присвоить ArrayList<Number> или ArrayList<Integer>, но не ArrayList<Object>. Метод get возвращает Number, за которым может скрываться экземпляр Integer или другого наследника Number.
Массивы также ковариантны.
Переопределение методов, начиная с Java 5, ковариантно относительно типа результата и исключений.


List<?> аналогичен List<? extends Object> со всеми вытекающими.


Контрвариантность (contravariance) — перенос наследования исходных типов на производные от них типы в обратном порядке.
Переменной типа List<? super T> разрешено присвоить экземпляр списка, параметризованного T или его родительским классом, но не его подклассом. В список типа List<? super T> можно добавить экземпляр T или его подкласса, но нельзя добавить экземпляр родительских для T классов. Из такого списка с гарантией можно прочитать только Object, за которым может скрываться неизвестно какой его подкласс.
Соответственно, List<? super Number> можно присвоить либо ArrayList<Number>, либо ArrayList<Object>, но не список наследников Number(т.е. никаких ArrayList<Integer>). Можно добавить экземпляр Integer или Double (можно было бы Number, но он абстрактный), но нельзя — Object. Метод get возвращает Object — точнее сказать нельзя.


Инвариантность — наследование исходных типов не переносится на производные.
Переменной типа List<T> разрешено присвоить экземпляр списка, параметризованного только T. В список можно добавить экземпляр T или его подкласса. Список возвращает T, за которым может скрываться экземпляр его подкласса.
Соответственно, List<Number> можно присвоить ArrayList<Number>, но не ArrayList<Integer> или ArrayList<Object>. Можно добавить экземпляр Integer или Double (можно было бы Number, но он абстрактный), но нельзя — Object. Метод get возвращает Number, за которым может скрываться экземпляр Integer или другого наследника Number.


Подробнее:



12. Как работает ConcurrentHashMap?

ConcurrentHashMap — это потокобезопасная мапа (карта, словарь, ассоциативный массив, но тут и далее просто "мапа"), у которой отсутствуют блокировки на всю мапу целиком.
Особенности реализации:


  • Поля элемента мапы (Node<K,V>) val (значение) и next(следующее значение по данному ключу в цепочке или дереве), а также таблица бакетов (Node<K,V>[] table) объявлены как volatile
  • Для операций вставки первого элемента в бакет используется CAS — алгоритм, а для других операций обновления в этой корзине (insert, delete, replace) блокировки
  • Каждый бакет может блокироваться независимо путём блокировки первого элемента в корзине
  • Таблице бакетов требуется volatile/atomic чтения, запись и CAS, поэтому используются intrinsics-операции (jdk.internal.misc.Unsafe)
  • Concurrent resizing таблицы бакетов
  • Ленивая инициализация таблицы бакетов
  • При подсчёте количества элементов используется специальная реализация LongAdder

В результате имеем:


  • Извлечение значения возвращает последний результат завершенного обновления мапы на момент начала извлечения. Или перефразируя, любой non-null результат, возвращаемый get(key) связан отношением happens-before со вставкой или обновлением по этому ключу
  • Итераторы по ConcurrentHashMap возвращают элементы отображающие состояние мапы на определённый момент времени — они не бросают ConcurrentModificationException, но предназначены для использования одним потоком одновременно
  • Нельзя полагаться на точность агрегирующих методов (size, isEmpty, containsValue), если мапа подвергается изменениям в разных потоках
  • Не позволяет использовать null, который однозначно воспринимается как отсутствие значения
  • Поддерживает потокобезопасные, затрагивающие все (или многие) элементы мапы, операции — forEach, search, reduce (bulk operations). Данные операции принимают на вход функции, которые не должны полагаться на какой-либо порядок элементов в мапе и в идеале должны быть чистыми (за исключением forEach). На вход данные операции также принимают parallelismThreshold — операции будут выполняться последовательно, если текущий размер мапы меньше parallelismThreshold. Значение Long.MAX_VALUE сделает операцию точно последовательной. Значение 1 максимизирует параллелизм и утилизацию ForkJoinPool.commonPool(), который будет использоваться для параллельных вычислений

На Хабре есть несколько устаревшая статья — будьте внимательны и осторожны с java 8 произошли изменения. Класс Segment<K,V> максимально урезан и сохранён только для обратной совместимости при сериализации, где и используется. concurrencyLevel также оставлен лишь для обратной совместимости и теперь служит в конструкторе только для увеличения initialCapacity до количества предполагаемых потоков-потребителей мапы:


if (initialCapacity < concurrencyLevel)   // Use at least as many bins
    initialCapacity = concurrencyLevel;   // as estimated threads

Есть более современная статья с примером реализации ConcurrentMap. Также можно почитать гайд по ConcurrentMap на Baeldung.


13. Что такое Xmx и Xms, Xss?

JVM стартует с Xms количеством выделенной под heap памяти и максимально может увеличить её до значения Xmx.
Xss флаг определяет размер выделенной под стек памяти.
Общий вид:


java -Xmx<количество><единица измерения>

Можно использовать различные единицы измерения, например килобайты (k), мегабайты (m) или гигабайты (g).
Пример:


java -jar my.jar -Xms256m -Xmx2048m

Подробнее:



14. Как работают Атомики?

Атомарная операция — это операция, которая выполняется полностью или не выполняется совсем, частичное выполнение невозможно.
Атомики — это классы, которые выполняют операции изменения своего значения атомарно, т.о. они поддерживают lock-free thread-safe использование переменных. Достигается это с помощью алгоритма compare-and-swap (CAS) и работает быстрее, чем аналогичные реализации с блокировками. На уровне инструкций большинства процессоров имеется поддержка CAS.


В общем случае работу Атомиков можно описать следующим образом. Атомик хранит некоторое volatile значение value, для изменения которого используется метод compareAndSet(current, new), поэтому предварительно читается текущее значение — current. Данный метод с помощью CAS изменяет значение value только в том случае, если оно равно ожидаемому значению (т.е. current), прочитанному перед запуском compareAndSet(current, new). Если значение value было изменено в другом потоке, то оно не будет равно ожидаемому. Следовательно, метод compareAndSet вернет значение false. Поэтому следует повторять попытки чтения текущего значения и запуска с ним метода compareAndSet(current, new) пока current не будет равен value.


Условно можно разделить методы Атомиков на:


  • compare-and-set — принимают current на вход и делают одну попытку записи через CAS
  • set-and-get — самостоятельно читают current и пытаются изменить значение с помощью CAS в цикле, как описано выше

Непосредственно изменение значения value делегируется либо VarHandle, либо Unsafe, которые в свою очередь выполняют его на нативном уровне. VarHandle — это динамически сильно типизированная ссылка на переменную или на параметрически определяемое семейство переменных, включающее статические поля, нестатические поля, элементы массива или компоненты структуры данных нестандартного типа. Доступ к таким переменным поддерживается в различных режимах, включая простой доступ на чтение/запись, volotile доступ на чтение/запись и доступ на compare-and-swap.


В java.util.concurrent.atomic имеется следующий набор атомиков:


  • AtomicBoolean, AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray — представляют атомарные целочисленные, булевы примитивные типы, а также два массива атомарных целых чисел.
  • AtomicReference — класс для атомарных операций со ссылкой на объект.
  • AtomicMarkableReference — класс для атомарных операций над парой [reference, boolean].
  • AtomicStampedReference — класс для атомарных операций над парой [reference, int].
  • AtomicReferenceArray — массив атомарных ссылок
  • AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater — классы для атомарного обновления полей по их именам через reflection.
  • DoubleAccumulator, LongAccumulator — классы, представляющие атомарные аккумуляторы, которые принимают на вход чистую функцию-аккумулятор (BinaryOperator) и начальное значение. Сохраняет весь набор операндов, а когда необходимо получить значение, то аккумулирует их с помощью функции-аккумулятора. Порядок операндов и применения функции-аккумулятора не гарантируется. Используется, когда записей намного больше, чем чтения.
  • DoubleAdder, LongAdder — классы, представляющие атомарные счётчики. Являются частным случаем атомарных аккумуляторов, у которых функция-аккумулятор выполняет простое суммирование, а начальным значением является 0.

С помощью атомиков можно реализовать блокировку, например так:


public class NonReentrantSpinLock {

    private AtomicReference<Thread> owner = new AtomicReference<>();

    public void lock() {
      Thread currentThread = Thread.currentThread();

      while (!owner.compareAndSet(null, currentThread)) {}
    }

    public void unlock() {
      Thread currentThread = Thread.currentThread();
      owner.compareAndSet(currentThread, null);
    }
}

Подробнее:



15. Что внутри и как работают TreeSet/TreeMap? В чем идея Красно-черного дерева?

TreeMap — реализация NavigableMap, основанная на красно-чёрном дереве. Элементы отсортированы по ключам в натуральном порядке или с помощью Comparator, указанного при создании мапы, в зависимости от использовавшегося конструктора. Гарантирует логарифмическое время выполнения методов containsKey, get, put и remove.
TreeSet — реализация NavigableSet, основанная на TreeMap. Элементы отсортированы в натуральном порядке или с помощью Comparator, указанного при создании множества, в зависимости от использовавшегося конструктора. Гарантирует логарифмическое время выполнения методов add, contains и remove.


Обе коллекции НЕ synchronized и итератор по ним может выбросить ConcurrentModificationException.


Если в эти коллекции при использовании натурального порядка сортировки в качестве ключа попытаться положить null, то получим NullPointerException. В случае с компаратором поведение с null будет зависеть от реализации компаратора. До 7-й Java с добавлением null в TreeMap и TreeSet был баг.


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


  • Вершина может быть либо красной, либо чёрной и имеет двух потомков
  • Красная вершина не может быть дочерней для красной вершины
  • Количество чёрных вершин от корня до листа включительно одинаково для любого листа
  • Корень дерева является чёрным
  • Все листья — чёрные и не содержат данных

Подробнее:



16. Что поменялось с Java 8 по Java <CURRENT_VERSION>?

Java имеет богатую историю. На данный момент проекты чаще всего разделяются на:


  • legacy-проекты с версией Java меньше 8
  • проекты на Java 8, самая распрастранённая и популярная
  • проекты на Java 9+ (точнее либо 11 LTS, либо последние полугодовые релизы)

Между 8 и 9 версиями случился небольшой разлом с частичной потерей обратной совместимости, а потом приколы лицензирования подъехали, поэтому миграция и в без того консервативном мире Java-приложений идёт медленно. Однако идёт, и если вы собеседуетесь в компанию, где этот переход уже осуществили, то, вероятно, у вас поинтересуются, что же там с Java 8 поменялось, чем живёт и дышит современная Java.


На момент выхода статьи, имеем:



Найти ссылки на документацию к API, языку и виртуальной машине, release notes и сравнить API между версиями можно в Java-альманахе.


Кроме всего прочего, есть ряд проектов, в рамках которых развиваются большие и ожидаемые сообществом изменения Java:


  • Amber — проект по реализации маленьких, но продуктивных улучшений языка Java. В рамках данного проекта постепенно реализуется и независимо выходит целый набор JEP: var (JDK 10), Switch Expressions, Sealed Types, Records, Text Blocks, Pattern Matching для instanceof и другие.
  • Panama — проект по улучшению взаимодействия между JVM и нативным кодом. На Хабре есть статья с разъяснениями и интервью с Владимиром Ивановым на эту тему.
  • Loom — проект по внедрению в Java легковесных потоков. На Хабре есть две прекрасные статьи с разъяснениями: раз и два.
  • Valhalla — это проект по созданию нескольких больших и сложных улучшений языка и VM. В него входят: Inline types, Generics over Primitive Types, Enhanced volatiles и другие возможные или необходимые в рамках проекта улучшения.
  • Lanai — проект по улучшению рендеринга настольных Java-приложений на MacOS путём использования Metal Apple platform API. C 14 мая 2020 появились Early-Access сборки.
  • и другие

Отдельно нужно упомянуть GraalVM — это JDK и виртуальная машина Java, которая создана, чтобы объединить необъединяемое:


  • быстрое выполнение Java
  • уменьшение времени старта и потребления памяти для Java
  • комбинирование и исполнение программ, написанных на различных ЯП, в том числе на платформо-зависимых
  • общие инструменты для всех ЯП
  • поддержка JIT и AOT-компиляции
  • и т.п.

Послушать на тему:


  • Два выпуска подкаста Javaswag: раз и два
  • Выпуск 172 Java подкаста Подлодка, в гости к которому пришёл Тагир Валеев

Почитать на Хабре:



Посмотреть:



17. В какой кодировке строки в Java? Как хранятся строки внутри класса String? Как устроен String?

До Java 9 все строки имели кодировку UTF-16 (2 байта на символ) и хранились в массиве char.
С Java 9 пришло такое изменение как Compact String. Если все символы строки входят в множество символов Latin-1 (а это подавляющее большинство строк), то каждый из них может поместиться в 1 байт, поэтому в этом случае массив char избыточен. В результате было принято решение заменить массив char на массив byte, что позволяет строкам Latin-1 расходовать меньше памяти. Кодировка строки хранится в отдельном поле byte coder, значение которого представляет Latin-1 или UTF-16.


Также интересной особенностью является кеширование классом String своего hashcode.


Строки являются неизменяемыми, наследоваться от строк запрещено (final class). Все операции по изменении строки возвращают её новый экземпляр, в том числе и конкатенация строк. Компилятор умеет оптимизировать конкатенацию и превращать её в объект StringBuilder и совокупность вызовов методов append. ОДНАКО! В Java 9 вошёл JEP 280: Indify String Concatenation, который изменил эту оптимизацию и пошёл ещё дальше. Теперь вместо StringBuilder генерируется bytecode для вызова StringConcatFactory через invokedynamic, поэтому стоит расслабиться и чаще выбирать +.


Ещё можно упомянуть про String pool — это выделяемое в heap пространство, которое используется для оптимизации потребления памяти при хранении строк. Благодаря ему одинаковые строковые литералы могут ссылаться на один и тот же объект.


Стоит помнить, что с помощью [String.intern()](https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/String.html#intern()) производительности особой не добиться, а можно наоборот пустить всё по миру. Лучше напишите свою реализацию. Подоробнее читайте в статье Алексея Шипилёва — JVM Anatomy Quark #10: String.intern().


Кроме того, equals и методы поиска (например indexOf) оптимизируются JIT компилятором на нативном уровне.


Посмотреть доклады Алексея Шипилёва на тему строк: Катехизис java.lang.String и The Lord of the Strings: Two Scours.


Подробнее:



18. Что такое ThreadLocal переменные?

ThreadLocal — класс в виде обёртки для хранения отдельной независимой копии значения переменной для каждого использующего её потока, что позволяет сделать работу с такой переменной потокобезопасной.


Данные ThreadLocal-переменных хранятся не в них самих, а непосредственно в объектах Thread. У каждого экземпляра класса Thread есть поле ThreadLocal.ThreadLocalMap threadLocals, которое инициализируется и используется ThreadLocal. ThreadLocal.ThreadLocalMap представляет собой специализированную версию HashMap, записи которой наследуют от WeakReference<ThreadLocal<?>>, используя ключ мапы как ref field слабой ссылки. Ключами такой мапы являются ThreadLocal, а значением — Object. Если ключ записи равен null, то такая запись называется просроченной (stale) и будет удалена из мапы.
Следует обратить внимание, что ThreadLocal изолирует именно ссылки на объекты, а не копии их значений. Если изолированные внутри потоков ссылки ведут на один и тот же объект, то возможны коллизии.
Когда у ThreadLocal-переменной запрашивается её значение (например через метод get), то она получает текущий поток, извлекает из него мапу threadLocals, и получает значение из мапы, используя себя в качестве ключа. Аналогично выполняются методы изменения значения ThreadLocal.
Из этого следует, что значение ThreadLocal-переменной должно устанавливаться в том же потоке, в котором оно будет использоваться.


Подробнее:



19. Сколько в байт занимает каждый из примитивных типов в памяти? А объект?

Казалось бы:


  • byte — 1 байт
  • short — 2 байта
  • int — 4 байта
  • long — 8 байт
  • char — 2 байта
  • float — 4 байта
  • double — 8 байт

А размер boolean не упоминается в спецификации вовсе. Однако также спецификация не запрещает использовать для хранения примитива больше памяти — главное, чтобы размер был достаточным для всех значений. Конкретный объём таки зависит от реализации JVM. Не последнюю роль в этом играет выравнивание данных в памяти.


Похожая ситуация и со ссылочными типами — спецификация JVM не требует какой-то определённой структуры для объектов и отдаёт её на откуп реализации. Все тонкости и секреты занимаемой объектами памяти раскрывает Алексей Шипилёв в своей статье Java Objects Inside Out.


Подробнее:



Если вас заинтересовало представление объектов в jvm и их реализация (и вы умеете-могёте читать C++), то можно пойти посмотреть исходники openjdk. Начать, например, отсюда:



20. Какие ссылки бывают в Java?

Типы ссылок в Java:


  • Strong reference — обычная переменная ссылочного типа в Java. Объект такой ссылки очищается GC не раньше, чем станет неиспользуемым (никто нигде на него больше не ссылается).
  • Слабые ссылки — сборщик мусора тем или иным образом не учитывает связь ссылки и объекта в куче при выявлении объектов, подлежащих удалению. Объект будет удалён даже при наличии слабой ссылки на него:
    • Soft reference — мягкая ссылка, экземпляр класса SoftReference. Объект гарантированно будет собран GC до возникновения OutOfMemoryError. Может использоваться для реализации кэшей, увеличивающихся без риска OutOfMemoryError для приложения.
    • Weak reference — слабая ссылка, экземпляр класса WeakReference. Не препятствует утилизации объекта и игнорируется GC при сборке мусора. Может использоваться для хранения некоторой связанной с объектом информации до момента его смерти. Также стоит обратить внимание на WeakHashMap.
    • Phantom reference — фантомная ссылка, экземпляр класса PhantomReference. Не препятствует утилизации объекта и игнорируется GC при сборке мусора и имеет ряд особенностей, описанных ниже. Может быть применена для получения уведомления, что объект стал неиспользуемым и можно освободить связанные с ним ресурсы (как более надёжный вариант, чем finalize(), вызов которого не гарантируется, может проводить сеансы воскрешения и вообще deprecated).

Чтобы достать объект из слабых ссылок, необходимо вызывать метод get(). Если объект недостижим, то метод вернёт null. Для фантомных ссылок всегда возвращается null.


При создании слабой ссылки в конструктор можно, а для PhantomReference необходимо, передать экземпляр ReferenceQueue — в очереди будет сообщение, когда ссылка протухнет. Для SoftReference и WeakReference это будет ДО финализации объекта, а для PhantomReference ПОСЛЕ. Однако фактическое удаление объекта фантомной ссылки из памяти не производится до момента её очистки.


Подробнее:



Spring


21. Какие есть scope в Spring? Какой по умолчанию? Чем singleton отличается от prototype? Можно ли сделать свой scope и как? Плавно переходит в вопрос 'Как заинжектить prototype в singleton?'

Spring scope:


  • singleton (по умолчанию)
  • prototype
  • request
  • session
  • application
  • websocket

Про scope подробнее можно прочитать в документации, Bealdung. И, конечно же, надо посмотреть Spring-потрошитель Ч. 2.


Про prototype в singleton можно вспомнить несколько вариантов:


  • @Lookup
  • Фабрика для создания экземпляров prototype-бинов
  • ProxyMod = ScopedProxyMode.TARGET_CLASS
    Подробнее о каждом варианте есть в Bealdung и смотрим Spring-потрошитель Ч. 2.

22. Часто спрашивают о циклических зависимостях бинов в Spring. Проблема ли это или что получим в результате? Если проблема, то как её решить?

Да, это проблема — будет выброшено исключение BeanCurrentlyInCreationException (при внедрении зависимостей через конструктор).


Варианты решения:


  • Ещё раз подумать, той ли дорогой мы держим путь — может не поздно сделать редизайн и избавиться от циклических зависимостей
  • Инициализировать один из бинов лениво с помощью @Lazy
  • Внедрение зависимостей в setter-метод, а не в конструктор

Подробнее есть в документации и в Bealdung


23. Бывают вопросы про жизненный цикл бина, этапы инициализации контекста, про устройство спринга внутри, про DI и как он работает

Тут однозначно надо смотреть Spring-потрошитель часть 1 и часть 2. Также благое дело — это почитать документацию.


Также по этапам инициализации контекста есть статья с красивыми картинками на хабре.


24. Расскажите про прокси и про @Transactional. Как работает и зачем? Какие могут быть проблемы? Можно ли навесить @Transactional на приватный метод? А если вызывать метод с @Transactional внутри другого метода с @Transactional одного класса - будет работать?

Для начала, если вы не знали или случайно забыли про паттерн Proxy в общем виде, то можно освежиться здесь.


Допустим, что наш сервис MyServiceImpl имеет 2 публичных метода, аннотированных @Transactionalmethod1 и method2(он с Propagation.REQUIRES_NEW). В method1 вызываем method2.


В связи с тем, что для поддержки транзакций через аннотации используется Spring AOP, в момент вызова method1() на самом деле вызывается метод прокси объекта. Создается новая транзакция и далее происходит вызов method1() класса MyServiceImpl. А когда из method1() вызовем method2(), обращения к прокси нет, вызывается уже сразу метод нашего класса и, соответственно, никаких новых транзакций создаваться не будет.

Это цитата и краткий ответ на вопрос из статьи на Хабре, где можно ознакомиться с подробностями.


Что тут можно ещё посоветовать? Spring-потрошитель опять и снова — часть 1 и часть 2. А также документация является несомненным и любимым первоисточником информации о Proxy и управление транзакциями.


25. Где у обычного (НЕ Boot) Spring-приложения main-класс?

Старое доброе обычное Spring-приложение деплоится в контейнер сервлетов (или сервер приложений), где и расположен main-класс. При этом оно собирается в war-архив. Когда war-файл разворачивается в контейнере, контейнер обычно распаковывает его для доступа к файлам, а затем запускает приложение. Spring Boot приложение также можно собрать как war и задеплоить его таким же образом.


Подробнее:



26. Как работает Spring Boot и его стартеры?

Во-первых, благодаря spring-boot-starter-parent, у которого родителем является spring-boot-dependencies, можно особо не париться о зависимостях и их версиях — большинство версии того, что может потребоваться прописано и согласовано в dependencyManagement родительского pom. Или можно заимпортировать BOM.


Spring Boot черпает свою мощь из стартеров — наборов сконфигурированных бинов со всеми необходимыми зависимостями, готовых к использованию и доступных для тонкой настройки через properties-файлы.


Для Spring Boot приложения создаётся main-класс с аннотацией @SpringBootApplication и запуском метода run класса SpringApplication, который возвращает ApplicationContext.


@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

Аннотация @SpringBootApplication просто скрывает за собой аннотации @EnableAutoConfiguration, @ComponentScan и @Configuration.


SpringBootApplication создаёт либо WebApplicationContext (если в classpath есть Servlet и ConfigurableWebApplicationContext), либо GenericApplicationContext.


При создании стартера используется файл META-INF/spring.factories — в нём ключу org.springframework.boot.autoconfigure.EnableAutoConfiguration приравнивается список из полных имён всех классов-конфигураций (а в них бины, @ComponentScan, @Import и т.п.) стартера через запятую.


Аннотация @EnableAutoConfiguration импортирует AutoConfigurationImportSelector, который и отвечает за поиск необходимых классов конфигурации. Вызов метода getCandidateConfigurations обращается к SpringFactoriesLoader и его методу loadFactoryNames, чтобы просканировать classpath на наличие файлов META-INF/spring.factories и имен классов-конфигураций в них, а затем загрузить их в контекст.


Также у Spring boot есть модуль spring-boot-autoconfigure со своим файлом META-INF/spring.factories. Чтобы не создавать все-все бины из конфигураций, у бинов используется аннотация @Conditional (или её вариации) с каким-либо условием.


Чтобы упаковать Spring boot в jar используется spring-boot-maven-plugin. У такого jar в META-INF/MANIFEST.MF будет прописан Main-Classorg.springframework.boot.loader.JarLauncher, а в Start-Class будет уже main-класс нашего приложения. JarLauncher формирует class path (в начале в нём только org.springframework.boot), который находится в BOOT-INF(там lib с зависимостями и class с классами приложения), а затем запускает Start-Class.


Посмотреть:



Почитать:



27. Как выполняется http-запрос в Spring?

В современном Spring есть два подхода к построению веб-приложений:


  • Spring MVC
  • Spring WebFlux

Работа Spring MVC строится вокруг DispatcherServlet, который является обычным Servlet'ом и реализует паттерн Front Controller: принимает Http-запросы и координирует их с требуемыми обработчиками. Для своей конфигурации DispatcherServlet использует WebApplicationContext. DispatcherServlet в обработке запроса помогают несколько "специальных бинов" следующим образом:


  1. После получения HTTP-запроса DispatcherServlet перебирает доступные ему (предварительно найденные в контексте) экземпляры HandlerMapping, один из которых определит, метод какого Controller должен быть вызван. Реализации HandlerMapping, использующиеся по умолчанию: BeanNameUrlHandlerMapping и RequestMappingHandlerMapping (создаёт экземпляры RequestMappingInfo по методам аннотированным @RequestMapping в классах с аннотацией @Controller). HandlerMapping по HttpServletRequest находит соответствующий обработчик — handler-объект (например, HandlerMethod). Каждый HandlerMapping может иметь несколько реализаций HandlerInterceptor — интерфейса для кастомизации пред- и постобработки запроса. Список из HandlerInterceptor'ов и handler-объекта образуют экземпляр класса HandlerExecutionChain, который возвращается в DispatcherServlet.
  2. Для выбранного обработчика определяется соответствующий HandlerAdapter из предварительно найденных в контексте. По умолчанию используются HttpRequestHandlerAdapter (поддерживает классы, реализующие интерфейс HttpRequestHandler), SimpleControllerHandlerAdapter (поддерживает классы, реализующие интерфейс Controller) или RequestMappingHandlerAdapter (поддерживает контроллеры с аннотацией @RequestMapping).
  3. Происходит вызов метода applyPreHandle объекта HandlerExecutionChain. Если он вернёт true, то значит все HandlerInterceptor выполнили свою предобработку и можно перейти к вызову основного обработчика. false будет означать, что один из HandlerInterceptor взял обработку ответа на себя в обход основного обработчика.
  4. Выбранный HandlerAdapter извлекается из HandlerExecutionChain и с помощью метода handle принимает объекты запроса и ответа, а также найденный метод-обработчик запроса.
  5. Метод-обработчик запроса из Controller (вызванный через handle) выполняется и возвращает в DispatcherServlet ModelAndView. При помощи интерфейса ViewResolver DispatcherServlet определяет, какой View нужно использовать на основании полученного имени.
    Если мы имеем дело с REST-Controller или RESTful-методом контроллера, то вместо ModelAndView в DispatcherServlet из Controller вернётся null и, соответственно, никакой ViewResolver задействован не будет — ответ сразу будет полностью содержаться в теле HttpServletResponse после выполнения handle. Чтобы определить RESTful-методы, достаточно аннотировать их @ResponseBody либо вместо @Controller у класса поставить @RestController, если все методы котроллера будут RESTful.
  6. Перед завершением обработки запроса у объекта HandlerExecutionChain вызывается метод applyPostHandle для постобработки с помощью HandlerInterceptorов.
  7. Если в процессе обработки запроса выбрасывается исключение, то оно обрабатывается с помощью одной из реализаций интерфейса HandlerExceptionResolver. По умолчанию используются ExceptionHandlerExceptionResolver (обрабатывает исключени из методов, аннотированных @ExceptionHandler), ResponseStatusExceptionResolver (используется для отображения исключений аннотированных @ResponseStatus в коды HTTP-статусов) и DefaultHandlerExceptionResolver (отображает стандартные исключения Spring MVC в коды HTTP-статусов).
  8. В случае с классическим Controller после того, как View создан, DispatcherServlet отправляет данные в виде атрибутов в View, который в конечном итоге записывается в HttpServletResponse. Для REST-Controller ответ данная логика не вызывается, ведь ответ уже в HttpServletResponse.

Когда HTTP запрос приходит с указанным заголовком Accept, Spring MVC перебирает доступные HttpMessageConverter до тех пор, пока не найдет того, кто сможет конвертировать из типов POJO доменной модели в указанный тип заголовка Accept. HttpMessageConverter работает в обоих направлениях: тела входящих запросов конвертируются в Java объекты, а Java объекты конвертируются в тела HTTP ответов.
По умолчанию, Spring Boot определяет довольно обширный набор реализаций HttpMessageConverter, подходящие для использования широкого круга задач, но также можно добавить поддержку и для других форматов в виде собственной или сторонней реализации HttpMessageConverter или переопределить существующие.


Также стоит упомянуть, что как и в случае любого другого сервлета, к обработке запроса в Spring MVC может быть применена одна из реализаций интерфейса javax.servlet.Filter как до выполнения запроса, так и после. Sring MVC предоставляет несколько уже готовых реализаций.


Отдельного разговора заслуживает путь запроса по внутренностям Spring Security, где используется множество различных фильтров. На хабре есть статья об этом.


Подробнее:



Документация:



Spring WebFlux — это реактивный веб-фреймворк, который появился в Spring Framework 5.0. Он не требует Servlet API (но может использовать Servlet 3.1+containers, хотя чаще это Netty (по умолчанию в Spring Boot) или Undertow), полностью асинхронный и неблокирующий, реализует спецификацию Reactive Streams при помощи проекта Reactor.
В Spring WebFlux используется большинство аннотаций из Spring MVC (RestController, RequestMapping и другие) для определения аннотированных контроллеров. Однако представляет новую возможность создания функциональных котроллеров, основанных на HandlerFunction.


В Spring WebFlux обработка запроса на стороне сервера строится в два уровня:


  • HttpHandler — это базовый интерфейс обработки HTTP-запросов с использованием неблокирующего I/O, Reactive Streams back pressure через адаптеры для Reactor Netty, Undertow и т.д.
  • WebHandler — интерфейс, который предоставляет верхнеуровневое API для обработки HTTP-запросов поверх аннотированных или функциональных контроллеров.

Контракт HttpHandler представляет обработку HTTP-запроса, как его прохождение через цепочку множества WebExceptionHandler, множества WebFilter и одного единственного WebHandler. Сборкой цепочки занимается WebHttpHandlerBuilder при помощи ApplicationContext.


Диспетчеризация запросов в Spring WebFlux выполняется DispatcherHandler, который является имплементацией интерфейса WebHandler и также реализует паттерн Front Controller: принимает Http-запросы и координирует их с требуемыми обработчиками. DispatcherHandler — это Spring bean, имплементирующий ApplicationContextAware для доступа к контексту, с которым он был запущен. DispatcherHandler с бин-именем webHandler обнаруживает WebHttpHandlerBuilder и помещает в цепочку в качестве WebHandler.


DispatcherHandler в ходе обработки http-запроса и ответа делегирует часть работы "специальным бинам", которые могут быть подвержены кастомизации, расширению и замене пользователем. Сам процесс обработки выглядит следующим образом:


    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        if (this.handlerMappings == null) {
            return createNotFoundError();
        }
        return Flux.fromIterable(this.handlerMappings)
                .concatMap(mapping -> mapping.getHandler(exchange))
                .next()
                .switchIfEmpty(createNotFoundError())
                .flatMap(handler -> invokeHandler(exchange, handler))
                .flatMap(result -> handleResult(exchange, result));
    }

  1. Каждый из экземпляров HandlerMapping пытается найти подобающий обработчик для данного запроса (какой-то метод, какого-то контроллера). В итоге выбирается первый найденный обработчик (handler). Основными доступными реализациями HandlerMapping являются:
  2. Если обработчик найден, то для него (в invokeHandler) выбирается подходящий HandlerAdapter, вызывается его метод handle для непосредственной обработки запроса выбранным обработчиком. Результат обработки упаковывается в HandlerResult, который возвращается в DispatcherHandler. Главная задача HandlerAdapter — скрыть детали и способ непосредственного вызова метода-обработчика от DispatcherHandler. Примерами доступных реализаций HandlerAdapter являются:
  3. Полученный HandlerResult обрабатывается (в handleResult) необходимым для него HandlerResultHandler. Здесь обработка завершается формированием ответа на запрос требуемым образом. По умолчанию доступно несколько реализаций HandlerResultHandler:

Документация:



Продолжение следует


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

Теги:
Хабы:
Всего голосов 13: ↑11 и ↓2+13
Комментарии7

Публикации

Истории

Работа

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

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

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань