Java基础总结—容器篇
1|0Java集合【重点】
集合存储的是对象的引用、内存 、集合体系结构图
1|11、Iterable接口:
-
Iterator方法 : 调用iterator方法,返回一个Iterator类型的迭代器
1|22、Collection 接口:
常用方法:
-
add(Object obj) :向集合中添加元素
-
size() : 返回集合中元素的个数,而非集合的容量
-
contains:底层调用的是euqals方法,本质是集合中的元素与其进行比较,如果重写equals,则比较内容,否则通过内存地址判断是否相等
-
remove:底层调用equals方法,本质与contains方法同理!
总结:equals方法是需要我们重写的!
其余接口中方法参考文档API
1|33、List 接口
3.1、ArrayList分析 *
ArrayList
是List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,线程不安全
ArrayList 又称动态数组,底层是基于数组实现的List,与数组的区别在于,其具备动态扩展能力。从继承体系图中可看出ArrayList:
- 实现了List, RandomAccess, Cloneable, java.io.Serializable等接口
- 实现了List,具备基础的添加、删除、遍历等操作
- 实现了RandomAccess,具备随机访问的能力
- 实现了Cloneable,可以被克隆(浅拷贝) list.clone()
- 实现了Serializable,可以被序列化
ArrayList初始化
JDK8以后 执行无参构造,底层先创建一个初始化容量为0的数组,当添加第一个元素的时候,初始化容量为10 !
补充:JDK6 new 无参构造的
ArrayList
对象时,直接创建了长度是 10 的Object[]
数组 elementData 。
细心的同学一定会发现 :以无参数构造方法创建 ArrayList
时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10
ArrayList扩容机制
扩容总结:当我们ArrayList的容量不够时,按照规则扩容至原来的1.5倍,如果扩容后仍然不满足需求的最小容量,则容量更新为要求的容量,此时检查当前容量是否超出ArrayList所定义的最大容量,若超出则更新为ArrayList所定义的最大容量MAX_ARRAY_SIZE
3.2、LinkedList
LinkedList是一个以双向链表实现的List,它除了作为List使用,还可以作为队列或者栈来使用
双向链表: 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。
源码分析:
可以看出LinkedList实现了Cloneable和Serializable接口,说明其可以被克隆,也可以被序列化!同样的,LinkedList被克隆的时候,和ArrayList一样二者均是浅拷贝。
1、LinkedList的基本属性
三个基本属性通过关键字transient修饰,使其不被序列化。
2、Node类(节点)
从代码中就可以看出,这是双向链表结构。
3、构造方法
双向链表和双向循环链表的区别
JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别
添加元素
LinkedList在中间添加元素的方法实现原理就是,典型的双链表在中间添加元素的流程!
- 在队列首尾添加元素很高效,时间复杂度为O(1)
- 在中间添加元素比较低效,首先要先找到插入位置的节点,再修改前后节点的指针,时间复杂度为O(n)
删除元素
- 在队列首尾删除元素很高效,时间复杂度为O(1)
- 在中间通过指定下标删除元素比较低效,首先要先找到要删除节点的位置,再进行删除,时间复杂度为O(n)
总结:
- LinkedList是一个以双链表实现的List;
- LinkedList还是一个双端队列,具有队列、双端队列、栈的特性;
- LinkedList在队列首尾添加、删除元素非常高效,时间复杂度为O(1);
- LinkedList在中间添加、删除元素比较低效,时间复杂度为O(n);
- LinkedList不支持随机访问,所以访问非队列首尾的元素比较低效;
3.3、Vector
ArrayList
是List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,线程不安全 ;Vector
是List
的古老实现类,底层使用Object[ ]
存储,线程安全的。 几乎等于ArrayList
从图中我们可以看出:Vector继承了AbstractList,实现了List,RandomAccess,Cloneable,Serializable接口,因此Vector支持快速随机访问,可以被克隆,支持序列化。
Vector扩容机制
1|44、Set 接口
特点:无序不可重复
原因:主要是由于底层的HashMap,采用的是Hash表(数组+链表+红黑树)的数据结构,我们的元素不一定放在哪里,所以说是无序的
4.1、HashSet
底层是一个HashMap,也是一个Hash表的数据结构
同样HashSet是可克隆,支持序列化操作
HashSet源码分析
构造方法
new HashSet的时候,底层new了一个HashMap
插入元素
向HashSet中添加元素,本质是向底层中的HashMap的key位置添加元素
删除元素
HashSet总结:
- HashSet内部使用HashMap的key存储元素,以此来保证元素不重复;
- HashSet是无序的,因为HashMap的key是无序的;
- HashSet中允许有一个null元素,因为HashMap允许key为null;
- HashSet是非线程安全的;
- HashSet是没有get()方法的;
4.2、TreeSet
底层是一个TreeMap,本质也是红黑树的数据结构
1|55、Map 接口:
clear()
: 清空Map集合size()
:统计Map集合当中的键值对的个数put(Object key ,Object value)
:添加一个键值对<key,value>get(Object key)
:通过一个key获取对应的valueremove(Object key)
:通过key删除一对键值对values()
:返回Map集合中的所有value,返回值类型为Collection类型keyset( )
:返回Map集合当中所有key,返回值类型为Set类型,因为key本身就是一个Set集合!entrySet<Map.Entry<key,value>>
:将Map集合中的每对键值对整合为整体,放入一个Set集合中,泛型类型为Map.Entry,本质是Map集合中的一个静态内部类
Map遍历
5.1、HashMap *
HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列(哈希表)。
- HashMap 实现了Cloneable接口,可以被克隆。
- HashMap 实现了Serializable接口,属于标记性接口,HashMap 对象可以被序列化和反序列化。
- HashMap 继承了AbstractMap,父类提供了 Map 实现接口,具有Map的所有功能,以最大限度地减少实现此接口所需的工作。
一、HashMap的底层实现
JDK1.8 之前
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过路由寻址法: (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
JDK 1.8 之后
jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储,同样的是如果红黑树上的节点数小于6的化会再自动转化为单链表。
扩展:所谓扰动函数(哈希算法)指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
二、HashMap的扩容机制
HashMap == 数组+散链表+红黑树
- HashMap 默认初始桶位数16(数组位),如果某个桶中的链表长度大于8,则先进行判断:
- 如果桶位数小于64,则先进行扩容(2倍),扩容之后重新计算哈希值,这样桶中的链表长度就变短了(之所以链表长度变短与桶的定位方式有关,请接着往下看)。
- 如果桶位数大于64,且某个桶中的链表长度大于8,则对链表进行树化(红黑树,即自平衡的二叉树)
- 如果红黑树的节点数小于6,树也会重新变会链表。
结论:所以得出树化条件:链表阈值大于8,且桶位数大于64(数组长度),才进行树化。
元素放入桶(数组)中,定位桶的方式(数组定位方式):通过数组下标 i 定位,添加元素时,目标桶位置 i 的计算公式,i = hash & (cap - 1)
,cap为容量
为什么优先扩容桶位数(数组长度),而不是直接树化?
- 这样做的目的是因为,当桶位数(数组长度)比较小时,应尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率。因为红黑树需要逬行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对要快些。所以结上所述为了提高性能和减少搜索时间,底层阈值大于8并且数组长度大于64时,链表才转换为红黑树
- 而当阈值大于 8 并且数组长度大于 64 时,虽然增了红黑树作为底层数据结构,结构变得复杂了,但是,长度较长的链表转换为红黑树时,效率也变高了。
三、HashMap特点:
- 存储无序
- 键和值位置都可以是 null,但是键位置只能存在一个 null;
- 容量:容量为数组的长度,亦即桶的个数,默认为16 ,最大为2的30次方,当容量达到64时才可以树化。
- 装载因子:装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75。
- 树化:树化,当容量达到64且链表的长度达到8时进行树化,当链表的长度小于6时反树化。
- 键位置是唯一的,是由底层的数据结构控制的;
- jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树;
- 阈值(边界值)> 8 并且桶位数(数组长度)大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询;
四、HashMap存储数据的过程
即向hash表中添加元素的执行过程
执行流程分析:
- 首先,HashMap<String, Integer> hashMap = new HashMap();当创建 HashMap 集合对象的时候,HashMap 的构造方法并没有创建数组,而是在第一次调用 put 方法时创建一个长度是16 的数组(即,16个桶) ,Node[] table (jdk1.8 之前是 Entry[] table)用来存储键值对数据 ;
- 将<K , V>封装成为一个Node(节点)对象,底层会调用key的
hashCode
方法得出hash值,然后通过哈希算法(哈希函数),将hash值转化为数组的下标,如果下标位置(桶位置)如果没有任何元素,就把Node节点添加到这个位置上,如果桶位置上有链表,此时,会拿着key和链表中每个节点中的key进行equals比较,如果返回false,那么这个节点添加到链表末尾,如果其中一个返回true,那么这个节点的value就会被当前Node覆盖 ;
注意:put的执行流程中必须重写实例的HashCode方法和equals方法 ;
五、HashMap相关面试题
具体原理我们下文会具体分析,这里先大概了解下面试的时候会问什么,带着问题去读源码,便于理解
1、HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?
答:对 key 的 hashCode 做 hash 操作,如果key为null则直接赋哈希值为0,否则,无符号右移 16 位然后做异或位运算,如,代码所示(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
除上面的方法外,还有平方取中法,伪随机数法 和 取余数法。这三种效率都比较低,而无符号右移 16 位异或运算效率是最高的。
2、当两个对象的 hashCode 相等时会怎么样?
答:会产生哈希碰撞(hash冲突)。若 key 值内容相同则替换旧的 value,不然连接到链表后面,链表长度超过阈值 8 就转换为红黑树存储
3、什么是哈希碰撞,如何解决哈希碰撞?
答:只要两个元素的 key 计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。
4、如果两个键的 hashCode 相同,如何存储键值对?
答:通过 equals 比较内容是否相同。
- 相同:则新的 value 覆盖之前的 value。
- 不相同:遍历该桶位的链表(或者树):如果找不到,则将新的键值对添加到链表(或者树)中
5、容量为什么必须是 2 的 n 次幂?如果输入值不是 2 的幂比如 10 会怎么样?
答:为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
6、如果创建HashMap对象时,输入的数组长度length是10,而不是2的n次幂会怎么样呢?
HashMap双参构造函数会通过tableSizeFor(initialCapacity)方法,得到一个最接近length且大于length的2的n次幂数(比如最接近10且大于10的2的n次幂数是16)
六、总结:
(1)HashMap是一种散列表,采用(数组 + 链表 + 红黑树)的存储结构;
(2)HashMap的默认初始容量为16(1<<4),默认装载因子为0.75f,容量总是2的n次方;
(3)HashMap扩容时每次容量变为原来的两倍;
(4)当桶的数量小于64时不会进行树化,只会扩容;
(5)当桶的数量大于64且单个桶中元素的数量大于8时,进行树化;
(6)当单个桶中元素数量小于6时,进行反树化;
(7)HashMap是非线程安全的容器;
(8)HashMap查找添加元素的时间复杂度都为O(1);
5.2、HashTable
因为线程安全的问题,
HashMap
(非线程安全) 要比Hashtable
(线程安全的) 效率高一点。另外,Hashtable
基本被淘汰,不要在代码中使用它。
Dictionary 类是一个已经被废弃的类(见其源码中的注释)。父类被废弃,自然其子类Hashtable也用的比较少了。
HashTable和HashMap的区别:
- 线程是否安全:
HashMap
是非线程安全的,Hashtable
是线程安全的,因为Hashtable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!); - 效率: 因为线程安全的问题,
HashMap
要比Hashtable
效率高一点。另外,Hashtable
基本被淘汰,不要在代码中使用它; - 对 Null key 和 Null value 的支持:
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException
。 - 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 - 底层数据结构: JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
Properties
HashTable的子类,key和value都是只支持字符串!<String , String >
setProperty :添加元素
getProperty :通过key删除
5.3、TreeMap
TreeMap使用红黑树存储元素,可以保证元素按key值的大小进行遍历。
TreeMap实现了Map、SortedMap、NavigableMap、Cloneable、Serializable等接口。
存储结构
TreeMap只使用到了红黑树(特殊的AVL(自平衡)二叉树),所以它的时间复杂度为O(log n),我们再来回顾一下红黑树的特性
源码分析
TreeMap的基本属性:
TreeMap的构造方法
TreeMap比较方法:
由于TreeMap是会给其中的元素进行排序的,然而排序方式需要我们自己定义!
-
方式一:让类实现compareable接口重写compareTo方法,让类本身可比较!
-
方式二:创建TreeMap的时候传入一个比较器Comaparator(自定义一个类,重写compare方法),按照比较器的规则比较!
TreeMap的遍历
当我们呢打印输出TreeMap集合的时候,是将自平衡二叉树进行了中序遍历
Treemap插入元素
即向二叉树中插入元素
1|66、红黑树
树 -> 二叉树 -> 二叉搜索树 -> AVL树 - > 红黑树
详情见有道云笔记 ;
左旋
右旋
__EOF__

本文链接:https://www.cnblogs.com/qxsong/p/15837295.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具