質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.99%

lombokのequalsメソッドがリスコフの置換原則を満たしているか?

解決済

回答 2

投稿

  • 評価
  • クリップ 0
  • VIEW 1,740

winor30

score 13

表題の通り、lombokの@EqualsAndHashCodeで実装されるequals()メソッドはリスコフの置換原則を満たしていないと思い質問させていただきました。
特に、equals()メソッドの中でも、canEqual()がリスコフの置換原則を完全に破棄していると思います。例えば、@EqualsAndHashCodeが使われた、下記のようなNameクラスとこのクラスを継承したFullNameクラスがあるとします。

@EqualsAndHashCode
@RequiredArgsConstructor
@Getter
public class Name {
  private final String firstName;
}

@Getter
@RequiredArgsConstructor
public class FullName extends Name {
  private final String LastName;
}

また、この場合のequals()メソッドとcanEqual()メソッドは下記のようになると思います。

public class Name {
...
  @Override 
  public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof Name)) return false;
    Name other = (Name) o;
    if (!other.canEqual((Object)this)) return false;
    if (this.getFirstName() == null ? other.getFirstName() != null : !this.getFirstName().equals(other.getFirstName())) return false;
    return true;
  }

  protected boolean canEqual(Object other) {
    return other instanceof Name;
  }
}

このとき、equalsメソッドで成り立たなければならない推移性や対称性等など条件は満たすと思います。
しかしながら、下記のようにHashSetを使う場合ではリスコフの置換原則は成り立たない気がします。(親クラスと子クラスが別々の挙動をする)

public static void main(String[] args) {
  Name name1 = new Name("山田");
  Name name2 = new FullName("山田", "太郎");

  Set<Name> set = new HashSet();
  set.add(new Name("山田")); 
  System.out.println(set.contain(name1)); // true
  System.out.println(set.contain(name2)); //false(trueとなることが、リスコフの置換原則を満たしている)
}

これは、推移性や対称性とリスコフの置換原則を上記のようなケースでは両立させることは不可能なので、lombokは諦めているという認識であっているでしょうか?

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • asahina1979

    2018/02/21 08:15 編集

    if (!other.canEqual((Object)this)) return false; デッドコード?

    キャンセル

  • winor30

    2018/02/21 08:36

    ご指摘ありがとうございます。私も最初デッドコードだと思ったのですが、今回の場合だと、FullName型のインスタンスから呼ばれたときに到達すると思います。

    キャンセル

  • asahina1979

    2018/02/21 08:46

    canEqualをオーバーライドしてないからデッドコードかなと、

    キャンセル

回答 2

checkベストアンサー

+2

単に「等値判定はリスコフの置換原則の判断基準とは関係ない」ということのような気がします。

(1) Name型のオブジェクトxに関して真となる属性をq(x)とする。
(2) FullName型がName型の派生でyがFullNameのオブジェクトならq(y)が真になる

というのがリスコフの置換原則が言わんとしていることだと思いますが、そもそもName型のあるインスタンスα、βに対してα.equals(β)が常に真であるわけではないので、「equalsの計算が真になること」はName型の派生であるかどうかを判断する際の基準になるような「属性」とは言えないのではないですか?

個人的に、リスコフの置換原則を実際に即して解釈するなら「equalsメソッドで等値判定ができること」は属性と言えますが、「equalsメソッドの結果が真であること」は属性ではないというふうに捉えています。

普段何気なく感じていることをコメントしただけですので厳密な解釈でもなんでもありません。もし間違ってたらスミマセン。


追記:回答コメントを受けて

質問者さんの主張を正しくとらえることができているかちょっと自信ないですがコメントから以下のように思いました。

例えばNameのequalsを派生クラスで「拡張」することも可能で

class Name {
  String name;

  Name(String name) { this.name = Objects.requireNonNull(name); }

  @Override public int hashCode() { return name.hashCode(); }

  @Override public boolean equals(Object o) {
    if (o instanceof Name)
      return name.equals(((Name)o).name);
    else
      return false;
  }
}

class FullName {
  String lastName;

  FullName(String firstName, String lastName) {
    super(firstName);
    this.lastName = Objects.requireNonNull(lastName);
  }

  @Override
  public int hashCode() {
    if (lastName == null || lastName.isEmpty()) {
      return super.hashCode();
    } else {
      return super.hashCode() ^ lastName.hashCode();
    }
  }

  @Override
  public boolean equals(Object o) {
    if (lastName.isEmpty())
      return super.equals(o);
    else if (o instanceof FullName)
      return lastName.equals(((FullName)o).lastName) && super.equals(o);
    else
      return false;
  }
}

こうしておけば
new FullName("太郎", "").equlas(new Name("太郎"))
といったことを成立させられます。つまりequalsの機能を「変更」するのではなく「拡張」できます。しかしlombokの@EqualsAndHashCodeではそのような「拡張」にはならないとおっしゃりたいのかも知れません。

もしそういう主張ならば「そのとおり」だと思います。

ただ派生クラスで基底クラスのequals(値の同一性)という性質を拡張したいなら、派生クラスで追加した属性群の状態が「どんな状態の場合に派生クラス独自の状態を持たないと考えるか」を規定せねばなりませんが、それはフレームワークや言語仕様で規定できるような性質のものではなく「アプリケーション設計者の考えによる」と思います。それを例えば「派生クラスの全ての属性値がデフォルトの値を持つ場合」なんて意味付けをしてしまうと「意味があまりないきつすぎる制約」になるのではないでしょうか?

equalsに期待する機能は普通「インスタンスの値の同一性のチェック」であり前述のように「アプリケーション設計者の設計如何によって独自に決めるもの」のため、フレームワークや言語仕様としてのデフォルトは「派生クラスで拡張するのではなく変更するもの」すなわち「リスコフの置換原則を満たすかどうかの観点には入れないもの」と捉えるのが普通ではないかと思います。


追記2:上記のコード例はかなりヘンテコです。自然な拡張とは言えませんね。
new Name("太郎").equlas(new FullName("太郎", "日本"))
なんてやると常に成立してしまうからです。equalsは交換則を満たすべきなので、上の例は「中途半端に拡張しようとしたがダメだった例」として捉えてください。equalsを派生で「変更」せずに「拡張」するのはそもそも無理があるということだと思います。おかしな例を挙げてしまいすみませんでした。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/02/21 08:19

    回答ありがとうございます。
    ›(1) Name型のオブジェクトxに関して真となる属性をq(x)とする。
    ›(2) FullName型がName型の派生でyがFullNameのオブジェクトならq(y)が真になる
    私が思うにここに今回のケースを当てはめると、q()はset.contain()だと思います。そうすると、Name型のインスタンスnameの明らかに派生であるFullName型のname2はこの条件を満たさないなと感じました。なんか、ここらへんがリスコフに反しているなと思っていました、、、

    キャンセル

  • 2018/02/21 09:27

    回答に追記してみました

    キャンセル

+2

上のコードでは、hashCodeに対する一般契約(Oracle)を守れていません。

a.equals(b)となる場合、a.hashCode() == b.hashCode()となる必要がありますが、この場合にLombokで生成したhashCodeは、NameFullNameで使うフィールドが違うので、equalsで一致したもののhashCodeは、一般には一致しなくなります。

ということで、Javaの契約違反のクラスとなってしまっているので、うまく動かないのは半ば必然です。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/02/21 08:05

    ご回答ありがとうございます。親クラスに@EqualsAndHashCodeを記述しているので、生成されるhashCodeメソッドで使われるフィールドはfirstNameのみだと思ったのですが、違いますでしょうか?

    キャンセル

  • 2018/02/21 08:24

    実際にhashCodeの値は確認してみましたか?

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.99%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる