《Java编程思想第四版》笔记---15章(1) 泛型编程基础

前半部分是网络摘录的写的比较好的文章,忘记是哪里的了,感谢原作者,后半部分是补充原作的内容。感觉Java编程思想泛型这部分写得太泛了,特意浓缩一下整理也方便以后复习。

一、泛型的基本概念

 泛型的定义:泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,在用到的时候在指定具体的类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

  泛型思想早在C++语言的模板(Templates)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,那Object转型为任何对象成都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会被转嫁到程序运行期之中。

  泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,List与List就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型

  Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList与ArrayList就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。(类型擦除在后面在学习)

  使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类来说尤其有用。

  泛型程序设计(Generic Programming)意味着编写的代码可以被很多不同类型的对象所重用。


实例分析:

  在JDK1.5之前,Java泛型程序设计是用继承来实现的。因为Object类是所用类的基类,所以只需要维持一个Object类型的引用即可。就比如ArrayList只维护一个Object引用的数组:

  1. public class ArrayList//JDK1.5之前的  
  2. {  
  3.     public Object get(int i){......}  
  4.     public void add(Object o){......}  
  5.     ......  
  6.     private Object[] elementData;  
  7. }  
这样会有两个问题:

1、没有错误检查,可以向数组列表中添加类的对象

2、在取元素的时候,需要进行强制类型转换

这样,很容易发生错误,比如:

  1. /**jdk1.5之前的写法,容易出问题*/  
  2. ArrayList arrayList1=new ArrayList();  
  3. arrayList1.add(1);  
  4. arrayList1.add(1L);  
  5. arrayList1.add("asa");  
  6. int i=(Integer) arrayList1.get(1);//因为不知道取出来的值的类型,类型转换的时候容易出错  
这里的第一个元素是一个长整型,而你以为是整形,所以在强转的时候发生了错误。

所以。在JDK1.5之后,加入了泛型来解决类似的问题。例如在ArrayList中使用泛型:

  1. /** jdk1.5之后加入泛型*/  
  2. ArrayList arrayList2=new ArrayList();  //限定数组列表中的类型  
  3. // arrayList2.add(1); //因为限定了类型,所以不能添加整形  
  4. // arrayList2.add(1L);//因为限定了类型,所以不能添加整长形  
  5. arrayList2.add("asa");//只能添加字符串  
  6. String str=arrayList2.get(0);//因为知道取出来的值的类型,所以不需要进行强制类型转换  

还要明白的是,泛型特性是向前兼容的。尽管 JDK 5.0 的标准类库中的许多类,比如集合框架,都已经泛型化了,但是使用集合类(比如 HashMap 和 ArrayList)的现有代码可以继续不加修改地在 JDK 1.5 中工作。当然,没有利用泛型的现有代码将不会赢得泛型的类型安全的好处。


在学习泛型之前,简单介绍下泛型的一些基本术语,以ArrayListArrayList做简要介绍:

整个成为ArrayList泛型类型

ArrayList中的 E称为类型变量或者类型参数

整个ArrayList 称为参数化的类型

ArrayList中的integer称为类型参数的实例或者实际类型参数

ArrayList中的念为typeof   Integer

ArrayList称为原始类型


二、泛型的使用

泛型的参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。下面看看具体是如何定义的。

1、泛型类的定义和使用

一个泛型类(generic class)就是具有一个或多个类型变量的类。定义一个泛型类十分简单,只需要在类名后面加上<>,再在里面加上类型参数:

  1. class Pair {  
  2.     private T value;  
  3.         public Pair(T value) {  
  4.                 this.value=value;  
  5.         }  
  6.         public T getValue() {  
  7.         return value;  
  8.     }  
  9.     public void setValue(T value) {  
  10.         this.value = value;  
  11.     }  
  12. }  

现在我们就可以使用这个泛型类了:
  1. public static void main(String[] args) throws ClassNotFoundException {  
  2.         Pair pair=new Pair("Hello");  
  3.         String str=pair.getValue();  
  4.         System.out.println(str);  
  5.         pair.setValue("World");  
  6.         str=pair.getValue();  
  7.         System.out.println(str);  
  8.     }  

Pair类引入了一个类型变量T,用尖括号<>括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以定义Pair类,其中第一个域和第二个域使用不同的类型:

public class Pair{......}

注意:类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示关键字与值的类型。(需要时还可以用临近的字母U和S)表示“任意类型”。


2、泛型接口的定义和使用

定义泛型接口和泛型类差不多,看下面简单的例子:
  1. interface Show{  
  2.     void show(T t,U u);  
  3. }  
  4.   
  5. class ShowTest implements Show{  
  6.     @Override  
  7.     public void show(String str,Date date) {  
  8.         System.out.println(str);  
  9.         System.out.println(date);  
  10.     }  
  11. }  
测试一下:
  1. public static void main(String[] args) throws ClassNotFoundException {  
  2.         ShowTest showTest=new ShowTest();  
  3.         showTest.show("Hello",new Date());  
  4.     }  

3、泛型方法的定义和使用

泛型类在多个方法签名间实施类型约束。在 List 中,类型参数 V 出现在 get()、add()、contains() 等方法的签名中。当创建一个 Map 类型的变量时,您就在方法之间宣称一个类型约束。您传递给 add() 的值将与 get() 返回的值的类型相同。

类似地,之所以声明泛型方法,一般是因为您想要在该方法的多个参数之间宣称一个类型约束。

举个简单的例子:

  1. public static void main(String[] args) throws ClassNotFoundException {  
  2.         String str=get("Hello""World");  
  3.         System.out.println(str);  
  4.     }  
  5.   
  6.     public static  T get(T t, U u) {  
  7.         if (u != null)  
  8.             return t;  
  9.         else  
  10.             return null;  
  11.     }  

4.泛型集合

Java中泛型集合使用的非常广泛,在Java5以前,java中没有引入泛型机制,使用java集合容器时经常遇到如下两个问题:

a.       java容器默认存放Object类型对象,如果一个容器中即存放有A类型对象,又存放有B类型对象,如果用户将A对象和B对象类型弄混淆,则容易产生转换错误,会发生类型转换异常。

b.       如果用户不知道集合容器中元素的数据类型,同样也可能会产生类型转换异常。

鉴于上述的问题,java5中引入了泛型机制,在定义集合容器对象时显式指定其元素的数据类型,在使用集合容器时,编译器会检查数据类型是否和容器指定的数据类型相符合,如果不符合在无法编译通过,从编译器层面强制保证数据类型安全。

(1).java常用集合容器泛型使用方法:

  1. public class New{  
  2.     public static  Map map(){  
  3.         return new HashMap();  
  4. }  
  5. public static  List list(){  
  6.     return new ArrayList() ;  
  7. }  
  8. public static  LinkedList lList(){  
  9.     return new LinkedList();  
  10. }  
  11. public static  Set set(){  
  12.     return new HashSet();  
  13. }  
  14. public static  Queue queue(){  
  15.     return new LinkedList() ;  
  16. }  
  17. ;public static void main(String[] args){  
  18.     Map> sls = New.map();  
  19.     List ls = New.list();  
  20.     LinkedList lls = New.lList();  
  21.     Set ss = New.set();  
  22.     Queue qs = New.queue();  
  23. }  
  24. }  


(2).Java中的Set集合是数学上逻辑意义的集合,使用泛型可以很方便地对任何类型的Set集合进行数学运算,代码如下:

  1. public class Sets{  
  2.     //并集  
  3.     public static  Set union(Set a, Set b){  
  4.         Set result = new HashSet(a);  
  5.         result.addAll(b);  
  6.         return result;  
  7. }  
  8. //交集  
  9. public static  Set intersection(Set a, Set b){  
  10.     Set result = new HashSet(a);  
  11.     result.retainAll(b);  
  12.     return result;  
  13. }  
  14. //差集  
  15. public static  Set difference(Set a, Set b){  
  16.     Set result = new HashSet(a);  
  17.     result.removeAll(b);  
  18.     return Result;  
  19. }  
  20. //补集  
  21. public static  Set complement(Set a, Set b){  
  22.     return difference(union(a, b), intersection(a, b));  
  23. }  
  24. }  

三、泛型变量的类型限定

1.泛型上边界

Java泛型编程时,编译器忽略泛型参数的具体类型,认为使用泛型的类、方法对Object都适用,这在泛型编程中称为类型信息檫除。


有这样一个简单的泛型方法:

  1. public static  T get(T t1,T t2) {  
  2.    if(t1.compareTo(t2)>=0);//编译错误  
  3.       return t1;  
  4. }  

因为,在编译之前,也就是我们还在定义这个泛型方法的时候,我们并不知道这个泛型类型T,到底是什么类型,所以,只能默认T为原始类型Object。所以它只能调用来自于Object的那几个方法,而不能调用compareTo方法。

可我的本意就是要比较t1和t2,怎么办呢?这个时候,就要使用类型限定,对类型变量T设置限定(bound)来做到这一点。

我们知道,所有实现Comparable接口的方法,都会有compareTo方法。所以,可以对做如下限定:

  1. public static extends Comparable> T get(T t1,T t2) { //添加类型限定  
  2.    if(t1.compareTo(t2)>=0);  
  3.      return t1;  
  4. }   

类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过要注意下面几点:

1、不管该限定是类还是接口,统一都使用关键字 extends

2、可以使用&符号给出多个限定,比如public static extends Comparable&Serializable> T get(T t1,T t2)  

3、如果限定既有接口也有类,那么类必须只有一个,并且放在首位置

public   static   extends  Object&Comparable&Serializable> T get(T t1,T t2)  

2.泛型通配符

泛型初始化过程中,一旦给定了参数类型之后,参数类型就会被限制,无法随着复制的类型而动态改变,如:

  1. class Fruit{  
  2. }  
  3. class Apple extends Fruit{  
  4. }  
  5. class Jonathan extends Apple{  
  6. }  
  7. class Orange extends Fruit{  
  8. }  
  9. 如果使用数组:  
  10. public class ConvariantArrays{  
  11.     Fruit fruit[] = new Apple[10];  
  12.     Fruit[0] = new Apple();  
  13.     Fruit[1] = new Jonathan();  
  14.     try{  
  15.         fruit[0] = new Fruit();  
  16. }catch(Exception e){  
  17.     System.out.println(e);  
  18. }  
  19. try{  
  20.         fruit[0] = new Orange();  
  21. }catch(Exception e){  
  22.     System.out.println(e);  
  23. }  
  24. }  

编译时没有任何错误,运行时会报如下异常:

java.lang.ArrayStoreException:Fruit

java.lang.ArrayStoreException:Orange

为了使得泛型在编译时就可以进行参数类型检查,我们推荐使用java的集合容器类,如下:

  1. public class NonConvariantGenerics{  
  2.     List flist = new ArrayList();  
  3. }  

很不幸的是,这段代码会报编译错误:incompatible types,不兼容的参数类型,集合认为虽然Apple继承自Fruit,但是List的Fruit和List的Apple是不相同的,因为泛型参数在声明时给定之后就被限制了,无法随着具体的初始化实例而动态改变,为解决这个问题,泛型引入了通配符”?”。

对于这个问题的解决,使用通配符如下:

  1. public class NonConvariantGenerics{  
  2.     Listextends Fruit> flist = new ArrayList();  
  3. }  

泛型通配符”?”的意思是任何特定继承Fruit的类,java编译器在编译时会根据具体的类型实例化。

另外,一个比较经典泛型通配符的例子如下:

public class SampleClass < T extendsS> {…}

假如A,B,C,…Z这26个class都实现了S接口。我们使用时需要使用到这26个class类型的泛型参数。那实例化的时候怎么办呢?依次写下

SampleClass a = new SampleClass();

SampleClass a = new SampleClass();

SampleClass a = new SampleClass();

这显然很冗余,还不如使用Object而不使用泛型,使用通配符非常方便:

SampleClass sc = newSampleClass();


3.泛型下边界

在1中大概了解了泛型上边界,使用extends关键字指定泛型实例化参数只能是指定类的子类,在泛型中还可以指定参数的下边界,是一super关键字可以指定泛型实例化时的参数只能是指定类的父类。

例如:

  1. class Fruit{  
  2. }  
  3. class Apple extends Fruit{  
  4. }  
  5. class Jonathan extends Apple{  
  6. }  
  7. class Orange extends Fruit{  
  8. }  
  9. public superTypeWildcards{  
  10.     public static void writeTo(Listsuper Apple> apples){  
  11.         apples.add(new Apple());  
  12.         apples.add(new Jonathan());  
  13. }  
  14. }  

通过? Super限制了List元素只能是Apple的父类。

泛型下边界还可以使用,但是注意不能使用,即super之前的只能是泛型通配符,如:

  1. public class GenericWriting{  
  2.     static List apples = new ArrayList();  
  3.     static List fruits = new ArrayList();  
  4.     static  void writeExact(List list, T item){  
  5.         list.add(item);  
  6. }  
  7. static  void writeWithWildcards(Listsuper T> list, T item){  
  8.     list.add(item);  
  9. }  
  10. static void f1(){  
  11.     writeExact(apples, new Apple());  
  12. }  
  13. static void f2(){  
  14. writeWithWildcards(apples, new Apple());  
  15.     writeWithWildcards(fruits, new Apple());  
  16. }  
  17. public static void main(String[] args){  
  18.     f1();  
  19.     f2();  
  20. }  
  21. }  

4.无边界的通配符

泛型的通配符也可以不指定边界,没有边界的通配符意思是不确定参数的类型,编译时泛型檫除类型信息,认为是Object类型。如:

  1. public class UnboundedWildcard{  
  2.     static List list1;  
  3.     static List list2;  
  4.     static Listextends Object> list3;  
  5.     static void assign1(List list){  
  6.         list1 = list;  
  7.         list2 = list;  
  8.         //list3 = list; //有未检查转换警告  
  9. }   
  10. static void assign2(List list){  
  11.         list1 = list;  
  12.         list2 = list;  
  13.     list3 = list;  
  14. }  
  15. static void assign3(Listextends Object> list){  
  16.         list1 = list;  
  17.         list2 = list;  
  18.     list3 = list;  
  19. }  
  20. public static void main(String[] args){  
  21.     assign1(new ArrayList());  
  22. assign2(new ArrayList());  
  23. //assign3(new ArrayList()); //有未检查转换警告  
  24. assign1(new ArrayList());  
  25. assign2(new ArrayList());  
  26. assign3(new ArrayList());   
  27. List wildList = new ArrayList();  
  28. assign1(wildList);  
  29. assign2(wildList);  
  30. assign3(wildList);   
  31. }  
  32. }  

List和List的区别是:List是一个原始类型的List,它可以存放任何Object类型的对象,不需要编译时类型检查。List等价于List,它不是一个原始类型的List,它存放一些特定类型,只是暂时还不确定是什么类型,需要编译时类型检查。因此List的效率要比List高。

5.实现泛型接口注意事项:

由于泛型在编译过程中檫除了参数类型信息,所以一个类不能实现以泛型参数区别的多个接口,如:


  1. interface Payable{  
  2. }  
  3. class Employee implements Payable{  
  4. }  
  5. class Hourly extends Employee implements Payable{  
  6. }  

类Hourly无法编译,因为由于泛型类型檫除,Payable和Payable在编译时是同一个类型Payable,因此无法同时实现一个接口两次。

6.泛型方法重载注意事项:

由于泛型在编译时将参数类型檫除,因此以参数类型来进行方法重载在泛型中要特别注意,如:


  1. public class GenericMethod{  
  2.     void f(List v) {  
  3. }  
  4. void f(List v){  
  5. }  
  6. }  

无法通过编译,因为泛型檫除类型信息,上面两个方法的参数都被看作为Object类型,使用参数类型已经无法区别上面两个方法,因此无法重载。

7.泛型中的自绑定:

通常情况下,一个类无法直接继承一个泛型参数,但是你可以通过继承一个声明泛型参数的类,这就是java泛型编程中的自绑定,如:


泛型的自绑定约束目的是用于强制继承关系,即使用泛型参数的类的基类是相同的,强制所有人使用相同的方式使用参数基类。


你可能感兴趣的:(《Java编程思想第四版》笔记)