15

Before concretizing my question, let me provide some background: My main programming languages are C++ and Java. When working with C++ I find it important to apply const correctness, that is declaring functions like this:

A::DoSomething( const B& arg ); // guarantees that arg is not modified
A::DoSomething() const; // guarantees that the object of type A is not modified
A::DoSomething( const B& arg ) const; // both of the above

In fact, I often wish that const would be the default and objects that are modified have to be marked somehow.

My main reasons for using constare:

  • Communication with other developers: It makes code more expressive.
  • Communication with the compiler: It helps to find issues at compile time and sometimes makes additional optimizations possible.

It is well-known that Java has no const keyword (you can't do the above with final) and this fact has been discussed here before, see for example here: Equivalent of const(C++) in Java.

The Java alternative that is usually proposed is to make your classes immutable. While this is not a full replacement of const because it works per class and not per context in which a class is used, it would work fine for me in most cases.

But there is one big issue with immutable classes: Immutability is not obvious. To find out if a class really is immutable, as far as I know, you basically have to check the full source code. Any method could have a backdoor through which the object could be modified.

So is there an easier way to check immutability? Or is there any best practice to somehow mark a class as being immutable?

Note: I know that both languages provide 'evil tricks' to bypass constness or immutability: C++ has const_cast and Java has reflection. But for the context of my question, let's assume that these are not used.

Community
  • 1
  • 1
Frank Puffer
  • 8,135
  • 2
  • 20
  • 45
  • 3
    Fun fact: Java does have [a const keyword](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html), but it is not used ;) – Jorn Vernee May 07 '16 at 11:20
  • One big hint to a class being immutable to me (or having that intent), is if it were marked ```final```. – Jorn Vernee May 07 '16 at 11:35
  • 1
    Very much related: [this Q/A which goes into detection of immutability for runtime code](http://stackoverflow.com/questions/203475/how-do-i-identify-immutable-objects-in-java) (if it wasn't I would have closed this Q down as a dupe). – Maarten Bodewes Feb 14 '17 at 15:04
  • There are JEP proposals of things like value types. – MC Emperor Jan 11 '23 at 17:18

3 Answers3

19

Java has no first-class immutability support, so you have no reliable way to detect if class is immutable.

Java Concurrency In Practice recommends (see Appendix A as reference) to use class-level @Immutable annotation from javax.annotation.concurrent and this is the most convenient, common and standard way to indicate immutability; custom javadoc is fine too. Note that it's only declaration, not actual constraint.

Design of the class is also a good indicator:

  • Have only final fields (but in rare cases it's possible to have some non-final fields and still be immutable, for example see String#hashCode)
  • It's properly constructed (this reference doesn't leak from constructor)
  • Object state cannot be modified (so class should not have setters and mutator methods)
  • Don't store external (passed to constructor) references to mutable objects (e.g. create defensive copy of passed collection argument)

Full list of immutable class design properties can be found in Oracle tutorials.

So, to check if class is immutable you first look at class level annotations and javadoc and only then at implementation itself.

To provide additional sanity-check (if you think that annotated as immutable class can be mutable by mistake), there is Mutability Detector plugin for FindBugs (static analysis tool), which effectively doing same thing listed above: checks class has @Immutable annotation and verifies (via reflection) that all immutability rules are satisfied (with some additional things like immutable collection support from Guava etc.). Mutability detector also can be used as library without FindBugs, which allows you to write tests like that:

@Test
public void testImmutable() {
    assertImmutable(MyClass.class);
}
qwwdfsad
  • 3,077
  • 17
  • 25
  • 1
    Im getting compiler error with @Immutable annotation in my code - `cannot find symbol @Immutable symbol: class Immutable 1 error` – roottraveller Jul 04 '17 at 10:33
  • @roottraveller You can find the `@Immutable` annotation in JSR305: ` com.google.code.findbugs jsr305 3.0.2 ` – Julien Kronegg Jun 15 '18 at 21:14
  • 1
    Regarding `final` fields: this only give the guaranty that the _reference_ will not be changed. However, the _value_ of a mutable field (e.g. `java.util.List`) can still be changed. – Julien Kronegg Jun 15 '18 at 21:18
  • Java now has [`record`](https://openjdk.java.net/jeps/384). Alas, it does not (yet?) declare `@Immutable`. – Aleksandr Dubinsky Feb 23 '21 at 09:15
  • @AleksandrDubinsky Java's record is shallow-immutable. I address that fully in the Answer I posted: https://stackoverflow.com/a/75043881/501113 – chaotic3quilibrium Jan 10 '23 at 15:11
3

The code in this answer is a simplified reduction of my full open-source version, which I have published on Github (please look at the unmerged genesis branch).

Please also checkout a very nicely packaged similar solution available in this StackOverflow Answer.


This answer assumes Java 16 or later

For prior versions of Java, a refactoring is required to remove dependencies to features introduced into Java and the Java platform up to and including Java 16.


Detecting Immutability in Java

Since the Java language and platform provide nothing explicit with which to reliably detect immutability, it must be custom written.

Before rolling a custom Java immutability detector, there are several useful distinctions to understand.

Distinction 1:

There are types of immutability:

  1. compile-time-vs-runtime
    • compile-time immutability, while desirable, isn't (yet?) available with Java
    • providing run-time immutability detection has a significant impact on enhancing code comprehension, reducing reasoning efforts, and enhancing security
  2. shallow-vs-deep
    • shallow - a Java record
    • middle - a Java enum
    • deep - java.lang.String
  3. total-vs-effective
    • class-level versus instance-level immutability
    • deals with the runtime timing of when the instance becomes irreversibly immutable
  4. verified-vs-trusted
    • deals with reversing an instance's immutability via Java reflection (innocent or malicious)

Detecting immutability in Java isn't an either/or. It is predicated on a spectrum of anticipated trust. To the degree one has control of one's environment correlates strongly to the degree the trust of any detected immutability. This trust continuum has been discretized into the Java enum, ImmutabilityType, below. The enum enables client code to decide upon what level of trust it would prefer to rely.

While it would be highly desirable to a large number of software engineers, Java does not provide compile-time enforced immutability detection. As an analog to Rust's beautiful memory management solution to C/C++'s undesirable memory utilization potholes, it would be a huge leap forward if/when Java acquired compile-time enforced immutability detection. However, this doesn't currently appear on any known Java roadmaps (as of 2023/Jan).

Distinction 2:

The use of the word "calcification" will describe the process of an instance moving from a mutable (writable or fluid) to an irreversibly immutable (read-only or calcified) state.

Every instantiation of a class involves a period of time where the instance is mutable. IOW, the default state of an instance, upon creation, is mutability. This means it is the responsibility of the class designer to decide if, and then when, the class becomes irreversibly immutable. If desiring immutability, this actually simplifies to deciding whether the instance remains mutable past the end of the execution of its constructor.

Distinction 3:

Because immutability is about permanently calcifying the state of an instance of a class, it is important to distinguish at run-time BY WHEN the state has become irreversibly immutable.

The archetype, or ideal, for implementing immutability is to design the state to be calcified by the end of the execution of the class's constructor for a given instance. However, there are contexts where it is required to delay the calcification until sometime after the constructor completes execution. Whether this is due to poor design choices is outside the scope of this response, as the use cases exist and must be addressed.

Distinction 4:

Due to the implications of the last distinction, detecting immutability must be done at each of the class and the instance levels. The description of class level immutability applies to all instance of that class. The description of instance level immutability applies to some subset of instances from their parent class.

As such, defining two different functions is needed for detecting immutability. One which takes a Class<?> as a parameter and returns the class level of immutability. And another which takes an Object instance as a parameter returns the instance-level of immutability at that point during runtime.


Working on the problem space

public enum ImmutabilityType {
  UNDEFINED,    //unable to assert any trustable degree of deep immutability
  PROPERTY_IRREVERSIBLY_IMMUTABLE, //trusting a post-instantiation window of mutability requiring an ImmutableEffective.isIrreversiblyImmutable() style of check
  BEFORE_END_OF_CLASS_CONSTRUCTOR,  //trusting the implementor to properly enforce the Immutable contract, see Immutable interface Javadoc
  JAVA_CONSTANT //trusting the Java compiler and platform
}

This Java enum describes the kinds of immutability detected.

The JavaDoc for the "immutability contract" referenced by the comment following the ImmutabilityType.BEFORE_END_OF_CLASS_CONSTRUCTOR enum instance is located here.

Two Target Functions:

Using the ImmutabilityType enum as their return type, the two target functions are defined below. The first function, classIs(), detects class-level immutability. And the second function, instanceIs(), detects instance-level immutability.

public static ImmutabilityType classIs(Class<?> class___) { ... }

public static ImmutabilityType instanceIs(Object o) { ... }

To implement the two target functions, the infrastructure supporting them must be defined.

Subset of well-known Java Platform immutable classes:

private static final Set<Class<?>> SUBSET_OF_KNOWN_JAVA_PLATFORM_IMMUTABLE_CLASSES =
  Set.of(
    //Meta-classes
    Class.class, MethodType.class,
    //primitive class wrappers
    Boolean.class, Byte.class, Short.class, Character.class, Integer.class, Long.class, Float.class, Double.class,
    //classes
    String.class, BigInteger.class, BigDecimal.class
  );

This is just a small subset of the many immutable classes defined in the Java platform. Many more can and should be added, like java.time.ZonedDateTime, etc.

Java Platform Types:

public static Optional<ImmutabilityType> classIsJavaConstant(Class<?> class___) {
  return SUBSET_OF_KNOWN_JAVA_PLATFORM_IMMUTABLE_CLASSES.contains(class___)
            || Constable.class.isAssignableFrom(class___)    //Covers: MethodHandle
            || ConstantDesc.class.isAssignableFrom(class___) //Covers: ClassDesc, MethodTypeDesc, MethodHandleDesc, and DynamicConstantDesc
            || class___.isEnum()
          ? Optional.of(ImmutabilityType.JAVA_CONSTANT)
          : Optional.empty();
}

It is somewhat risky to include the class___.isEnum() check. A Java enum is technically too open to assume by-default-immutabililty, even though the vast majority of its actual in the field usages are for the default enum which is immutable at the level of the Java platform.

While this particular implementation example doesn't offer the option, a design enhancement would enable some form of registering of an enum as mutable (ImmutabilityType.UNDEFINED), overriding the return value from this function.

Java Record:

private static ImmutabilityType internalClassIsRecord(Class<Record> classRecord) {
  var resolvedImmutabilityType = ImmutabilityType.JAVA_CONSTANT;
  var recordComponents = Arrays.stream(classRecord.getRecordComponents()).iterator();
  while ((resolvedImmutabilityType != ImmutabilityType.UNDEFINED) && recordComponents.hasNext()) {
    var recordComponent = recordComponents.next();
    if (!recordComponent.getType().isPrimitive()) { //within a Record, a primitive is always immutable
      var immutabilityType = classIs(recordComponent.getType());
      if (immutabilityType.ordinal() < resolvedImmutabilityType.ordinal())
        resolvedImmutabilityType = immutabilityType;
    }
  }

  return resolvedImmutabilityType;
}

public static Optional<ImmutabilityType> classIsRecord(Class<?> class___) {
  return class___.isRecord()
          ? Optional.of(internalClassIsRecord((Class<Record>) class___))
          : Optional.empty();
}

Java records only provide shallow immutability. To detect deep immutability, the record must be recursively validated. The recursion occurs with the call to classIs(recordComponent.getType()) (defined further down) in the function internalClassIsRecord.

Via JCIP Annotation or Marker Interfaces:

public interface Immutable {}
public interface ImmutableEffective {
  boolean isIrreversiblyImmutable();
}

public static Optional<ImmutabilityType> classIsJcipAnnotationOrImmutableInterface(Class<?> class___) {
  return class___.isAnnotationPresent(net.jcip.annotations.Immutable.class) || Immutable.class.isAssignableFrom(class___)
          ? Optional.of(ImmutabilityType.BEFORE_END_OF_CLASS_CONSTRUCTOR)
          : Optional.empty();
}

public static Optional<ImmutabilityType> classIsImmutableEffective(Class<?> class___) {
  return ImmutableEffective.class.isAssignableFrom(class___)
          ? Optional.of(ImmutabilityType.PROPERTY_IRREVERSIBLY_IMMUTABLE)
          : Optional.empty();
}

Using everything defined above, it's time to implement the two target functions.

Class-oriented classIs function:

public static ImmutabilityType classIs(Class<?> class___) {
  return class___.isArray()
      ? ImmutabilityType.UNDEFINED //java.lang.Array is always mutable
      : classIsJavaConstant(class___)
          .or(() -> classIsRecord(class___))
          .or(() -> classIsJcipAnnotationOrImmutableInterface(class___))
          .or(() -> classIsImmutableEffective(class___))
          .orElse(ImmutabilityType.UNDEFINED);
}

Instance-oriented instanceIs function:

private static boolean internalIsRecordHasAtLeastOneIrreversiblyImmutableStillUnset(Record record) throws InvocationTargetException, IllegalAccessException {
  boolean result = false; //when set to true, it has hit an isIrreversiblyImmutable() that remains false

  var classRecord = record.getClass();
  var recordComponents = Arrays.stream(classRecord.getRecordComponents()).iterator();
  while (!result && recordComponents.hasNext()) {
    var recordComponent = recordComponents.next();
    if (ImmutableEffective.class.isAssignableFrom(recordComponent.getType())) {
      var object = recordComponent.getAccessor().invoke(record);
      result = !((ImmutableEffective)object).isIrreversiblyImmutable();
    } else if (recordComponent.getType().isRecord()) {
      result = internalIsRecordHasAtLeastOneIrreversiblyImmutableStillUnset((Record)recordComponent.getAccessor().invoke(record));
    }
  }

  return result;
}

public static ImmutabilityType instanceIs(Object o) {
  var result = classIs(o.getClass());

  if (result == ImmutabilityType.PROPERTY_IRREVERSIBLY_IMMUTABLE)
    if (o instanceof ImmutableEffective immutableEffective) {
      if (!immutableEffective.isIrreversiblyImmutable())
        result = ImmutabilityType.UNDEFINED;
    } else
    if (o instanceof Record record) {
      boolean hasAtLeastOneIrreversiblyImmutableStillUnset;
      try {
        hasAtLeastOneIrreversiblyImmutableStillUnset = internalIsRecordHasAtLeastOneIrreversiblyImmutableStillUnset(record);
        if (hasAtLeastOneIrreversiblyImmutableStillUnset)
          result = ImmutabilityType.UNDEFINED;
      } catch (InvocationTargetException | IllegalAccessException ignored) {
        result = ImmutabilityType.UNDEFINED; //unable to resolve
      }
    } else
      result = ImmutabilityType.UNDEFINED; //unable to resolve

  return result;
}

Example usages:

//Setup definitions
static class ImmutableEffectiveImpl implements ImmutableEffective {
  private boolean irreversiblyImmutable;

  public ImmutableEffectiveImpl(boolean irreversiblyImmutable) {
    this.irreversiblyImmutable = irreversiblyImmutable;
  }

  public boolean isIrreversiblyImmutable() {
    return irreversiblyImmutable;
  }

  public void setAsIrreversiblyImmutable() {
    irreversiblyImmutable = true;
  }
}

@net.jcip.annotations.Immutable
public static class JcipAnnotationImmutable {}

//Code to produce output
Stream.of(
  new Object(),
  new ImmutableEffectiveImpl(false),
  new ImmutableEffectiveImpl(true),
  new JcipAnnotationImmutable(),
  new Immutable() {},
  "Jim"
).forEachOrdered(
  o -> {
    var oC = o.getClass();
    var className = oC.toString().substring(6);
    System.out.println(className + " - " + o);
    System.out.println("  isClass(): " + classIs(oC));
    System.out.println("  isInstance(): " + instanceIs(o));
  }
);

And below is what the above code produces.

Please specifically notice the contrast between line 6 (isInstance(): UNDEFINED) and line 9 (isInstance(): PROPERTY_IRREVERSIBLY_IMMUTABLE).

This is where the difference in the returned ImmutabilityTypes is exhibited between the class-level and instance-level function calls.

java.lang.Object - java.lang.Object@6e8cf4c6
  isClass(): UNDEFINED
  isInstance(): UNDEFINED
immutability_so.Main$ImmutableEffectiveImpl - immutability_so.Main$ImmutableEffectiveImpl@66a29884
  isClass(): PROPERTY_IRREVERSIBLY_IMMUTABLE
  isInstance(): UNDEFINED
immutability_so.Main$ImmutableEffectiveImpl - immutability_so.Main$ImmutableEffectiveImpl@4769b07b
  isClass(): PROPERTY_IRREVERSIBLY_IMMUTABLE
  isInstance(): PROPERTY_IRREVERSIBLY_IMMUTABLE
immutability_so.Main$JcipAnnotationImmutable - immutability_so.Main$JcipAnnotationImmutable@cc34f4d
  isClass(): BEFORE_END_OF_CLASS_CONSTRUCTOR
  isInstance(): BEFORE_END_OF_CLASS_CONSTRUCTOR
immutability_so.Main$1 - immutability_so.Main$1@277050dc
  isClass(): BEFORE_END_OF_CLASS_CONSTRUCTOR
  isInstance(): BEFORE_END_OF_CLASS_CONSTRUCTOR
java.lang.String - Jim
  isClass(): JAVA_CONSTANT
  isInstance(): JAVA_CONSTANT

The code in this answer is a simplified reduction of my full open-source version, which I have published on Github (please look at the unmerged genesis branch).

Please also checkout a very nicely packaged similar solution available in this StackOverflow Answer.

chaotic3quilibrium
  • 5,661
  • 8
  • 53
  • 86
  • The github project includes a .gitignore, a license, and a readme file, but not a single actual java source code file. – Mike Nakis May 09 '23 at 13:22
  • 1
    @MikeNakis I just haven't yet merged the "genesis" branch into "main". I hope to get to that in the next week or so. IOW, if you go back to that link, and look at the "genesis" branch, you can see all of the code. Here's a direct link to it: https://github.com/qalocate/immutability-detector/tree/genesis – chaotic3quilibrium May 10 '23 at 16:20
  • 1
    OK I see. I saw that branch but did not notice the `src` directory in it. – Mike Nakis May 10 '23 at 18:53
-3

According to Java documentation:

  • Don't provide "setter" methods — methods that modify fields or objects referred to by fields.
  • Make all fields final and private.
  • Don't allow subclasses to override methods. The simplest way to do this is to declare the class as final. A more sophisticated approach is to make the constructor private and construct instances in factory methods.
  • If the instance fields include references to mutable objects, don't allow those objects to be changed:
    • Don't provide methods that modify the mutable objects.
    • Don't share references to the mutable objects. Never store references to external, mutable objects passed to the constructor; if necessary, create copies, and store references to the copies. Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods.

The easiest way to enforce immutability is via @Value annotation in Lombok library.

If you use IntelliJ, you can check class byte code for above-mentioned bullets enter image description here

max3d
  • 1,437
  • 15
  • 16
  • 2
    This is not a good example because the Lombok `@Value` only helps you to have an immutable class, but the Lombok implementation is incomplete (at least at the time of writing). In your case, the `java.util.Date` field is still mutable even if `private final`: the Lombok generated constructor does not clone the Date, so it can be modified externally, e.g. `Date d = new Date(); FinalClass fc = new FinalClass(1,"",d); d.setTime(0);`. See also in https://projectlombok.org/features/Value : the `String[]` argument is not cloned in the constructor, so its content can be modified afterwards. – Julien Kronegg Jun 15 '18 at 21:31
  • 1
    Setting a field as `final` is not a guarantee to make it immutable. This is only the _reference_ that is immutable, not its _value_ (see your `java.util.Date` field). The reverse is also true: your field may be immutable even if not `final` (see the cached value of the `hashCode()` method from the immutable class `String`). – Julien Kronegg Jun 15 '18 at 21:39
  • 1
    This does not answer to the OP question (_how to find out if a class is immutable?_). – Julien Kronegg Jun 15 '18 at 21:49