Java 8 Functional Programming Interview questions focusing on FP. Getting a good handle on Java OOP concepts and FP concepts are very important NOT only from writing quality code perspective, but also from job interviews perspective to be able to do well in written tests & technical interviews.
Q1. Can you explain your understanding of Functional Programming (FP)?
A1. In a very simplistic approach FP means:
#1 Programming without assignments: For example, in imperative style programming like OOP, you can say “x = x + 5”, which is an assignment, but in mathematical or functional programming you need to say f(x) -> x + 5. If, x were to be 2, it is mathematically incorrect to say “2 = 2 + 5”. In imperative style you are assigning a new value of 7 to x by saying “x = x + 5”. So, imperative programming has state and you can assign a new state. FP treats computation as the evaluation of mathematical functions and avoids storing state and mutating data.
#2 FP focuses on WHAT to be done, and NOT on HOW to be done: For example, in the ‘functional style loop’ you’ll focus only on the action of what to do on each element and you don’t have to concentrate on how to go through each element. The “forEach” function will take care of it.
Here is a simple example that shows imperative (e.g. OOP) and functional styles to loop through 1 to 10, square each number, and print squared numbers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package com.mytutorial; import java.util.stream.IntStream; public class WorkWithNumbers { public static void main(String[] args) { WorkWithNumbers wwn = new WorkWithNumbers(); wwn.printSquaresUptoTenImperativeStyle(); System.out.println(); // newline wwn.printSquaresUptoTenFunctionalStyle(); } //Imperative style public void printSquaresUptoTenImperativeStyle() { for (int i = 1; i <= 10 ; i++) { System.out.print(i*i + " "); } } //FP style public void printSquaresUptoTenFunctionalStyle() { IntStream.rangeClosed(1, 10) // range function takes 2 parameters "startIclusive" and "endInclusive" .map(i -> i * i) // map function to transform each element .forEach(i -> { // forEch function focuses only on the "action" of what to do on each element System.out.print(i + " "); }); } } |
printSquaresUptoTenImperativeStyle
The variable “i” takes new value in each iteration. This is assignment as described in #1. The “for (int i = 1; i <= 10 ; i++)" defines "HOW to" go through each element as discussed in #2.
printSquaresUptoTenFunctionalStyle
1) Consists of 3 functions: “rangeClosed”, “map”, and “forEach”. The mutable state is avoided, and the output of a function is exclusively dependent on the values of its inputs. This means that if we call a function x amount of times with the same parameters we’ll get exactly the same result every time. The mutable state is avoided means a new copy of the IntStream is passed beetween calls to rangeClosed, map, and forEach without modifying the previous output.
2) The “rangeClosed” function takes two arguments “startIclusive” and “endInclusive” to produce a range of integers (i.e 1 to 10).
3) The “map” function transforms a Collection, List, Set or Map. By using a “map” function you can apply a predefined function or a user defined function. You can not only use a lambda expression (e.g. i -> i * i), but also you can use a method reference (e.g System.out::println).
4) The “forEach” function loops through each element and performs the “action” of printing each squared integers. Focuses on what to do as per #2. Does not have to worry about “how to” iterate as “forEach” function will take care of the iteration logic.
Q2. Can you list the differences between OOP and FP?
A2. Top 6 tips to transforming your thinking from OOP to FP with examples
Q3. What are the 6 characteristics of FP you need to be familiar with?
A3.
1) A focus on what is to be computed rather then how to compute it.
2) Function Closure Support
3) Higher-order functions
4) Use of recursion as a mechanism for flow control
5) Referential transparency
6) No side-effects
Each of the above is explained in detail with examples at Top 6 tips to transforming your thinking from OOP to FP with examples
Q4. What do you understand by the terms streams, lambdas, intermediate vs terminal ops, and lazy loading with regards to FP?
A4. A stream is an infinite sequence of consumable elements (i.e a data structure) for the consumption of an operation or iteration.
An Intermediate operation like “map” produces another stream, whereas a “Terminal” operation like “forEach” produces an object or an output that is not a stream.
Intermediate operations are lazy operations, which will be executed only after a terminal operation is called. For example, when you call the intermediate operation “.map(i -> i * i)” the lambda body isn’t executed until after the terminal operation “.forEach(i -> { System.out.print(i + ” “);});” is called.
Learn more in detail with examples & diagrams: Java 8 Streams, lambdas, intermediate vs terminal ops, and lazy loading with simple examples
Q5. Why is it essential to understand lazy loading with regards to FP?
A5. This is essential to understand from the viewpoint of adding break points in your IDE for debugging purpose.
Q6. What is a functional interface?
A6. java.lang.Runnable, java.util.Comparator, and java.util.concurrent.Callable are Single Abstract Method interfaces (SAM Interfaces). For example, java.lang.Runnable has a single abstract method
1 2 3 4 5 6 7 |
package java.lang; public interface Runnable { public abstract void run(); } |
and these methods were called by anonymous classes like
1 2 3 4 5 6 7 8 |
new Thread(new Runnable() { @Override public void run() { System.out.println("Running in a new thread"); } }).start(); |
Functional interfaces introduced in Java 8 are recreation of SAM(Single Abstract Method) interface to allow functional programming with lambda expressions by adding an annotation @FunctionalInterface which can be used for compiler level errors when the interface you have annotated is not a valid Functional Interface.
1 2 3 4 5 6 |
@FunctionalInterface public interface Summable { public int sum(int input1, int input2); //abstract method. keyword abstract is optional in an interface } |
Note: The method “sum” is an abstract method as the keyword “abstract” is optional in an interface.
Now, the following “lambda expression” is assignable to the functional interface type Summable.
1 2 3 |
Summable sumType = (a, b) -> a + b; |
So, the lambda expression has two parts:
Part 1: The body of the expression denoted by “(a, b) -> a + b;”. This is equivalent to an anonymous method. aka no name method.
Part 2: The signature of the lambda expression via a functional interface. A functional interface is a single method interface. The functional interface takes two input arguments of types integer and returns a result of type integer.
1 2 3 4 5 6 7 8 9 |
public class LambdaAssignedToFunctionalInterfaceType { public static void main(String[] args) { Summable sumType = (a, b) -> a + b; int result = sumType.sum(5, 6); System.out.println("result=" + result); } } |
Outputs:
1 2 3 |
result=11 |
Now, the same example with Generics included.
1 2 3 4 5 6 7 |
@FunctionalInterface public interface Summable<T, U, R> { public R sum(T input1, U input2); // abstract method. keyword abstract // is optional in an interface } |
Where, T, and U are input arguments type and R is a result type.
1 2 3 4 5 6 7 8 9 |
public class LambdaAssignedToFunctionalInterfaceType { public static void main(String[] args) { Summable<Integer, Integer, Integer> sumType = (a, b) -> a + b; int result = sumType.sum(5, 6); System.out.println("result=" + result); } } |
A new package “java.util.function” is added with a number of functional interfaces to provide target types for lambda expressions and method references. E.g. Function
This means, in most cases you don’t have to define your own Functional Interface like “Summabale”. You can reuse the “java.util.function.BiFunction” functional interface that takes 2 input arguments and returns a result as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 |
import java.util.function.BiFunction; public class LambdaAssignedToFunctionalInterfaceType { public static void main(String[] args) { BiFunction<Integer, Integer, Integer> sumType = (a, b) -> a + b; int result = sumType.apply(5, 6); System.out.println("result=" + result); } } |
Outputs:
1 2 3 |
result=11 |
Refer to:
1) Java 8 using the Predicate functional interface.
2) Java 8 API examples using lambda expressions and functional interfaces
Q7. What does the @FunctionalInterface do, and how does it differ from the @Override annotation?
A7. The @Override annotation takes advantage of the compiler checking to make sure you actually are overriding a method when you think you are. This way, if you make a common mistake of misspelling a method name or not correctly matching the parameters, you will be warned that you method does not actually override as you think it does. Secondly, it makes your code easier to understand because it is more obvious when methods are overwritten.
The annotation @FunctionalInterface acts similarly to @Override by signalling to the compiler that the interface is intended to be a functional interface. The compiler will then throw a compile-time error
1) If the interface has no abstract methods. Try removing the “sum” method shown above, you will get the following compile-time error: “Invalid ‘@FunctionalInterface’ annotation; Summable is not a functional interface”.
2) If the interface has multiple abstract methods. Following gives a compile-time error.
1 2 3 4 5 6 7 |
@FunctionalInterface public interface Summable { public int sum(int input1, int input2); public int subtract(int input1, int input2); } |
The “@FunctionalInterface” annotation is an optional facility to avoid accidental addition of abstract methods in the functional interfaces. It is a good practice to use this annotation where applicable.
Q8. Can you have method implantations in Java interfaces?
A8. Yes. Prior to Java 8, you could have only “method declarations” in the interfaces. One of the key design changes in Java 8 is that you can have default methods and static methods in the interfaces. This means, from Java 8 onwards you can define behaviors (i.e. implementation) in interfaces.
Q9. Does this mean that abstract classes are not required any more?
A9. No. You can only define behaviors via default and static methods, but you can’t maintain states by defining non-final variables. Absract classes are required to define default behavior with state.
Q10. Can a functional interface have any number of default and static methods?
A10. Yes. only 1 abstract method (e.g sum), and any number of default and static methods as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@FunctionalInterface public interface Summable { static final int var = 0; int sum(int input1, int input2); // static method static int subtract(int input1, int input2) { return input1 - input2; } // default method default int multiply(int input1, int input2) { return input1 * input2; } // default method default int divide(int input1, int input2) { return input1 / input2; } } |
Q11. Why were default methods introduced in Java 8?
A11. In order to support functional programming, new intermediate and terminal operations like map, forEach, etc had to be added so that the Java collection API can work with lambda expressions like “i -> i * i” and “i -> {System.out.print(i + ” “);}”. One way to enable this is by adding these new methods to existing interfaces like java.util.Collection, java.util.List, etc and then providing the implementations where required. But this approach has a problem. Once the JDK is published, it would not be possible to add new methods to those interfaces by extending those interfaces without breaking the existing implementation. Hence this new concept of “default methods” was introduced to provide default implementation of the declared behavior.
An example where a collection if integers is converted to a stream and then printed
1 2 3 4 |
List<Integer> numbers = Arrays.asList(1,2,3,4,5); numbers.stream().forEach(i -> System.out.print(i + " ")); |
The stream() method returns a “Stream” interface, which has all the methods that take functional interfaces like Supplier, BiConsumer, Function, etc as arguments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package java.util.stream; public interface Stream<T> extends BaseStream<T, Stream<T>> { <R> Stream<R> map(Function<? super T, ? extends R> mapper); void forEach(Consumer<? super T> action); <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner); //more abstract and static methods } |
Q12. What are the benefits of default and static methods?
A12.
1) Default methods enhance the Collection API to support lambda expressions.
2) Default methods will help you in extending existing interfaces without breaking the implementation classes as explained in “A11”.
3) Default methods eliminate the need for the base classes to provide default behaviors. The interfaces can provide default behaviors, and the implementing classes can choose to override the default behaviors.
4) Default and Static methods can be added to the interface itself without requiring separate utility classes like java.util.Collections, java.util.Arrays, etc.
5) The interface static methods are handy for providing utility methods like null checks and collection sorting/shuffling.
6) Unlike the default methods, the interface static methods prevent the implementing classes from accidentally overriding them.
Q13. What happens to a class that implements two or more interfaces with the same default method name and signature?
A13. You will get a compile-time error in the implementing class to define its own method with the same default method name and signature to resolve the conflict.
Q14. Can a default method override a method from java.lang.Object class?
A14. No. It’s not possible because the “java.lang.Object” is the implicit base class for all Java classes. This means even if you have Object class methods defined as default methods in interfaces, the Object class method will always be used. So, no point in allowing interfaces to override a method from java.lang.Object class.
Q15. Can an interface static method override a method from java.lang.Object class?
A15. No. You will get a compile-time error: “This static method cannot hide the instance method from Object”. As mentioned earlier, the Object is implicitly the base class for all the other Java objects and you can’t have one class level static method and another instance level method with the same signature.
Q16. What is currying? Does Java 8 support currying?
A16. Currying (named after Haskell Curry) is the fact of evaluating function arguments one by one, producing a new function with one argument less on each step. Java 8 still does not have first class functions, but currying is “practically” possible with verbose type signatures.
Learn more with examples: What is currying? Does Java 8 support currying?
Q17. What do you understand by the term referential transparency with respect to functional programming?
A17. In FP, a function will return the same result for invocations with the same parameters, and this is known as “referential transparency”. This allows the result to be easily cached and returned any number of times to improve performance. This enables lazy evaluation that I mentioned earlier to defer the computation of values until the point when they are needed.
Q18. What is a pure function?
A18. Functions with absolutely no side effects or functions that operate on immutable data are known as pure functions. This has several benefits. Firstly, since the data can not be changed accidentally or on purpose, it can be freely shared improving memory requirements and enabling parallelism.
Q19. What is a closure?
A19. A closure is a function that can be stored as a variable, and passed around to other functions with a special ability to access other variables local to the scope it was created in. Since functions are treated like first class citizens, it’s really useful to be able to pass them around together with their referencing environment (i.e. a reference to each non-local variable of that function). Note that Java 8’s support for closures is limited compared to the other functional programming languages like JavaScript. In Java, Lambda expressions as well as anonymous classes can only access to the final or effectively final variables of the enclosing scope.
Interviewers might be quizzing your understanding on this limitation. Learn more about this limited support for closures at Java 8 Functional Programming interview questions on Closures.
Java 8 Functional Programming interview questions & answers
1) Java 8 Functional Programming interview questions on currying.
2) 50+ Java 8 interview questions & answers.
3) Java 8: Functional programming interview questions.