十三、集合进阶
13.1 集合体系结构
集合整体上可以分为两类:单列集合Collection和双列集合Map。
图中,红色表示接口,蓝色表示实现类。
13.2 Collection集合
Collection介绍
Collection是单列集合的祖宗接口,他的功能是全部单列集合都可以继承使用的。
Collection常用方法
方法名称 | 说明 |
---|---|
pulic boolean add(E e) | 把给定的对象添加到当前集合中 |
public void clear() | 清空集合中所有的元素 |
public boolean remove(E e) | 把给定的对象在集合中删除 |
public boolean contains(Object obj) | 判断当前集合中是否包含给定的对象 |
public boolean isEmpty() | 判断当前集合是否为空 |
public int size() | 返回集合中欧给元素的个数/集合的长度 |
//Collection是一个接口,不能直接创建他的对象。所有学习方法只能创建他的实现类的对象。本例中采用ArrayList
Collection<String> coll = new ArrayList<>();//利用多态的方式创建对象
//1.添加元素
//如果往List系列集合中添加数据,那么方法永远返回true。因为List系列允许元素重复。
//如果往set系列集合中添加数据,如果添加元素不存在则返回true,否则返回false。
coll.add("aa");
//2.清空元素
coll,clear();
//3.删除
//因为Collection里面定义的是共性的方法,所以不能通过索引进行删除。只能通过元素的对象进行删除。
//删除失败返回false,如果删除的元素不存在,那么就会删除失败。
coll.remove("aa");
//4.判断元素是否包含
//底层实现依赖equals方法,如果存储的是自定义对象,在JavaBean类中,一定要重写equals方法。
//如果没有重写,则依赖Object类中的equals方法进行判断,即依赖地址值进行判断。
bool contains = coll.contains("aa");
//5.判断集合是否为空
//底层实现为判断size
bool isEmpty = coll.isEmpty();
//6.获取集合的长度
int size = coll.size();
collection的遍历方式
- 迭代器遍历
- 增强for遍历
- Lambda表达式遍历
迭代器遍历
迭代器在java中的类是Iterator,迭代器是集合专用的遍历方式。迭代器不依赖索引。
Collection集合获取迭代器与常用方法
方法名称 | 方法说明 |
---|---|
Iterator< E > iterator() | 返回迭代器对象,默认指向当前集合的0索引 |
boolean hashNext() | 判断当前位置是否有元素,有元素返回true,没有元素返回false |
E next() | 获取当前位置的元素,并将迭代器对象移向下一个位置 |
//1.创建指针
Iterator<String> it = list.iterator();
//2.判断是否有元素
while(it.hasNext()) {
//3。获取元素同时移动指针
String str = it.next();
System.out.println(str);
}
迭代器细节注意点
- 报错NoSuchElementException
- 迭代器遍历完毕,指针不会复位
- 循环中只能用一次next方法
- 迭代器遍历时,不能用集合的方法进行增加或者删除
增强for遍历
- 增强for的底层就是迭代器,为了简化迭代器的代码书写。
- JDK5之后出现,其内部原理就是一个Iterator迭代器。
- 所有的单列集合和数组才能用增强for进行遍历。
增强for格式
for(元素的数据类型 变量名:数组或者集合) {
}
//具体案例
//s其实就是一个第三方变量,在循环的过程中依次表示集合的每一个数据
//Idea快捷键 List.for
for(String s : List) {
System.out.println(s);
}
增强for的细节
修改增强for中的变量,不会改变集合中原本的数据。
Lambda表达式遍历
得益于JDK 8开始的新技术Lambda表达式,提供了一种更简单、更直接的遍历集合的方式。
方法名称 | 说明 |
---|---|
default void forEach(Consumer<? super T> action): | 集合Lambda遍历集合 |
//1.创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("1");
coll.add("2");
coll.add("3");
//2.利用匿名内部类形式
//底层原理:方法遍历集合得到每一个元素,把得到的每一个元素传递给accpet方法
coll.forEach(new Consumer<String>(){
@Override
//s依次表示集合中的每一个数据
public void accept(String s) {
System.out,println(s);
}
});
//3.Lambda表达式
coll.forEach(s -> System.out,println(s) );
13.3 List集合
List集合特点
- 有序:存和取的顺序一致
- 有索引:可以通过索引操作元素
- 可重复:存储的元素可以重复
List集合的特有方法
- Collection的方法List都继承了
- List集合因为有索引,所以多了很多索引操作的方法
方法名称 | 说明 |
---|---|
void add(int index, E element) | 在集合中的指定位置插入指定的元素 |
E remove(int Index) | 删除指定索引处的元素,返回被删除的元素 |
E set(int Index, E element) | 修改指定索引处的元素,返回被删除的元素 |
E get(int Index) | 返回指定索引处的元素 |
//1.创建一个集合
List<String> list = new ArrayList<>();
//2.添加元素
list.add("aaa");
list.add("bbb");
list.add("ccc");
//指定位置添加元素
list.add(1,"ttt");//[aaa,ttt,bbb,ccc]
//删除指定索引
String s = list.remove(0);
//修改指定索引
String s1 = list.set(0,"qqq");
//返回指定索引的元素
String s2 = list.get(0);
List集合的遍历方式
- 迭代器遍历 遍历过程需要删除元素使用迭代器
- 列表迭代器遍历 遍历过程中需要添加元素使用列表迭代器
- 增强for遍历
- Lambda表达式遍历
- 普通for遍历(List存在索引) 遍历时想要操作索引,使用普通for
//创建元素并添加元素
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
//1.迭代器
Iterator<String> it = list.iterator();
while(list.hasNext()) {
String str = it.next();
System.out.println(str);
}
//2.增强for
for(String s : list) {
System.out.println(s);
}
//3.Lambda表达式
coll.forEach(s -> System.out,println(s));
//4.普通for循环
for(int i = 0; i < list.size(); i++) {
String str = list.get(i);
System.out.println(s);
}
//5.列表迭代器
//与迭代器相比添加了一个方法:在遍历的过程中可以添加元素
ListIterator<String> it = list.listIterator();
while(it.hasNext()) {
String str = it.next();
if(str.equals("BBB")) {
it.add("qqq")
}
System.out.println(str);
}
13.4 ArrayList集合
ArrayList集合底层原理
- 利用空参构造的集合,在底层创建一个默认长度为0的数组。
- 添加第一个元素时,底层会创建一个新的长度为10的数组。
- 存满时,会扩容1.5倍。
- 如果一次添加多个元素,1.5倍放不下,则新创建的数组的长度以实际为准。
13.5 LinkedList集合
LinkedList集合
- 底层数据结构是双链表,查询慢,增删块,操作首尾元素速度块。
LinkedList方法
特有方法 | 说明 |
---|---|
public void addFirst(E e) | 在该列表开头插入指定的元素 |
public void addLast(E e) | 将指定的元素追加到此列表的末尾 |
public E getFirst() | 返回列表的第一个元素 |
public E getLast() | 返回列表的最后一个元素 |
public E removeFirst() | 从此列表中删除并放回第一个元素 |
public E removeLast() | 从此列表中删除并放回最后一个元素 |
13.6 泛型深入
泛型
泛型是JDK5引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。
泛型的格式:<数据类型>
注意:泛型只支持引用数据类型。
如果没有给集合指定类型,默认认为所有的数据类型都是Object类型,此时可以往集合添加任意的数据类型。此时的坏处时,获取数据的时候,无法使用特有行为。此时推出泛型,在添加数据的时候就把类型进行统一,而且在获取数据的时候不需要额外强转。
泛型的好处
- 统一数据类型。
- 把运行期间的问题提到了编译期间,避免强制类型转换可能出现的异常,因为在编译阶段类型就能确定下来。
拓展:java中的泛型是伪泛型。
泛型的细节
- 泛型中不能写基本数据类型。
- 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类型。
- 如果不写泛型,类型默认是Object。
泛型可以在很多地方定义
- 类后面 泛型类
- 方法上面 泛型方法
- 接口后面 泛型接口
泛型类
使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类。
//格式
修饰符 class 类名 <类型> {
}
//举例
public class ArrayList<E> {//创建类对象时,E就确定类型。
}
//此处E可以理解为变量,但是不是用来接收数据的,而是记录数据的类型,可以写成:T(Type)、E(Element)、K(Key)、V(Variable)等
//自己编写一个带有泛型的ArrayList
/*
* 当编写一个类时,如果不确定类型,那么这个类可以定义为泛型类。
**/
public class MyArrayList<E> {
Object[] obj = new Object[10];
int size;
/*
E:表示不确定的类型。该类型在类的后面已经定义过了。
e:形参的名字,变量名。
**/
public boolean add(E e) {
obj[size] = e;
size++;
return true;
}
public E get(int index) {
return (E)obj[index];
}
public String toString() {
return Arrays.toString(obj);
}
}
泛型方法
方法中形参类型不确定时,可以使用类名后面定义的泛型< E >。如果类中只有一个方法的形参不确定,此时没有必要把泛型定义在后面,此时可以把泛型定义在方法上。
当方法中参数类型不确定时
- 使用类名后面定义的泛型,类中的所有方法都能用。
- 在方法申明上定义自己的泛型,只有本方法能用。
//格式
修饰符 <类型> 返回值类型 方法名(类型 变量名) {
}
//举例
public <T> void show(T t) {
}
//此处T可以理解为变量,但是不是用来接收数据的,而是记录数据的类型,可以写成:T(Type)、E(Element)、K(Key)、V(Variable)等
//定义一个工具类ListUtil 类中定义静态方法addAll,用来添加多个集合元素
public class ListUtil {
private ListUtil(){};
/*
*参数一:集合
*参数二到最后:要添加的元素
**/
public static<E> void addAll(ArrayList<E> list, E e1, E e2, E e3) {
list.add(e1);
list.add(e2);
list.add(e3);
}
}
泛型接口
//格式
修饰符 interface 接口名<类型> {
}
//举例
public interface List<E> {
}
如何使用一个带泛型的接口
- 实现类给出具体类型
- 实现类延续泛型,创建对象时再确定
//1.实现类给出具体的类型
public class MyArrayList implments List<String> {
}
//2.实现类延续泛型,创建实现类对象时再确定类型
public class MyArrayList<E> implments List<E> {
}
泛型的继承和通配符
- 泛型不具备继承性,但是数据具备继承性。
//ye fu zi三个类存在继承关系
ArrayList<Ye> list1 = new arrayList<>();
ArrayList<Fu> list1 = new arrayList<>();
ArrayList<Zi> list1 = new arrayList<>();
method(list1);//正常
//报错,泛型不具备继承性
method(list2);
method(list3);
//正确运行,数据具备继承性
list1.add(new Ye());
list1.add(new Fu());
list1.add(new Zi());
public static<Ye> void method(ArrayList<Ye> list) {
}
//泛型的通配符
//?super E:传递E或E的父类
//?extends E:传递E或E的子类
public static void method(ArrayList<? super ye> list) {
}
public static void method(ArrayList<? extends ye> list) {
}
使用场景
- 如果再定义类、方法、接口时类型不确定,就可以定义泛型类、泛型方法、泛型接口。
- 如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以使用泛型通配符。
13.7 Set系列集合
Set系列集合特点
- 无序:存取顺序不一致
- 不重复:可以去除重复
- 无索引:没有带索引的方法,所以不能用普通for循环遍历,也不能通过索引来获取元素。
Set集合的实现类
- HashSet:无序、不重复、无索引
- LinkedHashSet:有序、不重复、无索引
- TreeSet:可排序、无索引、不重复
set接口中的方法基本上与Collection的API一致。
13.8 HashSet
HashSet底层原理
- HashSet集合底层采取哈希表存储数据
- 哈希表是一种对于增删改查数据性能都较好的结构
哈希表组成
- JDK8之前:数组+链表
- JDK8开始:数组+链表+红黑树
哈希值
- 根据hashCode方法计算出来的int类型的整数
- 该方法定义在object类中,所有的对象都可以调用,默认使用地址值进行计算
- 一般情况下,会重写hashcode方法,利用对象内部的属性值计算哈希值
对象的哈希值特点
- 如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
- 如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
- 在小部分情况下,不同属性值或者不同地址值计算出来的哈希值也有可能是一样的(哈希碰撞)
HashSet JDK8以前的底层原理
//1.创建一个默认长度16,默认加载因子为0.75的数组,数组名table
HashSet<String> hm = new HashSet<>();
//2.根据元素的哈希值跟数组的长度计算出应存入的位置。
int index = (数组长度 - 1) & 哈希值;
//3.判断当前位置是否为null,如果是null直接存入
//4.如果位置不为null,表示有元素,则调用equals方法比较属性值
//5.如果equals比较结果为true,不存入,如果是false,则存入数组,形成链表
//不同点:JDK8之前,新元素入数组,老元素挂在下面 JDK8之后,新元素直接挂在老元素下面
//扩容时期:当数组存的元素>16 * 0.75 数组就会扩容成原先的两倍。 链表的长度大于8而且数组的长度大于等于64链表就会变成红黑树。
13.9 LinkedHashSet
LinkedHashSet底层原理
- 有序、不重复、无索引
- 这里的有序指的是保证存储和取出的元素顺序一致
- 原理:底层数据结构依然是哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序
13.10 TreeSet
TreeSet特点
- 不重复、无索引、可排序
- 可排序:按照元素的默认规则(从小到大)排序
- TreeSet集合底层是基于红黑树数据结构实现排序的、增删改查性能都较好
//1.创建TreeSet对象
TreeSet<Integer> ts = new TreeSet<>();
//2.添加元素
ts.add(5);
ts.add(2);
ts.add(4);
ts.add(3);
ts.add(1);
//3.打印集合
System.out.println(ts)//[1,2,3,4,5]
TreeSet的两种比较方式
- 方式1:默认排序/自然排序”Javabean类实现Comparable接口指定比较规则
- 方式2:创建TreeSet对象时候,传递比较器Comparator指定规则
使用原则:默认使用第一种,如果第一种不能满足需求,就使用第二种。
13.11 双列集合
双列集合的特点
- 双列集合一次需要存一对数据,分别为键和值
- 键不能重复,值可以重复
- 键和值是一一对应的关系,每一个键只能找到自己对应的值
- 键+值这个整体称之为键值对或者键值对对象,在Java中叫做Entry对象。
Map集合的常见API
Map是双列集合的顶层接口,他的功能是全部双列集合都可以继承使用的。
方法名称 | 说明 |
---|---|
V put(K key, V value) | 添加元素 |
V remove(Object key) | 根据键删除键值对元素 |
void clear() | 移除所有的键值对元素 |
boolean containsKey(Object key) | 判断集合是否包含指定的键 |
boolean containsValue(Object value) | 判断集合是否包含指定的值 |
boolean isEmpty() | 判断集合是否为空 |
int size() | 集合的长度,也就是集合中键值对的个数 |
//1.创建Map集合的对象
Map<String, String> m = new HashMap<>();
//2.添加元素
//put方法的细节:添加/覆盖
//在添加数据的时候,如果键不存在,直接把键值对对象添加到map当中,方法返回null
//如果键是存在的,那么会把原有的键值对覆盖,会把覆盖的值进行返回
m.put("三年二班", "周杰伦");
//删除
String v = m.remove("三年二班");//周杰伦
//情况
m.clear();
//判断是否包含
boolean keyResult = m.containsKey("三年二班");
boolean valueResult = m.containsValue("周杰伦");
//判断是否为空
boolean result = m.isEmpty();
//集合的长度
int num = m.size();
Map的遍历方式
- 键找值
- 键值对
- Lambda表达式
键找值遍历
//1.创建map的对象
Map<String, String> map = new HashMap<>();
//2.添加元素
map.put("A", "1");
map.put("B", "2");
map.put("C", "3");
//3.键找值遍历
//3.1获取所有的键到单列结合中
Set<String> keys = map.keySet();
//3.2遍历单列集合得到每一个键
for(String key : keys) {
//3.3利用键获取值
String value = map.get(key);
}
键值对遍历
//1.创建map的对象
Map<String, String> map = new HashMap<>();
//2.添加元素
map.put("A", "1");
map.put("B", "2");
map.put("C", "3");
//3.键值对遍历
//3.1获取所有的键值对对象,返回一个Set集合
Set<Map.Entry<String, String>> entries = map.entrySet();
//3.2遍历entries集合得到每一个键值对对象
for(Map.Entry<String, String> entry : entries) {
//3.3利用entry调用get方法获取键和值
String key = entry.getKey();
String value = entry.getValue();
}
Lambda表达式
方法名称 | 说明 |
---|---|
default void forEach(BiConsumer<? super k, ? super V> action) | 结合Lambda遍历Map集合 |
//1.创建map的对象
Map<String, String> map = new HashMap<>();
//2.添加元素
map.put("A", "1");
map.put("B", "2");
map.put("C", "3");
//3.Lambda表达式遍历
map.forEach(new BiConsumer<String, String>(){
@Override
public void accept(String key, String value) {
System.out.println(key + "=" + value);
}
});
//底层原理:键值对遍历得到每一个键和值,再调用accept方法
map.forEach((key, value) -> System.out.println(key + "=" + value));
13.12 HashMap
HashMap的特点
- HashMap是Map里面的实现类。
- 没有额外的特有方法,直接使用Map里面的方法就可以。
- 特点都是由键决定的:无序、不重复、无索引。
- HashMap和HashSet的底层原理都是一模一样的,都是哈希表结构。
HashMap的底层原理
大致与HashSet相同,put元素底层会创建Entry对象,根据键的值计算出哈希值。不同之处在于,如果键的属性值相同则覆盖。
HashMap的键位置如果存储的是自定义对象,需要重写HashCode和equals方法。
13.13 LinkedHashMap
LinkedHashMap特点
- 由键决定:有序、不重复、无索引
- 这里的有序指的是保证存储和取出的顺序一致
- 原理:底层数据结构依然是哈希表,只是每个键值对元素又额外的多了一个双链表的机制记录存储的顺序。
13.14 TreeMap
TreeMap特点
- TreeMap和TreeSet底层原理一样,都是红黑树结构的。
- 由键决定特性:不重复、无索引、可排序
- 可排序:对键进行排序
- 注意:默认按照键的从小到大进行排序,也可以自己规定键的排序规则
代码书写规则
- 实现Comparable接口,指定比较规则
- 创建集合时传递Comparator对象,指定比较规则
新的统计思想
利用map集合进行统计。如果没有要求对结果排序,默认使用HashMap,如果要求对结果排序则使用TreeMap。
/*需求:
*字符串"adasdasnfakjsfhjaskfhdajsk"
*统计每个字符出现的次数
*按a(5)b(4)格式输出
*/
//1.定义字符串
String s = "adasdasnfakjsfhjaskfhdajsk";
//2.创建集合
TreeMap<Character, Integer> tm = new TreeMap<>();
//3.遍历字符串得到每一个字符
for(int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
//拿着C到集合中判断是否存在
if(tm.containsKey(c)) {
int count = tm.getValue(c);
count++;
tm.put(c,count);
}else {
tm.put(c,1)
}
}
HashMap源码分析
1.看源码之前需要了解的一些内容
Node<K,V>[] table 哈希表结构中数组的名字
DEFAULT_INITIAL_CAPACITY: 数组默认长度16
DEFAULT_LOAD_FACTOR: 默认加载因子0.75
HashMap里面每一个对象包含以下内容:
1.1 链表中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
Node<K,V> next; //下一个节点的地址值
1.2 红黑树中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
TreeNode<K,V> parent; //父节点的地址值
TreeNode<K,V> left; //左子节点的地址值
TreeNode<K,V> right; //右子节点的地址值
boolean red; //节点的颜色
2.添加元素
HashMap<String,Integer> hm = new HashMap<>();
hm.put("aaa" , 111);
hm.put("bbb" , 222);
hm.put("ccc" , 333);
hm.put("ddd" , 444);
hm.put("eee" , 555);
添加元素的时候至少考虑三种情况:
2.1数组位置为null
2.2数组位置不为null,键不重复,挂在下面形成链表或者红黑树
2.3数组位置不为null,键重复,元素覆盖
//参数一:键
//参数二:值
//返回值:被覆盖元素的值,如果没有覆盖,返回null
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
// true,表示老元素的值保留,不会覆盖
// false,表示老元素的值不保留,会进行覆盖
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//定义一个局部变量,用来记录哈希表中数组的地址值。
Node<K,V>[] tab;
//临时的第三方变量,用来记录键值对对象的地址值
Node<K,V> p;
//表示当前数组的长度
int n;
//表示索引
int i;
//把哈希表中数组的地址值,赋值给局部变量tab
tab = table;
if (tab == null || (n = tab.length) == 0){
//1.如果当前是第一次添加数据,底层会创建一个默认长度为16,加载因子为0.75的数组
//2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容的条件
//如果没有达到扩容条件,底层不会做任何操作
//如果达到了扩容条件,底层会把数组扩容为原先的两倍,并把数据全部转移到新的哈希表中
tab = resize();
//表示把当前数组的长度赋值给n
n = tab.length;
}
//拿着数组的长度跟键的哈希值进行计算,计算出当前键值对对象,在数组中应存入的位置
i = (n - 1) & hash;//index
//获取数组中对应元素的数据
p = tab[i];
if (p == null){
//底层会创建一个键值对对象,直接放到数组当中
tab[i] = newNode(hash, key, value, null);
}else {
Node<K,V> e;
K k;
//等号的左边:数组中键值对的哈希值
//等号的右边:当前要添加键值对的哈希值
//如果键不一样,此时返回false
//如果键一样,返回true
boolean b1 = p.hash == hash;
if (b1 && ((k = p.key) == key || (key != null && key.equals(k)))){
e = p;
} else if (p instanceof TreeNode){
//判断数组中获取出来的键值对是不是红黑树中的节点
//如果是,则调用方法putTreeVal,把当前的节点按照红黑树的规则添加到树当中。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
//如果从数组中获取出来的键值对不是红黑树中的节点
//表示此时下面挂的是链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//此时就会创建一个新的节点,挂在下面形成链表
p.next = newNode(hash, key, value, null);
//判断当前链表长度是否超过8,如果超过8,就会调用方法treeifyBin
//treeifyBin方法的底层还会继续判断
//判断数组的长度是否大于等于64
//如果同时满足这两个条件,就会把这个链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//e: 0x0044 ddd 444
//要添加的元素: 0x0055 ddd 555
//如果哈希值一样,就会调用equals方法比较内部的属性值是否相同
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
break;
}
p = e;
}
}
//如果e为null,表示当前不需要覆盖任何元素
//如果e不为null,表示当前的键是一样的,值会被覆盖
//e:0x0044 ddd 555
//要添加的元素: 0x0055 ddd 555
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){
//等号的右边:当前要添加的值
//等号的左边:0x0044的值
e.value = value;
}
afterNodeAccess(e);
return oldValue;
}
}
//threshold:记录的就是数组的长度 * 0.75,哈希表的扩容时机 16 * 0.75 = 12
if (++size > threshold){
resize();
}
//表示当前没有覆盖任何元素,返回null
return null;
}
TreeMap源码分析
1.TreeMap中每一个节点的内部属性
K key; //键
V value; //值
Entry<K,V> left; //左子节点
Entry<K,V> right; //右子节点
Entry<K,V> parent; //父节点
boolean color; //节点的颜色
2.TreeMap类中中要知道的一些成员变量
public class TreeMap<K,V>{
//比较器对象
private final Comparator<? super K> comparator;
//根节点
private transient Entry<K,V> root;
//集合的长度
private transient int size = 0;
3.空参构造
//空参构造就是没有传递比较器对象
public TreeMap() {
comparator = null;
}
4.带参构造
//带参构造就是传递了比较器对象。
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
5.添加元素
public V put(K key, V value) {
return put(key, value, true);
}
参数一:键
参数二:值
参数三:当键重复的时候,是否需要覆盖值
true:覆盖
false:不覆盖
private V put(K key, V value, boolean replaceOld) {
//获取根节点的地址值,赋值给局部变量t
Entry<K,V> t = root;
//判断根节点是否为null
//如果为null,表示当前是第一次添加,会把当前要添加的元素,当做根节点
//如果不为null,表示当前不是第一次添加,跳过这个判断继续执行下面的代码
if (t == null) {
//方法的底层,会创建一个Entry对象,把他当做根节点
addEntryToEmptyMap(key, value);
//表示此时没有覆盖任何的元素
return null;
}
//表示两个元素的键比较之后的结果
int cmp;
//表示当前要添加节点的父节点
Entry<K,V> parent;
//表示当前的比较规则
//如果我们是采取默认的自然排序,那么此时comparator记录的是null,cpr记录的也是null
//如果我们是采取比较去排序方式,那么此时comparator记录的是就是比较器
Comparator<? super K> cpr = comparator;
//表示判断当前是否有比较器对象
//如果传递了比较器对象,就执行if里面的代码,此时以比较器的规则为准
//如果没有传递比较器对象,就执行else里面的代码,此时以自然排序的规则为准
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 {
//把键进行强转,强转成Comparable类型的
//要求:键必须要实现Comparable接口,如果没有实现这个接口
//此时在强转的时候,就会报错。
Comparable<? super K> k = (Comparable<? super K>) key;
do {
//把根节点当做当前节点的父节点
parent = t;
//调用compareTo方法,比较根节点和当前要添加节点的大小关系
cmp = k.compareTo(t.key);
if (cmp < 0)
//如果比较的结果为负数
//那么继续到根节点的左边去找
t = t.left;
else if (cmp > 0)
//如果比较的结果为正数
//那么继续到根节点的右边去找
t = t.right;
else {
//如果比较的结果为0,会覆盖
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
}
//就会把当前节点按照指定的规则进行添加
addEntry(key, value, parent, cmp < 0);
return null;
}
private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
Entry<K,V> e = new Entry<>(key, value, parent);
if (addToLeft)
parent.left = e;
else
parent.right = e;
//添加完毕之后,需要按照红黑树的规则进行调整
fixAfterInsertion(e);
size++;
modCount++;
}
private void fixAfterInsertion(Entry<K,V> x) {
//因为红黑树的节点默认就是红色的
x.color = RED;
//按照红黑规则进行调整
//parentOf:获取x的父节点
//parentOf(parentOf(x)):获取x的爷爷节点
//leftOf:获取左子节点
while (x != null && x != root && x.parent.color == RED) {
//判断当前节点的父节点是爷爷节点的左子节点还是右子节点
//目的:为了获取当前节点的叔叔节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//表示当前节点的父节点是爷爷节点的左子节点
//那么下面就可以用rightOf获取到当前节点的叔叔节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
//叔叔节点为红色的处理方案
//把父节点设置为黑色
setColor(parentOf(x), BLACK);
//把叔叔节点设置为黑色
setColor(y, BLACK);
//把爷爷节点设置为红色
setColor(parentOf(parentOf(x)), RED);
//把爷爷节点设置为当前节点
x = parentOf(parentOf(x));
} else {
//叔叔节点为黑色的处理方案
//表示判断当前节点是否为父节点的右子节点
if (x == rightOf(parentOf(x))) {
//表示当前节点是父节点的右子节点
x = parentOf(x);
//左旋
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
//表示当前节点的父节点是爷爷节点的右子节点
//那么下面就可以用leftOf获取到当前节点的叔叔节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//把根节点设置为黑色
root.color = BLACK;
}
双列集合小结
-
TreeMap添加元素的时候,键是否需要重写hashCode和equals方法?
- 此时是不需要重写的。
-
HashMap是哈希表结构的,JDK8开始由数组,链表,红黑树组成的。既然有红黑树,HashMap的键是否需要实现Compareable接口或者传递比较器对象呢?
- 不需要的,因为在HashMap的底层,默认是利用哈希值的大小关系来创建红黑树的。
-
TreeMap和HashMap谁的效率更高?
- 如果是最坏情况,添加了8个元素,这8个元素形成了链表,此时TreeMap的效率要更高。但是这种情况出现的几率非常的少。一般而言,还是HashMap的效率要更高。
-
三种双列集合,以后如何选择?
-
默认:HashMap(效率最高)
-
如果要保证存取有序:LinkedHashMap
-
-
如果要进行排序:TreeMap
13.15 可变参数
JDK5提出的特性。方法形参的个数是可以发生变化的。格式:属性类型…名字。举例:int…args
底层原理:可变参数底层就是一个数组,只不过不需要自己创建,java底层会自动创建。
可变参数细节:方法的形参中最多只能写一个可变参数。在方法中除了可变参数之外还有其他形参,那么可变参数要写在最后。
13.16 Collections
Collections介绍
- java.util.Collections是集合工具类
- 作用:collections不是集合,而是集合的工具类
Collections常用API
方法名称 | 说明 |
---|---|
public static < T > boolean addAll(Collection< T > c, T…elements) | 批量添加元素 |
public static void shuffle(List<?> list) | 打乱List集合元素的顺序 |