java泛型代码编写
泛型的由来
我们先看下面这段代码:
List list = new ArrayList();
list.add(24); //向集合中添加一个 Integer 类型的数据
list.add("Tom"); //向集合中添加一个 String 类型的数据
for(int i = 0 ; i < list.size() ; i++){
Object obj = list.get(i); //注意这里每个类型都是 Object
System.out.println(obj);
}
//如果我们遍历的时候就想得到自己想要的数据类型
for(int i = 0 ; i < list.size() ; i++){
String obj = (String) list.get(i); //在取 Integer 的时候会报类型转换错误
System.out.println(obj);
}
报错信息如下:
也就是 集合中第二个数据是 Integer,但是我们取出来的时候将其转换为 String 了,所以报错。
那么这个如何解决呢?
①、我们在遍历的时候,根据每个数据的类型判断,然后进行强转。
那么我们说这个集合只有两条数据,我们可以进行判断强转,如果数据有成千上万条呢,我们都通过这样判断强转肯定不可取
②、在往集合中加入数据的时候,我们就做好限制,比如这个集合只能添加 String 类型的;下一个集合只能添加 Integer 类型的,那么我们在取数据的时候,由于前面已经限制了该集合的数据类型,那么就很好强转了。
这第二种解决办法,也就是我们这篇文章讲的 泛型
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
什么是泛型
在讲解什么是泛型之前,我们先观察Java标准库提供的ArrayList
,它可以看作“可变长度”的数组,因为用起来比数组更方便。
实际上ArrayList
内部就是一个Object[]
数组,配合存储一个当前分配的长度,就可以充当“可变数组”:
public class ArrayList {
private Object[] array;
private int size;
public void add(Object e) {...}
public void remove(int index) {...}
public Object get(int index) {...}
}
如果用上述ArrayList
存储String
类型,会有这么几个缺点:
-
需要强制转型;
-
不方便,易出错。
例如,代码必须这么写:
ArrayList list = new ArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String first = (String) list.get(0);
很容易出现ClassCastException,因为容易“误转型”:
list.add(new Integer(123));
// ERROR: ClassCastException:
String second = (String) list.get(1);
要解决上述问题,我们可以为String
单独编写一种ArrayList
:
public class StringArrayList {
private String[] array;
private int size;
public void add(String e) {...}
public void remove(int index) {...}
public String get(int index) {...}
}
这样一来,存入的必须是String
,取出的也一定是String
,不需要强制转型,因为编译器会强制检查放入的类型:
StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译错误: 不允许放入非String类型:
list.add(new Integer(123));
问题暂时解决。
然而,新的问题是,如果要存储Integer
,还需要为Integer
单独编写一种ArrayList
:
public class IntegerArrayList {
private Integer[] array;
private int size;
public void add(Integer e) {...}
public void remove(int index) {...}
public Integer get(int index) {...}
}
实际上,还需要为其他所有class单独编写一种ArrayList
:
- LongArrayList
- DoubleArrayList
- PersonArrayList
- ...
这是不可能的,JDK的class就有上千个,而且它还不知道其他人编写的class。
为了解决新的问题,我们必须把ArrayList
变成一种模板:ArrayList
,代码如下:
public class ArrayList {
private T[] array;
private int size;
public void add(T e) {...}
public void remove(int index) {...}
public T get(int index) {...}
}
T
可以是任何class。这样一来,我们就实现了:编写一次模版,可以创建任意类型的ArrayList
:
// 创建可以存储String的ArrayList:
ArrayList strList = new ArrayList();
// 创建可以存储Float的ArrayList:
ArrayList floatList = new ArrayList();
// 创建可以存储Person的ArrayList:
ArrayList personList = new ArrayList();
因此,泛型就是定义一种模板,例如ArrayList
,然后在代码中为用到的类创建对应的ArrayList
:
ArrayList strList = new ArrayList();
由编译器针对类型作检查:
strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!
这样一来,既实现了编写一次,万能匹配,又通过编译器保证了类型安全:这就是泛型。
向上转型
在Java标准库中的ArrayList
实现了List
接口,它可以向上转型为List
:
public class ArrayList implements List {
...
}
List list = new ArrayList();
即类型ArrayList
可以向上转型为List
。
要特别注意:不能把ArrayList
向上转型为ArrayList
或List
。
这是为什么呢?假设ArrayList
可以向上转型为ArrayList
,观察一下代码:
// 创建ArrayList类型:
ArrayList integerList = new ArrayList();
// 添加一个Integer:
integerList.add(new Integer(123));
// “向上转型”为ArrayList:
ArrayList numberList = integerList;
// 添加一个Float,因为Float也是Number:
numberList.add(new Float(12.34));
// 从ArrayList获取索引为1的元素(即添加的Float):
Integer n = integerList.get(1); // ClassCastException!
我们把一个ArrayList
转型为ArrayList
类型后,这个ArrayList
就可以接受Float
类型,因为Float
是Number
的子类。但是,ArrayList
实际上和ArrayList
是同一个对象,也就是ArrayList
类型,它不可能接受Float
类型, 所以在获取Integer
的时候将产生ClassCastException
。
实际上,编译器为了避免这种错误,根本就不允许把ArrayList
转型为ArrayList
。
ArrayList和ArrayList两者完全没有继承关系。
泛型就是编写模板代码来适应任意类型;
泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;
注意泛型的继承关系:可以把ArrayList
向上转型为List
(T
不能变!),但不能把ArrayList
向上转型为ArrayList
(T
不能变成父类)。
泛型的基本用法
对于上面的问题我们只需要将上述代码的 List list = new ArrayList() 改为 List list = new ArrayList();
List list = new ArrayList();
//list.add(22); //向集合中添加一个 Integer 类型的数据时,编译器会报错
list.add("Bob"); //向集合中添加一个 String 类型的数据
list.add("Tom"); //向集合中添加一个 String 类型的数据
//如果我们遍历的时候就想得到自己想要的数据类型
for(int i = 0 ; i < list.size() ; i++){
String obj = list.get(i); //这里就不需要强转了,前面添加的是什么类型,这里获取的就是什么类型
System.out.println(obj);
}
泛型是在编译阶段有效
List list1 = new ArrayList();
List list2 = new ArrayList();
Class c1 = list1.getClass();
Class c2 = list2.getClass();
System.out.println(c1==c2); //true
上述代码,由于我们知道反射是在运行时阶段,c1==c2为 true,说明了编译之后的 class 文件中是不包含任意的泛型信息的。如果不信,我们可以看 class 文件的反编译信息
java.util.List list1 = new ArrayList();
java.util.List list2 = new ArrayList();
Class c1 = list1.getClass();
Class c2 = list2.getClass();
System.out.println(c1.equals(c2));
我们可以看到 反编译之后的 list1和 list2完全一样。
结论:Java 泛型只在编译阶段有效,即在编译过程中,程序会正确的检验泛型结果。而编译成功后,class 文件是不包含任何泛型信息的
泛型类和泛型方法
public class Box {
private T box;
public T getBox(T t){
this.box = t;
return t;
}
public void getType(){
System.out.println("T的实际类型为:"+box.getClass().getName());
}
public static void main(String[] args) {
Box box = new Box();
System.out.println(box.getBox(1));
box.getType();
System.out.println(box.getBox("Tom"));
box.getType();
}
}
输出结果为:
1
T的实际类型为:java.lang.Integer
Tom
T的实际类型为:java.lang.String
泛型通配符
在泛型中,我们可以用 ? 来代替任意类型
public List wildCard(List list){
return list;
}
public static void main(String[] args) {
GenericTest gt = new GenericTest();
//构造一个 Interger 类型的集合
List integer = new ArrayList();
integer.add(1);
System.out.println(gt.wildCard(integer));
//构造一个 String 类型的集合
List str = new ArrayList();
gt.wildCard(str);
//构造一个 Object 类型的集合
List obj = new ArrayList();
obj.add(1);
obj.add("a");
System.out.println(gt.wildCard(obj));
//构造一个 任意类型的 集合,这和 List 存放数据没啥区别
List list = new ArrayList();
gt.wildCard(list);
}
泛型的上限和下限
①、上限: 语法(? extends className),即只能为 className 或 className 的子类
//通配符的下限,只能是 Number 或 Number的子类
public List wildCard(List) firstType;
System.out.println(typeClass); // Integer
}
class Pair {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
class IntPair extends Pair {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}
因为Java引入了泛型,所以,只用Class
来标识类型已经不够了。实际上,Java的类型系统结构如下:
┌────┐
│Type│
└────┘
▲
│
┌────────────┬────────┴─────────┬───────────────┐
│ │ │ │
┌─────┐┌─────────────────┐┌────────────────┐┌────────────┐
│Class││ParameterizedType││GenericArrayType││WildcardType│
└─────┘└─────────────────┘└────────────────┘└────────────┘
Java的泛型是采用擦拭法实现的;
擦拭法决定了泛型:
- 不能是基本类型,例如:
int
; - 不能获取带泛型类型的
Class
,例如:Pair.class
; - 不能判断带泛型类型的类型,例如:
x instanceof Pair
; - 不能实例化
T
类型,例如:new T()
。
泛型方法要防止重复定义方法,例如:public boolean equals(T obj)
;
子类可以获取父类的泛型类型。
extends通配符
我们前面已经讲到了泛型的继承关系:Pair
不是Pair
的子类。
假设我们定义了Pair
:
public class Pair { ... }
然后,我们又针对Pair
类型写了一个静态方法,它接收的参数类型是Pair
:
public class PairHelper {
static int add(Pair p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
上述代码是可以正常编译的。使用的时候,我们传入:
int sum = PairHelper.add(new Pair(1, 2));
注意:传入的类型是Pair
,实际参数类型是(Integer, Integer)
。
既然实际参数是Integer
类型,试试传入Pair
:
public static void main(String[] args) {
Pair p = new Pair(123, 456);
int n = add(p);
System.out.println(n);
}
static int add(Pair p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
class Pair {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
直接运行,会得到一个编译错误:
incompatible types: Pair cannot be converted to Pair
原因很明显,因为Pair
不是Pair
的子类,因此,add(Pair)
不接受参数类型Pair
。
但是从add()
方法的代码可知,传入Pair
是完全符合内部代码的类型规范,因为语句:
Number first = p.getFirst();
Number last = p.getLast();
实际类型是Integer
,引用类型是Number
,没有问题。问题在于方法参数类型定死了只能传入Pair
。
有没有办法使得方法参数接受Pair
?办法是有的,这就是使用Pair
通配符既没有extends
,也没有super
,因此:
- 不允许调用
set(T)
方法并传入引用(null
除外); - 不允许调用
T get()
方法并获取T
引用(只能获取Object
引用)。
换句话说,既不能读,也不能写,那只能做一些null
判断:
static boolean isNull(Pair p) {
return p.getFirst() == null || p.getLast() == null;
}
大多数情况下,可以引入泛型参数消除
通配符:
static boolean isNull(Pair p) {
return p.getFirst() == null || p.getLast() == null;
}
通配符有一个独特的特点,就是:
Pair
是所有Pair
的超类:
public static void main(String[] args) {
Pair p = new Pair(123, 456);
Pair p2 = p; // 安全地向上转型
System.out.println(p2.getFirst() + ", " + p2.getLast());
}
class Pair {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
上述代码是可以正常编译运行的,因为Pair
是Pair
的子类,可以安全地向上转型。
使用类似很少使用,可以用
替换,同时它是所有
类型的超类。
泛型和反射
Java的部分反射API也是泛型。例如:Class
就是泛型:
// compile warning:
Class clazz = String.class;
String str = (String) clazz.newInstance();
// no warning:
Class clazz = String.class;
String str = clazz.newInstance();
调用Class
的getSuperclass()
方法返回的Class
类型是Class
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?