Showing posts with label record. Show all posts
Showing posts with label record. Show all posts

Tuesday, February 23, 2021

Reflection support for record classes

Two new methods are added to java.lang.Class to examine if a class or a Record class and if so, to find out its components. 

These two methods are 

1. isRecord - which returns a boolean indicating if the class is a record or not

2. getRecordComponents - which returns an array of java.lang.reflect.RecordComponent objects with each element representing a component of the record in the same order

Below sample code demonstrates the usage of these two methods

import java.lang.reflect.RecordComponent;

class Dog {
}

record Cat(String color) {
}

record Pair(Cat cat, Dog dog) {
}

class Main {

    public static void main(String[] args) {
        checkRecord(Dog.class);
        checkRecord(Cat.class);
        checkRecord(Pair.class);
    }

    private static void checkRecord(Class classToCheck) {
        System.out.println("\n" + classToCheck.getName() + " is a record class: " + classToCheck.isRecord());
        if (classToCheck.isRecord()) {
            System.out.print("Its components are: ");

            RecordComponent[] rComponents = classToCheck.getRecordComponents();
            for (RecordComponent component : rComponents) {
                System.out.print(component + ", ");
            }
            System.out.println();
        }

    }
}

In the above code, we have one regular class Dog and two record classes - Cat & Pair. 

While Cat has one component - color of type string, Pair has two components - Cat as the first component and Dog as its second component. 

The output of the above program is

Dog is a record class: false

Cat is a record class: true
Its components are: java.lang.String color,

Pair is a record class: true
Its components are: Cat cat, Dog dog,

As expected, for Cat and Pair, isRecord return true and its components gets printed.

java.lang.reflect.RecordComponent is a new class introduced in Java 14 to provide reflection access to record components. API documentation for this class is available at https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/reflect/RecordComponent.html    


Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/Reflection-support-for-record-classes

Friday, February 19, 2021

Serialization of Record with circular references

There is a difference in the way records gets serialized and de-serialized. The complete spec for record serialization is available at https://docs.oracle.com/en/java/javase/15/docs/specs/records-serialization.html for reference.

Now lets consider one specific case where we have circular reference between objects that are serialized. 

Below code sample illustrates this

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * Cat class. Contains a member of type Kitten
 */
class Cat implements Serializable {

    private Kitten child;

    // Constructor
    public Cat(Kitten child) {
        this.child = child;
    }

    // Accessor to get child reference
    public Kitten child() {
        return this.child;
    }

    public String toString() {
        return "Mother@"+this.hashCode();
    }
}

/**
 * Kitten class. Contains a member of type Cat
 */
class Kitten implements Serializable {

    private Cat mother;

    // Accessor to get mother reference
    public Cat mother() {
        return mother;
    }

    // Setter to set mother of the Kitten
    public void setMother(Cat mother) {
        this.mother = mother;
    }

    
    public String toString() {
        return "Child@"+this.hashCode();
    }

}

public class SerializeTest {
    public static void main(String args[]) throws IOException, FileNotFoundException, ClassNotFoundException {
        
        Kitten child = new Kitten(); // Creating a Kitten object
        Cat mother = new Cat(child); // Creating a Cat object, with Kitten object created as its child
        child.setMother(mother); // Set the Cat object created in the previous step as mother for the Kitten object, thus creating circular reference

        System.out.println("****Before****");
        print(mother); // Print the object tree of the mother

        // Serialize and write the mother object to a file
        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("out.obj"));
        os.writeObject(mother);
        os.close();

        // De-serialize and read the mother object from the file
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("out.obj"));
        mother = (Cat) in.readObject();

        System.out.println("****After****");
        print(mother); // Print the object tree of the mother after de-serialization
    }

    // Utility method to print the mother object tree
    public static void print(Cat mother) {
        System.out.println("mother is: " + mother);
        System.out.println("mother.child is: " + mother.child());        
        System.out.println("mother.child.mother is: " + mother.child().mother());
    }
}

Here we have a Cat & Kitten classes. Cat has a reference to Kitten & Kitten has a reference to Cat. We create instances of Cat and Kitten and set references so as to create a circular reference. We then print the object tree to see the circular referencing of objects in the object tree. 

Now we write the object to a file thus serializing the object tree. We then read from the file and recreate the object tree thus performing de-serialization. Comments are added in the code snippet above providing details about the logic. Pls. refer the same for additional details.

Note that in this version of code Cat and Kitten are now regular classes. We will change the Cat implementation as a Record and see what happens shortly, but before that lets see how the serialization and deserialization of object tree behaves with regular classes.  

We execute the above code and get the output as shown below

****Before****
mother is: Mother@1325547227
mother.child is: Child@1528902577
mother.child.mother is: Mother@1325547227
****After****
mother is: Mother@584634336
mother.child is: Child@1469821799
mother.child.mother is: Mother@584634336

We see that circular references are preserved after de-serialization. Perfect!!! 

Now lets change the Cat implementation as Record. Below is the excact same code with Cat implementation alone changed as record class instead of a regular class

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * Cat class. Contains a member of type Kitten
 */
record Cat(Kitten child) implements Serializable {

    public String toString() {
        return "Mother@"+this.hashCode();
    }
}

/**
 * Kitten class. Contains a member of type Cat
 */
class Kitten implements Serializable {

    private Cat mother;

    // Accessor to get mother reference
    public Cat mother() {
        return mother;
    }

    // Setter to set mother of the Kitten
    public void setMother(Cat mother) {
        this.mother = mother;
    }

    
    public String toString() {
        return "Child@"+this.hashCode();
    }

}

public class SerializeTest {
    public static void main(String args[]) throws IOException, FileNotFoundException, ClassNotFoundException {
        
        Kitten child = new Kitten(); // Creating a Kitten object
        Cat mother = new Cat(child); // Creating a Cat object, with Kitten object created as its child
        child.setMother(mother); // Set the Cat object created in the previous step as mother for the Kitten object, thus creating circular reference

        System.out.println("****Before****");
        print(mother); // Print the object tree of the mother

        // Serialize and write the mother object to a file
        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("out.obj"));
        os.writeObject(mother);
        os.close();

        // De-serialize and read the mother object from the file
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("out.obj"));
        mother = (Cat) in.readObject();

        System.out.println("****After****");
        print(mother); // Print the object tree of the mother after de-serialization
    }

    // Utility method to print the mother object tree
    public static void print(Cat mother) {
        System.out.println("mother is: " + mother);
        System.out.println("mother.child is: " + mother.child());        
        System.out.println("mother.child.mother is: " + mother.child().mother());
    }
}

See how succinct and void of ceremonies the Cat implementation is as Record...

Now we execute this code and the output is...

****Before****
mother is: Mother@764977973
mother.child is: Child@764977973
mother.child.mother is: Mother@764977973
****After****
mother is: Mother@1811075214
mother.child is: Child@1811075214
mother.child.mother is: null

Before image object tree is the same as that with the regular classes. 

But after serialization and de-serialization, we see that circular reference is broken and the default value for object - 'null' is set instead for the child objects mother reference. 

This is because of the way a Record objects gets de-serialized:

  1. First all the component fields of the Record gets created - reading from the stream data
  2. And then Record object is created by calling the Record class canonical constructor  

When the first step is performed in our example, Kitten (child) object gets created and since at this time the Cat (mother) record object is not yet created, it set to the default null value. 

Then in the second step, Cat (mother) object is created with the Kitten (child) object created in the 1st step as its component. This Kitten (child) object has its mother set to null which explains the output above. 

For full technical details on serialization & de-serialization of record classes, see the spec link mentioned at the top of this post 


Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/Serialization-of-Record-with-circular-references

Compact constructor for Record classes

Consider a scenario where we wanted to validate the parameter before constructing the object. 

In our Cat record class example, we wanted to validate the color parameter before allowed an instance of Cat to be created. 

We can have a canonical constructor for our Cat record, in which we include the validation. Code for this shown below

record Cat(String color) {
    Cat(String color) {
        if (color.matches(".*\\d.*")) {
            throw new IllegalArgumentException("Invalid color");
        }
        this.color = color;
    }
}

Here, we validate the color parameter in the constructor to check if it contains any numeric character in it. 

And if it does, we throw an IllegalArgumentException. Otherwise we proceed to complete the creation of the object by assigning the color to auto-generated instance variable.

Note however that there is repetition of the parameter list - one at the record header and again the same list is repeated in the constructor. 

This can be avoided by using a compact form of constructor declaration supported for record classes. 

Below is the same code with compact constructor

record Cat(String color) {
    Cat {
        if (color.matches("*\\d*")) {
            throw new IllegalArgumentException("Invalid color");
        }
    }
}

The parameter list for compact constructor is implicit, and is the same as that of component list in record definition. 

Also note the missing assignment statement in the compact constructor form

this.color = color;

This again is implicit, all implicit parameters gets assigned to their corresponding instance variables. 

Another question is at what point in time is this assignment done? 

Well, its done at the end of the constructor, after all the custom validation code in the compact constructor is executed. To illustrate this, consider the following code 

record Cat(String color) {
    Cat {
        if (this.color.matches("*\\d*")) {
            throw new IllegalArgumentException("Invalid color");
        }
    }
}

Here we try to use this.color in our validation logic. 

This code fails to compile, throwing an error "variable color might not have been initialized", indicating that the parameter assignment to instance variables happens at the end of construtor.


Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/Compact-constructor

Wednesday, February 17, 2021

More restrictions with using record classes

Instance Variables not allowed

A record class cannot have instance variables defined in its body. 

The components in the record header gets converted to instance variables in its generated class. But we cannot declare an instance variable explicitly - either matching the one defined in the header or a completely new one. 

The below code will not compile

record Cat(String color) {
    
    // Not allowed - Declaring the instance variable for a record component explicitly
    private final String color; 

    // Not allowed - Declaring an instance variable not listed in the record header
    private final String breed; 

}

This code throws a compilation error stating "field declaration must be static" and a message "(consider replacing field with record component)"

So, the only set of instance variables that we can have in a record class is the components included in its header definition.

Instance initialization blocks not allowed

Also not allowed are instance initialization blocks inside the record body

record Cat(String color) {

    // Not allowed - Having instance initialization block
    {
        System.out.println("Creating an instance");
    }
}

Above code throws an error stating "instance initializers not allowed in records" during compilation.

Native methods not allowed

One more restriction that applies for record classes is that it cannot have any native methods defined in it

record Cat(String color) {
    
    // Not allowed - implementing a native method inside a record
    native void iAmNative();
}

This throws a compilation error "modifier native not allowed here"

Usage of APIs or Classes named "Record"

This is more of a convention to be taken care of when using any API or Class with the name as Record

This stems from the fact that Java language imports all classes in java.lang package using 

import java.lang.*;

With the introduction of java.lang.Record which is the base class of all record classes, the name Record conflicts with the Record class imported from java.lang

For example, lets say we have a our custom class named "Record" defined inside "test" package

package test;

public class Record {

}

If we want to import and use this class, as in the below code

import test.*;

class Main {
    public static void main(String args[]) {
        Record rec = new Record();
    }
}

This would give a compilation error stating "reference to Record is ambiguous"

Import with fully qualified name to get around this problem. The below code makes right reference to the Record class 

import test.Record;

class Main {
    public static void main(String args[]) {
        Record rec = new Record();
    }
}

We will get into exploring some more features of record classes in the next post.


Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/More-restrictions

Tuesday, February 16, 2021

Restrictions with record classes

In a previous post, we saw how a record class gets converted to a regular class definition during compilation. Though that conceptually shows how a record class behaves, it doesn't represent the conversion accurately. 

Lets see how exactly a record class is getting converted during compilation. We will use the same simple, single line Cat record definition for this analysis

record Cat(String color) {}

Lets compile this record class using the below command

javac --enable-preview --release 15 Cat.java

This will generate the Cat.class file in the same folder. We will analyze this class using reflection. For this purpose, we will use the below reflect utility - which prints out the basic details about the class to the console.

import java.lang.reflect.Modifier;

public class ReflectClass {

    public static void reflect(Class<?> ref) {
        log("Name",ref.getName());
        log("Module",ref.getModule().getName());
        log("PackageName",ref.getPackageName());
        log("Superclass",ref.getSuperclass().getName());
        log("Modifiers",Modifier.toString(ref.getModifiers()));
        log("DeclaredConstructors", ref.getDeclaredConstructors());
        log("DeclaredFields", ref.getDeclaredFields());
        log("DeclaredMethods", ref.getDeclaredMethods());
        
    }

    static void log(String name, Object[] values) {
        log(String.format("****%s****", name));
        int i = 0;
        for (Object val:values) {
            log(name+"-"+i++, val.toString());
        }
    }

    static void log(String name, String value) {
        log(String.format("%-25s: %s", name, value));
    }

    static void log(String data) {
        System.out.println(data);;
    }
    
    public static void main(String args[]) throws ClassNotFoundException {
        reflect(Class.forName(args[0]));
    }

}

Now lets try to analyze the Cat class using this utility by running the below command 

java --enable-preview ReflectClass Cat

This generates the output shown below

Name                     : Cat
Module                   : null
PackageName              :
Superclass               : java.lang.Record
Modifiers                : final
****DeclaredConstructors****
DeclaredConstructors-0   : Cat(java.lang.String)
****DeclaredFields****
DeclaredFields-0         : private final java.lang.String Cat.color
****DeclaredMethods****
DeclaredMethods-0        : public final boolean Cat.equals(java.lang.Object)
DeclaredMethods-1        : public final java.lang.String Cat.toString()
DeclaredMethods-2        : public final int Cat.hashCode()
DeclaredMethods-3        : public java.lang.String Cat.color()

Key details from the output highlighted in yellow. Inferences from this output are

1. The generated Cat class extends java.lang.Record. All record classes extend this class. It implies that a record class cannot extend any other class

2. Also we see the class generated is final - indicating that this record class cannot be extended by any other class

3. The record components gets converted to private final instance variables and there are not setter methods. The record components are immutable and cannot be changed after initialization. Note however that this immutability is shallow and reference attributes within a component may be mutable

4. A significant difference to note when working records is the deviation from the Java convention that the public accessor method is named color() and not getColor(). The generated accessor method does not follow the getter convention

Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/Restrictions-with-record-classes


Monday, February 15, 2021

Structure of record definition

In this post, lets look at the structure of a record definition and understand its constituents

A record definition looks like this



Each part of the definition explained below:

  • access specifier - Specifies the scope for the record type. This is optional and if specified should be public. When not given, the record is defined in default scope. 
  • Record keyword - The keyword indicating that this is a record definition
  • Name of the record - The Name for the record type defined
  • Components of the record - Specifies the list of attributes that comprise this record, each defined with its type. This is optional and a record can be defined without any components. This section is also referred to as the record header
  • Body of the record - Block containing the implementation code for the record. It can be left empty to have the default behavior of the record. To add new functionality to the record or to override the default behavior, implementation code should be provided in the body.   

Here is a bare minimum definition for a record

record Cat() {}

We touched upon how the record definition gets converted to a class definition with constructor, accessor and other method implementations during compilation in the previous post. 

Lets dig a bit deeper into this and see what actually happens in the next post.

Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/Structure-of-record-definition

Sunday, February 14, 2021

Record classes in Java

The main idea behind Record classes introduced in Java 14 as a preview feature and enhanced with additional features in Java 15 is to eliminate ceremonies associated with creating plain data aggregate classes. 

Consider for example a Cat class which has one attribute - a String that represents the color of the Cat. At a bare minimum, we will have an instance variable to hold the color attribute, a constructor to initialize the Cat object and accessor methods for the single instance variable.

Code for this simple Cat implementation is given below

public class Cat {

    private String color;

    public Cat(String color) {
        this.color = color;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

}

Now lets strengthen this class implementation to production grade. We will add the implementation for these 3 methods to our Cat class

1. equals() - which returns true if the two cats compared has the same color

2. hashCode() - which should return the same value for two cat objects if they are equal as determined by the equals() implementation

3. toString() - which returns readable String representation for the Cat object. 

Below is the code with these 3 methods added to the Cat class

import java.util.Objects;

public class Cat {

    private String color;

    public Cat(String color) {
        this.color = color;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public boolean equals(Object that) {
        if (this == that) {
            return true;
        } else if (!(that instanceof Cat)) {
            return false;
        } else {
            Cat thatCat = (Cat) that;
            return Objects.equals(this.color, thatCat.color);
        }
    }

    @Override
    public int hashCode() {
        return this.color.hashCode();
    }

    @Override
    public String toString() {
        return String.format("Cat[color=%s]", this.color);
    }
}

Now we want to make this class immutable. We do this by removing the only setter method in the class definition - the setColor() method.

We also want to make it non-extendable so that no other class inherits from this class. This we achieve of course by making the class final. 

Final class definition with all these changes is:

import java.util.Objects;

public final class Cat {

    private String color;

    public Cat(String color) {
        this.color = color;
    }

    public String getColor() {
        return color;
    }

    @Override
    public boolean equals(Object that) {
        if (this == that) {
            return true;
        } else if (!(that instanceof Cat)) {
            return false;
        } else {
            Cat thatCat = (Cat) that;
            return Objects.equals(this.color, thatCat.color);
        }
    }

    @Override
    public int hashCode() {
        return this.color.hashCode();
    }

    @Override
    public String toString() {
        return String.format("Cat[color=%s]", this.color);
    }
}

The above code can be replaced by a simple record definition code below

public record Cat(String color) {}

So a record allows us to create plan data aggregation classes through simple definition eliminating the need to create ceremonial code needed with regular class definition. 

And yes... java converts record definition into equivalent class during compilation. We will explore in-depth, the characteristics and behavior of record classes in the next few posts.

Structure of record definition

Restrictions with record classes

More restrictions with using record classes

Compact constructor for Record classes

Serialization of Record with circular references

Reflection support for record classes

 

Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/Record-classes