一、泛型是什么
二、泛型的使用场景
我们需要要以下代码来代入我们理解泛型:
public class MyArray {
private Object[] array = new Object[10];
public void set(int pos, Object val) {
array[pos] = val;
}
public Object get(int pos) {
return array[pos];
}
}
class TestDemo {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.set(0,1);//给0小标Integer类型
myArray.set(1,2);//给1小标Integer类型
myArray.set(2,"hallo");//给2小标String类型
System.out.println(myArray.get(1));
String s = myArray.get(2);//报错
System.out.println(s);
}
}
在获取2小标也就是String类型的时候报错了,强制类型转换为String类型后,不再报错。
结果:
但是我们发现有一些问题:
1. 任何类型数据都可以存放2. 2 号下标本身就是字符串,但是确编译报错。必须进行强制类型转换也就是说什么东西都可以往里面放,各种类型乱七八糟的,而且我要用某个小标的值还需要提前知道里面是什么类型的值。虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。而不是同时持有这么多类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译 器去做检查。 此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。
三、泛型的基本使用
简单介绍一下,方便我们理解类型擦除机制,后面再详细介绍
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
那么我们将刚刚的MyArray给改成泛型类:
public class MyArray<T>{
private Object[] array = new Object[10];//为什么Object不行为T
public void set(int pos, T val) {
array[pos] = val;
}
public T get(int pos) {
return (T)array[pos];
}
}
可能细心的朋友就要问:为什么数组类型不改为T仍然是Object
无论是哪一边改为T都不行,编译都编译不了:
最多可以这样,但是这样写并不算好,为什么一会再说:
最好还是写为:
private Object[] array = new Object[10];
类名后的 <T> 代表占位符,表示当前类是一个泛型类,实际上就是个名字写成什么都可以,但是行业有自己的一些行业规范:
【规范】类型形参一般使用一个大写字母表示,常用的名称有:
E 表示 Element 数组元素K 表示 KeyV 表示 ValueN 表示 NumberT 表示 TypeS, U, V 等等 - 第二、第三、第四个类型
泛型类的使用:
更推荐第二种方法
public static void main(String[] args) {
//传入类型 这里可以不传
MyArray<Integer> myArray1 = new MyArray<Integer>();
//推荐 自动推导出类型
MyArray<Integer> myArray = new MyArray<>();
}
四、类型擦除机制
我们来到MyArray生成的字节码文件路径下,在文件路径输出cmd,再输入 javap -c 文件名
可以看到所有关于T的类型全部被替换为了Object。
在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制。
在 Java 中,泛型的擦除机制是在编译阶段发生的,编译后的字节码文件中泛型类型参数会被擦除,对于没有指定上界的泛型类型参数,会被替换为
Object
类型;如果指定了上界(例如
class MyClass<T extends Number>
),则会被替换为所指定的上界类型(这里是Number
)现在还不知道上界,不要紧后面看就懂了
可能到这里就有朋友有疑问了,反正泛型都会被替换为Object那为什么不能用泛型创建数组呢?
先讲直接用泛型创建的数组:
private T[] array = new T[10];
- 我现在根本不知道它是什么类型,但是数组在创建的时候需要确定类型
用Object创建再强转的情况:
private T[] array = (T[])new Object[10];
我们用一个代码来解释:
public class MyArray<T>{ private Object[] array = new Object[10]; public void set(int pos, T val) { array[pos] = val; } public T get(int pos) { return (T)array[pos]; } public T[] getArray() { return (T[])array; } } class TestDemo { public static void main(String[] args) { MyArray<Integer> myArray = new MyArray<>(); Integer[] ret = myArray.getArray(); } }
报了一个这样的异常:类型异常
Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Integer; ([Ljava.lang.Object; and [Ljava.lang.Integer; are in module java.base of loader 'bootstrap') at demo1.TestDemo.main(MyArray.java:23)
原因:替换后的方法为:将 Object[] 分配给 Integer[] 引用,程序报错正确的方法:(了解)class MyArray<T> { public T[] array; public MyArray() { } /** * 通过反射创建,指定类型的数组 * @param clazz * @param capacity */ public MyArray(Class<T> clazz, int capacity) { array = (T[]) Array.newInstance(clazz, capacity); } 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 static void main(String[] args) { MyArray<Integer> myArray1 = new MyArray<>(Integer.class,10); Integer[] integers = myArray1.getArray(); } }
五、泛型类
1.泛型类的定义
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
public class Test<T> {
public T key;
public void set(T key) {
this.key = key;
}
public T get() {
return this.key;
}
}
在泛型类中,类型参数定义的位置有三处,分别为:
- 非静态的成员变量类型
- 非静态的常用方法形参
- 非静态的成员方法返回类型
其中静态的成员方法,可以用泛型方法的方式使用泛型,后面泛型方法讲解
2.泛型类的使用
public static void main(String[] args) {
//推荐
//在<>内传入你想使用的类型,后面的<>内可以不写会自动推导
Test<Integer> test = new Test<>();
//显示定义
Test<Integer> test1 = new Test<Integer>();
}
- 当我们传入Integer类型之后就不能传入其他类型了,如果传入其他类型编译器会直接报错,所以使用泛型是有安全检查机制,能帮我们更好的管理数据。
- 值得注意的是,泛型类,类型的确定是在实例化对象的时候,在此之前是Object
3.泛型被替换后
public class Test<Integer>{
public Integer key;
public void set(Integer key) {
this.key = key;
}
public Integer get() {
return this.key;
}
}
4.裸类型(Raw Type) (了解)
如果我们在实例化泛型类时,没有指定类型:
Test test = new Test();
那么里面的泛型类都会变为Object
public class Test {
public Object key;
public void set(Object key) {
this.key = key;
}
public Object get() {
return this.key;
}
}
注意: 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制下面的类型擦除部分,我们也会讲到编译器是如何使用裸类型的。小结:1. 泛型是将数据类型参数化,进行传递2. 使用 <T> 表示当前类是一个泛型类。3. 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换
六、泛型接口
1.泛型接口的定义
泛型接口和泛型类差别不大:
public interface 接口名<类型形参列表> {
...
}
public interface IUsb<T> {
// public T key; 报错,接口中的成员是静态的: public static final
public T get(T key); //抽象方法可以使用: public abstract
//默认的方法也可以使用
default T Func(T key) {
return key;
}
}
2.泛型接口的使用
泛型类实现泛型接口
class AA<T> implements IUsb<T> {
@Override
public T get(T key) {
return null;
}
}
普通类实现泛型接口:
就需要指定类型了
class BB implements IUsb<Integer> {
@Override
public Integer get(Integer key) {
return null;
}
}
接口情况:
泛型接口继承泛型接口:
interface AC<T> extends IUsb<T> {
//……
}
class CC implements AC<String> {
@Override
public String get(String key) {
return null;
}
}
都是泛型类:
interface AC<T> extends IUsb<T> {
//……
}
class CC<T> implements AC<T> {
@Override
public T get(T key) {
return null;
}
}
总结:
- 如果是泛型类实现或继承某个接口或类,在实例化对象的时候才确定具体类型。
- 如果不是泛型类或接口实现或继承某个接口或类,就需要在定义接口或类的时候指定类型
- 且接口中的成员是静态的,不能使用泛型
七、泛型方法
1.泛型方法的定义
要区分好使用泛型类泛型的方法,和本身就是泛型方法的区别:
public <类型参数> 返回类型 方法名(类型参数 变量名) { ... }
只有在方法返回值类型前面写了<?>的时候才是泛型方法
public class Test<T> {
public T key;
public void set(T key) {
this.key = key;
}
public T get() {
return this.key;
}
//这里的泛型虽然和类同名,但是在此方法里面,用的是方法的泛型
public <T> T test(T t) {
return t;
}
}
2.泛型方法的使用
public static void main(String[] args) { Test<Integer> test = new Test<>(); //传入一个不一样的类型参数,证明它们不一样 System.out.println(test.test("hallo")); //或者也可以显示定义类型,但一般没有这么写。不写也会根据你给的值确定类型 System.out.println(test.<String>test("bit")); }
结果:
3.静态泛型方法的定义
//这里的泛型虽然和类同名,但是在此方法里面,用的是方法的泛型
public static <T> T test(T t) {
return t;
}
4.静态泛型方法的使用
public static void main(String[] args) {
//通过类名直接调用
Test.test("hello");
//或者
Test.<String>test("word");
}
5.为什么静态方法可以使用泛型
不知道大家有没有这样的问题,为什么泛型类里面的静态方法不能使用泛型,但是静态方法却可以?
我们知道,泛型类里面的静态方法不能使用是因为,泛型类的类型参数是在实例化对象时才确定的,而静态方法在类加载时就存在,此时类的泛型类型参数还未确定,
静态泛型方法有自己独立声明的泛型类型参数,这个类型参数是在方法调用时才确定具体类型的,与泛型类的实例化无关。
八、泛型的上界
class 泛型类名称<类型形参 extends 类型边界> {
...
}
public class MyArray<E extends Number> {
……
}
意思是:你传入的参数类型(E)必须是Number 的子类或者它自己
更复杂一点的:
public class MyArray<E extends Comparable<E>> {
...
}
你传入的参数类型 E 必须是实现了Comparable,且类型是E
泛型的下届不能直接像泛型的上界一样使用,代码会报错:
但是通配符是可以使用下届的
九、通配符
? 用于在泛型的使用,即为通配符
1.通配符的作用
有这样一段程序
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
class TestDemo {
public static void main(String[] args) {
Message<String> message = new Message<>() ;
message.setMessage("比特就业课欢迎您");
fun(message);
}
// 如果我们传入的不是Stirng类型
public static void fun(Message<String> temp){
System.out.println(temp.getMessage());
}
}
如果我们不想只传入String类型,还想传入各种类型,并且我不希望里面的值被更改
此时就可以使用通配符了:
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
class TestDemo {
public static void main(String[] args) {
Message<String> message = new Message<>() ;
message.setMessage("比特就业课欢迎您");
fun(message);
}
public static void fun(Message<?> temp){
temp.setMessage("hello");//无法修改
System.out.println(temp.getMessage());
}
}
那我们想传入各种类型,但是允许更改数据我们继续用普通的泛型就可以了
public static <T> void fun(Message<T> temp){
temp.setMessage((T)"hello");//此时就可以做到更改了
System.out.println(temp.getMessage());
}
那我们希望可以指定一下传入数据的范围,就可以使用通配符的上界和下届:
? extends 类:设置通配符上限
? super 类:设置通配符下限
2.通配符的上界
语法:
<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类
class Animal{
}
class Dog extends Animal{
}
class Cat extends Animal{
}
class Message<T> { // 设置泛型
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Test {
public static void main(String[] args) {
Message<Dog> message = new Message<>();
message.setMessage(new Dog());
fun(message);
}
// 此时?可以接收Animal的子类,但是由于不确定类型,所以无法修改
public static void fun(Message<? extends Animal> temp){
// temp.setMessage(new Dog()); //仍然无法修改!
// temp.setMessage(new Animal()); //仍然无法修改!
// temp.setMessage(new Cat()); //仍然无法修改!
Animal animal = temp.getMessage();//但是可以接收
System.out.println(animal);
}
}
因为我们无法确定到底传入的是哪一个子类,所有通配符的上界是无法对内容进行更改的。
因此:通配符的上界,不能进行写入数据,只能进行读取数据。
那么普通的泛型上界,可以写入读取数据吗:
public static void main(String[] args) {
Message<Dog> message = new Message<>();
message.setMessage(new Dog());
fun(message);
}
public static <T extends Animal> void fun(Message<T> tmp) {
tmp.setMessage((T) new Dog());
tmp.setMessage((T) new Cat());
Animal animal = tmp.getMessage();
System.out.println(animal);
}
最终输出了Cat对象的地址,答案是可以做到的。
3.通配符的下界
语法
<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
class Animal{
}
class Dog extends Animal{
}
class DDD extends Dog {
}
class Cat extends Animal{
}
class Message<T> { // 设置泛型
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Test {
public static void main(String[] args) {
Message<Dog> message = new Message<>();
message.setMessage(new Dog());
fun(message);
}
public static void fun(Message<? super Dog> temp){
// 此时可以修改!!添加的是Dog 或者 Dog的子类
temp.setMessage(new DDD());//这个是Dog的子类
temp.setMessage(new Dog());//这个是Dog的本身
// Dog dog = temp.getMessage(); 不能接收,这里无法确定是哪个父类
System.out.println(temp.getMessage());//只能直接输出
}
}
