抽象利器 - 泛型

泛型的基本概念

简单介绍一下 Java 的泛型机制,以及使用泛型的好处?

Java 的泛型是 JDK 5 之后引入的一个新特性,主要的作用就是:提供编译时类型安全监测的机制

泛型的本质是参数化类型,就是说被操作的数据类型可以被当做参数一样被指定。

泛型的好处:

①:增加代码的可读性

②:提高了安全性,将运行时可能抛出的异常,在编译期规避掉。

③:消除了强制转换,避免 ClassCastException

什么是泛型擦除?

Java 中的泛型是伪泛型, Java 在编译阶段所有的泛型信息都会被擦除掉,这就是通常所说的泛型擦除。

为什么会有泛型擦除?

泛型是在 JDK 5 之后出现的新特性,在 JDK 5 之前的类库都没有泛型的概念,所以为了兼容以前的类库,就采用了泛型擦除的方法。

泛型擦除的代价?

泛型不能被用于显示地引用运行时类型的操作之中,例如:转型, instanceof  操作和 New 表达式。因为所有参数的类型信息都丢失了。

泛型擦除代码示例:

----------------------------   例子1   ---------------------------------------

ArrayList strList = new ArrayList<>();
ArrayList IntegerList = new ArrayList<>();
// 正常来说这边应该是返回 false 的;但是返回的是 true
System.out.println(strList.getClass() == IntegerList.getClass());


------------------------------   例子2   -----------------------------------------------

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

    List list = new ArrayList<>();

    list.add(12);
    // 这里直接添加会报错
    // list.add("a");
    Class clazz = list.getClass();
    Method add = clazz.getDeclaredMethod("add", Object.class);
    // 但是通过反射添加,是可以的
    add.invoke(list, "str");

    // [12, str] 打印出来的 List 中还有 str 这说明 Java 本质上是没有泛型的
    System.out.println(list);

}

泛型中常用的通配符:

①:? 表示不确定的 java 类型

②:T (type) 表示具体的一个 java 类型

③:K V (key value) 分别代表 java 键值中的 Key Value

④:E (element) 代表 Element

泛型方法与静态方泛型

泛型方法

泛型方法:使用类上面声明的泛型

package com.aha.test;

public class GenericOne {

    // 当类上面没有声明 T 的时候 这边就没有办法使用 T 这个泛型
    public T getT (T t) {
        return t;
    }

}

package com.aha.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestMain {

    public static void main(String[] args) {
        GenericOne stringGenericOne = new GenericOne<>();
        log.info(stringGenericOne.getT("aha"));
    }

}

静态泛型方法

静态泛型方法是没有办法使用类上面声明的泛型的,为什么?

java 中泛型只是一个占位符,必须在传递类型后才能使用。
类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。

解决办法:
静态方法自己声明自己的泛型类型。

package com.aha.test;

public class GenericOne {

    public T getT (T t) {
        return t;
    }
        
    // static 这边不生命 E的话就会报错
    public static E getE (E e) {
        return e;
    }

}

----
    
package com.aha.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestMain {

    public static void main(String[] args) {
        GenericOne stringGenericOne = new GenericOne<>();
        log.info(stringGenericOne.getT("aha"));

        log.info("getE:{}", GenericOne.getE(123));
    }

}

泛型高阶用法实战

业务场景说明

先来看一段代码:

/**
 * 处理产品类别 商业区间 和 是否接入网关
 * 是否接入网关: 只有 ServicePreHandleOrderVo 涉及
 * 
 * @param oldDate 数据库直接返回的老数据,上面三个字段在数据库中存储的都是 key 值
 * @return 将 产品类别 商业区间 和 是否接入网关 根据 key 值映射成 value 返回的新数据
 */
public static List preServiceOrderHandleData (List oldDate,BusinessDataProperties properties) {

    Map productTypeMap = new HashMap<>();
    Map busiScopeMap = new HashMap<>();

    List productType = properties.getProductType();
    List busiScope =properties.getBusiScope();

    // 根据数据库中的 key 从 KeyValueMap 中取到 value 并返回处理好的数据
    productType.forEach(e -> productTypeMap.put(e.getKey(),e.getValue()));
    busiScope.forEach(e -> busiScopeMap.put(e.getKey(),e.getValue()));

    return oldDate.stream().peek(e -> {
        // 产品类型
        e.setProductTypeValue(productTypeMap.get(e.getProductType()));
        // 商业区间
        e.setBusiScopeValue(busiScopeMap.get(e.getBusiScope()));

        // 是否接入网关  1:是  0:否
        if ("1".equals(e.getAccessDirect())) {
            e.setAccessDirectValue("是");
        } else if ("0".equals(e.getAccessDirect())) {
            e.setAccessDirectValue("否");
        }

    }).collect(Collectors.toList());

}

需求便是:将传入的参数 List 其中的某些字段存储的是 key 要将这些 key 映射成对应的 value,然后返回出去。然后呢,每次要处理的数据类似,但是传入参数类型不同,即 List 中的 ServicePreHandleOrderVo 不同,共同点就是每个类型都是处理这三个字段,这边直接使用 T 来代替 ServicePreHandleOrderVo 是不行的,因为在代码中使用到了 T 的具体方法,例如:e.setProductTypeValue(productTypeMap.get(e.getProductType())); 编译器在编译器没有办法确定 T 的具体类型,所以不能直接使用 T 的方法,这边就想到了,将这些需要处理的类的需要处理的字段抽取出来,使用 ? extends class 的方式让编译器可以知道需要处理类型的父类是谁,我们在代码中就可以使用父类的方法,将需要处理的公共属性,抽取到父类中就完美解决咱们的问题了。

提取出来的类:

/**
 *  为了统一处理 产品类型 和 商业区间 抽象出来的实体类
 *  @author WT
 *  @date 2021-07-14
 */
@Data
public class HandleDataExcel {

    /**
     * 产品类型
     */
    @ExcelIgnore
    private String productType;

    @ExcelProperty("产品类型")
    @TableField(exist = false)
    @ColumnWidth(value = 20)
    private String productTypeValue;

    /**
     * 商业区间
     */
    @ExcelIgnore
    private String busiScope;

    @ExcelProperty("商业区间")
    @TableField(exist = false)
    @ColumnWidth(value = 20)
    private String busiScopeValue;

}

然后将方法进行修改:

/**
 * 处理产品类型 商业区间
 *
 * @param oldDate 数据库直接返回的老数据,上面三个字段在数据库中存储的都是 key 值
 * @return 将 产品类别 商业区间 和 是否接入网关 根据 key 值映射成 value 返回的新数据
 */
public static  List handleData (List oldDate,BusinessDataProperties properties) {

    Map productTypeMap = new HashMap<>();
    Map busiScopeMap = new HashMap<>();

    List productType = properties.getProductType();
    List busiScope =properties.getBusiScope();

    productType.forEach(e -> productTypeMap.put(e.getKey(),e.getValue()));
    busiScope.forEach(e -> busiScopeMap.put(e.getKey(),e.getValue()));

    return oldDate.stream().peek(e -> {
        // 产品类型
        e.setProductTypeValue(productTypeMap.get(e.getProductType()));
        // 商业区间
        e.setBusiScopeValue(busiScopeMap.get(e.getBusiScope()));

    }).collect(Collectors.toList());

}
注意: 这个是关键,让编译器可以知道 T 父类的类型, 这样就可以使用 T 父类中的方法。我们就可以在方法中使用 e.setProductTypeValue(productTypeMap.get(e.getProductType()));

泛型中 ? super T? extends T

在上面的示例中用到了 extends 这个关键字,当时呢我是类比类的继承关系想到了上面那种解决办法,在写 Lambda 表达式的时候呢经常使用到

Supplier supplier 供给型 Consumer action 消费型 ,让我对 ? super T 这种泛型有了兴趣。所以就了解了一下,这边整理一下。

表示包括 T 在内的任何 T 的父类, 表示包括 T 在内的任何 T 的子类。

讲到这里,就不得不说一下 PECS:(Producer extends Consumer super) 原则了,这个也很好理解,就是当我们要定一个生产者的接口,在定义存放生产者的容器的时候就应该使用 这种泛型,消费者同理。

这是为什么呢?

List 这种数据类型,我们能做的就只有从这个集合中取数据,我们取出的数据可以是 T 的任意一个子类类型或者是 T 类型,但是呢我们没有办法往这个集合中直接塞数据的,因为你不知道这个集合中到底放的是什么类型的数据。一般都是新建立一个集合,在集合中有数据之后,然后将 List 指向新建立的那个集合。所以说生产者用这个类型。可以参考下面这段代码:

package com.train.pojo;

import lombok.Data;
import java.util.List;

@Data
public class NumberExample  {

    private List producerList;

}

// =================

package com.train.pojo;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
public class TestGeneric {

    public static void main(String[] args) {

        // List producerList2 = new ArrayList<>();
        // 像这一句就是没有办法被执行的
        // producerList2.add(1);

        ArrayList integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(3);
        integerList.add(5);

        NumberExample numberNumberExample = new NumberExample<>();
        // 但是像这样赋值是被允许的
        numberNumberExample.setProducerList(integerList);

        // 或者直接这样赋值
        List producerList = integerList;
        log.info(producerList.toString());
        log.info(numberNumberExample.getProducerList().toString());
        integerList.add(9);

    }

}

List consumer 则是与上面的相对立的。

package com.train.pojo;

import lombok.Data;
import java.util.List;

@Data
public class ConsumerExample {

    private List consumerList;

}


// ===========

ConsumerExample consumer = new ConsumerExample<>();
List consumerList = consumer.getConsumerList();
// consumer 则是与 producer 对立的  这个就可以直接往里面塞东西
consumerList.add(1);
consumerList.add(0.3);
// 取的话就是不允许的  只能是 object 类型的
Object object = consumerList.get(1);

你可能感兴趣的:(java)