泛型类和泛型方法有类型参数,这使得他们可以准确地描述用特定类型实例化时会发生什么。
在有泛型类之前,Java程序员都是使用Object来编写适用于多种类型的代码。这很繁琐同时很不完全。
1.为什么要使用泛型程序设计?
泛型程序设计意味着编写的代码可以对应多种不同类型的对象重用,比如你一一定不希望为收集到的String,File对象分别编写不同的类。事实上,确实不需要这么做,因为一个ArrayList类就可以收集任何类的对象。这就是泛型程序设计的一个例子。
1.1类型参数的好处
在Java中增加泛型类之前,泛型程序设计使用继承实现。ArrayList类只维护一个Object引用的数组:
public class ArrayList { private Object[] elements; //....... public Object get(int i){ return null; }; public void add(Object o){ } public static void main(String[] args) { ArrayList files = new ArrayList(); String filename = (String) files.get(0);
files.add(new File("xxxxxxx")); files.add(new Student());
} //这里的方法存在两个问题, //1.当获取一个值时由于get方法的返回值参数为Object我们调用add得到的返回值必须进行强制类型转换。 //2.此外,这里没有错误检查,我们可以向数组列表中添加任何类的值,我们定义的files只想插入文件类型但是我们定义一个Student类型之后也可以插入。 }
泛型提供了一个很好的解决方案:类型参数。
ArrayList有一个类型参数来指明元素的类型:
ArrayList<String> list = new ArrayList<>();
这使得代码的可读性很好,让人一眼看起来就知道这个数组列表中包含的是String对象。
此时我们调用String filename = files.get(0); 编译器知道返回类型是String了无需从Object的返回类型强转为String。
编译器还知道ArrayList<String>的add方法有一个String的参数,这比有一个可以包含万物的Object类型的参数要安全的多,现在编译器就可以检查,防止你插入错误的对象。
比如:
ArrayList<String> files = new ArrayList<>(); String filename = "xxxxxx"; files.add(filename); files.add(new Student()); //编译报错
第四行是无法编译通过的。不过,出现编译错误比运行时出现类的强制类型转换异常好得多。
这正是类型参数的魅力所在,它会让你的程序更易读更安全。
2.定义简单泛型类
泛型类就是有一个或者多个类型变量的类,我们使用一个Pair的简单类来作为例子。这个类使得我们可以只关注泛型,而不用为数据存储的细节而分心。
public class Pair<T>{ private T first; private T second; public Pair(T first, T second) { this.first = first; this.second = second; } public Pair(){ first = null; second = null; } }
Pair类引入了一个类型变量T,使用尖括号(<>)括起来,放在类名的后面。泛型类可以有多个类型变量。例如,Pair类,其中第一个字段和第二个字段使用不同的类型:
public class Pair<T , U>{.......}
类型变量<T, U>等在整个类定义中用于指定方法的返回类型以及字段和局部变量的定义类型。
可以用具体的类型替换类型变量来实例化泛型类型,比如:
public class Pair<String> { private String first; private String secodn; public Pair(String first, String secodn) { this.first = first; this.secodn = secodn; } public Pair() { } public String getFirst() { return first; } public void setFirst(String first) { this.first = first; } public String getSecodn() { return secodn; } public void setSecodn(String secodn) { this.secodn = secodn; } } 换句话说,泛型类相当于普通类的工厂。
如下代码中具体使用了Pair类。静态的minmax方法遍历了数组同时据算出最小值和最大值,他用一个Pair对象反回两个结果。
回想一下:compareTo方法比较两个字符串,如果字符串相同则返回0,按照字典顺序,如果第一个字符串比第二个字符串靠前,就返回一个负整数,如果第一个字符串比第二个字符串靠后就返回一个正整数。
public class PairTest1 { public static void main(String[] args) { String[] words = {"Marry","had","a","little","lamb"}; Pair<String> mm = ArrayAlg.minmax(words); System.out.println("min = " + mm.getFirst()); System.out.println("max = " + mm.getSecond()); } }
public class ArrayAlg{ public static Pair<String> minmax(String[] a){ if(a == null || a.length == 0) return null; String min = a[0]; String max = a[a.length-1]; 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); } }
3.泛型方法
我们不仅可以定义一个带参数的泛型类,还可以定义一个带有类型参数的方法。
public class ArrayAlg{ public static <T> T getMiddle(T[] a){ return a[a.length / 2]; } }
这个方法是在普通类中定义的,而不是在泛型类中。不过这确实是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符后面,并在泛型类型的前面。
泛型方法也可以定义在泛型类当中。
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面。
String middle1 = ArrayAlg.<String>getMiddle(words); System.out.println(middle1);
其实编译器可以推断出你想要的方法是哪个即方法调用中可以省略<String>类型参数。
String middle = ArrayAlg.getMiddle(words);
System.out.println(middle);
几乎在所有情况下泛型方法的类型推导都可以正常工作。偶尔,编译器也会提示错误,考虑下面这个案例:
4.类型变量的限定