Skip to content

Deep immutability and thread-safety assessment for Java objects

License

Notifications You must be signed in to change notification settings

mikenakis/Bathyscaphe

Repository files navigation

Bathyscaphe

Deep immutability and thread-safety assessment for Java objects


The Bathyscaphe logo, a line drawing of bathyscaphe Trieste
based on art found at bertrandpiccard.com

CI-Workflow status badge     Release-Workflow status badge IntelliJ IDEA badge          Number of files badge        Repository Size badge        Language badge               Code size badge              Repo size badge              Contributors badge           Commit activity badge        Last commit badge            License badge                GitHub pulse badge           GitHub dependencies badge    GitHub traffic badge         Issues badge

SPDX-FileCopyrightText: © 2022, Michael Belivanakis, a.k.a. MikeNakis, michael.gr
SPDX-License-Identifier: AGPL-3.0-only OR BATCL-1.0

Table of contents

Description

Bathyscaphe is an open-source java library that you can use to inspect objects at runtime and assert that they are immutable.

This document contains reference material about Bathyscaphe, assuming that you already understand what problem it solves, why it is a problem, why everyone has this problem, why it needs fixing, and why other tools fail to fix it. If not, please start by reading this article which introduces Bathyscaphe: michael.gr - Bathyscaphe

Highlights

  • It works, as opposed to static analysis tools, which do not work. For example, it will assess List.of( new StringBuilder() ) as mutable, but List.of( 1 ) as immutable.
  • It is lightning fast: the assert keyword ensures zero performance penalty on production.
  • It is very small: The Bathyscaphe JAR is about 100 kilobytes. The BathyscapheClaims JAR is a couple of kilobytes.
  • It has no dependencies outside of the core Java Runtime Environment.
  • It is easy to use: Just assert Bathyscaphe.objectMustBeImmutableAssertion( myObject ); and if it fails, it yields extensive diagnostics in human-readable form explaining precisely why this happened.
  • It is easy to integrate: Just add a maven-central dependency. (Coming soon.)
  • The annotations module, which will be used by most code out there, comes with a free-of-charge and very permissive license (MIT).
  • The actual assessment module comes with a choice of either a free-of-charge copyleft license (AGPL), or an inexpensive commercial license.

How it works

Bathyscaphe consists of two parts:

  1. Bathyscaphe
    • Repository: https://github.com/mikenakis/Bathyscaphe
    • Contains the immutability assessment library.
    • Few software systems are likely to invoke this library, and then only from a few places, where immutability needs to be ascertained. For example, a custom HashMap class might contain a call to bathyscaphe, to assert that keys added to it are immutable.
  2. BathyscapheClaims
    • Repository: https://github.com/mikenakis/BathyscapheClaims
    • Contains annotations, interfaces, etc. that you can add to your classes to guide assessment.
    • Most client code is expected to make use of only this module of Bathyscaphe.

When assessing whether an object is immutable or not, Bathyscaphe begins by looking at the class of the object, and issues one of the following assessments:

  1. Mutable (Conclusive)
  2. Immutable (Conclusive)
  3. Provisory (Inconclusive)

The first two are straightforward: if a class is conclusively assessed as mutable or immutable, then each instance of that class receives the same assessment, and we are done; however, if the class receives a provisory assessment, then Bathyscaphe proceeds to examine the contents of the object.

For example, if a class looks immutable in all aspects except that it declares a final field of interface type, Bathyscaphe will recursively assess the immutability of the object referenced by that field.

Note that this yields consistently accurate assessments in cases where static analysis tools fail, because they only examine classes, so when a class contains a field which might be mutable, (such as an interface, or any non-final type,) they have no option but to err on the side of caution and assess the containing class as mutable.

How to use

Asserting immutability

  • The objectMustBeImmutableAssertion() method

    The main thing you are likely to do with Bathyscaphe is this:

    assert Bathyscaphe.objectMustBeImmutableAssertion( myObject );  
    

    If myObject is immutable, this will succeed; otherwise, an ObjectMustBeImmutableException will be thrown.

    Note that the assertion statement itself will never fail, because objectMustBeImmutableAssertion() never returns false; It either returns true, or it throws ObjectMustBeImmutableException. The benefit of using the assert keyword is that the method will not be invoked unless assertions are enabled, which is how Bathyscaphe can boast zero performance overhead on production.

Adding pre-assessments

  • The addImmutablePreassessment() method

    Suppose that we have a class which is effectively immutable, meaning that it behaves immutably, but under the hood it is strictly speaking mutable, either because it is making use of lazy initialization, or simply because it contains an array. (Arrays in Java are mutable by nature.) If Bathyscaphe was to assess the immutability of this class, it would find it to be mutable; however, we know that the class behaves immutably, so we want to instruct Bathyscaphe to skip assessment and consider it as immutable. This is accomplished by adding what is known as a pre-assessment or assessment override, as follows:

    Bathyscaphe.addImmutablePreassessment( EffectivelyImmutableClass.class );
    

    One famous effectively immutable class is java.lang.String, which contains both an array of characters and a lazily initialized hash-code field. Bathyscaphe has a built-in pre-assessment for java.lang.String and a few other well-known effectively immutable classes of the JDK.

Pre-assessment should be used only on classes whose source code we have no control over, such as classes found in the JDK or in third-party libraries. For classes that we write and can thus modify, see next section.

Annotating fields

If you write an effectively immutable class, you should use the annotations found in the bathyscaphe-claims module to annotate each effectively immutable field of that class, thus allowing Bathyscaphe to assess the immutability of the remaining fields and issue an assessment for your class as a whole.

  • The @Invariable annotation

    Suppose that we have a non-final field in an otherwise immutable class. The presence of such a field would normally cause Bathyscaphe to assess the declaring class as mutable; however, we know that this particular field will behave as if it was final, so we would like to tell Bathyscaphe to consider it as final. This is accomplished as follows:

    @Invariable private int myLazilyInitializedHashCode;
    

    Thus, if the class meets all other requirements for immutability, Bathyscaphe will assess the class as immutable.

  • The @InvariableArray annotation

    Suppose that we have a field which is final, but it is of array type. Arrays are by definition mutable in Java, so the presence of this field would normally cause Bathyscaphe to assess the declaring class as mutable; however, we know that this particular field will behave as if it was immutable, so we would like to tell Bathyscaphe to refrain from assessing that field, and consider it as immutable. This is accomplished as follows:

    @InvariableArray private final byte[] mySha256Hash;
    

    Thus, if the class meets all other requirements for immutability, Bathyscaphe will assess the class as immutable.

Note that @Invariable and @InvariableArray can be combined.

Also note that it is illegal to use either of these annotations on non-private fields, because a class cannot give any promises about fields that may be mutated by other classes.

Also note that with these annotations we are only promising shallow immutability; Bathyscaphe will still perform all the checks necessary in order to ascertain deep immutability. So, for example, if the field was of type Foo instead of int, or if the array field was an array of Foo instead of an array of byte, then Bathyscaphe would recursively assess the immutability of Foo as part of assessing the immutability of the field.

Self-assessment

  • The ImmutabilitySelfAssessable interface

    Sometimes, the question whether an object is mutable or immutable can be so complicated, that only the object itself can answer the question for sure. (For an example, see freezable class in the glossary.) In order to accommodate such cases, the bathyscaphe-claims module defines the ImmutabilitySelfAssessable interface. If your class implements this interface, bathyscaphe will be invoking instances of your class, asking them whether they are immutable or not. Here is an example:

    public class MyFreezableClass implements ImmutabilitySelfAssessable
    {
        private int counter; //obviously mutable 
        private boolean frozen;
        public void mutate() { assert !frozen; mutable++; }
        public void freeze() { assert !frozen; frozen = true; }
        @Override public boolean isImmutable() { return frozen; }
    }
    

Obtaining diagnostics

  • The explain() method

    Suppose that there is a certain object which we intended to be immutable, but Bathyscaphe finds it to be mutable. We would like to know exactly why Bathyscaphe issues this assessment, so that we can locate the problem and fix it. Here is how:

    Object myObject = List.of( new StringBuilder() );
    try
    {
        assert Bathyscaphe.objectMustBeImmutableAssertion( myObject ); 
    }
    catch( ObjectMustBeImmutableException e ) 
    {
        Bathyscaphe.explain( e ).forEach( System.out::println );
    }
    

    The above code will emit to the standard output a detailed human-readable diagnostic message explaining exactly why the assessment was issued. The text will look something like this: (Note: the exact text is subject to change.)

    ■ instance of 'java.util.ImmutableCollections.List12' is mutable because index 0 contains mutable instance of 'java.lang.StringBuilder'. (MutableComponentMutableObjectAssessment)
    ├─■ type 'java.util.ImmutableCollections.List12' is provisory because it is preassessed by default as a composite class. (CompositeProvisoryTypeAssessment)
    └─■ instance of 'java.lang.StringBuilder' is mutable because it is of a mutable class. (MutableClassMutableObjectAssessment)
      └─■ class 'java.lang.StringBuilder' is mutable because it extends mutable class 'java.lang.AbstractStringBuilder'. (MutableSuperclassMutableTypeAssessment)
        └─■ class 'java.lang.AbstractStringBuilder' is mutable due to multiple reasons. (MultiReasonMutableTypeAssessment)
          ├─■ class 'java.lang.AbstractStringBuilder' is mutable because field 'value' is mutable. (MutableFieldMutableTypeAssessment)
          │ └─■ field 'value' is mutable because it is not final, and it has not been annotated with @Invariable. (VariableMutableFieldAssessment)
          ├─■ class 'java.lang.AbstractStringBuilder' is mutable because field 'coder' is mutable. (MutableFieldMutableTypeAssessment)
          │ └─■ field 'coder' is mutable because it is not final, and it has not been annotated with @Invariable. (VariableMutableFieldAssessment)
          └─■ class 'java.lang.AbstractStringBuilder' is mutable because field 'count' is mutable. (MutableFieldMutableTypeAssessment)
            └─■ field 'count' is mutable because it is not final, and it has not been annotated with @Invariable. (VariableMutableFieldAssessment)
    

Status (maturity) of the project

The Technology Readiness Level (TRL) so-to-speak of Bathyscaphe currently is 5: Technology validated in lab.

  • The library works, it appears to be problem-free, and it produces very good results; however, the only environment in which it is currently being put into use is the author's hobby projects, which is about as good as laboratory use.
  • There is at least one major (but optional) feature pending to be implemented: thread-safety assessment.
  • There is at least one major task pending to be done: publish on maven-central.
  • Since the project is still young, new releases are likely to contain breaking changes. (The major version number will always be incremented to indicate so.)

Dependencies

  • The bathyscaphe-test module necessarily depends on JUnit.

  • The bathyscaphe and bathyscaphe-claims modules do not depend on anything outside the Java Runtime Environment.

    • Let me repeat this: Bathyscaphe. Has. No. Dependencies. It depends on nothing. When you include the Bathyscaphe JARs in a project, you are including those JARs and nothing else.

Requirements

  • Module bathyscaphe-claims:
    • Requires java 8 to compile.
    • It could probably compile on older java versions, but I have not tried it.
    • It will almost certainly run on even older JREs, but I have not tried it.
  • Module bathyscape:
    • Requires java 17 to compile, and it actually makes use of java 17 features.
    • Is that too avant-garde? By the time Bathyscaphe becomes widely adopted, this version of Java will be old.
    • It might run on older JREs, but I have not tried it.
    • It will almost certainly run on older JREs if I specify an older <target> to the java-compiler-plugin, but I have not tried that either.
    • I have not tried these things because this kind of experimentation has very low priority at the moment.

Installation

  • In the near future, you will be able to include Bathyscaphe in any project by specifying it as a dependency which is obtained from maven central. For now, you can simply clone Bathyscaphe into your project, so that it builds along with your project.

Copyright & License

Bathyscaphe is copyright © 2022, Michael Belivanakis, a.k.a. MikeNakis, michael.gr

You may not use this library except in compliance with the license.

For information regarding licensing Bathyscaphe, please see LICENSE.md

Contacting the author

The author's e-mail address can be found on the sidebar of his blog: https://blog.michael.gr.

Glossary

Contributing

Code of Conduct

Sponsoring

  • If you would like to fund me to continue developing Bathyscaphe, or if you would like to see a DotNet version of Bathyscaphe sooner rather than later, you can bestow me with large sums of money; that always helps.

  • Sponsoring link: https://paypal.me/mikenakis

Coding style

  • When I write code as part of a team of developers, I use the teams' coding style, but when I write code for myself, I use My Very Own™ coding style.
  • As a result, Bathyscaphe uses My Very Own™ Coding Style.
  • More information: michael.gr - My Very Own™ Coding Style

Frequently Asked Questions (F.A.Q., FAQ)

Feedback

  • Please visit our discussions area to leave feedback, criticism, praise, feature requests, bug reports, haikus, whatever.

Issue Tracking

About

Deep immutability and thread-safety assessment for Java objects

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Sponsor this project

Packages 3

 
 
 

Languages