整理了一篇关于java集合的文章,有图有真相

1.概述

java集合是用来保存对象的,而数组只能存储基本数据类型,为什么要引入集合呢?我们先来看一看数组的特点和缺点,首先数组是一段连续的内存空间,在初始化之后长度就确定了,并且也确定了初始化时的类型,这两个特点就导致了数组存在一些弊端

  • 长度固定不易于扩展
  • 只能存储固定类型的数据
  • 数组提供的方法API少,增删改等操作不方便
  • 存储有序可重复的元素

在这里插入图片描述

基于这些问题,java推出了一系列的用不同数据结构实现的集合,可以动态的存储对象,并且提供了大量的API,方便我们进行操作,不管是在做算法题还是开发具体的应用,这些API都起到了非常重要的作用,举个例子,springMVC中视图和数据封装,JSON数据传递,Spring存储bean都是基于集合中的Map结构,下面我们就来具体了解以下这些集合吧

2.集合框架图

在这里插入图片描述
这是整个java集合的框架图,虚线框表示抽象类(接口可看作特殊的抽象类),粗实线是我们关注的重点集合类,仔细观察上面这张图,可以看出这个框架可以分为CollectionMap两个不同的部分

2.1 Collection系

在这里插入图片描述
基于Collection接口实现的集合又可以分为

  • Set:元素无序不可重复的集合
  • List:元素有序可重复的集合

2.2 Map系

在这里插入图片描述

3.Collection接口

这个接口是LIst , Set , Queue 的父接口,Collection接口的方法:
在这里插入图片描述

只要实现了这个接口,都可以使用这些API,需要注意的是,当我们的对象需要添加进容器时,最好重写equals方法,因为在contains或者remove时都会用equals判断该对象是否存在,比如ArrayList中的contains方法:

	public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

	public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

4.Foreach与迭代器

我们之前使用foreach都是在遍历数组的时候使用的,前面的继承图也指出了Collection可以得到迭代器:

  • public interface Collection<E> extends Iterable<E>

由于继承了Iterable接口,其内部有Iterator<E> iterator();来使实现了Collection的类可以通过这个方法获取迭代器,换句话说,只要实现了Iterable接口,就要重写Iterator<E> iterator();方法,就可以通过Foreach来遍历元素,下面举个例子:

  public static void main(String[] args) {
        ArrayList<Person> array = new ArrayList<Person>();
 
        Person p1 = new Person("Tom1");
        Person p2 = new Person("Tom2");
        Person p3 = new Person("Tom3");
        Person p4 = new Person("Tom4");
 
        array.add(p1);
        array.add(p2);
        array.add(p3);
        array.add(p4);
 
        Iterator<Person> iterator = array.iterator();
 
        for (Person pp : array){
            System.out.println(pp.getName());
        }
        
         while(iterator.hasNext()){
            System.out.println(iterator.next().getName()); //输出的是wang,而不是tom
        }

        

我们可以发现,用Foreach和迭代器实现的效果是一样的,其实Foreach就是利用迭代器实现的,这是因为程序在运行时,发现我们正在用foreach遍历集合,并且该集合实现了Iterable接口,就会在底层用迭代器的hasNext和next方法来实现遍历,需要注意的是:

由于迭代器和Collection实现类都有remove方法,且foreach是通过迭代器实现,故在使用增强for循环时,使用集合的remove方法则会导致原来的集合变化而导致错误,所以应该使用迭代器的remove方法

既然提到了使用迭代时remove方法会出错,大家就不妨看看这篇文章:
Java List的remove()方法陷阱以及性能优化

4.1迭代器执行原理

受制于篇幅以及不想重新写一遍,可以去看我的另一篇文章:
Iterator迭代器执行原理,hasNext和next指针问题

4.2使用for循环还是迭代器Iterator对比

采用ArrayList对随机访问比较快,而for循环中的get()方法,采用的即是随机访问的方法,因此在ArrayList里,for循环较快

采用LinkedList则是顺序访问比较快,iterator中的next()方法,采用的即是顺序访问的方法,因此在LinkedList里,使用iterator较快

从数据结构角度分析,for循环适合访问顺序结构,可以根据下标快速获取指定元素.而Iterator 适合访问链式结构,因为迭代器是通过next()和Pre()来定位的.可以访问没有顺序的集合.

而使用 Iterator 的好处在于可以使用相同方式去遍历集合中元素,而不用考虑集合类的内部实现(只要它实现了 java.lang.Iterable 接口),如果使用 Iterator 来遍历集合中元素,一旦不再使用 List 转而使用 Set 来组织数据,那遍历元素的代码不用做任何修改,如果使用 for 来遍历,那所有遍历此集合的算法都得做相应调整,因为List有序,Set无序,结构不同,他们的访问算法也不一样.(还是说明了一点遍历和集合本身分离了

5.List

5.1 List概述

List接口是Collection接口的子接口之一,这个接口的实现类存储元素的特点:有序可重复,集合中每个元素都可以根据索引获取:list.get(int index),可以将它理解为一个"动态数组",至于为什么动态下面再讲,它有三个主要的实现类:

  • ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
  • LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
  • Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素

在这里插入图片描述

5.2 ArrayList

这个是我们用的最多的List接口了,ArrayList底层使用
在这里插入图片描述
数组来存储元素,它的构造方法如下:

public ArrayList(int initialCapacity)//构造一个具有指定初始容量的空列表。    
public ArrayList()      //默认构造一个初始容量为10的空列表。    
public ArrayList(Collection<? extends E> c)//构造一个包含指定 collection 的元素的列表

这里要说明一下,在jdk7中,通过默认构造器创建ArrayList是在底层创建长度是10的Object数组,通过不断的add(E e)添加元素,当下次添加达到最大容量,就会触发扩容机制,即扩容为原来的1.5倍,并且将原数组中的内容复制到扩完容的新数组中,在jdk8中出现了一点变化,使用默认构造器初始化时:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

其实是创建了一个空的object数组,当我们在第一次添加数据的时候才会创建长度为10的数组,其他方面还是和JDK7一样

5.3 LinkedList

这个集合类底层使用双向链表来存储数据,内部没有声明数组,而是定义了一个内部类和两个指针:


transient Node<E> first;

transient Node<E> last;

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;
        }
    }

对于双向链表的增删改查大家可以去看看LinkedList的底层源码实现,并尝试手写出一个链表

5.4 Vector

这个类是一个线程安全的类,和ArrayList在jdk7中默认初始化容量为10的数组一样,不同的地方在于它默认扩容为原来的两倍,现在很少几乎不用这个类了

5.5 ArrayList和LinkedList

ArrayList

  • 优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)
  • 缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低

LinkedList

  • 优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景
  • 缺点:因为LinkedList要移动指针,所以查询操作性能比较低

比较

  • 当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList

5.6 ArrayList和Vector

ArrayList和Vector都是用数组实现的,区别:

  • Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰,这样就导致了Vector在效率上无法与ArrayList相比
  • 两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式不同,Vector可以设置增长因子,而ArrayList不可以

6.Set

6.1 概述

set接口没有定义新的方法,使用的都是collection接口中的方法,Set集合中不存在下标,因此无法通过下标遍历,可以用迭代器和forEach遍历,用hashSet举例来说明Set接口的特性:

  • 无序性:不等于随机性,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据哈希值决定的
  • 不可重复性:保证添加的元素按照equals()判断时,不能返回true,即相同的元素只能添加一个

它主要有三个实现类,简单介绍一下:

  • HashSet:主要实现类,可存储null值
  • LinkedHashSet:HashSet的子类,遍历其内部数据时,可按照添加的顺序遍历
  • TreeSet:可以按照添加对象的指定属性进行排序

6.2 HashSet

HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,但不能存多个null,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性,所以往HashSet添加的元素必须重写hashcode()和equals()方法

  • HashSet存储元素过程:

存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,就该存储了,此时就要采用哈希的解决地址冲突算法,在当前hashCode值处类似一个新的链表, 在同一个hashCode值的后面存储存储不同的对象,这样就保证了元素的唯一性

其实阅读过源码的人已经知道了,HashSet底层是用的HashMap实现的:
在这里插入图片描述
所以HashSet的存储元素的不可重复性,以及特殊的数组+链表存储元素的方式也和HashMap有关了

6.3 LinkedHashSet

底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高

6.4 TreeSet

底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性。根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法,返回0就认为两个对象相等

7.List和Set小结

  • List,Set都是继承自Collection接口
  • List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
  • Set和List对比:
    • Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
    • List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
  • TreeSet 是二叉树(红黑树的树据结构)实现的,Treeset中的数据是自动排好序的,不允许放入null值
  • HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet

在这里插入图片描述

8.Map

Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。
在这里插入图片描述

8.1 散列表

在讲HashMap之前我们需要先了解什么是散列表HashTable,首先它是一种数据结构,它会为存入它的对象计算哈希值(hashcode),也叫散列码,hashcode由特殊的算法产生,在java中,object类就有计算hashcode的方法,而且该方法应该与equals方法兼容,即equals相等的两对象hashcode相等,hashcode相等的两对象equals不一定相等,而hashcode相等了往往会进行equals进行二次判断,才能最终确定两个对象是否相等,在Java中,散列表利用数组+链表实现,其添加元素的具体思路就是:计算对象得到hashcode值,然后经过特定的算法得到在数组中存储的下标,如果该下标对应的内存空间没有其他元素就可以添加进去,如果有,就是我们说的散列冲突(哈希冲突),这时候就需要对已经存在的对象进行比较,看这个对象是否存在

在java8中,如果数组存储元素满了,则该数组对应索引上的链表变为平衡二叉树,也就是红黑树

想要更好的提高散列表的性能,需要指定一个初始长度的数组,有一种说法认为讲数组的长度设置为一个素数,为2的幂,通常是16,当需要扩容时,就再次进行2的幂运算,但是我们并不能知道要存储多少个元素,如果这个散列表小了,很容易存满,就需要再散列,即扩容,讲所有元素再次计算hashcode,插入新的散列表中,丢弃原有的表,装载因子决定何时对表进行再散列,一般是0.75,即表中超过75%的位置存储了数据,就对表进行再散列

8.2 HashMap

研究HashMap对于一个java开发人员来说必不可少,甚至是一个必须的过程了,众所HashMap底层是数组+链表+红黑树实现的,那么我们就来探讨一下这个过程,刚才讲到了散列表,HashMap就是用的散列表实现的,我们这里针对JDK8来探讨,先搞懂一下HashMap类中各个常量的意思:
在这里插入图片描述
HashMap中存入键值对数据其实是存入它的内部类Node数组中的:
在这里插入图片描述
在这里插入图片描述
已经可以看出,HashMap的散列表其实就是Node数组+Node类中的指针实现的,结合上面的散列表和相关常量的解释,现在来说一下HashMap的存储过程:
HashMap在调用默认构造器初始化的时候并没有立刻创建数组,而是在首次put时创建长度为16的数组,并使用默认加载因子,在第一次put时,扩容临界值threshold为12,如果之后的put一旦超过这个值就会触发扩容,扩容的方式为两倍,而添加元素的过程和HashSet一样,这里就不赘述了,需要注意的是,当数组长度>64,且链表长度>8时,就会将链表转化为红黑树

8.3 HashMap和HashTable的比较

在这里插入图片描述

8.4 TreeMap

在这里插入图片描述

8.5 Map的其他类

IdentityHashMap和HashMap的具体区别,IdentityHashMap使用 == 判断两个key是否相等,而HashMap使用的是equals方法比较key值。
在这里插入图片描述

9. 小结

HashMap 非线程安全
HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。
TreeMap:非线程安全基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
适用场景分析:
HashMap和HashTable:HashMap去掉了HashTable的contains方法,但是加上了containsValue()和containsKey()方法。HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允许空键值,而HashTable不允许。

HashMap:适用于Map中插入、删除和定位元素。
Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

线程安全类集合:Vector,HashTable
非线程安全集合类:LinkedList,ArrayList,HashSet,HashMap
根据数据结构分类
ArrayXxx:底层数据结构是数组,查询快,增删慢
LinkedXxx:底层数据结构是链表,查询慢,增删快
HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序

参考:
java集合超详解
《Java编程思想》
《Java核心技术卷一》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章