JAVA泛型

2024-12-10  本文已影响0人  Owen270

一文彻底搞懂Java泛型中的PECS原则(在坑里躺了多年终于爬出来了)

1.什么是泛型?

泛型,就我的理解就是类型参数化

一个栗子
      List arrayList = new ArrayList();
       arrayList.add("aaaa");
        arrayList.add(100);
        for(int i = 0; i< arrayList.size();i++){
            String item = (String)arrayList.get(i);
           System.out.println("泛型测试---item = " + item);      
    }

毫无疑问,程序的运行结果会以崩溃结束:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。

List<String> arrayList = new ArrayList<String>();
...
//arrayList.add(100); 在编译阶段,编译器就会报错

2.泛型特性

通过下面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

        List<String> stringList=new ArrayList<>();
        List<Integer> integerList=new ArrayList<>();
        if(stringList.getClass().equals(integerList.getClass())){
            //如果输出乱码,需要将文件设置成GB2312编码格式
            System.out.println("类型相同--->证明了泛型只是在编译阶段有效,编译成字节码就
                 无效了");
        }
image.png

3.泛型的具体使用场景

3.1泛型类
/****
 *一个普通的java泛型类
 * 此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
 在实例化泛型类时,必须指定T的具体类型
 * @param <T>
 */

public   class Generics<T> {
    //key这个成员变量的类型为T,T的类型由外部指定
    private T key;

    public Generics(T key) {
        this.key = key;
    }

    public T getKey() {
        return key;
    }

}
        Generics<Integer> integerGenerics=new Generics<>(123456);
        Generics<String>  stringGenerics=new Generics<>("one_two_three_four_five_six");
        System.out.println("key is "+integerGenerics.getKey());
        System.out.println("key is "+stringGenerics.getKey());
注意:定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
        Generics generics1=new Generics(111111);
        Generics generics2=new Generics("one_two_three_four_five_six");
        Generics generics3=new Generics(false);
        Generics generics4=new Generics(11.00f);
        System.out.println("key is "+generics1.getKey());
        System.out.println("key is "+generics2.getKey());
        System.out.println("key is "+generics3.getKey());
        System.out.println("key is "+generics4.getKey());
3.2泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器(构造方法)中,可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}
3.3泛型方法

在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了,泛型类,是在实例化类的时候指明泛型的具体类型;

泛型方法,是在调用方法的时候指明泛型的具体类型 ,必须通过<T,K,E>的形式声明。
常见的泛型方法:
   public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

public <T,K> K showKeyName(Generic<T> container){
  
}

这些不是泛型方法:

//这不是一个泛型方法,没有声明泛型E
   public E setKey(E key){
             this.key = keu
   }
//这不是一个泛型方法,没有声明泛型T
    public T getKey(){
            return key;
   }
 //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
    public void showKeyValue1(Generic<Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }



     /**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。**/
    public <T> T showKeyName(Generic<E> container){
       
    }

泛型类中的方法:

public class GenericFruit {
    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple是Fruit的子类,所以这里可以
        generateTest.show_1(apple);
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        //使用这两个方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用这两个方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}
3.4 通配符
public void showKeyValue1(Generic<Float> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
  Generics<Integer> integerG=new Generics<>(345);
 Generics<Float>  floatG=new Generics<>(678f);

看上面的例子:

showKeyValue(gInteger );//此时必定报错cannot be applied to Generic<java.lang.Float>

所有通配符在这个时候派上用场了,我们可以:

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

3.5.泛型中的PECS原则

3.5.1.PECS 是 Producer extends Consumer Super 的缩写, 生产者提供数据(get),用extends ,消费者使用数据 (add),用super .
3.5.2.上边界( ? extends T) 表示泛型的类型必须为T类型或者T的子类 【一般只用获取元素】

为了更好的理解,我们先定义一些类

public class Fruit{}
public class Apple extends Fruit{}
public class Banana extends Fruit{}

例如:List<? extends Fruit>中 <? extends Fruit> 表示泛型的类型是Fruit类或者是Fruit的子类,我们给list赋值的时候,可以是new ArrayList<Fruit>(), new ArrayList<Apple>(),new ArrayList<Banana>().

  private static List<? extends Fruit>  getExtendsList(){
        List<? extends Fruit> list;
       list=new ArrayList<Apple>();
       list=new ArrayList<Banana>();
       list=new ArrayList<Fruit>();
      return list;
 }

List<? extends Fruit>允许我们将ArrayList<Fruit>(),ArrayList<Apple>(),ArrayList<Banana>()赋值给它。然而,虽然我们可以从这个列表中获取元素(因为我们知道它们至少是Fruit类型),但是我们无法往列表中添加元素(除了null),因为编译器不知道list的具体类型,可能是new ArrayList<Banana>().

public static void m1(List<? extends Fruit> list){
       Fruit fruit=list.get(0);//安全的,因为我们知道列表中的数据类型至少是Fruit类型
   //  list.add(new Apple());//错误,编译器不允许,list可能是new ArrayList<Banana>()
}
3.5.3.下边界( ? super T) 表示泛型的类型必须为T类型或者T的父类 【一般只用于添加元素,因为获取的元素类型只能是Object类型,意义不大】

例如:List<? super Fruit> 表示泛型的类型必须是Fruit类型或者是Fruit的父类,我们给list赋值的时候,可以是new ArrayList<Fruit>(),new ArrayList<Object>().

private static List<? super Fruit> getSuperList(){
      List<? super Fruit> list;
      list=new ArrayList<Fruit>();
      list=new ArrayList<Object>();
      return list;  
 }

List<? super Fruit> 允许我们将ArrayList<Fruit>(),ArrayList<Object>()赋值给它,但是没法获取特定类型的元素,只能获取Object类型的元素。

 private static void m2(List<? super Fruit> list){ //list列表只能添加Fruit类型及其子类型
        list.add(new Apple()); //可以,因为Apple是Fruit类型
        list.add(new Banana());//可以,因为Banana是Fruit类型
       Object  item=list.get(0);//只能是Object,因为我们不知道具体的类型
}

4.深入理解泛型通配符

4.1.协变和逆变
4.2.泛型方法中的PECS (extends out super in )
   public static <T> void addToList(List<? super T> list,T item){
        list.add(item);
  }
  public static <T> void copyList(List<? extens T> source ,List<? super T>  destination){
        for(T item:source){
             destination.add(item);
        }
 }
Kotlin中的协变(out)和逆变(in)
open class Animal {}
class Cat : Animal() {}
interface Container<out T>{
     fun getItem():T
}
fun main(){
    val catContainer:Container<Cat> =object:Container<Cat>{
          override fun getItem():Cat{
                 return Cat();
         }
    }
    val animalContainer:Container<Animal>=catContainer ;
    val animal:Animal =animalContainer.getItem();
    println(animal);
}
interface Processor<in T> {
    fun process(item: T)
}
fun main() {
    val animalProcessor: Processor<Animal> = object : Processor<Animal> {
        override fun process(item: Animal) {
            println("Processing animal: $item")
        }
    }
    val catProcessor: Processor<Cat> = animalProcessor // 逆变
    catProcessor.process(Cat())
}
总结
上一篇 下一篇

猜你喜欢

热点阅读