Java-集合、Map-2-随笔

Set

set接口基本介绍
  • 无序(添加和取出的顺序不一致),没有索引
  • 不允许重复元素,所以最多包含一个null
  • 底层是HashMap<>();
接口和常用方法
  • 和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样
  • 遍历:可以用迭代器、for each、但是不能用索引的方式来获取,而且set接口也没有get(int index)方法
HashSet源码分析:
  1. HashSet 构造器:除了一个特殊的,其他底层都先创建了一个HashMap。

    public HashSet() {
            map = new HashMap<>();
        }
        
    public HashSet(Collection<? extends E> c) {
            map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
            addAll(c);
        }
        
    public HashSet(int initialCapacity, float loadFactor) {
            map = new HashMap<>(initialCapacity, loadFactor);
        }
        
    public HashSet(int initialCapacity) {
            map = new HashMap<>(initialCapacity);
        }
        
    /*
    Params:
    initialCapacity – the initial capacity of the hash map
    loadFactor – the load factor of the hash map
    dummy – ignored (distinguishes this constructor from other int, float constructor.)
    =================将次构造器与其他int,float构造器区别开、这个主要是用于支持LinkedHashSet=======
    */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
            map = new LinkedHashMap<>(initialCapacity, loadFactor);
        }
    
    
  2. add( ):添加元素

    整体思路:

    • 添加元素时,将element看作HashMap中的key,并用Object空对象PRESENT作为HashMap中的value占位符。先得到element的hashCode值,然后将此hashCode值转为HashMap的索引值hash,

    • 找到HashMap中的table,看对应的hash索引位置是否有元素

      • 如果没有,直接把新建一个Node,把<hash(element.hashCode()), element(K), PRESENT(value) >存储在此处

      • 如果该处存储的有某个对象ele1,那么继续后面的判断

        1. 首先需要比较hash是否相等,不相等,则直接判断下面的情况2,若相同,还需比较element对象地址和ele1对象地址是否相同,或者用ele1.equal(element)比较是否相等,若其中一个相同,则此次添加将无效,并返回element

          //这个条件满足的话,会插入失败。
          if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
          
        2. 若1没有满足,则检查这里存放ele1的节点是否是一个树节点,如果是的话,将element按照树的添加方法添加进去

          e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
          
        3. 若2也没有满足,则说明存放ele1的节点是个链中的一个节点,那么就循环的往后比较看ele1后面的各元素是否和要添加的元素element相等,如果相同,则放弃添加,如果都不同,则添加到最后。此处添加完,还需查记录当前节点是插入该链的第几个节点,若是该链插入此节点后,超过了8个节点 ( 注意是超过了8个,也就是说当前节点插进去之后,这个链变成了9个节点,才会考虑将该链转为红黑树 ) ,则判断是否需要将该链转换为红黑树:若HashMap.table为空或者长度小于64(HashMap.MIN_TREEIFY_CAPACITY)那么,只对HashMap.table进行resize()扩容即可,否则将该链变为红黑树。
          这里用8作为分界线是需要综合考虑空间和效率的均衡问题,当用红黑树时,每个节点占用的空间交大,但用单链时候,查询效率时线性n,但小于8的时候,也会很快。在理想条件下,一个链所能有的长度符合泊松分布,也就是某个链长度达到8时,其概率只有0.00000006,而若这种情况下,某个链长度还大于8了,那说名很大问题就是程序不够理想,把很多节点都放到一个链上了,此时为了查询效率不会下降,就舍弃空间问题,转为红黑树,源码中有一下注释。)。

          * usages with well-distributed user hashCodes, tree bins are
          * rarely used.  Ideally, under random hashCodes, the frequency of
          * nodes in bins follows a Poisson distribution
          * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
          * parameter of about 0.5 on average for the default resizing
          * threshold of 0.75, although with a large variance because of
          * resizing granularity. Ignoring variance, the expected
          * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
          * factorial(k)). The first values are:
          *
          * 0:    0.60653066
          * 1:    0.30326533
          * 2:    0.07581633
          * 3:    0.01263606
          * 4:    0.00157952
          * 5:    0.00015795
          * 6:    0.00001316
          * 7:    0.00000094
          * 8:    0.00000006
          * more: less than 1 in ten million
          
        4. 最后若添加成功了,则++size,++modCount,其中 ++modCount 表示该HashMap被修改过的次数加一,并根据新size判断是否需要resize()扩容:如:当超过临界值(threshold)12时,也就是添加第13个的元素的时候,将会扩容。

          ....
          ....
          
          ++modCount;
          if (++size > threshold)
          	resize();
          afterNodeInsertion(evict);
          return null;
          

    resize()扩容:

    • HashSet底层是HashMap,第一次添加的时候,table数组扩容到16,并将临界值(threshold)设置为12,其中装载因子设为0.75是因为即需要尽量少发生hash碰撞,又需要尽量不浪费空间,权衡而来的:

      static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //定义默认初始容量
      static final float DEFAULT_LOAD_FACTOR = 0.75f; //定义装载因子
      
      newCap = DEFAULT_INITIAL_CAPACITY; //第一次扩容时扩容为16
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //第一次扩容时将临界值转为16 * 0.75 = 12
      
    • 如果table数组使用到了临界值12,也就是放置完第13个元素的时候,就是会扩容到16 * 2 = 32,新的临界值依然是新容量的0.75为:32 * 0.75 = 24

      else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
      
    • 在JAVA8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认8),且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则继续采用数组扩容机制。

    //add函数进来,首先调了HashMap的put()方法;
    public boolean add(E e) {
        	//返回为空,则说明添加成功了,要不然返回一个需要添加的值e,说明未添加成功
            return map.put(e, PRESENT)==null;
        }
    
    //HastMap的put方法又调用了自己的putVal()方法:
    //其中传过来的key值是上层HashSet想要添加的element对象,值是HashSet的成员属性-->PRESENT
    //但这个present实际没什么意义,只是为了让HashSet能用上HashMap而进行的一个占位符,占位HashMap<K,V>中的 V
    //其定义如下:
    /*   Dummy value to associate with an Object in the backing Map
         private static final Object PRESENT = new Object();
    */
    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    //除此之外,我们还注意到hash(key),算得一个int类型得hash值,
    //其具体算法如下:
    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    
    
    //第一次进来得时候,因为tab = table是空的,所以先吧tab = resize了一下,而resize()做了什么呢,看下面
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
        	
        	//第一次往里面添加元素时,table为空,走这个地方
        	//table是HashMap的成员属性,	
            if ((tab = table) == null || (n = tab.length) == 0)
                //table第一次putVal得时候,是空的,所以在这里resize之后,
                //tab得长度经过默认初始化长度,成了16,且其阈值threshold = 0.75 * length = 12
                //0.75是设定 是权衡了  防止hash容易冲突 和 空间利用 而定下来的值
                //也就是第一次扩容的时候,扩容到空间为16,而扩容也只能是以2的倍数扩容,因为在下一行
                // p = tab[i = (n-1) & hash]时,这里其实是一个取模的位运算,但是需要保证n必须是2^n,
                // 相当于为了取模计算的快点,使得空间最开始就需要以2的倍数去扩容
                n = (tab = resize()).length;
        		// 令p指向 与想要放置节点 hash位置 相同的那个地方,如果p是空的,表示该位置没有存放过元素,
        		// 没存放过的话,在此新建一个节点,然后就跳到最后++modCount,然后返回就可以了
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
        
        		//如果在往table上hash的时候,冲突了,就分情况讨论判断了
            else {
                Node<K,V> e; K k;
                //条件1:走到这里的话,说名p指向的这个地方,不是空的,而是放过节点的,那么首先比较p这个节点,
                //与当前想要放置的节点,他们的key是否是同一个对象,或者他们的内容相同(equal返回true)
                if (p.hash == hash && 
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                //条件2:如果上面条件1的判等条件没进去,那么接下来判断p是不是一个红黑树节点,是的话,就将他插入红黑树
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                //条件3:如果上面的条件2也不是,那么意味着,这个节点跟p节点不同,而且p节点处也是一个链表
                //那么就把这个需要放置的节点挂到链表上
                else {
                    for (int binCount = 0; ; ++binCount) {
                        //如果p的下个节点e为空,直接放,放完判断容量到没到需要变成树(判断加入新节点后,整个
                        //链的长度到是否到达了TREEIFY_THRESHOLD,即到达了8(但其实仔细看的话,这时候判断大于8是指的当插
                        //入完新节点之后,该链表总共有9个元素的时候,开始尝试变成树),TREEIFY_THRESHOLD是一个final定义的int 8),
                        //而为什么这里要设置为8呢,因为当数量小于8的时候,链表查询效率高,而大于8的时候,红黑树效率高,
                        //最后判断完,就break,跳出for
                        
                        //需要注意的是,进入treeifyBin之后,不一定真的会变成树,因为在该函数内,还需要判断tab的长度是否达到了
                        //64(MIN_TREEIFY_CAPACITY),没到的话首先会扩容,否则才会树化
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        
                        //p的下个节点e不空,那么比较e的key和当前的key是不是一样,一样的话break,不一样的话继续往后找,
                        //直到走到链表的尾部
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
        	//放完node之后,再判断需不需要扩容
            if (++size > threshold)
                resize();
        	//这是一个空的函数,而起作用是为了以后其他类继承HashMap的时候,可以在最后做一些东西,比如使链表成为有序链表之类的
            afterNodeInsertion(evict);
        	//返回true,代表成果插入了
            return null;
        }
    
    
    // resize函数:
    
    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    
    
LinkedHashSet:
  1. 基本说明:

    • LinkedHashSet时HashSet的子类。

    • LinkedHashSet底层是一个 LinkedHashMap,LinkedHashMap底层又维护了一个数组+双向链表

      //HashSet里面:
      HashSet(int initialCapacity, float loadFactor, boolean dummy) {
          //因为1.HashSet拥有私有属性map(<E,Object>),而2.LinkedHashSet继承的HashSet,私有属性map需要是LinedkHashMap
          //所以在构造1的时候,map初始华为一个HashMap、而子类LinkedHashSet调用父类HashSet,初始化map为LinedHashMap的时候,
          //需要区分map到new出来什么对象,而dummy就是用来区分这个的。具体关系如下
          //
          //				HashSet: 属性map的类型 ------------> HashMap
          //					^							     ^
          //                  |(继承)							|(继承)
          //					|							     |
          //			LinkedHashSet: 属性map的类型 ------> LinkedHashMap)
              map = new LinkedHashMap<>(initialCapacity, loadFactor);
          }
      
      //LinkedHashSet
      public LinkedHashSet() {
              super(16, .75f, true);
          }
      
    • LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。

    • LinkedHashSet不允许添重复元素。

  2. 底层结构
    1

  3. add():添加元素(其实LinkedHashSet并没有显示的定义)

    1. 在LinkedHastSet中维护了一个hash表和双向链表(LinkedHashSet有head和tail )

    2. 每一个节点有pre和next属性,这样可以形成双向链表

    3. 在添加一个元素时,先求hash值,在求索引.,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样])
      tail.next = newElement!/简单指定
      newElement.pre = tail
      tail = newEelment;

    4. 这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致

    5. 第一次添加元素的时候,LinkedHashMap的table数组将扩容到16。

    6. *:LinkedHashMap中数组是HashMap N o d e [ ] , 但 存 放 的 节 点 类 型 是 L i n k e d H a s h M a p Node[],但存放的节点类型是LinkedHashMap Node[]LinkedHashMapEntry,说明LinkedHashMap E n t r y 与 H a s h M a p Entry与HashMap EntryHashMapNode存在某种继承或者实现关系,是一种多态)
      2

      //LinkedHashMap中有以下定义,可以看出来Entry是继承了HashMap的静态内部类:HashMap.Node<K,V>
      
      static class Entry<K,V> extends HashMap.Node<K,V> {
              Entry<K,V> before, after;
              Entry(int hash, K key, V value, Node<K,V> next) {
                  super(hash, key, value, next);
              }
          }
      

Map

Map接口和常用方法:

Map接口实现类的特点[很实用],JDK8

  1. Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value

  2. Map 中的key和 value可以是任何引用类型的数据,会封装到HashMap$Node
    对象中

  3. Map中的key 不允许重复,原因和HashSet 一样,前面分析过源码.

  4. Map 中的value可以重复

  5. Map的key可以为null, value也可以为null,注意key为null, 只能有一个,value为null,可以多个.

  6. 常用String类作为Map的key

  7. key 和 value之间存在单向一对一关系,即通过指定的key 总能找到对应的value

  8. 测试:

    import java.util.HashMap;
    
    public class Test06_HashMap {
        public static void main(String[] args) {
            HashMap hashMap = new HashMap();
    
            hashMap.put("ljh",7000_0000);
            System.out.println("hashMap = " + hashMap);
    
            hashMap.put("ljh",777_7777);
            System.out.println("hashMap = " + hashMap);
    
            hashMap.put("LJH",777_7777);
            System.out.println("hashMap = " + hashMap);
    
            hashMap.put(null,null);
            hashMap.put(null,1);
            hashMap.put("mo",null);
            System.out.println("hashMap = " + hashMap);
    
            System.out.println("hashMap.get(\"LJH\") = " + hashMap.get("LJH"));
        }
    }
    
    //输出
    //----------------------------------------------------------------------------
    hashMap = {ljh=70000000}
    hashMap = {ljh=7777777}
    hashMap = {ljh=7777777, LJH=7777777}
    hashMap = {null=1, mo=null, ljh=7777777, LJH=7777777}
    hashMap.get("LJH") = 7777777
    
    Process finished with exit code 0
    
HashMap(具体很多细节上面HashSet的时候已经提到)
  1. key不能重复,值可以重复,允许使用null键和null值。如果添加相同的key,则会覆盖原来的value(key不会替换,val会替换)
  2. k-v 最终存储的形式是 HashMap$ node = newNode(hash, key, value, null)。
  3. HashMap没有实现线程同步,因此是线程不安全的。
  4. k-v 为了方便程序员遍历,还会创建EntrySet集合,该集合存放的元素类型为Entry,而每一个Entry对象都有k,v,也可以有相应的getKey(),和getValue方法。 EntrySet<Entry<K, V>> 即 : transient Set<Map.Entry<K,V>> entrySet。EntrySet中定义的类型是Map.Entry,但实际还是指向的HashMap$Node,而且在HashMap中有如下定义:static class Node<K,V> implements Map.Entry<K,V>
  5. EntrySet其实可以理解成视图,在HashMap中,整一个entrySet方法和EntrySet类,主要是为了方便遍历,因为entrySet中存放的Entry(是一个接口),有getKey(),getValue()方法,方便拿到值,并且entrySet中存放的HashMap.Node类实现了Map.Entry类,并实现了getKey(),getValue()等方法。
  6. 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的,(JDK8 HashMap底层是数组+链表(红黑树))
  7. HashMap没有实现同步,因此是线程不安全的,方法没有做互斥(Synchronized)
底层机制及源码分析:

3

    • (K,V)是一个Node,实现了Map.Entry<K, V>,查看HashMap的源码可以看到。
    • JDK7.0的hasmap底层实现[数组+链表],jdk8.0底层[数组 + 链表 + 红黑树]
  1. 扩容机制:
    • HashMap底层维护了Node类型的数组table,默认为null
    • 当创建对象时,将加载因子(loadfactor)初始化为0.75.
    • 当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要打容。
    • 第1次添加,则需要扩容table容量为16,临界值(threshold)为12(loadFactor=0.75,16*loadFactor=12).
    • 以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍,即24,依次类推.
    • 在Java8中,如果一条链表的元素个数超过TREEIFY THRESHOLD(默认是8),并且table的大小> = MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)。

HashTable

基本介绍:
  1. 存放的元素是键值对: 即K-V
  2. hashtable的键和值都不能为null,否则会抛出NullPointerException
  3. Hashtable使用方法基本上和HashMap一样
  4. Hashtable是线程安全的(synchronized),hashMap 是线程不安全的
简单底层说明:
  1. 底层有数组,Hashtable$Entry[] 初始化大小为11
  2. 临界值 threashold 8 = 11*0.75
  3. 扩容:按照自己的扩容机制进行扩容 [ newCapacity = (oldCapacity << 1) + 1]
HashMap-Hashtable对比
版本线程安全(同步)效率允许null键null值?resize继承
HashMap1.2之后出来的不安全可以无参构造,初始默认0,第一次扩容将table扩容为16,后续每次扩容为原来2倍(原因与位运算快速取余有关),loadFactor为容量的0.75(而且到一个链上挂8个节点之后,在挂第9个节点时,当前这一条链会考虑转为红黑树(但也需要满足此时table容量大于等于64))HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>
Hashtable1.0之后出来的安全不可以无参默认table容量为11,每次扩容为原来容量的2倍+1,loadFactor为容量的0.75Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>

Properties

  1. Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形
    式来保存数据。
  2. 他的使用特点和Hashtable类似
  3. Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
  4. 说明:工作后xxx.properties 文件通常作为配置文件,这个知识点在IO流举例。
常用方法:
  • Collections.reverse(List):发转List中元素的顺序

  • Collections.shuffle(List):随机打乱

  • Collections.sort(List):自然排序

  • Collections.sort(List, new Comparator(){}):按comparator排序

  • Collections.swap(List, int i, int j):交换下表i,j的值

  • Collections.reverse(List):翻转表

  • Object Collections.max(Collection):自然排序情况下的最大值

  • Object Collections.max(Collection, new Comparator(){}):按comparator排序情况下的最大值

  • Object Collections.min(Collection):自然排序情况下的最小值

  • Object Collections.min(Collection, new Comparator(){}):按comparator排序情况下的最小值

  • int Collections.frequency(Collection, Object):返回指定集合中指定元素的出现次数

  • void Collections.copy(List dest, List src):将src中的内容复制到dest中

  • Boolean Collections.replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对向的所有旧值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值