1.泛型接口定义和实现、泛型类定义和继承
package com.test.generic;
public interface IGeneric<E> {
E doSearch();
}
package com.test.generic;
public class MyGeneric<T> {
private T data;
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
package com.test.generic;
/**
* 继承泛型类、实现泛型接口时传入类型实参
*/
public class MyChildGeneric extends MyGeneric<String> implements IGeneric<Integer> {
@Override
public Integer doSearch() {
return 1;
}
public static void main(String[] args) {
MyChildGeneric m = new MyChildGeneric();
m.setData("sss");
System.out.println(m.getData());
System.out.println(m.doSearch());
}
}
2.类型通配符和类型通配符上限、下限
package com.test.generic;
public class TestGeneric {
public static void test(MyGeneric<?> a) {
System.out.println(a.getData());
}
/**
* 传入的类型对象必须是Number本身或子类,即上限是Number
*/
public static void test2(MyGeneric<? extends Number> a) {
System.out.println(a.getData());
}
/**
* 传入的类型对象必须是Integer本身或父类或Integer实现的接口,即下限是Integer
*/
public static void test3(MyGeneric<? super Integer> a) {
System.out.println(a.getData());
}
public static void main(String[] args) {
// 类型通配符
MyGeneric<Integer> a = new MyGeneric<>();
a.setData(123);
TestGeneric.test(a);
MyGeneric<String> b = new MyGeneric<>();
b.setData("success");
TestGeneric.test(b);
// 类型通配符上限
MyGeneric<String> aa = new MyGeneric<>();
aa.setData("123L");
TestGeneric.test2(aa); // String 不是Number子类,此处编译报错
MyGeneric<Long> bb = new MyGeneric<>();
bb.setData(123L);
TestGeneric.test(bb);
// 类型通配符下限
MyGeneric<Comparable> aaa = new MyGeneric<>(); //Integer是Comparable的实现类
aaa.setData("123L");
TestGeneric.test3(aaa);
}
}
3.类型形参上限、泛型方法定义
这里说明一下,类型形参没有下限
package com.test.generic;
public class MyGeneric2<T extends Number> {
public void test(T t) {
System.out.println(t.toString());
}
/**
* 无返回值泛型方法
*
* @param m
* @param <M>
*/
public <M> void method(M m) {
System.out.println(m.toString());
}
/**
* 带返回值泛型方法
*
* @param m
* @param <M>
* @return
*/
public <M> M method2(M m) {
System.out.println(m.toString());
return m;
}
/**
* 限定类型形参上限带返回值泛型方法
*
* @param m
* @param <M>
* @return
*/
public <M extends Number> M method3(M m) {
System.out.println(m.toString());
return m;
}
}
4.类型形参和类型通配符的区别
- 资料上是这么写的,通配符是用来支持灵活的子类化的,泛型方法中的类型形参是用来表示一个或多个参数之间或者参数和返回值之间的依赖关系的
下面是我个人的理解: - 类型形参可用于类、接口、方法参数,方法返回值、方法体等几乎所有地方的定义,强调的是定义,使用是其次
- 类型通配符则强调的是使用,只能用于方法参数,变量申明,用于表示一种通用的不确定的类型
- 大部分的类型通配符场景都是可以用类型形参去替代的,只有少量的比如类型通配符下限,这个类型形参不支持
package com.test.generic;
import java.util.List;
public class MyGeneric3<E> {
public boolean containsAll(List<?> list) {
return true;
}
/**
* 类型形参可替换类型通配符
*
* @param list
* @param <T>
* @return
*/
public <T> boolean containsAll2(List<T> list) {
return true;
}
/**
* 类型形参跟返回值有依赖关系,用类型形参
*
* @param list
* @param <T>
* @return
*/
public <T> T containsAll3(List<T> list) {
return list.get(0);
}
/**
* 类型形参跟返回值有依赖关系,用通配符就会丢失类型信息
*
* @param list
* @return
*/
public Object containsAll4(List<?> list) {
return list.get(0);
}
/**
* 类型通配符下限
* @param list
*/
public void containsAll5(List<? super Number> list) {
}
/**
* 类型形参不支持,编译报错
*/
public <T super Number> void containsAll6(List<T> list) {
}
}
5.泛型使用场景
最后再讲讲泛型的使用场景,就拿最近项目上的实例来讲吧。我们在做一个数据迁移的功能,把这个功能简单化,将5张实体表数据,从环境A迁移到环境B,需要支持是三种迁移模式,即初始化、增量覆盖、纯增量。以增量覆盖来说,增量覆盖就是已存在的数据做更新操作,不存在的数据做新增操作,那么可以抽象出以下行为:
- 任意一张实体表,定义源环境数据A,目标环境数据B,表的唯一标识UUID
- 从B中找出A中存在的数据,根据UUID过滤,删除B中的这部分数据
- 将A全量插入
以上就是增量覆盖需要执行的步骤,每一张表的迁移过程都需要执行这些步骤,不同的只是表结构不同,对应的就是bean对象不同,所以我们就可以用泛型类型形参去替代这个对象
还有个问题,假如每张表的唯一标识不都是UUID怎么办?这个可以通过传入一个属性名,然后通过内省去获取属性的值;这里要求一个表必须要有一个字段能唯一标识行记录,联合主键虽然也能唯一标识,但是会大大增加程序的复杂性
伪代码:
public <T> void overrideUpdate(List<T> a, List<T> b) {
// A中UUID
List<String> aUuids = a.stream().map(T::Uuid).collect(Collectors.toList());
// 从B中过滤出A中存在的数据
List<String> filterUuids = b.stream().filter(e -> {
rerurn aUuids.contains(e.getUuid());
}).map(T::Uuid).collect(Collectors.toList());
// 删除B中的这部分数据
deleteByUuids(filterUuids);
// 全量插入A
insertBatch(a);
}