如下是定义了一个类,这个类中可以存放一组 int 类型的数据。
classNumber{ int[] arr; publicNumber(inta) { this.arr=newint[10]; } publicvoidsetArr(intn, intdata) { this.arr[n] =data; } publicintgetArr(intn) { returnarr[n]; } }
可是虽然这样写没有问题可是它的应用范围实在是太小了,它只能存储 int 类型的数据。如果我们现在想让这个类中可以存放任何类型的数据应该怎么做呢?
在 JAVA 中利用泛型就可以实现这点。
泛型
泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。
泛型的语法:
class 泛型类名称<类型形参列表> {
}
class ClassName<T1, T2, ..., Tn> {
}
类名后的 <T> 代表占位符,表示当前类是一个泛型类。
类型形参一般使用一个大写字母表示,常用的名称有:
- E 表示 Element
- K 表示 Key
- V 表示 Value
- N 表示 Number
- T 表示 Type
- S, U, V 等等 - 第二、第三、第四个类型
- 泛型是将数据类型参数化,进行传递
- 使用 <T> 表示当前类是一个泛型类。
- 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换
泛型类
所谓的泛型类其实就是类中的属性可以根据业务场景的不同而改变(这种改变只能发生在定义类的时候,当类一旦被定义完成就无法改变)
语法:
- 泛型类<类型实参> 变量名; // 定义一个泛型类引用
- new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
我们将开始的代码进行改进:
classNumber<T>{ Object[] arr;//这里可以暂时忽略Object,下文会说publicNumber(inta) { this.arr=newObject[10]; } //设置数组下标为n的元素值publicvoidsetArr(intn, Tdata) { this.arr[n] =data; } //获取数组下标为n的元素publicTgetArr(intn) { return (T)arr[n]; } }
此时这个类就是一个泛型类我们可以根据不同的业务场景来让它存储不同类型的值:
//存储整型Number<Integer>num1=newNumber<>(2); num1.setArr(0,1); num1.setArr(1,2); System.out.println(num1.getArr(0)); System.out.println(num1.getArr(1)); //存储字符串类型Number<String>num2=newNumber<>(2); num2.setArr(0,"zhangsan"); num2.setArr(1,"lisi"); System.out.println(num2.getArr(0)); System.out.println(num2.getArr(1));
我们都有泛型了为啥在泛型类中还要使用Object类来定义数组?
原因有两个:
- Object类是所有类的父类所以它可以存储所有类型的值;
- 在 JAVA 中由于数组比较特殊,在创建数组时必须要指明数组的类型,而泛型是一种不确定类型的写法所以无法定义成泛型数组 。
还有一种写法:
这种写法虽然不会报错,而且也可以运行但是其实写成这样也不太好,因为它只是骗过了编译器。
这里还是推荐将数组定义成这样:
调用的时候需要注意:
如果是简单类型(如:int , char……)必须要传对应的包装类。
基本数据类型和对应的包装类
基本数据类型 | 包装类 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
这个表看起来多其实除了 Integer 和 Character, 其余基本类型的包装类都是首字母大写。
泛型除了可以应用于类之外也可以应用于方法。
泛型方法
语法:
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
publicstatic<T>voidswap(T[] array, inti, intj) { Tt=array[i]; array[i] =array[j]; array[j] =t; }
泛型方法也和类一样不能使用基本类型,需要使用对应的包装类。
我们可以测试一下:
publicstaticvoidmain(String[] args) { Integer[] b= {1,2,3,4}; swap(b, 0, 1); System.out.println(Arrays.toString(b)); }
擦除机制
泛型如此灵活那么它到底是如何进行编译的呢?
其实泛型的编译是通过擦除机制来进行完成的。
以这个泛型类为例:
classNumber<T>{ T[] arr; publicNumber(inta) { this.arr= (T[])newObject[10]; } publicvoidsetArr(intn, Tdata) { this.arr[n] =data; } publicTgetArr(intn) { return (T)arr[n]; } }
我们可以在cmd中利用 javap -c 来打开一个字节码文件
我们可以看出在编译的时候已经将所有的 T 都替换为了 Object 类型。
Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。
泛型的上界
在定义泛型类时,有时需要对传入的类型变量做一定的约束,就可以通过类型边界来约束。
语法:
class 泛型类名称<类型形参 extends 类型边界> {}
classNumber<TextendsString>{ ... }
当我们此时 Number 就只能接受 String和String类的子类 ,否则就会报错。
Number<String>num1=newNumber<>(1); Number<Integer>num2=newNumber<>(1);
通配符
通配符:?
泛型虽然很好用但是此时又出现了一个新的问题,如果想写一个方法形参是泛型类那么就会很麻烦,因为我们根本不知道当前泛型的具体类型:
publicstaticvoidprintf(Number<String>a) { System.out.println(a.getArr(0)); }
如上我们简单定义了一个输出方法,方法形参是 Number 这个泛型类,当我们调用时:
Number<String>num1=newNumber<>(1); num1.setArr(0, "zhangsan"); Number<Integer>num2=newNumber<>(1); num2.setArr(0, 4); printf(num1); printf(num2);
当用这段代码测试后就会报错原因是类型不匹配,因为我们定义方法时使用的是 String 类型,可是这可是泛型,有可能还会是 Integer
此时就需要用到 通配符:?
当我们换成通配符之后再使用上述代码进行测试就可以正常运行了
publicstaticvoidprintf(Number<?>a) { System.out.println(a.getArr(0)); }
通配符也和泛型一样有时也需要对其范围进行限制:
通配符的上界
- 语法:
- <? extends 上界>
- 表示 ?代表的类只能是 上界 这个类本身 或 其子类
此时我们简单定义三个类:
classA {} classBextendsA{} classCextendsB{}
对之前的打印方法进行修改:
publicstaticvoidprintf(Number<?extendsB>a) { }
用以下代码进行测试:
Number<A>num1=newNumber<>(1); Number<B>num2=newNumber<>(1); Number<C>num3=newNumber<>(1); printf(num1); printf(num2); printf(num3);
因为 A 类并不是 B 的子类所以就会发生异常
通配符的下界
- 语法:
- <? super 下界>
- 表示 ?代表的类只能是 下界 这个类本身 或 其父类
对之前的打印方法进行修改:
publicstaticvoidprintf(Number<?superB>a) { }
还是用以下代码进行测试:
Number<A>num1=newNumber<>(1); Number<B>num2=newNumber<>(1); Number<C>num3=newNumber<>(1); printf(num1); printf(num2); printf(num3);
因为 C 类并不是 B 的父类所以发生异常。