`

java泛型详解

 
阅读更多

为什么使用泛型

泛型能使类型转换的错误在编译时被发现,从而增加程序的健壮性。看一个例子

public class Box{  
    private Object object;  
   
    public void set(Object object) {  
            this.object= object;  
       }  
    public Object get() {  
             return object;  
       }  
}  

 其中set方法可以接受任何java对象作为参数,加入在某个地方使用该类,预期object属性是Integer类型,但是实际set的是String类型,就会抛出一个运行时错误,这个错误在编译阶段无法检测。如:

Box box = new Box();
box.set("abc");
Integer a = (Integer)box.get();//编译不报错,运行时报ClassCastException

 使用泛型改造以上代码

public class Box<T>{  
    private T t;  
    public void set(T t) {  
            this.t= t;  
       }  
    public T get() {  
             return t;  
       }  
}  

 当我们使用这个Box类时会指定T的类型,该类型参数可以是类,接口,数组等,但是不能是基本数据类型。比如:

Box<Integer> box = new Box<Integer>; //指定了类型类型为Integer  
//box.set("abc");  该句在编译时就会报错  
box.set(new Integer(2));  
Integer a = box.get();  //不用转换类型  

可以看到,泛型还免除了我们手动进行类型转换。 

在引入泛型机制前,要在方法中支持多个数据类型,需要对方法进行重载,在引入泛型后可以更简洁的解决问题,更进一步可以定义多个参数以及返回值之间的关系。例如:

public void write(Integer i, Integer[] ia);  
public void write(Double  d, Double[] da);  
public void write(Long l, Long[] la);  

 的泛型版本为

public <T> void write(T t,T[] ta);

 总体来说,泛型机制能够在定义类、接口、方法时把“类型”作为一个参数,有点类似方法中的形参,如此我们就能通过不同的输入参数来实现方法的重用。不同于形参的是,泛型“参数”的输入是类型。

 

命名规则

类型参数的命名有一套默认规则,为了提高代码的维护性和可读性,强烈建议遵循这些规则。JDK中随处可见这些命名规则的应用。

E-Element

K-Key

V-Value

N-Number

T-Type

S,U,V etc. - 第二个、第三个、第四个参数

 

泛型原理简述

java中的泛型是个语法糖,作用发生在编译阶段。在编译过程中,正确检验泛型结果后,会将其擦除,并在对象进入和离开边界处添加类型检查和类型转换的方法。因此,成功编译后的class文件是不包含任何泛型信息的。

可以用一个反射的例子来证明

ArrayList<Integer> list = new ArrayList<Integer>();
		Class c = list.getClass();
		try {
			Method method = c.getMethod("add", Object.class);
			method.invoke(list, "abc");
			System.out.println(list.get(0));
		} catch (Exception e){
		}

 能正确打印出“abc”

 

 

泛型类与泛型方法的使用

泛型类的基本写法

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  }
}

 

泛型类,是在实例化类的时候指明泛型的具体类型;而泛型方法是在调用方法的使用才指明泛型具体类型

泛型方法基本写法

/**
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *   
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}

 

 

泛型的上下限和通配符

Integer是Number的子类,那么在Box<Number>作为形参的方法中能不能使用Box<Integer>呢?在上面的Box类中添加方法

public  static void showValue(Box<Number> box){
		System.out.println(box.getObject());
	}

 在该类中main方法编写代码

public static void main(String[] args) {
		Box<Integer> box = new Box<Integer>();
		box.setObject(3);
		showValue(box);//出错
	}

 看来不行?但是又想只要是泛型具体类型只要是Number子类都能用这个方法要怎么办呢?总不能每个子类写一遍方法把。

这里可以用通配符“?”

将上面静态方法改成

public  static void showValue(Box<? extends Number> box){
		System.out.println(box.getObject());
	}

 问题解决,<? extends Number> 代表可以接受Number以及他的子类作为类型参数,这种声明方式称为上限通配符。<? super Number>  代表可以接受Number以及他的父类作为类型参数。称为下限通配符。

 

?单独使用时称作无限定通配符。通常一下两种情况会使用无限定通配符:

1.编写一个方法,可以使用Object类中提供的功能来实现;

2.代码实现的功能与类型参数无关,比如List.clear()、List.size()等方法,还有经常使用的Class<?>方法,他们实现的功能都与类型参数无关。

通配符可以看做类型参数的实参

 

 

泛型使用的几个注意点

(1)不能用基本类型实例化类型参数

例如

 
  1. class Pair<K,V> {  
  2.    
  3.     private K key;  
  4.     private V value;  
  5.    
  6.     public Pair(K key, V value) {  
  7.         this.key = key;  
  8.         this.value = value;  
  9.     }  
  10.    
  11.     // ...  
  12. }  


当创建一个Pair类时,不能用基本类型来替代K,V两个类型参数。

 
  1. Pair<int,char> p = new Pair<>(8, 'a'); // 编译错误  
  2. Pair<Integer,Character> p = new Pair<>(8, 'a'); //正确写法  

 

 

(2)不可实例化类型参数

例如:

 
  1. public static <E> void append(List<E> list) {  
  2.     E elem = new E();  // 编译错误  
  3.     list.add(elem);  
  4. }  

 

 

但是,我们可以通过反射实例化带有类型参数的对象:

 
  1. public static <E> void append(List<E> list, Class<E> cls) throws Exception{  
  2.     E elem = cls.newInstance();   // 正确  
  3.     list.add(elem);  
  4. }  
  5.    
  6. List<String> ls = new ArrayList<>();  
  7. append(ls,String.class);  //传入类型参数的Class对象  

 

 

(3)不能在静态字段上使用泛型

通过一个反例来说明:

 
  1. public class MobileDevice <T> {  
  2.     private static T os;  //假如我们定义了一个带泛型的静态字段  
  3.    
  4.     // ...  
  5. }  
  6.    
  7. MobileDevice<Smartphone> phone = new MobileDevice<>();  
  8. MobileDevice<Pager> pager = new MobileDevice<>();  
  9. MobileDevice<TabletPC> pc = new MobileDevice<>();  

 

因为静态变量是类变量,被所有实例共享,此时,静态变量os的真实类型是什么呢?显然不能同时是Smartphone、Pager、TabletPC。

这就是为什么不能在静态字段上使用泛型的原因。

 

(4)不能对带有参数化类型的类使用cast或instanceof方法

 
  1. public static<E> void rtti(List<E> list) {  
  2.     if (list instanceof ArrayList<Integer>){  // 编译错误  
  3.         // ...  
  4.     }  
  5. }  

 

传给该方法的参数化类型集合为:

S = { ArrayList<Integer>,ArrayList<String> LinkedList<Character>, ... }

运行环境并不会跟踪类型参数,所以分辨不出ArrayList<Integer>与ArrayList<String>,我们能做的至多是使用无限定通配符来验证list是否为ArrayList:

 
  1. public static void rtti(List<?> list) {  
  2.     if (list instanceof ArrayList<?>){  // 正确  
  3.         // ...  
  4.     }  
  5. }  

 

 

同样,不能将参数转换成一个带参数化类型的对象,除非它的参数化类型为无限定通配符(<?>):

 
  1. List<Integer> li = new ArrayList<>();  
  2. List<Number>  ln = (List<Number>) li;  // 编译错误  

 

 

当然,如果编译器知道参数化类型肯定有效,是允许这种转换的:

 
  1. List<String> l1 = ...;  
  2. ArrayList<String> l2 = (ArrayList<String>)l1;  // 允许转变,类型参数没变化  

 

 

(5)不能创建带有参数化类型的数组

 

例如:

 
  1. List<Integer>[] arrayOfLists = new List<Integer>[2]; // 编译错误  

 

 

下面通过两段代码来解释为什么不行。先来看一个正常的操作:

 
  1. Object [] strings= new String[2];  
  2. string s[0] ="hi";   // 插入正常  
  3. string s[1] =100;    //报错,因为100不是String类型  

 

 

同样的操作,如果使用的是泛型数组,就会出问题:

 

[java] view plain copy
 
  1. Object[] stringLists = new List<String>[]; // 该句代码实际上会报错,但是我们先假定它可以执行  
  2. string Lists[0] =new ArrayList<String>();   // 插入正常  
  3. string Lists[1] =new ArrayList<Integer>();  // 该句代码应该报ArrayStoreException的异常,但是运行环境探测不到  

 

 

(6)不能创建、捕获泛型异常

泛型类不能直接或间接继承Throwable类

 

[java] view plain copy
 
  1. class MathException<T> extends Exception { /* ... */ }    //编译错误  
  2.    
  3. class QueueFullException<T> extends Throwable { /* ... */} // 编译错误  


方法不能捕获泛型异常:

 

 

[java] view plain copy
 
  1. public static<T extends Exception, J> void execute(List<J> jobs) {  
  2.     try {  
  3.         for (J job : jobs)  
  4.             // ...  
  5.     } catch (T e) {   // 编译错误  
  6.         // ...  
  7.     }  
  8. }  


但是,我们可以在throw子句中使用类型参数:

 

 

[java] view plain copy
 
  1. class Parser<T extends Exception> {  
  2.     public void parse(File file) throws T{     // 正确  
  3.         // ...  
  4.     }  
  5. }  

 

 

(7)不能重载经过类型擦除后形参转化为相同原始类型的方法

先来看一段代码:

 

[java] view plain copy
 
  1. List<String> l1 = new ArrayList<String>();  
  2. List<Integer> l2 = new ArrayList<Integer>();  
  3. System.out.println(l1.getClass()== l2.getClass());  

打印结果可能与我们猜测的不一样,打印出的是true,而非false,因为一个泛型类的所有实例在运行时具有相同的运行时类(class),而不管他们的实际类型参数。

 

事实上,泛型之所以叫泛型,就是因为它对所有其可能的类型参数,有同样的行为;同样的类可以被当作许多不同的类型。

认识到了这一点,再来看下面的例子:

 

[java] view plain copy
 
  1. public class Example {  
  2.     public void print(Set<String> strSet){ }  //编译错误  
  3.     public void print(Set<Integer> intSet) { }  //编译错误  
  4. }  

 

 

因为Set<String>与Set<Integer>本质上属于同一个运行时类,在经过类型擦出以后,上面的两个方法会共享一个方法签名,相当于一个方法,所以重载出错。

 

 

2.

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics