11 集合

11 集合

11.1 介绍

11.1.1 算法和数据结构

程序=数据结构+算法
一、算法:
可以解决具体的问题(解决流程),有设计解决的具体的流程(算法1、算法2),有评价这个算法的具体指标(时间复杂度、空间复杂度)
二、数据结构:
1.逻辑结构,思想上的结构。厨房、卧室、卫生间。线性表(数组、链表)、图、树、栈、队列。
2.物理结构,真实的结构。钢筋混凝土+牛顿力学。紧密结构(顺序结构)、跳转结构(链式结构)。
数组(紧密结构),优点:寻址快查找快,缺点:删除和增加元素效率低
链表(跳转结构)(单向链表/双向链表/循环链表),优点:删除和插入元素效率高,缺点:查询元素效率低

11.1.2 引入集合

数组、集合都是对多个数据进行存储操作的,简称为容器。
这里的存储指的是内存层面的存储,而不是持久化存储。

数组的特点:(1)一旦指定了长度,长度不可以更改;(2)一旦声明了类型,只能存放这个类型的数据。
数组的缺点:
因为上面的缺点,引入新的数据结构->集合。

11.1.3 简要集合结构图

Collection、Map

图片来自网络

11.2 Collection

package com.liweixiao.test01;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * @author:LiWeixiao
 * @date:2022/12/28
 * @description:
 */
public class Test01 {
    public static void main(String[] args) {
        /*
        Collection接口的常用方法:
        增加:add、addAll
        删除:clear、remove
        修改:
        查看:iterator、size
        判断:contains、equals、isEmpty
        */
        //创建对象
        Collection col = new ArrayList();
        //调用方法
        col.add(18);//集合只能存放引用数据类型的数据,不能是基本数据类型;基本数据类型自动装箱,对应包装类
        col.add(12);
        col.add(11);
        col.add(17);
        System.out.println(col);

        List list = Arrays.asList(new Integer[]{11, 15, 3, 7, 1});
        col.addAll(list);
        System.out.println(col);

        //col.clear();//清空集合
        System.out.println(col);
        System.out.println("集合中元素的数量:"+col.size());
        System.out.println("集合是否为空:"+col.isEmpty());

        boolean isRemove = col.remove(15);
        System.out.println(col);
        System.out.println("是否删除:"+isRemove);

        Collection col2 = new ArrayList();
        col2.add(18);
        col2.add(12);
        col2.add(11);
        col2.add(17);
        Collection col3 = new ArrayList();
        col3.add(18);
        col3.add(12);
        col3.add(11);
        col3.add(17);
        System.out.println(col2.equals(col3));//比较集合内的值,true
        System.out.println(col2 == col3);//==比较的是地址,false

        boolean isContains = col3.contains(17);
        System.out.println("是否包含元素:"+isContains);

    }
}

遍历方式:1.增强for循环,2.迭代器

package com.liweixiao.test01;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

/**
 * @author:LiWeixiao
 * @date:2022/12/28
 * @description:
 */
public class Test02 {
    public static void main(String[] args) {
        Collection col = new ArrayList();
        col.add(18);
        col.add(12);
        col.add(11);
        col.add(17);
        col.add("abc");
        col.add(1.01);

        //对集合遍历
        //方式1:普通for循环
        /*for (int i = 0; i < col.size(); i++) {
            col.
        }*/

        //方式2:增强for循环
        for (Object o:col) {
            System.out.println(o);
        }

        System.out.println("------------------");
        //方式3:iterator
        Iterator it = col.iterator();
        while (it.hasNext()){
            System.out.println(it.next());
        }
    }
}

11.2.1 List

遍历方式:1普通for循环,2增强for循环,3迭代器

package com.liweixiao.test01;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * @author:LiWeixiao
 * @date:2022/12/28
 * @description:
 */
public class Test03 {
    public static void main(String[] args) {
        /*
        List常用方法
        增加:add
        删除:remove(int)、remove(object)
        修改:set
        查看:get
        判断:
        */
        List list = new ArrayList();
        list.add(13);
        list.add(17);
        list.add(6);
        list.add(-1);
        list.add(2);
        list.add("abc");
        System.out.println(list);
        list.add(3,66);
        System.out.println(list);
        list.set(3,77);
        System.out.println(list);
        list.remove(2);//在集合中存入的是Integer类型数据,调用remove方法是int
        System.out.println(list);
        list.remove("abc");
        System.out.println(list);

        Object o = list.get(0);
        System.out.println(o);

        //方式1:普通for循环
        System.out.println("--------------");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
        //方式2:增强for循环
        System.out.println("//////////////");
        for (Object obj :
                list) {
            System.out.println(obj);
        }
        //方式3:迭代器
        System.out.println("**********");
        Iterator it = list.iterator();
        while (it.hasNext()){
            System.out.println(it.next());
        }
    }
}

11.2.2 ArrayList实现类

class ArrayList extends AbstractList implements List
class AbstractList extends AbstractCollection implements List
集团创始人承认了这个失误。
当数组中的10个位置都满了,就开始进行数组的扩容,扩容长度为 原数组的1.5倍。
JDK1.7和1.8的区别

package com.liweixiao.test02;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * @author:LiWeixiao
 * @date:2022/12/30
 * @description:
 */
public class Test01 {
    public static void main(String[] args) {
        //接口=实现类
        Collection col=new ArrayList();
        List list=new ArrayList();
        //直接创建实现类对象
        ArrayList al = new ArrayList();

        System.out.println(al.add("abc"));//true
        al.add("bcd");
        System.out.println(al);

    }
}

11.2.3 Vector实现类

联系:底层都是数组的扩容
区别:
ArrayList底层扩容长度为原数组的1.5倍,线程不安全,效率高
Vector底层扩容长度为原数组的2倍,线程安全,效率低(已淘汰)

11.3 泛型

泛型就相当于标签,形式<>,JDK1.5之后

11.3.1 泛型介绍

package com.liweixiao.test01;

import java.util.ArrayList;

/**
 * @author:LiWeixiao
 * @date:2022/12/30
 * @description:
 */
public class Test01 {
    public static void main(String[] args) {
        //创建一个ArrayList集合,存入学生程成绩
        //加入泛型的特定,在编译时期就会对类型进行检查
        //ArrayList<Integer> al = new ArrayList<Integer>();
        ArrayList<Integer> al = new ArrayList<>();
        al.add(98);
        al.add(18);
        al.add(39);
        al.add(60);
        //al.add("丽丽");
        //al.add(9.8);

        //遍历输出
        for (Object o : al) {
            System.out.println(o);
        }

        //遍历输出
        System.out.println("----------");
        for (Integer i:al) {
            System.out.println(i);
        }
    }
}

11.3.2 泛型类、泛型接口

1.实例化

package com.liweixiao.test02;

/**
 * @author:LiWeixiao
 * @date:2022/12/30
 * @description:GenericTest就是一个普通的类
 * GenericTest<E>是一个泛型类
 * <>里面是一个参数类型,类型不确定,相当于一个占位
 * 这个类型是一个引用数据类型,不是基本数据类型
 */
public class GenericTest<E> {
    int age;
    String name;
    E sex;

    public void a(E n){

    }

    public void b(E[] m){

    }
}

class Test{
    public static void main(String[] args) {
        //GenericTest进行实例化
        //(1)实例化时不指定泛型
        GenericTest gt1=new GenericTest();
        gt1.a("abc");
        gt1.a(17);
        gt1.a(9.8);
        gt1.b(new String[]{"a","b","c"});

        //(2)实例化时指定泛型
        //推荐:指定类型,进行限制
        GenericTest<String> gt2 = new GenericTest<>();
        gt2.sex="男";
        gt2.a("abc");
        gt2.b(new String[]{"a","b","c"});
    }
}

2.父类指定泛型

class SubGenericTest extends GenericTest<Integer>{

}

class Demo{
    public static void main(String[] args) {
        //指定父类泛型,那么子类就不需要再指定泛型了,可以直接使用
        SubGenericTest sgt = new SubGenericTest();
        sgt.a(19);
    }
}

3.父类不指定泛型

class SubGenericTest2<E> extends GenericTest<E>{

}

class Demo2{
    public static void main(String[] args) {
        SubGenericTest2<String> sgt2 = new SubGenericTest2<>();
        sgt2.a("abc");
        sgt2.sex="女";
        
    }
}

4.泛型可以定义多个参数类型

package com.liweixiao.test03;

import java.util.ArrayList;

/**
 * @author:LiWeixiao
 * @date:2022/12/30
 * @description:
 */
public class TestGeneric<A,B,C> {
    A age;
    B name;
    C sex;
    public void a(A m,B n,C x){

    }

    //构造方法不能有泛型参数
    /*public TestGeneric<A,B,C>(){

    }*/

    public void b(){
        ArrayList<String> list1=null;
        ArrayList<Integer> list2=null;
        //不同泛型的引用类型不可以相互赋值
        //list1=list2;
    }

    //静态方法不能使用泛型
    /*public static int c(A a){
        return 10;
    }*/
}

11.3.3 泛型方法

package com.liweixiao.test04;

/**
 * @author:LiWeixiao
 * @date:2022/12/30
 * @description:
 */
public class TestGeneric<E> {
    //不是泛型方法,不能是静态方法
    public void a(E e){

    }

    //是泛型方法,1.这个方法的泛型的参数类型要和当前的类的泛型无关
    //2.泛型方法定义的时候,前面要加上<T>
    //3.T的类型在调用方法的时候确定的
    //4.泛型方法可以是静态方法
    public static <T> void b(T t){

    }
}

class Demo{
    public static void main(String[] args) {
        TestGeneric<String> tg = new TestGeneric<>();
        tg.a("abc");
        tg.b("abc");
        tg.b(19);
        tg.b(false);
    }
}

泛型参数存在继承关系的情况

package com.liweixiao.test05;

import java.util.ArrayList;
import java.util.List;

/**
 * @author:LiWeixiao
 * @date:2023/1/3
 * @description:
 */
public class Test {
    public static void main(String[] args) {
        Object obj = new Object();
        String str = new String();
        obj=str;//父类引用指向子类,多态的一种形式

        Object[] objArr = new Object[10];
        String[] strArr = new String[10];
        objArr=strArr;//父类引用指向子类,多态的一种形式

        List<Object> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();
        //list1=list2;//ArrayList底层都是Object类型,是并列关系
        //总结:A和B是子类父类的关系,但是G<A>和G<B>不存在继承关系的,是并列关系。
    }
}

在没有通配符的时候,下面的a方法,相当于方法的重复定义,报错。

package com.liweixiao.test06;

import javafx.collections.ObservableList;

import java.util.ArrayList;
import java.util.List;

/**
 * @author:LiWeixiao
 * @date:2023/1/3
 * @description:
 */
public class Test {
    /*public void a(List<Object> list){

    }
    public void a(List<String> list){

    }
    public void a(List<Integer> list){

    }*/

    public void  a(List<?> list){
        //1.遍历:内部遍历的时候用Object即可,不用?
        for(Object a:list){
            System.out.println(a);
        }
        //2.数据的写入操作
        //list.add("abc");//出错,不能随意的添加数据
        list.add(null);

        //3.数据的读取操作
        Object obj = list.get(0);
    }
}

class T{
    public static void main(String[] args) {
        Test t = new Test();
        t.a(new ArrayList<Integer>());
        t.a(new ArrayList<String>());
        t.a(new ArrayList<Object>());
    }
}

引入通配符?后,G<?>就变成了G<A>和G<B>的父类。

package com.liweixiao.test06;

import java.util.ArrayList;
import java.util.List;

/**
 * @author:LiWeixiao
 * @date:2023/1/3
 * @description:
 */
public class Demo {
    public static void main(String[] args) {
        List<Object> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();
        List<Integer> list3 = new ArrayList<>();

        List<?> list=null;//通配符
        list=list1;
        list=list2;
        list=list3;
    }
}

泛型受限:泛型的上限,泛型的下限

package com.liweixiao.test07;

import java.util.ArrayList;
import java.util.List;

/**
 * @author:LiWeixiao
 * @date:2023/1/3
 * @description:
 */
public class Test {
    public static void main(String[] args) {
        //a,b,c三个集合是并列关系
        List<Object> a = new ArrayList<>();
        List<Person> b = new ArrayList<>();
        List<Student> c = new ArrayList<>();

        //开始使用泛型受限
        //泛型的上限:List<? extends Person>是List<Person>的父类,是List<Person子类>的父类
        List<? extends Person> list1=null;
        //list1=a;//报错
        list1=b;
        list1=c;

        //泛型的下限:List<? extends Person>是List<Person>的父类,是List<Person父类>的父类
        List<? super Person> list2=null;
        list2=a;
        list2=b;
        //list2=c;//报错
    }
}

11.2.4 LinkedList实现类

【LinkedList常用方法】

package com.liweixiao.test04;

import java.util.Iterator;
import java.util.LinkedList;

/**
 * @author:LiWeixiao
 * @date:2023/1/3
 * @description:
 */
public class Test {
    public static void main(String[] args) {
        /*LinkedList常用方法:
        增加:add、addFirst、addLast、
        offer、offerFirst、offerLast
        删除:poll、pollFirst、pollLast、
        remove、removeFirst、removeLast
        修改:
        查看:element、getFirst、getLast、indexOf、lastIndexOf、peak、peakFirst、peakLast
        判断:
        */
        LinkedList<String> list = new LinkedList<>();
        list.add("aaa");//添加到尾部
        list.add("bbb");
        list.add("ccc");
        list.add("ddd");
        list.add("eee");
        list.add("bbb");//可以添加重复的数据
        list.add("fff");
        System.out.println(list.size());//7
        list.addFirst("jj");//添加到头部
        list.addLast("hh");//添加到尾部

        list.offer("kk");//添加到尾部
        list.offerFirst("pp");//添加到头部
        list.offerLast("rr");//添加到尾部
        System.out.println(list);

        System.out.println(list.poll());//删除头部元素,并输出
        System.out.println(list.pollFirst());//删除头部元素,并输出
        System.out.println(list.pollLast());//删除尾部元素,并输出

        System.out.println(list.remove());//删除头部元素,并输出
        System.out.println(list.removeFirst());//删除头部元素,并输出
        System.out.println(list.removeLast());//删除尾部元素,并输出
        System.out.println(list);

        LinkedList<Object> list1 = new LinkedList<>();
        System.out.println(list1);//[]
        System.out.println(list1.pollFirst());//null, 从jdk1.6开始,提高了代码的健壮性
        //System.out.println(list1.removeFirst());//报错,从jdk1.0开始,没有头部元素

        //element、getFirst、getLast、indexOf、lastIndexOf、peek、peekFirst、peekLast
        System.out.println(list.element());//获取头部元素
        System.out.println(list.getFirst());//获取头部元素
        System.out.println(list.getLast());//获取尾部元素
        System.out.println(list.indexOf("ddd"));
        System.out.println(list.lastIndexOf("bbb"));
        System.out.println(list.peek());//获取头部元素
        System.out.println(list.peekFirst());//获取头部元素
        System.out.println(list.peekLast());//获取尾部元素

        //集合的遍历
        //1.普通for循环
        System.out.println("-------------");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
        //2.增强for循环
        System.out.println("///////////////");
        for (String str:
             list) {
            System.out.println(str);
        }
        //3.迭代器1
        System.out.println("*************");
        Iterator<String> it = list.iterator();
        while (it.hasNext()){
            System.out.println(it.next());
        }
        //4.迭代器2,这种方式好,节省内存,it的声明周期小
        System.out.println("............");
        for(Iterator<String> it1 = list.iterator();it1.hasNext();){
            System.out.println(it1.next());
        }
    }
}

【LinkedList底层原理】
ArrayList数据结构:物理结构,紧密结构;逻辑结构,线性表(数组)
LinkedList数据结构:物理结构,跳转结构;逻辑结构,线性表(链表),双向链表

以上图片来自网络

【模拟LinkedList源码】

package com.liweixiao.test05;

/**
 * @author:LiWeixiao
 * @date:2023/1/4
 * @description:节点类
 */
public class Node {
    //三个属性:
    //上一个元素的地址
    private Node pre;
    //当前存入的元素
    private Object obj;
    //下一个元素的地址
    private Node next;

    public Node getPre() {
        return pre;
    }

    public void setPre(Node pre) {
        this.pre = pre;
    }

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }

    @Override
    public String toString() {
        return "Node{" +
                "pre=" + pre +
                ", obj=" + obj +
                ", next=" + next +
                '}';
    }
}
package com.liweixiao.test05;

/**
 * @author:LiWeixiao
 * @date:2023/1/4
 * @description:
 */
public class MyLinkedList {
    //链中一定有个首节点
    Node first;
    //链中一定有个尾节点
    Node last;
    //计数器
    int count=0;

    //提供一个构造器
    public MyLinkedList(){

    }

    //添加元素方法
    public void add(Object o){
        if(first ==null){//添加第一个节点
            //将添加的元素封装为一个Node对象
            Node n = new Node();
            n.setPre(null);
            n.setObj(o);
            n.setNext(null);

            //当前链中第一个节点变为n
            first=n;
            //当前链中最后一个节点变为n
            last=n;
        }else {//不是第一个节点
            //将添加的元素封装为一个Node对象
            Node n = new Node();
            n.setPre(last);//n的上一个节点是当前链中最后一个节点last
            n.setObj(o);
            n.setNext(null);
            //当前链中最后一个节点的下一个元素,要指向n
            last.setNext(n);
            //将最后一个节点变为n
            last=n;
        }
        //链中元素数量加1
        count++;
    }

    //得到集合中元素的数量
    public int getSize(){
        return count;
    }

    //通过下标得到元素
    public Object get(int index){
        //获取链表头元素
        Node n=first;
        //一路next得到想要的元素
        for(int i=0;i<index;i++){
            n=n.getNext();
        }
        return n.getObj();
    }
}

class Test{
    public static void main(String[] args) {
        //创建一个MyLinkedList集合对象
        MyLinkedList ml = new MyLinkedList();
        ml.add("aa");
        ml.add("bb");
        ml.add("cc");
        System.out.println(ml.getSize());
        System.out.println(ml.get(1));
        System.out.println(ml.get(2));
    }
}

11.4 iterator()、Iterator、Iterable关系

图片来自网络

ListIterator

package com.liweixiao.test06;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;

/**
 * @author:LiWeixiao
 * @date:2023/1/4
 * @description:
 */
public class Test02 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        list.add("ee");

        //报错并发修改异常ConcurrentModificationException
        /*Iterator<String> it = list.iterator();
        while (it.hasNext()){
            if("cc".equals(it.next())){
                list.add("kk");
            }
        }*/

        ListIterator<String> it = list.listIterator();
        while (it.hasNext()){
            if("cc".equals(it.next())){
                it.add("kk");
            }
        }
        System.out.println(list);

        //正向遍历
        System.out.println(it.hasNext());//false
        System.out.println(it.hasPrevious());//true

        System.out.println("------------");
        //逆向遍历
        while (it.hasPrevious()){
            System.out.println(it.previous());
        }
        System.out.println(it.hasNext());//true
        System.out.println(it.hasPrevious());//false
    }
}

11.2.5 set接口

List,不唯一,有序;Set,唯一,无序(相对List接口来说的,无序不等于随机)

11.5 hashCode、equals

11.2.6 HashSet实现类

【1】放入Integer类型数据

package com.liweixiao.test07;

import java.util.HashSet;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class TestInteger {
    public static void main(String[] args) {
        //创建一个HashSet
        HashSet<Integer> hs = new HashSet<>();
        System.out.println(hs.add(19));//true
        hs.add(5);
        hs.add(20);
        System.out.println(hs.add(19));//false,这个19没有放入到集合中
        hs.add(41);
        hs.add(0);
        System.out.println(hs.size());//5
        System.out.println(hs);//[0, 19, 20, 5, 41],唯一、无序
    }
}

【2】放入String类型数据

package com.liweixiao.test07;

import java.util.HashSet;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class TestString {
    public static void main(String[] args) {
        HashSet<String> hs = new HashSet<>();
        hs.add("hello");
        System.out.println(hs.add("apple"));//true
        hs.add("banana");
        hs.add("html");
        System.out.println(hs.add("apple"));//false,这个apple没有放入到集合中
        hs.add("css");
        System.out.println(hs.size());//5
        System.out.println(hs);//[banana, apple, css, html, hello],唯一、无序
    }
}

【3】放入自定义的引用类型的数据,不满足唯一无序的原理

package com.liweixiao.test07;

import java.util.Objects;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class Student {
    //属性
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

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

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

    //重写hashCode和equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && name.equals(student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }
}
package com.liweixiao.test07;

import java.util.HashSet;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class TestStudent {
    public static void main(String[] args) {
        HashSet<Student> hs = new HashSet<>();
        hs.add(new Student(19,"lili"));
        hs.add(new Student(20,"lulu"));
        hs.add(new Student(18,"feifei"));
        hs.add(new Student(19,"lili"));
        hs.add(new Student(10,"nana"));

        System.out.println(hs.size());//5
        System.out.println(hs);//不满足唯一无序的原理
        //Student类重写hashCode和equals方法后,满足唯一无序的原理。
    }
}

【4】HashSet简要原理图
底层原理:数组+链表=哈希表。
注意:如果放入HashSet中的数据,一定要重写两个方法:hashCode、equals。

图片来自网络

11.2.7 LinkedHashSet实现类

唯一有序,按照输入顺序进行输出。
在HashSet基础上,多了一个总的链表,将放入的元素串在一起,方便有序的遍历。

11.7 比较器的使用

【1】比较int类型数据
【2】比较String类型数据
【3】比较double类型数据

package com.liweixiao.test08;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class Test01 {
    public static void main(String[] args) {
        int i1=10;
        int i2 =20;
        System.out.println(i1- i2);//-10
        System.out.println(((Integer) i1).compareTo((Integer) i2));//-1,返回-1/0/1三个值

        String str1="A";
        String str2="D";
        System.out.println(str1.compareTo(str2));//-3

        double d1=9.6;
        double d2=8.3;
        System.out.println(d1 - d2);//1.2999
        System.out.println(((Double) d1).compareTo((Double) d2));//1,返回-1/0/1三个值
    }
}

【4】比较自定义的数据类型
(1)内部比较器implements Comparable

package com.liweixiao.test08;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class Student implements Comparable<Student>{
    //属性
    private int age;
    private double height;
    private String name;

    public int getAge() {
        return age;
    }

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

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public String getName() {
        return name;
    }

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

    //构造器
    public Student(int age, double height, String name) {
        this.age = age;
        this.height = height;
        this.name = name;
    }

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

    @Override
    public int compareTo(Student o) {
        //1.按照年龄比较
        //return this.age-o.age;
        //return this.getAge()-o.getAge();
        //2.按照身高比较
        //return ((Double)(this.getHeight())).compareTo((Double)(o.getHeight()));
        //3.按照姓名比较
        return this.getName().compareTo(o.getName());
    }
}
package com.liweixiao.test08;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class Test02 {
    public static void main(String[] args) {
        //比较两个学生:
        Student s1 = new Student(10, 160.5, "lili");
        Student s2 = new Student(14, 170.5, "nana");
        System.out.println(s1.compareTo(s2));
    }
}

(2)外部比较器implements Comparator

class BiJiao01 implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge()-o2.getAge();
    }
}

class BiJiao02 implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return ((Double)(o1.getHeight())).compareTo((Double) (o2.getHeight()));
    }
}

class BiJiao03 implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getName().compareTo(o2.getName());
    }
}

class BiJiao04 implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        //先比较年龄;年龄相同的情况,比较身高
        int i = o1.getAge() - o2.getAge();
        if(i !=0){
            return i;
        }else {
            return ((Double)(o1.getHeight())).compareTo((Double) (o2.getHeight()));
        }
    }
}
package com.liweixiao.test09;


import java.util.Comparator;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class Test02 {
    public static void main(String[] args) {
        //比较两个学生:
        Student s1 = new Student(10, 160.5, "lili");
        Student s2 = new Student(14, 170.5, "nana");
        //获取比较器
        BiJiao01 bj1 = new BiJiao01();
        System.out.println(bj1.compare(s1, s2));//-4
        Comparator bj2 = new BiJiao02();
        System.out.println(bj2.compare(s1, s2));//-1
        Comparator bj3 = new BiJiao03();
        System.out.println(bj3.compare(s1, s2));//-2
        Comparator bj4=new BiJiao04();
        System.out.println(bj4.compare(s1, s2));
    }
}

【5】内部比较器和外部比较器 比较
外部比较器好用,多态,扩展性好

11.2.8 TreeSet实现类

【1】保存Integer类型数据

package com.liweixiao.test10;

import java.util.TreeSet;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class Test01 {
    public static void main(String[] args) {
        //创建一个TreeSet
        TreeSet<Integer> ts = new TreeSet<>();
        ts.add(12);
        System.out.println(ts.add(3));//true
        ts.add(7);
        ts.add(9);
        System.out.println(ts.add(3));//false
        ts.add(16);
        System.out.println(ts.size());//5
        System.out.println(ts);//[3, 7, 9, 12, 16],唯一,无序(没有按照输入顺序),有序(按照升序)
    }
}

【2】原理,底层:二叉树(数据结构)

图片来自网络

【3】放入String类型数据,底层实现 类内部比较器

package com.liweixiao.test10;

import java.util.TreeSet;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class Test02 {
    public static void main(String[] args) {
        TreeSet<String> ts = new TreeSet<>();
        ts.add("elili");
        ts.add("blili");
        ts.add("elili");
        ts.add("alili");
        ts.add("clili");
        ts.add("flili");
        ts.add("glili");
        System.out.println(ts.size());
        System.out.println(ts);//唯一,无序,有序
    }
}

【4】放入自定义的数据类型
(1)内部比较器

    @Override
    public int compareTo(Student o) {
        return this.getAge()-o.getAge();
    }
package com.liweixiao.test10;

import java.util.TreeSet;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class Test03 {
    public static void main(String[] args) {
        TreeSet<Student> ts = new TreeSet<>();
        ts.add(new Student(10,"elili"));
        ts.add(new Student(8,"blili"));
        ts.add(new Student(4,"alili"));
        ts.add(new Student(9,"elili"));
        ts.add(new Student(10,"flili"));
        ts.add(new Student(1,"dlili"));
        System.out.println(ts.size());
        System.out.println(ts);
    }
}

(2)外部比较器

class BiJiao implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getName().compareTo(o2.getName());
    }
}
package com.liweixiao.test10;

import java.util.Comparator;
import java.util.TreeSet;

/**
 * @author:LiWeixiao
 * @date:2023/1/5
 * @description:
 */
public class Test03 {
    public static void main(String[] args) {
        //利用外部比较器,必须自己指定:
        Comparator<Student> com = new BiJiao();
        TreeSet<Student> ts = new TreeSet<>(com);//一旦指定外部比较器,那么就会按照外部比较器来比较
        ts.add(new Student(10,"elili"));
        ts.add(new Student(8,"blili"));
        ts.add(new Student(4,"alili"));
        ts.add(new Student(9,"elili"));
        ts.add(new Student(10,"flili"));
        ts.add(new Student(1,"dlili"));
        System.out.println(ts.size());
        System.out.println(ts);
    }
}

实际开发中利用外部比较器多,因为扩展性好(多态)。

换一种写法:

        Comparator<Student> com = new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getName().compareTo(o2.getName());
            }
        };

【5】TreeSet底层的二叉树的遍历,是按照升序的结果

11.2.9 Collection整体结构图

11.7 Map

11.7.1 HashMap实现类

package com.liweixiao.test11;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @author:LiWeixiao
 * @date:2023/1/6
 * @description:
 */
public class Test01 {
    public static void main(String[] args) {
        /*
        增加:put
        删除:clear、remove
        修改:
        查看:entrySet、get、keySet、size、values
        判断:containsKey、containsValue、equals、isEmpty
        */
        //创建一个map集合
        Map<String,Integer> map = new HashMap<>();
        System.out.println(map.put("lili", 101010));//null
        map.put("nana",123456);
        map.put("feifei",111111);
        System.out.println(map.put("lili", 222222));//101010
        map.put("mingming",222222);
        //map.clear();//清空
        //map.remove("feifei");//移除
        //System.out.println(map.isEmpty());//false,判断是否为空
        System.out.println(map.size());//4
        System.out.println(map);//{nana=123456, lili=222222, mingming=222222, feifei=111111},唯一、无序

        System.out.println(map.containsKey("nana"));//true
        System.out.println(map.containsValue(222222));//true

        Map<String,Integer> map2 = new HashMap<>();
        map2.put("lili", 101010);//null
        map2.put("nana",123456);
        map2.put("feifei",111111);
        map2.put("lili", 222222);//101010
        map2.put("mingming",222222);
        System.out.println(map == map2);//false,==比较的是地址
        System.out.println(map.equals(map2));//true,equals进行了重写,比较的是集合中的值是否一致

        System.out.println(map.get("nana"));//123456

        Set<String> key = map.keySet();//输出集合中所有的key
        System.out.println(key);//[nana, lili, mingming, feifei]
        Collection<Integer> values = map.values();//输出集合中所有的value
        System.out.println(values);//[123456, 222222, 222222, 111111]
        Set<String> key2 = map.keySet();//根据key获得value
        for (String str:key2) {
            System.out.println(map.get(str));
        }

        System.out.println(map.entrySet());//[nana=123456, lili=222222, mingming=222222, feifei=111111]
        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        for (Map.Entry<String, Integer> e:entries) {
            System.out.println(e.getKey()+"---"+e.getValue());
        }
    }
}

11.7.2 Hashtable实现类

HashMap从jdk1.2开始,效率高,线程不安全,key可以存入null值
Hashtable从jdk1.0开始,效率低,线程安全,key不可以存入null值

11.7.3 LindedHashMap实现类

特点:唯一,有序,按照输入顺序进行输出,在HashMap上加了一个总的链表。

11.7.4 TreeMap实现类

特点:唯一,有序(按照升序)
原理:二叉树,key遵照二叉树的特点
放入集合的key的数据对应的类型内部一定要实现比较器(内部比较器 或者 外部比较器)
【1】key的类型是String类型

package com.liweixiao.test11;

import java.util.Map;
import java.util.TreeMap;

/**
 * @author:LiWeixiao
 * @date:2023/1/6
 * @description:
 */
public class Test02 {
    public static void main(String[] args) {
        Map<String, Integer> tm = new TreeMap<>();
        tm.put("blili",1234);
        tm.put("alili",5678);
        tm.put("blili",1111);
        tm.put("dlili",2222);
        tm.put("clili",2222);
        System.out.println(tm.size());//4
        System.out.println(tm);//{alili=5678, blili=1111, clili=2222, dlili=2222}
    }
}

【2】key的类型是自定义类型
(1)内部比较器

package com.liweixiao.test11;

/**
 * @author:LiWeixiao
 * @date:2023/1/6
 * @description:
 */
public class Student implements Comparable<Student>{
    //属性
    private int age;
    private String name;
    private double height;

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    //构造器
    public Student(int age, String name, double height) {
        this.age = age;
        this.name = name;
        this.height = height;
    }

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

    @Override
    public int compareTo(Student o) {
        //return this.getAge()-o.getAge();
        return this.getName().compareTo(o.getName());
    }
}
package com.liweixiao.test11;

import java.util.Map;
import java.util.TreeMap;

/**
 * @author:LiWeixiao
 * @date:2023/1/6
 * @description:
 */
public class Test03 {
    public static void main(String[] args) {
        Map<Student,Integer> tm = new TreeMap<>();
        tm.put(new Student(19, "blili", 170.5), 1001);
        tm.put(new Student(18,"blili",170.5),1002);
        tm.put(new Student(19, "alili", 180.5), 1002);
        tm.put(new Student(17,"dlili",140.5),1001);
        tm.put(new Student(10,"clili",160.5),1003);
        System.out.println(tm.size());
        System.out.println(tm);
    }
}

(2)外部比较器

        Map<Student,Integer> tm = new TreeMap<>(new Comparator<Student>() {//匿名内部类
            @Override
            public int compare(Student o1, Student o2) {
                return ((Double)(o1.getHeight())).compareTo((Double) (o2.getHeight()));
            }
        });

11.7.5 HashMap的底层原理

【1】代码展示

package com.liweixiao.test01;

import java.util.HashMap;
import java.util.Map;

/**
 * @author:LiWeixiao
 * @date:2023/1/6
 * @description:
 */
public class Test01 {
    public static void main(String[] args) {
        //创建一个HashMap的对象
        Map<Integer,String> hm = new HashMap<>();
        System.out.println(hm.put(12, "丽丽"));
        hm.put(7,"菲菲");
        hm.put(19,"露露");
        System.out.println(hm.put(12, "明明"));
        hm.put(6,"莹莹");
        System.out.println(hm.size());
        System.out.println(hm);
    }
}

【2】先弄清楚原理,再通过源码验证
7上8下

【3】HashMap的构造器
【4】HashMap的put方法


public class HashMap<K,V>
    extends AbstractMap<K,V> //【1】继承的AbstractMap中,已经实现了Map接口
        //【2】又实现了这个接口,多余,但是设计者觉得没有必要删除,就这么地了
    implements Map<K,V>, Cloneable, Serializable{
                
                
        //【3】后续会用到的重要属性:先粘贴过来:
    static final int DEFAULT_INITIAL_CAPACITY = 16;//哈希表主数组的默认长度
        //定义了一个float类型的变量,以后作为:默认的装填因子,加载因子是表示Hsah表中元素的填满的程度
        //太大容易引起哈西冲突,太小容易浪费  0.75是经过大量运算后得到的最好值
        //这个值其实可以自己改,但是不建议改,因为这个0.75是大量运算得到的
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        transient Entry<K,V>[] table;//主数组,每个元素为Entry类型
        transient int size;
        int threshold;//数组扩容的界限值,门槛值   16*0.75=12 
        final float loadFactor;//用来接收装填因子的变量
        
        //【4】查看构造器:内部相当于:this(16,0.75f);调用了当前类中的带参构造器
        public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
        //【5】本类中带参数构造器:--》作用给一些数值进行初始化的!
        public HashMap(int initialCapacity, float loadFactor) {
        //【6】给capacity赋值,capacity的值一定是 大于你传进来的initialCapacity 的 最小的 2的倍数
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
                //【7】给loadFactor赋值,将装填因子0.75赋值给loadFactor
        this.loadFactor = loadFactor;
                //【8】数组扩容的界限值,门槛值
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
                
                //【9】给table数组赋值,初始化数组长度为16
        table = new Entry[capacity];
                   
    }
        //【10】调用put方法:
        public V put(K key, V value) {
                //【11】对空值的判断
        if (key == null)
            return putForNullKey(value);
                //【12】调用hash方法,获取哈希码
        int hash = hash(key);
                //【14】得到key对应在数组中的位置
        int i = indexFor(hash, table.length);
                //【16】如果你放入的元素,在主数组那个位置上没有值,e==null  那么下面这个循环不走
                //当在同一个位置上放入元素的时候
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
                        //哈希值一样  并且  equals相比一样   
                        //(k = e.key) == key  如果是一个对象就不用比较equals了
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
                //【17】走addEntry添加这个节点的方法:
        addEntry(hash, key, value, i);
        return null;
    }
        
        //【13】hash方法返回这个key对应的哈希值,内部进行二次散列,为了尽量保证不同的key得到不同的哈希码!
        final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
                //k.hashCode()函数调用的是key键值类型自带的哈希函数,
                //由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。
        h ^= k.hashCode();
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
                /*
                接下来的一串与运算和异或运算,称之为“扰动函数”,
                扰动的核心思想在于使计算出来的值在保留原有相关特性的基础上,
                增加其值的不确定性,从而降低冲突的概率。
                不同的版本实现的方式不一样,但其根本思想是一致的。
                往右移动的目的,就是为了将h的高位利用起来,减少哈西冲突
                */
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
        //【15】返回int类型数组的坐标
        static int indexFor(int h, int length) {
                //其实这个算法就是取模运算:h%length,取模效率不如位运算
        return h & (length-1);
    }
        //【18】调用addEntry
        void addEntry(int hash, K key, V value, int bucketIndex) {
                //【25】size的大小  大于 16*0.75=12的时候,比如你放入的是第13个,这第13个你打算放在没有元素的位置上的时候
        if ((size >= threshold) && (null != table[bucketIndex])) {
                        //【26】主数组扩容为2倍
            resize(2 * table.length);
                        //【30】重新调整当前元素的hash码
            hash = (null != key) ? hash(key) : 0;
                        //【31】重新计算元素位置
            bucketIndex = indexFor(hash, table.length);
        }
                //【19】将hash,key,value,bucketIndex位置  封装为一个Entry对象:
        createEntry(hash, key, value, bucketIndex);
    }
        //【20】
        void createEntry(int hash, K key, V value, int bucketIndex) {
                //【21】获取bucketIndex位置上的元素给e
        Entry<K,V> e = table[bucketIndex];
                //【22】然后将hash, key, value封装为一个对象,然后将下一个元素的指向为e (链表的头插法)
                //【23】将新的Entry放在table[bucketIndex]的位置上
        table[bucketIndex] = new Entry<>(hash, key, value, e);
                //【24】集合中加入一个元素 size+1
        size++;
    }
    //【27】
        void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
                //【28】创建长度为newCapacity的数组
        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
                //【28.5】转让方法:将老数组中的东西都重新放入新数组中
        transfer(newTable, rehash);
                //【29】老数组替换为新数组
        table = newTable;
                //【29.5】重新计算
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
        //【28.6】
        void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                                //【28.7】将哈希值,和新的数组容量传进去,重新计算key在新数组中的位置
                int i = indexFor(e.hash, newCapacity);
                                //【28.8】头插法
                e.next = newTable[i];//获取链表上元素给e.next
                newTable[i] = e;//然后将e放在i位置 
                e = next;//e再指向下一个节点继续遍历
            }
        }
    }
}

11.7.6 HashSet底层原理

HashSet底层是HashMap实现

public class HashSet<E>{
    //重要属性:
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();
    //构造器:
    public HashSet() {
        map = new HashMap<>();//HashSet底层就是利用HashMap来完成的
    }
        
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }      
}

11.7.7 TreeMap底层源码

【1】原理大致结束
共6个属性,少说了1个属性:父节点的地址。

【2】源码

public class TreeMap<K,V>{
        //重要属性:
        //外部比较器:
        private final Comparator<? super K> comparator;
        //树的根节点:
        private transient Entry<K,V> root = null;
        //集合中元素的数量:
        private transient int size = 0;
        //空构造器:
        public TreeMap() {
        comparator = null;//如果使用空构造器,那么底层就不使用外部比较器
    }
        //有参构造器:
        public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;//如果使用有参构造器,那么就相当于指定了外部比较器
    }
        
        public V put(K key, V value) {//k,V的类型在创建对象的时候确定了
        //如果放入的是第一对元素,那么t的值为null
        Entry<K,V> t = root;//在放入第二个节点的时候,root已经是根节点了
                //如果放入的是第一个元素的话,走入这个if中:
        if (t == null) {
                        //自己跟自己比
            compare(key, key); // type (and possibly null) check
                        //根节点确定为root
            root = new Entry<>(key, value, null);
                        //size值变为1
            size = 1;
            modCount++;
            return null;
        }
                
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
                //将外部比较器赋给cpr:
        Comparator<? super K> cpr = comparator;
                //cpr不等于null,意味着你刚才创建对象的时候调用了有参构造器,指定了外部比较器
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);//将元素的key值做比较
                                //cmp返回的值就是int类型的数据:
                                //要是这个值《0 =0  》0
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else//cpm==0
                                //如果key的值一样,那么新的value替换老的value  但是key不变 因为key是唯一的
                    return t.setValue(value);
            } while (t != null);
        }
                //cpr等于null,意味着你刚才创建对象的时候调用了空构造器,没有指定外部比较器,使用内部比较器
        else {
            if (key == null)
                throw new NullPointerException();
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);//将元素的key值做比较
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;//size加1 操作
        modCount++;
        return null;
    }
        
        
}
 static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;//1.key
        V value;//2.value
        Entry<K,V> left = null;//3.左子树地址
        Entry<K,V> right = null;//4.右子树地址
        Entry<K,V> parent;//5.父节点地址
        boolean color = BLACK;//6.颜色
 }

11.7.8 TreeSet底层源码

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable{
                //重要属性:
                private transient NavigableMap<E,Object> m;
                private static final Object PRESENT = new Object();
                
                //在调用空构造器的时候,底层创建了一个TreeMap
                public TreeSet() {
                        this(new TreeMap<E,Object>());
                }
                
                TreeSet(NavigableMap<E,Object> m) {
                        this.m = m;
                }
                
                public boolean add(E e) {
                        return m.put(e, PRESENT)==null;
                } 
}

11.8 Collections工具类

类似数组的Arrays工具类

package com.liweixiao.test12;


import java.util.ArrayList;
import java.util.Collections;

/**
 * @author:LiWeixiao
 * @date:2023/1/9
 * @description:
 */
public class Test01 {
    public static void main(String[] args) {
        //Collections不支持创建对象,因为构造器私有化
        //Collections cols = new Collections();//报错
        //常用方法:
        //addAll添加多个元素
        ArrayList<String> al = new ArrayList<>();
        al.add("cc");
        al.add("bb");
        al.add("aa");
        Collections.addAll(al,"dd","ee","ff");
        Collections.addAll(al,new String[]{"gg","oo","pp"});
        System.out.println(al.size());
        System.out.println(al);

        //sort排序
        Collections.sort(al);//升序
        System.out.println(al);

        //binarySearch二分查找法
        System.out.println(Collections.binarySearch(al, "cc"));

        //copy替换方法
        ArrayList<String> al2 = new ArrayList<>();
        Collections.addAll(al2,"tt","ss");
        Collections.copy(al,al2);//把al2覆盖al
        System.out.println(al);//[tt, ss, cc, dd, ee, ff, gg, oo, pp]
        System.out.println(al2);//[tt, ss]

        //fill填充
        Collections.fill(al2,"yy");
        System.out.println(al2);//[yy, yy]
    }
}
posted @ 2022-12-21 09:40  LiWeixiao  阅读(41)  评论(0)    收藏  举报