【Java学习笔记六】——一文教你入门泛型
声明:本文章内容主要摘选自尚硅谷宋红康Java教程、《Java核心卷一》、廖雪峰Java教程,示例代码部分出自本人,更多详细内容推荐直接观看以上教程及书籍,若有错误之处请指出,欢迎交流。
一、简单定义泛型类
1.必要性
- 在Java中增加泛型类之前,泛型程序设计是用继承实现的。ArrayList类只维护一个Object引用的数组:
public class ArragList
{
public Object[] elementData;
...
public object get(int i){};
public void add(Object o){};
}
-
这种方法有两个问题。当获取一个值时必须进行强制类型转换。
Arraylist files=new Arraylist o; String filenane=(String)files.get(o);
-
此外,这里没有错误检查。可以向数组列表中添加任何类的对象。
-
files.add(new File("..."));对于这个调用,编译和运行都不会出错。然而在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误。
-
泛型提供了一个更好的解决方案:类型参数(type parameters)。ArrayList类有一个类型参数用来指示元素的类型:
Arraylist<String> files = new Arraylist<String>;
-
这使得代码具有更好的可读性和安全性。人们一看就知道这个数组列表中包含的是String对象。
2.简单定义泛型类
一个泛型类(generic class)就是具有一个或多个类型变量的类。我们使用一个简单的Order类作为例子。对于这个类来说,我们只关注泛型,而不会为数据存储的细节烦恼。
class Order<T> {
String orderName;
int orderId;
T orderT;
public Order(){};
public Order(String orderName, int orderId, T orderT) {
this.orderName = orderName;
this.orderId = orderId;
this.orderT = orderT;
}
public T getOrderT() {
return orderT;
}
public void setOrderT(T orderT){
this.orderT = orderT;
}
}
//使用泛型后,我们可以任意定义Order类的类型
Order<String> o1 = new Order<>();
Order<Integer> o2 = new Order<>();
//如果定义了泛型类,实例化没有指明类的泛型,则认为此泛型类型为0bject类型
Order o3 = new Order();
二、使用泛型
1.使用泛型
使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object:
// 编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);
此时,只能把<T>当作Object使用,没有发挥泛型的优势。
当我们定义泛型类型<String>后,List<T>的泛型接口变为强类型List<String>:
// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);
当我们定义泛型类型<Number>后,List<T>的泛型接口变为强类型List<Number>:
List<Number> list = new ArrayList<Number>();
list.add(new Integer(123));
list.add(new Double(12.34));
Number first = list.get(0);
Number second = list.get(1);
编译器看到泛型类型List<Number>就可以自动推断出后面的ArrayList<T>的泛型类型必须是ArrayList<Number>,因此,可以把代码简写为:
List<Number> list = new ArrayList<>();
2.泛型接口
除了ArrayList
我们可以直接对String数组进行排序:
String[] ss=new String[]{"orange","Apple","Pear"};
Arrays.sort(ss);
System.out.println(Arrays.toString(ss));
这是因为String本身已经实现了Comparable<String>接口。如果换成我们自定义的Person类型则需要让Person实现Comparable<T>接口:
class Person implements Comparable<Person>{
private String name;
private int age;
private double salary;
public Person(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
public int compareTo(Person other)
{
return this.name.compareTo(other.name);//按照姓名从小到大排列,若加负号则为从大到小;若将name改变成其他属性如age即可改变排序依据
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", salary=" + salary +
'}' + '\n';
}
}
@Test //单元测试方法
public void test1(){
Person[] ps=new Person[]{
new Person("Bob",61,1000),
new Person("Alice",88,3000),
new Person("Lily",75,2000),
};
Arrays. sort(ps);
System. out. println(Arrays. toString(ps));
}
拓展:除了Comparable<T>接口,还有Comparator接口
注:Comparator接口的使用:定制排序
1.背景:
当元素的类型没有实现Java.Lang.Comparable接口而又不方便修改代码,或者实现了java.Lang.Comparable接口的排序规定不适合当前的操作,么可以考感使用Comparator 的对象来排序.
2.重写compare(Object o1,Object o2)方法,比较o1和o2的大小:
如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
public void test() {
String[] str = {"AA", "II", "GG", "CC", "EE"};
Arrays.sort(str, new Comparator() {
@Override
public int compare(Object o1, Object o2){
if(o1 instanceof String && o2 instanceof String){
String s1 = (String)o1;
String s2 = (String)o2;
return -s1.compareTo(s2);//如果不加负号就是从小到大
}
throw new RuntimeException("输入的数据类型不一致");
}
});
System.out.println(Arrays.toString(str));
}
三、泛型继承
一个类可以继承自一个泛型类。例如:父类的类型是Pair
public class IntPair extends Pair<Integer> {
}
使用的时候,因为子类IntPair并没有泛型类型,所以,正常使用即可:
IntPair ip = new IntPair(1, 2);
四、通配符
1.extends通配符
我们前面已经讲到了泛型的继承关系:Pair
假设我们定义了Pair
public class Pair<T> { ... }
然后,我们又针对Pair
public class PairHelper {
static int add(Pair<Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
上述代码是可以正常编译的。使用的时候,我们传入:
int sum = PairHelper.add(new Pair<Number>(1, 2));
注意:传入的类型是Pair
既然实际参数是Integer类型,试试传入Pair
public static void main(String[] args){
Pair<Integer>p=new Pair<>(123,456);
int n=add(p);
System.out.println(n);
}
static int add(Pair<Number>p){
Number first=p.getFirst();
Number last=p.getLast();
return first.intValue()+last.intValue();
}
/*
直接运行,会得到一个编译错误:
incompatible types: Pair<Integer> cannot be converted to Pair<Number>
原因很明显,因为Pair<Integer>不是Pair<Number>的子类,因此,add(Pair<Number>)不接受参数类型Pair<Integer>。
问题在于方法参数类型定死了只能传入Pair<Number>。
此时使用Pair<? extends Number>使得方法接收所有泛型类型为Number或Number子类的Pair类型。我们把代码改写如下:*/
static int add(Pair<? extends Number>p){
Number first=p.getFirst();
Number last=p.getLast();
return first.intValue()+last.intValue();
}
这样一来,给方法传入Pair
除了可以传入Pair<Integer>类型,我们还可以传入Pair<Double>类型,Pair<BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类。
如果我们考察对Pair<? extends Number>类型调用getFirst()方法,实际的方法签名变成了:
<? extends Number> getFirst();
即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量:
Number x = p.getFirst();
然后,我们不可预测实际类型就是Integer,例如,下面的代码是无法通过编译的:
Integer x = p.getFirst();
这是因为实际的返回类型可能是Integer,也可能是Double或者其他类型,编译器只能确定类型一定是Number的子类(包括Number类型本身),但具体类型无法确定。
2.super通配符
我们前面已经讲到了泛型的继承关系:Pair
考察下面的set方法:
void set(Pair<Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}
传入Pair<Integer>是允许的,但是传入Pair<Number>是不允许的。
和extends通配符相反,这次,我们希望接受Pair<Integer>类型,以及Pair<Number>、Pair<Object>,因为Number和Object是Integer的父类,setFirst(Number)和setFirst(Object)实际上允许接受Integer类型。
我们使用super通配符来改写这个方法:
void set(Pair<? super Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}
注意到Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。
下面的代码可以被正常编译:
public static void main(String[] args){
Pair<Number>p1=new Pair<>(12.3,4.56);
Pair<Integer>p2=new Pair<>(123,456);
setSame(p1,100);
setSame(p2,200);
System. out. println(p1. getFirst()+","+pl. getLast());
System. out. println(p2. getFirst()+","+p2. getLast());
}
static void setsame(Pair<? super Integer>p, Integer n){
p.setFirst(n);
p.setLast(n);
}
考察Pair<? super Integer>的setFirst()方法,它的方法签名实际上是:
void setFirst(? super Integer);
因此,可以安全地传入Integer类型。
再考察Pair<? super Integer>的getFirst()方法,它的方法签名实际上是:
? super Integer getFirst();
这里注意到我们无法使用Integer类型来接收getFirst()的返回值,即下面的语句将无法通过编译:
Integer x = p.getFirst();
因为如果传入的实际类型是Pair<Number>,编译器无法将Number类型转型为Integer。
注意:虽然Number是一个抽象类,我们无法直接实例化它。但是,即便Number不是抽象类,这里仍然无法通过编译。此外,传入Pair<Object>类型时,编译器也无法将Object类型转型为Integer。
唯一可以接收getFirst()方法返回值的是Object类型:
Object obj = p.getFirst();
因此,使用<? super Integer>通配符表示:
允许调用set(? super Integer)方法传入Integer的引用;
不允许调用get()方法获得Integer的引用。
唯一例外是可以获取Object的引用:Object o = p.getFirst()。
换句话说,使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。
3.无限制通配符
我们已经讨论了<? extends T>和<? super T>作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?:
void sample(Pair<?> p) {
}
因为<?>通配符既没有extends,也没有super,因此:
- 不允许调用set(T)方法并传入引用(null除外);
- 不允许调用T get()方法并获取T引用(只能获取Object引用)。
-
通配符有一个独特的特点,就是:Pair是所有Pair
的超类
为什么要使用这样脆弱的类型?它对于许多简单的操作非常有用。例如,下面这个方法将用来测试一个pair是否包含一个null引用,它不需要实际的类型。
public static boolean hasNu1ls(Pair<?>p)
{
return p.getFirstO==null || p.getSecond() == null;
}
//通过将hasNulls转换成泛型方法,可以避免使用通配符类型:
public static<T> boolean hasNulls(Pair<T> p)
//但是,带有通配符的版本可读性更强
拓展内容
1.对比extends和super通配符
我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:
<? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
<? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。
一个是允许读不允许写,另一个是允许写不允许读。
先记住上面的结论,我们来看Java标准库的Collections类定义的copy()方法:
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
}
它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。
这个copy()方法的定义就完美地展示了extends和super的意图:
copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
copy()方法内部也不会修改src,因为不能调用src.add(T)。
这是由编译器检查来实现的。如果在方法代码中意外修改了src,或者意外读取了dest,就会导致一个编译错误:
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
T t = dest.get(0); // compile error!
src.add(t); // compile error!
}
}
//这个copy()方法的另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加:
// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);
// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);
//而这些都是通过super和extends通配符,并由编译器强制检查来实现的。
2.PECS原则
何时使用extends,何时使用super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。
即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。
还是以Collections的copy()方法为例:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i); // src是producer
dest.add(t); // dest是consumer
}
}
}
需要返回T的src是生产者,因此声明为List<? extends T>,需要写入T的dest是消费者,因此声明为List<? super T>。
此笔记仅针对有一定编程基础的同学,且本人只记录比较重要的知识点,若想要入门Java可以先行观看相关教程或书籍后再阅读此笔记。
最后附一下相关链接:
Java在线API中文手册
Java platform se8下载
尚硅谷Java教学视频
《Java核心卷一》