通俗易懂讲泛型
由于博客园的 markdown 语法有点坑,格式如果阅读中遇到问题,可以异步本人语雀文档查看:https://www.yuque.com/docs/share/57b89afd-91d8-4e64-82a0-243c74304004?# 《泛型》
泛型是什么
传统编程大多数都是面向对象类型编程,比如方法参数传入一个指定的类型,这类代码比较难复用,通常新增一个类型时就得增或者改代码。当然除了面对特定的类型编程还有面向基类和接口编程的多态编程,这类的代码会通用一些,方法入参传入一个基类或者接口,那么该方法就能适用于基类的所有派生类和接口的实现者,新增派生类也无需更改代码,但是这样就增加了代码的耦合度,必须继承指定基类或者接口才行。泛型就更加通用了,泛型实现了参数化类型,这样我们编写的通用方法就可以适用于多种类型,而不是一个具体的接口或者类。
个人理解泛型的主要作用是为了复用组件代码,当然在泛型出现之前也是可以编写通用的组件代码的,但是这样有点不安全。
泛型出来之前,集合内存储的元素是 Object 类型的,Object 是所有类的基类,因此,可以往集合内部添加任意类型,如上例子就往 listOld 集合内添加了:字符串,整形,对象,这三种类型。这样会导致一种问题,我们无法得知集合内的元素究竟是什么类型的,只能知道他们都是 Object 的子类。这样在使用的时候就得进行强制类型转化。使用的不当的话很容易报 “ClassCastException” 异常。
java 5 泛型出来之前,集合的使用方法:
public class Main {
public static void main(String[] args) {
List listOld = new ArrayList();
listOld.add("string");
listOld.add(123);
listOld.add(new Main());
for (Object o : listOld) {
if (o instanceof String) {
String str = (String) o;
System.out.println("this is string type");
}
if (o instanceof Integer) {
Integer i = (Integer) o;
System.out.println("this is Integer type");
}
if (o instanceof Main) {
Main m = (Main) o;
System.out.println("this is Object");
}
}
}
}
输出:
this is string type
this is Integer type
this is Object
泛型出来之后,我们就可以为集合表明一个确定的类型,这样就可以往集合内添加该类型或者该类型的子类。如果添加的类型不正确,那么编译期就会报错。
通过在集合引入泛型,那么编译器就会在编译器进行类型校验,如果往一个指定了类型的集合内部添加了错误类型,编译器就会报错。
而在使用元素的时候,也会自动的将集合内的元素转化为指定类型,这种转化是安全的,因为编译器确保了只能往集合内添加指定类型的元素。
java 5 泛型出来之后,集合的使用方法:
public class Main {
public static void main(String[] args) {
List<String> listNew = new ArrayList<>();
listNew.add("string");
// listNew.add(123);// 编译报错
for (String s : listNew) {
System.out.println(s);
}
}
}
输出:
string
那么,泛型这一套是怎么实现的呢,这就很有意思,jdk 有个传统,就是向上兼容,也就是说每发行一个版本,老版本的代码必定能够在新版本的 jdk 上运行。因此,为了兼容 java 5 之前的集合使用方式,jdk 的研发人员,采用了 “泛型擦除”的方式进行设计。
泛型擦除
老实说,刚开始知道泛型擦除这个概念的时候个人觉得有点拉闸..由于没使用过 c++ 和 python(我承认我菜..目前还没有去学习其他编程语言的想法),因此便不知道其他语言是怎么实现泛型的。
那么,什么是泛型擦除呢,泛型擦除是一种面向编译期设计的方法。所有的我们平时见到的如:
List <String > list;
List <T> list;
List<?> list;
List<? extends Object> list;
List<? super ArrayList> list;
这些泛型语法,经过编译期,到运行期的时候,统统变成了:
List list
也就是指定的类型统统向上转型成了 Object,所以我说泛型擦除是一种面向编译期设计的方法。
那么知道了泛型是什么,理解了泛型前后的编程规范,并且知道了泛型的实现后,让咱们来了解了解泛型怎么使用吧。
泛型怎么使用
如下例子,很简单,在创建类的时候在类名右边使用 尖括号括起来,然后里面随便定义个字母即可,这个字母就代表你可传进来的类型。
public class GenericClass<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
@Override
public String toString() {
return "GenericClass{" +
"obj=" + obj +
'}';
}
public static void main(String[] args) {
GenericClass<String> demo1 = new GenericClass<>();
demo1.setObj("demo1");
String obj = demo1.getObj();
System.out.println(obj);
System.out.println(demo1);
GenericClass<Integer> demo2 = new GenericClass<>();
demo2.setObj(1);
Integer obj1 = demo2.getObj();
System.out.println(obj);
System.out.println(demo2);
}
}
输出:
demo1
GenericClass{obj=demo1}
demo1
GenericClass{obj=1}
通配符
泛型有个概念是通配符,泛型通配符有三种
<?>无界通配符,接受任意类型,等同于 <Object>
<? extends Object >:上界通配符,这里的 Object 可以是任意类,代表的意思是接受任意继承自 Object 的类型
<? super Object > :下界通配符,这里的 Object 可以是任意类,代表的意思是接受任意 Object 的父类
无界通配符
无界通配符 <?> 看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型,但是它仍旧是很有价值的,因为,实际上它是在声明:“我是想用 Java 的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。
public class Main {
public static void main(String[] args) {
Map map1 = new HashMap();
Map<String, ?> map2 = new HashMap<String, Main>();
Map<String, ?> map3 = new HashMap<String, String>();
Map<String, ?> map4 = new HashMap<Integer, String>();//编译失败
Map<?, ?> map5 = new HashMap<String, String>();
Map<?, ?> map6 = new HashMap<Integer, Integer>();
}
}
上界通配符
上界通配符很有意思,如下代码,在 1 处其实会产生编译报错,因为上界通配符不允许 set 和 add 值,为什么呢,上界通配符的意思是我允许存放所有父类的子类,但是泛型代表的是具体类型,这里的具体类型画重点,就像 2 和 3的用法,虽然采用的上界通配符,但是里面的类型是确定的。
如果你想使用不确定类型,那么直接采用多态特性,比如 4 那样即可。
public class Main {
public static void main(String[] args) {
List<? extends Father> sonList = new ArrayList<>(); // 1
// sonList.add(new Son()); 编译报错
sonList = Arrays.asList(new Son(), new Son()); // 2
for (Father father : sonList) {
System.out.println(father.getClass().getName());
}
List<? extends Father> daughterList = Arrays.asList(new Daughter(), new Daughter()); // 3
List<Father> list1 = new ArrayList<>(); // 4
list1.add(new Son());
}
}
上界通配符也有一个有意思的点,由于 add 方法的入参是一个泛型,如下图:
由于编译器无法得知这里需要 Father的儿子还是女儿,因此它不会接受任何类型的 Father。如果你先把 Son 向上转型为 Father,也没有关系——编译器仅仅会拒绝调用像 add() 这样参数列表中涉及通配符的方法。
但是!对于入参是 Object 类型的方法,比如 contains(Object o),编译器允许调用他们。
下界通配符
下界通配符也很有意思,上界通配符其实已经指定了具体的类型,在下面的代码就是 Father,所以这个 list 可以随意的 add 值。因为这里 Son 和 Daughter 都是 Father 的子类,所以允许 add,但是 get 的时候就无法拿到具体值了。
public class Main {
public static void main(String[] args) {
List<? super Father> list = new ArrayList<>();
list.add(new Son());
list.add(new Daughter());
for (Object o : list) {
}
}
}
泛型业界内有两句总结:
- 频繁往外读取内容的,适合用上界Extends。
- 经常往里插入的,适合用下界Super。
其他问题
在学习泛型的过程中,遇到一个很有意思的关于数组的点也记录一下。
如下例子(这个例子是 java 编程思想里面的):
在 1 和 2 处竟然会抛出异常!!我之前都不知道的,一直以为这样能塞值成功。
这是因为,虽然定义的时候将 Apple 数组向上转型成了 Fruit 数组,但是运行时的数组机制知道它处理的是 Apple[],因此会在向数组中放置异构类型时抛出异常。
class Fruit {
}
class Apple extends Fruit {
}
class Jonathan extends Apple {
}
class Orange extends Fruit {
}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
fruit[2] = new Orange(); // 1
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // 2
} catch (Exception e) {
System.out.println(e);
}
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // 3
} catch (Exception e) {
System.out.println(e);
}
}
}
文章为本人学习过程中的一些个人见解,漏洞是必不可少的,希望各位大佬多多指教,帮忙修复修复漏洞!