1、如何创建可以存放各种类型的数组?
通过前面JavaSE的语法知识储备,如果现在让你们创建如标题一样的数组,你会怎么创建呢?
答案是:使用 Object 类来定义数组,因为 Object 是所有类的父类, 可以接收任意子类对象,也即实现了向上转型,于是我们就写出了这样的代码:
private Object[] array = new Object[3];
那么这种方法可取吗?显然是可取的,但只是使用起来会很不方便,具体不方便在哪,我们接着往后看,在这里我们要写一个类,里面提供了获取array指定下标的数据,和设置array指定下标的数据,于是写出了这样的代码:
public class DrawForth { private Object[] array = new Object[3]; public void setPosArray(int pos, Object o) { this.array[pos] = o; } public Object getPosValue(int pos) { return this.array[pos]; } }
代码到这里仍然是正确的,那我们就要去使用这个类,也就是在main方法中用这个类实例对象,去操作里面的数组,所以main方法的代码就是这个样子:
public static void main(String[] args) { DrawForth draw = new DrawForth(); draw.setPosArray(0, 123); draw.setPosArray(1, "hello"); draw.setPosArray(2, 12.5); int a = (int)draw.getPosValue(0); String str = (String)draw.getPosValue(1); double d = (double)draw.getPosValue(1); }
看到这里,你是不是就发现这样做很不方便呢?当我们往数组里面设置数据的时候开心了,想设置成什么类型就是什么类型,但是!当我们要获取对应位置的元素就麻烦了,我们必须知道他是什么类型,然后进行强制类型转换才能接收,(返回是Object类型所以需要强转),难道往后每次取数据的时候我还得看一看是什么类型吗?
2、泛型的概念
2.1 浅聊泛型
泛型是在JDK1.5引入的新的语法,通过上面的例子,由此我们就引出了泛型,泛型简单来说就是把类型当成参数传递,指定当前容器,你想持有什么类型的对象,你就传什么类型过去,让编译器去做类型检查!从而实现类型参数化(不能是基本数据类型,后面讲)
2.2 泛型的简单语法
class Test1<类型形参列表> { } class Test2<类型形参1, 类型形参2, ...> { }
2.3 类型形参列表的命名规范
类名后面的 <类型形参列表> 这是一个占位符,表示当前类是一个泛型类,形参列表里面如何写?通常用一个大写字母表示,当然,你也可以怎么开心怎么来,但是小心办公室谈话警告哈(dog),这里有几个常用的名称:
2.4 使用泛型知识创建数组
这里就来修改一下刚开始的代码,使用到泛型的知识,那么我们就可以这样修改:
public class DrawForth<T> { //private T[] array = new T[3]; error private T[] array = (T[])new Object[3]; public void setPosArray(int pos, T o) { this.array[pos] = o; } public T getPosValue(int pos) { return this.array[pos]; } public static void main(String[] args) { DrawForth<Integer> draw = new DrawForth<>(); draw.setPosArray(0, 123); //draw.setPosArray(1, "hello"); error //draw.setPosArray(2, 12.5); error draw.setPosArray(1, 1234); draw.setPosArray(2, 12345); int a = draw.getPosValue(0); int b = draw.getPosValue(1); int c = draw.getPosValue(2); } }
如上修改之后的代码,我们可以得到以下知识点:
- <T> 是一个占位符,仅表示这个类是泛型类
- 不能 new 泛型数组(原因后面讲),此代码的写法也不是最好的方法!
- 实例化泛型类的语法是:类名<类型实参>变量名 = new 泛型类<类型实参>(构造方法实参);
- 注意:new 泛型类<>尖括号中可以省略类型实参,编译器可以根据上下文推导!
- 编译时自动进行类型检查和转换。
2.5 什么是裸类型?
裸类型就是指在实例化泛型类对象的时候,没有传类型实参,比如下面的代码就是一个裸类型:
DrawForth draw = new DrawForth();
我现在可以告诉你,这样做编译完全正常,但我们不要去使用裸类型,因为这是为了兼容老版本的 API 保留的机制,毕竟泛型是 Java1.5 新增的语法。
3、泛型是如何编译的?
3.1 泛型的擦除机制
如果我们要看泛型是如何编译的,可以通过命令 javap -c 字节码文件 来进行查看:
如上代码是 2.4 段落中的代码,奇怪,明明传的实参是 Integer 类型,最后所有的 T 却变成了 Object 类型,这就是擦除机制,所以在Java中,泛型机制是在编译级别实现的,运行期间不会包含任何泛型信息。
提示:类型擦除,不一定是把 T 变成 Object(泛型的上界会提到)
3.2 再谈为什么不能实例化泛型数组?
知道了擦除机制后,那么 T[] array = new T[3]; 是不对的,编译的时候,替换为Object,不是相当于:Object[] array = new Object[3]吗?
在Java中,数组是一个很特殊的类型,数组是在运行时存储和检查类型信息, 泛型则是在编译时检查类型错误。而且Java设定擦除机制就只针对变量的类型和返回值的类型,所以在编译时候压根不会擦除 new T[3]; 这个 T ,所以自然编译就会报错!
我们前面通过强制类型转换的方式创建了泛型数组,说过那样写并不好,正确的方式是通过反射创建指定类型的数组,由于现在没学习到反射,这里先放着就行。
3.3 什么是泛型的上界?
有了擦除机制的学习,泛型在运行时都会被擦除成 Object 但是并不是所有的都是这样,泛型的上界就是对泛型类传入的类型变量做一定的约束,可以通过类型边界来进行约束。
语法:
class 泛型类名称<类型形参 extends类型边界> {
//...code
}
这里我们来举两个例子:
例1:
这里简单分析一下,Student 继承了 Person 类,而 Teacher 没有继承 Person 类,接着 Test 类给定了泛型的上界, 那么 Test 类中 <> 里面是什么意思呢?表示只接收 Person 或 Person 的子类作为 T 的类型实参。
通过 main 方法中的例子也可也看出,类型传参只能传 Person 或 Person 的子类。
例2:
还是简单分析一下,Student 类实现了 Comparable 接口,而 Teacher 类并没有实现, 接着 Test 类给定了泛型的上界, 那么 Test 类中 <> 里面是什么意思呢?表示 T 接收的类型必须是实现 Comparable 这个接口的!
通过 main 方法中的例子也可也看出,类型传参只能传实现了 Comparable 接口的类 。
注意:如果泛型类没有指定边界,则可以默认视为 T extends Object。
3.4 再谈擦除机制
如果给泛型设置了上界,则会擦除到边界处,也就不会擦除成 Object!
class Person {} class Student extends Person {} public class Main<T extends Person> { T array[] = (T[])new Object[10]; public static void main(String[] args) { Main<Student> main = new Main<>(); } }
这里 Main 方法中设定了泛型的上界,传的类型实参必须是Person的子类,所以编译时会不会被擦除成 Person呢?下面我们查看一下对应的字节码文件:
显而易见,确实被擦除成了泛型的上界!