【JavaSE】13-集合

十三、 集合

13.1 Java集合框架概述

1.集合是什么

集合是数据的容器,是数组的延申,简称 Java 容器。

说明:此时的存储主要指的是内存层面的存储,不涉及到持久化的硬盘存储。

2.数组的特点

在介绍集合之前,先回顾一下数组的特点:

① 首先,数组一旦初始化以后,其长度就确定了。

② 数组一旦定义好,其元素的类型也就确定了。如String[] 、int[]。

3.数组的缺点

① 一旦初始化以后,其长度就不可修改了。

② 数组中提纲的方法非常有限,对于添加、删除、插入数据等操作需要自己手写实现方法,非常不便。

③ 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用。

④ 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。

5.集合体系

Java 集合可分为 Collection 和 Map 两者体系,他们都是接口。

  • Collection接口下面没有提供实现类;而Map 提供了实现类。
  • Collection接口:单列集合,用来存储一个一个的对象。
  • Map接口:双列集合,用来存储一对 (key-value) 键值对数据。类似于函数 y = f ( x ) y=f(x) y=f(x) 的映射关系。

集合框架如下图所示:

Java集合
Collection接口
Map接口
List接口: 存储有序的, 可重复的数据, 也称为动态数组
ArrayList
LinkedList
Vector
Set接口: 存储无序的, 不可重复的数据
HashSet
LinkedHashSet
TreeSet
HashMap
LinkedHashMap
TreeMap
HashTable
Properties

13.2 Collection接口方法

方法描述
add(Object obj)添加一个元素
addAll(Collection coll)添加一个集合的全部元素
int size()获取有效元素的个数
void clear()清空集合元素
boolean isEmpty()判断是否空集合
boolean contains(Object obj)是否包含某个元素。通过元素的equals方法来判断是否是同一个对象
boolean containsAll(Collection coll)是否包含某个集合中的全部元素。也是调用元素的equals方法来比较,拿两个集合的元素挨个比较
boolean remove(Object obj)删除:通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
boolean removeAll(Collection coll)删除:取当前集合的差集
boolean retainAll(Collection coll)取两个集合的交集:把交集的结果存在当前集合中,不影响coll
boolean equals(Object obj)判断集合是否相等
Object[] toArray()转换成对象数组
hashCode()获取集合对象的哈希值
iterator()遍历:返回迭代器对象,用于集合遍历

1.add()、addAll()、size()、clear()、isEmpty()

@Test
public void test1() {
    Collection coll = new ArrayList();

    //add(Object obj):添加一个元素obj到集合中
    coll.add("AA");
    coll.add("BB");
    coll.add(123);//自动装箱,Integer包装类
    coll.add(new Date());

    //size():获取有效元素的个数
    System.out.println(coll.size());

    //addAll(Collection coll):添加一个集合的全部元素
    Collection coll1 = new ArrayList();
    coll1.add(456);
    coll1.add("CC");
    coll.addAll(coll1);

    System.out.println(coll.size());
    System.out.println(coll);

    //clear():清空集合元素
    coll.clear();

    //isEmpty():判断是否空集合
    System.out.println(coll.isEmpty());

输出:

4
6
[AA, BB, 123, Sat Apr 02 15:01:38 CST 2022, 456, CC]
true

2.判断是否包含元素contains()

使用要点:

  • 向 Collection 接口的实现类的对象中添加数据 object 时,要求 object 所在类要重写 equals() 方法。

写 contains() 测试类前,先定义了一个 Person 类:

public class Person {
    //属性
    private String name;
    private int age;

    //构造器
    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    //get、set方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    //重写toString方法
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试:

@Test
public void test1() {
    //创建集合1
    Collection coll = new ArrayList();
    coll.add(123);//Integer
    coll.add(new String("谢斯航"));
    coll.add(false);//Boolean包装类
    coll.add(new Person("刘清溪", 23));


    //判断是否包含元素
    boolean isContains = coll.contains(123);
    System.out.println(isContains);
    System.out.println(coll.contains(new String("谢斯航")));//true
    System.out.println(coll.contains(new Person("刘清溪", 23)));//false
}

输出:

true
true
false

刚开始我以为第二个判断 coll.contains(new String("谢斯航")) 的结果会是 false,因为我以为 contains 是比较两个对象的地址值。但事实是 contains() 方法会调用对象的 equals() 方法来比较内容是否相等,而 String 类是重写过 equals() 方法的,但是我自定义的类 Person 没有重写 equals() 方法,因此调用 contains() 的结果是 false。

接下来我在 Person 类中重写 equals() 方法,并在其中插入 System.out.println("Person equals..."); 来查看判断次数:

public class Person {
    //属性
    private String name;
    private int age;

    //构造器
    public Person() {
    }

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

    //get、set方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    //重写toString方法
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    
    //重写equals方法
    @Override
    public boolean equals(Object o) {
        System.out.println("Person equals...");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

//    @Override
//    public int hashCode() {
//        return Objects.hash(name, age);
//    }
}

再进行 contains() 方法的测试:

@Test
public void test1() {
    //创建集合1
    Collection coll = new ArrayList();
    coll.add(123);//Integer
    coll.add(new Person("刘清溪", 23));
    coll.add(new String("谢斯航"));
    coll.add(false);//Boolean包装类
    
    //判断是否包含元素
    boolean isContains = coll.contains(123);
    System.out.println(isContains);
    System.out.println(coll.contains(new String("谢斯航")));//true
    System.out.println(coll.contains(new Person("刘清溪", 23)));//true
}

输出:

true
true
Person equals...
Person equals...
true

可以看到,当自定义 Person 类重写了 equals() 方法后,再进行 contains() 方法判断结果为 true。且会逐一在集合里调用元素对象的 equals 方法,把 contains() 括号里的对象作为 equals 方法里的形参。当找到了匹配的对象时,就会立即结束。不再继续比较了。

containsAll() 方法测试:

@Test
    public void test1() {
        //创建集合1
        Collection coll = new ArrayList();
        coll.add(123);//Integer
        coll.add(new Person("刘清溪", 23));
        coll.add(new String("谢斯航"));
        coll.add(false);//Boolean包装类

        //containsAll()判断是否包含另一个集合的所有元素
        Collection coll2 = Arrays.asList("谢斯航", 123);
        System.out.println(coll.containsAll(coll2));
    }

输出:

true

这里用了一个新的创建集合的可变形参的方法:Arrays.asList("谢斯航", 123)

3.删除remove()

删除也是通过对象的 equals 方法来匹配删除对象,故要求自定义的类必须重写 equals 方法才能顺利进行删除操作。返回的是 boolean,若删除成功则为 true,否则为 false。

@Test
public void test2() {
    //创建集合1
    Collection coll = new ArrayList();
    coll.add(123);//Integer
    coll.add(new Person("刘清溪", 23));
    coll.add(new String("谢斯航"));
    coll.add(false);//Boolean包装类
    System.out.println("删除前:" + coll);

    //删除操作
    coll.remove(new Person("刘清溪", 23));
    System.out.println("删除后:" + coll);
}

输出:

删除前:[123, Person{name='刘清溪', age=23}, 谢斯航, false]
删除后:[123, 谢斯航, false]

removeAll() 方法测试:

@Test
    public void test2() {
        //创建集合1
        Collection coll = new ArrayList();
        coll.add(123);//Integer
        coll.add(new Person("刘清溪", 23));
        coll.add(new String("谢斯航"));
        coll.add(false);//Boolean包装类
        System.out.println("删除前:" + coll);

   		//removeAll求两个集合的差集
        Collection coll2 = Arrays.asList(1239, "谢斯航");
        coll.removeAll(coll2);
        System.out.println("删除后:" + coll);
    }

输出:

删除前:[123, Person{name='刘清溪', age=23}, 谢斯航, false]
删除后:[123, Person{name='刘清溪', age=23}, false]

removeAll() 相当于是求两个集合的差集。

4.交集retainAll()

@Test
public void test3() {
    //创建集合1
    Collection coll = new ArrayList();
    coll.add(123);//Integer
    coll.add(new Person("刘清溪", 23));
    coll.add(new String("谢斯航"));
    coll.add(false);//Boolean包装类
    System.out.println("交集前:" + coll);

    //交集retain
    Collection coll2 = Arrays.asList(false, new Person("刘清溪", 23), 456);
    coll.retainAll(coll2);
    System.out.println("交集后:" + coll);
}

输出:

交集前:[123, Person{name='刘清溪', age=23}, 谢斯航, false]
交集后:[Person{name='刘清溪', age=23}, false]

retainAll() 方法就是把交集存到原来的 coll 集合中了,没有重新返回一个新的集合。把原来的 coll 修改了。

5.判断是否相等equals()

要想返回 true,形参里也必须是集合。如果是有序集合,要求两个集合元素一致且排序一致才返回 true;如果是无序集合,要求两个集合元素一致即可返回 true。

@Test
    public void test3() {
        //创建集合1
        Collection coll = new ArrayList();
        coll.add(123);//Integer
        coll.add(new Person("刘清溪", 23));
        coll.add(new String("谢斯航"));
        coll.add(false);//Boolean包装类
        System.out.println("coll:" + coll);
        
        //创建集合2
        Collection coll2 = new ArrayList();
        coll2.add(new Person("刘清溪", 23));
        coll2.add(123);//Integer
        coll2.add(new String("谢斯航"));
        coll2.add(false);//Boolean包装类
        System.out.println("coll2:" + coll2);

        System.out.println(coll.equals(coll2));
    }

输出:

coll:[123, Person{name='刘清溪', age=23}, 谢斯航, false]
coll2:[Person{name='刘清溪', age=23}, 123, 谢斯航, false]
false

可以看到,由于两个集合都是 ArrayList 有序集合,coll2只是元素位置变动了一个,就造成了 equals 方法判断为 false。

6.获得哈希值hashCode()

@Test
public void test4() {
    //创建集合1
    Collection coll = new ArrayList();
    coll.add(123);//Integer
    coll.add(new Person("刘清溪", 23));
    coll.add(new String("谢斯航"));
    coll.add(false);//Boolean包装类

    //获取哈希值hashCode
    System.out.println(coll.hashCode());
}

输出:

695205073

Q:哈希值是什么?怎么计算得来的?有什么用处?

我发现,对同一个集合来说,无论运行多少次hashCode() 方法,得到的哈希值总是同一个。因此哈希值肯定是有固定的计算方法的。

7.转换为数组toArray()

//集合-->数组toArray()
Object[] arr = coll.toArray();
for (int i = 0; i < arr.length; i++) {
    System.out.print(arr[i] + "\t");
    System.out.println(arr[i].getClass());
}

输出:

123							  class java.lang.Integer
Person{name='刘清溪', age=23}	class day02.Person
谢斯航							class java.lang.String
false						  class java.lang.Boolean

8.数组–>集合Array.asList()

//数组-->集合Array.asList()
List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
System.out.println(list);
System.out.println(list.size());

//小心!下面两个的比较
List<Integer> list1 = Arrays.asList(new Integer[]{123, 456});
System.out.println(list1);
System.out.println(list1.size());

List<int[]> list2 = Arrays.asList(new int[]{123, 456});//把int[]数组整体当成一个元素了
System.out.println(list2);
System.out.println(list2.size());

输出:

[AA, BB, CC]
3
[123, 456]
2
[[I@2b05039f]
1

可以看到,把int[]数组整体当成一个元素了,要想当成是两个元素,要么直接在 Arrays.asList() 的括号里面写,要么用 Integer[] 数组。

13.3 Iterator迭代器接口

Iterator接口返回 Iterator 接口的实例,用于遍历集合元素。

13.3.1 Iterator的使用

@Test
    public void test1() {
        //创建集合1
        Collection coll = new ArrayList();
        coll.add(123);//Integer
        coll.add(new Person("刘清溪", 23));
        coll.add(new String("谢斯航"));
        coll.add(false);//Boolean包装类

        //Iterator创建
        Iterator iterator = coll.iterator();

        //方式一:不推荐
//        System.out.println(iterator.next());
//        System.out.println(iterator.next());
//        System.out.println(iterator.next());
//        System.out.println(iterator.next());

        //方式二:不推荐
//        for (int i = 0; i < coll.size(); i++) {
//            System.out.println(iterator.next());
//        }

        //方式三:推荐
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

输出:

123
Person{name='刘清溪', age=23}
谢斯航
false

13.3.2 Iterator执行原理

hasNext() 和 Next() 方法的执行原理:

image-20220405094633571

① iterator 生成一个指针,该指针在集合第一元素的上方。

② hasNext 判断指针下方是否有元素,若有返回 true,进入 while 循环体;

③ next 方法把指针下移一位,并将下移以后集合位置上的元素返回;

④ hasNext 判断指针下方是否有元素,若没有,返回 false,退出循环体。

13.4 Collection子接口一:List

Collection的子接口之一 List 接口下有 3 个实现类:ArrayList、LinkedList 和 Vector。它们都是有序的、可重复的集合。

13.4.1 ArrayList源码分析

13.4.2 LinkedList源码分析

LinkedList 作为 Collection接口的其中一个子接口 List 的实现类,其实现的也是有序、可重复的集合容器。但与 ArrayList 类不同的是:LinkedList 类底层数据结构用的是双向链表 (以下简称“链表”)。

1.双向链表

链表中的每一个元素都包含 3 部分,开头、中间、和结尾部分。源码中,创建了一个内部类 Node 类来表示链表数据结构,如下代码所示。分别声明了 prevelementnext 来对应链表中每个元素的开头、中间和结尾 3 个部分:

  • prev 存放的是指向前一个元素的地址;
  • element 存放的是数据;
  • next 存放的是指向后一个元素的地址。

这样,就在物理上内存空间就不是连续的,可以方便地删除和插入数据。

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;
    }
}

2.add(Object obj) 操作源码

public boolean add(E e) {
    linkLast(e);
    return true;
}

点进 linkLast(e) 方法中看一看:

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

第 5 行代码:定义了一个 Node 类的常量 l,赋值为 last 。也就是把当前链表的最后一个 Node 节点的地址值赋值给常量 l。新增节点后,l 将作为倒数第二个节点。

第 6 行代码:定义了一个 Node 类的常量 newNode,这个就是用来存放要添加的新数据 e 的节点。 newNode 的开头 prev 赋值是 l (即元素 last 的地址值),代表 newNode 的上一个元素是 last 。中间 element 赋值新数据 e,结尾 next 赋值为 null ,因为已经是最后一个元素了,所以没有下一个元素的地址。

第 7 行代码:因为 last 是 LinkedList 类的一个属性,代表该链表的最后一个节点。添加新节点后,要把 last 更改为 newNode 的地址值。

第 8~11 行代码:修改倒数第二个节点 lnext 地址值(它本来是 null),指向新增节点 newNode。这里的 if-else 主要考虑的是 l 为空的情况,即一个没有元素的链表的情况时,新增的节点就是该链表的第一个节点 first ,此时 newNode 节点既是第一个节点 first,也是最后一个节点 last (因为整个链表只有这一个节点)。

第 12~13 行代码:元素个数 size 加一、modCount 加一。

13.4.3 Vector源码分析

Vector 作为“前朝老臣”,自然不受官方待见。就算是 JDK 8 以后也没有得到像 ArrayList 懒汉式容量初始化的优化,而是一开始就初始化为容量为 10 的数组。如下代码所示:

public Vector() {
    this(10);
}

1.add(Object obj) 操作源码

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

可以看到 Vector 类的所有 public 方法都是 synchronized 线程安全的,意味着在多线程效率方面是不如 ArrayList 类的。elementCount 是 Vector 类里面的元素的总数,如果要新增元素的话,元素总数将会变成 elementCount + 1 个,因此要确认 Vector 数组容量是否足够。这个功能由第 3 行的 ensureCapacityHelper() 方法实现。

点进 ensureCapacityHelper(elemrntCount + 1) 方法看看:

private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

elementData 是存放 Vector 的数据的 Object[] 数组。第 3 行是说:如果新增之后的元素总数 - 当前 elementData 数组的长度>0的话,数组就需要扩容了。扩容功能由 grow() 方法实现。

再点进 grow(minCapacity) 方法看看怎么扩容的:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

第 3 行代码:定义老容量 oldCapacity,赋值为 elementData.length ,即当前数组长度。

第 4~5 行代码:定义新容量 newCapacity。如果属性 capacityIncrement > 0,那么新容量就等于旧容量加上 capacityIncrement ;否则,新容量就等于旧容量加上 oldCapacity ,即新容量 newCapacity 是原来旧容量的 2 倍。比 ArrayList 扩容大一些,ArrayList 只扩容 1.5 倍。

第 6~7 行代码:如果新容量仍然 < 新增后元素总数 minCapacity ,那么新容量直接等于新增后元素总数 minCapacity

第 8~9 行代码:如果新容量 newCapacity > 常量数组最大长度 MAX_ARRAY_SIZE,那么新容量 newCapacity 直接等于虚拟机限定的最大整数了。

第 10 行代码:把 elementData 数组中的数据复制到容量为 newCapacity 的新数组中。扩容操作完成。

2.关于线程安全问题

可能有人会说,ArrayList 和 LinkedList 都是线程不安全的,那如果我们涉及到多线程的安全问题时,是否会用 Vector 呢?

答案是:不会。就是这么无情,如下图所示:Collections.class 工具类下提供了 synchronizedList() 方法,返回一个线程安全的 List 接口的实现类。有了这个方法,就可以彻底和 Vector 说再见了。

image-20220406084515759

13.4.4 List接口常用方法

由于 List 是 Collection 的子接口,因此 Collection 中的常用方法在 List 中也能用。但由于 List 是有序的、可重复的集合,因此会有一些 Collection 中没有的特有的,带有索引的常用方法。下面介绍 List 中特有的常用方法。

方法作用
void add(int index, Object element)在 index 位置插入 element 元素
boolean addAll(int index, Collection elements)从 index 位置开始插入 elements 中的所有元素
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 element)设置指定 index 位置的元素为 element
List subList(int fromIndex, int toIndex)返回从 fromIndex 到 toIndex 位置的左闭右开子集合

1.插入

① add(int index, Object element)

@Test
public void test1() {
    //创建一个ArrayList
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(456);
    list.add("AA");
    list.add(new Person("Tom", 23));
    list.add(456);
    System.out.println("原始:" + list);

    //插入add
    list.add(1, "BB");
    System.out.println("插入后:" + list);
}

输出:

原始:[123, 456, AA, Person{name='Tom', age=23}, 456]
插入后:[123, BB, 456, AA, Person{name='Tom', age=23}, 456]

ArrayList 底层是数组,插入操作需要把索引位置的元素开始一个一个往后移,空出来的索引位置再插入新元素。

②boolean addAll(int index, Collection elements)

@Test
    public void test1() {
        //创建一个ArrayList
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 23));
        list.add(456);
        System.out.println("原始:" + list);
        
        //插入addAll
        List list1 = Arrays.asList(1, 2, 3);
        list.addAll(2, list1);
        System.out.println("插入后:" + list);
        System.out.println("元素个数:" + list.size());
    }

输出:

原始:[123, 456, AA, Person{name='Tom', age=23}, 456]
插入后:[123, 456, 1, 2, 3, AA, Person{name='Tom', age=23}, 456]
元素个数:8

这里需要注意一个点:如果你手抖把 addAll() 打成 add() 的话,就会把整个 list1 当成一个元素插入到 list 中,这样 list 中的元素个数就是 6 个了,如下代码所示:

@Test
    public void test1() {
        //创建一个ArrayList
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 23));
        list.add(456);
        System.out.println("原始:" + list);
        
        //插入addAll手抖打成add的后果
        List list1 = Arrays.asList(1, 2, 3);
        list.add(2, list1);
        System.out.println("插入后:" + list);
        System.out.println("元素个数:" + list.size());
    }

输出:

原始:[123, 456, AA, Person{name='Tom', age=23}, 456]
插入后:[123, 456, [1, 2, 3], AA, Person{name='Tom', age=23}, 456]
元素个数:6

2.获取元素get

@Test
    public void test1() {
        //创建一个ArrayList
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 23));
        list.add(456);
        System.out.println("原始:" + list);
        
        //获取元素get
        System.out.println(list.get(0));
    }

输出:

123

3.查找索引

① indexOf(Object obj)

@Test
    public void test1() {
        //创建一个ArrayList
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 23));
        list.add(456);
        System.out.println("原始:" + list);
        
        //获取元素位置indexOf
        System.out.println(list.indexOf("AA"));
        System.out.println(list.indexOf("AB"));
    }

输出:

2
-1

如果没有找到该元素,则返回 -1。

② lastIndexOf(Object obj)

@Test
public void test2() {
    //创建一个ArrayList
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(456);
    list.add("AA");
    list.add(new Person("Tom", 23));
    list.add(456);
    System.out.println("原始:" + list);

    //末次索引lastIndexOf
    int index = list.lastIndexOf(456);
    System.out.println(index);
}

输出:

原始:[123, 456, AA, Person{name='Tom', age=23}, 456]
4

4.删除

List 中的删除和 Collection 中的删除方法不同,List 是按索引删除元素,并返回被删除的元素;而 Collection 是直接按元素删除。

@Test
public void test2() {
    //创建一个ArrayList
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(456);
    list.add("AA");
    list.add(new Person("Tom", 23));
    list.add(456);
    System.out.println("原始:" + list);
    
    //删除remove
    Object o = list.remove(1);
    System.out.println("删除后:" + list);
    System.out.println("被删除的元素:" + o);
}

输出:

原始:[123, 456, AA, Person{name='Tom', age=23}, 456]
删除后:[123, AA, Person{name='Tom', age=23}, 456]
被删除的元素:456

ArrayList 底层是数组,删除操作需要把被删除的元素位置开始一个一个往前移,原来最后位置的元素赋值为 null。

5.修改set

修改完后,会返回被修改掉的元素。

@Test
    public void test2() {
        //创建一个ArrayList
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 23));
        list.add(456);
        System.out.println("原始:" + list);

        //修改set
        Object o = list.set(1, "CC");
        System.out.println("修改后:" + list);
        System.out.println("被修改的元素:" + o);
    }

输出:

原始:[123, 456, AA, Person{name='Tom', age=23}, 456]
修改后:[123, CC, AA, Person{name='Tom', age=23}, 456]
被修改的元素:456

6.子集合subList

会返回一个新的子集合,原有的集合不会受到影响。

@Test
    public void test2() {
        //创建一个ArrayList
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 23));
        list.add(456);
        System.out.println("原始:" + list);

        //获取子集合subList
        List subList = list.subList(1, 4);
        System.out.println("子集合:" + subList);
    }

输出:

原始:[123, 456, AA, Person{name='Tom', age=23}, 456]
子集合:[456, AA, Person{name='Tom', age=23}]

7.遍历

对 List 来说,遍历有 3 种方法:① Iterator迭代器、② 增强 for 循环和③ 普通循环。

@Test
    public void test2() {
        //创建一个ArrayList
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 23));
        list.add(456);
        System.out.println("原始:" + list);

        //遍历方式一: Iterator迭代器
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        //遍历方式二: 增强for循环
        for (Object obj : list) {
            System.out.println(obj);
        }

        //遍历方式三: 普通循环
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

输出:

原始:[123, 456, AA, Person{name='Tom', age=23}, 456]
123
456
AA
Person{name='Tom', age=23}
456

13.4.5 List面试题

注意区分 List 中方法的形参到底是索引,还是Object obj。即区分是 List 中的方法还是 Collection 中的方法。

@Test
    public void testListRemove() {
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        updateList(list);
        System.out.println(list);//
    }

    private void updateList(List list) {
        list.remove(2);
//        list.remove(new Integer(2));
    }

分析

第 3~6 行代码:创建一个 List ,包含元素 [1, 2, 3]。

第 7 行代码:私有方法 updateList() 。转到第 12 行看方法内的代码。

第 12 行代码:删除 list 中索引为 2 的元素,变为 [1, 2]。

第 8 行代码:输出 list ,结果为 [1, 2]。

[1, 2]

修改 12 行代码为第 13 行

第 13 行代码:调用了 Collecting 中的 remove(Object obj) 方法来删除元素。该方法是通过元素的 equals 方法判断是否是要删除的那个元素,因此输出结果是 [1, 3]。

[1, 3]

13.5 Collection子接口二:Set

Set 接口也是 Collection 接口的子接口,实现的是无序的、不可重复的集合。

注意:Set 接口没有额外定义新的方法,使用的都是 Collection 中声明过的方法。

13.5.1 Set 接口实现类对比

Set 有 3 个实现类:HashSet、LinkedHashSet 和 TreeSet。

  • HashSet:Set 接口的主要实现类,线程不安全的,可以存储 null 值。没有特殊需要主要就是用 HashSet。
  • LinkedSet:是 HashSet 的子类,在 HashSet 的基础上加了前后的指针,让它看上去像有序的一样,但其实是假的。遍历其内部数据时,可以按照添加的顺序遍历。
  • TreeSet:底层存储数据结构是二叉树 (准确地说是红黑树)。要求放入 TreeSet 中的数据必须都是同一类的对象。然后可以按照这些对象的某些属性进行排序。

13.5.2 Set 无序性与不可重复的理解

这一块的源码就不都看了,实际开发中用 Set 的比较少,用 List 和 Map 比较多。而 Set 是和后面的 Map 是有关联的,等到后面讲 Map 的时候会着重讲 Map 的源码。相当于都看了。

1.无序性

① 无序性 ≠ 随机性;存储的数据在底层数组中并非按照数组索引顺序依次添加,而是根据数据的**哈希值**决定添加的位置。

@Test
public void test1() {
    //创建一个新HashSet
    Set set = new HashSet();
    set.add(456);
    set.add(123);
    set.add("AA");
    set.add("CC");
    set.add(new Person("Tom", 23));
    set.add(129);

    //用迭代器Iterator遍历HashSet集合
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

输出:

AA
CC
Person{name='Tom', age=23}
129
456
123

可以看到,遍历 HashSet 元素的顺序和 add() 方法添加顺序是不一致的。倒也不是说遍历输出顺序与 add() 方法添加顺序一致就是有序性,比如说 LinkedHashSet 的遍历顺序就是与添加顺序一致,但它仍然是无序的,只是通过链表结构让它看起来是有序的而已。我们运行第二次看看:

AA
CC
Person{name='Tom', age=23}
129
456
123

就算运行 n 次,输出的顺序都不会变。这说明,Set 接口的无序性并不是随机性。而是 Hash 计算方法没变,因此每次执行的结果都是一样的。

2.不可重复性

① 保证添加的元素按照 equals() 方法判断时,不能返回 true。即,相同的元素只能添加一个。

新定义一个 User 类,和 Person 类一样,但没有重写 equals 方法:

public class User {
    //属性
    private String name;
    private int age;

    //空参构造器
    public User() {
    }

    //带参构造器
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //get、set方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    //重写toString方法
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    
    //注意!没有重写 equals 方法!
}

测试:

@Test
public void test1() {
	//创建一个新HashSet
    Set set = new HashSet();
    set.add(456);
    set.add(123);
    set.add(123);
    set.add("AA");
    set.add("CC");
    set.add(new User("Tom", 23));
    set.add(new User("Tom", 23));
    set.add(129);

    //用迭代器Iterator遍历HashSet集合
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
    	System.out.println(iterator.next());
    }
}

输出:

AA
CC
User{name='Tom', age=23}
129
456
User{name='Tom', age=23}
123

如上代码所示,我添加了 2 个 123 数据,但是输出只有 1 个 123。但是,我明明 new 了两个一模一样的 User 对象,但 HashSet 却给我原封不动地输出了。其实,这并不是意味着它是可重复的,恰恰相反,那是你不熟悉对象的底层。Set 的不可重复性是地址值不重复,而 2 个 new 的对象,即使属性相同,在栈中的是两个不同的地址值,因为并没有违反 Set 的不可重复性。出现这就是 Set 接口集合不可重复的体现。

这时候我想:有没有一种可能,是自定义类需要重写 equals 方法才可以避免出现属性相同的对象呢?下面我们仅仅重写 User 类中的equals() 方法看看:

public class User {
    //属性
    private String name;
    private int age;

    //空参构造器
    public User() {
    }

    //带参构造器
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //get、set方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    //重写toString方法
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //重写equals()方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age && Objects.equals(name, user.name);
    }
}

测试:

	@Test
    public void test1() {
        //创建一个新HashSet
        Set set = new HashSet();
        set.add(456);
        set.add(123);
        set.add(123);
        set.add("AA");
        set.add("CC");
        set.add(new User("Tom", 23));
        set.add(new User("Tom", 23));
        set.add(129);

        //用迭代器Iterator遍历HashSet集合
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

输出:

AA
CC
User{name='Tom', age=23}
129
456
User{name='Tom', age=23}
123

可以看到,光重写 equals() 方法是不能避免出现属性相同的对象。还要重写 hashCode() 方法,如下代码所示:

public class User {
    //属性
    private String name;
    private int age;

    //空参构造器
    public User() {
    }

    //带参构造器
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //get、set方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    //重写toString方法
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //重写equals()方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }

    //重写hashCode()方法
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}

测试:

@Test
public void test1() {
    //创建一个新HashSet
    Set set = new HashSet();
    set.add(456);
    set.add(123);
    set.add(123);
    set.add("AA");
    set.add("CC");
    set.add(new User("Tom", 23));
    set.add(new User("Tom", 23));
    set.add(129);

    //用迭代器Iterator遍历HashSet集合
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

输出:

AA
CC
User{name='Tom', age=23}
129
456
123

可以看到,重写 hashCode() 方法后,成功避免了相同属性的对象重复的情况。

Q:那问题来了,为什么会这样呢?究其原因,一切都离不开哈希值。

13.5.3 HashSet 中元素的添加过程

HashSet 的底层是由数组 + 链表组成的。主体是数组,链表像铁链子一样从数组上垂下来 (JDK 8)。

和 ArrayList 一样,在 JDK 7 中,HashSet 的数组长度一开始就定义好了,为 16;而在 JDK 8 中,HashSet 的数组是等到有添加操作才开始创建数组,长度为 16。

image-20220406163248508

HashSet 的添加过程就是分 3 步走:

① 添加元素,计算该元素哈希值。通过调用该元素所在类的 hashCode() 方法算的,我们定义,当对象的属性一样时,所算出来的哈希值是一样的;

② 根据该哈希值,通过一套固定的、复杂的算法 (散列函数) 得出该元素在数组的存放位置;

③ 先看看数组中该位置中有没有已经存放数组,如果没有,就放入数组;

④ 如果数组中要放的位置上已经存在数据了,就对比两个元素的哈希值,如果哈希值不同,就按【七上八下:JDK 7是新的元素指向旧元素;JDK 8 是旧元素指向新元素】地用链表存放;(注:存放位置一样,不代表哈希值一样,只是根据算法算出来的位置一样罢了) ,如果该元素已经有链表了,则依次遍历链表元素去比较,首先比哈希值,再调用 equals() 方法比较,确定两个元素不相同后,挂在链表最后面。

⑤ 如果哈希值相同,就调用要添加元素所在类的 equals() 方法比较,如果为 false,则按【七上八下】地用链表存放;如果为 true,则表示已经存放相同数据,该数据丢弃。

2.这种添加方法的好处

避免了每添加一个元素都要和前面已添加的所有元素 equals() 方法比较造成的效率低下。提高了添加效率。

3.关于 hashCode() 方法的重写

先看 User 类中重写后的 hashCode() 方法:

//重写hashCode()方法
@Override
public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + age;
    return result;
}

第 4 行代码:定义了一个 int 型的 result 存放返回的哈希值。判断属性 name 是否不等于 null:如果 true,则调用 String 类的 hashCode() 方法算出属性 name 的哈希值;如果 false,则 result 为 0。

第 5 行代码:为降低不同属性算出哈希值相同的概率,把属性 name 算出的哈希值乘以膨胀系数 31 31 31,再加上属性 age

Q:为什么要乘 31 31 31

image-20220409093558345

第 6 行代码:把 result 作为哈希值的结果返回。

4.总结

① 向 Set 中添加的数据,其所在的类一定要重写 hashCode() 和 equals()。

② 重写的 hashCode() 和 equals() 方法尽可能地保持一致性。即:“相等的对象必须具有相等的散列码”。对象中用作 equals() 方法比较的s属性,都应该用来计算 hashCode 值。总之用 IDEA 自动生成的就完事儿了。

③ 虽然面试时不会问 HashSet,但讲这么多是因为 HashSet 底层是 HashMap,如下源码所示。而 HashMap 则是面试最爱问的。因此这一节是为了给理解 HashMap 打下基础。

public HashSet() {
    map = new HashMap<>();
}

13.5.4 LinkedHashSet的使用

1.例子

@Test
public void test2() {
    //创建一个新LinkedHashSet
    Set set = new LinkedHashSet();
    set.add(456);
    set.add(123);
    set.add(123);
    set.add("AA");
    set.add("CC");
    set.add(new User("Tom", 23));
    set.add(new User("Tom", 23));
    set.add(129);

    //用迭代器Iterator遍历LinkedHashSet集合
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

输出:

456
123
AA
CC
User{name='Tom', age=23}
129

可以看到,LinkedHashSet 的输出顺序与元素添加顺序一致。但并不是说其就是有序的,LinkedHashSet 仍然是无序的。原因如下:

2.LinkedHashSet 底层分析

LinkedHashSet 作为 HashSet 的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据的前一个数据和后一个数据。其实就是双向链表。

优点:对于频繁的遍历操作,LinkedHashSet 的效率高于 HashSet。

image-20220409095901391

13.5.5 TreeSet的自然排序

TreeSet的底层存储数据结构是二叉树 (准确地说是红黑树)。要求放入 TreeSet 中的数据必须都是同一类的对象。然后可以按照这些对象的某些属性进行排序。分为两种排序方式:自然排序 (实现 Comparable 接口) 和定制排序 (实现 Comparator 接口) 。

1.使用要求

  • 只能添加同一类的对象,不能添加不同类的对象。

2.说明

  • 自然排序中,比较两个对象是否相同的标准为:compareTo() 方法返回 0。不再是 equals() 方法。

  • 定制排序中,比较两个对象是否相同的标准为:compare() 方法返回 0。不再是 equals() 方法。

  • TreeSet 的底层是二叉树 (准确地说是红黑树)。一个节点,比该节点小的放左边,比该节点大的放右边,每个节点都如此。如下图所示:

image-20220409105743197

例子1:

@Test
public void test1() {
    //创建一个新TreeSet
    TreeSet set = new TreeSet();
    //int型
    set.add(34);
    set.add(-34);
    set.add(43);
    set.add(11);
    set.add(8);

    //用迭代器Iterator遍历TreeSet集合
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

输出:

-34
8
11
34
43

可以看到,TreeSet 的 iterator 输出是按从小到大的顺序来排序的。

例子2:

@Test
    public void test1() {
        //创建一个新TreeSet
        TreeSet set = new TreeSet();
        //String类
        set.add("EE");
        set.add("AA");
        set.add("DD");
        set.add("CC");
        set.add("BB");

        //用迭代器Iterator遍历TreeSet集合
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

输出:

AA
BB
CC
DD
EE

例子3:

	@Test
    public void test1() {
        //创建一个新TreeSet
        TreeSet set = new TreeSet();
        //User类
        set.add(new User("Tom", 21));
        set.add(new User("Jerry", 12));
        set.add(new User("Jim", 32));
        set.add(new User("Mike", 65));
        set.add(new User("Jack", 47));

        //用迭代器Iterator遍历TreeSet集合
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

输出:

java.lang.ClassCastException

报错了,因为 User 类中没有重写 compareTo() 方法。

首先,让 User 类实现 Comparable 接口,再重写 compareTo() 方法。

public class User implements Comparable{
    //属性
    private String name;
    private int age;

    //空参构造器
    public User() {
    }

    //带参构造器
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //get、set方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    //重写toString方法
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //重写equals()方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }

    //重写hashCode()方法
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

    //重写compareTo()方法:按照姓名从小到大排列
    @Override
    public int compareTo(Object o) {
        if (o instanceof User){
            User user = (User) o;
            return this.name.compareTo(user.name);
        }else {
            throw new RuntimeException("对不起,输入的类型不一致!");
        }
    }
}

测试:

	@Test
    public void test1() {
        //创建一个新TreeSet
        TreeSet set = new TreeSet();
        //User类
        set.add(new User("Tom", 21));
        set.add(new User("Jerry", 12));
        set.add(new User("Jim", 32));
        set.add(new User("Mike", 65));
        set.add(new User("Jack", 47));

        //用迭代器Iterator遍历TreeSet集合
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

输出:

User{name='Jack', age=47}
User{name='Jerry', age=12}
User{name='Jim', age=32}
User{name='Mike', age=65}
User{name='Tom', age=21}

可以看到元素按姓名从小到大输出,如果想按姓名从大到小输出,只需要在下面这里加负号 - 即可:

return -this.name.compareTo(user.name);

如果我在 TreeSet 中再添加一个元素 new User("Jack", 9)

	@Test
    public void test1() {
        //创建一个新TreeSet
        TreeSet set = new TreeSet();
        //User类
        set.add(new User("Tom", 21));
        set.add(new User("Jerry", 12));
        set.add(new User("Jim", 32));
        set.add(new User("Mike", 65));
        set.add(new User("Jack", 47));
        set.add(new User("Jack", 9));

        //用迭代器Iterator遍历TreeSet集合
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

输出:

User{name='Jack', age=47}
User{name='Jerry', age=12}
User{name='Jim', age=32}
User{name='Mike', age=65}
User{name='Tom', age=21}

发现只有 1 个 Jack。若想 2 个Jack都出现在排序中,则 User 类中必须考虑二级比较不妨先按姓名从小到大排序,再按年龄从小到大排序:

public class User implements Comparable {
    //属性
    private String name;
    private int age;
    
    ……(省略,详见上面完整代码)

    //重写compareTo()方法:先按姓名从小到大排序,再按年龄从小到大排序
    @Override
    public int compareTo(Object o) {
        if (o instanceof User) {
            User user = (User) o;
            int compare = this.name.compareTo(user.name);
            if (compare != 0) {
                return compare;
            } else {
                return Integer.compare(this.age, user.age);
            }
        } else {
            throw new RuntimeException("对不起,输入的类型不一致!");
        }
    }

测试:

	@Test
    public void test1() {
        //创建一个新TreeSet
        TreeSet set = new TreeSet();t.add("BB");
        //User类
        set.add(new User("Tom", 21));
        set.add(new User("Jerry", 12));
        set.add(new User("Jim", 32));
        set.add(new User("Mike", 65));
        set.add(new User("Jack", 47));
        set.add(new User("Jack", 9));

        //用迭代器Iterator遍历TreeSet集合
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

输出:

User{name='Jack', age=9}
User{name='Jack', age=47}
User{name='Jerry', age=12}
User{name='Jim', age=32}
User{name='Mike', age=65}
User{name='Tom', age=21}

可以看到,顺利把 2 个 Jack 按年龄从小到大输出。

13.5.6 TreeSet的定制排序

定制排序中,比较两个对象是否相同的标准为:compare() 方法返回 0。不再是 equals() 方法。

定制排序是通过实现 Comparator 接口的实现类,再把实现类的对象放入 TreeSet 的构造器中,以实现定制排序。

@Test
public void test2() {
    //创建Comparator接口的匿名实现类的对象:按年龄从小到大排序,重复年龄按先来后到除去
    Comparator comparator = new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            if (o1 instanceof User && o2 instanceof User) {
                User user1 = (User) o1;
                User user2 = (User) o2;
                return Integer.compare(user1.getAge(), user2.getAge());
            } else {
                throw new RuntimeException("对不起,输入的类型不一致!");
            }
        }
    };

    //创建一个新TreeSet: 在构造器中放入Comparator接口的匿名实现类的对象
    TreeSet set = new TreeSet(comparator);
    //User类
    set.add(new User("Tom", 21));
    set.add(new User("Jerry", 12));
    set.add(new User("Jim", 32));
    set.add(new User("Mary", 32));
    set.add(new User("Mike", 65));
    set.add(new User("Jack", 47));
    set.add(new User("Jack", 9));

    //用迭代器Iterator遍历TreeSet集合
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

输出:

User{name='Jack', age=9}
User{name='Jerry', age=12}
User{name='Tom', age=21}
User{name='Jim', age=32}
User{name='Jack', age=47}
User{name='Mike', age=65}

可以看到,User 的对象都按年龄从小到大排序,年龄相同的都被丢弃了 (如同为 32 岁的 Mary),年龄相同的情况下,谁在前面,谁就得到保留。若不想年龄相同的被去除,可以写为二阶比较,改为先按年龄从小到大排序,再按姓名从小到大排序:

@Test
public void test2() {
    //创建Comparator接口的匿名实现类的对象:先按年龄从小到大排序,再按姓名从小到大排序
    Comparator comparator = new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            if (o1 instanceof User && o2 instanceof User) {
                User user1 = (User) o1;
                User user2 = (User) o2;
                int comResult = Integer.compare(user1.getAge(), user2.getAge());
                if (comResult != 0) {
                    return comResult;
                }else {
                    //二阶比较
                    return user1.compareTo(user2);
                }
            } else {
                throw new RuntimeException("对不起,输入的类型不一致!");
            }
        }
    };

    //创建一个新TreeSet: 在构造器中放入Comparator接口的匿名实现类的对象
    TreeSet set = new TreeSet(comparator);
    //User类
    set.add(new User("Tom", 21));
    set.add(new User("Jerry", 12));
    set.add(new User("Jim", 32));
    set.add(new User("Mary", 32));
    set.add(new User("Mike", 65));
    set.add(new User("Jack", 47));
    set.add(new User("Jack", 9));

    //用迭代器Iterator遍历TreeSet集合
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

输出:

User{name='Jack', age=9}
User{name='Jerry', age=12}
User{name='Tom', age=21}
User{name='Jim', age=32}
User{name='Mary', age=32}
User{name='Jack', age=47}
User{name='Mike', age=65}

这样,就不会因为年龄一样而被丢弃,而是先按年龄从小到大排序,再按姓名从小到大排序。

13.5.7 练习题

1.TreeSet的自然排序与定制排序

题目:

① 定义一个Employee类。

  • 该类包含:private成员变量name,age,birthday,其中 birthday 为 MyDate 类的对象;
  • 并为每一个属性定义 getter, setter 方法;
  • 并重写 toString 方法输出 name, age, birthday;

② MyDate类包含:

  • private成员变量year,month,day;并为每一个属性定义 getter, setter 方法;

③ 创建该类的 5 个对象,并把这些对象放入 TreeSet 集合中(下一章:TreeSet 需使用泛型来定义)分别按以下两种方式对集合中的元素进行排序,并遍历输出:

  • 1). 使Employee 实现 Comparable 接口,并按 name 排序
  • 2). 创建 TreeSet 时传入 Comparator对象,按生日日期的先后排序。

我的首次答案:

① Employee 类:

public class Employee implements Comparable {
    //属性
    private String name;//姓名
    private int age;//年龄
    private MyDate birthday;//生日

    //构造器
    public Employee() {
    }

    public Employee(String name, int age, MyDate birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    //get、set方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public MyDate getBirthday() {
        return birthday;
    }

    public void setBirthday(MyDate birthday) {
        this.birthday = birthday;
    }

    //重写toString方法
    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                '}';
    }

    //重写compareTo方法,按name从小到大排序
    @Override
    public int compareTo(Object o) {
        if (o instanceof Employee) {
            Employee employee = (Employee) o;
            return this.name.compareTo(employee.name);
        } else {
            throw new RuntimeException("输入类型不一致");
        }

    }
}

② MyDate 类:

public class MyDate implements Comparable {
    //属性
    private int year;//年
    private int month;//月
    private int day;//日

    //构造器
    public MyDate() {
    }

    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    //get、set方法
    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }

    //重写toString方法
    @Override
    public String toString() {
        return "MyDate{" +
                "year=" + year +
                ", month=" + month +
                ", day=" + day +
                '}';
    }

    //重写compareTo方法
    @Override
    public int compareTo(Object o) {
        if (o instanceof MyDate) {
            MyDate date = (MyDate) o;
            //先判断年份是否相同
            int c = Integer.compare(this.year, date.year);
            if (c != 0) {//如果年份不同,直接返回c
                return c;
            } else {//如果年份相同,则比较月份是否相同
                int c2 = Integer.compare(this.month, date.month);
                if (c2 != 0) {//如果月份不同,直接返回c2
                    return c2;
                } else {//如果月份相同,直接返回日期的比较结果
                    return Integer.compare(this.day, date.day);
                }
            }
        }
        throw new RuntimeException("输入类型不一致");
    }
}

③ 测试:

public class TreeSetExer {
    public static void main(String[] args) {
        //创建5个Employee对象
        Employee employee1 = new Employee("AdaLove", 23, new MyDate(1998, 11, 4));
        Employee employee2 = new Employee("Tom", 25, new MyDate(1996, 5, 8));
        Employee employee3 = new Employee("Gary", 24, new MyDate(1998, 11, 15));
        Employee employee4 = new Employee("Jerry", 22, new MyDate(1999, 9, 16));
        Employee employee5 = new Employee("Mary", 21, new MyDate(2000, 7, 3));

        //创建TreeSet
        Set set = new TreeSet();
        //把上述对象放入TreeSet中
        set.add(employee1);
        set.add(employee2);
        set.add(employee3);
        set.add(employee4);
        set.add(employee5);

        System.out.println("******************自然排序:实现Comparable接口,按name从小到大排序******************");
        //1.自然排序:实现Comparable接口,按name从小到大排序
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        System.out.println("******************定制排序:创建TreeSet时传入Comparator对象,按生日日期的先后排序******************");
        //2.定制排序:创建TreeSet时传入Comparator对象,按生日日期的先后排序

        //创建Comparator接口匿名实现类的对象,按生日日期的先后排序
        Comparator comparator = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof Employee && o2 instanceof Employee) {
                    Employee e1 = (Employee) o1;
                    Employee e2 = (Employee) o2;
                    return e1.getBirthday().compareTo(e2.getBirthday());
                } else {
                    throw new RuntimeException("输入类型不一致");
                }
            }
        };

        //创建TreeSet,把上述comparator对象传入TreeSet的构造器中
        Set set2 = new TreeSet(comparator);
        //把上述对象放入TreeSet中
        set2.add(employee1);
        set2.add(employee2);
        set2.add(employee3);
        set2.add(employee4);
        set2.add(employee5);

        //遍历输出set2
        Iterator iterator1 = set2.iterator();
        while (iterator1.hasNext()) {
            System.out.println(iterator1.next());
        }
    }
}

输出:

******************自然排序:实现Comparable接口,按name从小到大排序******************
Employee{name='AdaLove', age=23, birthday=MyDate{year=1998, month=11, day=4}}
Employee{name='Gary', age=24, birthday=MyDate{year=1998, month=11, day=15}}
Employee{name='Jerry', age=22, birthday=MyDate{year=1999, month=9, day=16}}
Employee{name='Mary', age=21, birthday=MyDate{year=2000, month=7, day=3}}
Employee{name='Tom', age=25, birthday=MyDate{year=1996, month=5, day=8}}
******************定制排序:创建TreeSet时传入Comparator对象,按生日日期的先后排序******************
Employee{name='Tom', age=25, birthday=MyDate{year=1996, month=5, day=8}}
Employee{name='AdaLove', age=23, birthday=MyDate{year=1998, month=11, day=4}}
Employee{name='Gary', age=24, birthday=MyDate{year=1998, month=11, day=15}}
Employee{name='Jerry', age=22, birthday=MyDate{year=1999, month=9, day=16}}
Employee{name='Mary', age=21, birthday=MyDate{year=2000, month=7, day=3}}

总结:String 类已经重写了 compareTo() 方法,可以直接用;int 型用 Integer.compare() 方法来实现比较;double、char 型等以此类推。如果有特殊需求的,用 if-else 来比较,按小于、等于、大于来返回 -1、0、1 即可。

2.在List内去除重复数字值,要求尽量简单

通常会把 List 放入 HashSet 中来去除重复数据,简单又高效。

public class test {
    //定义duplicateList方法

    /**
     * @param list:传入的待排序的List
     * @return {{@link List}}
     * @Author: Sihang Xie
     * @Description: 去除List中的重复数据
     * @Date: 2022/4/12 9:06
     */
    public static List duplicateList(List list) {
        HashSet set = new HashSet();
        set.addAll(list);
        return new ArrayList(set);
    }

    public static void main(String[] args) {
        //创建带有重复数据的List
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(2);
        list.add(4);
        list.add(4);

        //调用duplicateList方法
        List list1 = duplicateList(list);

        //遍历输出去重后的List
        Iterator iterator = list1.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

输出:

1
2
4

3.HashSet删除操作的深入理解

题目:分析以下代码,并写出其输出

@Test
public void test1() {//其中Person类中重写了hashCode()和equal()方法
    HashSet 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.setName("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);
}

输出:

[Person{ID=1002, name='BB'}, Person{ID=1001, name='AA'}]
[Person{ID=1002, name='BB'}, Person{ID=1001, name='CC'}]
[Person{ID=1002, name='BB'}, Person{ID=1001, name='CC'}, Person{ID=1001, name='CC'}]
[Person{ID=1002, name='BB'}, Person{ID=1001, name='CC'}, Person{ID=1001, name='CC'}, Person{ID=1001, name='AA'}]

第 13 行代码:我以为 HashSet 中删除了 p1 之后只剩下 p2,但结果是 1001, "CC"1002, "BB" 。我瞬间蒙了,马上回忆 [HashSet](#13.2 Collection接口方法) 添加相关知识点:在添加 1001, "AA" 的时候,通过计算这 2 个属性的哈希值,再经过某种算法确定该对象在数组中存放的位置,添加进去, 1002, "BB" 同理。

第 11~12 行代码:修改了 p1 中的 name 属性为 "CC",再执行删除操作时。这里要着重理解 HashSet remove() 方法的过程:首先计算当前 p1 两个属性值 1001, "CC" 的哈希值,再经过某种算法寻找该对象在数组中存放的位置。此时问题就来了,由于 p1 前后的属性值有变化,其哈希值的计算结果很大概率是不相同的。因此根据 1001, "CC" 去数组中寻找的位置很可能就不是原来 p1 存放的位置,可能是个空位置,因此删除失败,remove() 返回 false

第 14 行代码:根据上述的原理,再添加 1001, "CC" 对象是可以成功的。

第 16 行代码:根据上述的原理,再添加 1001, "AA" 对象,由于一开始就已经根据 1001, "AA" 算过哈希值,通过某种算法确定过 p1 的存放位置,这个位置上已经有数据了,即使 1001, "AA" 对象与当前 p1 1001, "CC" 属性值不相同,也不能再添加了。因此添加失败。

第 16 行代码:上面分析错了,确实,根据 1001, "AA" 算出来的哈希值确定的数组位置上确实已经有数据了,但别忘了,该位置上已经有数据并不会立即添加失败,而是调用 equals() 方法判断两个数据是否真的相同,很明显,之前 p1 已经改成 1001, "CC" 了,因此与 1001, "AA" 不同,因此按【七上八下】的链表添加到该位置上。

**总结体会:**在 HashSet 中,不能根据对象的属性值一样来判断重复数据。归根到底是判断属性值的哈希值变动来判断。

13.6 Map 接口

重点,着重理解!及其高频的面试题:

[1. HashMap 的底层实现原理?](#13.6.3 HashMap的底层原理)

2. HashMap 和 Hashtable 的异同?

3. CurrentHashMap 与 Hashtable 的异同?

上一节说过,Set 底层的源码其实就是 Map。它们的对于关系如下图所示:

Set
HashSet
LinkedHashSet
TreeSet
HashMap
LinkedHashMap
TreeMap
Map

13.6.1 Map接口及多个实现类对比

Map 是双列数据,存储 key-value 键值对的数据。类似于高中的函数 y = f ( x ) y=f(x) y=f(x)​ 。Map 接口继承树如下图所示:

image-20220412103237008

  • HashMap:作为 Map 接口的主要实现类,是线程不安全的,多线程时效率高;可以存储 null 的 key-value。其地位类似于 List 中的 ArrayList。
    • LinkedHashMap:是 HashMap 的子类。在 HashMap 的基础上加了指针形成链表结构,保证在遍历map元素时,可以按照添加顺序实现遍历。适用于频繁的遍历、插入和删除操作,其执行效率高于 HashMap。
  • TreeMap:保证按照添加的 key-value 对进行排序,实现排序遍历,此时只会根据 key 实现自然排序或定制排序。类似于 TreeSet。
  • Hashtable: Map 接口的古老实现类,是线程安全的,多线程时效率低;不能存储 null 的 key-value。其地位类似于 List 中的 Vector。
    • Properties:是 Hashtable 的子类。常用来处理配置文件,其 key-value 对都是 String 类型数据。

1.是否能存放 null 键值对的示例

① HashMap:

@Test
public void test1() {
    Map map = new HashMap();
    map.put(123, null);
    map.put(null, 456);
    map.put(null, null);
    System.out.println(map);
}

输出:

{null=null, 123=null}

② Hashtable:

@Test
public void test1() {
    Map map = new Hashtable();
    map.put(123, null);
    map.put(null, 456);
    map.put(null, null);
    System.out.println(map);
}

输出:

java.lang.NullPointerException
	at java.util.Hashtable.put()
	at day07.MapTest.test1()

报错,空指针异常。

13.6.2 Map中key-value的理解

Map结构如下图所示:

image-20220412130216106

  • Key 部分是用 Set 存储的,因为 Key 是无序的、不可重复的。如果是 HashMap,就用 HashSet 存储;如果是 LinkedHashMap,就用 LinkedHashSet 存储。key 所在的类要重写 equals() 和 hashCode() 方法。

  • Value 部分是用 Collection 存储的,Value 是无序的、可重复的。value 所在的类要重写 equals() 方法。

  • 一个键值对:key-value 构成了一个Entry对象。key-value 是 Entry 对象的 2 个属性。Map 中的 Entry 是无序的、不可重复的,使用 Set 存储所有 Entry。

13.6.3 HashMap的底层原理

1.JDK 7 中

image-20220412143551495

HashMap map = new HashMap();

在实例化以后,底层创建了长度为 16 的一维数组 Entry[] table。

……可能已经执行过多次 put() ……

map.put(key1, value1);

首先,调用 key1 所在类的 hashCode() 方法,计算出 key1 的哈希值。根据此哈希值,通过某种算法确定该 Entry 对象在 Entry 数组中存放的位置。

if (该位置上的数据为空) {
	key1-value1直接添加到该位置;
} else if(该位置上已经存在一个或多个数据,以链表形式存在) {
	if(key1的哈希值与已经存在的数据的哈希值都不一致) {
		按照【七上八下】的规则,以链表形式添加;
	} else if(key1的哈希值与某一个已经存在的数据key2-value2的哈希值相同) {
		调用 key1 所在类的 equals(key2) 方法;
		if(equals()返回false) {
			按照【七上八下】的规则,以链表形式添加;
		} else if(equals()返回true) {
			用 value1 去替换原来相同 key 的 value2;
		}
	}
}

个人总结:HashMap 添加过程与 HashSet 的过程除了最后一步 equals() 返回 true 时有差异 (HashMap 是用新 value 替换旧的,而 HashSet 是丢弃) 外,其余原理一模一样。

扩容方式:默认的扩容方式扩容为原来的 2 倍,并将原有的数据复制过来。

2.JDK 8 中相较 JDK 7在底层原理的不同

image-20220412143703539

① new HashMap() 时底层没有创建一个长度为 16 的数组;

② JDK 8 底层的数组是 Node[] 数组,而非 Entry[] 数组;

③ 首次调用 put() 方法时,底层创建长度为 16 的数字;

④ JDK 7 底层结构只有:数组+链表。而 JDK 8 中底层结构有:数组+链表+红黑树。当数组的某一个索引位置上的元素以链表结构存在的数据个数 > 8 且当前数组的长度 > 64 时:此索引位置上的所有数据改为使用红黑树存储。好处是:遍历效率高,如果是链表结构从头到尾遍历效率低,而红黑树是有序的,查找效率高。

3.HashMap 源码中的重要常量

  • DEFAULT_INITIAL_CAPACITY:HashMap 一维数组默认初始化长度,为常量 16。
  • DEFAULT_LOAD_FACTOR:默认负载因子,常量,其值为 0.75。
  • threshold:扩容的临界值,等于 容量 × 负载因子 = 16 × 0.75 = 12 16×0.75=12 16×0.75=12
  • TREEIFY_THRESHOLD:JDK 8 中的树型转化阈值,为常量 8 ,Bucket 中链表长度大于该默认值,链表就转化为红黑树。
  • MIN_TREEIFY_CAPACITY:JDK 8 中的常量。桶 (Bucket) 中的 Node 被树化时最小的 hash 表容量,为常量为 64 。

4.HashMap 中提前扩容的原因

HashMap 采取的是根据元素的 key 的哈希值确定存放位置的方式,很可能在 16 容量的情况下,如果等到数组完全满了才去扩容,但是几个位置始终都是空的,而很多都去形成了链表或者红黑树。

这里采取提前扩容,为的就是尽量减少 HashMap 中链表和红黑树结构的数量。这里的扩容阈值 threshold 是可以改的,改小了,数组的利用率 (填充率) 就低了;改大了,链表和红黑树结构的数量就多了。因此扩容阈值 threshold 0.75 0.75 0.75 是比较合适的值。

13.6.4 HashMap在JDK7中的源码分析

面试题:
谈谈你对 HashMap 中 [put/get 方法](put/get 方法)的认识?如果了解再谈谈 HashMap 的扩容机制?默认大小是多少?什么是负载因子 (或填充比)?什么是吞吐临界值 (或阈值、 threshold) ?

1.HashMap 的空参构造器[[JDK 8](#13.6.5 HashMap在JDK8中的源码分析)]

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
  • DEFAULT_INITIAL_CAPACITY:Entry 一维数组默认初始化长度,为常量 16。
  • DEFAULT_LOAD_FACTOR:默认负载因子,常量,其值为 0.75。

点进 this() 中看一看 HashMap 的另一个构造器:

	public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
	}

第 1 行代码:该构造器传入 2 个形参:int型的 initialCapacity 初始化长度和 float 型的 loadFactor 负载因子(默认空参构造器传过来的值是0.75) 。

【1、处理异常传入形参部分】

第 2~3 行代码:如果初始化长度 initialCapacity 小于0,则手动抛出异常 IllegalArgumentException

第 5~6 行代码:如果初始化长度 initialCapacity 大于 HashMap 支持的最大长度 MAXIMUM_CAPACITY (常量,定义为 2 30 = 1073741824 2^{30}=1073741824 230=1073741824),则令初始化长度 initialCapacity 等于最大长度 MAXIMUM_CAPACITY

第 7~9 行代码:如果负载因子 loadFactor 小于0 或者爆了,则手动抛出异常 IllegalArgumentException

【2、设置吞吐临界值和创建 Entry 一维数组】

第 12 行代码:定义 int型的容量 capacity,赋值为 1。

第 13~14 行代码:如果初始化长度 initialCapacity(默认空参构造器传过来的值是16) 大于 capacity(1),则 capacity 不断地左移 1 位(×2),直到 capacity 不比初始化长度 initialCapacity 小为止(默认循环停止时 capacity 为16)。

第 16 行代码:把传入的负载因子 loadFactor 参数赋给 HashMap 的属性负载因子 loadFactor

第 17 行代码:在capacity(16) × 负载因子 loadFactor(0.75) = 12,MAXIMUM_CAPACITY( 2 30 = 1073741824 2^{30}=1073741824 230=1073741824) 两个数中选择最小的那一个,强转成 int 型,赋给 HashMap 的属性吞吐临界值 (或阈值) threshold(默认情况下此处是 12)。threshold 值影响的是扩容,当 Entry[] 一维数组中已经存了threshold(12) 个数据时,就开始扩容。

第 18 行代码:创建一个 Entry[] 一维数组,其长度为 capacity(16) ,并赋给 HashMap 的属性 table

transient Entry<K,V>[] table;

2.HashMap的添加操作put()

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

第 1 行代码:传入 2 个参数:keyvalue。即传入 key-value 键值对。

第 2~3 行代码:如果 key 为 null,就存入 Entry 一维数组中。而在 Hashtable 中没有这个方法,因此 Hashtable 不支持存储 null。

【1、计算待添加数据的 key 的哈希值】

第 4 行代码:计算 key 的哈希值,赋给 int 型的 hash。计算过程复杂,暂时不深究了。

【2、根据这个哈希值算出在数组中的存放位置的索引】

第 5 行代码:这就是前面所说的根据特殊的算法,根据哈希值确定在 Entry 一维数组中存放的位置。indexFor() 方法,传入哈希值 hash,和 Entry[] 一维数组的长度 table.length,并赋值给 int 型的变量 i。点进 indexFor() 方法看一看:返回的是把哈希值 hashtable.length-1 作与运算的值,作为存放位置的索引。与运算,结果怎么样都不会超过数组长度,比如数组长度为8,其 (8-1=7) 的二进制数为 0111;哈希值假设是十进制的15,其二进制为 1111;两者作与运算的二进制结果为 0111,即十进制索引 7,放在数组的最后一位。

static int indexFor(int h, int length) {
    return h & (length-1);//与运算,结果怎么样都不会超过数组长度
}

【3、判断该索引的位置下是否已存在数据,如果存在判断是否 equals() 】

第 6 行代码:for循环。Entry<K,V> e = table[i] 把数组中索引为 i 的 Entry 类数据拿出来赋给 e;循环条件是 e 不为 null;迭代条件 e = e.next 是把下一个链表数据赋给 e。能进 for 循环说明第 5 行代码计算出来的索引 i 位置上已经有一个或者多个数据(链表结构存储)了。

	第 7 行代码:创建 Object 类对象 `k`。

	第 8 行代码:如果位置 `i` 上的数据 `e` 的哈希值与待添加数据的哈希值相等,且位置 `i` 上的数据 `e` 的 key 与待添加数据的 key 相等时,就意味着两个数据真的重复了。

​ 第 9 行代码:把原有数据 e 的 value 值赋给 oldValue

​ 第 10 行代码:把原有数据 e 的 value 值修改为待添加数据的 value 值。

​ 第 11 行代码:不知道,看名字是用来记录地址值的。

​ 第 12 行代码:返回原有数据 e 的 value 值 oldValue

【4、判断扩容、添加操作】

第 17 行代码:如果能运行到这,意味着是能添加成功的。真正进行添加数据的操作。点击 addEntry(hash, key, value, i) 方法查看源代码:

void addEntry(int hash, K key, V value, int bucketIndex) {
    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);
}

【判断是否需要进行扩容操作】

第 1 行代码:传入形参:待添加数据的 key 的哈希值 hashkeyvalue、数组存放位置索引i

第 2 行代码:如果要扩容,则必须同时满足 2 个条件:① 数组已添加的数据总数 >= 吞吐临界值 (或阈值) threshold;且② 数组存放位置不为空。意思是,如果 Entry 数组即时存放数据总数即时已经超过临界值 threshold,但如果发现往数组中添加位置是空的话,就不会扩容,直接添加到空位置即可。

​ 第 3 行代码:能进入代码块,说明必须要扩容了。把原来数组 table 的长度 × 2,并把数据复制过去。扩容后,某些在链表上的数据要重新计算哈希值、重新计算存放位置索引,可能会存到数组上而不是链表上。

​ 第 4 行代码:如果 key 不是 null,重新计算 key 的哈希值,并赋给 hash

​ 第 5 行代码:重新根据上述新哈希值 hash 计算数组存放索引 bucketIndex

【如果不需扩容,或者扩容完毕后,是真正的添加操作】

第 8 行代码:添加操作,点进 createEntry(hash, key, value, bucketIndex) 方法中查看源代码:

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

【真正的往 HashMap 添加数据的核心代码】

第 1 行代码:传入形参:待添加数据的 key 的哈希值 hashkeyvalue、数组存放位置索引 bucketIndex

第 2 行代码:把数组 table 中索引为 bucketIndex 的数据取出来,赋给变量 e

第 3 行代码:根据待添加数据,创建一个 Entry 对象,并存放在数组 table 索引为 bucketIndex 的位置上。构造器中特别注意最后一个形参 e,是赋给 Entry 对象的 next 属性,即原来这个位置上的原有数据 e 以链表结构挂在新数据的后面,称为 “头插”。这就解释了【七上八下】口诀中【七上】的来源。

Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}

第 4 行代码:添加成功,已存数据总数 size 加一。

13.6.5 HashMap在JDK8中的源码分析

1.HashMap 的空参构造器[ [JDK 7](#13.6.4 HashMap在JDK7中的源码分析)]

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

JDK 8 中一进来,并没有直接造数组。只是把默认负载因子 DEFAULT_LOAD_FACTOR(0.75) 赋给属性 loadFactor

2.底层数组变为 Node[],而不是 Entry[]。

transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

换名称,且实现了 Map.Entry<K,V> 接口,本质上和 Entry 没有太大区别。

3.HashMap的添加操作put()方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

第 1 行代码:传入 2 个形参:key 和 value。

第 2 行代码:① 计算 key 的哈希值 hash(key) ;传入 key 和 value。点击 putVal() 方法看源代码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        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 {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        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;
}

【1、判断是否首次添加,创建 Node[] 数组】

第 3 行代码:声明了 4 个变量,分别是 Node[] 数组 tab、Node 类型对象 p、int 型 ni

第 4~5 行代码:把同为 Node[] 数组的 table 赋给 tab,判断是否为空。一开始并没有为 table 赋值,因此为 true,执行 tab = resize() 操作。其中,resize() 方法就是创建 Node[] 数组的方法,点进去查看源代码:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    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 = 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)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

【1、resize() 方法有 2 个功能:创建数组和扩容数组。进来先判断到底是创建还是扩容?】

【这里是创建数组的情况,先初始化一些变量】

第 1 行代码:可知,该方法返回一个 Node[] 数组。

第 2 行代码:创建一个 Node[] 数组 oldTab ,意为旧数组,并赋值为 table (null) 。

第 3 行代码:判断数组 oldTab 是否为空,是,把 0 赋给 int 型变量 oldCap 。意为旧数组容量。

第 4 行代码:声明一个 int 型变量 oldThr ,意为旧临界值,并赋值为 threshold (0) 。

第 5 行代码:声明 2 个 int 型变量 newCapnewThr ,分别意为新数组容量和新临界值,并赋值为 0 。

【2、扩容的情况,创建数组可跳过】

第 6 行代码:

【3、创建数组】

第 18 行代码:新数组容量 newCap 赋值为默认初始化容量 DEFAULT_INITIAL_CAPACITY (16) 。

第 19 行代码:新临界值 newThr 赋值为:默认负载因子 DEFAULT_LOAD_FACTOR(0.75) × 默认初始化容量 DEFAULT_INITIAL_CAPACITY (16) = 12 。

第 21~25 行代码:新临界值 newThr 不为 0 ,跳过。

第 26 行代码:属性临界值 threshold 赋值为新临界值 newThr (12) 。

第 28 行代码:创建一个 Node[] 数组 newTab ,意为新数组,长度为新容量 newCap (16) 。

第 29 行代码:属性数组 table 赋值为新数组 newTab ,长度为 16 。

【4、扩容数组】

第 30~71 行代码:扩容数组的情况,跳过。

【5、返回新数组】

第 72 行代码:返回新数组 newTab

【回到 putVal 方法中,执行添加操作】

第 5 行代码:int 型的变量 n 赋值为创建好的新数组的长度 (16) 。并且 tab 就是创建好的新数组。

【判断要存的位置是否为空?要存的位置为空的情况】

第 6 行代码:判断。(n - 1) (15) 和 待添加数据的 key 的哈希值 hash 作与运算,得到的值赋给索引 i (这个数必定在0~15 之间) 。根据索引 i 找到数组 tab[i] 的元素,并赋给 Node 类的对象 p 。判断这个 p 是否为空?因为是刚创建的新数组,所以为 true。进入下一行:

第 7 行代码:数组 tab[i] 位置赋值为待添加的 Node 元素,成功添加进来。

【要存的位置不为空的情况】

第 8 行代码:能进入 else,说明要存的位置上已经有元素了。

第 9 行代码:声明 2 个变量,Node 类的对象 e 、K 类型的变量 k

【判断要存位置上的元素 p 的哈希值和 key 是否equals()】

第 10~11 行代码:if 判断语句。判断:① 要存位置上的元素 p 的 key 的哈希值是否与待添加元素的 key 的哈希值 hash 相等;② K 类型的变量 k 赋值为要存位置上的元素 p 的 key,并与待添加元素的 key 比较是否相同。如果 2 种情况都相同,说明要存位置上的元素 p 与待添加元素相同。

【哈希值相等、equals() 相等的情况】

第 12 行代码:两元素相同,Node 类的对象 e 赋值为旧元素 p 。然后直接跳到第 29 行代码。

第 29 行代码:if 判断语句。判断 e (旧元素 p ) 是否不为空?

第 30 行代码:如果为 true ,即为不空。则声明 V 类型的变量旧 value oldValue ,并赋值为旧元素 e 的 value 。

第 31 行代码:if 判断语句。判断 boolean 类型的形参 onlyIfAbsent 是否为 false ;或者旧 value oldValue 是否为空?(方法 boolean 类型的形参 onlyIfAbsent 一般都为 false ,所以 if 一般为 true )

第 32 行代码:旧元素 e 的 value 赋值为新 value 。后面 2 行代码不重要,略。

【哈希值或equals()不相等;且旧元素 p 是以红黑树方式存储的情况】

【哈希值或equals()不相等;且旧元素 p 不是以红黑树方式存储的情况。说明是以链表存的,把旧元素 p 下面的链表元素都比较一下,判断是否相等】

第 16 行代码:定义一个 for 循环。循环变量是 int 型的 binCount ,并赋值为 0 ;没有循环条件;迭代方式是 ++binCount

第 17 行代码:if 判断语句。这里Node 类的对象 e 用于存放每一次要对比的已存在元素,把数组上的旧元素 p 指向的链表下一个元素赋给Node 类的对象 e 。判断 e 是否为空?

第 18 行代码:如果为 true ,即为数组旧元素 p 的下一个链表元素为空,则添加成功。把待添加元素赋给数组上的旧元素 p 指向的链表下一个元素 p.next 。这就是【七上八下】规则中的 ”八下“ 来源,采取的是尾插。

第 19~20 行代码:if 判断语句。判断是否达到创建红黑树的条件。如果链表长度 binCount 超过 TREEIFY_THRESHOLD (8) 时,就进入 treeifyBin 创建红黑树方法。源代码见下方【第 20 行代码: treeifyBin(tab, hash) 方法源代码】。

第 21 行代码:新元素已经添加成功,break 退出循环。

第 23~24 行代码:if 判断语句。判断旧元素 p 指向的链表下一个元素 e 的哈希值与待添加元素的哈希值是否相同?且两者的 key 是否 equals() ?如果两者都相同,说明两者是重复元素,break 跳出循环,跳到第 29 行代码,作替换 value 的操作。如果两者不是重复元素,跳到第 26 行代码。

第 26 行代码:如果两者不是重复元素,就把 e 赋给 p 。继续循环链表下一个元素 p.next

【第 20 行代码: treeifyBin(tab, hash) 方法源代码,此方法是循环把链表元素转换成红黑树节点】

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

第 1 行代码: treeifyBin() 传入 2 个形参: Node[] 数组 tab 和 int 类型 hash (待添加元素 key 的哈希值)。

第 2 行代码:声明了 3 个变量,分别是 int 类型的 nindex ,以及 Node 类型的对象 e

第 3 行代码:if 判断语句。判断 Node[] 数组 tab 是否为空?或,数组 tab 的长度 n 是否小于最小树容量 MIN_TREEIFY_CAPACITY (64) ?

第 4 行代码:如果为 true ,则进入 resize() 方法创建 Node[] 数组 或 扩容。即,如果数组长度小于 64 ,就不要改成红黑树结构,只是扩容就可以了。只有当数组长度大于 64 时,才把链表长度大于 8 的一支改成红黑树结构。resize() 方法源码见上方】

第 5 行代码:else if 判断语句。判断数组上要存位置上的元素 e 是否不为空?

第 6 行代码:如果为 true ,即数组上要存位置上已经有元素了。则创建 2 个 TreeNode 红黑树类型的节点 hdtl ,暂时令它们为空。红黑树 TreeNode 类型的属性源代码如下所示:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;		   // 红黑属性

第 7~16 行代码:do-while 循环。

第 8 行代码:创建一个新的 TreeNode类对象 p ,赋值为 replacementTreeNode(e, null) 方法的返回值,是由旧元素转换成红黑树的节点对象。点击 replacementTreeNode(e, null) 进入查看源代码:

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

​ 第 1 行代码:传入数组上要存位置上的元素 e 和 null。

​ 第 2 行代码:返回一个新的 TreeNode 类的对象,该 TreeNode 类的对象的属性都是数组上要存位置上的元素 e 的属性,即,把数组上旧元素或链表上旧元素转换成红黑树节点。

【回到 treeifyBin 方法】

第 9~10 行代码:if 判断:判断 TreeNode 红黑树类型的节点 tl 是否为空?(true) ,为空,则 TreeNode 红黑树类型的节点 hd 赋值为 p (数组或链表转换来的红黑树节点对象)。

第 11~13 行代码:if-else 判断:如果 TreeNode 红黑树类型的节点 tl 不为空,则 TreeNode类对象 pp.prev 属性赋值为 tltltl.next 属性赋值为 p

第 15 行代码: TreeNode 红黑树类型的节点 tl 赋值为 p

第 16 行代码:do-while 循环条件:7~16 行代码如果满足要存放位置上的元素 e 及其链表元素不为空的条件,则一直循环,直到要存放位置上的元素 e 及其链表元素为空为止。

13.6.6 HashMap与HashSet的关系

1.HashSet构造器

当创建 HashSet 空参构造器时,其底层还是新创建了一个 HashMap 。如下代码所示:

public HashSet() {
    map = new HashMap<>();
}

2.HashSet添加 add()

当调用 HashSet 的 add() 方法时,底层其实是调用 HashMap 的 put() 方法。并且,数据是存放在 key 中 (不可重复性) 。源码如下所示:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

其中,value 是一个对象常量 PRESENT ,点进去看看源码是什么:

private static final Object PRESENT = new Object();

可以看到,为了避免用户遇到空指针异常,value 赋值为一个没有实际意义的 new Object() 。并且为了节省内存,还声明为 static 的,这样该对象就存放在常量池中,以后添加的所有 HashSet 对象的 value 全都指向这一个 Object。不得不感叹,Java 设计师的设计真是妙啊。

13.6.7 LinkedHashMap的底层实现(了解)

LinkedHashMap 是 HashMap 的子类,其底层大部分还是 HashMap 那一套。

1.LinkedHashMap 特点

可以按照添加顺序,按顺序输出。

① HashMap 的输出:

@Test
public void test1() {
    //创建HashMap
    Map map = new HashMap();
    //往HashMap中添加数据
    map.put(123, "AA");
    map.put(456, "BB");
    map.put(789, "CC");
    //输出HashMap
    System.out.println(map);
}

输出:

{789=CC, 456=BB, 123=AA}

可以看到,HashMap 的输出顺序与添加顺序不同。当然,两个输出与添加顺序不同并不是其被称作无序性的根本原因,而是上节源码中提到的根据元素的 key 的哈希值确定在数组中存放位置决定了其无序性。

② LinkedHashMap 的输出

@Test
public void test1() {
    //创建LinkedHashMap
    Map map = new LinkedHashMap();
    //往LinkedHashMap中添加数据
    map.put(123, "AA");
    map.put(456, "BB");
    map.put(789, "CC");
    //输出LinkedHashMap
    System.out.println(map);
}

输出:

{123=AA, 456=BB, 789=CC}

可以看到,LinkedHashMap 的输出顺序与添加顺序相同。但依然改变不了其无序性的特点,只是在 HashMap 的基础上添加了链表的结构,使得其能确定节点上一个元素和下一个元素。

2.LinkedHashMap 的底层实现

LinkedHashMap 的类中并没有自己的 put()putVal() 方法,这些方法都是继承其父类 HashMap 。唯一不同的是 LinkedHashMap 重写了父类 HashMap putVal() 方法中的 newNode() 方法,如下代码所示:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

而其中,LinkedHashMap 中的内部类为 Entry 类,其还是继承了 HashMap 中的内部类 Node 。其代码如下:

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);
    }
}

核心就是第 2 行代码,在 HashMap 中的内部类 Node 的基础上,定义了 2 个 Entry 类的属性 beforeafter ,分别用来记录元素的上一个元素和下一个元素的地址。这样就能形成链表结构。

作为对比,可以看看 HashMap 这的内部类 Node 类的代码,如下所示:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

13.6.8 Map的常用方法

1.添加 、 删除、修改

方法作用
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中的所有数据

例子:

@Test
public void test1() {
    //创建HashMap
    Map map = new HashMap();

    //1.添加、 删除、修改
    //添加
    map.put("AA", 123);
    map.put(45, 123);
    map.put("BB", 56);
    //修改value
    map.put("AA", 87);
    System.out.println(map);

    //添加全部putAll()
    Map map1 = new HashMap();
    map1.put("CC", 123);
    map1.put("DD", 123);
    map.putAll(map1);
    System.out.println(map);

    //删除
    Object value = map.remove("CC");
    System.out.println("删除后:" + map);
    System.out.println("被删除的value:" + value);

    //清空所有数据clear
    map.clear();//与map = null不同, 数据清空,map容器还在
    System.out.println(map.size());
    System.out.println(map);
}

输出:

{AA=87, BB=56, 45=123}
{AA=87, BB=56, CC=123, DD=123, 45=123}
删除后:{AA=87, BB=56, DD=123, 45=123}
被删除的value:123
0
{}

可以看到,输出顺序与添加顺序不同。其次,当添加第 4 个 key-value 对时,由于已经存在 key “AA”,因此不能重复添加。由上节 JDK 8 HashMap 源码分析中可以知道,当添加相同 Key 数据时,不会重复添加,只会把旧 value 修改为新 value。因此可以看到 key “AA” 的 value 值更新为 87 。

2.元素查询的操作

方法作用
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是否相等

例子:

@Test
public void test2() {
    //创建HashMap
    Map map = new HashMap();
    //添加
    map.put("AA", 123);
    map.put(45, 123);
    map.put("BB", 56);

    //2.元素查询的操作
    //获取指定key对应的value
    System.out.println("key45对应的value是:" + map.get(45));

    //是否包含指定的key
    System.out.println("是否包含key\"AA\":" + map.containsKey("AA"));
    System.out.println("是否包含key\"AAA\":" + map.containsKey("AAA"));

    //是否包含指定的value
    boolean isExist = map.containsValue(123);
    System.out.println("是否包含value123:" + isExist);

    //map中key-value对的个数
    System.out.println("map中key-value对的个数: " + map.size());

    //判断当前map是否为空
    System.out.println("判断当前map是否为空: " + map.isEmpty());

    //判断当前map和参数对象obj是否相等
    Map map1 = new HashMap();
    map1.put("AA", 123);
    map1.put(45, 123);
    map1.put("BB", 56);
    boolean isEquals = map.equals(map1);
    System.out.println("当前map和参数对象map1是否相等: " + isEquals);
}

输出:

key45对应的value是:123
是否包含key"AA":true
是否包含key"AAA":false
是否包含value123:true
map中key-value对的个数: 3
判断当前map是否为空: false
当前map和参数对象map1是否相等: true

3.Map转换成Set或Collection的方法

Map 是没有迭代器 Iterator 的。想要遍历,只能转换成 Set 或者 Collection ,再调用 Set 或者 Collection 的 iterator() 方法。这样才能实现 Map 的遍历操作。以下提供了转换方法:

方法作用
Set keySet()返回所有Key构成的Set集合
Collection values()返回所有value构成的Collection集合
Set entrySet()返回所有 key-value 对构成的Set集合

例子:

@Test
public void test3() {
    //创建HashMap
    Map map = new HashMap();
    //添加
    map.put("AA", 123);
    map.put(45, 1234);
    map.put("BB", 56);

    //遍历所有的key集:keySet()
    Set set = map.keySet();
    Iterator iterator = set.iterator();
    System.out.println("遍历所有的key集:");
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }

    //遍历所有的value集:values()
    Collection values = map.values();
    Iterator iterator1 = values.iterator();
    System.out.println("遍历所有的value集:");
    while (iterator1.hasNext()) {
        System.out.println(iterator1.next());
    }

    //遍历所有的key-value:
    //方式一:entrySet()
    Set entrySet = map.entrySet();
    Iterator iterator2 = entrySet.iterator();
    System.out.println("遍历所有的key-value方式一: ");
    while (iterator2.hasNext()) {
        Object obj = iterator2.next();
        //entrySet集合中的元素都是Entry类的对象,key-value对是一个整体
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getKey() + "----->" + entry.getValue());
    }
    //方式二:
    Set keySet = map.keySet();
    Iterator iterator3 = keySet.iterator();
    System.out.println("遍历所有的key-value方式二: ");
    while (iterator3.hasNext()) {
        Object key = iterator3.next();
        Object value = map.get(key);
        System.out.println(key + "======" + value);
    }
}

输出:

遍历所有的key集:
AA
BB
45
遍历所有的value集:
123
56
1234
遍历所有的key-value方式一: 
AA----->123
BB----->56
45----->1234
遍历所有的key-value方式二: 
AA======123
BB======56
45======1234

13.6.9 TreeMap的使用

向 TreeMap 中添加 key-value ,要求 key 必须是由同一个类创建的对象。因为要按照 key 进行排序。

掌握 2 种添加数据的方法:自然排序和定制排序。在下一章学习了泛型之后,就可以让加入 TreeMap 的数据类型保持一致了,现在先自己加入同类型的数据。User 类沿用前面的自定义类代码,

1.自然排序

① User 类已经实现 Comparable 接口,并重写了 compareTo() 方法。

//重写compareTo()方法:先按姓名从小到大排序,再按年龄从小到大排序
    @Override
    public int compareTo(Object o) {
        if (o instanceof User) {
            User user = (User) o;
//            return this.name.compareTo(user.name);
            int compare = this.name.compareTo(user.name);
            if (compare != 0) {
                return compare;
            } else {
                return Integer.compare(this.age, user.age);
            }
        } else {
            throw new RuntimeException("对不起,输入的类型不一致!");
        }
    }

测试:

@Test
public void test1() {
    //创建TreeMap
    TreeMap treeMap = new TreeMap();
    //创建User类对象
    User u1 = new User("Amy", 9);
    User u2 = new User("Rick", 17);
    User u3 = new User("Jack", 23);
    User u4 = new User("Jerry", 39);
    User u5 = new User("Rick", 55);
    //添加key-value
    treeMap.put(u1, 56);
    treeMap.put(u2, 98);
    treeMap.put(u3, 74);
    treeMap.put(u4, 65);
    treeMap.put(u5, 45);


    //转换成entrySet集合
    Set set = treeMap.entrySet();
    //遍历输出key-value
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

输出:

User{name='Amy', age=9}=56
User{name='Jack', age=23}=74
User{name='Jerry', age=39}=65
User{name='Rick', age=17}=98
User{name='Rick', age=55}=45

2.定制排序

@Test
public void test2() {
    //创建TreeMap, 定制排序,先按年龄从小到大排列,再按姓名从小到大排序
    TreeMap treeMap = new TreeMap(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            if (o1 instanceof User && o2 instanceof User) {
                User u1 = (User) o1;
                User u2 = (User) o2;
                int compare = Integer.compare(u1.getAge(), u2.getAge());
                if (compare != 0) {
                    return compare;
                } else {
                    return u1.getName().compareTo(u2.getName());
                }
            } else {
                throw new RuntimeException("传入的类型不一致");
            }
        }
    });

    //创建User类对象
    User u1 = new User("Amy", 99);
    User u2 = new User("Rick", 17);
    User u3 = new User("Jack", 23);
    User u4 = new User("Jerry", 39);
    User u5 = new User("Dick", 17);
    //添加key-value
    treeMap.put(u1, 56);
    treeMap.put(u2, 98);
    treeMap.put(u3, 74);
    treeMap.put(u4, 65);
    treeMap.put(u5, 45);


    //转换成entrySet集合
    Set entrySet = treeMap.entrySet();
    //遍历输出key-value
    Iterator iterator = entrySet.iterator();
    while (iterator.hasNext()) {
        Object obj = iterator.next();
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getKey() + "--------->" + entry.getValue());
    }
}

输出:

User{name='Dick', age=17}--------->45
User{name='Rick', age=17}--------->98
User{name='Jack', age=23}--------->74
User{name='Jerry', age=39}--------->65
User{name='Amy', age=99}--------->56

13.6.10 Properties处理属性文件

Hashtable: Map 接口的古老实现类,是线程安全的,多线程时效率低;不能存储 null 的 key-value。其地位类似于 List 中的 Vector。

Properties:是 Hashtable 的子类。常用来处理配置文件,其 key-value 对都是 String 类型数据。

这个配置文件是磁盘中真实存在的文件,通过 IO 流读到内存当中,这章的细节将在下下章详细讲解。现在只需要了解 Properties 的用途即可。

1.创建配置文件

在 IDEA 的工程下点击鼠标右键,选择 New 一个 Resource Bundle (资源包)。

image-20220416085434789

给配置文件起个名字,这里我起 Java 数据库连接库的英文缩写:

image-20220416085655182

创建成功,可以看到后缀就是 .properties 的配置文件:

image-20220416085827292

在配置文件中添加用户名和密码,注意不要添加空格:

image-20220416090124931

.properties 配置文件中的信息都是以 Map 的 key-value 对来存储的。

2.测试

public static void main(String[] args) {
    FileInputStream files = null;
    try {
        //创建Properties对象props
        Properties props = new Properties();
        //创建输入流
        files = new FileInputStream("jdbc.properties");
        //加载配置文件
        props.load(files);
        //读取配置文件数据
        String name = props.getProperty("name");
        String password = props.getProperty("password");
        //输出数据
        System.out.println("name=" + name + ", " + "password=" + password);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //关闭输入流
        if (files != null) {
            try {
                files.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

输出:

name=Tom汤姆, password=abc123

如果配置文件.properties 中的中文出现乱码,解决方案是去 Settings,勾选如图所示:

image-20220416084822269

然后把旧的配置文件.properties 删掉,重新创建,再编写中文就不会出现问题了。

13.7 Collections 工具类

13.7.1 简介

  • Collections 是一个操作 Set、List 和 Map 等集合的工具类。
  • Collections 中提供了一系列静态的方法对集合元素进行排序查询修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。

13.7.2 排序操作(均为 static 方法)

方法作用
reverse(List)反转 List 中元素的顺序
shuffle(List)对 List 集合元素进行随机排序
sort(List)根据元素的自然排序对指定 List 集合元素按升序排序
sort(List, Comparator)根据指定的Comparator产生的顺序对List集合元素进行排序
swap(List list, int i, int j)将指定 List 集合中的 i 处元素和 j 处元素进行交换
@Test
public void test1() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(43);
    list.add(765);
    list.add(-97);
    list.add(0);

    System.out.println("原始List:"+list);

    //反转
    Collections.reverse(list);//自身被修改了
    System.out.println("反转:"+list);

    //随机排序
    Collections.shuffle(list);
    System.out.println("打乱:"+list);//每次都不一样

    //交换
    Collections.swap(list, 2, 3);
    System.out.println("交换:"+list);
}

输出:

原始List:[123, 43, 765, -97, 0]
反转:[0, -97, 765, 43, 123]
打乱:[0, 123, 765, -97, 43]
交换:[0, 123, -97, 765, 43]

13.7.3 查找、替换操作

方法作用
Object max(Collection)根据元素的自然排序,返回给定集合中的最大元素
Object max(Collection, Comparator)根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)根据元素的自然排序,返回给定集合中的最小元素
Object min(Collection, Comparator)根据 Comparator 指定的顺序,返回给定集合中的最小元素
int frequency(Collection, Object)返回指定集合中指定元素的出现次数
void copy(List dest, List src)将 src 中的内容复制到 dest 中
boolean replaceAll(List list, Object oldVal, Object newVal)使用新值替换 List 对象的所有旧值
@Test
public void test1() {
    ArrayList list = new ArrayList();
    list.add(123);
    list.add(43);
    list.add(765);
    list.add(765);
    list.add(765);
    list.add(-97);
    list.add(0);

    System.out.println("原始List:" + list);
    
    //出现频率
    int frequency = Collections.frequency(list, 765);
    System.out.println("765出现的次数:" + frequency);

    //复制
    //错误写法,报异常IndexOutOfBoundsException: Source does not fit in dest
//    List dest = new ArrayList();
//    Collections.copy(dest, list);
    //正确写法
    List dest = Arrays.asList(new Object[list.size()]);//不是以整个数组作为一个元素
    Collections.copy(dest, list);
    System.out.println("复制后:" + dest);
}

输出:

原始List:[123, 43, 765, 765, 765, -97, 0]
765出现的次数:3
复制后:[123, 43, 765, 765, 765, -97, 0]

13.7.4 同步控制方法

Collections 类中提供了多个 synchronizedXxx () 方法,该方法可使将指定集
合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全
问题。

方法作用
Collection synchronizedCollection(Collection c)返回线程安全的 Collection
List synchronizedList(List list)返回线程安全的 List
Map synchronizedMap(Map m)返回线程安全的 Map
Set synchronizedSet(Set s)返回线程安全的 Set
SortedMap synchronizedSortedMap(SortedMap m)返回线程安全的有序Map
SortedSet synchronizedSortedSet(SortedSet m)返回线程安全的有序 Set
List list1 = Collections.synchronizedList(list);
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卡皮巴拉不躺平

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值