HastTable源码分析

本文详细分析了HashTable的源码,包括其内部数据结构、关键参数如初始容量和加载因子的影响,以及核心操作如get、put、remove和rehash的工作原理。还探讨了线程安全性、扩容策略以及不同遍历方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

概述

HashTable存储的键值对,它的key和value都不可以为Null。

为了能成功的存储健值对,做为key的对象必须实现hashCode()和equals()方法。

           HashTable实例有两个参数影响其性能,初始化容量和加载因子。初始容量是哈希表创建时的容量,注意HashTable的状态为Open。在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。加载因子 是对哈希表在其容量自动增加之前可以达到多满的一个尺度。初始容量和加载因子这两个参数只是对该实现的提示。关于何时以及是否调用 rehash 方法的具体细节则依赖于该实现。

       通常,默认加载因子(.75)在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。

       初始容量主要控制空间消耗与执行 rehash 操作所需要的时间损耗之间的平衡。如果初始容量大于 Hashtable 所包含的最大条目数除以加载因子,则永远 不会发生 rehash 操作。但是,将初始容量设置太高可能会浪费空间。

         如果很多条目要存储在一个 Hashtable 中,那么与根据需要执行自动 rehashing 操作来增大表的容量的做法相比,使用足够大的初始容量创建哈希表或许可以更有效地插入条目。

          HashTable是线程安全的,但是如果在不需要同步的情况下使用HashMap,在高并发同步的情况下使用ConcurrentHashMap。

源码分析

全局变量

HashTable的内部数据载体是一个Entry数组

 private transient Entry<?,?>[] table;

HashTable中Entry对象的总数量

 private transient int count;

table是否要扩容的阈值,当Entry实体的数码超过了这个阈值,就需要对HashTable进行扩容。threshold=capcity*loadFactor

 private int threshold;

加载因子loadFactory。。threshold=capcity*loadFactor,loadFactory意味着HashTable的容量越小,但是桶中的条目数量更多,这意味着查询条目需要花更多的时间,典型的时间换空间。如果loadFactor越小,则意味着要创建的桶的容量更大,相应的查询桶中条目就可以花更少的时间,典型的空间换时间。

private float loadFactor;

Hashtable内部结构变化的次数。包括添加、删除和扩容,都会导致Hashtable内部结构变化

构造方法

四个构造函数

    /**
	 创建一个空的包含初始容量和指定加载因子的HashTable
     */
    public Hashtable(int initialCapacity, float loadFactor) {
		//初始容量不能为负数
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

       /**
        参数为初始化容量
        */
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    /**
        容量为11,默认加载因子0.75
    */
    public Hashtable() {
        this(11, 0.75f);
    }

    /**
    根据原有集合创建Hashtable
    /*
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

get()函数

获取key对应的Value,如果没有就返回为Null。

 public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        //获取key对应的hash值
        int hash = key.hashCode();
		/**
        (hash & 0x7FFFFFFF)对桶的长度桶的容量tab.length取余,定位key具体位于哪个桶
		0x7FFFFFFF = 2的32次方-1=1111111111111111111111111111111
       */
        int index = (hash & 0x7FFFFFFF) % tab.length;
		//遍历桶下的链表,查询对应的key的对象
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

containsKey()函数

containsKey()函数的实现基本与get()函数一致,都是通过查询key对应的具体的桶,然后遍历该桶的链表。

public synchronized boolean containsKey(Object key) {
        Entry<?,?> tab[] = table;
		//获取key对应的hash值
        int hash = key.hashCode();
		//(hash & 0x7FFFFFFF)对桶的长度取余,定位数据具体位于哪个桶
        int index = (hash & 0x7FFFFFFF) % tab.length;
		//遍历桶下的链表,查询对应的节点
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return true;
            }
        }
        return false;
    }

 containsValue()函数
  根据值获取对应的keys,多个key对应一个value的情况,返回的key有多个。这个方法查询的代价比containsKey
  昂贵得多,因为要遍历所有的桶,同时要遍历桶的链表一一比较值。

 public boolean containsValue(Object value) {
        return contains(value);
    }

/**
    同步调用
*/ 
public synchronized boolean contains(Object value) {
        if (value == null) {
            throw new NullPointerException();
        }
	
        Entry<?,?> tab[] = table;
		//遍历所有的桶
        for (int i = tab.length ; i-- > 0 ;) {
		//遍历桶下所有的链表
            for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
                if (e.value.equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }

put()函数

添加新的Entry,首先定位到桶,然后再遍历桶的链表,如果存在则直接返回,不存在则添加。

/**
		添加键值对,key和value都不允许为空值
	*/
	public synchronized V put(K key, V value) {
        //确保净值对的值不能为空
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
		//获取key的hash值,如果key为空,则抛出空指针异常,所以key也不能为空
        int hash = key.hashCode();
		
		//获取hash值为具体的哪个桶
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
		//遍历桶中的链表,如果已经含有该key,则返回key对应的value,不再加入
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

	/**
	添加entry
	*/
	private void addEntry(int hash, K key, V value, int index) {
	   //因为添加了Entry,则Hashtable的结构体修改,则modeCount加1
        modCount++;

        Entry<?,?> tab[] = table;
		//如果桶中所有的Entry总数超过了阈值,则需要对桶进行扩容
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
			//获取key对应的hash值
            hash = key.hashCode();
			
			//hash值对应的哪个桶
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
		//加Entry加入到链表中
        tab[index] = new Entry<>(hash, key, value, e);
		//节点数+1
        count++;
    }

putAll()函数

该函数将传入的Map对象复制到新的Hashtable,循环调用以上函数put()

public synchronized void putAll(Map<? extends K, ? extends V> t) {
      //循环遍历Map,将键值对复制到新的Hashtable
        for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
            put(e.getKey(), e.getValue());
    }

rehash()函数

	/**
	扩容
	*/
	protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
		//新的容量扩大2倍+1
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
		//当前容量已经为最大值,则不再扩容
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
		//如果新的容量大于最大容量的值,则将新的容量设置为最大容量的值
            newCapacity = MAX_ARRAY_SIZE;
        }
		//创建新的tab
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
		//设置阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;

        for (int i = oldCapacity ; i-- > 0 ;) {
		//遍历当前桶数组
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;
				//在这里会重新计算e位于哪个桶中
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
				//将e插入到首个节点的前面(该首节点与e位于相同的桶中)
                e.next = (Entry<K,V>)newMap[index];
				//即将e设置为首节点
                newMap[index] = e;
            }
        }
    }

 

1、首先先将当期容量扩大2倍+1

2、如果新的容量超过了最大值MAX_ARRAY_SIZE,而且当前容量已经超过了最大值MAX_ARRAY_SIZE,则不再扩容直接返回

3、如果新的容量超过了最大值MAX_ARRAY_SIZE,而且当前容量小于最大值MAX_ARRAY_SIZE,则将新的容量设置为最大值MAX_ARRAY_SIZE

4、设置新的容量阈值为Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1)

5、创建新的桶数组tab[],同时将旧桶的数据全部复制到新桶中,而且每个hash值要重新计算为于新桶的哪个位置,这个代价比较昂贵。

看一下容量扩张后的数组复制的一个实例

由实例我们也能得知,扩张后,hash值的位于哪个桶是重新计算的,同时新加入的节点是在原来的链表的首节点前插入,从这里我们还可以得知Hashtable的顺序是变化的。

remove()函数

 /**
	 删除节点
	 */
    public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
		//获取key的hash值
        int hash = key.hashCode();
		//计算索引,(hash & 0x7FFFFFFF)对tab的容量取余,
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
		//遍历该桶下的链表
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
				//删除当前节点
                if (prev != null) {
				/**
				删除节点的上一节点prev不为空,
				将prev的下一节点设置为删除节点e的下一节点
				*/
                    prev.next = e.next;
                } else {
				/**
				删除节点的上一节点为空,则说明删除的是首节点
				将删除节点的下一节点设置为首节点
				*/
                    tab[index] = e.next;
                }
				//节点数减1
                count--;
                V oldValue = e.value;
                e.value = null;
				//返回删除节点的value
                return oldValue;
            }
        }
		//未找到删除的节点,返回为空
        return null;
    }

clear()函数

清空Hashtable

	 //清除Hashtable,
	 public synchronized void clear() {
        Entry<?,?> tab[] = table;
        modCount++;
		//遍历Hashtable所有的桶,将所有的桶清空
        for (int index = tab.length; --index >= 0; )
            tab[index] = null;
        count = 0;
    }

clone()

克隆,这里的克隆方法是浅复制,Hashtable的结构体会被复制,键值对如果是指向型对象是不复制的。

public synchronized Object clone() {
        try {
           //复制Hashtable结构体
            Hashtable<?,?> t = (Hashtable<?,?>)super.clone();
            t.table = new Entry<?,?>[table.length];
            for (int i = table.length ; i-- > 0 ; ) {
                t.table[i] = (table[i] != null)
           //复制链表结构体
                    ? (Entry<?,?>) table[i].clone() : null;
            }
            t.keySet = null;
            t.entrySet = null;
            t.values = null;
            t.modCount = 0;
            return t;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

结合一个实例看下Hashtable的克隆

package com.leadbank.faas.income.test;

import java.util.Hashtable;

public class HashTableTest {

	static Hashtable<String, Person> table = new Hashtable<String, Person>();

	static class Person{
		private String name;
		private int age;
		public String getName() {
			return name;
		}
		public void setName(String name) {
			this.name = name;
		} 
		public int getAge() {
			return age;
		}
		public void setAge(int age) {
			this.age = age;
		}
		@Override
		public String toString() {
			return "Person [name=" + name + ", age=" + age + "]";
		}
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Person p = new Person();
		p.setName("张三20");
		p.setAge(20);
		table.put("p", p );
		
		System.out.println("修改前table="+table.toString());
		Hashtable<String, Person> copyTab = (Hashtable) table.clone();
		System.out.println("修改前copyTab="+copyTab);
		
		Person p2 = (Person) table.get("p");
		p2.setAge(21);
		p2.setName("张三21");
		System.out.println("修改后table="+table.toString());
		System.out.println("修改后copyTab="+copyTab);
	}

}

执行结果:

修改前table={p=Person [name=张三20, age=20]}
修改前copyTab={p=Person [name=张三20, age=20]}
修改后table={p=Person [name=张三21, age=21]}
修改后copyTab={p=Person [name=张三21, age=21]}

如果是深度克隆,修改后coptyTable的Person信息不应该跟随table的信息而变化。

 

Enumerator

枚举器,elements()、keys()、entrySet()的迭代器功能都需要使用该枚举器Enumerator

 /**
	 枚举生成器。该枚举生成器实现了Enumeration和Iterator接口,禁用
	 迭代器方法创建单独的实例。这对于避免无意中增加通过枚举接口
	 授予用户能力是很有必要的。
     */
    private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
	//当前Hashtable对象的桶
        Entry<?,?>[] table = Hashtable.this.table;
	//桶的当前容量
        int index = table.length;
        Entry<?,?> entry;
    //遍历期间,上一次的值
        Entry<?,?> lastReturned;
	//0:返回key值,1:返回value,2:返回键和值
        int type;

        /**
		 true ->使用迭代器
		 false->使用枚举
		 迭代器比枚举类多了删除功能,而且方法名也做了修改,
         */
        boolean iterator;

        /**
		 modCount:Hashtable内部结构变更的次数,
		 expectedModCount:迭代器创建时候初始化的modCount的值。如果Hashtable结构因为
		 除使用迭代器以外的方法使Hashtable内部结构发生了变动(modCount变大),这个
		 时候modCount与expectedModCount是不相等的,一旦不相等,则这个迭代器就失效了,
		 不能再使用了。其实这个就是java集合的fail-fast失败机制。当多个线程对同一个
		 集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过
		 iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A
		 访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件 
		 如果 expectedModCount
         */
        protected int expectedModCount = modCount;

        Enumerator(int type, boolean iterator) {
            this.type = type;
            this.iterator = iterator;
        }

		/**
			判断该tab是否还有更多的节点。该方法每调用一次,
			就会判断当前节点是否为空,如果为空,就会上移一个桶,
			直到遍历完所有的桶(i==0),桶的遍历从最大的桶开始。
			而对应到具体桶的链表的节点的位置移动则交给nexElement()方法。
		*/
        public boolean hasMoreElements() {
            Entry<?,?> e = entry;
            int i = index;
            Entry<?,?>[] t = table;
            /* Use locals for faster loop iteration */
            while (e == null && i > 0) {
                e = t[--i];
            }
			//将当前节点e赋值给共有元素entry,以便nextElement()方法使用
            entry = e;
			//将当前桶位置赋值给共用index,以便nextElement()方法使用
            index = i;
			//当前节点为空,则表示没有更多的非空节点
            return e != null;
        }

        @SuppressWarnings("unchecked")
		/**
		获取下一个节点元素
		*/
        public T nextElement() {
		//将共有元素entry赋值给当前节点et
            Entry<?,?> et = entry;
		//当前迭代器指针所在桶的位置
            int i = index;
            Entry<?,?>[] t = table;
			/**
			如果当前节点为空,则获取上一个桶。如果外部
			调用了hasMoreElements()方法来判断,则这个基本
			不会发生。
			*/
            while (et == null && i > 0) {
                et = t[--i];
            }
			//将当前节点et赋值给共有元素entry
            entry = et;
			将当前桶位置赋值给共用index
            index = i;
			//如果et不为空
            if (et != null) {
                Entry<?,?> e = lastReturned = entry;
                entry = e.next;//下一个节点
				//根据type返回键还是值,还是键值对
                return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
            }
            throw new NoSuchElementException("Hashtable Enumerator");
        }

        // 迭代器方法,内部调用枚举的方法
        public boolean hasNext() {
            return hasMoreElements();
        }

		 // 迭代器方法,内部调用枚举的方法
        public T next() {
		//fail-fast事件发生
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            return nextElement();
        }

		/**
		 删除当前节点lastReturned,即刚刚调用next()方法的节点,
		使用remove()必须调用过next()方法来获取最近返回的节点
		*/
        public void remove() {
		/**
		如果用户使用的枚举,不是迭代器,则抛出异常,迭代器
		才拥有remove方法,枚举没有remove()方法
		*/
            if (!iterator)
                throw new UnsupportedOperationException();
		//如果最近返回的lastReturned为空,则没有可以删除的节点
            if (lastReturned == null)
                throw new IllegalStateException("Hashtable Enumerator");
		//fail-fast事件发生,当前迭代器失效不可用
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

		//删除的时候以Hashtable对象为锁
            synchronized(Hashtable.this) {
                Entry<?,?>[] tab = Hashtable.this.table;
				//删除节点为于哪个桶中
                int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;

                @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>)tab[index];
				//遍历对应桶的链表,删除节点。
                for(Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
                    if (e == lastReturned) {
                        modCount++;
                        expectedModCount++;
                        if (prev == null)
                            tab[index] = e.next;
                        else
                            prev.next = e.next;
                        count--;
                        lastReturned = null;
                        return;
                    }
                }
                throw new ConcurrentModificationException();
            }
        }
    }

keySet()

创建KeySet,KeySet拥有迭代器iterator,该迭代器iterator通过上文提到Enumerator来实现,然后用户就可以通过迭代器来访问tab内部数据了。当然KeySet本身也可以不通过迭代器,调用外部方法来实现查询和删除等功能的。

 public Set<K> keySet() {
        if (keySet == null)
		//创建线程安全的结合Set
            keySet = Collections.synchronizedSet(new KeySet(), this);
        return keySet;
    }

    private class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return getIterator(KEYS);
        }
        public int size() {
            return count;
        }
		//不同过迭代器,用外部的方法查询是否否包含该对象o
        public boolean contains(Object o) {
            return containsKey(o);
        }
		//不通过迭代器,调用外部的删除方法删除对象o
        public boolean remove(Object o) {
            return Hashtable.this.remove(o) != null;
        }
		//不通过迭代器,嗲用外部清除方法清除tab
        public void clear() {
            Hashtable.this.clear();
        }
    }

values()函数

该函数返回ValueCollection类型的value对象,该对象包含迭代器iterator,代器iterator通过上文提到Enumerator来实现,然后用户就可以通过迭代器来访问tab内部数据了。

 public Collection<V> values() {
        if (values==null)
            values = Collections.synchronizedCollection(new ValueCollection(),
                                                        this);
        return values;
    }

    private class ValueCollection extends AbstractCollection<V> {
        public Iterator<V> iterator() {
            return getIterator(VALUES);
        }
        public int size() {
            return count;
        }
        public boolean contains(Object o) {
            return containsValue(o);
        }
        public void clear() {
            Hashtable.this.clear();
        }
    }

最后看几个遍历集合的方法实例:

package com.leadbank.faas.income.test;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Consumer;

public class HashtableTest2 {

	static Hashtable<String, String> hashTable = new Hashtable<String, String>();
	public static void main(String[] args) {
		//声明BiConsumer类型的action
		hashTable.put("key1", "value1");
		hashTable.put("key2", "value2");
		hashTable.put("key3", "value3");
		
		//遍历key值方法
		//1、通过枚举Enumeration遍历集合的key
		System.out.println("-----------------枚举遍历key------------------------");
		Enumeration<String> keyNuneration = hashTable.keys();
		while(keyNuneration.hasMoreElements()){
			String key = keyNuneration.nextElement();
			System.out.println("key="+key);
		}
		
		//2、通过迭代器遍历集合的key
		System.out.println("-----------------迭代器遍历key------------------------");
		Set<String> keySet = hashTable.keySet();
		Iterator<String> keyIter = keySet.iterator();
		while(keyIter.hasNext()){
			String key = keyIter.next();
			System.out.println("key="+key);
		}
		
		//2.1、补充一下函数式编程方式
		System.out.println("-----------------函数式编程遍历key------------------------");
		Consumer<String> action = (key) -> System.out.println("key="+key);
		keySet.forEach(action);
		
		//遍历Value值方法
		//1、通过Enumeration遍历集合的Value值
		System.out.println("-----------------枚举遍历value------------------------");
		Enumeration<String> vEnumeration = hashTable.elements();	
		while(vEnumeration.hasMoreElements()){
			String value = vEnumeration.nextElement();
			System.out.println("value="+value);
		}
		
		//2、通过迭代器Iterator遍历集合集合的Value值
		System.out.println("-----------------迭代器遍历value------------------------");
		Collection<String> collection = hashTable.values();
		Iterator<String> collectionIter = collection.iterator();
		while(collectionIter.hasNext()){
			String value = collectionIter.next();
			System.out.println("value="+value);
		}
	
		//通过迭代器遍历集合的key-value
		System.out.println("-----------------迭代器遍历key-value------------------------");
		Set<Map.Entry<String, String>> set = hashTable.entrySet();
		Iterator<Entry<String, String>> kvIter = set.iterator();
		while(kvIter.hasNext()){
			Map.Entry<String, String> entry = kvIter.next();
			System.out.println("key="+entry.getKey()+"->value="+entry.getValue());
		}


		//2、补充一个函数式编程
		System.out.println("-----------------函数式编程遍历key-value------------------------");
		Consumer<Map.Entry<String, String>> biAction = (entry) ->{
			System.out.println("key="+entry.getKey()+"->value="+entry.getValue());
		};
		set.forEach(biAction);

	}

}

输出结果:

-----------------枚举遍历key------------------------
key=key3
key=key2
key=key1
-----------------迭代器遍历key------------------------
key=key3
key=key2
key=key1
-----------------函数式编程遍历key------------------------
key=key3
key=key2
key=key1
-----------------枚举遍历value------------------------
value=value3
value=value2
value=value1
-----------------迭代器遍历value------------------------
value=value3
value=value2
value=value1
-----------------迭代器遍历key-value------------------------
key=key3->value=value3
key=key2->value=value2
key=key1->value=value1
-----------------函数式编程遍历key-value------------------------
key=key3->value=value3
key=key2->value=value2
key=key1->value=value1
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值