Java高级语言特性 ---- 泛型

        泛型是jdk1.5开始引入的特性。泛型既是参数化类型,理解为将参数的类型作为参数。泛型可以作用在类、接口和方法上。分别称为泛型类、泛型接口和泛型方法。使用泛型的好处有:1、适用于多种数据类型执行相同的代码;2、获得编译期的类型安全,以及运行时更小的抛出ClassCsstException的可能。

        “适用于多种数据类型”,即是参数类型指定为泛型,在实际使用中根据需要传入具体的类型。例如下面代码:

// 定义一个泛型方法,接收T类型参数,T既是泛型
public <T> void method (T param1, T param2) {
    // ...
}
    
public void test() {
    method(0.1f, 0.2f); // 参数是float类型
    method("aaa", "bbb"); // 参数是double类型
    method(1, 2); // 参数是int类型
}

        关于“ “获得编译时的类型安全””。在Java语言处于还没有出现泛型时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。只有程序员和程序运行期才知道这个Object到底是什么类型。导致异常的风险转嫁到运行期。 

        例如定义一个List集合,向其中分别加入Interger类型和String类型。在使用时,可能会由于忘记之前加入的值的类型,导致使用类型错误,而在运行期抛出“ClassCastException”。使用泛型,则可以在编译期就能确定是否正确,从而获得运行期的安全。

// 定义一个存放String类型的List
private List<String> myList = new ArrayList<>();
    
// 该List只能存放String类型,如果存放其他类型,在编译器会报错
public void set() {
    myList.add("aa");
    myList.add("bb");
}

// 从容器中取出来的一定是String类型,无需进行强制类型转换
public void get(int index) {
    String str = myList.get(index);
}

泛型类、泛型接口、泛型方法

        泛型类的定义,引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等),用<>括起来,放在类名的后面。泛型类允许有多个类型变量。

public class Generator<T,E> {

    private T data;
    private E value;

    public void method(T data, E value) {
        this.data = data;
        this.value = value;
    }
}

        泛型接口与泛型类定义类似。泛型接口的实现类可以选择指定具体类型也可以选择不指定。

// 定义泛型接口
public interface IGenerator<T> {
    public T method();
}

// 实现类实现泛型接口,并指定泛型类型
// 这种方式,在创建该类型对象时和普通类没有区别
public class Generator1 implements IGenerator<String> {
    @Override
    public String method() {
        return null;
    }
}
Generator1 generator1 = new Generator1();

// 实现类实现泛型接口,但不指定泛型类型
// 这种方式在创建具体对象时,需要指定具体的类型
public class Generator2<T> implements IGenerator<T> {
    @Override
    public T method() {
        return null;
    }
}
Generator2<String> generator2 = new Generator2<>(); 

        泛型方法的定义,是将类型变量放在方法的访问修饰符和返回类型之间。泛型方法可以定义在普通类或者泛型类中。它是在调用方法的时候指明具体的类型。

        在泛型类中用类的泛型作为参数或返回值的方法,并不是泛型方法,认为是普通方法。只有按照泛型方法定义的才是泛型方法。泛型方法的类型变量也允许有多个。

public class Generic<T> {
    // 这只是泛型类的普通方法
    public void gMethod(T data) {
        this.data = data;
    }
}

// 这才是一个泛型方法,可以在普通类和泛型类中定义
public <T> void gereratorMethod(T param) {
    // ...
}

// 调用泛型方法,在调用方法时明确具体的类型
public void test1() {
    gereratorMethod("hello");
    gereratorMethod(123);
}

 限定类型变量、通配符

        限定类型变量,通常用于需要对类型变量进行约束的情形。如,泛型方法中,泛型类对象需要具有比较的功能。则将泛型T限制为需要实现带有比较方法接口的类。

public <T extends Comparable> void test(T a, T b) {
    if (a.compareTo(b) > 0) {
        // ...
    }
}

public interface Comparable<T> {
    public int compareTo(T o);
}

        限定类型变量需要用extends关键字实现。“T extends Comparable”,T表示应该绑定类型的子类型,Comparable表示绑定类型,子类型和绑定类型可以是类也可以是接口。经过限定类型修饰后,我们如果试图传入一个没有实现接口Comparable的类的实例,就会发生编译错误。

       子类型和绑定类型(即在extends左右两边)都允许有多个,例如“T,V extends Comparable & Serializable”。在extends右边,即绑定类型,最多只能有一个类类型,并且有类型类的话,必须放在第一个位置,如“T extends MyClass & MyInterface1 & MyInterface2 ”。

        通配符,适用于用存在继承关系的类具现化泛型的情况。如用存在继承关系的两个类对象,分别实例化泛型,则产生的泛型对象是完全不同的类,如下面的代码。如果需要在下面funtion函数中,也可以接收子类具现话的泛型,则要用到通配符。

// 定义泛型类
public class Generator<T> {}

// Apple和Fruite存在继承关系
public class Apple extends Fruit {}

// 需要一个Generator<Fruit>类型的参数
public void function(Generator<Fruit> gFruit) {}

public void method() {
    // 具现化泛型的类存在继承关系
    // 而Generator<Fruit> 和 Generator<Apple>是完全不同的类型
    Generator<Fruit> fruit = new Generator<>();
    Generator<Apple> apple = new Generator<>();
    
    // function方法可以接收fruit而不能接收apple
    function(fruit);
}

        上面代码function函数做一下修改,增加接收通配符类型的参数“? extends Fruit”。 则现在它也可以接收由子类具现化泛型的实例。

public void method() {
    Generator<Fruit> fruit = new Generator<>();
    Generator<Apple> apple = new Generator<>();
    
    // function方法可以接收fruit和apple
    function(apple);
    function(fruit);
}

public void function(Generator<? extends Fruit> gFruit) {}

         "? extends X" 是上界通配符,表示类型的上界,传递的类型参数必须是X的子类或本身;同时还有下界通配符“? super X”,表示类型的下界,传递的类型参数必须是X的超类或本身。下界通配符使用和上界通配符类似。

        上界通配符,通常用于安全的访问数据,即获取元素时使用,因为编译器可以确定获取到的基类类型;而下界通配符适用于安全的写入数据,即设置元素时使用,因为写入的数据是X或者是X的超类,X可以安全的转换为其超类。

        无限通配符,只用一个“?”表示,认为对类型没有限制,可以看成所有类型的父类。如“ArrayList<?> al=new ArrayList<?>()”表示集合元素可以是任意类型。这样修饰的容器没有实际的意义,获取元素只能是Object类型;无法设置元素,Object类型的元素也不行。

泛型的约束和局限

        1、不能用基本数据类型实例化泛型,如“Generator<double>” 是不允许的;

        2、不能创建泛型数组,如“Generator<Apple>[] appleArray = new Generator<>()[10]”是不允许的;

        3、泛型类型变量不能直接被实例化,如 "T t = new T()"是不允许的;

        4、不能直接捕获泛型类型,如“try { } catch (T t) { }”是不允许的;

        5、不能用于运行时类型查询,如“if(apple instanceof Generator<Apple>) {}” 和 “if(apple instanceof Generator<T>){}” 都是不允许的;

        6、静态域或方法中不能引用泛型类型(泛型方法可以是泛型方法)。这是因为泛型是要在对象创建的时候才能确定类型,而静态的内容在对象创建之前确定,此时虚拟机无法识别。

public class Generic<T> {
    // 静态域中不能用泛型
    // private static T data;
    
    // 静态方法中不能用泛型
    // public static void method(T data) {}

    // 可以定义泛型静态方法
    public static <E> void sMethod(E data) {}
}

JVM的泛型实现--泛型擦除

        泛型思想早在C++语言的模板(Template)中出现。但是泛型技术在C++和Java在实现上有着本质的不同。

        C++的泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言),或是运行期的CLR中,都是切实存在的。如,List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型

        Java语言中的泛型只存在程序源码中,在编译的字节码文件中,就已经替换为了原生类型(Raw Type,也称为裸类型),并在相应的位置插入了强制转型的代码。在运行期,ArrayList<int>与ArrayList<String>就是相同的类型 。Java语言的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型

        由于Java引入泛型,需要在多种应用场景(如,反射)下的使用,而产生新的需求。如,在泛型类中获取传入的参数化类型。因此,JCP组织针对泛型修改了虚拟机规范,引入了如Signature、LocalVariableTypeTable等属性用于解决泛型带来的参数类型识别问题。如Signature用来存储方法在字节码层面的特征签名,该属性中保存了泛型的信息。新的虚拟机规范要求能够识别49.0以上版本的Class文件的虚拟机都能够支持Signature参数。

        因此,从Signature属性上说,java泛型擦除,是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的依据。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值