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


    Когда-то я проходил серию собеседований на 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, базах данных, паттернах и практиках разработки, об одной популярной библиотеке, поддержке и сопровождении наших приложений, а также посмотрим на альтернативные шпаргалки и подведём итоги.

    Комментарии 6

      +4
      Беспорядочный набор ссылок на все подряд (включая битые) от коротких статей до часового видео и никакой структуры.
      В чем отличие от 1000 и 1 статей про «супер ответы на все вопросы с собеседований»?
        0
        Что ж, протухла одна ссылка пока писал. Бывает такое в интернете. Удалил. Спасибо за вашу бдительность.

        Отличие в том, что всё в одном месте + много ссылок на разные материалы, разного формата, длины, подробности на любой вкус. Чтобы и пробежаться перед началом забега по собесам можно было, и покапать какие-нибудь вопросы отдельно.

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

        Во второй части статьи напихал альтернатив, если мой вариант вам не подходит.
        0

        Рядом с вопросом про TreeMap добавьте ещё вопрос про бинарное дерево поиска.
        Ну и понимание того, как дерево строится, тоже будет полезным.

          0

          .

            +2

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

              0
              Вы даёте ссылку на статью про CHM с Java8 где не больше объяснений чем у вас, умещающееся в предложение «вместо деления на сегменты в java 8 массив бакетов представляет собой единый массив». ИМХО: слабова-то для разбора такой важной коллекции.

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

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

              Самое читаемое