Queue集合接口
Queue是单列集合下的一个接口,它和它的实现类中Java打包好了对队列的操作。
ArrayList 与 Vector
ArrayList和Vector都是List接口下的实现类,完全支持List接口的全部功能。
这两者的底层实现原理基本相同,都是基于一个动态的、允许在分配的Object[]类型的数组实现的;同时而这也有着相同的扩容机制和扩容方法。
如果创建ArrayList和Vector的对象时,没有给构造方法传递参数(调用空参构造),创建的就是一个长度默认为10 长度默认为 0 的Object数组。
当调用ArrayList的空参构造创建一个集合的时候:
创造了一个默认长度为0 的数组,当添加进元素之后,底层数组就会扩容成10;之后单独再添加元素,当存满数组就会扩容,就会扩容到原来的1.5倍;之后一次添加多个元素,如果超过了现有长度的1.5倍就会扩容至刚好容下添加的元素。
当调用含参构造时会创建一个指定长度的底层数组。疯狂Java讲义里面也介绍了两个控制容量的方法:
方法名称 | 说明 |
---|---|
void ensureCapacity(int minCapacity) | 将底层数组长度增加值大于等于minCapacity |
void trimToSize() | 将底层数组的长度调整为当前元素个数,可减少空间占用 |
Vector 和 ArrayList 的主要区别在于 Vector 是线程安全的,而ArrayList是线程不安全的。当多个线程访问同一个ArrayList集合时,如果有任何线程修改了ArrayList集合,则需要手动编写程序保证ArrayList集合的同步性;当多个线程同时访问 Vector 时,Vector 会使用 synchronized 关键字来保证线程安全,这会影响性能。因此,在单线程环境中,应该使用 ArrayList 而不是 Vector。
在 Java 1.2 之后,Collections.synchronizedList() 方法提供了一种将 ArrayList 转换成线程安全的 List 的方法,因此,除非需要使用 Vector 特有的方法,否则应该使用 Collections.synchronizedList() 方法来实现线程安全的 List。
Queue接口的常用API
方法名称 | 说明 |
---|---|
public void add(Object e) | 将指定元素加入此队列的尾部 |
public Object element() | 获取队列头部的元素,但是不删除该元素 |
public boolean offer(Object e) | 将指定元素加入到此队列尾部 |
public Object peek() | 获取队列头部的元素,但是不删除该元素。如果队列为空,返回null |
public Object poll() | 获取队列头部的元素,并删除该元素。如果队列为空,返回null |
public Object remove() | 获取队列头部元素,并删除该元素。如果队列为空,直接报错停止运行 |
当使用有容量限制的队列时,public boolean offer(Object e)方法通常比public void add(Object e)更好用。
当队列满了时,使用add(Object e)方法会抛出一个异常,而offer(Object e)方法会返回false。
当队列为空时,使用remove()方法会使程序出现异常停止运行,通常不如使用 poll()方法;
当然Queue也继承了单列集合中的
isEmpty():判断队列是否为空;
size():获取队列中的元素个数;
等方法。
PriorityQueue实现类
优先级队列:通过完全二叉树实现的小顶堆实现,属于线程不安全的队列。
特点:可排序
PriorityQueue保存队列元素的顺序不是按添加的顺序,而是按队列元素的大小(队首-->队尾 从小到大)进行重新排序。调用peek()方法或者poll()方法返回的不是最先入队列的元素,而是最小的元素。
注意:该实现类的集合不允许放入null元素。
底层原理
PriorityQueue底层是通过完全二叉树的小顶堆实现的,数据的存储使用数组(Object[] queue数组)实现的,这个数组的初始默认长度是11;堆是以二叉树的顺序存储法来用数组记录树的样子,对树进行操作。
在使用add() / offer() 添加、poll()删除队首(堆顶元素)等方法修改集合内容的时候会调用底层shiftUp()方法,该方法会根据指定的自定义规则对堆中的元素进行维护;如果没有自定义比较规则,就会按照自然排序规则进行维护。
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x, queue, comparator);
else
siftUpComparable(k, x, queue);
}
peek()查看堆顶元素,但不删除。
注意:要排序数据的时候不能直接遍历集合。
//类外导包
import java.util.PriorityQueue;
...
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.offer(9);
queue.offer(3);
queue.offer(7);
queue.offer(-1);
queue.offer(8);
queue.offer(20);
//按照队列现先进先出的规则:9 3 7 -1
//使用Lambda表达式遍历
queue.forEach(i -> System.out.print(i + " "));//-1 3 7 9 8 20
System.out.println();
while(!queue.isEmpty()){
System.out.print(queue.poll() + " ");
}//-1 3 7 8 9 20
}
注意:因为PriorityQueue集合底层是一个顺序存储二叉树的一个数组;所以要想要给元素排序不能直接遍历集合;而是要将元素逐个出队(poll()方法)。
构造方法
构造方法 | 说明 |
---|---|
PriorityQueue() | 使用默认的容量(11)创建一个队列,元素排序采用自然排序 |
PriorityQueue(int initialCapacity) | 使用指定的容量创建一个队列,元素排序采用自然规则 |
PriorityQueue(Comparator<? super E> comparator) | 使用默认的容量(11)创建一个队列,元素排序采用自定义的comparator |
排序方式
PriorityQueue有两大种排序方式:
自然排序:采用自然排序的PriorityQueue集合的元素的类必须实现了Comparable接口,并且重写了接口中的compareTo()方法;
自定义排序:创建PriorityQueue是调用构造函数PriorityQueue(Comparator<? super E> comparator);传入一个Comparator对象,该对象负责堆集合中的元素进行排序;使用这种方法时不要求实现Comparable接口。
自然排序
核心是继承Comparable接口,重写compareTo()方法
Java的包装类Integer、String等都是重写了compareTo的。如果需求符合他们重写的比较规则,那么可以直接使用无参构造。
对于数值类型:Integer、Double,默认按照从小到大排序;
对于字符、字符串类型:按照逐个字符的ASCII码表的从小到大排序(字典排序);compareTo();
但如果是自定义的类就需要手动实现Comparable接口,重写compareTo()方法。
class Student implements Comparable<Student>{
private int age;
private String name;
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return "Student{age = " + age + ", name = " + name + "}";
}
@Override
public int compareTo(Student o) {
//只看年龄排序
int result = this.age - o.age;//从小到大 ---> 前 - 后 从大到小 ---> 后 - 前
return result;
}
}
这是自定义的一个Student类,有年龄和姓名两个私有字段;
现在要求按照年龄从小到大排序。就是上面的代码。
排序的规则:
this:表示当前要添加的元素;
o:表示队列中已经存在的元素;
返回值:负值:认为要添加的元素小,存左边;
正值:认为要添加的元素大,存右边;
零:认为要添加的元素已经存在,舍弃;
在主函数中测试:
import java.util.PriorityQueue;
public class PriorityQueueDomo02 {
public static void main(String[] args) {
PriorityQueue<Student3> pq = new PriorityQueue<>();
pq.offer(new Student3(18,"zhansan"));
pq.offer(new Student3(22,"zhansan"));
pq.offer(new Student3(22,"zhansam"));
pq.offer(new Student3(10,"lisi"));
while(!pq.isEmpty()){
System.out.println(pq.poll());
}
}
}
自定义排序
核心通过比较器
1)构造比较的类或内部类
class CompareMethod implements Comparator<String>{
@Override
public int compare(String o1, String o2) {
//需求:排序字符串
/*
* 1.长度大的排在前
* 2.长度相同,逐个比对,ASCII码大的排在前
* */
int i = o2.length() - o1.length();
i = i == 0 ? o2.compareTo(o1) : i;
return i;
}
}
构造了一个专门比较的类,类继承Comparator,重写compare()方法制定排序规则;
因为这种方法时构造了一个比较器的实现类,所以一定要使用带参数的构造方法;PriorityQueue(Comparator<? super E>
main方法测试:
import java.util.Collections;
import java.util.Comparator;
import java.util.PriorityQueue;
public class PriorityQueueDomo02 {
public static void main(String[] args) {
//需求:排序字符串
/*
* 1.长度大的排在前
* 2.长度相同,逐个比对,ASCII码大的排在前
* */
PriorityQueue<String> pq = new PriorityQueue<>(new CompareMethod());
Collections.addAll(pq, "abs","abc","b","ij","a");
while(!pq.isEmpty()){
System.out.println(pq.poll());
}
}
}
运行测试结果:
class Student implements Comparator<Student>{
private int age;
private String name;
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return "Student{age = " + age + ", name = " + name + "}";
}
@Override
public int compare(Student o1, Student o2) {
//只看年龄排序(从小到大)
return o1.getAge() - o2.getAge();
}
}
main方法测试:
import java.util.PriorityQueue;
public class PriorityQueueDomo02 {
public static void main(String[] args) {
PriorityQueue<Student> pq = new PriorityQueue<>(new Student());
pq.offer(new Student(18,"zhansan"));
pq.offer(new Student(22,"zhansan"));
pq.offer(new Student(22,"zhansam"));
pq.offer(new Student(10,"lisi"));
while(!pq.isEmpty()){
System.out.println(pq.poll());
}
}
}
运行测试结果:
2)Comparator是一个函数式接口(Functional interface),所以可以写匿名内部类、Lambda表达式和方法引用的方式。
-
匿名内部类
import java.util.Collections;
import java.util.Comparator;
import java.util.PriorityQueue;
public class PriorityQueueDomo02 {
public static void main(String[] args) {
PriorityQueue<String> pq = new PriorityQueue<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//需求:排序字符串
/*
* 1.长度大的排在前
* 2.长度相同,逐个比对,ASCII码大的排在前
* */
int i = o2.length() - o1.length();
return i == 0 ? o2.compareTo(o1) : i;
}
});
Collections.addAll(pq, "abs","abc","b","ij","a");
while(!pq.isEmpty()){
System.out.println(pq.poll());
}
}
}
-
匿名内部类改写成Lambda表达式
import java.util.Collections;
import java.util.PriorityQueue;
public class PriorityQueueDomo02 {
public static void main(String[] args) {
PriorityQueue<String> pq = new PriorityQueue<>((o1, o2) -> (o2.length() - o1.length()) == 0 ? o2.compareTo(o1) : o2.length() - o1.length());
Collections.addAll(pq, "abs","abc","b","ij","a");
while(!pq.isEmpty()){
System.out.println(pq.poll());
}
}
}
-
方法引用
我这里写的在 main方法同类的 myCompare() 静态方法。不同的类型引用方法不太一样。
import java.util.Collections;
import java.util.PriorityQueue;
public class PriorityQueueDomo02 {
public static void main(String[] args) {
PriorityQueue<String> pq = new PriorityQueue<>(PriorityQueueDomo02::myCompare);
Collections.addAll(pq, "abs","abc","b","ij","a");
while(!pq.isEmpty()){
System.out.println(pq.poll());
}
}
public static int myCompare(String o1, String o2){
int i = o2.length() - o1.length();
return i == 0 ? o2.compareTo(o1) : i;
}
}
Comparable接口 和 Comparator接口
Comparable接口的方法是 public int compareTo(Object o)
它的比较是类中this和o的比较;
排序的规则:
this:表示当前要添加的元素;
o:表示队列中已经存在的元素;
Comparable接口完成的是一个有比较方法的数据类型 <T>,在主函数调用时不需要参数。
Comparator接口的方法是 public int compare(Object o1, Object o2)
排序的规则:
o1:表示当前要添加的元素;
o2:表示队列中已经存在的元素;
Comparator接口实现的是一个比较器,在主函数调用的时候作为PriorityQueue构造函数的参数。
返回值都是:负值:认为要添加的元素小,存左边;
正值:认为要添加的元素大,存右边;
零:认为要添加的元素已经存在,舍弃;
小结
使用原则:默认使用自然排序方式,如果第一种不能满足需求,就使用自定义排序方法。
如果自然排序方法和自定义排序方法都存在,那么以自定义排序方法为准。
PriorityQueue的排序方法定义和TreeSet一样。
使用场景:主要用于排序输出(例如入队一些元素,出队时按从小到大或从大到小输出),而可以用来模拟大小顶堆。
Deque接口
Deque接口是Queue接口的子接口,他代表一个双端队列,里面定义了一些双端队列的方法,这些方法允许从两端操作队列的元素,即可以从队列两端(头部和尾部)添加、删除、查看元素。
Deque接口有两个实现类:ArrayDeque 和 LinkedList,其中ArrayDeque不允许添加null元素,添加null元素会直接抛出异常,停止程序运行。
Deque的一些常用API
Deque用 first 代表队首的元素 last 代表队尾的元素。
-
会抛出异常的方法
-
-
添加元素
-
public void addFrist(Object e):将指定元素插入双端队列的开头
public void addLast(Object e):将指定元素插入双端队列的结尾
-
-
移除元素
public Object removeFirst():获取并删除该双端队列的第一个元素
public Object removeLast():获取并删除该双端队列的最后一个元素
-
-
-
检查元素
public Object getFirst():获取但不删除该双端队列的第一个元素
public Object getLast():获取但不删除该双端队列的最后一个元素
-
-
会返回特殊值的方法
-
-
添加元素
-
public boolean offerFirst(Object e):将指定元素插入该双端队列的开头
public boolean offerLast(Object e):将指定元素插入该双端队列的结尾
-
-
移除元素
public Object pollFirst():获取并删除该双端队列的第一个元素,如果队列为空,返回null
public Object pollLast():获取并删除该双端队列的最后一个元素,如果队列为空,返回null
-
-
-
检查元素
public Object peekFirst():获取但不删除该双端队列的第一个元素,如果队列为空,返回null
public Object peekLast():获取但不删除该双端队列的最后一个元素,如果队列为空,返回null
-
另外Deque接口中还有两个栈方法
public Object pop():将栈顶元素出栈,相当于removeFirst/pollFrist;
public void push():将元素压入栈顶,相当于addFirst/offerFirst;
还有public Object peek()方法,这个是队列方法和栈方法共用的一个方法。既可以查看栈顶元素,也可以查看队首元素。
注意这pop方法遇到栈空的特殊情况会直接抛出异常,所以使用的时候适当结合isEmpty方法判断栈是否为空。
Deque的实现类
ArrayDeque
底层实现是数组,创建Deque是可以指定一个numElements参数,该参数用于指定Object[] 数组的长度;如果不指定numElements参数,Deque底层数组的长度为16。
LinkedList
底层实现是链表。
对比两个实现类;ArratDeque和ArrayList一样是通过数组储存集合中的元素的,因此他们的随机访问集合元素的能力较强,但增加、删除元素的效率低。
因为LinkedList底层是靠链表储存集合元素的,所以他增加、删除元素的效率高,但随机访问集合元素的效率低;
两者看情况使用。
Deque--队列
验证双端队列作为队列的用法
import java.util.ArrayDeque;
public class ArrayDequeDomo02 {
public static void main(String[] args) {
ArrayDeque<Integer> ad = new ArrayDeque<>();
ad.offer(1);
ad.offer(2);
ad.offer(3);
ad.offer(4);
System.out.println("查看队首元素" + ad.peek());//1
while(!ad.isEmpty()){
System.out.println(ad.poll());//1 2 3 4
}
}
}
Deque是Queue的子接口,自然有普通队列的方法;
offer方法调用了offerLast实现;peek方法调用了peekFirst实现;poll方法调用了pollFirst实现。
Deque--栈
验证双端队列当作栈的用法
import java.util.ArrayDeque;
public class ArrayDequeDomo01 {
public static void main(String[] args) {
//双端队列作为栈使用
ArrayDeque<Integer> ad = new ArrayDeque<Integer>();
ad.push(1);
ad.push(2);
ad.push(3);
ad.push(4);
System.out.println("查看栈顶元素 " + ad.peek());//4
System.out.println("输出栈元素");
while(!ad.isEmpty()){
System.out.println(ad.pop());//4 3 2 1
}
}
}
这里演示的是ArrayDeque作为栈的使用。另一个实现栈的类Stack是集合Vector的子类,在前面就说过了Vector的特点:线程安全,效率低,性能差;Stack类也有这样的特点。所以一般推荐使用ArrayDeuqe(或者LinkedList)代替Stack使用。
拓展-阻塞队列接口BlockingQueue
主要通过put():将元素入队,如果队列已满,就等待队列不满的时候;take():将队首元素出队,如果队列为空,等待队列加入元素;完成对队列的阻塞操作。在多线程中使用。