JAVA中的泛型类是啥东西?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA中的泛型类是啥东西?相关的知识,希望对你有一定的参考价值。

在Java SE1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。 1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。  2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。  3、泛型的类型参数可以有多个。  4、泛型的参数类型可以使用extends语句,例如<Textends superclass>。习惯上成为“有界类型”。  5、泛型的参数类型还可以是通配符类型。例如Class<?> classType =Class.forName(java.lang.String);   参考技术A 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。 参考技术B 泛型(Generic type 或者generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
可以在集合框架(Collection framework)中看到泛型的动机。例如,Map类允许您向一个Map添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型(比如String)的对象。
因为Map.get()被定义为返回Object,所以一般必须将Map.get()的结果强制类型转换为期望的类型,如下面的代码所示:

Map m = new HashMap();

m.put("key", "blarg");

String s = (String) m.get("key");

要让程序通过编译,必须将get()的结果强制类型转换为String,并且希望结果真的是一个String。但是有可能某人已经在该映射中保存了不是String的东西,这样的话,上面的代码将会抛出ClassCastException。
理想情况下,您可能会得出这样一个观点,即m是一个Map,它将String键映射到String值。这可以让您消除代码中的强制类型转换,同时获得一个附加的类型检查层,该检查层可以防止有人将错误类型的键或值保存在集合中。这就是泛型所做的工作。
泛型的好处
Java 语言中引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了。这带来了很多好处:
· 类型安全。泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
Java 程序中的一种流行技术是定义这样的集合,即它的元素或键是公共类型的,比如“String列表”或者“String到String的映射”。通过在变量声明中捕获这一附加的类型信息,泛型允许编译器实施这些附加的类型约束。类型错误现在就可以在编译时被捕获了,而不是在运行时当作ClassCastException展示出来。将类型检查从运行时挪到编译时有助于您更容易找到错误,并可提高程序的可靠性。
· 消除强制类型转换。泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
尽管减少强制类型转换可以降低使用泛型类的代码的罗嗦程度,但是声明泛型变量会带来相应的罗嗦。比较下面两个代码例子。
该代码不使用泛型:

List li = new ArrayList();

li.put(new Integer(3));

Integer i = (Integer) li.get(0);

该代码使用泛型:

List<Integer> li = new ArrayList<Integer>();

li.put(new Integer(3));

Integer i = li.get(0);

在简单的程序中使用一次泛型变量不会降低罗嗦程度。但是对于多次使用泛型变量的大型程序来说,则可以累积起来降低罗嗦程度。
· 潜在的性能收益。泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的JVM 的优化带来可能。
由于泛型的实现方式,支持泛型(几乎)不需要JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
泛型用法的例子

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

前言:

泛型的知识其实在前面 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(以上是关于JAVA中的泛型类是啥东西?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 泛型中的 in 和 out

java 定义泛型类的问题

Java中的泛型 (上) - 基本概念和原理

java--泛型--泛型接口&泛型方法

java中啥是泛型,怎么用泛型?

Java泛型知识总结篇