《Java 核心卷1》ch 9 集合

@author:posper

@version 1.0: 2021/6/22-2021/6/25

@version 1.2: 2021/7/12


参考:jdk 1.8 官方文档

本文档是根据 动力节点 “集合” 视频以及《Java 核心卷1》ch9 整理的笔记

集合的学习阶段:

  • 掌握集合继承结构图
    • 每个集合接口的特点
    • 集合实现类的底层数据结构是什么?
  • 掌握在什么情况下用何种集合?
    • 如何创建一个集合?
    • 如何对一个具体集合进行 CRUD?
    • 如何迭代遍历一个集合?

第一遍暂时只掌握前两个阶段,源码阅读有点难顶…

ch 9 集合

1、集合概述

  • 集合中存储的是 Java 对象的内存地址(即,集合中存储的是对象的引用)
    • 集合不能直接存储基本数据类型,另外集合也不能直接存储 Java 对象,
  • Java 中的集合类和集合接口都在 java.util 包下

1.1 集合的继承结构图

集合的继承结构图

  • 通过集合对象的 iterator() 方法可以得到其对应的 Iterator 对象
  • Iterator 接口中有 4 个方法:
    • boolean hashNext();
    • E next();
    • remove();
    • default void forEachRemaining(Consumer<? super E> action);

1.2 Collection 继承结构图

Collection 继承结构图1

Collection 继承结构图2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mdss4gNV-1626169591784)(img\ch 9 集合\Collection.png)]

1.3 Map 继承结构图

Map 继承结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9d5dp3z8-1626169591787)(img\ch 9 集合\Map.png)]

1.4 集合特点总结

常见集合实现类底层数据结构
  • List 实现类
    • ArrayList:底层是数组
    • LinkedList:底层是双向链表
    • Vector:底层是数组,线程安全的,效率较低,现在使用较少(现在有其他的方法保证线程安全,基本不用 Vector)
  • Set 实现类
    • HashSet:底层是 HashMap,放到 HashSet 集合中的元素等同于放到 HashMap 集合 key 部分中
    • TreeSet:底层是 TreeMap,放到 TreeSet 集合中的元素等同于放到TreeMap 集合 key 部分了
  • Map 实现类
    • HashMap:底层是哈希表
    • Hashtable:底层也是哈希表,只不过是线程安全的,效率较低,现在使用较少
    • Properties:是线程安全的,是特殊的 Map,其中 key 和 value 只能存储字符串 String
    • TreeMap:底层是二叉树。TreeMap 集合的 key 可以自动按照大小顺序排序
  • Deueue 实现类
    • ArrayQueue:底层是循环数组。双端队列,队头队尾都可以入队/出队
    • LinkedList:还可以作为双端队列,底层是双向链表
  • PriorityQueue:底层是堆。队头元素保持是 max/min
常用集合接口的特点
  • List 集合存储元素的特点:

    • 有序可重复
    • 有序:存进去的顺序和取出的顺序相同,每一个元素都有下标
    • 可重复:存进去1,还可以在存储一个1
  • Set(Map)集合存储元素的特点:

    • 无序不可重复
    • 无序:存进去的顺序和取出的顺序不一定相同。另外,Set 中元素没有下标
    • 不可重复:存进去 1,不能再存储 1 了
  • SortedSet 集合存储元素的特点:

    • 首先是无序不可重复的,但是 SortedSet 集合中的元素是可排序的
    • 无序:存进去的顺序和取出的顺序不一定相同。另外,Set 中元素没有下标
    • 不可重复:存进去1,不能再存储 1 了
    • 可排列:可以按照大小顺序排列
  • SortedMap 集合存储元素的特点:

    • 存放于 SortedMap 中 key 部分元素的特点与 SortedSet 相同;
    • SortedMap 中 value 中的元素:无序,可重复,不会自动排序。
  • Map 集合的 key,就是一个 Set 集合

    • 往 Set 集合中存储数据,实际上是放到了 Map 集合的 key 部分了
    • Map 没有继承 Iterator 接口,所以不能直接用迭代器遍历 Map。
      • 但是,Map 的 key 是个 set ,可以利用 key 来进行迭代

2、Collection 接口

2.1 Collection 接口常见方法

  • add(Object obj)
    • 不使用泛型时,可以向 Collection 中添加任何对象类型
      • 基本数据类型会自动装箱
    • 使用泛型时,只能添加泛型类型对象
  • boolean remove(Object obj)
    • 删除集合中 obj 元素
  • int size()
    • 返回集合中元素个数
  • boolean isEmpty()
    • 判断集合是否为空(是否集合包含元素,即判断集合元素个数是否为 0)
  • boolean contains(Object obj)
    • 判断集合是否元素 obj
    • 底层会调用 Collection 中存放元素的 equals() 方法来比较元素是否相同
  • Object[] toArray()
    • 这个方法使用不多
    • 将集合转换为数组,但是返回的数组将是 Object[] 类型
  • <T> T toArray(T[] a)
    • 这个使用较多
    • 将集合转换成数组,且返回数组类型为特定类型(而不是 Object[])

2.2 利用迭代器访问集合元素

利用迭代器访问集合元素步骤(3 步)
  1. 获取集合对象的迭代器对象: iterator() 方法

    Collection c = new ArrayList(); // 创建一个未使用泛型的集合
    Iterator it = c.iterator(); // 1、获取集合对象的迭代器对象 it
    
  2. 通过 Iterator 的 hasNext() 方法判断当前集合是否还有没有被迭代的元素

  3. 通过 next() 方法返回迭代器中的下一个元素

    • 如果没有使用泛型,则 next() 方法返回的是 Object 类型;
    • 如果使用泛型,则返回泛型的具体类型
    // 2、通过获取的迭代器对象 it 开始迭代(遍历)集合
    while (it.hasNext()) { // hasNext 判断当前集合是否还有未被迭代的元素
        // 3、返回迭代器“越过的“元素
    	Object obj = it.next(); // 因为上面Collection 没用泛型,所以这里返回的是 Object 类型。用泛型的话,则返回泛型类型
    	System.out.println(obj);
    }
    

    利用迭代器访问集合元素

  • 利用迭代器遍历集合元素对于所有继承 Collection 接口的集合都通用…
  • for each 循环 可以处理任何实现了 Iterable 接口的对象
    • Collection 接口继承了 Iterable 接口。因此,Java 中集合都可以使用 “for each”循环

Iterator 还有个子接口,即列表迭代器: ListIterator,常用来迭代链表。

迭代器的注意事项

注意:集合结构(状态)只要改变(即,集合被增/删/改),迭代器必须重新获取!!!!

  • 当集合结构发生改变,迭代器未重新获取时,如果调用 next() 方法,则会抛出异常 ConcurrentModificationException
  • 在使用迭代器迭代集合的过程中,不能使用 Collection 中的 add() 和 remove() 方法
    • 本质其实还是,集合结构改变时,需要重新获取迭代器对象),会报异常~
    • 不能使用 Collection 中的 add() 和 remove() 方法
    • 但是,可以使用 Iterator 中的 remove() 方法
  • 如果一个集合同时关联多个迭代器,这些迭代器只能读取集合。
Collection c = new ArrayList();
// Iterator it = c.iterator(); // 1、error
c.add(1);
c.add(2);
// it.next(); // 异常
Iterator it = c.iterator(); // ok

2.3 contains() 方法详解

  • contains 方法在底层调用了集合所存放数据类型的 equals 方法
    • 如果集合存放的数据元素类型中重写了 equals 方法,则 contains 方法比较的是对象的内容是否相同;(比如,String)
    • 如果集合存放的数据元素类型中未重写 equals 方法,则调用的是 Object 中的 equals 方法,比较的是内存地址是否相同。
  • !!!结论:存放在集合中的类型,一定要重写 equals() 方法!!!!

注意:Java 中 String 类和 8 大基本数据类型对应的包装类型中都重写了 equals 方法.

public class CollectionTest05 {
	public static void main(String[] args) {
		Collection c = new ArrayList(); // 创建一个未使用泛型的集合
		User user1 = new User("Jack");
		c.add(user1);
		User user2 = new User("Jack");
		// 未在 User 中重写 equals() 方法时,返回 false
//		System.out.println(c.contains(user2));
		// 在 User 中重写 equals() 方法后,返回 true
		System.out.println(c.contains(user2));
        
        // 存放 String
        Collection c2 = new ArrayList(); // 创建一个未使用泛型的集合
		String s1 = new String("abc");
		c2.add(s1);
		String s2 = new String("abc"); // s1 和 s2内存地址不同,但是内容相同
        System.out.println(c.contains(s2)); // 这里返回true,因为 String 类中重写了 equals 方法,比较的是内容是否相同
	}
}

class User {
	private String name;

	public User(String name) {
		this.name = name;
	}

	@Override
	/**
	 * 重写 equals 方法 (参考《Java核心卷1》11版 p177)
	 */
	public boolean equals(Object otherObject) {
		if (this == otherObject) { // 1
			return true;
		}
		if (otherObject == null) { // 2
			return false;
		}
		if (getClass() != otherObject.getClass()) { // 3
			return false;
		}
		User user = (User) otherObject; // 4
		return Objects.equals(name, user.name); // 5
	}
}

2.4 remove() 方法详解

  • remove 方法在底层也调用了集合所存放数据类型的 equals 方法
  • 结论:存放在集合中的类型,一定要重写 equals 方法

**Note:**Collection 中的 remove() 方法其他细节同上 contains() 方法

2.5 打印 Collection 具体实现类中的内容

  • 方法1:使用 foreach 循环

  • 方法2:使用迭代器 (见 2.2)

  • 方法3:直接调用超类 AbstractCollection 中的 toString() 方法

    • 调试时用,最简洁;
    • 但是,只能按照固定格式输出
    List list = new ArrayList(); // 非线程安全的
    list.add(1);
    list.add(2);
    System.out.println(list); // [1, 2]
    
AbstractCollection 中的 toString() 源码
public abstract class AbstractCollection<E> implements Collection<E> {
    
    ....
        
    public String toString() {
        Iterator<E> it = iterator();
        if (! it.hasNext())
            return "[]";

        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (;;) {
            E e = it.next();
            sb.append(e == this ? "(this Collection)" : e);
            if (! it.hasNext())
                return sb.append(']').toString();
            sb.append(',').append(' ');
        }
    }
}

3、List 接口

List 接口特有方法:

  • E get(int index)
  • E set(int index, E val))
  • add(int index, E val)
  • … 详见 api 文档…

3.1 ArrayList

  • ArrayList 底层就是一个 Object[] 数组

  • ArrayList 底层数组默认初始化容量为 10

    • jdk1.8 中 ArrayList 底层先创建一个长度为 0 的数组
    • 第一次添加元素(调用 add() 方法)时,会初始化为一个长度为 10 的数组
  • 当 ArrayList 中的容量使用完之后,则需要对容量进行扩容

    • ArrayList 扩容后是原容量的 1.5 倍

    • ArrayList 容量使用完后,会“自动”创建容量更大的数组,并将原数组中所有元素拷贝过去,这会导致效率降低…

      • 参考:《Java 核心卷1》11版 p187
    • 优化:可以使用构造方法 ArrayList (int capacity) 或 ensureCapacity(int capacity) 提供一个初始化容量

      • 避免刚开始就一直扩容,造成效率较低…
  • ArrayList 构造方法

    1. ArrayList():创建一个初始化容量为 10 的空列表
    2. ArrayList(int initialCapacity):创建一个指定初始化容量为 initialCapacity 的空列表
    3. ArrayList(Collection<? extends E> c):创建一个包含指定集合中所有元素的列表
  • ArrayList 特点

    • 优点

      • 向 ArrayList 末尾添加元素(add() 方法)时,效率较高
        • 往往加元素情况较多…
      • 查询效率高
        • 不单纯是因为有下标,是因为其底层是数组(内存地址是连续的)
    • 缺点

      • 扩容会造成效率较低
        • 可以通过指定初始化容量,在一定程度上对其进行改善
      • 另外数组无法存储大数据量(因为很难找到一块很大的连续内存空间)
      • 向 ArrayList 中间添加元素(add(int index)),需要移动元素,效率较低
        • 但是,向 ArrayList 中间位置增/删元素的次数较少时不影响;
        • 如果增/删操作较多,可考虑改用链表
  • 如何将 ArrayList 变成线程安全的?

    • 调用 Collections 工具类中的 static <T> List<T> synchronizedList(List<T> list) 方法
    List list = new ArrayList(); // 非线程安全的
    list.add(1);
    list.add(2);
    Collections.synchronizedList(list); // 变成线程安全的了
    

    注意:ArrayList 是非线程安全

3.2 LinkedList

3.2.1 LinkedList 特点

底层数据结构:LinkedList 底层是一个双向链表

优点:

  • 增/删效率高

缺点:

  • 查询效率较低
    • LinkedList 有下标,但是是“虚假”的下标;
    • LinkedList 也可以调用 get(int index) 方法,返回链表中第 index 个元素
      • 但是,每次查找都要从头结点开始遍历
      • 如果频繁查找(调用 get(index) 方法)的话,可以改用 ArrayList,提高查询效率
3.2.2 LinkedList 部分源码解读
  • boolean add(E e) 方法
public class LinkedList<E> extends AbstractSequentialList<E> implements ... {
    // 均不参与序列化
	transient int size = 0;  // 链表长度
	transient Node<E> first; // 指向链表第一个节点
	transient Node<E> last;  // 指向链表最后一个节点

    public boolean add(E e) { // 添加元素
    	linkLast(e); // 向链表末尾添加元素 e
    	return true;
	}
    
    void linkLast(E e) { // 向链表末尾添加元素 e
        final Node<E> l = last; // 暂时保存最后一个元素的指针
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode; // newNode 作为最后一个节点
        if (l == null) // 当前链表为空
            first = newNode; // 第一次添加的节点,即为 first
        else // 链表不空
            l.next = newNode; // 当前链表的最后一个节点next 指向newNode
        size++;
        modCount++;
    }

	// LinkedList 底层是一个双向链表
	private static class Node<E> { // LinkedList 中的节点是一个 private 的静态内部类 Node
        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;
        }
    }
}
3.2.3 ListIterator 接口
  • LinkedList 类中的 void add(E e) 方法只能将数据添加到链表的末尾(见 3.2.2)
  • 如果要将对象添加到链表的中间位置,则需要使用 ListIterator 接口的 void add(E e) 方法

ListIterator 接口-常见方法:

ListIterator 接口-常见方法

  • ListIteratorremove() 方法
    • 调用 next() 之后,remove() 方法删除的是 “光标” 左侧的元素;(此时,类似键盘的 backspace)
    • 调用 previous() 之后,remove() 删除的是 “光标” 右侧的元素
  • ListIteratoradd() 方法
    • 调用 next() 之后,在 “光标” 左侧添加一个元素;
    • 调用 previous() 之后,add 是在 “光标” 右侧添加元素

3.3 Vector

  • Vector 底层是数组
  • 初始化容量为 10
  • 扩容:原容量使用完后,会进行扩容。新容量扩大为原始容量的 2 倍
  • Vector 是线程安全的(里面方法都带有 synchronized 关键字),效率较低现在使用较少
  • 如何将 ArrayList 变成线程安全的?

3.4 队列

3.4.1 Queue
  • (普通)队列:队尾插入(入队),队头删除(出队)
3.4.2 Deque
  • 双端队列:队头/队尾都可以入队/出队
3.4.3 PriorityQueue
  • 优先队列底层使用的是 “堆” 实现的
    • 保持***队头元素***是 min/max 的,并***未对所有元素都进行排序***;
  • 优先队列经典用法是进行任务调度
    • 每个任务都一个优先级;
    • 优先级“最高”的任务,位于队头
  • 注意:向优先队列中添加 自定义类型元素***时,必须给出***比较规则,否则抛出异常 ClassCastException

4、Set

4.0 泛型

详参考 ch 8 泛型程序设计

  • jdk 1.5 引入,之前都是使用 Object[]

  • 使用 Object[] 的缺点(2个)

    • 1)获取一个值时必须进行强制类型转换
    • 2)调用一个方法前必须使用 instanceof 判断对象类型
  • 泛型的好处

    • 1)减少了强制类型转换的次数
      • 获取数据值更方便
    • 2)类型安全
      • 调用方法时更安全
  • 泛型只在编译时期起作用,运行阶段 JVM 看不见泛型类型(JVM 只能看见对应的原始类型,因为进行了类型擦除)

  • 带泛型的类型,但是在使用时没有指定泛型类型时,默认使用 Object 类型

    List list = new HashTestrrayList(); // 默认可以放任意 Object 类型
    
  • lambda 表达式

4.1 HashSet

  • 特点:HashSet 无序(没有下标),不可重复
  • HashSet 底层是 HashMap,向 HashSet 中添加元素相当于插入到 HashMap 的 key 部分
    • HashMap 底层是哈希表,Java 中 HashMap 采用的是 “拉链法” 解决数据冲突
  • 注意:如果利用 HashSet 对自定义类型进行去重,必须同时重写 equals() 和 hashCode() 方法

详参考 6、HashMap 部分

4.2 TreeSet

  • 特点:TreeSet 无序(没有下标),不可重复,但是可以自动排序
  • TreeSet 底层是 TreeMap,向 TreeSet 中添加元素相当于插入到 TreeMap 的 key 部分
    • TreeMap 中采用的是 “红黑树” 对 key 进行排序

HashSet 为 HashMap 的 key 部分;TreeSet 为 TreeMap 的 key 部分。

所以,这里没有重点讲。重点掌握 HashMap 和 TreeMap。

5、Map

  • MapCollection 没有继承关系
  • Map 以 (key ,value) 的形式存储数据:键值对
    • key 和 value 存储的都是对象的内存地址(引用)

5.1 Map 接口常见方法

方法签名功能
Set keySet()返回 map 中得 keySet 视图
Collection values()返回 map 中的 value 集合(Collection) 视图
Set<Map.Entry<K, V>> entrySet()返回 map 对应的 Map.Entry 集合(set)视图
V put(K key, V value)向 map 中添加键为 key,值为 value的元素(map中有key,则更新)
V get(Object key)返回 map 中 key 对应的 value;如果 map 中不含 key,则返回 null
default V getOrDefault(Objectl) key, V] defaultValue)返回 map 中 key 对应的 value;如果 map 中不含 key,则返回 defaultValue
V remove(Object key)移除 map 中 key
void clear()清空 map
boolean isEmpty()判空(判断 map 中 size 是否为 0)
boolean containsKey(Object key)查看 map 中是否包含 key
boolean containsValue(Object value)查看 map 中是否包含 value
default void forEach(BiConsumer<? super K, ? super V> action)迭代访问 map 中所有的 key and value

注意:Map.Entry<K, V> 是 Map 中的一个内部接口。接口中的内部接口默认是 public static 的。

  • HashMap 中使用了一个静态内部类 Node 实现了 Map.Entry<K, V> 接口
    • 详见 6、HashMap 部分源码解析

5.2 Map 的遍历方法

两种常见方法是,通过获取 Map 的 keySet 或 Map.Entry<K, V> 视图,来遍历 key,value。

Note

  • 不能向 keySet 视图中添加元素;
  • 在视图上,调用迭代器 remove() 方法,会将原始 map 中对应的 key:value 删除
第一类方法

第一类方法:先获取 map 的 keySet,然后取出 key 对应的 value

特点:

  • 效率相对较低。(因为还要根据 key 从哈希表中查找对应的 value)

方法1

  • 通过 foreach 遍历 map.keySet(),取出对应的 value
public static void printMap1(Map<Integer, String> map) {	if (map.size() == 0) {		System.out.println("the map is empty...");		return;	}	System.out.println("the elements in the map are like below...");	for (Integer key : map.keySet()) {		System.out.println(key + " : " + map.getOrDefault(key, ""));	}}

方法2

  • 通过***迭代器*** 迭代 map.keySet(),来取出对应的 value

注意: 在视图上,调用迭代器 remove() 方法,会将原始 map 中对应的 key:value 删除

public static void printMap2(Map<Integer, String> map) {	Set<Integer> keySet = map.keySet();	Iterator<Integer> it = keySet.iterator();	while (it.hasNext()) {		Integer cntKey = it.next();        // it.remove(); // 在视图上,调用迭代器 remove() 方法,会将 map 中对应的 key:value 删除		System.out.println(cntKey + " : " + map.getOrDefault(cntKey, ""));	}}
第二类方法

调用 map.entrySet() 方法,获取 entrySet,然后直接从 entrySet 中同时获取 key 和 value。

特点:

  • 效率较高(直接从 node 中同时获取key,value)
  • 适用于大数据量 map 遍历

方法3

  • 调用 map.entrySet(),然后使用 foreach 遍历 entrySet
public static void printMap3(Map<Integer, String> map) {
	Set<Map.Entry<Integer, String>> entry = map.entrySet();
	for (Map.Entry<Integer, String> it : entry) {
		System.out.println(it.getKey() + " : " + it.getValue());
	}
}

方法4

  • 调用 map.entrySet(),然后使用***迭代器***遍历 entrySet

注意: 在视图上,调用迭代器 remove() 方法,会将 map 中对应的 key:value 删除

public static void printMap4(Map<Integer, String> map) {
	Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
	Iterator<Map.Entry<Integer, String>> it = entrySet.iterator();
	while (it.hasNext()) {
		Map.Entry<Integer, String> node = it.next();
        // it.remove(); // 在视图上,调用迭代器 remove() 方法,会将 map 中对应的 key:value 删除
		System.out.println(node.getKey() + " : " + node.getValue());
	}
}
第三类方法

使用 Map 中的 forEach 方法,以及 lambda 表达式

map.forEach((k, v) -> System.out.println(k + " : " + v));
第四类方法
  • 使用超类 AbstractMap 中的 toString() 方法
    • AbstractMap 重写了 toString 方法,打印格式为 {key=value,…}
  • 特点
    • 调试时,使用最为方便;
    • 但是,只能打印固定格式(不过一般也不影响调式)
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "1ab");
map.put(3, "ad");
map.put(2, "adc");
System.out.println(map); // {1=1ab, 2=adc, 3=ad}

5.3 HashMap

5.3.1 HashMap 概述
  • HashMap 底层是一个数组
    • 数组中每个元素是一个单向链表(即,采用拉链法解决哈希冲突)
      • 单链表的节点每个节点是 Node<K, V> 类型(见下 6.2 源码)
      • 同一个单链表中所有 Node 的 hash值不一定一样,但是他们对应的数组下标一定一样
        • 数组下标利用哈希函数/哈希算法(eg:对 len 取模)根据 hash值计算得到的
  • HashMap 是数组和单链表的结合体
    • 数组查询效率高,但是增删元素效率较低
    • 单链表在随机增删元素方面效率较高,但是查询效率较低
    • HashMap 将二者结合起来,充分它们各自的优点
  • HashMap 特点
    • 无序、不可重复
    • 无序:因为不一定挂在单链表的哪一个节点上了
    • 为什么不可重复?
      • 通过重写 equals() 方法保证的 (见 6.3 & 6.4 put/get 方法原理)

举个栗子:HashMap 有点像查字典。首先,从字典目录(对应 HashMap 数组)中查找要待查生字(key)的拼音/笔画(hashCode),然后根据拼音/笔画(hashCode)查找其对应的页码(利用哈希函数得到 hashCode 对应的数组下标),然后翻到指定页码(数组下标),再从当前页码(链表)中查找生字(从链表中查找 key)

5.3.2 HashMap 部分源码解析
  • HashMap 中使用了一个静态内部类 Node 实现了 Map.Entry<K, V> 接口
public class HashMap extends  AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
     // 数组 + 链表,即采用“拉链法”解决哈希冲突
    transient Node<K,V>[] table; // 哈希表(其中,数组中每个元素又是一个链表,称为一个“桶”)
        
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化容量(必须是 2 的次幂)
    static final float DEFAULT_LOAD_FACTOR = 0.75f; 	// 默认加载因子(当容量超过加载因子时,将进行再散列)
    static final int TREEIFY_THRESHOLD = 8; 			// 单链表元素超过 8 个,则转变为红黑树
    static final int UNTREEIFY_THRESHOLD = 6;			// 红黑树节点数量小于 6 时,会重新变为单链表
    
 	static class Node<K,V> implements Map.Entry<K,V> { // 静态内部类 Node
        final int hash; 	// 哈希值(由 key 经过哈希函数计算得到)
        final K key; 		// key 是 final 修饰
        V value;			// value 和 key 不能用泛型类型在 Map.Etry<K, V> 接口定义
        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; }   
     	
        ..... // 省略
    }
}
  • HashMap 默认初始化容量: 16

    • 必须是 2 的次幂,这也是 jdk 官方推荐的
    • 这是因为达到散列均匀,为了提高 HashMap 集合的存取效率,所必须的
  • HashMap 默认加载因子:0.75

    • 数组容量达到 3/4 时,开始扩容
  • JDK 8 之后,对 HashMap 底层数据结构(单链表)进行了改进:

    • 如果单链表元素超过 8 个,则将单链表转变为红黑树;

    • 如果红黑树节点数量小于 6 时,会将红黑树重新变为单链表。

    这种改进的目的,仍是为了提高检索效率,二叉树的检索会再次缩小扫描范围。提高效率。

5.3.3 put 方法原理
  1. 第一步:先将 key, value 封装到 Node 对象中
  2. 第二步:底层会调用 keyhashCode() 方法得出 hash 值
  3. 第三步:通过哈希函数/哈希算法,将 hash 值转换为数组的下标
    • 如果下标位置上没有任何元素,就把 Node 添加到这个位置上;
    • 如果下标位置上有单链表,此时会将当前 Node 中的 key 与单链表上每一个节点中的 key 进行 equals() 比较
      • 如果所有的 equals() 方法返回都是 false,那么这个新节点 Node 将被添加到链表的末尾;
      • 如果其中有一个 equals() 返回了 true,那么链表中对应的这个节点的 value 将会被新节点 Node 的 value 覆盖。(保证了不可重复)

HashMap允许 key 和 value 为 null,但是只能有一个(不可重复)!

HashTable 中 key 和 value 都不允许为 null。

5.3.4 get 方法原理
  1. 第一步:先调用 keyhashCode() 方法得出 hash 值
  2. 第二步:通过哈希函数/哈希算法,将 hash 值转换为数组的下标
    • 通过数组下标快速定位到数组中的某个位置:
      • 如果这个位置上什么也没有(没有链表),则返回 null;
      • 如果这个位置上有单链表,此时会将当前 Node 中的 key 与链表上每一个节点中的 key 进行 equals() 比较。
        • 如果所有的 equals 方法返回都是 false,那么 get 方法返回 null;
        • 如果其中有一个 equals 返回了 true,那么这个节点的 value 便是我们要找的 value,此时 get 方法最终返回这个要找的 value。

注意:放在 HashMap 中 key 的元素(或者放在 HashSet 中的元素)需要同时重写 hashCode() 和 equals() 方法!!!

5.3.5 同时重写 hashCode() 和 equals() 方法
  • 重写 hashCode() 方法时要达到散列分布均匀!!!
    • 如果 hashCode() 方法返回一个固定的值,那么 HashMap 底层则变成了一个单链表;
    • 如果 hashCode() 方法所有返回的值都不同,此时 HashMap 底层则变成了一个数组。
    • 这两种情况称之为,散列分布不均匀。

equals()hashCode()方法一定要同时重写(直接用 eclipse/IDEA 生成就行)

public class Student {	String name;        public Student(String name) {		this.name = name;	}		... // 重写 equals() 方法		... // 重写 hashCode() 方法}public static void main(String[] args) {    Set<Student> set = new HashSet<>();	Student stu1 = new Student("Amy");	Student stu2 = new Student("Amy");	set.add(stu1);	set.add(stu2);    // 如果只从重写 equals,但是未重写 hashCode 方法,这里 size 将会是 2	// 所以,equals 和 hashCode方法一定要同时重写(直接用 eclipse 生成就行)	System.out.println("size = " + set.size());    System.out.println(set); // 调用 AbstractCollection 中重写的 toString() 方法}

5.4 TreeMap

5.4.1 TreeMap 概述
  • TreeSet/TreeMap 底层是红黑树(自平衡二叉树)
  • TreeSet/TreeMap 迭代器采用的是中序遍历方式
  • TreeMap 特点:
    • 无序,不可重复,但是可根据 key 排序
5.4.2 排序规则
  • TreeSet/TreeMap中key 可以自动String 类型或8大基本类型的包装类型进行排序

  • 但是,TreeSet 无法直接自定义类型进行排序

    • 直接将自定义类型添加到 TreeSet/TreeMap中 key 会报错 java.lang.ClassCastException
    • 原因:是因为自定义类型没有实现 java.lang.Comparable 接口(此时,使用的是 TreeSet 的无参构造器
  • 对 TreeSet/TreeMap 中 key 中的元素,必须要指定排序规则。主要有两种解决方案

    1. 方法一:放在集合中的自定义类型实现 java.lang.Comparable 接口,并重写 compareTo 方法
    2. 方法二:选择 TreeSet/TreeMap 带比较器参数的构造器 ,并从写比较器中的 compare 方法
      • 比较器有 3 种常见的实现方法:
        1. 定义一个 Comparator 接口的实现类
        2. 使用匿名内部类
        3. lambda 表达式(Comparator 是函数式接口)
          • 利用 -> 的 lambda表达式 重写 compare 方法
          • 利用 Comparator.comparing 方法
  • 两种排序规则如何选择呢?

    • 当比较规则不会发生改变的时候,或者说比较规则只有一个的时候,建议实现 Comparable 接口;
    • 当比较规有多个,并且需要在多个比较规则之间频繁切换时,建议使用 Comparator 比较器。
方法1
// 利用 TreeSet 对自定义类型排序
/**
 * 方法1:自定义类型实现 Comparable 接口
 * @date 2021-06-25 14:33
 * @author preci
 * @version 1.0
 *
 */
public class Person implements Comparable<Person> {
	int age;

	public Person(int age) {
		this.age = age;
	}

	@Override
	public int compareTo(Person o) {
		return this.age - o.age; // 按照年龄升序排序
	}
}

public static void main(String[] args) {
    Set<Person> persons = new TreeSet<>();
	persons.add(new Person(1));
	persons.add(new Person(25));
	persons.add(new Person(10));
	persons.add(new Person(8));  // output:1, 8, 10, 25 (年龄升序)
}
方法2
/**
 * 方法2:利用比较器 Comparator
 * @date 2021-06-25 14:08
 * @author preci
 * @version 1.0
 *
 */
class Cat { // 没有实现 Comparable 接口
	int age;

	public Cat(int age) {
		this.age = age;
	}
}

public static void main(String[] args) {
    // 1、使用接口实现类
//	Set<Cat> set = new TreeSet<>(new MyCmp()); // 传递一个比较器对象给 TreeSet 构造器
    // 2、使用匿名内部类
    Set<Cat> set = new TreeSet<>(new Comparator<Cat>() {
		@Override
		public int compare(Cat o1, Cat o2) {
			return o1.age - o2.age;
		}
	});
    // (3) 使用 lambda 表达式,传递一个比较器对象
    // Set<Cat> set = new TreeSet<>((o1, o2) -> o1.age - o2.age); 
    
    set.add(new Cat(1));
	set.add(new Cat(15));
	set.add(new Cat(12)); // output:1,12,15
}

// 创建一个比较器类
class MyCmp implements Comparator<Cat> {
	@Override
	public int compare(Cat o1, Cat o2) {
		return o1.age - o2.age;
	}
}

5.5 HashTable

  • 作用同 HashMap 类似
  • 线程安全,但效率低。
    • HashTable 现已弃用,而是使用 ConcurrentHashMap 来⽀支持线程安全,ConcurrentHashMap 的效率会更更高,因为 ConcurrentHashMap 引⼊入了了分段锁。

5.6 Properties

  • Properties 是 HashTable 的直接子类;
  • Properties 是特殊的 Map,其中 key 和 values 都是 String
  • 常用方法
    • String getProperty(String key)
    • Object setProperty(String key, String) value)
  • 使用场景:Properties 常用配合 IO 来读/写属性配置文件(比如,username=password)

5.7 WeakHashMap

  • 作用:用来删除长期存活的 map 中的那些无用的 key;
  • JVM 的 GC 会跟踪活动的对象。只要 map 是活动的,其中的所有桶也就是活动的,尽管桶中有些 key 已经不会再被用到了,此时 GC 也无法对那些不用的 key 进行垃圾回收。
  • WeakHashMap 使用弱引用对象(WeakReference)保存 map 的 key
    • 正常情况下,GC 只会将不再活动的对象,进行回收;
    • 但是,如果某个对象只能由 WeakReference 引用,GC 也会将其回收

5.8 LinkedHashMap

  • 继承自 HashMap,因此具有和 HashMap 一样的快速查找特性

  • 内部维护了一个双向链表,⽤用来维护插入顺序或者 LRU 顺序

  • 利用 LinkedHashMap 实现 LRU 缓存步骤:

    1. 自定义一个类,继承 LinkedHashMap<K, V>
    2. 重写 removeEldestEntry 方法
    public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    	final static int MAX_SIZE = 100; // 最大容量,map 中的元素个数超过这个容量时,将会删除最近最久未使用元素
    	@Override
    	protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    		return size() > MAX_SIZE; // 大于 100 时,将删除最近最久未使用的元素
    	}
    	
    	public LRUCache() {
            // (初始化容量,装载因子,是否开启 LRU 顺序)
    		super(MAX_SIZE, 0.75f, true); // true:开启 LRU 顺序
    	}
    }
    

6、视图

  • 视图(View)可以获得其他实现了 Collection 接口或 Map 接口的对象。
    • 比如,keySet() 方法返回了一个实现了 Set 接口的类对象(但不是 HashSet,也不是 TreeSet),由这个类的方法操纵原有的 map,这种集合称为视图
  • 视图类型也实现了 Set 接口,Set 接口中的方法视图大多都能用
    • 但是,注意不可修改视图不调用 Set 中的增删改方法

6.1 子范围视图

为集合建立一个属于其一部分的子范围视图。

  • List (根据下标索引建立)
    • subList(int fromIndex, int toIndex); // 左闭右开区间
  • SortedSet (根据排序顺序)
    • SortedSet<E> subSet(E fromElement, E toElement); // 左闭右开区间
    • SortedSet<E> headSet(E toElement); // 左闭右开:[0, to)
    • SortedSet<E> tailSet(E fromElement); // 左闭右开区间
  • SortedMap (根据排序后的 key)
    • SortedMap<E> subMap(K fromElement, K toElement); // 左闭右开区间
    • SortedMap<E> headMap(K toElement); // 左闭右开:[0, to)
    • SortedMap<E> tailMap(K fromElement); // 左闭右开区间

注意:

  • 子范围视图修改,也会影响到原有集合;
  • 原集合被结构性修改后,子范围视图要重新获取;否则,再次操作子范围视图时,将会抛出异常 ConcurrentModificationException
    • 这里比较比较神奇,list 中的 增/删 才叫结构性修改,此时 subList 将会抛出异常;
    • 但是,list 进行 set 后,不叫结构性修改,此时 subList 会跟着 list 同步改变。
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 15; i++) {
	list.add(i);
}
		
// 子范围视图
List<Integer> subList = list.subList(5, 8); // 左闭右开:[666, 6, 7]
subList.remove(0); // 子范围视图修改,也会影响到原有集合(原集合 5 被删掉了)
System.out.println(subList); // [6, 7]
System.out.println(list); // [0, ..., 4, 6, 7, ..., 14] ”5被删了“

list.remove(0); // 修改原集合,结构性修改
// System.out.println(subList); // error。抛出异常 ConcurrentModificationException,需要重新获取 subList

list.set(5, new Student(11, "")); // 对于list来说,set 不是 结构性修改
System.out.println(subList); // OK。但是,子范围视图也会跟着改变,subList = [666, 6, 7]

6.2 不可修改的视图

  • Collections 类中以 unmodifiable 开头的 8 个静态方法,会生成集合的不可修改视图(unmodifiable view)
  • 不可修改视图如果被修改,将会抛出异常 java.lang.UnsupportedOperationException,原集合仍保持不变

注意:

  • 只是不能对“不可修改视图”调用集合的增/删/改方法,但是仍然可以通过集合的原始引用对集合进行修改。
    • 根据原集合引用修改集合后,unmodifiableList 也会跟着改变
// 不可修改的视图
List<Integer> unmodifiableList =  Collections.unmodifiableList(list); // list:[0, 1, ..., 5]
System.out.println(unmodifiableList);
// unmodifiableList.add(12); // error。异常,UnsupportedOperationException。不可修改视图不能修改
list.add(888); // ok。通过集合原始引用仍然可以修改
System.out.println(list);  // list:[0, 1, ..., 5, 888]
System.out.println(unmodifiableList);  // unmodifiableList:[0, 1, ..., 5, 888],跟着改变

6.3 同步视图

  • Collections 类中 synchronized 开头的静态方法,会将非线程安全的集合转换为线程安全
    • 将 List 转换为线程安全:static <T> List<T> synchronizedList(List<T> list)
    • 将 map 转换为线程安全:static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)

6.4 检查型视图

  • 作用:检查型视图用来对泛型类型可能出现的问题提供调试支持。
ArrayList<String> strings = new ArrayList<String>();
strings.add("abc");
ArrayList rawList = strings; // 没有使用泛型,默认可以为 Object 类型
rawList.add(new Date()); // 此时,strings 中包含 Date 对象了。如果执行 strings 的 get 方法,将会出错
		
// 检查型视图
List<String> safeStrings = Collections.checkedList(strings, String.class); // 此时,只能接受 String 类型
ArrayList rawList2 = (ArrayList) safeStrings;
rawList2.add(new Date()); // error。抛出异常,ClassCastException

7、Collections 工具类

7.1 排序

  • Collections.sort(List list)
  • Collections.sort(List list, Compataor cmp)
    • 如果需要对自定义类型排序,则需要给比较规则:
      • 1)方法1:自定义类型实现 Comparable 接口;
      • 2)方法2:实现一个比较器。

HashSet 可以利用 ArrayList 构造器转换为 list

Set<String> set = new HashSet<>();set.add("aa");set.add("b");ArrayList list = new ArrayList(set); // 将 set 转化为 listCollections.sort(list);
  • 对二维数组排序

    // 对二维数组排序,需要执行比较规则int[][] people = new int[100][2];Arrays.sort(people, new Comparator<int[]>() {	@Override	public int compare(int[] o1, int[] o2) {		if (o1[0] != o2[0]) {			return o2[0] - o1[0]; // 按照第一维降序排序		}		return o1[1] - o2[1]; // 若第一维相同,再按照第二维升序排序	}});
    

Java 中数组进 sort 采用的是优化后的快排;

而是用 sort 对链表进行排序时,是先将链表复制到一个数组中,是用 sort 对数组进行排序,然后再将排序后的序列复制到链表中。

  • 也可以使用 List 接口中的 default void sort(Comparator<? super E> c) 方法进行排序

    list.sort((o1, o2) -> o2 - o1); // 
    

7.2 集合和数组的转换

  • 数组 to 集合:
    • List.of(arr)
    • 但是,这个方法 Java 9 之后才能用
  • 集合 to 数组:
    • 不能直接使用 toArray() 方法,这样返回的是 Object[] 数组;
    • 使用 toArray() 方法的变体,toArray(new ElementType[length]);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值