T first;
T second;
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
可以看出来Pair类就是一个泛型类,与普通的类区别在于:
**1.类名后面多了一个<T>**
**2.参数first和second分别是泛型T类型**
那么这个T是什么呢?T是一种泛指,表示类型参数,泛型就是类型参数化,处理的数据不是固定的,而是可以动态指定类型作为参数传入。那么定义的泛型类如何使用呢?如下:
```java
Pair<Integer> minmax = new Pair<Integer>(1,100);
Integer min = minmax.getFirst();
Integer max = minmax.getSecond();
可以看到Pair中的Integer就是之前定义的泛型T的实际类型参数,当然这里的T可以是任何类型,我们这里可以指定为Integer,也可以指定为任何类型。同样的,泛型的参数类型数量不是固定的,我们可以申明多个不同类型的动态泛型类型,两个泛型之间使用逗号分割,如下:
public class Pair<U, V> {
U first;
V second;
public Pair(U first, V second){
this.first = first;
this.second = second;
}
public U getFirst() {
return first;
}
public V getSecond() {
return second;
}
}
改进后的Pair类可以这么使用:
Pair<String,Integer> pair = new Pair<String,Integer>("张三",100);
泛型的基本原理
看到上面的案例我们大概知道了一个简单的泛型如何定义,那么不禁会有一个疑惑,那就是泛型类型到底是什么呢?我们为什么一定要定义一个类型参数呢?熟悉Java多态特性的我们都知道,我们完全可以定义一个通用的父类类型,然后传递具体的子类型不也能实现这样的操作吗?同样的Java中也存在所有的类的基类–Object,如果我们直接使用Object不也可以吗?如下:
public class Pair {
Object first;
Object second;
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
使用的时候的代码只要这么改动:
Pair minmax = new Pair(1,100);
Integer min = (Integer)minmax.getFirst();//字段强制转换
Integer max = (Integer)minmax.getSecond();//字段强制转换
这样使用其实是可以的,事实上Java提供的泛型机制其实底层就是如此实现的。之所以这么设计,与Java当初设计的时候的jvm虚拟机编译机制有关系,要知道泛型设计的时候Java才到Jdk1.4版本,而我们都知道Java有编译器和Java虚拟机,编译器会帮我们把Java代码转换为**.Class**,虚拟机则是负责加载**.Class**,对于泛型类,Java编译器会把泛型部分的代码转换为普通的代码,即和上面的Object类型接管一样,将类型的T进行擦除,替换为Object,并且进行必要的类型的强制转换操作,所以在Java虚拟机执行Java字节码的过程中,其实和Object操作是一样的,并不知道泛型,也不存在泛型。那么既然泛型还是会转换为Object,进行泛型擦除,Java为什么要在1.5开始支持并设计出泛型机制呢?
泛型的好处
其实想要理解这点,我们不妨考虑一下,泛型的好处在哪?同时也去思考一下如果我们使用Object编程,缺陷会存在在哪?熟悉泛型的都知道,泛型有两个好处:
1.更好的安全性
2.更好的可读性
我们也知道Java语言在我们开发编译的阶段,ide就会进行代码检查,当我们的语法出现问题的时候,ide会在编译阶段就把错误标识出来,减少程序的潜在Bug数。但是我们不妨看下Object操作的代码:
Pair pair = new Pair("张三",1);
Integer id = (Integer)pair.getFirst();
String name = (String)pair.getSecond();
可以看出来,无论id是否为Integer类型,或者name是否为String类型,我们在编译阶段,由于类型为Object,我们都会进行强制转换操作,在编译期这些操作都是语法合理的,并不会报错,但是如果这些字段中存在类型错误,也必须等到程序运行到这里才会提示ClassCastException异常,但是如果我们使用的是泛型机制,并且使用的时候标明了类型为String和Integer,那么如果我们使用的类型不一致,在编译时已经报错,必须修改后才可以成功运行,如下:
Pair<String,Integer> pair = new Pair<>("张三",1);
Integer id = pair.getFirst(); //编译错误
String name = pair.getSecond(); //编译错误
所以很明显的可以看出来,如果使用了泛型后,类的后缀添加对应的泛型类型,我们很明确的知道具体的类型是什么,提高开发的可读性,并且因为ide会做类型检查,所以安全性也会更高
泛型方法
当然泛型的作用域范围比较广,我们不仅可以定义在类/接口的申明上,我们也可以将泛型作用在方法上,与类的泛型相互隔离,实现更精细粒度的泛型操作。并且需要注意的是,一个类的泛型定义和方法的泛型定义并无直接关系,两者是相互独立的,即类的泛型可以定义为T,而方法也可以定义为泛型T,但是这两个T并不属于同一个。首先我们先看一个泛型方法的案例:
public static <T> int indexOf(T[] arr, T elm){
for(int i=0; i<arr.length; i++){
if(arr[i].equals(elm)){
return i;
}
}
return -1;
}
可以看出来,indexOf方法就是一个泛型方法,使用的时候,我们可以如下:
indexOf(new Integer[]{1,3,5}, 10)
同样的泛型方法拥有和泛型类一样的所有特性,也可以定义多个泛型参数在方法上,比如:
public static <U,V> Pair<U,V> createPair(U first, V second){
Pair<U,V> pair = new Pair<>(first, second);
return pair;
}
但是与泛型类不同的是,使用的时候只需要传入确定类型的值即可,并不需要申明泛型类型后缀,如下:
createPair("张三",1);
泛型的上限界定
在前面的学习中我们都知道泛型擦除会转化为Object类型,但是我们能不能给Object的范围缩小呢?即限制泛型的父类类型上限是多少,在Java中其实是支持的,而泛型中支持这个上限界定是使用了extends关键字来表示的,当然这里的父类类型可以是接口、类或者类型参数,我们分别介绍下:
接口作为父类类型
比如我们开发中遇到一个场景,我们必须实现Comparable接口来实现动态的类型的比较,这个时候代码如下:
public static <T extends Comparable> T max(T[] arr){
T max = arr[0];
for(int i=1; i<arr.length; i++){
if(arr[i].compareTo(max)>0){
max = arr[i];
}
}
return max;
}
max是泛型类型T的数组的对应下标的值,不过这么编写代码的话,会被编译器警告,因为Comparable接口本身也是个泛型接口,所以我们写的时候建议也去指定Comparable接口的泛型上界,修改如下:
public static <T extends Comparable<T>> T max(T[] arr){
...................
}
此种方式可以实现泛型类型的递归类型限制传递
上界为具体类
还记得我们上面的实例Pair类使用的泛型类型,我们可以实现一个子类:
public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {
public NumberPair(U first, V second) {
super(first, second);
}
}
当我们限制了对应的类型范围后,我们就可以把first和second变量作为Number类型进行处理了,比如我们内部有一个求和的方法:
public double plus(){
return getFirst().doubleValue() + getSecond().doubleValue();
}
所以当我们定义完后,我们的使用即为如下这样:
NumberPair<Integer, Double> pair = new NumberPair<>(10, 12.34);
double sum = pair.plus();
可以看出来,限制了泛型类型范围后,编译器检查的会更严格,如果类型不对直接会报错,并且泛型擦除的时候转换的类型则为指定的范围上界的类型
泛型的通配符
上面我们提到了一些例子,就是使用了参数类型作为范围上界,但是这种写法比较繁琐,有木有更简化的写法呢?当然有,泛型支持通配符形式,可以简化范围上界的泛型写法,一个简单的通配符泛型如下:
public void addAll(DynamicArray<? extends E> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
一线互联网大厂Java核心面试题库
界的泛型写法,一个简单的通配符泛型如下:
public void addAll(DynamicArray<? extends E> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
一线互联网大厂Java核心面试题库
[外链图片转存中…(img-Jz7svK63-1628674593126)]
正逢面试跳槽季,给大家整理了大厂问到的一些面试真题,由于文章长度限制,只给大家展示了部分题目,更多Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等…已整理上传在我的腾讯文档【一线互联网大厂Java核心面试题库】点击即可领取,并会持续更新…感兴趣的朋友可以看看支持一波!