泛型程序设计分为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类型的,那么在存入的值的时候就会提示类型不匹配,从而避免上述问题。使得代码具有更好的可读性和安全性。
好了,在了解的泛型存在的意义之后,下面我们开始由表及里的去了解泛型。
一个泛型类就是具有一个或多个类型变量的类。
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
2、该类通过指定显示的类型参数String,对参数值进行了比较,用Pair对象返回了两个String类型的结果。
3、String类实现了Comparable接口,所以可以直接调用compareTo方法对String进行比较。
如何定义一个带有类型参数的简单方法呢?
public static <T> T getMiddle(T a){
..................
}
首先,这是一个泛型方法,从尖括号和类型变量可以看出。注意,类型变量
泛型方法可以定义在普通的类中,也可以定义在泛型类中。
很多时候,类或方法需要对类型变量加以约束。
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的比较。
重点:虚拟机泛型对象—–所有的对象都属于普通类。
无论何时定义一个泛型类型,虚拟机都会提供一个相应的原始类型。
例如,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
p.s. 擦除的概念很重要,贯穿在泛型代码的编译过程。
当调用泛型方法时,如果擦除返回类型。编译器将插入强制类型转换。例如:
Pair p = ...
Person p = p.getName();
编译器会把这个方法翻译为两条虚拟机指令:
调用p.getName()得到Object类型返回值
将返回的Object类型强制转化为Person类型
类型擦除也出现在泛型方法中。
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
总之,有关Java泛型转换需要记住: