前言:
泛型的知识其实在前面 Java 的泛型和包装类 这章介绍过了一些,但那些知识是为后面介绍 Java 集合框架做的铺垫,而今天这章再配合之前那章,将会完整的介绍 Java 中的泛型!
在之前那章我们介绍了泛型类的基本定义,这里我们直接来创建并使用一个使用了泛型的栈来回顾泛型的定义
// 出现的 就表示当前的类是一个泛型类,T 是一个占位符
class Stack<T>{
private T[] elem;
private int usedSize;
public Stack(){
this.elem=(T[])new Object[10];
}
// 入栈(不考虑栈满)
public void push(T val){
this.elem[this.usedSize++]=val;
}
// 出栈(不考虑栈空)
public T pop(){
this.usedSize--;
return this.elem[this.usedSize];
}
}
public class TestDemo{
public static void main(String[] args){
Stack<Integer> stack=new Stack<Integer>();
stack.push(1);
stack.push(2);
int val=stack.pop();
System.out.println(val);
System.out.println(stack);
}
}
// 结果为:2 和 Stack@1b6d3586
注意: 上述代码的构造方法为什么代码块是这样的:this.elem=(T[])new Object[10];
- 如果写成
this.elem=new T[10];
,那么我们在编译时根本不知道具体的类型是什么,因此不能直接使用泛型去实例化对象- 使用上述方式可以的原因是:此时发生了泛型的擦除机制,即将泛型 T 擦除为 Object,从而此时的泛型具有了 Object 的特质,所以如果写成这样
this.elem=new T[10];
就等价于代码是这样的this.elem=new Object[10];
- 但是我们想要的是一个非 Object 类型的不通用的数组,即后期不需要进行强制类型转换,故在擦除机制的前提下我们就可以写成
this.elem=(T[])new Object[10];
- 自动进行类型的检查,如:在编译期间会根据指定泛型的信息来检查你插入的值是否匹配,检查完后泛型的信息就被擦除了
- 自动进行类型的转换,如:只要我们使用了泛型,就可以在创建某个具体类型的实例的时候不必要进行强制类型转换
- 泛型是编译期间的一种机制,即擦除机制
- 擦除机制指的是:在编译的时候将泛型 T,擦除为了 Object(此时所有的泛型信息都被擦除了,在生成的 Java 字节码中是不包含泛型重点类型信息的)
证明方式:
- 如果不重写
toString
方法,输出某个类的实例化对象,结果为:类型@对象地址
- 而上述代码的打印结果为:
Stack@1b6d3586
,而不是Stack
,即泛型的的信息在编译期间就被擦除了@1b6d3586
一个类型形参
class 泛型类名称<类型形参>{
// 该代码块中可以直接使用类型参数
}
多个类型形参
class 泛型类名称<类型形参1, 类型形参2, ..., 类型形参n>{
// 该代码块中可以直接使用所有类型参数
}
泛型类可以继承类(包括泛型类)
class 泛型类名称<类型形参> extends 父类名称<类型形参>{
// 该代码块中可以直接使用所有类型参数
}
泛型类可以是一个接口
interface 泛型类名称<类型形参>{
// 该代码块中可以直接使用类型参数
}
常用类型形参: 类型形参一般使用一个大写字母表示,常有名称如下
E
:表示 Element,即元素,运用在集合中K
:表示 Key,即键V
:表示 Value,即值N
:表示 Number,即数值类型T
:表示 Type,即 Java 类型?
:表示不确定的 Java 类型
class Stack<T>{
private T[] elem;
private int usedSize;
public Stack(){
this.elem=(T[])new Object[10];
}
// 入栈(不考虑栈满)
public void push(T val){
this.elem[this.usedSize++]=val;
}
// 出栈(不考虑栈空)
public T pop(){
this.usedSize--;
return this.elem[this.usedSize];
}
}
定义在类内部的类叫做内部类
分类:
- 本地内部类:定义在方法里面的类,很少见
- 实例内部类:指没有用 static 修饰的内部类,有的地方也称为非静态内部类
- 静态内部类:指使用 static 修饰的内部类
- 匿名内部类:是没有名字的内部类
示例代码:
class OuterClass{
// 在外部类中成员变量都是可以正常定义的
public int data1=1;
public static int data2=2;
private int data3=3;
// 定义实例内部类
class InnerClass{
public int data4=4;
// 实例内部类中静态变量无法定义
// public static int data5=5; 该变量无法定义
// 但是增加一个 final 就可以定义了
public static final int data5=5;
private int data6=6;
public void func(){
System.out.println("这是一个实力内部类的 func 方法,也可以正常定义");
System.out.println(data1);
System.out.println(data2);
System.out.println(data3);
System.out.println(data4);
System.out.println(data5);
System.out.println(data6);
}
}
}
结论1: 在实例内部类当中,是不可以定义一个静态的成员变量
因为实例内部类的调用是需要依赖对象的,而 static 修饰的成员是静态的,是不依赖对象的,就如普通的方法中定义静态的变量也是不行的
结论2: 如果加一个 final,那么就可以在实例内部类中使用 static
因为此时表示的是常量了,而常量在编译期间就已经确定了
结论3: 实例化实例内部类的方式是:先实例化外部类,再通过下面第二行代码的形式去实例化
OuterClass outerClass=new OuterClass();
OuterClass.InnerClass innerClass=outerClass.new InnerClass();
结论4: 实例内部类中的方法也可以调用外部类的一些成员变量
innerClass.func();
// 结果为:
// 这是一个实力内部类的 func 方法,也可以正常定义
// 1 2 3 4 5 6
结论5: 如果实例内部类中定义的变量名和外部类中的某个变量名相同,那么实例内部类默认调用的是内部类的变量。即使用 this,也表示的是此时内部类的对象,如果要使用外部类的同名变量,则可以通过:外部类名.this.外部类变量名
来调用
结论6: 当我们去我们看我们定义的静态内部类的字节码文件时,它其实是这样的
应用:
比如我们自己创建链表时,Node 节点是定义在 LinkedList 类外部的,但是可以将 Node 类写成它的一个实例内部类
示例代码:
class OuterClass{
// 在外部类中成员变量都是可以正常定义的
public int data1=1;
public static int data2=2;
private int data3=3;
// 定义静态内部类
static class InnerClass{
public int data4=4;
public static final int data5=5;
private int data6=6;
public void func(){
System.out.println("这是一个实力内部类的 func 方法,也可以正常定义");
System.out.println(data1);
System.out.println(data2);
System.out.println(data3);
System.out.println(data4);
System.out.println(data5);
System.out.println(data6);
}
}
}
结论1: 以下是实例化静态内部类的方法,相比实例内部类,它不需要外部类去创建对象
OuterClass.InnerClass innerClass=new OuterClass.InnerClass();
结论2: 在静态内部类当中,不能调用外部类的普通成员变量
因为普通成员变量需要靠外部类的对象来调用
结论3: 如果要想在静态内部类中调用外部类的普通成员变量,则可以在静态内部类当中实例化一个外部类的对象,通过这个引用就可以访问外部类的普通成员变量
static class InnerClass{
public OuterClass out=new OuterClass();
System.out.println(out.data1);
}
结论4: 当内部类和外部类有同名的静态变量时,默认调用的是内部类本身的。要想调用外部类的,则可以通过:外部类名.变量名
来使用
实例代码:
不使用匿名内部类来实现抽象方法
abstract class Person {
public abstract void eat();
}
class Child extends Person {
public void eat() {
System.out.println("eat something");
}
}
public class TestDemo {
public static void main(String[] args) {
Person p = new Child();
p.eat();
}
}
// 结果为:eat something
如果上述 Child 类只使用一次,那么单独写一个类出来就比较麻烦,所以可以使用匿名内部类
abstract class Person {
public abstract void eat();
}
public class TestDemo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}
// 结果为:eat something
结论1: 由于没有名字,所以匿名内部类只能使用一次
结论2: 使用匿名内部类的前提是:必须继承一个父类或实现一个接口
结论3: 匿名内部类的形式就是直接在声明的对象后面接一个大括号,里面就写该类需要使用的内容
应用:
最常用的情况就是在多线程的实现上,因为要实现多线程必须继承 Thread 类或是继承 Runnable 接口
泛型类<类型实参> 变量名 = new 泛型类<类型实参>(构造方法实参);
Stack<Integer> stack=new Stack<Integer>();
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
上述示例就可以省略后面一个类型实参
Stack<Integer> stack=new Stack<>();
概念:
裸类型是一个泛型类但没有带着类型参数
示例: 上述代码创建的泛型类 Stack
,如果将 Stack
单拿出来不加
去使用的话,那么它就是一个裸类型,我们可以直接使用它去实例化对象
Stack list = new Stack();
注意:
我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制。如果使用他的话,就跟不用泛型没两样了,泛型的作用和意义也就没了
在定义泛型类时,有时需要对传入的类型参数做一定的约束,可以通过类型边界来约束
注意:
泛型只有上界,没有下界
class 泛型类名称<类型参数 extends 类型边界>{
}
上述泛型类可以传入的类型参数必须是类型边界的类或者子类
示例一: 让泛型参数只接受数值类 Number 的子类型
class Stack<T extends Number>{
}
故此时泛型参数传 Integer 是可以的,但传 String 是不行的
Stack<Integer> l1; // 正确,因为 Integer 是 Number 的子类型
Stack<String> l2; // 编译错误,因为 String 不是 Number 的子类型
示例二: 写一个泛型类 Algorithm,我们要这个类中有一个方法可以实现找到数组的最大值
其实我自己的第一想法,就是写成这样
class Algorithm<T>{
public T findMax(T[] array){
T max=array[0];
for(int i=0;i<array.length;i++){
if(array[i]>max){
max=array[i];
}
}
return max;
}
}
但是报错了,自己一想估摸是泛型参数其实是类类型,即大小比较的是引用值,那么估摸要使用 Comparable 接口或者 Comparator 接口
那么我就直接用 compareTo 方法,但是发现使用不了,原因如下
这是由于类型擦除,使得这个 T 被擦除成了 Object,而我们知道 Object 是所有类的祖先类,他是不继承任何类或者接口的。故 compareTo 方法就使用不了
为此,我们就有了这样的写法
class Algorithm<T extends Comparable<T>>{
public T findMax(T[] array){
T max=array[0];
for(int i=0;i<array.length;i++){
if(array[i].compareTo(max)>0){
max=array[i];
}
}
return max;
}
}
这里使用了类型边界来进行了一个约束,代表在进行擦除时,擦除到了 Comparable 接口的地方。通俗点讲,就是这样写,那么这个 T 就一定要实现 Comparable 接口,并且擦除时不会擦除成 Object,而是擦除成了 Comparable
问题: 示例二继承了 Comparable 接口为什么没有重写 compareTo 方法?
因为我们要传入的参数类型是本身一定要实现 Comparable 这个接口的,既然本身已经实现了,那么 compareTo 这个方法在这个参数类型中就得到了重写
- 泛型是作用在编译期间的一种机制,实际上运行上是没有这么多类的,那么运行期间是什么类型呢?这就是类型擦除所作的事情
- 类型擦除主要以其类型边界而定
补充: 编译器在类型擦除阶段所做什么?
- 将类型变量用擦除后的类型替换
- 加入必要的类型转换语句
- 加入必要的
bridge method
保证多态的正确性
示例一: 擦除后为 Object
class Stack<T>{
}
示例二: 擦除后为类型边界(这里是 Comparable)
class Stack<T extends Comparable<T>{
}
以下这个代码的目的是遍历顺序表
class Generic{
public static<T> void print(ArrayList<T> list){
for(T t: list){
System.out.print(t+" ");
}
System.out.println();
}
}
上述代码中我们使用了泛型,并且指定了它的类型参数是 T,故我们使用时这个方法已经知道它的类型是 T 了。而这个 T 是我们指定的,有时这个方法本身也不知道传入的这个顺序表的参数类型是什么?那该怎么写呢?
这里就要使用到通配符 ?
class Generic{
// 既然不知道具体类型,那么 static 后面也不需要加 了
public static void print(ArrayList<?> list){
// 由于不知道具体类型是什么,就使用 Object
for(Object obj: list){
System.out.println(obj+" ");
}
System.out.println();
}
}
语法:
<? extends 上界>
表示可以传入的类型实参是上界类型的子类的任意类型
示例:
// Stack 对象中可以传入的类型实参是 Number 子类的任意类型的 Stack
public static void printAll(Stack<? extends Number> stack){
}
// 以下调用都是正确的
printAll(new Stack<Integer>());
printAll(new Stack<Double>());
printAll(new Stack<Number>());
// 以下调用是编译错误的
printAll(new Stack<String>());
printAll(new Stack<Object>());
语法:
<? super 下界>
表示可以传入的类型实参是下界类型的父类的任意类型
示例:
// Stack 对象中可以传入的类型实参是 Integer 父类的任意类型的 Stack
public static void printAll(Stack<? Super Integer> stack){
}
// 以下调用都是正确的
printAll(new Stack<Integer>());
printAll(new Stack<Object>());
printAll(new Stack<Number>());
// 以下调用是编译错误的
printAll(new Stack<String>());
printAll(new Stack<Double>());
我们知道 Object
是 Number
的父类型,Number
是 Integer
的父类型
但是类如 Stack
就不是 Stack
的父类型, Stack
也不是 Stack
的父类型。
因为泛型的参数类型不参与类型的组成
如果要确定泛型的父子类型,则需要使用通配符,如
Stack>
是 Stack extends Number>
的父类型, Stack extends Number>
也是 Stack
的父类型
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表){
}
示例一: 写一个泛型类 Algorithm,我们要这个类中有一个方法可以实现数组中两个值的交换,要求使用这个方法不需要实例化对象
class Algorithm{
public static<T> swap(T[] array,T i, T j){
T tmp=array[i];
array[i]=array[j];
array[j]=tmp;
}
}
示例二: 写一个泛型类 Algorithm,我们要这个类中有一个方法可以实现找到数组的最大值,要求使用这个方法不需要实例化对象
class Algorithm{
public static<T extends Comparable<T>> T findMax(T[] array){
T max=array[0];
for(int i=1;i<array.length;i++){
if(array[i].compareTo(max)>0){
max=array[i];
}
}
return max;
}
}
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
示例:通过示例中的示例二的 Algorithm 类,去找到数组的最大值
Integer[] array={
1,4,2,9,10};
// 使用 表示我们要传入的值都是 Integer 类型的
Integer ret=Algorithm.<Integer>findMax(array);
但是由于我们通过上文可以判断这个值是 Integer 类型的,所以上述代码可以省略
Integer[] array={
1,4,2,9,10};
Integer ret=Algorithm.findMax(array);
- 泛型类型参数不支持基本数据类型
- 无法实例化泛型类型的对象
- 无法使用泛型类型声明静态的属性
- 无法使用
instanceof
判断带类型参数的泛型类型- 无法创建泛型类型数组
- 无法
create
、catch
、throw
一个泛型类异常,即异常不支持泛型- 泛型类型不是形参一部分,无法重载