1.为什么要使用集合
1.在开发中我们经常要集中保存多个数据,可以使用数组,但是使用数组的前提是我们必须知道我们要保存的数据的数量,数组 的长度一旦被定义就不可以再做改变
2.java集合相当于是一个长度可以变化的数组,是用于保存动态增长的数据的一个很好的选择
3.java集合因为是用来保存数据的,所以我们也称集合类为容器类,所有的集合类都位于java.util包下
4.后来为了处理多线程问题,JDK1.5中还在java.util.concurrent包下提供了支持多线程的java集合类
2.Collection和Map
- Collection ---一组对立的元素,这些元素都必须符合一些规则---
- List 必须保持元素特定的顺序
- Set 不能有重复的元素
- Queue 保持一个队列的顺序(即先进先出)
- Map ---一组成对的键值对对象---
- Collection和Map的区别在于容器中每个位置保存的元素个数
- Collection每个位置只能保存一个元素(对象)
- Map的每个位置保存的是一个键值对对象,每个键值对对象由一个key和一个value组成,我们可以通过key来寻找value
3.集合类的框架层次关系
Collection
Collection是最基本的集合接口,一个collection代表一组object对象,这些object是collection中的元素(这个接口不能实例化对象,只能用来定义规范)
-
Set
- Set继承自Collection接口,Set就像是一个罐子,其中的元素没有顺序,但是元素不可以重复(整个Set类层次的共有属性)
- Set在判断两个元素是否相同时,使用的是equals()方法而不是==,说明Set判断的是两个元素的值是否相同
- 向Set中添加新元素时,会先将新元素跟其他所有元素进行equals()比较,返回值都为false时,Set才会接受这个新元素
HashSet
HashSet实现了Set接口,使用HashSet时,第一件事就是
- 重写hashCode()和equals()方法,这样才能确保不会出现重复的元素(要保证这两个方法兼容)
重温一下hash码(散列码)的知识:
- hashCode是由对象导出的一个整数值,每一个对象都有一个默认的散列码,值就是对象的内存地址(将内存地址换算成一个整数)。(也有一些类的对象的散列码是另外经过计算的,例如String)。
- HashSet的add机制 假如我们有一个散列码是76268,而此时的HashSet有128个散列单元,那么这个数据很有可能会插入数组的108个散列链表中(76268%128=108)(就是元素的key值得哈希值对数组的长度取模来确定保存位置)。但这只是可能,如果第108个散列链表中经过equals比较为true的话,这个新元素将会被丢弃,不再加入散列链表中
HashSet的底层是由HashMap实现的,元素都存储在HashMap键值对的KEY上,而value值是一个统一的值private static final Object PRESENT = new Object();
HashSet的操作实际上都是使用HashMap的底层方法来完成的 ,所以.......请好好理解HashMap的底层实现原理
HashSet是无序的,它也没有提供get()方法,所以只能通过迭代来得到想要的元素,但是因为能够通过散列码%散列单元数,就能够快速找到存储这个元素的散列链表,所以HashSet的查找效率得到提升
加入新元素时,会先比较hash码值,再进行equals比较(两个不同对象的hash码值有可能会相同,因为hashCode()方法可能会被重写,不一定都是使用Object的hashCode()方法)
- hash码值不相同,说明是一个新元素,存入
- hash码值相同,再进行equals比较,如果返回true,说明已经存在,不存
- hash码值相同,再进行equals比较,如果返回false,说明不存在,存入
LinkedHashSet
LinkedHashSet也是根据元素的hashCode来决定存储元素的位置,但是与HashSet不同的是,linkedHashSet同时会使用链表来维护元素的次序,这样使得元素看起来是以插入顺序来存储的,因为要维护插入顺序,所以性能要低于HashSet
-
List
- List代表一个元素有序,且可以重复的集合,集合中每个元素都有其对应的元素索引
- 可以通过索引来访问特定位置的元素
- 默认按照元素的添加顺序来设定索引
ArrayList
继承自AbstractList类,实现了List、RondomAccess(快速访问)、Cloneable(复制)、Serializable(序列化)接口
底层基于实现了大小能动态变化,能重新分配的Object数组,允许null存在,默认长度10
数组的存储地址是连续的
底层实现的源码https://blog.csdn.net/zxt0601/article/details/77281231
Vector
- (与ArrayList的相同点)和ArrayList继承了相同的类,实现了相同的接口,底层实现也是动态数组,默认长度也是10
- 不同点
- Vector的方法都是同步的(Synynchronized),线程安全(thread-safe)的,ArrayList不是,但是线程同步就必定会影响性能,所以ArrayList的性能要优于Vector
- 当Vector和ArrayList的元素大小超过初始大小时,Vector会将容量翻倍,但是ArrayList值增加50%,因此ArrayList更节省空间大小
LinkedList
继承自AbstractSequentialList的双向链表,实现了List、Deque(队列)、Cloneable(复制)、Serializable(序列化)接口
链表的存储空间是不连续的
ArrayList和LinkedList的区别
- ArrayList是基于动态数组(顺序表)的数据结构,顺序表的存储地址是连续的,所以在查找的时候比较快,但是在插入和删除的时候,由于要把其他元素顺序向后或者向前移动,所以比较费时
- LinkedList是基于链表的数据结构,链表的存储地址是不连续的,每个存储地址通过指针指向,在查找的时候需要通过指针进行元素遍历,所以在查找的时候比较费时。由于链表插入时不需要移动其他元素,所以适合插入和删除
Map
Map是由一系列键值对组成的集合,KEY-VALUE,Map中的key不能相同,value值可以相同
从代码复用的角度来看,Java先是实现了Map,然后通过包装了一个所有value都是null的Map,就形成了Set
HashMap和Hashtable的区别
HashMap和Hashtable的关系类似于ArrayList和Vector
区别:
- Hashtable是线程安全的,HashMap是线程不安全的,所以性能上HashMap要优于Hashtable
- Hashtable不允许使用nul作为key和value的值,但是HashMap可以
HashMap的实现(https://www.cnblogs.com/chengxiao/p/6059914.html#t1)
--------------------------------------------------------------------更新---------------------------------------------------------------------------------------
HashMap的数据结构是哈希表,哈希表是由链表加数组组成的,数据结构示意图如下
数组的每个位置上保存的元素其实是一个链表的表头,链表的每个节点保存的是一个键值对,简称Entry(桶)
HashMap的默认初始化大小时16,负载因子是0.75,每当HashMap中保存的的元素数量超过16*0.75时,就会自动执行rehash方法,让数组的长度翻倍,数组的长度翻倍后,将会把原数组中的元素重新计算下标并且添加到新的数组中。这个扩容的过程是一个非常“消耗”的过程,所以如果我们在定义HashMap的时候就能清楚的知道我们有多少个元素要保存进去的话,将会极大程度的提高存储效率。
HashMap是如何确定添加进去的元素的保存位置的呢?每当添加一个新的Entry到HashMap的时候,就会计算KEY的hash值(这里要确保重写equals()和hashcode()),然后用hash值对数组的长度取模来得到保存位置的下标,如下图所示
12%16=12,28%16=12,108%16=12,140%16=12,所以他们都保存在数组中下标为12的位置,并且新添加的元素会作为链表的表头,最先添加的元素会放在链表的表尾。
使用put()方法时,先判断要存储的元素的key是否为null,如果为null的话,就会添加到数组的第一个位置(下标为0),如果不为null的话,就会按照上诉的方法来寻找存储位置,如果该链表中有hash值相同的元素的话,就会覆盖原来旧的value,如果没有相同hash值的话,就会添加到表头