说到泛型,大家应该都很熟悉,最常用的就是List<T> list这种。如果不是对方法的封装处理,利用反射,泛型类直接写的话,其实没什么需要特别注意的,缺少泛型,编译器自然会提醒,加上也就行了。
好了,我们先引入一个简单的泛型。
/**
* 这个Integer可以写成int么?为什么?
*/
List<Integer> list = new ArrayList<>();
我们知道创建一个集合,都是要定义泛型的,当然也可以模糊定义,比如List<T> list等。那么这个Integer可以写成int么?这个当然是不能的,如下图,我们可以看到,泛型一般都是继承于Object,也就是说是一个包装类,int只是一个基本类型,是无法作为泛型的
好了,我们接着说,<T>和<?>这两种泛型的使用不少伙伴应该都遇到过,可能在某一段时间内都比较茫然,这俩哥们都是类型的代表,有啥子区别啊?
这个呢,说开了其实很简单,<T>(当然<E>也是一样,都是一个字母代替)这种标识,大家可以看到,<T>这个字母是固定的,一个字母也就是代替一个包装类,也就是说,这个泛型是一个类,比如说T表示的是Integer,那么只要使用List<T>或者其他包含T的使用,都是代表的是Integer。
<?>泛型是一个问号,其实表示的也很清楚,就是说不知道会传来什么类型,什么类型都可以表示。
我们知道,泛型的使用一般是方法的封装,利用反射等技术,抽取代码中的公共部分,这个时候一般才会使用<T>,<?>,可能有些伙伴还不是很明白,我们展开下。
Java泛型是J2 SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法
概念的解释,我不是很擅长, 直接copy一份,下面直接讲使用哈。
先创建几个类(可以跳过)
/**
* 动物(祖父类级别)
*/
@Data
public class Animal {
/**
* 判断条件,默认为false
*/
private Boolean flag = false ;
/**
* 名字
*/
private String name;
/**
* 重量
*/
private String weight;
}
/**
* 鸟类
*/
@Data
public class Bird extends Animal{
/**
* 飞多高
*/
private String flyHeight;
}
/**
* 鹰
*/
@Data
public class Eagle extends Bird{
/**
* 住在山上
*/
private String mountain;
/**
* 吃肉
*/
private String meat;
}
/**
* 鹦鹉
*/
@Data
public class Parrot extends Bird {
/**
* 住的笼子
*/
private String cage;
/**
* 吃米饭
*/
private String rice;
}
我们写个来一步步看哈,先来个简单的,就是传来一个对象Animal,根据规则判断,然后进行赋值name,不用泛型,抽取方法中的公共部分,就是传来一个Bird会处理,传来一个Parrot也会处理,写一个简单的方法setAnimalName(Animal animal),实现下逻辑。
/**
* 设置动物名称
* 无泛型,入参直接写父类名称,子类也可以传进来处理
* 不过要处理的类型最好是父类的,不然要特殊处理(用反射)
* @param animal
*/
private static void setAnimalName(Animal animal){
if(animal.getFlag()){
animal.setName("animal");
}
}
public static void main(String[] args) {
Eagle eagle = new Eagle();
eagle.setMeat("beef");
eagle.setName("jake");
Parrot parrot = new Parrot();
parrot.setFlag(true);
parrot.setCage("red-cage");
setAnimalName(eagle);
setAnimalName(parrot);
System.out.println(" eagle:"+ JSON.toJSONString(eagle)+
"\n parrot:"+JSON.toJSONString(parrot));
}
可以看下打印输出,虽然没有用泛型,但是继承关系可以实现:
eagle:{"flag":false,"meat":"beef","name":"jake"}
parrot:{"cage":"red-cage","flag":true,"name":"animal"}
有些小伙伴总感觉这种应该要用泛型吧?为啥没用呢?当然,用也是可以的,我们来看下。
/**
* 使用泛型
* @param t
*/
private static <T extends Animal> void setAnimalName(T t){
if(t.getFlag()){
t.setName("animal");
}
}
是不是感觉有点意思的样子?结果是一样的,就不贴出来了。
下面延伸一下,假如我没有继承关系,我只知道传来的对象里有一个字段name和一个字段flag,这个时候,当然只能使用泛型了,就是使用简单的反射和递归,来看下。
/**
* 使用泛型
* @param t
*/
private static <T> void setAnimalName(T t){
Class<?> temp = t.getClass();
try {
//flag 可能在父类中,使用clzz = clzz.getSuperclass() 递归
Method method = getMethod(temp,"getFlag");
Object invoke = method.invoke(t);
if(invoke instanceof Boolean && (Boolean) invoke){
Method setNameMethod = getMethod(temp,"setName",String.class);
//设置名称
setNameMethod.invoke(t, "animal");
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 递归获取方法
* @param temp
* @param methodName
* @return
*/
private static Method getMethod(Class<?> temp,String methodName,Class<?>... parameterTypes){
Method method = null ;
while (method == null && !temp.getName().equalsIgnoreCase("java.lang.Object")){
try {
method = temp.getDeclaredMethod(methodName,parameterTypes);
} catch (NoSuchMethodException e) {
//源码中抛出了异常,所以还要捕获处理掉
}
temp = temp.getSuperclass();
}
return method;
}
}
常用的泛型处理就说到这,下面我们来看下PECS,简单的说就是 生产者(Producer)使用extends,消费者(Consumer)使用super,代码看的有点懵的话可以先看下面的附图。
/**
* PECS理解
*/
static public void test(){
//继承Bird 的类是有多个的,这个类不是固定的,所以使用?,使用T会报错
List<? extends Bird> pList = new ArrayList<>();
pList = Arrays.asList(new Bird(),new Bird());
// pList.add(new Bird());//报错
// pList.add(new Eagle());//报错
// pList.add(new Parrot());//报错
// pList.add(new Animal()); //报错
Bird bird = pList.get(0);
System.out.println("sList的大小:"+pList.size() +"\n bird"+bird);
List<? super Bird> sList = new ArrayList<>();
sList.add(new Bird());
sList.add(new Parrot());
sList.add(new Eagle());
// Object o = sList.get(0); //返回的是Object,没啥意义
System.out.println("sList的大小:"+sList.size());
}
上面就是PECS的操作代码,直接看的话有点晕,下面画了一张图,大家可以看看哈,不对的话可以指出。
PECS原则总结:
- 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
- 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
- 如果既要存又要取,那么就不要使用任何通配符。
接着来看一个经典的应用JDK 8 Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
大家可以看到,src是数据源头,赋值到dest,相当于src 生产(Producer)数据,dest消费(Customer)数据。dest重新赋值数据,数据类型是T或者T的子类,当然也是T父类的子类。而src进行去数据,获取的数据返回的类型都是T(其中可能有T的子类,不过向上转换了,也是T)。这段源码不是很难,但是其中的思想还是很经典的。大家可以看看哈。这个源码不是很繁琐,大伙应该都能看的懂,就不写注释了哈。
No sacrifice,no victory~