Должен ли каждый неизменяемый класс быть окончательным?

Я разрабатывал класс Card для использования в игре в блэкджек.

Я планировал создать класс Card с getValue (), который возвращает, например, 11 для J, 12 для Q и 13 для K, а затем расширить его с помощью класса BlackjackCard, чтобы переопределить этот метод, чтобы эти карты возвращали 10.

Потом меня осенило: объекты класса Card должны быть неизменяемыми. Итак, я перечитал «Эффективную Java 2-е издание», чтобы увидеть, что делать, и обнаружил, что неизменяемые классы должны быть окончательными, чтобы избежать подкласса, нарушающего неизменяемость.

Я тоже искал в Интернете, и все, кажется, согласны с этим.

Так должен ли класс Card быть окончательным?

Как вы можете нарушить неизменность этого класса, расширив его:

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();
  }
}

Спасибо.


person gaijinco    schedule 09.04.2011    source источник


Ответы (7)


Подкласс не может на самом деле изменять значения свойств private final в своем родительском элементе, но он может вести себя так, как если бы он это делал, а что Эффективная Java предупреждает:

Убедитесь, что класс не может быть расширен. Это не позволяет неосторожным или вредоносным подклассам нарушить неизменяемое поведение класса, так как если бы состояние объекта изменилось.

person Wayne    schedule 09.04.2011
comment
+1 уместно отметить, что класс или его методы не могут быть расширены, объявление класса final - лишь один из нескольких способов сделать это. - person Grundlefleck; 22.06.2011

Ответ - да, карта должна быть окончательной.

Объединив ответы K. Claszen и lwburk, см. Следующее:

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(); }
}

Это расширение полностью игнорирует родительское состояние и заменяет его своим собственным изменяемым состоянием. Теперь классы, использующие Card в полиморфных контекстах, не могут зависеть от его неизменности.

person Community    schedule 09.04.2011

Ты можешь это сделать:

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;
  }

}

Расширяющийся класс даже не должен объявлять тот же конструктор, что и родительский класс. Он может иметь конструктор по умолчанию и не заботится о последних членах родительского класса. [Это утверждение неверно - см. комментарии].

person FrVaBe    schedule 09.04.2011
comment
Ваш MyCard класс не компилируется. У него нет доступа к rank и suit. - person Wayne; 09.04.2011
comment
последнее утверждение не соответствует действительности. вы должны установить все конечные переменные-члены. Кроме того, ваш пример не будет компилироваться, потому что вы не можете сбросить this.rank и this.suit. но в остальном ваша точка зрения остается верной. - person jtahlborn; 09.04.2011
comment
@lwburk & @jtahlborn Вы правы - конечные члены не могут быть сброшены - набрал это быстро! - person FrVaBe; 10.04.2011

Когда они говорят, что неизменяемые классы должны быть окончательными, они имеют в виду, как вы можете гарантировать неизменяемость, а не потому, что что-то неизменяемое, оно должно быть окончательным. Это незначительное различие. Если вы не хотите, чтобы ваш класс когда-либо расширялся, он должен быть окончательным.

person Craig Suchanec    schedule 09.04.2011

В общем, это хорошая рекомендация. однако, если вы контролируете весь код, иногда полезно иметь возможность расширить неизменяемый класс (возможно, для создания другого неизменяемого класса с дополнительной информацией). как и в случае с большинством рекомендаций, вы должны делать осмысленный выбор, когда они имеют смысл, а когда нет.

person jtahlborn    schedule 09.04.2011

Если произвольный код может расширять неизменяемый класс, то произвольный код может создавать объекты, которые во многом похожи на неизменяемый класс, но не являются неизменяемыми. Если люди, которые пишут такой код, не смогут повредить ничего, кроме себя, то такая ситуация может быть терпимой. Если кто-то может использовать такой класс для обхода мер безопасности, то это недопустимо.

Обратите внимание, что иногда может быть полезно иметь расширяемый класс, но обещающий неизменность от имени всех производных классов. Такому классу и его потребителям, конечно, придется полагаться на наследников класса, чтобы они не делали ничего странного и дурацкого, но иногда такой подход может быть лучше любой альтернативы. Например, у кого-то может быть класс, который должен выполнять какое-то действие в определенное время, с условием, что объекты этого класса могут иметь произвольный псевдоним. Такой класс не был бы очень полезным, если бы ни класс, ни любое из его полей, ни какие-либо поля в любом из его производных классов и т. Д. Не могли использовать производные типы, поскольку определение класса ограничивало бы, какие типы действий могут быть выполненный. Лучшим подходом, вероятно, было бы не объявлять класс окончательным, но четко указать в документации, что поведение изменяемых подклассов не будет согласованным или предсказуемым.

person supercat    schedule 17.10.2011

Вы можете иметь Valuator, привязанный к игре, и удалить getValue из класса, таким образом Card может быть 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;
  }
}

А оценщики работают так:

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) {
    ...
  }
}

Теперь, если вы все еще хотите сохранить свою Card иерархию, отметка Card методов final предотвратит их переопределение дочерним классом.

person Jordão    schedule 29.03.2012