Java集合笔记

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 node(int index) {
    // assert isElementIndex(index);
    if (index < (size >> 1)) {
        Node x = first;
        for (int i = 0; i < index; i++)
          x = x.next; return x;
     } else {
      Node 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[] tab; Node 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 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)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 getNode(int hash, Object key) {
      Node[] tab; Node 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)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 s;
        //计算hash key值
        int hash = hash(key);
        //通过hash key值计算出存入Segment的位置
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment)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 node = tryLock() ? null :
    scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
    HashEntry[] tab = table;
    int index = (tab.length - 1) & hash;
    HashEntry first = entryAt(tab, index);
    for (HashEntry 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(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 extends AbstractMap
    implements ConcurrentMap, Serializable {
    transient volatile Node[] table;
}

static class Node implements Map.Entry {
        final int hash;
        final K key;
        volatile V val;
        volatile Node 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 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 parent;
    // split comparator and comparable paths
    Comparator 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 k = (Comparable) 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 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 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 k = (Comparable) key;
    Entry 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 p = getEntry(key); return (p==null ? null : p.value);
}

successor

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

successor就是中序遍历

LinkedHashMap

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

    void afterNodeAccess(Node e) { // move node to last         
    LinkedHashMap.Entry last;
         // 如果定义了accessOrder,那么就保证最近访问节点放到最后
          if (accessOrder && (last = tail) != e) { //将尾指针赋值给last
  //b 为前一个元素 ,a为后一个元素             
LinkedHashMap.Entry p = (LinkedHashMap.Entry)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 first;
// 如果定义了溢出规则,则执行相应的溢出
    if (evict && (first = head) != null && removeEldestEntry(fir
st)) {
K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

afterNodeRemoval

void afterNodeRemoval(Node e) { // unlink // 从链表中移除节点
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)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集合笔记)