文章目录
- 1.学习目标
- 2.什么是泛型
- 3.引入泛型
- 语法
- 4.泛型类的使用
- 语法
- 示例
- 6.泛型的上界
- 语法
- 示例
- 7.泛型的方法
- 定义语法
- 示例
- 8.通配符
- 通配符解决什么问题
- 通配符上界
- 通配符下界
- 9.包装类
- 基本数据类型和对应的包装类
- 装箱和拆箱
- 自动装箱和自动拆箱
1.学习目标
1.以能阅读 java 集合源码为目标学习泛型
2.了解泛型
3.了解通配符
2.什么是泛型
先来看下面的代码,如果要给方法传参,传递的参数必须是符合参数类型的。
public class Test {
public static void func (int a) {
}
public static void main(String[] args) {
func(10);
}
}
因为上述代码中的 func 方法的参数是 int 类型,此时调用方法时传递的参数就必须是 int 类型的。
如果是其他类型的这里就会报错。
通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化;简单来说就是在给方法传参时,可以将一些不同类型的参数一起传递过去。
3.引入泛型
解决下面的问题:
实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值
此时如果将这个数据定义为具体的某一个类型,那么它就只能传递具体的某一个参数类型。
比方说,如果是一个 int 的数组,它就只能传递 int 类型的参数;如果是一个 char 类型的参数,那么它就只能传递 char 类型的参数…
解决办法就是,将这个数组定义为 Object 数组 即可,这是因为 Object 是所有类的父类,也就是所有都继承这个类,如下代码片段所示。
class MyArray {
public Object[] objects = new Object[10];
// 给数组添加元素
public void setVal (int pos, Object val) {
objects[pos] = val;
}
// 返回数组中某个下标的值
public Object getVal (int pos) {
return objects[pos];
}
}
public class Test {
public static void main(String[] args) {
MyArray myArray = new MyArray();
// 调用方法给数组的 0 下标添加一个int类型的 10 元素
myArray.setVal(0, 10);
// 给数组 1 下标添加一个 String 类型的 “hello” 元素
myArray.setVal(1, "hello");
}
}
按照上述代码中调用 setVal 方法就可以给数组添加元素。
在调用 getVal 方法返回某个下标的的值时代码报错了,鉴于上述情况可以得出结论:
1、任何类型的数据都可以存放。
2、1号下标本身就是字符串,但是取数据的时候编译却报错了。此时必须进行强制类型转换
可以看到强转后就不报错了,按照上述的情况,必须要看看下标元素是什么类型后再取出,
这就比较不合适了。
虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。
而不是同时持有这么多类型。因此,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。
此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。
接下来修改代码使用 泛型 来解决这个问题。
class MyArrays<T> {
public T[] objects = (T[])new Object[10];
// 给数组添加元素
public void setVal (int pos, T val) {
objects[pos] = val;
}
// 返回数组中某个下标的值
public T getVal (int pos) {
return objects[pos];
}
}
public class Work {
public static void main(String[] args) {
// 此时指定的是 Integer 类型,只能传递整数
MyArrays<Integer> myArrays = new MyArrays<Integer>();
myArrays.setVal(0, 10);
myArrays.setVal(1, 20);
}
}
< T > : 相当于是一个占位符,表示当前类是一个泛型类。
括号里的字母不一定是 T,也可以别的,但是我们习惯括号里的字母可以将我们想要表达的意思表示出来,T 和 E 一般用来表示类型。
main 函数里的 < Intrger>表示当前只能传递 Integer 类型的参数,也就是只能传递整数。
如果此时要传递 Integer 类型以外的参数。
可以看到代码此时会报错。
如果要取出指定下标元素的值定义一个变量接收方法返回的值即可。
int ret = myArrays.getVal(1);
根据输出的结果可以判断出的确得到了下标为 1 的元素的值。
如果想要传递其他类型的参数,更改 <> 中的类型即可。
public static void main(String[] args) {
MyArrays<String> myArrays1 = new MyArrays<String>();
myArrays1.setVal(2, "hello");
myArrays1.setVal(3, "world");
String retString = myArrays1.getVal(2);
System.out.println(retString);
}
可以看到得到的结果也是正确的。
这就是泛型!!!
泛型存在的两个最大的意义:
1、存放元素的时候,会进行类型的检查。
2、取出元素的时候,会自动进行类型转换,不在需要类型的强转了。
泛型只要是编译时期的一种机制,这种机制叫做 擦除机制,而运行的时候是没有泛型概念的。
有关泛型擦除机制的文章介绍:擦除机制的文章介绍
语法
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
一个泛型类的参数列表可以指定多个类型,也就是 <> 里可以有多个字母,
如下图所示:
4.泛型类的使用
语法
泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
示例
MyArray<Integer> list = new MyArray<Integer>();
注意:泛型只能接受类,所有的基本数据类型必须使用包装类!,也就是<> 里只能是 类类型,不能是基本类型。
6.泛型的上界
在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
语法
class 泛型类名称<类型形参 extends 类型边界> {
...
}
示例
public class MyArray<E extends Number> {
...
}
只接受 Number 的子类型作为 E 的类型实参。
MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型
实现一个泛型类,求一个数组中的最大值
class MaxVal<T extends Comparable<T>> {
public T findMax (T[] array) {
T max = array[0];
for (int i = 1; i < array.length; i++) {
if(max.compareTo(array[i]) < 0 ) {
max = array[i];
}
}
return max;
}
}
public class MaxValue {
public static void main(String[] args) {
MaxVal<Integer> maxVal = new MaxVal<>();
Integer[] array = {1,2,3,4,5};
Integer ret = maxVal.findMax(array);
System.out.println(ret);
}
}
可以输出结果正确。
7.泛型的方法
定义语法
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
示例
public class Util {
//静态的泛型方法 需要在static后用<>声明泛型类型参数
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}
8.通配符
? 用于在泛型的使用,即为通配符。
通配符解决什么问题
通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Student 是 Person 的子类,那么 List 也应该是 List 的子类,但是泛型是不支持这样的父子类关系的。
泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围。
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Test3 {
public static void main(String[] args) {
Message<String> message = new Message() ;
message.setMessage("中土世界欢迎您");
fun(message);
}
public static void fun(Message<String> temp){
System.out.println(temp.getMessage());
}
}
以上是程序的输出结果。
以上程序会带来新的问题,如果现在泛型的类型设置的不是String,而是Integer。
可以看到程序会报错,因为 fun 方法被要求是 String 类型的。
解决办法就是引入 通配符 的概念,直接在代码中写入 ? 即可。
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Test3 {
public static void main(String[] args) {
Message<Integer> message = new Message() ;
message.setMessage(10);
fun(message);
}
public static void fun(Message<?> temp){
System.out.println(temp.getMessage());
}
}
可以看到改写好的代码,就没有报错了。
此时的问号表示的含义是程序也不知道此时的类型是什么类型。
此时就得到了正确的结果。
在 “?” 的基础上又产生了两个子通配符:
? extends 类:设置泛型上限
? super 类:设置泛型下限
通配符上界
语法
<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类
示例
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) {
// 可以看到p1 与 p2 调用 fun 方法后都是正确的
Plate<Apple> plate1 = new Plate<>();
plate1.setPlate(new Apple());
fun(plate1);
Plate<Banana> plate2 = new Plate<>();
plate2.setPlate(new Banana());
fun(plate2);
}
// 此时继承的是 Fruit 类,表名 fun 方法可以调用 Fruit、Apple or Banana
public static void fun(Plate<? extends Fruit> temp){
}
}
根据上面代码可以看到,plate1 设置为只能放 Apple,而 plate2 设置为了只能放 Banana。
实际上 fun 继承了 Fruit 类,在调用方法的时候就决定了可以是 Fruit、Apple、Banana其中一个。
因为 Apple 与 Banana 是 Fruit 的子类。
此时通过 fun 方法是不能往里面放元素的。
站在 fun 方法的角度考虑,此时他有可能接收的 Apple 也有可能接收的是 Banana,因此也就不可以往里面放元素。
取元素的时候是没问题的,但是放元素的时候是有问题的。因为取出的元素一定是 Fruit 或者它的子类,但是放进去的是什么就不能确定了。就好像可以说苹果是水果,但是不能说水果是苹果一样。
public static void fun(Plate<? extends Fruit> temp){
Fruit fruit = temp.getPlate();
}
需要注意的是在这里的代码只能用 Fruit 来引用,还是因为具体是什么类型是不确定的。
可以看到是错的,因此通配符的上界,不能进行写入数据,只能进行读取数据。
通配符下界
语法
<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
示例
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 fun(Plate<? super Fruit> temp){
temp.setPlate(new Apple());
temp.setPlate(new Banana());
temp.setPlate(new Fruit());
}
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);
}
}
下界和上界正好是相反的,可以放元素但是不可以取元素。因为不能确定这里存放的是什么。
可以看到取出的时候报错了。
下界这里方法调用的时候只能传递 Fruit 或者 它的父类作为参数。
可以看到传递 Fruit 的子类是出错了。
9.包装类
在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。
基本数据类型和对应的包装类
需要注意的是除了圈出的两个,其他的包装类都是首字母大写。
装箱和拆箱
int i = 10;
// 装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
Integer ii = Integer.valueOf(i);
Integer ij = new Integer(i);
// 拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中
int j = ii.intValue();
自动装箱和自动拆箱
// 自动拆箱和装箱
int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer)i; // 自动装箱
int j = ii; // 自动拆箱
int k = (int)ii; // 自动拆箱