基础知识
编译期和运行期
编译期:是指把源码交给编译器编译成计算机可执行文件的过程。
运行期:是指把编译后的文件交给计算机执行,直到程序结束。
在Java中就是把 .java文件编译成 .class文件,再把编译后的文件交给 JVM加载执行,如下图所示
泛型
泛型,即“参数化类型”,是JDK5.0出现的新特性,解决数据类型的安全性问题。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型类
类上定义泛型,作用于类的成员变量与函数,代码实例如下:
public class GenericClass<T>{
//成员变量
private T t;
public void function(T t){
}
public T functionTwo(T t){
//注意,这个不是泛型方法!!!
return t;
}
}
泛型接口
接口上定义泛型,作用于函数,代码实例如下:
public interface GenericInterface<T> {
public T get();
public void set(T t);
public T delete(T t);
default T defaultFunction(T t){
return t;
}
}
泛型方法
函数返回类型旁加上泛型,作用于函数,代码实例如下:
注意:
1)public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
3)表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
Java泛型中的标记符含义:
E - Element (在集合中使用,因为集合中存放的是元素)
T - Type(Java 类)
K - Key(键)
V - Value(值)
N - Number(数值类型)
? - 表示不确定的java类型
S、U、V - 2nd、3rd、4th types
public class GenericFunction {
public <T> void function(T t) {
}
public <T> T functionTwo(T t) {
return t;
}
public <T> String functionThree(T t) {
return "";
}
}
通配符
通配符是为了让Java泛型支持范围限定,这样使得泛型的灵活性提升,同时也让通用性设计有了更多的空间。
- <?>:无界通配符,即类型不确定,任意类型
- <? extends T>:上边界通配符,即?是继承自T的任意子类型,遵守只读不写
- <? super T>:下边界通配符,即?是T的任意父类型,遵守只写不读
相信大部分人,都是倒在通配符这块,这里多唠叨点,「通配符限定的范围是体现在确认“参数化类型”的时候,而不是“参数化类型”填充后」,可能这句话不太好理解,来看看下面的代码
/**
* 1.创建泛型为Number的List类,Integer、Double、Long等都是Number的子类
* new ArrayList<>() 等价于 new ArrayList<Number>()
*/
List<Number> numberList = new ArrayList<Number>();
/**
* 2.添加不同子类
*/
numberList.add(1);//添加Integer类型
numberList.add(0.5);//添加Double类型
numberList.add(10000L);//添加Long类型
/**
* 3.创建泛型为Number的List类,Integer、Double、Long等都是Number的子类
* 引用是泛型类别是Number,但具体实现指定的泛型是Integer
*/
List<Number> numberListTwo = new ArrayList<Integer>();//err 异常编译不通过
/**
* 4.创建泛型为Integer的List类,把该对象的引用地址指向泛型为Number的List
*/
List<Integer> integerList = new ArrayList<Integer>();
List<Number> numberListThree = integerList;//err 异常编译不通过
- 第一步:我们创建一个泛型为Number的List,编译器检查泛型类别是否一致,一致编译通过(确认参数化类型)
- 第二步:泛型Number已经填充完毕,调用add函数,此时add入参泛型T已经填充为Number,add可入参Number或其子类
- 第三步:我们又创建一个泛型为Number的List,编译器检查泛型类别是否一致,不一致编译失败,提示错误(确认参数化类型)
- 第四步:其实与第三步一样,只是做了一个间接的引用(确认参数化类型)
如果要解决上面的编译不通过问题,就需要使用通配符,代码如下:
/**
* 1.上边界通配符,Number与Number子类
*/
List<? extends Number> numberListFour = new ArrayList<Number>();
numberListFour = new ArrayList<Integer>();
numberListFour = new ArrayList<Double>();
numberListFour = new ArrayList<Long>();
/**
* 2.下边界通配符,Integer与Integer父类
*/
List<? super Integer> integerList = new ArrayList<Integer>();
integerList = new ArrayList<Number>();
integerList = new ArrayList<Object>();
/**
* 3. 无界通配符,类型不确定,任意类型
*/
List<?> list = new ArrayList<Integer>();
list = new ArrayList<Number>();
list = new ArrayList<Object>();
list = new ArrayList<String>();
最后再来说上边界通配符只读不写,下边界通配符只写不读到底是什么意思,用最简单的话来说:
- <? extends T>上边界通配符不作为函数入参,只作为函数返回类型,比如List<? extends T>的使用add函数会编译不通过,get函数则没问题
- <? super T>下边界通配符不作为函数返回类型,只作为函数入参,比如List<? super T>的add函数正常调用,get函数也没问题,但只会返回Object,所以意义不大
大家只需要记住上面的规则即可,如果想知道为什么这样设计,可以去了解下PECS (producer-extends,consumer-super)原则
最佳实践
无限通配符场景
使用泛型,类型参数不确定并且不关心实际的类型参数,就可以使用<?>,像下面的代码
/**
* 获取集合长度
*/
public static <T> int size(Collection<T> list){
return list.size();
}
/**
* 获取集合长度-2
*/
public static int sizeTwo(Collection<?> list){
return list.size();
}
/**
* 获取任意Set两个集合交集数量
*/
public static <T,T2> int beMixedSum(Set<T> s1,Set<T2> s2){
int i = 0;
for (T t : s1) {
if (s2.contains(t)) {
i++;
}
}
return i;
}
/**
* 获取任意两个Set集合交集数量-2
*/
public static int beMixedSumTwo(Set<?> s1,Set<?> s2){
int i = 0;
for (Object o : s1) {
if (s2.contains(o)) {
i++;
}
}
return i;
}
size与sizeTwo这两个函数都可以正常使用,但是站在设计的角度,sizeTwo会更合适,函数的目标是返回任意集合的长度,入参采用或<?>都可以接收,但是函数本身并不关心你是什么类型参数,仅仅只要返回长度即可,所以采用<?>。
beMixedSum与beMixedSumTwo这两个函数比较,道理同上面一样,beMixedSumTwo会更合适,函数的目标是返回两个任意Set集合的交集数量,beMixedSum函数虽然内部有使用到,但是意义不大,因为contains入参是Object,函数本身并不关心你是什么类型参数,所以采用<?>。
忘了补充另一个场景,就是原生态类型,上述代码使用原生态类型函数使用也没问题,但是强烈不推荐,因为使用原生态就丢失了泛型带来的安全性与描述性!!!
上下边界通配符场景
首先泛型是不变的,换句话说List