Modern Java interview questions (2023)

Are you going for a Java Interview ? This article contains a list of Interview Questions which are covering modern aspects of Java, such as Functional programming, Stream API, new IO API and much more.

Java Records

What is a Java Record and how it compares with a Java Class?

A record is a concise way to define a simple data class that represents a logical group of related values.

A record has a state description and a compact syntax, which consists of a keyword record, followed by the record name, a list of fields, and a special method called the canonical constructor.

For example, here is a simple record that represents a point in two-dimensional space:

record Point(int x, int y) { }

This is equivalent to writing the following class:

class Point {
    public final int x;
    public final int y;
    
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public int x() { return x; }
    public int y() { return y; }
    public boolean equals(Object o) { /*...*/ }
    public int hashCode() { /*...*/ }
    public String toString() { /*...*/ }
}

A record has a shorter syntax and less boilerplate code than a Class. Records are also immutable by default. This means that you cannot change the fields of a record after you set them.

While class is more generic and flexible, records are intended to be used as simple data classes, where most of the logic is based on the data it holds and also for functional programming cases like being used as key in a map or tuple like use cases.

What are Record Patterns ?

Record patterns, which are a new feature in Java 19, allow you to extract the values of the components of a record and assign them to separate variables. You can use these patterns with the instanceof and switch statements, as well as with guards. They are especially useful when you need to extract values from nested records or records that are part of a sealed hierarchy.

For example, consider this Record:

public record Task(User user, Group group)

Suppose you have an Object that might just be an instanceof Task. Here is how to check it using Record Patterns:

if (obj instanceof Task(var u, var g)) {
   // Do something with u and g
}

Here, Task(var u, var g) is a record pattern. If the value to be matched is an instance of the record, then the variables u and g in the pattern are bound to the record components.

The code is equivalent to:

if (obj instanceof Task p) {
   var u = p.user();
   var g = p.group();
   // Do something with u and g
}

A record pattern is a way to match a value against a specific record type and extract the values of its components. The record pattern is made up of a type, which is the record type you want to match against, a list of component patterns, which are used to extract the values of the components, and an optional identifier, which is used to give a name to the matched value. The variable associated with the pattern is then initialized with the matched value.

For example, if you have a record type called Point with two components i and j, a record pattern can be used to extract the values of i and j from an instance of Point. Record pattern can also use “var” instead of the type, in that case the type is inferred by the compiler.

Threads and Data structure Interview Questions

What are Java Virtual Threads ?

A virtual thread is a type of thread in Java that is managed by the Java runtime instead of the operating system. Unlike regular threads, virtual threads don’t take up an operating system thread while they’re waiting for other resources. When code running in a virtual thread calls a blocking I/O operation, the runtime will automatically suspend the virtual thread until the operation completes.

java interview questions java 19

It’s important to note that virtual threads aren’t faster than regular threads; they don’t run more instructions per second. However, virtual threads are particularly useful when it comes to waiting for resources. Because virtual threads don’t take up an operating system thread, it’s possible to have many more of them than regular threads, which allows them to wait for things like file system requests, database connections, or web service responses without consuming too many resources.

What is Structured Concurrency ?
Structured concurrency is a Java enhancement proposal (JEP) for developing concurrent applications. It aims to make it easier to write and debug multithreaded code in Java. Structured concurrency binds the lifetime of threads to the code block which created them.
For example:

String getDog() throws ExecutionException, InterruptedException {
  try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
  Future name = scope.fork(this::getName);
  Future breed = scope.fork(this::dogBreed); scope.join(); scope.throwIfFailed(); return "it's name is:" + name.resultNow() + ", and is a " + breed.resultNow(); }
}

The StructuredTaskScope shutdowns every running child thread if one of them throws an exception during execution.

What is the difference between the java.util.Vector API and the new Vector API ?

The java.util.Vector class is a legacy class that is part of the Java standard library. It is a type of data structure that stores a collection of objects and provides operations such as adding, removing, and accessing elements in the collection.

Java Vector API is a data structure API that makes it easy for programmers to use the data-processing power of modern computers in a way that works across different types of processors. Without this tool, it can be difficult to write code that works well on all types of computers. The Vector API makes it possible for programmers to write code that works the same way on all types of computers, without needing to know which type of computer the code will run on. This makes it easier for programmers to write code that takes advantage of the full power of modern computers without having to be experts on different types of processors.

Variables and Pattern Matching

What is Local Variable Type Inference in java ?

Local variable type inference is a feature that allows developers to omit the type of a local variable when it can be inferred from the context. This is done through the use of the “var” keyword, which indicates to the compiler that the type should be inferred from the initializer expression.

For example, consider the following code where you are declaring a variable “list” of type List with some Integer in it:

List<Integer> list = Arrays.asList(1, 2, 3);

With Local Variable Type Inference you can rewrite it as follows:

var list = Arrays.asList(1, 2, 3);

The type of the variable “list” is inferred to be List based on the type of the return value of the Arrays.asList method.

The use of “var” allows for less verbose code, which in turn can make it more readable and maintainable.

How you can improve the following switch with Java 12 and above:

int num = 2;
switch (num) {
    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("Other");
}

In this example, the switch statement determines the output from the value of the num variable. The case labels correspond to the possible values of num, and the code following each label is executed if num has that value. The break statement is used to exit the switch after matching/executing a case.

Here is how to rewrite is using a switch expression:

int num = 2;
String output = switch (num) {
    case 1 -> "One";
    case 2 -> "Two";
    case 3 -> "Three";
    default -> "Other";
};

System.out.println(output);

In this example, switch statement is used as a expression and result is assigned to a variable ‘output’. You can see that it uses the arrow -> operator instead of the colon : operator. and also the break statement is not needed in the case and also doesn’t required to have a default case.

besides, you can use the yield statement to return a value from the switch statement. It helps to return a value more cleanly and more clearly, as well as to make it clear that the switch is being used as an expression. For example:

int num = 2;
int output = switch (num) {
    case 1:
    case 3:
        yield num*2;
    case 2:
        yield num*3;
    default:
        yield num*4;
};

System.out.println(output);

What is the Pattern Matching for the switch functionality?

Firstly, a switch statement is a way to make your code do different things based on the value of a certain expression. It has multiple options to choose from, called “cases”, and will run the code associated with the case that matches the value of the expression.

With the introduction of Pattern Matching for the switch, you can now use different type of expressions for the selector, and the “cases” can also have patterns, so the switch statement will look for a match between the selector expression and the pattern in the case statement and act accordingly. This gives you more options to use different values and types in the switch statement and more flexibility in the decision making process.

public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        return switch (shape) {
            case Rectangle r -> 2 * r.length() + 2 * r.width();
            case Circle c    -> 2 * c.radius() * Math.PI;
            default          -> throw new IllegalArgumentException("Unrecognized shape");
        };
    }

What is Pattern matching for the instanceof functionality?

This functionality brings an improved version of the instanceof operator that both tests the parameter and assigns it to a binding variable of the proper type. For example:

if (animal instanceof Lion) {
    Lion lion = (Lion) animal;
    lion.roar();
    // other lion operations
} else if (animal instanceof Elephant) {
    Elephant elephant = (Elephant) animal;
    elephant.trumpet();
    // other elephant operations
}

You can rewrite it as follows:

if (animal instanceof Lion lion) {
    lion.roar();
} else if(animal instanceof Elephant elephant) {
    elephant.trumpe();
}

In this example, we first test the animal variable to see if it’s an instance of a Lion or Elephant. If so, it’ll be cast to our type. It is important to note that the variable names lion/elephant are not an existing variable, but instead a declaration of a pattern variable.

Java IO

Which libraries would you use to connect via HTTP from a Java application ?

A modern Java application would probably use the Java 11 HttpClient API. Other options include:

  • Use Java built-in HttpURLConnection
  • Using Apache HttpComponents HttpClient.
  • Use Unirest HTTP Api
  • Using OkHttp

More about Java HTTP Clients here: Top 5 solutions for Java Http Clients

How can you start a simple Web server without writing code in Java 18?
The following command starts the Simple Web Server:

$ jwebserver

By default, the server binds to the loopback address and port 8000 and serves the current working directory. If startup is successful, the server runs in the foreground and prints a message to System.out listing the local address and the absolute path of the directory being served, such as /cwd. For example

$ jwebserver
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /cwd and subdirectories on 127.0.0.1 port 8000
URL http://127.0.0.1:8000/

What Java API can you use to support Unix domain sockets for inter-process communication on the same physical host ?

In Java 16 you can use Unix Domain Sockets. The first step to creating a Unix domain client or server socket is to specify the protocol family using the open method.

// Create a Unix domain server
ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX);

// Or create a Unix domain client socket
SocketChannel client = SocketChannel.open(StandardProtocolFamily.UNIX);

The second step is to bind the socket to an address using the UnixDomainSocketAddress type, which can be created from either a string or a Path.

// Create an address directly from the string name and bind it to a socket
var socketAddress = UnixDomainSocketAddress.of("/foo/bar.socket");
socket.bind(socketAddress);

// Create an address from a Path and bind it to a socket
var socketAddress1 = UnixDomainSocketAddress.of(Path.of("/foo", "bar.socket"));
socket1.bind(socketAddress1);

Using Unix domain sockets offer improvements in latency and CPU as they bypass the TCP/IP stack.
then, then are inherently more secure as publishing the service as a local pathname, avoids the possibility of the service being accessed unexpectedly from remote clients.
Finally, in certain environments like Docker containers, it can be troublesome to set up communication links between processes in different containers using TCP/IP sockets. Using Unix domain sockets can simplify the communication between containers.

What is the use of the Java API FileSystems.newFileSystem() ?

The FileSystems.newFileSystem() method is used in Java to create a new FileSystem object. A FileSystem represents the file system to which a Java program can read and write files. The newFileSystem() method is a factory method that can be used to create a new FileSystem object for a particular file system provider.

For example, you can use the following code to create a new file system that is based on the file system provider for the zip file format, and that accesses a zip file called “example.zip”:

Path zipPath = Paths.get("example.zip");
FileSystem zipFileSystem = FileSystems.newFileSystem(zipPath, ClassLoader.getSystemClassLoader());

This code creates a new FileSystem object that provides access to the contents of the “example.zip” file. Once you have a FileSystem object, you can use it to read and write files within the file system, and to manipulate the file system itself.

How can you read or write to a File in Java String using an one-liner code ?

The most concise way is by using the Files.writeString and Files.readString of the Class java.nio.file.Files. For example:

Path filePath = Paths.get("/home/user", "java", "file.txt");

try
{
//Write content to file
Files.writeString(filePath, "Hi there!", StandardOpenOption.APPEND);

//Verify file content
String content = Files.readString(filePath);

System.out.println(content);
} 
catch (IOException e) 
{
e.printStackTrace();
}

What is the use of Files.mismatch() method in Java ?

The Files.mismatch() method is used in Java to compare two files and find the first byte that is different between them. This method returns the position of the first mismatch, or -1 if the two files are identical.

Here’s an example of how you might use Files.mismatch() in Java to compare the contents of two files:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FileComparison {
    public static void main(String[] args) {
        try {
            //Create path for both files
            Path file1 = Paths.get("file1.txt");
            Path file2 = Paths.get("file2.txt");

            // Compare the two files and find the first mismatch
            long mismatch = Files.mismatch(file1, file2);
            if (mismatch == -1) {
                System.out.println("The two files are identical");
            } else {
                System.out.println("The first mismatch is at position " + mismatch);
            }
        } catch (IOException e) {
            System.out.println("An error occurred while comparing the files: " + e);
        }
    }
}

Java Classes and Interfaces

What are Sealed Classes and Interfaces in Java ?

In Java, a sealed class is a class that can only be extended by a specific set of classes or interfaces. This is enforced at compile time, and can be used to prevent unexpected class hierarchies from being created. A sealed class is defined using the “sealed” keyword, along with a “permits” clause that lists the classes or interfaces that are allowed to extend the sealed class.

For example, consider a sealed class called “Shape”, which can only be extended by classes called “Circle”, “Square”, and “Triangle”:

sealed class Shape permits Circle, Square, Triangle {}

final class Circle extends Shape {}
final class Square extends Shape {}
final class Triangle extends Shape {}

Similarly, to seal an interface, we can apply the sealed modifier to its declaration. The permits clause then specifies the classes that are permitted to implement the sealed interface.

public sealed interface Service permits Task1, Task2 {

    int doSomething();

    default int getMaxTask() {
        return 100000;
    }

}

What are Hidden Classes ?

A hidden class is a class that is not visible to the outside world and cannot be referred to by name. Instead, a hidden class is created by the Java Virtual Machine (JVM) at runtime and is only accessible through the Class object that represents it.

Hidden classes are used to improve the performance and security of the Java application by allowing the JVM to optimize the usage of memory and reduce the risk of exposing internal implementation details.

Here is an example of how to create an Hidden Class:

byte[] classBytes = ...; // bytecode of the class
boolean shouldLookup = ...; // true if the class should be accessible via lookup
ClassLoader classLoader = ...; // class loader to use
Class<?> hiddenClass = Unsafe.getUnsafe().defineHiddenClass(classBytes, shouldLookup, classLoader);

The classBytes parameter is an array of bytes that contains the bytecode of the hidden class. The shouldLookup parameter specifies whether the hidden class should be accessible via the lookup() method, and the classLoader parameter is the class loader that should be used to load the class.

Once a hidden class is defined, it can be used like any other class. However, because it’s not visible by name, it cannot be referred to by its fully-qualified name, but it can be referred to by the returned Class object.

Can you have private methods in Java intefaces ?

Interface methods are public by default. A private interface method is a special type of Java method that is accessible inside the declaring interface only. This means that no class that extends the interface can access this method directly using an instance of the class.

Developers can use private interface methods to encapsulate code that you don’t want to share across implementations of the interface.

How can you add a method implementation in a Java interface ?

You can can do it in two ways: using static methods and defining a default method. For example:

public interface DefaultStaticExampleInterface {
   default void doSomething() {
      System.out.println("default method implementation - DefaultStaticExampleInterface");
   }
   static void anotherMethod() {
      System.out.println("In the Static interface method");
   }
}

There are however important differences:

  • Static interface methods belongs to the interface only. You can invoke them only on the interface.
  • Default methods can be used to provide common functionality in all implementing classes

What is a Functional interface in Java ?

A functional interface is an interface that contains only one abstract method. Although not mandatory, it is recommended to use the @FunctionalInterface in it, to enforce the usage of a single function in it. For example:

@FunctionalInterface

interface Square {
	int calculate(int x);
}

Functional Interfaces allow using a Lambda expression to invoke the calculate method. For example:

class Main {
	public static void main(String args[])
	{
		int x = 10;

		// use lambda expression to define the calculate method
		Square s = (int x) -> x * x;

		int value = s.calculate(a);
		System.out.println(value);
	}
}

How can you transform a String using a Functional interface ?

You can use the Java String transform() method to apply a function to this String. For example, here is how to uppercase a List of String objects:

List cities = new ArrayList<>();
cities.add("Rome");
cities.add("Paris");
cities.add("London");

List upperCaseList = cities.stream().map(s -> s.transform(str -> str.toUpperCase())).collect(Collectors.toList());

Java Stream API questions

How can you use the “reduce” method in the Stream API to implement a custom aggregation operation on a stream of elements?
The “reduce” method in the Stream API allows you to perform a custom aggregation operation on a stream of elements. The method takes two arguments: an initial value (also called the identity) and a BinaryOperator (a special type of BiFunction) that specifies how to combine the current result with the next element of the stream.

For example :

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); 
int sum = numbers.stream().reduce(0, (a, b) -> a + b);

In this example, we’re using the “reduce” method to perform a sum operation on a stream of integers. The initial value is 0, and the BinaryOperator is a lambda expression that adds its two arguments together.

How can you use the “flatMap” method to transform a stream of streams into a single stream?

The “flatMap” method allows you to transform a stream of streams into a single stream. The method takes a function as an argument, which is applied to each element of the input stream, and the function should return a stream of elements. Example:

Stream<List<Integer>> streamOfLists = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4), Arrays.asList(5, 6));
Stream<Integer> flatStream = streamOfLists.flatMap(x -> x.stream());

In this example, we’re using the “flatMap” method to flatten a stream of lists of integers into a single stream of integers.

How can you use the “peek” method to perform a side-effect operation, such as logging or debugging, on the elements of a stream as they are processed?
The “peek” method allows you to perform a side-effect operation, such as logging or debugging, on the elements of a stream as they are processed. The method takes a Consumer as an argument, which is applied to each element of the stream as they are consumed. For example:

Stream<Integer> stream = Stream.of(1, 2, 3); 
stream.peek(x -> System.out.println("Original: " + x)).map(x -> x + 1).peek(x -> System.out.println("Mapped: " + x));

In this example, we’re using the “peek” method to print out the original and mapped values of the elements in the stream.

How can you use the “collect” method to perform a mutable reduction, such as adding elements to a collection or concatenating strings?

The “collect” method allows you to perform a mutable reduction on a stream, such as adding elements to a collection or concatenating strings. The method takes a Collector as an argument, which describes how to accumulate the elements of the stream into a final result. For example:

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

Set<Integer> oddNumbers = numbers.parallelStream().filter(x -> x % 2 != 0).collect(Collectors.toSet());
System.out.println(oddNumbers); // [1, 3, 5]

What is the difference between the “groupingBy” and “partitioningBy” collectors ?

The groupingBy and partitioningBy collectors are two built-in collectors in the Java Stream API that can be used to perform advanced grouping and partitioning operations on a stream.

groupingBy is used to group elements of a stream by a certain criterion and return a Map> where K is the key used for grouping and T is the type of the elements in the stream. For example, the following code groups a stream of strings by their length:

Map<Integer, List<String>> groupedByLength = 
    strings.stream().collect(Collectors.groupingBy(String::length));

partitioningBy is similar to groupingBy, but it is used to partition a stream into two groups based on a predicate. It returns a Map> where the keys are true and false, representing the elements that satisfy the predicate and those that do not, respectively. For example, the following code partitions a stream of integers into even and odd numbers:

Map<Boolean, List<Integer>> evenOdd = 
    integers.stream().collect(Collectors.partitioningBy(i -> i % 2 == 0));

What is the use of Collectors.teeing() in Java?

The method teeing() is a static method of the java.util.stream.Collectors class. It returns a collector that aggregates the results of two supplied collectors. This method is available since Java version 12. For example:

public class Main {

    static class Customer {
        String name;
        int age;

        public Customer(String name, int age) {
            this.name = name;
            this.age = age;
        }
 
    }

    public static void main(String[] args) {
        List<Customer> customerList = Arrays.asList(new Customer("john", 34), new Customer("mark", 43),
                new Customer("mary", 81), new Customer("frank", 12));

        System.out.println("list of customers - " + customerList);

        Stream<Customer> customerStream = customerList.stream();

        Map<String, List<Customer>> result = customerStream.collect(Collectors.teeing(
                Collectors.filtering(p -> p.age % 2 == 0 , Collectors.toList()),
                Collectors.filtering(p -> p.age % 2 != 0 , Collectors.toList()),
                (res1, res2) -> {
                    Map<String, List<Customer>> map = new HashMap<>();
                    map.put("EvenAged customers", res1);
                    map.put("OddAged customers", res2);
                    return map;
                }));

        System.out.println("Result of applying teeing - " + result);

    }
}

JVM Options and GC

Which are the newest garbage collectors available in Java ?

The newest java Garbage Collectors are:

  • Z Garbage Collector (ZGC) (since JDK 15)
  • Shenandoah (since JDK 12)

The ZGC and Shenandoah GCs both focus on latency at the cost of throughput. They attempt to do all garbage collection work without noticeable pauses. Currently neither is generational.

How do you enable the Z Garbage Collector (ZGC) and how would you tune it ?

The Z Garbage Collector (ZGC) is a low-latency and scalable collector that you can use for applications that require low latency and/or have a very large heap. It performs expensive work concurrently, without stopping the application threads for more than 10ms. You can enable it using the command line option:

-XX:+UseZGC

The most important tuning option is setting the max heap size (-Xmx). The more memory you give to ZGC, the better the performance, but this should be balanced with the memory usage.

Another tuning option is setting the number of concurrent GC threads (-XX:ConcGCThreads), ZGC uses heuristics to automatically select this number but depending on the characteristics of the application this might need to be adjusted. This option essentially dictates how much CPU-time the GC can use.

What JVM flag can you use to get helpful hints when you have a NullPointerException?

You can add this option to the JVM

-XX:+ShowCodeDetailsInExceptionMessages

This flag will cause the JVM to include the bytecode index of the instruction that caused the NPE, as well as the source code line number, if available. This can help developers to quickly locate the cause of the NPE in their source code.

For example:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Example.printName()" because "this.name" is null
	at Example.printName(Example.java:10) ~[Example.class:?]
	at Example.main(Example.java:5) ~[Example.class:?]

In this example, the NPE occurred in the method Example.printName() at line 10, and the cause of the NPE is that the this.name field is null. This is shown in the message of the exception.

Various Questions

What is the use of the Optional Class?

The Optional class provides another way to handle situations when value may or may not be present by adding methods to explicitly deal with the cases where a value is present or absent. However, the advantage compared to null references is that the Optional class forces you to think about the case when the value is not present.

For example:

public Optional<Customer> findByLastName(String lastName) {
    if (lastName.equalsIgnoreCase("Smith")) {
        return Optional.of(new Customer("John", "Smith"));
    }
    return Optional.empty();
}
  • This method signature returns Optional, or in other words an Optional of type Customer
  • if lastName is “Smith”, we construct a Customer object and pass it to Optional.of. This creates an Optional containing the Customer that we return.
  • if lastName is anything else, we return Optional.empty(). This creates an empty Optional, which will force whoever is calling the method to handle the Customer not found scenario.

We can handle the result using the ifPresent method which takes a consumer function, passed as a lambda expression:

// returns Optional with value
Optional<Customer> lookupResult = customerService.findByLastName("Smith");
lookupResult.ifPresent(customer -> System.out.println(customer.toString()));

Which is the Default Charset in Java?

Pre-Java 18, every instance of Java virtual machine (JVM) has a default charset, which is determined at JVM startup and typically depends on the locale and charset of the underlying operating system. The default charset may not be one of the standard charsets supported. In Java 18, every instance of the JVM has the default charset of UTF-8 unless it has been overridden by a system settings.

What is the simplest way to parse a String like “1 thousand” into “1000” in java ?

You can use the Java 12 CompactNumberFormat. This is a concrete subclass of NumberFormat that formats a decimal number in its compact form. For example:

NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
System.out.println( fmt.parse("1 thousand") );  // output is "1000"

You would typically use compact number formatting for the environment where the space is limited, and the screen can display onlu short strings.

How to generate code Snippets in Java API Doc

Introduce an @snippet tag for JavaDoc’s Standard Doclet, to simplify the inclusion of example source code in API documentation. For example:

/**
 * The following code shows how to use {@code Optional.isPresent}:
 * {@snippet :
 * if (v.isPresent()) {
 *     System.out.println("v: " + v.get());
 * }
 * }
 */

What is Foreign Function & Memory API ?

Foreign Function & Memory API is an API that allows Java programs to interact with code and data that is outside of the Java runtime environment. The API would enable Java programs to call native libraries (libraries written in a different language like C or C++) and process native data in a safe and efficient way, without the limitations and risks of using a current method called JNI (Java Native Interface).

What are Java Text blocks and when would you use them?

Text Blocks are Java 14 feature that allows developers to write multi-line string literals more easily and with improved readability.

A text block is a string literal that is enclosed in triple quotes (i.e., “””), and can span multiple lines. Within a text block, the Java compiler automatically escapes any special characters and whitespace, making it easier to write and read multi-line strings without having to manually add escape characters or concatenate strings together.

Here’s an example of a text block:

String html1 = """
<html>
    <body>
        <p>Hello, World!</p>
    </body>
</html>""";

String html2 = """<html>
    <body>
        <p>Hello, World!</p>
    </body>
</html>
""";

Text blocks are particularly useful when working with large, complex strings, such as JSON or HTML, as they can make the code more readable and easier to maintain.

WExample.printName() at line 10, and the cause of the NPE is that the this.name field is null. This is shown in the message of the exception.

What are Enhanced Pseudo-Random Number Generators ?

The Java Development Kit (JDK) has introduced a new feature called Enhanced Pseudo-Random Number Generators (PRNGs) to improve the generation of random numbers in programs.

Pseudo-random number generators use algorithms to generate a sequence of numbers that appear random, but are actually deterministic. The current java.util.Random class has some limitations such as using a less secure algorithm and lacks comprehensive options for customization.

Enhanced Pseudo-Random Number Generators offers a new class java.util.Random.SplittableRandom which uses a more advanced and secure algorithm called splitmix64 generator. It also adds several new methods to the java.util.Random class which includes options for generating long, double, and boolean values with different probability distributions, and for generating streams of random values.

For example:

public static byte[] randomBytes(int length) {
 final byte[] bytes = new byte[length];
 final SplittableRandom random = new SplittableRandom();
 for (int i = 0; i < length; i++) {
  bytes[i] = (byte) random.nextInt(256);
 }
 return bytes;
}

How can you run a Java program without using the javac Compiler ?

You can do that using the “Launch Single-File Source-Code Programs” feature of Java 11. This feature allows you to execute a Java source code file directly using the java interpreter. The source code is compiled in memory and then executed by the interpreter, without producing a .class file on disk.

For example, you can launch a Java Class without using javac as follows:

java Hello.java 

You can create a powerful combination by combining this feature with Shebang files (#!). This allows you can to run Java as a shell script, as you run you *nix bash script from command-line.

Do you know any tool that combine these features ?

The JBang Project is a perfect example of that. Learn more here: JBang: Create Java scripts like a pro

What are multi-release JAR Files?

Multi-release JAR files (MRJARs) is a feature introduced in Java 9 that allows a JAR file to contain multiple versions of class files, with the version selected at runtime based on the version of the Java runtime environment.

Normally, when a JAR file is created, all the class files inside it are targeted to a specific version of the Java runtime environment (e.g. Java 8, Java 11, etc.). However, if a JAR file is marked as a multi-release JAR file, it can contain different versions of class files targeted to different versions of the Java runtime environment. This means that when the JAR file is loaded by the Java runtime environment, the appropriate version of the class file will be selected based on the version of the Java runtime environment in use.

The idea behind MRJARS is to help avoiding the problems with Library-dependency problem, like for example when a library is built for Java 8 and cannot be run on Java 11, with MRJARS you can build the library for Java 8, Java 11 and other versions, which allows developers to write code that takes advantage of new features added in recent versions of Java.

Found the article helpful? if so please follow us on Socials