1.Java集合框架
1.1集合概述
1.1.1 Java集合概览
Java集合,也叫容器,主要由两大接口派生而来:一个是Collection
接口,主要用于存放单一元素;另一个是Map
接口,主要用于存放键值对,对于Collection
接口,下面有三个主要的子接口:List
、Set
和Queen
Java 集合框架如下图所示:
注:图中只列举了主要的继承派生关系,并没有列举所有关系。比方省略了AbstractList
, NavigableSet
1.1.2 说说List,Set,Queue,Map四者的区别?
List
:存储的元素有序,可重复Set
:存储的元素无序,不可重复Queue
(队列):按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的Map
:使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value**,key 是无序的 、不可重复**的,value是无序的、可重复的,每个键最多映射到一个值
1.1.3 集合框架底层数据结构总结
先来看一下 Collection
接口下面的集合。
1.1.3.1. List
- Arraylist:objext[]数组
- Vector:object[]数组
- LinkedList:双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
1.1.3.2 Set
HashSet
(无序,唯一):基于HashMap实现的,底层采用HashMap来保存元素LinkedhashSet:LinkedHashSet
是HashSet
的子类,并且内部是通过LinkedHashMap
来实现的,有点类似LinkedHashMap
其内部是基于HashMap
实现一样,不过还是有一点点区别的- TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)
1.1.3.3 Queue
PriorityQueue
:Object[]
数组来实现二叉堆ArrayQueue
:Object[]
数组 + 双指针
再来看看 Map
接口下面的集合。
1.1.3.4 Map
-
HashMap: JDK1.8之前 HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突存在的(**“拉链法”**解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表装换位红黑树如果当数组的长度小于64,那么会选择先进行扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
-
LinkedHashMap:LinkedHashMap
继承自HashMap ,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 -
Hashtable:数组+链表组成的,数组是Hashtable的主体,链表是为了解决哈希冲突而存在的
-
TreeMap:红黑树(自平衡的排序二叉树)
1.1.4 如何选用集合
主要根据集合的特点来选用,比如我们需要根据键值对获取到元素值时就选用Map
接口下的集合,需要排序时选择TreeMap
,不需要排序时选择HashMap
,需要保证线程安全就选用ConcurrentHashMap
。
当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set
接口下的集合比如TreeSet和HashSet
,不需要就选择实现List
接口的比如ArrayList
或LinkedList
,然后再根据实现这些接口的集合的特点来选用
1.1.5 为什么要使用集合?
当我们需要保存一组相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是数组有一定的弊端,实际开发中,存储的数据类型是多种多样的,于是,就出现了集合,集合同样也是用于存储多个数据的。
数组的缺点:
1.一旦声明后,长度就不可变了
2.声明了数组是的数据类型也决定了该数组存储的数据类型
3.数组存储是有序的、可重复的、特点单一。
集合提高了数据存储的灵活性,Java集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据
1.2Collection子接口之List
1.2.1 Arraylist和vector的区别?
ArrayList
是List
的主要实现类,底层使用Object[]存储,适用于频繁的查找工作,线程不安全;
Vector
是List
的古老实现类,底层使用Object[]存储,线程安全的。
1.2.2 ArrayLsit于LinkedList区别?(重点)
**1.是否保持线程安全:**都不是同步的,也就是都不保证线程安全
2.底层数据结构:ArrayList底层是Object数组,LinkedList底层使用的是双向链表数据结构
3.插入和删除是否受元素位置的影响:
ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。在第i个元素后面插入和删除元素的话,再其后面的元素都需要向后/前移一位的操作
LinkedList采用链表存储,所以,如果在头尾插入或删除元素不受元素位置的影响,近似O(1),如果在指定位置i插入和删除元素的话,时间复杂度为O(n),因为需要先移动到指定位置在插入。
4.是否支持快速随机访问:LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)
**5.内存空间占用:**ArrayList的空间浪费在list列表的结尾会预留一定的空间容量,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据 )
1.2.2.1. 补充内容:双向链表和双向循环链表
双向链表: 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。
双向循环链表: 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。
1.2.3 ArrayList的扩容机制
1.3Collection子接口之Set
1.3.2 无序性和不可重复性的含义是什么?
1.什么是无序性?
无序性不等于随机性,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
2.什么是不可重复性?
不可重复性是指添加的元素按照**equals()判断时,返回false,需要同时重写equals()方法和HashCode()**方法。
1.3.3 比较HashSet、LinkedHashSet和TreeSet三者的异同
HashSet
是Set接口的主要实现类,HashSet的底层是HashMap,线程不安全的,可以存储null值;
LinkedHashSet
是HashSet的子类、能够按照添加的顺序遍历;
TreeSet
底层使用红黑树,元素是有序的,排序的方式有自然排序和定制排序
1.5 Map接口
1.5.1 HashMap和Hashtable的区别
1.线程是否安全:HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable内部的方法基本都经过Synchronized修饰。(如果要保证线程安全就使用ConcurrentHashMap)
**2.效率:**因为线程安全的问题,HashMap
要比Hashtable
效率搞高一点。另外Hashtable基本上被淘汰了。
3.对Null key和Null value的支持:HashMap
可以存储null的key和value,但null作为键只能有一个,null作为值可以有多个;Hashtable不允许有null键和null值,否则会抛出NullpointerException
**4.初始容量大小和每次扩容容量大小的不同:**①创建时如果不指定容量初始值,Hashtable
默认的初始值大小是11,之后每次扩容,容量变为原来的2n+1。HashMap
默认的初始化大小为16.之后每次扩容,容量变为原来的2倍。②创建时如果给定容量初始值,那么Hashtable
会直接使用给定的大小。而HashMap
会将其扩容为2的幂次方大小(HashMap
中的tableSizeFor
()方法保证,下面给出了源代码)。也就是说HashMap
总是使用2的幂次方作为哈希表的大小。
5.底层数据结构:JDK1.8以后HashMap
在解决哈希冲突的时候有了较大的变化,当链表长度大于阀值(默认为8)(将链表转换为红黑树前会判断,如果当数组长度小于64,那么会先进行数组扩容,而不是转换为红黑树)时,将链表转换为红黑树,以减少搜索时间。Hashtable没有这样的机制
1.5.2 HashMap和HashSet区别
HashSet
底层就是基于HashMap
实现的。
HashMap | HashSet |
---|---|
实现了 Map 接口 | 实现 Set 接口 |
存储键值对 | 仅存储对象 |
调用 put() 向 map 中添加元素 | 调用 add() 方法向 Set 中添加元素 |
HashMap 使用键(Key)计算 hashcode | HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals() 方法用来判断对象的相等性 |
1.5.3 HashMap和TreeSet区别
TreeMap
和HashMap
都继承自AbstractMap
,但是需要注意的是TreeMap
它还实现了NavigableMap
接口和SortedMap
接口
1.5.4 HashSet如何检查重复
当你把对象加入HashSet
时,HashSet
会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的hashcode
值作比较,如果没有相符的hashcode
, HashSet
会假设对象没有重复出现。但是如果发现有相同 hashcode
值的对象,这时会调用equals()
方法来检查 hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让加入操作成功。
hashcode()
与equals()
的相关规定:
1.如果两个对象相等,则hashcode
一定是相同的
2.两个对象相等,对两个equals()
方法返回true
3.两个对象有相同的hashcode值,他们也不一定是相等的
4.综上,equals()
方法被覆盖过在,则hashcode()
方法也必须被覆盖
5.hashcode()
的默认行为是对堆上的对象产生独特值。如果没有重写hashcode()
,则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
==与equals的区别
对于基本类型数据来说,==比较的是值是否相等;
对于引用类型来说,==比较的是两个引用是否指向同一个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同一个地方);
对于引用类型(包括包装类型)来说,equals如果没有被重写,对比它们的地址是否相等;如果equals()方法被重写(例如String),则比较的是地址的内容 。
1.5.5 HashMap的底层原理
1.5.5.1 JDK1.8之前
JDK1.8之前HashMap底层是数组和链表结合在一起使用也就是链表散列。HashMap通过key的hashcode经过扰动函数处理后得到hash值,然后通过(n-1)&hash判断当前位置存在元素的位置(这里的n指的是数组的长度),如果当前位置存在元素的话,就判断该元素要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不同的话就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
所谓拉链法就是:将数组和链表结合,也就是说创建一个链表数组,数组中每一格就是一个链表,若遇到哈希冲突,则将冲突的值加到链表中即可。
1.5.5.2 JDK1.8之后
解决哈希冲突时有了较大的变化,当链表长度大于阀值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择扩容数组,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
1.5.6HashMap的长度为什么是2的幂次方
HashMap得=的数组下标的计算方法时 (n-1)& hash
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
1.5.7 HashMap多线程操作导致死循环
主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
详情请查看:https://coolshell.cn/articles/9606.html