【深入理解java集合】-java容器类体系

一、java集合简介

1、集合的由来 

面向对象语言对事物都是以对象的形式来体现,为了方便对多个对象的操作,就需要将对象进行存储,集合就是存储对象最常用的一种方式。

2、集合的特点

  • 用于存储对象的容器。(容器本身就是一个对象,存在于堆内存中,里面存的是对象的地址)
  • 集合的长度是可变的。
  • 集合中不可以存储基本数据类型值。(只能存对象)

注意:想用集合存基本数据类型怎么办?  

利用自动装箱、拆箱特性。  

例:al.add(5); // 相当于al.add(new Integer(5))

3、集合和数组的区别

数组不是面向对象的,存在明显的缺陷,集合弥补了数组的缺点,比数组更灵活更实用而且不同的集合框架类可适用不同场合。如下:

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

二、java集合体系结构

1、体系结构图

实线代表继承,虚线代表实现。

1.1单列集合

List

Set

1.2双列集合

Map

1.3 总图

 

三、java集合顶层接口及主要实现

集合框架的另外一种数据类型的总接口是Map,基于Key-Value进行存储数据的,其中Key键值是不可重复的,主要是通过类的hashCode()和equal()进行保证的

1、Collection接口

Collection是java集合框架体系的总接口,其他集合框架都是实现Collection,封装了集合框架的公共操作:add(E),addAll(),remove(E),removeAll(),contains(),iterator(),size()…。

集合框架Collection的三种主要实现如下:List(列表),Set(散列集,有一个key-value的Map进行维护,其中key值保证Set集合里元素的唯一性),Queue(队列,先进先出,底层实现可以用List列表或者LinkedList链表)

2、List(列表)

List(序列),元素有序,可重复,也可以是null值,元素之间的顺序关系可以由添加到列表的先后来决定,也可以由元素值的大小来决定;

List接口使用下标访问元素,可以精确获取元素,控制元素的插入位置,或删除修改指定位置的元素;

List接口除继承Collection的方法外,新增许多方法,使之能够在列表中根据具体位置操作元素。

List常用方法:

  • void add(int index, Object element) :添加对象element到位置index上
  • boolean addAll(int index, Collection collection) :在index位置后添加容器collection中所有的元素
  • Object get(int index) :取出下标为index的位置的元素
  • int indexOf(Object element) :查找对象element 在List中第一次出现的位置
  • int lastIndexOf(Object element) :查找对象element 在List中最后出现的位置
  • Object remove(int index) :删除index位置上的元素
  • ListIterator listIterator(int startIndex) :返回一个ListIterator 跌代器,开始位置为startIndex
  • List subList(int fromIndex, int toIndex) :返回一个子列表List ,元素存放为从 fromIndex 到toIndex之前的一个元素

具体实现有ArrayList,LinkedList和Vector,各实现在公共用法上没有区别,但由于背后的支撑不同(数据结构),所以在功能(效率)上还是有区别的,同时也增加各自特色方法。

2.1 vector

Vector底层基于数组实现,可进行同步操作,增删,查询都很慢!100%延长(扩容),重量级组件,现在几乎不再使用(之所有还遗存,是为了对旧程序兼容),在多线程环境下可以考虑。

2.2 ArrayList

ArrayList底层基于Object数组实现(默认长度10),按顺序存储元素以及快速按照元素下标进行获取元素,不可同步的,替代了Vector,查询的速度快,增删速度慢。50%延长(扩容)。

同数组特性一样,查询时是从容器的第一个元素往后找,由于数组的内存空间是连续的,所以查询快;插入、删除的话受影响的元素内存地址都要前移或后退(除末尾增删外),时间复杂度为O(n),所以效率较低。

2.3 LinkedList

内部是双向循环链表数据结构(动态线性表,无默认长度),是不同步的。增删元素的速度很快。LinkedList可以在具体下标位置删除和添加元素,在许多需要根据具体下标添加和删除元素的应用场景下比ArrayList有更好的性能。

双向循环链表:在此链表上每一个数据节点都由三部分组成:前指针(指向唯一前继的节点位置),数据域,后指针(指向唯一后继的节点位置)。最后一个节点的后指针指向第一个节点(首)的前指针,形成一个循环。

链表的内存空间是不连续的,从头查询效率低,;增删时只需改变单个指针的指向,所以快;链接存储结构插入删除时间复杂度为n(1)。

常利用LinkedList实现栈(stack)、队列(queue)、双向队列(double-ended queue )。它具有方法addFirst()、addLast()、getFirst()、getLast()、removeFirst()、removeLast()等。

2.4 ArryList和Vector可变长度数组的原理:

思路:当默认长度的数组不够存储时,会建立一个新数组。将原来数组的内容拷贝到新的数组当中,并将新增加的元素追加到拷贝完的数组尾,如果仍然不够重复上述动作。其中,ArryList的增加是以原来50%长度进行增加,而Vector是按照100%延长。

实现机制:ArrayList.ensureCapacity(int minCapacity)

首先得到当前elementData属性的长度oldCapacity。

然后通过判断oldCapacity和minCapacity参数谁大来决定是否需要扩容, 如果minCapacity大于 oldCapacity,那么我们就对当前的List对象进行扩容。

扩容的的策略为:取(oldCapacity * 3)/2 + 1和minCapacity之间更大的那个。然后使用数组拷贝的方法,把以前存放的数据转移到新的数组对象中

如果minCapacity不大于oldCapacity那么就不进行扩容。

3、Set(集合)

散列集,不能保证存储元素的顺序,保证存储元素的不可重复性。

Set接口并没有声明其他方法,它的方法都是从Collection接口继承而来

具体实现有HashSet,LinkedHashSet,TreeSet:

3.1 HashSet

HashSet是哈希散列集,底层由HashMap支持的,主要使用hashCode()和equal()方法确保Key值不可重复性来保证元素的唯一性。HashSet采用散列(hash)算法来存取集合中的元素,因此具有比较好的读取和查找性能。

HashSet允许存入null,默认创建初始化容量是16,默认上座率为0.75(集合饱和度0.0~1.0之间),当集合中元素个数超过了容量与上座率的乘积,容量就会自动翻倍,也称再散列,产生一个新的散列表,所有元素存放到新的散列表中,原先的散列表将被删除。负载因子越高(越接近1.0),内存的使用效率越高,元素的寻找时间越长。负载因子越低(越接近0.0),元素的寻找时间越短,内存浪费越多。

HashSet的equals和HashCode:

当一个类有自己特有的逻辑相等概念(不同于对象身份的概念),需要重新equals(),重写equals()都应该重写HashCode()。

试想如果重写了equals方法但不重写hashCode方法,即相同equals结果的两个对象将会被HashSet当作两个元素保存起来,这与我们设计HashSet的初衷不符(元素不重复)。

另外如果两个元素HashCode相等但equals结果不为true,HashSet会将这两个元素保存在同一个位置,并将超过一个的元素以链表(拉链)方式保存,这将影响HashSet的效率。

3.2 LinkedHashSet

内部数据结构是哈希表和链表,是有顺序的HashSet,保持元素的添加顺序。

LinkedHashSet是HashSet的一个子类,其所有方法都继承自HashSet, 而它能维持元素的插入顺序的性质则继承自LinkedHashMap。LinkedHashSet底层由LinkedHashMap实现,则可以保证按照元素插入集合的顺序进行提取。

LinkedHashSet根据HashCode的值来决定元素的存储位置和唯一性,同时它还用一个链表来维护元素的插入顺序,插入的时候即要计算hashCode又要维护链表,而遍历的时候只需要按链表来访问元素。

LinkedHashSet需要用额外的链表维护元素的插入顺序,因此在插入时性能比HashSet低,但在迭代访问(遍历)时性能更高。

3.3 TreeSet

内部数据结构是有序的二叉树,它的作用是(仅仅是)提供有序的Set集合,所以声明有与排序位置(大小)有关的一系列方法,如:first(),last(),headSet(),higher()…等。

TreeSet的数据元素不能为null,且只允许存入同一类的元素(为了比较),数据元素要求实现Comparable接口,或者使用Comparator构造,即添加到 TreeSet 的元素必须是可排序的。

TreeSet底层是由TreeMap支持的,可以按照Comparable接口(自然排序)对存储对象排序或者Comparator比较器接口(定制排序)进行存储对象的比较排序。

HashSet是基于Hash算法实现的,其性能都优于TreeSet。我们通常都应该使用HashSet,在我们需要排序的功能时,才使用TreeSet。一般说来,先把元素添加到 HashSet,再把集合转换为 TreeSet 来进行有序遍历会更快(构造器转换)。

4、Queue(队列)

队列是一般按照先进先出First-In-First-Out的规则,元素被追加到队列末尾,在队列头进行删除,底层实现可以是数组,也可以是链表。主要实现有PriorityQueue和LinkedList,

其中PriorityQueue优先队列默认情况下以Comparable按照元素的自然顺序进行排序,最小值的元素优先级最高最先删除,也可以传入指定的比较器Comparator进行元素间的比较。

在java.util.concurrent包里有ArrayBlockingQueue,LinkedBlockingQueue等实现同步机制的队列数据结构,有兴趣可以查看源码进行研究。

5、Map(映射类)

Map提供了一种映射关系,元素是以键值对(key-value)的形式存储的,能根据key快速查找value;

Map中的键值对以Entry类型的对象实例形式存在;

key值不能重复,value值可以重复;

key对value是多(一)对一的关系;

Map接口提供了返回key值集合、value值集合、Entry值集合,的方法;

主要实现有HashMap,LinkedHashMap,TreeMap。

5.1 HashMap

在不需要保证元素的顺序情况下,HashMap是非常高效的,主要是通过hashCode()和equal()方法进行哈希化存储的,所以要求存储的key要实现hashCode()和equal()方法。

尤其强调当一个对象被当作键值(或索引)来使用的时候要重写hashCode()和equal()方法两个方法。覆写equals后,两个不同实例可能在逻辑上相等,但是根据Object.hashCode方法却产生不同的散列码,违反“相等的对象必须具有相等的散列码”。导致,当你用其中的一个作为键保存到hashMap、hashTable或hashSet中,再以“相等的”找另 一个作为键值去查找他们的时候,则根本找不到。

不同类型的hashCode取值

5.2 LinkedHashMap

LinkedHashMap可以保证存储元素的顺序;可以按照元素的存储顺序或者元素的访问顺序进行排序存储,它的底层是由HashMap加上循环双向链表实现的。

5.3 TreeMap

TreeMap在遍历排序好的键值是非常高效率的,默认是按照元素的实现Comprable接口方法进行排序的,也可以传入Comparator比较器接口进行比较排序。

5.4 Map的迭代方式

Map本身没有迭代器。

方法一:只需要Value,不需要Key的时候

利用Map接口的values()方法,返回此映射中包含的值的 Collection (值不唯一),然后通过Collecion的迭代器进行迭代。

方法二:keySet

通过keySet方法获取map中所有的键所在的Set集合(Key和Set的都具有唯一性),再通过Set的迭代器获取到每一个键,再对每一个键通过Map集合的get方法获取其对应的值即可。

方法三:Map.Entry<K,V>使用iterator。  

通过Map的entrySet()方法,将键和值的映射关系作为对象存储到Set集合中。这个映射关系的类型就是Map.Entry类型(结婚证)。再通过Map.Entry对象的getKey和getValue获取其中的键和值。

方法四:Map.Entry<K,V>使用加强for遍历

通过Map.entrySet()方法遍历key和value(推荐,尤其是容量大时)

6、Iterator接口

对 Collection 进行迭代的迭代器,即对所有的Collection容器进行元素取出的公共接口。

该迭代器对象依赖于具体容器,因为每一个容器的数据结构都不同,所以该迭代器对象是在具体容器中进行内部实现的。(内部类,可以看具体容器的源码)

对于使用容器者而言,具体的实现方法不重要,只要通过具体容器获取到该实现的迭代器的对象即可,也就是iterator()方法,而不用new。(Iterator<String> ite=list.iterator();) 

6.1 ListIterator接口(列表迭代器)

应用场景:顾名思义,只能用于List的迭代器。

在使用迭代器迭代的过程中需要使用集合中的方法操作元素,出现ConcurrentModificationException异常时。

7、总结

arraylist和linkedlist联系与区别

1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。

3.对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。 这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList(末尾添加删除)。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。

 

HashMap与TreeMap联系与区别

1、 HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。

2、在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。

 

两个map中的元素一样,但顺序不一样,导致hashCode()不一样。

同样做测试:

在HashMap中,同样的值的map,顺序不同,equals时,false;

而在treeMap中,同样的值的map,顺序不同,equals时,true,说明,treeMap在equals()时是整理了顺序了的。

 

猜你喜欢

转载自blog.csdn.net/qq_42022528/article/details/82838850