Java泛型

一、概述

1.1、什么是泛型?为什么要使用泛型?类型擦除?

什么是泛型?

泛型:参数化类型

​ 对于参数,通常情况下,我们理解为 方法有形参,然后调用方法的时候传递实参。

​ 而参数化类型是把类型由原来的具体的类型参数化(参数化是个动词)。即把类型当做是参数一样传递。

为什么要使用泛型?

在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法

  • 适用于多种数据类型执行相同的代码
  • 泛型中的类型在使用时指定
  • 优点:使用泛型时,在实际使用之前类型就已经确定了,不需要强制类型转换。

类型擦除

写代码时采用泛型写的类型参数,编译器会在编译时去掉,这称为“类型擦除”

泛型主要用于编译阶段,编译后生成的字节码 class 文件不包含泛型中的类型信息,涉及类型转换仍然是普通的强制类型转换。 类型参数在编译后会被替换成 Object,运行时虚
拟机并不知道泛型

1.2、一个例子

// ArrayList定义
public class ArrayList {
    private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

当我们创建一个ArrayList实例,并调用.add()方法存储元素时是没有问题的。因为可以向数组列表中添加任何类的对象。但是,当获取值的时候就必须进行强制转换

ArrayList list = new ArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String first = (String) list.get(0);

-------------------------------------------
list.add(new Integer(123));
// ERROR: ClassCastException:很容易出现“误转型”
String second = (String) list.get(1);

​ 为了应对不同Class而单独编写对应的ArrayList显然是不可取的。因此,把ArrayList变成一种模板:ArrayList<T>

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}
ArrayList<String> strList = new ArrayList<String>();
//编译器进行类型检查
strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!


/*
ArrayList<类型>
-----------------------------------------------
创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();

1.3、特性

public class Demo1 {
    public static void main(String[] args) {
        List<String> stringArrayList = new ArrayList<String>();
        List<Integer> integerArrayList = new ArrayList<Integer>();

        Class classStringArrayList = stringArrayList.getClass();
        Class classIntegerArrayList = integerArrayList.getClass();
        System.out.println(classIntegerArrayList);
        System.out.println(classStringArrayList);
        if(classStringArrayList.equals(classIntegerArrayList)){
            System.out.println("泛型相同,类型相同");
        }
    }
}
/*
class java.util.ArrayList
class java.util.ArrayList
泛型相同,类型相同

注意:

泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型

在Java标准库中的ArrayList<T>实现了List<T>接口,它可以向上转型为List<T>。但是不能把ArrayList<Integer>向上转型为ArrayList<Number>List<Number>

// 创建ArrayList<Integer>类型:
ArrayList<Integer> integerList = new ArrayList<Integer>();

// 添加一个Integer:
integerList.add(new Integer(123));

// “向上转型”为ArrayList<Number>:
ArrayList<Number> numberList = integerList;

// 添加一个Float,因为Float也是Number:
numberList.add(new Float(12.34));

// 从ArrayList<Integer>获取索引为1的元素(即添加的Float):
Integer n = integerList.get(1); // ClassCastException!


​ 把一个ArrayList<Integer>转型为ArrayList<Number>类型后,这个ArrayList<Number>就可以接受Float类型,因为FloatNumber的子类。

ArrayList<Number>实际上和ArrayList<Integer>是同一个对象,也就是ArrayList<Integer>类型,它不可能接受Float类型, 所以在获取Integer的时候将产生ClassCastException

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

总结:

  • 泛型就是编写模板代码来适应任意类型
  • 泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型
  • 泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查
  • 可以把ArrayList<Integer>向上转型为List<Integer>T不能变!),但不能把ArrayList<Integer>向上转型为ArrayList<Number>T不能变成父类)

二、泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

2.1、泛型类

​ **泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来。**泛型类的具体使用方法是在类的名称后添加一个或多个类型参数声明

最典型的就是各种容器类,如:List、Set、Map

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....
  }
}
/*
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
在实例化泛型类时,必须指定T的具体类型
*/
public class Generic<T[,K,...]>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    public Generic(T key) { 
   //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ 
   //泛型方法getey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

案例分析1

public class Demo2 {
    public static void main(String[] args) {
        //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
        //传入的实参类型需与泛型的类型参数类型相同,即为Integer.
        GenericTest<Integer> integerGenericTest = new GenericTest<>(123456);
        //传入的实参类型需与泛型的类型参数类型相同,即为String.
        GenericTest<String> genericString = new GenericTest<>("key_vlaue");
        System.out.println("泛型测试"+"key is " + integerGenericTest.getKey());
        System.out.println("泛型测试"+"key is " + genericString.getKey());
    }
	
    //泛型类
    static class GenericTest<T>{
        private T key; 
        public GenericTest(T key) {
            this.key = key;
        }
        public T getKey(){
            return key;
        }
    }
}

/* 输出结果
泛型测试:key is 123456
泛型测试:key is key_value

注意:

  • 如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。
  • 如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
  • 编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型。
GenericTest generic = new GenericTest("111111");
GenericTest generic1 = new GenericTest(4444);
GenericTest generic2 = new GenericTest(55.55);
GenericTest generic3 = new GenericTest(false);

System.out.println("泛型测试"+"key is " + generic.getKey());
System.out.println("泛型测试"+"key is " + generic1.getKey());
System.out.println("泛型测试"+"key is " + generic2.getKey());
System.out.println("泛型测试"+"key is " + generic3.getKey());

/*
泛型测试: key is 111111
泛型测试: key is 4444
泛型测试: key is 55.55
泛型测试: key is false

案例分析2

public class Demo4 {
    public static void main(String[] args) {
        // =========================如果不定义泛型类型时,泛型类型实际上就是Object===================
          // 编译警告
//        List list = new ArrayList();
//        list.add("Hello");
//        list.add("World");
//        String first = (String) list.get(0);
//        String second = (String) list.get(1);
        // ==================当我们定义泛型类型<String>后,List<T>的泛型接口变为强类型List<String>==========
        // 无编译器警告:
        
        List<String> list = new ArrayList<>();
        list.add("Hello");
        list.add("World");
        // 无强制转型:
        String first = list.get(0);
        String second = list.get(1);
        System.out.println(first);
        System.out.println(second);
    }
}

小结

泛型标记对应单词说明
EElement在容器中使用,表示容器中的元素
TType表示普通的JAVA类
KKey表示键,例如:Map 中的键 Key
VValue表示值
NNumber表示数值类型
?表示不确定的 JAVA 类型
  • 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
  • 传泛型实参可以起到限制作用。
  • 可以定义多种泛型类型

2.2、泛型接口

2.2.1、一般使用

//定义一个泛型接口
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;
    }
}

image-20210412230650752

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

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

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

2.2.2、通配符

IngeterNumber的一个子类,GenericTest<Ingeter>GenericTest<Number>实际上是相同的一种基本类型。在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实例传入呢?

public class Demo2 {
    public static void main(String[] args) {
        //创建一个Integer泛型和Number泛型
        GenericTest<Integer> integerGenericTest = new GenericTest<>(123456);
        GenericTest<Number> numberGenericTest = new GenericTest<>(456);
        showKeyValue1(numberGenericTest);
        showKeyValue1(integerGenericTest);
    }

    static class GenericTest<T>{
        private T key;
        public GenericTest(T key) {
            this.key = key;
        }

        public T getKey(){
            return key;
        }
    }

    public static void showKeyValue1(GenericTest<Number> obj){
        System.out.println("泛型测试" + "key value is " + obj.getKey());
    }
}
/*
java: 不兼容的类型: com.doublew2w.generic.Demo2.GenericTest<java.lang.Integer>无法转换为com.doublew2w.generic.Demo2.GenericTest<java.lang.Number>


GenericTest<java.lang.Integer> 不是 GenericTest<java.lang.Number>的子类
所以 showKeyValue1(GenericTest<Number> obj) 不接受 GenericTest<java.lang.Integer>

修改过后

    public static void showKeyValue1(GenericTest<?> obj){
        System.out.println("泛型测试" + "key value is " + obj.getKey());
    }

//成功编译

类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!此处’?’是类型实参,而不是类型形参此处’?’是类型实参,而不是类型形参 !再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型

可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

总结:

  • 要实现泛型接口时,泛型也要声明,否则编译器会报错
  • 类型通配符一般是使用代替具体的类型实参,可以把 ? 当成所有类型的父类,用来解决未知类型的问题。

2.3、泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型

调用泛型方法时,不需要像泛型类那样告诉编译器是什么类型,编 译器可以自动推断出类型来

2.3.1、基本介绍

public <泛型表示符号> void getName(泛型表示符号 name){
}
public <泛型表示符号> 泛型表示符号 getName(泛型表示符号 name){
}
/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回类型中间的<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> tClass) throws InstantiationException ,IllegalAccessException {
        T instance = tClass.newInstance();
        return instance;
}
public class Demo3 {
    public static void main(String[] args) {

    }
    //这是一个泛型类
    static class GenericTest<T>{
        private T key;
        public GenericTest(T key) {
            this.key = key;
        }

        /**
         * 第一种:
         * 这是一个普通方法,不是泛型方法。
         * 返回类型是泛型类已经声明过的泛型
         * 所以在这个方法中才可以继续使用 T 这个泛型
         * @return key
         */
        public T getKey(){
            return key;
        }

        /**
         * 第二种:
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
         public E setKey(E key){
         this.key = key
         }
         */

        /**
         *
         * @param container:泛型类
         * @param <T>: 泛型方法的类型
         * @return : 类型参数为T
         * public <T> T genericMethod(Class<T> tClass)
         */
        public <T> T showKeyName(Demo3.GenericTest<T> container){
            System.out.println("container key :" + container.getKey());
            T test = container.getKey();
            return test;
        }


        /**
         * 这是一个普通的方法,使用了Generic<Number>这个泛型类做形参
         * @param obj: 形参
         */
        public void showKeyValue1(Demo3.GenericTest<Number> obj){
            System.out.println("泛型测试" + "key value is " + obj.getKey());
        }

        /**
		 * 并未声明泛型参数E
         public <T> T showKeyName(GenericTest<E> container){
         ...
         }
         */

        /**
         * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
         * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
         * 所以这也不是一个正确的泛型方法声明。
         public void showkey(T genericObj){
         }
         */
    }
}

2.3.3、非静态方法

当泛型方法出现在泛型类中时

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

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

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

    /**
     * 这是一个泛型类
     * @param <T>:泛型
     */
    static 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<Fruit> generateTest = new GenerateTest<Fruit>();

        /**
         * show_1是个普通方法
         * apple是Fruit的子类,所以这里可以
         */
        generateTest.show_1(apple);//apple
        
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        /**
         * 泛型方法,泛型类型是个全新的类型,所有哪一个类型参数都可以
         */
        generateTest.show_2(apple);
        generateTest.show_2(person);

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

2.3.4、泛型方法与可变参数

public <泛型表示符号> void showMsg(泛型表示符号... agrs){
}
public <T> void printMsg( T... args){
    for(T t : args){
        System.out.println("泛型测试"+"t is " + t);
    }
}

2.3.5、静态方法与泛型

静态方法使用泛型的注意点:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法

public static <泛型表示符号> void setName(泛型表示符号 name){
}
public static <泛型表示符号> 泛型表示符号 getName(泛型表示符号 name){
}
public class StaticGenerator<T> {
    ....
    ....
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     * 如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法
     */
    public static <T> void show(T t){

    }
}

2.4、通配符和上下限定

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

无界通配符

?表示类型通配符,用于代替具体的类型。它只能在 <> 中使用。可以解决当具体类型不确定的问题。

public class MethodGeneric {

    public <T> void setName(T name){
        System.out.println(name);
    }

    public <T> T getName(T name){
        return name;
    }
}
public class ShowMsg {
    public void showFlag(Generic<?> generic){
        System.out.println(generic.getFlag());
    }
}
public class Demo4 {
    public static void main(String[] args) {
        ShowMsg showMsg = new ShowMsg();
        Generic<Integer> generic = new Generic<>();
        generic.setFlag(20);
        showMsg.showFlag(generic);

        Generic<Number> generic1 = new Generic<>();
        generic1.setFlag(50);
        showMsg.showFlag(generic1);

        Generic<String> generic2 = new Generic<>();
        generic2.setFlag("hello");
        showMsg.showFlag(generic2);
    }
}

上界限定符(extends)

上限限定表示通配符的类型是 T 类以及 T 类的子类或者 T 接口以及 T 接口的子接口

该方式同样适用于与泛型的上限限定

简单例子
public class ShowMsg {
    public void showFlag(Generic<? extends Number> generic){
        System.out.println(generic.getFlag());
    }
}
public class Demo5 {
    public static void main(String[] args) {
        ShowMsg showMsg = new ShowMsg();
        Generic<Integer> generic = new Generic<>();
        generic.setFlag(20);
        showMsg.showFlag(generic);

        Generic<Number> generic1 = new Generic<>();
        generic1.setFlag(50);
        showMsg.showFlag(generic1);
    }
}

深入学习
public class PairHelper {
    // 静态方法,接收的参数类型是Pair<Number>
    static int add(Pair<Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }

    public static void main(String[] args) {
        /* 
        传入的类型是Pair<Number>,实际参数类型是(Integer, Integer)
         int sum = PairHelper.add(new Pair<Number>(1, 2));
	     System.out.println(sum);
		*/
        
        //既然实际参数是Integer类型,试试传入Pair<Integer>
        Pair<Integer> p = new Pair<>(123, 456);
        int n = add(new Pair<Integer>(123, 456));
        System.out.println(n);
        //incompatible types: Pair<Integer> cannot be converted to Pair<Number>
    }
    
	    public static class Pair<T> {
        private T first;
        private T last;
        public Pair(T first, T last){
            this.first = first;
            this.last = last;
        }

        public T getFirst() {
            return first;
        }

        public T getLast() {
            return last;
        }
    }
}

image-20210725131934138

引入问题:

为什么实际类型是Integer,引用类型是Number,而传入Pair<Integer>不行?

  • 方法参数类型定死了只能传入 Pair<Number> ,而且 Pair<Integer> 不是 Pair<Number> 的子类
static int add(Pair<? extends Number> p) {
    Number first = p.getFirst();
    Number last = p.getLast();
    return first.intValue() + last.intValue();
}

使得方法中可以接受Number以及Number的子类的泛型

注意点:
  1. 考察对Pair<? extends Number>类型调用getFirst()方法
当我们调用getFirst()方法时,实际的方法签名为 <? extends Number> getFirst();
因此返回值,肯定是Number或者Number的子类,因此可以安全赋值给Number类型的变量。但是不可预测的是Integer类型。
也就是说
Number x = p.getFirst();  //ok
Integer x = p.getFirst(); //无法编译通过
原因:编译器只能确定类型一定是Number的子类(包括Number类型本身),但具体类型无法确定。

2.考察对Pair<? extends Number>类型调用set()方法

public class Main { 
	public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        int n = add(p);
        System.out.println(n);
    }

    static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        p.setFirst(new Integer(first.intValue());
        p.setLast(new Integer(last.intValue());
        return p.getFirst().intValue() + p.getFirst().intValue();
    }
}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

​ 由上文可知p.setFirst()传入的参数是Integer类型,而前面我们又说过,Pair<? extends Number>这个定义是允许p是Number类或者Number的子类,那为什么不能传入Integer?

image-20210725133307871

​ 因为Java是利用擦拭法实现泛型的,Integer和Double都是Number的子类。如果传入的pPair<Double>,虽然符合定义。但是Pair<Double>setFirst()显然无法接受Integer类型。

总结:

  • 使用类似<? extends Number>通配符作为方法参数时表示
    • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();
    • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);

使用extends通配符表示可以读,不能写

  • 使用类似<T extends Number>定义泛型类时表示:
    • 泛型类型限定为Number以及Number的子类
    • StringObject都不符合<T extends Number>

下界限定符(super)

​ 下限限定表示通配符的类型是 T 类以及 T 类的父类或者 T 接口以及 T 接口的父接口。

​ 注意:该方法不适用泛型类

简单例子
public class ShowMsg {
    public void showFlag(Generic<? super Integer> generic){
        System.out.println(generic.getFlag());
    }
}
public class Demo6 {
    public static void main(String[] args) {
        ShowMsg showMsg = new ShowMsg();
        Generic<Integer> generic = new Generic<>();
        generic.setFlag(20);
        showMsg.showFlag(generic);
        System.out.println("generic.getClass() = " + generic.getClass());

        Generic<Number> generic1 = new Generic<>();
        generic1.setFlag(50);
        showMsg.showFlag(generic1);
        System.out.println("generic1.getClass() = " + generic1.getClass());
    }
}
深入学习
public class Main { 
	public static void main(String[] args) {
		...
    }
	void set(Pair<Integer> p, Integer first, Integer last) {
    	p.setFirst(first);
    	p.setLast(last);
	}
}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

void set(Pair<Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}
/*
在这个方法下,传入Pair<Integer>是允许的,但是传入Pair<Number>是不允许的

​ 如果希望接受Pair<Integer>类型,以及Pair<Number>Pair<Object>呢??

public class Main {    
	public static void main(String[] args) {
        Pair<Number> p1 = new Pair<>(12.3, 4.56);
        Pair<Integer> p2 = new Pair<>(123, 456);
        setSame(p1, 100);
        setSame(p2, 200);
        System.out.println(p1.getFirst() + ", " + p1.getLast()); //100, 100
        System.out.println(p2.getFirst() + ", " + p2.getLast()); //200, 200
    }

    static void setSame(Pair<? super Integer> p, Integer n) {
        p.setFirst(n);
        p.setLast(n);
    }
}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}
  1. 考察Pair<? super Integer>setFirst()方法
实际上方法签名为:void setFirst(? super Integer);
可以安全地传入Integer类型
  1. 再考察Pair<? super Integer>getFirst()方法
实际上方法签名为:
? super Integer getFirst();

使用Integer类型来接收getFirst()的返回值
Integer x = p.getFirst(); //无法编译

如果传入的实际类型是Pair<Number>,编译器无法将Number类型转型为Integer
    
即便Number不是抽象类,这里仍然无法通过编译
    
唯一可以接收getFirst()方法返回值的是Object类型
Object obj = p.getFirst();

总结比较

  • <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
  • <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)

无界通配符2

特点:

  • 不允许调用set(T)方法并传入引用(null除外);

  • 不允许调用T get()方法并获取T引用(只能获取Object引用)

    • 既不能读,也不能写,那只能做一些null判断
  • 大多数情况下,可以引入泛型参数<T>消除<?>通配符

  • Pair<?>是所有Pair<T>的超类

何时使用?

PECS原则:Producer Extends Consumer Super

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

泛型方法的例子:

//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加
//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"

public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    T test = container.getKey();
    return test;
}

泛型的上下边界添加,必须与泛型的声明在一起

2.5、尝试编写泛型类

2.5.1、简单的泛型类

  • 首先先按照某种具体的类型来编写类
public class Pair {
    private String first;
    private String last;
    public Pair(String first, String last){
        this.first = first;
        this.last = last;
    }

    public String getFirst() {
        return first;
    }

    public String getLast() {
        return last;
    }
}
  • String 换成 T,并声明<T>
public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last){
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }

    public T getLast() {
        return last;
    }
}

2.5.2、添加静态泛型方法

  • 编写泛型类时,要特别注意,泛型类型不能用于静态方法。
  • 在static修饰符后面加一个,编译就能通过。这个和Pair类型的已经没有任何关系了
public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last){
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }

    public T getLast() {
        return last;
    }
    //编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法。
    /*
    public static Pair<T> create(T first, T last) {
        return new Pair<T>(first, last);
    }
    */

}
  • 静态泛型方法应该使用其他类型区分:
public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last){
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }

    public T getLast() {
        return last;
    }
    //编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法。
    /*
    public static Pair<T> create(T first, T last) {

        return new Pair<T>(first, last);
    }
    */
    // 编写静态泛型方法
    // 静态泛型方法应该使用其他类型区分:
    public static <K> Pair<K> create(K first, K last){
        return new Pair<K>(first, last);
    }
}

三、补充

泛型数组

在java中是**”不能创建一个确切的泛型类型的数组”**的。

List<String>[] ls = new ArrayList<String>[10];
//会报错 
// compile error:
public class Abc<T> {
    T[] createArray() {
        return new T[5];
    }
}

使用通配符创建泛型数组是可以的

List<?>[] ls = new ArrayList<?>[10];  

List<String>[] ls = new ArrayList[10]; //ok
Pair<String>[] ps = null; // ok
// 借助Class<T>
T[] createArray(Class<T> cls) {
    return (T[]) Array.newInstance(cls, 5);
}

下面使用Sun的一篇文档的一个例子来说明这个问题:

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Unsound, but passes run time store check    
String s = lsa[1].get(0); // Run-time error: ClassCastException.

这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。

而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。

下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type. 
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));
// Correct.
oa[1] = li;     
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);

/*
// Error.
List<String>[] lsa = new List<?>[10];

擦拭法

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

image-20210413011130070

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

  • 编译器把类型<T>视为Object
  • 编译器根据<T>实现安全的强制转型。

image-20210413011323642

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

Java泛型的局限:

  • 局限一:<T>不能是基本类型,例如int,因为实际类型是ObjectObject类型无法持有基本类型

    • Pair<int> p = new Pair<>(1, 2); // compile error!
      
  • 局限二:无法取得带泛型的Class

    • 因为TObject,我们对Pair<String>Pair<Integer>类型获取Class时,获取到的是同一个Class,也就是Pair类的Class
    • 所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是某个类<Object>
  • 局限三:无法判断带泛型的类型

    • Pair<Integer> p = new Pair<>(123, 456);
      //Compile error:
      if (p instanceof Pair<String>) {
      }
      
    • 并不存在Pair<String>.class,而是只有唯一的Pair.class

  • 局限四:不能实例化T类型

    • public class Pair<T> {
          private T first;
          private T last;
          public Pair() {
              // Compile error:
              first = new T();
              last = new T();
          }
      }
      
      /*
      first = new T();
      last = new T();
      会变成
      first = new Object();
      last = new Object();
      
    • 要实例化T类型,我们必须借助额外的Class<T>参数

      • public class Pair<T> {
            private T first;
            private T last;
            public Pair(Class<T> clazz) {
                first = clazz.newInstance();
                last = clazz.newInstance();
            }
        }
        
        =======================
        Pair<String> pair = new Pair<>(String.class);
        Class<T>参数并通过反射来实例化T类型,使用的时候,也必须传入Class<T>
        

泛型继承

一个类可以继承自一个泛型类

//父类的类型是Pair<Integer>,子类的类型是IntPair
public class IntPair extends Pair<Integer> {
}
//因为子类并没有泛型类型,所以ok
IntPair ip = new IntPair(1, 2);

​ 在父类是泛型类型的情况下,编译器就必须把类型T(对IntPair来说,也就是Integer类型)保存到子类的class文件中,不然编译器就不知道IntPair只能存取Integer这种类型。

在继承了泛型类型的情况下,子类可以获取父类的泛型类型。

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class Main {
    public static void main(String[] args) {
        // 实例化一个类型为IntPair的Class实例
        Class<IntPair> clazz = IntPair.class;
        // 获取Class实例的父类泛型类
        Type t = clazz.getGenericSuperclass();
        // 判断是否是参数化类型(泛型)
        if (t instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) t;
            
            Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
            Type firstType = types[0]; // 取第一个泛型类型
            
            Class<?> typeClass = (Class<?>) firstType;
            System.out.println(typeClass); // class java.lang.Integer
        }
    }
}

// 泛型类
class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

// 一个子类继承泛型类
class IntPair extends Pair<Integer> {
    public IntPair(Integer first, Integer last) {
        super(first, last);
    }
}

小结:

  • 擦拭法决定了泛型<T>的局限
    • 不能是基本类型,例如:int
    • 不能获取带泛型类型的Class,例如:Pair<String>.class
    • 不能判断带泛型类型的类型,例如:x instanceof Pair<String>
    • 不能实例化T类型,例如:new T()
  • 泛型方法要防止重复定义方法,例如:public boolean equals(T obj)
  • 子类可以获取父类的泛型类型<T>

内容参考


Java泛型详解

廖雪峰的官方网站

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值