【Java 基础语法】详解 Java 中的泛型

x33g5p2x  于2021-11-09 转载在 Java  
字(8.8k)|赞(0)|评价(0)|浏览(381)

前言:
泛型的知识其实在前面 Java 的泛型和包装类 这章介绍过了一些,但那些知识是为后面介绍 Java 集合框架做的铺垫,而今天这章再配合之前那章,将会完整的介绍 Java 中的泛型!

1. 前章回顾

1.1 泛型类的代码示例

在之前那章我们介绍了泛型类的基本定义,这里我们直接来创建并使用一个使用了泛型的栈来回顾泛型的定义

  1. // 出现的 <T> 就表示当前的类是一个泛型类,T 是一个占位符
  2. class Stack<T>{
  3. private T[] elem;
  4. private int usedSize;
  5. public Stack(){
  6. this.elem=(T[])new Object[10];
  7. }
  8. // 入栈(不考虑栈满)
  9. public void push(T val){
  10. this.elem[this.usedSize++]=val;
  11. }
  12. // 出栈(不考虑栈空)
  13. public T pop(){
  14. this.usedSize--;
  15. return this.elem[this.usedSize];
  16. }
  17. }
  18. public class TestDemo{
  19. public static void main(String[] args){
  20. Stack<Integer> stack=new Stack<Integer>();
  21. stack.push(1);
  22. stack.push(2);
  23. int val=stack.pop();
  24. System.out.println(val);
  25. System.out.println(stack);
  26. }
  27. }
  28. // 结果为: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];

1.2 泛型类的意义

  • 自动进行类型的检查,如:在编译期间会根据指定泛型的信息来检查你插入的值是否匹配,检查完后泛型的信息就被擦除了
  • 自动进行类型的转换,如:只要我们使用了泛型,就可以在创建某个具体类型的实例的时候不必要进行强制类型转换

1.3 泛型是如何编译的

  • 泛型是编译期间的一种机制,即擦除机制
  • 擦除机制指的是:在编译的时候将泛型 T,擦除为了 Object(此时所有的泛型信息都被擦除了,在生成的 Java 字节码中是不包含泛型重点类型信息的)

证明方式:

  • 如果不重写 toString 方法,输出某个类的实例化对象,结果为:类型@对象地址
  • 而上述代码的打印结果为:Stack@1b6d3586,而不是 Stack<Integer>@1b6d3586,即泛型的的信息在编译期间就被擦除了

2. 泛型类的定义

2.1 语法

  • 一个类型形参
  1. class 泛型类名称<类型形参>{
  2. // 该代码块中可以直接使用类型参数
  3. }
  • 多个类型形参
  1. class 泛型类名称<类型形参1, 类型形参2, ..., 类型形参n>{
  2. // 该代码块中可以直接使用所有类型参数
  3. }
  • 泛型类可以继承类(包括泛型类)
  1. class 泛型类名称<类型形参> extends 父类名称<类型形参>{
  2. // 该代码块中可以直接使用所有类型参数
  3. }
  • 泛型类可以是一个接口
  1. interface 泛型类名称<类型形参>{
  2. // 该代码块中可以直接使用类型参数
  3. }

常用类型形参: 类型形参一般使用一个大写字母表示,常有名称如下

  • E:表示 Element,即元素,运用在集合中
  • K:表示 Key,即键
  • V:表示 Value,即值
  • N:表示 Number,即数值类型
  • T:表示 Type,即 Java 类型
  • ? :表示不确定的 Java 类型

2.2 示例

  1. class Stack<T>{
  2. private T[] elem;
  3. private int usedSize;
  4. public Stack(){
  5. this.elem=(T[])new Object[10];
  6. }
  7. // 入栈(不考虑栈满)
  8. public void push(T val){
  9. this.elem[this.usedSize++]=val;
  10. }
  11. // 出栈(不考虑栈空)
  12. public T pop(){
  13. this.usedSize--;
  14. return this.elem[this.usedSize];
  15. }
  16. }

3. 内部类

3.1 概念

定义在类内部的类叫做内部类

分类:

  • 本地内部类:定义在方法里面的类,很少见
  • 实例内部类:指没有用 static 修饰的内部类,有的地方也称为非静态内部类
  • 静态内部类:指使用 static 修饰的内部类
  • 匿名内部类:是没有名字的内部类

3.2 实例内部类

示例代码:

  1. class OuterClass{
  2. // 在外部类中成员变量都是可以正常定义的
  3. public int data1=1;
  4. public static int data2=2;
  5. private int data3=3;
  6. // 定义实例内部类
  7. class InnerClass{
  8. public int data4=4;
  9. // 实例内部类中静态变量无法定义
  10. // public static int data5=5; 该变量无法定义
  11. // 但是增加一个 final 就可以定义了
  12. public static final int data5=5;
  13. private int data6=6;
  14. public void func(){
  15. System.out.println("这是一个实力内部类的 func 方法,也可以正常定义");
  16. System.out.println(data1);
  17. System.out.println(data2);
  18. System.out.println(data3);
  19. System.out.println(data4);
  20. System.out.println(data5);
  21. System.out.println(data6);
  22. }
  23. }
  24. }

结论1: 在实例内部类当中,是不可以定义一个静态的成员变量

因为实例内部类的调用是需要依赖对象的,而 static 修饰的成员是静态的,是不依赖对象的,就如普通的方法中定义静态的变量也是不行的

结论2: 如果加一个 final,那么就可以在实例内部类中使用 static

因为此时表示的是常量了,而常量在编译期间就已经确定了

结论3: 实例化实例内部类的方式是:先实例化外部类,再通过下面第二行代码的形式去实例化

  1. OuterClass outerClass=new OuterClass();
  2. OuterClass.InnerClass innerClass=outerClass.new InnerClass();

结论4: 实例内部类中的方法也可以调用外部类的一些成员变量

  1. innerClass.func();
  2. // 结果为:
  3. // 这是一个实力内部类的 func 方法,也可以正常定义
  4. // 1 2 3 4 5 6

结论5: 如果实例内部类中定义的变量名和外部类中的某个变量名相同,那么实例内部类默认调用的是内部类的变量。即使用 this,也表示的是此时内部类的对象,如果要使用外部类的同名变量,则可以通过:外部类名.this.外部类变量名 来调用

结论6: 当我们去我们看我们定义的静态内部类的字节码文件时,它其实是这样的

应用:
比如我们自己创建链表时,Node 节点是定义在 LinkedList 类外部的,但是可以将 Node 类写成它的一个实例内部类

3.3 静态内部类

示例代码:

  1. class OuterClass{
  2. // 在外部类中成员变量都是可以正常定义的
  3. public int data1=1;
  4. public static int data2=2;
  5. private int data3=3;
  6. // 定义静态内部类
  7. static class InnerClass{
  8. public int data4=4;
  9. public static final int data5=5;
  10. private int data6=6;
  11. public void func(){
  12. System.out.println("这是一个实力内部类的 func 方法,也可以正常定义");
  13. System.out.println(data1);
  14. System.out.println(data2);
  15. System.out.println(data3);
  16. System.out.println(data4);
  17. System.out.println(data5);
  18. System.out.println(data6);
  19. }
  20. }
  21. }

结论1: 以下是实例化静态内部类的方法,相比实例内部类,它不需要外部类去创建对象

  1. OuterClass.InnerClass innerClass=new OuterClass.InnerClass();

结论2: 在静态内部类当中,不能调用外部类的普通成员变量

因为普通成员变量需要靠外部类的对象来调用

结论3: 如果要想在静态内部类中调用外部类的普通成员变量,则可以在静态内部类当中实例化一个外部类的对象,通过这个引用就可以访问外部类的普通成员变量

  1. static class InnerClass{
  2. public OuterClass out=new OuterClass();
  3. System.out.println(out.data1);
  4. }

结论4: 当内部类和外部类有同名的静态变量时,默认调用的是内部类本身的。要想调用外部类的,则可以通过:外部类名.变量名 来使用

3.4 匿名内部类

实例代码:

不使用匿名内部类来实现抽象方法

  1. abstract class Person {
  2. public abstract void eat();
  3. }
  4. class Child extends Person {
  5. public void eat() {
  6. System.out.println("eat something");
  7. }
  8. }
  9. public class TestDemo {
  10. public static void main(String[] args) {
  11. Person p = new Child();
  12. p.eat();
  13. }
  14. }
  15. // 结果为:eat something

如果上述 Child 类只使用一次,那么单独写一个类出来就比较麻烦,所以可以使用匿名内部类

  1. abstract class Person {
  2. public abstract void eat();
  3. }
  4. public class TestDemo {
  5. public static void main(String[] args) {
  6. Person p = new Person() {
  7. public void eat() {
  8. System.out.println("eat something");
  9. }
  10. };
  11. p.eat();
  12. }
  13. }
  14. // 结果为:eat something

结论1: 由于没有名字,所以匿名内部类只能使用一次

结论2: 使用匿名内部类的前提是:必须继承一个父类或实现一个接口

结论3: 匿名内部类的形式就是直接在声明的对象后面接一个大括号,里面就写该类需要使用的内容

应用:
最常用的情况就是在多线程的实现上,因为要实现多线程必须继承 Thread 类或是继承 Runnable 接口

4. 泛型类的使用

4.1 语法

  1. 泛型类<类型实参> 变量名 = new 泛型类<类型实参>(构造方法实参);

4.2 示例

  1. Stack<Integer> stack=new Stack<Integer>();

4.3 类型推导(Type Inference)

当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写

上述示例就可以省略后面一个类型实参

  1. Stack<Integer> stack=new Stack<>();

5. 裸类型(Raw Type)

概念:
裸类型是一个泛型类但没有带着类型参数

示例: 上述代码创建的泛型类 Stack<T> ,如果将 Stack 单拿出来不加 <T> 去使用的话,那么它就是一个裸类型,我们可以直接使用它去实例化对象

  1. Stack list = new Stack();

注意:

我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制。如果使用他的话,就跟不用泛型没两样了,泛型的作用和意义也就没了

6. 泛型类的类型边界

6.1 概念

在定义泛型类时,有时需要对传入的类型参数做一定的约束,可以通过类型边界来约束

注意:

泛型只有上界,没有下界

6.2 语法

  1. class 泛型类名称<类型参数 extends 类型边界>{
  2. }

上述泛型类可以传入的类型参数必须是类型边界的类或者子类

6.3 示例

示例一: 让泛型参数只接受数值类 Number 的子类型

  1. class Stack<T extends Number>{
  2. }

故此时泛型参数传 Integer 是可以的,但传 String 是不行的

  1. Stack<Integer> l1; // 正确,因为 Integer 是 Number 的子类型
  2. Stack<String> l2; // 编译错误,因为 String 不是 Number 的子类型

示例二: 写一个泛型类 Algorithm,我们要这个类中有一个方法可以实现找到数组的最大值

  • 其实我自己的第一想法,就是写成这样
  1. class Algorithm<T>{
  2. public T findMax(T[] array){
  3. T max=array[0];
  4. for(int i=0;i<array.length;i++){
  5. if(array[i]>max){
  6. max=array[i];
  7. }
  8. }
  9. return max;
  10. }
  11. }

但是报错了,自己一想估摸是泛型参数其实是类类型,即大小比较的是引用值,那么估摸要使用 Comparable 接口或者 Comparator 接口

  • 那么我就直接用 compareTo 方法,但是发现使用不了,原因如下
    这是由于类型擦除,使得这个 T 被擦除成了 Object,而我们知道 Object 是所有类的祖先类,他是不继承任何类或者接口的。故 compareTo 方法就使用不了
  • 为此,我们就有了这样的写法
  1. class Algorithm<T extends Comparable<T>>{
  2. public T findMax(T[] array){
  3. T max=array[0];
  4. for(int i=0;i<array.length;i++){
  5. if(array[i].compareTo(max)>0){
  6. max=array[i];
  7. }
  8. }
  9. return max;
  10. }
  11. }

这里使用了类型边界来进行了一个约束,代表在进行擦除时,擦除到了 Comparable 接口的地方。通俗点讲,就是这样写,那么这个 T 就一定要实现 Comparable 接口,并且擦除时不会擦除成 Object,而是擦除成了 Comparable

问题: 示例二继承了 Comparable 接口为什么没有重写 compareTo 方法?
因为我们要传入的参数类型是本身一定要实现 Comparable 这个接口的,既然本身已经实现了,那么 compareTo 这个方法在这个参数类型中就得到了重写

7. 类型擦除

7.1 概念

  • 泛型是作用在编译期间的一种机制,实际上运行上是没有这么多类的,那么运行期间是什么类型呢?这就是类型擦除所作的事情
  • 类型擦除主要以其类型边界而定

补充: 编译器在类型擦除阶段所做什么?

  1. 将类型变量用擦除后的类型替换
  2. 加入必要的类型转换语句
  3. 加入必要的 bridge method 保证多态的正确性

7.2 示例

示例一: 擦除后为 Object

  1. class Stack<T>{
  2. }

示例二: 擦除后为类型边界(这里是 Comparable)

  1. class Stack<T extends Comparable<T>{
  2. }

8. 通配符的使用(Wildcards)

8.1 引入

以下这个代码的目的是遍历顺序表

  1. class Generic{
  2. public static<T> void print(ArrayList<T> list){
  3. for(T t: list){
  4. System.out.print(t+" ");
  5. }
  6. System.out.println();
  7. }
  8. }

上述代码中我们使用了泛型,并且指定了它的类型参数是 T,故我们使用时这个方法已经知道它的类型是 T 了。而这个 T 是我们指定的,有时这个方法本身也不知道传入的这个顺序表的参数类型是什么?那该怎么写呢?

这里就要使用到通配符 ?

  1. class Generic{
  2. // 既然不知道具体类型,那么 static 后面也不需要加 <T> 了
  3. public static void print(ArrayList<?> list){
  4. // 由于不知道具体类型是什么,就使用 Object
  5. for(Object obj: list){
  6. System.out.println(obj+" ");
  7. }
  8. System.out.println();
  9. }
  10. }

8.2 通配符——上界

语法:

  1. <? extends 上界>

表示可以传入的类型实参是上界类型的子类的任意类型

示例:

  1. // Stack 对象中可以传入的类型实参是 Number 子类的任意类型的 Stack
  2. public static void printAll(Stack<? extends Number> stack){
  3. }
  4. // 以下调用都是正确的
  5. printAll(new Stack<Integer>());
  6. printAll(new Stack<Double>());
  7. printAll(new Stack<Number>());
  8. // 以下调用是编译错误的
  9. printAll(new Stack<String>());
  10. printAll(new Stack<Object>());

8.3 通配符——下界

语法:

  1. <? super 下界>

表示可以传入的类型实参是下界类型的父类的任意类型

示例:

  1. // Stack 对象中可以传入的类型实参是 Integer 父类的任意类型的 Stack
  2. public static void printAll(Stack<? Super Integer> stack){
  3. }
  4. // 以下调用都是正确的
  5. printAll(new Stack<Integer>());
  6. printAll(new Stack<Object>());
  7. printAll(new Stack<Number>());
  8. // 以下调用是编译错误的
  9. printAll(new Stack<String>());
  10. printAll(new Stack<Double>());

9. 泛型中的父子类型

我们知道 ObjectNumber 的父类型,NumberInteger 的父类型

但是类如 Stack<Object> 就不是 Stack<Number> 的父类型, Stack<Number> 也不是 Stack<Integer> 的父类型。

因为泛型的参数类型不参与类型的组成

如果要确定泛型的父子类型,则需要使用通配符,如

Stack<?>Stack<? extends Number> 的父类型, Stack<? extends Number> 也是 Stack<Integer> 的父类型

10. 泛型方法

10.1 语法

  1. 方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表){
  2. }

10.2 示例

示例一: 写一个泛型类 Algorithm,我们要这个类中有一个方法可以实现数组中两个值的交换,要求使用这个方法不需要实例化对象

  1. class Algorithm{
  2. public static<T> swap(T[] array,T i, T j){
  3. T tmp=array[i];
  4. array[i]=array[j];
  5. array[j]=tmp;
  6. }
  7. }

示例二: 写一个泛型类 Algorithm,我们要这个类中有一个方法可以实现找到数组的最大值,要求使用这个方法不需要实例化对象

  1. class Algorithm{
  2. public static<T extends Comparable<T>> T findMax(T[] array){
  3. T max=array[0];
  4. for(int i=1;i<array.length;i++){
  5. if(array[i].compareTo(max)>0){
  6. max=array[i];
  7. }
  8. }
  9. return max;
  10. }
  11. }

10.3 类型型推导(Type Inference)

当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写

示例:通过示例中的示例二的 Algorithm 类,去找到数组的最大值

  1. Integer[] array={1,4,2,9,10};
  2. // 使用 <Integer> 表示我们要传入的值都是 Integer 类型的
  3. Integer ret=Algorithm.<Integer>findMax(array);

但是由于我们通过上文可以判断这个值是 Integer 类型的,所以上述代码可以省略 <Integer>

  1. Integer[] array={1,4,2,9,10};
  2. Integer ret=Algorithm.findMax(array);

11. 泛型的限制

  • 泛型类型参数不支持基本数据类型
  • 无法实例化泛型类型的对象
  • 无法使用泛型类型声明静态的属性
  • 无法使用 instanceof 判断带类型参数的泛型类型
  • 无法创建泛型类型数组
  • 无法 createcatchthrow 一个泛型类异常,即异常不支持泛型
  • 泛型类型不是形参一部分,无法重载

相关文章