泛型简介
Java 泛型(generics
)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制
,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型
,也就是说,所操作的数据类型被指定为一个参数
。在创建对象或调用方法的时候才明确具体的类型。
类型擦除
正确理解泛型概念的首要前提是理解类型擦除
。
更多关于类型擦除的问题,可以查看这篇文章:《Java泛型类型擦除以及类型擦除带来的问题》
什么是
类型擦除
?
Java的泛型是伪泛型
,这是因为Java在编译期间,所有的泛型信息都会被擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的。
- 使用泛型的时候加上类型参数,在编译器编译的时候参数会被去掉,这个过程称为
类型擦除
。
如在代码中定义List<String>
等类型,在编译后都会变成List
,由泛型附加的类型信息对JVM是看不到的JVM看到的只是List。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是Java的泛型与C++模板机制
实现方式之间的重要区别。
通配符
更多关于Java 泛型中的通配符可以查看这篇文章:《聊一聊-JAVA 泛型中的通配符 T,E,K,V,?》
常用的通配符为: T、E、K、V、?
?
表示不确定的 Java 类型T
表示具体的一个Java类型(Type)K
、V
分别代表 Java 键值中的 Key ValueE
代表Element
泛型的三种使用方式
泛型一般有三种使用方式:泛型类
、泛型接口
、泛型方法
。
泛型类
此处T
可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
在实例化泛型类时,必须指定T的具体类型。
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
泛型接口
public interface Generator<T> {
public T method();
}
- 实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
- 实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
泛型方法
public static <E> void printArray(E[] inputArray){
for(E element : inputArray){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray(intArray);
printArray(stringArray);
泛型的优点
使用泛型的好处就是:
-
代码更加简洁,不再需要强制类型转换
-
程序更加健壮,在编译期间没有警告,在运行期就不会出现
ClassCastException
异常 -
提高代码的复用性,比如:在动态代理中,通过泛型可以代理任何需要被代理的类
public class ServiceProxy<T> implements InvocationHandler {
private T target;
public ServiceProxy(T target) {
this.target = target;
}
}
泛型的应用
- 操作集合时
容器中之所以可以存储各种类型的对象,就是因为引入了泛型
List lists = new ArrayList<String>();
- 用于基础组件时
因为组件的要求是需要做到一定的通用性,需要支持不同的类型,而泛型的特点是:在创建对象或调用方法的时候才明确具体的类型。可以参考SpringData JPA
的JpaRepository
的写法:
public interface JpaRepository<T, ID> extends
PagingAndSortingRepository<T, ID>, QueryExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
<S extends T> List<S> saveAll(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
void deleteInBatch(Iterable<T> entities);
void deleteAllInBatch();
T getOne(ID id);
@Override
<S extends T> List<S> findAll(Example<S> example);
@Override
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
此外,在组件中,还离不开Java的反射机制
,一般是反射+泛型
再举个实际的例子
比如有个需求是将某些数据库表的某些字段进行聚合,SQL语句就是
select sum(column1),sum(column2) from table group by field1,field2
需要sum
和group by
的列是由业务方自己传入,而SQL表其实就是我们的POJO,传入的字段肯定也是POJO的属性,单个业务实际可以在参数上将POJO写死,但是如果参数设置为泛型,便可以提高代码的复用性。
拿到参数后,通过反射获取其字段具体的值,就可以做累加了。具体代码示例如下:
// 传入需要 group by 和 sum 的字段名
public cacheMap(List<String> groupByKeys, List<String> sumValues) {
this.groupByKeys = groupByKeys;
this.sumValues = sumValues;
}
private void excute(T e) {
// 从pojo 取出需要group by 的字段 list
List<Object> key = buildPrimaryKey(e);
// primaryMap 是存储结果的Map
T value = primaryMap.get(key);
// 如果从存储结果找到有相应记录
if (value != null) {
for (String elem : sumValues) {
// 反射获取对应的字段,做累加处理
Field field = getDeclaredField(elem, e);
if (field.get(e) instanceof Integer) {
field.set(value, (Integer) field.get(e) + (Integer) field.get(value));
} else if (field.get(e) instanceof Long) {
field.set(value, (Long) field.get(e) + (Long) field.get(value));
} else {
throw new RuntimeException("类型异常,请处理异常");
}
}
// 处理时间记录
Field field = getDeclaredField("updated", value);
if (null != field) {
field.set(value, DateTimeUtils.getCurrentTime());
}
} else {
// group by 字段 第一次进来
try {
primaryMap.put(key, Tclone(e));
createdMap.put(key, DateTimeUtils.getCurrentTime());
}catch (Exception ex) {
log.info("first put value error {}" , e);
}
}
}