泛型作为程序的守门员,能在编译期间就能检查发现问题,是一颗语法糖,能很大程度上帮助程序员写出正确高效的代码。
文章目录
1️⃣泛型的引入
Object
类可以接收所有类型,有包装类的自动拆装箱,基本类型自动装箱变为Integer
或Double
让Object
接收。
定义一个坐标类,定义不同的对象接收不同类型:
public class Point {
private Object x;
private Object y;
public Object getX() {
return x;
}
public Object getY() {
return y;
}
public void setX(Object x) {
this.x = x;
}
public void setY(Object y) {
this.y = y;
}
public static void main(String[] args) {
Point point = new Point();
//自动装箱
point.setX(10.1);
point.setY(5.5);
//自动拆箱
double x = (double) point.getX();
double y = (double) point.getY();
System.out.println("x = " + x + ",y = " + y);
Point point1 = new Point();
point1.setX("东经101度");
point1.setY("北纬55度");
String x1 = (String) point1.getX();
String y1 = (String) point1.getY();
System.out.println("x = " + x1 + ",y = " + y1);
}
}
//输出结果:
x = 10.1,y = 5.5
x = 东经101度,y = 北纬55度
10.1和5.5都是double;东经101度和北纬55度都是字符串。
此时 x 和 y 都是相同的类型,如果用户不小心对 x 和 y 输入不同的类型,那么在强转时就会发生错误。
当 x 和 y 不小心设置为不同的类型时,再进行强制类型转换就会发生运行时异常(类型转换异常),这个错误是无法在编译期发现的。
这个时候,就需要泛型的引入了。使用泛型,在编译期就可以检查类型是否正确。
2️⃣泛型的基础使用
所谓的泛型就是指在类定义时不会设置类中的属性或方法参数的具体类型,而是在类使用时(创建对象的时候)再进行类型的定义。
1.语法
泛型类的定义
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
💡举例
public class MyClass<T,S,U> extends ParentClass<T>{
private T valeu1;
private S valeu2;
}
泛型类对象的定义
泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
💡举例
MyClass<String,Integer,Double> myClass = new MyClass<String,Integer,Double>();
类型推导
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
// 可以推导出实例化需要的类型实参为 String
MyClass<String,Integer,Double> myClass = new MyClass<>();
2.使用
< >
叫做钻石操作符,JDK7新增的,表示泛型,在一个类或者方法上看到< >
表示使用了泛型。
<T>
T称为类型参数,可以用任意字符来表示,一般用单个的大写字母来表示。
出于规范而言,类型参数用单个的大写字母来代替。常见的如下:
- E 表示 Element
- K 表示 Key
- V 表示 Value,和K搭配使用,一般用来表示Map<K,V>
- N 表示 Number
- T 表示 Type
- S, U, V 等等 - 第二、第三、第四个类型
使用了泛型后,在编译期间就可以检查出错误:
![](https://img-blog.csdnimg.cn/a5466ab9006b45649396a6f6d32d09d4.png)
3️⃣泛型方法,泛型接口
1.泛型方法
我们来看下面两个方法:
public T getX() {
return x;
}
public void setX(T x){
this.x = x;
}
这两个方法是否是泛型方法呢?
这两个方法都不是泛型方法,只不过是返回值或者参数使用了泛型变量而已,此时的T是泛型类的参数。
//泛型方法
public class MyClass<T,E> {
private T valeu1;
private E valeu2;
//泛型方法
public <T> void test(T t){
System.out.println(t);
}
}
此时此方法是个泛型方法,有一个类型参数<T>
不是返回值,只是告诉编译器这是一个泛型下声明。
那么泛型类中的类型参数T
和泛型方法中的类型参数T
是一个东西吗?
public class MyClass<T,E> {
private T valeu1;
private E valeu2;
//泛型方法
//T类型参数是方法自己的,E是类中的
//T的类型可以随便设置,E必须和类中的声明保持一致
public <T> void test(T t,E e){
System.out.println(t);
}
public static void main(String[] args) {
//创建这个泛型类的时候明确类型为整型
MyClass<Integer,String> myClass = new MyClass<>();
//此时T是Integer,E必须是String,否则会报错
myClass.test(11,"11.1");
//此时T是Double,E是String
myClass.test(11.1,"11.1");
}
}
- 泛型方法始终以自己的类型参数为准,和类中的类型参数无关!即使类型参数所用的字母一样。
- 如果使用了类中一样的类型参数而没有在
<>
中声明,那么就用的是类的类型参数。- 为了避免混淆,一般定义泛型方法时,尽量避免使用类中使用过的类型参数字母!
- 在方法中使用过的类型参数必须在类中或者方法中声明,否则编译会报错。
![](https://img-blog.csdnimg.cn/577e3b72bd4047bc9df7b64782c58678.png)
2.泛型接口
子类实现接口时,有两种选择,那么继续保留泛型,就是一个泛型类;要么就在实现的时候规定类型参数。
public class MyImpl {
public static void main(String[] args) {
IMessage<String> msg1 = new MessageImpl1<>();
msg1.printMsg("hello");
IMessage<Integer> msg2 = new MessageImpl2();
msg2.printMsg(10);
}
}
//泛型接口,带一个类型参数
interface IMessage<T>{
void printMsg(T t);
}
//1.继续保留泛型
class MessageImpl1<T> implements IMessage<T>{
@Override
public void printMsg(T t) {
System.out.println(t);
System.out.println("仍然是泛型类");
}
}
//就是一个普通类,实现接口的时候规定好参数的类型就是Integer类型
class MessageImpl2 implements IMessage<Integer>{
@Override
public void printMsg(Integer t) {
System.out.println(t);
System.out.println("类型为Integer的普通类");
}
}
//运行结果:
hello
仍然是泛型类
10
类型为Integer的普通类
4️⃣泛型和内部类
需要了解内部类的知识戳以下链接
👇
类和对象(代码块,内部类)
☝️
非静态内部类(成员内部类)在没有声明自己的泛型参数时,会复用外部类的泛型参数,静态内部类不会复用外部类的泛型参数。
public class MyOutter <T>{
//成员内部类
private class Inner{
public void test(T t2){
System.out.println(t2);
}
}
//静态内部类
private static class Inner1<T>{
public void fun(T t1){
System.out.println(t1);
}
}
public static void main(String[] args) {
//有泛型参数时的定义
//MyOutter<String>.Inner<String> inner = new MyOutter().new Inner();
//无泛型参数的定义
MyOutter<String>.Inner inner = new MyOutter().new Inner();
inner.test("123");
inner.test(123);//类型错误
MyOutter.Inner1<Integer> inner1 = new Inner1<>();
//和外部类的类型参数无关!!
inner1.fun(123);
}
}
5️⃣通配符
通配符的引入问题:
1.使用通配符
通配符:<?>
,一般用在方法参数,表示可以接受该类所有类型的泛型变量。
![](https://img-blog.csdnimg.cn/c7b8794c75584b2a815bef7d3996249e.png)
那么fun
方法能调用对象的getter
方法来获取属性,那能否调用setter
设置对象的属性呢?
答案是:不行
因为在定义方法fun
时,不知道外部到底会传入什么类型的Message
对象,无法确定传入对象的类型,因此无法调用对象的setter
来设置值。
2.上限通配符
在定义泛型类或者方法传入泛型类对象时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
语法
<类型形参 extends 类型边界>
此时类型参数可以指代任意类型,但是该类型必须是后面类的子类。
在方法中使用
public static void fun(Message<? extends Number> msg){
System.out.println(msg.getMsg());
}
![](https://img-blog.csdnimg.cn/9cb41540b9514216bcf7ec0e103b9890.png)
此时? <= Number
,即?
只能设置Number
类本身或者是Number
的子类,例如:Integer,Double
等等,除此之外的例如String
等的其他类型都是不行的。
![](https://img-blog.csdnimg.cn/55c1b0449c364837ab1b9f7eb8f01bd9.png)
那么此时能否设置对象的属性呢?
?
表示可以接收Number
及其子类,但是子类之间是无法相互转换的,所以泛型的上限通配符仍然不能set
属性,只能用getter
获取。
在类中使用
public class Message<T extends Number> {
...
}
extends
可以并且唯一可以用在类的泛型类型上的关键字
![](https://img-blog.csdnimg.cn/17f5d0de12134ca387e11ad3cc900562.png)
此时T
可以指代Number
及其子类的类型
3.下限通配符
语法
<? super 类>
在方法中使用
public static void fun(Message<? super String> msg){
System.out.println(msg.getMsg());
}
?
表示可以指代该类及其父类,这里只能指代String
或Object
。
下限通配符只能用在方法参数,不能作用于类的类型参数
![](https://img-blog.csdnimg.cn/d4d0cf6521604c20b3e0c4ce3741145e.png)
下限通配符可以设置对象的值吗?
无论
?
是什么类型,规定好的下限对象一定可以向上转型变为父类。
所以下限通配符是可以调用对象的setter
方法设置一个具体的属性值(下限类型的)
6️⃣类型擦除
泛型是典型的”语法糖“(只在编译期生效,属于为了方便程序开发引入的一种机制)。
实际上泛型在通过javac
编译之后,泛型就不见了。
所有泛型类型参数,若没有设置泛型上限,则编译之后统一擦除为Object
类型。
若设置了类型上限,则编译后统一擦除为对应的泛型上限。
没有设置泛型上限
public class Message <T>{
private T msg;
public T getMsg() {
return msg;
}
public void setMsg(T msg) {
this.msg = msg;
}
public static void main(String[] args) throws NoSuchFieldException {
Message<String> msg = new Message<>();
Message<Integer> msg1 = new Message<>();
Message<Double> msg2 = new Message<>();
Field field = msg.getClass().getDeclaredField("msg");
Field field1 = msg1.getClass().getDeclaredField("msg");
Field field2 = msg2.getClass().getDeclaredField("msg");
System.out.println(field.getType());
System.out.println(field1.getType());
System.out.println(field2.getType());
}
}
输出结果:
![]()
可以看到,在没有设置泛型上限时,统一擦除为
Object
类了。
设置了泛型上限
public class Message <T extends Number>{
private T msg;
public T getMsg() {
return msg;
}
public void setMsg(T msg) {
this.msg = msg;
}
public static void main(String[] args) throws NoSuchFieldException {
Message<Integer> msg1 = new Message<>();
Message<Double> msg2 = new Message<>();
Field field1 = msg1.getClass().getDeclaredField("msg");
Field field2 = msg2.getClass().getDeclaredField("msg");
System.out.println(field1.getType());
System.out.println(field2.getType());
}
}
输出结果:
![]()
类型统一擦除为泛型上限
Number
了
往期文章:
👉《学习笔记3(三大特殊类:String,Object,包装类)》
👉《堆和优先级队列(Comparator比较器的使用)》
👉《认识String类(特性和常用方法)》