JavaHashMap源碼是什么

本篇內(nèi)容主要講解“Java HashMap源碼是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Java HashMap源碼是什么”吧!

讓客戶滿意是我們工作的目標,不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領域值得信任、有價值的長期合作伙伴,公司提供的服務項目有:空間域名、虛擬空間、營銷軟件、網(wǎng)站建設、石泉網(wǎng)站維護、網(wǎng)站推廣。

簽名(signature)

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

可以看到HashMap繼承了

  • 標記接口Cloneable,用于表明HashMap對象會重寫java.lang.Object#clone()方法,HashMap實現(xiàn)的是淺拷貝(shallow copy)。

  • 標記接口Serializable,用于表明HashMap對象可以被序列化

比較有意思的是,HashMap同時繼承了抽象類AbstractMap與接口Map,因為抽象類AbstractMap的簽名為

public abstract class AbstractMap<K,V> implements Map<K,V>

Stack Overfloooow上解釋到:

在語法層面繼承接口Map是多余的,這么做僅僅是為了讓閱讀代碼的人明確知道HashMap是屬于Map體系的,起到了文檔的作用

AbstractMap相當于個輔助類,Map的一些操作這里面已經(jīng)提供了默認實現(xiàn),后面具體的子類如果沒有特殊行為,可直接使用AbstractMap提供的實現(xiàn)。

Cloneable接口

<code>It's evil, don't use it. </code>

Cloneable這個接口設計的非常不好,最致命的一點是它里面竟然沒有clone方法,也就是說我們自己寫的類完全可以實現(xiàn)這個接口的同時不重寫clone方法。

關于Cloneable的不足,大家可以去看看《Effective Java》一書的作者給出的理由,在所給鏈接的文章里,Josh Bloch也會講如何實現(xiàn)深拷貝比較好,我這里就不在贅述了。

Map接口

在Eclipse中的outline面板可以看到Map接口里面包含以下成員方法與內(nèi)部類:

Java HashMap源碼是什么
Map_field_method

可以看到,這里的成員方法不外乎是“增刪改查”,這也反映了我們編寫程序時,一定是以“數(shù)據(jù)”為導向的。

在上篇文章講了Map雖然并不是Collection,但是它提供了三種“集合視角”(collection views),與下面三個方法一一對應:

  • Set<K> keySet(),提供key的集合視角

  • Collection<V> values(),提供value的集合視角

  • Set<Map.Entry<K, V>> entrySet(),提供key-value序?qū)Φ募弦暯?,這里用內(nèi)部類Map.Entry表示序?qū)?/p>

AbstractMap抽象類

AbstractMapMap中的方法提供了一個基本實現(xiàn),減少了實現(xiàn)Map接口的工作量。

舉例來說:

如果要實現(xiàn)個不可變(unmodifiable)的map,那么只需繼承AbstractMap,然后實現(xiàn)其entrySet方法,這個方法返回的set不支持add與remove,同時這個set的迭代器(iterator)不支持remove操作即可。

相反,如果要實現(xiàn)個可變(modifiable)的map,首先繼承AbstractMap,然后重寫(override)AbstractMap的put方法,同時實現(xiàn)entrySet所返回set的迭代器的remove方法即可。

設計理念(design concept)

哈希表(hash table)

HashMap是一種基于哈希表(hash table)實現(xiàn)的map,哈希表(也叫關聯(lián)數(shù)組)一種通用的數(shù)據(jù)結(jié)構(gòu),大多數(shù)的現(xiàn)代語言都原生支持,其概念也比較簡單:key經(jīng)過hash函數(shù)作用后得到一個槽(buckets或slots)的索引(index),槽中保存著我們想要獲取的值,如下圖所示

Java HashMap源碼是什么
hash table demo

很容易想到,一些不同的key經(jīng)過同一hash函數(shù)后可能產(chǎn)生相同的索引,也就是產(chǎn)生了沖突,這是在所難免的。
所以利用哈希表這種數(shù)據(jù)結(jié)構(gòu)實現(xiàn)具體類時,需要:

  • 設計個好的hash函數(shù),使沖突盡可能的減少

  • 其次是需要解決發(fā)生沖突后如何處理。

后面會重點介紹HashMap是如何解決這兩個問題的。

HashMap的一些特點

  • 線程非安全,并且允許key與value都為null值,HashTable與之相反,為線程安全,key與value都不允許null值。

  • 不保證其內(nèi)部元素的順序,而且隨著時間的推移,同一元素的位置也可能改變(resize的情況)

  • put、get操作的時間復雜度為O(1)。

  • 遍歷其集合視角的時間復雜度與其容量(capacity,槽的個數(shù))和現(xiàn)有元素的大?。╡ntry的個數(shù))成正比,所以如果遍歷的性能要求很高, 不要把capactiy設置的過高或把平衡因子(load  factor,當entry數(shù)大于capacity*loadFactor時,會進行resize,reside會導致key進行rehash)設置的過 低。

  • 由于HashMap是線程非安全的,這也就是意味著如果多個線程同時對一hashmap的集合試圖做迭代時有結(jié)構(gòu)的上改變(添加、刪除entry,只改變entry的value的值不算結(jié)構(gòu)改變),那么會報ConcurrentModificationException,專業(yè)術(shù)語叫fail-fast,盡早報錯對于多線程程序來說是很有必要的。

  • Map m = Collections.synchronizedMap(new HashMap(...)); 通過這種方式可以得到一個線程安全的map。

源碼剖析

首先從構(gòu)造函數(shù)開始講,HashMap遵循集合框架的約束,提供了一個參數(shù)為空的構(gòu)造函數(shù)與有一個參數(shù)且參數(shù)類型為Map的構(gòu)造函數(shù)。除此之外,還提供了兩個構(gòu)造函數(shù),用于設置HashMap的容量(capacity)與平衡因子(loadFactor)。

public HashMap(int initialCapacity, float loadFactor) {     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(); } public HashMap(int initialCapacity) {     this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() {     this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }  從代碼上可以看到,容量與平衡因子都有個默認值,并且容量有個***值  /** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;

可以看到,默認的平衡因子為0.75,這是權(quán)衡了時間復雜度與空間復雜度之后的***取值(JDK說是***的),過高的因子會降低存儲空間但是查找(lookup,包括HashMap中的put與get方法)的時間就會增加。

這里比較奇怪的是問題:容量必須為2的指數(shù)倍(默認為16),這是為什么呢?解答這個問題,需要了解HashMap中哈希函數(shù)的設計原理。

哈希函數(shù)的設計原理

/**   * Retrieve object hash code and applies a supplemental hash function to the   * result hash, which defends against poor quality hash functions.  This is   * critical because HashMap uses power-of-two length hash tables, that   * otherwise encounter collisions for hashCodes that do not differ   * in lower bits. Note: Null keys always map to hash 0, thus index 0.   */ final int hash(Object k) {      int h = hashSeed;      if (0 != h && k instanceof String) {          return sun.misc.Hashing.stringHash42((String) k);      }      h ^= k.hashCode();      // This function ensures that hashCodes that differ only by      // constant multiples at each bit position have a bounded      // number of collisions (approximately 8 at default load factor).      h ^= (h >>> 20) ^ (h >>> 12);      return h ^ (h >>> 7) ^ (h >>> 4); } /**   * Returns index for hash code h.   */ static int indexFor(int h, int length) {      // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";      return h & (length-1); }

看到這么多位操作,是不是覺得暈頭轉(zhuǎn)向了呢,還是搞清楚原理就行了,畢竟位操作速度是很快的,不能因為不好理解就不用了。

網(wǎng)上說這個問題的也比較多,我這里根據(jù)自己的理解,盡量做到通俗易懂。

在哈希表容量(也就是buckets或slots大小)為length的情況下,為了使每個key都能在沖突最小的情況下映射到[0,length)(注意是左閉右開區(qū)間)的索引(index)內(nèi),一般有兩種做法:

  1. 讓length為素數(shù),然后用hashCode(key) mod length的方法得到索引

  2. 讓length為2的指數(shù)倍,然后用hashCode(key) & (length-1)的方法得到索引

HashTable用的是方法1,HashMap用的是方法2。

因為本篇主題講的是HashMap,所以關于方法1為什么要用素數(shù),我這里不想過多介紹,大家可以看這里。

重點說說方法2的情況,方法2其實也比較好理解:

因為length為2的指數(shù)倍,所以length-1所對應的二進制位都為1,然后在與hashCode(key)做與運算,即可得到[0,length)內(nèi)的索引

但是這里有個問題,如果hashCode(key)的大于length的值,而且hashCode(key)的二進制位的低位變化不大,那么沖突就會很多,舉個例子:

Java中對象的哈希值都32位整數(shù),而HashMap默認大小為16,那么有兩個對象那么的哈希值分別為:0xABAB00000xBABA0000,它們的后幾位都是一樣,那么與16異或后得到結(jié)果應該也是一樣的,也就是產(chǎn)生了沖突。

造成沖突的原因關鍵在于16限制了只能用低位來計算,高位直接舍棄了,所以我們需要額外的哈希函數(shù)而不只是簡單的對象的hashCode方法了。

具體來說,就是HashMap中hash函數(shù)干的事了

首先有個隨機的hashSeed,來降低沖突發(fā)生的幾率

然后如果是字符串,用了sun.misc.Hashing.stringHash42((String) k);來獲取索引值

***,通過一系列無符號右移操作,來把高位與低位進行異或操作,來降低沖突發(fā)生的幾率

右移的偏移量20,12,7,4是怎么來的呢?因為Java中對象的哈希值都是32位的,所以這幾個數(shù)應該就是把高位與低位做異或運算,至于這幾個數(shù)是如何選取的,就不清楚了,網(wǎng)上搜了半天也沒統(tǒng)一且讓人信服的說法,大家可以參考下面幾個鏈接:

  • http://stackoverflow.com/questions/7922019/openjdks-rehashing-mechanism/7922219#7922219

  • http://stackoverflow.com/questions/9335169/understanding-strange-java-hash-function/9336103#9336103

  • http://stackoverflow.com/questions/14453163/can-anybody-explain-how-java-design-hashmaps-hash-function/14479945#14479945

HashMap.Entry

HashMap中存放的是HashMap.Entry對象,它繼承自Map.Entry,其比較重要的是構(gòu)造函數(shù)

static class Entry<K,V> implements Map.Entry<K,V> {     final K key;     V value;     Entry<K,V> next;     int hash;     Entry(int h, K k, V v, Entry<K,V> n) {         value = v;         next = n;         key = k;         hash = h;     }     // setter, getter, equals, toString 方法省略     public final int hashCode() {         //用key的hash值與上value的hash值作為Entry的hash值         return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());     }     /**      * This method is invoked whenever the value in an entry is      * overwritten by an invocation of put(k,v) for a key k that's already      * in the HashMap.      */     void recordAccess(HashMap<K,V> m) {     }     /**      * This method is invoked whenever the entry is      * removed from the table.      */     void recordRemoval(HashMap<K,V> m) {     } }

可以看到,Entry實現(xiàn)了單向鏈表的功能,用next成員變量來級連起來。

介紹完Entry對象,下面要說一個比較重要的成員變量

/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
//HashMap內(nèi)部維護了一個為數(shù)組類型的Entry變量table,用來保存添加進來的Entry對象
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

你也許會疑問,Entry不是單向鏈表嘛,怎么這里又需要個數(shù)組類型的table呢?

我翻了下之前的算法書,其實這是解決沖突的一個方式:鏈地址法(開散列法),效果如下:

Java HashMap源碼是什么
鏈地址法處理沖突得到的散列表

就是相同索引值的Entry,會以單向鏈表的形式存在

鏈地址法的可視化

網(wǎng)上找到個很好的網(wǎng)站,用來可視化各種常見的算法,很棒。瞬間覺得國外大學比國內(nèi)的強不知多少倍。

下面的鏈接可以模仿哈希表采用鏈地址法解決沖突,大家可以自己去玩玩

  • https://www.cs.usfca.edu/~galles/visualization/OpenHash.html

get操作

get操作相比put操作簡單,所以先介紹get操作

public V get(Object key) {     //單獨處理key為null的情況     if (key == null)         return getForNullKey();     Entry<K,V> entry = getEntry(key);     return null == entry ? null : entry.getValue(); } private V getForNullKey() {     if (size == 0) {         return null;     }     //key為null的Entry用于放在table[0]中,但是在table[0]沖突鏈中的Entry的key不一定為null     //所以需要遍歷沖突鏈,查找key是否存在     for (Entry<K,V> e = table[0]; e != null; e = e.next) {         if (e.key == null)             return e.value;     }     return null; } final Entry<K,V> getEntry(Object key) {     if (size == 0) {         return null;     }     int hash = (key == null) ? 0 : hash(key);     //首先定位到索引在table中的位置     //然后遍歷沖突鏈,查找key是否存在     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; }

put操作(含update操作)

因為put操作有可能需要對HashMap進行resize,所以實現(xiàn)略復雜些

private void inflateTable(int toSize) {     //輔助函數(shù),用于填充HashMap到指定的capacity     // Find a power of 2 >= toSize     int capacity = roundUpToPowerOf2(toSize);     //threshold為resize的閾值,超過后HashMap會進行resize,內(nèi)容的entry會進行rehash     threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);     table = new Entry[capacity];     initHashSeedAsNeeded(capacity); } /** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. */ public V put(K key, V value) {     if (table == EMPTY_TABLE) {         inflateTable(threshold);     }     if (key == null)         return putForNullKey(value);     int hash = hash(key);     int i = indexFor(hash, table.length);     //這里的循環(huán)是關鍵     //當新增的key所對應的索引i,對應table[i]中已經(jīng)有值時,進入循環(huán)體     for (Entry<K,V> e = table[i]; e != null; e = e.next) {         Object k;         //判斷是否存在本次插入的key,如果存在用本次的value替換之前oldValue,相當于update操作         //并返回之前的oldValue         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {             V oldValue = e.value;             e.value = value;             e.recordAccess(this);             return oldValue;         }     }     //如果本次新增key之前不存在于HashMap中,modCount加1,說明結(jié)構(gòu)改變了     modCount++;     addEntry(hash, key, value, i);     return null; } void addEntry(int hash, K key, V value, int bucketIndex) {     //如果增加一個元素會后,HashMap的大小超過閾值,需要resize     if ((size >= threshold) && (null != table[bucketIndex])) {         //增加的幅度是之前的1倍         resize(2 * table.length);         hash = (null != key) ? hash(key) : 0;         bucketIndex = indexFor(hash, table.length);     }     createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) {     //首先得到該索引處的沖突鏈Entries,有可能為null,不為null     Entry<K,V> e = table[bucketIndex];     //然后把新的Entry添加到?jīng)_突鏈的開頭,也就是說,后插入的反而在前面(***次還真沒看明白)     //需要注意的是table[bucketIndex]本身并不存儲節(jié)點信息,     //它就相當于是單向鏈表的頭指針,數(shù)據(jù)都存放在沖突鏈中。     table[bucketIndex] = new Entry<>(hash, key, value, e);     size++; } //下面看看HashMap是如何進行resize,廬山真面目就要揭曉了 void resize(int newCapacity) {     Entry[] oldTable = table;     int oldCapacity = oldTable.length;     //如果已經(jīng)達到***容量,那么就直接返回     if (oldCapacity == MAXIMUM_CAPACITY) {         threshold = Integer.MAX_VALUE;         return;     }     Entry[] newTable = new Entry[newCapacity];     //initHashSeedAsNeeded(newCapacity)的返回值決定了是否需要重新計算Entry的hash值     transfer(newTable, initHashSeedAsNeeded(newCapacity));     table = newTable;     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } /** * Transfers all entries from current table to newTable. */ void transfer(Entry[] newTable, boolean rehash) {     int newCapacity = newTable.length;     //遍歷當前的table,將里面的元素添加到新的newTable中     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);             e.next = newTable[i];             //***這兩句用了與put放過相同的技巧             //將后插入的反而在前面             newTable[i] = e;             e = next;         }     } } /** * Initialize the hashing mask value. We defer initialization until we * really need it. */ final boolean initHashSeedAsNeeded(int capacity) {     boolean currentAltHashing = hashSeed != 0;     boolean useAltHashing = sun.misc.VM.isBooted() &&             (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);     //這里說明了,在hashSeed不為0或滿足useAltHash時,會重算Entry的hash值     //至于useAltHashing的作用可以參考下面的鏈接     // http://stackoverflow.com/questions/29918624/what-is-the-use-of-holder-class-in-hashmap     boolean switching = currentAltHashing ^ useAltHashing;     if (switching) {         hashSeed = useAltHashing             ? sun.misc.Hashing.randomHashSeed(this)             : 0;     }     return switching; }

remove操作

public V remove(Object key) {     Entry<K,V> e = removeEntryForKey(key);     //可以看到刪除的key如果存在,就返回其所對應的value     return (e == null ? null : e.value); } final Entry<K,V> removeEntryForKey(Object key) {     if (size == 0) {         return null;     }     int hash = (key == null) ? 0 : hash(key);     int i = indexFor(hash, table.length);     //這里用了兩個Entry對象,相當于兩個指針,為的是防治沖突鏈發(fā)生斷裂的情況     //這里的思路就是一般的單向鏈表的刪除思路     Entry<K,V> prev = table[i];     Entry<K,V> e = prev;     //當table[i]中存在沖突鏈時,開始遍歷里面的元素     while (e != null) {         Entry<K,V> next = e.next;         Object k;         if (e.hash == hash &&             ((k = e.key) == key || (key != null && key.equals(k)))) {             modCount++;             size--;             if (prev == e) //當沖突鏈只有一個Entry時                 table[i] = next;             else                 prev.next = next;             e.recordRemoval(this);             return e;         }         prev = e;         e = next;     }     return e; }

到現(xiàn)在為止,HashMap的增刪改查都介紹完了。
一般而言,認為HashMap的這四種操作時間復雜度為O(1),因為它hash函數(shù)性質(zhì)較好,保證了沖突發(fā)生的幾率較小。

HashMap的序列化

介紹到這里,基本上算是把HashMap中一些核心的點講完了,但還有個比較嚴重的問題:保存Entry的table數(shù)組為transient的,也就是說在進行序列化時,并不會包含該成員,這是為什么呢?

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

為了解答這個問題,我們需要明確下面事實:

  • Object.hashCode方法對于一個類的兩個實例返回的是不同的哈希值

我們可以試想下面的場景:

我們在機器A上算出對象A的哈希值與索引,然后把它插入到HashMap中,然后把該HashMap序列化后,在機器B上重新算對象的哈希值與索引,這與機器A上算出的是不一樣的,所以我們在機器B上get對象A時,會得到錯誤的結(jié)果。

所以說,當序列化一個HashMap對象時,保存Entry的table是不需要序列化進來的,因為它在另一臺機器上是錯誤的。

因為這個原因,HashMap重現(xiàn)了writeObjectreadObject 方法

private void writeObject(java.io.ObjectOutputStream s)     throws IOException {     // Write out the threshold, loadfactor, and any hidden stuff     s.defaultWriteObject();      // Write out number of buckets     if (table==EMPTY_TABLE) {         s.writeInt(roundUpToPowerOf2(threshold));     } else {        s.writeInt(table.length);     }      // Write out size (number of Mappings)     s.writeInt(size);      // Write out keys and values (alternating)     if (size > 0) {         for(Map.Entry<K,V> e : entrySet0()) {             s.writeObject(e.getKey());             s.writeObject(e.getValue());         }     } }  private static final long serialVersionUID = 362498820763181265L;  private void readObject(java.io.ObjectInputStream s)      throws IOException, ClassNotFoundException {     // Read in the threshold (ignored), loadfactor, and any hidden stuff     s.defaultReadObject();     if (loadFactor <= 0 || Float.isNaN(loadFactor)) {         throw new InvalidObjectException("Illegal load factor: " +                                            loadFactor);     }      // set other fields that need values     table = (Entry<K,V>[]) EMPTY_TABLE;      // Read in number of buckets     s.readInt(); // ignored.      // Read number of mappings     int mappings = s.readInt();     if (mappings < 0)         throw new InvalidObjectException("Illegal mappings count: " +                                            mappings);      // capacity chosen by number of mappings and desired load (if >= 0.25)     int capacity = (int) Math.min(                 mappings * Math.min(1 / loadFactor, 4.0f),                 // we have limits...                 HashMap.MAXIMUM_CAPACITY);      // allocate the bucket array;     if (mappings > 0) {         inflateTable(capacity);     } else {         threshold = capacity;     }      init();  // Give subclass a chance to do its thing.      // Read the keys and values, and put the mappings in the HashMap     for (int i = 0; i < mappings; i++) {         K key = (K) s.readObject();         V value = (V) s.readObject();         putForCreate(key, value);     } } private void putForCreate(K key, V value) {     int hash = null == key ? 0 : hash(key);     int i = indexFor(hash, table.length);      /**      * Look for preexisting entry for key.  This will never happen for      * clone or deserialize.  It will only happen for construction if the      * input Map is a sorted map whose ordering is inconsistent w/ equals.      */     for (Entry<K,V> e = table[i]; e != null; e = e.next) {         Object k;         if (e.hash == hash &&             ((k = e.key) == key || (key != null && key.equals(k)))) {             e.value = value;             return;         }     }      createEntry(hash, key, value, i); }

簡單來說,在序列化時,針對Entry的key與value分別單獨序列化,當反序列化時,再單獨處理即可。

到此,相信大家對“Java HashMap源碼是什么”有了更深的了解,不妨來實際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關內(nèi)容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!

新聞標題:JavaHashMap源碼是什么
標題鏈接:http://muchs.cn/article2/jpgioc.html

成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供用戶體驗、微信小程序、標簽優(yōu)化、企業(yè)建站、App開發(fā)、App設計

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

手機網(wǎng)站建設