Java Records

Java 14 introduces a new language feature, a Record.

Records provide a compact syntax for declaring classes which are transparent holders for shallowly immutable data.

The semantic goal of Records is to enable modeling data as data. Records are clear and concise declarations of shallowly-immutable, well-behaved nominal data aggregates.

A Record has a name and a state description. The state description declares the components of the record.
The classic example is a 2D point:

record Point(int x, int y) {}

A Record is a new kind of type declaration. It’s a restricted form of class similar to an enum. The API of a Record is defined by its representation unlike classes which have the ability to decouple API from representation. Therefore, a Record acquires the following members automatically:

  • A private final field for each component of the state description
  • A public read accessor method for each component of the state description
  • A public constructor, whose signature is the same as the state description
  • Implementations of equals() and hashCode()
  • An implementation of toString()

However, a Record has the following restrictions:

  • Cannot extend any class
  • Cannot be extended
  • Cannot have non-final instance fields

Records support all other features of normal classes. They can implement interfaces, have instance methods, constructors, all kinds of static members, annotations and everything else.

A more sophisticated example of how Records can help with modeling data would be the idea of describing a 3D object with a transform which has information about rotation, translation and scale.

record Rotator(int pitch, int roll, int yaw) {}
record Vector(int x, int y, int z) {}
record Transform(Rotator rotation, Vector translation, Vector scale) {}

No other code is needed to start using our transform data model:

Vector scale = new Vector(1, 1, 1);
Vector translation = new Vector(100, 0, 0);
Rotator rotation = new Rotator(0, 0, 90);
Transform t1 = new Transform(rotation, translation, scale);

System.out.println(t1);
System.out.println(t1.translation().x());
System.out.println(t1.rotation().yaw());

The example above produces the following output:

Transform[rotation=Rotator[pitch=0, roll=0, yaw=90], translation=Vector[x=100, y=0, z=0], scale=Vector[x=1, y=1, z=1]]
100
90

How much code would be needed to achieve the same with normal classes is left to the imagination of the reader.

Of course, Records can be used for much more. Some of the obvious use-cases include:

  • Multiple return values or tuples
  • Data transfer objects
  • Compound map keys
  • Messages
  • Value wrappers

LVM2: Extend file system

LVM2 refers to the userspace toolset that provide logical volume management facilities on Linux. It is reasonably backwards-compatible with the original LVM toolset.

Resizing volumes with the LVM2 toolset is easy. First you have to adjust your physical partition table using fdisk. Let’s say you have a 60GB physical disk and a 50GB physical partition on it.

You want to resize the partition to take the whole disk. To do that you delete the partition and create it again with the new geometry.

The highlighted rows show the commands issued to fdisk. After the partition table is rewritten reboot the system.

The new partition table should now look like this:

Now that the new partition is setup you can assign the new space to a PV (physical volume). You can check the physical volumes with the pvs command.

In this case you have only one PV (physical volume) of size 50GB. Since you know your partition is 60GB you can very easily resize the PV to take all space with the pvresize command.

As you can see now the PV is of size 60GB and there is 10GB of free space. You can now use the free space to resize an LV (logical volume) using the lvextend command.

The -r option tells lvextend to resize the underlying filesystem (e.g. ext4 or btrfs) along with the LV. The -l option tells lvextend to set the LV size in units of logical extents. In this case the argument to -l is +100%FREE which means that 100% of the free space will be used. In other words all free space will be added to the system/root LV. After lvextend returns the file system should be resized and operational without system reboot.

Git cheats

git --everything-is-local

Git is a great and very powerful tool. There are probably over a hundred command line options that allow you to take advantage of its features. Here are few interesting things you can do easily.


Restore a deleted file

To restore a file that has been deleted several commits ago you have to find the commit that deleted the file. Then checkout the version of the file that was before the delete commit. Suppose you have the following in your repository.

As you can see originally there was a file named main.c but at some point it got deleted by: 2626b9b. To restore the content of main.c you can find the last commit that changed it.

And checkout the previous version (denoted by the caret character: ^).

Of course you checkout the previous version because the last commit that changed the file is the one that deleted it.


Rename files and directories

Git is very smart when it comes to renaming files. You don’t actually have to tell git that you’re renaming a file, it will figure it out on its own. Here’s an example, first we rename the file with whatever tool we like.

At this point git knows that you deleted a file that was in the repository (Main.java) and created a new file that is still untracked (Launcher.java).

Then you can just add these changes to the staging area and git will know that you renamed the file because the content is the same.

Actually it will detect that you renamed a file even if you made changes to the content. In this case I also changed the name of the class to keep it a valid Java file.

As you can see git calculates a similarity index of 68% and correctly concludes this is a rename operation.


Revert commits

Sometimes you’ll need to revert the changes introduced by one or more commits. There are generally two approaches to do that. One is to change the existing git history and the other to create new commits. Changing history can lead to issues if the commits are shared (when other people have them in their repositories). So probably the easiest and safest way is to use the revert command to create new commits with the opposite changes. Lets look at the following history.

Commit 329d44c adds a main method.

You can create a new commit that has exactly the opposite changes.

Now the history contains a new commit that reverted the changes 329d44c introduced.

Arbitrary Java object serialization to YAML

There are several good options for java object serialization and deserialization. One of these is to use the YAML data serialization standard.

YAML (rhymes with “camel“) is a human-friendly, cross language, Unicode based data serialization language designed around the common native data types of agile programming languages. It is broadly useful for programming needs ranging from configuration files to Internet messaging to object persistence to data auditing.

There are also several Java libraries that implement the YAML standard (as listed on yaml.org).

  • JvYaml
  • SnakeYAML
  • YamlBeans
  • JYaml

For the purpose of this article we’ll be using SnakeYAML as a solid modern implementation of the YAML 1.1 standard.

Consider the following simple example:

Source: Weapon.java

This Java bean can be serialized and deserialized with just the following code:

Source: WeaponTest.java

Simple Java beans are automatically serialized and deserialized, the data looks like this:

Of course in reality things are not always that simple. Your domain model may contain types that are not Java beans like java.net.URL, java.util.UUID, java.net.InetAddress and others. In this case the serialization process will ignore the non-bean objects and deserialization will fail altogether.

Fortunately SnakeYAML’s represent and construct functionality can easily be extended. Consider the following more complex example:

Source: Cyborg.java

Now we have non-bean types and java.util.Collection. To handle serialization and deserialization properly we need to use custom representer and constructor.

The representer takes the non-bean objects and transforms them into custom-tagged scalars. Also RepresentInetAddress is registered for both Inet4Address and Inet6Address to handle both protocols.

Source: ApplicationRepresenter.java

The constructor does exactly the opposite of the representer does.

Source: ApplicationConstructor.java

Our custom representer and constructor are passed to the constructor of the YAML processor. Putting everything together looks like this:

Source: CyborgTest.java

Now the type hierarchy is represented in YAML like this:

As you can see the non-bean type values are tagged with custom tags: !inet and !uuid. That’s how the YAML document is later properly deserialized.