A helpful guide to Java features, FAQs, and interview tips for Java developers.

8.Can a lambda expression reference variables from its surrounding scope?
Yes, lambda expressions can access variables from their surrounding scope, with one condition: the variable has to be effectively final, meaning it gets assigned once and then it is not changed any further, allowing the safe use in the lambda expression in concurrent contexts.
Example:
int num = 5;
Runnable task = () -> System.out.println(num);
In this example, num is accessed within the lambda expression because it does not change after its first assignment. If you attempt to change num later in your code, you will get a compilation error. This limitation makes lambda expressions more predictable and ensures thread safety when they are used in multi-threaded environments.


9.What are the limitations of using lambda expressions?

Although lambda expressions have many advantages, they have a few limitations:
1.Single Abstract Method: The Lambda expression is supported only on the functional interface having exactly one abstract method. Interfaces with multiple abstract methods can be used as targets for Lambda, and the whole thing ends in an error.
2.Flexibility Issue: Unlike an anonymous inner class, a lambda expression cannot be applied to create additional methods or overriding default methods defined in the interfaces. Its application is kept pretty simple.
3. Readability Issues: Though lambda expressions can reduce code complexity, it can sometimes increase the complexity of the code if they are too many or include complex logic. The productivity of such code may jeopardize its readability.
4. No Meaningful Names: Lambda expressions do not allow meaningful names for their implementation, hence difficult to debug.
5. Properly Final Variables: Lambda expressions demand variables of the enclosing scope to be properly final, and hence, the use of the variables is possible in only a few contexts.


10.Method References vs Lambda Expressions
Method references are an abbreviation form of lambda expression where the lambda only invokes some existing method.
It improves code readability and makes the code more redundant-free.
Following is an example of the following lambda expression,
list.forEach(item -> System.out.println(item));
it can be written as.
list.forEach(System.out::println);
The :: operator is used to refer to a method.
In Java there are four types of method references.
1.Arbitrary Object Method Reference: ClassName::instanceMethod
2.Constructor Reference: ClassName::new
3.Instance Method Reference: instance::instanceMethod
4.Static Method Reference: ClassName::staticMethod
Method references make your code cleaner, more readable and less repetitive and they work smoothly with functional interfaces.


11.What is Streams API and Why was it created in Java 8?
Streams API in Java 8 was introduced for having an efficient yet expressive way to work with sequences of data. One can perform lots of operations such as filtering, mapping and reduction on collections more easily and more readable as well as more flexible than old approaches.
Key functionality includes streams. This allows you to process data within a pipeline paradigm. That's to say that you can perform operations in streams in a concatenated manner, keeping the code extremely concise and thus readable. Furthermore, streams support another dimension of power. Intermediate operations get executed only after some terminal operation (like collect(), in this case), which means computations are done when needed with a subsequent potential performance increase.
Furthermore, the Streams API allows for parallel processing, so that operations can be divided across several CPU cores in order to make better use of large data sets. This radically changes how data is processed in Java, aligning it with more functional programming principles and making loops unnecessary in many cases, which are always complex and error prone. It is a step toward cleaner and more declarative coding practices in Java.


12.How to create stream from collection or array?
Creating a stream from either a collection or an array is straightforward with the Streams API. Below are the common ways to make streams from these data types:
From a Collection (like List, Set, or Queue):
List<String> list = Arrays.asList("Mango", "Guva", "Pineapple");
Stream<String> streamFromList = list.stream();
From an Array:
String[] array = { "Mango", "Guva", "Pineapple" };
Stream<String> streamFromArray = Arrays.stream(array);
You have the ability with a stream, for example to filter elements of a stream with the help of the mapping operation or even reduction of a stream into some final result. What is more streams provide an alternative approach to manipulating data using the concept of function or expression that declares exactly what you would like to perform and therefore simplifying ways of handling collection in your code using less hard-core loops along with manual iteration of large-scale data as well as operations.


13.Intermediate and terminal operations in the Streams API.
There are two kinds of operations, namely intermediate operations and terminal operations, in the Streams API.
Intermediate operations: These change the stream and bring out another, by which a new stream will let you string. They do execute lazily which means they won't get performed when there are non terminal methods executed. Intermediary operators common include, the filter() mapper() or sometimes distinct. Most of this ensures that declaratively you construct that pipeline processing.
The streams of following: filteredStream
String sstream =stream. filter( t-> s.s tart wi a ("A"));
In this case, the filter() method creates a new stream containing only the elements that start with the letter "A", but the filtering is not done until a terminal operation is called.
Terminal operations: These operations initiate the actual processing of the stream and result in a result or side effect. Once a terminal operation is called, the stream is consumed and cannot be reused again. Examples of terminal operations are collect(), forEach(), reduce(), and count(). Once a terminal operation is called, the processing of the stream is done.
Example:
long count = stream.count();
Count is a method within the stream whose purpose is counting the number of elements in that particular stream. It serves as a final operation and effectively terminates the whole operation of the pipeline.


14.What does a filter() function do in the pipeline of streams?
The filter() method is an intermediate operation in the Streams API. It allows the filtering of elements in a stream based on some condition. The method accepts a predicate, that is a function returning a boolean value. It returns a new stream containing only elements that meet a specified condition. This operation is lazy and it only performs the processing at the time when a terminal operation is called.
Example:
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Avocado");
List<String> filteredFruits = fruits.stream()
.filter(f -> f.startsWith("A"))
.collect(Collectors.toList());
In this example, the filter() method filters the stream to include only those fruits whose names start with the letter "A". The resulting list contains "Apple" and "Avocado". The filtering is not performed immediately but instead happens when a terminal operation like collect() is invoked. This lazy nature of filter() enhances performance by postponing unnecessary computations until required.


15.What is difference between map() and flatMap() in java? Map and flatMap are the intermediate operations, used inside Streams API for the transformation of the elements inside the stream but it is much differentiated from others due to how transformed elements are being processed.
map(): It applies a function to each element in the stream and returns a new stream of transformed elements. The structure of the stream is preserved, so if the original stream has n elements, the new stream will also have n elements but each element is transformed into another form.
Example:
List<String> words = Arrays.asList("apple", "banana", "cherry");
List<String> upperCaseWords = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
In this line of code, the map() method converts every word in the list to uppercase and gives a new list where every word is transformed but the structure remains the same.
flatMap(): Like flatMap() applies a function to each element of the stream but flattens the results into one stream. It's useful in the case of nested structures in which the elements in the stream are collections or streams and you need to join them all together in one single stream. It "flattens" the structure.

List of lists:
List<List<String>> listOfLists = Arrays.asList(
Arrays.asList("apple", "banana"),
Arrays.asList("cherry", "date")
);
List flatList = listOfLists.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
It simply flattens all the inner lists into one single stream so all the elements appear in a flattened structure within the same list.


16.How do you retrieve the output of a stream pipeline?
To extract the output from a stream pipeline, you'll use a final operation the collect() method. It has the effect of transforming the stream into some different form frequently a collection. There is at least one other you should be well familiar with Collectors.toList(). Other common collectors are Collectors.joining() to create a String or more often when using group, Collectors.groupingBy().
Example:
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry");
List<String> result = fruits.stream().filter(f -> f.startsWith("A")).collect(Collectors.toList());
It aggregates the filtered stream into a List by using collect() in this example. Applying filter() picks fruits whose name begins with the letter "A" and the resulting collection returned as a List using the collector Collectors.toList()


17.What are parallel streams, and when should they be used?
Parallel streams allow processing stream elements concurrently with the aid of multiple threads, which makes them a candidate for performance gain in the presence of large data sets or complex computations. Parallel streams can be obtained either through calling parallelStream() on a collection or through conversion of a given stream to a parallel stream by using the parallel() method.
Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.parallelStream().forEach(System.out::println);
Parallel streams are most efficient when the operation performed on each element is independent of the others. For example, summing numbers or filtering large datasets can be accelerated by parallel processing. However, parallel streams should be used with caution because managing multiple threads can create problems, such as ensuring thread safety and handling the additional overhead of thread management. For smaller data, this overhead even degrades the performance making parallel streams inefficient for such use cases.
Parallel Streams should be used when
• The size of the data set is such that parallel processing improves performance
• The operations to be applied to each element of the stream do not depend on each other.
• Shared mutable state can be avoided completely to minimize any form of concurrency.


18.How are reduce() and collect() different from each other in Streams API?
Both reduce() and collect() are terminal operations in the Streams API, though they serve to perform different functions and are applied for different kinds of data transformation:
reduce(): This method combines all elements of a stream into a single result. It needs a binary operator (a function that merges two elements) and applies it successively to all elements in the stream. This is perfect for operations like summing numbers, finding the minimum or maximum value, or concatenating strings.
Example:
int sum = numbers.stream().reduce(0, Integer::sum);
Here, it is used with the reduce() function to add up all the elements in the stream, starting with a value of 0, using the Integer::sum function for the accumulation:.
collect(): The collect() method is applied to collect elements of a stream into a collection: List, Set, or Map. It's more flexible than reduce() as it can accommodate more complex transformations and build collections from streams; thus, for example, operations like grouping, partitioning, or joining the elements are available.
Example:
List<String> result = numbers.stream()
.map(String::valueOf)
.collect(Collectors.toList());
Here, collect() converts every number in the stream into its string representation and collects them into a List.


19.What are the short-circuiting stream operations?
A short-circuiting operation of streams is that it allows stream processing to finish when the results are known early, so a stream doesn't have to perform unnecessary evaluation. Such operations become useful for further performance improvement where it avoids fully processing the rest of the stream given that the sought result can already be determined very early. Most common short circuiting operations involve:
anyMatch(): This operation checks whether any element in the stream satisfies a given condition. It stops the processing and returns true as soon as it finds a match.
allMatch(): It checks whether all elements in the stream satisfy a given condition. It halts the processing immediately if one element fails to match the condition and returns false.
findFirst(): This operation returns the first element in the stream and stops further processing immediately.
Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0); /* Short-circuits when it finds an even number */
AnyMatch() here short-circuit and stop after finding an even number, saving performance by not processing the rest of the elements.


20.Can a stream be reused once it is consumed?
No, a stream once being consumed by a terminal operation cannot be used a second time. Streams are meant to be processed once. Once the collection or the forEach() terminal operation is executed, it cannot be reused for another operation. Instead, in order to process the same data again, you have to create a new stream from the original data source.
Example:
stream.forEach(System.out::println);
stream.forEach(System.out::println); /* Error: stream is already consumed */
Above, the second call to the method forEach() will cause an error since the stream has already been consumed by the first terminal operation.


21.What is the Optional class in Java used for?
The Optional class in Java is a container object which either may contain a non-null value or be empty. Introduced in Java 8, it was meant to minimize the chance of getting a NullPointerException and provide a safer way of handling null values in a more structured way. Instead of returning null from methods, Optional allows developers to return an instance that explicitly communicates whether a value is present or not. That makes the code more expressive and easy to understand.
You avoid the hassle of manual null checking using Optional and take advantage of methods like ifPresent(), orElse() and map() when handling the potential nulls in a functional programming way. That brings you clean, readable code without those null-related bugs.


22.How do you create an Optional object using of(), ofNullable() and empty()?
The main ways to obtain an instance of Optional are with the of(), ofNullable() and empty() methods. The idea is to apply each one for a concrete situation:
of(): Used when you're sure the value is not null. It returns an Optional containing the specified value. If you attempt to pass in a null, it will raise a NullPointerException.
Optional<String> optional = Optional.of("Hello, World!");
ofNullable(): This method is used to deal with possibly-null values. If it has a non-null value, it returns an Optional containing the value. If the value is null, it returns an empty Optional.
Optional<String> optional = Optional.ofNullable(null); // Returns Optional.empty()
empty(): It returns an explicitly empty Optional, which means there is no value in it.
Optional<String> emptyOptional = Optional.empty();
These methods ensure a safe and flexible approach to dealing with optional values in Java. Instead of using the rather old-school checks of null data, a more recent approach offers these benefits.


23.Difference between isPresent() and ifPresent() in Optional?
The methods isPresent() and ifPresent() in the Optional class of Java are there to check for the presence of a value inside the Optional object, but it is used differently in both the methods:
1. isPresent()
Return Type: It returns a boolean value. It will return true, if the Optional contains a non-null value otherwise, it returns false.
Usage: It is very often used in the traditional control flow structures like if else allowing manual handling of the Optional content.
Example:
Optional<String> optional = Optional.of("Hello");
if (optional.isPresent()) {
System.out.println("Value is present: " + optional.get());
} else {
System.out.println("No value present");
}
In this example:
isPresent() checks if the Optional contains a value.
If it does, the value is retrieved using get() and printed.
If it's empty, it will print "No value present".
2. ifPresent()
Return Type: It returns void and takes a Consumer as a parameter. The code block inside the Consumer is executed only if the Optional contains a value.
Usage: This is a more modern and functional approach, encouraging the use of lambda expressions to perform actions when a value is present.
Example:
Optional<String> optional = Optional.of("Hello");
optional.ifPresent(value -> System.out.println("Value is present: " + value));
In this example:
ifPresent() automatically checks if the Optional contains a value.
If available, it calls the lambda function that prints the value.
No explicit if-else statement is required.
Summary:
isPresent() is a traditional approach, returning a boolean to manually check the presence of a value.
ifPresent() is a more modern, functional approach executing code when a value is present and encouraging lambda expressions.


24.What is the difference between orElse(), orElseGet(), and orElseThrow()?
The orElse(), orElseGet(), and orElseThrow() methods of the Optional class are used to provide a fallback value when the Optional is empty. Here's how they differ in terms of usage and behavior:
1. orElse()
Usage: It returns the value contained in the Optional if the Optional is non-empty. Otherwise, it returns the value passed as an argument. The default value is always evaluated even if the Optional does contain a value.
Example:
Optional<String> optional = Optional.empty();
String result = optional.orElse("Default Value");
System.out.println(result); // Prints: Default Value
In this example:
orElse() returns "Default Value" because the Optional is empty.
2. orElseGet()
Usage: Similar to orElse() but instead of passing a value directly, accepts a Supplier a function producing a value. This method thus is more effective if the cost of fallback computation is expensive since it need be computed when the Optional would otherwise be empty.
Example:
Optional<String> optional = Optional.empty();
String result = optional.orElseGet(() -> "Computed Default Value");
System.out.println(result); // prints: Computed Default Value
orElseGet() defers the computation of "Computed Default Value" until the Optional is empty.
3. orElseThrow()
Usage: Throws an exception if the Optional is empty. You can pass a supplier that provides a custom exception, or use a predefined one. This is useful when you want to enforce that a value must be present.
Example:
Optional<String> optional = Optional.empty();
String result = optional.orElseThrow(() -> new IllegalArgumentException("Value is missing"));
In this example:
Since the Optional is empty, an IllegalArgumentException is thrown with the message "Value is missing".
Summary:
orElse() returns the fallback value immediately and always does the computation.
orElseGet() defers doing the computation for the fallback value and uses a Supplier, if the computation expensive.
orElseThrow() throws the exception if the Optional is empty, ensuring the value must not be missing.


25.How can Optional help prevent NullPointerException?
The Optional class in Java helps prevent NullPointerException by offering a more explicit way to handle cases where a value might be absent. Instead of returning null, a method can return an Optional, which clearly indicates that the value might be missing. This forces the caller to handle the potential absence of a value in a safe and predictable manner, rather than risking a NullPointerException when trying to access a null reference.
Some ways Optional helps avoid NullPointerException:
•Explicit handling: With methods like isPresent(), ifPresent(), orElse(), and orElseThrow(), the developer is required to explicitly handle the case when the value is not present. This leads to safer, more predictable code.
•No null values: By using Optional, you avoid returning null values and reduce the need for null checks scattered throughout the codebase.
•Cleaner code: It leads to more readable and maintainable code, as it encourages the use of functional-style methods to handle the absence of a value rather than relying on conditional null checks.


26.What are default methods in interfaces? Why were they introduced?
Default methods in Java are the methods defined in an interface which come with a body (implementation). In the traditional interface, methods are abstract and do not have any implementation. Default methods were introduced in Java 8 to solve the problem of backward compatibility when adding new methods to interfaces.
Reasons for Introduction:
Adding a new method to an interface prior to Java 8 will break existing code for all implementing classes because every class needs to implement the new method. Now, using default methods, developers can add new methods to interfaces without breaking the existing classes that implement those interfaces.
Introducing default methods assists in keeping backward compatibility with previous versions of the interfaces but provides the scope to evolve and extend the interfaces in the long run. This helps in avoiding forced changes in all the classes implementing the interface, hence making it easy to evolve the libraries and APIs.
Example
interface MyInterface {
//Default method with implementation
default void defaultMethod(){
System.out.println("This is a default method");
}
}
class MyClass implements MyInterface {
// No need to implement defaultMethod(), it's inherited
}
public class Main{
public static void main(String[] args {
MyClass obj = new MyClass();
obj.defaultMethod(); // Output: This is a default method
}
}
Summary:
Default methods enable the addition of new methods with an implementation to interfaces without causing existing code to break. They support backward compatibility by not forcing all implementing classes to update and implement new methods. Default methods provide more flexibility in the evolution of an interface.


27.How do static methods in interfaces differ from normal static methods?
There are static methods in interfaces and regular static methods in classes. Although the difference between the two is almost negligible, where they are defined and how to access them does differ as below:
1. Static Methods in Interfaces
Static methods in an interface are declared using the keyword static inside the interface. Their usage is accessible directly using the interface name.
Purpose: Interface is used for utility methods that are relevant to the interface. They serve as an auxiliary function for the functionality of an interface. In an interface, static methods don't require any instance of the interface or any implementing class to be called.
Available from: Java 8
Example:
interface MyInterface {
static void staticMethod() {
System.out.println("This is a static method in an interface");
}
}
public class Main {
public static void main(String[] args) {
MyInterface.staticMethod(); /* Output: This is a static method in an interface*/
}
}
2. General Static Methods in Classes
Definition: Static methods of a class are defined inside the class and called using the class name. It is not coupled with any interface and generally pertains to some purpose other than the class.
Purpose: In classes, static methods give utility functions or operations that have no dependency upon the state of the instances; therefore, a static method can be called without making an instance of the class.
Example:
class MyClass{
static void staticMethod() {
System.out.println("This is a static method in a class");
}
}
public class Main {
public static void main(String[] args) {
MyClass.staticMethod(); /* Output: This is a static method in a class*/
}
}
Static methods are provided in an interface to present utility methods. Such utility methods are related solely to the interface and can be accessed using the name of the interface. Static methods in classes find much more vast application and use. They are referred to using class name. In both these contexts, static methods are accessed without an instance creation of the respective class or interface but the difference is in their context and application.


28.Can overriding be done with default methods in implementing classes?
Yes, default methods in interfaces can be overridden in implementing classes. Default methods are methods in an interface that have an implementation and a class implementing the interface can choose to override these methods with its own specific behavior. If the class does not override the default method, the default implementation from the interface will be used. This feature provides flexibility in evolving interfaces without breaking existing implementations.
For example:
interface GreetingService {
default void greet() {
System.out.println("Hello, this is the default greeting!");
}
}
class CustomGreetingService implements GreetingService {
@Override
public void greet() {
System.out.println("Hello, this is a custom greeting!");
}
}
public class OrdMain
{
public static void main(String[] args) {
CustomGreetingService customGreetingService = new CustomGreetingService();
customGreetingService.greet(); /* Output: Hello, this is a custom greeting! */
}
}
In the above example, CustomGreetingService overrides the greet() method to provide a custom greeting. If it had not overridden the method the default greeting would have been used.


29.What happens when two interfaces provide default methods with the same signature?
When a class implements two interfaces that have default methods with the same signature, i.e. same name and parameters a compilation error is raised. This is because the Java compiler cannot determine which default method to use. To resolve this conflict, the implementing class must explicitly override the method and provide its own implementation. This ensures that there is no ambiguity and the behavior is clearly defined.
For example:
interface FirstInterface {
default void printMessage() {
System.out.println("Message from FirstInterface");
}
}
interface SecondInterface {
default void printMessage()
{
System.out.println("Message from SecondInterface");
}
}
class ConflictResolver implements FirstInterface, SecondInterface
{
@Override
public void printMessage()
{
System.out.println("Message from ConflictResolver class");
}
}
public class Main { public static void main(String[] args)
{
ConflictResolver conflictResolver = new ConflictResolver();
conflictResolver.printMessage(); /* Output: Message from ConflictResolver class*/
}
}
In this case, ConflictResolver solves the conflict by overriding the printMessage() method by specifying its own implementation.


30.How does default and static methods help backward compatibility?
Backward compatibility Java 8 allowed interfaces to use default and static methods. Before it was introduced, if a new method was to be added in an interface then all the classes which implement that interface have to be updated to override that method otherwise their code will start breaking. Through default and static methods, developers can add new methods to interfaces without requiring changes to the implementing classes.
The default methods provide means to add new functionality without breaking existing implementations. It means that if the class does not implement the new default method, then the default behavior implemented by the interface will be used.