目录
1、什么是泛型
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。------来源《Java编程思想》对泛型的介绍。
泛型是在 JDK1.5 引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。
2、引出泛型
实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?
思路:
1.我们以前学过的数组,只能存放指定类型的元素,例如:
int[] nums = new int[10];
String[] str = new String[10];
int[] 只能存放整形,而 String[] 只能存放字符串
2.所有类的父类,默认为 Object 类。数组是否可以创建为 Object 呢?
代码示例:
class MyArray {
public Object[] array = new Object[10];
public void setVal(int pos, Object val){
this.array[pos] = val;
}
public Object getPos(int pos) {
return this.array[pos];
}
}
public class Test2 {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.setVal(0, 1);
myArray.setVal(1, "hello");
String str = myArray.getPos(1); //编译报错
System.out.println(str);
}
}
可以看到用 Object 类型创建的数组,可以存放整型,也可以存放字符型,它不仅仅可以存放这两种类型,实际上它可以存放任何类型,只要这个类继承了 Object(自定义的类型也可以存放)
问题: 既然任何类型都可以存放,但是为什么会编译报错呢?
因为这样写编译器只会认为你要存放的数据类型是Object,但是当你要取出查看这个元素时,编译器并不知道你要取元素的具体类型,所有这里你要明确指出具体类型,也就是需要强制类型转换,告诉编译器你要取元素的具体类型是什么。
虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,还是希望他只能持有一种数据类型,而不是同时持有很多种类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型作为参数传递,需要什么类型,就传入什么类型。
2.1泛型的语法
伪代码:
class 泛型类名称<类型形参列表> {
//这里可以使用类型形参
}
class ClassName<T1,T2,...,Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/*这里可以使用类型参数*/ {
//这里可以使用类型形参
}
class ClassName<T1,T2,...,Tn> extends ParentClass<T1> {
//可以只使用部分类型形参
}
上述伪代码进行改写后:
class MyArray<T> {
public T[] array = (T[]) new Object[10];// 1
public void setVal(int pos, T val){
this.array[pos] = val;
}
public T getPos(int pos) {
return this.array[pos];
}
}
public class Test2 {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>(); // 2
myArray.setVal(0, 1);
myArray.setVal(1, 10);
int ret = myArray.getPos(1); // 3
System.out.println(ret);
myArray.setVal(2,"hello");// 4 编译报错
}
}
代码解释:
1.类名后的 <T> 代表占位符,表示当前类是一个泛型类
【规范】类型参数一般使用一个大写字母表示,常用的名称有:
E 表示 Element
K 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
2.注释 1 处,不能使用 new 关键字创建泛型类型的数组
T[] t = new T[5]; // 这是不对的
3.注释 2 处,类型后加入 <Integer> 指定当前类型
4.注释 3 处,不需要进行强制类型转换
5. 注释 4 处,代码编译报错,因为之前在注释 2 处指定了类当前的类型,此时编译器会在存放元素的时候帮我们进行类型检查。
3、泛型类的使用
3.1语法
泛型类<泛型实参> 变量名;// 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); //实例化一个泛型类对象
3.2示例
MyArray<Integer> array = new MyArray<Integer>();
注意:泛型只能接受类,所有的基本数据类型必须使用包装类!!!
3.3类型推导(Type Interface)
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
MyArray<Integer> array = new MyArray<>();
这里可以推导出实例化需要的类型实参为 Integer ,所以后面的 Integer 可以省略
4.裸类型(Raw Type)
4.1说明
裸类型是一个泛型类但没有带着类型实参,例如 MyArray 就是一个裸类型
MyArray array = new MyArray();
注意: 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制
总结:
1.泛型是将数据类型参数化,进行传递
2.使用 <T> 表示当前类是一个泛型类
3.泛型的优点:数据类型参数化,编译时自动进行类型检查和转换
5、泛型是如何编译的
5.1擦除机制
泛型到底是怎么编译的呢?这个问题,也是曾经的一个面试题。泛型本质是一个比较难的语法,所以要理解好还是需要一定的时间的。
下面我们通过反汇编来看一个泛型到底是怎么编译的。
打开终端
通过命令:javap -c 查看字节码文件,所有的 T 都是 Object

在编译的过程中,将所有的泛型类型形参 T 替换为 Object 这种机制,称为:擦除机制。
Java的泛型机制是在编译级别实现的。编译器生成的字节码文件在运行期间并不包含泛型的类型信息。
问题:
1.编译的时候会替换为 Object ,那么为什么 T[] t = new T[10]; 是不对的呢?
这不就相当于 Object[] t = new Object[10] 吗?
2.类型擦除,一定是把 T 变成 Object 吗?
5.2为什么不能实例化泛型类型数组
代码示例:
class MyArray<T> {
public T[] array = (T[]) new Object[10];
public void setVal(int pos, T val){
this.array[pos] = val;
}
public T getPos(int pos) {
return this.array[pos];
}
public T[] getArray() {
return array;
}
}
public class Test2 {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>();
Integer[] num = myArray.getArray();
}
/*
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
at test.Test2.main(Test2.java:27)
*/
}
可以看到当运行程序时,会报异常
原因:替换后的方法:将 Object[] 分配给 Integer[] 引用,程序报错
public Object[] getArray() {
return array;
}
通俗的讲就是:返回的 Object 数组里面,可能存放的是任何的数据类型,可能是 String ,可能是自定义类型,运行的时候,直接传给 Integer 类型的数组,编辑器认为是不安全的。
正确的方式:
通过反射创建泛型类型数组
import java.lang.reflect.Array;
class MyArray<T> {
public T[] array;
public MyArray() {
}
public MyArray(Class<T> clazz, int size) {
array = (T[]) Array.newInstance(clazz,size);
}
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, T val) {
this.array[pos] = val;
}
public T[] getArray() {
return array;
}
}
public class Test2 {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>(Integer.class, 10);
Integer[] num = myArray.getArray();
}
}
6、泛型的上界
在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
6.1 语法
class 泛型类名称<类型形参 extends 类型边界> {
......
}
6.2 示例
public class MyArray<E extends Number> {
...
}
只接受 Number 的子类型作为 E 的类型实参
MyArray<Integer> array1; // 1
MyArray<String> array2; // 2
java: 类型参数 java.lang.String 不在类型变量T的范围内
注释 1 处:正常,因为 Integer 是 Number 的子类型
注释 2 处:正常,因为 String 不是 Number 的子类型
6.3 复杂示例
public class MyArray1<E extends Comparable<E>> {
...
}
E 必须是实现了 Comparable 接口的
7、泛型方法
7.1 定义语法
方法限定符<类型形参列表> 返回值类型 方法名称(形参列表){ ... }
7.2 示例
静态的泛型方法需要在 static 后用 <> 声明泛型类型参数
public class Method {
public static <E> void swap(E[] a, int i, int j) {
E temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
7.3 使用示例 - 可以类型推导
Integer[] a = { ... };
swap(a, 0, 9);
String[] b = { ... };
swap(b, 0, 9);
7.4 使用示例 - 不使用类型推导
Integer[] a = { ... };
Method.<Integer>swap(a, 0, 9);String[] b = { ... };
Method.<Integer>swap(b, 0, 9);
8、通配符
? 用于在泛型的使用,即为通配符
8.1 通配符能解决什么问题
示例:
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo {
public static void main(String[] args) {
Message<String> message = new Message<>() ;
message.setMessage("hello demo");
fun(message);
}
public static void fun(Message<String> temp) {
System.out.println(temp.getMessage());
}
}
以上程序会带来新的问题,如果现在泛型的类型设置的不是 String 而是 Integer
public class TestDemo {
public static void main(String[] args) {
Message<String> message = new Message<>() ;
message.setMessage(10); //编译报错,不能接收Integer,只能接收String
fun(message);
}
public static void fun(Message<String> temp) {
System.out.println(temp.getMessage());
}
}
我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改,这种情况就需要使用通配符来处理。
使用通配符示例:
public class TestDemo {
public static void main(String[] args) {
Message<Integer> message = new Message<>();
message.setMessage(10);
fun(message);
}
//此时使用通配符 ? 描述它可以接收任何类型,但是由于不确定类型,所以无法修改
public static void fun(Message<?> meg) {
// msg.setMessage(10); 无法修改!
System.out.println(meg.getMessage());
}
}
在 通配符 ? 的基础上又产生了两个子通配符
? extends 类 : 设置通配符上界
? super 类 : 设置通配符下界
8.2 通配符上界
语法:
<? extends 上界>
<? extends Fruit> // 可以传入的实参类型:Fruit 或者 Fruit 的子类

代码示例:
class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class Plate<T> { //设置泛型
private T plate;
public T getPlate() {
return plate;
}
public void setPlate(T plate) {
this.plate= plate;
}
}
public class TestDemo {
public static void main(String[] args) {
Plate<Apple> plate1 = new Plate<>();
plate1.setPlate(new Apple());
fun(plate1);
Plate<Banana> plate2 = new Plate<>();
plate2 .setPlate(new Banana());
fun(plate2);
}
//此时使用通配符 ? 描述它可以接收任何类型,但是由于不确定类型,所以无法修改
public static void fun(Plate<? extends Fruit> plate) {
//plate.setPlate(new Apple()); //无法修改!
//plate.setPlate(new Banana()); //无法修改!
System.out.println(plate.getPlate());
}
}
此时无法再 fun 函数中对 plate 进行添加元素,因为 plate 接收的是 Fruit 和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!但是可以获取元素
public static void fun(Plate<? extends Fruit> plate) {
//plate.setPlate(new Apple()); //无法修改!
//plate.setPlate(new Banana()); //无法修改!
Fruit f = plate.getPlate();
System.out.println(f);
}
结论:通配符的上界,不能进行写入数据,只能进行读取数据
8.3 通配符下界
语法:
<? super 下界>
<? super Fruit> // 可以传入的实参类型:Fruit 或者 Fruit 的父类

代码示例:
class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class Plate<T> { //设置泛型
private T plate ;
public T getPlate() {
return plate;
}
public void setPlate(T plate) {
this.plate = plate;
}
}
public class TestDemo {
public static void main(String[] args) {
Plate<Fruit> plate1 = new Plate<>();
plate1.setPlate(new Fruit());
fun(plate1);
Plate<Food> plate2 = new Plate<>();
plate2.setPlate(new Food());
fun(plate2);
}
public static void fun(Plate<? super Fruit> plate) {
//此时可以修改 添加的是Fruit 或者 Fruit 的子类
plate.setPlate(new Fruit()); //这个是 Fruit 的本身
plate.setPlate(new Banana()); //这个是 Fruit 的子类
//Fruit f = plate.getPlate(); 不能接收,这里无法确定是哪个父类
System.out.println(plate.getPlate()); // 只能直接输出
}
}
结论:通配符的下界,不能进行读取数据,只能写入数据



1035

被折叠的 条评论
为什么被折叠?



