JAVA高级(5)—— 集合讲解

一、简单总结

JAVA高级(5)—— 集合讲解_第1张图片
Colletion(粗体:类;非粗体:接口)
  • Collection 是对象集合, Collection 有两个子接口List 和 Set,List 可以通过索引来取得值,值可以重复;而 Set 只能通过游标来取值,并且值是不能重复的。
  • List ,关心的是顺序,它保证维护元素特定的顺序(允许有相同元素)。
  • ArrayList 是线程不安全的, Vector 是线程安全的,这两个类底层都是由数组实现的。
  • LinkedList 是线程不安全的,底层是由链表实现的。
  • Set ,只关心某元素是否属于 Set (不允许有相同元素 ),而不关心它的顺序。


    JAVA高级(5)—— 集合讲解_第2张图片
    Map
  • Map 是键值对集合,键不能重复,值可以
    如果添加元素的顺序对你很重要,应该使用 LinkedHashSet 或者 LinkedHashMap.
    由于采用了哈希散列,查找元素时明显比 ArrayList 快。
  • HashTable 和 HashMap 是 Map 的实现类
  • HashTable 是线程安全的,不能存储 null 值
  • HashMap 不是线程安全的,可以存储 null 值

二、详细介绍

1、List 接口

  • 保证维护元素特定的顺序。
  • 允许有相同的元素。

除了具有 Collection 接口必备的 iterator() 方法外, List 还提供一个 listIterator() 方法,返回一个 ListIterator接口, ListIterator 相比标准的 Iterator 多了一些 add() 之类的方法,允许添加,删除,设定元素, 还能向前或向后遍历。

1.1、LinkedList 类

  • 线程不安全。一种解决方法是在创建 List 时构造一个同步的 List : List list = Collections.synchronizedList(new LinkedList(...));
  • 对顺序访问进行了优化。向 List 中间插入与删除的开销并不大。随机访问则相对较慢。 ( 使用 ArrayList 代替。 )
  • 实现了 List 接口,允许 null 元素。

提供额外的 addFirst(), addLast(), getFirst(), getLast(), removeFirst(), removeLast(), insertFirst(), insertLast() 方法在 LinkedList 的首部或尾部,使 LinkedList 可被用作堆栈( stack ),队列( queue )或双向队列( deque )。

1.2、ArrayList 类

  • 它允许所有元素,包括 null 。
  • 线程不安全。
  • 可变大小的数组实现。

每个 ArrayList 实例都有一个容量( Capacity ),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加。当需要插入大量元素时,在插入前可以调用 ensureCapacity 方法来增加 ArrayList 的容量以提高插入效率
允许对元素进行快速随机访问,但是向 List 中间插入与移除元素的速度很慢。 ListIterator 只应该用来由后向前遍历 ArrayList, 而不是用来插入和移除元素。因为那比 LinkedList 开销要大很多。

1.3、Vector 类

  • 类似 ArrayList ,但是线程安全的。

由 Vector 创建的 Iterator ,虽然和 ArrayList 创建的 Iterator 是同一接口,但是,因为 Vector 是同步的,当一个 Iterator 被创建而且正在被使用,另一个线程改变了 Vector 的状态(例如,添加或删除了一些元素),这时调用 Iterator 的方法时将抛出 ConcurrentModificationException ,因此必须捕获该异常。

1.4、Stack 类

Stack 继承自 Vector ,实现一个后进先出的堆栈。 Stack 提供 5 个额外的方法使得 Vector 得以被当作堆栈使用。基本的 push 和 pop 方法,还有 peek 方法得到栈顶的元素, empty 方法测试堆栈是否为空, search 方法检测一个元素在堆栈中的位置。 Stack 刚创建后是空栈。

2、Set 接口

  • Set 是一种不包含重复的元素的 Collection ,不保证维护元素的次序。
  • 最多有一个 null 元素。
  • Set 具有与 Collection 完全一样的接口,因此没有任何额外的功能 。实际上 Set 就是 Collection ,只是行为不同。

2.1、HashSet 类

为快速查找设计的 Set 。存入 HashSet 的对象必须定义 hashCode() 。

2.2、LinkedHashSet 类

具有 HashSet 的查询速度,且内部使用链表维护元素的顺序 ( 插入的次序 ) 。于是在使用迭代器遍历 Set 时,结果会按元素插入的次序显示。

3、Map 接口

Map 接口提供 3 种集合的视图, Map 的内容可以被当作一组 key 集合,一组 value 集合,或者一组 key-value 映射。
主要方法:

- put(Object key, Object value)
- get(Object key) 
- containsKey()
- containsValue
  • 如果关心元素添加的顺序,应该使用 LinkedHashSet 或者 LinkedHashMap。
  • 查询比List快。
    HashMap 使用了特殊的值,称为“散列码” (hash code) ,来取代对键的缓慢搜索。“散列码”是“相对唯一”用以代表对象的 int 值,它是通过将该对象的某些信息进行转换而生成的。所有 Java 对象都能产生散列码,因为 hashCode() 是定义在基类 Object 中的方法 。 HashMap 就是使用对象的 hashCode() 进行快速查询的。此方法能够显著提高性能。

3.1、Hashtable类

  • 线程安全的。

实现一个 key-value 映射的哈希表。任何非空的对象都可作为 key 或者 value 。添加数据使用 put(key, value) ,取出数据使用 get(key) 。
Hashtable 通过初始化容量 (initial capacity) 和负载因子 (load factor) 两个参数调整性能。通常缺省的 load factor 0.75 较好地实现了时间和空间的均衡。增大 load factor 可以节省空间但相应的查找时间将增大,这会影响像 get 和 put 这样的操作。
由于作为 key 的对象将通过计算其散列函数来确定与之对应的 value 的位置,因此任何作为 key 的对象都必须实现 hashCode 方法和 equals 方法。 hashCode 方法和 equals 方法继承自根类 Object ,如果你用自定义的类当作 key 的话,要相当小心,按照散列函数的定义,如果两个对象相同,即 obj1.equals(obj2)=true ,则它们的 hashCode 必须相同,但如果两个对象不同,则它们的 hashCode 可能相同,如果两个不同对象的 hashCode 相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的 hashCode() 方法,能加快哈希表的操作。
如果相同的对象有不同的 hashCode ,对哈希表的操作会出现意想不到的结果(期待的 get 方法返回 null ),要避免这种问题,只需要牢记一条:要同时复写 equals 方法和 hashCode 方法,而不要只写其中一个。

3.2、HashMap 类

  • 线程不安全
  • 允许 null ,即 null value 和 null key

HashMap 和 Hashtable 类似,也是基于散列表的实现。将 HashMap 视为 Collection 时( values() 方法可返回 Collection ),插入和查询“键值对”的开销是固定的,但其迭代子操作时间开销和 HashMap 的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将 HashMap 的初始化容量 (initial capacity) 设得过高,或者负载因子 (load factor) 过低。

3.2.1、LinkedHashMap 类

类似于 HashMap ,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用 (LRU) 的次序。只比 HashMap 慢一点。而在迭代访问时反而更快,因为它使用链表维护内部次序。

3.3、WeakHashMap 类

弱键( weak key ) Map 是一种改进的 HashMap ,它是为解决特殊问题设计的,对 key 实行 “ 弱引用 ” 。

4、总结一:比较

4.1、数组 (Array) ,数组类 (Arrays)

Java 所有“存储及随机访问一连串对象”的做法, array 是最有效率的一种。但缺点是容量固定且无法动态改变。 array 还有一个缺点是,无法判断其中实际存有多少元素, length 只是告诉我们 array 的容量。
Java 中有一个数组类 (Arrays) ,专门用来操作 array 。数组类 (arrays) 中拥有一组 static 函数。

equals()   //比较两个 array 是否相等。 array 拥有相同元素个数,且所有对应元素两两相等
fill()  //将值填入 array 中
sort()  //用来对 array 进行排序 
binarySearch()  //在排好序的 array 中寻找元素。
System.arraycopy()   //array 的复制。

若编写程序时不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库, array 不适用。

4.2、容器类与数组的区别

容器类仅能持有对象引用,而不是将对象信息 copy 一份至数列某位置。

4.3、容器 (Collection) 与 Map 的联系与区别

Collection 类型,每个位置只有一个元素。
Map 类型,持有 key-value 对 (pair) ,像个小型数据库。
Collections 是针对集合类的一个帮助类。提供了一系列静态方法实现对各种集合的搜索、排序、线程完全化等操作。相当于对 Array 进行类似操作的类—— Arrays 。
如, Collections.max(Collection coll); 取 coll 中最大的元素。
Collections.sort(List list); 对 list 中元素排序

5、总结二:需要注意的地方

  • Collection 只能通过 iterator() 遍历元素,没有 get() 方法来取得某个元素。
  • Set 和 Collection 拥有一模一样的接口。但排除掉传入的 Collection 参数重复的元素。
  • List ,可以通过 get() 方法来一次取出一个元素。
  • Map 用 put(k,v) / get(k) ,还可以使用 containsKey()/containsValue() 来检查其中是否含有某个 key/value 。
    HashMap 会利用对象的 hashCode 来快速找到 key 。
    哈希码 (hashing) 就是将对象的信息经过一些转变形成一个独一无二的 int 值,这个值存储在一个 array 中。我们都知道所有存储结构中, array 查找速度是最快的。所以,可以加速查找。发生碰撞时,让 array 指向多个 values 。即,数组每个位置上又生成一个梿表。
  • Map 中元素,可以将 key 序列、 value 序列单独抽取出来。
    使用 keySet() 抽取 key 序列,将 map 中的所有 keys 生成一个 Set 。
    使用 values() 抽取 value 序列,将 map 中的所有 values 生成一个 Collection 。
    为什么一个生成 Set ,一个生成 Collection ?那是因为, key 总是独一无二的, value 允许重复。

6、总结三:如何选择

  • 从效率角度:
    在各种 Lists ,对于需要快速插入,删除元素,应该使用 LinkedList (可用 LinkedList 构造堆栈 stack 、队列 queue ),如果需要快速随机访问元素,应该使用 ArrayList 。最好的做法是以 ArrayList 作为缺省选择。 Vector 总是比 ArrayList 慢,所以要尽量避免使用。
    在各种 Sets 中, HashSet 通常优于 HashTree (插入、查找)。只有当需要产生一个经过排序的序列,才用 TreeSet 。 HashTree 存在的唯一理由:能够维护其内元素的排序状态。
    在各种 Maps 中 HashMap 用于快速查找。
    最后,当元素个数固定,用 Array ,因为 Array 效率是最高的。
    所以结论:最常用的是 ArrayList , HashSet , HashMap , Array 。
  • 更近一步分析:
    如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
    要特别注意对哈希表的操作,作为 key 的对象要同时正确复写 equals 方法和 hashCode 方法。
    尽量返回接口而非实际的类型,如返回 List 而非 ArrayList ,这样如果以后需要将 ArrayList 换成 LinkedList 时,客户端代码不用改变。这就是针对抽象编程。

三、BlockingQueue

1、认识BlockingQueue

  • 在新增的Concurrent包中,高效并且线程安全
  • 阻塞队列(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒),可用来处理消费者生产者问题

2、核心方法

offer(anObject)    //存数据,如果可以容纳,则返回true,否则返回false(不阻塞当前执行方法的线程)。
offer(E o, long timeout, TimeUnit unit)   //可以设定等待的时间,如果在指定的时间内,还不能往队列中加入,则返回失败。
put(anObject)   //把anObject加到BlockingQueue里,如果没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续。

poll(time)   //取走排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。
poll(long timeout, TimeUnit unit)   //取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
take()   //取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入。
drainTo()    //一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数), 通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

四、BlockingQueue实现子类

公平锁:配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
非公平锁:配合一个LIFO队列来管理多余的生产者和消费者,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

1、ArrayBlockingQueue

  • 基于数组实现,内部维护了一个定长数组和两个整形变量,分别缓存着队列中的数据对象及标识队列的头部和尾部在数组中的位置。生产和消费时不会产生或销毁任何额外的对象实例。
  • 在放入数据和获取数据,都是共用同一个锁对象,两者无法真正并行运行。
  • 在创建时,默认采用非公平锁。可以控制对象的内部锁是否采用公平锁,

2、LinkedBlockingQueue

  • 基于链表实现,也维持着一个数据缓冲队列(该队列由一个链表构成)。生产和消费的时候会产生Node对象
  • 生产者端和消费者端分别采用了独立的锁来控制数据同步,高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
  • 构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

LinkedBlockingQueue和ArrayBlockingQueue的异同
相同
最常用的阻塞队列,当队列为空,消费者线程被阻塞;当队列装满,生产者线程被阻塞;
区别
a. 底层实现机制不同:LinkedBlockingQueue基于链表实现,在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大;而ArrayBlockingQueue内部维护了一个数组,在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例。
b. LinkedBlockingQueue中的消费者和生产者是不同的锁,而ArrayBlockingQueue生产者和消费者使用的是同一把锁;
c. LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小;ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值

3、PriorityBlockingQueue

基于优先级的阻塞队列,存储的对象必须是实现Comparable接口。队列通过这个接口的compare方法确定对象的priority。越小优先级越高,优先级越高,越优先取出
但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
使用案例

4、DelayQueue

  • 只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
  • 一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

使用场景:DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。

5、SynchronousQueue

  • 一种没有数据缓冲的等待队列。类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
  • 声明一个SynchronousQueue有两种不同的方式:公平锁和非公平锁。
  • 生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。
  • 队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中,队列中最多只有一个元素。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。

一个使用场景:
是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

SynchronousQueue创建:

// 如果为 true,则等待线程以 FIFO 的顺序竞争访问;否则顺序是未指定的。
// SynchronousQueue sc =new SynchronousQueue<>(true);//fair
SynchronousQueue sc = new SynchronousQueue<>(); // 默认不指定的话是false,不公平的

由于SynchronousQueue是没有缓冲区的,所以如下方法不可用:

sc.peek();// Always returns null
sc.clear();
sc.contains(1);
sc.containsAll(new ArrayList());
sc.isEmpty();
sc.size();
sc.toArray();
Integer [] in = new Integer[]{new Integer(2)};
sc.toArray(in);
sc.removeAll(new ArrayList());
sc.retainAll(new ArrayList());
sc.remove("a");
sc.peek();

不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。由于SynchronousQueue 队列中最多只有一个元素,所以这些方法是没有意义的,所以在对方法的实现体中阉割掉了。

SynchronousQueue 获取元素:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue sc = new SynchronousQueue<>(); // 默认不指定的话是false,不公平的
//      sc.take();// 没有元素阻塞在此处,等待其他线程向sc添加元素才会获取元素向下执行
        sc.poll();//没有元素不阻塞在此处直接返回null向下执行
        sc.poll(5,TimeUnit.SECONDS);//没有元素阻塞在此处等待指定时间,如果还是没有元素直接返回null向下执行
    }
}

SynchronousQueue 存入元素:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue sc = new SynchronousQueue<>(); // 默认不指定的话是false,不公平的
        // sc.put(2);//没有线程等待获取元素的话,阻塞在此处等待一直到有线程获取元素时候放到队列继续向下运行
        sc.offer(2);// 没有线程等待获取元素的话,不阻塞在此处,如果该元素已添加到此队列,则返回 true;否则返回 false
        sc.offer(2, 5, TimeUnit.SECONDS);// 没有线程等待获取元素的话,阻塞在此处等待指定时间,如果该元素已添加到此队列,则返回true;否则返回 false
    }
}

总结:

take和put是阻塞的获取和存储元素的方法,poll和offer是不阻塞的获取元素和存储元素的方法,并且poll和offer可以指定超时时间。

Demo:

public class SynchronousQueueMain {
    public static void main(String[] args) throws Exception {
        // 如果为 true,则等待线程以 FIFO 的顺序竞争访问;否则顺序是未指定的。
        // SynchronousQueue sc =new SynchronousQueue<>(true);//fair
               // 默认不指定的话是false,不公平的
        SynchronousQueue sc = new SynchronousQueue<>();
        new Thread(() -> { //生产者线程,使用的是lambda写法,需要使用JDK1.8
            while (true) {
                try {
                    sc.put(new Random().nextInt(50));
                    //将指定元素添加到此队列,如有必要则等待另一个线程接收它。
                    // System.out.println("sc.offer(new Random().nextInt(50)): "+sc.offer(new      
                                           Random().nextInt(50))); 
                    // 如果另一个线程正在等待以便接收指定元素,则将指定元素插入到
                                           此队列。如果没有等待接受数据的线程则直接返回false
                    // System.out.println("sc.offer(2,5,TimeUnit.SECONDS):
                    // "+sc.offer(2,5,TimeUnit.SECONDS));//如果没有等待的线程,则等待指定的
                                        时间。在等待时间还没有接受数据的线程的话,直接返回false
                    System.out.println("添加操作运行完毕...");//是操作完毕,并不是添加或获
                                        取元素成功!
                    Thread.sleep(1000);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() -> {//消费者线程。使用的是lambda创建的线程写法需要使用jdk1.8
            while (true) {
                try {
                    System.out.println("-----------------> sc.take: " + sc.take());
                    System.out.println("-----------------> 获取操作运行完毕...");//是操作完毕,并不
是添加或获取元素成功!
                    Thread.sleep(1000);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

6、总结

BlockingQueue不光实现了一个完整队列所具有的基本功能,同时在多线程环境下,他还自动管理了多线间的自动等待和唤醒功能,从而使得程序员可以忽略这些细节,关注更高级的功能。

参考文献

java中各种集合的用法和比较
BlockingQueue

你可能感兴趣的:(JAVA高级(5)—— 集合讲解)