成人在线亚洲_国产日韩视频一区二区三区_久久久国产精品_99国内精品久久久久久久

您的位置:首頁技術文章
文章詳情頁

JAVA中哈希表HashMap的深入學習

瀏覽:4日期:2022-08-29 11:59:43

深入淺出學Java——HashMap

哈希表(hash table)

也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,本文會對java集合框架中HashMap的實現原理進行講解,并對JDK7的HashMap源碼進行分析。

一、什么是哈希表

在討論哈希表之前,我們先大概了解下其他數據結構在新增,查找等基礎操作執行性能

數組:采用一段連續的存儲單元來存儲數據。對于指定下標的查找,時間復雜度為O(1);通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間復雜度為O(n),當然,對于有序數組,則可采用二分查找,插值查找,斐波那契查找等方式,可將查找復雜度提高為O(logn);對于一般的插入刪除操作,涉及到數組元素的移動,其平均復雜度也為O(n)

線性鏈表:對于鏈表的新增,刪除等操作(在找到指定操作位置后),僅需處理結點間的引用即可,時間復雜度為O(1),而查找操作需要遍歷鏈表逐一進行比對,復雜度為O(n)

二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均復雜度均為O(logn)。

哈希表:相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希沖突的情況下(后面會探討下哈希沖突的情況),僅需一次定位即可完成,時間復雜度為O(1),接下來我們就來看看哈希表是如何實現達到驚艷的常數階O(1)的。

我們知道,數據結構的物理存儲結構只有兩種:順序存儲結構和鏈式存儲結構(像棧,隊列,樹,圖等是從邏輯結構去抽象的,映射到內存中,也這兩種物理組織形式),而在上面我們提到過,在數組中根據下標查找某個元素,一次定位就可以達到,哈希表利用了這種特性,哈希表的主干就是數組。

比如我們要新增或查找某個元素,我們通過把當前元素的關鍵字 通過某個函數映射到數組中的某個位置,通過數組下標一次定位就可完成操作。

這個函數可以簡單描述為:存儲位置 = f(關鍵字) ,這個函數f一般稱為哈希函數,這個函數的設計好壞會直接影響到哈希表的優劣。舉個例子,比如我們要在哈希表中執行插入操作:插入過程如下圖所示

JAVA中哈希表HashMap的深入學習

查找操作同理,先通過哈希函數計算出實際存儲地址,然后從數組中對應地址取出即可。

哈希沖突

然而萬事無完美,如果兩個不同的元素,通過哈希函數得出的實際存儲地址相同怎么辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然后要進行插入的時候,發現已經被其他元素占用了,其實這就是所謂的哈希沖突,也叫哈希碰撞。前面我們提到過,哈希函數的設計至關重要,好的哈希函數會盡可能地保證 計算簡單和散列地址分布均勻,但是,我們需要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生沖突。那么哈希沖突如何解決呢?哈希沖突的解決方案有多種:開放定址法(發生沖突,繼續尋找下一塊未被占用的存儲地址),再散列函數法,鏈地址法,而HashMap即是采用了鏈地址法,也就是數組+鏈表的方式。

二、HashMap的實現原理

HashMap的主干是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。(其實所謂Map其實就是保存了兩個對象之間的映射關系的一種集合)

//HashMap的主干數組,可以看到就是一個Entry數組,初始值為空數組{},主干數組的長度一定是2的次冪。//至于為什么這么做,后面會有詳細分析。transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;Entry是HashMap中的一個靜態內部類。代碼如下

static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構 int hash;//對key的hashcode值進行hash運算后得到的值,存儲在Entry,避免重復計算 /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }

所以,HashMap的總體結構如下:

JAVA中哈希表HashMap的深入學習

簡單來說,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那么查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對于添加操作,其時間復雜度為O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對于查找操作來講,仍需遍歷鏈表,然后通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能才會越好。

其他幾個重要字段

/**實際存儲的key-value鍵值對的個數*/transient int size;/**閾值,當table == {}時,該值為初始容量(初始容量默認為16);當table被填充了,也就是為table分配內存空間后,threshold一般為 capacity*loadFactory。HashMap在進行擴容時需要參考threshold,后面會詳細談到*/int threshold;/**負載因子,代表了table的填充度有多少,默認是0.75加載因子存在的原因,還是因為減緩哈希沖突,如果初始桶為16,等到滿16個元素才擴容,某些桶里可能就有不止一個元素了。所以加載因子默認為0.75,也就是說大小為16的HashMap,到了第13個元素,就會擴容成32。*/final float loadFactor;/**HashMap被改變的次數,由于HashMap非線程安全,在對HashMap進行迭代時,如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),需要拋出異常ConcurrentModificationException*/transient int modCount;

HashMap有4個構造器,其他構造器如果用戶沒有傳入initialCapacity 和loadFactor這兩個參數,會使用默認值

initialCapacity默認為16,loadFactory默認為0.75

我們看下其中一個

public HashMap(int initialCapacity, float loadFactor) { //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230) if (initialCapacity < 0) throw new IllegalArgumentException('Illegal initial capacity: ' + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException('Illegal load factor: ' + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity;  init();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現 }

從上面這段代碼我們可以看出,在常規構造器中,沒有為數組table分配內存空間(有一個入參為指定Map的構造器例外),而是在執行put操作的時候才真正構建table數組

OK,接下來我們來看看put操作的實現

public V put(K key, V value) { //如果table數組為空數組{},進行數組填充(為table分配實際內存空間),入參為threshold, //此時threshold為initialCapacity 默認是1<<4(24=16) if (table == EMPTY_TABLE) { inflateTable(threshold); } //如果key為null,存儲位置為table[0]或table[0]的沖突鏈上 if (key == null) return putForNullKey(value); int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻 int i = indexFor(hash, table.length);//獲取在table中的實際位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,并返回舊value Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++;//保證并發訪問時,若HashMap內部結構發生變化,快速響應失敗 addEntry(hash, key, value, i);//新增一個entry return null; }

inflateTable這個方法用于為主干數組table在內存中分配存儲空間,通過roundUpToPowerOf2(toSize)可以確保capacity為大于或等于toSize的最接近toSize的二次冪,比如toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.

private void inflateTable(int toSize) { int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪 /**此處為threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值, capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大于1 */ threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }

roundUpToPowerOf2中的這段處理使得數組長度一定為2的次冪,Integer.highestOneBit是用來獲取最左邊的bit(其他bit位為0)所代表的數值.

private static int roundUpToPowerOf2(int number) { // assert number >= 0 : 'number must be non-negative'; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; }

hash函數

/**這是一個神奇的函數,用了很多的異或,移位等運算對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置盡量分布均勻*/final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }

以上hash函數計算出的值,通過indexFor進一步處理來獲取實際的存儲位置

/** * 返回數組下標 */ static int indexFor(int h, int length) { return h & (length-1); }

h&(length-1)保證獲取的index一定在數組范圍內,舉個例子,默認容量16,length-1=15,h=18,轉換成二進制計算為index=2。位運算對計算機來說,性能更高一些(HashMap中有大量位運算)

所以最終存儲位置的確定流程是這樣的:

JAVA中哈希表HashMap的深入學習

再來看看addEntry的實現:

void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//當size超過臨界閾值threshold,并且即將發生哈希沖突時進行擴容 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }

通過以上代碼能夠得知,當發生哈希沖突并且size大于閾值的時候,需要進行數組擴容,擴容時,需要新建一個長度為之前數組2倍的新的數組,然后將當前的Entry數組中的元素全部傳輸過去,擴容后的新數組長度為之前的2倍,所以擴容相對來說是個耗資源的操作。

三、為何HashMap的數組長度一定是2的次冪?

我們來繼續看上面提到的resize方法

void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }

如果數組進行擴容,數組長度發生變化,而存儲位置 index = h&(length-1),index也可能會發生變化,需要重新計算index,我們先來看看transfer這個方法

void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //for循環中的代碼,逐個遍歷鏈表,重新計算索引位置,將老數組數據復制到新數組中去(數組不存儲實際數據,所以僅僅是拷貝引用而已) for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); //將當前entry的next鏈指向新的索引位置,newTable[i]有可能為空,有可能也是個entry鏈,如果是entry鏈,直接在鏈表頭部插入。 e.next = newTable[i]; newTable[i] = e; e = next; } } }

這個方法將老數組中的數據逐個鏈表地遍歷,扔到新的擴容后的數組中,我們的數組索引位置的計算是通過 對key值的hashcode進行hash擾亂運算后,再通過和 length-1進行位運算得到最終數組索引位置。

HashMap的數組長度一定保持2的次冪,比如16的二進制表示為 10000,那么length-1就是15,二進制為01111,同理擴容后的數組長度為32,二進制表示為100000,length-1為31,二進制表示為011111。從下圖可以我們也能看到這樣會保證低位全為1,而擴容后只有一位差異,也就是多出了最左位的1,這樣在通過 h&(length-1)的時候,只要h對應的最左邊的那一個差異位為0,就能保證得到的新的數組索引和老數組索引一致(大大減少了之前已經散列良好的老數組的數據位置重新調換),個人理解。

JAVA中哈希表HashMap的深入學習

還有,數組長度保持2的次冪,length-1的低位都為1,會使得獲得的數組索引index更加均勻

JAVA中哈希表HashMap的深入學習

我們看到,上面的&運算,高位是不會對結果產生影響的(hash函數采用各種位運算可能也是為了使得低位更加散列),我們只關注低位bit,如果低位全部為1,那么對于h低位部分來說,任何一位的變化都會對結果產生影響,也就是說,要得到index=21這個存儲位置,h的低位只有這一種組合。這也是數組長度設計為必須為2的次冪的原因。

JAVA中哈希表HashMap的深入學習

如果不是2的次冪,也就是低位不是全為1此時,要使得index=21,h的低位部分不再具有唯一性了,哈希沖突的幾率會變的更大,同時,index對應的這個bit位無論如何不會等于1了,而對應的那些數組位置也就被白白浪費了。

get方法:

public V get(Object key) { //如果key為null,則直接去table[0]處去檢索即可。 if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }

get方法通過key值返回對應value,如果key為null,直接去table[0]處檢索。我們再看一下getEntry這個方法

final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //通過key的hashcode值計算hash值 int hash = (key == null) ? 0 : hash(key); //indexFor (hash&length-1) 獲取最終數組索引,然后遍歷鏈表,通過equals方法比對找出對應記錄 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }

可以看出,get方法的實現相對簡單,key(hashcode)?>hash?>indexFor?>最終索引位置,找到對應位置table[i],再查看是否有鏈表,遍歷鏈表,通過key的equals方法比對查找對應的記錄。要注意的是,有人覺得上面在定位到數組位置之后然后遍歷鏈表的時候,e.hash == hash這個判斷沒必要,僅通過equals判斷就可以。其實不然,試想一下,如果傳入的key對象重寫了equals方法卻沒有重寫hashCode,而恰巧此對象定位到這個數組位置,如果僅僅用equals判斷可能是相等的,但其hashCode和當前對象不一致,這種情況,根據Object的hashCode的約定,不能返回當前對象,而應該返回null,后面的例子會做出進一步解釋。

四、重寫equals方法需同時重寫hashCode方法

最后我們再聊聊老生常談的一個問題,各種資料上都會提到,“重寫equals時也要同時覆蓋hashcode”,我們舉個小例子來看看,如果重寫了equals而不重寫hashcode會發生什么樣的問題

public class MyTest { private static class Person{ int idCard; String name; public Person(int idCard, String name) { this.idCard = idCard; this.name = name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()){ return false; } Person person = (Person) o; //兩個對象是否等值,通過idCard來確定 return this.idCard == person.idCard; } } public static void main(String []args){ HashMap<Person,String> map = new HashMap<Person, String>(); Person person = new Person(1234,'喬峰'); //put到hashmap中去 map.put(person,'天龍八部'); //get取出,從邏輯上講應該能輸出“天龍八部” System.out.println('結果:'+map.get(new Person(1234,'蕭峰'))); }}實際輸出結果:null

如果我們已經對HashMap的原理有了一定了解,這個結果就不難理解了。盡管我們在進行get和put操作的時候,使用的key從邏輯上講是等值的(通過equals比較是相等的),但由于沒有重寫hashCode方法,所以put操作時,key(hashcode1)?>hash?>indexFor?>最終索引位置 ,而通過key取出value的時候 key(hashcode1)?>hash?>indexFor?>最終索引位置,由于hashcode1不等于hashcode2,導致沒有定位到一個數組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數組位置,但是也會判斷其entry的hash值是否相等,上面get方法中有提到。)

所以,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證通過equals判斷相等的兩個對象,調用hashCode方法要返回同樣的整數值。而如果equals判斷不相等的兩個對象,其hashCode可以相同(只不過會發生哈希沖突,應盡量避免)。

五、JDK1.8中HashMap的性能優化

假如一個數組槽位上鏈上數據過多(即拉鏈過長的情況)導致性能下降該怎么辦?JDK1.8在JDK1.7的基礎上針對增加了紅黑樹來進行優化。即當鏈表超過8時,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能,其中會用到紅黑樹的插入、刪除、查找等算法。關于這方面的探討我們以后的文章再做說明。

附:HashMap put方法邏輯圖(JDK1.8)

JAVA中哈希表HashMap的深入學習

到此這篇關于哈希表HashMap的深入學習的文章就介紹到這了,更多相關哈希表HashMap的學習內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!

標簽: Java
相關文章:
成人在线亚洲_国产日韩视频一区二区三区_久久久国产精品_99国内精品久久久久久久
一区二区三区中文字幕| 不卡的av中国片| 亚洲欧洲综合另类| 中文字幕在线不卡视频| 久久免费的精品国产v∧| 日韩免费高清av| 久久综合久久综合久久| 精品国产区一区| 国产喂奶挤奶一区二区三区| 久久丝袜美腿综合| 国产精品色在线观看| 亚洲欧洲精品一区二区三区| 亚洲私人影院在线观看| 亚洲高清中文字幕| 奇米影视一区二区三区| 国产综合成人久久大片91| 国产精品中文字幕欧美| 成人毛片在线观看| 欧美成人有码| 99精品视频网| 色狠狠色噜噜噜综合网| 欧美一级免费大片| 久久一留热品黄| 亚洲日本va午夜在线影院| 亚洲一级电影视频| 久久国产麻豆精品| www.日韩在线| 最新日韩av| 色婷婷av一区| 精品国偷自产国产一区| 国产精品久久久久久久久免费丝袜| 一区二区三区日韩精品| 美国三级日本三级久久99| 成人一道本在线| 亚洲国产午夜| 在线视频你懂得一区| 日韩欧美在线一区二区三区| 国产精品久久久久久久岛一牛影视| 亚洲成人在线网站| 国产福利一区二区| 一区精品久久| 欧美日韩精品一区二区三区| 久久久国产精品不卡| 亚洲午夜激情av| 国产精品亚洲一区二区三区妖精| 欧美日韩大片一区二区三区| 久久九九免费| 精品剧情在线观看| 日韩高清在线不卡| 99v久久综合狠狠综合久久| 国产一区二区三区久久| 日韩欧美中文字幕公布| 亚洲一区二区成人在线观看| 成人精品gif动图一区| 久久精品国产清高在天天线 | 亚洲欧美高清| 精品久久久久久最新网址| 亚洲一区自拍偷拍| jlzzjlzz亚洲日本少妇| 日本福利一区二区| 国产精品天干天干在观线| 麻豆精品久久久| 永久久久久久| 日韩精品中午字幕| 日韩精品电影一区亚洲| 欧美人与禽猛交乱配| 欧美一区永久视频免费观看| 一区二区三区免费网站| eeuss影院一区二区三区| 91福利区一区二区三区| 又紧又大又爽精品一区二区| av在线不卡网| 欧美绝品在线观看成人午夜影视| 亚洲欧美区自拍先锋| 99国产精品视频免费观看| 欧美午夜寂寞影院| 亚洲高清视频在线| 亚洲午夜电影| 国产色91在线| 成人激情小说乱人伦| 欧美色中文字幕| 午夜国产精品一区| 一区二区三区视频在线播放| 国产偷国产偷精品高清尤物| 成人av电影免费在线播放| 久久综合九色综合久99| 亚洲精品国产第一综合99久久 | aa国产精品| 国产精品国产三级国产普通话99| 99视频国产精品| 欧美一二三区在线观看| 九九视频精品免费| 欧美性猛片xxxx免费看久爱| 亚洲夂夂婷婷色拍ww47| 色综合久久综合网欧美综合网| 日韩欧美你懂的| 国产成人免费视频一区| 欧美日韩国产高清一区二区三区 | 欧美激情资源网| 91天堂素人约啪| 久久久久国产精品人| 97aⅴ精品视频一二三区| 日韩精品一区二区三区在线观看| 国产精品99久久久久久宅男| 欧美日韩在线电影| 精品在线观看免费| 91精品国产色综合久久 | 3751色影院一区二区三区| 麻豆久久久久久久| 欧美三级日本三级少妇99| 极品少妇xxxx精品少妇偷拍| 欧美日韩精品系列| 成人免费视频app| 久久久噜噜噜久久人人看| 成人av电影在线| 国产精品久久毛片av大全日韩| 怡红院精品视频在线观看极品| 亚洲精品videosex极品| 鲁鲁狠狠狠7777一区二区| 午夜精品久久久久久久久久 | 日日摸夜夜添夜夜添国产精品| 色婷婷久久久综合中文字幕| 久久电影网电视剧免费观看| 在线综合+亚洲+欧美中文字幕| 国产91综合网| 欧美激情在线观看视频免费| 亚洲国产精品久久久久婷婷老年| 亚洲国产综合色| 欧美日韩国产一区二区三区地区| 成人福利视频网站| 1000部国产精品成人观看| 久久亚洲国产精品日日av夜夜| 精品在线观看免费| 国产喷白浆一区二区三区| 一本一本久久a久久精品综合妖精| 日韩精品成人一区二区在线| 日韩精品在线一区二区| 欧美午夜精彩| 青青青爽久久午夜综合久久午夜 | 激情图片小说一区| 久久精品人人做| 久久av二区| 99久久精品国产精品久久| 亚洲精选视频免费看| 欧美日韩午夜在线视频| 欧美三级不卡| 日本成人在线网站| 国产色婷婷亚洲99精品小说| 久久久综合网| 97久久精品人人澡人人爽| 午夜精品福利一区二区蜜股av| 日韩一二三区不卡| 午夜在线一区二区| 99久久精品免费看国产免费软件| 亚洲国产日韩一级| 久久婷婷国产综合国色天香| 久久综合影音| 欧美日韩精品免费观看| 麻豆久久一区二区| 亚洲人成网站在线| 精品久久人人做人人爱| 久久久久久久久久久一区| 欧美午夜不卡| 国产a区久久久| 日欧美一区二区| 一区在线观看免费| 日韩三级视频在线看| 亚洲一区二区精品在线| 91在线国产福利| 精品中文字幕一区二区小辣椒 | 国产一区二区三区电影在线观看| 中文字幕在线免费不卡| 7777精品久久久大香线蕉| 亚洲综合好骚| 午夜日韩视频| 不卡欧美aaaaa| 国产在线不卡视频| 亚洲三级电影网站| 国产欧美视频在线观看| 日韩欧美在线综合网| 欧美精品xxxxbbbb| 久久婷婷麻豆| 亚洲一区在线免费| 国产一区二区三区四区hd| 成人爽a毛片一区二区免费| 精品在线播放免费| 免费在线观看一区二区三区| 亚洲午夜影视影院在线观看| 日韩一区欧美小说| 国产精品久久久久久一区二区三区 | 色哟哟国产精品| 久久久xxx| 可以免费看不卡的av网站| 99亚洲精品| 91久久久久| 国产精品免费一区二区三区观看| 在线不卡欧美| 日韩视频在线观看国产| 激情国产一区| 最新日韩av|