集合容器笔记(持续更新)
Collection单列集合
List接口
迭代器iterator
- 因为Collection继承了Iterator迭代器,所以iterator接口提供了遍历Collection的接口。可以从一个Collection中使用迭代器方法来获取迭代器实例
- 使用方法
Iterator的接口定义:
public interface Iterator{
boolean hasNext();
Object next();
void remove();
}
其中:
- hasNext(): 判断容器内是否还有可供访问的元素
- next(): 返回迭代器刚用过的元素引用,返回Object,需要强转为具体的类型 remove(): 删除迭代器刚越过的元素
使用方法代码如下:
List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if ("HelloWorld".equals(s)) {
iterator.remove();
}
}
- 因为Iterator是单项遍历的,所以更加安全,它可以确保在当前遍历的集合元素被更改的时候抛出ConcurrentModificationException异常
Iterator和ListIterator有什么区别
- Iterator可以遍历Set和List集合,而ListIterator只能遍历List。
- Iterator只能单项遍历,而ListIterator可以双i想遍历
- ListIterator实现了Iterator接口,添加了额外的功能,比如添加元素,替换元素,获取前面或者后面元素的索引
一个List的几种遍历方式
- for循环遍历:在集合外部维护一个计数器,依次读取每一个位置的元素,读到最后一个元素位置
- 迭代器Iterator遍历:Iterator为OOP的一个设计模式,目的是为了屏蔽不同集合的特点,统一遍历集合的接口
- foreach遍历:foreach内部也是采用了Iterator的方式实现,使用时不用显示声明Iterator或者计数器,优点为代码简洁,缺点是功能简单,不能再遍历时操作数据集合,如,增加、删除等操作。
ArrayList的优缺点
优点
- ArrayList底层为数组,采取随机访问模式,因此查询速度很快,并且顺序添加很方便
缺点
- 删除,插入操作采用元素复制操作,如果元素非常多,性能将十分低下
使用场景
- 适合随机访问,顺序添加删除的场景
List和数组之间的转换
- 数组转换为List:Arrays.asList(array)
- List转换为Array:使用List自带的toList()方法
ArrayList和LinkedList区别
ArrayList底层是动态数组,LinkedList底层实双向链表
- 随机访问效率:ArrayList比LinkedList在随机访问时的效率要高,因为LinkedList是线性存储,在查找时需要移动指针从前往后依次进行
- 增叫删除效率:在非首尾的增删操作上,LinkedList比AraayList效率要高,因为ArrayList采用移动复制元素的操作,会影响到其他数组内元素的下标
- 内存空间占用差别:LinkedList比ArrayList更占空间,LinkedList的节点不仅要存储数据,还要存储两个引用,一个指向直接前驱元素,一个指向直接后驱元素
- 线程安全:两个都是不同步的,即两个都是线程不安全的
总结:ArrayList适合读取比较多的场景,LinkedList适合插入删除操作比较多的场景
ArrayList和Vector的区别
- 两个类都实现了List接口(List继承了Collection接口),都是有序集合
线程安全:Vector使用了synchronized来实现线程同步,是线程安全的,ArrayList是线程不安全的 - 性能:因为Vector是线程安全的,所以性能低于ArrayList
扩容:ArrayList和Vector都能根据实际情况动态扩容,不过Vector每次增加一倍,ArrayList每次增加50%
总结:在不需要线程安全时,使用ArrayLsit以保证效率
多线程下使用ArrayList
- ArrayList线程不安全,如果要在多线程场景下使用,可用通过Coolections的synchronizedList方法将其转换为线程安全的容器再使用:如
ArrayList<String > list = new ArrayList<>();
list.add("Hello");
list.add("World");
List<String> synArrayList = Collections.synchronizedList(list);
for (String s : synArrayList) {
System.out.println(s);
}
List和Set区别
- List和Set都是继承于Collection接口
- List有序(元素存入顺序和取出顺序一致),元素可重复,可以插入多个null元素,元素有索引,常用实现类有ArrayList、LinkedList和Vector;
- Set无序,元素不可重复,只允许存入一个null元素,必须保证元素唯一性,常用实现类为HshSet、LinkedHashSet和TreeSet
- List支持for循环,即通过下标遍历,也支持迭代器遍历;而Set只能使用迭代器遍历,因为无序,无法使用下标来遍历
效率:
- Set:检索效率低,增删效率高,增删不会影响元素位置改变
- List:长度动态增长,检索效率高,增删效率低,因为增删会影响元素位置改变
Set接口
HashSet的实现原理
先看一下部分源码
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
//实际上调用的HsahMap的put方法,present实际上为一个一直相同的虚值
return map.put(e, PRESENT)==null;
}
- HashSet是基于HashMap实现的,HashSet的值存放在HashMap的Key上,HashMap的value统一为present,因此HashSet的实现比较简单,相关HashSet的操作基本上都是直接调用底层的HashMap的相关方法来完成,并且HshSet不允许重复的值。
HashSet如何检查重复?HashSet是如何保证数据不重复的?
- 在HashSet中add元素时,判断元素是否存在的依据,不仅要比较Hash值,还要结合equals()方法比较
- HashMap比较key是否相等,是先比较hashCode,再比较equals
- HashSet的add()实际上时HashMap的put()方法。HashMap中的key是唯一的,由源码可以看出HashSet中add()的value实际上是HashMap中put的key,在HashMap中如果K,V相同,则新V会覆盖旧V,并返回旧V
,所以不会重复
hashCode()和equals的相关说明
- hashcode:为jdk根据对象的地址或字符串或数字算出来的int类型的数值
如果两个对象相等,则hashcode一定是相等的;但如果两个对象hashcode值相等,对象不一定相等 - 如果两个对象相等,则equals返回为true
- equals方法被覆盖,hashCode也必须被覆盖
- hashCode()的默认行为是对在堆上的对象产生独特值,如果没有重写hashCode(),则该class的两个对象无论如何都不会相等
HashMap与HashSet的区别
| HashMap | HashSet |
|---|---|
| 实现Map接口 | 实现Set接口 |
| 存储键值对 | 仅存储对象 |
| 实现Map接口 | 实现Set接口 |
| 使用put()添加元素 | 使用add()添加元素 |
| HashMap使用Key计算hashcode值 | HashSet使用成员对象来计算hashcode值 ;如果两个对象hashcode值相同,则使用equals()来判断相等性 |
| HashMap使用唯一key来获取value,因此速度相较于HashSet更快 | 相较于HashMap,HashSet更慢 |
Map
HashMap的实现原理
-
HashMap是基于哈希表的Map接口的非同步实现,此实现提供所有可选的映射操作,并且允许null值和null键。HashMap不保证映射的顺序,特别是不保证顺序恒久不变;
-
HashMap的底层数据结构是链表散列,即链表加数组,主要是数组,链表是为了解决hash冲突;
-
HashMap基于Hash算法实现:
a) 在往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象元组在数组中的下标;
b) 存储时如果存在hashCode相同的key,有两种情况:i) 如果key相同,则覆盖原值; ii) 如果key不同,则将当前的key-value放入链表中(解决hash冲突);
获取HashMap值时,通过hash值找到对应下标,再进一步判断key是否相同,从而找到对应值;
- HashMap解决hash冲突的关键就在于使用数组加链表的方式存储数据。主要使用数组存储数据,然后将冲突的key的对象放入链表中做进一步对比;
附:jdk1.8之后对HashMap的实现进行优化,当链表的容量超过8个之后,链表会转换成红黑树来提高查询效率,后面会详细说明。
HashMap在jdk1.7和1.8的底层实现及如何解决hash冲突
在JDK1.8前采用拉链法解决哈希冲突
- hash冲突:不同的key可能对应着相同的hash值,这就是hash冲突
拉链法:即将数组和链表结合起来,发挥各自的优势(数组的查询速度快,链表的增删速度快)。方法为创建一个数组链表,数组的每一个元素就是一个链表,如果发生哈希冲突,将冲突的值加进链表
在JDK1.8后的方法
- 当链表长度大于阈值(默认8)时,将链表转换成红黑树,以减小搜索时间
HashMap在JDK1.8与JDK1.7比较
JDK优化了以下地方- resize扩容优化
- 引入红黑树,避免单条链表过长而影响效率
- 解决多线程死循环问题,但仍然是线程不安全的
| JDK1.7 | JDK1.7 | |
|---|---|---|
| 数据结构 | 数组+链表 | 数组+链表+红黑树 |
| 初始化方式 | 单独函数:inflateTable() | 直接集成到扩容函数resize() |
| hash计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
| 数据存储规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突&链表长度<8,存放单链表;冲突&链表长度<8,树化存放在红黑树 |
| 数据插入方式 | 头插法 | 尾插法 |
| 扩容后存储位置的计算方式 | 按原来方式计算 | 按扩容规则计算 |

浙公网安备 33010602011771号