# 8. Java 集合
# 1. Collections
Collection 接口:单列数据,定义了存取一组对象的方法的集合。
- List:元素有序、可重复的集合
- ArrayList:基于数组实现,增删慢,查询快,线程不安全
- Vector:基于数组实现,增删慢,查询快,线程安全
- LinkedList:基于双向链表实现,增删快,查询慢,线程不安全
- Set:元素无序、不可重复的集合
- HashSet:HashTable 实现,无序
- TreeSet:红黑树实现
- LinkedHashSet:HashTable 实现数据存储,双向链表记录顺序。
# 1.1 Collection 接口
# 介绍
Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
JDK 不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set 和 List)实现。
在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。
# 常用方法
1、添加
- add(Object obj)
- addAll(Collection coll)
2、获取有效元素的个数
- int size()
3、清空集合
- void clear()
4、是否是空集合
- boolean isEmpty()
5、是否包含某个元素
- boolean contains(Object obj):是通过元素的
equals 方法
来判断是否是同一个对象 - boolean containsAll(Collection c):也是调用元素的
equals方法
来比较的,拿两个集合的元素挨个比较。
6、删除
- boolean remove(Object obj) :通过元素的
equals方法
判断是否是要删除的哪个元素。只会删除找到的第一个元素 - boolean removeAll(Collection coll):取当前集合的差集
7、取两个集合的交集
- boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响 c
8、集合是否相等
- boolean equals(Object obj)
9、转成对象数组
- Object[] toArray()
10、获取集合对象的哈希值
- hashCode()
11、遍历
- iterator():返回迭代器对象,用于集合遍历
# 1.2 Iterator 迭代器接口
# 介绍
Iterator
对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。GOF 给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”。
Collection 接口继承了 java.lang.Iterable 接口,该接口有一个 iterator() 方法,那么所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象。
Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。
集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合 的第一个元素之前。
# 常用方法
- boolean hasNext():判断是否有下一个元素
- E next():取出下一个元素(使用前必须先用
hasNext()
进行判断)- ① 指针下移
- ② 将下移以后集合位置上的元素返回
- void remove():删除迭代器最后返回的元素
- Iterator 可以删除集合的元素,但是是遍历过程中通过迭代器对象的 remove 方法,不是集合对象的 remove 方法;
- 如果还未调用 next() 或在上一次调用 next() 方法之后已经调用了 remove 方法,再调用 remove 都会报 IllegalStateException。
# 1.3 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 位置的子集合
# ArrayList
- ArrayList 是 List 接口的典型实现类、主要实现类
- 本质上,ArrayList 是对象引用的一个变长数组
- ArrayList 的 JDK1.8 之前与之后的实现区别?
- JDK1.7:ArrayList 像饿汉式,直接创建一个初始容量为 10 的数组
- JDK1.7:每次数组容量的增长大约是其原容量的 1.5 倍。
- JDK1.8:ArrayList 像懒汉式,一开始创建一个长度为 0 的数组,当添加第一个元素时再创建一个始容量为 10 的数组。
- JDK1.8:每次数组的容量增长大约是其原容量的 1.5 倍。
- Arrays.asList(...) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实例。 Arrays.asList(...) 返回值是一个固定长度的 List 集合。
# LinkedList
对于频繁的插入或删除元素的操作,建议使用 LinkedList 类,效率较高。
新增方法:
- void addFirst(Object obj)
- void addLast(Object obj)
- Object getFirst()
- Object getLast()
- Object removeFirst()
- Object removeLast()
LinkedList:双向链表,内部没有声明数组,而是定义了 Node 类型的 first 和 last, 用于记录首末元素。同时,定义内部类 Node,作为 LinkedList 中保存数据的基本结构。Node 除了保存数据,还定义了两个变量:
- prev: 变量记录前一个元素的位置
- next: 变量记录下一个元素的位置
# Vector
Vector 是一个古老的集合,JDK1.0 就有了。大多数操作与 ArrayList 相同,区别之处在于 Vector 是线程安全的。
在各种 list 中,最好把 ArrayList 作为缺省选择。当插入、删除频繁时, 使用 LinkedList。因为 Vector 总是比 ArrayList 慢,所以尽量避免使用。
Vector 每次扩容是扩容为 2 倍。
新增方法:
- void addElement(Object obj)
- void insertElementAt(Object obj,int index)
- void setElementAt(Object obj,int index)
- void removeElement(Object obj)
- void removeAllElements()
# 请问 ArrayList/LinkedList/Vector 的异同? 谈谈你的理解? ArrayList 底层是什么? 扩容机制? Vector 和 ArrayList 的最大区别?
1. ArrayList 和 LinkedList 的异同
二者都线程不安全,相对线程安全的 Vector,执行效率高。 此外,ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。
对于随机访问 get 和 set,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。对于新增和删除操作 add (特指插入)和 remove,LinkedList 比较占优势,因为 ArrayList 要移动数据。
2. ArrayList 和 Vector 的区别
Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步类(synchronized),属于强同步类。因此开销就比 ArrayList 要大,访问要慢。正常情况下,大多数的 Java 程序员使用 ArrayList 而不是 Vector,因为同步完全可以由程序员自己来控制。
Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是1.5倍。
Vector 还有一个子类 Stack。
# 1.4 Set 接口
- Set 接口是 Collection 的子接口,Set 接口没有提供额外的方法
- Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
- Set 判断两个对象是否相同不是使用 == 运算符,而是根据
equals()
方法。
# 如何理解无序?
1. 无序性 ≠ 随机性
2. 无序性是指数据在底层存储中不是内存连续的
# 如何理解不可重复性?
1. 元素是不可重复的
2. 比较是否重复的时候先用 hashCode() 再用 equals(),都过关了才相等
# HashSet
- HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
- HashSet 按
Hash 算法
来存储集合中的元素,因此具有很好的存取、查找、删除性能。
特点:
- 不能保证元素的排列顺序
- HashSet 不是线程安全的
- 集合元素可以是 null
HashSet 集合判断两个元素相等的标准:
- 两个对象通过
hashCode()
方法比较相等,并且两个对象的equals()
方法返回值也相等。
对于存放在 Set 容器中的对象,对应的类一定要重写 equals() 和 hashCode(Object obj) 方法,以实现对象相等规则。即“相等的对象必须具有相等的散列码”。
向 HashSet 中添加元素的过程:
插入过程
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode()
方法 来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象 在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在 数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布, 该散列函数设计的越好)。
如果两个元素的 hashCode 值相等,会再继续调用 equals()
方法,如果 equals()
方法结果为 true,添加失败;如果为 false,那么会保存该元素,但是该数组的位置已经有元素了,那么会通过链表的方式继续链接。
如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。
数组上已经有元素了,那么链表中如何添加?
- JDK 7:元素 a 放到数组中,指向原来的元素(头插法)
- JDK 8:原来的元素在数组中,指向元素 a(尾插法) 总结:七上八下
HashSet 扩容原理:
HashSet 底层也是数组,初始容量为`16`,当如果使用率超过 `0.75`,(16*0.75=12) 就会扩大容量为原来的 2 倍。(16 扩容为 32,依次为 64,128.... 等)
重写 hashCode() 方法的基本原则:
1. 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
2. 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
3. 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
重写 equals() 方法的基本原则:
当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写 hashCode(),根据一个类的 equals 方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据 Object.hashCode() 方法, 它们仅仅是两个对象。
因此,违反了“相等的对象必须具有相等的散列码”。
# 结论:重写 equals 方法的时候一般都需要同时重写 hashCode 方法。通常参与计算 hashCode 的对象的属性也应该参与到 equals() 中进行计算。
示例:
@Override
public boolean equals(Object o) {
//自己的话直接true
if (this == o) return true;
//o不能为空,所属类型也应该一直,否则 false
if (o == null || getClass() != o.getClass()) return false;
//每个字段根据自己的业务需求进行比较
Hedon hedon = (Hedon) o;
return Objects.equals(name, hedon.name) &&
Objects.equals(age, hedon.age) &&
Objects.equals(birth, hedon.birth);
}
@Override
public int hashCode() {
return Objects.hash(name, age, birth);
}
# 为什么用 Eclipse/IDEA 复写 hashCode 方法,有 31 这个数字?
1. 选择系数的时候要选择尽量大的系数。因为如果计算出来的 hash 地址越大,所谓的 “冲突”就越少,查找起来效率也会提高。(减少冲突)
2. 并且 31 只占用 5bits,相乘造成数据溢出的概率较小。
3. 31 可以 由 i*31== (i<<5)-1 来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
4. 31 是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有 1 来整除!(减少冲突)
# LinkedHashSet
- LinkedHashSet 是 HashSet 的子类。
- LinkedHashSet 根据元素的
hashCode
值来决定元素的存储位置, 但它同时使用双向链表
维护元素的次序,这使得元素看起来是以插入顺序保存的。 - LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
- LinkedHashSet 不允许集合元素重复。
Set set = new LinkedHashSet(); set.add(new String("AA")); set.add(456);
set.add(456);
set.add(456);
set.add(new Customer("刘德华", 1001));
# TreeSet
TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
TreeSet 底层使用红黑树
结构存储数据(底层其实利用了 TreeMap)。
新增的方法如下:
Comparator comparator()
Object first()
Object last()
Object lower(Object e)
Object higher(Object e)
SortedSet subSet(fromElement, toElement)
SortedSet headSet(toElement)
SortedSet tailSet(fromElement)
TreeSet 两种排序方法:
- 自然排序(默认情况下,TreeSet 采用自然排序)
- 也就是存储的对象实现 Comparable 接口,然后重新 compareTo() 方法,根据这个来判断大小。
- 定制排序
- 在 new TreeSet() 的时候传入一个 Comparator 比较器。
TreeSet 的不可重复性:
- 根据比较结果 this.compareTo(o) 或 compare(o1,o2) 来判断是否重复,而不是根据 equals()。
# 2. Map
Map 接口:双列数据,保存具有映射关系 “key-value"
的集合。
- HashMap:JDK7 数组+链表 存储数据,JDK 8 数组+链表+红黑树 存储数据,线程不安全,
- ConcurrentHashMap:JDK7 是基于 分段锁 实现线程安全,JDK8 是 CAS + synchronized 实现线程安全。
- HashTable:线程安全
- TreeMap:基于红黑树实现。
- LinkedHashMap:基于 HashTable 数据结构,使用链表保存插入顺序
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 中的所有数据
# 元素查询的操作
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 集合
# HashMap
- 允许使用 null 键和 null 值,与 HashSet 一样,不保证映射的顺序;
- 所有的 key 构成的集合是 Set
- 无序的、不可重复的;
- 所以,key 所在的类要重写 equals() 和 hashCode();
- 所有的 value 构成的集合是 Collection
- 无序的、可以重复的;
- 所以,value 所在的类要重写 equals();
- 一个键值对:key-value 构成了一个 Entry 对象(JDK8 以后叫 Node);
- Map 中的 Entry:无序的、不可重复的,使用 Set 存储所有的 Entry。
HashMap 判断两个 key 相等的标准是:
- 两个 key 通过 equals() 方法返回 true, hashCode 值也相等。
HashMap 判断两个 value 相等的标准是:
- 两个 value 通过 equals() 方法返回 true。
HashMap 底层实现原理(JDK7):
- 数组+链表
1. 实例化
HashMap map = new HashMap(): 在实例化以后,底层创建了长度是 `16` 的一维数组 Entry[] table。
...可能已经执行过多次put...
2. 插入
map.put(key1,value1):
首先,调用 key1 所在类的 `hashCode()` 计算 key1 哈希值,此哈希值经过某种算法计算以后,得到在 Entry 数组中的存放位置。
如果此位置上的数据为空,此时的 key1-value1 添加成功。 ---- 情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较 key1 和已经存在的一个或多个数据
的哈希值:
如果 key1 的哈希值与已经存在的数据的哈希值都不相同,此时 key1-value1 添加成功。---- 情况2
如果 key1 的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用 key1 所在类的 `equals(key2)` 方法,比较:
如果 equals() 返回 false:此时 key1-value1 添加成功。---- 情况3
如果 equals() 返回 true:使用 value1 替换 value2。 ---- 情况4
补充:关于情况 2 和情况 3:此时 key1-value1 和原来的数据以链表的方式存储。
在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的 `2` 倍,并将原有的数据复制过来。
HashMap 底层实现原理(JDK8):
- 数组+链表+红黑树
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` 时,此时此索引位置上的所数据改为使用 `红黑树` 存储。
* 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
JDK7 的 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 的性能。
JDK8 的 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 个,也会把树再转为链表。
# 相比于 JDK7,JDK8 中 HashMap 有何不同?
1.HashMap map = new HashMap();//JDK8 默认情况下,先不创建长度为 16 的数组,当首次调用 map.put() 时,再创建长度为 16 的数组
2.数组为 Node 类型,在 JDK7 中称为 Entry 类型
3.形成链表结构时,新添加的 key-value 对在链表的尾部(七上八下)
4.当数组指定索引位置的链表长度 >8 时,且 map 中的数组的长度 > 64 时,此索引位置上的所有 key-value 对使用红黑树进行存储。
面试题:
# 谈谈你对HashMap中put/get方法的认识?如果了解再谈谈 HashMap的扩容机制?默认大小是多少?什么是负载因子( 或填充比)?什么是吞吐临界值(或阈值、threshold)?
# 负载因子值的大小,对HashMap有什么影响?
- 负载因子的大小决定了 HashMap 的数据密度。
- 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为 0.7~0.75,此时平均检索长度接近于常数。
# HashMap 并发情况下死循环造成 CPU 100%?
并发情况下可能在链表上插入两个数据可能会造成互相引用,造成死循环。
# LinkedHashMap
- LinkedHashMap 是 HashMap 的子类;
- 在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序;
- 与 LinkedHashSet 类似,LinkedHashMap 可以维护 Map 的迭代 顺序:迭代顺序与 Key-Value 对的插入顺序一致
# TreeMap
- TreeMap 存储 Key-Value 对时,需要根据 key-value 对进行排序。 TreeMap 可以保证所有的 Key-Value 对处于有序状态。
- TreeSet 底层使用红黑树结构存储数据。
- TreeMap 判断两个 key 相等的标准:两个 key 通过 this.compareTo(o) 方法或者 compare(o1,o2) 方法返回 0。
# HashTable
- Hashtable 是个古老的 Map 实现类,JDK1.0 就提供了。不同于 HashMap, Hashtable 是线程安全的。
- Hashtable 实现原理和 HashMap 相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
- 与 HashMap 不同,Hashtable 不允许使用 null 作为 key 和 value。
- 与 HashMap 一样,Hashtable 也不能保证其中 Key-Value 对的顺序。
- Hashtable 判断两个 key 相等、两个 value 相等的标准,与 HashMap 一致。
# Properties
- Properties 类是 Hashtable 的子类,该对象用于处理属性文件。
- 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型。
- 存取数据时,建议使用 setProperty(String key,String value) 方法和 getProperty(String key) 方法。
Properties pros = new Properties();
//方式一
//配置文件默认在当前的 Module 下
FileInputStream fis = new FileInputStream("jdbc.properties");
pros.load(fis);
//方式二
//配置文件默认在当前类路径下
ClassLoader cl = this.getClass().getClassLoader();
InputStream is = cl.getResourceAsStream("jdbc.properties");
pros.load(is)
String user = pros.getProperty("user");
String password = pros.getProperty("password");