下载APP

Java泛型


theme: channing-cyan highlight: a11y-light

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情


1.为什么要有泛型

泛型可以理解为标签,比如药店里会在某一类药品处贴上标签方便寻找。

定义:把元素的类型设计成一个参数,这个类型参数叫做泛型

比如List<String>这表明该List只能保存字符串类型的对象

那么使用或不使用泛型有什么区别呢?看下面的代码

@Test
public void test(){
    ArrayList list = new ArrayList();
    //需求:存放学生的成绩
    list.add(78);
    list.add(60);
    list.add(30);
    list.add(99);
    //问题1:类型不安全
    list.add("Tom");

    for (Object score : list){
        //问题2:强转时,可能出现ClassCastException
        int stuScore = (Integer) score;
        System.out.println(stuScore);
    }
}

我只想在list里存放int类型的数据,但是它可以存放其他类型数据,而且编译运行都不报错,在强转时,非int类型数据就会报ClassCastException的错误,这就是不使用泛型的缺点。


2.在集合中使用泛型

  1. 集合接口或集合类在JDK5.0时都修改为带泛型的结构。
  2. 在实例化集合类时,可以指明具体的泛型类型
  3. 指明完后,在集合类或接口中凡是定义类或接口时,内部结构(方法,构造器,属性等)使用到类的泛型的位置,都指定为实例化时的泛型类型

比如:add(E e) --->实例化以后:add(Integer e)

  1. 泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置拿包装类替换
  2. 如果实例化时,没有指明泛型的类型,默认为Object类型

2.1 以ArrayList为例

ArrayList<Integer> list = new ArrayList<Integer>();

这样就可以在编译时,进行类型检查,保证数据的安全

上面的代码中问题2的解决更方便

for (Integer score : list){
    //避免了强转操作
    int stuScore =  score;
    System.out.println(stuScore);
}

2.2 以HashMap为例

@Test
public void test(){
    Map<String,Integer> map = new HashMap<String,Integer>();
    map.put("Tom",66);
    map.put("Jerry",56);
    map.put("Jo",76);
    //泛型的嵌套
    Set<Map.Entry<String, Integer>> entry = map.entrySet();
}

3.自定义泛型结构

自定义泛型结构有:泛型类、泛型接口;泛型方法

3.1 泛型类和接口

比如我现在定义一个泛型类叫Order,格式即下方代码所示

public class Order<T> {}

我可以在这个类里面定义一个T类型的属性,比如T orderT; 但这并不是说有一个类叫T,我们会在类的实例化时指定说明这个T到底是什么类型。就算到时候不指明,也会默认为Object类型。

比如Order<String> order1 = new Order<String>,就表示T是String类型,不打算指明就把两边的<String>去掉。

另外一个是泛型类子类的问题,比如我现在创建一个类叫SubOrder继承于Order,那么我可以在继承时就指明T类型。如下

public class SubOrder extends Order<Integer>{
}

注意:此时实例化子类SubOrder时不再需要写泛型,直接实例化。比如SubOrder sub = new SubOrder();那么肯定SubOrder里也只能加Integer类型

不过这里的SubOrder只能是一个普通的类,而不是泛型类。那么如何让子类也是泛型类呢?只要继承时父类继续不指明类型就行。

比如:

public class SubOrder<T> extends Order<T> {}

🎈下面是总结自定义泛型类和接口时需要注意的点:

  1. 泛型类可以有多个参数,多个参数都放在同一个<>中,例如<E1,E2,E3>
  2. 泛型类的构造器不带<>
  3. 实例化时记得带<>
  4. 泛型不同的引用不能相互赋值,比如
ArrayList<String> list1 = null;
ArrayList<Integer> list2 = null;

虽然都是ArrayList,但list1和list2不能再相互赋值,即list1 = list2;会编译报错

  1. 泛型要使用的话就一直用,要不用,就一直不用(针对同一程序中)
  2. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
  3. JDK1.7,泛型可以简化操作:Order<String> order1 = new Order<>();即自动类型推断
  4. 不可使用基本数据类型,要用时用包装类代替
  5. 异常类不可使用泛型,比如
public class SubOrder<T> extends Exception{
}
  1. 静态方法中不可使用类的泛型;因为泛型是创建类的对象时指定的,静态方法要早于类对象的创建。比如上面的自定义Order泛型类中,我声明如下的静态方法就会报错
public static void show(T orderT){
    System.out.println(orderT);
}
  1. 打算造一个T类型(T为暂时不指明的类型)的数组,不可以T[] arr = new T[10];这么写。要写只能T[] arr = (T[])new Object[10];这么写。
  2. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型
  1. 子类不保留父类的泛型:按需实现;
  • 没有类型,擦除
  • 具体类型
  1. 子类保留父类的泛型:泛型子类
  • 全部保留
  • 部分保留

3.2 泛型方法

在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。也就是说,泛型方法所属的类是不是泛型类都没关系

泛型方法可以是static静态的,因为泛型方法中的泛型和类中的泛型无关。

比如我在一个泛型类Order里加一个泛型方法

public <E> List<E> copyFromArrayToList(E[] arr){
    ArrayList<E> list = new ArrayList<>();
    for (E e :arr){
        list.add(e);
    }
    return list;
}

List< E >是返回值类型,泛型方法要在返回类型前加<>。下面简单地调用一下

@Test
public void test(){
    Order<String> order = new Order<>();
    Integer[] arr = new Integer[]{1,2,3,4};
    List<Integer> list = order.copyFromArrayToList(arr);
    System.out.println(list);
}

可以看到,泛型类Order中的泛型类型和泛型方法中的泛型类型一点关系都没有。


4.泛型在继承上的体现

类A是类B的父类,但G< A>和G< B>二者不具备子父类的关系,二者是并列关系。但他们有个共同的父类叫G< ?>,在下面通配符的时候就会讲述。

举个例子👇

Object obj = null;
String str = null;
obj = str;

我们可以看出String作为Object的子类,String类型的变量是可以赋值给Object类型的变量的,这就是继承体现。

但下面的代码就不行

List<Object> l1 = null;
List<String> l2 = null;
l1 = l2;

这属于并列关系,这两个类都在泛型里

补充:类A是类B的父类,A< G>是B< G>的父类


5.通配符的使用

通配符就是英文格式下的 "?" 问号

@Test
public void test1(){
    List<Object> list1 = null;
    List<String> list2 = null;
    
    List<?> list3 = null;
    
    list3 = list1;
    list3 = list2;
}

比如我现在要打印list1和list2,但我又不想造两次打印函数,因为<>里的类型不一样。此时我就可以用通配符去造一次函数,把list1和list2扔进去打印就行了。

public void print(List<?> list){
    Iterator<?> iterator = list.iterator();
    while (iterator.hasNext()){
        Object obj = iterator.next();
        System.out.println(obj);
    }
}

打印list1可以直接print(list1);就行了。

5.1通配符中数据的读取和写入要求

添加时,对于List< ?>就不能向其内部添加数据。

比如:

@Test
public void test1(){
    List<?> list = null;
    List<String> list1 = new ArrayList<>();
    list1.add("aa");
    list1.add("bb");
    list = list1;

    //当对list进行添加操作时就会报错
    list.add("cc");
}

当然有个例外,添加null就不回报错。

读取数据时可以进行正常get操作,读取的类型为Object Object o = list.get(0);


5.2有限制的通配符使用

比如我新建两个类,一个Person一个Student,其中Student类继承于Person类。

下面就是研究两个问题:即对于? extends Person? super Person的理解

public void test(){
    List<? extends Person> list1 = null;
    List<? super Person> list2 = null;
    
    List<Student> list3 = null;
    List<Person> list4 = null;
    List<Object> list5 = null;
    
    list1 = list3;
    list1 = list4;
    //list1 = list5;
    
    //list2 = list3;
    list2 = list4;
    list2 = list5;
}

上方代码中注释掉的就表示编译不通过

? 可以看做负无穷到正无穷

extends可以看做小于等于,Student类又是继承于Person的,所以list1 = list3; list1 = list4;的编译是通过的

super可以看做大于等于,所以Student的list3赋值给list2时编译不通过。

总结:

  • ? extends Person:可以作为G< A>和G< B>的父类,其中B是A的子类
  • ? super Person:可以作为G< A>和G< B>的父类,其中B是A的父类
在线举报