作者简介:大家好,我是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
进群,大家一起学习,一起进步,一起对抗互联网寒冬