[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;
    }
}

2.2 泛型接口

接口其实本身类似于一个很特殊的类,所以泛型接口的声明和泛型类的声明方式一致,泛型声明在接口的接口名之后。如下例:
public interface IPool <T> {
    T get();
    int add(T t);
}

2.3 泛型方法

泛型方法,即带有类型参数的方法。泛型声明在方法的返回参数之前。如下例:
public class ArrayAlg {
    public static <T> T getMiddle(T[] a) {
        return a[a.length / 2];
    }
}

再举个例子,像ArrayList中我们知道可以存入任意Object,如果定义了泛型取出时则不必进行强制转换,其get调用了名为elementData的方法:
E elementData(int index) {
    return (E) elementData[index];
}

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(); //编译错误
}

但是如果我们对泛型参数做一个限定,规定它只要是Animal的子类就可以:
public static <E extends Animal> void execute(E ele) {
    ele.eat();
}

这就是泛型类型的限定。

3.1 泛型参数的上界

public class NumberGenericPool< T extends Number>
  • 上述方式的声明规定了NumberGenericPool类所能处理的参数类型和Number有继承关系
  • extends关键字声明的上界既可以是一个类,也可以是一个接口
  • 当泛型参数如此声明时,在实例化一个泛型类时,需要明确类型必须为上界类型或其子类

另外,需要注意的是,如果给泛型参数限定多个类型,则需要使用符号&
<T extends Runnable & Serializable>
同时要注意的是,限定多个类型时,class只能放在第一个且只能有一个(单继承),interface则放在之后。

3.2 泛型参数的下界

相对的,泛型参数的下界限定关键字为super,用以固定泛型参数的类型为某种类型或其父类
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;
}

所以通配符是拿来使用定义好的泛型,再次注意,是使用,比如之前提到过的例子:
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;}
}

现在我们要使用这个泛型类Plate,用它定义一个水果盘子,逻辑上认为水果盘子可以放苹果:
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());
这样就可以定义一个“啥水果都能放的盘子”,但同时要注意的是,这种方式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());

可以看到,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;
}



4、其他

4.1 不能实例化泛型对象

T t= new T();//error 
T.class.newInstance();//error 
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); 
}

4.2 不能在泛型类的静态域中使用泛型类型

public class Singleton<T>{
    private static T singleton; //error
    public static T getInstance(){} //error
    public static void print(T t){} //error 
}

但是,静态的泛型方法可以使用泛型类型:
public static <T> T getInstance(){return null;} //ok 
public static <T> void print(T t){} //ok

可以这样理解:
  • 泛型类中,<T>称为类型变量,实际上就相当于在类中隐形的定义了一个不可见的成员变量:“private T t;”,这是对象级别的,对于泛型类型变量来说是在对象初始化时才知道其具体类型的。而在静态域中,不需要对象初始化就可以调用,这是矛盾的;
  • 静态的泛型方法,是在方法层面定义的,就是说在调用方法时,T所指的具体类型已经明确了。



5、参考链接



posted @ 2017-09-12 20:37  Dulk  阅读(363)  评论(0编辑  收藏  举报