2. 泛型的使用
泛型有三种使用方式,分别为:泛型类、泛型接口和泛型方法。
2.1 泛型类
对于普通类
public class Box {
private String object;
public void set(String object) { this.object = object; }
public String get() { return object; }
}
在不使用泛型的情况下,普通类中的数据类型必须保持为类定义时的类型,例如,如果现在有另外一个Box类中要求object属性为Integer类型则必须重新定义Box类,代码不能得到复用,这和Java中的多态设计思想相悖。
通过使用泛型类,可以有效解决以上问题,泛型类的定义原则如下:
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()); //123456
Log.d("泛型测试","key is " + genericString.getKey()); //key_value
定义的泛型类,就一定要传入泛型类型实参么?
并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
Generic generic = new Generic("111111");
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
Log.d("泛型测试","key is " + generic.getKey()); //111111
Log.d("泛型测试","key is " + generic2.getKey()); //55.55
Log.d("泛型测试","key is " + generic3.getKey()); //"false"
使用泛型类的时需要注意继承的逻辑,这一点在通配符部分将会详细讲解
2.2 泛型接口
泛型接口与泛型类的定义及使用基本相同。
具体泛型接口
//定义泛型接口
public interface Generator<T> {
public T next();
}
未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中,即:class FruitGenerator
如果不声明泛型,如:class FruitGenerator implements Generator
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
当实现泛型接口的类,传入泛型实参时定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator
在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
即:Generator
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)];
}
}
2.3 通配符
Ingeter是Number的一个子类,Generic
那么问题来了,在使用Generic
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
showKeyValue(gNumber);
// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer>
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);
通过报错信息我们可以看到Generic
也就是说,Java中的泛型不是协变的
在 Java 语言中,数组是协变的,也就是说,如果 Integer 扩展了 Number,那么不仅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以传递或者赋予 Integer[]。(更正式地说,如果 Number是 Integer 的超类型,那么 Number[] 也是 Integer[]的超类型)。
但是在泛型类型中 List< Number> 不是 List< Integer> 的超类型,也就是说在需要 List< Number> 的地方不能传递 List< Integer>。如果不这么做,将破坏要提供的类型安全泛型。泛型是为了在编译期,检查参数类型的是否正确。如果子类赋值给超类,将破坏了类型安全
举例来说,对于数组而言,String[]数组是可以赋值给Object[]数组的,但是集合采用这种思想来写就会出现错误。
List<String> strList = new ArrayList<>();
// 编译 Error:(14, 32) java: 不兼容的类型: java.util.List<java.lang.String>无法转换为java.util.List<java.lang.Object>
List<Object> objList = strList;
同样地,如果对应具体类型就重新编写类和多态的思想相违背,通过引入通配符的概念可以解决这个问题。
类型通配符一般是使用 ? 代替具体的类型实参。
注意了,此处是类型实参,而不是类型形参!
(此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。)
且Generic <?>
在逻辑上是Generic< Integer>、Generic< Number>...等所有Generic <具体类型实参>的父类。由此,我们依然可以定义泛型方法,来完成此类需求
泛型的使用需要经过定义和赋值两个阶段,当赋值的类型不确定的时候,可以通过引入通配符来代替。
通过将上述方法用通配符代替,则可以传入多种类型的泛型类
public void showKeyValue1(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
2.3.1 通配符边界
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
List<?> unknownList;
List<? extends Number> unknownNumberList;
List<? super Integer> unknownBaseLineIntgerList;
- ? extends T类型的上界是 T,参数化类型可能是 T 或 T 的子类
- ? super T表示类型的下界是 T,参数化类型可以是 T 或 T 的超类
泛型的上下边界添加,必须与泛型的声明在一起。
// 在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,
// 即在泛型声明的时候添加:public <T> T showKeyName(Generic<T extends Number> container)
// 否则,编译器会报错:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
T test = container.getKey();
return test;
}
2.3.2 PECS原则——”Producer Extends, Consumer Super”
通配符的边界中涉及到的一个重要问题是泛型的PECS原则。
- ? extends T
通过设置泛型上限我们可以从list里面get元素,那么我们可不可以往list里面add元素呢?
Tip:通配符类型无法实例化
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
//flist = new ArrayList<? extends Fruit>(); // compile error: 通配符类型无法实例化
// Compile Error: can't add any type of object:
// flist.add(new Apple())——error
// flist.add(new Orange())——error
// flist.add(new Fruit())——error
// flist.add(new Object())——error
flist.add(null); // Legal but uninteresting
// We Know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
答案是否定,Java编译器不允许我们这样做,为什么呢?对于这个问题我们不妨从编译器的角度去考虑。因为List<? extends Fruit> flist它自身可以有多种含义,"?"代表的实际参数是不确定的,也即,表示的Fruit的字类可以是多种形式的。
在向flist中添加元素时,编译器会检查类型是否符合要求。因为编译器只知道flist是Fruit某个子类的List,但并不知道这个子类具体是什么类,为了类型安全,只好阻止向其中加入任何子类。
当我们尝试add一个Fruit的时候,这个Fruit可以是任何类型的Fruit,而flist可能只想某种特定类型的Fruit,编译器无法识别所以会报错。
但是在从中读取数据时,因为编译器确定集合中存储的位Fruit的子类,因此list.get()的调用并不受影响。
-
Producer Extends:对于实现了<? extends T>的集合类只能将它视为Producer向外提供(get)元素,而不能作为Consumer来对外获取(add)元素。
-
? super T
通过设置泛型下限,可以解决上述的问题,达到向泛型集合中元素的目的。也即list.add()方法调用正确,list.get()方法调用报错。
public class GenericWriting {
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static void f1() {
writeExact(apples, new Apple());
writeExact(fruit, new Apple());
}
static <T> void writeWithWildcard(List<? super T> list, T item) {
list.add(item)
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruit, new Apple());
}
public static void main(String[] args) {
f1(); f2();
}
}
这样我们可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素了,原因类似,编译器的角度考虑,对于List<? super Apple> list,add的元素确定是Apple或者其父类,那么就一定可以赋值给T(对于父类编译器会自动向上转型)但是从中提取到的元素的类型不确定,因此编译器会禁止此操作。
根据上面的例子,我们可以总结出一条规律:"Producer Extends, Consumer Super":
"Producer Extend" —— 如果你需要一个只读List,用它来produce T,那么使用? extends T;
"Consumer Super" ——如果你需要一个只写List,用它来consume T,那么使用? super T
2.4 泛型方法
相比于泛型类和泛型接口,泛型方法的定义和使用上会比较复杂。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4) <T>的位置位于方法的返回值之前
* 5)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
不能将方法中包含有泛型的成为泛型方法,泛型方法的定义和使用必须遵循相应的原则。
public class GenericTest {
//这个类是个泛型类,在上面已经介绍过
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类时已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明和方法中都未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们在方法中声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public <T> T showKeyName(Generic<E> container){
...
}
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型既没有在GenericTest类中声明也没有在showkey方法前制定,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
}
}
3. 泛型相关问题
3.1 泛型类和泛型成员方法
泛型类的使用是为了在类的实例化时可以引入多种类型,除外,在泛型类中定义的成员方法同样也可以定义为泛型方法,那么这时候就会有一个作用域问题,简单来说,泛型的定义和名称无关,泛型方法中的泛型可以覆盖外部泛型类中定义的泛型。同时,泛型方法中定义的泛型即使在类中没有事先声明,只要在泛型法中指定了,也同样可以调用。
public class GenericFruit {
class Fruit{
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
class Person{
@Override
public String toString() {
return "Person";
}
}
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型,也即覆盖了类中之前定义的泛型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
//实例化泛型类,将泛型赋值为Fruit类型
GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
//apple是Fruit的子类,所以这里可以
generateTest.show_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
//generateTest.show_1(person);
//使用这两个方法都可以成功,泛型类的定义不限制泛型方法中对泛型的设置
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
3.2 静态方法和泛型
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
参考链接: