How to

#Java #interview #Java 8 #Functional Programming

Interview Questions for Java 8

What new features were introduced in Java 8 and JDK 8?

Back to Table of Contents

What is a “lambda”? What is the structure and unique characteristics of using a lambda expression?

A lambda is a set of instructions that can be separated into a standalone variable and then called multiple times in different parts of the program.

The core of a lambda expression consists of a lambda operator, represented by the arrow ->. This operator separates the lambda expression into two parts: the left part contains the parameter list of the expression, while the right part provides the body of the lambda expression, where all actions are executed.

A lambda expression doesn’t execute by itself; it forms an implementation of a method defined in a functional interface. Importantly, the functional interface must contain only one abstract method with no implementation.

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
}

In effect, lambda expressions are a sort of shorthand for the anonymous inner classes previously used in Java.

operation = (int x, int y) -> x + y;
//When defining the lambda expression itself, the type of parameters may be omitted:
(x, y) -> x + y;
//If the method does not take any parameters, empty parentheses are written, for example:
() -> 30 + 20;
//If the method accepts only one parameter, parentheses can be omitted:
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));
}

Back to Table of Contents

What variables can lambda expressions access?

The access to variables in the outer scope from a lambda expression is very similar to access from anonymous objects. You can refer to:

Access to default methods in the implementing functional interface is forbidden within the lambda expression.

Back to Table of Contents

How can you sort a list of strings using a lambda expression?

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

Back to Table of Contents

What is a “method reference”?

If an existing method within a class already does everything that is needed, you can use the mechanism of method reference to directly pass this method. Such a reference is passed in the form of:

The result will be exactly the same as defining a lambda expression that calls that method.

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

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

Method references are potentially more efficient than using lambda expressions. Additionally, they provide the compiler with better information about the type, and whenever there is an option to choose between using a method reference and using a lambda expression, method reference should always be preferred.

Back to Table of Contents

What types of method references do you know?

Back to Table of Contents

Explain the expression System.out::println.

This expression illustrates the mechanism of instance method reference: passing a reference to the println() method of the static field out from the System class.

Back to Table of Contents

What are “functional interfaces”?

A functional interface is an interface that defines only one abstract method.

To explicitly define an interface as functional, the annotation @FunctionalInterface is added, working like @Override. It denotes intention and prevents defining a second abstract method in the interface.

An interface can include any number of default methods and still remain a functional interface since default methods are not abstract.

Back to Table of Contents

What are the purposes of functional interfaces Function<T,R>, DoubleFunction<R>, IntFunction<R>, and LongFunction<R>?

Function<T, R> is an interface used to implement a function that takes an instance of class T as input and returns an instance of class R.

Default methods can be used to build call chains (compose, andThen).

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

Back to Table of Contents

What are the purposes of functional interfaces UnaryOperator<T>, DoubleUnaryOperator, IntUnaryOperator, and LongUnaryOperator?

UnaryOperator<T> (Unary Operator) takes an object of type T as a parameter, performs operations on it, and returns the result as an object of type T:

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

Back to Table of Contents

What are the purposes of functional interfaces BinaryOperator<T>, DoubleBinaryOperator, IntBinaryOperator, and LongBinaryOperator?

BinaryOperator<T> (Binary Operator) is an interface used to implement a function that takes two instances of class T as input and returns an instance of class T.

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

Back to Table of Contents

What are the purposes of functional interfaces Predicate<T>, DoublePredicate, IntPredicate, and LongPredicate?

Predicate<T> (Predicate) is an interface used to implement a function that takes an instance of class T as input and returns a value of type boolean.

The interface contains various default methods allowing for building complex conditions (and, or, negate).

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

Back to Table of Contents

What are the purposes of functional interfaces Consumer<T>, DoubleConsumer, IntConsumer, and LongConsumer?

Consumer<T> (Consumer) is an interface used to implement a function that takes an instance of class T, performs an action with it, and returns nothing.

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

Back to Table of Contents

What are the purposes of functional interfaces Supplier<T>, BooleanSupplier, DoubleSupplier, IntSupplier, and LongSupplier?

Supplier<T> (Supplier) is an interface used to implement a function that takes no input but returns a result of class T;

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

Back to Table of Contents

What is the purpose of the functional interface BiConsumer<T,U>?

BiConsumer<T,U> represents an operation that accepts two arguments of classes T and U, performs an action with them, and returns nothing.

Back to Table of Contents

What is the purpose of the functional interface BiFunction<T,U,R>?

BiFunction<T,U,R> represents an operation that takes two arguments of classes T and U and returns a result of class R.

Back to Table of Contents

What is the purpose of the functional interface BiPredicate<T,U>?

BiPredicate<T,U> represents an operation that takes two arguments of classes T and U and returns a result of type boolean.

Back to Table of Contents

What are the functional interfaces of the type _To_Function useful for?

Back to Table of Contents

What are the purposes of functional interfaces ToDoubleBiFunction<T,U>, ToIntBiFunction<T,U>, and ToLongBiFunction<T,U>?

Back to Table of Contents

What are the purposes of functional interfaces ToDoubleFunction<T>, ToIntFunction<T>, and ToLongFunction<T>?

Back to Table of Contents

What are the purposes of functional interfaces ObjDoubleConsumer<T>, ObjIntConsumer<T>, and ObjLongConsumer<T>?

Back to Table of Contents

What is StringJoiner?

The StringJoiner class is used to create a sequence of strings, separated by a specified delimiter, with the ability to attach a prefix and suffix to the resulting string:

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

Back to Table of Contents

What are default methods in an interface?

Java 8 allows adding non-abstract method implementations in an interface using the default keyword:

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

Back to Table of Contents

How do you call a default method of an interface within a class that implements the interface?

Using the super keyword along with the interface name:

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

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

Back to Table of Contents

What is a static method in an interface?

Static methods in interfaces are similar to default methods, except they cannot be overridden in the classes that implement the interface.

Back to Table of Contents

How do you call a static method in an interface?

Using the interface name:

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

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

Back to Table of Contents

What is Optional?

An optional value in Optional is a container for an object that may or may not contain a null value. This wrapper is a convenient way to prevent NullPointerException, as it has some higher-order functions that eliminate the need for repetitive if-null/notNull checks:

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"

Back to Table of Contents

What is Stream?

The java.util.Stream interface represents a sequence of elements that you can perform various operations on.

Stream operations are either intermediate or terminal. Terminal operations return a result of a specific type, while intermediate operations return the same stream. Therefore, you can build chains of several operations on the same stream.

A stream can have any number of calls to intermediate operations and ends with a terminal operation. All intermediate operations execute lazily, meaning that until a terminal operation is called, no actions really take place (similar to creating a Thread or Runnable object without calling start()).

Streams are created based on various sources, such as classes from java.util.Collection.

Associative arrays (maps), for example, HashMap, are not supported.

Stream operations can be performed sequentially or in parallel.

Streams cannot be reused. Once a terminal operation has been called, the stream closes.

In addition to universal object-oriented streams, there are specific types of streams for dealing with primitive data types int, long, and double: IntStream, LongStream, and DoubleStream. These primitive streams operate similarly to regular object streams but have the following distinctions:

Back to Table of Contents

What are the existing ways to create a stream?

  1. From a collection:
Stream<String> fromCollection = Arrays.asList("x", "y", "z").stream();
  1. From specific values:
Stream<String> fromValues = Stream.of("x", "y", "z");
  1. From an array:
Stream<String> fromArray = Arrays.stream(new String[]{"x", "y", "z"});
  1. From a file (each line in the file becomes a separate element in the stream):
Stream<String> fromFile = Files.lines(Paths.get("input.txt"));
  1. From a string:
IntStream fromString = "0123456789".chars();
  1. Using Stream.builder():
Stream<String> fromBuilder = Stream.builder().add("z").add("y").add("z").build();
  1. Using Stream.iterate() (infinite):
Stream<Integer> fromIterate = Stream.iterate(1, n -> n + 1);
  1. Using Stream.generate() (infinite):
Stream<String> fromGenerate = Stream.generate(() -> "0");

Back to Table of Contents

What is the difference between Collection and Stream?

Collections allow you to work with elements individually, while streams do not, but instead provide the ability to perform functions over data as a whole.

It’s also important to note the fundamental concept: a Collection primarily represents a Data Structure. For instance, a Set not only stores elements but embodies the idea of a collection with unique elements, whereas a Stream is primarily an abstraction necessary for implementing a pipeline of computations, hence the results of a pipeline yield certain Data Structures or results of checks/searches, etc.

Back to Table of Contents

What is the purpose of the collect() method in streams?

The collect() method is a terminal operation that is used to represent the result as a collection or some other data structure.

collect() takes a Collector<SourceType, AccumulatorType, ResultType> as input, which comprises four stages: supplier - initializing the accumulator, accumulator - processing each element, combiner - combining two accumulators during parallel execution, [finisher] - an optional method for final processing of the accumulator. In Java 8, several common collectors have been implemented in the Collectors class:

It is also possible to create a custom collector via Collector.of():

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

Back to Table of Contents

What is the purpose of the forEach() and forEachOrdered() methods in streams?

Back to Table of Contents

What are the purposes of map() and mapToInt(), mapToDouble(), mapToLong() methods in streams?

The map() method is an intermediate operation that transforms each element of the stream in a specified way.

mapToInt(), mapToDouble(), mapToLong() are analogous to map() but return the corresponding numeric stream (i.e., a stream composed of numeric primitives):

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

Back to Table of Contents

What is the purpose of the filter() method in streams?

The filter() method is an intermediate operation that takes a predicate which filters out all elements, returning only those that conform to the condition.

Back to Table of Contents

What is the purpose of the limit() method in streams?

The limit() method is an intermediate operation that allows for limiting the selection to a specified number of leading elements.

Back to Table of Contents

What is the purpose of the sorted() method in streams?

The sorted() method is an intermediate operation that allows for sorting values either in natural order or by specifying a Comparator.

The order of elements in the original collection remains unchanged - sorted() merely creates a sorted representation of it.

Back to Table of Contents

What are the purposes of flatMap(), flatMapToInt(), flatMapToDouble(), flatMapToLong() methods in streams?

The flatMap() method is similar to map, but it can create multiple objects from a single element. Therefore, each object will be transformed into zero, one, or multiple other objects supported by the stream. The most straightforward use case of this operation is transforming container elements with functions that return containers.

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() are analogous to flatMap(), returning the corresponding numeric stream.

Back to Table of Contents

Discuss parallel processing in Java 8.

Streams can be sequential or parallel. Operations on sequential streams are executed in a single processor thread, while parallel streams utilize multiple processor threads. Parallel streams use a common ForkJoinPool accessible via the static ForkJoinPool.commonPool() method. If the environment is not multi-core, the stream will execute sequentially. Essentially, using parallel streams means that data in the streams will be divided into parts, each part processed on a separate processor core, and then those parts are merged back together, and terminal operations are performed on them.

You can also create a parallel stream from a collection using the parallelStream() method of the Collection interface.

To make a regular sequential stream parallel, call the parallel() method on the Stream object. The isParallel() method allows you to check whether the stream is parallel.

Using the parallel() and sequential() methods enables you to determine which operations can be parallelized and which should remain sequential. It is also possible to convert any sequential stream into a parallel one and vice versa:

collection
.stream()
.peek(...) // this operation is sequential
.parallel()
.map(...) // this operation can run in parallel,
.sequential()
.reduce(...) // this operation is sequential again

Typically, elements are passed to the stream in the order in which they are defined in the source data. When working with parallel streams, the system retains the sequence of elements. An exception is the forEach() method, which may output elements in an arbitrary order. To maintain the sequence, the forEachOrdered() method must be employed.

Criteria that can influence performance in parallel streams include:

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

Back to Table of Contents

What terminal methods for streams do you know?

For numeric streams, additional methods are available:

Back to Table of Contents

What intermediate methods for streams do you know?

For numeric streams, an additional method mapToObj() is available, which transforms a numeric stream back into an object stream.

Back to Table of Contents

How can you print 10 random numbers using forEach()?

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

Back to Table of Contents

How can you print unique squares of numbers using the map() method?

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

Back to Table of Contents

How can you print the count of empty strings using the filter() method?

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

Back to Table of Contents

How can you print 10 random numbers in ascending order?

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

Back to Table of Contents

How can you find the maximum number in a set?

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

Back to Table of Contents

How can you find the minimum number in a set?

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

Back to Table of Contents

How can you get the sum of all numbers in a set?

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

Back to Table of Contents

How can you get the average of all numbers?

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

Back to Table of Contents

What additional methods for working with associative arrays (maps) were introduced in 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"]

Back to Table of Contents

What is LocalDateTime?

LocalDateTime combines LocaleDate and LocalTime, containing both date and time in the ISO-8601 calendar system without being bound to a time zone. The time is stored with precision up to nanoseconds. It contains many convenient methods, such as plusMinutes, plusHours, isAfter, toSecondOfDay, etc.

Back to Table of Contents

What is ZonedDateTime?

java.time.ZonedDateTime is analogous to java.util.Calendar, a class with the most complete range of information about the time context in the ISO-8601 calendar system. It includes a time zone, hence all operations with time shifts conducted by this class consider it.

Back to Table of Contents

How can you get the current date using the Date Time API from Java 8?

LocalDate.now();

Back to Table of Contents

How can you add 1 week, 1 month, 1 year, or 10 years to the current date using the Date Time API?

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

Back to Table of Contents

How can you get the next Tuesday using the Date Time API?

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

Back to Table of Contents

How can you get the second Saturday of the current month using the Date Time API?

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

Back to Table of Contents

How can you get the current time with millisecond precision using the Date Time API?

new Date().toInstant();

Back to Table of Contents

How can you get the current time in local time with millisecond precision using the Date Time API?

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

Back to Table of Contents

How can you define a repeatable annotation?

To define a repeatable annotation, you need to create a container annotation for a list of repeatable annotations and designate the repeatable meta-annotation with @Repeatable:

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

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

Back to Table of Contents

What is Nashorn?

Nashorn is a JavaScript engine developed in Java by Oracle. Its purpose is to allow the embedding of JavaScript code within Java applications. Compared to Rhino, maintained by the Mozilla Foundation, Nashorn offers 2 to 10 times higher performance since it compiles code and directly hands over bytecode to the Java Virtual Machine in memory. Nashorn can compile JavaScript code and generate Java classes, which are loaded via a special class loader. It is also possible to call Java code directly from JavaScript.

Back to Table of Contents

What is jjs?

jjs is a command-line utility that allows you to run JavaScript programs directly from the console.

Back to Table of Contents

What class was introduced in Java 8 for encoding/decoding data?

Base64 is a thread-safe class that implements a data encoder and decoder using the base64 encoding scheme according to RFC 4648 and RFC 2045.

Base64 contains 6 main methods:

getEncoder()/getDecoder() - returns the base64 encoder/decoder that corresponds to the RFC 4648 standard; getUrlEncoder()/getUrlDecoder() - returns a URL-safe base64 encoder/decoder, corresponding to the RFC 4648 standard; getMimeEncoder()/getMimeDecoder() - returns a MIME encoder/decoder that corresponds to the RFC 2045 standard.

Back to Table of Contents

How can you create a Base64 encoder and decoder?

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

Back to Table of Contents

Sources

Back to Table of Contents