java泛型约束_Java 基础-泛型的约束和局限性

Java基础-泛型的约束和局限性

Java中的泛型是一个非常重要知识点,在这里,简单的介绍一下Java泛型的几个注意点。这里不会讲解Java中的泛型是怎么使用的,只会讲解在Java中使用的泛型的注意点

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

不能使用类型参数代替基本数据。因此,没有Pair,只有Pair(这里我们假设Pair是一个public class Pair 类型的一个类)。这个非常的好理解,想一想我们在使用List集合时,不能这样子来定义一个集合:List list = new ArrayList<>(),通常都是这样来定义一个int类型的集合:List list = new ArrayList<>();

这个是什么原因呢?有人可能要问。我们这里需要讲一下Java中泛型的类型擦除

(1).Java泛型的类型擦除

在我们定义一个泛型类的时候,都会自动的给我们提供一个相应的原始类型(这个原始类型不是像Integer对应的是原始数据类型是int)。这里的原始数据类型就是删除类型参数之后的泛型类型名、擦除类型变量,并且替换为限定类型(如果没有限定类型,那么就用Object来代替)。

例如:

擦除类型之前的Pair类

public class Pair {

private T first = null;

private T second = null;

public Pair() {

this.first = null;

this.second = null;

}

public Pair(T first, T second) {

this.first = first;

this.second = second;

}

public void setFirst(T first) {

this.first = first;

}

public void setSecond(T second) {

this.second = second;

}

public T getFirst() {

return first;

}

public T getSecond() {

return second;

}

}

擦除类型之后的Pair类

public class Pair {

private Object first;

private Object second;

public Pair() {

this.first = null;

this.second = null;

}

public Pair(Object first, Object second) {

this.first = first;

this.second = second;

}

public void setFirst(Object first) {

this.first = first;

}

public void setSecond(Object second) {

this.second = second;

}

public Object getFirst() {

return first;

}

public Object getSecond() {

return second;

}

}

我们会发现在在擦除之前,Pair里面的成员变量类型都是T类型,也就是泛型类型。但是在擦除之后,所有T类型都变成了Object类型。这个也就是我们之前说的,如果一个类型是无限定类型的话,会被替换成为Object类型。

如果泛型类型被限制了的,也就是T extends其他的类或者接口,就取extends关键字之后第一个类型来作为擦除之后的类型。为什么这里要强调是extends关键字之后第一个类型呢?因为extends关键字之后可以跟多个类或者接口,多个类或者接口使用&来连接。

例如,可以这样写:

public class Interval{

private T lower;

private T upper;

.....

}

擦除类型之后:

public class Interval{

private Comparable lower;

private Comparable upper;

......

}

(2).不用基本数据类型的原因

非常的明显,这里的原因肯定是类型擦除导致的。擦除之后,Pair类含有Object类型的成员变量,但是Object不能存储double类型的值。

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

在Java中,我们知道可以使用instanceof关键字来判断一个引用是否是一个类的对象。在泛型里面,这种代码是不支持的:

if(a instanceof Pair)

或者是强制类型转换:

Pair p = (Pair)a;

同样的道理,使用getClass方法返回的原始类型:

Pair stringPair = new Pair<>();

Pair integerPair = new Pair<>();

if(stringPair.getClass() == integerPair.getClass()){ //true

}

他们的比较结果是true,因为两次调用getClass方法都将返回的是Pair.class对象,是同一个对象。

3. 不能创建泛型类型的数组

不能创建泛型类型的数组,例如:

Pair pairs[] = new Pairs[10];

这个是为什么呢?

假设,记住这里是假设,如果能够创建泛型类型的数组,也就是说,我们上面的pairs数组是定义成功了的,那么我们如此操作,编译器是会报错的:

pairs[0] = "Hello";

这个报错的原因是非常简单的,Pair类型的数组,不能存储一个String类型的数据。

但是类型擦除会导致这个机制失效(类型匹配的机制)。因为如果定义泛型类型的数组成功的话,在擦除类型之后,数组的类型就从Pair[]类型转换为Pair[],那么Pair[]类型可以变为Object[]类型:

Pair pairs[] = new Pairs[10];//这里重新定义一个Pair类型的数组,表示类型擦除

Object[] obejcts = pairs;//将Pair类型的数组转换为Object类型的数组

objects[0] = "Hello";//这里就不会报错,因为这里数组存储的是Object类型,所以不会报错。

由于这个原因--能够通过数组存储数组的类型检查,出于这个原因,不允许创建泛型类型的数组。

需要说明的是,只是不允许创建泛型类型的数组,而生命类型为Pair[]的变量仍是合法的,只是不允许使用new Pair[10]这种方式来初始化变量。

注意:可以声明通配类型的数组,然后进行强制类型转换:Pair pairs = (Pair[]) new Pair>[10]

4. Varagrs警告

在上一节中,我们已经了解到了Java中不支持泛型类型的数组。这一节中我们再来讨论一下相关的问题:向参数个数可变的方法传递一个泛型类型的对象。

例如:

public static void addAll(Collection coll, T...ts){

for(T t:ts){

coll.add(t);

}

}

我们知道,在addAll方法中的ts参数是一个数组。

现在我们这样调用这个方法:

Collection> coll = new ArrayList<>();

Pair pair1 = new Pair<>();

Pair pair2 = new Pair<>();

Pair pair3 = new Pair<>();

addAll(coll, pair1, pair2, pair3);

为了成功的调用addAll方法,Java虚拟机必须为我们创建一个Pair类型的数组,这个就违反了前面的规则。不过,对于这种情况,规则有所放松,这里只是一个警告,而不是错误。

可以采用两种方法来抑制这个警告。一种方法是在addAll方法的前面增加注解@SuppressWarnings("unchecked");或者在Java7中,还可以使用@SafeVarargs直接标注addAll方法:

@SafeLVarargs

public static void addAll(Collection coll, T...ts)

注意:

这里我们可以使用@SafeVarargs注解来消除泛型数组的有关限制,方法如下:

@SafeVarargs

public static E[] array(E...array){

return array;

}

现在可以调用:

Pair[] pairs = array(pair1, pair2);

这个看起来非常的方便,不过隐藏着危险,以下代码:

Object[] oejcts = pairs;

objects[0] = new Pair();

这里能够顺利运行而且不会出现ArrayStoreException异常(因为数组存储时,只会检查擦除之后的类型),但是在处理pairs[0]时,有可能会在别处得到一个异常。

5. 不能创建泛型类型的变量

不能使用像new T(...)、new T[...]或者T.class这样的表达式。例如,下面Pair的构造方法是非法:

public Pair(){

this.first = new T();

this.second = new T();

}

类型擦除之后,将T变为了Object,而且本意上不是调用Object().在Java 8 出现之后,最好的解决办法是:让调用提供一个构造器的表达式,例如:

Pair p = Pair.makePair(String::new);

makePair方法接收一个Supplier类型的对象,这是一个函数式接口,表示一个无参数但是返回类型为T的函数:

public static Pair makePair(Supplier constr){

return new Pair<>(constr.get(), constr.get());

}

这种方式在Java 8比较适用,如果各位读者对Java 8不是很熟悉的,可以先去看看Java 8中方法引用,这里其实就是将Lambda表达式简写成为了方法引用的形式,也就是所谓的语法糖。

但是在传统的想法中,我们比较倾向于通过反射调用Class.newInstance方法来创建泛型对象:

first = T.class.newInstance();

但是遗憾的是,细节比较复杂,而且不能调用。表达式T.class是不合法的,因为擦除之后,类型成为Object.class。所以必须通过以下方法来设计,以便得到一个Class对象:

public static Pair makePair(Class clazz){

try {

return new Pair<>(clazz.newInstance(), clazz.newInstance());

}catch(Exception e) {

return null;

}

}

然后通过如下方法来调用:

Pair pair = Pair.makePair(String.class);

注意,Class类本身是泛型。例如,String.class是一个Class的对象。因此,makePair方法能够判断出pair的类型。

6. 不能构造泛型数组

就像不能创建一个泛型类型的对象,也不能创建泛型类型的数组。不过原因有所不同,毕竟数组会填充null值,构造是看上去是安全的。不过,数组本身也有类型,用来监控在虚拟机中的数组,这个类型会被擦除。例如:

public static T[] minAndMax(T a[]){

T ts[] = new T[2];

......

return ts;

}

类型擦除会让这个方法永远构造Comparable类型的数组。

但是如果数组是一个类的私有成员变量,就可以使用Object类型的数组,并且在获取元素时,进行类型转化。例如,ArrayList类可以这样实现:

public class ArrayList{

private Object[] elements;

......

@SuppressWarnings("unchecked")

public E get(int index){

return (E)elements[index];

}

public void set(E e, int index){

elements[index] = e;

}

}

实际上也可以这样写:

public class ArrayList {

private E[] elements;

@SuppressWarnings("unchecked")

public ArrayList() {

this.elements = (E[])new Object[10];

}

}

在minAndMax方法中,由于该方法返回的是一个泛型类型的数组,所以像上面的操作不能进行,但是如果想要实现功能的话,可以如下实现:

public static T[] minAndMax(T...ts){

Object[] objects = new Object[10];

......

return (T[]) objects;

}

然后调用代码:

String ss[] = ArrayAlg.minAndMax("pby", "pby123", "pby456");

上面这段代码在编译阶段是没有错误的,但是当我们调用这个方法会抛出一个ClassCastException异常。

在这种情况下,可以让用户提供一个数组的构造器表达式:

String [] ss = ArrayAlg.minAndMax(String[]::new, "pby", "pby123", "pby456");

然后在minAndMax方法中使用这个参数生成一个正确类型的数组:

public static T[] minAndMax(IntFunction constr, T...ts){

return constr.apply(2);

}

上面的写法是基于Java 8中的方法引用。如果使用老式的Java反射,调用Array.newInstance方法:

public static T[] minAndMax(T...a){

return (T[])Array.newInstance(a.getClass().getComponentType(), 2);

}

7.泛型类的静态上下文在泛型类型中无效

静态变量不能定义泛型类型,静态方法的返回类型不能定义为泛型类型。例如,下面的写法是错误的:

public class Interval{

private static T singleInstance; //错误,静态变量的类型不能为泛型类型

public static T getSingleInstance(){ //错误,静态方法的返回类型为泛型类型。

return singleInstance;

}

}

8.不能抛出或者捕获泛型类的异常

在Java中,不能对泛型类的异常对象进行抛出捕获。实际上,泛型类继承于Throwable类都是不合法的,例如,以下的代码是错误的:

public class Problem extends Throwable{

}

同时不能再catch语句中使用泛型类型的异常对象。例如:

public static void doWork(Class clazz){

try{

}catch(T e){ //错误,不能抛出泛型类型的异常对象

}

}

不过,在异常规范中,使用泛型类型的对象是允许的:

public static void doWork(T t){

throws T

try{

}catch(Throwable realCause){

t.initCause(realCause);

throw t;

}

}

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

Java异常处理的一个基本规则是:必须为所有受查异常提供一个处理器。不过我们可以利用这个泛型来取消这个限制。例如:

public abstract class Block {

public abstract void body() throws Exception;

public Thread toThread() {

return new Thread() {

@Override

public void run() {

try {

body();

}catch(Throwable t) {

Block.throwAs(t);

}

}

};

}

@SuppressWarnings("unchecked")

public static void throwAs(Throwable t) throws T{

throw (T) t;

}

}

然后我们在main方法里面开启一个线程来调用我们的方法。

public class Demo {

public static void main(String []args) {

new Block() {

@Override

public void body() throws Exception{

}

}.toThread().start();

}

}

有人可能会问这个有什么意义上呢?正常情况下,我们必须捕获run方法里面所有受查异常,不能从run方法里面向外面抛出一个异常,因为在Thread类里面的run方法没有抛出任何的异常,所以我们这里向外抛出任何的异常,所有的受查异常都必须爱run方法里面进行捕获。但是我们这里的操作就是,将受查异常包装为非受查异常,然后在catch里面抛出

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中,泛型是一种强类型机制,它可以让你在编译时检查类型错误,从而提高代码的安全性和可读性。在使用泛型时,我们经常会遇到父类和子类的泛型转换问题。 首先,我们需要明确一点:子类泛型不能转换成父类泛型。这是因为Java中的泛型是不协变的。例如,如果有一个类A和它的子类B,那么List<A>和List<B>之间是不存在继承关系的。 下面我们来看一个例子: ```java public class Animal { //... } public class Dog extends Animal { //... } public class Test { public static void main(String[] args) { List<Animal> list1 = new ArrayList<>(); List<Dog> list2 = new ArrayList<>(); list1 = list2; // 编译错误 } } ``` 在这个例子中,我们定义了Animal类和它的子类Dog。然后我们定义了两个List,分别是List<Animal>和List<Dog>。如果将List<Dog>赋值给List<Animal>,会出现编译错误。这是因为List<Animal>和List<Dog>之间不存在继承关系。 那么,如果我们想要让子类泛型转换成父类泛型,应该怎么办呢?这时我们可以使用通配符来解决问题。通配符可以表示任意类型,包括父类和子类。例如,我们可以将List<Dog>赋值给List<? extends Animal>,这样就可以实现子类泛型转换成父类泛型了。 下面我们来看一个使用通配符的例子: ```java public class Animal { //... } public class Dog extends Animal { //... } public class Test { public static void main(String[] args) { List<Animal> list1 = new ArrayList<>(); List<Dog> list2 = new ArrayList<>(); list1 = list2; // 编译错误 List<? extends Animal> list3 = new ArrayList<>(); list3 = list2; // 正确 } } ``` 在这个例子中,我们定义了List<? extends Animal>来表示任意继承自Animal的类型。然后我们将List<Dog>赋值给List<? extends Animal>,这样就可以实现子类泛型转换成父类泛型了。 总结一下,Java中的泛型是不协变的,子类泛型不能转换成父类泛型。如果需要实现子类泛型转换成父类泛型,可以使用通配符来解决问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值