Java基础 - java容器

  Java的容器是用来存放对象数据,根据不同的业务需求选择不同的容器类型(List、Set、Queue、Map)。
这里写图片描述
  通过上述的图可以了解到Java的容器大致分成两类map类和Collection类

  • Collection 对象集合
    • List 对象有顺序集合
    • Set 对象不重复集合
    • Queue 对象先进先出集合
  • Map 对象键值对集合

一、Collection 容器

1.1 List 容器

  List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,也就是说它是有顺序的,类似于Java的数组。和Set不同,List允许有相同的元素。

1.1.1 ArrayList

  基于数组实现的List类,它封装了一个动态的增长的、允许再分配的Object[]数组,相当于顺式存储。当向ArrayList中添加对象时,数组的大小也相应的改变。快速随即访问,你可以随即访问每个元素而不用考虑性能问题,通过调用get(i)方法来访问下标为i的数组元素。向其中添加对象速度慢,当你创建数组时并不能确定其容量,所以当改变这个数组时就必须在内存中做很多事情。操作其中对象的速度慢,当你要向数组中任意两个元素中间添加对象时,数组需要移动所有后面的对象。

1.1.2 LinkedList

  LinkedList相当于链式存储,它是通过节点直接彼此连接来实现的。每一个节点都包含前一个节点的引用,后一个节点的引用和节点存储的值。当一个新节点插入时,只需要修改其中保持先后关系的节点的引用即可,当删除记录时也一样。操作其中对象的速度快,只需要改变连接,新的节点可以在内存中的任何地方。不能随即访问,虽然存在get()方法,但是这个方法是通过遍历接点来定位的,所以速度慢。

1.1.3 Vector

  Vector和ArrayList在用法上几乎完全相同,但由于Vector是一个古老的集合,所以Vector提供了一些方法名很长的方法,但随着JDK1.2以后,java提供了系统的集合框架,就将Vector改为实现List接口,统一归入集合框架体系中。

1.1.4 Stack

  Stack是Vector提供的一个子类,用于模拟"栈"这种数据结构(LIFO后进先出)

1.2 Set 容器

   Set是一种不包含重复的元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。Set判断两个对象相同不是使用"=="运算符,而是根据equals方法。也就是说,我们在加入一个新元素的时候,如果这个新元素对象和Set中已有对象进行注意equals比较都返回false。

1.2.1 HashSet

   HashSet是Set接口的典型实现,HashSet使用HASH算法来存储集合中的元素,因此具有良好的存取和查找性能。当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值决定该对象在HashSet中的存储位置。此类允许使用 null 元素。

1.2.2 LinkedHashSet

  LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但和HashSet不同的是,它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历)。

1.2.3 TreeSet

  TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。

1.3 Queue 容器

  Queue用于模拟"队列"这种数据结构(先进先出 FIFO)。队列的头部保存着队列中存放时间最长的元素,队列的尾部保存着队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素,队列不允许随机访问队列中的元素。

1.3.1 PriorityQueue

  PriorityQueue并不是一个比较标准的队列实现,PriorityQueue保存队列元素的顺序并不是按照加入队列的顺序,而是按照队列元素的大小进行重新排序。

1.3.2 Deque

  Deque接口代表一个"双端队列",双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可以当成队列使用、也可以当成栈使用。

1.3.3 ArrayDeque

  是一个基于数组的双端队列,和ArrayList类似,它们的底层都采用一个动态的、可重分配的Object[]数组来存储集合元素,当集合元素超出该数组的容量时,系统会在底层重新分配一个Object[]数组来存储集合元素。

1.3.4 LinkedList

  LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。

二、Map 容器

  Map用于保存具有"映射关系"的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value。key和value都可以是任何引用类型的数据。Map的key不允
许重复,即同一个Map对象的任何两个key通过equals方法比较结果总是返回false。

2.1 LinkedHashMap

  LinkedHashMap也使用双向链表来维护key-value对的次序,该链表负责维护Map的迭代顺序,与key-value对的插入顺序一致(注意和TreeMap对所有的key-value进行排序进行区分)。

2.2 HashMap

  和HashSet集合不能保证元素的顺序一样,HashMap也不能保证key-value对的顺序。并且类似于HashSet判断两个key是否相等的标准也是: 两个key通过equals()方法比较返回true,同时两个key的hashCode值也必须相等。

2.3 Hashtable

  底层是哈希表数据结构,不可以存入null键和null值,但是他是线程安全的。相比其他线程安全方式,hashtable的线程安全更耗性能。

三、对比

3.1 ArrayList、Vector和LinkedList

  首先,只有Vector是线程安全的,但它基本被淘汰了,他在读写的时候使用synchronized(加在方法上)锁住对象,以达到线程安全的目的。
  ArrayList、Vector底层都是使用数组,但是Vector的同步锁机制的加入,导致Vector的性能比ArrayList要低。在扩容的时候,ArrayList扩容增长目前数组长度的50%,而Vector是达到100%。

3.2 HashMap 和 Hashtable

  HashMap是线程不安全的,Hashtable是线程安全(也是使用加在方法上的synchronized来锁定对象),Hashtable基本被淘汰,不要在代码中使用它(可以使用同样线程安全的ConcurrentHashMap)。
  HashMap的键和值都可以是null,但是Hashtable键不可以为null,会抛出NullPointerException错误。

3.3 HashMap 和 HashSet

  看似两个容器没啥关系,HashMap用来存储键值对,HashSet用来存储唯一对象。但HashSet底层使用HashMap来存储数据。只有几个Set接口对于HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
  HashSet添加对象,用的是底层HashMap的put,键为对象,值为静态的内部对象objet,在key的判断时,使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,先用==来判断是否指向同一对象,再equals()方法用来判断对象的相等性。

3.4 ConcurrentHashMap 和 Hashtable

  共同点都是线程安全的容器,底层数据结构从JDK1.8以后都采用了数组+链表/红黑二叉树。在JDK1.8前, 两者底层采用分段的数组+链表实现。
  Hashtable采用的是全表锁,非常吃性能,不管查还是加,都是全表上锁。
  在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。

四、初始化和扩容机制

4.1 ArrayList

  ArrayList有3种构建的方式,默认/指定容量/指定容器。如果是默认或指定容量为0的时候,都是指向空数组,直到插入一个元素才会创建空间。
  当添加第一个元素的时候,会判断,如果当前是指向空数组(构造使用默认构造),默认创建长度为10的空间,如果放不下,则进入正常的扩容机制中。

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //DEFAULT_CAPACITY=10
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

  扩容机制:增加的容量为原先的一半(如果原先长度是奇数,(原长度-1)/2),如原长度为6,新长度就为9。原长度为9,新长度就为13。如果新数组长度过大,大于MAX_ARRAY_SIZE,则考虑真实存放的数据所需要的大小,给Integer.MAX_VALUE或者MAX_ARRAY_SIZE长度。

 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        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);
    }

4.2 Vector

  四种构建方式:

//指定初始化容量和容器增长数量
public Vector(int initialCapacity, int capacityIncrement);
//带初始化容量
public Vector(int initialCapacity); 
//默认容量为10
public Vector() {
    this(10);
}
 //从其他容器中复制
 public Vector(Collection<? extends E> c);

  Vector初始化的时候如果没有指定容量,默认创建10个元素容量。
  扩容机制:如果设定了增长的幅度(即capacityIncrement > 0),则按照增长幅度来,否者长度扩充原来的一倍。如果长度太长了,超过MAX_ARRAY_SIZE,则考虑真实存放的数据所需要的大小,给Integer.MAX_VALUE或者MAX_ARRAY_SIZE长度。

private void grow(int minCapacity) {
 // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

4.3 LinkedList

  两种构建方式:默认/其他容器中复制。
  LinkedList底层使用的是链表的形式,没有所谓扩容的策略,只要一个一个往上加就行,数量的最大值就是int size的int最大值。

4.4 HashMap

   HashMap有四种构造方式:

//初始化容量和负载系数,
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

//指定初始化容量和默认负载系数(0.75)
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//空容量和默认负载系数(0.75)
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//从别容器中复制过来
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

初始化时候,带初始化容量的数据怎么处理

  HashMap的存储方式,在JDK1.7中采用数组+链表的方式存储,JDK1.8中采用数据+链表+红黑二叉树存储的。
  所以这里扩容其实是两种意思,第一种,对hash数组进行扩容,第二种桶进行扩容。
  先了解一下基础概念:
  容量:必须是2的幂 & <最大容量(2的30次方),因为存的hash产生的key,所以是2的幂次。 默认容量 是16。
  加载因子:HashMap在其容量自动增加前可达到多满的一种尺度。
  扩容阈值:当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量),对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。扩容阈值 = 容量 x 加载因子。
  当存第一个数据的时候,这个时候才会创建hash数组,默认长度是16(或者构造时指定的大于容量的2的指数大小),同时设置扩容阈值=16*0.75(默认)。之后每次扩充,容量变为原来的2倍,直到到达MAXIMUM_CAPACITY = 2^30,不再进行扩容。扩容之后会重新遍历原Hash数组,重新计算hash值,所以在这步比较消耗性能。
  JDK1.8后引入红黑树来解决Hash值冲突(即不同对象也有可能有相同的hash值)。当桶内链表长度达到桶的树化阈值,就会将桶内数据转化成红黑树形式,以便高效查询。同样到数据量减少会重新转化成链式结构。JDK1.7只是用链式结构存储。
  关于HashMap的树化有3个重要的参数:

     //树化桶元素最少元素量
    static final int TREEIFY_THRESHOLD = 8;
    
     //取消树形化,数值
    static final int UNTREEIFY_THRESHOLD = 6;

    //树化时Hash表需要满足最小长度
    static final int MIN_TREEIFY_CAPACITY = 64;

4.5 Hashtable (未整理完)

①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。

②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
  JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

总结

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值