集合的使用
主要内容
- 集合总体结构介绍
- List 实现类
- Set的实现类
- Map实现类
- Iterator
- Collections
学习目标
知识点 | 要求 |
---|---|
集合总体结构介绍 | 掌握 |
List实现类 | 掌握 |
Set的实现类 | 掌握 |
Map实现类 | 掌握 |
Iterator | 掌握 |
Collections | 掌握 |
一. 集合介绍
1. 介绍
集合又称容器。是 Java 中对数据结构(数据存储方式的具体实现。
我们可以利用集合存放数据,也可以对集合进行新增、删除、修改、查看等操作。
集合中数据都是在内存中,当程序关闭或重启后集合中数据会丢失。所以集合是一种临时存储数据的容器。
2. JDK中集合结构图(常见面试题)
集合作为一个容器,可以存储多个元素,但是由于数据结构的不同,java提供了多种集合类。将集合类中共性的功能,不断向上抽取,最终形成了集合体系结构。(虚线框是接口,实线是实现类)
2.1 List接口和Set接口
List和Set的父接口
1. List 接口:存储有序, 可重复数据。
1. Vector:List 的实现类,底层为可变长度数组实现。所有方法都是同步操作(线程安全),每次扩容成本增长。新数组长度为原数组长度的2倍
2. ArrayList:List 的实现类,底层为可变长度数组实现。所有方法都是非同步操作(非线程安全的),以1.5倍的方式在扩容。常用于: 查询较多的情况
3. LinkedList:List 的实现类,双向非循环链表的实现。常用于: 删 增 较多的情况
Set接口:存储无序,不可重复数据。
1. HashSet:Set 实现类,底层是 HashMap 散列表(数组+链表+(红黑树 jdk1.8及之后))。所有添加到 HashSet 中的元素实际存储到了 HashMap 的 key 中
2. LinkedHashSet:HashSet 子类. 使用 LinkedHashMap 来存储它的元素,存储的值插入到LinkedHashMap 的可以 key 中, 底层实现(数组+链表+(红黑树 jdk1.8 及之后) + 链表), 可以记录插入的顺序
3. TreeSet:Set 实现类,底层是 TreeMap(红黑树实现), 存入到 TreeSet 中的元素, 实际存储到了 TreeMap 中, 根据存储元素的大小可以进行排序
2.2 Map接口
Map:独立的接口。每个元素都包含 Key(名称)和 Value(要存储的值)两个值。
1. HashMap:Map 实现类, 对散列表 (数组+链表+(红黑树Java8及之后))的具体实现,非同步操作(非线程安全的)。存储时以 Entry 类型存储(key, value)
2. LinkedHashMap: HashMap 的子类,是基于 HashMap 和链表来实现的。在 hashMap 存储结构之上再添加链表, 链表只是为了保证顺序
3. TreeMap:Map 实现类, 使用的不是散列表, 而是对红黑树的具体实现。根据 key 值的大小, 放入红黑树中, 可实现排序的功能(key 值大小的排序)
4. HashTable:Map 实现类, 和 HashMap 数据结构一样,采用散列表(数组+链表+(红黑树 jdk1.8及之后))的方法实现, 对外提供的 public 函数几乎都是同步的(线程安全)。
常用的集合: ArrayList, HashMap, HashSet
二. Collection接口
**1. **介绍
List 和 Set 接口的父接口, 还有其他的实现类或子接口。
2. 继承关系
3. 包含的API
泛型中<? extends E> 代表:只要是E类型或E类型的子类都可以。
三. List接口
**1. **介绍
Collection 接口的子接口。Collection 中包含的内容 List 接口中可以继承。
List 专门存储有序,可重复数据的接口。
2. 包含的API
四. ArrayList
**1. ** 介绍
实现了 List 接口, 底层实现可变长度数组。存储有序、可重复数据, 有下标。
**2. ** 实例化
常用向上转型进行实例化。绝大多数集合都支持泛型,如果不写泛型认为泛型是<Object>,使用集合时建议一定要指定泛型。
List<泛型类型> 对象 = new ArrayList<>( );
**3. ** 内存结构图
4. 常用API
public class TestArrayList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//按照下标添加
list.add("aa");
//添加到指定的位置
list.add(1, "bb");
//查看元素个数
int size = list.size();
//根据指定的下标修改
list.set(0, "aaa");
//判断
int bb = list.indexOf("bb"); //元素存在, 返回元素的下标. 不存在, 返回-1
boolean b = list.contains("bb"); //元素存在, 返回true. 不存在, 返回false
//删除
list.remove(0); //根据下标删除
list.remove("bb"); //根据指定元素删除
//更新
list.set(1, 65);
//查询
for (String s : list) {
System.out.println(s);
}
for (int i = 0; i < size; i++) {
System.out.println(list.get(i));
}
}
}
remove()方法强调
五. 泛型为集合类型
1. 介绍
在集合中泛型都是任意引用类型。既然是任意引用类型,也可以是集合类型。(集合 List 的泛型仍是一个集合 List ,如下)
2. 实例化语法
//例如:集合中可以再保存集合
List<List<Integer>> list = new ArrayList<>();
3. 内存结构图
4. 代码示例
public class Test {
public static void main(String[] args) {
List<List<Integer>> list = new ArrayList<>();
List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
List<Integer> list2 = new ArrayList<>();
list2.add(11);
list2.add(22);
list2.add(33);
list.add(list1);
list.add(list2);
//因此遍历出集合中元素需要两层for循环
for (List<Integer> integerList : list) {
for (Integer i : integerList) {
System.out.println(i);
}
}
}
}
六. LinkedList
1. 介绍
LinkedList 是 Java 中对双向非循环链表的实现。实现了 List 接口。
具有 ArrayList 所有常用方法,额外还添加了头尾操作方法(实现了 Deque 接口),这些方法在 List 接口中是不存在的,所以如果希望使用这些头尾操作方法,实例化时不用向上转型。
2. 实例化语法
LinkedList<泛型> 对象 = new LinkedList<>();
3. 常用API
ArrayList 里面常用 API 在 LinkedList 中都可以使用。
下面的演示为 LinkedList 比 ArrayList 多的常用方法。
public class TestLinkedList {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addFirst("aa");//头插
linkedList.addLast("bb");//尾插
//获取头结点 没有头结点 java.util.NoSuchElementException
String first = linkedList.getFirst();
System.out.println(first);
//获取尾结点 没有尾结点 java.util.NoSuchElementException
String last = linkedList.getLast();
System.out.println(last);
//获取头结点 没有头结点 返回 null
String s = linkedList.peekFirst();//不会报异常了,直接返回null
System.out.println(s);
//获取尾结点 没有尾结点 放回 null
String s1 = linkedList.peekLast();
System.out.println(s1);
//删除头结点 没有头结点 java.util.NoSuchElementException
String s2 = linkedList.removeFirst();
System.out.println(s2); //被删除结点中的值
//删除尾结点 没有尾结点 java.util.NoSuchElementException
String s3 = linkedList.removeLast();
System.out.println(s3);//被删除结点中的值
//删除头结点 没有头结点 返回 null
String s4 = linkedList.pollFirst();//不会报异常了,直接返回null
System.out.println(s4);
//删除尾结点 没有尾结点 放回 null
String s5 = linkedList.pollLast();
System.out.println(s5);
}
}
**问题1:**将 ArrayList 替换成 LinkedList 之后,变化的是什么?
底层的结构变了
ArrayList:数组 LinkedList:双向非循环链表
问题2:到底是使用 ArrayList 还是 LinkedList
根据使用场合而定
大量的根据索引查询的操作,大量的遍历操作(按照索引0–n-1逐个查询一般),建议使用 ArrayList
如果存在较多的添加、删除操作,建议使用 LinkedList
问题3:LinkedList 增加了哪些方法
增加了对添加、删除、获取首尾元素的方法
addFirst( )、addLast( )、removeFirst( )、removeLast( )、getFirst( )、getLast( )
七、Java中栈和队列的实现类
Vector过时了,被ArrayList替代了,Stack也就过时了
public class Stack<E> extends Vector<E>
Deque和Queue的实现类,用的非常少了解即可
1.ArrayDeque 顺序栈 数组
2.LinkedList 链栈 链表
public interface Queue<E> extends Collection<E>
public interface Deque<E> extends Queue<E>
1. 早期的栈结构实现类 Stack
public class Test1 {
public static void main(String[] args) {
Stack<String> stack =new Stack<String>();
// 入栈方法
stack.push("马云");
stack.push("马化腾");
stack.push("马明哲");
stack.push("马老师");
// 跳栈 弹栈 取出栈顶元素
String pop = stack.pop();
System.out.println(pop);
System.out.println(stack);
}
}
2.Queue单端队列
public class Test2 {
public static void main(String[] args) {
Queue<String> q=new LinkedList<String>();
// 入队方法
q.offer("张三丰");
q.offer("张翠山");
q.offer("张无忌");
System.out.println(q);
// 出队方法 取出队首
String poll = q.poll();
System.out.println(poll);
System.out.println(q);
}
}
3.Deque双端队列
public class TestLinkedList2 {
public static void main(String[] args) {
//摞盘子
Deque<String> deque1 = new LinkedList<String>();
deque1.offerFirst("盘子1");//队首存值
deque1.offerLast("盘子2");//队尾存值
deque1.pollFirst();//队首取值
deque1.pollLast();//队尾取值
System.out.println(deque1.size());
}
}
八. Set接口
**1. **介绍
Set 继承了 Collection 接口。继承的都是 Collection 中的方法, 没有提供额外方法。
Set 经常称为实现无序, 不重复数据集合, 指的就是 HashSet 实现类
2. 包含API
九. HashSet
1. 介绍
完全基于 HashMap (数组 + 链表 + (红黑树)) 实现的。
存储无序, 无下标, 元素不重复数据。
无序:存入和取出的顺序没有关系,一般就不一致,也就是存入的顺序没有被维持
不重复:存入的重复值直接被覆盖
2. 代码示例
public class TestHashSet {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
//添加元素
set.add(1);
set.add(2);
//元素个数
int size = set.size();
//是否包含指定的元素
boolean contains = set.contains(1);
ArrayList<Integer> list = new ArrayList<>();
list.add(3);
list.add(4);
list.add(4);
//将其它集合中的元素添加到set集合中
set.addAll(list);
//删除指定的元素
set.remove(2);
//查询元素
for (Integer integer : set) {
System.out.println(integer);
}
}
}
十. TreeSet
**1. **介绍
底层是基于 TreeMap 红黑树。
public class TestTreeSet {
public static void main(String[] args) {
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(2);
treeSet.add(1);
treeSet.add(3);
//查询元素
for (Integer integer : treeSet) {
System.out.println(integer);
}
}
}
2.使用Set集合分别存储学生对象
public class TestSet2 {
public static void main(String[] args) {
//创建一个集合set对象
//Set<Student> set = new TreeSet<Student>();
//Set<Student> set = new HashSet<Student>();
Set<Student> set = new LinkedHashSet<Student>();
//添加多个学生
Student stu2 = new Student(2, "lisi", 23, 98);
Student stu3 = new Student(3, "wangwu", 22, 87);
Student stu1 = new Student(1, "zhangsan", 23, 90);
Student stu4 = new Student(1, "zhangsan", 23, 90);
set.add(stu1);
set.add(stu2);
set.add(stu3);
set.add(stu4);
//输出学生
System.out.println(set.size());
System.out.println(set);
}
}
问题1:HashSet、LinkedHashSet :为什么 String 有重复,会保持唯一;为什么 Student 有重复,不会保持唯一。
解答1:HashSet、LinkedHashSet 需要 Student 实现 hashCode( ) 和 equals( )
问题2:TreeSet 为什么 String 可以添加,而 Student 就不让添加到 TreeSet 中呢? 而是抛出异常:
java.lang.ClassCastException: com.bjsxt.entity.Student cannot be cast to java.lang.Comparable
思考:String 是系统类,Student 是自定义类,应该是 String 已经做了某些事情,但是 Student 没有做
解答2:TreeSet 需要 Student 实现 Comparable 接口并指定比较的规则
在这里更加清晰认识到接口是一种规范,接口里面可以没有什么东西,但是它定义了一种默认的功能,想要实现这个功能就要通过实现这个接口,再自定义相关的功能,如此代码才能认识这个功能。
3.重写Student的equals()和hashCode()方法
public class Student implements Comparable<Student>{
private int sno;
private String name;
private int age;
private double score;
@Override
public int compareTo(Student o) {
//return this.sno - o.sno;
//return o.sno - this.sno;
return -(this.sno - o.sno);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (sno != student.sno) return false;
if (age != student.age) return false;
if (Double.compare(student.score, score) != 0) return false;
return name != null ? name.equals(student.name) :
student.name == null;
}
@Override
public int hashCode() {
int result;
long temp;
result = sno;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
temp = Double.doubleToLongBits(score);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}
4.比较器Comparator的作用和使用
import java.util.Objects;
public class Student implements Comparable<Student> {
private int age;
private String name;
private String sex;
public Student(int age, String name, String sex) {
this.age = age;
this.name = name;
this.sex = sex;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
'}';
}
@Override
public int compareTo(Student o) {
int a= this.age-o.age;
return a;
}
}
十一. Map接口
**1. **接口
Map 是独立的接口,和 Collection 没有关系。
Map 中每个元素都是 Entry 类型,每个元素都包含 Key(键)和 Value (值)
相当于 key 是钥匙——value 是物品,有了锁才可以拿到物品,锁必须是唯一的,但箱子里的物品可以不唯一。
2. 继承关系
3. 包含的API
十二. HashMap
**1. **介绍
HashMap 是对散列表的具体实现。
底层是 Hash 表
里面都包含 Key-Value 值。
2. 代码演示
public class TestHashMap {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
//向集合中添加元素 key value 进行添加 添加方法 put
//如果key相同了,后者的value会把前者相同key的value覆盖掉
map.put("aa", 11);
map.put("bb", 22);
map.put("cc", 33);
map.put("cc", 33);
map.put(null, 44);//Tree中不允许key出现空对象的情况
//获取存储键值个数
System.out.println(map.size());
//是否包含指定的key
System.out.println(map.containsKey("cc"));
System.out.println(map.containsKey("dd"));
//根据key获取value
System.out.println(map.get("cc"));
//根据key 删除键值对 返回被删除的值
Integer cc = map.remove("cc");
System.out.println(cc);
//遍历
/*
* 键遍历
* 值遍历
* 键值遍历
* */
//键遍历 获取所有的键
Set<String> strings = map.keySet();
for (String key : strings) {
System.out.println("key-> " + key + " value-> " + map.get(key));
}
//值遍历
Collection<Integer> values = map.values();
for (Integer value : values) {
System.out.println(value);
}
//键值遍历
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
System.out.println(entry.getKey() + "- " + entry.getValue());
}
}
}
十三. TreeMap
1. 简介
红黑树的具体实现。
2. 代码示例
总体和 HashMap 使用非常类型
public class TestTreeMap {
public static void main(String[] args) {
Map<Integer, String> treeMap = new TreeMap<>();
treeMap.put(2, "bb");
treeMap.put(1, "aa");
treeMap.put(3, "cc");
for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
}
十四. Iterator
1. 简介
中文名称:迭代器。是一个接口,每个集合中实现类都对 Iterator 提供了内部类的实现。
通过 Iterator 可以实现遍历集合的效果。
存在意义:
隐藏集合实现细节,无论是哪种集合都是通过 Iterator 进行操作,而不是直接操作集合。通过一套 API 实现所有集合的遍历。
可以在遍历时删除集合中的值。
Iterator 源码:
2. 实例化
每个实现类都提供了.iterator( );返回值就是迭代器对象。
public class TestIterator {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//获取集合的迭代器
Iterator<Integer> iterator = list.iterator();
//.hasNext() 判断是否有下一个元素
while (iterator.hasNext()) {
//获取下一个元素
Integer next = iterator.next();
System.out.println(next);
}
}
}
3. ConcurrentModificationException
在循环遍历集合时,向集合中插入值或删除集合中值时会出现这个异常。
public class Test {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
for (Integer integer : list) {
list.remove(integer);
}
}
}
异常截图:
为什么使用for(int i =0;i<list.size();i++){}时不出现这个异常?
因为这种循环其实是多次执行 get,调用 get( ) 方法时,集合其他元素删除或新增是没有要求的。而增强for循环是把集合看做一个整体,在遍历集合时,不允许对整个集合进行操作。
4. 遍历集合时删除元素内容
public class Test {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//获取集合的迭代器
Iterator<Integer> iterator = list.iterator();
//.hasNext() 判断是否有下一个元素
while (iterator.hasNext()) {
//获取下一个元素
Integer next = iterator.next();
//删除当前元素
iterator.remove();
}
}
}
总结:
- Iterator 专门为遍历集合而生,集合并没有提供专门的遍历的方法
Iterator 实际上迭代器设计模式的实现
-
哪些集合可以使用 Iterator 遍历
层次1:Collection、List、Set可以、Map不可以
层次2:提供 iterator( ) 方法的就可以将元素交给 Iterator;
层次3:实现 Iterable 接口的集合类都可以使用迭代器遍历
- for-each 循环和 Iterator 的联系
for-each 循环(遍历集合)时,底层使用的是 Iterator
- for-each 循环和 Iterator 的区别
for-each 还能遍历数组,Iterator 只能遍历集合
使用 for-each 遍历集合时不能删除元素,会抛出异常 ConcurrentModificationException 使用 Iterator 遍历合时能删除元素
5.ListIterator
ListIterator 和 Iterator 的关系
- public interface ListIterator<E> extends Iterator<E>
- 都可以遍历 List
ListIterator 和 Iterator 的区别
- 使用范围不同
- Iterator 可以应用于更多的集合,Set、List 和这些集合的子类型。
- ListIterator 只能用于 List 及其子类型。
public class TestListIterator {
public static void main(String[] args) {
//创建一个集合对象
List<Integer> list = new ArrayList<Integer>();
//向集合中添加分数
list.add(78);
list.add(80);
list.add(89);
ListIterator<Integer> lit = list.listIterator();
while(lit.hasNext()){
lit.next();
}
while(lit.hasPrevious()){
int elem = lit.previous();
System.out.println(elem +" "+lit.nextIndex()+" "+lit.previousIndex());
}
}
}
十五. Collections
1. 介绍
Collections 是一个工具类型,一个专门操作集合的工具类。
Collection 是集合接口
2. 代码示例
public class TestCollections {
public static void main(String[] args) {
//添加元素
List<Integer> list = new ArrayList();
Collections.addAll(list, 10, 50, 30, 90, 85, 100);//6
System.out.println(list);
//排序
Collections.sort(list);//默认按照内部比较器
System.out.println(list);
//查找元素(元素必须有序)
int index = Collections.binarySearch(list, 500);//不存在返回负数
System.out.println(index);
//获取最大值和最小值
int max = Collections.max(list);
int min = Collections.min(list);
System.out.println(max + " " + min);
//填充集合
Collections.fill(list, null);
System.out.println(list);
//复制集合
List list2 = new ArrayList();
Collections.addAll(list2, 10, 20, 30, 50);
System.out.println(list2);
Collections.copy(list, list2);//dest.size >= src.size 目标列表的长度至少必须等于源列表。
System.out.println(list);
//同步集合
//StringBuffer 线程安全效率低 StringBuilder 线程不安全,效率高
//Vector 线程安全 效率低 ArrayList 线程不安全,效率高
//难道是要性能不要安全吗,肯定不是。
//在没有线程安全要求的情况下可以使用ArrayList
//如果遇到了线程安全的情况怎么办
//方法1:程序员手动的将不安全的变成安全的
//方法2:提供最新的线程安全并且性能高的集合类
List list3 = new ArrayList();
Collections.addAll(list3, 10, 90, 30, 40, 50, 23);
System.out.println(list3);
//将list3转换成线程安全的集合类
list3 = Collections.synchronizedList(list3);
//下面再操作,就线程安全了
}
}
十六. 综合实战案例 - 简易电话本
要求:
1. 所有控制台输入、输出代码只能出现在 SystemService 的实现类中。
2. 每个人必须输入两个电话号码,并且不可以重复。
3. 查看电话本、录入联系人、删除联系人、修改联系人手机号码等所有功能都是联系人的相关功能。
包含的功能:
1. 查看电话本功能
a) 先查看所有的联系人
b) 选择具体的联系人, 再查看其电话号码
2. 录入联系人功能
3. 删除联系人功能
源码分析
一、哈希表
1. 引入Hash表
2. 哈希表的结构和特点
3. 哈希表是如何添加数据的
4. 哈希表更多
4.1 如何查询数据
4.2 HashCode和equals到底有什么作用
4.3 各种类型数据的哈希码值该如何获取 HashCode()
不管怎么计算,都是为了减少碰撞
5. 装填因此/加载因子
二、HashMap底层源码分析(JDK1.7及以前)
1. 结构简介
2. 内部成员变量含义
3. put() 方法
4. addEntry() 方法
void addEntry(int hash, K key, V value, int bucketIndxe){
//如果达到了阈值,容量扩充为原来容量的2倍 16--32
if((size >= threshold) && (null != table[bucketIndex])){
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//添加结点
createEntry(hash, key, value, bucketIndex);
}
5. get() 方法
public V get(Object key){
//根据key找到Entry(Entry中有key和value)
Entry<K, V> entry = getEntry(key);
//如果entry == null,返回null,否则返回value
return null == entry ? null : entry.getValue();
}
三、HashMap底层源码分析(JDK1.8及以后)
1. 基本属性
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable{
//序列化和反序列化时使用相同的id
private static final long serialVersionUID = 362498820763181265L;
//初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树形阈值
static final int TREEIFY_THRESHOLD = 8;
//取消阈值
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形容量
static final int MIN_TREEIFY_CAPACITY = 64;
//节点
transient Node<K,V>[] table;
//存储键值对的个数
transient int size;
//散列表被修改的次数
transient int modCount;
//扩容临界值
int threshold;
//负载因子
final float loadFactor;
}
2. 构造方法
//无参构造器,加载因子默认为0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//指定容量大小的构造器,但调用了双参数的构造器,加载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//全参构造器
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//HashMap的最大容量只能是MAXIMUM_CAPACITY,哪怕传入的数值大于最大值,也按照最大容量赋值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子必须大于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//将传入的子Map中的全部元素逐个添加到HashMap中
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
3. Node结点
1.7及以前是 Entry 节点,1.8及以后是 Node 节点,其实相差不大,因为都是 实现了 MapEntry(Map接口中的Entry接口)接口,即:实现了getKey( ) getValue( ) equals(Object o) hashCode( )等方法
static class Node<K,V> implements Map.Entry<K,V> {
//hash值
final int hash;
//键
final K key;
//值
V value;
//后继,链表下一节点
Node<K,V> next;
//全参构造器
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//返回与此项对应的键
public final K getKey() { return key; }
//返回与此项对应的值
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//hash值
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断2个Entry是否相等,必须key和value都相等,才返回true
public final boolean equals(Object o) {
if (o == this)
return true;
return o instanceof Map.Entry<?, ?> e
&& Objects.equals(key, e.getKey())
&& Objects.equals(value, e.getValue());
}
}
4. 添加键值对
4.1 put() 方法
//添加键值对
public V put(K key, V value) {
/*
参数一:调用hash()方法
参数二:键
参数三:值
*/
return putVal(hash(key), key, value, false, true);
}
4.2 hash() 方法
static final int hash(Object key) {
int h;
//hashCode和h移位 右移16位进行按位异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
4.3 putVal() 方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//声明tab和p用于操作原数组和节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果原数组为空或者原数组长度为0,那么通过resize()方法进行创建初始化
if ((tab = table) == null || (n = tab.length) == 0)
//获取到创建后数组的长度n
n = (tab = resize()).length;
//通过key的hash值和数组长度-1 计算出存储元素节点的数组中位置
//并且,如果该位置为空时,则直接创建元素节点赋值给该位置,后继元素节点为null
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//否则,说明该位置存在元素
Node<K,V> e; K k;
//判断table[i]的元素的key是否与添加的key相同,若相同则直接用新value覆盖旧value
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断是否是红黑树的节点,如果是直接在树中添加或者更新键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//否则就是链表,则在链表中添加或替换
else {
//遍历table[i],并判断添加的key是否已经存在,和之前判断一样,hash和equals
//遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据
for (int binCount = 0; ; ++binCount) {
//如果遍历的下一个节点为空,那么直接插入
//该插入方式是尾插法,与1.7不同
//将p的next赋值给e进行以下判断
if ((e = p.next) == null) {//后面没有了
//直接创建新节点连接在上一个节点的后继上
p.next = newNode(hash, key, value, null);
//如果插入节点后,链表的节点数大于等于7时,则进行红黑树的转换
//注意不仅仅是链表大于8,并且会在treeifyBin方法中判断数组是否为空或数组长度是否小于64
//如果小于64则进行扩容,并且不是直接转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//完成后直接退出循环
break;
}
//不退出循环时,则判断两个元素的key是否相同
//若相同,则直接退出循环,进行下面替换的操作
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
//否则,让p只想下一个元素节点
p = e;
}
}
//接着上面的第二个break,如果e不为空,直接用新的value覆盖旧的value并且返回旧的value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
4.4 resize() 方法
//该函数有两种使用情况:初始化哈希值或前数组容量过小,需要扩容
final Node<K,V>[] resize() {
//获取原数组
Node<K,V>[] oldTab = table;
//获取原数组的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取原扩容阈值
int oldThr = threshold;
//新的容量和阈值目前都为0
int newCap, newThr = 0;
if (oldCap > 0) {
//如果原数组容量大于等于最大容量,那么不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
//若没有超过最大容量,那么扩容为原来的2倍
}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容为原来的2倍
newThr = oldThr << 1; // double threshold
//经过上面的if,那么这步为初始化容量(使用有参构造器的初始化)
}else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//否则,使用的无参构造器
//那么,容量为16,阈值为12(16*0.75)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//使用新的容量重建一个新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新的数组引用赋值给table
table = newTab;
//如果数组不为空,那么进行元素的移动
if (oldTab != null) {
//遍历原数组中每个位置的元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//如果该位置元素不为空,那么上一步获取元素接着置为空
oldTab[j] = null;
//判断该元素上是否有链表
if (e.next == null)
//如果无链表,确定元素存放的位置,扩容前元素的位置为(oldCap-1)&e.hash,所以这
//里的新位置只有两种可能:1.位置不变 2.变为原来位置+oldCap,下面会详细介绍
newTab[e.hash & (newCap - 1)] = e;
//判断是否是树节点,如果是则执行树操作
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//否则说明该元素上存在链表,那么进行元素的移动
//根据变化的最高位的不同,也就是0或1,将链表拆分开
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//最高位为0时,则将节点加入loTail.next
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
//最高位为1则将节点加入hiTail.next
}else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//通过loHead hiHead来保存链表的头结点,然后将两个头结点放到newTab[j] 与newTab[j+oldCap]上面去
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
5. 问题
问题一:
存储在 Node 中的 Hash 值,是否就是 key 的 hashCode( )?
static final int hash(Object key){
int h;
//hashCode和右移16进行按位异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >> 16);
}
答案:不是,存储的是对 Key 先做 hashCode( ) 计算,然后再无符号右移16,再按位异或。
问题二:
如何知道一个节点到底存储在Hash表(散列表)的哪个位置?
答案:根据 key 计算相关的 hash 值(并不是简单的 hashCode( )),(数组长度-1)& hash 进行计算得出具体的下标,如果下标只有这一个节点,直接返回,非一个节点,继续在链表或红黑树中查找。
问题三:
什么时候需要把链表转化为红黑树?
答案:链表的节点数大于8(从0开始,所以判断条件是 >=7),数组的长度必须大于等于64,那么就会进行数组的扩容。
问题四:
什么时候扩容?
答案:
情况一:HashMap 的 Size 达到 Hash 中数组长度*loadFactor(扩容因子)时扩容。即比 threshold 大,进行扩容。每次都扩容为原数组的一倍。
情况二:Hash 表中某个链表长度达到8,且 Hash 表中数组的长度小于64.
问题五:
Hash表中数组最大长度是多少?
答案:最大长度为 1<<30,即2的30次方
计算操作时,发现 Hash 表中数组长度为2的倍数效率最高,需要一直保持长度为2的倍数。数组长度最大值取为2的31次方减一。所以里面最大的2的倍数为2的30次方。
问题六:
1.Hash表中使用的是单向链表还是双向链表?
答案:单向链表。
2.数组扩容时,链表使用的是尾加还是头加?
答案:尾加。
JDK1.8 及以后用的是尾插法,JDK1.7 及以前使用的头插法
问题七:
链表转化为红黑树时,数组中是所有的链表都转化为红黑树,还是什么情况?
答案:只有数组里某个下标的节点个数 >8 ,并且数组长度 >64,该下标中的链表转换为红黑树。
问题八:
为什么Java8中长度超过8以后将链表变为红黑树?
答案:红黑树的查询效率高于链表。
问题九:
为什么选择8作为转换值?
答案:元素个数为8的红黑树中,高度为4,最多查找4次就能找到需要的值,长度为8的链表,最多找7次。
例如长度为4就转换,红黑树高度为3,最多找3次,链表最多找3次。
例如长度为7就转换,红黑树高度为3,最多找3次,链表最多6次,多找3次和转换的性能消耗相比不值得。
从源码上可以看出,在理想状态下,受随机分布的 hashCode 影响。链表中的节点遵循泊松分布,而且根据统计,链表中节点数是8的概率已经接近千分之一,而且此时链表的性能已经很差了,所以在这种罕见和极端的情况下,才会把链表转变为红黑树。
6. 总结HashMap底层原理(常见面试题)
从 Java8 开始 HashMap 底层由数组+链表变成数组+链表+红黑树。
使用 HashMap 时,当使用无参构造器实例化时,设置扩容因子默认为 0.75.
当向 HashMap 添加内容时,会对 Key 做 Hash 计算,把得到的 Hash 值和数组长度-1按位与,计算出存储的位置。
如果数组中没有内容,直接存入数组中(Node 节点对象),该下标中有 Node 对象了,把内容添加到对应的链表或红黑树中。
如果添加后链表长度大于等于8,会判断数组长度是否大于64,如果小于64,对数组扩容,扩容长度为原长度的2倍。扩容后把原 Hash 表内容重新放入新的 Hash 表中。如果 Hash 长度大于等于64会把链表转化为红黑树。
最终判断 HashMap 中元素个数是否已经达到扩容阈值(threshold),如果达到扩容值,需要扩容,扩容一倍。反之,如果删除元素后,红黑树的元素个数小于6,由红黑树转换为链表。
四、TreeMap底层原理
1. 介绍
2. 基本属性
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
@SuppressWarnings("serial") // Conditionally serializable
//外部比较器,是自然排序,还是定制排序,使用final修饰,表明一旦赋值就不可更改
private final Comparator<? super K> comparator;
//红黑树的根节点
private transient Entry<K,V> root;
//TreeMap中存放的键值对的数量
private transient int size = 0;
//修改的次数
private transient int modCount = 0;
3. 节点
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;//键
V value;//值
Entry<K,V> left;//左孩子节点
Entry<K,V> right;//右孩子节点
Entry<K,V> parent;//父节点
boolean color = BLACK;//节点的颜色,在红黑树中,只有两种颜色,红色和黑色
//省略有参构造 无参构造 equals() 和 hashCode() getter 和 setter
}
4. 构造器
//构造方法,comparator比较器
public TreeMap() {
comparator = null;
}
//构造方法,提供比较器,用指定比较器排序
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
5. 添加键值
5.1 put()方法
private V put(K key, V value, boolean replaceOld) {
Entry<K,V> t = root;
if (t == null) {
addEntryToEmptyMap(key, value);
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else {
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
} else {
Objects.requireNonNull(key);
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else {
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
}
addEntry(key, value, parent, cmp < 0);
return null;
}
红黑树原理
5.2 Comparator()默认比较器
5.3 fixAfterInsertion( )方法
6. 总结
五、TreeSet 和 HashSet
TreeSet 和 HashSet底层是TreeMap和HashMap
把Set的值当作Map的Key,Map中Value存储new Object( )