Java-泛型

文章出处:

参考+摘抄: https://www.yuque.com/qingkongxiaguang/javase/rk6if6#dcab57b7

青空の霞光

image-20230104163636308

泛型类

泛型其实就一个待定类型,我们可以使用一个特殊的名字表示泛型,泛型在定义时并不明确是什么类型,而是需要到使用时才会确定对应的泛型类型。

我们可以将一个类定义为一个泛型类

package cn.meowrain.generic;

public class Main {
    public static void main(String[] args) {
        Score<String> score01 = new Score<String>("meow",13,"优秀");
        String value01 = score01.score;
        System.out.println(value01); // 优秀
        Score<Integer> score02 = new Score<Integer>("meowrain",18,100);
        int value02 = score02.score;
        System.out.println(value02); // 100

    }
}
class Score<T> {//泛型类需要使用<>,我们需要在里面添加1 - N个类型
    String name;
    int age;
    T score;//T会根据使用时提供的类型自动变成对应类型

    public Score(String name, int age, T score) {
        //这里T可以是任何类型,但是一旦确定,那么就不能修改了
        this.name = name;
        this.age = age;
        this.score = score;
    }
}

泛型将数据类型的确定控制在了编译阶段,在编写代码的时候就能明确泛型的类型,如果类型不符合,将无法通过编译!因为是具体使用对象时才会明确具体类型,所以说静态方法中是不能用的:

只不过这里需要注意一下,我们在方法中使用待确定类型的变量时,因为此时并不明确具体是什么类型,那么默认会认为这个变量是一个Object类型的变量,因为无论具体类型是什么,一定是Object类的子类:

image-20230104124334991

我们可以对其进行强制类型转换,但是实际上没多大必要:

public void test(T t){
    String str = (String)t; //向下转型,Object转型为String类型
}

因为泛型本身就是对某些待定类型的简单处理,如果都明确要使用什么类型了,那大可不必使用泛型。还有,不能通过这个不确定的类型变量就去直接创建对象和对应的数组:

image-20230104125217426

注意,具体类型不同的泛型类变量,不能使用不同的变量进行接收:image-20230104125539437

?通配符

如果要让某个变量支持引用确定了任意类型的泛型,那么可以使用?通配符:

package cn.meowrain.generic;


public class Main {
    public static void main(String[] args) {
        Test<?> test = new Test<>("meowrain",18,98.5);
        System.out.println(test.name + " " + test.age + " " + test.score);
    }
}
class Test<T> {
    T name;
    T age;
    T score;

    public Test(T name, T age, T score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
}

定义多个泛型变量

当然,泛型变量不止可以只有一个,如果需要使用多个的话,我们也可以定义多个:

package cn.meowrain.generic;

import cn.meowrain.Poly.poly04.Test;

public class Main {
    public static void main(String[] args) {
        Score<String,Integer,Double> score = new Score<>("meowrain",17,98.5);

    }
}
class Score<A,B,C> {//泛型类需要使用<>,我们需要在里面添加1 - N个类型
    A name;
    B age;
    C score;//T会根据使用时提供的类型自动变成对应类型

    public Score(A name, B age, C score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
}

只要是在类中,都可以使用类型变量:

public class Test<T>{
    
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

注意事项

只不过,泛型只能确定为一个引用类型,基本类型是不支持的:

image-20230104154133059

如果要存放基本数据类型的值,我们只能使用对应的包装类:

package cn.meowrain.generic;


public class Main {
    public static void main(String[] args) {
        Test<Integer> test = new Test<Integer>();
        test.value = 10;
        System.out.println(test.value);
    }
}

class Test<T> {
    T value;
}

image-20230104154222524

当然,如果是基本类型的数组,因为数组本身是引用类型,所以说是可以的:

package cn.meowrain.generic;


public class Main {
    public static void main(String[] args) {
        Test<int[]> test = new Test<>();
        test.value = new int[]{1, 2, 3, 4, 5, 6};
        for(int i = 0;i<test.value.length;i++){
            System.out.println(test.value[i]);
        }
    }
}

class Test<T> {
    T value;
}

image-20230104155119155

泛型与多态

不只是类,包括接口、抽象类,都是可以支持泛型的:

interface Study<T> {
 T learn();
}

当子类实现此接口时,我们可以选择在实现类明确泛型类型,或是继续使用此泛型让具体创建的对象来确定类型:

package cn.meowrain.generic;


public class Main {
 public static void main(String[] args) {
     Student stu1 = new Student("xiaoming");
     System.out.println(stu1.learn());//xiaoming is learning
 }
}

class Student implements Study<String> {
 //在实现接口或是继承父类时,如果子类是一个普通类,那么可以直接明确对应类型
 String name;

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

 @Override
 public String learn() {
     return (this.name + " is learning");
 }
}

interface Study<T> {
 T learn();
}

继续使用泛型:

package cn.meowrain.generic;


public class Main {
 public static void main(String[] args) {
     Student<String> stu1 = new Student<>("xiaoming");
     System.out.println(stu1.learn());//null
 }
}

class Student<T> implements Study<T> {
 String name;

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

 @Override
 public T learn() {
     ///让子类继续为一个泛型类,那么可以不用明确
     return null;
 }
}

interface Study<T> {
 T learn();
}

继承也是同样的

package cn.meowrain.generic;


public class Main {
 public static void main(String[] args) {
     Student stu1 = new Student("xiaoming",17,"game","girls",99);
     System.out.println(stu1);
 }
}

class Human<T> {
 String name;
 int age;
 String hobby;
 T like;

 public Human(String name, int age, String hobby, T like) {
     this.name = name;
     this.age = age;
     this.hobby = hobby;
     this.like = like;
 }

}
class Student extends Human<String>{
 int score;

 public Student(String name, int age, String hobby, String like, int score) {
     super(name, age, hobby, like);
     this.score = score;
 }

 @Override
 public String toString() {
     return "Student{" +
         "score=" + score +
         ", name='" + name + '\'' +
         ", age=" + age +
         ", hobby='" + hobby + '\'' +
         ", like=" + like +
         '}';
 }
}

泛型方法

当然,类型变量并不是只能在泛型类中才可以使用,我们也可以定义泛型方法。

当某个方法(无论是是静态方法还是成员方法)需要接受的参数类型并不确定时,我们也可以使用泛型来表示:

package cn.meowrain.generic;


public class Main {
    public static void main(String[] args) {
        String str = test("mike");
        System.out.println(str);
    }

    public static <T> T test(T t) { ////在返回值类型前添加<>并填写泛型变量表示这个是一个泛型方法
        //返回类型为T ,参数类型为T
        return t;

    }
}


泛型方法会在使用时自动确定泛型类型,比如上我们定义的是类型T作为参数,同样的类型T作为返回值,实际传入的参数是一个字符串类型的值,那么T就会自动变成String类型,因此返回值也是String类型。

package cn.meowrain.generic;


import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] strings = new String[1];
        Main main = new Main();
        main.add(strings,"hello");
        System.out.println(Arrays.toString(strings));//[hello]
        Integer[] arr = new Integer[1];
        main.add(arr,1);
        System.out.println(Arrays.toString(arr));//[1]
    }

    public <T> void add(T[] arr, T t) {
        arr[0] = t;
    }
}


泛型在工具类中的应用

实际上泛型方法在很多工具类中也有,比如说Arrays的排序方法:

package cn.meowrain.generic;


import java.util.Arrays;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        Integer[] arr = {1, 4, 5, 2, 6, 3, 0, 7, 9, 8};
        //从小到大排列
        Arrays.sort(arr, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        System.out.println(Arrays.toString(arr));
        
        
        //从大到小排序
        Arrays.sort(arr, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });//[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
        System.out.println(Arrays.toString(arr));
    }

}


因为我们前面学习了Lambda表达式,像这种只有一个方法需要实现的接口,直接安排了:

package cn.meowrain.generic;


import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Integer[] arr = {1, 4, 5, 2, 6, 3, 0, 7, 9, 8};
        //从小到大排列
        Arrays.sort(arr, (o1, o2) -> o1 - o2);//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        System.out.println(Arrays.toString(arr));


        //从大到小排序
        Arrays.sort(arr, (o1, o2) -> o2 - o1);//[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
        System.out.println(Arrays.toString(arr));
    }

}


包括数组复制方法:

package cn.meowrain.generic;


import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] arr = {"aaa", "bbb", "ccc"};
        String[] newArr = Arrays.copyOf(arr, arr.length);
        System.out.println(Arrays.toString(newArr));//[aaa, bbb, ccc]

    }

}


泛型的界限

上限

现在有一个新的需求,现在没有String类型的成绩了,但是成绩依然可能是整数,也可能是小数,这时我们不希望用户将泛型指定为除数字类型外的其他类型,我们就需要使用到泛型的上界定义:

package cn.meowrain.generic;

public class Main {
    public static void main(String[] args) {
        Score<Integer> stu1 = new Score<>("xiaoming",1,100);
        Score<Double> stu2 = new Score<>("xiaohong",2,120.03);
        System.out.println(stu1.getValue()); // 100
        System.out.println(stu2.getValue()); // 120.03
    }

}
class Score<T extends Number>{
    ////设定类型参数上界,必须是Number或是Number的子类
    private final String name;
    private final int id;
    private final T value;

    public Score(String name, int id, T value) {
        this.name = name;
        this.id = id;
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public String getName() {
        return name;
    }

    public int getId() {
        return id;
    }
}

只需要在泛型变量的后面添加extends关键字即可指定上界,使用时,具体类型只能是我们指定的上界类型或是上界类型的子类,不得是其他类型。否则一律报错:

image-20230104163324415

像是这样

image-20230104163552430

泛型通配符在泛型界限中的使用

同样的,当我们在使用变量时,泛型通配符也支持泛型的界限:

package cn.meowrain.generic;

public class Main {
    public static void main(String[] args) {
        Score<? extends Number> score = new Score<>("数据结构与算法", "EP074512", 60);
    }

}
class Score<T>{
    private final String name;
    private final String id;
    private final T value;

    public Score(String name, String id, T value) {
        this.name = name;
        this.id = id;
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }
}

那么限定了上界后,我们再来使用这个对象的泛型成员,会变成什么类型呢?

package cn.meowrain.generic;

public class Main {
    public static void main(String[] args) {
        Score<? extends Number> score = new Score<>("数据结构与算法", "EP074512", 60.5);
        Number o = score.getValue();
        System.out.println(o);//60.5
    }

}
class Score<T>{
    private final String name;
    private final String id;
    private final T value;

    public Score(String name, String id, T value) {
        this.name = name;
        this.id = id;
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }
}

image-20230104164755843

可以看到,此时虽然使用的是通配符,但是不再是Object类型,而是对应的上界

但是我们限定下界的话,因为还是有可能是Object,所以说依然是跟之前一样:


那么既然泛型有上界,那么有没有下界呢?有

下限

image-20230104164608433

package cn.meowrain.generic;

public class Main {
    public static void main(String[] args) {
        Score<? super Integer> score = new Score<>("数据结构与算法", "EP074512", 60.5);
        Object o = score.getValue();
        System.out.println(o);//60.5
    }

}
class Score<T>{
    private final String name;
    private final String id;
    private final T value;

    public Score(String name, String id, T value) {
        this.name = name;
        this.id = id;
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }
}

但是我们限定下界的话,因为还是有可能是Object,所以说依然是跟之前一样:

通过给设定泛型上限,我们就可以更加灵活地控制泛型的具体类型范围。

类型擦除

前面我们已经了解如何使用泛型,那么泛型到底是如何实现的呢,程序编译之后的样子是什么样的?

public abstract class A <T>{
    abstract T test(T t);
}

实际上在Java中并不是真的有泛型类型(为了兼容之前的Java版本)因为所有的对象都是属于一个普通的类型,一个泛型类型编译之后,实际上会直接使用默认的类型:

public abstract class A {
    abstract Object test(Object t);  //默认就是Object
}

当然,如果我们给类型变量设定了上界,那么会从默认类型变成上界定义的类型:

public abstract class A <T extends Number>{   //设定上界为Number
    abstract T test(T t);
}

那么编译之后:

public abstract class A {
    abstract Number test(Number t);  //上界Number,因为现在只可能出现Number的子类
}

因此,泛型其实仅仅是在编译阶段进行类型检查,当程序在运行时,并不会真的去检查对应类型,所以说哪怕是我们不去指定类型也可以直接使用:

public static void main(String[] args) {
    Test test = new Test();    //对于泛型类Test,不指定具体类型也是可以的,默认就是原始类型
}

只不过此时编译器会给出警告:

image-20230104182530620

同样的,由于类型擦除,实际上我们在使用时,编译后的代码是进行了强制类型转换的:

package cn.meowrain.generic;

public class test {
    public static void main(String[] args) {
        A<String> a = new B();
        String i = a.test("10");
        System.out.println(i);
    }
}

class A<T> {
    public T test(T t) {
        return t;
    }
}

class B extends A<String> {
    @Override
    public String test(String s) {
        return null;
    }
}

实际上编译之后:

image-20230104183448839

不过,我们思考一个问题,既然继承泛型类之后可以明确具体类型,那么为什么@Override不会出现错误呢?我们前面说了,重写的条件是需要和父类的返回值类型和形参一致,而泛型默认的原始类型是Object类型,子类明确后变为其他类型,这显然不满足重写的条件,但是为什么依然能编译通过呢?

public class B extends A<String>{
    @Override
    String test(String s) {
        return null;
    }
}

我们来看看编译之后长啥样:

// Compiled from "B.java"
public class com.test.entity.B extends com.test.entity.A<java.lang.String> {
  public com.test.entity.B();
  java.lang.String test(java.lang.String);
  java.lang.Object test(java.lang.Object);   //桥接方法,这才是真正重写的方法,但是使用时会调用上面的方法
}

通过反编译进行观察,实际上是编译器帮助我们生成了一个桥接方法用于支持重写:

public class B extends A {

    public Object test(Object obj) {   //这才是重写的桥接方法
        return this.test((Integer) obj);   //桥接方法调用我们自己写的方法
    }

    public String test(String str) {   //我们自己写的方法
        return null;
    }
}

类型擦除机制其实就是为了方便使用后面集合类(不然每次都要强制类型转换)同时为了向下兼容采取的方案。因此,泛型的使用会有一些限制:

首先,在进行类型判断时,不允许使用泛型,只能使用原始类型:

image-20230104184731564

只能判断是不是原始类型,里面的具体类型是不支持的:

package cn.meowrain.generic;

public class Main {
    public static void main(String[] args) {
       Hello<String> hello = new Hello<>("meow");
        System.out.println(hello instanceof Hello); // true
    }

}

class Hello<T> {
    T name;

    public Hello(T name) {
        this.name = name;
    }
}

image-20230104185303420

image-20230104185442606