java容器浅析

181 阅读9分钟

容器总览关系

container.JPG

List族

ArrayList

  1. 特点-适用场景

    查找快,非尾部插入和删除需要移动插入处后续的元素,所以插入慢

    适用于频繁访问,非尾部修改少的场景

  2. 实现原理

    数组

    非线程安全

    是连续空间,内存空间不足时,会主动扩容

    无参数构造,初始化容量为0,当真正添加元素时候容量为10

    动态扩容过程: 当添加元素时候发现容量已满,则动态扩容为当前容量的1.5倍

    动态扩容采用 Arrays.copyOf(elementData, newCapacity) 实现,返回新的数组,但是拷贝是浅拷贝, 两个数组里面的内容都是指向同一个引用的

Vector

  1. 特点-适用场景

    和ArrayList一样,并适用于多线程访问场景

  2. 实现原理

    数组

    线程安全,安全采用synchronized整个数组的方式,所以加锁范围广,并发效率低

    无参构造初始化容量为10

    是连续空间,内存空间不足时,会主动扩容

    动态扩容过程: 初始化可以传入增量大小,当容量已满,扩容为 当前容量 + 增量大小,如果没有传递增量大小,则 扩容为 当前容量的2倍

    动态扩容同样采用: Arrays.copyOf(elementData, newCapacity),返回新的数组,但是拷贝是浅拷贝, 两个数组里面的内容都是指向同一个引用的

LinkedList 双向链表

  1. 特点-适用场景 插入快,删除快,查找慢,适用于插入多,访问少的

    删除直接修改元素记录的地址值即可,不需要大量移动元素,所以删除快

  2. 实现原理

    链表头节点、链表尾节点和节点数据对象 LinkedList是离散空间所以不需要主动扩容

    单链表结构特点: 头结点 尾结点 数据对象data中有next指向

    双向链表: 头结点 尾结点 数据对象data中有 pre和next指向

    循环链表: 结构是单链表,特点是无头结点,尾结点指向第一个节点对象

自我构建链表对象类似:


public class LinkList {
    public Node root = null; //定义头节点,默认为空
    public Node tail;  //定义尾节点
    public int length; //定义链表长度
    public class Node{ //声明节点类
        public int data; //定义数据对象
        public Node next = null; //定义指针,默认为空
        public Node(int data) {this.data = data; } //重写构造方法,初始化节点
}

Stack

  1. 特点-适用场景

    先进后出

    线程安全

  2. 实现原理

    继承自vector,具有vector的特点和原理特性

Set族

元素存储和取出顺序是无序的

HashSet

  1. 特点-适用场景

元素存储和取出顺序是无序的 没有角标 不能进行精确地定位 不允许重复的元素 内容相同的只保留一个 它是通过复写equals()方法来判断是否的重复元素

  1. 实现原理

    内部通过使用hashmap来实现内容的存储,key值即为add进去的对象,value值为固定对象



         public HashSet() {
105          map = new HashMap<>();
106      }

         public boolean add(E e) {
219          return map.put(e, PRESENT)==null;
220      }
221  
222      /**
223       * Removes the specified element from this set if it is present.
224       * More formally, removes an element <tt>e</tt> such that
225       * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>,
226       * if this set contains such an element.  Returns <tt>true</tt> if
227       * this set contained the element (or equivalently, if this set
228       * changed as a result of the call).  (This set will not contain the
229       * element once the call returns.)
230       *
231       * @param o object to be removed from this set, if present
232       * @return <tt>true</tt> if the set contained the specified element
233       */
234      public boolean remove(Object o) {
235          return map.remove(o)==PRESENT;
236      }

TreeSet

  1. 特点-适用场景

    元素按照comparable排序,插入到内部二叉树的TreeMap中

    能够对元素按照某种规则进行排序。

    特点:排序和唯一

    底层数据结构是红黑树(红黑树是一种自平衡的二叉树),保证了元素的排序和唯一性

    内部实现排序,也可以自定义排序规则

    自然排序、比较器排序保证元素排序

    根据比较的返回值是否是0来保证元素唯一性

  2. 实现原理

    内部封装了treemap,value对象是固定对象

3      public TreeSet() {
124          this(new TreeMap<E,Object>());
125      }
126  
127      /**
128       * Constructs a new, empty tree set, sorted according to the specified
129       * comparator.  All elements inserted into the set must be <i>mutually
130       * comparable</i> by the specified comparator: {@code comparator.compare(e1,
131       * e2)} must not throw a {@code ClassCastException} for any elements
132       * {@code e1} and {@code e2} in the set.  If the user attempts to add
133       * an element to the set that violates this constraint, the
134       * {@code add} call will throw a {@code ClassCastException}.
135       *
136       * @param comparator the comparator that will be used to order this set.
137       *        If {@code null}, the {@linkplain Comparable natural
138       *        ordering} of the elements will be used.
139       */
140      public TreeSet(Comparator<? super E> comparator) {
141          this(new TreeMap<>(comparator));
142      }
143  

map族

HashTable

  1. 特点-适用场景

    多线程的hashmap,由于效率不高,当前一般使用ConcurrentHashMap锁分段的并发容器代替

  2. 实现原理

    数组 + 链表

    线程安全

    HashTable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,所以锁的效率不高

HashMap

  1. 特点-适用场景

是无序的

允许null值做key和value

扩容系数即负载因子为0.75 , 初始大小 16 ,是否需要扩容取决于添加元素时候,判断当前存储大小是否已经达到3/4的容量,达到则需要扩容一倍

HashMap 的容量值都是 2^n

容器初始化大小 16,最大容量2^30(1位符号位,最多左移30位,左移计算容量,0到31位,1位符号位则是0到30位)

  1. 实现原理

    数组 + 链表 (单链表,next记录下一个元素位置)

    非线程安全

    容量大小需要为大小2^n ,因为hash计算规则原因

    利用hash值确定index方式:

    n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n

    hash值生成方式:

    hash是 32位的,但是容器的个数n是2^4 四位 ~ 30位的,为了让高位也参与其中,所以采用高位和hashcode异或的形式得到 hash值

       static final int hash(Object key) { 
         int h;
        return (key == null) ? 0 :(h = key.hashCode()) ^ (h >>> 16);
}

String的hashCode实现
public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
这里有个数字 31 ,为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:
  ①、31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。
  ②、31可以被 JVM 优化,31 * i = (i << 5) - i。因为移位运算比乘法运行更快更省性能。

源码添加元素时候的判断时序

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
625                     boolean evict) {
626          Node<K,V>[] tab; Node<K,V> p; int n, i;
//首先查看是否需要扩容
627          if ((tab = table) == null || (n = tab.length) == 0)
628              n = (tab = resize()).length;
//然后查找hashcode为此值的 index是否存在,不存在才newNode节点
629          if ((p = tab[i = (n - 1) & hash]) == null)
630              tab[i] = newNode(hash, key, value, null);
631          else {
632              Node<K,V> e; K k;
//如果存在,则判断hash值,key值均相同的情况下,则返回此值
633              if (p.hash == hash &&
634                  ((k = p.key) == key || (key != null && key.equals(k))))
635                  e = p;
//如果hash值相同,可以值不同则发下是TreeNode则插入
636              else if (p instanceof TreeNode)
637                  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
638              else {
//否则 生成节点,插入到数组下的链表中,即hash值相同,key值不同的情况
639                  for (int binCount = 0; ; ++binCount) {
640                      if ((e = p.next) == null) {
641                          p.next = newNode(hash, key, value, null);
642                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
643                              treeifyBin(tab, hash);
644                          break;
645                      }
646                      if (e.hash == hash &&
647                          ((k = e.key) == key || (key != null && key.equals(k))))
648                          break;
649                      p = e;
650                  }
651              }
//对于放置相同key值的情况,返回相同的对象,但是用新的value替换旧的value
652              if (e != null) { // existing mapping for key
653                  V oldValue = e.value;
654                  if (!onlyIfAbsent || oldValue == null)
655                      e.value = value;
656                  afterNodeAccess(e);
657                  return oldValue;
658              }
659          }
660          ++modCount;
661          if (++size > threshold)
662              resize();
663          afterNodeInsertion(evict);
664          return null;
665      }


TreeMap

  1. 特点-适用场景

    可根据comparable排序的map集合

    无序,不允许重复(无序指元素顺序与添加顺序不一致)

    TreeMap集合默认会对键进行排序,所以键必须实现自然排序和定制排序中的一种

    底层使用的数据结构是二叉树

  2. 实现原理

    内部元素是以 entry这种key value形式的结构,并且entry有 left 和 right元素,按照comparable比较决定插入左节点 还是 右节点

    可以接收传入SortedMap对象 或者 comparator接口对象实现比较排序

    使用方式:


Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
            public int compare(Student p1, Student p2) {
                return p1.score > p2.score ? -1 : 1;
            }
        });
        map.put(new Student("Tom", 77), 1);
        map.put(new Student("Bob", 66), 2);
        map.put(new Student("Lily", 99), 3);
        for (Student key : map.keySet()) {
            System.out.println(key);
        }

LinkedHashMap

  1. 特点-适用场景

    继承自hashmap,具有hashmap的特点,并且对于添加进去的元素按照先后顺序排序

  2. 实现原理

    HashMap+双向链表

    初始化容量和装载因子(默认为0.75)。accessOrder为是否按照访问顺序排列,如果是,则被访问的entry将排列到最后一个,默认是false,即按照输入顺序排列,即遍历输出的顺序与put顺序一致

    accessOrder默认是false,get操作不会改变顺序,由put决定,如何设置true则get方法将元素移动到队列尾部,容量超过从头部移除

    hashmap对应的数组被修改为下面的对象:

		
		 static class Entry<K,V> extends HashMap.Node<K,V> {
193          Entry<K,V> before, after;
194          Entry(int hash, K key, V value, Node<K,V> next) {
195              super(hash, key, value, next);
196          }
197      }

   /**
202       * The head (eldest) of the doubly linked list.
203       */
204      transient LinkedHashMap.Entry<K,V> head;
205  
206      /**
207       * The tail (youngest) of the doubly linked list.
208       */
209      transient LinkedHashMap.Entry<K,V> tail;

利用hashmap中的增加、删除、访问的钩子方法,对于每次新增加数组元素的时候,建立元素的前后引用关系

  void afterNodeAccess(Node<K,V> p) { }
1767      void afterNodeInsertion(boolean evict) { }
1768      void afterNodeRemoval(Node<K,V> p) { }

每次newNode方法,使用LinkedHashMap.Entry:


Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
   LinkedHashMap.Entry<K,V> p =
257              new LinkedHashMap.Entry<K,V>(hash, key, value, e);

   static class Entry<K,V> extends HashMap.Node<K,V> {
193          Entry<K,V> before, after;
194          Entry(int hash, K key, V value, Node<K,V> next) {
195              super(hash, key, value, next);
196          }
197      }

ConcurrentHashMap

  1. 特点-适用场景

    具有hashmap的特点,并且多线程安全,使用锁分段技术

  2. 实现原理

    (1)

    在JDK1.7中,ConcurrentHashMap使用的是segment锁,是继承自ReentrantLock,一旦初始化完成,就不能再改变了,但是segment数组中的HashEntry数组是可以改变的

    结构是 segment锁数组 + hashentry数组 + 链表数据

    定位一个元素的过程需要进行两次 Hash 操作。 第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部

17jdk.JPG

segment含义 segment数组,segment结构- hashentry数组,每个hashentry是个链表结构

segment结构继承自 ReentrantLock

一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构,

一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,

每个 Segment 守护者一个 HashEntry 数组里的元素, 当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。

(2) jdk1.8中的ConcurrentHashMap中废弃了segment锁,直接使用了数组元素,数组中的每个元素都可以作为一个锁,

在元素中没有值的情况下,可以直接通过CAS操作来设值,同时保证并发安全,

如果元素里面已经存在值的话,那么就使用synchronized关键字对元素加锁,再进行之后的hash冲突处理

重点:synchronized根据操作的链表对象进行加锁,来保证同一个锁中 操作相同的对象

LruCache

  1. 特点-适用场景

    内部封装了 LinkedHashMap

  2. 实现原理

    当序列达到设置的内存上限时, 丢弃序列中最近最少使用的元素

    //初始容量 0, 扩容系数),0.75,排序方式 true表示按照get访问排序,false按照 put插入顺序排序

    this.maxSize = maxSize;初始化传入 最大容量大小

    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);

Queue队列

  1. 特点-适用场景

    先进先出 队列是一种受限的数据结构,插入操作只能从一端操作,这一端叫作队尾;而移除操作也只能从另一端操作,这一端叫作队头

  2. 实现原理

    队列的实现有两种方式:数组和链表

参考链接

数组:

www.cnblogs.com/kuoAT/p/677… blog.csdn.net/dietime1943… blog.csdn.net/qq_42592994…

set

blog.csdn.net/Piconjo/art…

zhuanlan.zhihu.com/p/105958354

map

blog.csdn.net/hm_135/arti… cloud.tencent.com/developer/a… blog.csdn.net/baidu_37107… www.liaoxuefeng.com/wiki/125259… www.jianshu.com/p/181df0893… blog.csdn.net/wxgxgp/arti… 锁分段 www.infoq.cn/article/Con…

blog.csdn.net/LO_YUN/arti…

www.cnblogs.com/jelly12345/…

queue

data.biancheng.net/view/109.ht…

cs.xieyonghui.com/java/java-q…