java集合框架

本篇复习一下java中实现的数据结构,以及数据结构的一些原理

一. 容器基础

1. Java 容器都有哪些?

Java集合框架主要包括两种类型的容器:1. Collection:存储一个元素集合;2. Map:存储键/值对映射。

下面列取了java容器的一些实现类:
Collection
List:ArrayList、LinkedList、Vector、Stack
Set:HashSet、LinkedHashSet、TreeSet
Map
HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap、HashTable

 

2. 常见的数据结构

线性表:数组实现、链表
栈与队列
树和二叉树:树、二叉树、二叉查找树、平衡二叉树与红黑树

 

3. List、Set、Map 之间的区别是什么?

List、Set、Map 的区别主要体现在两个方面:元素是否有序、是否允许元素重复。
List:元素有序,允许元素重复。
Set:都不允许重复,HashSet 是无序的,TreeSet的顺序和大小有关,LinkedHashHashSet 和插入顺序有关。
Map:key不允许重复,value可以重复。如果在 Map 中添加重复的键,那么该键对应的值会覆盖掉原来的值。

 

4. 哪些集合类是线程安全的?

Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,不建议使用。
hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。
ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。(推荐使用)
 

5. fail-fast

a. 描述

a. 当多个线程同时对集合中的内容进行修改时可能就会抛出ConcurrentModificationException异常.
b. 在单线程中用增强 for 循环中一边遍历集合一边修改集合的元素也会抛出上述异常.

List<Integer> list = new ArrayList<>();
for(Integer i : list) list.remove(i); //运行时抛出ConcurrentModificationException异常

Iterator<Integer> it = list.iterator();
while(it.hasNext()) it.remove();

 

b. java的设计-对于对列表修改时的报错

当modCount与expectedModCount值不一致时会报错。

final void checkForComodification() {
	if (modCount != expectedModCount)
		throw new ConcurrentModificationException();
}

看下源码注释下有关快速失败的信息

//如果在创建迭代器后对列表进行结构修改,则除了通过迭代器自己的remove或add方法之外,迭代器都会报错。
ArrayList{
//记录了这个列表在结构上被修改的次数。
//结构修改是那些改变列表大小的修改,或者以其他方式扰乱它,使得正在进行的迭代可能产生不正确的结果。
//add()系列,remove()系列,clear(),replace()等,都会使得modCout++ 
protected transient int modCount = 0;

	//expectedModCount是Iterator实现类或其他方法的自己的变量,也就是说跟modCount的作用范围不一致。
	private class Itr implements Iterator<E> {
  		int expectedModCount = modCount;
	}
    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
。。。

因为两个变量的作用范围不同,所以可以理解类注释所描述的:当通过迭代器遍历元素进行操作时,除了自己对列表修改,其他例如在增强循环内修改元素等,都会报错。

 

c. 多线程对列表操作的报错

从上述展示的源码可以知道,modCount 是个共享变量而expectedModCount 是“较为局部的”所以属于线程各自的。那既然两个变量对于线程的可见程度是不一样的:比如线程1更新了自己的expectedModCount,线程二是看不到的,所以两个线程同时操作一个方法时,expectedModCount的消息不同步导致 imodCount != expectedModCount ,抛出 ConcurrentModificationException 异常。
 

6. fail-safe

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是复制原有集合内容,在拷贝的集合上进行遍历。所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会抛出ConcurrentModificationException 异常。

缺点
迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生了修改,迭代器是无法访问到修改后的内容。
 

 

二. Collection

1. 迭代器 Iterator

简介
Collection接口实现了Iterator接口。迭代器提供了Collection实现类遍历元素的能力,且允许在迭代过程中移除元素。

public interface Collection<E> extends Iterable<E>{
	// Query Operations
}

使用
Iterator 只能单向遍历,但更加安全,因为它可以确保,在当前遍历的集合元素被更改
的时候,就会抛出 ConcurrentModificationException 异常。

使用案例一:

List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()) String obj = it. next();

使用案例二:遍历时修改集合结构

Iterator<Integer> it = list.iterator();
while(it.hasNext()){
	// do something*
	it.remove();
}

Iterator 和 ListIterator

Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元
素、获取前面或后面元素的索引位置。

 

2. ArrayList 和 LinkedList

a. 数据结构实现

ArrayList 是动态数组的数据结构实现
LinkedList 是双向链表的数据结构实现

b. 随机访问效率
ArrayList 比 LinkedList 在随机访问的时候效率要高

ArrayList可以通过下标进行访问元素,时间复杂度=O(1)
LinkedList需要从头开始遍历元素,时间复杂度最坏=O(n)

c. 增加和删除效率
LinkedList 要比ArrayList 效率要高

ArrayList的内存空间是连续的,如果在非收尾位置进行增删,那就会影响到其他元素
LinkedList进行增删时,只需要操作元素的前驱和后继即可

d. 内存空间占用
LinkedList 比ArrayList 更占内存

LinkedList 的节点除了存储数据,还存储了两个引用
链表存储的空间不连续

e. 线程安全
ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全
 

使用场景

在需要频繁读取集合中的元素时,更推荐使用 ArrayList
在插入和删除操作较多时,更推荐使用 LinkedList。

 

3. ArrayList 和 Vector

Vector 使用了 Synchronized 来实现线程同步,是线程安全的。 ArrayList 是非线程安全的,所以性能ArrayList 在性能方面要优于 Vector。可以通过以下方式实现ArrayList的多线程场景。

List<String> synchronizedList = Collections.synchronizedList(list);

扩容:Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
 

4. List 和 Set

List 、Set 都是继承自Collection 接口

特点

List :一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有ArrayList、LinkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及TreeSet。

遍历方式

List 支持for循环,通过下标来遍历,也可以用迭代器
set只能用迭代,因为他无序,无法用下标来取得想要的值

查询和插入

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

 
 

三. Map和Set

1. HashMap

1.1 HashMap 底层数据结构

先看一张图:
左边竖着的是HashMap的数组结构,元素可能包含:单个node,链表或是红黑树
如图:index=1的就是一个链表;index=8的就是一个红黑树。
JDK1.8底层数据结构

数据结构描述
数组数组的下标索引是通过 key 的 hashcode 计算出来的。
方便快速查找,时间复杂度是 O(1)
链表当遇到hash碰撞( key的hashcode一致,但key值不同)时,单个 Node 就会转化成链表。
链表的查询复杂度是 O(n)。
红黑树当链表的长度大于等于 8 并且数组的大小超过 64 时,链表就会转化成红黑树。根据柏松分布知道产生这种结构的概率不大,而且性能也只提高了7%~8% 左右。
红黑树的查询复杂度是 O(log(n))

选择8的原因:
a. 转换的原因:
链表查询的时间复杂度是 O (n),红黑树的查询复杂度是 O (log (n))。
链表数量不多时,遍历比较快,多到一定程度时需要转换为红黑树,但红黑树占用空间时链表的2倍
所以考虑到转换的时间和空间损耗,需要定义转换边界值。

b. 转换时机
由泊松分布得出结论,链表的长度=8时命中概率为: 0.00000006,不到千万分之一,所以=8时进行转换。

版本之间的不同
JDK1.7:数组+链表
JDK1.8:数组+链表+红黑树
 

1.2. HashMap 的容量设计

默认初始容量是16。且HashMap 的容量必须是2的N次方。

为什么是2的N次方
原因是为了实现一个能让key尽量均匀分布的hash函数。求取元素位置函数:index=Hash(Key)&(Length-1)的二进制。

例如:Hash(Key)=101111100100 1011;(Length-1)=000000001111
index的结果只取决于Value的最后四位,此时可以知道使用位运算,效果上和取模(index=Hash数据(Key)%Length)相当,同时提高了性能。

最大的容量是 2的30次方
 

1.3.加载因子是0.75的原因?

加载因子:表示的是当数组空间被占0.75时,会触发扩容。

如果设置的太大,比如 1,也就是每个空位都要填满,这时会产生大量的哈希碰撞和链表,查询效率很低,而且每次扩容都需要重建hash表。
如果设置的过小,比如 0.5,虽然减少了哈希碰撞,且查询效率很高,但消耗了大量空间。
因此,我们就需要在时间和空间上做一个折中,选择最合适的负载因子以保证最优化,取到了0.75

 

2. HashSet与HashMap

实现上
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present。

public class HashSet<E>...{
	private static final Object PRESENT = new Object();
	...
	public HashSet()  map = new HashMap<>();
    ...
	public boolean add(E e)  return map.put(e, PRESENT)==null;
}

判断唯一性
HashMap使用键(Key)计算Hashcode;
HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false。

所以HashMap相对于HashSet较快,因为它是使用唯一的键获取对象。

 

2. TreeMap

简介
TreeMap基于红黑树(Red-Black tree)实现的有序的key-value集合。
该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序。

HashMap 、TreeMap的使用场景

HashMap:更适合在Map中插入、删除和定位元素这类操作。
TreeMap:需要对一个有序的key集合进行遍历时。

 

3. currentHashMap

看一下其底层实现
JDK1.7
在这里插入图片描述
在JDK1.7中,采用Segment + HashEntry的方式进行实现,如上图。一个 ConcurrentHashMap 里包含一个 Segment 数组,每个Segment 包含一个 HashEntry 数组。

Segment:和HashMap类似,是一种数组和链表结构,用于充当锁(可重入锁 ReentrantLock),用于守护它对应的HashEntry。

HashEntry :链表结构,用于存储映射表的<key,value>。

操作简述:
将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程(获取了一个Segment的锁)访问其中一个段数据时,其他段的数据也能被其他线程访问。

 
JDK1.8
在这里插入图片描述

在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保
证并发安全进行实现,synchronized只锁定当前Node(链表或红黑二叉树)的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

4. ConcurrentHashMap 、 HashTable和HashMap

ConcurrentHashMap对数据分段加锁,HashTable的synchronized在每次同步执行时都要锁住整个结构,所以前者并发性能更好,而HashMap没有锁机制,不是线程安全的。
HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

参考:
java.util.ArrayList
java.util.HashSet
https://blog.youkuaiyun.com/qq_30999361/article/details/124503952

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roman_日积跬步-终至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值