1. Введение
В этом руководстве мы собираемся изучить некоторые вопросы, связанные с JDK8, которые могут возникнуть во время интервью.
Java 8 — это версия платформы, содержащая новые функции языка и библиотечные классы. Большинство этих новых функций направлены на создание более чистого и компактного кода, а некоторые добавляют новые функции, которые никогда ранее не поддерживались в Java.
2. Общие сведения о Java 8
Q1. Какие новые функции были добавлены в Java 8?
Java 8 поставляется с несколькими новыми функциями, наиболее важными из которых являются следующие:
- Лямбда-выражения — новая функция языка, позволяющая нам рассматривать действия как объекты.
- Ссылки на методы — позволяют нам определять лямбда-выражения, ссылаясь на методы напрямую, используя их имена.
Необязательный
— специальный класс-оболочка, используемый для выражения необязательности- Функциональный интерфейс — интерфейс с максимум одним абстрактным методом; реализация может быть предоставлена с использованием лямбда-выражения
- Методы по умолчанию — дают нам возможность добавлять полные реализации в интерфейсы помимо абстрактных методов.
- Nashorn, JavaScript Engine — движок на основе Java для выполнения и оценки кода JavaScript.
Stream
API — специальный класс итератора, который позволяет нам функционально обрабатывать коллекции объектов.- Date API — улучшенный, неизменяемый API Date, вдохновленный JodaTime.
Наряду с этими новыми функциями, множество внутренних улучшений функций реализовано как на уровне компилятора, так и на уровне JVM.
3. Ссылки на методы
Q1. Что такое ссылка на метод?
Ссылка на метод — это конструкция Java 8, которую можно использовать для ссылки на метод без его вызова. Он используется для обработки методов как лямбда-выражений. Они работают только как синтаксический сахар, чтобы уменьшить многословность некоторых лямбда-выражений. Таким образом, следующий код:
(o) -> o.toString();
Может стать:
Object::toString();
Ссылка на метод может быть идентифицирована двойным двоеточием, разделяющим имя класса или объекта и имя метода. Он имеет различные варианты, такие как ссылка на конструктор:
String::new;
Ссылка на статический метод:
String::valueOf;
Ссылка на метод связанного экземпляра:
str::toString;
Ссылка на метод несвязанного экземпляра:
String::toString;
Мы можем прочитать подробное описание ссылок на методы с полными примерами, перейдя по этой ссылке и этой .
Q2. Что означает выражение String::Valueof Expression?
Это ссылка статического метода на метод valueOf класса
String
.
4. Дополнительно
Q1. Что необязательно
? Как это можно использовать?
Необязательный
— это новый класс в Java 8, который инкапсулирует необязательное значение, то есть значение, которое либо есть, либо нет. Это оболочка вокруг объекта, и мы можем думать о ней как о контейнере из нуля или одного элемента.
Необязательный
имеет специальное значение Optional.empty()
вместо обернутого нуля
. Таким образом, во многих случаях его можно использовать вместо значения, допускающего значение NULL, чтобы избавиться от NullPointerException .
Мы можем прочитать специальную статью о необязательных
здесь .
Основная цель option
, задуманная его создателями, состоит в том, чтобы быть возвращаемым типом методов, которые ранее возвращали бы null
. Такие методы потребовали бы от нас написания шаблонного кода для проверки возвращаемого значения, и иногда мы могли забыть сделать защитную проверку. В Java 8 необязательный
возвращаемый тип явно требует, чтобы мы по-разному обрабатывали обернутые значения null или non-null.
Например, метод Stream.min()
вычисляет минимальное значение в потоке значений. Но что, если поток пуст? Если бы не option
, метод вернул бы null
или выдал бы исключение.
Однако он возвращает необязательное
значение, которое может быть Optional.empty()
(второй случай). Это позволяет нам легко обрабатывать такие случаи:
int min1 = Arrays.stream(new int[]{1, 2, 3, 4, 5})
.min()
.orElse(0);
assertEquals(1, min1);
int min2 = Arrays.stream(new int[]{})
.min()
.orElse(0);
assertEquals(0, min2);
Стоит отметить, что Optional
не является классом общего назначения, как Option
в Scala. Не рекомендуется использовать его в качестве значения поля в классах сущностей, на что явно указывает то, что он не реализует интерфейс Serializable .
5. Функциональные интерфейсы
Q1. Описать некоторые функциональные интерфейсы в стандартной библиотеке
В пакете java.util.function есть множество функциональных интерфейсов.
Наиболее распространенные из них включают, но не ограничиваются:
Функция
– принимает один аргумент и возвращает результатПотребитель
— принимает один аргумент и не возвращает результата (представляет собой побочный эффект).Поставщик
— не принимает аргументов и возвращает результатПредикат
— принимает один аргумент и возвращает логическое значение.BiFunction
— принимает два аргумента и возвращает результатBinaryOperator
— похож наBiFunction
, принимает два аргумента и возвращает результат. Два аргумента и результат относятся к одному типу.UnaryOperator
— похож наFunction
, принимает один аргумент и возвращает результат того же типа.
Дополнительные сведения о функциональных интерфейсах см. в статье «Функциональные интерфейсы в Java 8» .
Q2. Что такое функциональный интерфейс? Каковы правила определения функционального интерфейса?
Функциональный интерфейс — это интерфейс с одним-единственным абстрактным методом ( методы по умолчанию
не в счет), ни больше, ни меньше.
Если требуется экземпляр такого интерфейса, вместо него можно использовать лямбда-выражение. Говоря более формально: функциональные интерфейсы
предоставляют целевые типы для лямбда-выражений и ссылок на методы.
Аргументы и тип возвращаемого значения такого выражения полностью совпадают с таковыми у единственного абстрактного метода.
Например, интерфейс Runnable
— это функциональный интерфейс, поэтому вместо:
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("Hello World!");
}
});
Мы могли бы просто сделать:
Thread thread = new Thread(() -> System.out.println("Hello World!"));
Функциональные интерфейсы обычно снабжаются аннотацией @FunctionalInterface
, которая является информативной и не влияет на семантику.
6. Метод по умолчанию
Q1. Что такое метод по умолчанию и когда мы его используем?
Метод по умолчанию — это метод с реализацией, которую можно найти в интерфейсе.
Мы можем использовать метод по умолчанию для добавления новой функциональности в интерфейс, сохраняя при этом обратную совместимость с классами, которые уже реализуют интерфейс:
public interface Vehicle {
public void move();
default void hoot() {
System.out.println("peep!");
}
}
Обычно, когда мы добавляем в интерфейс новый абстрактный метод, все реализующие классы ломаются до тех пор, пока не реализуют новый абстрактный метод. В Java 8 эта проблема была решена с помощью метода по умолчанию.
Например, в интерфейсе Collection
нет объявления метода forEach .
Таким образом, добавление такого метода просто сломает весь API коллекций.
В Java 8 введен метод по умолчанию, так что интерфейс Collection
может иметь реализацию метода forEach
по умолчанию , не требуя, чтобы классы, реализующие этот интерфейс, реализовывали то же самое.
Q2. Будет ли компилироваться следующий код?
@FunctionalInterface
public interface Function2<T, U, V> {
public V apply(T t, U u);
default void count() {
// increment counter
}
}
Да, код будет скомпилирован, потому что он следует спецификации функционального интерфейса, определяющей только один абстрактный метод. Второй метод count
— это метод по умолчанию, который не увеличивает количество абстрактных методов.
7. Лямбда-выражения
Q1. Что такое лямбда-выражение и для чего оно используется?
Проще говоря, лямбда-выражение — это функция, на которую мы можем ссылаться и передавать ее как объект.
Кроме того, лямбда-выражения вводят обработку в функциональном стиле в Java и облегчают написание компактного и легко читаемого кода.
В результате лямбда-выражения являются естественной заменой анонимных классов, таких как аргументы методов. Одним из их основных применений является определение встроенных реализаций функциональных интерфейсов.
Q2. Объясните синтаксис и характеристики лямбда-выражения
Лямбда-выражение состоит из двух частей: части параметров и части выражений, разделенных стрелкой вперед:
params -> expressions
Любое лямбда-выражение имеет следующие характеристики:
- Необязательное объявление типа — при объявлении параметров в левой части лямбды нам не нужно объявлять их типы, поскольку компилятор может вывести их из их значений. Таким образом ,
int param -> …
иparam ->…
все допустимы. - Необязательные круглые скобки — когда объявлен только один параметр, нам не нужно заключать его в круглые скобки. Это означает , что
param -> …
и(param) -> …
все допустимы, но когда объявлено более одного параметра, требуются круглые скобки. - Необязательные фигурные скобки — когда часть выражений содержит только один оператор, в фигурных скобках нет необходимости. Это означает, что
param -- > statement
иparam -- > {statement;}
допустимы, но фигурные скобки необходимы, когда имеется более одного оператора. - Необязательный оператор возврата — когда выражение возвращает значение и оно заключено в фигурные скобки, нам не нужен оператор возврата. Это означает , что
(a, b) – > {return a+b;}
и(a, b) – > {a+b;}
оба действительны .
Чтобы узнать больше о лямбда-выражениях, перейдите по этой ссылке и этой .
8. Нашорн Javascript
Q1. Что такое Насхорн в Java8?
Nashorn — это новый механизм обработки Javascript для платформы Java, поставляемый с Java 8. До JDK 7 платформа Java использовала Mozilla Rhino для той же цели, что и механизм обработки Javascript.
Nashorn обеспечивает лучшее соответствие нормализованной спецификации JavaScript ECMA и лучшую производительность во время выполнения, чем его предшественник.
Q2. Что такое JJS?
В Java 8 jjs
— это новый исполняемый файл или инструмент командной строки, который мы используем для выполнения кода Javascript на консоли.
9. Потоки
Q1. Что такое поток? Чем он отличается от коллекции?
Проще говоря, поток — это итератор, роль которого состоит в том, чтобы принимать набор действий для применения к каждому из содержащихся в нем элементов.
Поток представляет собой последовательность объектов из источника, такого как коллекция, которая поддерживает агрегатные операции .
Они были разработаны, чтобы сделать обработку коллекций простой и лаконичной. В отличие от коллекций, логика итерации реализована внутри потока, поэтому мы можем использовать такие методы, как map
и flatMap,
для выполнения декларативной обработки.
Кроме того, Stream
API работает плавно и позволяет выполнять конвейерную обработку:
int sum = Arrays.stream(new int[]{1, 2, 3})
.filter(i -> i >= 2)
.map(i -> i * 3)
.sum();
Еще одно важное отличие от коллекций заключается в том, что потоки по своей природе лениво загружаются и обрабатываются.
Q2. В чем разница между промежуточными и терминальными операциями?
Мы объединяем потоковые операции в конвейеры для обработки потоков. Все операции являются либо промежуточными, либо терминальными.
Промежуточные операции — это те операции, которые возвращают сам поток
, что позволяет выполнять дальнейшие операции над потоком.
Эти операции всегда ленивы, т.е. они не обрабатывают поток в месте вызова. Промежуточная операция может обрабатывать данные только при наличии терминальной операции. Некоторыми из промежуточных операций являются filter
, map
и flatMap
.
Напротив, терминальные операции завершают конвейер и инициируют потоковую обработку. Поток проходит через все промежуточные операции при вызове терминальной операции. Терминальные операции включают forEach
, reduce, Collect
и sum
.
Чтобы довести эту мысль до конца, давайте рассмотрим пример с побочными эффектами:
public static void main(String[] args) {
System.out.println("Stream without terminal operation");
Arrays.stream(new int[] { 1, 2, 3 }).map(i -> {
System.out.println("doubling " + i);
return i * 2;
});
System.out.println("Stream with terminal operation");
Arrays.stream(new int[] { 1, 2, 3 }).map(i -> {
System.out.println("doubling " + i);
return i * 2;
}).sum();
}
Вывод будет следующим:
Stream without terminal operation
Stream with terminal operation
doubling 1
doubling 2
doubling 3
Как мы видим, промежуточные операции запускаются только тогда, когда существует терминальная операция.
Q3. В чем разница между Map
и потоковой операцией flatMap
?
Между map
и flatMap
есть разница в сигнатуре . Вообще говоря, операция map
заключает возвращаемое значение в свой порядковый номер, а flatMap
— нет.
Например, в Optional
операция сопоставления
вернет тип Optional<String>
, а flatMap
вернет тип String
.
Таким образом, после сопоставления нам нужно развернуть (читай «сгладить») объект, чтобы получить значение, тогда как после плоского сопоставления в этом нет необходимости, поскольку объект уже сплющен. Мы применяем ту же концепцию к отображению и плоскому отображению в Stream
.
И map
, и flatMap
являются промежуточными операциями потока, которые получают функцию и применяют эту функцию ко всем элементам потока.
Разница в том, что для карты
эта функция возвращает значение, а для flatMap
эта функция возвращает поток. Операция flatMap
«сглаживает» потоки в один.
Вот пример, где мы берем карту имен пользователей и списков телефонов и «сглаживаем» ее до списка телефонов всех пользователей с помощью flatMap
:
Map<String, List<String>> people = new HashMap<>();
people.put("John", Arrays.asList("555-1123", "555-3389"));
people.put("Mary", Arrays.asList("555-2243", "555-5264"));
people.put("Steve", Arrays.asList("555-6654", "555-3242"));
List<String> phones = people.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
Q4. Что такое Stream Pipelining в Java 8?
Потоковая конвейеризация — это концепция объединения операций в цепочку. Мы делаем это, разделяя операции, которые могут происходить в потоке, на две категории: промежуточные операции и терминальные операции.
Каждая промежуточная операция возвращает экземпляр самого Stream при запуске. Поэтому мы можем настроить произвольное количество промежуточных операций для обработки данных, формируя конвейер обработки.
Затем должна быть терминальная операция, которая возвращает окончательное значение и завершает конвейер.
10. API даты и времени Java 8
Q1. Расскажите нам о новом API даты и времени в Java 8
Давней проблемой для разработчиков Java была неадекватная поддержка манипуляций с датой и временем, необходимых обычным разработчикам.
Существующие классы, такие как java.util.Date
и SimpleDateFormatter
, не являются потокобезопасными, что приводит к потенциальным проблемам параллелизма для пользователей.
Плохой дизайн API также является реальностью в старом API данных Java. Вот простой пример: годы в java.util.Date
начинаются с 1900, месяцы начинаются с 1, а дни начинаются с 0, что не очень интуитивно понятно.
Эти и некоторые другие проблемы привели к популярности сторонних библиотек даты и времени, таких как Joda-Time.
Чтобы решить эти проблемы и обеспечить лучшую поддержку в JDK, для Java SE 8 в пакете java.time
был разработан новый API даты и времени, свободный от этих проблем .
11. Заключение
В этой статье мы рассмотрели несколько важных технических вопросов для интервью с акцентом на Java 8. Это ни в коем случае не исчерпывающий список, но он содержит вопросы, которые, по нашему мнению, с наибольшей вероятностью появятся в каждой новой функции Java 8.
Даже если мы только начинаем, незнание Java 8 — не лучший способ пройти собеседование, особенно когда Java сильно упоминается в резюме. Поэтому важно, чтобы мы потратили некоторое время, чтобы понять ответы на эти вопросы и, возможно, провести больше исследований.
Удачи на собеседовании.