目录
通过前面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类型所以需要强转),难道往后每次取数据的时候我还得看一看是什么类型吗?
泛型是在JDK1.5引入的新的语法,通过上面的例子,由此我们就引出了泛型,泛型简单来说就是把类型当成参数传递,指定当前容器,你想持有什么类型的对象,你就传什么类型过去,让编译器去做类型检查!从而实现类型参数化(不能是基本数据类型,后面讲)
- class Test1<类型形参列表> {
-
- }
- class Test2<类型形参1, 类型形参2, ...> {
-
- }
类名后面的 <类型形参列表> 这是一个占位符,表示当前类是一个泛型类,形参列表里面如何写?通常用一个大写字母表示,当然,你也可以怎么开心怎么来,但是小心办公室谈话警告哈(dog),这里有几个常用的名称:
| E | 表示 Element |
| K | 表示 Key |
| V | 表示 Value |
| N | 表述 Number |
| T | 表示 Type |
| S,U,V | 表示,第二,第三,第四个类型 |
这里就来修改一下刚开始的代码,使用到泛型的知识,那么我们就可以这样修改:
- public class DrawForth
{ - //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
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);
- }
- }
如上修改之后的代码,我们可以得到以下知识点:
裸类型就是指在实例化泛型类对象的时候,没有传类型实参,比如下面的代码就是一个裸类型:
DrawForth draw = new DrawForth();
我现在可以告诉你,这样做编译完全正常,但我们不要去使用裸类型,因为这是为了兼容老版本的 API 保留的机制,毕竟泛型是 Java1.5 新增的语法。
如果我们要看泛型是如何编译的,可以通过命令 javap -c 字节码文件 来进行查看:

如上代码是 2.4 段落中的代码,奇怪,明明传的实参是 Integer 类型,最后所有的 T 却变成了 Object 类型,这就是擦除机制,所以在Java中,泛型机制是在编译级别实现的,运行期间不会包含任何泛型信息。
提示:类型擦除,不一定是把 T 变成 Object(泛型的上界会提到)
知道了擦除机制后,那么 T[] array = new T[3]; 是不对的,编译的时候,替换为Object,不是相当于:Object[] array = new Object[3]吗?
在Java中,数组是一个很特殊的类型,数组是在运行时存储和检查类型信息, 泛型则是在编译时检查类型错误。而且Java设定擦除机制就只针对变量的类型和返回值的类型,所以在编译时候压根不会擦除 new T[3]; 这个 T ,所以自然编译就会报错!
我们前面通过强制类型转换的方式创建了泛型数组,说过那样写并不好,正确的方式是通过反射创建指定类型的数组,由于现在没学习到反射,这里先放着就行。
有了擦除机制的学习,泛型在运行时都会被擦除成 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。
如果给泛型设置了上界,则会擦除到边界处,也就不会擦除成 Object!
- class Person {}
-
- class Student extends Person {}
-
- public class Main
extends Person> { - T array[] = (T[])new Object[10];
- public static void main(String[] args) {
- Main
main = new Main<>(); - }
- }
这里 Main 方法中设定了泛型的上界,传的类型实参必须是Person的子类,所以编译时会不会被擦除成 Person呢?下面我们查看一下对应的字节码文件:

显而易见,确实被擦除成了泛型的上界!
在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了 一个包装类型。
| 基本数据类型 | 包装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
装箱和拆箱也可也被称为装包和拆包。
装箱:将一个基本数据类型值放入对象的某个属性中。
拆箱:将一个包装类型中的值取出放到一个基本数据类型中。
这里我们举例来更清楚的认识装箱和拆箱:
- public class Test {
- public static void main(String[] args) {
- int a = 10;
- Integer integer1 = new Integer(a); //手动装箱
- Integer integer2 = Integer.valueOf(100); //手动装箱
-
- int b = integer1.intValue(); //手动拆箱
- }
- }
由上面的例子我们可以看出,手动装箱和拆箱会带来不少的代码量,为了减少开发者的负担,Java中提供了自动转换机制,比如:
- public class Test {
- public static void main(String[] args) {
- Integer integer = 100; //自动装箱
- int a = integer; //自动拆箱
- }
- }
以下代码输出什么?
- public class Test {
-
- public static void main(String[] args) {
- Integer a1 = 100;
- Integer a2 = 100;
- System.out.println(a1 == a2);
- Integer a3 = 200;
- Integer a4 = 200;
- System.out.println(a3 == a4);
- }
- }
结果是:true false
为什么是这样的答案?这里我们去看一下对应的字节码文件再分析:

通过观察字节码文件,我们可以看到,在自动装箱的过程中,调用了 Integer.valueOf 方法,那么我们就去看一看 valueOf 方法中做了一件什么事:

通过查看源码,我们也能看出此方法将始终缓存 -128到127范围内的值, 通过查看对应的 low 和 high 值也可也发现 low为 -128,high为127,cache 是一个缓存数组。
接着我们来阅读下这段代码的操作,如果传入的值是介于 -128和127 之间,则直接返回缓存数组对应下标的值,比如传入的值是 -127 也就返回 chache[-127+(-(-128))],也即1下标位置的值!
如果超出了 -128到127 的范围则是新 new 一个对象返回,只要是 new 就一定是一个新对象,地址也是唯一的。
而且引用类型用 == 比较,比较的是引用的对象的地址,看完上面的介绍,你能弄明白为什么输出 true 和 false 吗?
定义泛型方法的语法:
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) {
//...code
}
这里我们就举一个很简单的例子:
- public class Test {
- public
T getValue(T value) { - return value;
- }
- public static void main(String[] args) {
- Test test = new Test();
- int ret = test.
getValue(150); //不使用类型推导 - System.out.println(ret);
-
- double d = test.getValue(12.5); //使用类型推导
- System.out.println(d);
- }
- }
这就是泛型方法,这里面有个关键词,类型推导,什么是类型推导呢?
类型推导就是编译器会根据你传参的数据,自动推断出你要传递的类型实参,你也可以不使用类型推导,他们的效果都是一样的。
既然有普通泛型方法,同理,也有静态的泛型方法,也就是在修饰符后面加上 static,静态泛型方法跟普通静态方法一样,都是通过类名访问,不依赖于对象:
- public class Test {
- public static
T getValue(T value) { - return value;
- }
- public static void main(String[] args) {
- int ret = Test.
getValue(150); //不使用类型推导 - System.out.println(ret);
-
- double d = getValue(12.5); //使用类型推导(静态方法可以直接访问同类中静态方法,可以不借助类名)
- System.out.println(d);
- }
- }
我们先来看这样的一段代码:
- class Message
{ - private T message ;
- public T getMessage() {
- return message;
- }
- public void setMessage(T message) {
- this.message = message;
- }
- }
- public class TestDemo {
- public static void fun(Message
temp) { - System.out.println(temp.getMessage());
- }
- public static void main(String[] args) {
- Message
message = new Message<>(); - message.setMessage("欢迎来到篮球哥的博客!");
- fun(message);
- }
- }
如果你仔细观察,TestDemo 类中的 fun 方法是有局限性的,他的形参就限制了传过来的 Missage类的类型必须是String,也就是说,形参能接收的对象的类型参数必须是String类型。
所以如果我们 new Missage对象时,类型实参传的是 Integer 呢?fun方法就会报错:

所以为了解决以上的问题,就有了通配符的概念!
泛型T是确定的类型,一旦传类型了,就定下来了,而通配符的出现,就会使得更灵活,或者说更不确定,就好像他是一个垃圾箱,可以接收所有的泛型类型,但又不能让用户随意更改!
通配符:?
现在我们就把上面的代码更改一下,运用上通配符:
- public class TestDemo {
- public static void fun(Message> temp){
- System.out.println(temp.getMessage());
- }
- public static void main(String[] args) {
- Message
message1 = new Message<>(); - message1.setMessage(123);
- fun(message1);
- Message
message2 = new Message<>(); - message2.setMessage("欢迎来到篮球哥的博客!");
- fun(message2);
- }
- }
这样我们的代码就不会出错,但是,你不能通过 fun 方法去修改你传递对象的内容,为什么呢?
站在 fun 的角度,他使用了 ? 接收可以任意泛型类,所以他不能确定自己接收了什么对象的!也就无法对对象的值进行更改!
这样代码还是不够好,如果真的什么泛型类都能接收,那不是乱套了,所以在此基础上,又增加了通配符的上界和下界!
语法: extends 上界> 例如: extends Person>
表示只能接收的实参类型是 Person 或者 Person的子类!
图例:

这里我们写一段伪代码,更改上面用例的方法:
- public static void fun(Message extends Person> temp){
- //temp.setMessage(new Student()); //仍然无法修改!
- //temp.setMessage(new Person()); //仍然无法修改!
- Person person = temp.getMessage();
- System.out.println(person);
- }
为什么还是不能修改对象的属性呢?因为 temp 接收的是 Person 或 Person的子类,此时接收的是哪个子类无法确定,也就无法设置对象的属性。
因为我们知道只能接收 Person以及他的子类,所以我们就可以拿 Person 类型来接收 getMessage 的对象,因为 Person是他们的父类,获取的是子类对象就可以实现向上转型,是安全的。
总结: 通配符的上界,不能进行写入数据,只能进行读取数据。
语法: extends 下界> 例如: super Person>
表示只能接收的实参类型是 Person 或者 Person的父类!
图例:

这里我们写一段伪代码,更改上面用例的方法:
- public static void fun(Message super Person> temp){
- temp.setMessage(new Student()); //可以修改,因为添加的是他的子类
- temp.setMessage(new Person()); //可以修改,因为添加的是他本身
- //Person person = temp.getMessage(); // 不能接收,不知道获取的是哪个父类
- System.out.println(temp.getMessage()); //只能输出
- }
为啥下界就可以设置对象的属性呢?因为只能接收本身以及父类的类型,所以我们可以setMessage 传子类对象,但是不能传递父类,因为修改成子类对象是向上转型是安全的,如果 setMessaget 传父类对象的话就是向下转型则不安全!
为啥不能 getMessage呢?因为你不知道形参接收的类型是哪个父类,只能去输出内容!
总结:通配符的下界,不能进行读取数据,只能写入数据。

下期预告:【Java 数据结构】手撕顺序表