Java泛型--初级篇

泛型程序设计分为3个能力级别。基本级别是,仅仅使用泛型类——典型的是像ArrayList这样的集合,而不去考虑它们的工作方式于原因。大多数程序员将会停留在这一级别上。例如,俺就思!
所以,为了提高自己对于Java泛型的理解,而不仅仅停留在会用的阶段,俺要好好学习一下Java泛型,目标如下:

  • (1)能够定义简单的泛型类、泛型方法。

  • (2)知晓类型变量的限定规则和应用

  • (3)理解虚拟机是如何解析泛型代码

  • (4)明白在使用泛型的时候需要注意的一些限制

  • (5)理解泛型类型的继承规则

  • (6)学会使用通配符类型

  • (7)知道反射和泛型之间的关系以及如何利用反射操作泛型代码

上面(1)~(5)属于泛型的初级知识,而(6)~(7)属于泛型的高级知识。(俺是这样理解的,勿喷~)


咳咳(清清嗓子)!下面我就以上面的目标为主线,来对Java的泛型知识做一个总结归纳。

核心知识点

泛型存在的意义就是:代码可以被很多不同类型的对象所重用。

举个栗子:

List list = new ArrayList();  
list.add("CSDN_SEU_Calvin");  
list.add(100);  
for (int i = 0; i < list.size(); i++) {  
  String name = (String) list.get(i); //取出Integer时,运行时出现异常  
  System.out.println("name:" + name);  
}  

例子中向list类型集合中加入了一个字符串类型的值和一个Integer类型的值(这样是合法的,因为此时list默认的类型为Object类型)。但是在取值的时候就会出现问题,运行时会出现java.lang.ClassCastException。

泛型提供了一种解决方案:类型参数。例如下面代码:

List<String>list = new ArrayList<String>();  

这样编译器就知道存入的值是String类型的,那么在存入的值的时候就会提示类型不匹配,从而避免上述问题。使得代码具有更好的可读性和安全性。

好了,在了解的泛型存在的意义之后,下面我们开始由表及里的去了解泛型。


1 定义简单泛型类

一个泛型类就是具有一个或多个类型变量的类。

public class Pair 
{
   public Pair() { first = null; second = null; }

   public Pair(T first, T second) {
       this.first = first;  
       this.second = second; 
    }

   public T getFirst() { return first; }
   public T getSecond() { return second; }

   public void setFirst(T newValue) { first = newValue; }
   public void setSecond(T newValue) { second = newValue; }

   private T first;
   private T second;
}

在Pair类中, 就是类型变量,放在类名后面。泛型类可以有很多个类型变量。例如,

public class Pair{......}

重点:类型变量指定方法的返回类型以及域和局部变量的类型。

例如上面的Pair类中,返回值类型就是,域和局部变量的类型都是。当然在实际运用中 会用实际类型代替

Pair<String> 

则Pair类中的返回值类型,域和局部变量的类型都是String类型的。

其实,泛型类就是普通类的工厂。

下面看如何使用这个泛型类,重点!!

public class PairTest1
{
   public static void main(String[] args)
   {
      String[] words = { "Mary", "had", "a", "little", "lamb" };
      Pair mm = ArrayAlg.minmax(words);
      System.out.println("min = " + mm.getFirst());
      System.out.println("max = " + mm.getSecond());
   }
}

class ArrayAlg
{
   public static Pair minmax(String[] a)
   {
      if (a == null || a.length == 0) return null;
      String min = a[0];
      String max = a[0];
      for (int i = 1; i < a.length; i++)
      {
         if (min.compareTo(a[i]) > 0) min = a[i];
         if (max.compareTo(a[i]) < 0) max = a[i];
      }
      return new Pair<>(min, max);
   }
}

上面的PairTest1类是比较得出”Mary”, “had”, “a”, “little”, “lamb”这些单词中最大的值和最小值。

对于上面的类,有一下几点需要掌握:

  • 1、通过利用泛型类Pair完成了对于传入类型参数的比较。这个里面可以是String也可以是Double等类型。

  • 2、该类通过指定显示的类型参数String,对参数值进行了比较,用Pair对象返回了两个String类型的结果。

  • 3、String类实现了Comparable接口,所以可以直接调用compareTo方法对String进行比较。

2 泛型方法

如何定义一个带有类型参数的简单方法呢?

public static <T> T getMiddle(T a){
  ..................
}

首先,这是一个泛型方法,从尖括号和类型变量可以看出。注意,类型变量放在修饰符(public static)的后面,返回值类型 T 的前面。

泛型方法可以定义在普通的类中,也可以定义在泛型类中。

3 类型变量的限定

很多时候,类或方法需要对类型变量加以约束。

class ArrayAlg
{
   public static  Pair minmax(T[] a) 
   {
      if (a == null || a.length == 0) return null;
      T min = a[0];
      T max = a[0];
      for (int i = 1; i < a.length; i++)
      {
         if (min.compareTo(a[i]) > 0) min = a[i];
         if (max.compareTo(a[i]) < 0) max = a[i];
      }
      return new Pair(min, max);
   }
}

这里有一个问题:变量min和max类型是T,也就是说T可以是任意一个类的对象,那编译器怎么知道T所属的类有compareTo方法呢?

解决这个问题的方法就是将T限制为实现了Comparable接口的类。这就需要通过对类型变量T设置限定来实现:

public static  T min(T[] a)

这里T表示的是绑定类型的子类型。T和绑定类型可以是类,也可以是接口。

下面的类更详细的说明了这个问题

public class PairTest2
{
   public static void main(String[] args)
   {
      GregorianCalendar[] birthdays = 
         { 
            new GregorianCalendar(1906, Calendar.DECEMBER, 9), 
            new GregorianCalendar(1815, Calendar.DECEMBER, 10), 
            new GregorianCalendar(1903, Calendar.DECEMBER, 3),
            new GregorianCalendar(1910, Calendar.JUNE, 22), 
         };
      Pair mm = ArrayAlg.minmax(birthdays);
      System.out.println("min = " + mm.getFirst().getTime());
      System.out.println("max = " + mm.getSecond().getTime());
   }
}

class ArrayAlg
{
   public static  Pair minmax(T[] a) 
   {
      if (a == null || a.length == 0) return null;
      T min = a[0];
      T max = a[0];
      for (int i = 1; i < a.length; i++)
      {
         if (min.compareTo(a[i]) > 0) min = a[i];
         if (max.compareTo(a[i]) < 0) max = a[i];
      }
      return new Pair(min, max);
   }
}

通过指定< T extends Comparable > 表明类型参数需要是实现Comparable接口的类。从而实现了对于GregorianCalendar的比较。

4 虚拟机如何解析泛型代码

重点:虚拟机泛型对象—–所有的对象都属于普通类。

无论何时定义一个泛型类型,虚拟机都会提供一个相应的原始类型。

例如,Pair类在虚拟机中会被识别为:

public class Pair 
{
   public Pair() { first = null; second = null; }
   public Pair(T first, T second) { this.first = first;  this.second = second; }

   public T getFirst() { return first; }
   public T getSecond() { return second; }

   public void setFirst(T newValue) { first = newValue; }
   public void setSecond(T newValue) { second = newValue; }

   private T first;
   private T second;
}

//虚拟机识别为

public class Pair
{
   public Pair() { first = null; second = null; }
   public Pair(Object first, Object second) { this.first = first;  this.second = second; }

   public Object getFirst() { return first; }
   public Object getSecond() { return second; }

   public void setFirst(Object newValue) { first = newValue; }
   public void setSecond(Object newValue) { second = newValue; }

   private Object first;
   private Object  second;
}

在这里原始类型的名字Pair就是Pair类型擦除之后的泛型类型名称。将类型变量替换为限定类型(< T extends Comparable >替换为Comparable ),无限定类型就用Object。这里T是一个无限定的变量,所以直接用Object替换。

p.s. 擦除的概念很重要,贯穿在泛型代码的编译过程。

4.1 翻译泛型表达式

当调用泛型方法时,如果擦除返回类型。编译器将插入强制类型转换。例如:

Pair p = ...
Person p = p.getName();

编译器会把这个方法翻译为两条虚拟机指令:

  • 调用p.getName()得到Object类型返回值

  • 将返回的Object类型强制转化为Person类型

4.2 翻译泛型方法(难点)

类型擦除也出现在泛型方法中。

public static  T min(T[] a)

类型擦除之后变为

public static Comparable min(Comparable[] a)

这种方法擦除在有些时候会带来一些问题。

class DateInterval extends Pair<Date>{
    public void setSecond(Date second){
      if(second.CompareTo(getFirst())>=0){
        .......
      }
    }
}

这个类在擦除之后变为

class DateInterval extends Pair{
    public void setSecond(Date second){.........}
}

但是,DateInterval类继承了Pair类,该类本身就有一个setSecond方法

public void setSecond(Object second)

这时,如果调用下面的语句

DateInterval interval = new DateInterval(......);
Pair<Date> pair = inteval;
pair.setecond(aDate);

编译器是会调用自身的setSecond(Date second),还是继承自Pair类中的setSecond(Object second)呢?

这里,setSecond的调用是具有多态性的。由于pair引用了DateInterval 对象,所以就应该是调用DateInterval.setSecond。问题就在于,类型的擦除与多态之间发生了冲突。

为了解决这个问题,编译器在DateInterval 类中生成了一个桥方法

public void setSecond(Object second){
    setSecond((Date) second);
}

那么,编译器的调用过程就是:首先pair已经声明为类型Pair,这个类型的方法就是setSecond(Object second)。虚拟机用pair引用的对象调用这个方法,但是这个对象是DateInterval 类型的,所以会调用DateInterval.setSecond(Object)方法。这个方法就是合成的桥方法。该方法会调用DateInterval .setSecond(Date)(见上面代码)。如此,完成了我们希望的调用。

总之,有关Java泛型转换需要记住:

  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 为保持类型安全性,必要时插入强制类型转换。
  • 桥方法被用来合成多态。

你可能感兴趣的:(JavaSE)