Java8基础知识(十)泛型的约束与局限性

泛型的约束与局限性

由于泛型是通过类型擦除、强制类型转换和桥方法来实现的,所以存在某些局限(大多来自于擦除)。

不能使用基本类型实例化类型参数

类型参数都是类,要用包装器将基本类型包装才可以作为类型参数(原因在于擦除类型后Object类不能存储基本类型的值)。当包装器类不能接受类型参数替换时,可以使用独立的类和方法进行处理。

运行时类型查询只适用于原始类型

由于虚拟机中的对象都有特定的原始类型,所以类型查询只能查询原始类型。

// 只能测试a是否为某种Pair类型
if (a instanceof Pair<String>) {...}
if (a instanceof Pair<T>) {...}
Pair<String> p = (Pair<String>) a;

当代码试图使用instanceof查询对象是否属于泛型类型时,编译器会报错;如果使用强制类型转换会得到警告。

同样,即使使用getClass方法也只能返回原始类型。

不能创建参数化类型的数组

由于擦除类型后,只剩下原始类型,所以如果允许创建参数化类型的数组的话,就可以在之后将这个数组向上转型为Object[]然后向其中添加提供的参数类型以外的参数类型定义的泛型对象。这违背了类型安全的要求。

Pair<String> table = new Pair<String>[10];
Object[] = objArray = table;
// objArray记住了table的原始类型Pair,因此存储String类会抛出ArrayStoreException异常
objArray[0] = "Hello";
// objArray只记住了Pair,因此添加Pair<Employee>也不会编译报错,但会导致类型错误
objArray[0] = new Pair<Employee>();

注:不允许创建数组不代表不允许声明数组变量,只是不能用new来对其初始化。

注2:可以声明通配类型的数组,然后通过类型转换来初始化变量,但这仍是不安全的。

提示:唯一安全有效地收集参数化类型对象的方法是ArrayList:ArrayList<Pair<String>>

Varargs警告

当向一个参数个数可变的方法传递泛型类型实例时,实际上虚拟机必须建立一个参数化类型的数组作为方法的参数,这就违反了不允许创建参数化类型数组的规定。不过编译器只会发出警告,而不是错误。

能够确定调用正确时,可以使用注解或@SafeVarargs标注方法来取消警告。

不能实例化类型变量

不能实例化T类型,因为其会被擦除为Object类型。

最好的解决办法是让调用者提供一个构造器表达式,即设计一个方法接收Supplier<T>函数式接口来获取T类型实例。此时T是由传入的函数式接口决定的,不必担心被擦除。

// 向该方法传入构造器引用即可获取T类型的实例
public static <T> Pair<T> makePair(Supplier<T> constr) {
    return new Pair<>(constr.get(), constr.get());
}

不能构造泛型数组

存在类型限制的情况下,类型擦除会导致生成的数组总是限制类型。

如果泛型数组仅作为类的私有实例域,可以将这个数组声明为Object[],在获取元素时进行类型转换即可。

如果泛型数组别有它用,应当提供构造器引用来生成正确类型的数组。方法与实例化泛型变量类似。

泛型类的静态上下文中类型变量无效

如果设计一个返回静态泛型域的静态方法,类型擦除会导致该方法返回Object类型的静态域。由于该静态域会被该泛型类中的所有对象共享,所以可能无法应用到对象上。

// 擦除类型后静态变量为Object类型
public class Singleton<T> {
    private static T singleInstance;
    private static T getSingleInstance() {
        if (singleInstance == null) // construct new instance of T
        return singleInstance;
    }
}
// 无法确定返回类型
SingletonA = new Singleton<Integer>();
SingletonB = new Singleton<Boolean>();
Singleton.getSingleInstance();

不能抛出或捕获泛型类的实例

泛型类禁止扩展Thorwable接口。但泛型类型T可以扩展Throwable接口,但不能捕获或抛出。

// 不合法
public static <T extends Throwable> void doWork(Class<T> t) {
    try {
		// doWork
    }catch (T e) {
        Logger.global.info(...);
    }
}
// 合法
public static <T extends Throwable> void doWork(T t) throws T {
    try {
		// doWork
    }catch (Throwable realCause) {
        t.initCause(realCause);
        // t被擦除为Throwable类,然后返回后被调用者强制转换为T类型
        throw t;
    }
}

可以消除对受查异常的检查

使用泛型可以同时为所有的受查异常提供处理器。

考虑一个线程程序:

package tClass;

public abstract class Block {
    public abstract void body() throws Exception;

    public Thread toThread(){
        return new Thread() {
            // run方法声明不抛出异常
            public void run() {
                // 将所有异常转换为编译器认为的非受查异常
                try{
                    body();
                }catch (Throwable t) {
                    Block.<RuntimeException>throwAs(t);
                }
            }
        };
    }

    @SuppressWarnings("unchecked")
    public static <T extends Throwable> void throwAs(Throwable e) throws T {
        throw (T)e;
    }
}
package tClass;

import java.io.File;
import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        new Block() {
            // body方法打开不存在的文件,抛出受查异常FileNotFoundException
            public void body() throws Exception {
                Scanner in = new Scanner(new File("notExistedFile"), "UTF-8");
                while(in.hasNext())
                    System.out.println(in.next());
            }
        }.toThread().start();
    }
}

通过上述方法,不必捕获run方法中的受查异常,只需令编译器认为抛出的是非受查异常。在run方法产生多种异常的情况下,这种技术可以避免编写捕获多个异常并包装为非受查异常抛出的繁琐代码。

注意擦除后的冲突

当泛型类重载了超类中的方法时,若重载的参数类型是T,则会被擦除为Object。这可能会导致冲突。

public class Pair<T> {
    // 被擦除为eqauls(Object value)与从Object类继承的equals(Object)冲突
    public boolean equals(T value) {return first.equals(value) && second.eqauls(value);}
}

泛型规范原则:要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口时同一接口的不同参数化。

存在上述原则的原因在于:实现接口参数化的类会获得接口的桥方法,如果实现两个同一接口的不同参数化,就会获得两个签名完全相同的桥方法,导致冲突。

原文地址:https://www.cnblogs.com/aries99c/p/12616634.html

时间: 2024-08-02 20:31:14

Java8基础知识(十)泛型的约束与局限性的相关文章

黑马程序员——集合基础知识(泛型)

集合:泛型基础知识 泛型.(泛型就是类型参数化,默认的时object,虽然不用强制类型转换,这个就要你自己去写特性方法,比如compareto是string的特有方法吧,你可以写但是父类肯定调用不了) itnex t对象都是obj要使用对象的特性功能必须强.编译的时候没问题,因为都不知道你会传什么对象,你橙子也可以当作apple来传,设计的时候并不知道! 泛型作用.1.用于解决安全问题.运行时期出现的问题classcastexception转移到编译时期.2.迭代器里面的itnext()不用强转

ASP.NET Core 2.2 基础知识(十六) SignalR 概述

原文:ASP.NET Core 2.2 基础知识(十六) SignalR 概述 我一直觉得学习的最好方法就是先让程序能够正常运行,才去学习他的原理,剖析他的细节. 就好像这个图: 所以,我们先跟着官方文档,创建一个 SignalR 应用: https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/signalr?view=aspnetcore-2.2&tabs=visual-studio 这个例子一共涉及到下面几个步骤: 自定义中心 ChatH

Java 泛型的约束与局限性

Java 泛型的约束与局限性 @author ixenos 不能用基本类型实例化类型参数 不能用类型参数代替基本类型:例如,没有Pair<double>,只有Pair<Double>,其原因是类型擦除.擦除之后,Pair类含有Object类型的域,而Object不能存储double值.这体现了Java语言中基本类型的独立状态. 运行时类型查询只适用于原始类型(raw type) 运行时:通常指在Classloader装载之后,JVM执行之时 类型查询:instanceof.getC

集合框架、泛型、迭代(java基础知识十六)

1.ArrayList存储自定义对象并遍历 此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的 remove 或 add 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出 ConcurrentModificationException.* ArrayList存储自定义对象并遍历 ArrayList<Person> list = new ArrayList<>(); list.

Java8基础知识(九)泛型

泛型 在增加泛型类前,泛型程序设计是用继承实现的,要将方法参数和域的类型设计为Object,通过强制类型转换实现设计.由于Object在编译阶段几乎不会报错,所以很难通过静态类型检查发现这种设计下隐藏的错误. 使用类型参数后,通过编译器就可以检测提供的参数类型是否错误,使程序具有更好的可读性和安全性. 但实现泛型类也存在一定的困难,因为设计的方法同样要对所有的类型都能够编译且正确运行. // 已知在ArrayList中设计addAll方法来向当前list中添加另一个list的所有元素 Array

C#基础知识之泛型

泛型在c#中有很重要的位置,对于写出高可读性,高性能的代码有着关键的作用. 其实官方文档说明的很详细,我这边算是做个记录吧 一.什么是泛型? 泛型是 2.0 版 C# 语言和公共语言运行库 (CLR) 中的一个非常重要的新功能. 泛型(Generic) 允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候.换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法.您可以通过数据类型的替代参数编写类或方法的规范.当编译器遇到类的构造函数或方法的函数调用时,它会生

java基础知识十二

第十二章 异常 异常(Exception):又称为例外,是程序在运行过程中发生的非正常事件,其发生会影响程序的正常执行.Exception是程序级错误,可由程序本身处理:Error是系统级错误,程序可不用处理.Java异常类都必须继承Throwable类或其子类.用户通过继承自定义异常.常见异常:除数为零.负数开方.数组越界.I/O异常. 抛出的异常由catch捕获,未被捕获的异常逐层传播直到main.如果main也没有处理该异常,则操作系统会终止main执行. 处理异常时,也可以抛出新异常,也

Iava基础知识(十)

一.继承方式实现多线程和实现方式实现多线程对比1.继承方式(继承Thread类)(1)实现步骤A.将类声明为 Thread 的子类:B.该子类应重写 Thread 类的 run 方法:C.在主线程进行该自定义的线程类的对象的创建.(2)实现特点A.启动线程时,不使用run()方法原因:ran()方法不能作为启动线程的方法,该方法的调用相当于普通方法,并不能体现出线程执行的随机性.B.启动线程用Start()方法,start()方法通过JVM调用run()方法.一个线程不能连续启动,会出现非法状态

Android学习之基础知识十四 — Android特色开发之基于位置的服务

一.基于位置的服务简介 LBS:基于位置的服务.随着移动互联网的兴起,这个技术在最近的几年里十分火爆.其实它本身并不是什么时髦的技术,主要的工作原理就是利用无线电通讯网络或GPS等定位方式来确定出移动设备所在的位置,而这种技术早在很多年前就已经出现了. 那么为什么LBS技术直到最近几年才开始流行呢?这主要是因为,在过去移动设备的功能及其有限,即使定位到了设备所在的位置,也就仅仅只是定位到了而已,我们并不能在位置的基础上进行一些其他的操作.而现在就大大不同了,有了Android系统作为载体,我们可