|
当你觉得路难走时,那一定是上坡路

目录
1. 泛型的初识 1.1 什么是泛型
《Java 编程思想》一书中对泛型的介绍中有一段话:通用类和方法只能使用特定类型:要么是基本类型,要么是自定义类。如果你想写出适用于多种类型代码的代码,这种严格的限制可能对代码有很大的限制
以下代码:
<p><pre> <code class="language-java">class Array {
public int[] arr = new int[10];
public void setArr(int val,int pos) {
this.arr[pos] = val;
}
public int getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array array = new Array();
array.setArr(1,0);
int ret = array.getArr(0);
System.out.println(ret);
}
}</code></pre></p>
一般在方法的形参和返回值中使用特定的类型,限制了代码。例如:我们实例化一个 Array 类型的对象。当我们使用这个对象调用setArr方法来设置数组的值时,因为形参是整数,所以只能传入整数的实参。当我们使用这个对象调用getArr方法来获取数组中某个位置的值,因为返回值是一个整数,所以只能返回一个整数值,那么这种刚性限制对代码的限制会很大。
JDK 1.5 中引入了泛型来防止上述限制。
Generic 是一种参数化类型。说到参数,大家都会想到方法的形参。当我们要调用一个方法时,我们需要传入实际参数。什么是参数化类型?顾名思义,形参的类型是根据实参的类型来确定的。
那么我们现在需要在上面的代码数组中存储任意类型的数据,那么我们需要如何修改代码呢?
思路:所有类的父类,默认是Object类。所以类型有包装类,那么它们的包装类都继承了Object类,那么我们可以将数组设置为Object类。所以所有子类都可以被父类接收,那么我们将数组设置为Object类型后就可以接收任意类型的值了
<p><pre> <code class="language-java">class Array {
public Object[] arr = new Object[10];
public void setArr(Object val,int pos) {
this.arr[pos] = val;
}
public Object getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array array = new Array();
array.setArr(1,0);
array.setArr("obj",1);
//int ret = (int)array.getArr(0);
}
}</code></pre></p>
将数组设置为Object类型后,就可以将不同类型的值放在数组的不同位置,但是当我们获取到数组中指定位置的值时,就需要对类型进行强制转换。在大多数情况下,我们想要的是它只能保存一种数据类型,而不是同时保存这么多类型。泛型的主要目的是指定当前容器,保存什么类型的对象,并让编译器检查。此时,您需要将类型作为参数传递。需要什么类型,传入什么类型即可。
1.2 泛型类语法 1.2.1 泛型类定义
<p><pre> <code class="language-java">class 泛型类名称<类型形参列表> {
}</code></pre></p>
<p><pre> <code class="language-java">class Array<T> {

}</code></pre></p>
注意:一个泛型类型参数列表可以指定多种类型
类型参数一般用大写字母表示,类型参数命名为:
1.2.2 泛型类使用语法
<p><pre> <code class="language-java">泛型类<类型实参> 变量名 = new 泛型类<类型实参>(构造方法实参); </code></pre></p>
泛型类变量名:定义泛型类引用
新的泛型类(构造函数参数):实例化一个泛型类对象
<p><pre> <code class="language-java">Array<Integer> array = new Array<Integer>();</code></pre></p>
类型扣除:
<p><pre> <code class="language-java">Array<Integer> array = new Array<>();</code></pre></p>
当编译器可以根据上下文推导出类型实参时,类型实参的填充可以省略
1.2.3 泛型类的使用
<p><pre> <code class="language-java">class Array<T> {
public T[] arr = (T[])(new Object[10]);
public void setArr(T val,int pos) {
this.arr[pos] = val;
}
public T getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array<Integer> array = new Array<Integer>();
array.setArr(1,0);
array.setArr(1,1);
Integer ret = array.getArr(0);
System.out.println(ret);
}
}</code></pre></p>
泛型存在的意义:
1.2.4 裸类型
裸类型:定义和实例化泛型类时不提供类型参数
<p><pre> <code class="language-java">泛型类 变量名 = new 泛型类(构造方法实参); </code></pre></p>
<p><pre> <code class="language-java">Array array = new Array();</code></pre></p>
裸类型存在的意义:是为了兼容老版本API保存的机制
2.泛型是如何编译的 2.1 擦除机制
<p><pre> <code class="language-java">class Array<T> {
public T[] arr = (T[])(new Object[10]);
public void setArr(T val,int pos) {
this.arr[pos] = val;
}
public T getArr(int pos) {
return this.arr[pos];
}
}</code></pre></p>
接下来我们通过DOS窗口查看上述泛型类的字节码文件:

我们使用命令 javap -c 在 Dos 窗口中查看字节码文件。此时,所有的 T 都是 Object。那么在编译过程中,将所有T替换为Object的机制称为擦除机制。
Java 的泛型机制是在编译级别实现的,编译器生成的字节码在运行时不包含泛型类型信息。
问题:为什么我们不能新建一个泛型类型数组?
答:T[] arr = new T[],编译过程中新的T不会被擦除到Object。Java规定擦除机制只针对变量的类型和返回值的返回类型,不会擦除实例化时的泛型类型除外,所以当创建新的T数组时,不会擦除T入Object,则编译器不识别T,编译时编译器会报错。
3.泛型的上限
泛型的上限:定义泛型时,有时需要对传入的类型做一个约束。在这种情况下,可以使用泛型的上限对其进行约束。例如:我们要传递的类型是继承School类,那么我们可以使用泛型的上界对其进行约束。如果传入的类型没有继承School类,会报错
3.1 语法
<p><pre> <code class="language-java">class 泛型类名称<类型形参 extends 类型上界> {
}</code></pre></p>
3.2 演示
<p><pre> <code class="language-java">class Student<T extends School> {
}</code></pre></p>
此时只接受 School 的子类型作为 T 的类型参数,在编译时擦除时会作为其父类擦除,即作为 School 擦除
注意:如果没有指定泛型的上界,则默认为T extends Object
<p><pre> <code class="language-java">public class MyArray<E extends Comparable<E>> {
}</code></pre></p>
此时传递的实际参数类型必须实现Comparable接口
4. 泛型方法 4.1 语法
<p><pre> <code class="language-java">方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) {
}</code></pre></p>
4.2 例子
<p><pre> <code class="language-java">public class Test {
public static <T> void swap(T[] array,int i,int j) {
T t = array;
array = array[j];
array[j] = t;
}
}</code></pre></p>
4.2.1 使用示例——可以类型推演
<p><pre> <code class="language-java">public static void main(String[] args) {
Integer[] arr = {1,2,3,4,5,6};
swap(arr,0,1);
}</code></pre></p>
当编译器可以根据上下文推导出类型实参时,类型实参的填充可以省略
4.2.2 使用示例——不带类型推导
<p><pre> <code class="language-java">public static void main(String[] args) {
Integer[] arr = {1,2,3,4,5,6};
Test.<Integer>swap(arr,0,1);
}</code></pre></p>
手动将类型参数传递给类型参数
注意:将类型传递给泛型时,必须传递类,不能传递基本数据类型,但可以传递它们的包装类
5. 通配符
通配符:?用于泛型,这个?是通配符
5.1 通配符的作用
<p><pre> <code class="language-java">class Array<T> {
public T[] arr = (T[])(new Object[10]);
public void setArr(T val,int pos) {
this.arr[pos] = val;
}
public T getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array<Integer> array = new Array<Integer>();
fun(array);
}
public static void fun(Array<Integer> arr) {
arr.setArr(1,0);
System.out.println(arr.getArr(0));
}
}</code></pre></p>
在main函数中实例化了一个Array泛型类,传入的类型是Integer。在 fun 方法的形参中只能接收 Array 类型的 Integer 类型的对象。调用 fun 方法时,可以使用类型推导。如果传入不是Array类型的Integer类型的对象,会报错。Array 是一个泛型类。实例化时可以传入不同类型的参数,所以 fun 有一定的局限性。
然后我们可以将fun方法的形参中Array中的类型设置为通配符,这样就可以接收Array类的任何类型的对象。
<p><pre> <code class="language-java">class Array<T> {
public T[] arr = (T[])(new Object[10]);
public void setArr(T val,int pos) {
this.arr[pos] = val;
}
public T getArr(int pos) {
return this.arr[pos];
}
}
public class Test {
public static void main(String[] args) {
Array<Integer> array = new Array<Integer>();
array.setArr(1,0);
fun(array);
}
public static void fun(Array<?> arr) {
System.out.println(arr.getArr(0));
}
}</code></pre></p>
使用通配符“?” 表示可以接收任意类型的Array对象,但由于类型不确定,不能修改
5.2 子通配符
在通配符的基础上,生成两个子通配符:
5.2.1 通配符上限
① 语法
<p><pre> <code class="language-java"><? extends 上界></code></pre></p>
<p><pre> <code class="language-java"><? extends Number></code></pre></p>
传入的参数类型必须是 Number 本身或 Number 的子类
②示例
<p><pre> <code class="language-java">class Fruits {
}
class Apple extends Fruits {
}
class Message<T> {
private T message;
public void setMessage(T message) {
this.message = message;
}
public T getMessage() {
return message;
}
}
public class Main {
public static void main(String[] args) {
Message<Apple> message = new Message<>();
message.setMessage(new Apple());
fun(message);
}
public static void fun(Message<? extends Fruits> tmp) {
//tmp.setMessage(new Apple());
Fruits fruits = tmp.getMessage();
}
}</code></pre></p>
fun的形参类型传递的实参类型必须是Fruits类型或者Fruits的子类。如果我们使用通配符来接收 fun 中的类型,那么我们不知道具体的类型。我们只知道是 Fruits 类型或者 Fruits 子类类型,那么就不能调用 setMessage 来设置消息的值,因为子类类型不能存储父类对象。我们知道main函数传递给fun方法的对象类型要么是Fruits类型,要么是Fruits子类类型,那么我们可以调用getMessage获取消息的值,并用Fruits接收。
通配符的上限,不能写数据,只能读数据
5.2.2 通配符的下界
① 语法
<p><pre> <code class="language-java"><? super 下界></code></pre></p>
<p><pre> <code class="language-java"><? super Integer></code></pre></p>
传入的类型必须是Integer自己的类或者Integer的父类
②示例
<p><pre> <code class="language-java">class Fruits {
}
class Apple extends Fruits {
}
class Message<T> {
private T message;
public void setMessage(T message) {
this.message = message;
}
public T getMessage() {
return message;
}
}
public class Main {
public static void main(String[] args) {
Message<Fruits> message = new Message<>();
message.setMessage(new Fruits());
fun(message);
}
public static void fun(Message<? super Fruits> tmp) {
tmp.setMessage(new Apple());
//Fruits fruits = tmp.getMessage();
}
}</code></pre></p>
fun 参数类型传递的实际参数类型必须是 Fruits 类型或者 Fruits 类型的父类,那么当我们通过 fun 方法中的 setMessage 设置消息的值时,我们就知道 main 传递的对象类型fun方法的功能是最低的,一定是Fruits方法。通过setMessage来设置消息值,我们可以将其设置为Fruits子类对象,因为Fruits子类肯定也是Fruits父类的子类,所以可以设置消息的值。但是我们无法通过getMessge获取message的值。原因是我们只知道main函数传递给fun的实际参数类型是Fruits类型或者Fruits类型的父类。如果传递的实参类型是 Fruits 父类泛目录解析,
通配符的下界,不能读数据,只能写数据
6.包装类
在 Java 中,基本类型不继承 Object 类。为了支持泛型代码中的基本类型,Java 为每个基本类型对应一个包装类型。
6.1 基本类型对应的包装类基本数据类型包装类
字节
字节
喊
喊
整数
整数
长
长
漂浮
漂浮
双倍的
双倍的
字符
特点
布尔值
布尔值
6.2 装箱和拆箱 6.2.1 装箱
<p><pre> <code class="language-java">int a = 10;
Integer i = Integer.valueOf(a);</code></pre></p>
Integer.valueof:主要将一个基本数据类型的值存入Integer类的对象中
上述代码装箱操作新建了一个Integer类型的对象泛目录解析,并将i的值放入对象的一个属性中
在Integer类的valueOf方法中,如果你存储的值在-128(IntegerCache.low: -128)到127(IntegerCache: 127)的范围内,则存储在数组i+(-整数缓存.low)。如果超过这个范围,则创建一个新的 Integer 对象,并通过构造方法存储在 value 属性中
6.2.2 开箱
<p><pre> <code class="language-java">int a = 10;
Integer i = Integer.valueOf(a);//装箱
int b = i.intValue();//拆箱</code></pre></p>
拆箱操作从 Integer 对象中取值并将其放入基本数据类型
6.2.3 自动装箱和自动拆箱
从上面的手动打包和解包可以看出,手动打包和解包给开发者带来了很多代码。为了减轻开发者的负担,Java提供了自动打包和解包机制。
<p><pre> <code class="language-java">public class Demo {
public static void main(String[] args) {
int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer)i; // 自动装箱
int j = ii; // 自动拆箱
int k = (int)ii; // 自动拆箱
}
}</code></pre></p>
从字节码文件可以看出,装箱和拆箱会在编译时自动进行,恢复为装箱和拆箱
豪侠泛目录站群程序,专业泛目录,站群,二级目录,泛站群程序! |
|