why java records aren’t quite immutable

A new type called a “record” was previewed in Java 14 and released in Java 16. One of the benefits is that it creates immutable objects. Kind of.

An immutable record

This is record is an immutable object. Since it is a record, it is automatically final and has no setters. All is good.

public record Book (String title, int numPages) { }

A mutable record

There there is this record. Do you see why it is mutable?

public record Book (String title, int numPages, List<String> chapters) { }

The problem is that records use shallow immutability. The caller can’t change the “chapters” object to a different reference. The caller can change the values in the “chapters” to their heart’s content. That means this object is still mutable.

Here’s an example showing that the code prints [1, 2, 3] and therefore changes the list of chapters.

List<String> chapters = new ArrayList<>();
chapters.add("1");

Book book = new Book("Breaking and entering", 289, chapters);

chapters.add("2");
book.chapters().add("3");
System.out.println(book.chapters());

Making the record actually be immutable

It’s pretty easy to make the Book record actually be immutable. In fact, it only requires three extra lines! Records have a compact constructor which takes care of the setting fields for you. However, you can choose to change that behavior. In this example, I rely on the default set of “title” and “numPages”. However, for “chapters”, I choose to make an immutable copy to prevent changing the list.

public record Book (String title, int numPages, List<String> chapters) {

    public Book {
        chapters = List.copyOf(chapters);
    }
}

Now the test program fails with an UnsupportedOperationException. Much better. A real immutable record.

Setting java aliases with zshell on Mac

When I’m working on different versions of Java, I set up a bunch of aliases so I can run commands quickly and compare differences. I recently upgraded my Mac and accepted the default shell as zshell instead of bash. (This change was made a long time ago but I didn’t upgrade for a long time because I didn’t want to incur the risk of being without my computer when everything was closed for the pandemic.)

Since I needed to move this from .bash_profile to .zshenv, I took the opportunity to shed a lot of them. I now have just Java 11 and Java 17 that I am using for comparisons. (LTS to LTS).

My .zshenv now contains this. (Yes, I know I should have patched Java 11, but alas.) I ran “source .zshenv” and am good to go.

Now I can run java11 JeanneTest.java and java17 JeanneTest.java to easily compare differences.

alias javac17=/Library/Java/JavaVirtualMachines/jdk-17-ea.jdk/Contents/Home/bin/javac
alias java17=/Library/Java/JavaVirtualMachines/jdk-17-ea.jdk/Contents/Home/bin/java
alias jar17=/Library/Java/JavaVirtualMachines/jdk-17-ea.jdk/Contents/Home/bin/jar
alias javac11=/Library/Java/JavaVirtualMachines/jdk-11.0.11.jdk/Contents/Home/bin/javac
alias java11=/Library/Java/JavaVirtualMachines/jdk-11.0.11.jdk/Contents/Home/bin/java
alias jar11=/Library/Java/JavaVirtualMachines/jdk-11.0.11.jdk/Contents/Home/bin/jar
alias jshell11=/Library/Java/JavaVirtualMachines/jdk-11.0..jdk/Contents/Home/bin/jshell
alias jdeps11=/Library/Java/JavaVirtualMachines/jdk-11.0.11.jdk/Contents/Home/bin/jdeps
alias jmod11=/Library/Java/JavaVirtualMachines/jdk-11.0.11.jdk/Contents/Home/bin/jmod

export JAVA_HOME="/Library/Java/JavaVirtualMachines/jdk-17-ea.jdk/Contents/Home"

Switching your Gradle builds from JCenter to Maven Central

JCenter is being decommissioned on May 1st, 2021. Since many Gradle builds use JCenter by default, this means your Gradle build file is likely to have jcenter() in it. This means you have a few months to switch to Maven Central. Don’t worry, it’s easy.

Note: If you work for a company, you are hopefully using an internal binary repository proxy. (ex: Nexus, Artifactory, etc)

What’s the difference between JCenter and Maven Central?

The main benefits of JCenter are:

  • Some artifacts are in JCenter and not Maven Central – their authors are working on moving them to Maven Central. This is unlikely to affect FRC teams, but might affect people using an artifact that is more specialized.
  • It’s easier to publish to JCenter – Sonatype has been working on making it easier for Maven Central. Some of that is intrinsic though because Sonatype does a lot of verification.
  • JCenter is faster – Remember that artifacts are cached on your machine. So once you’ve downloaded the artifacts, your build performance is the same.

How do I find the affected files in GitHub?

Searching on github for either of these does not do what you might expect

  • jcenter user:boyarsky filename:build.gradle- the code tab returns 0 results
  • jcenter org:stuypulse filename:build.gradle – the code tab returns 2 results

That’s because github search only looks in files that have been updated or returned in search results in the past year. Unfortunately, the last year has not been particularly representative of a normal year. And people edit/search the contents of build.gradle files way less frequently than other file types.

I recommend just searching for the filename. That returns all your build.gradle files so you can edit them. (And since you are probably consistent in your choice of binary repository, you can sample a few matches to see if you have to change.

  • user:boyarsky filename:build.gradle – the code tab returns 5 results
  • org:stuypulse filename:build.gradle – the code tab returns 24 results

Tip: You may also have a reference to jcenter in your settings.gradle so I recommend searching that as well.

How do I edit the file?

GitHub has good Rest APIs so you can script this if you have a lot. If you don’t have a ton, either of the following is viable. (I had 5 build.gradle files in my personal repo and 3 of them were already using Maven)

Option 1 – Use the browser

  1. Open each link from the code tab of the search
  2. Choose the default branch (ex: main/master) from the pull down – search often returns a specific commit
  3. Drill down to the build.gradle file if not already there.
  4. Click the edit/pencil icon in the top right (just above the code)
  5. Change jcenter() to mavenCentral()
  6. Enter a commit comment and save
  7. If you want to make sure your build still works, run it (ex: Travis)

Option 2 – Clone the repos

(Tested on Mac; I don’t have Git Bash on my home computer so don’t know if it works exactly the same. I have used find on Git Bash though so I think it does)

  1. Clone the affected repos (if you don’t already have them)
  2. Go to a parent directory of the github repos (ex: <userHome>/git
  3. Run a UNIX command to update the list files find . -name build.gradle
  4. Run a UNIX command to update the affected files: find . -name build.gradle -exec sed -i ” -e ‘s/jcenter/mavenCentral/g’ {} \;
  5. Commit/push the affected repos (listed in step 3)
  6. If you want to make sure your build still works, run it (ex: ./gradlew build)