Java学习----泛型详解

文章目录

  • 泛型
    • 优点
    • 泛型类
    • 泛型接口
    • 泛型方法
      • 泛型类中的泛型方法
      • 方法中的可变参数类型也可以为泛型。
      • 静态方法与泛型
    • 泛型在继承方面的细节
    • 泛型通配符
      • 常用泛型通配符
      • '?'无界通配符
        • 基本用法
      • extends和super上下界
    • ? 和 T 的区别
    • 泛型原理(泛型擦除)
      • 类型擦除
      • 类型擦除后保留的原始类型
        • 泛型方法调用
      • 编译时类型安全检测
      • 类型擦除与多态的冲突及其解决办法
      • 泛型类型不能是基本数据类型
      • 编译时集合的instanceOf
    • 协变、逆变与不变
      • 常见类型转换的三种特性
        • 泛型
        • 数组
        • 方法
    • 泛型实现协变和逆变
      • 小总结

泛型

在创建对象或调用方法的时候才明确下具体的类型

优点

  1. 泛型的本质是为了参数化类型,即在不创建新类型的情况下,通过泛型指定不同的类型来控制形参的类型。提高代码的复用性

  2. 泛型的引入提高了安全性,泛型提供了编译时类型安全检测机制,可以在编译阶段检测到非法的类型

  3. 没有泛型的情况下,要实现参数的任意化要使用"Object"。这么做的缺点是每次都要做显式的强制类型转化,这种转化需要开发者对于实际参数类型可以预知的情况下进行,若强制类型转换出错,只能在运行时发现,本身就是一个安全隐患。

总结:泛型的好处就是可以在编译时检查类型安全,且所有的强制类型转换都是自动和隐式的。

泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

泛型类在实例化时,可以传入泛型类型实参,限制其类型。若不传入,则在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中

泛型方法

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

public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。

只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。后面的T代表方法的返回值。

public <T> T genericMethod(T a) {

    return a;
}

泛型类中的泛型方法

class GenerateTest<T> {
    /**
     * 1、使用了泛型的方法,并不是泛型方法
     * 2、该类实例化后传入的泛型类型就是该方法的形参类型
     */
    public void show_1(T t) {
        System.out.println(t.toString());
    }


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

方法中的可变参数类型也可以为泛型。

静态方法与泛型

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

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

静态方法中不能使用类的泛型。

1、如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法) 即使静态方法要使用泛型类中已经声明过的泛型也不可以。如:public static void show(T t){…},此时编译器会提示错误信息: “StaticGenerator cannot be refrenced from static context”

2、泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。换句话说,泛型方法所属的类是不是泛型类都没有关系。

3、泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在初始化类时确定,所以无所谓

泛型在继承方面的细节

虽然类A是类B的父类,但是GG二者不具备子父类关系,二者是并列关系。

补充:类A是类B的父类,AB的父类

泛型通配符

常用泛型通配符

约定:为了提高可读性

  • ? 表示不确定的 java 类型
  • T (Type) 表示具体的一个java类型
  • K V (Key Value) 分别代表java键值中的Key Value
  • E (element) 代表Element

'?'无界通配符

基本用法
package keyAndDifficultPoints.Generic;

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

public class Test_Wildcard_Character {

    public static void main(String[] args) {
        List<Dog> dogList = new ArrayList<>();
        test(dogList);
        test1(dogList);
    }

    static void test(List<? extends Animal> animals) {
        System.out.println("test输出:");
        for (Animal animal : animals) {
            System.out.print(animal.toString() + "-");
        }
    }

    static void test1(List<Animal> animals) {
        System.out.println("test1输出:");
        for (Animal animal : animals) {
            System.out.print(animal.toString() + "-");
        }
    }


}

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

class Dog extends Animal {
    @Override
    public String toString() {
        return "Dog";
    }
}

对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 ),表示可以持有任何类型。像 test()方法中,限定了上界,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错,而test1()就不行。

extends和super上下界

  • extends确定上界 格式:,表示该参数可能为E,也可能为E的子类。上界为E
    • 如果传入的类型不是 E 或者 E 的子类,编译不成功
    • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
  • super确定下界 格式:。在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。下界为E

?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行,

T 是一个 确定的 类型,通常用于泛型类泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法

? 和 T 的区别

  1. T可以保证泛型参数的一致性,
  2. T可以通过&进行多重限定 比如public static void test2(T t) {}
  3. ? 可以使用超类限定而T不行
    1. T只有一种 类型限定方式 T extends A
    2. ? 有两种类型限定方式 extends和super 。

泛型原理(泛型擦除)

类型擦除

Java的泛型是伪泛型,在编译期间,所有的泛型信息都会被擦除掉,我们常称为泛型擦除

Java中的泛型基本上都是在编译器这个层次来实现的,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,编译器在编译的时候去掉,这个过程就称为类型擦除

如在代码中定义的ListList等类型,在编译后都会变成List,JVM看到的只是List。而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。

类型擦除后保留的原始类型

原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除,并使用其限定类型(无限定的变量用Object替换)。

注意:没运行前,泛型规定了多重限定时,在编译的时候取最小范围或共同子类

泛型方法调用

在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。

class Test {
    public static void main(String[] args) {
        //不指定泛型的时候
        int a1 = add(1, 2); //这两个参数都是Integer,所以T为Integer类型
        Number b1 = add(1, 1.2);//这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number
        Object c1 = add(1, "my");//这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Object

        //指定泛型的时候
        int a = Test.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
//        int b = Test.add(1, 2.2);//编译错误,指定了Integer,不能为Float
        Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
    }

    //这是一个简单的泛型方法
    public static <T> T add(T x, T y) {
        return x;
    }
}

编译时类型安全检测

ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add(1); //编译报错

ArrayList<String> arrayList1 = new ArrayList(); //第一种 情况
arrayList1.add(1); //编译报错

ArrayList arrayList2 = new ArrayList<String>();//第二种 情况
arrayList2.add(1);

泛型变量的使用,是会在编译之前检查的。

类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

类型擦除与多态的冲突及其解决办法

public class Test_principle05 {
    public static void main(String[] args) {
    }
}
class Generic<T> {
    //key这个成员变量的类型为T,T的类型由外部指定
    private T var;

    public T getVar() {
        return var;
    }

    public void setVar(T var) {
        this.var = var;
    }
}

class MyGeneric extends Generic<Integer>{
    @Override
    public Integer getVar() {
        return super.getVar();
    }
    @Override
    public void setVar(Integer var) {
        super.setVar(var);
    }
}

泛型擦除之后,父类Generic会变成下面这样

class Generic {
    private Object var;

    public Object getVar() {
        return var;
    }

    public void setVar(Object var) {
        this.var = var;
    }
}

子类MyGeneric不变,为了与泛型擦除后的父类对比,把子类也贴在下面

class MyGeneric extends Generic<Integer>{
    @Override
    public Integer getVar() {
        return super.getVar();
    }
    @Override
    public void setVar(Integer var) {
        super.setVar(var);
    }
}

我们通过对比可以发现,setVar方法应该是重载,而不是重写

重写与重载

重载(Overload):首先是位于一个类之中或者其子类中,具有相同的方法名,但是方法的参数不同,返回值类型可以相同也可以不同。

重写(override):一般都是表示子类和父类之间的关系,其主要的特征是:方法名相同,参数相同,但是具体的实现不同。

但是通过验证,我们可以知道确实是重写,而不是重载

 public static void main(String[] args) {
        MyGeneric myGeneric = new MyGeneric();
        myGeneric.setVar(new Integer(1));
        myGeneric.setVar(new Object());//编译错误
    }

如果是重载的话,第四行代码是不会报错的,因为调的是不同的重载方法。但是发现编译报错了,也就是说没有参数是Object的这样的重载函数。所以说是重写了,导致MyGeneric对象只能调用自己重写的方法。

原因是JVM采用桥方法来实现这样的重写。

通过查看字节码文件,我们本意重写setValue和getValue方法的子类,竟然有4个方法。最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。

泛型类型不能是基本数据类型

不能用类型参数替换基本类型。就比如,没有ArrayList,只有ArrayList。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

编译时集合的instanceOf

ArrayList<String> arrayList = new ArrayList<String>();

因为类型擦除之后,ArrayList只剩下原始类型,泛型信息String不存在了。

那么,编译时进行类型查询的时候使用下面的方法是错误的

if( arrayList instanceof ArrayList<String>)

协变、逆变与不变

详见

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);

  • f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
  • f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
  • f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。

常见类型转换的三种特性

泛型

f(A)=ArrayList,那么f(⋅)时逆变、协变还是不变的呢?如果是逆变,则ArrayListArrayList的父类型;如果是协变,则ArrayListArrayList的子类型;如果是不变,二者没有相互继承关系。开篇代码中用ArrayList实例化list的对象错误,则说明泛型是不变的。

数组

f(A)=[]A,容易证明数组是协变的:

Number[] numbers = new Integer[3]; 
方法

调用方法result = method(n);根据Liskov替换原则,传入形参n的类型应为method形参的子类型,即typeof(n)≤typeof(method's parameter);result应为method返回值的基类型,即typeof(methods's return)≤typeof(result)

泛型实现协变和逆变

  • 实现了泛型的协变,比如:
List<? extends Number> list = new ArrayList<Integer>();
  • 实现了泛型的逆变,比如:
List<? super Number> list = new ArrayList<Object>();

小总结

  • 要从泛型类取数据时,用extends;
  • 要往泛型类写数据时,用super;
  • 既要取又要写,就不用通配符(即extends与super都不用)。

你可能感兴趣的:(Java,java,开发语言,后端,泛型,java-ee)