美文网首页
Java集合笔记

Java集合笔记

作者: 小李同学今天博学了吗 | 来源:发表于2020-08-18 21:37 被阅读0次

    Java集合分为四类分别为

    Map:无序,具有key-value映射的集合,里面有一个entry类,里面有key和value
    Set:无序,不可重复的集合
    List:有序,可重复的集合
    Queue:队列,先进先出

    Map与Set集合、List集合的关系
    1.与Set的关系,如果把Map中的所有key放到一起看,他就组成了一个set(无序且不可重复),其中hashSet的底层就是用hashMap实现的

    2.与List的关系:如果把Map的所有value放到一起来看,他就组成了一个list(根据索引查找、可重复)

    HashMap月HashSet的区别

    1.接口不同:HashMap实现Map接口,HashSet实现Set接口
    2.存储对象不同:HashMap存储键值对,HashSet存储一个对象
    3.增加数据方式不同:HashMap使用put,HashSet使用add
    4.hashcode对象不同:hashmap是key,hashSet使成员对象
    5.hashmap较快,hashset较慢
    6.hashset的底层是使用hashmap来实现的,就是add添加的value设置为键,hashmap的value为一个final static的object对象

    数组和集合的区别

    1.数组定长,集合不定长,数组实现的集合有自动扩容机制,集合可以保存具有映射关系的数据
    2.数组可以是基本数据类型,也可以是对象。集合只能存储对象

    ArrayList

    已数组实现的集合,初始大小为10,当超出限制时会增加50%的容量,在进行扩容、删除时使用System.arrayCopy()来进行数据的而移动,大量移动性能不太高
    自动扩容的代码:

    private void ensureCapacityInternal(int minCapacity) {
              if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); }
              ensureExplicitCapacity(minCapacity);
    }
    private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
        // overflow-conscious code
          if (minCapacity - elementData.length > 0) 
          grow(minCapacity);
        }
    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            // 扩展为原来的1.5倍
            int newCapacity = oldCapacity + (oldCapacity >> 1); 
            // 如果扩为1.5倍还不满足需求,直接扩为需求值
            if (newCapacity - minCapacity < 0)
                  newCapacity = minCapacity;
          if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
           // minCapacity is usually close to size, so this is a win:
          elementData = Arrays.copyOf(elementData, newCapacity); 
    }
    

    LinkedList

    已双向链表实现,容量没有空间限制,需要用指针来操作
    里面主要的方法是node,set和get都利用了这个方法

    Node<E> node(int index) {
        // assert isElementIndex(index);
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
              x = x.next; return x;
         } else {
          Node<E> x = last;
            for (int i = size - 1; i > index; i--)
              x = x.prev;
         return x;
          }
     }
    
    

    这个方法将链表的时间复杂度从O(n)降到O(n/ 2)

    HashMap

    hashMap是由数组(默认为16)、链表、红黑树构成的集合,他存储的是具有映射关系的数据,其键不重复,loadfactor = 0.75,属于非同步的

    1.工作原理:当put和get时,对key的hashcode做hash,即(key的hashcode高16位异或低16位),之后&上bucket的长度-1,(hash % length == (length -1) & hash),通过hash得到bucket的位置,取余的操作是使bucket平衡,下一步确定值通过equals来确定值

    2.hash实现: h = (h.hashCode() ^ h >>>16);这样的目的是为了减少碰撞,

    3.hashMap超过负载因子(load factor * 容量)大小时,将会resize一个原来长度的hashMap,就是bucket变为原来的两倍

    put函数的代码

    public V put(K key, V value) {
        // 对key的hashCode()做hash
        return putVal(hash(key), key, value, false, true);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
              Node<K,V>[] tab; Node<K,V> p; int n, i;
              // tab为空则创建
              if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;
            // 计算index,并对null做处理
            if ((p = tab[i = (n - 1) & hash]) == null)
              tab[i] = newNode(hash, key, value, null);
          else {
              Node<K,V> e; K k;
              // 节点存在
            if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k)
    )))
            e = p;
            // 该链为树
            else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    
    // 该链为链表 
          else {
          for (int binCount = 0; ; ++binCount) { 
    
      if ((e = p.next) == null) {
              p.next = newNode(hash, key, value, null);
              if (binCount >= TREEIFY_THRESHOLD - 1) // -1
                  treeifyBin(tab, hash);
              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;
    // 超过load factor*current capacity,resize
     if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    
    

    整体的过程就是先判断数组是否为空,如果为空则创建,之后判断数组中是否存在该元素,如果不存在就存入数组,如果数组存在就判断是否为树,是树就调用putTreeval方法,如果是链表就经过for循环得到得到插入的位置,如果插入的个数达到转化为红黑树的数量就存为二叉树,如果已经存在则取代以前的值

    get函数的代码

    final Node<K,V> getNode(int hash, Object key) {
          Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
          // 直接命中
          if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first; // 未命中
                
                if ((e = first.next) != null) { // 在树中get
                if (first instanceof TreeNode)
                        return ((TreeNode<K,V>)first).getTreeNode(hash,
                    // 在链表中get 
                      do {
                          if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.e
                      return e;
                  } while ((e = e.next) != null);
        }
    }
        return null;
    }
    

    总的来说流程就是先找该bucket位置的,如果不是再去树种找,看是否是树,如果不是则去链表中找,如何后面都没有则返回null;

    concurrentHashMap

    为了解决hashmap在多线程下使用put操作会引起死循环
    hashTable使用synchronized来保证线程安全的,但是在线程竞争激烈的情况下hashtable的效率低下,当一个线程访问hashtable的同步方法时,其他线程会进入阻塞状态

    分段锁:
    jdk1.7:

    ConcurrrentHashmap是segment数组和hashEntry数组构成,setgment继承自reentrantLock,其中一个segmen里面包含一个 hashEntry数组,一个hashEntry数组里面包含多个hashEntry

    HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。(简书其他作者的理解)

    put的流程:得到k的hash计算值 & segment数组,得到segment,之后在segment里面的put是hash值得到在segment里面的位置,然后为调用put方法,其中put方法调用tryLock方法,如果获取锁失败则地调用scanAndForput方法来就是循环调用tryLock,如果超过了我们设置的最大循环次数,就执行lock方法,阻塞等待,直到获取segment锁为止

    public V put(K key, V value) {
            Segment<K,V> s;
            //计算hash key值
            int hash = hash(key);
            //通过hash key值计算出存入Segment的位置
            int j = (hash >>> segmentShift) & segmentMask;
            if ((s = (Segment<K,V>)UNSAFE.getObject          
                 (segments, (j << SSHIFT) + SBASE)) == null) 
                //初始化Segment
                s = ensureSegment(j);
             //添加
            return s.put(key, hash, value, false);
    }
    
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        //segment操作加锁,使用尝试获取锁方式。如果获取失败,进入scanAndLockForPut方法
        HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
        V oldValue;
        try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
            K k;
            if ((k = e.key) == key ||
                (e.hash == hash && key.equals(k))) {
                oldValue = e.value;
                if (!onlyIfAbsent) {
                e.value = value;
                ++modCount;
                }
                break;
            }
            e = e.next;
            }
            else {
            if (node != null)
                node.setNext(first);
            else
                node = new HashEntry<K,V>(hash, key, value, first);
            int c = count + 1;
            if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        //扩容, 这是后续做解释
                rehash(node);
            else
                setEntryAt(tab, index, node);
            ++modCount;
            count = c;
            oldValue = null;
            break;
            }
        }
        } finally {
        //释放锁
        unlock();
        }
        return oldValue;
    }
    
    
    

    get方法类似

    总结:get方法不需要加锁,因为get不会改变map中数据

    jdk1.8
    采用Node + CAS + synchronizeds设计

    public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
        implements ConcurrentMap<K,V>, Serializable {
        transient volatile Node<K,V>[] table;
    }
    
    static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            volatile V val;
            volatile Node<K,V> next;
    }
    
    

    总结:

    Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低。
    ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。
    应该根据具体的应用场景选择合适的HashMap。

    TreeMap

    这个是一个有顺序的map,可以根据key大小进行排序
    put函数:如果存在的话,old value被替换,如果不存在,则新添一个节点,然后做红黑树的平衡操作

    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        // 如果该节点存在,则替换值直接返
        if (cpr != null) {
        do {
          parent = t;
          cmp = cpr.compare(key, t.key);
           if (cmp < 0)
          t = t.left; 
         else if (cmp > 0)
          t = t.right;
    else
            return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
    do {
         parent = t;
          cmp = k.compareTo(t.key); 
          if (cmp < 0)
          t = t.left; else if (cmp > 0)
          t = t.right;
        else
          return t.setValue(value);
        } while (t != null);
    }
    // 如果该节点未存在,则新建
    Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0)
    parent.left = e;
    else
    parent.right = e;
    // 红黑树平衡调整 fixAfterInsertion(e); size++;
    modCount++;
    
    return null;
    }
    

    get函数

    final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
    // 按照二叉树搜索的方式进行搜索,搜到返回
     while (p != null) {
    int cmp = k.compareTo(p.key); 
    if (cmp < 0)
        p = p.left; 
    else if (cmp > 0)
        p = p.right;
     else
                return p;
    }
        return null;
    }
    public V get(Object key) {
    Entry<K,V> p = getEntry(key); return (p==null ? null : p.value);
    }
    

    successor

    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { 
          if (t == null)
              return null;
          else if (t.right != null) {
                  // 有右子树的节点,后继节点就是右子树的“最左节点” 
                // 因为“最左子树”是右子树的最小节点
                Entry<K,V> p = t.right;
                while (p.left != null)
                p = p.left; 
                return p;
          } else {
                        // 如果右子树为空,则寻找当前节点所在左子树的第一个祖先节点               
                        // 因为左子树找完了,根据LDR该D了
                Entry<K,V> p = t.parent;
              Entry<K,V> ch = t;
            // 保证左子树
              while (p != null && ch == p.right) {
                ch = p;
                p = p.parent; 
          }
          return p; 
        }
    }
    

    successor就是中序遍历

    LinkedHashMap

    继承于HashMap的集合类,可以维持插入时的顺序和访问顺序
    1.afterNodeAccess

        void afterNodeAccess(Node<K,V> e) { // move node to last         
        LinkedHashMap.Entry<K,V> last;
             // 如果定义了accessOrder,那么就保证最近访问节点放到最后
              if (accessOrder && (last = tail) != e) { //将尾指针赋值给last
      //b 为前一个元素 ,a为后一个元素             
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    //使访问后的元素为尾元素,即after为null
    p.after = null; 
     //如果访问元素的前一个元素为null,那么他的后一个元素就为头指针
        if (b == null)
          head = a;
        else
          b.after = a; // 将前一个元素的下一个值指定为他的后一个元素 
         if (a != null) //  如果后一个元素不为空,那么后一个元素的前一个元素为b(访        
        问元素的前一个元素)
        a.before = b; 
        else
        last = b; //如果是空,则将last设置为b,即访问元素的前一个为尾指针
        if (last == null)
        head = p; //如果last的空,这样的情况是链表只存在一个数据,但是数据被访 
       问 了,则将head 设置成最后那个数据
        else {
       //否则将p的前指针指向尾指针,将尾指针的下一个元素指向访问的元素
      p.before = last; 
      last.after = p; 
     
    }
    tail = p;
    ++modCount;
       }
     }
    

    afterNodeInsertion

    void afterNodeInsertion(boolean evict) { // possibly remove elde
    st
    LinkedHashMap.Entry<K,V> first;
    // 如果定义了溢出规则,则执行相应的溢出
        if (evict && (first = head) != null && removeEldestEntry(fir
    st)) {
    K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
    

    afterNodeRemoval

    void afterNodeRemoval(Node<K,V> e) { // unlink // 从链表中移除节点
    LinkedHashMap.Entry<K,V> p =
    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.before = p.after = null;
        if (b == null)
          head = a;
      else
          b.after = a; 
       if (a == null)
          tail = b;
       else
        a.before = b;
    }
    
    

    相关文章

      网友评论

          本文标题:Java集合笔记

          本文链接:https://www.haomeiwen.com/subject/avjkjktx.html