详解Java中PriorityQueue的作用和源码实现

 更新时间:2024年02月04日 08:33:53   作者:一灯架构  
这篇文章主要为大家详细介绍了Java中阻塞队列PriorityQueue的作用和源码实现的相关知识,文中的示例代码讲解详细,需要的小伙伴可以了解下

引言

前面文章我们讲解了ArrayBlockingQueueLinkedBlockingQueue源码,这篇文章开始讲解PriorityQueue源码。从名字上就能看到ArrayBlockingQueue是基于数组实现的,而LinkedBlockingQueue是基于链表实现,而PriorityQueue是基于什么数据结构实现的,看不出来,好像是实现了优先级的队列。

由于PriorityQueue跟前几个阻塞队列不一样,并没有实现BlockingQueue接口,只是实现了Queue接口,Queue接口中定义了几组放数据和取数据的方法,来满足不同的场景。

操作抛出异常返回特定值
放数据add()offer()
取数据(同时删除数据)remove()poll()
查看数据(不删除)element()peek()

这两组方法的区别是:

  • 当队列满的时候,再次添加数据,add()会抛出异常,offer()会返回false。
  • 当队列为空的时候,再次取数据,remove()会抛出异常,poll()会返回null。

PriorityQueue也会有针对这几组放数据和取数据方法的具体实现。

类结构

先看一下PriorityQueue类里面有哪些属性:

public class PriorityQueue<E> 
        extends AbstractQueue<E>
        implements java.io.Serializable {

    /**
     * 数组初始容量大小
     */
    private static final int DEFAULT_INITIAL_CAPACITY = 11;

    /**
     * 数组,用于存储元素
     */
    transient Object[] queue; // non-private to simplify nested class access

    /**
     * 元素个数
     */
    private int size = 0;

    /**
     * 比较器,用于排序元素优先级
     */
    private final Comparator<? super E> comparator;

}

可以看出PriorityQueue底层是基于数组实现的,使用Object[]数组存储元素,并且定义了比较器comparator,用于排序元素的优先级。

初始化

PriorityQueue常用的初始化方法有4个:

  • 无参构造方法
  • 指定容量大小的有参构造方法
  • 指定比较器的有参构造方法
  • 同时指定容量和比较器的有参构造方法
/**
 * 无参构造方法
 */
PriorityQueue<Integer> blockingQueue1 = new PriorityQueue<>();

/**
 * 指定容量大小的构造方法
 */
PriorityQueue<Integer> blockingQueue2 = new PriorityQueue<>(10);

/**
 * 指定比较器的有参构造方法
 */
PriorityQueue<Integer> blockingQueue3 = new PriorityQueue<>(Integer::compareTo);

/**
 * 同时指定容量和比较器的有参构造方法
 */
PriorityQueue<Integer> blockingQueue4 = new PriorityQueue<>(10, Integer::compare);

再看一下对应的源码实现:

/**
 * 无参构造方法
 */
public PriorityQueue() {
    // 使用默认容量大小11,不指定比较器
    this(DEFAULT_INITIAL_CAPACITY, null);
}

/**
 * 指定容量大小的构造方法
 */
public PriorityQueue(int initialCapacity) {
    this(initialCapacity, null);
}

/**
 * 指定比较器的有参构造方法
 */
public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}

/**
 * 同时指定容量和比较器的有参构造方法
 */
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
    if (initialCapacity < 1) {
        throw new IllegalArgumentException();
    }
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

可以看出PriorityQueue的无参构造方法使用默认的容量大小11,直接初始化数组,并且没有指定比较器。

放数据源码

放数据的方法有2个:

操作抛出异常返回特定值
放数据add()offer()

offer方法源码

先看一下offer()方法源码,其他放数据方法逻辑也是大同小异,都是在链表尾部插入。 offer()方法在队列满的时候,会直接返回false,表示插入失败。

/**
 * offer方法入口
 *
 * @param e 元素
 * @return 是否插入成功
 */
public boolean offer(E e) {
    // 1. 判空,传参不允许为null
    if (e == null) {
        throw new NullPointerException();
    }
    modCount++;
    int i = size;
    // 2. 当数组满的时候,执行扩容
    if (i >= queue.length) {
        grow(i + 1);
    }
    size = i + 1;
    // 3. 如果是第一次插入,就直接把元素插入到数组头部
    if (i == 0) {
        queue[0] = e;
    } else {
        // 4. 如果不是第一次插入,就找个合适的位置插入(需要保证插入后数组有序)
        siftUp(i, e);
    }
    return true;
}

offer()方法逻辑也很简单,先判断是否需要扩容,如果需要扩容先执行扩容逻辑,然后把元素插入到数组中。如果是第一次插入,就直接把元素插入到数组头部。如果不是,就找个合适的位置插入,需要保证插入后数组仍是有序的。 再看一下扩容的源码:

/**
 * 扩容
 */
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // 1. 如果原数组容量小于64,就执行2倍扩容,否则执行1.5扩容
    int newCapacity = oldCapacity + 
            ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
    // 2. 校验最大容量不能超过Integer最大值
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        newCapacity = hugeCapacity(minCapacity);
    }
    // 3. 直接扩容后新数组赋值给原数组
    queue = Arrays.copyOf(queue, newCapacity);
}

扩容的源码设计充满了作者的巧思,在数组容量较小的时候,为了避免频繁扩容,就采用2倍扩容法。在数组容量较大的时候,为了避免扩容后浪费空间,就采用1.5倍扩容法。

PriorityQueue为了快速的插入和删除,采用了最小堆,而不是直接使用有序数组,这样既可以保证插入和删除的时间复杂度都是O(logn),又能避免移动过多元素。

最小堆的定义: 除叶子节点外,每个节点的值都小于等于左右子节点的值。

下面就是一个简单的最小堆和映射数组:

再看一下siftUp()方法源码,是怎么保证插入元素,数组仍是有序的? 其实就是循环跟父节点比较元素大小,找个合适的位置插入。

// 把元素插入到合适的位置
private void siftUp(int k, E x) {
    // 1. 如果初始化的时候,自定义了比较器,就使用自定义比较器的插入方法,否则使用默认的。
    if (comparator != null) {
        siftUpUsingComparator(k, x);
    } else {
        siftUpComparable(k, x);
    }
}

// 自定义比较器的插入方法
private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        // 1. 找到父节点
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        // 2. 如果当前节点元素比父节点的元素小,就把父节点元素向下移动(给当前元素腾出位置)
        if (comparator.compare(x, (E) e) >= 0) {
            break;
        }
        queue[k] = e;
        k = parent;
    }
    // 3. 把当前元素插入到父节点的位置
    queue[k] = x;
}

// 默认的插入方法
private void siftUpComparable(int k, E x) {
    // 1. 使用默认比较器
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        // 2. 找到父节点
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        // 3. 如果当前节点元素比父节点的元素小,就把父节点元素向下移动(给当前元素腾出位置)
        if (key.compareTo((E) e) >= 0) {
            break;
        }
        queue[k] = e;
        k = parent;
    }
    // 4. 把当前元素插入到父节点的位置
    queue[k] = key;
}

再看一下add()方法源码:

add方法源码

add()方法底层直接调用的是offer()方法,作用相同。

/**
 * add方法入口
 *
 * @param e 元素
 * @return 是否添加成功
 */
public boolean add(E e) {
    return offer(e);
}

弹出数据源码

弹出数据(取出数据并删除)的方法有2个:

操作抛出异常返回特定值
取数据(同时删除数据)remove()poll()

poll方法源码

看一下poll()方法源码,其他方取数据法逻辑大同小异,都是从数组头部弹出元素。 poll()方法在弹出元素的时候,如果队列为空,直接返回null,表示弹出失败。

/**
 * poll方法入口
 */
public E poll() {
    // 1. 如果数组为空,返回null
    if (size == 0) {
        return null;
    }
    int s = --size;
    modCount++;
    // 2. 暂存数组头节点,最后返回
    E result = (E) queue[0];
    // 3. 暂存数组尾节点,调整最小堆的时候,需要上移
    E x = (E) queue[s];
    // 4. 删除尾节点
    queue[s] = null;
    // 5. 调整最小堆
    if (s != 0) {
        siftDown(0, x);
    }
    return result;
}

remove方法源码

再看一下remove()方法源码,如果队列为空,remove()会抛出异常。

/**
 * remove方法入口
 */
public E remove() {
    // 1. 直接调用poll方法
    E x = poll();
    // 2. 如果取到数据,直接返回,否则抛出异常
    if (x != null) {
        return x;
    } else {
        throw new NoSuchElementException();
    }
}

查看数据源码

再看一下查看数据源码,查看数据,并不删除数据。

操作抛出异常返回特定值
查看数据(不删除)element()peek()

peek方法源码

先看一下peek()方法源码,如果数组为空,直接返回null。

/**
 * peek方法入口
 */
public E peek() {
    // 返回数组头节点
    return (size == 0) ? null : (E) queue[0];
}

element方法源码

再看一下element()方法源码,如果队列为空,则抛出异常。

/**
 * element方法入口
 */
public E element() {
    // 1. 调用peek方法查询数据
    E x = peek();
    // 2. 如果查到数据,直接返回
    if (x != null) {
        return x;
    } else {
        // 3. 如果没找到,则抛出异常
        throw new NoSuchElementException();
    }
}

总结

这篇文章讲解了PriorityQueue阻塞队列的核心源码,了解到PriorityQueue队列具有以下特点:

  • PriorityQueue实现了Queue接口,提供了两组放数据和读数据的方法,来满足不同的场景。
  • PriorityQueue底层基于数组实现,按照最小堆存储,实现了高效的插入和删除。
  • PriorityQueue初始化的时候,可以指定数组长度和自定义比较器。
  • PriorityQueue初始容量是11,当数组容量小于64,采用2倍扩容,否则采用1.5扩容。
  • PriorityQueue每次都是从数组头节点取元素,取之后需要调整最小堆。

今天一起分析了PriorityQueue队列的源码,可以看到PriorityQueue的源码非常简单,没有什么神秘复杂的东西,下篇文章再一起接着分析其他的阻塞队列源码。

以上就是详解Java中PriorityQueue的作用和源码实现的详细内容,更多关于Java PriorityQueue的资料请关注脚本之家其它相关文章!

相关文章

  • SpringBoot如何读取war包jar包和Resource资源

    SpringBoot如何读取war包jar包和Resource资源

    这篇文章主要介绍了SpringBoot如何读取war包jar包和Resource资源,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • spring mvc+localResizeIMG实现HTML5端图片压缩上传

    spring mvc+localResizeIMG实现HTML5端图片压缩上传

    这篇文章主要为大家详细介绍了使用spring mvc+localResizeIMG实现HTML5端图片压缩上传,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-04-04
  • Java超详细分析垃圾回收机制

    Java超详细分析垃圾回收机制

    一个运行中的程序, 产生的对象是大量的, 如果对象不被继续使用, 就会成为垃圾, 最后越堆越多, 最后占满内存, 所以我们要对这些垃圾进行回收,保持程序的正常运行
    2022-05-05
  • 详解Nacos配置中心的实现

    详解Nacos配置中心的实现

    Spring Cloud Alibaba 是阿里巴巴提供的一站式微服务开发解决方案。而 Nacos 作为 Spring Cloud Alibaba 的核心组件之一,提供了两个非常重要的功能:注册中心和配置中心,我们今天来了解和实现一下二者
    2022-08-08
  • Java多线程从基础到高级应用示例小结

    Java多线程从基础到高级应用示例小结

    本章介绍了Java多线程编程的基础知识,包括线程的创建与管理、线程同步、线程通信和线程池,重点内容包括线程的三种创建方式,通过本章的学习,读者掌握了Java多线程编程的基本技能,感兴趣的朋友跟随小编一起看看吧
    2026-01-01
  • 如何使用IntelliJ IDEA的HTTP Client进行接口验证

    如何使用IntelliJ IDEA的HTTP Client进行接口验证

    这篇文章主要介绍了如何使用IntelliJ IDEA的HTTP Client进行接口验证,本文给大家分享最新完美解决方案,感兴趣的朋友跟随小编一起看看吧
    2024-06-06
  • SpringCloud Config统一配置中心问题分析解决与客户端动态刷新实现

    SpringCloud Config统一配置中心问题分析解决与客户端动态刷新实现

    springcloud config是一个解决分布式系统的配置管理方案。它包含了 client和server两个部分,server端提供配置文件的存储、以接口的形式将配置文件的内容提供出去,client端通过接口获取数据、并依据此数据初始化自己的应用
    2022-10-10
  • 深入理解java虚拟机的故障处理工具

    深入理解java虚拟机的故障处理工具

    大家都知道在给系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。Java开发人员可以在jdk安装的bin目录下找到除了java,javac以外的其他命令。这些命令主要是一些用于监视虚拟机和故障处理的工具,下面来看看详细的介绍。
    2016-11-11
  • Java数组的基本操作方法整理

    Java数组的基本操作方法整理

    这篇文章主要介绍了Java数组的基本操作方法整理,是Java入门学习中的基础知识,需要的朋友可以参考下
    2015-08-08
  • JDK动态代理与CGLib动态代理的区别对比

    JDK动态代理与CGLib动态代理的区别对比

    今天小编就为大家分享一篇关于JDK动态代理与CGLib动态代理的区别对比,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-02-02

最新评论