手记

【Java数据结构及算法实战】系列009:Java队列03——数组实现的阻塞队列ArrayBlockingQueue

2022-04-27 22:00:542214浏览

老卫

1实战 · 202手记 · 2推荐

顾名思义,ArrayBlockingQueue是基于数组实现的有界阻塞队列。该队列对元素进行FIFO排序。队列的首元素是在该队列中驻留时间最长的元素。队列的尾部是在该队列中停留时间最短的元素。新的元素被插入到队列的尾部,队列检索操作获取队列头部的元素。


 


ArrayBlockingQueue是一个经典的“有界缓冲区(bounded buffer)”,其中内部包含了一个固定大小的数组,用于承载包含生产者插入的和消费者提取的元素。ArrayBlockingQueue的容量一旦创建,不可更改。试图将一个元素放入一个满队列将导致操作阻塞;试图从空队列中取出一个元素也同样会阻塞。


 


ArrayBlockingQueue支持排序的可选公平策略,用于等待生产者和消费者线程。默认情况下,不保证此顺序。然而,一个由公平性设置为true构造的队列允许线程以FIFO顺序访问。公平性一般会降低吞吐量,但可以减少可变性,避免线程饿死。


 


ArrayBlockingQueue类及其迭代器实现了Collection和Iterator接口的所有可选方法。ArrayBlockingQueue是Java Collections Framework的一个成员。


 


 


1.   ArrayBlockingQueue的声明


ArrayBlockingQueue的接口和继承关系如下


 


public class ArrayBlockingQueue<E> extends AbstractQueue<E>


        implements BlockingQueue<E>, java.io.Serializable {


 


   …


}


 


 


完整的接口继承关系如下图所示。


 



 


 


 


 


从上述代码可以看出,ArrayBlockingQueue既实现了BlockingQueue<E>和java.io.Serializable接口,又继承了java.util.AbstractQueue<E>。其中,AbstractQueue是Queue接口的抽象类,核心代码如下。


 


 


package java.util;


 


public abstract class AbstractQueue<E>


    extends AbstractCollection<E>


    implements Queue<E> {


 


    protected AbstractQueue() {


    }


 


    public boolean add(E e) {


        if (offer(e))


            return true;


        else


            throw new IllegalStateException("Queue full");


    }


 


    public E remove() {


        E x = poll();


        if (x != null)


            return x;


        else


            throw new NoSuchElementException();


    }


 


    public E element() {


        E x = peek();


        if (x != null)


            return x;


        else


            throw new NoSuchElementException();


    }


 


    public void clear() {


        while (poll() != null)


            ;


    }


 


    public boolean addAll(Collection<? extends E> c) {


        if (c == null)


            throw new NullPointerException();


        if (c == this)


            throw new IllegalArgumentException();


        boolean modified = false;


        for (E e : c)


            if (add(e))


                modified = true;


        return modified;


    }


 


}


 


 


2.   ArrayBlockingQueue的成员变量和构造函数


 


 


以下是ArrayBlockingQueue的构造函数和成员变量。


 


    // 元素数组


    final Object[] items;


 


    // 消费索引,用于take、poll、peek或remove操作


    int takeIndex;


 


    // 生产索引,用于put、offer或add操作


    int putIndex;


 


    // 队列中的元素个数


    int count;


 


    /*


     * 使用经典的双条件算法(two-condition algorithm)实现并发控制


     */


 


    // 操作数组确保原子性的锁


    final ReentrantLock lock;


 


    // 数组非空,唤醒消费者


    private final Condition notEmpty;


 


    // 数组非满,唤醒生产者


private final Condition notFull;


 


// 迭代器状态


transient Itrs itrs;


 


public ArrayBlockingQueue(int capacity) {


        this(capacity, false);


    }


 


    public ArrayBlockingQueue(int capacity, boolean fair) {


        if (capacity <= 0)


            throw new IllegalArgumentException();


        this.items = new Object[capacity];


        lock = new ReentrantLock(fair);


        notEmpty = lock.newCondition();


        notFull =  lock.newCondition();


    }


 


    public ArrayBlockingQueue(int capacity, boolean fair,


                              Collection<? extends E> c) {


        this(capacity, fair);


 


        final ReentrantLock lock = this.lock;


        lock.lock(); // 只锁可见,不互斥


        try {


            final Object[] items = this.items;


            int i = 0;


            try {


                for (E e : c)


                    items[i++] = Objects.requireNonNull(e);


            } catch (ArrayIndexOutOfBoundsException ex) {


                throw new IllegalArgumentException();


            }


            count = i;


            putIndex = (i == capacity) ? 0 : i;


        } finally {


            lock.unlock();  // 解锁


        }


}


 


 


从上述代码可以看出,构造函数有三种。构造函数中的参数含义如下


 


l  capacity用于设置队列容量


l  fair用于设置访问策略,如果为true,则对线程的访问在插入或移除时被阻塞,则按FIFO顺序处理;如果为false,则访问顺序未指定


l  c用于设置最初包含给定集合的元素,按集合迭代器的遍历顺序添加


 


类成员items是一个数组,用于存储队列中的元素。关键字final指明了,当ArrayBlockingQueue构造完成之后,通过new Object[capacity]的方式初始化items数组完成后,则后续items的容量将不再变化。


 


访问策略是通过ReentrantLock来实现的。通过两个加锁条件notEmpty、notFull来实现并发控制。这是典型的双条件算法(two-condition algorithm)。


 


ArrayBlockingQueue生产则增加putIndex,消费则增加takeIndex。


 


Itrs用于记录当前活动迭代器的共享状态,如果已知不存在任何迭代器,则为null。 允许队列操作更新迭代器状态。迭代器状态不是本节的重点,不再深入探讨。


3.   ArrayBlockingQueue的核心方法


以下对ArrayBlockingQueue常用核心方法的实现原理进行解释。


 


 


3.1.     offer(e)


执行offer(e)方法后有两种结果


 


l  队列未满时,返回 true


l  队列满时,返回 false


 


ArrayBlockingQueue的offer (e)方法源码如下:


 


public boolean offer(E e) {


        Objects.requireNonNull(e);


        final ReentrantLock lock = this.lock;


        lock.lock(); // 加锁


        try {


            if (count == items.length)


                return false;


            else {


                enqueue(e); // 入队


                return true;


            }


        } finally {


            lock.unlock();  // 解锁


        }


}


 


 


从上面代码可以看出,执行offer(e)方法时,分为以下几个步骤:


 


l  为了确保并发操作的安全先做了加锁处理。


l  而后判断count是否与数组items的长度一致,如果一致则证明队列已经满了,直接返回false;否则执行enqueue(e)方法做元素的入队,并返回true。


l  最后解锁。


 


enqueue(e)方法源码如下:


 


 


private void enqueue(E e) {


        final Object[] items = this.items;


        items[putIndex] = e;


        if (++putIndex == items.length) putIndex = 0;


        count++;


        notEmpty.signal(); // 唤醒等待中的线程


}


 


上面代码比较简单,在当前索引(putIndex)位置放置待入队的元素,而后putIndex和count分别递增,并通过signal()方法唤醒等待中的线程。其中一个注意点是,当putIndex 等于数组items长度时,putIndex置为0。


 


思考:当putIndex 等于数组items长度时,putIndex为什么置为0呢?


 


3.2.     put(e)


执行put(e)方法后有两种结果:


•      


l  队列未满时,直接插入没有返回值


l  队列满时,会阻塞等待,一直等到队列未满时再插入


 


ArrayBlockingQueue的put (e)方法源码如下:


 


public void put(E e) throws InterruptedException {


        Objects.requireNonNull(e);


        final ReentrantLock lock = this.lock;


        lock.lockInterruptibly();  // 获取锁


        try {


            while (count == items.length)


                notFull.await();  // 使线程等待


            enqueue(e);  // 入队


        } finally {


            lock.unlock();  // 解锁


        }


    }


 


从上面代码可以看出,put(e)方法的实现,分为以下几个步骤:


 


l  先是要获取锁。


l  而后判断count是否与数组items的长度一致,如果一致则证明队列已经满了,就等待;否则执行enqueue(e)方法做元素的入队。


l  最后解锁。


 


3.3.     offer(e,time,unit)


offer(e,time,unit)方法与offer(e)方法不同之处在于,前者加入了等待机制。设定等待的时间,如果在指定时间内还不能往队列中插入数据则返回false。执行offer(e,time,unit)方法有两种结果:


•      


l  队列未满时,返回 true


l  队列满时,会阻塞等待,如果在指定时间内还不能往队列中插入数据则返回 false


 


ArrayBlockingQueue的put (e)方法源码如下:


 


public boolean offer(E e, long timeout, TimeUnit unit)


        throws InterruptedException {


 


        Objects.requireNonNull(e);


        long nanos = unit.toNanos(timeout);


        final ReentrantLock lock = this.lock;


        lock.lockInterruptibly();  // 获取锁


        try {


            while (count == items.length) {


                if (nanos <= 0L)


                    return false;


                nanos = notFull.awaitNanos(nanos);  // 使线程等待指定的时间


            }


            enqueue(e);


            return true;


        } finally {


            lock.unlock();  // 解锁


        }


    }


 


从上面代码可以看出,offer(e,time,unit)方法的实现,分为以下几个步骤:


 


l  先是要获取锁。


l  而后判断count是否与数组items的长度一致,如果一致则证明队列已经满了,就等待;否则执行enqueue(e)方法做元素的入队。


l  最后解锁。


 


3.4.     add(e)


执行add(e)方法后有两种结果


 


l  队列未满时,返回 true


l  队列满时,则抛出异常


 


ArrayBlockingQueue的add(e)方法源码如下:


 


    public boolean add(E e) {


        return super.add(e);


    }


 


 


从上面代码可以看出,add(e)方法的实现,直接是调用了父类AbstractQueue的add(e)方法。而AbstractQueue的add(e)方法源码如下:


 


 


public boolean add(E e) {


        if (offer(e))


            return true;


        else


            throw new IllegalStateException("Queue full");


}


 


 


从上面代码可以看出,add(e)方法又调用了offer(e)方法。offer(e)方法此处不再赘述。


 


 


 


3.5.     poll ()


执行poll ()方法后有两种结果:


 


l  队列不为空时,返回队首值并移除


l  队列为空时,返回 null


 


 


ArrayBlockingQueue的poll()方法源码如下:


 


public E poll() {


        final ReentrantLock lock = this.lock;


        lock.lock();  // 加锁


        try {


            return (count == 0) ? null : dequeue(); // 出队


        } finally {


            lock.unlock();  // 解锁


        }


    }


 


从上面代码可以看出,执行poll()方法时,分为以下几个步骤:


 


l  为了确保并发操作的安全先做了加锁处理。


l  而后判断count是否等于0,如果等于0则证明队列为空,直接返回null;否则执行dequeue()方法做元素的出队。


l  最后解锁。


 


dequeue()方法源码如下:


 


 


private E dequeue() {


        final Object[] items = this.items;


        @SuppressWarnings("unchecked")


        E e = (E) items[takeIndex];


        items[takeIndex] = null;  // 删除数据


        if (++takeIndex == items.length) takeIndex = 0;


        count--;


        if (itrs != null)


            itrs.elementDequeued();


        notFull.signal(); // 唤醒等待中的线程


        return e;


}


 


上面代码比较简单,在当前索引(takeIndex)位置取出待出队的元素并删除队列中的元素,而后takeIndex递增count递减,并通过signal()方法唤醒等待中的线程。其中一个注意点是,当takeIndex等于数组items长度时,takeIndex置为0。


 


3.6.     take()


执行take()方法后有两种结果:


 


l  队列不为空时,返回队首值并移除


l  队列为空时,会阻塞等待,一直等到队列不为空时再返回队首值


 


ArrayBlockingQueue的take ()方法源码如下:


 


public E take() throws InterruptedException {


        final ReentrantLock lock = this.lock;


        lock.lockInterruptibly();  // 获取锁


        try {


            while (count == 0)


                notEmpty.await(); // 使线程等待


            return dequeue();  // 出队


        } finally {


            lock.unlock();  // 解锁


        }


    }


 


从上面代码可以看出,执行take()方法时,分为以下几个步骤:


 


l  先是要获取锁。


l  而后判断count是否等于0,如果等于0则证明队列为空,会阻塞等待;否则执行dequeue()方法做元素的出队。


l  最后解锁。


 


dequeue()方法此处不再赘述。


 


3.7.     poll(time,unit)


poll(time,unit)方法与poll()方法不同之处在于,前者加入了等待机制。设定等待的时间,如果在指定时间内队列还为空,则返回null。执行poll(time,unit)方法后有两种结果:


 


l  队列不为空时,返回队首值并移除


l  队列为空时,会阻塞等待,如果在指定时间内队列还为空则返回 null


 


ArrayBlockingQueue的poll(time,unit)方法源码如下:


 


public E poll(long timeout, TimeUnit unit) throws InterruptedException {


        long nanos = unit.toNanos(timeout);


        final ReentrantLock lock = this.lock;


        lock.lockInterruptibly();  // 获取锁


        try {


            while (count == 0) {


                if (nanos <= 0L)


                    return null;


                nanos = notEmpty.awaitNanos(nanos); // 使线程等待指定的时间


            }


            return dequeue();  // 出队


        } finally {


            lock.unlock();  // 解锁


        }


    }


从上面代码可以看出,执行poll(time,unit)方法时,分为以下几个步骤:


 


l  先是要获取锁。


l  而后判断count是否等于0,如果等于0则证明队列为空,会阻塞等待;否则执行dequeue()方法做元素的出队。


l  最后解锁。


 


dequeue()方法此处不再赘述。


 


 


3.8.     remove()


执行remove()方法后有两种结果:


 


l  队列不为空时,返回队首值并移除


l  队列为空时,抛出异常


 


ArrayBlockingQueue的remove()方法其实是调用了父类AbstractQueue的remove()方法,源码如下:


 


public E remove() {


        E x = poll();


        if (x != null)


            return x;


        else


            throw new NoSuchElementException();


}


 


从上面代码可以看出,remove()直接调用了poll()方法。如果poll()方法返回结果为null,则抛出NoSuchElementException异常。


 


poll()方法此处不再赘述。


 


3.9.     peek()


执行peek()方法后有两种结果:


 


l  队列不为空时,返回队首值但不移除


l  队列为空时,返回null


 


 


peek()方法源码如下:


 


public E peek() {


        final ReentrantLock lock = this.lock;


        lock.lock(); // 加锁


        try {


            return itemAt(takeIndex); // 空则返回null


        } finally {


            lock.unlock();  // 解锁


        }


}


 


final E itemAt(int i) {


        return (E) items[i];


}


 


从上面代码可以看出,peek()方法比较简单,直接就是获取了数组里面的索引为takeIndex的元素。


 


3.10.            element()


执行element()方法后有两种结果:


 


l  队列不为空时,返回队首值但不移除


l  队列为空时,抛出异常


 


 


element()方法其实是调用了父类AbstractQueue的element()方法,源码如下:


 


public E element() {


        E x = peek();


        if (x != null)


            return x;


        else


            throw new NoSuchElementException();


}


 


从上面代码可以看出,执行element()方法时,先是获取peek()方法的结果,如果结果是null,则抛出NoSuchElementException异常。


 


 


4.   ArrayBlockingQueue的单元测试


 


ArrayBlockingQueue的单元测试如下:


 


 


package com.waylau.java.demo.datastructure;


 


import static org.junit.jupiter.api.Assertions.assertEquals;


import static org.junit.jupiter.api.Assertions.assertFalse;


import static org.junit.jupiter.api.Assertions.assertNotNull;


import static org.junit.jupiter.api.Assertions.assertNull;


import static org.junit.jupiter.api.Assertions.assertThrows;


import static org.junit.jupiter.api.Assertions.assertTrue;


 


import java.util.NoSuchElementException;


import java.util.concurrent.ArrayBlockingQueue;


import java.util.concurrent.BlockingQueue;


import java.util.concurrent.TimeUnit;


 


import org.junit.jupiter.api.Test;


 


/**


 * ArrayBlockingQueue Tests


 *


 * @since 1.0.0 2020年5月3日


 * @author <a href="https://waylau.com">Way Lau</a>


 */


class ArrayBlockingQueueTests {


    @Test


    void testOffer() {


        // 初始化队列


        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列未满时,返回 true


        boolean resultNotFull = queue.offer("Java");


        assertTrue(resultNotFull);


 


        // 测试队列满则,返回 false


        queue.offer("C");


        queue.offer("Python");


        boolean resultFull = queue.offer("C++");


        assertFalse(resultFull);


    }


 


    @Test


    void testPut() throws InterruptedException {


        // 初始化队列


        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列未满时,直接插入没有返回值;


        queue.put("Java");


 


        // 测试队列满则, 会阻塞等待,一直等到队列未满时再插入。


        queue.put("C");


        queue.put("Python");


        queue.put("C++");  // 阻塞等待


    }


 


    @Test


    void testOfferTime() throws InterruptedException {


        // 初始化队列


        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列未满时,返回 true


        boolean resultNotFull = queue.offer("Java", 5, TimeUnit.SECONDS);


        assertTrue(resultNotFull);


 


        // 测试队列满则,返回 false


        queue.offer("C");


        queue.offer("Python");


        boolean resultFull = queue.offer("C++", 5, TimeUnit.SECONDS); // 等5秒


        assertFalse(resultFull);


    }


 


    @Test


    void testAdd() {


        // 初始化队列


        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列未满时,返回 true


        boolean resultNotFull = queue.add("Java");


        assertTrue(resultNotFull);


 


        // 测试队列满则抛出异常


        queue.add("C");


        queue.add("Python");


 


        Throwable excpetion = assertThrows(IllegalStateException.class, () -> {


            queue.add("C++");// 抛异常


        });


 


        assertEquals("Queue full", excpetion.getMessage());


    }


 


    @Test


    void testPoll() throws InterruptedException {


        // 初始化队列


        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列为空时,返回 null


        String resultEmpty = queue.poll();


        assertNull(resultEmpty);


 


        // 测试队列不为空时,返回队首值并移除


        queue.put("Java");


        queue.put("C");


        queue.put("Python");


        String resultNotEmpty = queue.poll();


        assertEquals("Java", resultNotEmpty);


    }


 


    @Test


    void testTake() throws InterruptedException {


        // 初始化队列


        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列不为空时,返回队首值并移除


        queue.put("Java");


        queue.put("C");


        queue.put("Python");


        String resultNotEmpty = queue.take();


        assertEquals("Java", resultNotEmpty);


 


        // 测试队列为空时,会阻塞等待,一直等到队列不为空时再返回队首值


        queue.clear();


        String resultEmpty = queue.take(); // 阻塞等待


        assertNotNull(resultEmpty);


    }


 


    @Test


    void testPollTime() throws InterruptedException {


        // 初始化队列


        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列不为空时,返回队首值并移除


        queue.put("Java");


        queue.put("C");


        queue.put("Python");


        String resultNotEmpty = queue.poll(5, TimeUnit.SECONDS);


        assertEquals("Java", resultNotEmpty);


 


        // 测试队列为空时,会阻塞等待,如果在指定时间内队列还为空则返回 null


        queue.clear();


        String resultEmpty = queue.poll(5, TimeUnit.SECONDS); // 等待5秒


        assertNull(resultEmpty);


    }


 


    @Test


    void testRemove() throws InterruptedException {


        // 初始化队列


        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列为空时,抛出异常


        Throwable excpetion = assertThrows(NoSuchElementException.class, () -> {


            queue.remove();// 抛异常


        });


 


        assertEquals(null, excpetion.getMessage());


 


        // 测试队列不为空时,返回队首值并移除


        queue.put("Java");


        queue.put("C");


        queue.put("Python");


        String resultNotEmpty = queue.remove();


        assertEquals("Java", resultNotEmpty);


    }


 


@Test


    void testPeek() throws InterruptedException {


        // 初始化队列


        Queue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列不为空时,返回队首值并但不移除


        queue.add("Java");


        queue.add("C");


        queue.add("Python");


        String resultNotEmpty = queue.peek();


        assertEquals("Java", resultNotEmpty);


        resultNotEmpty = queue.peek();


        assertEquals("Java", resultNotEmpty);


        resultNotEmpty = queue.peek();


        assertEquals("Java", resultNotEmpty);


 


        // 测试队列为空时,返回null


        queue.clear();


        String resultEmpty = queue.peek();


        assertNull(resultEmpty);


    }


 


    @Test


    void testElement() throws InterruptedException {


        // 初始化队列


        Queue<String> queue = new ArrayBlockingQueue<String>(3);


 


        // 测试队列不为空时,返回队首值并但不移除


        queue.add("Java");


        queue.add("C");


        queue.add("Python");


        String resultNotEmpty = queue.element();


        assertEquals("Java", resultNotEmpty);


        resultNotEmpty = queue.element();


        assertEquals("Java", resultNotEmpty);


        resultNotEmpty = queue.element();


        assertEquals("Java", resultNotEmpty);


 


        // 测试队列为空时,抛出异常


        queue.clear();


        Throwable excpetion = assertThrows(NoSuchElementException.class, () -> {


            queue.element();// 抛异常


        });


 


        assertEquals(null, excpetion.getMessage());


    }


}


 


5.   ArrayBlockingQueue的应用案例


以下是一个生产者-消费者的示例。该示例模拟了1个生产者,2个消费者。当队列满时,则会阻塞生产者生产;当队列空时,则会阻塞消费者消费。


 


package com.waylau.java.demo.datastructure;


 


import java.util.concurrent.ArrayBlockingQueue;


import java.util.concurrent.BlockingQueue;


 


/**


 * ArrayBlockingQueue Demo


 *


 * @since 1.0.0 2020年5月3日


 * @author <a href="https://waylau.com">Way Lau</a>


 */


public class ArrayBlockingQueueDemo {


 


    public static void main(String[] args) {


        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);


       


        // 1个生产者


        Producer p = new Producer(queue);


       


        // 2个消费者


        Consumer c1 = new Consumer("c1", queue);


        Consumer c2 = new Consumer("c2", queue);


       


        // 启动线程


        new Thread(p).start();


        new Thread(c1).start();


        new Thread(c2).start();


    }


}


 


class Producer implements Runnable {


    private final BlockingQueue<String> queue;


 


    Producer(BlockingQueue<String> queue) {


        this.queue = queue;


    }


 


    public void run() {


        try {


            while (true) {


                // 模拟耗时操作


                Thread.sleep(1000L);


 


                queue.put(produce());


            }


        } catch (InterruptedException ex) {


            ex.printStackTrace();


        }


    }


 


    String produce() {


        String apple = "apple: " + System.currentTimeMillis();


        System.out.println("produce " + apple);


        return apple;


    }


}


 


class Consumer implements Runnable {


    private final BlockingQueue<String> queue;


 


    private final String name;


 


    Consumer(String name, BlockingQueue<String> queue) {


        this.queue = queue;


        this.name = name;


    }


 


    public void run() {


        try {


            while (true) {


                // 模拟耗时操作


                Thread.sleep(2000L);


 


                consume(queue.take());


            }


        } catch (InterruptedException ex) {


            ex.printStackTrace();


        }


    }


 


    void consume(Object x) {


        System.out.println(this.name + " consume " + x);


    }


}


 


 


 


 


运行上述程序,输出内容如下:


 


produce apple: 1590308383034


c2 consume apple: 1590308383034


produce apple: 1590308384034


c1 consume apple: 1590308384034


produce apple: 1590308385036


c2 consume apple: 1590308385036


produce apple: 1590308386036


c1 consume apple: 1590308386036


produce apple: 1590308387036


c2 consume apple: 1590308387036


produce apple: 1590308388036


c1 consume apple: 1590308388036


produce apple: 1590308389041


c2 consume apple: 1590308389041


produce apple: 1590308390041


c1 consume apple: 1590308390041


produce apple: 1590308391042


c2 consume apple: 1590308391042


produce apple: 1590308392042


c1 consume apple: 1590308392042


6.   参考引用


本系列归档至《Java数据结构及算法实战》:https://github.com/waylau/java-data-structures-and-algorithms-in-action
《数据结构和算法基础(Java语言实现)》(柳伟卫著,北京大学出版社出版):https://item.jd.com/13014179.html

0人推荐
随时随地看视频
慕课网APP