The Bathyscaphe logo, a line drawing of bathyscaphe Trieste
based on art found at bertrandpiccard.com
SPDX-FileCopyrightText: © 2022, Michael Belivanakis, a.k.a. MikeNakis, michael.gr
SPDX-License-Identifier: AGPL-3.0-only OR BATCL-1.0
- Description
- Highlights
- How it works
- How to use
- Status (Maturity) of the project
- Dependencies
- Requirements
- Installation
- Copyright & License
- Contacting the author
- Glossary
- Contributing
- Code of Conduct
- Sponsoring
- Coding style
- Frequently Asked Questions
- Feedback
- Issue Tracking
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, butList.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. TheBathyscapheClaims
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:
- 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.
- 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:
- Mutable (Conclusive)
- Immutable (Conclusive)
- 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()
methodThe main thing you are likely to do with Bathyscaphe is this:
assert Bathyscaphe.objectMustBeImmutableAssertion( myObject );
If
myObject
is immutable, this will succeed; otherwise, anObjectMustBeImmutableException
will be thrown.Note that the assertion statement itself will never fail, because
objectMustBeImmutableAssertion()
never returnsfalse
; It either returnstrue
, or it throwsObjectMustBeImmutableException
. The benefit of using theassert
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()
methodSuppose 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 forjava.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
annotationSuppose 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
annotationSuppose 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
interfaceSometimes, 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()
methodSuppose 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
andbathyscaphe-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
- See GLOSSARY.md
Contributing
- See CONTRIBUTING.md
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)
- See FAQ.md
Feedback
- Please visit our discussions area to leave feedback, criticism, praise, feature requests, bug reports, haikus, whatever.
Issue Tracking
- See ISSUES.md