Java 集合, 也叫作容器,一般用来存储和操作数据,主要是由两大接口派生而来:一个是 Collection
接口,主要用于存放单一元素有三个子接口,分别是set、List、Queue;另一个是 Map
接口,主要用于存放键值对。其中比较常用的是arraylist、hashset、hashmap。arraylist本质是一个可变长度的数组,容量可以动态增长,并且允许存储重复元素,保证元素存放的顺序,支持高效的随机访问,但是线程不安全。hashset保证元素的顺序,不允许存储重复的元素。hashmap则主要存储键值对,其中,键是唯一的,不允许重复,不是线程安全的。如果要求线程安全,可以使用concurrenthashmap吧。
(图源自JavaGuide)
1、集合和数组的区别:
2、List, Set, Queue, Map 四者的区别
List
: 存储的元素是有序的、可重复的。Set
: 存储的元素是无序的、不可重复的。Queue
: 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
: 使用键值对(key-value)存储,key 是无序的、不可重复的,value 是无序的、可重复的。
3、集合框架底层数据结构总结
Collection
接口下面的集合。
List
ArrayList
:Object[]
数组Vector
:Object[]
数组LinkedList
: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
Set
HashSet
(无序,唯一): 底层数据结构是HashMap
LinkedHashSet
:LinkedHashSet
是HashSet
的子类,底层数据结构通过LinkedHashMap
。TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)
Queue
PriorityQueue
:Object[]
数组来实现二叉堆ArrayQueue
:Object[]
数组 + 双指针
Map
接口下面的集合
HashMap
: JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 HashMap的是由数组、链表+红黑树组成,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间LinkedHashMap
:LinkedHashMap
继承自HashMap
,因此他的底部仍然采用数组+链表或红黑树的方式。此外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序,同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。Hashtable
: 数组+链表组成。TreeMap
: 红黑树(自平衡的排序二叉树)
如何选择集合
要根据具体的需求决定,如果只需要存放元素值时,则选择选择实现Collection
接口的集合,如果还要保证元素的唯一性,则选择set接口的集合;如果存放键值对的方式,则选择Map接口的集合,并按照相应的要求进行进一步的选择。
解决hash冲突的方法
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
二次探测:发生冲突时通过平方加减来找到第二次插入的位置。
链地址法:如果发生冲突,将元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
4、List:
ArrayList 和 Vector 的区别
ArrayList
是List
的主要实现类,底层使用Object[]
存储,适用于频繁的查找工作,线程不安全 ;Vector
也是List
的实现类,底层使用Object[]
存储,线程安全的。
ArrayList 与 LinkedList 区别
1、两者都是线程不安全的。
2、ArrayList
底层使用的是 Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
3、ArrayList
支持高效的随机访问,LinkedList
不支持。
4、ArrayList
的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间
5、ArrayList的扩容机制(重要)
底层结构是动态数组。它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity
操作来增加 ArrayList
实例的容量。
(JDK8) 以无参数构造方法创建 ArrayList
时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,初始数组容量扩为 10。然后继续向其中添加元素,直到添加完第10个元素,此时还没有扩容。如果要添加第11个元素,则会进行扩容,主要的扩容方法是老的容量右移一位,即/2,新容量为老容量+/2之后的容量之和,即老容量的1.5倍,以此类推。
6、CompareTo 和 Comparator的联系和区别
- 两者都常用于比较
- Comparable接口位于java.lang包下;Comparator位于java.util包下。
- Comparable接口只提供了一个compareTo()方法;Comparator接口不仅提供了compara()方法,还提供了其他默认方法,如reversed()、thenComparing()。
- 如果要用Comparable接口,则必须实现这个接口,并重写comparaTo()方法;但是Comparator接口可以在类外部使用,通过将该接口的一个匿名类对象当做参数传递给Collections.sort()方法或者Arrays.sort()方法实现排序。这样我们就可有不用改变类本身的代码而实现对类对象排序。
类中继承Comparable接口,并实现compareTo方法,完成自定义排序
public class User implements Comparable<User>{
private Integer id;
private Integer age;
public User() {
}
public User(Integer id, Integer age) {
this.id = id;
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
'}';
}
//升序排列效果
public int compareTo(User o) {
if(this.age > o.getAge()) {
return 1; //如果此对象大于目标对象,返回正整数1
}else if(this.age < o.getAge()) {
return -1; //如果此对象小于目标对象,返回负整数-1
}else{
return 0; //相等
}
}
}
类中继承Comparator接口,并实现compare方法,完成自定义排序
import java.util.Comparator;
public class Child implements Comparator<Child> {
private Integer id;
private Integer age;
public Child() {
}
public Child(Integer id, Integer age) {
this.id = id;
this.age = age;
}
//升序效果
@Override
public int compare(Child o1, Child o2) {
return o1.getAge() > o2.getAge() ? 1 : (o1.getAge() == o2.getAge() ? 0 : -1);
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Child{" +
"id=" + id +
", age=" + age +
'}';
}
}
也可以通过匿名内部类的方式,将该接口的一个匿名类对象当做参数传递给Collections.sort()方法或者Arrays.sort()方法实现排序
public class Test {
public static void main(String[] args) {
Child child1 = new Child(1, 14);
Child child2 = new Child(2, 12);
Child child3 = new Child(3, 10);
List<Child> list = new ArrayList<>();
list.add(child1);
list.add(child2);
list.add(child3);
//匿名内部类
Collections.sort(list, new Comparator<Child>() {
@Override
public int compare(Child o1, Child o2) {
return o1.getAge() > o2.getAge() ? 1 : (o1.getAge() == o2.getAge() ? 0 : -1);
}
});
// 或者使用JDK8中的Lambda表达式
//Collections.sort(list, (o1, o2) -> (o1.getAge()-o2.getAge()));
list.stream().forEach(System.out::println);
}
}
多字段自定义排序
public class TestSort {
public static void main(String[] args) {
List<Student> studentList = Arrays.asList(
new Student(1L, "张三", 95.5d),
new Student(2L, "李四", 97.5d),
new Student(3L, "王五", 95.5d),
new Student(4L, "赵六", 96.5d),
new Student(5L, "钱七", 98.5d),
new Student(6L, "小二", 97d)
);
Comparator<Student> byScore = Comparator.comparing(Student::getScore, (s1, s2) -> {
return s2.compareTo(s1);
});//按照分数降序
Comparator<Student> byName = Comparator.comparing(Student::getName);//按照名字字典升序
Collections.sort(studentList,byScore.thenComparing(byName));//先按照分数降序,再按照名字升序
System.out.println(studentList);
}
}
7、Set
- 无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
- 不可重复性是指添加的元素按照
equals()
判断时 ,返回 false,需要同时重写equals()
方法和hashCode()
方法
hashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的- 三者的底层数据结构不同,
HashSet
的底层数据结构是哈希表,LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。三者适用于不同的场景。
8、Queue
Queue是单端队列,而Deque是双端队列
ArrayDeque 与 LinkedList 的区别
-
ArrayDeque
是基于可变长的数组和双指针来实现,而LinkedList
则通过链表来实现。 -
ArrayDeque
不支持存储NULL
数据,但LinkedList
支持。 -
ArrayDeque
插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然LinkedList
不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。
从性能的角度上,选用 ArrayDeque
来实现队列要比 LinkedList
更好。此外,ArrayDeque
也可以用于实现栈
PriorityQueue
元素出队顺序是与优先级相关的,优先级最高的元素先出队。
PriorityQueue
利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据PriorityQueue
通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。PriorityQueue
是非线程安全的,且不支持存储NULL
和non-comparable
的对象。PriorityQueue
默认是小顶堆,但可以接收一个Comparator
作为构造参数,从而来自定义元素优先级的先后。
Map:
HashMap 和 Hashtable 的区别:
线程安全方面:HashMap
是非线程安全的,Hashtable
是线程安全的,因为Hashtable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!)- 因为线程安全的问题,
HashMap
要比Hashtable
效率高 HashMap
可以存储 null 的 key 和 value,hashtable不允许- 初始容量大小和每次扩充容量大小的不同 :① 创建时如果不指定容量初始值,
Hashtable
默认为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么Hashtable
会直接使用你给定的大小,而HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。 - JDK1.8 以后的
HashMap
引入了红黑树,当链表的长度超过阈值时,默认为8,会将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。hashtable则是数组+链表
HashMap 和 HashSet 区别
HashSet
底层就是基于 HashMap
实现的
( 表源自JavaGuide)
HashMap 和 TreeMap 区别
TreeMap
和HashMap
都继承自AbstractMap
,但是TreeMap
它还实现了NavigableMap
接口和SortedMap
接口
实现 NavigableMap
接口让 TreeMap
有了对集合内元素的搜索的能力。
实现SortedMap
接口让 TreeMap
有了对集合中的元素根据键排序的能力。
HashMap底层实现:
JDK1.7之前,采用数组+链表的结构,在JDK1.8之后,引入了红黑树。当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间,提高查询效率。
当我们向HsahMap中存放元素时,它会根据Key的 hashcode
经过扰动函数(hash())处理
(拿着key的哈希值,先“>>>”无符号右移16位,然后“^”异或上key的哈希值,得到一个值,再拿着这个值去“&”上数组长度减一)这样做的目的是让hash值的散列度更高,减少hash冲突,提高查询性能。
过后得到 hash 值,然后进行取模运算,判断当前元素存放的位置。但是,这样会遇到哈希冲突的情况。两个不同哈希值的Key经过取模运算((n - 1) & hash == hash%n)位运算效率更高后,得到的数可能相同。因此,借助拉链法来解决hash冲突,将冲突的值添加到链表中(采用尾插法)。同时,为了防止链表过长而导致的查询效率下降,当链表长度大于8并且数组长度大于等于64时,将链表转化为红黑树,提高查询效率。
红黑树简单介绍:
1.结点是红色或黑色。
2.根结点是黑色。
3.每个叶子结点都是黑色的空结点(NIL结点)。
4 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
5.从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。
红黑树是一种自平衡的二叉查找树,红黑树的时间复杂度为O(logN),链表的为O(N)
HashMap 的长度为什么是 2 的幂次方
1、可以让数据更散列更均匀的分布,更充分的利用数组的空间
2、在扩容迁移的时候不需要再重新通过哈希定位新的位置了。扩容后,元素新的位置,要么在原脚标位,要么在原脚标位+扩容长度这么一个位置
HashMap的扩容机制:
默认长度为16,当元素个数达到临界值时,会触发扩容机制。(图片来自B站跟着Mic学架构https://space.bilibili.com/1031543543)
扩容为原来的2倍。
为什么扩容因子是0.75.
因为扩容因子表示的是hash表中的元素的填充程度,扩容因子越大,则触发扩容的元素的个数会越多,空间利用率高,但是发生hash冲突的概率也会增大,设置为0.75很好的达到了空间成本和时间成本的平衡。
ConcurrentHashMap
是在hashmap基础上实现的,但是他是线程安全的。
在JDK1.8之前,ConcurrentHashMap
采用分段的数组+链表实现,使用分段锁,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
在JDK1.8之后,ConcurrentHashMap采
用 Node
数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized
和 CAS 来操作。
Hashtable
(同一把锁) :使用 synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
JDK1.7:
首先将数据分为一段一段(每一段就是一个Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
JDK1.8之后:
Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode
。当冲突链表达到一定长度时,链表会转换成红黑树。
Java 8 中,锁粒度更细,是数组中的某一个节点(只锁定当前链表或红黑二叉树的首节点),在1.7中,锁定的是一个片段,性能更高。
扩容:ConcurrentHashMap采用多线程并发
扩容,多个线程对原始数组进行分片,然后每个线程去负责每一个分片后的数据迁移,从而提升了数据迁移效率。
获取元素总个数。当线程竞争不激烈的时候,直接采用CAS方式,实现元素个数的原子递增;当比较激烈时,使用数组维护元素个数,当需要增加元素个数时,在数组中随机选择一个,再通过CAS算法来实现原子递增。