[06] 泛型
1、泛型
泛型也称之为参数化类型,就是在定义类、接口和方法时,不去指定具体的类型,而是把类型也当成一种参数,在运行时再去指定具体的类型。
Java中泛型的作用:
- 编译安全,避免非法类型的传入
- 对于如集合而言,省略了强制类型转换
- 可读性,从字面上就可以判断内容类型
2、泛型的声明
- 泛型的声明都包裹在尖括号 <> 中,多个泛型类声明用逗号隔开
- 泛型声明的类型参数只能代表引用数据类型,不能是基本数据类型(像int、double等)
- 泛型的命名通常用一个大写字母定义,没有特别的约束,但是有些约定俗成的规范(当然,你也可以不遵守)
- E:Element 常用在集合中作为元素,如List<E>
- K,V:Key,Value,代表Map的键值对
- N:Number,数字
- T:Type,类型,如String,Integer等
2.1 泛型类
所谓的泛型类就是具有一个或多个类型参数的类,泛型声明在该类的类名之后。如下例:
public class Pair <E1, E2> {
private E1 element1;
private E2 element2;
public E1 getElement1() {
return element1;
}
public void setElement1(E1 element1) {
this.element1 = element1;
}
public E2 getElement2() {
return element2;
}
public void setElement2(E2 element2) {
this.element2 = element2;
}
}
20
1
public class Pair <E1, E2> {
2
private E1 element1;
3
private E2 element2;
4
5
public E1 getElement1() {
6
return element1;
7
}
8
9
public void setElement1(E1 element1) {
10
this.element1 = element1;
11
}
12
13
public E2 getElement2() {
14
return element2;
15
}
16
17
public void setElement2(E2 element2) {
18
this.element2 = element2;
19
}
20
}
2.2 泛型接口
接口其实本身类似于一个很特殊的类,所以泛型接口的声明和泛型类的声明方式一致,泛型声明在接口的接口名之后。如下例:
public interface IPool <T> {
T get();
int add(T t);
}
4
1
public interface IPool <T> {
2
T get();
3
int add(T t);
4
}
2.3 泛型方法
泛型方法,即带有类型参数的方法。泛型声明在方法的返回参数之前。如下例:
public class ArrayAlg {
public static <T> T getMiddle(T[] a) {
return a[a.length / 2];
}
}
5
1
public class ArrayAlg {
2
public static <T> T getMiddle(T[] a) {
3
return a[a.length / 2];
4
}
5
}
再举个例子,像ArrayList中我们知道可以存入任意Object,如果定义了泛型取出时则不必进行强制转换,其get调用了名为elementData的方法:
E elementData(int index) {
return (E) elementData[index];
}
3
1
E elementData(int index) {
2
return (E) elementData[index];
3
}
2.4 类型擦除
我们知道,Java有Java编译器和Java虚拟机,编译器将Java源代码转换为.class文件,虚拟机加载并运行.class文件。
对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,将类型参数T擦除,插入必要的强制类型转换。Java虚拟机实际执行的时候,它是不知道泛型这回事的,它只知道普通的类及代码。
如果限定了类型,对应的泛型参数会替换为限定的类,如果没有限定类型,则替换为Object。
3、泛型参数的限定
我们知道,泛型的引入实际上和Object有些类似,允许你放入任何的引用数据类型,但是也正因为它无法确定一个类型范围,所以你无法通过这个定义的泛型参数来调用方法,如下例中有Animal类,在某个泛型方法中期望传入为Animal类时执行其eat方法是编译无法通过的:
public abstract class Animal {
public abstract void eat();
}
//在其他类中定义execute方法,期望当传入参数为Animal类时可以执行eat方法
public static <E> void execute(E ele) {
ele.eat(); //编译错误
}
10
1
public abstract class Animal {
2
3
public abstract void eat();
4
5
}
6
7
//在其他类中定义execute方法,期望当传入参数为Animal类时可以执行eat方法
8
public static <E> void execute(E ele) {
9
ele.eat(); //编译错误
10
}
但是如果我们对泛型参数做一个限定,规定它只要是Animal的子类就可以:
public static <E extends Animal> void execute(E ele) {
ele.eat();
}
3
1
public static <E extends Animal> void execute(E ele) {
2
ele.eat();
3
}
这就是泛型类型的限定。
3.1 泛型参数的上界
public class NumberGenericPool< T extends Number>
1
1
public class NumberGenericPool< T extends Number>
- 上述方式的声明规定了NumberGenericPool类所能处理的参数类型和Number有继承关系
- extends关键字声明的上界既可以是一个类,也可以是一个接口
- 当泛型参数如此声明时,在实例化一个泛型类时,需要明确类型必须为上界类型或其子类
另外,需要注意的是,如果给泛型参数限定多个类型,则需要使用符号&:
<T extends Runnable & Serializable>
1
1
<T extends Runnable & Serializable>
同时要注意的是,限定多个类型时,class只能放在第一个且只能有一个(单继承),interface则放在之后。
3.2 泛型参数的下界
相对的,泛型参数的下界限定关键字为super,用以固定泛型参数的类型为某种类型或其父类
List<? super Fruit> flist = new ArrayList<Fruit>();
1
1
List<? super Fruit> flist = new ArrayList<Fruit>();
注意,super是不能使用多个限定的符号&的:<T super File & Runnable> // 错误
3.3 通配符?
?代表了一个未知的类型。
?和 T 的区别在于,两者都可以表示不确定的类型,但是T的话,在函数中可以进行操作,而?则不行:
- 类型参数“<T>”主要用于声明泛型类或泛型方法
- 无界通配符“<?>”主要用于使用泛型类或泛型方法
通配符正因为其不确定性,所以不能用来声明泛型,如下是错误的:
//Error Example
class Box<?> {
private ? item1;
private ? item2;
}
5
1
//Error Example
2
class Box<?> {
3
private ? item1;
4
private ? item2;
5
}
所以通配符是拿来使用定义好的泛型,再次注意,是使用,比如之前提到过的例子:
List<? super Fruit> flist = new ArrayList<Fruit>();
1
1
List<? super Fruit> flist = new ArrayList<Fruit>();
3.3.1 例子1
看个例子来理解这个所谓的通配符,参考《Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同》中 胖胖 的回答:
假如有泛型类的盘子:
class Plate<T> {
private T item;
private Plate(T t) {item = t;}
public void set(T t) {item = t;}
public T get() {return item;}
}
6
1
class Plate<T> {
2
private T item;
3
private Plate(T t) {item = t;}
4
public void set(T t) {item = t;}
5
public T get() {return item;}
6
}
现在我们要使用这个泛型类Plate,用它定义一个水果盘子,逻辑上认为水果盘子可以放苹果:
Plate<Fruit> p = new Plate<Apple>(new Apple());
1
1
Plate<Fruit> p = new Plate<Apple>(new Apple());
实际上编译无法通过,编译器认为苹果“IS-A”水果,但是装苹果的盘子“IS-NOT-A”装水果的盘子。为了解决这个问题,出现了:
- <? extends T> 上界通配符
- <? super T> 下界通配符
Plate<? extends Fruit> p = new Plate<Apple>(new Apple());
1
1
Plate<? extends Fruit> p = new Plate<Apple>(new Apple());
这样就可以定义一个“啥水果都能放的盘子”,但同时要注意的是,这种方式get有效,却会让往盘子放东西的set方法失效。取出来的操作只能存放在Fruit或者Fruit的父类中;而set的时候并不能确定类型,我是会放入Apple,还是Orange呢,编译器并不知道,所以不让你通过编译。
所以通配符<?>和类型参数<T>的区别就在于,对编译器来说所有的T都代表同一种类型。但通配符<?>没有这种约束,Plate<?>单纯的就表示:盘子里放了一个东西,是什么我不知道。
同样,再回过头来理解:
// compile error
// List <? extends Fruit> appList2 = new ArrayList();
// appList2.add(new Fruit());
// appList2.add(new Apple());
// appList2.add(new RedApple());
List <? super Fruit> appList = new ArrayList();
appList.add(new Fruit());
appList.add(new Apple());
appList.add(new RedApple());
x
1
// compile error
2
// List <? extends Fruit> appList2 = new ArrayList();
3
// appList2.add(new Fruit());
4
// appList2.add(new Apple());
5
// appList2.add(new RedApple());
6
7
List <? super Fruit> appList = new ArrayList();
8
appList.add(new Fruit());
9
appList.add(new Apple());
10
appList.add(new RedApple());
可以看到,extends时集合的set操作编译错误;super时集合的set操作编译通过。
如果是<? extends Fruit>,那么定义了该List是某种水果,所以你取出来的肯定是水果(继承链向下走方法固定会拥有父类方法,所以元素可用),但是不允许你放,因为不确定放什么,是苹果?香蕉?所以这种List禁止插入,只能读取。
而<? super Fruit>则定义了该List的元素只要是水果的父类即可,却没有统一的根(除了Object),所以不能确定取出来时候元素是什么,所以元素你没法用(继承链向上走方法层出不穷无法确定),所以只能做插入,不能做读取。
相当于<? extends Fruit>确定了取出来的一定是水果,但是不能确定放进去的具体类型所以禁止插入;而<? super Fruit>不确定取出来的类型所以只能插入不能做读取。
3.3.2 例子2
最后,再看一个例子:Class类也是一个泛型类 class Class<T> implements java.io.Serializable ...
这意味着要求你在使用时候传入某个具体的类作为T进行替换,比如Class<Fruit>,但是我在使用时还是不确定类型,就可以Class<? extends Fruit>进行限定。
所以,?使用的前提是泛型T已经被定义,我们不确定传入某个参数,比如Class<?>,这个符号的前提是Class<T>已经被某处定义。在定义Class<T>的时候,我们不用?而用T,因为T要在Class的定义体里被用到,要是个名词,而?号是无法被代码使用的。
贴个代码示意下Class<?>的用法,在已经定义Class为泛型类的情况下,期望该类型参与运算,但是此时还是无法确定具体的类型,于是给一个限定范围:
@Transactional
protected static boolean hasValue(Class<? extends Entityable> entityClass, String property, Object value) {
//property只能由数字_字母组成
if(!propertyPattern.matcher(property).matches())
throw new IllegalArgumentException("property值只能由字母数字和下划线组成");
String hql = "select count(t) from " + entityClass.getName() + " t where t." + property + "=:value";
org.hibernate.Query query = sessionFactory.getCurrentSession().createQuery(hql);
query.setParameter("value", value);
return ((Number) query.uniqueResult()).longValue() > 0;
}
10
1
2
protected static boolean hasValue(Class<? extends Entityable> entityClass, String property, Object value) {
3
//property只能由数字_字母组成
4
if(!propertyPattern.matcher(property).matches())
5
throw new IllegalArgumentException("property值只能由字母数字和下划线组成");
6
String hql = "select count(t) from " + entityClass.getName() + " t where t." + property + "=:value";
7
org.hibernate.Query query = sessionFactory.getCurrentSession().createQuery(hql);
8
query.setParameter("value", value);
9
return ((Number) query.uniqueResult()).longValue() > 0;
10
}
4、其他
4.1 不能实例化泛型对象
T t= new T();//error
T.class.newInstance();//error
T.class;//error
3
1
T t= new T();//error
2
T.class.newInstance();//error
3
T.class;//error
解决办法是传入Class<T> t参数,调用t.newInstance():
public <T> void sayHi(Class<T> c){
T t = null;
try {
t = c.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Hi "+t);
}
9
1
public <T> void sayHi(Class<T> c){
2
T t = null;
3
try {
4
t = c.newInstance();
5
} catch (Exception e) {
6
e.printStackTrace();
7
}
8
System.out.println("Hi "+t);
9
}
4.2 不能在泛型类的静态域中使用泛型类型
public class Singleton<T>{
private static T singleton; //error
public static T getInstance(){} //error
public static void print(T t){} //error
}
5
1
public class Singleton<T>{
2
private static T singleton; //error
3
public static T getInstance(){} //error
4
public static void print(T t){} //error
5
}
但是,静态的泛型方法可以使用泛型类型:
public static <T> T getInstance(){return null;} //ok
public static <T> void print(T t){} //ok
1
public static <T> T getInstance(){return null;} //ok
2
public static <T> void print(T t){} //ok
可以这样理解:
- 泛型类中,<T>称为类型变量,实际上就相当于在类中隐形的定义了一个不可见的成员变量:“private T t;”,这是对象级别的,对于泛型类型变量来说是在对象初始化时才知道其具体类型的。而在静态域中,不需要对象初始化就可以调用,这是矛盾的;
- 静态的泛型方法,是在方法层面定义的,就是说在调用方法时,T所指的具体类型已经明确了。