Hey, Nice to meet You. 

必有过人之节.人情有所不能忍者,匹夫见辱,拔剑而起,挺身而斗,此不足为勇也,天下有大勇者,猝然临之而不惊,无故加之而不怒.此其所挟持者甚大,而其志甚远也.          ☆☆☆所谓豪杰之士,

夯实Java基础(十九)----集合

1、前言

集合在Java中的地位想必大家都知道,不用多BB了。无论是在我们现在的学习中还是在今后的工作中,集合这样一个大家族都无处不在,无处不用。在前面讲到的数组也是一个小的容器,但是数组不是面向对象对象的,它存在明显的缺陷,而集合恰好弥补了数组带来的缺陷。集合比数组更加灵活、更加实用。而且不同的集合框架可用于不同的场景。

我们简单来比较一下数组和集合区别:

  1. 数组能存放基本数据类型和对象,而集合类中只能存放对象。
  2. 数组容量固定无法动态改变,集合类容量可以动态改变。
  3. 数组无法判断其中实际存有多少元素,length只告诉了数组的容量,而集合的size()可以确切知道元素的个数。
  4. 集合有多种实现方式和不同适用场合,不像数组仅采用顺序表方式。
  5. 集合以类的形式存在,具有封装、继承、多态等类的特性,通过简单的方法和属性即可实现各种复杂操作,从而大大提高了软件的开发效率。

为了清晰的认识Java集合大家族,下面是整个Java集合的框架图:

image

image

通过上面的图片可以看到集合大家族的成员以及他们之间的关系。发现这也太多了吧,不过不要太慌张。我们现在只需要抓住它们的主干即可,即Collection、Map和Iterator,额外的还有两个对集合操作的工具类Collections和Arrays。其中虚框表示的是接口或抽象类,实框是类,虚箭头是实现,实箭头是继承。然后把它们捋一捋给分个类就应该清楚了。

把上面捋一捋可以总结集合主要分为两大系列:Collection和Map,Collection 表示一组对象,Map表示一组映射关系或键值对。

image

2、集合的分类

上面的Java集合框架图看起来比较的杂乱,所以我们对它们进行了分类处理,这样更加直观。

①、Collection接口:最基本的集合接口(单列数据)

  • List接口:所有元素按照进入的先后顺序有序存储,可重复集合
    • ArrayList:接口实现类,用数组实现,随机访问,增删慢,查询快,没有同步,线程不安全
    • LinkedList:接口实现类,用双向链表实现, 插入删除快,查询慢, 没有同步,线程不安全
    • Vector:接口实现类,用数组实现,它和ArrayList几乎一样,但是它是同步, 线程安全的(Vector几乎已经不用了)
      • Stack:继承自Vector类,Stack具有后进先出的特点
  • Set接口:不允许包含重复的值(可以有null,但是只有一个),无序,不可重复集合
    • HashSet:使用hash表(数组)存储元素,无序,其底层是包装了一个HashMap去实现的,所以查询插入速度较快
      • LinkedHashSet:继承HashSet类,它新增了一个重要特性,就是元素按照插入的顺序存储
    • TreeSet:底层基于TreeMap实现的,它支持2种排序方式:自然排序(Comparable)和定制排序(Comparator)
  • Queue接口:队列,它的特点是先进先出

②、Map接口:键值对的集合接口,不允许含有相同的key,有则输出一个key-value组合(双列数据)

  • Hashtable:接口实现类,用hash表实现,不可重复key,key不能为null,同步,效率稍低,线程安全
    • Properties:是Java 语言的配置文件所使用的类,以key=value 的 键值对的形式进行存储值。 key值不能重复。
  • HashMap:接口实现类 ,用hash表实现,不可重复key,key可以为null,没有同步,效率稍高 ,线程不安全
    • LinkedHashMap:双向链表和hash表实现,按照key的插入顺序存放
  • WeakHashMap:和HashMap一样,但它的键是“弱键”,垃圾收集器会自动的清除没有在其他任何地方被引用的键值对
  • TreeMap:用红黑树算法实现,它默认按照所有的key进行排序
  • IdentifyHashMap:它是一个特殊的Map实现,它的内部判断key是否相等用的是 ==,而HashMap则更加复杂

简单完成分类之后我们就从集合的特点和区别来进行一 一讲解。

3、Collection接口

Collection是最基本的集合接口,它是单列数据集合。在JDK中不提供Collection接口的任何直接实现,它只提供了更具体的子接口(即继承自Collection接口),例如List列表,Set集合,Queue队列,然后再由具体的类来实现这些子接口。通过具体类实现接口之后它们的特征就得以凸显出来,有些集合中的元素是有序的,而其他的集合中的元素则是无序的;有些集合允许重复的元素,而其他的集合则不允许重复的元素;有些集合允许排序,而其他的集合则不允许排序。

既然Collection接口是集合的层次结构的根接口,那么必定有常用的方法,我们来看一下:

  1. boolean add(E e):向集合中添加一个元素。集合更改则添加成功返回true,如果该集合不允许重复并且已经包含指定的元素。返回false。部分子类的add方法可能会限制添加到集合中的元素类型,或者不会将NULL添加到集合中。
  2. boolean addAll(Collection<? extends E> c):将指定集合中的所有元素添加到此集合中。在添加过程中如果被添加的集合发生了更改,addAll方法不具有幂等性。
  3. void clear():清空掉集合中的所有元素。
  4. boolean contains(Object o):如果集合中包含指定元素那么返回true。特别的,如果集合中也包含NULL元素的时候并且要查找的元素也是NULL的时候也返回true。
  5. boolean containsAll(Collection<?> c):如果该集合中包含指定集合中的所有元素的时候返回true。
  6. boolean isEmpty():如果集合中不包含元素返回true。
  7. Iterator iterator():返回在此集合的元素上进行迭代的迭代器。关于元素返回的顺序没有任何保证,除非此集合是某个能提供保证顺序的类实例。
  8. boolean remove(Object o):删除集合中的指定的元素。如果存在NULL,也删除。
  9. boolean removeAll(Collection<?> c):删除当前集合中所有等于指定集合中的元素。
  10. boolean retainAll(Collection<?> c):仅保留此集合中那些也包含在指定集合的元素。移除此集合中未包含在指定集合中的所有元素。
  11. int size():返回该集合中元素的个数。如果超过了Integer.MAX_VALUE,那么返回Integer.MAX_VALUE。
  12. Object[] toArray():返回包含此集合中所以元素的数组。
  13. T[] toArray():返回包含此集合中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。

4、Iterator接口

我们从上面Collection结构中可以看到其内部有一个iterator()方法,这个方法不是Collection中所特有的,而是重写了父类Iterable中的。因为Collection接口继承了java.lang.Iterable接口,而该接口中有一个iterator()方法。也就是说所有实现了Collection接口的集合类中都有iterator()方法,它用来返回实现了Iterator接口的迭代器对象。

介绍一下迭代的概念:迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。

Iterator接口的内部结构比较简单,其内部只定义的四个方法,它们分别是:

  1. boolean hasNext():如果迭代具有更多元素,则返回true。
  2. E next():返回迭代器中游标的下一元素。
  3. default void remove():从集合中删除此迭代器返回的最后一个元素。每次调用next后只能调用一次此方法,不能多次调用,否则会报错。如果在进行迭代时用调用此方法之外的其他方式修改了该迭代器所指向的集合,那么则迭代器的行为是不确定的。
  4. default void forEachRemaining(Consumer<? super E> action):对每个剩余元素执行给定的操作,直到所有元素都被处理或动作引发异常。如果指定了迭代的顺序,则按迭代的顺序执行。操作引发的异常将被转发给调用者。

接下来我们通过案例学习如何使用Iterator迭代集合中元素:

    @Test
    public void test() {
        Collection collection = new ArrayList();
        collection.add(123);
        collection.add("AA");
        collection.add('a');
        collection.add(true);
        collection.add(new String("ArrayList"));
        //创建迭代器对象
        Iterator iterator = collection.iterator();
        //hasNext()、next()测试
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
        System.out.println("-------remove()-------");
        Iterator iterator1 = collection.iterator();
        while (iterator1.hasNext()){
            Object next = iterator1.next();
            //iterator1.remove();不能在next()前先调用
            if (next.equals(123)){
                iterator1.remove();
                //iterator1.remove();不能在next()后再次调
            }
        }
        Iterator iterator2 = collection.iterator();
        while (iterator2.hasNext()){
            System.out.println(iterator2.next());
        }
    }

image

简易分析一下:当返回了Iterator对象之后可以理解为有一个指针,它指在第一个对象的上面(即123的上面,此时指针为空),当我们调用iterator.hasNext()的时候,判断是否还有下一个元素,如果有则返回true。然后调用iterator.next()使指针往下移并且返回下移以后集合位置上的元素,这样以此类推就输出了以上结果。

迭代器的实现原理:

我们在之前案例已经完成了Iterator遍历集合的整个过程。当遍历集合时,首先通过调用集合的iterator()方法获得迭代器对象,然后使用hashNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了能更好地理解迭代器的工作原理,接下来通过一个图例来演示Iterator对象迭代元素的过程:

image

在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,指向第一个元素,当第一次调用迭代器的next方法时,返回第一个元素,然后迭代器的索引会向后移动一位,指向第二个元素,当再次调用next方法时,返回第二个元素,然后迭代器的索引会再向后移动一位,指向第三个元素,依此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。

tips:在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生java.util.NoSuchElementException没有集合元素的错误。

5、List接口

List接口直接继承了Collection接口,它对Collection进行了简单的扩充,从而让集合凸显出它们各自的特征。在List中所有元素的存储都是有序的,而且是可重复存储的。用户可以根据元素存储位置的索引来操作元素。实现了List接口的集合主要有以下几个:ArrayList、LinkedList、Vector和Stack。

了解:List集合它有一个特有的迭代器——ListIterator。Iterator的子接口ListIterator是专门给List集合提供的迭代元素的接口,它的内部对Iterator功能进行一些扩充。例如增加的方法有hasPrevious()、nextIndex()、previousIndex()等等。

List接口特点:

  1. List集合所有的元素是以一种线性方式进行存储的,例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)
  2. 它是一个元素存取有序的集合。即元素的存入顺序和取出顺序有保证。
  3. 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  4. 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。

既然List接口直接继承了Collection接口,而且List是有序存储结构,那么List除了从Collection中继承的方法之外,必定会自己添加一些根据索引来操作集合元素的方法,我们来看一下:

  1. void add(int index, E element):将指定的元素插入此列表中的指定位置(可选操作)。
  2. boolean addAll(int index, Collection<? extends E> c):将指定集合中的所有元素插入到此列表中的指定位置。
  3. E get(int index):返回此列表中指定位置的元素。
  4. int indexOf(Object o):返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。
  5. int lastIndexOf(Object o):返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。
  6. E remove(int index):删除该列表中指定位置的元素(可选操作)。
  7. E set(int index, E element):用指定的元素(可选操作)替换此列表中指定位置的元素。
  8. List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置上的子集合。

5.1、ArrayList

ArrayList应该是我们最常见的集合,同时也是List中最重要的一个。ArrayList的特点是:随机访问、查询快,增删慢,轻量级,线程不安全。它的底层是用Object数组实现的,我们可以把ArrayList看做是一个可改变大小的数组。随着越来越多的元素被添加到ArrayList中,其容量是动态增加的(初始化容量是10,增量是原来的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1);)。

ArrList的特点:随机访问、查询快,增删慢,轻量级,线程不安全,初始化容量是10,增量是原来的1.5倍。

ArrayList源码分析总结(这里就不去看源码了,不是很难):

ArrayList底层实现:可变长的数组,有索引,查询效率高,增删效率低
     构造方法:
             new ArrayList():
                     jdk6中,空参构造直接创建10长度的数组
                     jdk7(新版)jdk8中,默认初始容量0,在添加第一元素时初始化容量为10
              new ArrayList(int initialCapacity):
                     指定初始化容量,第一次直接扩容为initialCapacity大小
     添加元素:add(E e);
         -首次添加元素,初始化容量为10
         -每次添加修改modCount属性值
         -每次添加检查容量是否足够,容量不足时需要扩容,扩容大小为原容量的1.5倍
     移除元素:remove(E e);
         -每次成功移除元素,修改modCount值
         -每次成功移除需要需要移动元素,以保证所以元素是连续存储的(删除操作效率低的原因)

5.2、LinkedList

LinkedList底层是通过双向链表实现的,所以它不能随机访问,而且需要查找元素必须要从开头或结尾(从靠近指定索引的一端)开始一个一个的找。使用双向链表则让增加、删除元素比较的方便,但查询变得困难。

所以LinkedList的特点是:查询慢,增删快,线程不安全。

由于LinkedList是双向链表实现的,所以它除了有List中的基本操作方法外还额外提供了一些方法在LinkedList的首部或尾部进行操作的方法(其实是继承Deque中的),如addXXX()、getXXX()、removeXXX()等等。同时,LinkedList还实现了Queue接口的子接口Deque,所以他还提供了offer(), peek(), poll()、pop()、push()等方法。我们简单来看一下:

  1. void addFirst(E e):在该列表开头插入指定的元素。
  2. void addLast(E e):将指定的元素追加到此列表的末尾。
  3. E element():检索但不删除此列表的头(第一个元素)。
  4. E getFirst():返回此列表中的第一个元素。
  5. E getLast():返回此列表中的最后一个元素。
  6. boolean offer(E e):将指定的元素添加为此列表的尾部(最后一个元素)。
  7. boolean offerFirst(E e):在此列表的前面插入指定的元素。
  8. boolean offerLast(E e):在该列表的末尾插入指定的元素。
  9. E poll XXX():检索并删除此列表的头(第一个元素)。
  10. E peek XXX():检索但不删除此列表的头(第一个元素)。
  11. E pop():从此列表表示的堆栈中弹出一个元素。
  12. void push(E e):将元素推送到由此列表表示的堆栈上。
  13. E remove XXX():从列表中删除指定元素的第一个出现(如果存在)。

LinkedList底层结构:

image

源码中节点使用Node对象表示一个完整的元素节点:

private static class Node<E> {
        E item;//数据元素
        Node<E> next;//下一个元素节点
        Node<E> prev;//上一个元素节点

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

5.3、Vector

Vector和ArrayList几乎一样,它们都是通过Object数组来实现的。但是Vector是线程安全的,和ArrayList相比,Vector中很多方法是用synchronized关键字处理过来保证证数据安全,这就必然会影响其效率,所以你的程序如果不涉及到线程安全问题,那么推荐使用ArrayList集合。其实无论如何大家都会选择ArrayList的,因为Vector已经很少用了,几乎面临淘汰。

另外二者还有一个区别就是Vector和ArrayList的扩容策略不一样,Vector的扩容增量是原来容量的2倍,而ArrayList是原来的1.5倍。

5.4、Stack

Stack的名称是堆栈。它是继承自Vector这个类,这就意味着,Stack也是通过数组来实现的。Stack的特性是:先进后出(FILO, First In Last Out)。此外Stack中还提供5个额外的方法使得Vector得以被当作堆栈使用。我们来看一下这五个方法:

  1. boolean empty():测试堆栈是否为空。Stack刚创建后就是空栈。
  2. E peek():查看此堆栈顶部的对象,而不从堆栈中删除它。
  3. E pop():出栈,删除此堆栈顶部的对象,并将该对象作为此函数的值返回。
  4. E push(E item):压栈,将元素推送到此堆栈的顶部。
  5. int search(Object o):返回一个对象在此堆栈上的基于1的位置。

6、Set接口

Set和List一样都是继承自Collection接口,但是Set和List的特点完全不一样。Set集合中的元素是无顺序的,且没有重复的元素。如果你试图将多个相同的对象添加到Set中,那么不好意思,它会立马阻止。 Set中会用equals()和hashCode()方法来判断两个对象是否相同,只要该方法的结果是true,Set就不会再次接收这个对象了。实现了Set接口主要有一下几个:HashSet、LinkedHashSet、TreeSet、EnumSet。Set中没有增加任何新的方法,用的都是继承自中Collection中的。

6.1、HashSet

HashSet是 按照哈希算法(hashCode)来存储集合中的对象,所以它是无序的,同时也不能保证元素的排列顺序。 其底层是包装了一个HashMap去实现的,所以其查询效率非常高。而且在增加和删除的时候由于运用hashCode的值来比较确定添加和删除元素的位置,所以不存在元素的偏移,效率也非常高。因此HashSet的查询、增加和删除元素的效率都是非常高的。但是HashSet增删的高效率是通过花费大量的空间换来的:因为空间越大,取余数相同的情况就越小。HashSet这种算法会建立许多无用的空间。使用HashSet接口时要注意,如果发生冲突,就会出现遍历整个数组的情况,这样就使得效率非常的低。

image

HashSet 集合判断两个元素相等的标准:1.两个对象通过 hashCode() 方法比较相等,2.并且两个对象的 equals() 方法返回值也相等。因此,存储到HashSet的元素要重写hashCode和equals方法。

HashSet使用简单举例:

    @Test
    public void test1() {
        //创建HashSet的实例
        HashSet<String> hashSet = new HashSet<>();
        //添加了两个AA元素
        hashSet.add("AA");
        hashSet.add("AA");
        hashSet.add("YYY");
        hashSet.add("BB");
        hashSet.add("HashSet");
        //添加了两个CC元素
        hashSet.add("CC");
        hashSet.add("CC");
        //使用增强for循坏
        for (String s : hashSet) {
            System.out.println("元素值:"+s+"--hash值:"+s.hashCode());
        }
    }

image

6.2、LinkedHashSet

LinkedHashSet继承自HashSet类,它不仅实现了哈希算法(hashCode),还实现了链表的数据结构,提供了插入和删除的功能。他有HashSet全部特性,但它新增了一个重要特性,就是元素按照插入的顺序存储。所以当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。正是因为多加了这样一种数据结构,所以它的效率较低,不建议使用,如果要求一个集合急要保证元素不重复,也需要记录元素的先后添加顺序,才选择使用LinkedHashSet。

image

LinkedHashSet使用简单举例:

    @Test
    public void test2() {
        //创建LinkedHashSet实例
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        //无序添加元素
        linkedHashSet.add("DD");
        linkedHashSet.add("BB");
        linkedHashSet.add("AA");
        linkedHashSet.add("EE");
        linkedHashSet.add("CC");
        //遍历打印值
        for (String s : linkedHashSet) {
            System.out.println("元素值:"+s+",hash值:"+s.hashCode());
        }
        boolean b = linkedHashSet.add("GG");
        System.out.println(b);
        //删除GG元素
        boolean gg = linkedHashSet.remove("GG");
        System.out.println(gg);
    }

image

6.3、TreeSet

TreeSet的底层是 基于TreeMap中实现的,都是基于红黑树实现的,红黑树是一种平衡二叉树,查询效率高于链表。它不仅能保证元素的唯一性,还能对元素按照某种规则进行排序。它继承了AbstractSet抽象类,实现了NavigableSet ,Cloneable,可序列化接口。而NavigableSet 又继承了SortedSet接口,此接口主要用于排序操作,即实现此接口的子类都属于排序的子类,有可排序的功能。TreeSet中的元素支持2种排序方式:自然排序或者定制排序,使用方式具体取决于我们使用的构造方法(默认使用自然排序)。

TreeSet特点:

  • 元素唯一
  • 实现排序(取出的元素是经过排序的)。

TreeSet是如何如何排序?如果TreeSet中的元素要实现元素唯一和排序,那么这些元素就必须是可以进行比较的,如何保证元素可比较呢?要么元素本身实现Comparable接口,从而实现可比较;要么给TreeSet容器传入一个实现了Comparator接口的比较器,使其可以对存入的元素进行比。

自然排序或者定制排序可以去参考博客:夯实Java基础(十五)——Java中Comparable和Comparator

 Java提供的核心类很多都实现了Comparable接口比如常见的String类,包装类,日期类等
      BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
      Character:按字符的 unicode值来进行比较
      Boolean:true 对应的包装类实例大于 false 对应的包装类实例
      String:从第一个字符开始,比较每一个字符,如果字符都相同再比较长度。
      Date、Time:比较毫秒值,后边的时间、日期比前面的时间、日期大

TreeSet使用简单举例(TreeSet传入实现Comparator实现类):

    @Test
    public void test3() {
        //创建TreeSet实例、使用Comparator定制排序
        TreeSet<Integer> treeSet = new TreeSet<>(new Comparator<Integer>() {
            //排序方式为:降序
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o1 > o2) {
                    return -1;
                } else if (o1 < o2) {
                    return 1;
                } else {
                    return 0;
                }
            }
        });
        treeSet.add(22);
        treeSet.add(44);
        treeSet.add(11);
        treeSet.add(33);
        for (Integer integer : treeSet) {
            System.out.println(integer);
        }
    }

image

6.4、EnumSet

EnumSet是专门为枚举类而设计的有序集合类,EnumSet中所有元素都必须是指定枚举类型的枚举值,在创建EnumSet时必须显式或隐式指定它对应的枚举类。

EnumSet使用简单举例:

@Test
public void test4(){
    //EnumSet的简单使用
    EnumSet<Season> seasons = EnumSet.allOf(Season.class);
    //遍历
    for (Season season : seasons) {
        System.out.println(season);
    }
}
//创建枚举类
enum Season {
    SPRING,SUMMER,AUTUMN,WINNER;
}

image

7、Queue接口

Queue接口与List、Set是同一级别的,都继承了Collection接口。Queue表示的是队列,它的特点是:先进先出(FIFO,First-in-First-Out) 。队列主要分为两大类:一类是BlockingDeque阻塞队列(Queue的子接口),它的主要实现类包括ArrayBlockQueue、PriorityBlockingQueue. LinkedBlockingQueue。另一类是Deque双端队列(也是Queue的子接口),支持在头部和尾部两端插入和移除元素,主要实现类包括:ArrayDeque、LinkedList。

Queue接口中包含的方法有:

  1. booleab add(E e):插入指定的元素要队列中,并返回true或者false,如果队列数量超过了容量,则抛出IllegalStateException的异常。
  2. boolean offer(E e):插入指定的元素到队列,并返回true或者false,如果队列数量超过了容量,不会抛出异常,只会返回false。
  3. E remove():搜索并删除最顶层的队列元素,如果队列为空,则抛出一个Exception。
  4. E poll():搜索并删除最顶层的队列元素,如果队列为空,则返回null。
  5. E element():检索但不删除并返回队列中最顶层的元素,如果该队列为空,则抛出一个Exception。
  6. E peek(): 检索但不删除并返回最顶层的元素,如果该队列为空,则返回null。

对于BlockingDeque阻塞队列的详解可以参考这篇博客:BlockingQueue(阻塞队列)详解 ,写的非常不错。

8、Collection系列的集合小结

image

Collection:集合根接口,存储一组对象。

  • List:接口,特点是,元素可重复,有序(存取顺序一致)
    • ArrayList:底层结构为数组,查询快,增删慢,线程不安全
    • LinkedList:底层结构为链表,查询慢,增删快
    • Vector:底层结构为数组,线程安全,效率低,不推荐使用
  • Set:接口,特点是,元素唯一
    • HashSet:底层结构为Hash表,查询和增删效率都高
    • TreeSet:底层结构为红黑树,查询效率高于链表,增删效率高于数组,元素实现排序
    • LinkedHashSet:底层结构为hash表+链表,链表保证元素的有序

9、Map接口

Map接口与Collection接口是完全不同的。Map中保存的是具有“映射关系”的数据,即是由一系列键值对组成的集合,提供了key到value的映射,也就是说一个key对应一个value,其中key和value都可以是任何引用数据类型。但是Map中不能存在相同的key值,对于相同key值的Map对象会通过equals()方法来判断key值是否相等,只要该方法的结果是true,Map就不会再次接收这个对象了,当然value值可以相同。实现了Map接口的类主要的有以下几个:HashMap、Hashtable、LinkedHashMap、WeakHashMap、TreeMap、IdentifyHashMap、EnumMap。其中Map集合还和Set集合有着非常紧密的联系,因为很多Set集合中底层就是用Map来实现的。

我们来看一下Map键值对根接口中的方法:

  1. void clear():删除该map集合中的所有键值对映射。
  2. boolean containsKey(Object key):检测map中有没有包含指定值为key的元素,如果有则返回true,否则返回false。
  3. boolean containsValue(Object value):检测map中有没有包含指定值为value的元素,如果有则返回true,否则返回false。
  4. Set<Map.Entry<K,V>> entrySet():返回map到一个Set集合中,以map集合中的Key=Value的形式返回到set中。
  5. V get(Object key):根据map集合中指定的key值来获取相应value值。
  6. Set keySet():返回map集合中所有key。
  7. V put(K key,V value):向map集合中添加key的值和value的值,当添加成功时返回null,否则返回value。
  8. void putAll(Map<? extends K,? extends V> m):把一个map集合合并到另一个map集合里。
  9. V remove(Object key):删除指定值为key的元素。
  10. int size():返回map集合中元素大小。
  11. Collection values():返回map集合中所有的value值到一个Collection集合。

9.1、Hashtable

Hashtable是一个古老的Map实现类,在JDK1.0就提供了,它继承自Dictionary类, 底层基于hash表结构+数组+链表实现实现,内部所有方法都是同步的,即线程很安全,但是效率低。 注意在Hashtable中key和value均不可为null

9.2、HashMap

HashMap是Map集合中使用最多的,也是集合中最最最重要的一个类,它是Hashtable的一个轻量级版本,它是继承了Abstractmap类。JDK1.7 HashMap底层基于数组+链表;JDK1.8 HashMap是基于数组+链表+红黑树,当链表长度大于8时会将链表转化成红黑树。内部所有方法是不同步的,即线程不安全,但是效率高。在 HashtMap 中 key 和 value 均均可为 null

JDK1.7 HashMap底层基于数组+链表,插入数据使用头插法(头插法多线程情况下可能死循环);

image


JDK1.8 HashMap是基于数组+链表+红黑树,当链表长度大于8时并且当容量大于64时会将链表转化成红黑树,插入数据使用尾插发。

image

9.2.1、HashMap的特点

  • 存储的元素为Key-Value的形式
  • Key可以为null,Value也可以为null
  • Key的值是唯一的
  • 元素是无序的
  • 线程不安全

9.2.2、HashMap的底层结构

  • JDK 1.7 采用数组+链表的方式,插入元素使用头插法(头插法在多线程的情况下可能会造成死循环)。
  • JDK 1.8 采用数组+链表+红黑树的方式,插入元素使用尾插法。

9.2.3、Hash算法

HashMap进行put一个新元素时,应该把新元素放到什么位置?如何计算这个新元素的位置?

这里通过源码可以发现有三步:

①、获取key的hashCode值

  • 哈希值要尽量与key对象的每项数据相关,并且尽量唯一

②、哈希值进行高16位与低16位的异或运算

  • 保证高低位都参与运算,为下一步准备
        //对key进行二次hash计算
        static final int hash (Object key){
            int h;
            //hash值进行高16异或低16位运算,为下一步取模运算做准备
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }

③、模运算

  • hash值& (length-1),结果为0到length-1范围的值,也就是整个数组的索引范围的值。

最终确定元素存入数组中的索引位置。以上算法目的是尽量保证每个元素均衡分布在数组中。

9.2.4、扩容机制

如果链表太长,也会影响查询效率,所以JDK8后,引入了红黑树,以进一步提升查询效率。当链表长度达到8时(并且数组长度>=64),链表转为红黑树,当树节点数量降为6时再次退化成链表。当元素数量继续上升,红黑树节点数量还会不断增加,查询效率还是逐渐降低,这时会进行数组扩容,重新分布元素,以降低树的高度,提升查询效率。

  • 如何扩容?

默认加载因子为0.75,容器中元素个数达到容器容量*0.75时,如16*0.75=12时,数组进行扩容,新容量为原来的2倍,这时会将所有元素重新计算哈希,重新分布元素位置。总之扩容很消耗性能,所以在预知元素个数的情况下,尽量指定合适容器容量,以减少扩容操作。

image

9.2.5、JDK1.8的HashMap简单总结

  • 底层结构哈希表,数组+链表+红黑树,增删改查综合效率较高
  • key是唯一,需要重写hashCode和equals
  • 初始化容量,jdk8中第一次添加元素时,扩容容量为16,早期版本(jdk8之前)创建集合对象时初始化容量为16.
  • 如果通过有参构造,指定初始容量为cap,实际初始容量为2的n次幂,并且大于等于cap。
  • 当然元素个数达到临界值,即加载因子0.75*容量时,进行扩容,扩容为原来的2倍
  • 链表大于8时,并且数组容量达到64,链表转为红黑树,链表小于6时,红黑树转为链表。(注意:当链表大于8时,而数组没有达到64则继续扩容数组至64为止)
  • 采用尾插发插入元素

HashMap的使用简单举例:

    @Test
    public void test5(){
        HashMap<String, String> map = new HashMap<>();
        map.put("1","张三");
        map.put("2","李四");
        map.put("3","王五");
        map.put("4","赵六");
        map.put("5","孙琦");
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        for (Map.Entry<String, String> entry : entrySet) {
            System.out.println("key="+entry.getKey()+",value = "+entry.getValue());
        }
    }

下面介绍一下Java中遍历Map集合的四种方式:

  • 方法一(推荐):在for-each循环中使用entry来遍历,通过Map.entrySet遍历key和value,这是最常见的并且在大多数情况下也是最可取的遍历方式。在键值都需要时使用。
@Test
    public void test5() {
        HashMap<String, String> map = new HashMap<>();
        map.put("1", "张三");
        map.put("2", "李四");
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        for (Map.Entry<String, String> entry : entrySet) {
            System.out.println("key=" + entry.getKey() + ",value = " + entry.getValue());
        }
    }

注:Map.Entry方法解释:Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)

  • 方法二:在for-each循环中遍历keys或values,如果只需要map中的键或者值,你可以通过keySet或values来实现遍历,而不是用entrySet。该方法比entrySet遍历在性能上稍好(快了10%),而且代码更加干净。
    @Test
    public void test5() {
        HashMap<String, String> map = new HashMap<>();
        map.put("1", "张三");
        map.put("2", "李四");
        //遍历map中的键
        for(String key:map.keySet()){
            System.out.println("key="+key);
        }
        //遍历map中的值
        for(String value:map.values()){
            System.out.println("value ="+value);
        }
    }
  • 方法三:使用Iterator遍历
    @Test
    public void test5() {
        HashMap<String, String> map = new HashMap<>();
        map.put("1", "张三");
        map.put("2", "李四");
        Iterator<Map.Entry<String,String>> entries = map.entrySet().iterator();
        while(entries.hasNext()){
            Map.Entry<String,String> entry = entries.next();
            System.out.println("key="+entry.getKey()+",value = "+entry.getValue());
        }
    }

该种方式看起来冗余却有其优点所在。首先,在老版本java中这是唯一遍历map的方式。另一个好处是,你可以在遍历时调用iterator.remove()来删除entries,另两个方法则不能。根据javadoc的说明,如果在for-each遍历中尝试使用此方法,结果是不可预测的。

从性能方面看,该方法类同于for-each遍历(即方法二)的性能。

  • 方法四:通过键找值遍历,这个代码看上去更加干净;但实际上它相当慢且无效率。
    @Test
    public void test5() {
        HashMap<String, String> map = new HashMap<>();
        map.put("1", "张三");
        map.put("2", "李四");
        for (String key : map.keySet()) {
            String value = map.get(key);
            System.out.println("Key = " + key + ", Value = " + value);
        }
    }

结论:一般来讲使用entrySet的方式进行遍历是效率最高的,因为hashMap内部的存储结构就是基于Entry的数组,在用这种方式进行遍历时,只需要遍历一次即可。而使用其他方式的时间复杂度可以会提高,例如:keySet方式,每次都需要通过key值去计算对应的hash,然后再通过hash获取对应的结果值,因此效率较低。

9.3、LinkedHashMap

LinkedHashMap基于双向链表和数组实现,内部结构和HashMap类似,就是多加了一个双向链表结构。根据key的插入顺序进行存储。(注意和TreeMap对所有的key-value进行排序进行区分)

9.4、WeakHashMap

WeakHashMap与HashMap的用法基本相似。区别在于,HashMap的key保留了对实际对象的"强引用",这意味着只要该HashMap对象不被销毁,该HashMap所引用的对象就不会被垃圾回收。但WeakHashMap的key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,当垃圾回收了该key所对应的实际对象之后,WeakHashMap也可能自动删除这些key所对应的key-value对。

9.5、TreeMap

TreeMap底层是用红黑树算法实现,它实现SortMap接口,所以其内部元素默认按照所有的key进行排序。当然也支持2种排序方式:自然排序或者定制排序。其中TreeMap中的key不可null,而它非线程安全的。使用可以参考上面的TreeSet示例,使用方式差不多。

9.6、IdentifyHashMap

IdentityHashMap是一种可重复key的集合类。它和HashMap类似,所以我们一般都是拿IdentityHashMap和HashMap来进行比较,因为它们两者判断重复key的方式不一样。IdentifyHashMap中判断重复key相等的条件是:(k1= =k2),也就是说它只比较普通值是否相等,不比较对象中的内容。而HashMap中类判断重复key相等的条件是: (k1= =null?k2==null:k1.equals(k2))= =true),它不仅比较普通值,而且比对象中的内容是否相等。

IdentityHashMap的简单举例:

    @Test
    public void test6(){
        //创建HashMap实例,添加Integer实例为key
        HashMap hashMap=new HashMap();
        hashMap.put(1,"hello");
        hashMap.put(1,"hello");
        hashMap.put(new Integer(2),"hello");
        hashMap.put(new Integer(2),"hello");
        hashMap.put(new Integer(2),"hello");
        System.out.println("HashMap:"+hashMap.toString());
        //创建IdentityHashMap实例,添加Integer实例为key
        IdentityHashMap<Object, Object> identityHashMap = new IdentityHashMap<>();
        identityHashMap.put(3,"world");
        identityHashMap.put(3,"world");
        identityHashMap.put(new Integer(4),"world");
        identityHashMap.put(new Integer(5),"world");
        identityHashMap.put(new Integer(4),"world");
        System.out.println("IdentityHashMap:"+identityHashMap.toString());
    }

image

9.7、EnumMap

EnumMap这个类是专门为枚举类而设计的有键值对的集合类。集合中的所有键(key)都必须是单个同一个类型的枚举值,创建EnumMap时必须显式或隐式指定它对应的枚举类。当EnumMap创建后,其内部是以数组形式保存,所以这种实现形式非常紧凑高效。EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护来维护key-value对的次序。可以通过keySet()、entrySet()、values()等方法来遍历EnumMap即可看到这种顺序。EnumMap不允许使用null作为key值,但允许使用null作为value。如果试图使用null作为key将抛出NullPointerException异常。如果仅仅只是查询是否包含值为null的key、或者仅仅只是使用删除值为null的key,都不会抛出异常。

EnumMap的代码示例如下:

@Test
public void test7(){
    //EnumMap的简单使用
    EnumMap<Season,String> map=new EnumMap<Season, String>(Season.class);
    map.put(Season.SPRING,"春暖花开");
    map.put(Season.SUMMER,"夏日炎炎");
    map.put(Season.AUTUMN,"秋高气爽");
    map.put(Season.WINNER,"冬暖夏凉");
    //遍历map有很多种方式,这里是map遍历的一种方式,这种方式是最快的
    for (EnumMap.Entry<Season,String> entry:map.entrySet()){
        System.out.println(entry.getKey()+","+entry.getValue());
    }
}

enum Season {
    SPRING, SUMMER, AUTUMN, WINNER;
}

image

10、Collections工具类

Collections 是一个操作 Set、List 和 Map 等集合的工具类。Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法(泛型被屏蔽掉了):

  1. public static boolean addAll(Collection<? super T> c,T... elements)将所有指定元素添加到指定 collection 中。
  2. public static int binarySearch(List<? extends Comparable<? super T>> list,T key)在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且必须是可比较大小的,即支持自然排序的。而且集合也事先必须是有序的,否则结果不确定。
  3. public static int binarySearch(List<? extends T> list,T key,Comparator<? super T> c)在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且集合也事先必须是按照c比较器规则进行排序过的,否则结果不确定。
  4. public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)在coll集合中找出最大的元素,集合中的对象必须是T或T的子类对象,而且支持自然排序
  5. public static T max(Collection<? extends T> coll,Comparator<? super T> comp)在coll集合中找出最大的元素,集合中的对象必须是T或T的子类对象,按照比较器comp找出最大者
  6. public static void reverse(List<?> list)反转指定列表List中元素的顺序。
  7. public static void shuffle(List<?> list) List 集合元素进行随机排序,类似洗牌
  8. public static <T extends Comparable<? super T>> void sort(List list)根据元素的自然顺序对指定 List 集合元素按升序排序
  9. public static void sort(List list,Comparator<? super T> c)根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
  10. public static void swap(List<?> list,int i,int j)将指定 list 集合中的 i 处元素和 j 处元素进行交换
  11. public static int frequency(Collection<?> c,Object o)返回指定集合中指定元素的出现次数
  12. public static void copy(List<? super T> dest,List<? extends T> src)将src中的内容复制到dest中
  13. public static boolean replaceAll(List list,T oldVal,T newVal):使用新值替换 List 对象的所有旧值
  14. Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
  15. Collections类中提供了多个unmodifiableXxx()方法,该方法返回指定 Xxx的不可修改的视图。

参考链接:

posted @ 2019-08-30 11:43  唐浩荣  阅读(542)  评论(0编辑  收藏  举报