技术咸鱼 技术咸鱼
首页
  • 《Django教程》
  • 《VuePress教程》
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
关于
头像

公众号:技术咸鱼
首页
  • 《Django教程》
  • 《VuePress教程》
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
关于
  • Java泛型总结
    • 什么是Java泛型
    • 为什么出现Java泛型
    • 泛型的使用
      • 泛型类/泛型接口
      • 泛型方法
    • 泛型类的继承规则
    • 通配符使用
      • 数组的协变
      • 通配符的使用
      • 上边界通配符
      • 下边界通配符
      • 无边界通配符
    • 泛型中的约束和局限性
    • 虚拟机中泛型的实现-类型擦除
  • java
技术咸鱼
2019-04-21

标题 Java泛型总结

# 什么是Java泛型

Java泛型(Java generic)是JDK 5引入的一个新特性.其本质就是参数化类型,也就是把数据类型视作为一个参数,在使用的时候再指定具体类型,这种参数化类型就是泛型.泛型可以用在类,接口,方法上,分别称之为泛型类,泛型接口,泛型方法. 泛型的出现为程序员提供了一种编译时类型安全的监测机制, 使程序员能够在编译期间找出非法类型的存在,提高开发的安全性和效率. Java中的泛型一种伪泛型,使用了类型擦除实现的,本质上是Java语言的语法糖.


# 为什么出现Java泛型

看下面的情形

public int add(int a, int b) {
        return a + b;
    }

    public float add(float a, float b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }
1
2
3
4
5
6
7
8
9
10
11

我们为了实现不同数据类型的add方法,就需要给每种类型都写一个重载方法,这显然不符合我们开发的需求.

如果我们在这里使用泛型的话,就不需要给每种数据类型都增加一个重载方法.

public <T extends Number> double add(T a, T b) {
        return a.doubleValue() + b.doubleValue() ;
    }
1
2
3

如果我们在这里使用泛型的话,就不需要给每种数据类型都增加一个重载方法.

再看下面的情形

#

step1

在上面代码我们定义了一个List类型的集合,先向里面加入了两个String类型的值,然后又加入了一个Integer类型的值,这在Java编译期间是允许的,因为List默认的类型是Object.在后面的循环中,因为之前加入的数据类型不一样,很可能出现ClassCastException异常.

从上我们可以大概总结到泛型有以下好处

  • 1,代码复用,增加代码拓展性
  • 2,消除强制类型转换,类型安全

# 泛型的使用

泛型可以用在类,接口,方法上,分别可以称为泛型类,泛型接口,泛型方法.

# 泛型类/泛型接口

泛型类和泛型接口的定义基本相同,就是引入一个类型变量T(其他大写字母也OK,一般约定俗成的有T,E,V,K,N等),并用<>括起来,放在类名/接口名的后面,泛型类和接口允许有多个类型变量

//泛型类(1)
public class GenericClass<T> {
    private T data;
}
//泛型类(2)
public class GenericClass<T,K> {
    private T data;
    private K result;
}

//泛型接口
public interface IGeneric<T> {
    T getData();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型,泛型方法可以在任意地方任意场景中使用,包括普通类和泛型类.

    //普通方法
    public T getData() {
        return data;
    }
    
    //泛型方法
    public <V> V handleData(V data) {
        return data;
    }
1
2
3
4
5
6
7
8
9
注:一定要注意的是,并不是方法中参数或者返回值包含了泛型的就是泛型方法,只有在调用的时候需要指明泛型的才是泛型方法.

##限定类型变量

有时候,我们需要对泛型的类型进行限定,比如说我们写一个泛型方法比较两个变量的大小,我们怎么确保传入的两个变量都一定有compareTo方法呢?这个时候我们就可以使用T extends Comparable对泛型的类型变量进行限制, step2

T表示应该绑定类型的子类型,Comparable表示绑定的类型,子类型和绑定类型可以试类,也可以是接口 这个时候如果我们试图传入一个没有实现接口Comparable的实例变量,将会发生变异错误 step3

# 泛型类的继承规则

注:1,泛型参数是继承关系的,泛型类之间没有继承关系
public class Animal {}

public class Cat extends Animal { }

public class Pet<T> { }
public class ExtendPet<T> { }

Pet<Animal> genericClass =new Pet<Cat>(); //错误

1
2
3
4
5
6
7
8
9
注:2,泛型类是可以继承其他泛型类的,比如List和ArrayList
public class Animal {}

public class Cat extends Animal { }

public class Pet<T> { }
public class ExtendPet<T> { }

Pet<Animal> genericClass =new ExtendPet<>(); //正确
1
2
3
4
5
6
7
8

# 通配符使用

# 数组的协变

在讲泛型通配符之前,我们先了解下数组,在Java中,数组是可以协变的,什么是协变,我们以下面的例子讲解下.

public class Animal { }

public class Dog extends Animal { }

public class JingBa extends Dog{ }

public class Cat extends Animal { }

public class GenericClass {
    public static void main(String[] args) {
       Animal[] animals = new Dog[5];
       animals[0] = new Dog();//可以
       animals[1] = new JingBa();//可以
       System.out.println(animals.getClass());
       System.out.println(animals[0].getClass());
       System.out.println(animals[1].getClass());
       //animals[2] = new Animal();//ArrayStoreException
       // animals[3] = new Cat();//ArrayStoreException
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//打印结果
----------------------------------------------------
class [Lcom.company.genneric.Dog;
class com.company.genneric.Dog
class com.company.genneric.JingBa
----------------------------------------------------
1
2
3
4
5
6

在上面的代码中,创建一个Dog数组并将他赋值给Animal数组的引用.像这种具有子父类关系的类,子类数组也是父类数组的情况就是数组协变 不过在使用数组协变,也有些事情要注意,就是有些问题在运行的时候才能发现.还是看上面的代码.尽管Dog[]可以"向上转型"为Animal[],但是数组中实际的类型还是Dog对象,我们在上面放入Dog或者Dog的子类JingBa的对象时都是可以的,但是在放入Animal或者Cat对象的时候会在运行的时候报ArrayStoreException异常. 泛型设计的目的之一就是将一些运行时候的错误暴露在编译期间,我们下面使用Java提供的泛型容器List,看下会发生什么.

    public static void main(String[] args) {
        ArrayList<Animal> list = new ArrayList<Dog>();
    }
1
2
3

step7 看到了,上面的代码根本无法编译,直接报错,当涉及到泛型的时候,尽管Dog是Animal的子类,ArrayList却不是ArrayList的子类,也就是说泛型不支持协变

# 通配符的使用

如果我们要实现类似上面数组的协变怎么办呢,这时候我们就用到了通配符.Java中泛型通配符分为3种,我们依次来讲.

# 上边界通配符

使用<? extends Parent>的就是上边界通配符,他指定了泛型类型的上边界,类型参数都是Parent的子类,通过这种通配符可以实现泛型的"向上转型".

    public static void main(String[] args) {
        List<? extends Animal> list = new ArrayList<Dog>();

//        list.add(new Dog());//编译错误,Compile Error:cant't add any type of object
//        list.add(new JingBa());//编译错误,Compile Error:cant't add any type of object
//        list.add(new Cat()));//编译错误,Compile Error:cant't add any type of object
//        list.add(new Object()));//编译错误,Compile Error:cant't add any type of object
        list.add(null);
        Animal a = list.get(0);
    }
1
2
3
4
5
6
7
8
9
10

在上面的代码中,list的类型是List<? extends Animal>,可以把list看成是一个类型的List,这个类型是可以继承Animal的,但是需要注意的是,这并不是说这个List就可以持有Animal的任意类型.通配符代表的是某种特定的类型,但是上面的list没有指定实际的类型,它可以是Animal的任何子类型,Animal是它的上边界.

既然我们不知道这个list是什么类型,那我们如果安全的添加一个对象呢?在上面的例子中我们也看到了,无论是添加Dog,JingBa,Cat还是Object对象,编译器都会报错,唯一能通过编译的就是null.所以如果我们写了向上转型<? extends Parent>的泛型那么我们的List将失去添加任务对象的能力,及时Object对象也不行.

另外如果我们获取返回Animal的方法,这是可以的,因为在这个list中,不管它实际的类型到底是什么,肯定可以转型成Animal的,所有向上转型返回数据是允许的.

  • 总结:主要用于安全地访问数据,可以访问Parent及其子类型,并且不能写入非null的数据

# 下边界通配符

使用<? super Child>的就是下边界通配符,他指定了泛型类型的下边界,类型参数都是Child的基类,通过这种通配符可以实现泛型的"向下转型".

        List<? super Dog> list = new ArrayList<>();
        list.add(new Dog());
        list.add(new JingBa());
        list.add(null);
//        list.add(new Cat());//编译错误,Compile Error:cant't add any type of object
//        list.add(new Object());//编译错误,Compile Error:cant't add any type of object
        Object object = list.get(0);

1
2
3
4
5
6
7
8

在上面的代码中,我们也不能确定list里的是什么类型,但是我们知道这个类型一定是Dog的基类(父类),因此我们向里面添加一个Dog对象或者Dog子类型的对象是安全的,这些对象都可以向上转型为Dog.我们在取出list里面的数据的时候,返回的一定是Dog的基类(父类),但到底是哪一个基类(父类)我们是不知道的,但在java中所有的类型都继承自Object,所有在list取出的数据,返回的一定是Object.

  • 总结:主要用于安全地写入数据,可以写入Child及其子类型

# 无边界通配符

<?>无边界通配符,没有任何限定.

        List<?> list = new ArrayList<>();
//        list.add(new Animal());//编译错误,Compile Error:cant't add any type of object
//        list.add(new Dog());//编译错误,Compile Error:cant't add any type of object
//        list.add(new Cat());//编译错误,Compile Error:cant't add any type of object
//        list.add(new Object());//编译错误,Compile Error:cant't add any type of object
        list.add(null);
        Object object = list.get(0);
1
2
3
4
5
6
7

List<?>表示持有某种特定类型的List,但是这种List并没有指定具体类型,这是不安全的,所以我们不能向里面添加除null以外的对象

# List<?>与List的区别?

List没有泛型参数,表明这个List持有元素的类型是Object,因此可以添加任何类型的对象,不过编译器会有警告信息.

# 泛型中的约束和局限性

  • 1,不能用基本数据类型实例化类型参数

step4

  • 2,运行时类型查询只适用于原始类型

step5

  • 3,泛型类的静态变量或者方法不能使用泛型类型
注:静态方法本身就是泛型方法除外

step6 不能在静态方法和变量中引用泛型类型变量,因为泛型是在对象创建的时候才知道是什么类型,而对象创建代码的执行顺序是static,构造方法,所以在对象初始化之前static的部分已经执行了.

  • 4,不能创建泛型数组

step8

# 虚拟机中泛型的实现-类型擦除

Java泛型是在Java1.5以后才出现的,在Java早期版本中并没有泛型概念,为了向下兼容,Java泛型只存在在编译期,在编译后,就会替换成原生类型,并在相应的地方插入强制转换类型的代码,因此对于运行期的Java语言来说,ArrayList和ArrayList就是一个类,这种编译后去除类型信息的方式就叫做类型擦除.

Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1==c2);
1
2
3

泛型参数会擦除到他的第一个边界,如果参数类型是单独的一个T,那么最终会擦除到Object,相当于所有使用T的地方都会被Object替换,对于虚拟机来说,最终保存的参数类型还是Object.之所以还可以取出来我们传入的参数类型,是因为编译器在编译生成字节码的过程中,插入了类型转换的代码.

#Java#泛型
上次更新: 2021/06/24, 11:50:15
最近更新
01
VuePress | 介绍
06-16
02
plugin-pwa
06-16
03
VuePress | SEO优化
06-15
更多文章>
Copyright © 2019-2021 oopanda | 皖ICP备19017961号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×