亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 開發 > Java > 正文

Java編程中避免equals方法的隱藏陷阱介紹

2024-07-13 10:14:08
字體:
來源:轉載
供稿:網友

摘要

本文描述重載equals方法的技術,這種技術即使是具現類的子類增加了字段也能保證equal語義的正確性。

在《Effective Java》的第8項中,Josh Bloch描述了當繼承類作為面向對象語言中的等價關系的基礎問題,要保證派生類的equal正確性語義所會面對的困難。Bloch這樣寫到:

除非你忘記了面向對象抽象的好處,否則在當你繼承一個新類或在類中增加了一個值組件時你無法同時保證equal的語義依然正確

在《Programming in Scala》中的第28章演示了一種方法,這種方法允許即使繼承了新類,增加了新的值組件,equal的語義仍然能得到保證。雖然在這本書中這項技術是在使用Scala類環境中,但是這項技術同樣可以應用于Java定義的類中。在本文中的描述來自于Programming in Scala中的文字描述,但是代碼被我從scala翻譯成了Java

常見的等價方法陷阱

javascript/48645.html">java.lang.Object 類定義了equals這個方法,它的子類可以通過重載來覆蓋它。不幸的是,在面向對象中寫出正確的equals方法是非常困難的。事實上,在研究了大量的Java代碼后,2007 paper的作者得出了如下的一個結論:

幾乎所有的equals方法的實現都是錯誤的!

這個問題是因為等價是和很多其他的事物相關聯。例如其中之一,一個的類型C的錯誤等價方法可能意味著你無法將這個類型C的對象可信賴的放入到容器中。比如說,你有兩個元素elem1和elem2他們都是類型C的對象,并且他們是相等,即 elem1.equals(elm2) 返回ture。但是,只要這個equals方法是錯誤的實現,那么你就有可能會看見如下的一些行為:

Set hashSet<c> = new java.util.HashSet<c>();hashSet.add(elem1);hashSet.contains(elem2);  // returns false!</c></c>

當equals重載時,這里有4個會引發equals行為不一致的常見陷阱:

定義了錯誤的equals方法簽名(signature) Defining equals with the wrong signature.

重載了equals的但沒有同時重載hashCode的方法。 Changing equals without also changing hashCode.

建立在會變化字域上的equals定義。 Defining equals in terms of mutable fields.

不滿足等價關系的equals錯誤定義 Failing to define equals as an equivalence relation.

在剩下的章節中我們將依次討論這4中陷阱。

陷阱1:定義錯誤equals方法簽名(signature)

考慮為下面這個簡單類Point增加一個等價性方法:

public class Point {  private final int x;  private final int y;  public Point(int x, int y) {    this.x = x;    this.y = y;  }  public int getX() {    return x;  }  public int getY() {    return y;  }  // ...}

看上去非常明顯,但是按照這種方式來定義equals就是錯誤的。

// An utterly wrong definition of equalspublic boolean equals(Point other) { return (this.getX() == other.getX() && this.getY() == other.getY());}

這個方法有什么問題呢?初看起來,它工作的非常完美:

Point p1 = new Point(1, 2);Point p2 = new Point(1, 2);Point q = new Point(2, 3);System.out.println(p1.equals(p2)); // prints trueSystem.out.println(p1.equals(q)); // prints false

然而,當我們一旦把這個Point類的實例放入到一個容器中問題就出現了:

import java.util.HashSet; HashSet<point> coll = new HashSet<point>();coll.add(p1); System.out.println(coll.contains(p2)); // prints false</point></point>

為什么coll中沒有包含p2呢?甚至是p1也被加到集合里面,p1和p2是是等價的對象嗎?在下面的程序中,我們可以找到其中的一些原因,定義p2a是一個指向p2的對象,但是p2a的類型是Object而非Point類型:

Object p2a = p2;

現在我們重復第一個比較,但是不再使用p2而是p2a,我們將會得到如下的結果:

System.out.println(p1.equals(p2a)); // prints false

到底是那里出了了問題?事實上,之前所給出的equals版本并沒有覆蓋Object類的equals方法,因為他的類型不同。下面是Object的equals方法的定義

public boolean equals(Object other)

因為Point類中的equals方法使用的是以Point類而非Object類做為參數,因此它并沒有覆蓋Object中的equals方法。而是一種變化了的重載。在Java中重載被解析為靜態的參數類型而非運行期的類型,因此當靜態參數類型是Point,Point的equals方法就被調用。然而當靜態參數類型是Object時,Object類的equals就被調用。因為這個方法并沒有被覆蓋,因此它仍然是實現成比較對象標示。這就是為什么雖然p1和p2a具有同樣的x,y值,”p1.equals(p2a)”仍然返回了false。這也是會什么HasSet的contains方法返回false的原因,因為這個方法操作的是泛型,他調用的是一般化的Object上equals方法而非Point類上變化了的重載方法equals

一個更好但不完美的equals方法定義如下:

// A better definition, but still not perfect@Override public boolean equals(Object other) {  boolean result = false;  if (other instanceof Point) {    Point that = (Point) other;    result = (this.getX() == that.getX() && this.getY() == that.getY());  }  return result;}

現在equals有了正確的類型,它使用了一個Object類型的參數和一個返回布爾型的結果。這個方法的實現使用instanceof操作和做了一個造型。它首先檢查這個對象是否是一個Point類,如果是,他就比較兩個點的坐標并返回結果,否則返回false。

陷阱2:重載了equals的但沒有同時重載hashCode的方法

如果你使用上一個定義的Point類進行p1和p2a的反復比較,你都會得到你預期的true的結果。但是如果你將這個類對象放入到HashSet.contains()方法中測試,你就有可能仍然得到false的結果:

Point p1 = new Point(1, 2);Point p2 = new Point(1, 2); HashSet<point> coll = new HashSet<point>();coll.add(p1); System.out.println(coll.contains(p2)); // 打印 false (有可能)</point></point>

事實上,這個個結果不是100%的false,你也可能有返回ture的經歷。如果你得到的結果是true的話,那么你試試其他的坐標值,最終你一定會得到一個在集合中不包含的結果。導致這個結果的原因是Point重載了equals卻沒有重載hashCode。

注意上面例子的的容器是一個HashSet,這就意味著容器中的元素根據他們的哈希碼被被放入到”哈希桶 hash buckets”中。contains方法首先根據哈希碼在哈希桶中查找,然后讓桶中的所有元素和所給的參數進行比較?,F在,雖然最后一個Point類的版本重定義了equals方法,但是它并沒有同時重定義hashCode。因此,hashCode仍然是Object類的那個版本,即:所分配對象的一個地址的變換。所以p1和p2的哈希碼理所當然的不同了,甚至是即時這兩個點的坐標完全相同。不同的哈希碼導致他們具有極高的可能性被放入到集合中不同的哈希桶中。contains方法將會去找p2的哈希碼對應哈希桶中的匹配元素。但是大多數情況下,p1一定是在另外一個桶中,因此,p2永遠找不到p1進行匹配。當然p2和p2也可能偶爾會被放入到一個桶中,在這種情況下,contains的結果就為true了。

最新一個Point類實現的問題是,它的實現違背了作為Object類的定義的hashCode的語義。

如果兩個對象根據equals(Object)方法是相等的,那么在這兩個對象上調用hashCode方法應該產生同樣的值

事實上,在Java中,hashCode和equals需要一起被重定義是眾所周知的。此外,hashCode只可以依賴于equals依賴的域來產生值。對于Point這個類來說,下面的的hashCode定義是一個非常合適的定義。

public class Point {  private final int x;  private final int y;  public Point(int x, int y) {    this.x = x;    this.y = y;  }  public int getX() {    return x;  }  public int getY() {    return y;  }  @Override public boolean equals(Object other) {    boolean result = false;    if (other instanceof Point) {      Point that = (Point) other;      result = (this.getX() == that.getX() && this.getY() == that.getY());    }    return result;  }  @Override public int hashCode() {    return (41 * (41 + getX()) + getY());  }}

這只是hashCode一個可能的實現。x域加上常量41后的結果再乘與41并將結果在加上y域的值。這樣做就可以以低成本的運行時間和低成本代碼大小得到一個哈希碼的合理的分布(譯者注:性價比相對較高的做法)。

增加hashCode方法重載修正了定義類似Point類等價性的問題。然而,關于類的等價性仍然有其他的問題點待發現。

陷阱3:建立在會變化字段上的equals定義

讓我們在Point類做一個非常微小的變化

public class Point {  private int x;  private int y;  public Point(int x, int y) {    this.x = x;    this.y = y;  }  public int getX() {    return x;  }  public int getY() {    return y;  }  public void setX(int x) { // Problematic    this.x = x;  }  public void setY(int y) {    this.y = y;  }  @Override public boolean equals(Object other) {    boolean result = false;    if (other instanceof Point) {      Point that = (Point) other;      result = (this.getX() == that.getX() && this.getY() == that.getY());    }    return result;  }  @Override public int hashCode() {    return (41 * (41 + getX()) + getY());  }}

唯一的不同是x和y域不再是final,并且兩個set方法被增加到類中來,并允許客戶改變x和y的值。equals和hashCode這個方法的定義現在是基于在這兩個會發生變化的域上,因此當他們的域的值改變時,結果也就跟著改變。因此一旦你將這個point對象放入到集合中你將會看到非常神奇的效果。

Point p = new Point(1, 2); HashSet<point> coll = new HashSet<point>();coll.add(p); System.out.println(coll.contains(p)); // 打印 true</point></point>

現在如果你改變p中的一個域,這個集合中還會包含point嗎,我們將拭目以待。

p.setX(p.getX() + 1); System.out.println(coll.contains(p)); // (有可能)打印 false

看起來非常的奇怪。p去那里去了?如果你通過集合的迭代器來檢查p是否包含,你將會得到更奇怪的結果。

Iterator<point> it = coll.iterator();boolean containedP = false;while (it.hasNext()) {  Point nextP = it.next();  if (nextP.equals(p)) {    containedP = true;    break;  }} System.out.println(containedP); // 打印 true</point>

結果是,集合中不包含p,但是p在集合的元素中!到底發生了什么!當然,所有的這一切都是在x域的修改后才發生的,p最終的的hashCode是在集合coll錯誤的哈希桶中。即,原始哈希桶不再有其新值對應的哈希碼。換句話說,p已經在集合coll的是視野范圍之外,雖然他仍然屬于coll的元素。

從這個例子所得到的教訓是,當equals和hashCode依賴于會變化的狀態時,那么就會給用戶帶來問題。如果這樣的對象被放入到集合中,用戶必須小心,不要修改這些這些對象所依賴的狀態,這是一個小陷阱。如果你需要根據對象當前的狀態進行比較的話,你應該不要再重定義equals,應該起其他的方法名字而不是equals。對于我們的Point類的最后的定義,我們最好省略掉hashCode的重載,并將比較的方法名命名為equalsContents,或其他不同于equals的名字。那么Point將會繼承原來默認的equals和hashCode的實現,因此當我們修改了x域后p依然會呆在其原來在容器中應該在位置。

陷阱4:不滿足等價關系的equals錯誤定義

Object中的equals的規范闡述了equals方法必須實現在非null對象上的等價關系:

自反原則:對于任何非null值X,表達式x.equals(x)總返回true。

等價性:對于任何非空值x和y,那么當且僅當y.equals(x)返回真時,x.equals(y)返回真。

傳遞性:對于任何非空值x,y,和z,如果x.equals(y)返回真,且y.equals(z)也返回真,那么x.equals(z)也應該返回真。

一致性:對于非空x,y,多次調用x.equals(y)應該一致的返回真或假。提供給equals方法比較使用的信息不應該包含改過的信息。

對于任何非空值x,x.equals(null)應該總返回false.

Point類的equals定義已經被開發成了足夠滿足equals規范的定義。然而,當考慮到繼承的時候,事情就開始變得非常復雜起來。比如說有一個Point的子類ColoredPoint,它比Point多增加了一個類型是Color的color域。假設Color被定義為一個枚舉類型:

public enum Color {  RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET;}

ColoredPoint重載了equals方法,并考慮到新加入color域,代碼如下:

public class ColoredPoint extends Point { // Problem: equals not symmetric  private final Color color;  public ColoredPoint(int x, int y, Color color) {    super(x, y);    this.color = color;  }  @Override public boolean equals(Object other) {    boolean result = false;    if (other instanceof ColoredPoint) {      ColoredPoint that = (ColoredPoint) other;      result = (this.color.equals(that.color) && super.equals(that));    }    return result;  }}

這是很多程序員都有可能寫成的代碼。注意在本例中,類ColoredPointed不需要重載hashCode,因為新的ColoredPoint類上的equals定義,嚴格的重載了Point上equals的定義。hashCode的規范仍然是有效,如果兩個著色點(colored point)相等,其坐標必定相等,因此它的hashCode也保證了具有同樣的值。

對于ColoredPoint類自身對象的比較是沒有問題的,但是如果使用ColoredPoint和Point混合進行比較就要出現問題。

Point p = new Point(1, 2);ColoredPoint cp = new ColoredPoint(1, 2, Color.RED);System.out.println(p.equals(cp)); // 打印真 trueSystem.out.println(cp.equals(p)); // 打印假 false

“p等價于cp”的比較這個調用的是定義在Point類上的equals方法。這個方法只考慮兩個點的坐標。因此比較返回真。在另外一方面,“cp等價于p”的比較這個調用的是定義在ColoredPoint類上的equals方法,返回的結果卻是false,這是因為p不是ColoredPoint,所以equals這個定義違背了對稱性。

違背對稱性對于集合來說將導致不可以預期的后果,例如:

Set<point> hashSet1 = new java.util.HashSet<point>();hashSet1.add(p);System.out.println(hashSet1.contains(cp));  // 打印 false Set<point> hashSet2 = new java.util.HashSet<point>();hashSet2.add(cp);System.out.println(hashSet2.contains(p));  // 打印 true</point></point></point></point>

因此雖然p和cp是等價的,但是contains測試中一個返回成功,另外一個卻返回失敗。

你如何修改equals的定義,才能使得這個方法滿足對稱性?本質上說有兩種方法,你可以使得這種關系變得更一般化或更嚴格。更一般化的意思是這一對對象,a和b,被用于進行對比,無論是a比b還是b比a 都返回true,下面是代碼:

public class ColoredPoint extends Point { // Problem: equals not transitive  private final Color color;  public ColoredPoint(int x, int y, Color color) {    super(x, y);    this.color = color;  }  @Override public boolean equals(Object other) {    boolean result = false;    if (other instanceof ColoredPoint) {      ColoredPoint that = (ColoredPoint) other;      result = (this.color.equals(that.color) && super.equals(that));    }    else if (other instanceof Point) {      Point that = (Point) other;      result = that.equals(this);    }    return result;  }}

在ColoredPoint中的equals的新定義比老定義中檢查了更多的情況:如果對象是一個Point對象而不是ColoredPoint,方法就轉變為Point類的equals方法調用。這個所希望達到的效果就是equals的對稱性,不管”cp.equals(p)”還是”p.equals(cp)”的結果都是true。然而這種方法,equals的規范還是被破壞了,現在的問題是這個新等價性不滿足傳遞性。考慮下面的一段代碼實例,定義了一個點和這個點上上兩種不同顏色點:

ColoredPoint redP = new ColoredPoint(1, 2, Color.RED);ColoredPoint blueP = new ColoredPoint(1, 2, Color.BLUE);

redP等價于p,p等價于blueP

System.out.println(redP.equals(p)); // prints trueSystem.out.println(p.equals(blueP)); // prints true

然而,對比redP和blueP的結果是false:

System.out.println(redP.equals(blueP)); // 打印 false

因此,equals的傳遞性就被違背了。

使equals的關系更一般化似乎會將我們帶入到死胡同。我們應該采用更嚴格化的方法。一種更嚴格化的equals方法是認為不同類的對象是不同的。這個可以通過修改Point類和ColoredPoint類的equals方法來達到。你能增加額外的比較來檢查是否運行態的這個Point類和那個Point類是同一個類,就像如下所示的代碼一樣:

// A technically valid, but unsatisfying, equals methodpublic class Point {  private final int x;  private final int y;  public Point(int x, int y) {    this.x = x;    this.y = y;  }  public int getX() {    return x;  }  public int getY() {    return y;  }  @Override public boolean equals(Object other) {    boolean result = false;    if (other instanceof Point) {      Point that = (Point) other;      result = (this.getX() == that.getX() && this.getY() == that.getY()          && this.getClass().equals(that.getClass()));    }    return result;  }  @Override public int hashCode() {    return (41 * (41 + getX()) + getY());  }}

你現在可以將ColoredPoint類的equals實現用回剛才那個不滿足對稱性要的equals實現了。

public class ColoredPoint extends Point { // 不再違反對稱性需求  private final Color color;  public ColoredPoint(int x, int y, Color color) {    super(x, y);    this.color = color;  }  @Override public boolean equals(Object other) {    boolean result = false;    if (other instanceof ColoredPoint) {      ColoredPoint that = (ColoredPoint) other;      result = (this.color.equals(that.color) && super.equals(that));    }    return result;  }}

這里,Point類的實例只有當和另外一個對象是同樣類,并且有同樣的坐標時候,他們才被認為是相等的,即意味著 .getClass()返回的是同樣的值。這個新定義的等價關系滿足了對稱性和傳遞性因為對于比較對象是不同的類時結果總是false。所以著色點(colored point)永遠不會等于點(point)。通常這看起來非常合理,但是這里也存在著另外一種爭論——這樣的比較過于嚴格了。

考慮我們如下這種稍微的迂回的方式來定義我們的坐標點(1,2)

Point pAnon = new Point(1, 1) {  @Override public int getY() {    return 2;  }};

pAnon等于p嗎?答案是假,因為p和pAnon的java.lang.Class對象不同。p是Point,而pAnon是Point的一個匿名派生類。但是,非常清晰的是pAnon的確是在坐標1,2上的另外一個點。所以將他們認為是不同的點是沒有理由的。

canEqual 方法

到此,我們看其來似乎是遇到阻礙了,存在著一種正常的方式不僅可以在不同類繼承層次上定義等價性,并且保證其等價的規范性嗎?事實上,的確存在這樣的一種方法,但是這就要求除了重定義equals和hashCode外還要另外的定義一個方法?;舅悸肪褪窃谥剌dequals(和hashCode)的同時,它應該也要要明確的聲明這個類的對象永遠不等價于其他的實現了不同等價方法的超類的對象。為了達到這個目標,我們對每一個重載了equals的類新增一個方法canEqual方法。這個方法的方法簽名是:

public boolean canEqual(Object other)

如果other 對象是canEquals(重)定義那個類的實例時,那么這個方法應該返回真,否則返回false。這個方法由equals方法調用,并保證了兩個對象是可以相互比較的。下面Point類的新的也是最終的實現:

public class Point {  private final int x;  private final int y;  public Point(int x, int y) {    this.x = x;    this.y = y;  }  public int getX() {    return x;  }  public int getY() {    return y;  }  @Override public boolean equals(Object other) {    boolean result = false;    if (other instanceof Point) {      Point that = (Point) other;      result =(that.canEqual(this) && this.getX() == that.getX() && this.getY() == that.getY());    }    return result;  }  @Override public int hashCode() {    return (41 * (41 + getX()) + getY());  }  public boolean canEqual(Object other) {    return (other instanceof Point);  }}

這個版本的Point類的equals方法中包含了一個額外的需求,通過canEquals方法來決定另外一個對象是否是是滿足可以比較的對象。在Point中的canEqual宣稱了所有的Point類實例都能被比較。

下面是ColoredPoint相應的實現

public class ColoredPoint extends Point { // 不再違背對稱性  private final Color color;  public ColoredPoint(int x, int y, Color color) {    super(x, y);    this.color = color;  }  @Override public boolean equals(Object other) {    boolean result = false;    if (other instanceof ColoredPoint) {      ColoredPoint that = (ColoredPoint) other;      result = (that.canEqual(this) && this.color.equals(that.color) && super.equals(that));    }    return result;  }  @Override public int hashCode() {    return (41 * super.hashCode() + color.hashCode());  }  @Override public boolean canEqual(Object other) {    return (other instanceof ColoredPoint);  }}

在上顯示的新版本的Point類和ColoredPoint類定義保證了等價的規范。等價是對稱和可傳遞的。比較一個Point和ColoredPoint類總是返回false。因為點p和著色點cp,“p.equals(cp)返回的是假。并且,因為cp.canEqual(p)總返回false。相反的比較,cp.equals(p)同樣也返回false,由于p不是一個ColoredPoint,所以在ColoredPoint的equals方法體內的第一個instanceof檢查就失敗了。

另外一個方面,不同的Point子類的實例卻是可以比較的,同樣沒有重定義等價性方法的類也是可以比較的。對于這個新類的定義,p和pAnon的比較將總返回true。下面是一些例子:

Point p = new Point(1, 2);ColoredPoint cp = new ColoredPoint(1, 2, Color.INDIGO);Point pAnon = new Point(1, 1) {  @Override public int getY() {    return 2;  }};Set<point> coll = new java.util.HashSet<point>();coll.add(p);System.out.println(coll.contains(p)); // 打印 trueSystem.out.println(coll.contains(cp)); // 打印 falseSystem.out.println(coll.contains(pAnon)); // 打印 true</point></point>

這些例子顯示了如果父類在equals的實現定義并調用了canEquals,那么開發人員實現的子類就能決定這個子類是否可以和它父類的實例進行比較。例如ColoredPoint,因為它以”一個著色點永遠不可以等于普通不帶顏色的點重載了” canEqual,所以他們就不能比較。但是因為pAnon引用的匿名子類沒有重載canEqual,因此它的實例就可以和Point的實例進行對比。

canEqual方法的一個潛在的爭論是它是否違背了Liskov替換準則(LSP)。例如,通過比較運行態的類來實現的比較技術(譯者注:canEqual的前一版本,使用.getClass()的那個版本),將導致不能定義出一個子類,這個子類的實例可以和其父類進行比較,因此就違背了LSP。這是因為,LSP原則是這樣的,在任何你能使用父類的地方你都可以使用子類去替換它。在之前例子中,雖然cp的x,y坐標匹配那些在集合中的點,然而”coll.contains(cp)”仍然返回false,這看起來似乎違背得了LSP準則,因為你不能這里能使用Point的地方使用一個ColoredPointed。但是我們認為這種解釋是錯誤的,因為LSP原則并沒有要求子類和父類的行為一致,而僅要求其行為能一種方式滿足父類的規范。

通過比較運行態的類來編寫equals方法(譯者注:canEqual的前一版本,使用.getClass()的那個版本)的問題并不是違背LSP準則的問題,但是它也沒有為你指明一種創建派生類的實例能和父類實例進行對比的的方法。例如,我們使用這種運行態比較的技術在之前的”coll.contains(pAnon)”將會返回false,并且這并不是我們希望的。相反我們希望“coll.contains(cp)”返回false,因為通過在ColoredPoint中重載的equals,我基本上可以說,一個在坐標1,2上著色點和一個坐標1,2上的普通點并不是一回事。然而,在最后的例子中,我們能傳遞Point兩種不同的子類實例到集合中contains方法,并且我們能得到兩個不同的答案,并且這兩個答案都正確。

總結

以上就是本文關于Java編程中避免equals方法的隱藏陷阱介紹的全部內容,希望對大家有所幫助。


注:相關教程知識閱讀請移步到JAVA教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
久久精彩免费视频| 精品国产自在精品国产浪潮| www.亚洲人.com| 精品亚洲男同gayvideo网站| 亚洲精品久久久久久下一站| 欧美精品久久久久a| 欧美精品久久一区二区| 色婷婷av一区二区三区久久| 亚洲曰本av电影| 97国产精品视频| 亚洲精品短视频| 欧美亚洲另类在线| 九九久久精品一区| 久久av在线看| 日本精品免费一区二区三区| 亚洲国产成人精品久久久国产成人一区| 欧美日韩国产影院| 夜夜嗨av色综合久久久综合网| 精品亚洲va在线va天堂资源站| 欧美日韩国产成人高清视频| 57pao国产成人免费| 亚洲а∨天堂久久精品喷水| www.日韩.com| 久久精品国产欧美激情| 亚洲日本中文字幕| 中文日韩在线观看| 日韩福利在线播放| 国产精品入口福利| 狠狠爱在线视频一区| 欧美日韩亚洲网| 欧美性少妇18aaaa视频| 国产精品激情av电影在线观看| 成人天堂噜噜噜| 日本最新高清不卡中文字幕| 国产欧美日韩最新| 久久久久久久91| 日韩电影大全免费观看2023年上| 国产成人精品视| 亚洲欧洲在线播放| 国产日韩在线视频| 国产剧情日韩欧美| 欧美日韩国产在线| 九九久久精品一区| 亚洲一区二区中文字幕| 992tv在线成人免费观看| 国产成人精品av| 欧美成人国产va精品日本一级| 成人精品aaaa网站| 亚洲国产日韩欧美在线动漫| 成人精品福利视频| 国产一区二区三区丝袜| 国产精品国语对白| 精品国产乱码久久久久久婷婷| 日韩不卡在线观看| 亚洲一区av在线播放| 欧洲成人在线视频| 尤物九九久久国产精品的分类| www.日本久久久久com.| 欧美大尺度电影在线观看| 亚洲精品电影在线观看| 2019最新中文字幕| 精品久久久精品| www.久久久久久.com| 亚洲国产精品视频在线观看| 91精品国产综合久久香蕉最新版| 久久人人爽人人爽人人片av高请| 精品国产欧美一区二区三区成人| 日韩电影在线观看永久视频免费网站| 91国在线精品国内播放| 欧美亚洲成人xxx| 欧美黑人xxxⅹ高潮交| 亚洲最大成人免费视频| 亚洲欧美制服另类日韩| 亚洲最大中文字幕| 成人国产在线激情| 韩国日本不卡在线| 亚洲国产日韩精品在线| 亚洲欧美另类中文字幕| 欧美电影在线观看高清| 亚洲精品网站在线播放gif| 日韩在线国产精品| 久久久亚洲影院你懂的| 国产成人啪精品视频免费网| 国产主播欧美精品| 国产综合在线视频| 一区二区三区无码高清视频| 欧美福利在线观看| 国产成人精品一区二区在线| 97av在线视频| 97超碰蝌蚪网人人做人人爽| 日韩免费在线电影| 亚洲精品久久久久久久久久久久| 最近2019中文字幕第三页视频| 日韩精品在线播放| 欧美国产视频一区二区| www日韩中文字幕在线看| 国产欧美日韩专区发布| 中文字幕日韩av综合精品| 7777精品久久久久久| 久久免费国产精品1| 国产精品91久久久久久| 国模私拍一区二区三区| 国产成人精品视频在线观看| 欧美激情影音先锋| 97视频国产在线| 久久视频国产精品免费视频在线| 亚洲在线第一页| 91欧美激情另类亚洲| 777午夜精品福利在线观看| 91最新在线免费观看| 亚洲精品一区二区三区不| 亚洲人成电影网站| 欧美另类极品videosbestfree| 国语自产精品视频在线看抢先版图片| 精品小视频在线| 国产91精品久久久久久久| 亚洲国产高清高潮精品美女| 国产成人啪精品视频免费网| 日韩久久午夜影院| 国产精品露脸自拍| 国产99视频在线观看| 亚洲天天在线日亚洲洲精| 91豆花精品一区| 国产视频福利一区| 日韩av在线看| 国产精品久久久久久久久久久不卡| 久久精品国产91精品亚洲| 欧美色图在线视频| 久热在线中文字幕色999舞| 亚洲视屏在线播放| 欧美色另类天堂2015| 亚洲精品视频免费在线观看| 亚洲aa在线观看| 另类天堂视频在线观看| 亚洲欧美国产精品专区久久| 亚洲自拍另类欧美丝袜| 日韩av在线免费播放| 国产美女被下药99| 精品人伦一区二区三区蜜桃网站| 亚洲久久久久久久久久久| 亚洲欧美一区二区精品久久久| 久久五月天色综合| www国产精品com| 69影院欧美专区视频| 国产在线播放91| 久久精品夜夜夜夜夜久久| 国产精品视频网址| 亚洲成年网站在线观看| 亚洲精品短视频| 日本成人黄色片| 久久久精品一区| 欧美黄色片视频| 久久频这里精品99香蕉| 在线视频日本亚洲性| www国产亚洲精品久久网站| 久久天天躁狠狠躁夜夜躁| 欧美日韩国产中文字幕| 一区二区在线视频| 日韩在线国产精品| 亚洲国产美女久久久久| 欧美激情视频在线观看| 亚洲人成网站在线播| 日韩欧美第一页| 亚洲国产精品推荐|