目录
前言
Java中有许多类型的集合,我们比较常用的ArrayList也是集合的一种。
Java中集合的体系结构分为Collection单列集合以及Map双列集合。
下面介绍单列集合。
一、Collection集合
红色的是接口,蓝色的代表接口的实现类。
List系列集合:添加的元素是有序、可重复、有索引。
Set系列集合:添加的元素是无序、不重复、无索引。
1.常用方法
Collection是单列集合的祖宗接口,它的功能所有的单列集合都可以继承使用。下面这些方法List系列和Set系列的集合都可以使用。
对于contains方法,我们需要注意,它在底层是以来equals方法进行判断是否存在的。所以,如果集合中存储的是自定义对象,也想通过contains方法来判断是否包含,那么在Javabean类中,必须重写euqals方法。
2.Collection的遍历方法
因为Collection中有Set系列的集合,所以我们不能直接用索引来遍历Collection集合。所以一共有三种方法来进行遍历,分别是迭代器遍历、增强for遍历、Lambda表达式遍历。List集合和Set集合都可以用这三种方法来进行遍历。
a.迭代器遍历
迭代器在Java中的类是Iterator,迭代器是集合专用的遍历方式。
当我们创建了一个迭代器对象时,指针会指向第一个元素。执行完next方法后,指针会向后移,直到遍历完集合。当指针不再指向集合元素时,再执行next方法,就会报错NoSuchElementException异常。迭代器遍历完毕之后,指针不会复位。同时,迭代器遍历时,不能用集合的方式进行增加或者删除。如果非要删除的话,只能用迭代器提供的remove方法进行删除。
public class CollectDemo1 {
public static void main(String[] args) {
// 创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("A");
coll.add("B");
coll.add("C");
coll.add("D");
//1. 获取迭代器对象
Iterator<String> it = coll.iterator();
//2. 判断当前指针是否有元素,并打印
// 在遍历过程中,不能用集合的方法增加或者删除元素
while (it.hasNext()) {
String str = it.next();
System.out.println(str);
if (str.equals("B")) {
// 迭代器类中提供的remove方法
it.remove();
}
}
// 遍历结束后,我们就可以用Collection提供的方法来对集合进行操作
System.out.println(coll);
}
}
b.增强for遍历
增强for的底层就是迭代器,为了简化迭代器的书写。所有的单列集合以及数组才能使用增强for进行遍历。
public class CollectDemo1 {
public static void main(String[] args) {
// 创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("A");
coll.add("B");
coll.add("C");
coll.add("D");
// 增强for遍历
for (String s : coll) {
System.out.println(s);
}
}
}
细节:修改增强for中的变量,不会改变集合中原本的数据。增强for中的s就是一个第三方变量,改变它的值,不会影响原集合。
c.Lambda表达式
利用该方法对集合进行遍历。Consumer在底层源码中是一个接口,所以我们传入的参数应该是Consumer的实现类对象。
public class CollectionDemo2 {
public static void main(String[] args) {
/*
default void forEach(Consumer<? super T> action):
*/
// 1. 创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("zhansgan");
coll.add("liss");
coll.add("wangwu");
//2. 使用匿名内部类的形式
// 底层原理:
// 其实也会自己遍历集合,依次得到每一个元素
// 把得到的每一个元素(s就是得到的每一个元素),传递给下面的accept方法
coll.forEach(new Consumer<String>() {
@Override
// s依次表示集合中的每一个数据
public void accept(String s) {
System.out.println(s);
}
});
//3. 利用Lambda表达式
coll.forEach(s -> System.out.println(s));
}
}
二、List集合
1.List中特有的方法
List中存在索引,所以多了许多索引操作的方法。
public class ListDemo1 {
public static void main(String[] args) {
// 1.创建一个集合
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
// add方法
list.add(1, "QQQ");
// remove方法
String s = list.remove(2);
System.out.println(s);
System.out.println(list);
}
}
2.List集合的遍历方式
除了在Collection集合中所提及的三种遍历方式,List集合还有两种自己独有的遍历方式-------列表迭代器遍历以及普通for循环。
a.普通for循环遍历
利用get方法获取指定索引处的元素。
public class ListDemo2 {
public static void main(String[] args) {
// 1.创建一个集合
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
// 普通for循环,
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
}
}
b.列表迭代器遍历
列表迭代器与Collection的迭代器类似。只不过后者创建的是Iterator对象,前者创建的是ListIterator对象。相比于迭代器,列表迭代器可以在遍历的时候添加元素,但是只能使用ListIterator自身的add方法进行添加。
public class ListDemo2 {
public static void main(String[] args) {
// 1.创建一个集合
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
// 列表迭代器ListIterator,跟迭代器Iterator相似
// 额外添加了一个方法:在遍历过程中,可以添加元素
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
String str = lit.next();
if ("bbb".equals(str)) {
// 在遍历过程中向集合中添加元素
lit.add("QQQ");
}
}
System.out.println(list);
}
}
c.五种遍历方式对比
- 迭代器遍历:在遍历过程中需要删除元素,使用迭代器。
- 列表迭代器:在遍历过程中需要添加元素,使用列表迭代器。
- 增强for、Lambda表达式:仅仅想要遍历,使用这两种方式。
- 普通for:如果遍历的时候想要操作索引,可以使用普通for遍历。
三、ArrayList集合
ArrayList可以使用Collection和List的所有方法,所以这里不在额外说其他的方法。说一下ArrayList集合的底层原理。ArrayList底层利用的是数组这个数据结构。
- 利用空参创建集合,在底层会创建一个默认长度为0的数组,这个数组的名字是elementData,并且会创建一个变量size,用来记录数组中的元素个数。
- 添加第一个元素时,底层会创建一个新的长度为10的数组。
- 当数组存满时,会扩容1.5倍。
- 如果一次添加多个元素,1.5倍还放不下,则新创建的数组的长度以实际长度为准。例如我一次添加100个元素,则新创建的数组长度为110.
四、LinkedList集合
LinkedList集合底层数据结构是双向链表,查询慢、增删快。但是如果操作的是首尾元素,速度也很快。所以多了许多首尾操作特有的API。
下图表示底层源码和结构,在创建LinkedList对象的时候,会直接创建一个头结点和尾结点:
五、泛型
泛型是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。
泛型的格式:<数据类型>,只支持引用数据类型。如果不写泛型,类型默认是Object。
Java之中的泛型是伪泛型,例如此时我们有一个集合, ArrayList arr = new ArrayList<>();,指定的泛型只会检查加入集合的数据是不是String类型的,但等到真正加入到集合中时,集合还是会把这些数据当做是Object类型,只不过取出来的时候,集合的底层会吧Object类型重新转换为String类型。
如下图所示,Java代码在编译的时候,就会消除泛型,叫做泛型的擦除。
1.泛型类
使用场景:当一个类中,某个变量的数据类型不确定的时候,就可以定义带有泛型的类。
此处的E可以理解为一个变量,但是不是用来记录数据的,而是记录数据的类型,可以写成T、E、K、V等。
下面的代码中,我们定义了一个泛型类。
public class MyArrayList<E>{
// 因为不确定类型,所以我们统一用Object数组还存数据,运用了多态的原理
Object[] obj = new Object[10];
int size;
// 不确定要添加元素的类型,所以使用泛型
public boolean add(E e) {
obj[size] = e;
size++;
return true;
}
// 泛型方法
public E get(int index) {
return (E)obj[index];
}
@Override
public String toString() {
return Arrays.toString(obj);
}
}
下面是测试类
public class GenericsDemo1 {
public static void main(String[] args) {
// 创建所定义泛型的对象,然后添加元素。
MyArrayList<String> arr = new MyArrayList<>();
arr.add("abd");
arr.add("abc");
arr.add("ae");
String str = arr.get(1);
System.out.println(str);
System.out.println(arr);
}
}
2.泛型方法
当方法中形参类型不确定时,我们就可以在方法上声明泛型。
定义一个工具类,下面的addAll方法我们不知道形参传入什么类型,所以定义泛型:
public class ListUtil {
private ListUtil() {}
// 泛型方法,不知道形参需要传入什么类型的数据
// T...t表示我们可以传入任意个T类型的元素
public static <T> void addAll(ArrayList<T> arr, T...t) {
for (T element: t) {
arr.add(element);
}
}
}
这是测试类:
public class GenericsDemo2 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
ListUtil.addAll(list, 1, 2, 3, 4);
ListUtil.addAll(list2, "123", "zhansgan");
System.out.println(list);
System.out.println(list2);
}
}
3.泛型的通配符
泛型不具备继承性,数据具备继承性。
在下面的代码中,我们可以发现Fu继承Ye,Zi继承Fu,但是Fu类型和Zi类型的集合都不能传入method方法中。此时我们就需要用到泛型的通配符““?”。
? 也可以表示不确定的类型,进行类型的限定
? extends E: 表示可以传递E或者E所有子类类型
? super E: 表示可以传递E或者E所有的父类类型
public class GenericsDemo3 {
public static void main(String[] args) {
ArrayList<Ye> list1 = new ArrayList<>();
ArrayList<Fu> list2 = new ArrayList<>();
ArrayList<Zi> list3 = new ArrayList<>();
ArrayList<Student> list4 = new ArrayList<>();
method(list1);
method(list2);
method(list3);
// 这里传递学生类型就会报错
method(list4);
}
// 本方法虽然不确定类型,但是我希望只能传递Ye,Fu,Zi
// 此时就可以使用泛型的通配符:
// ?也可以表示不确定的类型,进行类型的限定
// ? extends E: 表示可以传递E或者E所有子类类型
// ? super E: 表示可以传递E或者E所有的父类类型
// 这里表明,形参可以传递Ye以及它的多有子类类型
public static void method(ArrayList<? extends Ye> arr){
}
}
class Ye{}
class Fu extends Ye{}
class Zi extends Fu{}
class Student{}
六、Set集合
Set系列集合是无序、不重复、无索引的。同时Set集合没有什么额外的方法,基本上与Collection的API一致。
无序:存取顺序不一致
无重复:可以去除重复
无索引:没有带索引的方法,所以不能使用普通for循环进行遍历,也不能通过索引来获取元素。
在了解Set集合之前,我们要先知道二叉树这种数据结构,以及二叉排序树、平衡二叉树、红黑树等等。这里只讲解红黑树。
1.红黑树
红黑树是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色。每一个节点可以是红或者黑,红黑树不是高度平衡的,它的平衡是通过“红黑规则”实现的。
下图就是一个标准的红黑树,最下面的Nil就代表的是叶子结点。
红黑树在添加节点的时候,添加的结点默认是红色的。 有如下的规则。
我们通过20、18、23、22、17、24、19、15、14来创建红黑树,过程如下:
通过上面添加节点规则,就可以创建出一个红黑树。
2.HashSet
HashSet集合无序、不重复、无索引。
a.底层原理
HashSet集合底层采取哈希表存储数据。JDK8以前哈希表由数组+链表组成,JDK8以后哈希表由数组+链表+红黑树组成。
对于一个数据在哈希表中的索引,是通过哈希值进行计算的。如下图所示,我们通过公式计算出该元素在哈希表中的索引是4。
自定义对象时,一定要重写hashcode方法,否则将对象加入到Set集合时会直接报错。
加载因子为0.75,当哈希表中存满16x0.75 = 12个元素时,这个数组就会扩容为原来的两倍。
同时当链表的长度大于8而且数组长度大于64的时候,挂在哈希表上的链表就会自动转换成红黑树。
b.LinkedHashSet
LinkedHashSet继承于HashSet,但是它是有序的。可以保证存储和取出的元素顺序一致。
底层数据结构依然是哈希表,只是每个元素额外多了一个双链表的机制记录存储的顺序。
public class HashSetDemo1 {
public static void main(String[] args) {
/*
存储多个学生对象,如果学生对象属性值相同,则认为是同一个对象
*/
HashSet<Student> hs = new HashSet<>();
LinkedHashSet<Student> lhs = new LinkedHashSet<>();
lhs.add(new Student("lisi", 23));
lhs.add(new Student("zhansgan", 23));
lhs.add(new Student("zhansgan", 23));
lhs.add(new Student("wangwu", 23));
lhs.add(new Student("zhansgan", 23));
for (Student h : lhs) {
System.out.println(h.toString());
}
}
}
class Student {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
// 重写的hashCode方法
public int hashCode() {
return Objects.hash(name, age);
}
public String toString() {
return "Student{name = " + name + ", age = " + age + "}";
}
}
使用LinkedHashSet,最终结果是有序的。
3.TreeSet
TreeSet的特点是不重复、无索引、可排序。可排序指的是按照元素的默认规则(由小倒大)进行排序。
TreeSet的集合底层是基于红黑树的数据结构进行排序的,增删改查性能都比较良好。
a. 第一种排序方式
默认排序:Javabean类实现Comparable接口指定排序规则。
public class TreeSetDemo1 {
public static void main(String[] args) {
/*
TreeSet会将数据进行自动排序
*/
TreeSet<Integer> ts = new TreeSet<>();
TreeSet<Teacher> ts2 = new TreeSet<>();
// 如果添加自定义对象,我们要对排序方法进行重写
ts2.add(new Teacher("zhansgan", 24));
ts2.add(new Teacher("lisu", 23));
System.out.println(ts2);
}
}
// 需要实现Comparable这个接口,然后重写compareTo方法,才能对自定义对象进行排序
class Teacher implements Comparable<Teacher>{
private String name;
private int age;
public String toString() {
return "Teacher{name = " + name + ", age = " + age + "}";
}
@Override
// this:表示要添加的元素
// o:表示红黑树中已经存在的元素
// 这里是按年龄进行排序
public int compareTo(Teacher o) {
return this.getAge() - o.getAge();
}
}
b. 第二种排序方式
第二种排序方式是利用比较器进行比较。创建TreeSet对象时候,传递比较器Comparator指定规则。
我们默认使用第一种,如果第一种不能满足当前需求,那么我们就使用第二种。
public class TreeSetDemo2 {
public static void main(String[] args) {
/*
使用比较器进行排序
存入四个字符串,"c", "ab", "df", "qwer"
按照长度排序,如果一样长按照首字母进行排序
*/
// TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
// @Override
// public int compare(String o1, String o2) {
// // 按照长度排序
// int i = o1.length() - o2.length();
// // 如果长度相同
// i = i == 0 ? o1.compareTo(o2) : i;
// return i;
// }
// });
// Lambda表达式
TreeSet<String> ts = new TreeSet<>((o1, o2) -> {
// 按照长度排序
int i = o1.length() - o2.length();
// 如果长度相同
i = i == 0 ? o1.compareTo(o2) : i;
return i;
});
ts.add("c");
ts.add("ab");
ts.add("df");
ts.add("gwer");
System.out.println(ts);
}
}
4.Set特有的方法
a. 求两个set集合的交集
七、ArrayDeque双端队列
在 Java 中,ArrayDeque 是一个基于动态循环数组实现的双端队列(Double-Ended Queue),它既可以用作队列(FIFO),也可以用作栈(LIFO)。相较于 LinkedList,ArrayDeque 在大多数操作中具有更高的性能(时间复杂度更低且内存占用更紧凑)。
ArrayDeque可以作为双端队列、队列以及栈进行使用。
栈也可以使用Stack类来实现,但是Stack继承于Vector, 不推荐使用。
ArrayDeque<Integer> deque = new ArrayDeque<>();
deque.addFirst(10); // 头部插入
deque.addLast(20); // 尾部插入
System.out.println(deque.removeFirst()); // 输出 10
System.out.println(deque.removeLast()); // 输出 20
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(1); // 入队
queue.offer(2);
System.out.println(queue.poll()); // 出队,输出 1
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 压栈
stack.push(2);
System.out.println(stack.pop()); // 弹栈,输出 2
八、PriorityQueue堆类
PriorityQueue 是 Java 中基于优先级堆(通常是最小堆)实现的队列,继承自 AbstractQueue 类。
PriorityQueue默认为小根堆,若要创建大根堆,需要利用比较器。
import java.util.PriorityQueue;
import java.util.Collections;
public class MaxHeapExample {
public static void main(String[] args) {
// 方法1:使用 Collections.reverseOrder()
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// 方法2:自定义比较器 (o2 - o1)
PriorityQueue<Integer> maxHeap2 = new PriorityQueue<>((a, b) -> b - a);
// 添加元素
maxHeap.add(3);
maxHeap.add(1);
maxHeap.add(5);
// 输出堆顶(最大值)
System.out.println(maxHeap.peek()); // 输出: 5
}
}
核心方法如下:
PriorityQueue底层使用动态数组存储元素,表现为完全二叉树。