Java Interview Review
6.16K members
13 photos
260 links
Популярные вопросы и ответы с собеседований на Java-разработчика.

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

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

http://itsobes.ru/
Download Telegram
to view and join the conversation
Как применяют технологию SPI (2/2)

Для получения провайдеров всех библиотек приложения используется класс ServiceLoader. Это итератор по сервис-провайдерам, а создается он статическим методом load, в который параметром передается интерфейс/абстрактный класс интересующего сервиса.

Доступ к файлам-ресурсам из classpath обеспечивается загрузчиком классов, поэтому дополнительно при загрузке можно указать специфический загрузчик. С появлением модульности в Java 9 можно также указать модуль.

SPI повсеместно используется в стандартной библиотеке JDK. С его помощью подключаются JDBC-драйверы. Через ServiceLoader также загружаются таймзоны, системные настройки, кодировки, провайдеры файловой системы и многое другое.

Пример реализации собственного SPI-сервиса можно найти в этой статье на хабре.

#Классы
Где у Java приложения точка входа?

В обычном Java приложении всегда должен быть main class, содержащий метод main. С него начинается исполнение всей программы. Main class-ом может быть не только класс, но и интерфейс или енам. Для JavaFX приложения главный класс должен реализовывать javafx.application.Application.

main обязательно public static. Дополнительно, методу разрешено иметь модификатор strictfp. На аннотации и список исключений ограничений не накладывается.

В главном методе должен быть объявлен единственный аргумент – массив строк. Обе конструкции String[] и String... компилируются в один и тот же байт-код, так что приемлемы оба варианта. Название массива может быть любым, а значение будет содержать аргументы командной строки.

Когда приложение запускается как classpath, главный класс передается параметром командной строки. Если выполняется единственный исходник, он и описывает main class.

Для исполняемого jar-файла (java -jar MyJar.jar), его главный класс должен быть указан в манифесте. Внутри архива, в файл META-INF/MANIFEST.MF добавляется строчка вида Main-Class: ru.itsobes.MyClass. Иначе запуск завершается ошибкой «no main manifest attribute».

В случае, когда в указанном главном классе не оказывается метода, который бы удовлетворял всем критериям главного метода, программа падает с ошибкой «Main method not found».

В апплетах вместо main входной точкой служат методы init и start. Начиная с версии Java 9 технология апплетов объявлена устаревшей, а с 11 – совсем удалена. Не будем останавливаться на них подробнее.

#Язык
В чем разница между JavaEE, JavaSE и JavaME?

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

• Standard Edition (SE) – основной набор.

• Enterprise Edition (EE) – стандартная версия, с дополнительными интерфейсами корпоративных web-технологий.

• Micro Edition (ME) – для платформ, сильно ограниченных в ресурсах. Интернет вещей, Raspberry PI, умные телевизоры. Если помните, игры для старых телефонов имели расширение .jar.

• Java Card – джава для банковских и SIM-карт. Подмножество основного языка, с урезанной библиотекой, измененным байткодом, и упором на безопасность. Когда в окне инсталлятора баннер заявляет «3 Billion Devices Run Java», в счёт идут и карточки.

• JavaFX – платформа для десктопных приложений, замена Swing. Сейчас живет как отдельный opensource-проект.

#Язык
Чем синхронный сервер отличается от асинхронного?

Вопрос может быть сформулирован как «сравните Jetty и Netty», или «зачем нужен Spring WebFlux».

Большинство современных Java web-серверов синхронные. Это значит, что для каждого пришедшего HTTP-запроса выделяется отдельный поток. Даже если такой поток переиспользуется с помощью пула, он остается занятым до конца обработка запроса.

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

Альтернативное решение – асинхронные сервера. В них для потоков обработки HTTP-запросов используется work stealing. В широком смысле, вызовы асинхронных функций не блокируют выполнение, а их результат вместо return value возвращается параметром коллбэка. В Java этот результат зачастую возвращается в виде объекта Future.

Чтобы вся обработка запроса стала действительно асинхронной, необходимо также избавиться от блокирующих операций. Иначе преимущество подхода с work stealing выродится в простой пул потоков. Блокирующая работа с файлами и сетью должна быть заменена на NIO, а для БД должен быть использован асинхронный драйвер.

#JavaEE
#Сеть
Что происходит внутри HashMap.put()? (1/2)

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

1. Вычисляется хэш ключа. Если ключ null, хэш считается равным 0. Чтобы достичь лучшего распределения, результат вызова hashCode() «перемешивается»: его старшие биты XOR-ятся на младшие.

2. Значения внутри хэш-таблицы хранятся в специальных структурах данных – нодах, в массиве. Из хэша высчитывается номер бакета – индекс для значения в этом массиве. Полученный хэш обрезается по текущей длине массива. Длина – всегда степень двойки, так что для скорости используется битовая операция &.

3. В бакете ищется нода. В ячейке массива лежит не просто одна нода, а связка всех нод, которые туда попали. Исполнение проходит по этой связке (цепочке или дереву), и ищет ноду с таким же ключом. Ключ сравнивается с имеющимися сначала на ==, затем на equals.

4. Если нода найдена – её значение просто заменяется новым. Работа метода на этом завершается.

5. Если ноды с таким же ключом в бакете пока нет – добавляемая пара ключ-значение запаковывается в новый объект типа Node, и прикрепляется к структуре существующих нод бакета. Ноды составляют структуру за счет того, что в ноде хранится ссылка на следующий элемент (для дерева – следующие элементы). Кроме самой пары и ссылок, чтобы потом не считать заново, записывается и хэш ключа.

#Коллекции
Что происходит внутри HashMap.put()? (2/2)

6. В случае, когда структурой была цепочка а не дерево, и длина цепочки превысила 7 элементов – происходит процедура treeification – превращение списка в самобалансирующееся дерево. В случае коллизии это ускоряет доступ к элементам на чтение с O(n) до O(log(n)). У comparable-ключей для балансировки используется их естественный порядок. Другие ключи балансируются по порядку имен их классов и значениям identityHashCode-ов. Для маленьких хэш-таблиц (< 64 бакетов) «одеревенение» заменяется увеличением (см. п.8).

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

8. Когда количество занятых бакетов массива превысило пороговое (capacity * load factor), внутренний массив увеличивается вдвое, а для всего содержимого выполняется рехэш – все имеющиеся ноды перераспределяются по бакетам по тем же правилам, но уже с учетом нового размера.

#Коллекции
Какой у Spring бинов скоуп по умолчанию?

В Spring Framework во всех определениях бизнес-сущностей (bean) явно или неявно присутствует атрибут scope. В Java-конфигурации он передается в аннотации @Scope, в xml – в атрибуте scope тега <bean>.

Атрибут scope – это строка-идентификатор, которая ставит бину в соответствие экземпляр класса org.springframework.beans.factory.config.Scope. Скоуп – реализация паттерна «стратегия» для фабрик бинов, инструкция по созданию бизнес-объектов.

В простейшем Spring-приложении всегда существует два сокоупа:
singleton – объект создается однажды, при последующих внедрениях переиспользуется. Полезен для большинства случаев: различные сервисы, объекты без состояния, неизменяемые объекты. Стоит заметить, это не класс-синглтон: при объявлении двух бинов одного класса их экземпляров будет два. Это скоуп по умолчанию.
prototype – при каждом внедрении фабрика бинов создает новый объект. Нужен для изменяемых бинов с состоянием.

Spring Web добавляет 4 дополнительных скоупа, которые делают бин синглтоном в пределах обработки одного сетевого запроса (request), клиентской сессии (session), контекста сервлета (application) и вебсокет-сессии (websocket).

Разработчик может добавлять собственные скоупы. Пример реализации одного можно найти в самих исходниках Spring: SimpleThreadScope, который делает бин тред-локальным. Для использования его, как и пользовательские скоупы, нужно сначала зарегистрировать в BeanFactory.

#Spring
Что такое функциональный интерфейс?

Так называется специальная разновидность интерфейса, который определяет тип-функцию, коллбэк.

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

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

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

Дополнительно функциональный интерфейс принято помечать аннотацией @FunctionalInterface. Наличие этой аннотации не необходимо, но оно даёт дополнительную валидацию: её присутствие на нефункциональном типе спровоцирует ошибку компиляции.

Типичные примеры функциональных интерфейсов: Callable, Supplier, Comparable.

#Язык
Как инициализировать переменную функционального интерфейса?

Функциональный интерфейс – всё ещё интерфейс, поэтому остаются доступными стандартные способы. Интерфейс можно реализовать обычным классом, и затем создать его экземпляр оператором new. Можно совместить эти два действия, и создать экземпляр анонимного класса.

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

1. Лямбда-выражение: (x, y) -> x * y
2. Ссылка на метод: Math::sqrt

На эти способы накладывается небольшое ограничение: тип функционального параметра/переменной должен быть указан явно. Это значит, что лямбдой или метод-референсом нельзя инициализировать переменную, объявленную ключевым словом var. Также, чтобы передать лямбду или референс в параметр generic-типа, этот тип должен быть ограничен функциональным интерфейсом (должен стираться в него).

#Язык
Что можно импортировать статически?

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

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

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

Языковая конструкция static import обязана располагаться там же, где и обычные импорты – обязательно между package и объявлением основного класса файла.

#Язык
Что такое classpath?

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

Кроме непосредственно Java-приложений (команда java), этот параметр применим и для других утилит JDK, таких как javac, javadoc и другие.

Есть два основных способа установки classpath: в переменной окружения ОС CLASSPATH, и в аргументе командной строки -cp (синоним -classpath). Второй способ предпочтительнее, потому что позволяет устанавливать разные значения для разных приложений. Значение по умолчанию – текущая директория.

В параметре передаются пути к jar-файлам и корневым директориям с пакетами. Пути разделяют символом : в параметре командной строки, или же ; в переменной окружения. Чтобы включить все файлы директории, разрешается использовать в конце пути символ *.

Если приложение запускается из jar-файла (java -jar), classpath должен быть указан в его манифесте.

#JVM
Как использовать ReadWriteLock?

Стандартный интерфейс ReadWriteLock предоставляет потокобезопасный разделенный доступ на чтение и на запись. Для этих целей в нём объявлены два метода: readLock() и writeLock(). Они возвращают объекты под интерфейсом Lock.

Оба типа блокировок одного экземпляра ReadWriteLock связаны. Пока какой-то поток не заберет блокировку на запись, сколько угодно потоков могут читать не мешая друг другу. Блокировкой readLock закрывается часть кода с семантикой «только чтения» некоторого условного «ресурса». В критической секции кода writeLock осуществляется модификация ресурса.

Свойства этих локов защищают программу от ситуаций конкурентной записи ресурса и чтения во время записи. Подобно copy-on-write коллекциям, этот подход становится выгодным, когда ресурс читают сильно чаще чем модифицируют.

Интерфейс реализуется классом ReentrantReadWriteLock, который во многом похож на обычный ReentrantLock.

#Многопоточность
Какие отличия между @⁠Component, @⁠Service, @⁠Repository и @⁠Controller?

@Component – простой способ сделать объявление класса объявлением Spring-бина. Из всех компонентов, которые попали в сканирование (о которых знает @ComponentScan), будут созданы бин-дефинишны.

Остальные аннотации – это алиасы аннотации @Component. Сами по себе они не добавляют поведения, и технически в рамках ядра Spring Framework работают так же.

Эти аннотации называют «Stereotype annotations». Их главное отличие – семантика, логическая роль компонентов:
• @⁠Service – реализация бизнес-логики;
• @⁠Repository – хранилище данных: «репозиторий» из Domain-Driven Design или классический DAO;
• @⁠Controller – обработка веб-запросов (методы @RequestMapping)

Сторонние компоненты могут пользоваться этой семантикой. Например, трансляция исключений Persistence API работает именно на компонентах стереотипа @Repository. Таким образом, в отдельных случаях кроме семантики может меняться и поведение кода библиотек.

#Spring
Перечислите стандартные функциональные интерфейсы

Стандартная библиотека содержит пакет java.util.function, в котором хранятся функциональные интерфейсы для большинства случаев жизни. Их можно разделить на 5 групп:

Функции
Обычная обобщенная функция – интерфейс Function<T, R>. Принимает параметр и возвращает значение другого типа. Для примитивов есть не-generic специализации – семейство интерфейсов XtoYFunction. (Здесь и далее вместо X и Y подставляются названия примитивов).

Бинарные функции – функции с двумя параметрами и возвращаемым значением. BiFunction<T, U, R>, ToXBiFunction<T, U>.

Поставщики (Suppliers)
Интерфейсы Supplier<T>, XSupplier – не принимают ничего, возвращают (поставляют) значение.

Потребители (Consumers)
Consumer<T>, XConsumer – принимают (потребляют) значение, ничего не возвращают.
Бинарный вариант, BiConsumer<T, U> и XYConsumer, потребляет два параметра.

Предикаты
Predicate<T>, XPredicate – принимают параметр, возвращают boolean. Кроме самой функции содержат дефолтные реализации логических операций.

Операторы
Унарный (UnaryOperator<T>) и бинарный (BinaryOperator<T>) – просто функция и би-функция с одинаковым типом параметров и результата. Специализации для примитивов XUnaryOperator и XBinaryOperator вдобавок содержат дефолтные реализации методов для композиции операторов.

#Классы
Что происходит внутри TreeMap.put()?

Недавно мы в деталях рассматривали, какие процессы происходят при добавлении элемента в HashMap. Теперь поговорим о TreeMap. Здесь не так много тонкостей, как в хэш-таблице.

TreeMap требует либо задать порядок ключей вручную (передать в конструктор Comparator), либо чтобы они имели собственный естественный порядок (были Comparable).

Подобно нодам в хэш-таблице, внутренняя структура дерева строится из объектов внутреннего класса узла – Entry. В каждом узле хранится информация о данных (пара key-value), и о положении в структуре (ссылки на родительский узел, левую и правую ветви).

Сама структура представляет из себя красно-чёрное дерево относительно ключей. Не будем здесь углубляться в детали его реализации. О нем важно знать два факта:

1. Это бинарное дерево поиска. Значит, каждый новый элемент начинает искать свое место в дереве, сравниваясь с узлами начиная с корневого. Меньшие элементы движутся влево, большие – вправо. Для этого и требуется наличие метода compare. Дойдя до конца, пара ключ-значение «повисает» новым узлом.

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

Чтобы осознать процесс построения RB-дерева, есть интерактивное демо.

#Коллекции
Какие задачи решает Spring Data?

Это проект, который упрощает работу с системами доступа к данным: реляционными и нереляционными базами данных, map-reduce фреймворками и облачными хранилищами. Центральная концепция проекта – репозитории из предметно-ориентированного дизайна (Domain-driven design, DDD).

Spring Data состоит из множества отдельных библиотек для разных случаев жизни. Вот самые популярные из них:

• Spring Data JPA – адаптер для реализаций Java Persistence API, таких как Hibernate.
• Spring Data JDBC – более простой и ограниченный чем JPA адаптер для JDBC-драйверов.
• Spring Data REST – создание готовых hypermedia-driven RESTful сервисов на основе репозиториев.
• Spring Data KeyValue – работа с хранилищами типа ключ-значение.
Библиотеки поддержки конкретных реализаций хранилищ: MongoDB, Redis, Cassandra, LDAP, и других.

#Spring
Можно ли хранить null в стандартных коллекциях?

Все интерфейсы Collections Framework позволяют своим реализациям самостоятельно решать, поддерживать ли null-значения. Если реализация не может принять null, она выбрасывает NullPointerException или ClassCastException.

Большинство списков (LinkedList, ArrayList) принимают null без проблем. Большинство очередей (Queue и Deque) не хранят null – возвращая из читающего метода null они сообщают пользователю о пустоте коллекции.

Unmodifiable Maps не допускают null-ов совсем. Обычные изменяемые мапы обычно не испытывают трудности со значениями null. А вот с ключами дело обстоит интереснее.

HashMap не может посчитать hash-сумму от null. Но вместо этого для таких ключей просто используется бакет номер 0.

Иногда этот вопрос дается как задача с подвохом про TreeMap. Nullability её ключей зависит от готовности к этому компаратора. Натуральный порядок (который работает для Comparable ключей) не поддерживает null. Раньше в реализации был баг, который позволял положить значение по ключу null в корень дерева без выброса исключения.

Для значений Set-ов действуют те же правила, что для ключей лежащих в основе их Map-ов.

#Коллекции
Как работают фильтры сервлетов?

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

Filter Chain – типичный пример реализации паттерна Chain of responsibility. Каждый фильтр может модифицировать запрос/ответ, и либо отправить на обработку следующему фильтру, либо заворачивать обратно. В фильтрах удобно выполнять некий общий код обработки запросов: отклонение неавторизованных обращений, логгирование, обогащение запроса/ответа данными из контекста.

Фильтр состоит из трех методов: init, doFilter и destroy. doFilter – основная реализация фильтрации, он вызывается для каждого запроса. Инициализация и уничтожение вызываются строго по одному разу. Кроме того, сервлет-контейнер гарантирует, что их вызовы не будут пересекаться: doFilter не начнет работать до конца выполнения init, и закончит до начала destroy.

#JavaEE
Когда используется StampedLock?

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

Во-первых, если блокировка ReadWriteLock возвращает объекты типа Lock, то StampedLock возвращает числа типа long, которые и называется «штампами». Штамп служит идентификатором лока, он передается параметром в методы по работе с ранее захваченной блокировкой чтения или записи. Специальный штамп 0 означает неудавшийся захват.

StampedLock в отличие от ReentrantReadWriteLock – не реентрант. Это накладывает бóльшую ответственность на программиста: можно устроить дедлок на одном потоке.

В StampedLock расширена функциональность. Новые методы с префиксом try* не висят в ожидании. Методы tryOptimistic* реализуют оптимистичную блокировку. Методы tryConvert* дают возможность изменять «уровень» заблокированности: можно попытаться превратить readLock во writeLock, и наоборот.

Не смотря на похожесть, StampedLock не наследуется от ReadWriteLock. Но для совместимости в нём предусмотрены методы-адаптеры asReadWriteLock, asReadLock и asWriteLock.

Итого, блокировка на штампах решает те же задачи, что ReadWriteLock, но дает больше возможностей и лучшую производительность.

#Многопоточность
Сравните репозитории Spring Data

Основная часть работы в Spring Data строится вокруг интерфейса Repository. Это маркерный интерфейс. От него наследуются интерфейсы-специализации, которые уже содержат методы для работы с сущностями базы данных. Все эти интерфейсы параметризуются двумя типами: самой сущности и её идентификатора.

CrudRepository – базовый набор операций над сущностями: создание, чтение, изменение и удаление (CRUD).

PagingAndSortingRepository – добавляет к CRUD возможность постраничной загрузки данных с определенной сортировкой.

JpaRepository – расширение PagingAndSortingRepository, полноценно реализующее Java Persistence API. Добавляет ряд методов, таких как например flush и deleteInBatch.

MongoRepository – расширение PagingAndSortingRepository, специфичное для MongoDB.

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

#Spring
Что хранится в файле манифеста?

В JAR архиве можно найти файл META-INF/MANIFEST.MF. Это манифест архива – хранилище его метаинформации. Манифест обычно добавляется той же утилитой, которой собирается jar-файл: maven-jar-plugin, команда JDK jar.

Манифест – текстовый файл, который состоит из заголовков, строчек вида ключ: значение. Заголовки разделены на секции. Файл начинается с главной секции, описывающей метаинформацию всего архива. Следом, отделенные пустыми строками, идут секции для отдельных пакетов и файлов. В них могут переопределяться общие заголовки. JVM игнорирует неизвестные ей заголовки, что позволяет сторонним утилитам хранить в манифесте свою специфичную метаинформацию.

Вот некоторые из часто используемых заголовков:
• Информация об архиве: Manifest-Version, Created-By, Multi-Release, Built-By
• Main-class – точка входа приложения
Classpath приложения
• Информация об экстеншне (Specification и Implementation, deprecated)
• Заголовки OSGI бандла
• Типы и хэши файлов архива (особенно применимо в Android приложениях)

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

#JVM