前言
泛型是jdk1.5之后出现的新特性,其本质是参数化类型(type parameters),通过参数化类型让代码可以应用于多种类型。
"泛型"这个概念产生的目的是希望类或方法能够具备最广泛的表达能力,怎么做呢,正是通过解耦类或方法与之所使用的类型之间的约束来达成的。
如何理解泛型呢?
让我们以方法的参数作为切入点开始这个话题:
//方法
private static void saySomeThing(String parameter){
System.out.println(parameter);
}
//使用
String par = "hello,world";
saySomeThing(par);
上述代码中,在方法定义阶段
saySomeThing方法中的parameter
称之为形参
,它没有任何具体的值。而在方法调用阶段
saySomeThing(par),方法中传入的参数par
叫做实参
,它有具体的值"hello,world"。
引申到泛型中,以ArrayList为例:
//ArrayList类定义阶段
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{……}
//使用阶段
ArrayList<String> list = new ArrayList<String>();
类定义阶段:
- ArrayList<E>称为
泛型类型
- ArrayList是
原始类型
- <>译为"
typeof
" - E称为
类型变量或者类型参数
(理解成形式类型参数
更简单些),它没有具体的类型,使用的时候类型作为参数传入
类使用阶段:
- ArrayList<String> 称为
参数化的类型
- String称为
实际类型参数
,此处实际类型参数为String类型
这样就对泛型有了初步了解,接下来将会了解泛型的好处和泛型的使用。
泛型的意义
泛型通过类型参数的方式指定元素的类型,这样就能把类型不匹配的错误在编译阶段暴露出来。比如ArrayList中add():Integer类型的数据,在get()方法中却把获取的元素赋值给String类型的变量,这就会导致程序运行的时候出现类型不匹配的错误。
泛型的使用
泛型的定义
public class Generic<T>{
private T arg1;
public Generic(){}
public Generic(T s){
arg1 = s;
}
public T getArg(){
return arg1;
}
}
类声明后<>中的这个T叫做类型参数,可以指代任意类型,这里可以用T也可以用A,T只是一个代表。不过因为JDK规范,类型参数用单个的大写字母来代替,通常如下,
- T:代表任意类型(S、U也可以)
- E:表示Element或是异常
- K:与V搭配使用,通常用于表示键值对中的key
- V:与K搭配使用,通常用于表示键值对中的value
泛型中可以拥有多个类型参数,在定义它的<>中对多个参数使用逗号隔开即可。注意,使用时泛型中的实际类型参数只能是引用类型
。
泛型类
一个泛型类(generic class) 就是具有一个或多个类型变量的类。
public static class Generic2<T> {
private T arg1;
public Generic2() {
}
public Generic2(T s) {
arg1 = s;
}
public T getArg() {
return arg1;
}
}
//使用
Generic2<String> g2 = new Generic2<String>("str");
System.out.println(g2.getArg().getClass().getTypeName());
Generic2<Integer> g3 = new Generic2<Integer>(1);
System.out.println(g3.getArg().getClass().getTypeName());
拥有多个类型参数的泛型类,在定义它的<>中对多个参数使用逗号隔开即可。
class Generic3<K, V> {
private K k;
private V v;
public Generic3() {}
public Generic3(K ka, V va) {
k = ka;
v = va;
}
private String get() {
return k.toString()+v.toString();
}
}
再来个内部类也是泛型类的例子。
class LinkedStack<T> {
private static class Node<U> {
U item;
Node<U> next;
public Node() {
item = null;next = null;
}
public Node(U item, Node<U> next) {
this.item = item;this.next = next;
}
}
private Node<T> top = new Node<T>();
public boolean end(){return top.item == null || top.next == null;}
public T pop(){
T result = null;
if (!end()){
result = top.item;
top = top.next;
}
return result;
}
public void push(T item){
top = new Node<T>(item,top);
}
}
测试:
public class Generic4Demo {
public static void main(String[] args) {
LinkedStack<String> stack = new LinkedStack<>();
for (String s : "hello world I am coming".split(" ")) {
stack.push(s);
}
String str;
while ((str = stack.pop()) != null) {
System.out.println(str);
}
}
}
结果:
coming
am
I
world
hello
上述实例中使用链表的方式实现了一个向下压栈的Stack,使用单链表数据结构实现。
泛型方法
泛型方法让该方法具备了能独立于类而产生变化的能力,将泛型参数列表放到返回值之前即可定义泛型方法。注意:static方法无法访问泛型类的类型参数。
//泛型类
class Generic3<T> {
private T t;
public Generic3() {
t = null;
}
public Generic3(T t) {
this.t = t;
}
//泛型类中的方法具备访问泛型类的类类型参数的能力
public T get() {
return t;
}
public void setT(T t) {
this.t = t;
}
//将泛型参数列表(<E>)放在返回值之前(定义泛型方法),让这个静态方法具备泛型的能力
public static <E> ArrayList<E> getArrayList() {
return new ArrayList<E>();
}
}
//test
public class Generic3Demo {
public static void main(String[] args) {
//泛型类中使用的方法具备访问泛型类的类型参数的能力
Generic3<String> g = new Generic3<String>();
g.setT("java");
System.out.println(g.get());
//普通类中的泛型方法的使用
new Generic3Demo().f(new Generic3<Integer>());
//具备泛型能力的静态方法
ArrayList<String> list = Generic3.getArrayList();
list.add("aaa");
System.out.println(list.get(0));
}
//普通类中的泛型方法
public <T> void f(T t) {
System.out.println(t.getClass().getName());
}
}
泛型限定
无边界通配符(Unbounded Wildcards):
声明格式:<?>
, 比如List<?>。
无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。?通配符定义的变量主要用作引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。比如集合中的add()方法,使用了?的集合就不能再调用这个方法。
public void printCollection(Collection<?> col){}
也就是说Collection<?> col = new ArrayList<String>();编译器让这个表达式被编译通过。
上边界通配符(Upper Bounded Wildcards):
声明格式:<? extends E>
使用上边界通配符的泛型,就能够接受指定类及其子类类型的数据。 这里的E就是该泛型的上边界,表示实现或者继承E的所有类都可以使用,此时E就是上边界。
下边界通配符(Lower Bounded Wildcards):
声明格式:<? super E>
使用下边界通配符的泛型,就能够接受指定类及其父类类型的数据。这里的E就是该泛型的下边界.
注意:
- 只能一个泛型指定上边界或下边界,不能同时指定上下边界。
- 定义泛型的时候也可以限定类型如:public <A extends Serializable&cloneable> void method(){}
- 普通方法、构造方法和静态方法中都可以定义泛型。编译器不允许创建类型变量的数组
- 可以用类型变量表示异常,称为参数化的异常,可以用于方法的throws列表,但是不能用于catch子句中。
泛型的擦除
java中的泛型是假泛型,在字节码阶段所有泛型的类型信息都会被擦除,比如ArrayList 和ArrayList的字节码都是ArrayList.class,字节码阶段并不存在ArrayList.class 和ArrayList.class。,如果强行操作,idea会提示错误。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8luC2L6d-1668923670663)(%E2%80%BB%20Res/generic_01.png)]
也正是由于泛型的擦除,所以我们可以通过反射实现List<String> list = new ArrayList<String>()
的对象list中存取Integer类型的对象。
public class GenericListReflectDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
try {
Method addMethod = List.class.getDeclaredMethod("add", Object.class);
addMethod.invoke(list,2);
Method getMethod = List.class.getDeclaredMethod("get", int.class);
System.out.println(getMethod.invoke(list,0));
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意
参数化类型不考虑类型参数的继承问题。也就是说泛型类型中的类型参数,等号两边只要不是同一种,那么编译就一定不会通过。
泛型的参数类型只能是引用类型。
创建数组实例时,数组的元素不能使用参数化的类型。因为,编译器是严格按照步骤走的。它检测每一行代码的信息是否有误,但是不会用执行时候的答案来检测代码的正确性。