Java-8 Interview Questions
These Java 8 interview questions are based on my personal interview experience. Keep following this post for regular updates.
Also Read Core Java Interview Questions
Also Read Java Multithreading (Concurrency) Interview Questions
Q1. What new features introduced in Java 8?
Java 8 introduced several new features but the most significant are the following:-
- Lambda Expressions is introduction of functional programming in Java to treat actions (functions) as Objects
- Method References is used to reference method as lambda expression using double colon
::
operator - Functional Interface is an interface with maximum one abstract method, implementation can be provided using a Lambda Expression
- Default methods give us the ability to add full implementations in interfaces besides abstract methods
- Stream APIs are a special iterator class that allows processing collections of objects in a functional manner
- Optional are special wrapper class used for expressing optionality
- Enhanced Date and Time API is an improved, immutable JodaTime-inspired Date API
- Nashorn, JavaScript Engine is Java-based engine for executing and evaluating JavaScript code
- Permanent Generation i.e. PermGen space which has limited memory size by default and often leads to OutOfMemory error. It is completely replaced by Metaspace which grows automatically by default and auto trigger the garbage collection when usage reach
-XX:MaxMetaspaceSize
Along with these new features, lots of feature enhancements are done under-the-hood, at both JVM and compiler level.
Q2. What is a Functional Interface?
A Functional Interface is an interface -
- having class annotated with
@FunctionalInterface
- which is informative and does not affect the semantics. - must have one and only one abstract method
- can have one or more default and static methods with implementation.
Function Interfaces can be implemented using lambda expressions. Let’s look at two examples:-
Example 1
Java has some built-in functional interface such as java.lang.Runnable
which is having only one run()
abstract method.
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
This is how we implement Runnable
Functional Interface in general to create a new Thread
:-
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("Hello World!");
}
});
This is how we can implement Runnable
using lambda expression:-
Thread thread = new Thread(() -> System.out.println("Hello World!"));
Lambda expression is short and sweet.
Example 2
Another functional interface is java.util.Comparator
which is having one compare()
abstract method, and many default and static methods.
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
return Collections.reverseOrder();
}
}
This is how we implement Comparator
Functional Interface in general to sort the list :-
List<String> a1 = Arrays.asList("equity", "stocks", "gold", "foreign exchange", "fixed income", "future");
a1.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
This is how we can implement Comparator
using lambda expression:-
List<String> a1 = Arrays.asList("equity", "stocks", "gold", "foreign exchange", "fixed income", "future");
a1.sort((o1, o2) -> o2.compareTo(o1));
Q3. What is a default method and when do we use it?
Before Java 8, an interface can have only abstract methods and no default methods.
In Java 8, Function Interface can have one or more default methods with implementation.
For example, below Functional Interface has two default methods compose
and andThen
:-
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
When you implement the above Functional Interface, you don’t need to implement compose
and andThen
default methods. You only implement apply
abstract method.
Usually, when a new abstract method is added to an interface, all implementing classes will break until they implement the new abstract method. default methods solves that problem as you do not need to implement them.
The default methods were introduced so that:-
- Old interfaces like
Runnable
andComparator
can be used with lambda expression as they have only one abstract method and many default and static methods, they qualify as Functional Interface - Old interfaces like
Collection
can add new functionality using default method while maintaining backward compatibility with classes that are already implementing the interface.
Q4. Will the following code compile?
@FunctionalInterface
public interface MyFunction<T, U, V> {
public V apply(T t, U u);
default void print(T t, U u) {
System.out.println(t + u);
}
}
Yes. The code will compile because it meets the functional interface specification having single abstract method apply
and one or more default method print
. Note that you don’t need to explicitly use the abstract keyword with apply
method definition.
Q5. Describe some of the Functional Interfaces in the Standard Library.
There are a lot of functional interfaces in the java.util.function package, the more common ones include but not limited to:
Class Name | Description | Usage |
---|---|---|
Function | accepts one argument and produces a result | .map(s -> s.toUpperCase()) |
Consumer | accepts one argument and returns no result | .forEach(s -> System.out.println(s)) |
Supplier | returns a result each time it invoked | .collect(Collectors.toList()) |
Predicate | accepts one argument and returns a boolean | filter(s -> s.startsWith("A")) |
BiFunction | accepts two arguments and produces a result | |
BiConsumer | accepts two arguments and returns no result | |
BinaryOperator | accepts two argument of same type and produces a result of same type | .reduce(0, (a, b) -> a+b) |
Q6. What is a Method Reference?
A method reference is a Java 8 construct that can be used for referencing a method without invoking it using double colon ::
operator. It is used for treating methods as Lambda Expressions. They only work as syntactic sugar to reduce the verbosity of some lambdas. See below code, how method reference has reduced the verbosity over lambda expression.
List<String> languages = Arrays.asList("java", "javascript", "css");
// Lambda expression
languages.forEach(str -> System.out.println(str));
// Method Reference
languages.forEach(System.out::println);
Double colon ::
is basically refers to a single method, and this single method can be a
- A Static method
ClassName::staticMethodName
e.g. String::valueOf, Integer::parseInteger, Double::parseDouble - An Instance method
Object::instanceMethodName
e.g. String::toString, System.out::println, String::toUpperCase - A Constructor
ClassName::new
e.g. String::new - A Super method
super::parentMethodName
Read Double Colon Operators in Java 8 for more details on method reference.
Q7. What is a Lambda Expression and What is it used for?
In very simple terms, a lambda expression is a function that can be referenced and passed around as an object.
Lambda expressions introduce functional style programming in Java and facilitate the writing of compact and easy-to-read code.
Because of this, lambda expressions are a natural replacement for anonymous classes as method arguments. One of their main uses is to define inline implementations of functional interfaces.
Initialize a thread
//Java 7
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("Thread started in Java7");
}
});
t1.start();
//Java 8 Lambda Expression
Thread t2 = new Thread(() -> System.out.println("Thread started in Java8"));
t2.start();
Sort a list
List<String> a1 = Arrays.asList("equity", "stocks", "gold", "foreign exchange","fixed income", "future");
//Java 7
a1.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
//Java 8 Lambda Expression
a1.sort((o1, o2) -> o2.compareTo(o1));
Q8. Explain the Syntax and Characteristics of a Lambda Expression?
A lambda expression consists of two parts: the parameter part and the expressions part separated by a forward arrow as below:
// Syntax
param -> expression
Any lambda expression has the following characteristics:
- Optional type declaration – when declaring the parameters on the left-hand side of the lambda, we don’t need to declare their types as the compiler can infer them from their values.
- Optional parentheses – when only a single parameter is declared, we don’t need to place it in parentheses. But when more than one parameter is declared, parentheses are required
- Optional curly braces – when the expressions part only has a single statement, there is no need for curly braces. But curly braces are required when there is more than one statement
- Optional return statement – when there is only single statement, then we can omit return keyword from return statement.
// Lamda Expression
Stream.of("java", "spring", "spring boot").filter((String s) -> { return s == "java"; });
// Removed Optional Type Declaration, Parentheses, Curly Braces and Return Keyword
Stream.of("java", "spring", "spring boot").filter(s -> s == "java");
Q9. What is a Stream? How does it differ from a Collection?
In simple terms, a stream is an iterator whose role is to accept a set of actions to apply on each of the elements it contains.
The stream represents a sequence of objects from a source such as a collection, which supports aggregate operations. They were designed to make collection processing simple and concise. Contrary to the collections, the logic of iteration is implemented inside the stream, so we can use methods like map and flatMap for performing a declarative processing.
Another difference is that the Stream API is fluent and allows pipelining:
int sum = Arrays.stream(new int[]{1, 2, 3})
.filter(i -> i >= 2)
.map(i -> i * 3)
.sum();
And yet another important distinction from collections is that streams are inherently lazily loaded and processed.
Read Streams in Java 8 for more examples on streams.
Q10. What is the difference between Intermediate and Terminal Operations?
Stream operations are combined into pipelines to process streams. All operations are either intermediate or terminal.
Intermediate vs Terminal Operations
- Output: Output of intermediate operation is another stream whereas output of terminal operation is a collection, array or primitive.
- Chaining: Stream operation pipeline can have as many as intermediate operators chained together but pipeline must end with terminal operator.
- Lazy Evaluation: Intermediate operations are evaluated lazily whereas terminal operations are eager. The intermediate operations just remain as a pipeline, and executed only when the terminal operation is executed
- Pipeline: Stream operations pipeline can have many intermediate operations but only one terminal operation.
Examples of Operations are as follows:-
- Intermediate Operations:
filter()
,map()
,flatMap()
,sorted()
,distinct()
,limit()
,skip()
- Terminal Operations:
forEach()
,collect()
,reduce()
,min()
,max()
,count()
,average()
,sum()
,anyMatch()
,allMatch()
,noneMatch()
,findFirst()
,findAny()
Lazy Loading of Intermediate Operations
Let’s look at the example to understand the lazy evaluation of intermediate operation:-
System.out.println("Stream without terminal operation");
Arrays.stream(new int[] { 1, 2, 3 })
.map(i -> {
int n = i*2;
System.out.println("doubling " + i + "=" + n);
return n;
});
Output
Stream without terminal operation
We see that map()
intermediate operations is not executed when there is no terminal operation.
System.out.println("Stream with terminal operation");
Arrays.stream(new int[] { 1, 2, 3 })
.map(i -> {
int n = i*2;
System.out.println("doubling " + i + "=" + n);
return n;
}).sum();
Output
Stream with terminal operation
doubling 1=2
doubling 2=4
doubling 3=6
We see that map()
intermediate operations is executed only when a terminal operation sum()
exists.
Q11. What is the difference between Map and flatMap Stream Operations?
Both map and flatMap are intermediate stream operations that receive a function and apply this function to all elements of a stream.
The difference is that for the map, this function returns a value, but for flatMap, this function returns a stream. The flatMap operation “flattens” the streams into one.
Here is an example of using map
to get list of user’s name in uppercase and flatMap
to get a flat list of user’s phone numbers:-
Map<String, List<String>> user = new HashMap<>();
user.put("John", Arrays.asList("555-1123", "555-3389"));
user.put("Mary", Arrays.asList("555-2243", "555-5264"));
user.put("Steve", Arrays.asList("555-6654", "555-3242"));
List<String> names = user.keySet().stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.print(names); //[STEVE, JOHN, MARY]
List<String> phones = user.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(phones); //[555-6654, 555-3242, 555-1123, 555-3389, 555-2243, 555-5264]
Q12. What is the difference between findFirst and findAny Stream Operations?
findFirst
operation always return the first elements of the stream irrespective of sequential or parallel streamfindAny
operation behavior is explicitly nondeterministic. It is free to select any element in the stream. This is to allow for maximal performance in parallel operations.
List<String> users = Arrays.asList("David", "Jack", "Duke", "Jill", "Dany", "Julia", "Peter");
Optional<String> findFirst = users.parallelStream().filter(s -> s.startsWith("D")).findFirst();
Optional<String> findAny = users.parallelStream().filter(s -> s.startsWith("D")).findAny();
System.out.println(findFirst.get()); //Always print David
System.out.println(findAny.get()); //Print David or Duke or Dany
Q13. What is Effectively Final in Java 8?
Java 8 has introduced a new concept called “effectively final” variable. A non-final local variable or method parameter whose value is never changed after initialization is known as effectively final.
If you remember, prior to Java 8, we cannot use a local variable in an anonymous class. If you have to access a local variable in the Anonymous class, you have to make it final
.
In Java 8, you can use non-final local variable in an anonymous class or lambda expression, if its value is never changed. Java internally consider these variables as effectively final and allow them to use.
The below code will throw error prior to Java 8 since message
variable is not final
, however it works just fine with Java 8 and above.
// Effectively Final "message" variable
String message = "Thread Started";
// Use in Anonymous Class
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
System.out.println(message + " in Java7");
}
});
t1.start();
// Use in Lambda Expression
Thread t2 = new Thread(() -> System.out.println(message + " in Java8"));
t2.start();
Q14. What are the enhancements in Data and Time APIs in Java 8?
The older date and time APIs before Java 8 are:-
java.util.Date
- represents a specific instant in time, with millisecond precisionjava.util.Calendar
- an abstract class that provides methods for converting between a specific instant in time and a set of calendar fieldsjava.util.Timezone
- represents a time zone offset, and also figures out daylight savings
The new data and time APIs in java.time.*
package introduced in Java 8:-
java.time.LocalDate
- A date without time-zone in the ISO-8601 calendar system, such as 2007-12-03java.time.LocalTime
- A time without a time-zone in the ISO-8601 calendar system, such as 10:15:30java.time.LocalDateTime
- A date-time without a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30java.time.ZonedDateTime
- A date-time with a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30+01:00 Europe/Parisjava.time.Period
- A date-based amount of time in the ISO-8601 calendar system, such as ‘2 years, 3 months and 4 days. used to measure quantity of time in terms of years, months and daysjava.time.Duration
- A time-based amount of time, such as ‘34.5 seconds’. used to measure quantity of times in terms of seconds and nanoseconds.
Enhancement in Java 8 date and time APIs are as follows:-
- Java 8 date and time APIs in
java.time.*
package are immutable and thread-safe whereas olderjava.util.*
package APIs are not. - Java 8 date and time APIs provide many useful methods for date and time calculation, which are missing in older APIs
- Java 8 provides
ZonedDateTime
to deal with time zone specific date and time directly, which is not easy to deal in older APIs - Java 8 provides many other classes such as
Clock
,Instant
,DayOfWeek
,Month
,Year
to deal with date and time at granular level
Q15. What is Optional in Java 8?
Before Java 8, there was no mechanism to check whether there is null value expected out of an API, and we had to just rely on null check.
Java 8 introduced java.util.Optional
, which is a container for an object which may or may not contain a non-null value. This class has various utility methods:-
empty()
- returns an emptyOptional
objectof(T value)
- returns anOptional
with the specified present non-null value.ofNullable(T value)
- Returns an Optional describing the specified value, if non-null, otherwise returns an empty Optional.isPresent()
- returns true if value is presentget()
- returns value of the objectifPresent()
- execute the block of code if value is presentorElse()
- return default value if value not present
Optional<String> empty = Optional.empty();
empty.isPresent(); //false
Optional<String> opt = Optional.of("hello");
opt.isPresent(); //true
opt.get() //hello
opt.ifPresent(name -> System.out.println(name.length())); //5
Optional<String> nullable = Optional.ofNullable(null);
nullable.isPresent(); //false
nullable.orElse("world"); //world
nullable.orElseGet(() -> "world") //world
// usage with sptreams API
User user = users.stream().findFirst().orElse(new User("default", "1234"));