js css html

Java泛型

2022-09-06  本文已影响0人  AC编程

一、基础

1.1 什么是泛型

1.2 泛型的作用
1.3 泛型的优点
1.4 泛型的使用场景
1.5 泛型的设计原则

编译时没有出现警告,运行时就不会出现 ClassCastException 异常。

1.6 泛型的实现原理

Java 中的泛型,基本上都是在编译器这个层次来实现的。在生成的 Java 字节码里面,没有包含泛型中的类型信息。我们在使用泛型的时候添加的类型参数,将在编译时被擦除掉。这也是 Java 的泛型也被称作为“伪泛型”的原因。

1.6.1 类型擦除(Type Erasure)

测试代码:

public class GenericType {
    public static void main(String[] args) {  
      ArrayList<String> arrayString=new ArrayList<String>();
      ArrayList<Integer> arrayInteger=new ArrayList<Integer>();

      System.out.println(arrayString.getClass());
      System.out.println(arrayString.getClass()==arrayInteger.getClass());
    }  
}

输出:

class java.util.ArrayList

true

注意:是类型相同,而不是对象相同。

在这个示例中,我们定义了两个 ArrayList 数组:

数组一:ArrayList<String> 泛型类型,只可以存储字符串;
数组二:ArrayList<Integer> 泛型类型,只可以存储整型。
最后,我们通过 ArrayString 对象、和 ArrayInteger 对象的 GetClass 方法,获取它们的类信息并比较,发现结果为 true

这是为什么呢,明明我们定义了两种不同的类型呀?这是因为:
在编译期,所有的泛型信息都会被擦除,List<Integer> 和 List<String> 类型在编译后,都会变成 List 类型(原始类型)。

1.7 泛型的特性

Java 中的泛型,只在编译的时候有效。

1.8 泛型的规则限制
1.9 泛型常用术语
1
1.10 泛型的类型表示
2

二、为什么要使用泛型

1、在 JDK5 之前,集合中没有泛型,我们是通过继承来实现泛型的程序设计的,使用继承实现有两个弊端

2、在 JDK5 之后,集合中有了泛型,我们就可以使用泛型来实现了,泛型提供了编译时类型安全检测机制,该机制:

2.1 实例

电商系统中,有普通用户、商户用户两种类型。当用户获取信息详情时,系统要先将其中的敏感信息设置为“空”,再返回给用户。此时,我们需要写一个通用方法,将敏感字段设置为“空”。具体怎么实现呢?

我们可能想到的三种实现方式:继承、方法重载机制、泛型机制

2.1.1 第一种实现方式:继承

在 Java 中,所有的类都继承了 Object ,我们可以将 Object 作为传入参数,再使用反射去操作字段,设置 password 为空。

public class Client {
    public static void main(String[] args) {
        // 初始化
        ShopUser shopUser = new ShopUser(0L, "shopUser", "123456");
        ClientUser clientUser = new ClientUser(0L, "clientUser", "123456");

        // 输出原始信息
        System.out.println("过滤前:");
        System.out.println("          " + shopUser);
        System.out.println("          " + clientUser);

        // 执行过滤
        shopUser = (ShopUser) removeField(shopUser);
        clientUser = (ClientUser) removeField(clientUser);

        // 输出过滤后信息
        System.out.println("过滤后:");
        System.out.println("          " + shopUser);
        System.out.println("          " + clientUser);
    }

    public static Object removeField(Object obj) throws Exception {
        Set<String> fieldSet = new HashSet();
        fieldSet.add("password");

        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (fieldSet.contains(field.getName())) {
                field.setAccessible(true);
                field.set(obj, null);
                return obj;
            }
        }
        return obj;
    }
}

运行结果

过滤前:
      ShopUser{id=0, username='shopUser', password='123456'}
      ClientUser{id=0, username='clientUser', password='123456'}
过滤后:
      ShopUser{id=null, username='shopUser', password='null'}
      ClientUser{id=null, username='clientUser', password='null'}

通过继承实现,运行结果没问题,但要强制转换对象,代码不够简洁。

2.1.2 第二种实现方式:方法重载机制

Java 不是有方法重载机制吗,我们再来试试这个方法。

业务方法:

public static ShopUser removeField(ShopUser user) throws Exception {

     // 强转,并返回对象
     return (ShopUser) remove(user);
 }
 public static ClientUser removeField(ClientUser user) throws Exception {

     // 强转,并返回对象
     return (ClientUser) remove(user);
 }

核心方法:

// 把敏感字段设置为空
    public static Object remove(Object obj) throws Exception {
        // 需要过滤的敏感字段
        Set<String> fieldSet = new HashSet<String>();
        fieldSet.add("password");

        // 获取所有字段:然后获取这个类所有字段
        Field[] fields = obj.getClass().getDeclaredFields();

        // 敏感字段设置为空
        for (Field field : fields) {
            if (fieldSet.contains(field.getName())) {
                // 开放字段操作权限
                field.setAccessible(true);
                // 设置空
                field.set(obj, null);
            }
        }

        // 返回对象
        return obj;
    }
}

通过方法重载机制实现,重复方法会很多,我们每添加一个供应商用户,就要再写一个方法、修改源码,简直太繁琐了,还极容易出错。

2.1.3 第三种实现方式:泛型
public static <T> T removeField(T obj) throws Exception {
    Set<String> fieldSet = new HashSet();
    fieldSet.add("password");

    Field[] fields = obj.getClass().getDeclaredFields();
    for (Field field : fields) {
        if (fieldSet.contains(field.getName())) {
            field.setAccessible(true);
            field.set(obj, null);
            return obj;
        }
    }
    return obj;
}

通过泛型实现,我们就不用把参数的类型代码写死,并且可以在使用的时候,再去确定具体的类型;在调用方法的时候,也不用强转对象、写很多重复方法,代码更加简洁、安全。

三、哪些情况下需要使用泛型

当接口、类及方法中操作的引用数据类型不确定时,就可以用泛型来表示,这样能避免强转,将运行问题转移到编译期。

需要注意的是:

四、泛型类、泛型接口、泛型方法

4.1 泛型类
4.1.1 泛型类的定义

泛型类,是在实例化类时指明泛型的具体类型,是拥有泛型特性的类,本质还是一个 Java 类,可以被继承。泛型类是最常见的泛型使用方式,最常见的运用就是各种集合类和接口,例如List、ArrayList 等。

4.1.2 泛型类的格式

格式:public class 类名<数据类型,…> { }
示例:public class Generic<T>{ }

关于数据类型:

4.1.3 泛型类的使用
4.1.4 泛型类的示例代码
public class GenericsBox<T> {
    private T t;

    public void add(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}
4.1.5 泛型类的使用注意事项
4.2 泛型接口
4.2.1 泛型接口的定义

将泛型用在接口 interface 上,被称为泛型接口,泛型接口经常被用在各种类的生产器中。

4.2.2 泛型接口的格式

格式:修饰符 interface 接口名 <类型>{ }
示例:public interface ShangHaiJieKou<T> { }

泛型接口格式,类似于泛型类的格式;接口中的方法的格式,类似于泛型方法的格式。

以下有两个类,和一个接口,我们以接口类为例:

1. Genericimpl(接口实现类)

2. Generic_泛型接口<T>(interface接口类)

3. GenericDemo_泛型接口(main方法实现类)
4.2.3 泛型接口的使用
4.2.4 泛型接口的示例代码
public interface GenericsInterface<T> {

    public abstract void genericsInterface1(T element);

    public abstract <T> void genericsInterface2();
}
4.2.5 泛型接口的使用注意事项

实现泛型的类,必须传入泛型实参,然后在实现类中用该实参来替换 T 。

4.3 泛型方法
4.3.1 泛型方法的定义

泛型方法,是在调用方法时指明泛型的具体类型,既可用在类和接口上,还可用在方法上。

4.3.2 泛型方法的格式
4.3.3 泛型方法的使用
4.3.4 泛型方法的示例代码

打印各种类型的数组中的元素

public static <T> void printLog(T[] inputArray) {
    for (T element : inputArray) {
         LogUtil.e("打印数组中的元素", element + "");
    }
}
4.3.5 编程原则

泛型方法是为了调高代码的重用性和程序安全性,尽量设计泛型方法去解决问题,如果设计泛型方法,可以取代泛型整个类,就选择泛型方法。另外,泛型方法可以搞定的,就不建议使用泛型类了。

五、泛型类的继承

泛型类被继承的情况有两种:子类明确泛型类的类型参数变量、子类不明确泛型类的类型参数变量。

5.1 子类明确泛型类的类型参数变量

把泛型定义在接口上

public interface Inter<T> {
    public abstract void show(T t);
}

定义一个子类来实现接口

public class InterImpl implements Inter<String> {//子类明确泛型类的类型参数变量为String:
    @Override
    public void show(String s) {
        System.out.println(s);

    }
}

测试代码

public class mingquefanxingcanshu {
    public static void main(String[] args) {
        Inter<String> i = new InterImpl();
        i.show("hello java!");
    }
}
5.2 子类不明确泛型类的类型参数变量

把泛型定义在接口上

public interface Inter<T> {
    public abstract void show(T t);
}

定义一个子类来实现接口

public class InterImpl<T> implements Inter<T> {//实现类不明确泛型类的类型参数变量,实现类也要定义出<T>类型的
    @Override
    public void show(T s) {//子类方法也要继承类的数据类型T
        System.out.println(s);

    }
}

测试代码

public class mingquefanxingcanshu {
    public static void main(String[] args) {
        Inter<String> i = new InterImpl();
        i.show("hello java!");
    }
}
5.3 泛型类的继承注意事项

六 、泛型通配符

6.1 泛型通配符的基本概述

泛型通配符用 ? 表示,代表不确定的类型,是泛型的一个重要组成。

相关术语及范例:

6.2 为什么要用泛型通配符

泛型通配符的引入,是为了解决类型被限制死之后,不能动态根据实例来确定的缺点。先来看代码示例,我们现在有这样一个函数

public void test(List<Number> data) {

}

根据泛型规则,这个函数只能传进来 List<Number> 这一种类型, List<Object> 和 List<Integer> 是传不进来的。如果我们既要泛型,又想把这两个类型的子类、或者父类的泛型传进去,就需要使用到通配符泛型。

6.3 泛型通配符的优点

可以在保证运行时类型安全的基础上,提高参数类型的灵活性。

6.4 泛型通配符的作用

泛型本身不支持协变和逆变,通配符能解决协变和逆变的问题。

在 Java 中:

泛型 T 是确定的类型,而通配符更加灵活(不确定),更多用于扩充参数的范围。泛型 T就像是变量,将传来的一个具体的类型拿来使用,而通配符则是一种规定,规定你能传哪些参数,就像是一个特殊的实际类型参数。

6.5 泛型通配符的使用场景

注意:泛型通配符只能用于泛型类的使用(声明变量、方法的参数),不能用于泛型定义、New 泛型实例。

6.6 泛型通配符的三种形式
6.6.1 无边界通配符 <?>

无边界通配符的概述:

无边界通配符的作用:可以让泛型能够接受未知类型的数据。

6.6.2 固定上边界通配符 <? extends T>

固定上边界通配符的概述:

固定上边界通配符的作用:
当我们不希望 List<?> 是任何泛型 List 的父类,只希望它代表某一类泛型 List 的父类时,我们就使用固定上边界通配符的泛型,来接受指定类、以及其子类类型的数据。

6.6.3 固定下边界通配符 <? super T>

固定下边界通配符的概述:

固定下边界通配符的作用:
我们可以使用固定下边界通配符的泛型,来接受指定类及其父类类型的数据。需要重点注意的是,一个泛型可以单独指定类型通配符的上边界、或下边界,但不能同时指定上边界、下边界。

6.7 泛型通配符的使用

我们确定类型变量时,如果不能明确类型变量,就可以使用泛型通配符(泛型通配符代表不确定的类型)。反之,如果我们能明确地知道类型变量,就不需要使用泛型通配符。

6.7.1 代码示例1 :不需要使用泛型通配符

我们明确地知道 count() 方法用在哪儿,在编码时,可以直接指明这是一个 Integer 集合。

public static Integer count(List<Integer> list) {
    int total = 0;
    for (Integer number : list) {
        total += number;
    }
    list.add(total);
    return total;
}

在调用方法时,如果不传指定的数据进来,编译会报错:

public static void main(String[] args) {
    // 不传指定数据,编译报错
    List<String> strList = Arrays.asList("0", "1", "2");
    int totalNum = count(strList);
}

即便我们绕过了编译,程序也很可能无法正常运行:

public static void main(String[] args) {
    // 绕过了编译,运行报错
    List strList1 = Arrays.asList("0", "1", "2");
    int totalNum = count(strList1);
}

所以,如果你已经明确自己要做什么,清楚地知道类型变量,就没必要使用泛型通配符。

6.7.2 代码示例2 :使用泛型通配符,实现一些通用算法

但是,在一些通用方法中,什么类型的数据都能传进来,不能确认类型变量,这时候该怎么办呢?此时,我们就可以使用泛型通配符,在不用确认类型变量的情况下,来实现一些通用算法。例如:我们需要写一个通用方法,把传入的 List 集合输出到控制台。

public static void print(List<?> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

Integer 集合,可以运行:

public static void main(String[] args) {
    //Integer 集合,可以运行
    List<Integer> intList = Arrays.asList(0, 1, 2);
    print(intList);
}

String 集合,可以运行:

public static void main(String[] args) {
    //String 集合,可以运行
    List<String> strList = Arrays.asList("0", "1", "2");
    print(strList);
}

List<?> list 指不确定 List 集合装的是什么类型,有可能是 Integer、或 String、或其他。但这不用理会,我们只需要传一个 List 集合进来,这个方法就能正常运行了。这就是泛型通配符的作用所在。

我们还可以使用泛型通配符,来实现一些特殊算法。泛型通配符也适用于一些特殊算法,但可使用的范围不大。例如:用户分为普通用户、商家用户,但用户有一些特殊功能,其它角色都没有。这时候,又该怎么办呢?我们可以通过给泛型通配符设定边界,来限定类型变量的范围。

6.8 PECS原则

泛型通配符的用法有些复杂,为了更好的帮助我们理解、及正确使用 Java 泛型通配符。Joshua Bloch 在《Effective Java 》第 3 版中提出了 PECS 原则。PECS 的英文全称是 Producer Extends , Consumer Super ,字面意思是“读取时使用 extends ,写入时使用 super ”。

也就是说:

6.9 通配符与类型参数的区别

一般而言,通配符能干的事情都可以用类型参数替换。例如:

public void testWildCards(Collection<?> collection){}

可以用以下来取代:

public <T> void test(Collection<T> collection){}

值得注意的是,如果用泛型方法来取代通配符,上面代码中的 collection 是能进行写操作的,但要进行强制转换。

public <T> void test(Collection<T> collection){
    collection.add((T)new Integer(12));
    collection.add((T)"123");
}

需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。

public class Test2 <T,E extends T>{
    T value1;
    E value2;
}

如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。

public T test1(T t){
    return value1;
}
6.10 泛型通配符和泛型方法的使用选型

在实际应用场景中,有很多时候,我们可以使用泛型方法、来替代泛型通配符。使用通配符:

public static void test(List<?> list) {

}

使用泛型方法:

public <T> void test2(List<T> t) {

}

既然通配符和泛型方法都可用,具体该怎样选型呢?
通配符和泛型方法的选型参考:

七、Java 泛型中的 T,E,K,V

本质上都是通配符,只是一种编码约定俗成。我们可以将 T 换成 A-Z 之间的任意字母,不会影响程序的正常运行。但是,如果换成其他的字母代替 T ,可读性可能会差一些。通常情况下,T,E,K,V,?是这样约定的:

3

资料来源:
Java泛型基础最全详解,超级详细

为什么要使用泛型,什么时候使用泛型?

泛型的3种使用方式:泛型类、泛型接口、泛型方法

泛型通配符超详解,一文彻底搞懂

一线互联网面试真题,最全最新整理

上一篇下一篇

猜你喜欢

热点阅读