Categories
Java

Hidden Classes in Java 14

This feature was introduced in Java 14, and it allows you to create classes at runtime and not load them into the classpath. These classes are created using the Lookup.defineHiddenClass method and it can only be accessed using the Lookup object that created the class. This feature can be used to improve the performance and security of the application, as it allows you to load classes only when they are needed and also it can be used to hide the implementation details of the classes from the users.

Here’s an example of how to use Hidden Classes:

Lookup lookup = MethodHandles.lookup();
byte[] classBytes = // bytecode of the class
Class<?> hiddenClass = lookup.defineHiddenClass(classBytes, true).lookupClass();

In this example, we are creating a hidden class using the Lookup.defineHiddenClass method and passing the bytecode of the class, and the class is not loaded into the classpath and it can only be accessed using the Lookup object that created the class.

This feature can be useful in situations where you want to improve the performance of the application and also to hide the implementation details of the classes from the users, but it’s important to note that hidden classes are not supported by all JVMs and it’s a new feature that’s not widely used yet.

Practical example

Let’s say you have an application that allows users to install and run different plugins, and each plugin is implemented as a separate Java class.

Normally, with traditional Java classes, all of the plugin classes would need to be loaded at the same time and linked with the application’s main class, even if the user is only using a small subset of the available plugins. This can lead to increased startup time and memory usage.

With the hidden classes feature, you can now create an “interface” class and a “factory” class that are both visible to the main application. The plugin classes themselves can be hidden, so they are not loaded and linked with the main class until they are actually needed. This allows for lazy loading of the plugins, which can improve startup time and reduce memory usage.

In this case, the hidden classes feature allows the developer to only load the classes that are required by the user, reducing the memory usage and initial loading time, making the application more efficient and responsive.

Source code:

interface Plugin {
    void run();
}

class PluginFactory {
    private final Map<String, Class<? extends Plugin>> hiddenPlugins = new HashMap<>();

    void registerPlugin(String name, byte[] bytecode) {
        Class<? extends Plugin> pluginClass = defineHiddenClass(bytecode, Plugin.class).asSubclass(Plugin.class);
        hiddenPlugins.put(name, pluginClass);
    }

    Plugin createPlugin(String name) {
        Class<? extends Plugin> pluginClass = hiddenPlugins.get(name);
        if (pluginClass == null) {
            throw new IllegalArgumentException("Unknown plugin: " + name);
        }
        try {
            return pluginClass.getConstructor().newInstance();
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException("Failed to create plugin instance", e);
        }
    }
}

class Main {
    public static void main(String[] args) {
        PluginFactory factory = new PluginFactory();
        factory.registerPlugin("example", getBytecodeForExamplePlugin());
        Plugin plugin = factory.createPlugin("example");
        plugin.run();
    }
}

In this example, the Plugin interface and PluginFactory class are visible to the main application. The individual plugin classes are hidden, and their bytecode is passed to the registerPlugin method of the PluginFactory class. The createPlugin method can then instantiate a new instance of the hidden class by calling the getConstructor().newInstance() method, and then invoke its run() method.

Categories
Java

Yield in Java 14

The “yield” statement allows you to return a value from a method that uses the yield keyword and it also allows the method to be resumed later on, when the next value is requested.

This allows you to implement a simple form of generator, which can be useful when you want to generate a sequence of values without having to create an explicit data structure to hold all of the values.

Here’s an example why to use the yield statement:

class PrimeNumbers {
    static Iterator<Integer> sequence() {
        int current = 2;
        while (true) {
            boolean isPrime = true;
            for (int i = 2; i <= Math.sqrt(current); i++) {
                if (current % i == 0) {
                    isPrime = false;
                    break;
                }
            }
            if (isPrime) {
                yield current;
            }
            current++;
        }
    }
}

In this example, we are creating an Iterator that generates the prime numbers, the method sequence uses the yield statement to return the next prime number in the sequence and the method will be resumed when the next value is requested.

Without using the yield statement, we would have to create an explicit data structure, like an array, to hold all of the prime numbers, and it would consume more memory and resources, also it would make the code more complex and harder to read.

By using the yield statement, we don’t have to create an explicit data structure to hold all of the prime numbers, and it consumes less memory and resources, also it makes the code more readable and efficient, as the method only generates the next prime number when it’s requested.

It’s worth noting that testing code that uses the yield statement can be more difficult as well, as the code can have multiple execution paths depending on the values requested.

Categories
Java

Switch expressions

This feature was introduced in Java 12 and allows you to use the switch statement in a more powerful and expressive way, by allowing to return a value or to assign a value to a variable directly from the switch statement.

With switch expressions, you can use the switch statement in combination with an arrow operator -> and the variable declaration or assignment, and the compiler will automatically extract the value of the matched case and return it or assign it to the variable.

Here’s an example of how using switch expressions can be more useful and efficient than the previous traditional way of using if-else statements:

enum Size {
    SMALL, MEDIUM, LARGE, EXTRA_LARGE;
}

int getDiscount(Size size) {
    return switch (size) {
        case SMALL -> 5;
        case MEDIUM -> 10;
        case LARGE -> 15;
        case EXTRA_LARGE -> 20;
    };
}

In this example, we have an enum Size which has 4 possible values, and we want to return a discount percentage based on the value of the size enum.

Using switch expressions, we can use the switch statement to match the values of the size enum, and the corresponding case will be selected and the value following the arrow operator will be returned. This way the code is more concise, readable, and efficient.

Without switch expressions, we would have to use the traditional if-else statements, like this:

int getDiscount(Size size) {
    if (size == Size.SMALL) {
        return 5;
    } else if (size == Size.MEDIUM) {
        return 10;
    } else if (size == Size.LARGE) {
        return 15;
    } else if (size == Size.EXTRA_LARGE) {
        return 20;
    } else {
        throw new IllegalArgumentException("Invalid size: " + size);
    }
}

As you can see, in the above example, the code is more verbose, less readable and not as efficient.

Categories
Java

Pattern matching in Java

This feature was introduced in Java 14 and allows you to use the instanceof operator in a more powerful and expressive way.

With pattern matching, you can use the instanceof operator in combination with a variable declaration or assignment, and the compiler will automatically extract the value of the matched type and assign it to the variable.

Here’s an example of how to use pattern matching for instanceof:

List<Object> items = Arrays.asList("Hello", 1, "world", 2);
for (Object item : items) {
    if (item instanceof String s) {
        System.out.println("String: " + s);
    } else if (item instanceof Integer i) {
        System.out.println("Integer: " + i);
    }
}

In this example, we have a List of Objects that contains a mix of String and Integer values. We want to iterate through the list and perform different actions based on the type of the object.

Using pattern matching for instanceof, we can use the instanceof operator in combination with a variable declaration, and the compiler will automatically extract the value of the matched type and assign it to the variable, in this case s for String and i for Integer. This way we can directly use the matched variable s or i to perform the actions we want.

Without pattern matching, we would have to use the traditional instanceof operator and then use a separate casting statement, like this:

for (Object item : items) {
    if (item instanceof String) {
        String s = (String) item;
        System.out.println("String: " + s);
    } else if (item instanceof Integer) {
        Integer i = (Integer) item;
        System.out.println("Integer: " + i);
    }
}

As you can see, in the above example, the code is more verbose and less readable. Also, if the casting statement is incorrect, it will throw a runtime exception, whereas the pattern matching feature is checked by the compiler, which can catch type errors at compile-time.

So, in this case, pattern matching makes the code more concise, readable, and less prone to errors.

Categories
Java

Records in Java

Lombok is dead?

This feature was introduced in Java 14 and allows you to define a simple data class, with an automatically generated constructor, accessors, equals, hashCode, and toString methods.

A record is defined by using the record keyword, followed by the class definition and the (fields) clause, which specifies the fields of the record.

Here’s an example of how to define a record:

record Person(String firstName, String lastName, int age) { }

In this example, the Person record is defined with three fields: firstName, lastName, and age.

This feature can be useful in situations where you want to define simple data classes, where the primary purpose of the class is to hold data, and you don’t need to add any additional behavior. The automatically generated methods make it easy to handle the state of the object, and it allows for less boilerplate code and more readable and maintainable code.

Categories
Java

Sealed classes in Java

An advanced and less well-known feature in the Java is the use of “sealed classes.” This feature was introduced in Java 14 and allows you to indicate that a class can only be subclassed by a specific set of classes or interfaces.

A sealed class is defined by using the sealed keyword, followed by the class definition and the permits clause, which specifies the set of classes or interfaces that are allowed to subclass the sealed class.

Here’s an example of how to define a sealed class:

public sealed class Shape permits Circle, Square {
    // class definition
}

public final class Circle extends Shape {
    // class definition
}

public final class Square extends Shape {
    // class definition
}

In this example, the Shape class is defined as a sealed class, and it can only be subclassed by the Circle and Square classes. Any attempt to subclass the Shape class with any other class would result in a compile-time error.

This feature can be useful in situations where you want to control the set of classes that can inherit from a certain class, to improve the maintainability and robustness of your code.

It’s worth noting that this feature is relatively new and not widely used yet, and requires understanding of the OOP concepts and proper usage of the sealed classes.

Categories
Java

Java: invokedynamic

The use of “invokedynamic” instruction is another advanced feature in Java that is less well-known. This JVM instruction allows for dynamic method invocation at runtime, instead of the traditional static linking that happens at compile-time.

The main advantage of using invokedynamic is that it allows for more flexibility and performance when working with dynamically typed languages, such as JavaScript or Ruby, that run on the JVM using a language runtime.

Invokedynamic also allows for more advanced features such as:

  • Method handle: A MethodHandle is a typed, directly executable reference to an underlying method, constructor, field, or similar low-level operation, with optional transformations of arguments or return values.
  • Dynamic linking: The Java Language Specification allows for dynamic linking, which means that you can change the behavior of a method at runtime.
  • Improved performance: Invokedynamic can improve the performance of your code by avoiding the overhead of reflection and dynamic method dispatch.

Invokedynamic is an advanced feature that requires knowledge of the JVM internals, and it’s not commonly used by Java developers, but for developers who work on dynamic languages or write performance-critical code, it can be a powerful tool to have in your toolbox.

Lets start with stupid Hello World example:

import java.lang.invoke.*;

public class InvokeDynamicExample {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType type = MethodType.methodType(String.class, String.class);
        CallSite site = LambdaMetafactory.metafactory(
                lookup, "concat", MethodType.methodType(String.class, String.class),
                type.erase(), lookup.findVirtual(String.class, "concat", type), type);
        MethodHandle mh = site.getTarget();
        String result = (String) mh.invokeExact("Hello, ", "world!");
        System.out.println(result); // prints "Hello, world!"
    }
}

This example uses the LambdaMetafactory class to create a dynamic invocation of the concat method on the String class, using a MethodHandle to invoke the method at runtime.

This example uses the MethodHandles and MethodType classes to define the signature of the concat method, and the CallSite class to represent the target of the invocation.

This allows for a more flexible way of invoking methods, that doesn’t require knowing the target method at compile-time and allows for more dynamic behavior.

One more useful example:

import java.lang.invoke.*;

public class InvokeDynamicExample {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType type = MethodType.methodType(int.class, int.class);
        CallSite site = LambdaMetafactory.metafactory(
                lookup, "compare", MethodType.methodType(int.class, Object.class, Object.class),
                type.erase(), lookup.findVirtual(Comparable.class, "compareTo", type), type);
        MethodHandle mh = site.getTarget().asType(type);
        int result = (int) mh.invokeExact(5, 3);
        System.out.println(result); // prints 2
    }
}

This example uses the LambdaMetafactory class to create a dynamic invocation of the compareTo method on the Comparable interface, using a MethodHandle to invoke the method at runtime.

This allows for comparing objects of any type that implements the Comparable interface, without knowing the specific type at compile time.

This example can be useful in cases where you have a collection of objects that implement the Comparable interface and you want to sort them without knowing the specific type of the objects, you can use this example to sort them in a generic way!

Categories
Java

Memory leaks in {{ }}

One not very well-known feature in Java is the use of the “double brace initialization” technique. This technique allows for the creation of anonymous inner classes to initialize a collection, such as a List or Set, in a more concise and readable way.

For example, instead of using a traditional for loop to add elements to a List, you can use double brace initialization to create and initialize the List in a single line of code:

List<String> list = new ArrayList<String>() {{
    add("item1");
    add("item2");
    add("item3");
}};

However, this technique should be used with caution, as it can lead to memory leaks if not used properly.

The reason that double brace initialization can lead to memory leaks is that it creates an anonymous inner class, which has an implicit reference to the enclosing class (i.e. the class in which the inner class is defined).

This reference will be kept alive as long as the inner class is alive, which means that it will prevent the enclosing class from being garbage collected.

If the inner class is used to initialize a static field, or if it’s a member of a singleton, then the reference to the enclosing class will be kept alive for the lifetime of the application, causing a memory leak.

To avoid memory leaks, it’s recommended to use the double brace initialization technique only for local variables, and make sure that the inner class doesn’t hold references to the enclosing class, or any other objects that can’t be garbage collected.

Alternatively, instead of using double brace initialization, you can use the Collections.addAll() method to add multiple items to a collection in a more efficient and less error-prone way.