集合概述
Java 集合概览
Java集合, 也叫作容器,主要是由两大接口派生而来:Collection接口,主要用于存放单一元素;Map接口,主要用于存放键值对
对于Collection接口,下面有三个主要的子接口:List、Set、Queue
List、Set、Queue、Map的区别
List(对付顺序的好帮手):存储的元素是有序的、可重复的
Set(注重独一无二的性质):存储的元素不可重复的
Queue(实现排队功能的叫号机):按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的
Map(用key来搜索的专家):使用键值对(key-value)存储,类似于数学上的函数y=f(x),x代表key,y代表value,key是无序的、不可重复的,value是无序的、可重复的,每个键最多映射到一个值
集合框架底层数据结构总结
Collection接口下的集合
List
ArrayList:Object[]数组
Vector:Object[]数组
LinkedList:双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
Set
HashSet(无序,唯一):基于HashMap实现,底层采用HashMap来保存元素
LinkedHashSet:是HashSet的子类,并且其内部是通过LinkedHashMap来实现的
TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)
Queue
PriorityQueue:Object[]数组实现小顶堆
DelayQueue:PriorityQueue
ArrayDeque:可扩容动态双向数组
Map接口下的集合
Map
HashMap:JDK1.8之前HashMap由数组+链表组成,数组是HashMap的主体,链表主要是为了解决哈希冲突而存在的(拉链法解决冲突);JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,若当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap:LinkedHashMap继承自HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成,另外,LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序,同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
HashTable:数组+链表组成,数组是HashTable的主体,链表主要是为了解决哈希冲突而存在的
TreeMap:红黑树(自平衡的排序二叉树)
如何选用集合
主要根据集合的特点来选择合适的集合
需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap
只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合例如TreeSet或HashSet,不需要就选择实现List接口的集合例如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用
为什么要使用集合
当需要存储一组类型相同的数据时,数组是最常用且最基本的容器之一,但使用数组存储对象存在一些不足之处,在实际开发中,存储的数据类型多种多样且数量不确定,这时,Java集合就派上用场了
与数组相比,Java集合提供了更灵活、更有效的方法来存储多个数据对象,Java集合框架中的各种集合类和接口可以存储不同类型和数量的对象,同时还具有多样化的操作方式,相较于数组,Java集合的优势在于它们的大小可变、支持泛型、具有内建算法等,总的来说,Java集合提高了数据的存储和处理灵活性,可以更好地适应现代软件开发中多样化的数据需求,并支持高质量的代码编写
List
ArrayList和Array(数组)的区别
ArrayList内部基于动态数组实现,比Array(静态数组)使用起来更加灵活:
ArrayList会根据实际存储的元素动态地扩容或缩容,而Array被创建之后就不能改变它的长度了
ArrayList允许使用泛型来确保类型安全,Array则不可以
ArrayList中只能存储对象,对于基本类型数据,需要使用其对应的包装类(如Integer、Double等),Array可以直接存储基本类型数据,也可以存储对象
ArrayList支持插入、删除、遍历等常见操作,并且提供了丰富的API操作方法,例如add()、remove()等,Array只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力
ArrayList创建时不需要指定大小,而Array创建时必须指定大小
下面是二者使用的简单对比:
// Array
// 初始化一个String类型的数组
String[] stringArr = new String[]{"hello", "world", "!"};
// 修改数组元素的值
stringArr[0] = "goodbye";
System.out.println(Arrays.toString(stringArr)); // [goodbye, world, !]
// 删除数组中的元素, 需要手动移动后面的元素
for (int i = 0; i < stringArr.length - 1; i++) {
stringArr[i] = stringArr[i + 1];
}
stringArr[stringArr.length - 1] = null;
System.out.println(Arrays.toString(stringArr)); // [world, !, null]
// ArrayList
// 初始化一个String类型的ArrayList
ArrayList<String> stringList = new ArrayList<>(Arrays.asList("hello", "world", "!"));
// 添加元素到ArrayList中
stringList.add("goodbye");
System.out.println(stringList); // [hello, world, !, goodbye]
// 修改ArrayList中的元素
stringList.set(0, "hi");
System.out.println(stringList); // [hi, world, !, goodbye]
// 删除ArrayList中的元素
stringList.remove(0);
System.out.println(stringList); // [world, !, goodbye]
ArrayList和Vector的区别
ArrayList是List的主要实现类,底层使用Object[]存储,适用于频繁的查找工作,线程不安全
Vector是List的古老实现类,底层使用Object[]存储,线程安全
Vector和Stack的区别
Vector和Stack两者都是线程安全的,都是使用synchronized关键字进行同步处理
Stack继承自Vector,是一个后进先出的栈,而Vector是一个列表
随着Java并发编程的发展,Vector和Stack已经被淘汰,推荐使用并发集合类(例如ConcurrentHashMap、CopyOnWriteArrayList等)或者手动实现线程安全的方法来提供安全的多线程操作支持
ArrayList可以添加null值吗
ArrayList中可以存储任何类型的对象,包括null值,不过不建议向ArrayList中添加null值,null值无意义,会让代码难以维护,例如忘记做判空处理就会导致空指针异常
示例代码:
ArrayList<String> listOfStrings = new ArrayList<>();
listOfStrings.add(null);
listOfStrings.add("java");
System.out.println(listOfStrings);
输出:
[null, java]
ArrayList插入和删除元素的时间复杂度
对于插入:
- 头部插入:由于需要将所有元素都依次向后移动一个位置,因此时间复杂度是O(n)
- 尾部插入:当ArrayList的容量未达到极限时,往列表末尾插入元素的时间复杂度是O(1),因为它只需要在数组末尾添加一个元素即可;当容量已达到极限并且需要扩容时,则需要执行一次O(n)的操作将原数组复制到新的更大的数组中,然后再执行O(1)的操作添加元素
- 指定位置插入:需要将目标位置之后的所有元素都向后移动一个位置,然后再把新元素放入指定位置,这个过程需要移动平均n/2个元素,因此时间复杂度为O(n)
对于删除:
- 头部删除:由于需要将所有元素依次向前移动一个位置,因此时间复杂度是O(n)
- 尾部删除:当删除的元素位于列表末尾时,时间复杂度为O(1)
- 指定位置删除:需要将目标元素之后的所有元素向前移动一个位置以填补被删除的空白位置,因此需要移动平均n/2个元素,时间复杂度为O(n)
这里简单列举一个例子:
// ArrayList的底层数组大小为10,此时存储了7个元素
+---+---+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | | | |
+---+---+---+---+---+---+---+---+---+---+
0 1 2 3 4 5 6 7 8 9
// 在索引为1的位置插入一个元素8,该元素后面的所有元素都要向右移动一位
+---+---+---+---+---+---+---+---+---+---+
| 1 | 8 | 2 | 3 | 4 | 5 | 6 | 7 | | |
+---+---+---+---+---+---+---+---+---+---+
0 1 2 3 4 5 6 7 8 9
// 删除索引为1的位置的元素,该元素后面的所有元素都要向左移动一位
+---+---+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | | | |
+---+---+---+---+---+---+---+---+---+---+
0 1 2 3 4 5 6 7 8 9
LinkedList插入和删除元素的时间复杂度
- 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为O(1)
- 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为O(1)
- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均n/2个元素,时间复杂度为O(n)
列举:若要删除节点9,需要先遍历链表找到该节点,然后再执行相应节点指针指向的更改
LinkedList为什么不能实现RandomAccess接口
RandomAccess是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素),由于LinkedList底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现RandomAccess接口
ArrayList与LinkedList区别
- 是否保证线程安全:ArrayList和LinkedList都是不同步的,即不保证线程安全
- 底层数据结构:ArrayList底层使用的是Object数组;LinkedList底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环)
- 插入和删除是否受元素位置的影响:
- ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响,例如执行add(E e)方法的时候,ArrayList会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度为O(1);若要在指定位置i插入和删除元素(add(int index, E element)),时间复杂度为O(n),因为在进行上述操作的时候集合中第i和第i个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作
- LinkedList采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(add(E e)、addFirst(E e)、addLast(E e)、removeFirst()、removeLast()),时间复杂度为O(1);若要在指定位置i插入和删除元素(add(int index, E element)、remove(Object o)、remove(int index)), 时间复杂度为O(n) ,因为需要先移动到指定位置再插入和删除
- 是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而ArrayList(实现了RandomAccess接口) 支持,快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)
- 内存空间占用:ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)
在项目中一般是不会使用到LinkedList的,需要用到LinkedList的场景几乎都可以使用ArrayList来代替,并且性能通常会更好,就连LinkedList的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用LinkedList
不要下意识地认为LinkedList作为链表就最适合元素增删的场景,LinkedList仅仅在头尾插入或者删除元素的时候时间复杂度近似O(1),其他情况增删元素的平均时间复杂度都是O(n)
双向链表和双向循环链表
双向链表:包含两个指针,一个prev指向前一个节点,一个next指向后一个节点
双向循环链表:最后一个节点的next指向head,而head的prev指向最后一个节点,构成一个环
RandomAccess接口
public interface RandomAccess {
}
查看源码发现实际上RandomAccess接口中什么都没有定义,所以RandomAccess接口不过是一个标识罢了,标识实现这个接口的类具有随机访问功能
在binarySearch()方法中,它要判断传入的list是否RandomAccess的实例,若是,调用indexedBinarySearch()方法,若不是,那么调用iteratorBinarySearch()方法
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size() < BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
ArrayList实现了RandomAccess接口, 而LinkedList没有实现
ArrayList底层是数组,而LinkedList底层是链表
数组天然支持随机访问,时间复杂度为O(1),所以称为快速随机访问;链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为O(n),所以不支持快速随机访问
ArrayList实现了RandomAccess接口,就表明它具有快速随机访问功能,RandomAccess接口只是标识,并不是说ArrayList实现RandomAccess接口才具有快速随机访问功能的
ArrayList扩容机制
Set
Comparable和Comparator的区别
Comparable接口和Comparator接口都是Java中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用:
- Comparable接口实际上是出自java.lang包,它有一个compareTo(Object obj)方法用来排序
- Comparator接口实际上是出自java.util包,它有一个compare(Object obj1, Object obj2)方法用来排序
一般需要对一个集合使用自定义排序时,就要重写compareTo()方法或compare()方法,当需要对某一个集合实现两种排序方式,例如一个song对象中的歌名和歌手名分别采用一种排序方法,可以重写compareTo()方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collection.sort()
Comparator定制排序
ArrayList<Integer> arrayList = new ArrayList<Integer>();
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);
// void reverse(List list): 反转
Collections.reverse(arrayList);
System.out.println("Collections.reverse(arrayList):");
System.out.println(arrayList);
// void sort(List list), 按自然排序的升序排序
Collections.sort(arrayList);
System.out.println("Collections.sort(arrayList): ");
System.out.println(arrayList);
// 定制排序的用法
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);
输出:
原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后:
[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;
}
/**
* 重写compareTo方法实现按年龄来排序
*/
@Override
public int compareTo(Person o) {
if (this.age > o.getAge()) {
return 1;
}
if (this.age < o.getAge()) {
return -1;
}
return 0;
}
}
输出:
5-小红
10-王五
20-李四
30-张三
无序性和不可重复性的含义
- 无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的
- 不可重复性是指添加的元素按照equals()判断时,返回false,需要同时重写equals()方法和hashCode()方法
HashSet、LinkedHashSet、TreeSet
- HashSet、LinkedHashSet和TreeSet都是Set接口的实现类,都能保证元素唯一,并且都不是线程安全的
- HashSet、LinkedHashSet和TreeSet的主要区别在于底层数据结构不同,HashSet的底层数据结构是哈希表(基于HashMap实现),LinkedHashSet的底层数据结构是链表和哈希表,元素的插入和取出顺序满足FIFO,TreeSet底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序
- 底层数据结构不同又导致这三者的应用场景不同,HashSet用于不需要保证元素插入和取出顺序的场景,LinkedHashSet用于保证元素的插入和取出顺序满足FIFO的场景,TreeSet用于支持对元素自定义排序规则的场景
Queue
Queue与Deque的区别
Queue是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循先进先出(FIFO)规则
Queue扩展了Collection的接口,根据因为容量问题而导致操作失败后处理方式的不同可以分为两类方法:一种在操作失败后会抛出异常,另一种则会返回特殊值