泛型类与泛型方法

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

回顾泛型类

我们来回顾一下泛型类是怎么出现的。

话说JDK1.5以前,还没引入泛型时ArrayList大概是这样的:

public class ArrayList {
    private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

它有这个最大的问题是:无法限制传入的元素类型、取出时又容易发生ClassCastException。最直观的做法是使用期望类型的元素数组,比如:

public class StringArrayList {
    // 因为这种ArrayList只存String,所以不需要用Object[]兼容所有类型,只要String[]即可
    private String[] array;
    private int size;
    public void add(String e) {...}
    public void remove(int index) {...}
    public String get(int index) {...}
}

但不可能为所有类型都编写专门的XxxArrayList,于是JDK就推出了泛型:抽取一种类模板,方法、变量都写好了,但变量的类型抽取成“形式类型参数”:

public class ArrayList<T> {
    private Object[] array; // 本质还是Object数组,只对add/get做约束。就好比垃圾桶本身并没有变化,只是加了检测器做投递约束
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

再配合编译器做约束:比如ArrayList<User>表示告诉编译器,帮我盯着点,我只存取User类型的元素。

简而言之,泛型类的出现就是为了“通用”,而且还能在编译期约束元素的类型。

“泛型类的方法”并不是泛型方法

以前使用Hibernate时,基本都会抽取BaseDao:

class BaseDao<T> {
    public T get(T t) {
        
    }
    
    public boolean save(T t) {
        
    }
}

方法里的T和类上的T是同一个,也正因为多个方法操作的POJO类型一致,所以才会被抽取到类上统一声明。

这个T具体是什么类型,或者说里面的get()、save()到底操作什么类型的POJO,取决于BaseDao到底被谁继承。比如:

class UserDao extends BaseDao<User> {
}

那么UserDao从BaseDao继承过来的get()、save()其实已经被约束为“只能操作User类型的元素”。

但要清楚,上面的get()、save()只是“泛型类的方法”,而不是所谓的“泛型方法”。

什么是泛型方法

泛型类上的<T>其实是一种“声明”,如果把类型参数T也看做一种特殊的变量,那么<T>就是变量声明(不是我们一般概念中的变量声明,一个作用于运行期,一个作用于编译期)。

由于泛型类上已经声明了T,所以类中的字段、方法都可以自由使用T。但是当T被“赋值”为某种类型后,就会在编译器的帮助下形成一种强制类型约束,此时这个通用的代码模板也就不再通用了。因而你会发现,对于编译器而言UserDao extends BaseDao<User>里的方法只能操作User,CarDao extends BaseDao<Car>里的方法只能操作Car。

这好吗?一般来说,这很好,因为一个Dao操作的肯定是同一张表同一个对象,限定为某个类型反而能避免出错。但大家想想,如果不是BaseDao,而是一个工具类呢?BaseUtils提供通用的操作,XxUtils extends BaseUtils<Xx>固然没问题,但如果XxUtils希望提供一个方法处理Yy怎么办?如果还想处理Zz呢?

问题的症结并不在于后期想要处理什么类型或者有多少种类型,而是T被过早确定了,从而早早地放弃了“可变性”。

因为对于泛型类的T来说,当UserDao继承BaseDao或者XxUtils继承BaseUtils时,T就被确定为User和Xx了,且已经拒绝了其他可能性,也就无法复用于其他类型。

那么,有没有办法延迟T的确定呢?

有,但泛型类的T已经没办法了,需要另辟蹊径,引入泛型方法。

和泛型类一样,泛型方法使用类型参数前也需要“声明”(<T>要放在返回值的前面):

public class DemoForGenericMethod {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3", "4", "5"));
        List<String> stringList = reverseList(list);
    }

    /**
     * 一个普通类,也可以定义泛型方法(比如静态泛型方法)
     * @param list
     * @param <T>
     * @return
     */
    public static <T> List<T> reverseList(List<T> list) {
        List<T> newList = new ArrayList<>();
        for (int i = list.size() - 1; i >= 0; i--) {
            newList.add(list.get(i));
        }
        return newList;
    }

}

泛型方法可以大致分为两种:静态泛型方法、普通泛型方法。上面定义静态泛型方法主要是因为main是static的,为了方便调用而已。

泛型方法中T的确定时机是使用方法时。

泛型方法和泛型类没有必然联系,你可以理解为这两个东西可以各自使用,也可以硬把它们凑在一块,不冲突:

class BaseDao<T> {
    // 泛型类的方法
    public T get(T t) {

    }

    /**
     * 泛型方法,无返回值,所以是void。<E>出现在返回值前,表示声明E变量
     * @param e
     * @param <E>
     */
    public <E> void methodWithoutReturn(E e) {
        
    }

    /**
     * 泛型方法,有返回值。入参和返回值都是V。注意,即使这个方法也用E,也和上面的E不是同一个
     * @param v
     * @param <V>
     * @return
     */
    public <V> V methodWithReturn(V v) {
        return v;
    }
}

泛型类与泛型方法的使用场景

简单来说,一个类拥有多种同类型方法时使用泛型类,一个方法处理多种类型时使用泛型方法。

比如,在做数据访问层的时候,对一种类型的实体有一系列统一的访问方法,此时采用泛型类会比较合适,而对于接口的统一结果封装则采用泛型方法比较合适,比如:

@Data
@NoArgsConstructor
public class Result<T> implements Serializable {

    private Integer code;
    private String message;
    private T data;

    private Result(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    private Result(Integer code, String message) {
        this.code = code;
        this.message = message;
        this.data = null;
    }

    /**
     * 带数据成功返回
     * 请注意,虽然泛型方法也用了T,但和Result<T>里的没关系
     * 这里之所以这么写,是因为实际开发时你们会见到很多这种“迷惑性”的写法,放出来作为“反例”,推荐最好使用其他符号,比如K
     *
     * @param data
     * @param <T>
     * @return
     */
    public static <T> Result<T> success(T data) {
        return new Result<>(ExceptionCodeEnum.SUCCESS.getCode(), ExceptionCodeEnum.SUCCESS.getDesc(), data);
    }
}

关于统一结果封装,请参考小册其他章节。

又比如封装工具类,也非常常用:

public class ConvertUtil {

    private ConvertUtil() {
    }

    /**
     * 将List转为Map
     *
     * @param list         原数据
     * @param keyExtractor Key的抽取规则
     * @param <K>          Key
     * @param <V>          Value
     * @return
     */
    public static <K, V> Map<K, V> listToMap(List<V> list, Function<V, K> keyExtractor) {
        if (list == null || list.isEmpty()) {
            return new HashMap<>();
        }
        Map<K, V> map = new HashMap<>(list.size());
        for (V element : list) {
            K key = keyExtractor.apply(element);
            if (key == null) {
                continue;
            }
            map.put(key, element);
        }
        return map;
    }
}

静态方法无法使用泛型类的类型参数,换言之,静态方法如果想要使用泛型,只能是静态泛型方法,此时类型参数是自己声明的。

一个要留心的骚操作

在后面我们会学习到自己基于Redis做一个分布式锁:

public interface RedisService {

    // 省略其他方法...

    /**
     * 从队列取出消息
     *
     * @param queue    自定义队列名称
     * @param timeout  最长阻塞等待时间
     * @param timeUnit 时间单位
     * @return
     */
    Object popQueue(String queue, long timeout, TimeUnit timeUnit);
    
    // 省略其他方法...
}

使用时:

但如果使用泛型方法:

public interface RedisService {

    // 省略其他方法...

    /**
     * 从队列取出消息
     *
     * @param queue    自定义队列名称
     * @param timeout  最长阻塞等待时间
     * @param timeUnit 时间单位
     * @return
     */
     <T> T popQueue(String queue, long timeout, TimeUnit timeUnit);
    
    // 省略其他方法...
}

就不用强制转换了。

但这并不保险,只是骗过了编译器而已。你会发现你用任意类型接收都是不会编译报错的:

个人不推荐这种写法。

补充:泛型数组

在《泛型概述(上)》的末尾我提到过,Java并不支持泛型数组,所以对于ArrayList,底层仍旧采用Object[]。但在日常开发中又确实可能遇到需要new T[]的场景。

public class Test {

    public static void main(String[] args) {
        Integer[] array = new Integer[]{2, 4, 5, 9, 7, 1, 1, 6};
        Integer[] copyArray = copy(array);
    }

    private static <E> E[] copy(E[] array) {
        // 创建同类型数组
        E[] arrayTemp = ...
        
        for (int i = 0; i < array.length; i++) {
            arrayTemp[i] = array[i];
        }
        return arrayTemp;
    }
    
}

和new T()一样,new T[]也是不合法的。

要想new T(),我们之前介绍过,要么方法传入Class<T>,要么通过GenericType获取,最终要通过反射创建T对象(总之要明确Class对象)。那么对于T[],如何获取数组的元素类型呢?

JDK另外提供了所谓的ComponentType指代数组的元素类型:

泛型机制对普通对象、容器对象都很好,唯独对数组苛刻了些,以至于我们回想泛型,基本都想不起数组和泛型有啥关系。这不,为了弥补对数组的亏欠,Java特别给数组搞了ComponentType,其他两个可没有哦。

好了,既然知道了数组元素的类型,那就可以创建对应类型的数组咯:

public class Test {

    public static void main(String[] args) {
        Integer[] array = new Integer[]{2, 4, 5, 9, 7, 1, 1, 6};
        Integer[] copyArray = copy(array);
    }

    private static <E> E[] copy(E[] array) {
        Class<?> componentType = array.getClass().getComponentType();
        
        // 创建同类型数组
        E[] arrayTemp = (E[]) Array.newInstance(componentType, array.length);
        
        for (int i = 0; i < array.length; i++) {
            arrayTemp[i] = array[i];
        }

        return arrayTemp;
    }
    
}

当然,实际开发中要想拷贝数组,有很多其他简单的方式:

public class Test {

    public static void main(String[] args) {
        Integer[] array = new Integer[]{2, 4, 5, 9, 7, 1, 1, 6};
        Integer[] copyArray = copy(array);
    }

    private static <E> E[] copy(E[] array) {
        return Arrays.copyOfRange(array, 0, array.length);
    }
    
}
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值