Set接口和常用方法+HashSet分析
  TEZNKK3IfmPf 2023年11月13日 24 0

1)无序(添加和取出的顺序不一致),没有索引[后面演示]

2)不允许重复元素,所以最多包含一个null

3) JDK API中Set接口的实现类有:

Set接口和常用方法+HashSet分析

2. Set 接口的常用方法

和 List 接口一样, Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样. 

2.1 Set 接口的遍历方式  517

同Collection的遍历方式样,因为Set接口是Collection接口的子接口。

1.可以使用迭代器

2.增强for

3.不能使用索引的方式来获取.

代码在com.stulzl.set_method.包中

Set_Method
package com.stulzl.set_method;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

//Set接口常用方法  517
@SuppressWarnings({"all"})
public class Set_Method {
    public static void main(String[] args) {
        //老韩解读
        //1. 以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法
        //2. set 接口的实现类的对象(Set 接口对象), 不能存放重复的元素, 可以添加一个 null
        //3. set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
        //4. 注意:取出的顺序的顺序虽然不是添加的顺序,但是他的固定.
        Set set = new HashSet();
        set.add("john");
        set.add("lucy");
        set.add("john");//重复
        set.add("jack");
        set.add("hsp");
        set.add("mary");
        set.add(null);//
        set.add(null);//再次添加 null
        System.out.println("set="+set);
        //取10次
        for(int i = 0; i <10;i ++) {
            System.out.println("set=" + set);
        }

        //遍历可以使用迭代器
        System.out.println("====使用迭代器====");
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            System.out.println("obj="+obj);
        }
        //使用增强for
        System.out.println("====使用增强for====");
        for (Object o :set) {
            System.out.println("o="+o);
        }
        
        //set接口对象不能通过索引来获取,因此也就不能使用普通for循环遍历
        //删除
        set.remove("jack");
        System.out.println("set="+set);
    }
}

3. Set 接口实现类-HashSet   518

3.1 HashSet 的全面说明

1) HashSet实现了 Set接口

2) HashSet实际上是HashMap,看下源码. (图)

Set接口和常用方法+HashSet分析

3)可以存放null值,但是只能有一个null

4) HashSet不保证元素是有序的,取决于hash后,再确定索引的结果(即,不

保证存放元素的顺序和取出顺序一 致)

5)不能有重复元素/对象.在前面Set接口使用已经讲过

代码在com.stulzl.set_hashset.包中

Set_HashSet
package com.stulzl.set_hashset;

import java.util.HashSet;
import java.util.Set;

//HashSet 的全面说明  518
@SuppressWarnings({"all"})
public class Set_HashSet {
    public static void main(String[] args) {
        //解读
        //1. 构造器走源码
        /*
        public HashSet() {
        map = new HashMap<>();
        }
        */
        //2. HashSet 可以存放 null ,但是只能有一个 null,即元素不能重复
        //不能有重复元素/对象.在前面Set接口使用已经讲过
        Set hashSet = new HashSet();
        hashSet.add(null);
        hashSet.add(null);
        System.out.println("hashSet="+hashSet);
    }
}

4. HashSet案例说明  深度解析其特性518

代码在com.stulzl.hashset_exercise01.包中

HashSet_Exercise01

package com.stulzl.hashset_exercise01;

import java.util.HashSet;

//HashSet案例说明  518
@SuppressWarnings({"all"})
public class HashSet_Exercise01 {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        //说明
        //1. 在执行 add 方法后,会返回一个 boolean 值
        //2. 如果添加成功,返回 true, 否则返回 false

        System.out.println(set.add("john"));//T
        System.out.println(set.add("lucy"));//T
        System.out.println(set.add("john"));//F
        System.out.println(set.add("jack"));//T
        System.out.println(set.add("Rose"));//T
        //3. 可以通过 remove 指定删除哪个对象
        set.remove("john");
        System.out.println("set=" + set);//3个 set=[Rose, lucy, jack]

        //逐步深入研究HashSet特性
        set = new HashSet();
        System.out.println("set="+set);//0set=[]
        //4 Hashset 不能添加相同的元素/数据
        set.add("lucy");//添加成功
        set.add("lucy");//加入不了
        set.add(new Dog("tom"));//OK
        set.add(new Dog("tom"));//Ok
        System.out.println("set=" + set);//set=[Dog{name='tom'}, lucy, Dog{name='tom'}]

        //再次深入,非常经典的面试题
        set.add(new String("lzl"));//ok
        set.add(new String("lzl"));//不行,看源码分析可知
        System.out.println("set="+set);

    }
}
class Dog{
    private String name;

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

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

5. HashSet底层机制说明

5.1 模拟简单的数组+链表结构  519

➢分析HashSet底层是HashMap, HashMap底层是(数组+链表+红黑树)为了让大家真正理解,模拟简单的数组+链表结构

Set接口和常用方法+HashSet分析

代码在com.stulzl.hashset_structure.包中

HashSet_Structure
package com.stulzl.hashset_structure;

//模拟简单的数组+链表结构  519
@SuppressWarnings({"all"})
public class HashSet_Structure {
    public static void main(String[] args) {
        //模拟HashSet的底层(HashMap的底层结构)
        //1. 创建一个数组,数组的类型是 Node[]
        //2. 有些人,直接把 Node[] 数组称为 表
        Node[] table = new Node[16];
        System.out.println("table="+table);

        //创建一个节点将jonh放在索引为2的位置
        Node jonh = new Node("jonh",null);
        table[2] = jonh;//把jonh放在table表的索引为2的位置
        Node jack = new Node("jack", null);//在创建一个结点jack
        jonh.next = jack;//将jack结点挂载到jonh后面
        Node rose = new Node("Rose", null);
        jack.next = rose;//将Rose挂载到jack后面
        
        Node lucy = new Node("lucy", null);
        table[3] = lucy;//把Lucy放在table表的索引为3的位置
        System.out.println("table="+table);
    }
}
class Node{//结点 存储数据 可以指向下一个节点从而形成链表
    Object item;//存储数据
    Node next;//指向下一个结点

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }
}

5.2 HashSet底层分析  520-521-522

➢分析HashSet的添加元素底层是如何实现(hash() + equals())先说结论,再Debug源码+图解[前方高能,非战斗人员,做好撤退准备]

Set接口和常用方法+HashSet分析

1. HashSet底层是HashMap

2.添加一个元素时,先得到hash值会转成-->索引值

3.找到存储数据表table ,看这个索引位置是否已经存放的有元素

4.如果没有,直接加入

5.如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则挂载到最后面

6.在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小 > =MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)

代码在com.stulzl.hashset_source.包中

HashSet_Source
package com.stulzl.hashset_source;

import java.util.HashSet;

//HashSet底层原码分析  521-522  太复杂了,看视频解析吧
@SuppressWarnings({"all"})
public class HashSet_Source {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add("java");//到此位置,第1次add分析完毕.
        hashSet.add("php");//到此位置,第2次add分析完毕
        hashSet.add("java");
        System.out.println("set=" + hashSet);//set=[java, php]
        /*
        对HashSet 的源码解读
        1. 执行 HashSet()
            public HashSet() {
                map = new HashMap<>();
            }
        2. 执行 add()
           public boolean add(E e) {//e = "java"
                return map.put(e, PRESENT)==null;//(static) PRESENT = new Object();
           }
         3.执行 put() , 该方法会执行 hash(key) 得到key对应的hash值 算法h = key.hashCode()) ^ (h >>> 16)
             public V put(K key, V value) {//key = "java" value = PRESENT 共享
                return putVal(hash(key), key, value, false, true);
            }
         4.执行 putVal
         final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                Node[] tab; Node p; int n, i; //定义了辅助变量
                //table 就是 HashMap 的一个数组,类型是 Node[]
                //if 语句表示如果当前table 是null, 或者 大小=0
                //就是第一次扩容,到16个空间.
                if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;

                //(1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置
                //并把这个位置的对象,赋给 p
                //(2)判断p 是否为null
                //(2.1) 如果p 为null, 表示还没有存放元素, 就创建一个Node (key="java",value=PRESENT)
                //(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null)

                if ((p = tab[i = (n - 1) & hash]) == null)
                    tab[i] = newNode(hash, key, value, null);
                else {
                    //一个开发技巧提示: 在需要局部变量(辅助变量)时候,在创建
                    Node e; K k; //
                    //如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
                    //并且满足 下面两个条件之一:
                    //(1) 准备加入的key 和 p 指向的Node 结点的 key 是同一个对象
                    //(2)  p 指向的Node 结点的 key 的equals() 和准备加入的key比较后相同
                    //就不能加入
                    if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                        e = p;
                    //再判断 p 是不是一颗红黑树,
                    //如果是一颗红黑树,就调用 putTreeVal , 来进行添加
                    else if (p instanceof TreeNode)
                        e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
                    else {//如果table对应索引位置,已经是一个链表, 就使用for循环比较
                          //(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
                          //    注意在把元素添加到链表后,立即判断 该链表是否已经达到8个结点
                          //    , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
                          //    注意,在转成红黑树时,要进行判断, 判断条件
                          //    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
                          //            resize();
                          //    如果上面条件成立,先table扩容.
                          //    只有上面条件不成立时,才进行转成红黑树
                          //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break

                        for (int binCount = 0; ; ++binCount) {
                            if ((e = p.next) == null) {
                                p.next = newNode(hash, key, value, null);
                                if (binCount >= TREEIFY_THRESHOLD(8) - 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;
                //size 就是我们每加入一个结点Node(k,v,h,next), size++
                if (++size > threshold)
                    resize();//扩容
                afterNodeInsertion(evict);
                return null;
            }
         */
    }
}

6. 分析HashSet的扩容和转成红黑树机制  523-524

先说结论,代码演示补充说明threshold

1. HashSet底层是HashMap, 第次添加时,table 数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75 = 12

2.如果table数组使用到了临界值12,就会扩容到16*2 = 32,新的临界值就是32*0.75 = 24,依次类推

3.在Java8中,如果-条链表的元素个数到达TREEIFY_THRESHOLD(默认是8 ),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制

Set接口和常用方法+HashSet分析

代码在com.stulzl.hashset_increment.包中

HashSetIncrement

package com.stulzl.hashset_increment;

import javax.crypto.spec.PSource;
import java.util.HashSet;
import java.util.Objects;

//分析HashSet的扩容和转成红黑树机制  523-524
@SuppressWarnings({"all"})
public class HashSetIncrement {
    public static void main(String[] args) {
        /*  523
        HashSet底层是HashMap, 第一次添加时,table 数组扩容到 16,
        临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12
        如果table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,
        新的临界值就是 32*0.75 = 24, 依次类推
         */
        HashSet hashSet = new HashSet();
//        for (int i = 0; i <= 100; i++) {
//            hashSet.add(i);//1,2,3,4,5...100
//        }

         /*  523
        在Java8中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ),
        并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),
        否则仍然采用数组扩容机制

         */
//        for(int i =1;i<=12;i++){
//            hashSet.add(new A(i));
//        }
//        System.out.println("hashSet="+hashSet);

        /*  524
            当我们向hashset增加一个元素,-> Node -> 加入table , 就算是增加了一个size++
        */
        for(int i = 1; i <= 7; i++) {//在table的某一条链表上添加了 7个A对象
            hashSet.add(new A(i));//
        }

        for(int i = 1; i <= 7; i++) {//在table的另外一条链表上添加了 7个B对象
            hashSet.add(new B(i));//
        }
    }
}
class B{
    private int B;

    public B(int b) {
        B = b;
    }
    @Override
    public int hashCode() {
        return 200;
    }
}
class A{
    private int n;

    public A(int n) {
        this.n = n;
    }

    //重写hashcode方法,保证返回的hashcode值一样从而使数据再一条链表上
    @Override
    public int hashCode() {
        return 100;
    }
}

7. HashSet练习  525

重写hash值方法小提示Alt+Insert

Set接口和常用方法+HashSet分析

定义一个Employee类, 该类包含: private成员属性name,age要求:

1.创建3个Employee对象放入HashSet中

2.当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中

代码在com.stulzl.hashset_exercise01.包中

HashSet_Exercise01

package com.stulzl.hashset_exercise01;

import java.util.HashSet;
import java.util.Objects;

//HashSet练习 525
//定义一个Employee类, 该类包含: private成员属性name,age要求:
//1.创建3个Employee对象放入HashSet中
//2.当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
@SuppressWarnings({"all"})
public class HashSet_Exercise01 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("milan",18));//ok
        hashSet.add(new Employee("smith",18));//ok
        hashSet.add(new Employee("milan",18));//no
        
        //重写hash值方法之后
        System.out.println("hashSet="+hashSet);

    }
}
class Employee{
    private String name;
    private int age;

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

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

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //如果name和age相同则返回相同的hash值

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return age == employee.age &&
                Objects.equals(name, employee.name);
    }

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

8. HashSet练习2  526

//定义一个Employee类,该类包含: private成员属性name,sal,birthday(MyDate类型), 526

// 其中birthday为MyDate类型(属性包括: year, month, day),要求:

//1.创建3个Employee放入HashSet中

//2.当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中

代码在com.stulzl.hashset_exercise02.包中

HashSet_Exercise02

package com.stulzl.hashset_exercise02;

import java.util.HashSet;
import java.util.Objects;

//定义一个Employee类,该类包含: private成员属性name,sal,birthday(MyDate类型), 526
// 其中birthday为MyDate类型(属性包括: year, month, day),要求:
//1.创建3个Employee放入HashSet中
//2.当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
@SuppressWarnings({"all"})
public class HashSet_Exercise02 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("milan",1800,new MyDate(2020,5,3)));
        hashSet.add(new Employee("smith",1850,new MyDate(2021,5,3)));
        hashSet.add(new Employee("milan",1820,new MyDate(2020,5,3)));
        System.out.println("hashSet="+hashSet);
    }
}
class MyDate{
    private int year;
    private int month;
    private int day;

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

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

    @Override
    public String toString() {
        return  year + "-" + month + "-" + day;
    }

    //重写birthday的equals和hash值
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MyDate myDate = (MyDate) o;
        return year == myDate.year &&
                month == myDate.month &&
                day == myDate.day;
    }

    @Override
    public int hashCode() {
        return Objects.hash(year, month, day);
    }
}
class Employee{
    private String name;
    private double salary;
    private MyDate birthday;

    public Employee(String name, double salary, MyDate birthday) {
        this.name = name;
        this.salary = salary;
        this.birthday = birthday;
    }

    public String getName() {
        return name;
    }

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

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    public MyDate getBirthday() {
        return birthday;
    }

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

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                ", birthday=" + birthday +
                '}';
    }

    //重写name和birthday的equals和hash值,会涉及动态绑定
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return name.equals(employee.name) &&
                birthday.equals(employee.birthday);
    }

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

9. Set 接口实现类-LinkedHashSet  527

9.1 LinkedHashSet 的全面说明  527

1) LinkedHashSet是HashSet的子类

Set接口和常用方法+HashSet分析

2) LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双

向链表

3) LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。

4) LinkedHashSet不允许添重复元素

3.2 LinkedHashSet底层  528

Set接口和常用方法+HashSet分析

说明

1)在LinkedHastSet 中维护了一个hash表双向链表( LinkedHashSet有head和tail )

2)每一个节点有before和after属性,这样可以形成双向链表

3)在添加一个元素时,先求hash值,在求索引确定该元素在table的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样])

tail.next = newElement //示意代码

newElement.pre = tail

tail = newEelment;

4)这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致

代码在com.stulzl.linkedhashset_source.包中

linkedhashset_source
package com.stulzl.linkedhashset_source;

import java.util.LinkedHashSet;
import java.util.Set;

//LinkedHashSet_Source底层源码  528 看视频
@SuppressWarnings({"all"})
public class LinkedHashSet_Source {
    public static void main(String[] args) {
        //分析一下LinkedHashSet的底层机制
        Set set = new LinkedHashSet();
        set.add(new String("AA"));
        set.add(456);
        set.add(456);
        set.add(new Customer("刘", 1001));
        set.add(123);
        set.add("HSP");

        System.out.println("set=" + set);
        //解读
        //1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致
        //2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
        //3. LinkedHashSet 底层结构 (数组table+双向链表)
        //4. 添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry
        //5. 数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap$Entry类型
        /*
                //继承关系是在内部类完成.
                static class Entry extends HashMap.Node {
                    Entry before, after;
                    Entry(int hash, K key, V value, Node next) {
                        super(hash, key, value, next);
                    }
                }

         */
    }
}
class Customer {
    private String name;
    private int no;

    public Customer(String name, int no) {
        this.name = name;
        this.no = no;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "name='" + name + '\'' +
                ", no=" + no +
                '}';
    }
}

10. LinkedHashSet 课后练习题  529

Car类(属性:name,price),如果 name和price一一样,

则认为是相同元素,就不能添加。

代码在com.stulzl.linkedhashset_exercise01.包中

LinkedHashSet_Exercise01

package com.stulzl.linkedhashset_exercise01;

import java.util.LinkedHashSet;
import java.util.Objects;

//Car类(属性:name,price),如果 name和price一样,  529
//则认为是相同元素,就不能添加。
@SuppressWarnings({"all"})
public class LinkedHashSet_Exercise01 {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add(new Car("奥拓", 1000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//OK
        linkedHashSet.add(new Car("法拉利", 10000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了
        linkedHashSet.add(new Car("保时捷", 70000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了
        System.out.println("linkedHashSet="+linkedHashSet);

    }
}
class Car{
    private String name;
    private double price;

    public Car(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "\nCar{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }

    //重写hash值和equals方法
    //当 name 和 price 相同时, 就返回相同的 hashCode 值, equals返回t

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Double.compare(car.price, price) == 0 &&
                Objects.equals(name, car.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }
}
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月13日 0

暂无评论

推荐阅读
  TEZNKK3IfmPf   21天前   24   0   0 接口
TEZNKK3IfmPf