Java 集合

Java 集合的用途就是“保存对象”,并将其划分为两个不同的概念 ,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

1)Collection。表示一个独立的元素序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,Set不能有重复的元素。Queue按照队列的和规则来确定对象存放的顺序

2) Map。一组成对的“键值对”对象,使用键来查找值

List 接口 有序,可重复

  • ArrayList,底层数据结构是数组,查询快,增删慢,线程不安全。初始长度为10,超出后扩容为原来的1.5倍
  • LinkedList,底层数据结构是双向链表,查询慢,增删快,线程不安全。
  • Vector,底层数据结构是数组,线程不安全,速度比ArrayList慢。

Set接口 无序,唯一

  • HashSet,底层数据结构是Hash表(链地址法)。无序,唯一。查看HashSet源码发现其实是一个HashMap,初始大小16,负载因子0.75

/**
 * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
 * default initial capacity (16) and load factor (0.75).
 */
public HashSet() {
    map = new HashMap<>();
}
  • LinkedHashSet,底层数据结构是Hash表(链地址法)。(FIFO插入有序,唯一),由链表保证元素有序,由哈希表保证元素唯一。是一个LinkedHashMap

public LinkedHashSet() {
    super(16, .75f, true);
}
  • TreeSet,底层数据结构是红黑树。(唯一,有序)。是一个TreeMap

public TreeSet() {
    this(new TreeMap<E,Object>());
}

Map接口,有三个比较重要的实现类,分别是HashMap、HashTable、和TreeMap。

  • HashMap实现原理,HashMap底层使用Hash表(链地址法)数据结构

/**
 * The default initial capacity - MUST be a power of two.
 * 默认初始大小2^4=16,必须是2的次幂
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * The load factor used when none specified in constructor.
 *  默认负载因子0.75,元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大 一倍,然后重新计算每个元素在数组中的位置
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 * 链表长度超过8将链表转换为红黑树
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 * 小于6将树转换为链表
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 * 转换为树的最小长度
 */
static final int MIN_TREEIFY_CAPACITY = 64;

可以看出HashMap的结构其实是这样的: 

 当我们往HashMap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾(头插法)。从HashMap中get元素时,首先计算key的HashMap,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。从这里我们可以得知,如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的。

Java8 中,当链表中的元素超过了 8 个以后, 会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。小于6时又会将树转换为链表

HashMap的hash算法

在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。

理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。HashMap扩容之前的数组初始大小才16。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标,源码中模运算是在这个getNode方法中完成的

“扰动函数”,取得初始hash值右移16位异或混合。

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

然后把得到的散列值和数组长度做一个"与"操作

这也正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1)正好相当于一个“低位掩码”,“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。二进制表示为00000000 00000000 0000 1111。

和散列值一  10100101 11000100 0010  0101 做“与”操作如下,结果就是截取了最低的四位值。

             10100101 11000100 0010  0101
       &    00000000 00000000 0000 1111
      ---------------------------------------------------------------
             00000000 00000000 0000 0101   //高位全部归零,只保留末四位 index 为5

扩容后长度为32,32-1=31 二进制表示为00000000 00000000 0001 1111。

             10100101 11000100 0010  0101
       &    00000000 00000000 0001 1111
      ---------------------------------------------------------------
             00000000 00000000 0000 0101   //高位全部归零,只保留末四位 index 还为5

和散列值二  10100101 11000100 0001  0101 做“与”操作如下,结果就是截取了最低的四位值。

                  10100101 11000100 0001  0101
       &         00000000 00000000 0000 1111
      ---------------------------------------------------------------
                   00000000 00000000 0000 0101   //高位全部归零,只保留末四位 index 为5 

扩容后长度为32,32-1=31 二进制表示为00000000 00000000 0001 1111。

                  10100101 11000100 0001  0101
       &         00000000 00000000 0001 1111
      ---------------------------------------------------------------
                  00000000 00000000 0001 0101 //高位全部归零, index 变为5+16=21

因此,在扩容时,不需要重新计算元素的hash了,只需要在与n-1与操作后判断最高位是1还是0就好了。 是0,index不变,是1,index=index+数组原大小

注意:HashMap是线程不安全的,在并发情况下扩容时可能会造成环形链表,造成get时进入死循环 

  •  Hashtable(线程安全)

Hashtable的底层实现和HashMap是一样的也是使用了Hash表(链地址法)

不过Hashtable是线程安全的,HashMap不是线程安全的(并发情况下扩容时会产生环形链表);

Hashtable不允许null值,HashMap允许null值(key和value都允许,最多允许一条记录的键为null)

Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,会将链表的整个数组锁住并发性能不如ConcurrentHashMap, 因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全 的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。
  • TreeMap(可排序)

TreeMap的底层实现使用了红黑树,有序的,

TreeMap有两种排序方式:

 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则会抛出ClassCastException。

定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。

如果需要使用排序的映射,建议使用 TreeMap。
  • LinkedHashMap(记录插入顺序)

LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历

LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序

ps: 

ConcurrentHashMap:采用了链表+红黑树+Segment+分段锁的方式实现 ,

  • ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
  • 内部结构:ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。如下图是ConcurrentHashMap的内部结构图:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值