目录
(一)  创造
(二)  泛型类
(三)  泛型方法
1.普通类中泛型方法
2.泛型类中泛型方法
(四)  虚拟机中的泛型
1.虚拟机中没有泛型,只有普通的类和方法
2.翻译泛型方法,桥方法被使用以保持多态性
(五)  通配符 “?”
1. 子类型限定和超类型限定(上下界限定)
2. 无限定通配符
(六)  泛型注意事项
1. 不能用基本类型实例化类型参数
2. 不能创建参数化类型数组
3. 运行时类型查询只适用于原始类型
4. 不能实例化类型变量
5. 泛型类的静态上下文中类型变量无效
6. 不能抛出或者捕获泛型类实例
7. 继承关系的类分别成为类型参数后无关系
8. 泛型类的静态方法必须作为泛型方法
9. 通配符不能作为一种类型使用
(七)  总结
首页 Java java教程 java泛型综合详解

java泛型综合详解

Mar 01, 2017 pm 01:13 PM

在日常生活中,我们经常用到泛型,但是泛型数据有些时候会报一些莫名其妙的错,而且一些通配符等语法、泛型在虚拟机中的真正操作方式也有我们值得研究之处,今天我们就一起来讨论一下泛型。

(一)  创造

在java增加泛型之前,当年都是用继承来处理现在用泛型操作的程序的。

ArrayList files = new ArrayList();String filename = (String) files.get(0);

ArrayList<String> files2 = new ArrayList<>();
//后一个尖括号中的内容可以省略,在1.7之后String filename2= files2.get(0);String addname = "addname";

files2.add(addname);
//在add函数调用之时,ArrayList泛型会自动检测add的内容是不是String类型
files2.add(true);
//报错 The method add(String) in the type ArrayList<String> is not applicable for the arguments (boolean)
登录后复制

比如ArrayList类只是维护了一个Object引用的数组,在获取值时需要类型强转(上面前两行),在传入内容的时候没有保证。有了泛型之后这个数据结构变得方便、可读性好且安全,我们可以一下看出ArrayList数组中存入类型为String,而且类似ArrayList的add方法也会自动的关联起String类型的参数,其他类型的值并不能add到列表中。

除此之外,还有一点想要说明的是,上面代码的前两行是没有错误的。因为在ArrayList中,不使用<>也是可以的,泛型类的限制还是很多的,在需要的情况下,也有要用到ArrayList类的时候。

(二) 泛型类

直接来一个简单的泛型类:

public class Pair<T> {    
private T first;    
private T second;    
public Pair() {
        first = null;
        second = null;
    }    public Pair(T first, T second) {        
    this.first = first;        
    this.second = second;
    }    
    public T getFirst() {        
    return first;
    }    
    public void setFirst(T first) {        
    this.first = first;
    }    
    public T getSecond() {        
    return second;
    }    
    public void setSecond(T second) {        
    this.second = second;
    }

}
登录后复制

其实泛型类可以看作是普通类的工厂,将T替换成我们需要的类型即可。那么对于需要两种类型的情况,我们只要写成Pair即可。T和U这两个域分别使用不同的类型。

上面的例子过于简单,也并不能做什么实质性的工作,那么请大家思考这样一个问题,如果我们在上面的泛型类中添加一个函数:

public T min(T[] array) {...}//返回传入数组中的最小值
登录后复制

那么这个时候,我们想要实现这个函数,我们至少要保证一点T类型的对象是可以比较大小的,怎么样去检测这个情况呢?实际上我们可以通过对类型变量T设置限定而解决这个问题。

public <T extends Comparable>T min(T[] array){...}//T如果没有实现compare方法则报错不执行min函数
登录后复制

<T extends Comparable>的意思大家也能猜出个八九不离十,就是要求T这个类型变量要继承Comparable这个接口。现在泛型的min方法只能被实现了Comparable接口的类(比如String,Date…)的数组调用,其他的类会产生一个编译错误。
相同的,也可以有多个限定:<T,U extends Comparable&Serializable>也是被允许的。逗号来分隔类型变量,&来分隔限定类型。

实际上Comparable本身就是一个泛型接口,为了上面内容能够简单的被理解,我们就装糊涂当作他不是泛型接口。但是要注意的是,其实上面的真正正确的写法是:>而不是<T extends Comparable>

(三) 泛型方法

1.普通类中泛型方法

泛型类定义的泛型在整个类中有效。就是说泛型类的对象在明确要操作的具体类型后,所有要操作的类型就已经固定了。比如ArrayList,整个ArrayList的泛型和String类型绑定了,不能修改。但在某些情况下我们需要更多的灵活性,为了让方法可以操作各种不确定类型,可以将泛型定义在方法上。

    public <T> T getMid(T[] array){        
    return array[array.length/2];
    }
登录后复制

比如上面的这个getMid的函数,在返回值T的前面加上<T>就使他变成了一个泛型方法。请注意,他是出于一个普通类中的,泛型方法不一定非要存在于泛型类中。下面给出调用泛型方法的例子,当调用一个泛型方法时,编译器会根据后面的类型来推断调用的方法,所以并不需要显示的表示出T是什么类型:

String mid = ArrayAlg.getMid("Jone" , "Q" , "Peter");
//okdouble mid2 = ArrayAlg.getMid(3.14 , 25.9 , 20);
//error,编译器会把20自动打包成Integer类型,而其他打包成Double类型,我们尽量不要让这种错误发生
登录后复制

2.泛型类中泛型方法

在普通类中,我们可以通过上述泛型方法来灵活调用不同的类型参数。在泛型类中,我们需要这么灵活的泛型方法么?我认为是不需要的。我们为泛型类提供了类型参数,实际上我们很少会在一个类型中用到其他的类型,如果一定要用到,我们一般采用Pair这样多个类型参数的方法,或者干脆再定义一个要使用类型的泛型类。
那么泛型方法在泛型类中有什么用呢?我们可以在泛型类中用泛型方法来为泛型变量做限定,比如我们在上面提到的 <T extends Comparable>T方法只能被有Comparable接口的T使用,否则会出错。

@SuppressWarnings("hiding")
public static <T extends Comparable<?> & Serializable> T min(T[] array) {...}
登录后复制

(四) 虚拟机中的泛型

1.虚拟机中没有泛型,只有普通的类和方法

java虚拟机中没有泛型类型对象——所有的对象都属于普通类,那么虚拟机是怎么用普通类来模拟出泛型的效果呢?
只要定义了一个泛型类,虚拟机都会自动的提供一个原始类型。原始类型的名字就是删除类型参数的泛型类的名字。擦除类型变量,并替换为第一个限定类型(如果没有限定则用Object来替换)。
比如,我们在一开始给出的简单泛型类Pair<T>在虚拟机中会变成如下的情况:

public class Pair {    
private Object first;    
private Object second;    
public Pair() {
        first = null;
        second = null;
    }    
    public Pair(Object first, Object second) {        
    this.first = first;        
    this.second = second;
    }    
    public Object getFirst() {        
    return first;
    }    
    public void setFirst(Object first) {        
    this.first = first;
    }    
    public Object getSecond() {        
    return second;
    }    
    public void setSecond(Object second) {        
    this.second = second;
    }

}
登录后复制

这就是擦除了类型变量,并且将没有限制的T换成Object之后的情况,如果有限定类型,比如,我们的泛型类是Pair按照替换为第一个限定类型的规定,替换后的情况如下:

public class Pair {    
private Compararble first;    
private Compararble second;    
public Pair() {
        first = null;
        second = null;
    }    
    public Pair(Compararble first, Compararble second) {        
    this.first = first;        
    this.second = second;
    }    
    public Compararble getFirst() {        
    return first;
    }    
    public void setFirst(Compararble first) {        
    this.first = first;
    }    
    public Compararble getSecond() {        
    return second;
    }    
    public void setSecond(Compararble second) {        
    this.second = second;
    }

}
登录后复制

那么既然类型都被擦除了,类型参数也被替换了,怎么起到泛型的效果呢?当程序调用泛型方法时,如果返回值是类型参数(就是返回值是T),那么编译器会插入强制类型转换:

Pair<String> a = ...;
String b = a.getFirst();//编译器强制将a.getFirst()的Object类型的结果转换为String类型
登录后复制

编译器将上述过程翻译位两条虚拟机指令:
- 对原始方法Pair.getFirst的调用。
- 将返回的Object类型强制转换为String类型。

2.翻译泛型方法,桥方法被使用以保持多态性

对于类型擦出也出现在泛型方法中,但是泛型方法中的擦除带来很多问题:

//----------------类A擦除前----------------------class A extends Pair<Date>{
    public void setSecond(Date second){...}
}//----------------类A擦除后----------------------class A extends Pair{
    public void setSecond(Date second){...}
}
登录后复制

从上面的代码中,我们可以看出,类A重写了Pair中的setSecond方法,看似合情合理,但是一旦擦除之后就发生了问题。将Pair擦除后,原函数变为:

public Class Pair{
    public void setSecond(Object second){...}
}
登录后复制

突然发现,父类Pair中的setSecond方法参数变为Object,和子类中的参数不同。擦除之后,父类的setSecond方法和子类的setSecond方法完全变为两个不一样的方法!这就没有了重写之说,那么接着考虑下面的语句:

A a = new A();Pair<Date> pair = a;pair.setSecond(new Date());
//当pair去调用setSecond函数时,有两个不一样的setSecond函数可以被调用,一个是父类中参数为Object的,一个是子类中参数为Date的。
登录后复制

第三行的调用出现了两种情况,这绝对不是我们想要的结果,我们开始的时候只是重写了父类中的setSecond方法,但是现在有两个不同的setSecond方法可以被使用,而且这时编译器不知道要去调用哪个。
为了防止这种情况的发生,编译器会在A类(子类)中生成一个桥方法

//------------桥方法--------------
public void setSecond(Object second){ setSecond((Date)second); }
登录后复制

这个桥方法,让Object参数的方法去调用Date参数的方法,从而将两个方法合二为一,这个桥方法不是我们自己写的,而是在虚拟机中自动生成的,让代码变得安全,让运行结果变得符合我们的期望。

(五) 通配符 “?”

当两个有关系的类分别作为两个泛型类的类型变量的时候,这两个泛型类是没有关系的。这个时候如果需要涉及到继承规则之类的内容时,那么就需要使用通配符——“?”。

1. 子类型限定和超类型限定(上下界限定)

有的时候两个类间有继承关系,但是分别作为泛型类型变量之后就没了关系,在函数调用和返回值的时候,这种不互通尤为让人头痛。好在通配符的上下界限定类型为我们安全的解决了这个难题。
? super A的意思是“?是A类型的父类型”,? extends A的意思是“?是A类型的子类型”。
既然知道了意思,我们看一下下面这四种用法:

public static void printA (Pair<? super A> p) {...} //ok
public static void printA (Pair<? extends A> p) {...} //error
public static Pair<? extends A> printA () {...} //ok
public static Pair<? super A> printA () {...} //error
登录后复制

为什么四种中有两种有错误呢?
实际上,要牢记这句话:带有超类型限定的通配符可以向泛型对象写入(做参数),带有子类型限定的通配符可以从泛型对象读取(做返回值)。道理这里就不详细讲了,如果有兴趣研究的话可以思考一下,思考的方向无非是继承关系之间的引用转换,只有上面两行ok的方法才是转换安全的。现在我们可以试着去理解上面的>了。

2. 无限定通配符

无限定通配符的用法其实很单一。无限定通配符修饰的返回值只能为Object,而其做参数是不可以的。那么无限定通配符的用处在哪里呢?源于他的可读性好。

public static boolean hasNulls(Pair<?>){...}
登录后复制

上面的代码的意思是对于任何类型的Pair泛型判断是否为空,这样写比用T来表示可读性确实好的多。

(六) 泛型注意事项

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

八大基本类型务必要使用包装类来实例化,否则泛型参数一擦除我们就傻眼了,怎么把int的数据放到Object里面呢?

2. 不能创建参数化类型数组

参数化的类型数组是不能被创建的。我们完全可以用多个泛型嵌套来避免这种情况的发生,如果创建了泛型数组,擦除之后类型会变成Object[ ],如果有一个类型参数不同的泛型存入这个数组时,因为都被擦除成Object,所以不会报错,导致了错误的发生。需要说明的是,只是不允许创建这些数组,而声明类型为Pair[]的变量是合法的,只不过不能用new Pair[10]初始化这个变量。更确切地表达是:数组类型不能是类型变量,采用通配符的方式是可以允许的

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

要记住在虚拟机中,每个对象都有一个特定的非泛型类。所以,所有的类型查询只产生原始类型。比如:

if(a instanceof Pair<String>) //只能测试a是否是任意类型的一个Pair
Pair<String> sp = ...;
sp.getClass(); //获得的也是Pair(原始类型)
登录后复制

4. 不能实例化类型变量

有的时候我们需要将类型变量实例化成它自己本身的类型,但是一定要注意写法,不可以直接实例化类型变量:

public Pair() {    this.first = new T();//错误,类型擦除后T变成Object,new Object()肯定不是想要的
    this.first = T.class.newInstance();//错误,T.class是不合法的写法}
登录后复制

上述的两种写法都是错的,如果一定要这样做的话,也只能在调用泛型类的时候构造出一个方法(构造的方法不在泛型类中,在调用泛型类的普通类中),将你要用的类型作为参数传进去,想办法来实例化。

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

不能在静态域中引用类型变量,静态方法本来就是和对象无关,他怎么能知道你传进来的是什么类型变量呢?

6. 不能抛出或者捕获泛型类实例

既不能抛出也不能捕获泛型对象。事实上,甚至泛型类扩展Throwable都是不可以的,但是平时我们不去编写泛型类的时候,这一条并不需要注意过多。

7. 继承关系的类分别成为类型参数后无关系

当两个有关系的类分别作为两个泛型类的类型变量的时候,这两个泛型类是没有关系的。这个时候如果需要涉及到继承规则之类的内容时,一定要使用通配符,坚决不要手软。

8. 泛型类的静态方法必须作为泛型方法

泛型类的静态方法因为是静态,所以也不能获得类型变量,在这个时候唯一的解决办法是——所有的静态方法都是泛型方法。

9. 通配符不能作为一种类型使用

? t = p.getA(); //error
登录后复制

所有用“?”来作为类型的写法都是不被允许的,我们需要用T来作为类型参数,有必要的时候可以做一个辅助函数,用T类型来完成工作,用?通配符来做外面的包装,既达到了目的,又提高了可读性。

(七)  总结

泛型为我们提供了许许多多的便利,包装出来的很多泛型类让我们能更快速安全的工作。而泛型的底层实现原理也是很有必要研究一下的,一些需要我们注意的事项和通配符的使用,此类细节也确实有许多值得我们学习之处。

在日常生活中,我们经常用到泛型,但是泛型数据有些时候会报一些莫名其妙的错,而且一些通配符等语法、泛型在虚拟机中的真正操作方式也有我们值得研究之处,今天我们就一起来讨论一下泛型。

 以上就是java泛型综合详解的内容,更多相关内容请关注PHP中文网(www.php.cn)!


本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

Java 中的史密斯数 Java 中的史密斯数 Aug 30, 2024 pm 04:28 PM

Java 史密斯数指南。这里我们讨论定义,如何在Java中检查史密斯号?带有代码实现的示例。

Java Spring 面试题 Java Spring 面试题 Aug 30, 2024 pm 04:29 PM

在本文中,我们保留了最常被问到的 Java Spring 面试问题及其详细答案。这样你就可以顺利通过面试。

突破或从Java 8流返回? 突破或从Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一种强大且表达力丰富的处理数据集合的方式。然而,使用Stream时,一个常见问题是:如何从forEach操作中中断或返回? 传统循环允许提前中断或返回,但Stream的forEach方法并不直接支持这种方式。本文将解释原因,并探讨在Stream处理系统中实现提前终止的替代方法。 延伸阅读: Java Stream API改进 理解Stream forEach forEach方法是一个终端操作,它对Stream中的每个元素执行一个操作。它的设计意图是处

Java 中的时间戳至今 Java 中的时间戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的时间戳到日期指南。这里我们还结合示例讨论了介绍以及如何在java中将时间戳转换为日期。

Java程序查找胶囊的体积 Java程序查找胶囊的体积 Feb 07, 2025 am 11:37 AM

胶囊是一种三维几何图形,由一个圆柱体和两端各一个半球体组成。胶囊的体积可以通过将圆柱体的体积和两端半球体的体积相加来计算。本教程将讨论如何使用不同的方法在Java中计算给定胶囊的体积。 胶囊体积公式 胶囊体积的公式如下: 胶囊体积 = 圆柱体体积 两个半球体体积 其中, r: 半球体的半径。 h: 圆柱体的高度(不包括半球体)。 例子 1 输入 半径 = 5 单位 高度 = 10 单位 输出 体积 = 1570.8 立方单位 解释 使用公式计算体积: 体积 = π × r2 × h (4

PHP与Python:了解差异 PHP与Python:了解差异 Apr 11, 2025 am 12:15 AM

PHP和Python各有优势,选择应基于项目需求。1.PHP适合web开发,语法简单,执行效率高。2.Python适用于数据科学和机器学习,语法简洁,库丰富。

PHP:网络开发的关键语言 PHP:网络开发的关键语言 Apr 13, 2025 am 12:08 AM

PHP是一种广泛应用于服务器端的脚本语言,特别适合web开发。1.PHP可以嵌入HTML,处理HTTP请求和响应,支持多种数据库。2.PHP用于生成动态网页内容,处理表单数据,访问数据库等,具有强大的社区支持和开源资源。3.PHP是解释型语言,执行过程包括词法分析、语法分析、编译和执行。4.PHP可以与MySQL结合用于用户注册系统等高级应用。5.调试PHP时,可使用error_reporting()和var_dump()等函数。6.优化PHP代码可通过缓存机制、优化数据库查询和使用内置函数。7

创造未来:面向零基础的 Java 编程 创造未来:面向零基础的 Java 编程 Oct 13, 2024 pm 01:32 PM

Java是热门编程语言,适合初学者和经验丰富的开发者学习。本教程从基础概念出发,逐步深入讲解高级主题。安装Java开发工具包后,可通过创建简单的“Hello,World!”程序实践编程。理解代码后,使用命令提示符编译并运行程序,控制台上将输出“Hello,World!”。学习Java开启了编程之旅,随着掌握程度加深,可创建更复杂的应用程序。

See all articles