View on GitHub

java-interview

Вопросы для собеседования на разработчика Java

Вопросы для собеседования

Java 8

Что такое «лямбда»? Какова структура и особенности использования лямбда-выражения?

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

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

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

interface Operationable {
    int calculate(int x, int y);
}

public static void main(String[] args) {
    Operationable operation = (x, y) -> x + y;
    int result = operation.calculate(10, 20);
    System.out.println(result); //30
}

По факту лямбда-выражения являются в некотором роде сокращенной формой внутренних анонимных классов, которые ранее применялись в Java.

operation = (int x, int y) -> x + y;
//При написании самого лямбда-выражения тип параметров разрешается не указывать:
(x, y) -> x + y;
//Если метод не принимает никаких параметров, то пишутся пустые скобки, например,
() -> 30 + 20;
//Если метод принимает только один параметр, то скобки можно опустить:
n -> n * n;
interface Printable {
    void print(String s);
}

public static void main(String[] args) {
    Printable printer = s -> System.out.println(s);
    printer.print("Hello, world");
}
Operationable operation = (int x, int y) -> {
    if (y == 0) {
        return 0;
    }
    else {
        return x / y;
    }
};
interface Condition {
    boolean isAppropriate(int n);
}

private static int sum(int[] numbers, Condition condition) {
    int result = 0;
    for (int i : numbers) {
        if (condition.isAppropriate(i)) {
            result += i;
        }
    }
    return result;
}

public static void main(String[] args) {
    System.out.println(sum(new int[] {0, 1, 0, 3, 0, 5, 0, 7, 0, 9}, (n) -> n != 0));
}

к оглавлению

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

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

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

к оглавлению

Как отсортировать список строк с помощью лямбда-выражения?

public static List<String> sort(List<String> list){
    Collections.sort(list, (a, b) -> a.compareTo(b));
    return list;
}

к оглавлению

Что такое «ссылка на метод»?

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

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

private interface Measurable {
    public int length(String string);
}

public static void main(String[] args) {
    Measurable a = String::length;
    System.out.println(a.length("abc"));
}

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

к оглавлению

Какие виды ссылок на методы вы знаете?

к оглавлению

Объясните выражение System.out::println

Данное выражение иллюстрирует механизм instance method reference: передачи ссылки на метод println() статического поля out класса System.

к оглавлению

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

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

Чтобы точно определить интерфейс как функциональный, добавлена аннотация @FunctionalInterface, работающая по принципу @Override. Она обозначит замысел и не даст определить второй абстрактный метод в интерфейсе.

Интерфейс может включать сколько угодно default методов и при этом оставаться функциональным, потому что default методы - не абстрактные.

к оглавлению

Для чего нужны функциональные интерфейсы Function<T,R>, DoubleFunction<R>, IntFunction<R> и LongFunction<R>?

Function<T, R> - интерфейс, с помощью которого реализуется функция, получающая на вход экземпляр класса T и возвращающая на выходе экземпляр класса R.

Методы по умолчанию могут использоваться для построения цепочек вызовов (compose, andThen).

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123");     // "123"

к оглавлению

Для чего нужны функциональные интерфейсы UnaryOperator<T>, DoubleUnaryOperator, IntUnaryOperator и LongUnaryOperator?

UnaryOperator<T> (унарный оператор) принимает в качестве параметра объект типа T, выполняет над ними операции и возвращает результат операций в виде объекта типа T:

UnaryOperator<Integer> operator = x -> x * x;
System.out.println(operator.apply(5)); // 25

к оглавлению

Для чего нужны функциональные интерфейсы BinaryOperator<T>, DoubleBinaryOperator, IntBinaryOperator и LongBinaryOperator?

BinaryOperator<T> (бинарный оператор) - интерфейс, с помощью которого реализуется функция, получающая на вход два экземпляра класса T и возвращающая на выходе экземпляр класса T.

BinaryOperator<Integer> operator = (a, b) -> a + b;
System.out.println(operator.apply(1, 2)); // 3

к оглавлению

Для чего нужны функциональные интерфейсы Predicate<T>, DoublePredicate, IntPredicate и LongPredicate?

Predicate<T> (предикат) - интерфейс, с помощью которого реализуется функция, получающая на вход экземпляр класса T и возвращающая на выходе значение типа boolean.

Интерфейс содержит различные методы по умолчанию, позволяющие строить сложные условия (and, or, negate).

Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false

к оглавлению

Для чего нужны функциональные интерфейсы Consumer<T>, DoubleConsumer, IntConsumer и LongConsumer?

Consumer<T> (потребитель) - интерфейс, с помощью которого реализуется функция, которая получает на вход экземпляр класса T, производит с ним некоторое действие и ничего не возвращает.

Consumer<String> hello = (name) -> System.out.println("Hello, " + name);
hello.accept("world");

к оглавлению

Для чего нужны функциональные интерфейсы Supplier<T>, BooleanSupplier, DoubleSupplier, IntSupplier и LongSupplier?

Supplier<T> (поставщик) - интерфейс, с помощью которого реализуется функция, ничего не принимающая на вход, но возвращающая на выход результат класса T;

Supplier<LocalDateTime> now = LocalDateTime::now;
now.get();

к оглавлению

Для чего нужен функциональный интерфейс BiConsumer<T,U>?

BiConsumer<T,U> представляет собой операцию, которая принимает два аргумента классов T и U производит с ними некоторое действие и ничего не возвращает.

к оглавлению

Для чего нужен функциональный интерфейс BiFunction<T,U,R>?

BiFunction<T,U,R> представляет собой операцию, которая принимает два аргумента классов T и U и возвращающая результат класса R.

к оглавлению

Для чего нужен функциональный интерфейс BiPredicate<T,U>?

BiPredicate<T,U> представляет собой операцию, которая принимает два аргумента классов T и U и возвращающая результат типа boolean.

к оглавлению

Для чего нужны функциональные интерфейсы вида _To_Function?

к оглавлению

Для чего нужны функциональные интерфейсы ToDoubleBiFunction<T,U>, ToIntBiFunction<T,U> и ToLongBiFunction<T,U>?

к оглавлению

Для чего нужны функциональные интерфейсы ToDoubleFunction<T>, ToIntFunction<T> и ToLongFunction<T>?

к оглавлению

Для чего нужны функциональные интерфейсы ObjDoubleConsumer<T>, ObjIntConsumer<T> и ObjLongConsumer<T>?

к оглавлению

Что такое StringJoiner?

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

StringJoiner joiner = new StringJoiner(".", "prefix-", "-suffix");
for (String s : "Hello the brave world".split(" ")) {
    joiner.add(s);
}
System.out.println(joiner); //prefix-Hello.the.brave.world-suffix

к оглавлению

Что такое default методы интерфейса?

Java 8 позволяет добавлять неабстрактные реализации методов в интерфейс, используя ключевое слово default:

interface Example {
    int process(int a);
    default void show() {
        System.out.println("default show()");
    }
}

к оглавлению

Как вызывать default метод интерфейса в реализующем этот интерфейс классе?

Используя ключевое слово super вместе с именем интерфейса:

interface Paper {
    default void show() {
        System.out.println("default show()");
    }
}

class Licence implements Paper {
    public void show() {
        Paper.super.show();
    }
}

к оглавлению

Что такое static метод интерфейса?

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

к оглавлению

Как вызывать static метод интерфейса?

Используя имя интерфейса:

interface Paper {
    static void show() {
        System.out.println("static show()");
    }
}

class Licence {
    public void showPaper() {
        Paper.show();
    }
}

к оглавлению

Что такое Optional?

Опциональное значение Optional — это контейнер для объекта, который может содержать или не содержать значение null. Такая обёртка является удобным средством предотвращения NullPointerException, т.к. имеет некоторые функции высшего порядка, избавляющие от добавления повторяющихся if null/notNull проверок:

Optional<String> optional = Optional.of("hello");

optional.isPresent(); // true
optional.ifPresent(s -> System.out.println(s.length())); // 5
optional.get(); // "hello"
optional.orElse("ops..."); // "hello"

к оглавлению

Что такое Stream?

Интерфейс java.util.Stream представляет собой последовательность элементов, над которой можно производить различные операции.

Операции над стримами бывают или промежуточными (intermediate) или конечными (terminal). Конечные операции возвращают результат определенного типа, а промежуточные операции возвращают тот же стрим. Таким образом вы можете строить цепочки из несколько операций над одним и тем же стримом.

У стрима может быть сколько угодно вызовов промежуточных операций и последним вызов конечной операции. При этом все промежуточные операции выполняются лениво и пока не будет вызвана конечная операция никаких действий на самом деле не происходит (похоже на создание объекта Thread или Runnable, без вызова start()).

Стримы создаются на основе источников каких-либо, например классов из java.util.Collection.

Ассоциативные массивы (maps), например, HashMap, не поддерживаются.

Операции над стримами могут выполняться как последовательно, так и параллельно.

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

Кроме универсальных объектных существуют особые виды стримов для работы с примитивными типами данных int, long и double: IntStream, LongStream и DoubleStream. Эти примитивные стримы работают так же, как и обычные объектные, но со следующими отличиями:

к оглавлению

Какие существуют способы создания стрима?

  1. Из коллекции:
Stream<String> fromCollection = Arrays.asList("x", "y", "z").stream();
  1. Из набора значений:
Stream<String> fromValues = Stream.of("x", "y", "z");
  1. Из массива:
Stream<String> fromArray = Arrays.stream(new String[]{"x", "y", "z"});
  1. Из файла (каждая строка в файле будет отдельным элементом в стриме):
Stream<String> fromFile = Files.lines(Paths.get("input.txt"));
  1. Из строки:
IntStream fromString = "0123456789".chars();
  1. С помощью Stream.builder():
Stream<String> fromBuilder = Stream.builder().add("z").add("y").add("z").build();
  1. С помощью Stream.iterate() (бесконечный):
Stream<Integer> fromIterate = Stream.iterate(1, n -> n + 1);
  1. С помощью Stream.generate() (бесконечный):
Stream<String> fromGenerate = Stream.generate(() -> "0");

к оглавлению

В чем разница между Collection и Stream?

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

Также стоит отметить важность самой концепции сущностей: Collection - это прежде всего воплощение Структуры Данных. Например, Set не просто хранит в себе элементы, он реализует идею множества с уникальными элементами, тогда как Stream, это прежде всего абстракция необходимая для реализации конвейера вычислений, собственно поэтому, результатом работы конвейера являются те или иные Структуры Данных или же результаты проверок/поиска и т.п.

к оглавлению

Для чего нужен метод collect() в стримах?

Метод collect() является конечной операцией, которая используется для представление результата в виде коллекции или какой-либо другой структуры данных.

collect() принимает на вход Collector<Тип_источника, Тип_аккумулятора, Тип_результата>, который содержит четыре этапа: supplier - инициализация аккумулятора, accumulator - обработка каждого элемента, combiner - соединение двух аккумуляторов при параллельном выполнении, [finisher] - необязательный метод последней обработки аккумулятора. В Java 8 в классе Collectors реализовано несколько распространённых коллекторов:

Так же существует возможность создания собственного коллектора через Collector.of():

Collector<String, List<String>, List<String>> toList = Collector.of(
    ArrayList::new,
    List::add,
    (l1, l2) -> { l1.addAll(l2); return l1; }
);

к оглавлению

Для чего в стримах применяются методы forEach() и forEachOrdered()?

к оглавлению

Для чего в стримах предназначены методы map() и mapToInt(), mapToDouble(), mapToLong()?

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

mapToInt(), mapToDouble(), mapToLong() - аналоги map(), возвращающие соответствующий числовой стрим (то есть стрим из числовых примитивов):

Stream
    .of("12", "22", "4", "444", "123")
    .mapToInt(Integer::parseInt)
    .toArray(); //[12, 22, 4, 444, 123]

к оглавлению

Какова цель метода filter() в стримах?

Метод filter() является промежуточной операцией принимающей предикат, который фильтрует все элементы, возвращая только те, что соответствуют условию.

к оглавлению

Для чего в стримах предназначен метод limit()?

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

к оглавлению

Для чего в стримах предназначен метод sorted()?

Метод sorted() является промежуточной операцией, которая позволяет сортировать значения либо в натуральном порядке, либо задавая Comparator.

Порядок элементов в исходной коллекции остается нетронутым - sorted() всего лишь создает его отсортированное представление.

к оглавлению

Для чего в стримах предназначены методы flatMap(), flatMapToInt(), flatMapToDouble(), flatMapToLong()?

Метод flatMap() похож на map, но может создавать из одного элемента несколько. Таким образом, каждый объект будет преобразован в ноль, один или несколько других объектов, поддерживаемых потоком. Наиболее очевидный способ применения этой операции — преобразование элементов контейнера при помощи функций, которые возвращают контейнеры.

Stream
    .of("H e l l o", "w o r l d !")
    .flatMap((p) -> Arrays.stream(p.split(" ")))
    .toArray(String[]::new);//["H", "e", "l", "l", "o", "w", "o", "r", "l", "d", "!"]

flatMapToInt(), flatMapToDouble(), flatMapToLong() - это аналоги flatMap(), возвращающие соответствующий числовой стрим.

к оглавлению

Расскажите о параллельной обработке в Java 8

Стримы могут быть последовательными и параллельными. Операции над последовательными стримами выполняются в одном потоке процессора, над параллельными — используя несколько потоков процессора. Параллельные стримы используют общий ForkJoinPool доступный через статический ForkJoinPool.commonPool() метод. При этом, если окружение не является многоядерным, то поток будет выполняться как последовательный. Фактически применение параллельных стримов сводится к тому, что данные в стримах будут разделены на части, каждая часть обрабатывается на отдельном ядре процессора, и в конце эти части соединяются, и над ними выполняются конечные операции.

Для создания параллельного потока из коллекции можно также использовать метод parallelStream() интерфейса Collection.

Чтобы сделать обычный последовательный стрим параллельным, надо вызвать у объекта Stream метод parallel(). Метод isParallel() позволяет узнать является ли стрим параллельным.

С помощью, методов parallel() и sequential() можно определять какие операции могут быть параллельными, а какие только последовательными. Так же из любого последовательного стрима можно сделать параллельный и наоборот:

collection
.stream()
.peek(...) // операция последовательна
.parallel()
.map(...) // операция может выполняться параллельно,
.sequential()
.reduce(...) // операция снова последовательна

Как правило, элементы передаются в стрим в том же порядке, в котором они определены в источнике данных. При работе с параллельными стримами система сохраняет порядок следования элементов. Исключение составляет метод forEach(), который может выводить элементы в произвольном порядке. И чтобы сохранить порядок следования, необходимо применять метод forEachOrdered().

Критерии, которые могут повлиять на производительность в параллельных стримах:

collection.parallelStream()
    .sorted()
    .unordered()
    .collect(Collectors.toList());

к оглавлению

Какие конечные методы работы со стримами вы знаете?

Для числовых стримов дополнительно доступны:

к оглавлению

Какие промежуточные методы работы со стримами вы знаете?

Для числовых стримов дополнительно доступен метод mapToObj(), который преобразует числовой стрим обратно в объектный.

к оглавлению

Как вывести на экран 10 случайных чисел, используя forEach()?

(new Random())
    .ints()
    .limit(10)
    .forEach(System.out::println);

к оглавлению

Как можно вывести на экран уникальные квадраты чисел используя метод map()?

Stream
    .of(1, 2, 3, 2, 1)
    .map(s -> s * s)
    .distinct()
    .forEach(System.out::println);

к оглавлению

Как вывести на экран количество пустых строк с помощью метода filter()?

System.out.println(
    Stream
        .of("Hello", "", ", ", "world", "!")
        .filter(String::isEmpty)
        .count());

к оглавлению

Как вывести на экран 10 случайных чисел в порядке возрастания?

(new Random())
    .ints()
    .limit(10)
    .sorted()
    .forEach(System.out::println);

к оглавлению

Как найти максимальное число в наборе?

Stream
    .of(5, 3, 4, 55, 2)
    .mapToInt(a -> a)
    .max()
    .getAsInt(); //55

к оглавлению

Как найти минимальное число в наборе?

Stream
    .of(5, 3, 4, 55, 2)
    .mapToInt(a -> a)
    .min()
    .getAsInt(); //2

к оглавлению

Как получить сумму всех чисел в наборе?

Stream
    .of(5, 3, 4, 55, 2)
    .mapToInt()
    .sum(); //69

к оглавлению

Как получить среднее значение всех чисел?

Stream
    .of(5, 3, 4, 55, 2)
    .mapToInt(a -> a)
    .average()
    .getAsDouble(); //13.8

к оглавлению

Какие дополнительные методы для работы с ассоциативными массивами (maps) появились в Java 8?

map.putIfAbsent("a", "Aa");

map.forEach((k, v) -> System.out.println(v));

map.compute("a", (k, v) -> String.valueOf(k).concat(v)); //["a", "aAa"]

map.computeIfPresent("a", (k, v) -> k.concat(v));

map.computeIfAbsent("a", k -> "A".concat(k)); //["a","Aa"]

map.getOrDefault("a", "not found");

map.merge("a", "z", (value, newValue) -> value.concat(newValue)); //["a","Aaz"]

к оглавлению

Что такое Spliterator?

Spliterator (Сплитератор) — это интерфейс, который содержит 8 методов, причём четыре из них уже имеют реализацию по умолчанию. Оставшиеся методы — это tryAdvance, trySplit, estimateSize и characteristics. Существуют также специальные модификации сплитератора для примитивных типов int, long и double: они добавляют несколько дополнительных методов, чтобы избежать упаковки (boxing). Spliterator похож на обычный итератор. Основное отличие — умение разделиться (split) на две части — лежит в основе параллельной работы потоков. Также в целях оптимизации сплитератор имеет ряд флагов-характеристик и может сообщить точно или приблизительно свой размер. Наконец, сплитератор никогда не модифицирует источник данных: у него нет метода remove как у итератора.

к оглавлению

Что такое LocalDateTime?

LocalDateTime объединяет вместе LocaleDate и LocalTime, содержит дату и время в календарной системе ISO-8601 без привязки к часовому поясу. Время хранится с точностью до наносекунды. Содержит множество удобных методов, таких как plusMinutes, plusHours, isAfter, toSecondOfDay и т.д.

к оглавлению

Что такое ZonedDateTime?

java.time.ZonedDateTime — аналог java.util.Calendar, класс с самым полным объемом информации о временном контексте в календарной системе ISO-8601. Включает временную зону, поэтому все операции с временными сдвигами этот класс проводит с её учётом.

к оглавлению

Как получить текущую дату с использованием Date Time API из Java 8?

LocalDate.now();

к оглавлению

Как добавить 1 неделю, 1 месяц, 1 год, 10 лет к текущей дате с использованием Date Time API?

LocalDate.now().plusWeeks(1);
LocalDate.now().plusMonths(1);
LocalDate.now().plusYears(1);
LocalDate.now().plus(1, ChronoUnit.DECADES);

к оглавлению

Как получить следующий вторник используя Date Time API?

LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.TUESDAY));

к оглавлению

Как получить вторую субботу текущего месяца используя Date Time API?

LocalDate
    .of(LocalDate.now().getYear(), LocalDate.now().getMonth(), 1)
    .with(TemporalAdjusters.nextOrSame(DayOfWeek.SATURDAY))
    .with(TemporalAdjusters.next(DayOfWeek.SATURDAY));

к оглавлению

Как получить текущее время с точностью до миллисекунд используя Date Time API?

new Date().toInstant();

к оглавлению

Как получить текущее время по местному времени с точностью до миллисекунд используя Date Time API?

LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault());

к оглавлению

Как определить повторяемую аннотацию?

Чтобы определить повторяемую аннотацию, необходимо создать аннотацию-контейнер для списка повторяемых аннотаций и обозначить повторяемую мета-аннотацией @Repeatable:

@interface Schedulers
{
    Scheduler[] value();
}

@Repeatable(Schedulers.class)
@interface Scheduler
{
    String birthday() default "Jan 8 1935";
}

к оглавлению

Что такое Nashorn?

Nashorn - это движок JavaScript, разрабатываемый на Java компанией Oracle. Призван дать возможность встраивать код JavaScript в приложения Java. В сравнении с Rhino, который поддерживается Mozilla Foundation, Nashorn обеспечивает от 2 до 10 раз более высокую производительность, так как он компилирует код и передает байт-код виртуальной машине Java непосредственно в памяти. Nashorn умеет компилировать код JavaScript и генерировать классы Java, которые загружаются специальным загрузчиком. Так же возможен вызов кода Java прямо из JavaScript.

к оглавлению

Что такое jjs?

jjs это утилита командной строки, которая позволяет исполнять программы на языке JavaScript прямо в консоли.

к оглавлению

Какой класс появился в Java 8 для кодирования/декодирования данных?

Base64 - потокобезопасный класс, который реализует кодировщик и декодировщик данных, используя схему кодирования base64 согласно RFC 4648 и RFC 2045.

Base64 содержит 6 основных методов:

getEncoder()/getDecoder() - возвращает кодировщик/декодировщик base64, соответствующий стандарту RFC 4648; getUrlEncoder()/getUrlDecoder() - возвращает URL-safe кодировщик/декодировщик base64, соответствующий стандарту RFC 4648; getMimeEncoder()/getMimeDecoder() - возвращает MIME кодировщик/декодировщик, соответствующий стандарту RFC 2045.

к оглавлению

Как создать Base64 кодировщик и декодировщик?

// Encode
String b64 = Base64.getEncoder().encodeToString("input".getBytes("utf-8")); //aW5wdXQ==
// Decode
new String(Base64.getDecoder().decode("aW5wdXQ=="), "utf-8"); //input

к оглавлению

Источники

Вопросы для собеседования