Should every immutable class be final?

2019-04-09 15:17发布

问题:

I was designing a Card class to be used in a Blackjack game.

My design was to make a Card class with a getValue() that returns, for example, 11 for J, 12 for Q and 13 for K, and then extend it with a BlackjackCard class to override that method so that those cards return 10.

Then something hit me: objects of the Card class should be immutable. So I re-read Effective Java 2nd Edition to see what to do and I there I found that immutable classes need to be final, to avoid a subclass to break the immutability.

I also looked in Internet and everyone seems to agree in that point.

So should the Card class be final?

How can you break the immutability of this class, be extending it:

class Card {
  private final Rank rank;
  private final Suit suit;
  public Card(Rank rank, Suit suit) {
    this.rank = rank;
    this.suit = suit;
  }
  public Rank getRank() {
    return rank;
  }
  public Suit getSuit() {
    return suit;
  }
  public int getValue() {
    return rank.getValue();
  }
}

Thanks.

回答1:

A subclass cannot actually modify the values of private final properties in its parent, but it could behave as though it has, which is what Effective Java warns against:

Ensure that the class can't be extended. This prevents careless or malicious subclasses from compromising the immutable behavior of the class by behaving as if the object's state has changed.



回答2:

Answer is yes, Card needs to be final.

Combining the responses of K. Claszen and lwburk, see the following:

public class MyCard extends Card {
    private Rank myRank;
    private Suit mySuit;

    public MyCard(Rank rank, Suit suit) {
        this.myRank = rank;
        this.mySuit = suit;
    }

    @Override public Rank getRank() { return myRank; }

    public void setRank(Rank rank) { this.myRank = rank; }

    @Override public Suit getSuit() { return mySuit; }

    public void setSuit(Suit suit) { this.mySuit = suit; }

    @Override public int getValue() { return myRank.getValue(); }
}

This extension completely ignores the parent state and replaces it with its own, mutable state. Now classes that use Card in polymorphic contexts can't depend upon its being immutable.



回答3:

You can do this:

class MyCard extends Card {

  public MyCard(Rank rank, Suit suit) {
    super(rank, suit);
  }

  @Override
  public Rank getRank() {
    // return whatever Rank you want
    return null;
  }

  @Override
  public Suit getSuit() {
    // return whatever Suit you want
    return null;
  }

  @Override
  public int getValue() {
    // return whatever value you want
    return 4711;
  }

}

The extending class does even not have to declare the same constructor as the parent class. It can have a default constructor and does not care anything about the final members of the parent class. [That statement is wrong - see the comments].



回答4:

When they say immutable classes should be final they are referring to how you can ensure immutability not that because something is immutable it has to be final. Its a minor distinction. If you don't want your class ever extended it should be final.



回答5:

In general, this is a good recommendation. however, if you control all the code, then sometimes it is useful to be able to extend an immutable class (possibly to create another immutable class with additional info). as with most recommendations, you have to make intelligent choices as to when they make sense and when they may not.



回答6:

If arbitrary code can extend an immutable class, then arbitrary code can produce objects that behave a lot like the immutable class, but aren't immutable. If people who write such code would be unable to damage anything but themselves, then such a situation may be tolerable. If someone could use such a class to bypass security measures, then it should not be tolerated.

Note that it can at times be useful to have a class which is extensible, but which promises immutability on behalf of all derived classes. Such a class and its consumers would, of course, have to rely upon inheritors of the class not to do anything weird and wacky, but sometimes such an approach may still be better than any alternative. For example, one might have a class which is supposed to perform some action at some particular time, with the proviso that objects of the class may be arbitrarily aliased. Such a class would not be terribly useful if neither the class, nor any of its fields, nor any fields in any of its derived classes, etc. could use derived types, since the definition of a class would restrict what types of actions could be performed. The best approach would probably be to have the class not be declared final, but make very clear in the documentation that the behavior of mutable subclasses would not be consistent or predictable.



回答7:

You could have a Valuator that's tied to the game and remove the getValue from the class, this way Card can be final:

final class Card {
  private final Rank rank;
  private final Suit suit;
  public Card(Rank rank, Suit suit) {
    this.rank = rank;
    this.suit = suit;
  }
  public Rank getRank() {
    return rank;
  }
  public Suit getSuit() {
    return suit;
  }
}

And valuators work like this:

interface CardValuator {
  int getValue(Card card);
}

class StandardValuator implements CardValuator {
  @Override
  public int getValue(Card card) {
    return card.getRank().getValue();
  }
}

class BlackjackValuator implements CardValuator {
  @Override
  public int getValue(Card card) {
    ...
  }
}

Now, if you still want to keep your Card hierarchy, marking the Card's methods final will prevent a child class from overriding them.