• Java 泛型


    目录

    一、泛型理解

    二、使用泛型

    三、向上转型(泛型和子类型)

    四、编写泛型

    4.1 、定义泛型类

    4.2 、定义泛型方法

    4.2.1、普通方法

    4.2.2、静态方法

     4.3 、定义泛型接口

    4.3.1、实现类方式一

    4.3.2、实现类方式二

    五、擦拭法

    六、extends 通配符

    6.1、小结

    七、super 通配符

    7.1、小结

    八、PECS原则

    九、无限定通配符

    9.1、小结


    一、泛型理解

            JDK 1.5 为 Java 编程语言引入了几个新的扩展。其中泛型就是其一。

            先看个示例

    1. import java.util.*;
    2. public class Main {
    3. public static void main(String[] args) {
    4. List myIntList = new LinkedList(); // 1
    5. myIntList.add(new Integer(0)); // 2
    6. Integer x = (Integer) myIntList.iterator().next(); // 3
    7. }
    8. }

            以上 //3 行代码是不是很烦人,添加进集合是Intger类型数据,遍历的时候却需要强制转型成Integer 类型。这是因为编程人员是知道添加和遍历是同种类型,但编译器只能保证迭代器返回Object,为了确保对Integer类型的变量的赋值是类型安全的,需要进行强制转换。

            程序员可以实际表达他们的意图,并将列表标记为受限制的,以包含特定的数据类型,这是泛型背后的核心思想。下面是上面使用泛型给出的程序片段的一个版本。

    1. import java.util.*;
    2. public class Main {
    3. public static void main(String[] args) {
    4. List<Integer> myIntList = new LinkedList<Integer>(); // 1'
    5. myIntList.add(new Integer(0)); // 2'
    6. Integer x = myIntList.iterator().next(); // 3'
    7. }
    8. }

            注意变量myIntList的类型声明。它指定这不是一个任意的List,而是一个Integer的List,写的List。我们说List是一个泛型接口,它接受一个类型参数——在本例中是Integer。我们还在创建列表对象时指定类型参数。还要注意,// 3 上的 强制转化(Integer) 已经消失了。
            现在,会认为所完成的只是把这些杂乱的东西移走。我们没有在 // 3 上强制转换为Integer,而是在 // 1 上使用Integer作为类型参数。然而,这里有一个非常大的区别。编译器现在可以在编译时检查程序的类型正确性。当我们用类型List声明myIntList时,这告诉了我们关于变量myIntList的一些信息,无论何时何地使用它,它都为真,编译器将保证这一点。

    二、使用泛型

            以下是java.util包中List和Iterator接口定义部分代码。

    1. public interface List <E> {
    2. void add(E x);
    3. Iterator iterator();
    4. }
    5. public interface Iterator<E> {
    6. E next();
    7. boolean hasNext();
    8. }

             这些代码应该都很熟悉,除了尖括号中的内容。它们是接口List和Iterator的形式类型参数的声明。
            类型参数可以在泛型声明中使用,基本上可以在使用普通类型的地方使用。看到了泛型类型声明List的调用,例如List。在调用中(通常称为参数化类型),所有出现的形式类型参数(示例中为E)都被实际的类型参数(示例中为Integer)替换。
            当我们定义泛型类型后,List的泛型接口变为强类型List

    1. import java.util.*;
    2. public class Main {
    3. public static void main(String[] args) {
    4. // 无编译器警告:
    5. List<Integer> list = new ArrayList<Integer>();
    6. list.add(1);
    7. list.add(2);
    8. // 无强制转型:
    9. Integer first = list.get(0);
    10. Integer second = list.get(1);
    11. }
    12. }

            定义泛型类型后,List的泛型接口变为强类型List

    1. import java.util.*;
    2. public class Main {
    3. public static void main(String[] args) {
    4. List<Number> list = new ArrayList<Number>();
    5. list.add(new Integer(123));
    6. list.add(new Double(12.34));
    7. Number first = list.get(0);
    8. Number second = list.get(1);
    9. }
    10. }

            当我们定义泛型类型后,List的泛型接口变为强类型List

    1. // 可以省略后面的Number,编译器可以自动推断泛型类型:
    2. List<Number> list = new ArrayList<>();

    三、向上转型(泛型和子类型)

              在Java标准库中的ArrayList实现了List接口,它可以向上转型为List

    1. public class ArrayList<T> implements List<T> {
    2. ...
    3. }
    4. List<String> list = new ArrayList<String>();

            即类型ArrayList可以向上转型为List

            要特别注意:不能把ArrayList向上转型为ArrayList或List

    1. import java.util.*;
    2. public class Main {
    3. public static void main(String[] args) {
    4. // 创建ArrayList<Integer>类型:
    5. ArrayList<Integer> integerList = new ArrayList<Integer>();
    6. // 添加一个Integer:
    7. integerList.add(new Integer(123));
    8. // “向上转型”为ArrayList<Number>
    9. ArrayList<Number> numberList = integerList; //编译不通过
    10. // 添加一个Float,因为Float也是Number
    11. numberList.add(new Float(12.34));
    12. // 从ArrayList<Integer>获取索引为1的元素(即添加的Float):
    13. Integer n = integerList.get(1); // ClassCastException!
    14. }
    15. }

            把一个ArrayList转型为ArrayList类型后,这个ArrayList就可以接受Float类型,因为Float是Number的子类。但是,ArrayList实际上和ArrayList是同一个对象,也就是ArrayList类型,它不可能接受Float类型, 所以在获取Integer的时候将产生ClassCastException。

            实际上,编译器为了避免这种错误,根本就不允许把ArrayList转型为ArrayList

            ArrayList和ArrayList两者完全没有继承关系。

    四、编写泛型

    4.1 、定义泛型类

            示例 1 

    1. public class Pair<T> {
    2. private T first;
    3. private T last;
    4. public Pair(T first, T last) {
    5. this.first = first;
    6. this.last = last;
    7. }
    8. public T getFirst() {
    9. return first;
    10. }
    11. public T getLast() {
    12. return last;
    13. }
    14. }

            示例 2 多个泛型类型

    1. public class Pair<K,V> {
    2. private K key;
    3. private V value;
    4. public Pair(K first, V last) {
    5. this.key = first;
    6. this.value = last;
    7. }
    8. public K getKey() {
    9. return key;
    10. }
    11. public void setKey(K key) {
    12. this.key = key;
    13. }
    14. public V getValue() {
    15. return value;
    16. }
    17. public void setValue(V value) {
    18. this.value = value;
    19. }
    20. }

    4.2 、定义泛型方法

    4.2.1、普通方法

    1. public class Person{
    2. /**
    3. * 泛型普通方法编写
    4. */
    5. public <T> void say(T t) {
    6. }
    7. }

    4.2.2、静态方法

    1. public class Person{
    2. public static void say(T t){
    3. }
    4. }

     4.3 、定义泛型接口

    1. public interface IM <T>{
    2. public void show(T name);
    3. }

    4.3.1、实现类方式一

            实现类可以指定泛型类型。

    1. public class Person implements IM<String> {
    2. @Override
    3. public void show(String name) {
    4. }
    5. }

    4.3.2、实现类方式二

            实现类可以指定泛型类型,相当于将该泛型T传递父类。

    1. public class Person implements IM{
    2. @Override
    3. public void show(T name) {
    4. }
    5. }

    五、擦拭法

            Java语言的泛型实现方式是擦拭法(Type Erasure)。

            所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

            一个泛型类Pair,这是编译器看到的代码:

    1. public class Pair<T> {
    2. private T first;
    3. private T last;
    4. public Pair(T first, T last) {
    5. this.first = first;
    6. this.last = last;
    7. }
    8. public T getFirst() {
    9. return first;
    10. }
    11. public T getLast() {
    12. return last;
    13. }
    14. }

            而虚拟机根本不知道泛型。这是虚拟机执行的代码:

    1. public class Pair {
    2. private Object first;
    3. private Object last;
    4. public Pair(Object first, Object last) {
    5. this.first = first;
    6. this.last = last;
    7. }
    8. public Object getFirst() {
    9. return first;
    10. }
    11. public Object getLast() {
    12. return last;
    13. }
    14. }

            Java使用擦拭法实现泛型,导致了:

            1、编译器把类型视为Object。

            2、编译器根据实现安全的强制转型。

            使用泛型的时候,我们编写的代码也是编译器看到的代码:

    1. public class Pair<T> {
    2. private T first;
    3. private T last;
    4. public Pair(T first, T last) {
    5. this.first = first;
    6. this.last = last;
    7. }
    8. public T getFirst() {
    9. return first;
    10. }
    11. public T getLast() {
    12. return last;
    13. }
    14. public static void main(String[] args) {
    15. Pair<String> p = new Pair<>("Hello", "world");
    16. String first = p.getFirst();
    17. String last = p.getLast();
    18. }
    19. }

            而虚拟机执行的代码并没有泛型:

    1. public class Pair<T> {
    2. private T first;
    3. private T last;
    4. public Pair(T first, T last) {
    5. this.first = first;
    6. this.last = last;
    7. }
    8. public T getFirst() {
    9. return first;
    10. }
    11. public T getLast() {
    12. return last;
    13. }
    14. public static void main(String[] args) {
    15. Pair p = new Pair("Hello", "world");
    16. String first = (String) p.getFirst();
    17. String last = (String) p.getLast();
    18. }
    19. }

            所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

            Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限:

            1、不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:

     Pair<int> p = new Pair<>(1, 2); // 编译不通过

            2、无法取得带泛型的Class。观察以下代码:

    1. public class Pair<T> {
    2. private T first;
    3. private T last;
    4. public Pair(T first, T last) {
    5. this.first = first;
    6. this.last = last;
    7. }
    8. public T getFirst() {
    9. return first;
    10. }
    11. public T getLast() {
    12. return last;
    13. }
    14. public static void main(String[] args) {
    15. Pair<String> p1 = new Pair<>("Hello", "world");
    16. Pair<Integer> p2 = new Pair<>(123, 456);
    17. Class c1 = p1.getClass();
    18. Class c2 = p2.getClass();
    19. System.out.println(c1==c2); // true
    20. System.out.println(c1==Pair.class); // true
    21. }
    22. }

            因为T是Object,我们对Pair和Pair类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。

            换句话说,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair

            3、无法判断带泛型的类型:

    1. Pair<Integer> p = new Pair<>(123, 456);
    2. // 编译错误
    3. if (p instanceof Pair<String>) {
    4. }

            原因和前面一样,并不存在Pair.class,而是只有唯一的Pair.class

            4、不能实例化T类型:

    1. public class Pair<T> {
    2. private T first;
    3. private T last;
    4. public Pair() {
    5. // 编译错误
    6. first = new T();
    7. last = new T();
    8. }
    9. }

            擦拭后实际上变成了:

    1. public class Pair<T> {
    2. private T first;
    3. private T last;
    4. public Pair() {
    5. // 编译错误
    6. first = new T(); // 擦拭后 first = new Object();
    7. last = new T(); // 擦拭后 last = new Object();
    8. }
    9. }

            不恰当的覆写方法

    1. public class Pair {
    2. public boolean equals(T t) {
    3. return this == t;
    4. }
    5. }

            这是因为,定义的equals(T t)方法实际上会被擦拭成equals(Object t),而这个方法是继承自Object的,编译器会阻止一个实际上会变成覆写的泛型方法定义。

            换个方法名,避开与Object.equals(Object)的冲突就可以成功编译:  

    1. public class Pair {
    2. public boolean same(T t) {
    3. return this == t;
    4. }
    5. }

    六、extends 通配符

            示例

    1. /**
    2. * 泛型类
    3. */
    4. class Pair<T> {
    5. private T first;
    6. private T last;
    7. public Pair(T first, T last) {
    8. this.first = first;
    9. this.last = last;
    10. }
    11. public T getFirst() {
    12. return first;
    13. }
    14. public T getLast() {
    15. return last;
    16. }
    17. /**
    18. * 直接运行
    19. * 不兼容的类型: Pair无法转换为Pair
    20. */
    21. public static void main(String[] args) {
    22. Pair<Integer> p = new Pair<>(123, 456);
    23. int n = add(p); // 编译失败
    24. System.out.println(n);
    25. }
    26. static int add(Pair<Number> p) {
    27. Number first = p.getFirst();
    28. Number last = p.getLast();
    29. return first.intValue() + last.intValue();
    30. }
    31. }

            直接运行提示:

            不兼容的类型: Pair无法转换为Pair

            原因:

            因为Pair不是Pair的子类,因此,add(Pair)不接受参数类型Pair
    但是从add()方法的代码可知,传入Pair是完全符合内部代码的类型规范,因为语句:

    1. Number first = p.getFirst();
    2. Number last = p.getLast();

            实际类型是Integer,引用类型是Number,没有问题。问题在于方法参数类型定死了只能传入Pair

            上界通配符就可以解决这个问题,既可以传入 Number 或者 Integer 类型。

            示例修改如下:

    1. /**
    2. * 泛型类
    3. */
    4. class Pair<T> {
    5. private T first;
    6. private T last;
    7. public Pair(T first, T last) {
    8. this.first = first;
    9. this.last = last;
    10. }
    11. public T getFirst() {
    12. return first;
    13. }
    14. public T getLast() {
    15. return last;
    16. }
    17. public static void main(String[] args) {
    18. Pair<Integer> p = new Pair<>(123, 456);
    19. int n = add(p);
    20. System.out.println(n);
    21. }
    22. /**
    23. *
    24. * 上界通配符使用, 泛型类型T的上界限定在Number了
    25. * 除了可以传入Pair类型,还可以传入Pair类型,Pair类型等,因为Double和BigDecimal都是Number的子类。
    26. * Pair p
    27. */
    28. static int add(Pair<? extends Number> p) {
    29. Number first = p.getFirst();
    30. Number last = p.getLast();
    31. return first.intValue() + last.intValue();
    32. }
    33. }

            修改之后,传入Pair类型时,它符合参数Pair类型。这种使用的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。
            除了可以传入Pair类型,我们还可以传入Pair类型,Pair类型等,因为Double和BigDecimal都是Number的子类。
            如果对Pair类型调用getFirst()方法,实际的方法签名变成了:

    <? extends Number> getFirst();

            即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量:

    Number x = p.getFirst();

            通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number)

    1. /**
    2. * 泛型类
    3. */
    4. class Pair<T> {
    5. private T first;
    6. private T last;
    7. public Pair(T first, T last) {
    8. this.first = first;
    9. this.last = last;
    10. }
    11. public T getFirst() {
    12. return first;
    13. }
    14. public T getLast() {
    15. return last;
    16. }
    17. public void setFirst(T first) {
    18. this.first = first;
    19. }
    20. public void setLast(T last) {
    21. this.last = last;
    22. }
    23. public static void main(String[] args) {
    24. Pair<Integer> p = new Pair<>(123, 456);
    25. int n = add(p);
    26. System.out.println(n);
    27. }
    28. static int add(Pair<? extends Number> p) {
    29. Number first = p.getFirst();
    30. Number last = p.getLast();
    31. /**
    32. * 编译错误
    33. * 原因在于擦拭法。如果我们传入的p是Pair<Double>,显然它满足参数定义Pair<? extends Number>,然而,Pair<Double>的setFirst()显然无法接受Integer类型。
    34. * 这就是<? extends Number>通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number)
    35. */
    36. // p.setFirst(new Integer(first.intValue() + 100));
    37. // p.setLast(new Integer(last.intValue() + 100));
    38. /**
    39. * 唯一的例外是可以给方法参数传入null
    40. */
    41. //p.setFirst(null);
    42. // p.getFirst().intValue();
    43. return first.intValue() + last.intValue();
    44. }
    45. }

            extends通配符的作用

            Java标准库的java.util.List接口,它实现的是一个类似“可变数组”的列表,主要功能包括:

    1. public interface List<T> {
    2. int size(); // 获取个数
    3. T get(int index); // 根据索引获取指定元素
    4. void add(T t); // 添加一个新元素
    5. void remove(T t); // 删除一个已有元素
    6. }

            自己定义一个泛型扩展方法:

    1. int sumOfList(List<? extends Integer> list) {
    2. int sum = 0;
    3. for (int i=0; i<list.size(); i++) {
    4. Integer n = list.get(i);
    5. sum = sum + n;
    6. }
    7. return sum;
    8. }

            方法参数类型是List而不是List,从sumOfList方法内部代码看,传入List或者List是完全一样的,但是,注意到List的限制:

    •         允许调用get()方法获取Integer的引用。
    •         不允许调用set(? extends Integer)方法并传入任何Integer的引用(null除外)。

            因此,方法参数类型List表明了该方法内部只会读取List的元素,不会修改List的元素(因为无法调用add(? extends Integer)、remove(? extends Integer)这些方法。换句话说,这是一个对参数List进行只读的方法(恶意调用set(null)除外)。

    6.1、小结

            表明:

    •         方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();
    •         方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);

            换句话说,使用extends通配符表示可以读,不能写。

            使用类似定义泛型类时表示:

            泛型类型限定为Number以及Number的子类。

    七、super 通配符

            查看如下代码

    1. void set(Pair<Integer> p, Integer first, Integer last) {
    2. p.setFirst(first);
    3. p.setLast(last);
    4. }

            传入Pair是允许的,但是传入Pair是不允许的。
    和extends通配符相反,这次,希望接受Pair类型,以及Pair、Pair,因为Number和Object是Integer的父类,setFirst(Number)和setFirst(Object)实际上允许接受Integer类型。

            这时候需要使用super通配符来改写这个方法:

    1. void set(Pair<? super Integer> p, Integer first, Integer last) {
    2. p.setFirst(first);
    3. p.setLast(last);
    4. }

            需要注意的是Pair表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。

    1. class Pair<T> {
    2. private T first;
    3. private T last;
    4. public Pair(T first, T last) {
    5. this.first = first;
    6. this.last = last;
    7. }
    8. public T getFirst() {
    9. return first;
    10. }
    11. public T getLast() {
    12. return last;
    13. }
    14. public void setFirst(T first) {
    15. this.first = first;
    16. }
    17. public void setLast(T last) {
    18. this.last = last;
    19. }
    20. static void setSame(Pair<? super Integer> p, Integer n) {
    21. p.setFirst(n);
    22. p.setLast(n);
    23. }
    24. public static void main(String[] args) {
    25. Pair<Number> p1 = new Pair<>(12.3, 4.56);
    26. Pair<Integer> p2 = new Pair<>(123, 456);
    27. setSame(p1, 100);
    28. setSame(p2, 200);
    29. System.out.println(p1.getFirst() + ", " + p1.getLast());
    30. System.out.println(p2.getFirst() + ", " + p2.getLast());
    31. }
    32. }

            Pair的setFirst()方法,它的方法签名实际上是:

    void setFirst(? super Integer);

            因此,可以安全地传入Integer类型。

            Pair的getFirst()方法,它的方法签名实际上是:

    ? super Integer getFirst();

            这里无法使用Integer类型来接收getFirst()的返回值,即下面的语句将无法通过编译:

    Integer x = p.getFirst();

            因为如果传入的实际类型是Pair,编译器无法将Number类型转型为Integer。
    注意:虽然Number是一个抽象类,我们无法直接实例化它。但是,即便Number不是抽象类,这里仍然无法通过编译。此外,传入Pair类型时,编译器也无法将Object类型转型为Integer。
    唯一可以接收getFirst()方法返回值的是Object类型:

    Object obj = p.getFirst();

    7.1、小结

            因此,使用通配符表示:

    •         允许调用set(? super Integer)方法传入Integer的引用。
    •         不允许调用get()方法获得Integer的引用。

            对比extends和super通配符

            作为方法参数,类型和类型的区别在于:

    •         允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外)。
    •         允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

            一个是允许读不允许写,另一个是允许写不允许读。

            类似通配符作为方法参数时表示:

    •         方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);;
    •         方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();。

            即使用super通配符表示只能写不能读。
            使用extends和super通配符要遵循PECS原则。

    八、PECS原则

            为了便于记忆,何时使用extends,super,我们可以用PECS原则:Producer Extends Consumer Super。

            即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。

            Java标准库的Collections类定义的copy()方法:

    1. import java.util.List;
    2. public class Collections {
    3. // 把src的每个元素复制到dest中:
    4. public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    5. for (int i=0; i<src.size(); i++) {
    6. T t = src.get(i); // src是producer
    7. // T t = dest.get(0); // 编译失败
    8. // src.add(t); // 编译失败
    9. dest.add(t); // dest是consumer
    10. }
    11. }
    12. }

            它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List,表示目标List,第二个参数List,表示要复制的List。可以简单地用for循环实现复制。在for循环中,可以看到,对于类型的变量src,我们可以安全地获取类型T的引用,而对于类型的变量dest,我们可以安全地传入T的引用。

            copy()方法的定义就完美地展示了extends和super的意图:

    •         copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用。
    •         copy()方法内部也不会修改src,因为不能调用src.add(T)。

    九、无限定通配符

            作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?

    1. void sample(Pair p) {
    2. }

            因为通配符既没有extends,也没有super,因此:

    •         不允许调用set(T)方法并传入引用(null除外);
    •         不允许调用T get()方法并获取T引用(只能获取Object引用)。

            换句话说,既不能读,也不能写,那只能做一些null判断:

    1. static boolean isNull(Pair<?> p) {
    2. return p.getFirst() == null || p.getLast() == null;
    3. }

            大多数情况下,可以引入泛型参数消除通配符:

    1. static <T> boolean isNull(Pair<T> p) {
    2. return p.getFirst() == null || p.getLast() == null;
    3. }

            通配符有一个独特的特点,就是:Pair是所有Pair的超类:

    1. class Pair<T> {
    2. private T first;
    3. private T last;
    4. public Pair(T first, T last) {
    5. this.first = first;
    6. this.last = last;
    7. }
    8. public T getFirst() {
    9. return first;
    10. }
    11. public T getLast() {
    12. return last;
    13. }
    14. public void setFirst(T first) {
    15. this.first = first;
    16. }
    17. public void setLast(T last) {
    18. this.last = last;
    19. }
    20. public static void main(String[] args) {
    21. Pair<Integer> p = new Pair<>(123, 456);
    22. Pair<?> p2 = p; // 安全地向上转型
    23. System.out.println(p2.getFirst() + ", " + p2.getLast());
    24. }
    25. }

    9.1、小结

            无限定通配符很少使用,可以用替换,同时它是所有类型的超类。

  • 相关阅读:
    基于Spring Boot的体育馆管理系统的设计与实现
    Hutool 工具类之日期时间工具-DateUtil mysql日期字段
    英国入境前需要准备什么?
    yolov3原理记录
    【瑞吉外卖】day07:新增套餐、套餐分页查询、 删除套餐
    扬帆际海—跨境电商怎么做才好?
    【Redis】源码分析、redis模块扩展、redisbloom
    git代码管理工具使用全流程
    CentOS 7 mysql 安装以及常用语句(select、update、alter、rename、drop等)速查
    14:00面试,14:06就出来了,问的问题有点变态。。。
  • 原文地址:https://blog.csdn.net/u012965203/article/details/128138702