JavaSE
9. 集合
9.0 base
一、集合的框架
1. 集合、数组都是对多个数据进行存储操作的结构,简称Java 容器。
说明;此时的存储,主要是指能存层面的存储,不涉及到持久化的存储(. txt, . jpg, . avi, 数据库中)
2.1 数组在存储多个数据封面的特点:
> 一旦初始化以后,它的长度就确定了。
> 数组一旦定义好,它的数据类型也就确定了。我们就只能操作指定类型的数据了。
比如:String [ ] arr; int [ ] str;
2.2 数组在存储多个数据方面的特点:
> 一旦初始化以后,其长度就不可修改。
> 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
> 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
> 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
二、集合框架
| -- - Collection 接口:单列集合,用来存储一个一个的对象
| -- - List 接口:存储有序的、可重复的数据。 -- > “动态”数组
| -- - ArrayList 、LinkedList 、Vector
| -- - Set 接口:存储无序的、不可重复的数据 -- > 高中讲的“集合”
| -- - HashSet 、LinkedHashSet 、TreeSet
| -- - Map 接口:双列集合,用来存储一对( key - value) 一对的数据 -- > 高中函数:y = f ( x)
| -- - HashMap 、LinkedHashMap 、TreeMap 、Hashtable 、Properties
9.1 Collection
9.1.1 Collection 中定义的方法
添加
add ( Object obj)
addAll ( Collection coll)
获取有效元素的个数
int size ( )
清空集合 【PS】不等于 coll = null
void clear ( )
是否是空集合
boolean isEmpty ( )
是否包含某个元素
boolean contains ( Object obj) :是通过元素的equals方法来判断是否是同一个对象
boolean containsAll ( Collection c) :也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
删除
boolean remove ( Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
boolean removeAll ( Collection coll) :取当前集合的差集
取两个集合的交集
boolean retainAll ( Collection c) :把交集的结果存在当前集合中,不影响c
集合是否相等
boolean equals ( Object obj)
转成对象数组
Object [ ] toArray ( )
获取集合对象的哈希值
hashCode ( )
遍历
iterator ( ) :返回迭代器对象,用于集合遍历
9.1.2 集合遍历 => Iterator迭代器接口
Iterator 对象称为迭代器( 设计模式的一种) ,主要用于遍历Collection 集合中的元素。
GOF 给迭代器模式的定义为:提供一种方法访问一个容器( container) 对象中各个元素,而又不需暴露该对象的内部细节。
迭代器模式,就是为容器而生。类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”。
Collection 接口继承了java. lang. Iterable 接口,该接口有一 iterator ( ) 方法,那么所有实现了 Collection 接口的集合类都有一个 iterator ( ) 方法,用以返回一个实现了Iterator 接口的对象。
Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
集合对象每次调用iterator ( ) 方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。 = > index = - 1 ;
【PS: 】可类别 JDBC 中的结果集 ( ResultSet ) = > rs. next ( )
Iterator iterator = coll. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
System . out. println ( iterator. next ( ) ) ;
}
iterator = coll. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
Object obj = iterator. next ( ) ;
if ( "Tom" . equals ( obj) ) {
iterator. remove ( ) ;
}
}
9.1.3 增强 for 循环(JDK5.0)
jdk 5.0 新增了for - each循环, 用于遍历集合、数组, 底层仍是迭代器
coll. forEach ( System . out :: println ) ;
System . out. println ( "***********************" ) ;
for ( Object o : coll) {
System . out. println ( o) ;
}
9.2 List
9.2.0 base
List 接口框架
| -- -- Collection 接口:单列集合,用来存储一个一个的对象
| -- -- List 接口:存储有序的、可重复的数据。 -- > “动态”数组, 替换原有的数组
| -- -- ArrayList :作为List 接口的主要实现类;线程不安全的,效率高;底层使用Object [ ] elementData存储
| -- -- LinkedList :对于频繁的插入、删除操作,使用此类效率比ArrayList 高;底层使用双向链表存储
| -- -- Vector :作为List 接口的古老实现类;线程安全的,效率低;底层使用Object [ ] elementData存储
9.2.1 ArrayList
1. jdk 7 情况下:
ArrayList list = new ArrayList ( ) ;
list. add ( 123 ) ;
. . .
list. add ( 11 ) ;
默认情况下,扩容为原来的容量的 1.5 倍, 如果数组容量仍不够,则直接扩容为当前大小 (开始摆烂了hhhhh),同时需要将原有数组中的数据复制到新的数组中。
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList ( int capacity)
2. jdk 8 中ArrayList 的变化:
ArrayList list = new ArrayList ( ) ;
list. add ( 123 ) ;
. . .
后续的添加和扩容操作与jdk 7 无异。
小结:jdk7中的ArrayList 的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
扩容 :
private void grow ( int var1) {
int var2 = this . elementData. length;
int var3 = var2 + ( var2 >> 1 ) ;
if ( var3 - var1 < 0 ) {
var3 = var1;
}
if ( var3 - 2147483639 > 0 ) {
var3 = hugeCapacity ( var1) ;
}
this . elementData = Arrays . copyOf ( this . elementData, var3) ;
}
9.2.2 LinkedList
LinkedList 的源码分析:
LinkedList list = new LinkedList ( ) ; 内部声明了Node 类型的first 和 last 属性,默认值为null
list. add ( 123 ) ;
其中,Node 定义为:体现了LinkedList 的双向链表的说法
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;
}
}
9.2.3 Vector
Vector 的源码分析:
jdk7 和 jdk8 中通过 Vector ( ) 构造器创建对象时,底层都创建了长度为10 的数组。(饿汉式初始化,没人管滴娃真惨咳咳)
public Vector ( ) {
this ( 10 ) ;
}
在扩容方面,默认扩容为原来的数组长度的2 倍。
扩容 :
private void grow ( int var1) {
int var2 = this . elementData. length;
int var3 = var2 + ( this . capacityIncrement > 0 ? this . capacityIncrement : var2) ;
if ( var3 - var1 < 0 ) {
var3 = var1;
}
if ( var3 - 2147483639 > 0 ) {
var3 = hugeCapacity ( var1) ;
}
this . elementData = Arrays . copyOf ( this . elementData, var3) ;
}
9.2.4 ArrayList、LinkedList、Vector 的区别
ArrayList 和 LinkedList 的异同
1. 二者都线程不安全,相对线程安全的 Vector ,ArrayList 执行效率高。
2. ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。
3. 对于随机访问 get 和 set ,ArrayList 绝对优于 LinkedList ,因为LinkedList 要移动指针。
4. 对于新增和删除操作 add ( 特指插入) 和 remove,LinkedList 比较占优势,因为ArrayList 要移动数据。
ArrayList 和 Vector 的区别
Vector 和 ArrayList 几乎是完全相同的, 唯一的区别在于 Vector 是同步类 synchronized ,属于强同步类。因此开销就比 ArrayList 要大,访问要慢。正常情况下, 大多数的Java 程序员使用 ArrayList 而不是 Vector , 因为同步完全可以由程序员自己来控制。Vector 每次扩容请求其大小的2 倍空间,而 ArrayList 是 1.5 倍。Vector 还有一个子类 Stack 。
9.2.5 List 接口的常用方法
List 除了从Collection 集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法 :
void add ( int index, Object ele) : 在index位置插入ele元素
boolean addAll ( int index, Collection eles) : 从index位置开始将eles中的所有元素添加进来
Object get ( int index) : 获取指定index位置的元素
int indexOf ( Object obj) : 返回obj在集合中首次出现的位置
int lastIndexOf ( Object obj) : 返回obj在当前集合中末次出现的位置
Object remove ( int index) : 移除指定index位置的元素,并返回此元素
Object set ( int index, Object ele) : 设置指定index位置的元素为ele
List subList ( int fromIndex, int toIndex) : 返回从fromIndex到toIndex位置的子集合
总结:常用方法
增:add ( Object obj)
删:remove ( int index) / remove ( Object obj)
改:set ( int index, Object ele)
查:get ( int index)
插:add ( int index, Object ele)
长度:size ( )
遍历:① Iterator 迭代器方式
② 增强for 循环
③ 普通的循环
【PS】:区分 List 中 remove ( int index) 和 remove ( Object obj)
此时 int 和 Integer 不存在自动装箱和自动拆箱
9.3 Set
9.3.0 base
Set 接口的框架 :
| -- -- Collection 接口:单列集合,用来存储一个一个的对象
| -- -- Set 接口:存储无序的、不可重复的数据 -- > 高中讲的“集合”
| -- -- HashSet :作为Set 接口的主要实现类;线程不安全的;可以存储null 值
| -- -- LinkedHashSet :作为HashSet 的子类;遍历其内部数据时,可以按照添加的顺序遍历, 对于频繁的遍历操作,LinkedHashSet 效率高于HashSet .
| -- -- TreeSet :可以按照添加对象的指定属性,进行排序。
Set : 存储无序的、不可重复的数据
1. 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
2. 不可重复性:保证添加的元素按照equals ( ) 判断时,不能返回true . 即:相同的元素只能添加一个。
9.3.1 HashSet
HashSet 底层:数组+ 链表的结构。
底层有一个数组结构,通过对存入数据的hash值经过某种算法('&' )得到数组中存储的索引位置,在同一索引位置的几个数据以链表的形式存储
添加元素的过程:以 HashSet 为例:
我们 向HashSet 中添加元素 a , 首先调用元素 a 所在类的 hashCode ( ) 方法,计算元素a的哈希值,
此哈希值接着通过某种算法计算出在 HashSet 底层数组中的存放位置(即为:索引位置),判断
数组此位置上是否已经有元素:
如果此位置上没有其他元素,则元素 a 添加成功。 -- -> 情况 1
如果此位置上有其他元素 b ( 或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
如果hash值不相同,则元素a添加成功。-- -> 情况 2
如果hash值相同,进而需要调用元素a所在类的 equals ( ) 方法:
equals ( ) 返回true , 元素a添加失败
equals ( ) 返回false , 则元素a添加成功。-- -> 情况 3
对于添加成功的情况2 和情况3 而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
jdk 7 : 元素a放到数组中,指向原来的元素。
jdk 8 : 原来的元素在数组中,指向元素a
总结:七上八下
HashSet 是Set 接口的典型实现,大多数时候使用Set 集合时都使用这个实现类。
HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
HashSet 具有以下特点:
不能保证元素的排列顺序
HashSet 不是线程安全的
集合元素可以是null
底层也是数组,初始容量为16 ,当如果使用率超过0.75 ,(16 * 0.75 = 12 )就会扩大容量为原来的2 倍。(16 扩容为32 ,依次为64 , 128 …等)
【详情参考 9.4 .1 HashMap 】
HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode ( ) 方法比较相等,并且两个对象的equals ( ) 方法返回值也相等。
对于存放在 Set 容器中的对象,对应的类一定要重写 equals ( ) 和 hashCode ( Object obj) 方法,以实现对象相等规则。即:“相等
的对象必须具有相等的散列码”。
9.3.2 LinkedHashSet
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
LinkedHashSet 插入性能略低于 HashSet ,但在迭代访问 Set 里的全部元素时有很好的性能。
LinkedHashSet 不允许集合元素重复。
9.3.3 TreeSet
1. 必须是相同类的对象
2. 必须可比较大小:自然排序 || 定制排序
【PS】:底层数据结构红黑树
9.3.4 关于 hashCode() 和 equals() 的重写
关于 hashCode ( ) 和 equals ( ) 的重写
1. 重写 hashCode ( ) 方法的基本原则
在程序运行时,同一个对象多次调用 hashCode ( ) 方法应该返回相同的值。
当两个对象的 equals ( ) 方法比较返回 true 时,这两个对象的 hashCode ( ) 方法的返回值也应相等。
对象中用作 equals ( ) 方法比较的 Field ,都应该用来计算hashCode值。
2. 重写equals ( ) 方法的基本原则
以自定义的 Customer 类为例,何时需要重写 equals ( ) ?
当一个类有自己特有的“逻辑相等”概念, 当改写 equals ( ) 的时候,总是要改写 hashCode ( ) ,根据一个类的 equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据 Object . hashCode ( ) 方法,它们仅仅是两个对象。因此,违反了“相等的对象必须具有相等的散列码”。
结论:复写 equals 方法的时候一般都需要同时复写 hashCode 方法。通常参与计算 hashCode 的对象的属性也应该参与到 equals ( ) 中进行计算。
Eclipse / IDEA工具里hashCode ( ) 的重写
以 Eclipse / IDEA 为例,在自定义类中可以调用工具自动重写 equals 和 hashCode。问题:为什么用 Eclipse / IDEA 复写 hashCode 方法,有 31 这个数字?
选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高 (减少冲突)
并且 31 只占用 5 bits, 相乘造成数据溢出的概率较小。
31 可以由 i* 31 == ( i<< 5 ) - 1 来表示, 现在很多虚拟机里面都有做相关优化。(提高算法效率)
31 是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1 来整除!( 减少冲突)
2. 要求:向Set ( 主要指:HashSet 、LinkedHashSet ) 中添加的数据,其所在的类一定要重写 hashCode ( ) 和 equals ( )
要求:重写的 hashCode ( ) 和 equals ( ) 尽可能保持一致性:相等的对象必须具有相等的散列码
重写两个方法的小技巧:对象中用作 equals ( ) 方法比较的 Field ,都应该用来计算 hashCode 值。
9.3.5 exer
public static void Set3_2 ( ) {
Set set = new HashSet ( ) ;
Person p1 = new Person ( 1001 , "AA" ) ;
Person p2 = new Person ( 1002 , "BB" ) ;
set. add ( p1) ;
set. add ( p2) ;
System . out. println ( set) ;
p1. name = "CC" ;
set. remove ( p1) ;
System . out. println ( set) ;
set. add ( new Person ( 1001 , "CC" ) ) ;
System . out. println ( set) ;
set. add ( new Person ( 1001 , "AA" ) ) ;
System . out. println ( set) ;
}
【PS】:HashSet 添加元素,依靠计算出的底层索引
9.4 Map
9.4.0 base
一、Map 的实现类的结构:
| -- -- Map : 双列数据,存储key- value对的数据 -- - 类似于高中的函数:y = f ( x)
| -- -- HashMap : 作为Map 的主要实现类;线程不安全的,效率高;存储null 的key和value
| -- -- LinkedHashMap : 保证在遍历map元素时,可以按照添加的顺序实现遍历。
原因:在原有的HashMap 底层结构基础上,添加了一对指针,指向前一个和后一个元素。
对于频繁的遍历操作,此类执行效率高于HashMap 。
| -- -- TreeMap : 保证按照添加的key- value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
底层使用红黑树
| -- -- Hashtable : 作为古老的实现类;线程安全的,效率低;不能存储null 的key和value
| -- -- Properties : 常用来处理配置文件。key和value都是String 类型
HashMap 的底层:数组+ 链表 (jdk7及之前)
数组+ 链表+ 红黑树 (jdk 8 )
二、Map 结构的理解 :
Map 中的key: 无序的、不可重复的,使用Set 存储所有的key -- -> key所在的类要重写 equals ( ) 和 hashCode ( ) (以HashMap 为例)
Map 中的value: 无序的、可重复的,使用Collection 存储所有的 value -- -> value 所在的类要重写 equals ( )
一个键值对:key- value构成了一个Entry 对象。
Map 中的 entry:无序的、不可重复的,使用Set 存储所有的entry
Map 与 Collection 并列存在。用于保存具有映射关系的数据: key- value
Map 中的 key 和 value 都可以是任何引用类型的数据
Map 中的 key 用 Set 来存放,不允许重复,即同一个 Map 对象所对应的类,须重写 hashCode ( ) 和 equals ( ) 方法
常用 String 类作为 Map 的“键”
key 和 value 之间存在单向一对一关系,即通过指定的key总能找到唯一的、确定的 value
Map 接口的常用实现类:HashMap 、TreeMap 、LinkedHashMap 和 Properties 。其中,HashMap 是 Map 接口使用频率最高的实现类
9.4.1 HashMap
1. HashMap 是 Map 接口使用频率最高的实现类。
2. 允许使用 null 键和 null 值,与 HashSet 一样,不保证映射的顺序。
3. 所有的 key 构成的集合是 Set : 无序的、不可重复的。所以, key 所在的类要重写: equals ( ) 和 hashCode ( )
4. 所有的 value 构成的集合是 Collection : 无序的、可以重复的。所以, value 所在的类要重写: equals ( )
5. 一个 key- value 构成一个 entry, 所有的entry构成的集合是Set : 无序的、不可重复的
6. HashMap 判断两个 key 相等的标准是:两个 key 通过 equals ( ) 方法返回true , hashCode值也相等 ( 参照 HashSet )
7. HashMap 判断两个 value 相等的标准是:两个 value 通过 equals ( ) 方法返回true 。
HashMap 的底层实现原理:
JDK 7 及以前版本:HashMap 是数组+ 链表结构( 即为链地址法)
JDK 8 版本发布以后:HashMap 是数组+ 链表+ 红黑树实现。
HashMap 源码中的重要常量 :
DEFAULT_INITIAL_CAPACITY : HashMap 的默认容量: 16
DEFAULT_LOAD_FACTOR:HashMap 的默认加载因子: 0.75
threshold: 扩容的临界值 = 容量 * 填充因子: 16 * 0.75 = > 12
TREEIFY_THRESHOLD: Bucket 中链表长度大于该默认值, 转化为红黑树: 8
MIN_TREEIFY_CAPACITY: 桶中的Node 被树化时最小的 hash 表容量: 64
HashMap 在 JDK7 中的底层实现原理 :
HashMap 的内部存储结构其实是数组和链表的结合。当实例化一个 HashMap 时,系统会创建一个长度为 Capacity 的 Entry 数组, 这个长度在哈希表中被称为容量 ( Capacity ) , 在这个数组中可以存放元素的位置我们称之为"桶" ( bucket) , 每个 bucket 都有自己的索引, 系统可以根据索引快速的查找 bucket 中的元素. 每个bucket中存储一个元素, 即一个Entry 对象, 但每一个 Entry 对象可以带一个引用变量, 用于指向下一个元素, 因此, 在一个桶中, 就有可能生成一个Entry 链. 而且新添加的元素作为链表的head.
添加元素的过程( 可参考 HashSet ) :
向HashMap 中添加entry1 ( key,value) ,需要首先计算entry1中key的哈希值( 根据key所在类的hashCode ( ) 计算得到) ,此哈希值经过处理以后,得到在底层Entry [ ] 数组中要存储的位置i。
如果位置i上没有元素, 则entry1直接添加成功。
如果位置 i 上已经存在 entry2 ( 或还有链表存在的entry3, entry4) , 则需要通过循环的方法, 依次比较 entry1 中 key 的 hash 值和其他的 entry 的 hash 值.
如果彼此 hash 值不同, 则直接添加成功。
如果hash值相同,继续比较二者是否equals
如果返回值为true , 则使用 entry1 的 value 去替换 equals 为 true 的 entry 的 value.
如果遍历一遍以后, 发现所有的 equals 返回都为 false , 则 entry1 仍可添加成功. entry1 指向原有的 entry 元素。
三、HashMap 的底层实现原理?
以jdk7:
HashMap map = new HashMap ( ) :
在实例化以后, 底层创建了长度是 16 的一维数组 Entry [ ] table.
. . . 可能已经执行过多次put. . .
map. put ( key1, value1) :
首先, 调用 key1 所在类的 hashCode ( ) 计算 key1 哈希值, 此哈希值经过某种算法计算以后, 得到在 Entry 数组中的存放位置.
如果此位置上的数据为空,此时的key1- value1添加成功。 -- -- 情况1
如果此位置上的数据不为空 ( 意味着此位置上存在一个或多个数据( 以链表形式存在) ) , 比较 key1 和已经存在的一个或多个数据的哈希值:
如果 key1 的哈希值与已经存在的数据的哈希值都不相同, 此时 key1- value1 添加成功。-- -- 情况2
如果 key1 的哈希值和已经存在的某一个数据( key2- value2) 的哈希值相同, 继续比较: 调用 key1 所在类的 equals ( ) 方法, 比较:
如果equals ( ) 返回false : 此时key1- value1添加成功。-- -- 情况3
如果equals ( ) 返回true : 使用value1替换value2。
【补充】: 关于情况2 和情况3 : 此时 key1- value1 和原来的数据以链表的方式存储。
在不断的添加过程中,会涉及到扩容问题,当超出临界值 ( 且要存放的位置非空) 时, 扩容: 默认的扩容方式, 扩容为原来容量的2 倍, 并将原有的数据重新计算数组索引, 复制过来.
HashMap 的扩容
当HashMap 中的元素越来越多的时候, hash 冲突的几率也就越来越高, 因为数组的长度是固定的。所以为了提高查询的效率, 就要对HashMap 的数组进行扩容, 而在HashMap 数组扩容之后, 最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置, 并放进去, 这就是resize.
那么HashMap 什么时候进行扩容呢?
当HashMap 中的元素个数超过数组大小( 数组总大小length, 不是数组中个数size) * loadFactor时,就 会 进 行 数 组 扩 容,loadFactor的默认值( DEFAULT_LOAD_FACTOR) 为0.75 ,这是一个折中的取值。也就是说,默认情况下,数组大小( DEFAULT_INITIAL_CAPACITY) 为 16 ,那么当HashMap 中元素个数超过16 * 0.75 = 12 (这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2 * 16 = 32 ,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap 中元素的个数,那么预设元素的个数能够有效的提高HashMap 的性能。
HashMap 在JDK8中的底层实现原理
HashMap 的内部存储结构其实是 数组 + 链表 + 红黑树 的结合。当实例化一个HashMap 时,会初始化initialCapacity和loadFactor,在put第
一对映射关系时,系统会创建一个长度为 initialCapacity 的 Node 数组, 这个长度在哈希表中被称为容量( Capacity ) ,在这个数组中可以存放元
素的位置我们称之为“桶”( bucket) ,每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素,每个bucket中存储一个元素,即一个Node 对象,但每一个Node 对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node 链。也可能是一个一个TreeNode 对象,每一个TreeNode 对象可以有两个叶子结点left和right,因此,在一个桶中,就
有可能生成一个TreeNode 树。而新添加的元素作为链表的last,或树的叶子结点。
那么HashMap 什么时候进行扩容和树形化呢?
当HashMap 中的元素个数超过数组大小 ( 数组总大小length, 不是数组中个数size) * loadFactor时,就会进行数组扩容,loadFactor的默认值( DEFAULT_LOAD_FACTOR) 为0.75 ,这是一个折中的取值。也就是说,默认情况下,数组大小( DEFAULT_INITIAL_CAPACITY) 为16 ,那么当 HashMap 中元素个数超过 16 * 0.75 = 12 (这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2 * 16 = 32 ,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap 中元素的个数,那么预设元素的个数能够有效的提高HashMap 的性能。当 HashMap 中的其中一个链的对象个数如果达到了 8 个,此时如果 capacity 没有达到64 ,那么HashMap 会先扩容解决,如果已经达到了64 ,那么这个链会变成红黑树,结点类型由Node 变成TreeNode 类型。当然,如果当映射关系被移除后,下次 resize 方法时判断树的结点个数低于6 个,也会把红黑树再转为链表。
关于映射关系的key是否可以修改?answer:不要修改
映射关系存储到HashMap 中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry 或Node (TreeNode )的hash值了,因此如果已经put到Map 中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。
总结:
jdk8 相较于jdk7在底层实现方面的不同:
1. new HashMap ( ) : 底层没有创建一个长度为16 的数组
2. jdk 8 底层的数组是:Node [ ] , 而非Entry [ ]
3. 首次调用put ( ) 方法时,底层创建长度为16 的数组
4. jdk7底层结构只有:数组+ 链表。jdk8中底层结构:数组+ 链表+ 红黑树。
4.1 形成链表时,七上八下(jdk7: 新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64 时,此时此索引位置上的所数据改为使用红黑树存储。
9.4.2 LinkedHashMap
LinkedHashMap 的底层实现原理(了解)
LinkedHashMap 是 HashMap 的子类
在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
与 LinkedHashSet 类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key - Value 对的插入顺序一致
HashMap 中的内部类:Node
LinkedHashMap 中的内部类:Entry
源码中:
static class Entry < K , V > extends HashMap. Node < K , V > {
Entry < K , V > before, after;
Entry ( int hash, K key, V value, Node < K , V > next) {
super ( hash, key, value, next) ;
}
}
9.4.3 TreeMap
TreeMap 两种添加方式的使用
TreeMap 存储Key - Value 对时,需要根据key- value对进行排序。TreeMap 可以保证所有的Key - Value 对处于有序状态。
TreeSet 底层使用红黑树结构存储数据
TreeMap 的Key 的排序:
自然排序:TreeMap 的所有的Key 必须实现Comparable 接口,而且所有的Key 应该是同一个类的对象,否则将会抛出ClassCastException
定制排序:创建TreeMap 时,传入一个Comparator 对象,该对象负责对TreeMap 中的所有key 进行排序。此时不需要Map 的Key 实现Comparable 接口
TreeMap 判断两个key相等的标准:两个key通过compareTo ( ) 方法或者compare ( ) 方法返回0 。
9.4.4 HashTable
Hashtable
Hashtable 是个古老的Map 实现类,JDK1. 0 就提供了。不同于HashMap ,Hashtable 是线程安全的。
Hashtable 实现原理和HashMap 相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
与HashMap 不同,Hashtable 不允许使用null 作为key和value
与HashMap 一样,Hashtable 也不能保证其中Key - Value 对的顺序
Hashtable 判断两个key相等、两个value相等的标准,与HashMap 一致。
9.4.5 Properties
Properties 处理属性文件 ( 暂略 参考 IO )
Properties 类是Hashtable 的子类,该对象用于处理属性文件
由于属性文件里的key、value都是字符串类型,所以* * Properties 里的key和value都是字符串类型* *
存取数据时,建议使用setProperty ( String key, Stringvalue ) 方法和getProperty ( String key) 方法
9.4.6 Map 中常用方法
添加、删除、修改操作:
Object put ( Object key, Object value) :将指定key- value添加到( 或修改) 当前map对象中
void putAll ( Map m) : 将m中的所有key- value对存放到当前map中
Object remove ( Object key) :移除指定key的key- value对,并返回value
void clear ( ) :清空当前map中的所有数据
Map map = new HashMap ( ) ;
map. put ( "AA" , 123 ) ;
map. put ( 45 , 123 ) ;
map. put ( "BB" , 56 ) ;
map. put ( "AA" , 87 ) ;
System . out. println ( map) ;
Map map1 = new HashMap ( ) ;
map1. put ( "CC" , 123 ) ;
map1. put ( "DD" , 456 ) ;
map. putAll ( map1) ;
System . out. println ( map) ;
Object value = map. remove ( "CC" ) ;
System . out. println ( value) ;
System . out. println ( map) ;
map. clear ( ) ;
System . out. println ( map. size ( ) ) ;
System . out. println ( map) ;
元素查询的操作:
Object get ( Object key) :获取指定key对应的value
boolean containsKey ( Object key) :是否包含指定的key
boolean containsValue ( Object value) :是否包含指定的value
int size ( ) :返回map中key- value对的个数
boolean isEmpty ( ) :判断当前map是否为空
boolean equals ( Object obj) :判断当前map和参数对象obj是否相等
元视图操作的方法:
Set keySet ( ) :返回所有key构成的Set 集合
Collection values ( ) :返回所有value构成的Collection 集合
Set entrySet ( ) :返回所有key- value对构成的Set 集合
Set set = map. keySet ( ) ;
Iterator iterator = set. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
System . out. println ( iterator. next ( ) ) ;
}
Collection values = map. values ( ) ;
for ( Object obj : values) {
System . out. println ( obj) ;
}
Set entrySet = map. entrySet ( ) ;
Iterator iterator1 = entrySet. iterator ( ) ;
while ( iterator1. hasNext ( ) ) {
Object obj = iterator1. next ( ) ;
Map. Entry entry = ( Map. Entry ) obj;
System . out. println ( entry. getKey ( ) + "---->" + entry. getValue ( ) ) ;
}
Set keySet = map. keySet ( ) ;
Iterator iterator2 = keySet. iterator ( ) ;
while ( iterator2. hasNext ( ) ) {
Object key = iterator2. next ( ) ;
Object value = map. get ( key) ;
System . out. println ( key + "=====" + value) ;
}
总结:常用方法:
添加:put ( Object key, Object value)
删除:remove ( Object key)
修改:put ( Object key, Object value)
查询:get ( Object key)
长度:size ( )
遍历:keySet ( ) / values ( ) / entrySet ( )
9.4.7 HashMap 和 Hashtable 的异同
HashMap 和 Hashtable 的异同?
1. HashMap与Hashtable 都实现了Map 接口。由于HashMap 的非线程安全性,效率上可能高于Hashtable 。Hashtable 的方法是Synchronize 的,而HashMap 不是,在多个线程访问Hashtable 时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。
2. HashMap允许将null 作为一个entry的key或者value,而Hashtable 不允许。
3. HashMap把Hashtable 的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。
4. Hashtable继承自Dictionary 类,而HashMap 是Java1 .2 引进的Map interface 的一个实现。
5. Hashtable和HashMap 采用的hash/ rehash算法都大概一样,所以性能不会有很大的差异。
9.5 Collections工具类
操作数组的工具类:Arrays
Collections 是一个操作 Set 、List 和 Map 等集合的工具类
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
排序操作:(均为static 方法)
reverse ( List ) :反转 List 中元素的顺序
shuffle ( List ) :对 List 集合元素进行随机排序
sort ( List ) :根据元素的自然顺序对指定 List 集合元素按升序排序
sort ( List ,Comparator ) :根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap ( List ,int ,int ) :将指定list 集合中的i处元素和j 处元素进行交换
Object max ( Collection ) :根据元素的自然顺序,返回给定集合中的最大元素
Object max ( Collection ,Comparator ) :根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min ( Collection )
Object min ( Collection ,Comparator )
int frequency ( Collection ,Object ) :返回指定集合中指定元素的出现次数
void copy ( List dest, List src) :将src中的内容复制到dest中
boolean replaceAll ( List list,Object oldVal,Object newVal) :使用新值替换 List 对象的所有旧值
void copy ( List dest, List src) :将src中的内容复制到dest中
List list = new ArrayList ( ) ;
list. add ( 43 ) ;
list. add ( - 97 ) ;
list. add ( 0 ) ;
List dest = Arrays . asList ( new Object [ list. size ( ) ] ) ;
System . out. println ( dest. size ( ) ) ;
Collections . copy ( dest, list) ;
List list1 = Collections . synchronizedList ( list) ;
9.6 Collection 和 Collections 的区别
面试题:Collection 和 Collections 的区别?
Collection 是集合类的上级接口,继承于他的接口主要有Set 和List.
Collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作.
9.7 小结
字符串:
String :
初始化:final char [ ] -> 不可变!
StringBuffer :
初始化:char [ 16 ]
线程:线程安全,效率低
扩容:2 倍+ 2
StringBuilder :
初始化:char [ 16 ]
线程:线程不安全,效率高
扩容:2 倍+ 2
集合:
1.L ist:(有序,可重复)
1.1 ArrayList:
初始化:Object [ 10 ] ( jdk7)
Object [ ] { } , 第一次add的时候,初始化容量为10 ( jdk8)
线程:线程不安全,查询效率高,增删改对底层数组不友好,故增删改效率不得行
扩容:1.5 倍,不够的话,直接变为当前需要的长度(摆烂!!!)
1.2L inkedList:
底层:Node < prev, item, next> 双向链表
线程:线程不安全,增删改效率高,查询效率低(指针遍历导致)
扩容:无扩容机制
1.3 Vector:(究极大冤种!)
初始化:Object [ 10 ]
线程:线程安全,效率低
扩容:2 倍
2. Set:(无序,不可重复)
2.1 HashSet:
初始化:Entry [ 16 ] ( jdk7)
Node [ ] 第一次 add 的时候,初始化容量为16 ( jdk8)
底层:( HashMap ) 数组 + 链表
线程:线程不安全
扩容:[ 当超过临界值( 当前容量 * 扩容因子) 时] 2 倍 < add 七上八下>
方法:equals ( ) , hashCode ( )
2.2L inkedHashSet:
线程:线程不安全
底层:( LinkedHashMap ) 数组 + 链表
扩容:无扩容机制
方法:equals ( ) , hashCode ( )
2.3 TreeSet:
存储:同种类型的对象
底层:( TreeMap ) 红黑树
线程:线程不安全
方法:自然排序 || 定制排序
3. Map:(k- v 存储)
3.1 HashMap:
初始化:Entry [ 16 ] ( jdk7)
Node [ ] 第一次 put 的时候,初始化容量为16 ( jdk8)
底层:数组 + 链表 ( jdk7)
数组 + 链表 + 红黑树 ( jdk8)
以链表形式存在的数据个数 > 8 && 当前数组的长度 > 64 时,转化红黑树存储
线程:线程不安全
常量:
DEFAULT_INITIAL_CAPACITY : HashMap 的默认容量: 16
DEFAULT_LOAD_FACTOR:HashMap 的默认加载因子: 0.75
threshold: 扩容的临界值 = 容量 * 填充因子: 16 * 0.75 = > 12
TREEIFY_THRESHOLD: Bucket 中链表长度大于该默认值, 转化为红黑树: 8
MIN_TREEIFY_CAPACITY: 桶中的Node 被树化时最小的 hash 表容量: 64
3.2L inkedHashMap:
底层:Entry
线程:线程不安全
3.3 TreeMap:
底层:红黑树
方法:自然排序 || 定制排序
3.4 Hashtable:
线程:线程安全