具有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]