具有Set属性的Queue

前言


在最近的工作中,遇到了一个特殊的需求:我们需要一个队列来存放某数据对象,但是这个对象的数量是巨大的,如果将这些对象都存入队列的话,很显然内存会爆表,但是这些对象有一个特征是,相同的数据对象类型的数据是可更新的。换句话说,对于同一类数据对象,后面来的对象的值一定比前面的新,是可以完全覆盖前面的。所以如果对象是可更新的,我们可以通过重写对象的hashcode和equals方法,然后放在Set集合中进行保存。但是我们还需要此数据结构能保存队列的FIFO特性,这时候该怎么办呢?所以在这里,我们需要能够自定义一个具有Set属性的Queue

去重队列:UniqueQueue


在讲述具有Set属性的Queue之前,我们先来看一个初级版本的SetQueue:具有去重属性的队列(此处不需要更新对象,当队列中添加了相同对象的时候)。

要想实现这个功能需求的Queue,过程并不复杂,我们只需额外Queue做一层包装,添加额外的Set对象来记录当前已添加的对象,然后利用此Set做去重判断

此Queue的类变量定义如下:

/**
 * Thread unsafe implementation of UniqueQueue.
 */
public class UniqueQueue<T> implements Queue<T> {
    private final Queue<T> queue = new LinkedList<T>();
    /** Set集合用来去重 **/
    private final Set<T> set = new HashSet<T>();
    private ReentrantLock lock = new ReentrantLock();
    ...

在Queue的实现接口中,我们可以先只实现offer、poll方法,对应的操作就是加入、取出操作。

offer方法实现代码如下:

    @Override
    public boolean offer(T t) {
        // 首先进行锁定操作
        lock.lock();
        try {
            // 直接调用Set的add方法,如果当前集合已经存在此对象,则方法会返回失败
            if (set.add(t)) {
                // 否则将对象加入到队列中
                queue.add(t);
                return true;
            } else {                
                return false;
            }
        } finally {
            // 进行锁释放操作
            lock.unlock();
        }
    }

poll方法实现代码如下:

    @Override
    public T poll() {
        lock.lock();
        try {
            // 从队列首部取出对象
            T t = queue.poll();
            // 如果对象不为空,则从Set集合中相应的移出
            if (t != null) {
                set.remove(t);
            }
            return t;
        } finally {
            lock.unlock();
        }
    }

通过以上的实现,我们就实现了一个带有去重属性的Queue。大家可以自行对此Queue进行测试,自定义的添加对象是需要重载比较方法的,否则会被认为是不同对象,这一点需要注意。UniqueQueue的全部实现代码在文章末尾链接中将会给出。

去重可更新队列:UpdatedQueue


基于上部分UniqueQueue,下面我们来看对此的一个扩展:Queue不仅仅需要是去重的,而且它需要是可更新的。这个需求在很多应用场景中会碰到,比如说实时展示商品销售信息。按照往常的做法,我们的做法是定期的获取商品销售信息并进行展示,但是如果说商品信息的量在非常大的情况下时,怎么办,如果一次性没法更新完毕,下一周期的数据又来了,这又会增加新的压力。在这种情况下,可更新队列可以巧妙地帮我们解决这个问题。

首先是此队列结构的定义:

/**
 * The queue is unique and meanwhile can be updated
 */
public class UpdatedQueue <K, T> implements Queue<T> {
    protected final Queue<T> queue = new LinkedList<T>();
    // HashMap此处用来做重复对象的查找与更新
    protected final HashMap<K, T> map = new HashMap<K, T>();
    // 用ReentrantLock实现线程安全
    protected ReentrantLock lock = new ReentrantLock();

    @Override
    public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return queue.size();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public boolean isEmpty() {
        boolean isEmpty = false;

        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            isEmpty = queue.isEmpty();
        } finally {
            lock.unlock();
        }

        return isEmpty;
    }
    ...
}

上述类只是一个基础类,去重队列的核心操作我们可以实现在子类实现中,后面大家可以再进一步地对此进行抽象化。我新建了一个BallUpdatedQueue的实现子类,以及相应的实体类Ball。

Ball对象代表的意思是一个球,它具有如下属性,其中球类名称作为唯一区别符:

public class Ball {
    // 球类的名称
    String ballName;
    // 球的单价
    int price;
    // 球的销售量
    int numSale;

    public Ball(String ballName, int price, int numSale) {
        this.ballName = ballName;
        this.price = price;
        this.numSale = numSale;
    }

    /**
     * 球信息更新操作
     * @param newBall
     */
    public void valueUpdated(Ball newBall) {
        this.price = newBall.price;
        this.numSale = newBall.numSale;
    }

    public String getUniqueKey() {
        return this.ballName;
    }

    @Override
    public String toString() {
        return "Ball [ballName=" + ballName + ", price=" + price + ", numSale=" + numSale + "]";
    }
}

然后是可更新队列的实现:

public class BallUpdatedQueue extends UpdatedQueue<String, Ball> {
    @Override
    public boolean offer(Ball newBall) {
        Ball ball = null;
        // 获取对象的唯一键值
        String uniqueKey = newBall.getUniqueKey();

        lock.lock();
        try {
            // 如果当前图中已经包含此键值,表明队列中已有同属性对象
            if (map.containsKey(uniqueKey)) {
                // 取出此键值对应的对象,进行属性值的更新,因为map中的对象与queue中的对象是同一引用,所以能达到更新的效果
                ball = map.get(uniqueKey);
                ball.valueUpdated(newBall);
            } else {
                map.put(uniqueKey, newBall);
                queue.offer(newBall);
            }
        } finally {
            lock.unlock();
        }

        return true;
    }

    @Override
    public Ball poll() {
        Ball ball = null;
        String uniqueKey;

        lock.lock();
        try {
            // 取出队列头部对象
            ball = queue.poll();

            if (ball != null) {
                // 取出对象的唯一键值
                uniqueKey = ball.getUniqueKey();
                // 从图中也同样移除
                map.remove(uniqueKey);
            }
        } finally {
            lock.unlock();
        }

        return ball;
    }
}

在上述代码中已经进行了详细的注释,这里就不进行过多的解释了,这里的一个核心点在于利用了HashMap进行去重更新的处理。

下面是相应的测试方法:

public class TestUpdatedQueue {
    public static void main(String[] args){
        Ball footBall = new Ball("football", 10, 0);
        Ball basketBall = new Ball("basketball", 20, 0);
        Ball baseBall = new Ball("baseball", 15, 0);

        BallUpdatedQueue ballInfoQueue = new BallUpdatedQueue();
        ballInfoQueue.offer(footBall);
        ballInfoQueue.offer(basketBall);
        ballInfoQueue.offer(baseBall);

        // 这里的球总数应该是3个
        System.out.println("The total ball number: " + ballInfoQueue.size());

        // 调整足球的价格和销售数量,然后加入到队列中
        footBall = new Ball("football", 30, 10);
        ballInfoQueue.offer(footBall);
        // 队列中的球的数量应该还是3个
        System.out.println("The total ball number: " + ballInfoQueue.size());

        // 取出当前队列头部的对象,即足球对象,输出此球对象的信息,信息已被更新
        Ball ball = ballInfoQueue.poll();
        System.out.println(ball);
    }
}

本地测试输出的结果如下:

The total ball number: 3
The total ball number: 3
Ball [ballName=football, price=30, numSale=10]

代码链接


[1].https://github.com/linyiqun/SetQueues

posted @ 2020-01-12 19:08  回眸,境界  阅读(169)  评论(0编辑  收藏  举报