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

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

新規登録して質問してみよう
ただいま回答率
85.48%
Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Q&A

解決済

2回答

4229閲覧

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

winor30

総合スコア15

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

0グッド

0クリップ

投稿2018/02/15 16:22

表題の通り、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は諦めているという認識であっているでしょうか?

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2018/02/20 23:43 編集

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

2018/02/20 23:36

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

退会済みユーザー

2018/02/20 23:46

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

回答2

0

ベストアンサー

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

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

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

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

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


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

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

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

java

1class Name { 2 String name; 3 4 Name(String name) { this.name = Objects.requireNonNull(name); } 5 6 @Override public int hashCode() { return name.hashCode(); } 7 8 @Override public boolean equals(Object o) { 9 if (o instanceof Name) 10 return name.equals(((Name)o).name); 11 else 12 return false; 13 } 14} 15 16class FullName { 17 String lastName; 18 19 FullName(String firstName, String lastName) { 20 super(firstName); 21 this.lastName = Objects.requireNonNull(lastName); 22 } 23 24 @Override 25 public int hashCode() { 26 if (lastName == null || lastName.isEmpty()) { 27 return super.hashCode(); 28 } else { 29 return super.hashCode() ^ lastName.hashCode(); 30 } 31 } 32 33 @Override 34 public boolean equals(Object o) { 35 if (lastName.isEmpty()) 36 return super.equals(o); 37 else if (o instanceof FullName) 38 return lastName.equals(((FullName)o).lastName) && super.equals(o); 39 else 40 return false; 41 } 42}

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

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

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

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


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

投稿2018/02/15 16:48

編集2018/02/21 00:41
KSwordOfHaste

総合スコア18394

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

winor30

2018/02/20 23:19

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

0

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

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

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

投稿2018/02/15 23:42

maisumakun

総合スコア145183

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

winor30

2018/02/20 23:05

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

2018/02/20 23:24

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問