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

x33g5p2x  于2022-01-11 转载在 Java  
字(8.8k)|赞(0)|评价(0)|浏览(375)

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

1. 前章回顾

1.1 泛型类的代码示例

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

// 出现的 <T> 就表示当前的类是一个泛型类,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];

1.2 泛型类的意义

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

1.3 泛型是如何编译的

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

证明方式:

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

2. 泛型类的定义

2.1 语法

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

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

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

2.2 示例

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];
    }
}

3. 内部类

3.1 概念

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

分类:

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

3.2 实例内部类

示例代码:

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 类写成它的一个实例内部类

3.3 静态内部类

示例代码:

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: 当内部类和外部类有同名的静态变量时,默认调用的是内部类本身的。要想调用外部类的,则可以通过:外部类名.变量名 来使用

3.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 接口

4. 泛型类的使用

4.1 语法

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

4.2 示例

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

4.3 类型推导(Type Inference)

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

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

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

5. 裸类型(Raw Type)

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

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

Stack list = new Stack();

注意:

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

6. 泛型类的类型边界

6.1 概念

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

注意:

泛型只有上界,没有下界

6.2 语法

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

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

6.3 示例

示例一: 让泛型参数只接受数值类 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 这个方法在这个参数类型中就得到了重写

7. 类型擦除

7.1 概念

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

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

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

7.2 示例

示例一: 擦除后为 Object

class Stack<T>{
    
}

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

class Stack<T extends Comparable<T>{
    
}

8. 通配符的使用(Wildcards)

8.1 引入

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

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 后面也不需要加 <T> 了
    public static void print(ArrayList<?> list){
        // 由于不知道具体类型是什么,就使用 Object
        for(Object obj: list){
            System.out.println(obj+" ");
        }
        System.out.println();
    }
}

8.2 通配符——上界

语法:

<? 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>());

8.3 通配符——下界

语法:

<? 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>());

9. 泛型中的父子类型

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

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

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

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

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

10. 泛型方法

10.1 语法

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

10.2 示例

示例一: 写一个泛型类 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;
    }
}

10.3 类型型推导(Type Inference)

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

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

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

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

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

11. 泛型的限制

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

相关文章