泛型知识点总结
更新2020年5月8日11:16:07 :加入PECS的内容
1. 泛型
在JDK5之前,想要表示可变类型,基本都会用上Object类,但是这样会出现类型转换失败的风险。
于是从JDK5开始,Java引入了泛型。泛型的本质是类型参数化,解决不确定具体对象类型的问题。将明确类型这项工作推迟到创建对象时再决定。
- 参数化类型:是指将类型当成参数的一种来传递。且泛型中的类型只能是引用类型不能是基本数据类型。.
- 泛型只存在于编译阶段,在编译结束后就不会有泛型概念了。
2. 泛型的基本用法
泛型可以广泛的运用于类,接口,方法当中。编译器通过尖括号<>内部的字母来解析泛型。这个字母可以随意取名,也可以设置多个泛型,中间用 ‘,’ 隔开。
一般我们规定:
- E表示Element,集合中的某个元素。
- T表示Type of object,表示某个类
- K表示key,V表示Value,表示键值对的元素。
2.1 泛型类
在类上面定义泛型,直到编程者使用这个类的时候,才定义泛型具体的类型。定义在类上的泛型可以在类的内部随意调用。最原始使用泛型的方法是用setter和getter来获取对象的实例。
public class GenericTest<T> {
private T target;
public void setTarget(T target) {
this.target = target;
}
public T getTarget() {
return target;
}
}
public class Test {
public static void main(String []){
// 直到要用到这个类的时候,才明确泛型的具体类型,这里用String
GenericTest<String> genericTest = new GenericTest<String>();
//genericTest.setTarget(new Integer(10)); // 由于已经明确类型,就不能再用别的类型了。
genericTest.setTarget(new String("generic"));
// 明确类型后可以调用String独有的getBytes()方法。
System.out.println(genericTest.getTarget().getBytes());
}
}
但是对于泛型的对象由于没有明确类型,只能调用object的方法。只有在外部明确类型之后,才能调用对象的所有方法。
2.2 泛型方法
有时候,泛型并不需要运用到整个类中,只有某个方法需要使用到泛型,这个时候就可以只把泛型设置在这个方法上。
要注意, 代表泛型的尖括号只能写在返回类型和其他修饰符的中间,不可调换位置。
public class Test {
public static <T>void func(T t){
System.out.println(t.toString());
}
public static void main(String[] args) {
func(new String("genericMethod")); // 和泛型类一样,在调用的那一刻才决定类型
func(555);
}
}
2.3 泛型可以随意取名
看如下这个例子。
public class GenericDemo<T> {
static <String, T, Ali> String get(String string, Ali ali){
return string;
}
public static void main(String[] args){
Integer a = 100;
Boolean b = false;
Integer c = get(a, b);
}
}
这段代码可以编译且可以正常执行,get方法中,String出现在<>中,就不再代表我们熟知的java.lang.String类,而仅仅代表一个参数名,而在main的调用中,String明显就是值Integer类型。
3. 泛型的继承
3.1 子类明确泛型的类型
子类可以直接写明父类泛型中具体的类型。这种情况下子类本身就不用再使用尖括号标明泛型了。这种时候,子类完全脱离泛型,就直接当成普通的类使用。
public class SonGeneric extends GenericTest<String> {
}
public class Test {
public static void main(String []){
// 不需要再用<>标明泛型
SonGeneric sonGeneric = new SonGeneric();
//sonGeneric.setTarget(new Integer(10)); // 由于已经明确类型,就不能再用别的类型了。
sonGeneric.setTarget(new String("sonGeneric"));
// 明确类型后可以调用String独有的getBytes()方法。
System.out.println(sonGeneric.getTarget().getBytes());
}
}
3.2 子类不明确泛型的类型
当子类无法明确泛型的时候,有如下几种情况:
-
子类不添加泛型,也不重新注明父类的泛型,这种情况下父类的泛型直接当object类型判断。
public class SonGeneric extends GenericTest { }
-
子类添加原有的泛型。首先这种情况是不能通过编译的。
然后子类在添加泛型的时候,可以给泛型重新命名,但是还是表明是同一个参数。// 重新取名为A,但是还是原来的泛型 public class SonGeneric1<A> extends GenericTest<A> { }
-
子类重新编写泛型列表,这种情况下如果有引用父类的泛型,则是同一个,不是则代表是新增的。
// C虽然是第三个,但是却代表了父类中的T public class SonGeneric2<A, B, C> extends GenericTest<C> { }
注意看提示中的类型是Boolean
4. 泛型通配符
先用表格和示例表明List,List和List<?>的区别。
public static void main(String[] args) {
// 泛型出现之前的集合定义方法
List a1 = new ArrayList();
a1.add(new Object());
a1.add(new Integer(10));
a1.add(new String("hello generic1"));
// 泛型定义为Object,可以添加各种类型的数据
List<Object> a2 = a1;
a2.add(new Object());
a2.add(new Integer(11));
a2.add(new String("hello generic2"));
// 泛型定义为Integer,只能添加Integer类型
List<Integer> a3 = a1;
a3.add(new Integer(12));
/*
// 由于已经确定是Integer类型,不能添加别的类型
a3.add(new Object());
a3.add(new String("hello generic3"));
*/
// 使用了通配符,不能添加元素,但是可以做其他的操作
List<?> a4 = a1;
/*
// 使用通配符的时候,不允许添加元素
a4.add(new Object());
a4.add(new Integer(14));
a4.add(new String("hello generic4"))
*/
a4.remove(1);
a4.clear();
}
用法 | 名称 | 可以被赋值的类型 | 添加元素 | 特点 |
List | 原始类型 泛型出现前的类型 | 所有List< E>且包括List<?> | 允许 | 没有规定类型,元素都是Object 使用强制类型转换容易ClassCastException |
List<?> | 通配符类型 | 所有List< E>且包括List<?> | 禁止 | 由于不能添加元素,一般用于方法的参数中 |
List< Object> | 实际类型参数为Object | 仅可以接受List和其本身类型 | 允许 | 相比List有被赋值的限制,安全性强。 |
// 可以被赋值的类型样例代码
List a = new ArrayList();
List<?> b = new ArrayList();
List<Object> c = new ArrayList();
List<Integer> d = new ArrayList();
List list;
list = a;
list = b;
list = c;
list = d;
List<?> list1;
list1 = a;
list1 = b;
list1 = c;
list1 = d;
// Integer类型同Object类型,只能赋值a和d
List<Object> list2;
list2 = a;
//list2 = b;
list2 = c;
//list2 = d;
4.1 无边界通配符
通配符的作用就是它的限制:不能写入数据和可以接受任何类型的赋值。我们一般将其放在方法的参数列表中。既能保证灵活性(各种各样的List都能赋值),又能保证安全性(不可插入元素)。
public void fun(List<?> list){
}
但是使用通配符还有个问题:List<?>不能使用get方法, 只有Object类型是个例外,但是获取Object类型的意义不大。
这个时候为了限定可以获取的类型,但是又不限定成一个而是多个类型,就要用到extends和super关键字了。用法不同于是类继承中的extends和方法中的super,但意义相近。
下面先列举三个类
public class A {
public void funA(){}
}
class B extends A {
public void funB() {}
}
class C extends B {
public void func() {}
}
4.2 上边界通配符<? extends E>
表明参数化的类型只能是这个类或这个类的子类。
public class ExtendsGeneric {
public static <T extends A>void fun1(T t){
t.funA();
}
public static void fun2(List<? extends A> list){
for (A a : list)
a.funA();
}
public static void main(String[] args) {
fun1(new A());
fun1(new C());
fun2(new ArrayList<A>());
fun2(new ArrayList<C>());
}
}
有定义和实例化的两种写法,一种是 T extends E,另一种是? extends E。
4.3 下边界通配符<? super E>
表明参数化的类型只能是这个类或这个类的父类。由于是父类的不确定性,所以只能调用Object的方法。
public class superGeneric {
public static void fun1(List<? super A> list){
for (Object o: list)
o.toString();
}
public static void main(String[] args) {
fun1(new ArrayList<A>());
//fun1(new ArrayList<C>()); 报错,C不是A的父类
}
}
只有一种写法。
4.4 使用场景
泛型的通配符主要是遵循一个PECS原则(Producer extends,Consumer super)。
- 当你的泛型只读(比如只用list.get(),不用list.add())时,使用extends。
- 当你的泛型只写(比如只用list.add(),不用list.get())时,使用super。
- 当你的泛型只读且只需要使用Object类的方法时,使用无边界通配符。
- 如果你需要泛型既能读又能写的话,不要用通配符,使用普通的泛型。
5. 泛型擦除
之前说泛型只存在于编译阶段,意思是说在Java文件编译生成Class文件后,泛型的概念就不复存在,class文件也不会带有任何泛型的信息。这个经过编译后取消泛型内容我们一般就称作泛型擦除。
参考材料
码出高效 Java开发手册 p87-90,p168-173
泛型就这么简单 - Java知识点大全 - SegmentFault 思否
https://segmentfault.com/a/1190000014120746
泛型通配符详解 - 竹马今安在 - 博客园
https://www.cnblogs.com/wxw7blog/p/7517343.html