前言

数组(Array)的特性之一是其长度不可变,需要开发者对数组扩容,开发者操作索引重新组织数据,比较浪费时间和精力;(面向过程思想)

使用集合存储数据会自动进行空间扩容和索引重组,不需要开发者关心底层存储机制,所以使用起来比较方便。(面向对象思想)

但是数组既可以保存基本数据类型也可以保存引用数据类型,而集合只能保存引用数据类型(受泛型只能是引用数据类型的限制);(注意)

 

如图上所示:Collection接口是List和Set实现类的顶层接口 ,Collection的实现类创建的对象存储数据的特点是一次存储1个数据,我们称Collection下的实现类为单列集合

Map接口和Collection接口无关;

如下图所示:Map接口是HashMap实现类的父接口,LinkedHashMap向上继承了HashMap,Map下的实现类创建出来的对象存储数据的特点是一次存储2个数据,我们称为双列集合

  

一、Collection单列集合顶层接口

java.util.Collection接口是Java中所有单列集合的根接口,Collection接口下面所有子接口,以及子接口下面的实现类拥有Collection接口中定义的所有方法;

1.Collection接口常用方法

//想要学习学习Collection集合接口中方法,可以使用多态让Collection接口引用 指向子类对象
        //Collection下面的实现类:ArrayList肯定实现了Collection接口中的方法,编左看右就不能调佣ArrayList实现类的特有方法;
        Collection<String> coll = new ArrayList<>();

        //1.add()添加元素到集合的末尾
        coll.add("宋公明");
        coll.add("黑旋风");
        coll.add("公孙胜");
        coll.add("孙二娘");

        //2.addAll():方法传入1个单列集合实现类对象作为参数,将传入的单列集合中所有内容添加到当前集合中
        ArrayList<String> strList = new ArrayList();
        strList.add("宋万");
        strList.add("宋清");
        LinkedList<String> strLinkedList = new LinkedList<>();
        strLinkedList.add("阮小五");
        strLinkedList.add("阮小六");
        strLinkedList.add("阮小七");
        coll.addAll(strList);
        coll.addAll(strLinkedList);
        System.out.println(coll);

        //3.remove():尝试删除集合元素
        coll.remove("宋万");
        System.out.println(coll);

        //4.contains():判断集合中是否包含指定元素
        boolean isContains = coll.contains("宋万");
        System.out.println(isContains);

        //5.size():获取集合的长度
        //6.clear():清空集合中所有元素
        //7.isEmpty():判断当前集是否是空集合

        //8.toArray():将集合转换成1个对象数组
        //通过将集合中每一个元素通过多态的方式向上转型为1个Object对象引用,存储到1个Object[]数组中
        Object[] objectArray = coll.toArray();
        for (int i = 0; i < objectArray.length; i++) {
            String str = (String) objectArray[i];//向下转型
            System.out.println("本次向下转型获取到的String内容:" + str);
        }

        //9.toArray(对应类型的数组):将集合转换成1个指定数据类型的数组(不需要向下转型)
        String[] stringArray = coll.toArray(new String[0]);
        for (int i = 0; i < stringArray.length; i++) {
            String item = stringArray[i];//不需要向下转型
            System.out.println("本次获取到的String内容:" + item);
        }

    }
View Code

 

2.Collection接口iterator方法

迭代器是所有单列集合通用的遍历方式;

Collection接口中定义了1个iterator方法,通过集合实现类对象的iterator()方法,可获取1个和当前的集合对应的迭代器对象;

这个迭代器对象是Iterator迭代器接口实现类实例出来的对象,实现了迭代器协议固拥有hasNext()和next()方法;

由于List接口实现类有索引可以实现遍历,但是Set接口实现类中没有索引,所以需要利用迭代器的hasNext()和next()方法完成无索引集合的遍历

 

二、List接口

List接口继承自Cellection接口的同时又扩展了如下功能;

  • List集合中元素是有序的
  • List集合允许存储重复元素
  • List集合中每个元素具有索引

java.util.List接口下面有ArrayList和LikedList两个实现类,这两个实现类的后缀类名都是List,具备所有List接口的特点;

 

1.List接口常用方法

public static void main(String[] args) {
        //List接口有序带有索引,可以保存重复元素
        //通过多态的方式创建List接口引用指向实现类对象
        List<String> strList = new ArrayList<>();
        //List接口之所以具备索引的概念是由于里面提供了可以使用索引操作集合元素的方法
        //1.add():    添加元素到末尾
        strList.add("刘备");
        strList.add("关羽");
        strList.add("张飞");
        strList.add("赵云");
        strList.add("马超");
        strList.add("黄忠");
        System.out.println(strList);
        //2:add(索引,元素):添加元素到指定索引位置
        strList.add(3, "司马炎");
        //3.remove(): 删除指定元素
        strList.remove("戏志才");
        //4.set(索引,指定元素):     将指定元素处的元素替换为参数元素
        strList.set(3, "司马师");
        System.out.println(strList);
        strList.set(3, "司马昭");
        //5.get(索引):     获取指定索引处的索引
        String hero = strList.get(3);
        System.out.println(hero);
    }
View Code

 

二、List接口实现类

List接口下有ArrayList和LikedList两个实现类;

1.ArrayList实现类

ArrayList集合数据存储基于数组,其特点是元素增删操作慢、查找快、线程不安全,运行速度快;

 

2.LinkedList实现类

LinkedList集合数据存储基于双向链表,其优势是元素增删操作慢、查询慢(从头开始找到下一个,在到下一个.....)。

  //操作头元素和尾元素是LikeList特有的方法,不需要通过多态创建对象
        LinkedList<String> strLikedList = new LinkedList<>();
        //由于LikedList是list接口的实现类,那么LinkedList具备List接口中定义的所有方法;
        strLikedList.add("孙悟空");
        strLikedList.add("猪悟能");
        strLikedList.add("沙悟净");
        strLikedList.add("唐三藏");
        strLikedList.add("虎力大仙");
        strLikedList.add("鹿力大仙");
        strLikedList.add("羊力大仙");
        //LikedList的特有方法
        //1.addFirst:添加头元素
        strLikedList.addFirst("金角大王");
        System.out.println(strLikedList);
        //2.getFirst:获取头元素
        String firstElement = strLikedList.getFirst();
        System.out.println("获取到的头元素" + firstElement);
        //3.removeFirst():删除头元素并返回
        firstElement = strLikedList.removeFirst();
        System.out.println("被删除的头元素" + firstElement);
        //4.addLast():添加元素到集合末尾
        System.out.println(strLikedList);
        strLikedList.addLast("车迟国王");
        System.out.println(strLikedList);
        //5.getLast():获取集合中的尾部元素
        String lastElement = strLikedList.getLast();
        System.out.println(lastElement);
        //6.removeLast():删除集合中的尾元素并返回
        lastElement = strLikedList.removeLast();
        System.out.println(lastElement);

        //7.push():将1个元素添加到链表的头节点
        strLikedList.push("银角大王");
        System.out.println("push方法把元素push到链表的头节点" + strLikedList);
        //8.pop():
        String popedElelment=strLikedList.pop();
        System.out.println("poped元素是"+popedElelment);
LikedList集合特有方法

 

三、Set接口

想较于List接口下的实现类ArrayList和LikedList来说,Set接口下的实现类HashSet、LinkedHashSet 具备去重功能

 

四、Set接口实现类

HashSet向上实现了Set接口,LinkedHashSet又继承了HashSet实现类扩展了元素顺序存取功能。

TreeSet向上实现了Set接口。

 

1.HashSet实现类

HashSet实现了Set接口,元素不重复并且是无序的;

 

1.2.LikedHashSet实现类

LikedHashSet实现类继承自HashSet实现类,除了具备对集合内的元素进行去重之外,还具备元素存取排序功能

 

2.TreeSet实现类: 

底层结构通过红黑树而不是hash表

通过自定义排序规则来保证元素不重复

保存元素的时候可以按照指定排序规则

package com.itheima.set;

import com.itheima.entity.Employee;
import com.itheima.entity.Student;

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

//TreeSet
public class setDemo5 {
    public static void main(String[] args) {
        //TreeSet:保证元素唯一的方式不是哈希表,底层是红黑树,还可以对元素进行排序
        //在创建TreeSet对象时,就要讲自定义的排序规则作为参数传递。
        TreeSet<Employee> employeeTreeSet = new TreeSet<>(new Comparator<Employee>() {
            //添加的元素和已存在的元素通过 元素的Compare()方法对比,结果为0的时候,会认为元素重复;
            //年龄相同的对象将无法存储到当前TreeSet中
            @Override
            public int compare(Employee o1, Employee o2) {
                return o1.getAge() - o2.getAge();
            }
        });
        //TreeSet如何认为元素重复?条件:
        // 添加的元素和已存在的元素通过 元素的Compare()方法对比,结果为0的时候,会认为元素重复;
        Employee e1 = new Employee("甄嬛", 30);
        Employee e2 = new Employee("甄嬛", 30);
        Employee e3 = new Employee("安陵容", 28);
        Employee e4 = new Employee("华妃", 32);
        Employee e5 = new Employee("果郡王", 30);
        Employee e6 = new Employee("浣碧", 30);
        Collections.addAll(employeeTreeSet, e1, e2, e3, e4, e5);

        //迭代器是所有集合的通用方法
        for (Employee employee : employeeTreeSet) {
            System.out.println(employee.getName());
        }

        //解决:TreeSet排序规则比较苛刻的方法
        /*
        * 1.可以先通过HashSet去除掉内容重复的元素,再转换为ArrayList,通过Collections.Sort()排序
        * 2.给元素添加唯一项(将唯一项作为排序的次要条件)
        * */

    }


}
TreeSet实现类

 

五、Map双列集合顶层接口

Map是一个接口,是Java提供好的一种保存数据的方式,Map下的实现类存储数据的特点是一次存储2个数据,所以继承Map的实现类的对象称为双列集合;

Map接口和Collection单列集合的顶层接口无关,由于单列集合一次保存1个数据,双列集合一次保存2个数据,我们称Map为双列集合;

 

六、Map实现类

hashMap向上实现了Map接口,TreeMap又继承了HashSet类扩展了键值对顺序存取功能

 

1.HashMap实现类

HashMap实现类实现了Map接口,底层基于hash表的结果,键是唯一的,所以HashMap是无序的,但查询效率高。

      //1.HashMap:无序的
        Map map = new TreeMap();
        map.put("zay", "周杰伦");
        map.put("wf", "汪峰");
        map.put("tz", "陶喆");
        map.put("zbc", "周笔畅");
        //{tz=陶喆, wf=汪峰, zay=周杰伦, zbc=周笔畅}
        System.out.println(map);

 

2.TreeMap实现类

TreeMap底层存储数据的机制是hash表+链表,所以键是唯一的,存储有序,但查询效率低。

     //2.TreeMap:TreeMap会自动根据key排序
        Map map1 = new TreeMap();
        map1.put(11, "周杰伦");
        map1.put(22, "蔡徐坤");
        map1.put(33, "六小灵通");
        map1.put(44, "汪峰");
        //{11=周杰伦, 22=蔡徐坤, 33=六小灵通, 44=汪峰}
        System.out.println(map);     

 

3.遍历Map的3种方式

HashMap和TreeMap都实现了Map接口,所以两者可调用的方法也一致;

Map有3种遍历方式分别如下:

A.纵向遍历Map:通过键找值的方式遍历map

//遍历方式1.纵向遍历map:通过键找值的方式遍历map
public class MapDemo4 {
    public static void main(String[] args) {
        HashMap<String, Integer> nameMap = new HashMap<>();
        nameMap.put("赵四", 43);
        nameMap.put("王大拿", 53);
        nameMap.put("谢广坤", 33);
        nameMap.put("六小龄童", 73);
        nameMap.put("徐晓庆", 63);
        //获取map中所有的key,通过key找值的方式,纵向遍历map集合。
        Set<String> keys = nameMap.keySet();
        for (String key : keys) {
            Integer value = nameMap.get(key);
            System.out.println(value);
        }

    }
}
纵向遍历map

B.横向遍历Map:Map转换成entrySet再通过遍历键值对对象(entry)来获取键和值

//遍历方式2横向遍历:通过键值对对象获取键和值
public class MapDemo5 {
    public static void main(String[] args) {
        HashMap<String,Integer> nameMap=new HashMap<>();
        nameMap.put("张三",23);
        nameMap.put("李四",24);
        nameMap.put("王五",25);
        nameMap.put("田六",26);

        //1,通过map集合调用entrySet()方法获取到键值对对象组成的Set集合
        Set<Map.Entry<String, Integer>> entries = nameMap.entrySet();
        //2.遍历entrySet获取到每一个键值对对象
        for (Map.Entry<String, Integer> entry : entries) {
            //3.通过entry对象获取键和值
            String key=entry.getKey();
            Integer value=entry.getValue();
            System.out.println("本次循环获取到的key: "+key+" value:"+value );
        }

    }
}
横向遍历

C.函数式遍历:Java 8新增了函数式编程思想:forEach让开发人员专注于每个元素的使用逻辑?而不是关注怎么把元素获取到?

不仅是双列集合,所以单列集合List/Set接口都实现了forEach方法

//遍历方式3函数式遍历:forEach循环遍历Map
public class MapDemo7 {
    public static void main(String[] args) {
        HashMap<String, Integer> nameMap = new HashMap<>();
        nameMap.put("张三", 23);
        nameMap.put("李四", 24);
        nameMap.put("王五", 25);
        nameMap.put("田六", 26);
        //Java 8新增了函数式编程思想:forEach让开发人员专注于每个元素的使用逻辑?而不是关注怎么把元素获取到?
        nameMap.forEach(new BiConsumer<String, Integer>() {
            @Override
            public void accept(String key, Integer value) {
                System.out.println("本次获取到的key:" + key + " value:" + value);
            }
        });

        //不仅是双列集合,所以单列集合List/Set接口都实现了forEach方法
        Set<String> keySet = nameMap.keySet();
        keySet.forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        });

        Collection<Integer> values = nameMap.values();
        values.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer value) {
                System.out.println(value);
            }
        });


    }
}
forEach方法遍历

 

七、Collections工具类

Collections是单列集合的工具类,工具类提供了很多操作单列集合的静态方法;

我们主要使用Collections工具类,对集合进行排序、求极值;

 

1.Collections常用静态方法

package CollectionTools;

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

public class Test01 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();
        list.add(132);
        list.add(2019);
        list.add(8866);
        System.out.println(list);

        //求最大和最小值
        Integer maxNumber = Collections.max(list);
        Integer minNumber = Collections.min(list);
        System.out.println(maxNumber);
        System.out.println(minNumber);

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

        //洗牌
        Collections.shuffle(list);
        System.out.println(list);

        //1次性在列表中添加多个值
        Collections.addAll(list, 1, 2, 3, 3);
        System.out.println(list);

    }
}

 

2.Collections排序

Collections可以对实现了List接口的数据类型进行自定义排序;

 Collections.sort(entryList, new Comparator<Map.Entry<String, Student>>() {
            @Override
            public int compare(Map.Entry<String, Student> o1, Map.Entry<String, Student> o2) {
                return o1.getValue().getAge() - o2.getValue().getAge();
            }
        });

 

八、泛型

泛型的本质:帮助程序员在类/方法/接口中提前预支地使用未知的数据类型

 泛型的好处:

  • 避免强转错误的发生
  • 运行期异常提前到编译器
  • 代码模板化
  • 把数据类型当做参数进行传递

 

1.为什么要引入泛型?

由于集合没有对集合中元素做任何数据类型限定,任意数据类型的元素都可以存放进集合中;

当集合中的元素存放进去之后,默认提升为Object类型;

当我们取出每一个Object类型对象时,必须采用强制类型转换,操作不慎就出现ClassCastException(类型转换异常)

Collection coll = new ArrayList();
        coll.add("abc");
        coll.add("张根");
        coll.add(1);
        //由于集合没有对集合中元素做任何限定,任意类型都可以存放在其中;
        //当集合中的元素存放之后默认提升为Object类型
        //当我们取出每一个Object类型对象时,必须采用强制类型转换;
        Iterator iterator = coll.iterator();
        while (iterator.hasNext()) {
            //需要打印每个字符串的长度,就需要把迭代出来的对象转成字符串String类型。
            String str = (String) iterator.next();
            System.out.println(str);
        }
        //问题当我们以上代码就会出现ClassCastException(类型转换异常)
        System.out.println(coll);
        //解决问题Collection虽然可以存储各种对象,但实际上通常Collection只会存储同一类型的对象
        //因此JDK5之后,新增了泛型(Generic)语法,让你在设计API时可以指定类、方法支持泛型。
        //这样我们使用API的时候也会变得简介,并通过编译时期的语法检查
        //泛型的本质:帮助程序员在类/方法/接口中提前预支地使用未知的数据类型
        //特点:代码模板化、把数据类型当做参数进行传递

泛型可以在类、方法、接口中进行使用;

 

2.泛型类

(1).泛型类的声明

//泛型类格式:class 类名<泛型名称>{}
class ArrayList01<E> {
    public void add(E e) {
        System.out.println(e);
    }
}

 

(2).泛型类中如何使用这个泛型?

泛型:本质上是一个位置的数据类型,不是1个变量也不是1个值,而是1个数据类型。

泛型能使用的地方也就是在能用到数据类型的地方;

成员变量的数据类型

成员方法的形参数据类型

成员方法返回值的数据类型

  //1.把Box类变成1个泛型类
class Box<T> {
    private T data;

    public Box() {
    }

    /*
    2.如果要存储这个数据类型,应该提供1个set方法,现在不明确返回值(data)的具体的数据类型,
    可以使用1个预支的数据类型(泛型)T来作为data的数据类型,作为返回值的数据类型;
    */
    public T getData() {
        return data;
    }

    /*
    3.如果要存储这个数据类型,应该提供1个set方法,现在不明确形参(data)的数据类型,
    可以使用T预支的数据类型(泛型)T来作为data的数据类型,作为形参的的数据类型;
    */
    public void setData(T data) {
        this.data = data;
    }
}  
泛型类

 

(3).泛型类声明了泛型什么时候进行具体类型确定?

泛型类创建对象的时候,会指定泛型的具体数据类型。

        Box<String> box1 = new Box<String>();
        box1.setData("遥控器");
      //box1.setData(222)  ×;
        String data1 = box1.getData();
        System.out.println(data1);

        Box<Integer> box2 = new Box<Integer>();
        box2.setData(666);
        Integer data2 = box2.getData();
        System.out.println(data2);

 

3.泛型方法

泛型方法:分为静态泛型方法和成员泛型方法;

泛型方法:可以保证数组/集合中元素的数据类型必须一致;

泛型方法的泛型什么时候确定时机

(1).泛型方法的声明

/*
     * 声明1个静态方法,该方法可以给1个ArrayList集合添加元素
     * 在编写静态方法的时候并不知道ArrayList集合的泛型是什么,要添加元素的数据类型是什么?
     * 需要在静态方法中进行泛型的声明
     * */
    public static <E> ArrayList<E> addElement(ArrayList<E> arrayList, E element) {
        arrayList.add(element);
        return arrayList;
    }

    /*
     * 声明1个成员方法,该方法可以给1个数组添加元素
     * 在编写成员方法的时候并不知道数组的泛型是什么,要添加元素的数据类型是什么?
     * 需要在成员方法中进行泛型的声明
     * */
    public <T> T[] addElement(T[] array, T element) {
        //T现在虽然是1个未知的数据类型,但是并不能使用T去创建对象
        //T[] array1 =new T[1];  × 因为无法确定实际的数据类型一定有无参构造方法
        //但是可以进行强制类型转换,作为强转的出现; √
        T[] newArray = (T[]) new Object[array.length + 1];
        System.arraycopy(array, 0, newArray, 0, array.length);
        newArray[newArray.length - 1] = element;
        return newArray;

    }

 

(2).泛型方法的泛型什么时候确定?

泛型方法的泛型会在调用方法的时候确定,但并非显示确定的;

只需要传递对应数据类型的数据,就会默认分析数据类型作为泛型的类型;

        //泛型方法的泛型什么时候确定时机
        //调用静态泛型方法:
        ArrayList<String> strList = new ArrayList<>();
        addElement(strList, "元素"); //静态方法的调用
        System.out.println("调用了addElement方法后,strList内容是:" + strList);
        //调用成员泛型方法
        GenericDemo2 g2 = new GenericDemo2();
        Integer[] arr = {10, 20, 30};
        Integer[] resultArr = g2.addElement(arr, 40);
        System.out.println(resultArr);

 

4.泛型接口

泛型接口就是我们在定义接口时,要使用到1个未知的数据类型;我们可以通过泛型预先使用这个数据类型;

(1).泛型在接口中的声明

interface Machine<K> {
}

 

(2).泛型在接口中的使用

泛型在接口中只能作为抽象方法的参数/返回值的数据类型存在。

interface Machine<K> {
    //在接口中定义抽象方法的时候,发现不能明确返回值的类型是什么?
    //可以使用泛型
    K careae(K k);

}

 

(3).泛型在接口中的泛型什么时候确定

A.当实现类实现类实现接口的时候,明确泛型的具体数据类型,那么可以在实现的时候指定泛型的具体类型;

//(1).实现类实现接口的时候就能明确具体泛型的数据类型
class IceCreamMachine implements Machine<String> {

    @Override
    public String careae(String o) {
        return "冰淇淋机器做了1个草莓冰淇淋!";
    }
}

B.当实现类实现泛型接口的时候依然不能明确具体泛型的数据类型,那么实现类必须是1个泛型类//只能等实现类创建对象的时候才能明确具体的数据类型

//(2).当实现类实现泛型接口的时候依然不能明确具体泛型的数据类型,那么实现类必须是1个泛型类
//只能等实现类创建对象的时候才能明确具体的数据类型
class AMachine<K> implements Machine<K> {
    @Override
    public K careae(K k) {
        return k;
    }
}

 

5.泛型通配符

泛型:    是指数据类型是未知的;

通配符:是指数据类型明确了,但该数据类型的泛型是未知;(有些数据类型中也包含泛型;例如ArrayList<?>);

例如:

public static void show(ArrayList<?> list){
    for (Object o : list) {
        System.out.println(o);
    }
}
 

当方法的参数是T,代表可以传递任意数据类型的参数。

当方法的参数是ArrayList<?>,也就是尖括号里面是问号时,代表这个参数里面保存的元素的数据类型是任意的

 通配符只能用于参数的声明,不能用于数据的创建.

 

6.泛型的上限和下限

泛型的上限和下限可以帮助我们设定泛型的要求;

如果使用?作为参数的泛型通配符,可以接收任意数据类型的集合。

但是使用?通配符的弊端是,真正在方法中使用参数的时候,只能使用参数继承自Object类共性内容的,无法使用该参数的特有方法

  格式 意义
泛型的上限 类型名称<? extends 类> 对象名称 只能接收该类型及其子类
泛型的下限 类型名称<? super 类> 对象名称 只能接收该类型及其父类

 

package com.itheima.generic;

import java.util.ArrayList;

public class GenericDemo5 {
    public static void main(String[] args) {
        ArrayList<Benz> benzList = new ArrayList<>();
        ArrayList<Bmw> bmwlist = new ArrayList<>();
        ArrayList<Dog> dogList = new ArrayList<>();

        show(benzList);
        show(bmwlist);
        show(dogList);


    }

    //如果使用?作为参数的泛型通配符,可以接收任意数据类型的集合
    //使用?通配符的弊端是,真正在方法中使用list的时候,只能使用Object类共性内容;

    public static void show(ArrayList<? extends Car> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }
}


class Car {

}

class Benz extends Car {

}

class Bmw extends Car {

}

class Dog {

}
泛型的上限

 --------------------

 public static void main(String[] args) {
        ArrayList<Benz> benzList = new ArrayList<>();
        ArrayList<Bmw> bmwlist = new ArrayList<>();
        ArrayList<Dog> dogList = new ArrayList<>();

        show(benzList);
        show(bmwlist);
        show(dogList);


    }

    //如果使用?作为参数的泛型通配符,可以接收任意数据类型的集合
    //使用?通配符的弊端是,真正在方法中使用list的时候,只能使用Object类共性内容;
    
    public static void show(ArrayList<? super Benz> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }
}


class Car {

}

class Benz extends Car {

}

class Bmw extends Car {

}

class Dog {

}
泛型的下限

 

 

 

 

 参考

posted on 2021-11-20 18:56  Martin8866  阅读(96)  评论(0编辑  收藏  举报