泛型

泛型和继承

 

泛型就是广泛的类型,就是在不同类型之间都可以使用的就叫做泛型,使用的是类型里面的方法,所以如果要使用泛型就需要保证使用泛型的类需要有共同的方法,泛型(generics,genericity)又称为“参数类型化(parameterized type)”或“模板(templates)”,是和继承(inheritance)不同而互补的一种组件复用机制。

 

       继承和泛型的不同之处在于——在一个系统中,继承层次是垂直方向,从抽象到具体,而泛型是水平方向上的。当运用继承,不同的类型将拥有相同的接口,并获得了多态性;当运用泛型,将拥有许多不同的类型,并得以相同的算法作用在它们身上。因此,一般说来,当类型与实现方法无关时,使用泛型;否则,用继承,既然如此可以更进一步的总结为,使用泛型的类之间它们应该有共同的方法,否则泛型也就没有意义了。

 

  而且本身泛型的T这个字符因为代表的是Object类型,所以任何的关于Object类型可以使用的方法和参数它都可以使用。

 

  泛型类的本质是:让泛型类里面的类类型的参数可识别化(在真正使用的时候需要指定参数类型)。

 

  泛型方法的本质是:让泛型方法里面的类类型参数可识别化(在真正使用的时候需要指定参数类型)。

 

  泛型接口的本质是:让泛型接口里面的类类型参数可识别化(在真正使用的时候需要指定参数类型)。

 

  定义泛型类:

  

public class Gen {
 
  public T obj;
 
  public T getObj() {
 
  return obj;
 
  }
 
  public void setObj(T obj) {
 
  this.obj = obj;
 
  }
 
  public Gen(T obj) {
 
  super();
 
  this.obj = obj;
 
  }
 
  //打印类型
 
  public void showType() {
 
  System.out.println(obj.getClass().getName());
 
  }
 
}

定义非泛型类:

public class NoGen {
 
  private Object obj;
 
  public Object getObj() {
 
  return obj;
 
  }
 
  public void setObj(Object obj) {
 
  this.obj = obj;
 
  }
 
  public NoGen(Object obj) {
 
  super();
 
  this.obj = obj;
 
  }
 
  public void showType() {
 
  System.out.println(obj.getClass().getName());
 
  }
 
}

定义Person类:

package chapter11.sub08;
 
  public class Person {
 
  private String name;
 
  private int age;
 
  public String getName() {
 
  return name;
 
  }
 
  public void setName(String name) {
 
  this.name = name;
 
  }
 
  public int getAge() {
 
  return age;
 
  }
 
  public void setAge(int age) {
 
  this.age = age;
 
  }
 
  public Person(String name, int age) {
 
  super();
 
  this.name = name;
 
  this.age = age;
 
  }
 
  @Override
 
  public String toString() {
 
  return "姓名:\t"+this.getName()+"\t年龄:\t"+this.age;
 
  }
 
  public void display() {
 
  System.out.println("你可以调用这个方法吗?");
 
  }
 
}

定义测试类:

public class TestGen {
 
  public static void main(String[] args) {
 
  //使用泛型类
 
  Gen p1=new Gen(new Person("dada", 23));
 
  System.out.println("--------------泛型类------------------");
 
  p1.showType();
 
  System.out.println("p1 value:\t"+p1.getObj().toString());
 
  //使用泛型的时候你可以直接的调用这个类里面的任何的方法,因为它的
 
  //类型是确定的
 
  p1.getObj().display();
 
  //不使用泛型
 
  NoGen p2=new NoGen(new Person("dandan", 21));
 
  System.out.println("--------------非泛型类------------------");
 
  p2.showType();
 
  System.out.println("p2 value:\t"+p2.getObj().toString());
 
  //这个时候你直接去调用除了Object类型里面所定义的方法之外的其他任何的方法都是
 
  //错误的,因为这个时候你所获得的那个对象是Object类型的,尽管它本质上是可以转换
 
  //为Person类型的,但是谁知道呢?编译器是不知道的,因此你就需要多一步的转换的过程
 
  //多这一步的转换过程就有可能会出错,因为你可能有很多的类都要调用这个方法,这很显
 
  //然是会多出许多的重复的无用的代码的,这里只是写了一个类,如果是成百上千的类,那么
 
  //就需要多写成百上千的转换,谁能保证不出错?你能?反正我不能.
 
  p2.getObj().display();
 
  }
}

泛型方法:

package chapter11.sub08;
 
  import java.util.ArrayList;
 
  import java.util.Collection;
 
  import java.util.Iterator;
 
  public class TestGenericFunction {
 
  public static void main(String[] args) {
 
  String[] strs={"1","2","abc","def"};
 
  Collection listStr=TestGenericFunction.fromArrayToCollection(strs, new ArrayList());
 
  Iterator s=listStr.iterator();
 
  while(s.hasNext()) {
 
  System.out.println(s.next());
 
  }
 
  }
 
  public static Collection fromArrayToCollection(T[] a, Collection c) {
 
  for (T t : a) {
 
  c.add(t);
 
  }
 
  return c;
 
  }
}

 

类型擦除

泛型是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语言中的泛型实现方法称为类型 擦除,基于这种方法实现的泛型被称为伪泛型。

也就是说Java中的泛型是个假泛型,仅仅只是在编译器那边做了语法检查而已,和C#里的泛型不一样的。 
基本上,不管你在List<>里面写什么类型,编译通过了以后运行时全部都是Object。

代码清单10-2是一段简单的Java泛型例子,我们可以看一下它编译后的结果是怎样的?

代码清单 10-2 泛型擦除前的例子

public static void main(String[] args) {  
    Map<String, String> map = new HashMap<String, String>();  
    map.put("hello", "你好");  
    map.put("how are you?", "吃了没?");  
    System.out.println(map.get("hello"));  
    System.out.println(map.get("how are you?"));  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

把这段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型,如代码清单10-3所示。

代码清单 10-3 泛型擦除后的例子

public static void main(String[] args) {  
    Map map = new HashMap();  
    map.put("hello", "你好");  
    map.put("how are you?", "吃了没?");  
    System.out.println((String) map.get("hello"));  
    System.out.println((String) map.get("how are you?"));  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

当初JDK设计团队为什么选择类型擦除的方式来实现Java语言的泛型支持呢?是因为实现简单、兼容性考虑还是别的原因?我们已不得而知,但确实有不少 人对Java语言提供的伪泛型颇有微词,当时甚至连《Thinking In Java》一书的作者Bruce Eckel也发表了一篇文章《这不是泛型!》 来批评JDK 1.5中的泛型实现。 
(注1:原文:http://www.anyang-window.com.cn/quotthis-is-not-a-genericquot-bruce-eckel-eyes-of-the-generic-java/

当时众多的批评之中,有一些是比较表面的,还有一些从性能上说泛型会由于强制转型操作和运行期缺少针对类型的优化等从而导致比C#的泛型慢一些,则是完 全偏离了方向,姑且不论Java泛型是不是真的会比C#泛型慢,选择从性能的角度上评价用于提升语义准确性的泛型思想,就犹如在讨论刘翔打斯诺克的水平与 丁俊晖有多大的差距一般。但笔者也并非在为Java的泛型辩护,它在某些场景下确实存在不足,笔者认为通过擦除法来实现泛型丧失了一些泛型思想应有的优 雅,例如下面代码清单10-4的例子:

代码清单 10-4 当泛型遇见重载 1

public class GenericTypes {  

    public static void method(List<String> list) {  
        System.out.println("invoke method(List<String> list)");  
    }  

    public static void method(List<Integer> list) {  
        System.out.println("invoke method(List<Integer> list)");  
    }  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

想一想,上面这段代码是否正确,能否编译执行?也许您已经有了答案,这段代码是不能被编译的,是因为参数List和 List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模 一样。初步看来,无法重载的原因已经找到了,但是真的就是如此吗?只能说,泛型擦除成相同的原生类型只是无法重载的其中一部分原因,请再接着看一看代码清 单10-5中的内容。

代码清单 10-5 当泛型遇见重载 2

public class GenericTypes {  

    public static String method(List<String> list) {  
        System.out.println("invoke method(List<String> list)");  
        return "";  
    }  

    public static int method(List<Integer> list) {  
        System.out.println("invoke method(List<Integer> list)");  
        return 1;  
    }  

    public static void main(String[] args) {  
        method(new ArrayList<String>());  
        method(new ArrayList<Integer>());  
    }  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17


执行结果:

invoke method(List<String> list)  
invoke method(List<Integer> list)  
  • 1
  • 2

代码清单10-5与代码清单10-4的差别,是两个method方法添加了不同的返回值,由于这两个返回值的加入,方法重载居然成功了,即这段代码可以被编译和执行 了。这是我们对Java语言中返回值不参与重载选择的基本认知的挑战吗?

(注 2:测试的时候请使用Sun JDK的Javac编译器进行编译,其他编译器,如Eclipse JDT的ECJ编译器,仍然可能会拒绝编译这段代码,ECJ编译时会提示“Method method(List) has the same erasure method(List) as another method in type GenericTypes”。) 
  代码清单10-5中的重载当然不是根据返回值来确定的,之所以这次能编译和执行成功,是因为两个 mehtod()方法加入了不同的返回值后才能共存在一个Class文件之中。第6章介绍Class文件方法表(method_info)的数据结构时曾 经提到过,方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个Class文件中的。

由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如在泛型类中如何获取传 入的参数化类型等。所以JCP组织对虚拟机规范做出了相应的修改,引入了诸如Signature、LocalVariableTypeTable等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范 要求所有能识别49.0以上版本的Class文件的虚拟机都要能正确地识别Signature参数。

(注3:在《Java虚拟机规范第二版》 (JDK 1.5修改后的版本)的“§4.4.4 Signatures”章节及《Java语言规范第三版》的“§8.4.2 Method Signature”章节中分别都定义了字节码层面的方法特征签名,以及Java代码层面的方法特征签名,特征签名最重要的任务就是作为方法独一无二不可 重复的ID,在Java代码中的方法特征签名只包括了方法名称、参数顺序及参数类型,而在字节码中的特征签名还包括方法返回值及受查异常表,本书中如果指的是字节码层面的方法签名,笔者会加入限定语进行说明,也请读者根据上下文语境注意区分。)

从上面的例子可以看到擦除法对实际编码带来的影响,由于List和List擦除后是同一个类型,我们只能添加两个并不需要实际使用到的返回值才能完成重载,这是一种毫无优雅和美感可言的解决方案。同时,从Signature属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。

同时值得注意的是:泛型与继承的关系

例如:

public void inspect(List<Object> list) 
{ 
     for(Object obj : list) 
     { 
         System.out.println(obj); 
     } 
     list.add(1); //这个操作在当前方法的上下文是合法的。
}
public void test()
{ 
     List<String> strs = new ArrayList<String>();
     inspect(strs); //编译错误 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这段代码中,inspect方法接受List作为参数,当在test方法中试图传入List的 时候,会出现编译错误。假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法 看来,其声明为List的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会 抛出ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。

【继承和泛型】

在使用子类一般类型参数时,必须在子类级别重复在基类级别规定的任何约束。例如,派生约束: 
这里写图片描述 
基类可以定义其签名使用一般类型参数的虚拟方法。在重写它们时,子类必须在方法签名中提供相应的类型: 
这里写图片描述 
您可以定义一般接口、一般抽象类,甚至一般抽象方法。这些类型的行为像其他任何一般基类型一样: 
这里写图片描述

泛型中K T V E

        jdk中的K,V,T,E等泛型名称很多人以为是固定写法,其实这些名称是可以改的,比如改成zhangsan,lisi都可以,jdk为了容易看懂,所以用K表示键,V表示值,T表示type类型,E表示enum枚举,其实这四个都只是符号,都是表示泛型名称,下面的例子的T全部可以换成E,也可以换成K,V,zhangsan,都没关系。 
   ? 表示不确定的类型 
   Object java中所有类的父类。 
2.使用方法 
 

1.意思 
   jdk中的K,V,T,E等泛型名称很多人以为是固定写法,其实这些名称是可以改的,比如改成zhangsan,lisi都可以,jdk为了容易看懂,所以用K表示键,V表示值,T表示type类型,E表示enum枚举,其实这四个都只是符号,都是表示泛型名称,下面的例子的T全部可以换成E,也可以换成K,V,zhangsan,都没关系。 
   ? 表示不确定的类型 
   Object java中所有类的父类。 
2.使用方法 
  

Java代码  收藏代码
import java.util.ArrayList;  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
//T1,T2都是随便定义的东西,注意1:他们不会关联到其他类,只是在本类中通用,只是告诉我们new的时候要加入泛型  
public class Test<T1, T2> {  
    public static void main(String[] args) {  
        System.out.println(new Test().getaa());  
        new Test<String, String> ().getbb("");  
        new Test().getcc(Test.class);  
        //注意下6:面这个HashMap的括号里面不能是T,E,T1,T2等不确定的东西,但可以是?  
        HashMap<Object, String> map = new HashMap<Object, String>();  
        List<?> list = new ArrayList<String>();  
    }  
  
    T2 getaa() {  
        //注意2:T2将自动转型为String,这个不需要去担心  
        return (T2) "few";  
  
    }  
  
    public <T> void getbb(T x) {  
        //注意3:Class<T>前面缺少<T>将编译错误  
        System.out.println(x.getClass().getName());  
    }  
  
    public <T> Class<?>  getcc(Class<T> a) {  
        //getcc前面的Class<T>前面缺少<T>将编译错误,注意4:Class<?>里面的问号可以换成T  
        System.out.println(a.getClass().getName());  
        //注意5:参数里面的Class<T>最大的好处是如果方法里面定义了泛型,可以自动获取类型值,比如如        下的List<T>可以自动获取到a的类型,不必强调死  
        List<T> aa=new ArrayList<T>();  
        System.out.println(aa);  
        return a;  
    }  
}  

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值