Showing posts with label Java. Show all posts
Showing posts with label Java. Show all posts

Monday, 8 May 2023

Java 9 Module System

Java 9 introduces a new level of abstraction above packages, formally known as the Java Platform Module System (JPMS), or “Modules” for short.

Java 9 modules are one of the biggest changes in the structure of Java.

What is Module?

A Module is a group of closely related packages and resources along with a new module descriptor file (module-info.java).

In other words, it's a “package of Java Packages” abstraction that allows us to make our code even more reusable.

Problems of Current Java System?

"Why we need Java SE 9 Module System” means the problems of the Current Java SE8 or earlier System.

  • As JDK is too big, it is a bit tough to scale down to small devices. Java SE 8 has introduced 3 types of compact profiles to solve this problem: compact1, compact2, and compact3. But it does not solve this problem.
  • JAR files like rt.jar etc are too big to use in small devices and applications.
  • There is no Strong Encapsulation in the current Java System because the “public” access modifier is too open. Everyone can access it.
  • As JDK, JRE is too big, it is hard to Test and Maintain applications.
  • As the public is too open, They are not to avoid accessing of some Internal Non-Critical APIs like the sun.*, *.internal.*, etc. Security is also a big issue.

Advantages of Java SE 9 Module System

Java SE 9 Module System is going to provide the following benefits

  • As Java SE 9 is going to divide JDK, JRE, JARs, etc, into smaller modules, we can use whatever modules we want. So it is very easy to scale down the Java Application to Small devices.
  • Ease of Testing and Maintainability.
  • As the public is not just public, it supports very Strong Encapsulation.
  • We cannot access Internal Non-Critical APIs anymore.
  • Modules can hide unwanted and internal details very safely, and we can get better Security.
  • The application is too small because we can use only whatever modules we want.
  • It's easy to support Single Responsibility Principle (SRP).

Compare JDK 8 and JDK 9

We know what JDK software contains. After installing JDK 8 software, we can see a couple of directories like bin, jre, lib etc in Java Home folder. However, Oracle Corp has changed this folder structure a bit differently as shown below.java 9 modules

Mother of Java 9 Module System

As of now, Java 9 Module System has 95 modules in Early Access JDK. Oracle Corp has separated JDK jars and Java SE Specifications into two sets of Modules.

  • All JDK Modules start with “jdk.*”
  • All Java SE Specifications Modules start with “java.*”

Java 9 Module System has a “java.base” Module. It’s known as Base Module. It’s an Independent module and does NOT dependent on any other modules. By default, all other Modules are dependent on this module. That’s why “java.base” Module is also known as The Mother of Java 9 Modules. It’s the default module for all JDK Modules and User-Defined Modules just like Object class is a base class of all the other classes.


Module Descriptor

When we create a module, we include a descriptor file that defines several aspects of our new module:

  • Name – the name of our module
  • Dependencies – a list of other modules that this module depends on
  • Public Packages – a list of all packages we want accessible from outside the module
  • Services Offered – we can provide service implementations that can be consumed by other modules
  • Services Consumed – allows the current module to be a consumer of a service
  • Reflection Permissions – explicitly allows other classes to use reflection to access the private members of a package

Module Types

There are four types of modules in the new module system:

  • System Modules – These are the modules listed when we run the list-modules command above. They include the Java SE and JDK modules.
  • Application Modules – These modules are what we usually want to build when we decide to use Modules. They are named and defined in the compiled module-info.class file included in the assembled JAR.
  • Automatic Modules – We can include unofficial modules by adding existing JAR files to the module path. The name of the module will be derived from the name of the JAR. Automatic modules will have full read access to every other module loaded by the path.
  • Unnamed Module – When a class or JAR is loaded onto the classpath, but not the module path, it's automatically added to the unnamed module. It's a catch-all module to maintain backward compatibility with previously-written Java code.

We can see what these modules are by typing into the command line:

java --list-modules

Module Declarations

To set up a module, we need to put a special file at the root of our packages named module-info.java.

This file is known as the module descriptor and contains all of the data needed to build and use our new module.


Requires
Our first directive is requires. This module directive allows us to declare module dependencies:
Now, my.module has both a runtime and a compile-time dependency on module.name.

And all public types exported from a dependency are accessible by our module when we use this directive.


Requires Static

Sometimes we write code that references another module, but that users of our library will never want to use.

For instance, we might write a utility function that pretty-prints our internal state when another logging module is present. But, not every consumer of our library will want this functionality, and they don't want to include an extra logging library.

In these cases, we want to use an optional dependency. By using the requires static directive, we create a compile-time-only dependency:


Requires Transitive

We commonly work with libraries to make our lives easier.

But, we need to make sure that any module that brings in our code will also bring in these extra ‘transitive' dependencies or they won't work.

Luckily, we can use the requires transitive directive to force any downstream consumers also to read our required dependencies:

Now, when a developer requires my.module, they won't also have also to say requires module.name for our module to still work.

Exports

By default, a module doesn't expose any of its API to other modules. This strong encapsulation was one of the key motivators for creating the module system in the first place.

Our code is significantly more secure, but now we need to explicitly open our API up to the world if we want it to be usable.

We use the exports directive to expose all public members of the named package:

Now, when someone does requires my.module, they will have access to the public types in our com.my.package.name package, but not any other package.

Exports... To

We can use exports…to to open up our public classes to the world.

But, what if we don't want the entire world to access our API?

We can restrict which modules have access to our APIs using the exports…to the directive.

Similar to the exports directive, we declare a package as exported. But, we also list which modules we are allowing to import this package as a requires. Let's see what this looks like:



Uses

A service is an implementation of a specific interface or abstract class that can be consumed by other classes.

We designate the services our module consumes with the uses directive.

Note that the class name we use is either the interface or abstract class of the service, not the implementation class:


Provides With

A module can also be a service provider that other modules can consume.

The first part of the directive is the provides keyword. Here is where we put the interface or abstract class name.

Next, we have the with directive where we provide the implementation class name that either implements the interface or extends the abstract class.

Here's what it looks like put together:


Open

We mentioned earlier that encapsulation was a driving motivator for the design of this module system.

Before Java 9, it was possible to use reflection to examine every type and member in a package, even the private ones. Nothing was truly encapsulated, which can open up all kinds of problems for developers of the libraries.

Because Java 9 enforces strong encapsulation, we now have to explicitly grant permission for other modules to reflect on our classes.

If we want to continue to allow full reflection as older versions of Java did, we can simply open the entire module up:


Opens

If we need to allow reflection of private types, but we don't want all of our code exposed, we can use the opens directive to expose specific packages.

But remember, this will open the package up to the entire world, so make sure that is what you want:


Opens ... To

Okay, so reflection is great sometimes, but we still want as much security as we can get from encapsulation. We can selectively open our packages to a pre-approved list of modules, in this case, using the opens…to directive:



Thursday, 4 May 2023

Try With Resources Improvement in Java 9

We know, Java SE 7 has introduced a new exception-handling construct: Try-With-Resources to manage resources automatically. The main goal of this new statement is “Automatic Better Resource Management”. Java SE 9 is going to provide some improvements to this statement to avoid some more verbosity and improve some Readability.



Consider the example below. When the try block ends the resources are automatically released. We do not need to create a separate finally block.

Java SE 7 example

public static void main(String[] args) {
//Cannot access the 'br' BufferedReader object outside the try block
try (BufferedReader br = new BufferedReader(new FileReader("FILE_PATH")))
{
String line;
while((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}

How does try with resources work?

try-with-resources is available to any class that implements the AutoCloseable interface. In the above example, BufferedReader implements an AutoCloseable interface.

public interface AutoCloseable {
void close() throws Exception;
}

Java 9 example

public static void main(String[] args) throws FileNotFoundException {
//Access the 'br' BufferedReader object outside the try block
BufferedReader br = new BufferedReader(new FileReader("FILE_PATH"));
try (br)
{
String line;
while((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}

Switch Expression in Java 12

Java 12, JEP 325: Switch Expressions enhanced the traditional switch statement to support the following new features:

  • Multiple case labels
  • Switch expression returning value via break (replaced with yield in Java 13)
  • Switch expression returning value via label rules (arrow)
//Traditional switch statement
public String traditionalSwitch(int number) {
String result = "";
switch (number) {
case 1:
case 2:
result = "one or two";
break;
case 3:
result = "three";
break;
case 4:
case 5:
case 6:
result = "four or five or six";
break;
default:
result = "unknown";
}
return result;
}

//Now, this switch is an expression (returning something), also called value breaks.
public String newSwitch(int number) {
String result = "";
switch (number) {
case 1, 2:
result = "one or two";
break;
case 3:
result = "three";
break;
case 4, 5, 6:
result = "four or five or six";
break;
default:
result = "unknown";
}
return result;
}

//We also can return value via arrow or label rules.
public String newSwitchWithReturn(int number) {
String result = switch (number) {
case 1, 2 -> "one or two";
case 3 -> "three";
case 4, 5, 6 -> "four or five or six";
default -> "unknown";
};
return result;
}

//In Java 13, we can use yield to return a value.
private static int newSwitchWithYield(String mode) {
int result = switch (mode) {
case "a", "b":
yield 1;
case "c":
yield 2;
case "d", "e", "f":
// do something here...
System.out.println("Supports multi line block!");
yield 3;
default:
yield -1;
};
return result;
}

Sealed Classes in Java 15

Sealed classes and interfaces restrict which other classes or interfaces may extend or implement them.

For background information about sealed classes and interfaces, see JEP 409.

One of the primary purposes of inheritance is code reuse: When you want to create a new class and there is already a class that includes some of the code that you want, you can derive your new class from the existing class. In doing this, you can reuse the fields and methods of the existing class without having to write (and debug) them yourself.

However, what if you want to model the various possibilities that exist in a domain by defining its entities and determining how these entities should relate to each other? For example, you're working on a graphics library. You want to determine how your library should handle common geometric primitives like circles and squares. You've created a Shape class that these geometric primitives can extend. However, you're not interested in allowing any arbitrary class to extend Shape; you don't want clients of your library declaring any further primitives. By sealing a class, you can specify which classes are permitted to extend it and prevent any other arbitrary class from doing so.

Declaring Sealed Classes

To seal a class, add the sealed modifier to its declaration. Then, after any extends and implements clauses, add the permits clause. This clause specifies the classes that may extend the sealed class.

//For example, the following declaration of Shape specifies three permitted subclasses, Circle, Square, and Rectangle:
//Define the following three permitted subclasses, Circle, Square, and Rectangle, in the same module or in the same
//package as the sealed class:
sealed class Shape
permits Circle, Square, Rectangle {
}
final class Circle extends Shape {
public float radius;
}

non-sealed class Square extends Shape {
public double side;
}

sealed class Rectangle extends Shape permits FilledRectangle {
public double length, width;
}

final class FilledRectangle extends Rectangle {
public int red, green, blue;
}

Constraints on Permitted Subclasses

Permitted subclasses have the following constraints:

  • They must be accessible by the sealed class at compile time.

    For example, to compile Shape.java, the compiler must be able to access all of the permitted classes of Shape: Circle.java, Square.java, and Rectangle.java. In addition, because Rectangle is a sealed class, the compiler also needs access to FilledRectangle.java.

  • They must directly extend the sealed class.

  • They must have exactly one of the following modifiers to describe how it continues the sealing initiated by its superclass:

    • final: Cannot be extended further

    • sealed: Can only be extended by its permitted subclasses

    • non-sealed: Can be extended by unknown subclasses; a sealed class cannot prevent its permitted subclasses from doing this

    For example, the permitted subclasses of Shape demonstrate each of these three modifiers: Circle is final while Rectangle is sealed and Square is non-sealed.

  • They must be in the same module as the sealed class (if the sealed class is in a named module) or in the same package (if the sealed class is in the unnamed module, as in the Shape.java example).

    For example, in the following declaration of sample.Shape, its permitted subclasses are all in different packages. This example will compile only if Shape and all of its permitted subclasses are in the same named module.


sealed class Shape
permits sample.Circle,
sample.Rectangle,
sample.Square { }


Record Classes in Java 14

Record classes, which are a special kind of class in Java-14, help to model plain data aggregates with less ceremony than normal classes. 

For background information about record classes, see JEP 395.

 Why do we need the Record Classes?

One of the common complaints with Java has been its verbosity. If you have to create a simple POJO class, it requires the following boilerplate code.

  • Private fields
  • Getter and Setter Methods
  • Constructors
  • hashCode(), equals(), and toString() methods.

This verbosity is one of the reasons for the high interest in Kotlin and Lombok.

For example, here is a record class with two fields:

record Rectangle(double length, double width) { }

This concise declaration of a rectangle is equivalent to the following normal class:

public final class Rectangle {

private final double length;
private final double width;

public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}

public double getLength() {
return length;
}

public double getWidth() {
return width;
}

// An implementation of toString() that returns a string
// representation of all the record class's fields,
// including their names.
@Override
public String toString() {
return "Rectangle{" +
"length=" + length +
", width=" + width +
'}';
}

// Implementation of equals() and hashCode(), which specify
// that two record objects are equal if they
// are of the same type and contain equal field values.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Rectangle rectangle = (Rectangle) o;
return Double.compare(rectangle.length, length) == 0 && Double.compare(rectangle.width, width) == 0;
}

@Override
public int hashCode() {
return Objects.hash(length, width);
}
}

As record classes are just special kinds of classes, you create a record object (an instance of a record class) with the new keyword, for example:

Rectangle r = new Rectangle(4,5);

The Canonical Constructor of a Record Class

The following example explicitly declares the canonical constructor for the Rectangle record class. It verifies that length and width are greater than zero. If not, it throws an IllegalArgumentException:
record Rectangle(double length, double width) {
public Rectangle(double length, double width) {
if (length <= 0 || width <= 0) {
throw new java.lang.IllegalArgumentException(
String.format("Invalid dimensions: %f, %f", length, width));
}
this.length = length;
this.width = width;
}
}

Repeating the record class's components in the signature of the canonical constructor can be tiresome and error-prone. To avoid this, you can declare a compact constructor whose signature is implicit (derived from the components automatically).

For example, the following compact constructor declaration validates length and width in the same way as in the previous example:

record Rectangle(double length, double width) {
public Rectangle {
if (length <= 0 || width <= 0) {
throw new java.lang.IllegalArgumentException(
String.format("Invalid dimensions: %f, %f", length, width));
}
}
}

This succinct form of constructor declaration is only available in a record class. Note that the statements this.length = length; and this.width = width; which appear in the canonical constructor but do not appear in the compact constructor. At the end of a compact constructor, its implicit formal parameters are assigned to the record class's private fields corresponding to its components.

Important Points about Record Classes

  1. A Record class is final, so we can’t extend it.
  2. The Record classes implicitly extend java.lang.Record class.
  3. All the fields specified in the record declaration are final.
  4. The record fields are “shallow” and immutable and depend on the type. For example, we can change the addresses field by accessing it and then making updates to it.
  5. A single constructor is created with all the fields specified in the record definition.
  6. The Record class automatically provides accessor methods for the fields. The method name is the same as the field name, not like generic and conventional getter methods.
  7. The Record class provides hashCode(), equals(), and toString() implementations too.

Conclusion

Java Records is a welcome addition to the core programming features. You can think of it as a “named tuple”. It’s meant to create a data carrier object with a compact structure, avoiding all the boiler-plate code.