user5078574
user5078574

Reputation:

Immutable Classes and Subclasses

I'm trying to learn about mutable/immutable classes and I came across this post

Part of the answer provided was:

If you want to enforce immutability, you cannot have subclasses. See for example java.lang.String, which is a final class for this reason: To prevent people from subclassing String to make it mutable.

Alright, I understand it, but, how would you handle this issue. Let's say you were given the task of creating 3 Employee classes, Accountant, ITDepartment, and QualityAssurance. Now, you could create an abstract class called Employee with common methods that would be shared by all (employee ID, name, salary etc..), however, your classes are no longer immutable.

Using Java, How then would you solve this problem ? Would you create the 3 classes, make them final, and don't implement an abstract method? (So, no subclassing whatsoever) or would you use an interface, and provide only the getters?

Upvotes: 9

Views: 8509

Answers (4)

HereAndBeyond
HereAndBeyond

Reputation: 1494

Java 17 and "Sealed Classes and Interfaces" feature

I think with Java 17 and "Sealed Classes and Interfaces" feature it became a bit more easier to follow immutability while using inheritance.

Now we can just restrict classes which may extend the parent class to ones that we are trusted to.

Let me show an example based on the suggested entities and the feature (pay attention on MutabilityBreaker which is not compiled due to the restriction of Employee class):

/**
 * Class that we would like to be immutable.
 * Pay attention on 'permits' section
 */
public sealed class Employee permits Accountant, ITWorker {

    private final String id;

    public Employee(String id) {
        this.id = id;
    }

}

/**
 * Class that we would like to inherit
 * from {@link Employee} and not break immutability
 */
public final class Accountant extends Employee {

    private final List<String> qualifications;

    // I'm very responsible. I don't want to break immutability, so I
    // carefully handle input collection
    public Accountant(String id, List<String> qualifications) {
        super(id);
        this.qualifications = List.copyOf(qualifications);
    }

}

/**
 * Another class that we would like to inherit
 * from {@link Employee} and not break immutability
 */
public final class ITWorker extends Employee {

    private final List<String> skills;

    // I'm very responsible too, I don't want to break
    // immutability too, so I carefully handle input collection too
    public ITWorker(String id, List<String> skills) {
        super(id);
        this.skills = List.copyOf(skills);
    }

}

/**
 * Class that was written to break immutability
 * <p>
 * COMPILE ERROR: 'MutabilityBreaker' is not allowed in the sealed hierarchy
 */
public class MutabilityBreaker extends Employee {

    private final List<String> mutableCollection;

    // I'm not responsible, I don't want to follow immutability
    public MutabilityBreaker(String id, List<String> mutableCollection) {
        super(id);
        this.mutableCollection = mutableCollection;
    }

}

Upvotes: 0

Mike Samuel
Mike Samuel

Reputation: 120526

If you want to enforce immutability, you cannot have subclasses.

This is almost true, but not entirely. To restate it:

If you want to enforce immutability, you must ensure that all sub-classes are immutable.

The problem with allowing subclassing is that normally anyone who can author a class can subclass any public non-final class.

But all subclasses must invoke one of their super-class's constructors. Package-private constructors can only be invoked by subclasses in the same package.

If you seal packages so that you control which classes are in your package, you can constrain subclassing. First define a class you want to subclass:

public abstract class ImmutableBaseClass {
  ImmutableBaseClass(...) {
    ...
  }
}

Since all sub-classes have to have access to the super-constructor, you can ensure all the sub-classes in the package you define follow immutable discipline.

public final class ImmutableConcreteClass extends ImmutableBaseClass {
  public ImmutableConcreteClass(...) {
    super(...);
  }
}

To apply this to your example,

public abstract class Employee {
  private final Id id;
  private final Name name;

  // Package private constructor in sub-classable class.
  Employee(Id id, Name name, ...) {
    // Defensively copy as necessary.
  }
}

public final class Accountant extends Employee {
  // Public constructos allowed in final sub-classes.
  public Accountant(Id id, Name name, ...) {
    super(id, name, ...);  // Call to super works from same package.
  }
}

public final class ITWorker extends Employee {
  // Ditto.
  public ITWorker(Id id, Name name, ...) {
    super(id, name, ...);
  }
}

Upvotes: 7

ZhongYu
ZhongYu

Reputation: 19682

java.lang.String is special, very special - it's a basic type used everywhere. In particular, java security framework heavily depends on Strings, therefore it is critical that everybody sees the same content of a String, i.e. a String must be immutable (even under unsafe publication!) (Unfortunately a lot of people blindly apply those strict requirements of String to their own classes, often unjustified)

Even so, it's not really a big deal if String can be subclassed, as long as all methods in String are final, so that a subclass cannot mess with what a String is supposed to be like. If I'm accepting a String, and you give me a subclass of String, I don't care what you do in the subclass; as long as the content and the behavior of the String superclass is not tempered with.

Of course, being such a basic type, it's wise to mark String as final to avoid all confusions.


In your use case, you can just have an abstract Employee, and make sure all subclasses are implemented as immutable. Any Employee object, at runtime, must belong to a concrete subclass, which is immutable.

If, you cannot control who subclasses Employee, and you suspect that they are either morons that don't know what they are doing, or villains that intend to cause troubles, you can at least protect the part in Employee so that subclasses cannot mess it up. For example, the name of an Employee is immutable no matter what subclass does -

final String name;

protected Employee(String name) { this.name = name; }

public final String getName(){ return name; }  // final method!

However, such defensive design is often unjustified in most applications by most programmers. We don't have to be so paranoid. 99% of coding are cooperative. No big deal if someone really need to override something, let them. 99% of us are not writing some core APIs like String or Collection. A lot of Bloch's advices, unfortunately, are based on that kind of use cases. The advices are not really helpful to most programmers, especially new ones, churning out in-house apps.

Upvotes: 2

hugh
hugh

Reputation: 2280

It's worth thinking why immutability is desirable - usually it's because you have data which you need to assume is not going to change. One approach to do this is to make a final Employee class with a non-final member containing the details specific to the subclasses:

public final class Employee {
    private final long employeeId;
    private final String firstName;
    private final String lastName;
    private final DepartmentalDetails details;

    public Employee(long employeeId, String firstName, String lastName,
            DepartmentalDetails details) {
        super();
        this.employeeId = employeeId;
        this.firstName = firstName;
        this.lastName = lastName;
        this.details = details;
    }
}

abstract class DepartmentalDetails {
}

final class AccountantDetails extends DepartmentalDetails {
    // Things specific to accountants
}

final class ITDetails extends DepartmentalDetails{
    // Things specific to IT
}

final class QualityAssuranceDetails extends DepartmentalDetails{
    // Things specific to QA
}

This isn't technically immutable (since an implementer could write a mutable implementation of DepartmentalDetails) but it does encapsulate the mutability, so it gives the same benefits while allowing some extensibility. (This is related to this is the concept of composition vs inheritance, although I don't believe this pattern is how that is normally used.)

One other possibility I think is worth considering - you could make the three subclasses as you suggest, make them all final, and stick a big comment on the abstract class saying all implementations should be immutable. It's not bulletproof, but on a small development project the complexity saving is likely to be worth the tiny risk.

Upvotes: 2

Related Questions