筑基#泛型

47 阅读5分钟

java筑基#注解

java筑基#多线程编程

java筑基#序列化

java筑基#内存模型

java筑基#泛型

一.为什么需要泛型?

  1. 使用编译时提示来解决运行时类型转换异常!
  2. 使得程序更加灵活复用。

二.泛型限定和通配符

1.通配符

泛型只是在编译时存在,运行时擦除的,所以泛型并没有继承,本质上是一个class,如List和 List作为参数时其实是一种类型,如下面的代码,编译器就会提示我们报错。

    public void testGeneric(List<Number> numberList){
        
        
    }

    public void testGeneric(List<Integer> list){


    }

但是需要注意的是,List中可以添加Integer类型,因为他们是Number和Integer父子关系,但是List和List却不是。

我们使用通配符?来表示任意不确定类型,List<?>可以传入任意类型,

    @Test
    public void showList(){
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        testShow(list);

        List<String > listStr=new ArrayList<>();
        listStr.add("one");
        listStr.add("two");
        testShow(listStr);
    }

    public void testShow(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);

        }
//        list.add("1");//不可以添加,因为不确定它的类型
    }

List<?>这个和List有什么区别?

通配符?标识的是任意类型,比如你传入的是Integer,它就是Integer,如果你传入的是String,他就是String,而Object就比较刚性,我们来看下下面的代码:

   @Test
    public void showList2() {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        testShow2(list);

        List<String > listStr=new ArrayList<>();
        listStr.add("one");
        listStr.add("two");
        testShow2(listStr);

    }

    public void testShow2(List<Object> list) {
        for (Object obj : list) {
            System.out.println(obj);

        }
//        list.add("1");//不可以添加,因为不确定它的类型
    }

这段代码就会报错,提示参数的泛型是Object,无法传递。

2.泛型限定

泛型的extends和super可以约束泛型的上界和下界,extends可以使用在方法上、类上、也可以在参数上,super一般放在参数和方法限定泛型上。

1.PECS

前面我们学习到了参数类型可以加通配符?来让参数类型更加灵活,我们还可以搭配extends和super来确定上下界,这个就是PECS,我们来看代码:

   @Test
    public void showList() {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        testShow(list);

    }

    //上界 只可以从里面读取,因为是父类接受,不会是上转型(kt中的out)
    public void testShow(List<? extends Number> list) {
        for (Number obj : list) {
            System.out.println(obj);
            
        }
        //list.add(1);//不可以添加,因为不确定它的类型
    }

上面这段就是上界搭配通配符限定泛型类型参数只可以是Number的子类,并且我们只能从里面获取,所以它是Produce Extends,下面我们再来看看通配符和super搭配使用作为形参时的特性:

    @Test
    public void showList2() {
        List<P> list=new ArrayList<>();
//        List<S> list=new ArrayList<>(); 不行 show2参数不匹配
        list.add(new S());//这个可以添加
        list.add(new S());
        testShow2(list);
    }
    //下界,泛型参数至少是P及其父类
    // 可以添加下界的子类包括它,因为它的参数类型至少是它自己或者其父类
    //当然可以接受其子类,不涉及到向上转型,安全
    public void testShow2(List<? super P> list) {

        list.add(new P());//不可以添加,因为不确定它的类型
        list.add(new S());//不可以添加,因为不确定它的类型
    }

如代码展示,下界和通配符搭配使用时,可以添加其类型及其子类,因为不涉及到向上转型,这种只能存入的,我们称之为Consumer Super,和Produce Extends合称为PECS规则。

2.通配符的其他应用

1.泛型类中约束上界

   static class P {
    }

    static class S extends P {
    }

    //上届搭配泛型类,约束泛型范围
    static class Gen<T extends P>{
        T t;
        public Gen(){
        }
        
        public void attach(T t){
            this.t=t;
        }
    }
    @Test
    public void test(){
        Gen<P> pGen = new Gen<>();
    }

2.泛型方法中约束泛型参数上界

  public <T extends  Number> void  show(T t){
        System.out.println(t);
    }

三.泛型类、接口与方法。

1.泛型类

父类是泛型,子类可以是泛型,可以不是泛型,如果不是需要指定父类泛型的类型。

    static abstract class P<T,V>{
       abstract V app(T t);
    }
    
    //1.子类不是泛型类 需要指明父类泛型类型
    static class S extends P<String,Integer>{

        @Override
        Integer app(String s) {

            return Integer.parseInt(s);
        }
    }
    //2.子类是泛型类,参数带到父类中去
    static  class S2<K,V> extends P<K,V>{

        @Override
        V app(K k) {
            return null;
        }
    }

    @Test
    public void testGenClz(){
        System.out.println(new S().app("1"));
    }

2.泛型接口

同泛型类

3.泛型方法

在返回值前使用 <T> 声明的方法才是泛型方法,即使该方法属于泛型类。如:

public void show(T t){}


四.泛型擦除

1.泛型擦除

无边界的它会以Object形式在字节码中,右边界会以边界的类型存在在字节码中。

2.擦除的弊端

1.无法在运行时获取泛型类型信息

泛型擦除,会让我们在需要反射创建对象时无法获取到泛型的真实类型,我们可以通过在构造方法中传入泛型类型或者利用java中当一个类继承了一个带有泛型的父类时,这个泛型的具体类型信息会被保留在子类的字节码的常量池中

2.泛型数组的创建

Java 数组是协变的,即如果 Sub Super 的子类,那么 Sub[] 也是 Super[] 的子类。这意味着可以将一个 Sub[] 类型的数组赋值给一个 Super[] 类型的变量。

class Fruit {}
class Apple extends Fruit {}
Apple[] apples = new Apple[5];
Fruit[] fruits = apples; // 合法,数组的协变特性
 // 运行时会抛出 ArrayStoreException 异常
fruits[0] = new Orange(); 
  
}

泛型数组的创建受到限制:不能直接创建泛型数组,因为泛型擦除会导致数组在运行时无法确定其元素的具体类型,可能会引发类型安全问题。


        // 错误示例,不能直接创建泛型数组
     List<String>[] stringListArray = new List<String>[10];

        // 正确示例,可以使用通配符创建数组 通配符标识任意类型,但是你要确定好
//        List<?>[] listArray = new List[10];

可能有点不好理解,我们反过来来看,如果允许创建泛型数组,编译器无法在编译时阻止上述类型错误的赋值操作,因为类型擦除后, List<String>[] List<Integer>[] 在运行时都是 List[] ,这会破坏泛型的类型安全保证。

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

// 假设这是合法的
// List<String>[] stringLists = new List<String>[5]; 

List<Integer> intList = new ArrayList<>();
intList.add(10);

// 由于数组协变,如果泛型数组合法,下面的赋值可能会通过编译
// stringLists[0] = intList; 

// 后续从 stringLists[0] 中获取元素时,会导致类型错误
// String str = stringLists[0].get(0