目录
基本概念
1.发展
2.术语
3.探究
类型限定
1.不对类型参数设置界限
2.对类型参数设置界限
类型擦除
1.原始类型
2.类型参数的类型
3.类型检查
4.类型擦除与多态的冲突
注意事项
首页 Java java教程 11.Java 基础 - 泛型

11.Java 基础 - 泛型

Feb 27, 2017 am 10:43 AM

基本概念

泛型的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,在用到的时候在指定具体的类型。

这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法


1.发展

在 JDK 1.5 之前,只能通过 Object 是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。

因此在编译期间,编译器无法检查这个 Object 的强制转型是否成功,这样容导致发生 ClassCastException (强制转换异常) 。

下面我们来看一个实例,就能知道泛型的作用:

  • 不使用泛型的情况(1.5 之前)

ArrayList arrayList = new ArrayList();
arrayList.add(100);
arrayList.add("abc");//因为不知道取出来的值的类型,类型转换的时候容易出错  String str = (String) arrayList.get(0);
登录后复制
  • 使用泛型的情况(1.5 之后)

ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("abc");//因为限定了类型,所以不能添加整形,编译器会提示出错arrayList.add(100);
登录后复制

2.术语

// 以 ArrayList<E>,ArrayList<Integer> 为例:ArrayList<E>:泛型类型

E:类型变量(或者类型参数)

ArrayList<Integer> :参数化的类型

Integer:类型参数的实例(或实际类型参数)

ArrayList :原始类型
登录后复制

3.探究

泛型类

class Demo<T> {    private T value;

    Demo(T value) {        this.value = value;
    }    public T getValue() {        return value;
    }    public void setValue(T value) {        this.value = value;
    }
}public class Test {
    public static void main(String[] args) {
        Demo<String> demo = new Demo("abc");
        demo.setValue("cba");
        System.out.println(demo.getValue()); // cba
    }
}
登录后复制

泛型接口

interface Demo<K, V> {    void print(K k, V v);
}

class DemoImpl implements Demo<String, Integer> {    @Override
    public void print(String k, Integer v) {
        System.out.println(k + "-" + v);
    }
}public class Test {
    public static void main(String[] args) {
        Demo<String, Integer> demo = new DemoImpl();
        demo.print("abc", 100);
    }
}
登录后复制

泛型方法

public class Test {
    public static void main(String[] args) {        int num = get("abc", 100);
        System.out.println(num);
    }    // 关键 --> 多了 <K, V> ,可以理解为声明此方法为泛型方法
    public static <K, V> V get(K k, V v) {        if (k != null) {            return v;
        }        return null;
    }
}
登录后复制

类型限定

类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过要注意下面几点:

  • 不管该限定是类还是接口,统一都使用关键字 extends

  • 可以使用 & 符号给出多个限定

  • 如果限定既有接口也有类,那么类必须只有一个,并且放在首位置。例如:

public static <T extends Comparable&Serializable> T get(T t1,T t2)
登录后复制

下面再来分析下类型限定的作用…


1.不对类型参数设置界限

观察下面的代码,在没有对类型参数进行类型限定时会出现编译错误。原因如下:

  • 因为在编译之前,编译器并不能确认泛型类型(T)是什么类型

  • 因此它默认 T 为原始类型(Object)。

  • 所以只能调用 Object 的方法,而不能调用 compareTo 方法。

public static <T> T get(T t1,T t2) {    //编译错误
    if(t1.compareTo(t2)>=0);    return t1;
}
登录后复制

2.对类型参数设置界限

当对类型参数 T 设置界限(bound)后,编译错误不再发生。因为此时编译器默认 T 的原始类型为 Comparable。

public static <T extends Comparable> T get(T t1,T t2) {    if(t1.compareTo(t2)>=0);    return t1;
}
登录后复制

类型擦除

  • Java 中的泛型基本上都是在编译器这个层次来实现的。

  • 在生成的 Java 字节码中是不包含泛型中的类型信息的。

  • 使用泛型时加上的类型参数,会在编译器在编译的时候去掉,这个过程就称为类型擦除。


来看下面的这个例子:

public class Test {
    public static void main(String[] args) {
        ArrayList<String> arrayList1 =new ArrayList<String>();
        ArrayList<Integer> arrayList2 = new ArrayList<Integer>();        // true
        System.out.println(arrayList1.getClass() == arrayList2.getClass());
    }
}
登录后复制

观察代码,这里定义了两个ArrayList数组:

  • 一个是ArrayList泛型类型,只能存储字符串,一个是ArrayList泛型类型,只能存储整形。

  • 通过比较它们的类对象,发现结果为 true。

  • 说明泛型类型 String 和 Integer 在编译过程中都被擦除掉了,只剩下了原始类型(即 Object)。


再来看一个例子:

public class Test {
    public static void main(String[] args) throws Exception{
        ArrayList<String> arrayList =new ArrayList<String>();
        arrayList.add("abc");
        arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, 100);         for (int i=0;i<arrayList.size();i++) {  
                System.out.println(arrayList.get(i));  
            }  
    }
}
登录后复制

观察代码,这里定义了一个ArrayList 泛型类型实例化为 Integer 的对象

  • 如果直接调用 add 方法,那么只能存储整形的数据。

  • 利用反射调用 add 方法,却可以存储字符串。

  • 说明 Integer 泛型实例在编译之后被擦除了,只保留了原始类型。


1.原始类型

  • 原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型

  • 任意一个泛型的类型参数,都存在对应的原始变量。

  • 一旦类型变量被擦除(crased),就会使用其限定类型(无限定的变量用 Object)替换。

// 此时 T 是一个无限定类型,所以原始类型就是 Objectclass Pair<T> { } 

// 类型变量有限定,原始类型就用第一个边界的类型变量来替换,即Comparableclass Pair<T extends Comparable& Serializable> { }  

// 此时原始类型为 Serializable,编译器在必要的时要向 Comparable 插入强制类型转换
// 为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界限定列表的末尾class Pair<T extends Serializable&Comparable>
登录后复制

2.类型参数的类型

在下面的例子中,类型参数指 T,T 的类型就是所谓的【类型参数】的类型。

观察代码,可以得出如下结论:

  • 不指定【类型参数 T 】的类型,当参数的类型不一致时,原始类型取同一父类的最小级

  • 指定【类型参数 T 】的类型时,原始类型只能为其指定的类型或类型的子类

public class Test {    // 定义泛型方法
    public static <T> T add(T x, T y) {        return y;
    }    public static void main(String[] args) {        // 1.不指定泛型

        // 两个参数都是 Integer,所以 T 为 Integer 类型
        int i = Test.add(1, 2); 

        // 两个参数分别是 Integer,Float,取同一父类的最小级,T 为 Number 类型
        Number f = Test.add(1, 1.2);        // T 为 Object
        Object o = Test.add(1, "asd");        // 2.指定泛型

        // 指定了Integer,所以只能为 Integer 类型或者其子类
        int a = Test.<Integer> add(1, 2);        //编译错误,指定了 Integer,不能为Float
        int b=Test.<Integer>add(1, 2.2); 

         // 指定为Number,所以可以为 Integer,Float
        Number c = Test.<Number> add(1, 2.2);
    }

}
登录后复制

3.类型检查

泛型的类型检查是针对引用的,而不针对被引用的对象本身。

在下面的例子中,list 是引用对象,因此类型检查是针对它的。

// 没有进行类型检查,等价于 ArrayList list = new ArrayLis()ArrayList list = new ArrayList<String>();
list.add(100);
list.add("hello");// 进行编译检查,等价于 ArrayList<String> list = new ArrayList<String>();ArrayList<String> list = new ArrayList();
list.add("hello");
list.add(100);  // 编译错误
登录后复制

4.类型擦除与多态的冲突

来看下面的例子,这里定义了一个泛型类 Parent,一个实现它的子类 Son,并在子类中重写了父类的方法。

class Parent<T> {    private T value;    public T getValue() {        return value;
    }    public void setValue(T value) {        this.value = value;
    }
}

class Son extends Parent<String>{    @Override
    public void setValue(String value) {         super.setValue(value);
    }    @Override
    public String getValue(){        return super.getValue();}
    }
登录后复制

在上面提到过泛型的类型参数在编译时会被类型擦除,因此编译后的 Parent 类如下:

class Parent {    private Object value;    public Object getValue() {        return value;
    }    public void setValue(Object value) {        this.value = value;
    }
}
登录后复制

此时对比 Parent 与 Son 的 getValue/setValue 方法,发现方法的参数类型已经改变,从 Object -> String,这也意味着不是重写(overrride) 而是重载(overload)。

然而调用 Son 的 setValue 方法, 发现添加 Object 对象时编译错误。说明也不是重载。

public class Test {
    public static void main(String[] args) {
        Son son = new Son();
        son.setValue("hello");        // 关键 -->编译错误
        son.setValue(new Object());
    }
}
登录后复制

那么问题来了,通过上面的分析?Son 中定义的方法到底是重写还是重载?答案是:重写。这里 JVM 采用了桥方法(Brige)来解决类型擦除和多态引起的冲突。

我们对 Son 进行反编译(”Javap -c 类名.class”),得到如下内容:

Compiled from "Test.java"class Son extends Parent<java.lang.String> {
  Son();
    Code:       0: aload_0       
       1: invokespecial #8                  // Method Parent."<init>":()V
       4: return        

  public void setValue(java.lang.String);
    Code:       0: aload_0       
       1: aload_1       
       2: invokespecial #16                 // Method Parent.setValue:(Ljava/lang/Object;)V
       5: return        

  public java.lang.String getValue();
    Code:       0: aload_0       
       1: invokespecial #23                 // Method Parent.getValue:()Ljava/lang/Object;
       4: checkcast     #26                 // class java/lang/String
       7: areturn       

  public java.lang.Object getValue();
    Code:       0: aload_0       
       1: invokevirtual #28                 // Method getValue:()Ljava/lang/String;
       4: areturn       

  public void setValue(java.lang.Object);
    Code:       0: aload_0       
       1: aload_1       
       2: checkcast     #26                 // class java/lang/String
       5: invokevirtual #30                 // Method setValue:(Ljava/lang/String;)V
       8: return        }
登录后复制

发现这里共有 4 个 setValue/getValue 方法,除了 Son 表面上重写的 String 类型,编译器又自己生成了 Object 类型的方法,也称为桥方法。结果就是,编译器通过桥方法真正实现了重写,只是在访问时又去调用表面的定义的方法。


注意事项

  • 不能用基本类型实例化类型参数,可以用对应的包装类来实例化类型参数

// 编译错误ArrayList<int> list = new ArrayList<int>();// 正确写法ArrayList<Integer> list = new ArrayList<Integer>();
登录后复制
  • 参数化类型的数组不合法

Demo<T >{
}public class Test {
    public static void main(String[] args) {        // 编译错误 --> 类型擦除导致数组变成 Object [],因此没有意义
        Demo<String>[ ]  demo =new Demo[10];
    }
}
登录后复制
  • 不能实例化类型变量

// 编译错误,需要类型参数需要确定类型Demo<T> demo = new Demo<T>
登录后复制
  • 泛型类的静态上下文中不能使用类型变量

public class Demo<T> {
    public static T name;    public static T getName() {
        ...
    }
}
登录后复制
  • 不能抛出也不能捕获泛型类的对象

//异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉。会导致这里捕获的类型一致try{  
}catch(Problem<Integer> e1){  
    //do Something... }catch(Problem<Number> e2){  
    // do Something ...}
登录后复制

 以上就是11.Java 基础 - 泛型的内容,更多相关内容请关注PHP中文网(www.php.cn)!


本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

Java 中的平方根 Java 中的平方根 Aug 30, 2024 pm 04:26 PM

Java 中的平方根指南。下面我们分别通过例子和代码实现来讨论平方根在Java中的工作原理。

Java 中的完美数 Java 中的完美数 Aug 30, 2024 pm 04:28 PM

Java 完美数指南。这里我们讨论定义,如何在 Java 中检查完美数?,示例和代码实现。

Java 中的随机数生成器 Java 中的随机数生成器 Aug 30, 2024 pm 04:27 PM

Java 随机数生成器指南。在这里,我们通过示例讨论 Java 中的函数,并通过示例讨论两个不同的生成器。

Java中的Weka Java中的Weka Aug 30, 2024 pm 04:28 PM

Java 版 Weka 指南。这里我们通过示例讨论简介、如何使用weka java、平台类型和优点。

Java 中的史密斯数 Java 中的史密斯数 Aug 30, 2024 pm 04:28 PM

Java 史密斯数指南。这里我们讨论定义,如何在Java中检查史密斯号?带有代码实现的示例。

Java Spring 面试题 Java Spring 面试题 Aug 30, 2024 pm 04:29 PM

在本文中,我们保留了最常被问到的 Java Spring 面试问题及其详细答案。这样你就可以顺利通过面试。

突破或从Java 8流返回? 突破或从Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一种强大且表达力丰富的处理数据集合的方式。然而,使用Stream时,一个常见问题是:如何从forEach操作中中断或返回? 传统循环允许提前中断或返回,但Stream的forEach方法并不直接支持这种方式。本文将解释原因,并探讨在Stream处理系统中实现提前终止的替代方法。 延伸阅读: Java Stream API改进 理解Stream forEach forEach方法是一个终端操作,它对Stream中的每个元素执行一个操作。它的设计意图是处

Java 中的时间戳至今 Java 中的时间戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的时间戳到日期指南。这里我们还结合示例讨论了介绍以及如何在java中将时间戳转换为日期。

See all articles