Java集合框架:List、Set、Map 以及源码解析
Java集合
List、Set、Map都是接口,List
和Set
类继承Collection
接口,Map
为独立接口
List、Set、Map三者区别
- List:存储元素有序、可重复
- ArrayList(Object[]数组)
- 优点:底层数据结构是数组,查询快,增删慢
- 缺点:线程不安全,效率低
- Vector(Object[]数组)
- 优点:底层数据结构是数组,查询快,增删慢
- 缺点:线程安全,效率低
- LinkedList(双向链表,JDK1.6之前是循环链表,JDK1.7取消了循环)
- 优点:底层数据结构是链表,查询慢,增删快
- 缺点:线程不安全,效率高
- ArrayList(Object[]数组)
- Set:无序、不可重复
- HashSet(基于HashMap实现)
- 底层数据结构是哈希表。(无序唯一)
- 保证元素唯一性(hashCode()、equals())
- 线程不安全,可以存储null值
- LinkedHashSet(基于LinkedHashMap实现)
- 底层数据结构是链表和哈希表。(FIFO插入有序,唯一)
- 链表保证元素有序
- 哈希表保证元素唯一
- 能够按照添加的顺序遍历
- TreeSet(红黑树,自平衡排序二叉树)
- 底层数据结构是红黑树 (唯一,有序)
- 元素排序:自然排序,比较器排序
- 元素唯一性:根据比较的返回值是否为0来决定
- 能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序
- HashSet(基于HashMap实现)
- Map:使用key-value存储。key无序、不可重复;value无序、可重复。一一对应
- HashMap:JDK1.8之前由数组+链表组成,数组是主体,链表是为解决哈希冲突;JDK1.8之后,在解决哈希冲突时,如果链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转换为红黑树,以减少搜索时间
- LinkedHashMap:LinkedHashMap继承自HashMap,底层仍然基于数组和链表或红黑树组成,增加了一条双向链表,保持键值对的插入顺序。
- Hashtable:数组+链表组成,数组是Hashtable的主体,链表则是为了解决哈希冲突而存在的
- TreeMap:红黑树(自平衡的排序二叉树)
List
ArrayList
ArrayList的底层是数组队列,相当于动态数组。与Java中的数组相比,它的容量可以动态增长。通过ensureCapacity
来增加ArrayList
实例的容量,可以减少递增式再分配的数量
ArrayList继承于AbstractList,实现了List、RandomAcess、Cloneable、java.io.Serializable这些接口
public class ArrayList<E> extends AbstractList<E> implements List<E>,RandomAcess,Cloneable,java.io.Serializable{
}
- RandomAcess:表明List集合是支持快速随机访问的。在ArrayList中,即可以通过元素的序号快速获取元素对象。
- Cloneable:覆盖clone(),能被复制
- java.io.Serializable:支持序列化,能通过序列化去传输
ArrayList源码解析
LinkedList源码解析
HashMap源码分析
ConcurrentHashMap源码分析
ArrayList 和 Vector 区别
- ArrayList 是 List的主要实现类,底层使用 Object[]存储,适用频繁的查找工作,线程不安全,用
Collections.synchronizedList
包装ArrayList成一个线程安全的数组容器,原理和Vector一样 - Vector 是 List的古老实现类,底层使用 Object[]存储。线程安全,对所有方法都加上
synchronized
ArrayList 和 LinkedList 区别
- 是否保证线程安全:都是不同步的,都不保证线程安全
- 底层数据结构:ArrayList底层使用的是Object数组;LinkedList底层使用 双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消循环)
- 插入和删除是否受元素位置的影响:
- ArrayList采用数组存储,插入和删除元素的时间复杂度受元素位置影响。
add(E e)
时,ArrayList 会默认将元素追加到列表的末尾,时间复杂度为O(1).但如果在指定位置插入和删除的话add(int index,E element)
时间复杂度为O(n-i).第i位之后的元素都要向后/向前移一位。 - LinkedList采用链表存储,对于add(E e)插入,删除元素时间复杂度不受元素位置影响,近似O(1)。如果在指定位置插入和删除元素(add(int index,E element))时间复杂度近似为O(n),需要先移动到指定位置再插入
- ArrayList采用数组存储,插入和删除元素的时间复杂度受元素位置影响。
- 是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而ArrayList支持。快速随机访问是通过元素的序号快速获取元素对象
- 内存空间占用:ArrayList的空间浪费主要体现在list列表的结尾会预留一定容量空间。LinkedList的空间花费则体现在每一个元素都需要消耗比ArrayList更多的空间。(需要存放直接后继和直接前驱以及数据)
双向链表和双向循环链表
双向链表:包含两个指针,一个prev指向前一个节点,一个next指向后一个节点
双向循环链表:最后一个节点的next指向head,head的prev指向最后一个节点,构成一个环
Set
comparable 和 Comparator的区别
- comparable 接口实际上是出自
java.lang
包,它有一个compareTo(Object obj)
方法用来排序 - Comparator 接口实际上是出自
java.util
包,它有一个compare(Object obj1,Object obj2)
方法用来排序
一般对集合使用自定义排序时,就要重写compareTo()
方法或compare()
方法。
Comparator 定制排序
public static void main(String[]args){
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
System.out.println("原始数组");
System.out.println(arrayList); // [-1,3,3,-5,7,4,-9,-7]
// 反转
Collections.reverse(arrayList);
System.out.println("Collections.reverse 反转数组");
System.out.println(arrayList); // [-7,-9,4,7,-5,3,3,-1]
// 升序排序
Collections.sort(arrayList);
System.out.println("Collections.sort 升序");
System.out.println(arrayList); // [-9, -7, -5, -1, 3, 3, 4, 7]
// 定制排序
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
System.out.println("定制排序后");
System.out.println(arrayList); // [7, 4, 3, 3, -1, -5, -7, -9]
}
重写compareTo方法实现按年龄排序
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/**
* T重写compareTo方法实现按年龄来排序
*/
@Override
public int compareTo(Person o) {
if (this.age > o.getAge()) {
return 1;
}
if (this.age < o.getAge()) {
return -1;
}
return 0;
}
}
测试
public static void main(String[] args) {
TreeMap<Person, String> pdata = new TreeMap<Person, String>();
pdata.put(new Person("张三", 30), "zhangsan");
pdata.put(new Person("李四", 20), "lisi");
pdata.put(new Person("王五", 10), "wangwu");
pdata.put(new Person("小红", 5), "xiaohong");
// 得到key的值的同时得到key所对应的值
Set<Person> keys = pdata.keySet();
for (Person key : keys) {
System.out.println(key.getAge() + "-" + key.getName()); //5-小红 10-王五 20-李四 30-张三
}
}
无序性和不可重复性
- 无序性:不等于随机性。无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的
- 不可重复性:
Map
HashMap 和 Hashtable 的区别
- 线程是否安全:HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable内部方法都基本经过
synchronized
修饰。如果要保证线程安全使用ConcurrentHashMap - 效率:因为线程安全问题,HashMap要比Hashtable效率高一点。Hashtable基本被淘汰,不要在代码中使用它
- 对Null key 和 Null value的支持:HashMap可以存储null的key和value,但null作为键只能有一个,null作为值可以有多个。Hashtable不允许有null键和null值,否则抛出异常
- 初始容量大小和每次扩充容量大小的不同:
- 创建时如果不指定容量初始值,HashMap默认初始化大小为16.之后每次扩充,容量变为原来的2倍;Hashtable默认初始大小为11,之后每次扩充,容量变为原来的2n+1.
- 创建时如果给定了容量初始值,那么Hashtable会直接使用给定大小;HashMap会将其扩充为2的幂次方。
- 底层数据结构:JDK1.8之后HashMap在解决哈希冲突时有了较大变化,当链表长度大于阈值(默认为8)(将链表转换为红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable没有这样的机制
HashMap 和 HashSet 区别
- HashMap
- 实现了Map接口
- 存储键值对
- 调用put()向map中添加元素
- HashMap使用键(key)计算hashCode
- HashSet
- 实现set接口
- 存储对象
- 调用add()方法向Set中添加元素
- HashSet使用成员对象来计算hashcode,对于两个对象来说,hashcode可能相同,所以equals()方法用来判断对象的相等性
HashMap 和 TreeMap 区别
TreeMap 和 HashMap都继承 AbstractMap,但需要注意的是 TreeMap还实现了 NavigableMap接口和 SortedMap接口
实现NavigableMap接口,让TreeMap有了对集合内元素的搜索的能力
实现SortedMap接口,让TreeMap有了对集合元素根据键排序的能力,默认是按key的升序排序。也可以指定自定义排序器排序
public class Person {
private Integer age;
public Person(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
public static void main(String[] args) {
// TreeMap<TreemapTest,String> treeMap = new TreeMap(new Comparator<TreemapTest>() {
// @Override
// public int compare(TreemapTest o1, TreemapTest o2) {
// int num = o1.getAge() - o2.getAge();
// return Integer.compare(num,0);
// }
// });
//将代码替换成 Lambda 表达式实现的方式:
TreeMap<TreemapTest,String> treeMap = new TreeMap<>((o1,o2) -> {
int num = o1.getAge() - o2.getAge();
return Integer.compare(num, 0);
});
treeMap.put(new Person(3), "person1");
treeMap.put(new Person(18), "person2");
treeMap.put(new Person(35), "person3");
treeMap.put(new Person(16), "person4");
treeMap.entrySet().stream().forEach(personStringEntry -> {
System.out.println(personStringEntry.getValue()); //person1 person4 person2 person3
});
}
}
HashSet如何检查重复
- 当你把对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置
- 并且和其他加入对象的hashCode值做比较,如果没有相符的hashCode,直接插入
- 如果发现有相同的hashcode值的对象,调用equals()方法检查hashcode值相等的对象是否相同,如果相同,HashSet就不会加入
**hashCode() 与 equals()**:
- 如果两个对象相等,则hashCode一定相同
- 如果两个对象相等,对两个equals()返回true
- 如果两个对象有相同的hashCode值,它们不一定是相等的
- equals()方法被覆盖过,则hashCode()方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等。(即使两个对象指向相同的数据)
== 和 equals() 区别
- 对于基本类型来说, == 比较的是值是否相等
- 对于引用类型来说,==比较的是两个引用是否指向同一个对象地址。两者在内存中存放的地址(堆内地址)是否指向同一个地方
- 对于引用类型(包括包装类型)来说,equals如果没有被重写,对比它们的地址是否相等;如果equals()方法被重写(例如String),比较的是地址里的内容
HashMap 多线程操作导致死循环问题
jdk1.7版本主要原因在于并发下的Rehash会造成元素之间形成一个循环链表。jdk1.8版本解决了这个问题,但在多线程下使用HashMap会存在数据丢失等问题。并发下推荐使用ConcurrentHashMap.
ConcurrentHashMap 和 Hashtable的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同
- 底层数组结构:JDK1.7的ConcurrentHashMap底层采用分段的数组Segment+链表实现,JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑树;Hashtable和JDK1.8之前的HashMap一样采用 数组+链表的形式,数组是Hashtable的主体,链表则是为了解决哈希冲突
- 实现线程安全的方法:
- 在JDK1.7,ConcurrentHashMap(分段锁)对整个桶数组进行分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了JDK1.8时,摒弃Segment的概念,直接使用Node数组+链表+红黑树的数据结构,并发控制使用
synchronized
和CAS来操作。 - Hashtable(同一把锁):使用
synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈,效率越低
- 在JDK1.7,ConcurrentHashMap(分段锁)对整个桶数组进行分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了JDK1.8时,摒弃Segment的概念,直接使用Node数组+链表+红黑树的数据结构,并发控制使用
Hashtable
JDK1.7的ConcurrentHashMap
JDK1.8的ConcurrentHashMap
Collections 工具类
Collections工具类常用方法:
- 排序
- 查找,替换操作
- 同步控制(不推荐,需要线程安全的集合类型时,请考虑使用JUC包下的并发集合)
排序
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
查找,替换操作
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target)
boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素
同步控制
Collections提供了多个synchronizedXxx()
方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题
HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap都是线程不安全的。Collections提供了多个静态方法可以把它们包装成线程同步的集合。
最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。
synchronizedCollection(Collection<T> c) //返回指定 collection支持的同步(线程安全的)collection
synchronizedList(List<T> list) //返回指定列表支持的同步(线程安全的)List
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的)set。
参考
- 本文链接:https://wentianhao.github.io/2021/08/01/%E5%AE%B9%E5%99%A8/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。
若没有本文 Issue,您可以使用 Comment 模版新建。