本文转载自微信公众号“小姐姐的味道”,作者小姐姐养的狗。转载本文请联系味觉小姐公众号。要想了解Java的API有多变态,就不得不提到队列接口。许多工作多年的人对此仍然很困惑。队列虽然是计算机算法中的一个基本结构,但它并不是只有add方法。看完这篇文章,再看看add、offer、put,别再晕了!1.一小段代码猜猜下面的代码会输出什么?voidrun(Callablec){try{System.out.println(c.call());}catch(Exceptionex){System.out.println(ex);}}voidtestSynchronousQueue(){Queueq1=newSynchronousQueue();run(()->q1.add(1));Queueq2=newSynchronousQueue();run(()->q1.offer(1));}很失望,既执行失败。java.lang.IllegalStateException:Queuefullfalse第一次使用add方法,程序抛出异常,提示队列已满;第二次,程序返回false,证明添加失败。由于无法向队列中添加元素,因此没有地方指定队列的大小。那这个队列有什么用呢!2.队列方法在了解这个队列的使用之前,我们先看一下Queue接口定义的方法。add(Ee)在队列末尾插入一个元素。如果不能插入,则抛出异常offer(Ee)向队列中插入一个元素是Eremove()从队头移除一个元素,如果队列为空,则抛出异常Epoll()移除从队头开始一个元素,如果队列为空,返回nullEelement()查看对面元素,如果队列为空,抛出异常Epeek()查看对面元素,如果队列为空,returnnull可以看到队列只有三个基本操作:插入新元素,查看队头,从队头取出一对。根据是否抛出异常分为两类。3x2=6,共6种方法。喜欢刷题的同学一定要用offer、poll、peek,这样可以避免烦人的异常处理。普通编码也建议使用非异常的API,但是为什么Java会提供两套方法给我们使用呢?原因是Queue接口继承了Collection接口,add、remove等方法属于Collection接口,Queue要实现一个集合。其实add方法直接调用了offer方法。为什么会有这么一套额外的API,真是一个谜。publicbooleanadd(Ee){if(offer(e))returntrue;elsethrownewIllegalStateException("Quueefull");}如果不抛出异常,很容易被遗忘和处理,这确实是一个牵强的理由。基于此,人们能在这么重要的基础类库中创建这么多不同名称的方法吗?3.Put和Take相对于上面纠结的add和offer,put和take方法确实有用。但是put和take不属于Queue接口,而是属于BlockingQueue。不好意思,不小心跳到了并发包。put和take表示阻塞。如果操作不成功,它会一直阻塞在那里。如果想让它们正常运行,就需要多个线程的配合。下面的代码会向队列中发送一个1,然后take方法将其取出并打印出来。voidtestBlockingSynchronousQueue()throwsInterruptedException{BlockingQueueq1=newSynchronousQueue();newThread(()->{try{q1.put(1);}catch(InterruptedExceptione){e.printStackTrace();}}).start();newThread(()->{try{System.out.println(q1.take());}catch(InterruptedExceptione){e.printStackTrace();}}).start();}所以,让我们看看这对的方法。put(Ee)插入元素,如果队列满了,会一直阻塞等待Etake()拿到队头元素,如果队列为空,会一直等待,可以看到put和采取合作,很容易实现一个线程安全的生产者消费者模型。与使用Queue接口方式相比,我们只能通过无限循环来检测,所以阻塞方式特别节省资源。但这还没有结束。被阻塞的take和put方法只能被中断。如何让程序阻塞一段时间然后恢复运行?然后只添加一个带有时间戳的阻塞方法。BlockingQueue选择了offer和poll方法而不是take和put,我们不明白为什么。Epoll(longtimeout,TimeUnitunit)booleanoffer(Ee,longtimeout,TimeUnitunit)还是有返回值的4.你以为就这样结束了吗?你认为这是结束了吗?一点也不。我们需要把目光转向LinkedList,这是一个用图例中的几行代码实现LRU缓存的类。ArrayList是一个比较纯粹的List,只实现了List接口,而LinkedList的胃口更大。因为API设计者想尽办法让这个链表更加强大,所以它继承了Deque接口。由于Deque继承了Queue,所以这个链表不仅是一个队列,还是一个双向队列。因此,他们多了一堆API来描述是操作在队头还是队尾。addFirst操作队头,添加元素addLast操作队尾,添加元素offerFirst操作队头,添加元素offerLast操作队尾,添加元素removeFirst操作队头,删除元素removeLast操作队尾,删除元素pollFirst操作队头,删除元素pollLast操作队列的尾部,删除元素getFirst获取队列的头部元素,类似element。TMD,这里为什么不用element呢?getLast获取队尾元素peekFirst获取队头元素peekLast获取队尾元素当然这里也有pop和push,pop=removeFirst,push=addFirst。//建议不要用,太难记了。很好,因为有了head和tail的概念,API的大小变成了3x2x2=12!加上原来的6个,一共18个(忽略pop,直接push)。你要说了,为什么没有take和put这样的阻塞方法。原因是LinkedList不是并发集合。你要找的函数在LinkedBlockingDeque中肯定会有takeFirst、takeLast、putLast、putFirst等。5.队列大小回过头来看我们刚刚开始的SynchronousQueue,为什么不管往里面加元素还是取出元素都返回failure?它的容量是多少?这是一个很奇怪的类,它的内部容量为0!已经硬编码到代码中。publicintsize(){return0;}它只是建立一个通道。一旦有生产,消费者可以立即得到它。它不存储任何数据。Executors.newCachedThreadPool()使用SynchronousQueue。常用的LinkedBlockingQueue和ArrayBlockingQueue都是有界的。但是这里还有一个奇怪的类,那就是ConcurrentLinkedQueue。看名字就知道,它不是阻塞并发类,所以没有take、put等方法。此外,它是无界的,因此请格外小心地使用它。你可能会说,我每次判断它的size()方法,看它是否越界,它就可以了。publicintsize(){intcount=0;for(Nodep=first();p!=null;p=succ(p))if(p.item!=null)//Collection.size()specsaystomaxoutif(++count==Integer.MAX_VALUE)break;returncount;}如上代码所示,这里就是坑的地方。size方法不是O(1)时间级别的。xjjdog曾经深受其害,但最终不敢乱用。从上面的描述可以看出结束。对于一个队列来说,一共有三套接口:insert、pop、detect;根据是否抛出异常分为两组,一组会抛出异常,一组直接返回值。加上双向队列,需要区分peer队列的尾部;如果是阻塞队列,则必须添加另一个维度。因此,对于一个阻塞双向队列,其基本操作方式为:(3[基本]x2[异常及返回值]+4[阻塞加超时])x3[队列头尾]=5x2x3=30个方法,这是LinkedBlockingDeque之王。这样的代码,反正我吐了。你呢?作者简介:品味小姐姐(xjjdog),一个不允许程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。我的个人微信xjjdog0,欢迎加好友进一步交流。
让我告诉你,Java中的方法爆炸!相关文章