Mastering Java Lambda Expressions: A Comprehensive Guide


Introduction:

Java lambda expressions revolutionized the way we write code by introducing functional programming concepts to the language. Lambda expressions allow us to write more concise and expressive code, enhancing readability and promoting modular design. In this tutorial, we’ll explore lambda expressions in Java, covering their syntax, common use cases, and best practices.

Table of Contents:

  1. What are Lambda Expressions?
  2. Syntax of Lambda Expressions
  3. Functional Interfaces
  4. Working with Lambda Expressions
    • Using Lambda Expressions as Method Arguments
    • Assigning Lambda Expressions to Variables
    • Lambda Expressions with Multiple Parameters
    • Accessing Variables from the Enclosing Scope
  5. Method References vs. Lambda Expressions
  6. Benefits of Lambda Expressions
  7. Common Use Cases
  8. Best Practices for Using Lambda Expressions
  9. Conclusion

Section 1: What are Lambda Expressions?

Lambda expressions are a feature introduced in Java 8 that allows you to write more concise and expressive code by treating functionality as a first-class citizen. In simple terms, lambda expressions enable you to represent anonymous functions as values.

In traditional Java programming, you would typically define an interface with a single abstract method and create an instance of a class that implements that interface to provide the implementation for that method. Lambda expressions provide a more compact alternative by allowing you to define the implementation of the method directly inline, without the need for a separate class.

Lambda expressions are often used in conjunction with functional interfaces, which are interfaces that have exactly one abstract method. The lambda expression provides an implementation for that method, making it a concise way to represent behavior.

The key idea behind lambda expressions is to treat behavior as a value that can be passed around, assigned to variables, and used as method arguments. This functional programming approach promotes modularity and flexibility in your code.

Section 2: Syntax of Lambda Expressions

Lambda expressions consist of three main parts:

  1. Parameters: These are the input parameters that the lambda expression takes. If there are no parameters, you can leave the parentheses empty. If there are multiple parameters, separate them with commas.
  2. Arrow Operator: The arrow operator (->) separates the parameters from the body of the lambda expression. It serves as a visual indicator that the parameters are used to produce the result defined by the expression.
  3. Body: The body of the lambda expression represents the computation or action that the lambda expression performs. It can be a single statement or a block of statements enclosed in curly braces.

Here’s an example of a lambda expression that adds two numbers:

(int a, int b) -> a + b

In this example, the lambda expression takes two integer parameters (a and b) and returns their sum (a + b).

Lambda expressions are commonly used in functional programming constructs and APIs that accept functional interfaces. They enable you to write more expressive and concise code by representing behavior directly inline, without the need for additional classes and method declarations.

Lambda expressions have brought a significant shift in the way Java code is written, enabling developers to embrace functional programming concepts and write cleaner, more modular code.

Section 3: Functional Interfaces

Functional interfaces are a fundamental concept in Java that are closely related to lambda expressions and enable functional programming in the language. In simple terms, a functional interface is an interface that has exactly one abstract method. They provide a way to define the contract for a lambda expression or any other implementation of a single-method interface.

In Java, functional interfaces are annotated with the @FunctionalInterface annotation. While the annotation is not strictly required, it serves as a marker to indicate that the interface is intended to be used as a functional interface. The compiler will enforce the rule of having only one abstract method within an interface marked with @FunctionalInterface.

Functional interfaces can have default methods or static methods, but the key requirement is that they must have exactly one abstract method. This single abstract method represents the primary behavior that the interface expects to define. The other methods can provide additional utility or default implementations.

Java 8 introduced a set of functional interfaces in the java.util.function package to facilitate functional programming and lambda expressions. Some commonly used functional interfaces include:

  1. Supplier<T>: Represents a supplier of results. It has a single abstract method T get() and does not take any arguments but returns a value.
  2. Consumer<T>: Represents an operation that takes a single input argument and returns no result. It has a single abstract method void accept(T t).
  3. Predicate<T>: Represents a predicate (a condition) that takes an argument and returns a boolean value. It has a single abstract method boolean test(T t).
  4. Function<T, R>: Represents a function that takes an argument of type T and returns a result of type R. It has a single abstract method R apply(T t).
  5. BiFunction<T, U, R>: Represents a function that takes two arguments of types T and U and returns a result of type R. It has a single abstract method R apply(T t, U u).

These functional interfaces provide a standardized way to represent common functional programming patterns and facilitate the use of lambda expressions.

By using functional interfaces, you can define behavior that can be passed as arguments to methods, stored in variables, and used as return types. Lambda expressions can be used to implement the single abstract method of a functional interface, allowing for concise and expressive code.

Functional interfaces play a crucial role in enabling functional programming constructs in Java and provide a foundation for leveraging the power of lambda expressions and writing more modular and flexible code.

Section 4: Working with Lambda Expressions

Lambda expressions can be used in various contexts, such as:

  • Method arguments: You can pass lambda expressions as arguments to methods. For example, when working with collections, you can use lambda expressions to define custom sorting or filtering logic.
  • Return values: Lambda expressions can be returned from methods. This is useful when you want to create flexible and reusable code components.
  • Assignments: You can assign lambda expressions to variables and use them as if they were objects.
  • Streams API: Lambda expressions are extensively used with the Streams API to perform operations on collections in a functional and declarative way.

Section 5: Method References vs. Lambda Expressions

  1. Using Lambda Expressions as Method Arguments: Lambda expressions can be passed as arguments to methods, allowing you to define behavior inline without the need for separate classes or explicit implementations. This is commonly used in functional programming constructs and APIs that accept functional interfaces. For example:
List numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(n -> System.out.println(n));

In the above example, the forEach method of the List interface accepts a Consumer functional interface. Instead of explicitly implementing the Consumer interface with a separate class, we pass a lambda expression (n -> System.out.println(n)) that defines the behavior of consuming each element of the list.

  1. Assigning Lambda Expressions to Variables: Lambda expressions can be assigned to variables of functional interface types. This allows you to reuse the lambda expression and provide a more descriptive name for the behavior it represents. For example:
Predicate<Integer> evenNumberFilter = n -> n % 2 == 0;
List<Integer> evenNumbers = numbers.stream()
    .filter(evenNumberFilter)
    .collect(Collectors.toList());

In this example, we create a variable evenNumberFilter of type Predicate<Integer>, which represents a lambda expression that checks if a number is even. We can then use this variable to filter the numbers list using the filter method of the Stream API.

  1. Lambda Expressions with Multiple Parameters: Lambda expressions can take multiple parameters. If you have multiple parameters, separate them with commas. For example:
BiFunction<Integer, Integer, Integer> addFunction = (a, b) -> a + b;
int sum = addFunction.apply(3, 5);  // sum = 8

In this case, we define a lambda expression (a, b) -> a + b that represents a function that takes two integers (a and b) and returns their sum. We assign this lambda expression to a variable of type BiFunction<Integer, Integer, Integer> and then use it to compute the sum of two numbers.

  1. Accessing Variables from the Enclosing Scope: Lambda expressions can access variables from the enclosing scope. These variables are effectively final or effectively effectively final, meaning they are not allowed to be modified within the lambda expression. This allows lambda expressions to capture and use values from the surrounding context. For example:
int factor = 2;
Function<Integer, Integer> multiplier = n -> n * factor;
int result = multiplier.apply(5);  // result = 10
In this example, the lambda expression (n -> n * factor) captures the factor variable from the enclosing scope. The factor variable is effectively final, and we can use it within the lambda expression to multiply the input value.

Working with lambda expressions allows you to write concise and expressive code by representing behavior directly inline. They provide a more modular and flexible way of defining behavior, making your code easier to read and maintain. By leveraging lambda expressions, you can achieve greater code clarity and focus on the core logic of your application.

Section 6: Benefits of Lambda Expressions

Lambda expressions in Java provide several benefits that make your code more concise, readable, and maintainable. Here are some of the key advantages of using lambda expressions:

  1. Conciseness: Lambda expressions allow you to express instances of single-method interfaces (functional interfaces) more concisely. This reduction in boilerplate code makes your code cleaner and easier to understand.
  2. Readability: Lambda expressions can make your code more readable by eliminating unnecessary details. They allow you to focus on the essential logic of a function or operation.
  3. Expressiveness: Lambda expressions enable a more expressive syntax, making it clear what the code is doing. They often read like a sentence, improving the understanding of the programmer’s intent.
  4. Flexibility: Lambda expressions make it easier to pass behavior as an argument to methods. This flexibility is especially useful when working with collections, sorting, filtering, or defining custom behavior.
  5. Functional Programming: Lambda expressions promote functional programming practices in Java. You can write code in a more functional and declarative style, which can lead to more efficient and robust programs.
  6. Parallelism: Lambda expressions are particularly useful when working with the Java Streams API. They allow you to take advantage of parallel processing easily, as operations can be expressed in a way that doesn’t depend on the order of execution.
  7. Reduced Code Duplication: Lambda expressions can help reduce code duplication by allowing you to encapsulate reusable behavior in a concise form. This promotes the DRY (Don’t Repeat Yourself) principle.
  8. Improved API Design: When designing APIs, lambda expressions can provide a more intuitive and user-friendly way for clients to interact with your code. It allows you to design APIs that accept functional interfaces, making them more versatile.
  9. Easier Maintenance: Code that uses lambda expressions is often easier to maintain because it’s more self-contained and less prone to bugs introduced by accidental changes to shared state.
  10. Compatibility: Lambda expressions are backward-compatible, meaning you can use them in Java 8 and later versions without any issues. This makes it possible to gradually adopt newer language features while maintaining compatibility with older code.
  11. Reduced Anonymity: Lambda expressions provide a name (though not explicit) to otherwise anonymous functions, making it easier to identify and debug issues in stack traces and logs.
  12. Improved Performance: In some cases, lambda expressions can lead to improved performance. The JVM can optimize certain operations performed with lambda expressions more effectively than equivalent code written with anonymous inner classes.

Overall, lambda expressions are a valuable addition to Java, enabling more modern and expressive coding styles while maintaining compatibility with older Java code. They encourage best practices, such as code reusability, readability, and functional programming, ultimately leading to more maintainable and efficient applications.

Section 7: Common Use Cases

Lambda expressions in Java are a versatile tool that can be used in a wide range of scenarios to make your code more concise and expressive. Here are some common use cases where you can benefit from using lambda expressions:

  1. Collections and Streams: Lambda expressions are often used with the Java Collections API and Streams API for tasks like filtering, mapping, and reducing elements in a collection.
  2. Sorting: You can use lambda expressions to specify custom sorting criteria for collections.
  3. Event Handling: Lambda expressions are useful when defining event handlers for GUI components or other event-driven programming scenarios.
  4. Concurrency: Lambda expressions can be employed when working with the java.util.concurrent package to define tasks for execution in threads or thread pools.
  5. Functional Interfaces: Implementing and using functional interfaces is a primary use case for lambdas. You can define custom functional interfaces to model specific behaviors and then use lambda expressions to provide implementations.
  6. Optional: Lambda expressions can be used with Java’s Optional class to define actions that should occur if a value is present or not present.
  7. Functional Programming: Lambda expressions enable functional programming techniques in Java, allowing you to write code that treats functions as first-class citizens. This includes passing functions as arguments, returning functions from other functions, and more.
  8. Custom Iteration: When iterating over custom data structures or performing complex iterations, lambda expressions can simplify the code.
  9. Resource Management: In cases where resources need to be managed explicitly, such as opening and closing files or database connections, lambda expressions can be used to define actions to be taken during resource initialization and cleanup.
  10. Dependency Injection: Lambda expressions can be used in dependency injection frameworks to provide implementations of functional interfaces or to specify custom behaviors for components.

Section 8: Best Practices for Using Lambda Expressions

Using lambda expressions effectively in Java can lead to more readable and maintainable code. To ensure you’re following best practices when working with lambda expressions, consider the following guidelines:

  1. Use Lambda Expressions with Functional Interfaces: Lambda expressions are most powerful when used with functional interfaces. Ensure that the interface you are working with has only one abstract method. If it has more than one, the lambda expression won’t be able to determine which method to implement.
  2. Choose Descriptive Parameter Names: Use meaningful parameter names in your lambda expressions. Descriptive names make the code more readable and help others understand the purpose of the lambda.
    • (x, y) -> x + y // Less readable
    • (value1, value2) -> value1 + value2 // More readable
  3. Keep Lambda Expressions Short and Focused: Lambda expressions should be concise and focused on a single task. If a lambda becomes too complex, it may be a sign that it should be refactored into a separate method or function.
  4. Use Method References When Appropriate: If your lambda expression simply calls an existing method, consider using method references for cleaner and more concise code. Method references are often more readable, especially for common operations like System.out::println.
    • list.forEach(System.out::println);
  5. Explicitly Specify Types When Necessary: While Java can often infer types, explicitly specifying types in your lambda expressions can make the code more readable and less error-prone, especially in complex scenarios.
    • (String s) -> s.length() // Explicit type s -> s.length() // Inferred type
  6. Use Parentheses for Clarity: When your lambda expression has multiple parameters or a complex body, use parentheses to make it clearer.
    • (a, b) -> a + b // Clearer
    • a, b -> a + b // Less clear
  7. Avoid Side Effects: Lambda expressions should ideally be stateless and avoid modifying external variables (unless they are effectively final). Avoid side effects that can make code harder to reason about and test.
  8. Exception Handling: Be cautious with exception handling within lambda expressions. Consider wrapping lambda bodies with try-catch blocks when necessary. If exceptions occur, they may be wrapped in UncheckedIOException or UncheckedExecutionException.
  9. Think About Parallelism: When using lambda expressions with the Streams API, think about the potential for parallelism. Ensure that your lambda expressions don’t have any side effects that could cause issues when used in parallel streams.
  10. Testing: When writing unit tests, use lambda expressions to define behavior that can be easily tested. Lambda expressions make it straightforward to pass mock implementations or behavior to test components.
  11. Documentation: Document the intent and purpose of your lambda expressions, especially if they perform complex operations or are part of a public API. Clear documentation helps other developers understand how to use your code effectively.
  12. Code Reviews: As with any code, it’s essential to conduct code reviews when using lambda expressions, especially in team environments. Reviews can help catch issues related to readability, maintainability, and adherence to best practices.
  13. Code Style: Follow your team’s or organization’s coding style guidelines when using lambda expressions. Consistency in coding style helps maintain code readability and understandability.
  14. Profile for Performance: While lambda expressions are generally efficient, it’s a good practice to profile your code to identify any performance bottlenecks, especially when using them in critical sections of your application.

By following these best practices, you can make the most of lambda expressions in Java and ensure that your code remains clean, readable, and maintainable. Lambda expressions are a powerful tool when used appropriately, and they can lead to more expressive and efficient code.

Section 9: Conclusion

Remember that lambda expressions are most beneficial when used with functional interfaces, which have a single abstract method. These interfaces are designed to work seamlessly with lambda expressions and provide a clear and concise way to define behavior. Additionally, lambda expressions encourage a more functional and declarative style of programming, which can lead to cleaner and more maintainable code.

Happy coding with lambda expressions in Java!

Java 17 Features with Detailed Explanation


Java 17 was released on September 14, 2021, and it includes several new features and improvements that developers can use to build better and more efficient applications. In this tutorial, we’ll take a closer look at some of the most important features of Java 17 and how to use them in your projects.

In this tutorial, we’ll cover the following features:

  1. Sealed Classes and Interfaces
  2. Pattern Matching for instanceof
  3. Records
  4. Text Blocks
  5. Switch Expressions
  6. Helpful NullPointerExceptions
  7. Foreign-Memory Access API (Incubator)
  8. Vector API (Incubator)
  9. Enhanced Pseudo-Random Number Generators
  10. Enhanced NUMA-Aware Memory Allocation for G1

1. Sealed Classes and Interfaces:

Sealed Classes and Interfaces, a new language feature that allows developers to restrict the inheritance hierarchy of a class or interface. Sealed classes and interfaces provide greater control over how classes and interfaces can be extended, improving the design of object-oriented systems and making them more secure and maintainable.

Sealed classes and interfaces are defined using the sealed keyword, which restricts the set of classes or interfaces that can extend or implement the sealed class or interface. This restricts the inheritance hierarchy, preventing unauthorized subclasses or interfaces from being created.

The syntax for defining a sealed class or interface is as follows:

public sealed class MyClass permits SubClass1, SubClass2, ... {
    // class definition
}

In this example, the sealed keyword is used to define the class MyClass as a sealed class, and the permits keyword is used to list the permitted subclasses SubClass1, SubClass2, and so on. This restricts the set of classes that can extend MyClass to the specified subclasses.

The same syntax applies to sealed interfaces, as shown in the following example:

public sealed interface MyInterface permits SubInterface1, SubInterface2, … {<br>// interface definition<br>}

In this example, the sealed keyword is used to define the interface MyInterface as a sealed interface, and the permits keyword is used to list the permitted subinterfaces SubInterface1, SubInterface2, and so on. This restricts the set of interfaces that can extend MyInterface to the specified subinterfaces.

Sealed classes and interfaces provide several benefits, including:

  • Improved design: Sealed classes and interfaces provide greater control over the inheritance hierarchy, improving the overall design of the system and making it easier to reason about.
  • Security: Sealed classes and interfaces prevent unauthorized subclasses or interfaces from being created, reducing the risk of security vulnerabilities.
  • Maintainability: Sealed classes and interfaces make it easier to maintain the system over time, as changes to the inheritance hierarchy can be made more safely and with greater confidence.

In summary, sealed classes and interfaces are a new language feature in Java 17 that allow developers to restrict the inheritance hierarchy of a class or interface. By providing greater control over the inheritance hierarchy, sealed classes and interfaces improve the design of object-oriented systems and make them more secure and maintainable.

2. Pattern Matching for instanceof

Pattern matching for instanceof is a new language feature in Java 17 that allows developers to write more concise and expressive code when checking the type of an object. With pattern matching for instanceof, developers can combine a type check with a type cast into a single expression, making the code more readable and less error-prone.

Prior to Java 17, developers would typically use an if statement to check the type of an object and then cast it to the appropriate type. For example:

if (myObject instanceof MyClass) {
    MyClass myClass = (MyClass) myObject;
    // use myClass
}

With pattern matching for instanceof, the above code can be simplified into a single expression:

if (myObject instanceof MyClass myClass) {<br>// use myClass<br>}

In this example, the type check and the cast are combined into a single expression. If myObject is an instance of MyClass, it will be cast to MyClass and assigned to the new variable myClass, which can be used within the if block.

Pattern matching for instanceof also supports the use of the else keyword to specify a default branch, as shown in the following example:

if (myObject instanceof MyClass myClass) {
    // use myClass
} else {
    // handle other types
}

In this example, if myObject is not an instance of MyClass, the code in the else block will be executed instead.

Pattern matching for instanceof provides several benefits, including:

  • Concise and expressive code: Pattern matching for instanceof allows developers to write more concise and expressive code, making it easier to read and understand.
  • Fewer errors: By combining the type check and the cast into a single expression, pattern matching for instanceof reduces the risk of errors that can arise from separate type checks and casts.
  • Improved performance: Pattern matching for instanceof can improve performance by reducing the number of unnecessary casts.

In summary, pattern matching for instanceof is a new language feature in Java 17 that allows developers to write more concise and expressive code when checking the type of an object. By combining the type check and the cast into a single expression, pattern matching for instanceof reduces the risk of errors and improves performance.

3. Records

Records is a new feature introduced in Java 16 and finalized in Java 17 that provides a concise and immutable way to declare classes whose main purpose is to hold data. Records are essentially classes that are designed to store data rather than represent objects with behavior.

In Java, classes are typically created to represent objects that have both data and behavior. However, sometimes we need to create classes that are only used to hold data without any additional behavior. In such cases, creating a traditional class with fields, getters, setters, equals, hashCode, and toString methods can be quite verbose and repetitive.

With records, the syntax is much simpler and more concise. A record is defined using the record keyword, followed by the class name, and then a list of properties within parentheses. Here’s an example of a record definition:

public record Person(String name, int age) {}

In this example, we’ve created a record called Person with two properties: name of type String and age of type int. Note that we didn’t need to explicitly declare constructors, getters, setters, or other methods, because they are automatically generated by the compiler.

With records, you can also add additional methods, such as custom constructors or instance methods. Here’s an example:

public record Person(String name, int age) {
    public Person {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
    
    public String getName() {
        return name.toUpperCase();
    }
}

In this example, we’ve added a custom constructor that checks if the age is negative, and an instance method that returns the uppercase name.

Records also provide a compact and readable way to override the default equals, hashCode, and toString methods. For example, the following record definition:

public record Person(String name, int age) {
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

overrides the default toString method to return a string representation of the Person record.

In summary, records are a new feature in Java 16/17 that provide a concise and immutable way to declare classes whose main purpose is to hold data. They simplify the creation of classes that are only used to hold data without any additional behavior, and provide automatic generation of constructors, getters, setters, equals, hashCode, and toString methods. With records, you can also add additional methods and override default methods in a compact and readable way.

4. Text Blocks

Text blocks provide a more readable way to declare multi-line strings in Java 17. Text blocks can contain line breaks and other whitespace characters without requiring special escape sequences.

String html = """
    <html>
        <head>
            <title>Hello, world!</title>
        </head>
        <body>
            <h1>Hello, world!</h1>
        </body>
    </html>
""";

In this example, the html string contains an HTML document declared using a text block. The text block starts with """ and ends with """, and the document is indented for readability.

Here’s an example that demonstrates how to use placeholders and expressions inside text blocks:

String name = "Alice";
int age = 30;
String message = """
                 Hello, ${name}!
                 
                 You are ${age} years old.
                 
                 Your age in dog years is ${age * 7}.
                 """;
System.out.println(message);

In this example, we define two variables (name and age) and use them inside a text block to create a message. The ${expression} syntax is used to include the values of the variables inside the message, and we also include an expression (age * 7) to calculate the age in dog years.

Text blocks can also be used with other features in Java, such as switch expressions and lambda expressions. For example, you can use a text block inside a switch expression to define a case label:

String day = "Monday";
String message = switch (day) {
    case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> """
                                                                   It's a weekday.
                                                                   
                                                                   Time to go to work.
                                                                   """;
    case "Saturday", "Sunday" -> """
                                 It's the weekend.
                                 
                                 Time to relax and have fun!
                                 """;
    default -> """
               Invalid day.
               
               Please enter a valid day of the week.
               """;
};
System.out.println(message);

In this example, we use a text block to define the message for each case label in the switch expression. This makes the code easier to read and maintain, and reduces the amount of boilerplate code that is required.

Overall, text blocks are a useful feature that can make Java code more concise and readable, especially in cases where you need to write multiline strings or include formatting whitespace.

5. Switch Expressions

Switch expressions are a new feature introduced in Java 17 that provide a more concise and expressive syntax for switch statements. Switch statements are commonly used in Java to evaluate a single value and perform different actions based on different cases. Prior to Java 17, switch statements could only be used to execute a block of code, but with switch expressions, you can now assign the result of the switch statement to a variable.

The syntax for switch expressions is similar to the syntax for switch statements, with a few differences. In switch expressions, the cases are defined using the -> operator instead of the : operator, and the switch expression returns a value instead of executing a block of code.

Here’s an example that demonstrates how to use switch expressions in Java 17:

String day = "Monday";
String result = switch (day) {
    case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> "Weekday";
    case "Saturday", "Sunday" -> "Weekend";
    default -> "Invalid day";
};
System.out.println(result); // Output: Weekday

In this example, we first define a string variable day with the value “Monday”. We then use a switch expression to evaluate the value of day and assign the result to a string variable called result. The switch expression has two cases: one for weekdays and one for weekends. If the value of day matches one of the weekdays, the switch expression will return the string “Weekday”, and if it matches one of the weekends, it will return the string “Weekend”. If day does not match any of the defined cases, the switch expression will return the string “Invalid day”.

One of the benefits of switch expressions is that they can make code more concise and easier to read. They can also reduce the amount of code you need to write in some cases. For example, consider the following code snippet that uses a switch statement to perform an action based on the value of a variable:

int value = 10;
switch (value) {
    case 1:
        System.out.println("One");
        break;
    case 2:
        System.out.println("Two");
        break;
    case 3:
        System.out.println("Three");
        break;
    default:
        System.out.println("Unknown");
        break;
}

With switch expressions, you can write the same code in a more concise way:

int value = 10;
String result = switch (value) {
    case 1 -> "One";
    case 2 -> "Two";
    case 3 -> "Three";
    default -> "Unknown";
};
System.out.println(result); // Output: Unknown

Switch expressions can be especially useful in situations where you need to perform a switch statement and assign the result to a variable, or when you need to perform complex operations based on the value of a variable.

6. Helpful NullPointerExceptions

Helpful NullPointerExceptions aims to provide more detailed information about null pointer exceptions (NPEs) at runtime. The goal of this feature is to make it easier for developers to identify the source of null pointer exceptions and fix them more quickly.

In previous versions of Java, when a null pointer exception occurred, the error message provided limited information about where the exception occurred and which variable was null. This made it difficult for developers to debug their code and find the root cause of the problem.

With the new Helpful NullPointerExceptions feature, the error message now includes additional details that can help developers identify the source of the problem. For example, the error message might now include information about the method or line number where the exception occurred, as well as the name of the variable that was null.

Here’s an example of how the error message for a null pointer exception might look with the Helpful NullPointerExceptions feature enabled:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
	at com.example.MyClass.myMethod(MyClass.java:10)

In this example, the error message includes the name of the method (myMethod) where the exception occurred, as well as the line number (10) and the name of the variable that was null (s).

To enable the Helpful NullPointerExceptions feature, you can use the -XX:+ShowCodeDetailsInExceptionMessages option when running your Java application. This option is only available in JDK 17 and later versions.

Overall, the Helpful NullPointerExceptions feature is a useful addition to Java that can make it easier for developers to debug their code and find and fix null pointer exceptions more quickly. By providing more detailed error messages, developers can spend less time searching for the source of the problem and more time fixing it.

7. Foreign-Memory Access API (Incubator)

Foreign-Memory Access API, which provides a way for Java developers to directly access and manipulate memory outside of the Java heap. This API is designed for use cases where high-performance access to memory is required, such as in graphics processing, machine learning, and database systems.

The Foreign-Memory Access API allows developers to create and manage direct buffers that are backed by native memory. These buffers can be used to read and write data directly to and from the memory, without going through the Java heap. This can significantly improve the performance of memory-intensive operations, as it avoids the overhead of copying data between the Java heap and native memory.

To use the Foreign-Memory Access API, you first need to create a memory segment that represents the native memory. This can be done using the MemorySegment class, which provides methods for allocating, deallocating, and accessing memory segments. Once you have a memory segment, you can create a direct buffer that is backed by the segment using the MemorySegment.asByteBuffer() method. This buffer can be used to read and write data to and from the memory segment, as you would with any other byte buffer.

Here’s an example of how to use the Foreign-Memory Access API to allocate a memory segment and create a direct buffer:

import jdk.incubator.foreign.*;
public class MemoryExample {
    public static void main(String[] args) {
        // Allocate a memory segment of 1024 bytes
        MemorySegment segment = MemorySegment.allocateNative(1024);
        // Create a direct buffer backed by the memory segment
        ByteBuffer buffer = segment.asByteBuffer();
        // Write some data to the buffer
        buffer.putInt(0, 123);
        buffer.putDouble(4, 3.14);
        // Read the data back from the buffer
        int i = buffer.getInt(0);
        double d = buffer.getDouble(4);
        // Print the values
        System.out.println("i = " + i);
        System.out.println("d = " + d);
        // Deallocate the memory segment
        segment.close();
    }
}

In this example, we first allocate a memory segment of 1024 bytes using the MemorySegment.allocateNative() method. We then create a direct buffer backed by the memory segment using the MemorySegment.asByteBuffer() method. We write some data to the buffer using the putInt() and putDouble() methods, and then read the data back using the getInt() and getDouble() methods. Finally, we deallocate the memory segment using the close() method.

Note that the Foreign-Memory Access API is an incubating feature in Java 17, which means that it is still under development and subject to change in future releases. It should only be used in production environments with caution and after thorough testing.

8. Vector API (Incubator)

Vector API provides a set of vectorized operations for working with SIMD (Single Instruction Multiple Data) instructions on modern CPU architectures. This API is designed for use cases where high-performance processing of large data sets is required, such as in scientific computing, machine learning, and graphics processing.

The Vector API allows developers to perform arithmetic and logical operations on vectors of data in a way that takes advantage of SIMD instructions, which can perform multiple calculations in parallel. This can significantly improve the performance of certain types of computations, as it reduces the number of instructions that need to be executed and maximizes the use of available CPU resources.

To use the Vector API, you first need to create a vector using one of the factory methods provided by the API. These factory methods create vectors of a specific type (such as IntVector or FloatVector) and with a specific size (such as 128 bits or 256 bits). Once you have a vector, you can perform various operations on it, such as addition, subtraction, multiplication, and comparison.

Here’s an example of how to use the Vector API to perform a vectorized addition operation:

In this example, we first create two vectors of four floats each using the FloatVector.fromArray() method. We then add the two vectors together using the add() method and store the result in a third vector. Finally, we print the result.

import jdk.incubator.vector.*;
public class VectorExample {
    public static void main(String[] args) {
        // Create two vectors of four floats each
        FloatVector a = FloatVector.fromArray(VectorSpecies_128.F_128, new float[]{1, 2, 3, 4});
        FloatVector b = FloatVector.fromArray(VectorSpecies_128.F_128, new float[]{5, 6, 7, 8});
        // Add the two vectors together
        FloatVector c = a.add(b);
        // Print the result
        System.out.println("c = " + c);
    }
}

Note that the Vector API is an incubating feature in Java 17, which means that it is still under development and subject to change in future releases. It should only be used in production environments with caution and after thorough testing. Additionally, the Vector API requires hardware support for SIMD instructions, which may not be available on all systems.

9. Enhanced Pseudo-Random Number Generators

Java 17 introduces enhancements to the existing Pseudo-Random Number Generators (PRNG) in the java.util.random package. These enhancements provide developers with more flexibility and control over the generation of random numbers, as well as improved security.

The enhancements include three new algorithms, new methods for generating random bytes and random integers, and improvements to the existing SplittableRandom class.

New PRNG Algorithms

Java 17 introduces three new PRNG algorithms:

  • LXM
  • PCG64
  • Xoshiro

These algorithms provide different trade-offs between performance and randomness, and allow developers to choose the one that best fits their specific use case.

New Methods for Generating Random Bytes and Integers

Java 17 also introduces new methods in the java.util.random package for generating random bytes and random integers. These methods include:

  • RandomGenerator.nextInt(int bound) and RandomGenerator.nextLong(long bound): These methods generate random integers and longs respectively within the specified range.
  • RandomGenerator.nextBytes(byte[] bytes): This method generates random bytes and fills them into the specified array.

These new methods provide more convenience and flexibility to developers, making it easier to generate random numbers with specific characteristics.

Improvements to SplittableRandom

Java 17 also introduces improvements to the SplittableRandom class, which provides a way to generate repeatable sequences of random numbers. The improvements include:

  • A new split() method that returns a new instance of the SplittableRandom class with a different seed, allowing for the generation of independent sequences of random numbers.
  • Improved performance for generating large numbers of random numbers in parallel.

These improvements make the SplittableRandom class more useful for applications that require large amounts of random data, such as Monte Carlo simulations and statistical analysis.

The enhancements to the Pseudo-Random Number Generators in Java 17 provide developers with more flexibility and control over the generation of random numbers, as well as improved security. With the introduction of new algorithms and methods, and improvements to the SplittableRandom class, Java 17 makes it easier to generate random numbers with specific characteristics, and to generate large amounts of random data efficiently.

10. Enhanced NUMA-Aware Memory Allocation for G1

Java 17 introduces an enhancement to the Garbage-First Garbage Collector (G1) that improves its ability to allocate memory in a Non-Uniform Memory Access (NUMA) architecture. This enhancement is designed to improve the performance of applications running on NUMA systems, which are increasingly common in modern high-performance computing environments.

In NUMA architectures, memory is distributed across multiple nodes, each with its own local memory and access latency. Applications running on these systems can experience performance degradation if memory allocation is not optimized to take into account the NUMA topology.

The enhanced NUMA-aware memory allocation in G1 improves performance by allocating memory in a way that takes into account the NUMA topology of the system. Specifically, it attempts to allocate memory on the local node whenever possible, reducing the need for remote memory accesses that can result in increased latency and reduced performance.

The enhanced allocation strategy works by first identifying the NUMA topology of the system and then using that information to allocate memory in a way that maximizes locality. The strategy also takes into account the current state of the system, such as the availability of free memory and the current load on each node, to ensure that allocations are made in an efficient and effective manner.

To enable NUMA-aware memory allocation in G1, developers can set the -XX:+UseNUMA flag when running their application. This flag tells the JVM to use the enhanced allocation strategy, which can result in improved performance on NUMA architectures.

In addition to the -XX:+UseNUMA flag, developers can also use the -XX:NumAProximityPolicy flag to control the proximity policy used by G1 when allocating memory. The default policy is compact, which attempts to allocate memory on the closest node first. Other policies, such as scatter and balance, are also available, allowing developers to fine-tune the allocation strategy to meet the specific needs of their application.

In summary, the enhanced NUMA-aware memory allocation in G1 in Java 17 provides a valuable tool for developers working with applications running on NUMA architectures. By taking into account the NUMA topology of the system, G1 can allocate memory in a way that maximizes locality and minimizes remote memory accesses, resulting in improved performance and reduced latency.

Selenium Automation – Open URL in multiple browsers


This example shows how to open a url in multiple browsers for browser based testing using Selenium and WebdriverManager.

package seleniumProjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.safari.SafariDriver;

import io.github.bonigarcia.wdm.WebDriverManager;
import io.github.bonigarcia.wdm.config.DriverManagerType;

public class StartBrowser{
	
	static WebDriver driver = null;
	static String[] appList= {"chrome","firefox","edgedriver", "safari"};
	
	public static void main(String[] args) throws Exception {
		
		for(int i=0;i<appList.length;i++) {
		browserStart(appList[i],"http://google.com");
		Thread.sleep(5000);
		browserClose();
	
		}
	}
	

	public static void browserStart(String appName, String appUrl)
			throws InstantiationException, IllegalAccessException {

		if (appName.equals("chrome")) { //Run Chrome browser
			WebDriverManager.chromedriver().setup();
			driver = new ChromeDriver();
		} else if (appName.equals("firefox")) { //Run in Firefox broweser
			WebDriverManager.firefoxdriver().setup();
			driver = new FirefoxDriver();
		} else if (appName.equals("edgedriver")) { // Run in Edge browser
			WebDriverManager.edgedriver().setup();
			driver = new EdgeDriver();
		} else if (appName.equals("safari")) { //Run in Safari browser
            //For Safari browser, you need enable 
			//'Allow Remote Automation' under develop menu
			DriverManagerType safari = DriverManagerType.SAFARI;
			WebDriverManager.getInstance(safari).setup();
			driver = new SafariDriver();
		}

		driver.get(appUrl);
		driver.manage().window().maximize();

	}

	public static void browserClose() {
		driver.close();
	}
}

Algorithms in Java Interviews


In this post, we will see algorithm problems with their solutions which are asked during Java interviews.

How to check if a number is Palindrome?

void checkPalindrome(int n){
  int temp, sum = 0;
  int input=n;

  while(n>0) {
     temp = n%10;
     sum = (sum*10) + temp;
     n = n/10;
  }

  if(input == sum){
   System.out.println("Palindrome");
  } else {
   System.out.println("Not Palindrome");
  }
}

How to check if a number is Prime in Java8?

void checkPrime(int n) {
if(n > 1 && IntStream.range(2, n).noneMatch(i -> i%n==0)) {
System.out.println("Prime");
} else {
System.out.println("Non-Prime");
}
}

How to sort objects in reverse order in Java8?

Student student1 = new Student(372,"Venkat",1);
Student student2 = new Student(2,"Sachin",4);
Student student3 = new Student(2345,"Ganguly",6);
Student student4 = new Student(72,"Karthik",2);
List studlist = new CopyOnWriteArrayList();
studlist.add(student1);
studlist.add(student2);
studlist.add(student3);
studlist.add(student4);

// Iterate in Java8
studlist.forEach(s -> System.out.println(s.name));

// Sort by Ids
studlist.sort((Student s1,Student s2) -> s1.getId() - s2.getId());

// Sort by Rank in reverse Order
studlist.sort((Student s1,Student s2) -> s2.getRank() - s1.getRank());

Find second highest number in an Array?

int arr[] = {45,89, 29,1, 9, 100};
int highest = 0, secondHighest = 0;

for(int i=0; i<arr.length;i++) {   if(arr[i] > highest) {
     highest = arr[i];
  } else if(arr[i] > secondHighest) {
     secondHighest = arr[i];
  }
}

Find Nth highest Salary from a SQL Table?

SELECT MIN(SALARY) FROM EMPLOYEE
       WHERE SALARY IN (SELECT DISTINCT TOP N 
                               FROM EMPLOYEE ORDER BY SALARY desc);

Print Only Numerics from a String?

String sampleStr = "fdsha3430d3kdjafl0737434833";
String numericsOnlyStr = sampleStr.replaceAll("[^0-9]", "");

Print Duplicates in an Array?

for(int i=0;i<arr.length;i++) {
  for(int j=i+1; j< arr.length; j++) {
     if(arr[i] == arr[j]) {
           System.out.println(arr[j]);
     }
  }
}

Fetch Frequency of Elements repeated in an Array?

  Map<Integer, Integer> mp = new HashMap<>(); 
  
        // Iterating through array elements 
        for (int i = 0; i < n; i++) 
        { 
            if (mp.containsKey(arr[i]))  { 
                mp.put(arr[i], mp.get(arr[i]) + 1); 
            } else { 
                mp.put(arr[i], 1); 
            } 
        } 
        
        // Iterating through Map and Printing frequencies 
        for (Map.Entry<Integer, Integer> entry : mp.entrySet()) { 
            System.out.println(entry.getKey() + " " + entry.getValue()); 
        }

Find Triplets in an array whose sum is equal to n?

public class Triplets {
public static List<List> findTriplets(int[] numbers, int sum) {
List<List> tripletsCombo = new ArrayList<List>();
HashSet set = new HashSet();
List triplets = new ArrayList();

if (numbers.length == 0 || sum <= 0) {
   return tripletsCombo;
}

Arrays.sort(numbers);

for (int i = 0; i < numbers.length - 2; i++) {
int j = i + 1;
int k = numbers.length - 1;

while (j < k) {
   if (numbers[i] + numbers[j] + numbers[k] == sum) {
      String str = numbers[i] + "," + numbers[j] + "," +       numbers[k];
      // Check for the unique Triplet
      if (!set.contains(str)) {
               triplets.add(numbers[i]);
               triplets.add(numbers[j]);
               triplets.add(numbers[k]);
               tripletsCombo.add(triplets);
               triplets = new ArrayList();
               set.add(str);
     }
     j++;
     k--;
} else if (numbers[i] + numbers[j] + numbers[k] < sum) {    j++; } else { // numbers[i] + numbers[j] + numbers[k] > sum
   k--;
}
}
}

return tripletsCombo;
}

public static void main(String[] args) {
int[] numbers = { 2, 3, 1, 5, 4 };
int sum = 9;
List<List> triplets = findTriplets(numbers, sum);

if (triplets.isEmpty()) {
   System.out.println("No triplets are found");
} else {
   System.out.println(triplets);
}
}
}

How to check if two strings are Anagrams?

Two strings are called Anagrams if they contain same set of characters but in different order.  Examples:  “Astronomer – Moon starer”, “A gentleman – Elegant man”, “Dormitory – Dirty Room”, “keep – peek”.

void isAnagram(String input1, String input2) {
   //Removing all white spaces from s1 and s2
   String s1_nonSpaces = input1.replaceAll("\\s", "");
   String s2_nonSpaces = input2.replaceAll("\\s", "");

   boolean status = true;
   if(s1_nonSpaces.length() != s2_nonSpaces.length()) {
      status = false;
   } else {
      char[] s1Array = s1_nonSpaces.toLowerCase().toCharArray();
      char[] s2Array = s2_nonSpaces.toLowerCase().toCharArray();
      Arrays.sort(s1Array); 
      Arrays.sort(s2Array); 
      status = Arrays.equals(s1Array, s2Array);
   }
   System.out.print(status?"Anagrams":"Non-Anagrams");
}

Swap numbers without using temp/third variable?

void swapWithoutTemp(int a, int b) {
 a = a+b;
 b = a-b;
 a = a-b;
}

Find number of combinations for Sum of Two Elements from two arrays is equal to N?

We have two arrays of numbers, suppose we take one element from first array and another element from second array. Their sum should be equal to N(given number).

sumOfTwoElementsInTwoArrays() {
  int arr1[] = {4,8,10,12,7};
  int arr2[] = {6,90,34,45};

  int sumValue = 44; 
  HashSet complements = new HashSet();
  int pairCount = 0;

  for(int i=0;i<arr1.length;i++) {
    complements.add(arr1[i] - sumValue);
  }

  for(int j=0;j<arr1.length;j++) {
    if(complements.contains(arr2[j])) {
      pairCount++;
    }
 }

System.out.print("Number of pairs is "+pairCount);
}

First non repeated character in a String?

String str = "BANANA";
char firsNonRepeatedCharacter;
HashMap<Character, Integer> hmp = new HashMap<Character, Integer>();

for(int z=0;z<s.length();z++) {
  if(hmp.containsKey(str.charAt(z))) {
    hmp.put(str.charAt(z), hmp.get(str.charAt(z))+1);
  } else {
     hmp.put(str.charAt(z), 1);
  }
}

Set characterSet = hmp.keySet();
for(Character c:characterSet){
  if(hmp.get(c).toString()equals("1")) {
    firsNonRepeatedCharacter = c;
    break;
  }
}

Find the number of occurrence of an element in an array using Java8?

int b[] = {1,2,34,1};

List bList = Arrays.stream(b).boxed().collect(Collectors.toList());

System.out.println(bList.stream().filter(z -> z.toString().equalsIgnoreCase("1")).count());

100 doors toggle open/close

There are 100 doors in a row, all doors are initially closed. A person walks through all doors multiple times and toggle (if open then close, if close then open) them in following way:

In first walk, the person toggles every door, In second walk, the person toggles every second door, i.e., 2nd, 4th, 6th, 8th, …, In third walk, the person toggles every third door, i.e. 3rd, 6th, 9th, …

Find in nth walk, what will be the status of all doors

doorsOpenClosed(int no_of_walks) {
  int door_id, walk_id;
  int doors[] = new int[101];
  for(int i=0;i<100;i++) {
   doors[i] = 0;
  }

for (walk_id = 1; walk_id <= no_of_walks; walk_id++) {
  for (door_id = walk_id; door_id <= 100; door_id += walk_id) {
    if(door_id%walk_id == 0) {
      doors[door_id]=(doors[door_id] == 0)?1:0;
    }
  }
}

for (int j = 0; j <= 100; j++) {
 if(doors[j] == 1) {
   System.out.println("Open Door number::::"+j);
 }
}

}

Core Java and Java 8 Concepts


In this post, you will see some important Core Java/Java 8 concepts related to Collections, Exception Handling, Multi-threading, Concurrency etc.

Comparable Vs Comparator

Comparable Comparator
Comparable provides a single sorting sequence. In other words, Sorting of  collection is based on a single property of a class such as ID, ItemName or quantity etc. The Comparator provides multiple sorting sequences. In other words, sorting of collection can based of multiple properties such as ID, ItemName, and quantity etc.
Comparable affects the original class, i.e., the actual class is modified. Comparator doesn’t affect the original class, i.e., the actual class is not modified.
Comparable provides compareTo() method to sort elements. Comparator provides compare() method to sort elements.
Comparable is from  java.lang package. A Comparator is from java.util package.
Sorting list of Objects-Comparable type can be done using Collections.sort(List) method. Sorting list of Objects-Comparator type by Collections.sort(List, Comparator) method.

JVM Architecture

Different types of Class Loaders?

  • Bootstrap class Loader
  • Extensions class Loader
  • System class Loader

Boostrap class loader loads the classes from jdk/jre/lib/rt.jar. Extension class loader loads the classes from jdk/lib/ext folder jars. System class loader loads the classes from CLASSPATH.

Difference between ClassNotFoundException and NoClassDefFoundError

  • ClassNotFoundException is an Exception, while NoClassDefFoundError is an Error.
  • ClassNotFoundException occurs when CLASSPATH does not get updated with required JAR files while NoClassDefFoundError occurs when required class definition is not present at runtime.

Example for NoClassDefFoundError :

class Shape {
  public void draw() {
     System.out.println("Drawing Shape!");
  }
}

public class DrawingApp {
  public void draw() {
     System.out.println("Drawing Shape!");
  }
  public static void main(String[] args) {
     Shape shape = new Shape();
     shape.draw();
  }
}

After compilation, Shape.class and DrawApp.class are generated, If Shape.class is deleted and DrawApp is run then NoClassDefFoundError is thrown.

Difference between ConcurrentHashMap and SynchronizedMap

  • ConcurrentHashMap is designed for concurrency and improves performance while Collections.synchronizedMap(map) which is non-synchronized by sort can be synchronized by applying a wrapper using Collections.synchronizedMap(map).
  • ConcurrentHashMap doesn’t support null keys or null values while synchronized HashMap supports one null key.
  • Locking in SynchronizedMap is at object level, so read/write operations performance is slower.
  • Locking in ConcurrentHashMap is at a much finer granularity at a hashmap bucket level.

Differences betwen equals() and hashcode() methods

equals() and hashCode() are methods present in Object class and hashCode method should not be used to check if two object references are same. Reason: hashCode just returns int value for an Object, even two different objects can have same hashCode integer. The value returned by hashCode() is the object’s hash code, which is the object’s memory address in hexadecimal. equals() checks if the two object references are same. If two objects are equal then their hashCode must be the same, but the reverse is not true.

O(1) vs O(n) vs O(log n)

These are measures of time complexity of running a piece of code.

O(1) – if execution time is constant, it requires the same amount of time regardless of the size. Example:  array – accessing any element int i = a[0];

O(n) – if execution time is directly proportional to the size.  Example: Linear search for an element has a time complexity of O(n).

O(log n) – if execution time is proportional to the logarithm of the input size. Example: Performing Binary Search on array of elements

Changes to HashMap in Java8

  • In case of Hash collision entry objects are stored as a node in a LinkedList and equals() method is used to compare keys. That comparison to find the correct key with in a linked-list is a linear operation so in a worst case scenario the complexity becomes O(n).
  • To address this issue, Java 8 hash elements use Balanced Tree instead of LinkedList after a certain threshold is reached. Which means HashMap starts with storing Entry objects in linked list but after the number of items in a hash becomes larger than a certain threshold, the hash will change from using a LinkedList to a Balanced Tree, which will improve the worst case performance from O(n) to O(log n).

Fail Fast Vs Fail Safe Iterators

Fail-Fast Iterators Fail-Safe Iterators
Fail-Fast iterators doesn’t allow  modifications of a collection while iterating over it. Fail-Safe iterators allow modifications of a collection while iterating over it.
Concurrent Modification Exception is thrown if a collection is modified while iterating over it. These iterators don’t throw any exceptions if a collection is modified while iterating over it.
They use original collection to traverse over the elements of the collection. They use copy of the original collection to traverse over the elements of the collection.
These iterators don’t require extra memory. These iterators require extra memory to clone the collection.
Ex : Iterators returned by ArrayList, Vector, HashMap. Ex : Iterator returned by CopyOnWriteArrayList, ConcurrentHashMap.

Difference between map() and flatmap() in Java8

Lets suppose we are applying map and flatmap on stream of streams. Example given below

Stream<List<Character>> stream = Stream.of({'a','b'},{'c','d'})

with map:  For input Stream of two lists {‘a’,’b’} and {‘c’,’d’}, output will be {{‘a’,’b’},{‘c’,’d’}} .Here two lists are placed inside a list, so the output will be list containing lists

With flat map: For input Stream of two lists {‘a’,’b’} and {‘c’,’d’}, output will be {{a,b,c,d}} .Here two lists are flattened and only the values are placed in list, so the output will be list containing only elements

What are Functional interfaces how we can define them?

Functional interfaces are interfaces which have only one single abstract method in it. Example:  Runnable Interface since it has only single abstract method, run().

From Java8, we can use @FunctionalInterface to define a functional interface. Although this annotation is optional, once it is used then declaring more than one abstract method will throw compile time error.

Rules of Method Overloading and Method Overriding

There are specific rules while we implement method overloading and overriding in Java with regards to increasing/decreasing visibility of methods of parent class in child class and throwing Checked Exceptions in child class. Complete rules are posted in this below link

https://malliktalksjava.com/2020/05/29/rules-of-method-overloading-and-overriding/

Exception Handling flow having return statements in try/catch/finally blocks

  • Once try block encounters a return statement, the flow immediately transfers to finally block. Let say,it prints “print statement from finally”.
  • Upon the completion of finally block execution, control goes back to the return statement in the try block and returns “returning from try block”.
  • If finally block has a return statement, then the return statements from try/catch blocks will be overridden.

Exception Handling flow while exceptions thrown in catch/finally blocks

  • If the catch block completes normally, then the finally block is executed. Then there is a choice:
  • If the finally block completes normally, then the try statement completes normally. If the finally block completes abruptly for any reason, then the try statement completes abruptly for the same reason.
  • If the catch block completes abruptly for reason R, then the finally block is executed. Then there is a choice:
    If the finally block completes normally, then the try statement completes abruptly for reason R.
    If the finally block completes abruptly for reason S, then the try statement completes abruptly for reason S (and reason R is discarded).

FixedThreadPool vs CachedThreadPool vs ScheduledThreadPool

  • newCachedThreadPool(): creates an expandable thread pool executor. New threads are created as needed, and previously constructed threads are reused when they are available. Idle threads are kept in the pool for one minute. This executor is suitable for applications that launch many short-lived concurrent tasks.
  • newFixedThreadPool(int n): creates an executor with a fixed number of threads in the pool. This executor ensures that there are no more than n concurrent threads at any time. If additional tasks are submitted when all threads are active, they will wait in the queue until a thread becomes available. If any thread terminates due to failure during execution, it will be replaced by a new one. The threads in the pool will exist until it is explicitly shutdown. Use this executor if you and to limit the maximum number of concurrent threads.
  • newScheduledThreadPool(int corePoolSize): creates an executor that can schedule tasks to execute after a given delay, or to execute periodically. Consider using this executor if you want to schedule tasks to execute concurrently.

What is ThreadLocal?

ThreadLocal class provides thread-local variables. It enables you to create variables that can only be read and write by the same thread. If two threads are executing the same code and that code has a reference to a ThreadLocal variable then the two threads can’t see the local variables of each other.

Diffence Volatile vs AtomicInteger?

volatile keyword is used on variables to solve the visibility problem in multi-threaded environment.  AtomicInteger is used if we perform compound operations(incrementing(i++) decrementing(i–)) on variables.

volatile is used on boolean flags, AtomicInteger is used for counters.

 

Differences between yield, join, & sleep

yield() method pauses the currently executing thread temporarily for giving a chance to the remaining waiting threads of the same priority to execute. If there is no waiting thread or all the waiting threads have a lower priority then the same thread will continue its execution. The yielded thread when it will get the chance for execution is decided by the thread scheduler whose behavior is vendor dependent.

join() If any executing thread t1 calls join() on t2 i.e; t2.join() immediately t1 will enter into waiting state until t2 completes its execution.

sleep() Based on our requirement we can make a thread to be in sleeping state for a specified period of time

Differences between Runnable and Callable

  • Runnable object does not return a result whereas a Callable object returns a result.
  • Runnable object cannot throw a checked exception wheras a Callable object can throw an exception.
  • The Runnable interface has been around since Java 1.0 whereas Callable was only introduced in Java 1.5.
class ThreadA implements Runnable {
@Override
public void run() { }
}

public class ThreadB implements Callable<String> {
@Override
public String call() throws Exception {
return "Thread B ran Successfully";
}
}

What is Semaphore in concurrency?

Semaphore is used to restrict the entry to a service to a fixed number of threads at a given time. This is generally used on slow services to make it available for fixed number of requests.

Semaphore semaphore = new Semphore(no_of_permits);

In run() method of a thread, we can use semaphore.acquire() before accessing the slow service and semaphore.release() after to ensure fixed number (defined as no_of_permits) of threads are eligible to access it.

Difference between CyclicBarrier and CountDownLatch?

Both CyclicBarrier and CountDownLatch are used in Multi threading scenario where one Thread waits for one or more Thread to complete their job before it continues processing but main difference between two is that, you can not reuse same CountDownLatch instance once count reaches to zero and latch is open, on the other hand, CyclicBarrier can be reused by resetting Barrier, Once barrier is broken.

  • Initialization of countdownlatch is CountDownLatch latch = new CountDownLatch(4);
  • Method used to countdown (generally used inside run method of thread at a specific point) is latch.countDown()
  • Method used to await a specific thread till countdown number completes is latch.await()
  • Phaser can be used either to perform functionality of both CyclicBarrier and CountDownLatch

References:
https://docs.oracle.com/en/java/javase/
https://stackoverflow.com/
https://dzone.com/

Rules of method overloading and overriding


In this post we will see the rules which needs to adhered while implementing method overriding and overloading with regards to increasing/decreasing visibility of methods of parent class in child class and throwing Checked Exceptions in child class.

Method Overriding rules

For terminology, original method is known as overridden method and new method is known as overriding method. Below rules must be followed to override a methods in Java :

  • Overriding method cannot throw checked exception which is higher in hierarchy than the checked Exception thrown by overridden method. For example if overridden method throws IOException which is checked Exception, than overriding method can not throw java.lang.Exception because it comes higher in type hierarchy.
    "Exception 'Exception' is not compatible with throws clause in" 
    
    **** Overriding method can have Runtime Exceptions declared even if Overridden method does not throw any type of Exceptions.
  • Overriding method can not reduce access of overridden method. It means if overridden method is defined as public than overriding method can not be protected or package private. Similarly if original method is protected then overriding method cannot be package-private. You can see what happens if you violate this rule in Java,
     "You cannot reduce visibility of inherited method of a class".
  • Overriding method can increase access of overridden method. This is opposite of earlier rule.
    ****According to this if overridden method is declared as protected then overriding method can be protected or public
  • private, static, final methods can not be overridden.
    "Cannot override the final method from Parent"
  • Return type of overriding method must be same as overridden method. Changing return type of method in child class will throw compile time error
    "return type is incompatible with parent class method"
    

Method Overloading rules

Here is the list of rules which must be followed to overload a method:

  • First rule to overload a method is to change method signature. method signature is made of number of arguments, type of arguments and order of arguments if they are of different types.  One can change any of these or combinations of them to overload a method in Java.
  • Return type of method is not part of method signature, hence changing the return type alone will not overload a method in Java.  In fact, it will result in compile time error.

Difference between ClassNotFoundException and NoClassDefFoundError


1. java.lang.ClassNotFoundException :  This exception indicates that the class was not found on the classpath. This indicates that we were trying to load the class definition, and the class did not exist on the classpath.

2. java.lang.NoClassDefFoundError :  This exception indicates that the JVM looked in its internal class definition data structure for the definition of a class and did not find it. This is different than saying that it could not be loaded from the classpath. Usually this indicates that we previously attempted to load a class from the classpath, but it failed for some reason – now we’re trying to use the class again (and thus need to load it, since it failed last time), but we’re not even going to try to load it, because we failed loading it earlier (and reasonably suspect that we would fail again). The earlier failure could be a ClassNotFoundException or an ExceptionInInitializerError (indicating a failure in the static initialization block) or any number of other problems. The point is, a NoClassDefFoundError is not necessarily a classpath problem.

How to read a URL using Java?


  • Below program consists of steps to read data from the URL
package in.malliktalksjava;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

/**
* @author malliktalksjava
*
*/
public class URLConnectionReader {

public static void main(String[] args) throws Exception {

URL oracle = new URL("http://malliktalksjava.in/");
URLConnection yc = oracle.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(
yc.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
System.out.println(inputLine);
in.close();
}
}

Example Program: Search Word in Folder files and print output


To Search a word in in list of files available in Folder, you need to find the list of files first and then scan each and every for required word. Below is the sample program to find the a given word Java in D:\\test folder of files.

package in.javatutorials;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.regex.MatchResult;

/**
 * Search for the files in a folder and prints all file details.
 */
public class WordCrawlerInFolder {

private static String directoryPath = "D://test";
private static String searchWord = "Java";

public WordCrawlerInFolder() {
super();
}

public static void main(String[] args) {
   WordCrawlerInFolder crawler = new WordCrawlerInFolder();
    File directory = new File(directoryPath);

    if (directory == null || !directory.exists()) {
           System.out.println("Directory doesn't exists!!!");
           return;
    }
    crawler.directoryCrawler(directory, searchWord);
}

/**
* Gets all the file and directories and prints accordingly
* @param directory
* Directory path where it should search
*/
public void directoryCrawler(File directory, String searchWord) {

// Get List of files in folder and print
File[] filesAndDirs = directory.listFiles();

// Print the root directory name
//System.out.println("-" + directory.getName());

// Iterate the list of files, if it is identified as not a file call
// directoryCrawler method to list all the files in that directory.
for (File file : filesAndDirs) {

if (file.isFile()) {
searchWord(file, searchWord);
//System.out.println(" |-" + file.getName());
} else {
directoryCrawler(file, searchWord);
}
}
}

/**
* Search for word in a given file.
* @param file
* @param searchWord
*/
private void searchWord(File file, String searchWord){
Scanner scanFile;
try {
scanFile = new Scanner(file);
while (null != scanFile.findWithinHorizon("(?i)\\b"+searchWord+"\\b", 0)) {
MatchResult mr = scanFile.match();
System.out.printf("Word found : %s at index %d to %d.%n", mr.group(),
mr.start(), mr.end());
}
scanFile.close();
} catch (FileNotFoundException e) {
System.err.println("Search File Not Found !!!!! ");
e.printStackTrace();
}
}
}

We have used some escape characters in above class searchWord() method, below is the notation for the same.

  1. (?i) turn on the case-insensitive switch
  2. \b means a word boundary
  3. java is the string searched for
  4. \b a word boundary again.

If search term contain special characters, it would be suggested to use \Q and \E around the string, as it quotes all characters in between. Make sure the input doesn’t contain \E itself.

Other Useful Links:

Javac/Java searching algorithm for other classes

Example program to reverse a Number in Java

How to find count of duplicates in a List

Threads Interview questions in Java