我们都知道,使用集合保存对象引用时,都会被转换成Object类型,取出时需要进行类型转换。这时容易出现的问题就是,集合中存储了不同的类型,转换时发生 java.lang.ClassCastException异常。
泛型正是为解决这个问题而来的。
什么是泛型
泛型,就是参数化的类型。 就是类型由原来的具体的类型参数化,类似了方法中的变量参数。 这时,类型定义成参数形式(可以称为类型形参), 使用类型时传入具体的类型(类型实参)。
还是举个例子吧,说的自己都晕。
public class GenericTest {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("zhangsan");
list.add("lisi");
//list.add(100); // 1 提示编译错误
for (int i = 0; i < list.size(); i++) {
String name = list.get(i); // 2 这里不用再进行类型转换了
System.out.println("name:" + name);
}
}
}
上面的代码中,String就是List类型的实参。使用泛型后,集合中只能存储形参规定的类型,取出数据时,数据的类型也是形参的类型。
泛型是如何实现的
我们直接来看ArrayList的代码吧
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
transient Object[] elementData;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
.....
}
泛型其实只是在编译阶段有效的,看下面的代码:
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList.equals(classIntegerArrayList)){
Log.d("泛型测试","类型相同");
}
上面代码的输出是:泛型测试: 类型相同
这说明编译之后的程序是要去泛型化的。 在编译过程中,泛型检查正确后,会将泛型信息擦除,并且在对象进入和离开方法时添加类型检查和类型转换(类型检查和转换,是如何实现的呢)。
泛型的使用方式
包括3中使用方式:泛型类,泛型接口,泛型方法
泛型类
就是将泛型用于类的定义中,最典型的就是集合类,如List,Set 和 Map
泛型类的基本写法
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
}
class People<T>{
private T loveToEat;
public People(T loveToEat){
this.loveToEat = loveToEat;
}
public void setLoveToEat(T loveToEat) {
this.loveToEat = loveToEat;
}
public T getLoveToEat(){
return this.loveToEat;
}
}
public class test {
public static void main(String[] args) {
People<String> liming = new People<String>("字符串");
People<Integer> zhangsan = new People<Integer>(123);
System.out.println(liming.getLoveToEat().getClass().getName());
System.out.println(zhangsan.getLoveToEat().getClass().getName());
}
}
输出是:
java.lang.String
java.lang.Integer
泛型定义中的上限
//用extends指定上限,这里extends后面既可以跟class也可以跟interface
class Box<T extends Number>{
private T data;
public Box(T data){
this.data = data;
}
public T getData() {
return data;
}
}
public class test {
public static void main(String[] args) {
//Box<String> name = new Box<String>("zhangsan"); String不是继承自Number,报错
Box<Integer> age = new Box<Integer>(88);
Box<Number> number = new Box<Number>(123);
//Box<Object> obj = new Box<Object>(123); Object不是继承自Number,报错
}
}
需要注意的地方:
- 泛型的类型参数只能是类类型,不能是基本类型
- 泛型类使用时,也可以不传入实参的,这时就不再进行类型检查了,就像你用ArrayList,里面可以保存各种类型的对象。
- 对于一个确切的泛型类型,不能使用instanceof。if(ex_num instanceof Generic<Number>){ }
泛型接口
泛型接口的定义与泛型类基本相同
public interface Generator<T> {
public T next();
}
interface Generator<T> {
public T next();
}
/**
* 我们要实现一个泛型类,实现Generator泛型接口
* */
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
/**
* 我们要实现一个普通类,实现Generator泛型接口。这时必须为泛型接口传入实参,同时
* 实现几口函数时的返回值类型也必须是我们选定的实参类型
* */
class FoodGenerator implements Generator<String>{
@Override
public String next() {
return null;
}
}
泛型方法
参考 ,讲的非常好
在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。 我们上面泛型类中的方法,不是泛型方法。
只有满足了下面的定义格式的方法,才是泛型方法:
修饰符 <类型参数列表> 返回类型 方法名(形参列表) { 方法体 }
public static <T, S> int func(List<T> list, Map<Integer, S> map) { ... }
其中T和S是泛型类型参数
类型参数也是有作用域的:
- class A<T> { ... },这里的T的作用域是整个A。 但这里要注意,对于A中静态区域和静态方法,是不能使用这个类型T的
- public <T> func(...) { ... },这里T的作用域是整个func
- 也存在作用域覆盖,内部会覆盖外部
class A<T> {
// A已经是一个泛型类,其类型参数是T
public static <T> void func(T t) {
// 再在其中定义一个泛型方法,该方法的类型参数也是T
}
}
//当上述两个类型参数冲突时,在方法中,方法的T会覆盖类的T,即和普通变量的作用域一样,内部覆盖外部,外部的同名变量是不可见的。
//除非是一些特殊需求,一定要将局部类型参数和外部类型参数区分开来,避免发生不必要的错误,因此一般正确的定义方式是这样的:
class A<T> {
public static <S> void func(S s) {
}
}
泛型方法,可以指定类型参数的上限(不能指定下限? 我试过,确实不能)。但必须在类型参数声明的地方,在其他地方指定上限会报错。
<T extends X> void func(List<T> list){ ... }
上面说的都是如何定义泛型方法,那么如何调用泛型方法呢?
1. 标准格式是这样的: object.<String> func(...) 其中String就是我们指定的类型实参,这样所有用到类型参数的地方,都会被替换成String类型。
2. 除了想上面那样显示调用,也支持隐式调用,不指明泛型参数,编译器根据传入的实参类型自动推断类型参数,举个例子:
<T> void func(T t){ ... }
隐式调用, 编译器根据"name"的类型String,推断出类型参数是String
object.func("name")
3. 隐式调用会产生歧义
<T> void func(T t1, T t2){ ... }
//这里编译器无法推断出类型参数,应该是String还是Integer,编译器会报错
object.func("name", 123)
泛型方法与通配符
通配符在下面会看到,可以先看一下。
- 泛型方法可以完全替代通配符
void func(List<? extends A> list)
我可以用下面的泛型方法代替
<T extends A> void func(List<T> list)
- 使用通配符后,对象list是只读的, 但是泛型方法中list是可写的
- 如果只读,那就用通配符,可以提供安全性,防止误修改。 如果需要修改则使用泛型方法。
- 如果返回值、多个参数间有类型依赖关系,则使用泛型方法
需求是第一个参数list中的中的类型,是第二个参数t的类型或者其子类型
<T> void func(List<? extends T> list, T t)
- 当多个类型参数间有依赖关系时,可以进行归约
<T, E extends T> void func(List<T> l1, List<E> l2);
这里E在形参中只出现了一次,没有其他地方用到它,那么我们可以用通配符来进行简化
<T> void func(List<T> l1, List<? extends T> l2);
- 典型用法
public static <T> void Collections.copy(List<T> dest, List<? extends T> src) { ... }
从src拷贝到dest,那么dest最好是src的类型或者其父类,因为这样才能类型兼容,
并且src只是读取,没必要做修改,因此使用“?”还可以强制避免对src做不必要的修改,增加的安全性。
泛型中的通配符
看例子吧,简单粗暴!! 通配符用在使用泛型的过程中,不是泛型定义中。 通配符可以与上下限共同使用。 【注意:我们可以在泛型定义中使用上限,但是无法使用下限。】
class Box<T>{
private T data;
public Box(T data){
this.data = data;
}
public T getData() {
return data;
}
}
public class test {
//不使用通配符
public static void showData1(Box<Number> box){
System.out.println("data:" + box.getData());
}
//使用通配符,此处Box<?> box,?代表的是类型实参,Box<?>是所有Box<xxx>的父类
public static void showData2(Box<?> box){
System.out.println("data:" + box.getData());
}
//使用?实参,代表没有限制。但有时我们需要限制
//通配符上限,就是实参类型必须是继承自某个类,或者就是那个类本身
public static void showData3(Box<? extends Number> box){
System.out.println("data:" + box.getData());
}
//通配符下限,就是实参类型必须是某个类的父类(或父类的父类等),或者就是那个类本身
public static void showData4(Box<? super Integer> box){
System.out.println("data:" + box.getData());
}
public static void main(String[] args) {
Box<String> name = new Box<String>("zhangsan");
Box<Integer> age = new Box<Integer>(88);
Box<Number> number = new Box<Number>(123);
Box<Object> obj = new Box<Object>(123);
//可以看出来,Box<Number>和Box<Integer>是不兼容的,虽然Integer继承了Number
showData1(number);
//showData1(age); //The method getData(Box<Number>) in the t ype test is not applicable for the arguments (Box<Integer>)
//使用?通配符,这个3个都可以正常调用
showData2(number);
showData2(age);
showData2(name);
//使用通配符上限,这2个都可以正常调用
showData3(number);
showData3(age);
//showData3(name); 不可用,因为String不是继承自Number
//使用通配符下限,这3个都可以正常调用
showData4(number);
showData4(age);
//showData4(name); 不属于Integer继承树中的一员
showData4(obj);
}
}
需要注意的是,使用了?通配符后,对象变成只读的(如果尝试写操作,编译无法通过了):
class Box<T>{
private T data;
private String name;
public Box(T data){
this.data = data;
}
public T getData() {
return data;
}
}
public class test {
static void showInfo(Box<?> box){
//box是只读的,因为?代表的是什么是不确定的
//所以下面的修改操作会报错:Error:(33, 21) java: 不兼容的类型: int无法转换为capture#1, 共 ?
//box.setData(123);
System.out.println("" + box.getData());
}
public static void main(String[] args) {
Box<Integer> age = new Box<Integer>(88);
Box<Number> number = new Box<Number>(123);
showInfo(age);
}
}