目录
1 什么是泛型?
泛型,即“参数化类型”。
百度百科:
泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
“泛型” 意味着编写的代码可以被不同类型的对象所重用。泛型的提出是为了编写重用性更好的代码。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
2 为什么引入泛型
在Java增加泛型类型之前,通用程序的设计就是利用继承实现的,例如,ArrayList类只维护一个Object引用的数组,Object为所有类基类。
public class BeforeGeneric {
/**
* 泛型之前的通用程序设计
*/
static class ArrayList{
private Object[] elements=new Object[0];
public Object get(int i){
return elements[i];
}
/**
* 这里的实现,只是为了演示,不具有任何参考价值
* */
public void add(Object o){
int length = elements.length;
Object[] newElements = new Object[length+1];
for(int i=0; i<length; i++){
newElements[i] = elements[i];
}
newElements[length] = o;
elements = newElements;
}
}
public static void main(String[] args) {
ArrayList stringValues = new ArrayList();
//可以向数组中添加任何类型的对象
stringValues.add(1);
//问题1——获取值时必须强制转换
String str = (String) stringValues.get(0);
//问题2——上述强制转型编译时不会出错,而运行时报异常java.lang.ClassCastException
System.out.println(str);
}
}
面临的问题
- 每次使用时都需要强制转换成想要的类型
- 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全
引入泛型
针对利用继承来实现通用程序设计所产生的问题,泛型提供了更好的解决方案:类型参数。例如,ArrayList类用一个类型参数来指出元素的类型。
ArrayList<String> stringValues=new ArrayList<String>();
这样的代码具有更好的可读性,我们一看就知道该集合用来保存String类型的对象,而不是仅仅依赖变量名称来暗示我们期望的类型。
public class GenericType {
public static void main(String[] args) {
ArrayList<String> stringValues=new ArrayList<String>();
stringValues.add("str");
stringValues.add(1); //编译错误
}
}
现在,如果我们向ArrayList<String>添加Integer类型的对象,将会出现编译错误。
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method add(int, String) in the type ArrayList<String> is not applicable for the arguments (int)
at generic.GenericType.main(GenericType.java:8)
编译器会自动帮我们检查,避免向集合中插入错误类型的对象,从而使得程序具有更好的安全性。
总之,泛型通过类型参数使得我们的程序具有更好的可读性和安全性。
3 泛型的使用
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
3.1 泛型类
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
}
例:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
使用:
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);
//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());
3.2 泛型接口
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
当实现泛型接口的类,未传入泛型实参时:
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
当实现泛型接口的类,传入泛型实参时:
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
3.3 泛型方法
泛型方法是指使用泛型的方法,如果它虽在的类是一个泛型类,那就很简单了,直接使用类声明的参数。
如果一个方法所在的类不是泛型类,或者他想要处理不同于泛型类声明类型的数据,那它就需要自己声明类型。
/*
传统的方法,会有unchecked ... raw type 的警告
*/
public Set union(Set s1, Set s2){
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
/*
泛型方法,介于方法修饰符和返回值之间的称作 类型参数列表<A,V,F,E....>(可以有多个)
类型参数列表 指定参数、返回值中泛型的参数类型范围,命名惯例与泛型相同。
*/
public <E> Set<E> union2(Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
4 泛型通配符
通配符:传入的类型有一个指定的范围,从而可以进行一些特定的操作
泛型中有三种通配符形式:
1.<?>无限制通配符
2.<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
3.<? super E> super 关键字声明了类型的下界,表示参数化类型可能是指定类型,或者是此类型的父类。
5 泛型原理
类型擦除
Java 中的泛型和 C++ 中的模板有一个很大的不同:
- C++ 中模板的实例化会为每一种类型都产生一套不同的代码,这就是所谓的代码膨胀。
- Java 中并不会产生这个问题。虚拟机中并没有泛型类型对象,所有的对象都是普通类。
在 Java 中,泛型是 Java 编译器的概念,用泛型编写的 Java 程序和普通的 Java 程序基本相同,只是多了一些参数化的类型同时少了一些类型转换。
当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做 类型擦除(type erasure)。
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(Strings.getClass()==integers.getClass());//true
上面代码输出结果并不是预期的false,而是true。其原因就是泛型的擦除。
6. 总结
总的来说C++的template会生成更大的二进制代码,但会执行的比较快,但大个的二进制代码可能会导致更多的I/O,所以也不一定完全是优势。
Java生成的代码只有一份,运行时会有一些type cast开销,但可以在运行时支持新类型,比如用ClassLoader动态加载进来的类。
参考资料
《Java编程思想》
https://blog.csdn.net/wangfengfan1/article/details/48266749
https://www.zhihu.com/question/33304378
https://www.cnblogs.com/coprince/p/8603492.html
https://blog.csdn.net/sunxianghuang/article/details/51982979
https://blog.csdn.net/laomumu1992/article/details/83148943