Java之泛型程序设计
概述
泛型程序设计意味着编写的代码可以被很多不同的对象所重用。使得程序具有更好的可读性和安全性。类似于C++中的模板,唯一明显的区别是java没有专用的template关键字,除此之外,两者机制有着本质的区别。
定义简单泛型类
一个泛型类就是具有一个或多个类型变量的类。下面是Pair类型代码:
public class Pair<T>
{
private T first;
private T second;
public Pair(){ first=null;second=null;}
public Pair(T f,T s) { first=f;second=s;}
public T getFirst() { return first; }
public T getSecond() { return second; }
public void setFirst(T f){first=f; }
public void setSecond(T s) {second=s;}
}
Pair类引入了一个类型变量T,用尖括号<>括起来,并放在类名的后面。泛型类可以有多个类型变量。类型变量使用大写字母,且比较短。
泛型方法
除了泛型类,还可以定义一个带类型参数的简单方法。
class ArrayAlg
{
public static <T> T getMiddle(T...a)
{
return a[a.length/2];
}
}
**注意:**类型变量放在修饰符的后面,返回类型放在前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型。
String middle=ArrayAlg.<String>getMiddle("John","Q","Public");
类型变量的限定
有时候,类或方法需要对类型变量加以约束。例如:
public static <T> T min(T[] a)
{
if(a==null||a.length==0) return null;
T min=a[0];
for(T b:a)
if(min.compareTo(b)>0) min=b;
return min;
}
变量min的类型为T,这就意味着其可以为任何一个类的对象。那么怎么能确定T所属的类含有compareTo方法呢?
解决这个问题的方法就是讲T限制为实现了compareTo接口(只含有一个方法compareTo的标准接口)的类。可以通过对类型变量T设置限定实现这一点。
public static <T extends Comparable> T min(T[] a)
注释:在C++中不能对模板参数的类型加以限制。
一个类型变量或者通配符可以有多个限定。例如
T extends Comparable & Serializable
限定类型用“&”分隔,而逗号用来分隔类型变量。
在java的继承中,可以根据需要拥有多个接口超类型。但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。
package pair2;
public class Pair<T>
{
private T first;
private T second;
public Pair()
{
first=null;
second=null;
}
public Pair(T f,T s)
{
first=f;
second=s;
}
public T getFirst()
{
return first;
}
public T getSecond()
{
return second;
}
public void setFirst(T f)
{
first=f;
}
public void setSecond(T s)
{
second=s;
}
}
package pair2;
import java.time.LocalDate;
public class Pair2Test {
public static void main(String[] agrs)
{
LocalDate[] birthdays= {LocalDate.of(1906, 12, 9),
LocalDate.of(1815, 12, 10),
LocalDate.of(1903, 12, 3),
LocalDate.of(1910, 6, 22)};
Pair<LocalDate> mm=ArrayAlg.minmax(birthdays);
System.out.println("min="+mm.getFirst());
System.out.println("max="+mm.getSecond());
}
}
class ArrayAlg
{
@SuppressWarnings({ "rawtypes", "unchecked" })
public static <T extends Comparable> Pair<T> minmax(T[] a)
{
if(a==null||a.length==0) return null;
T min=a[0];
T max=a[0];
for(T b:a)
{
if(min.compareTo(b)>0) min=b;
if(max.compareTo(b)<0) max=b;
}
return new Pair<>(min,max);
}
}
泛型代码和虚拟机
虚拟机没有泛型类型对象------所有对象都是普通类。
类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定的变量用Object)。例,Pair的原始类型如下所示:
public class Pair
{
private Object first;
private Object second;
public Pair(Object f,Object s) { first=f;second=s;}
public Object getFirst() { return first; }
public Object getSecond() { return second; }
public void setFirst(Object f){first=f; }
public void setSecond(Object s) {second=s;}
}
注释:java泛型和C++模板有很大的不同。C++中每个模板的实例化产生不同的类型,这一现象称为“模板代码膨胀”。Java不存在这个问题的困扰。
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。
翻译泛型表达式
当程序调用泛型方法是,如果擦除返回类型,编译器插入强制类型转换。例如:
Pair<Employee> buddies=...;
Employee buddy=buddies.getFirst();
擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。也就是说,编译器把这个调用翻译为两条虚拟机指令:
1、对原始方法Pair.getFirst的调用;
2、将返回的Object类强制转换为Employee类型。
翻译方法调用
通常一个完整的方法族为:
public static <T extends Comparable> T min(T[] a)
擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
当类型擦除和多态发生冲突时,就需要编译器生成一个桥方法
在虚拟机中,用参数类型和返回类型确定一个方法。
Java泛型转换的事实
1)虚拟机中没有泛型,只有普通的类和方法
2)所有的类型参数都用它们的限定类型转换
3)桥方法被合成来保持多态
4)为保持类型安全,必要时插入强制类型转换
约束和局限性
不能用类型参数代替基本类型。因此,没有Pair,只有Pair。原因是类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储double值。
运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的查询只产生原始类型。
查询一个对象是否属于某一个泛型类型时,倘若适用instanceof会得到一个编译器错误,如果使用强制类型转换会得到一个警告。同样的道理,getClass方法总是返回原始类型。
不能创建参数化类型的数组
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常。
提示:如果需要手机参数化类型对象,只有一种安全而有效地方法:使用ArrayList:ArrayList<Pair>。
不能构造泛型数组
数组本身也有类型,用来监控存储在虚拟机中的数组,这个类型会被擦除。例
public static <T extends Comparable> T[] minmax(T[] a) //Error类型擦除会让这个方法永远无法构造Comparable[2]数组
{
T[] mm=new T[2];
...
}
如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object[],并且在获取元素是进行类型转换。
不能抛出和捕获泛型类的实例
catch子句不能使用类型变量,不过在异常规范中使用类型变量是允许的。
可以消除对受查异常的检查
java异常处理的一个基本原则,必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。关键在于一下方法
@suppressWarning("uncheck") //消除对受查异常的检查
public static <T extends Throwable> void throws(Throwable e) throw T
{
throw(T) e;
}
通过使用泛型类、擦除和@SupperssWarning注解,就能消除Java类型系统的部分基本限制。
注意擦除后的冲突
当泛型被擦除时,无法创建应发冲突的条件。例
public class Pair<T>
{
public boolean equals(T value){return first.equal(value)&&second.equal(value);}
...
}
考虑一个Pair。从概念上将,他有两个equal方法:
boolean equals(String) //define in Pair
boolean equals(Object) //inherited from Object
但是,方法擦除后,boolean equals(T)就是boolean equals(Object),与方法Object.equals冲突。
补救的办法就是重新命名应发错误的方法。
泛型规范说明还提到另一个规则:“要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时称为两个接口类型的子类,而这两个接口是同一个接口的不同参数化”。
泛型类型的继承规则
无论S和T什么联系,通常Pair和Pair没有什么联系。
Pair类之间没有继承关系
永远可以将参数化类型转换为一个原始类型。
通配符类型
固定的泛型类型系统使用起来并没有那么友好,Java的设计者发明了一种巧妙的解决方法:通配符类型。
通配符概念
通配符类型中,允许类型参数变化。例
Pair<? extends Employee>
//表示任何Pair类型,类型参数时Employee的子类,如Pair<Manger>,但不能使Pair<String>
使用通配符的子类型关系
通配符的超类型限定
通配符还有一个附加的能力,即可以制定一个超类型限定,如
Pair<? extends Manger>
//表示任何Pair类型,类型参数时Manger的所有超类,如Pair<Employee>
带有超类型限定的通配符可以为方法提供参数,但是不能使用返回值。例,下面这个方法可以接受任何适当的Pair:
private static void minmaxBonus(Manger[] a, Pair<? super Manger> result) {
if(a.length==0) return;
Manger min=a[0];
Manger max=a[0];
for(Manger b:a)
{
if(min.getBonus()>b.getBonus()) min=b;
if(max.getBonus()<b.getBonus()) max=b;
result.setFirst(min);
result.setSecond(max);
}
}
直观的讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
无限定通配符
还可以有无限定通配符。例
public static boolean hasNulls(Pair<?> p)
{
return p.getFirst()==null||p.getSecond()==null;
}
Pair<?>和Pair的本质不同在于:可以用任意的Object对象调用原始Pair类的setObject方法。
该程序将本节所讲内容综合在一起,大家可以从里面看到彼此之间的关联。
package equals;
import java.time.LocalDate;
import java.util.Objects;
public class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String n,double s,int year,int month,int day)
{
name=n;
salary=s;
hireDay=LocalDate.of(year, month, day);
}
public String getName()
{
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPrecent)
{
salary=salary+salary*byPrecent/100;
}
public boolean equals(Object otherObject)
{
if(this==otherObject) return true;
if(otherObject==null) return false;
if(getClass()!=otherObject.getClass()) return false;
Employee other=(Employee) otherObject;
return Objects.equals(name,other.name)&&salary==other.salary&&Objects.equals(hireDay,other.hireDay);
}
public int hashCode()
{
return Objects.hash(name,salary,hireDay);
}
public String toString()
{
return getClass().getName()+"[name="+name+",salary="+salary+",hireDay="+hireDay+"]";
}
}
package equals;
public class Manger extends Employee {
private double bonus;
public Manger(String n, double s, int year, int month, int day) {
super(n, s, year, month, day);
bonus=0;
}
public double getSalary()
{
double baseSalary=super.getSalary();
return baseSalary+bonus;
}
public void setBonus(double bonus)
{
this.bonus=bonus;
}
public double getBonus()
{
return bonus;
}
public boolean equals(Object otherObject)
{
if(!super.equals(otherObject)) return false;
Manger other=(Manger) otherObject;
return bonus==other.bonus;
}
public int hashCode()
{
return super.hashCode()+17*new Double(bonus).hashCode();
}
public String toString()
{
return super.toString()+"[bonus="+bonus+"]";
}
}
package pair2;
public class Pair<T>
{
private T first;
private T second;
public Pair()
{
first=null;
second=null;
}
public Pair(T f,T s)
{
first=f;
second=s;
}
public T getFirst()
{
return first;
}
public T getSecond()
{
return second;
}
public void setFirst(T f)
{
first=f;
}
public void setSecond(T s)
{
second=s;
}
}
package pair3;
import equals.Employee;
import equals.Manger;
import pair2.Pair;
public class PairTest3 {
public static void main(String[] args)
{
Manger ceo=new Manger("Gus Greeky",800000,2003,12,15);
Manger cfo=new Manger("Sid Greeky",600000,2003,12,15);
Pair<Manger> buddies=new Pair<>(ceo,cfo);
printBuddies(buddies);
ceo.setBonus(100000);
cfo.setBonus(50000);
Manger[] mangers= {ceo,cfo};
Pair<Employee> result=new Pair<>();
minmaxBonus(mangers,result);
System.out.println("First:"+result.getFirst().getName()+" Second:"+result.getSecond().getName());
maxminBonus(mangers,result);
System.out.println("First:"+result.getFirst().getName()+" Second:"+result.getSecond().getName());
}
private static void minmaxBonus(Manger[] a, Pair<? super Manger> result) {
if(a.length==0) return;
Manger min=a[0];
Manger max=a[0];
for(Manger b:a)
{
if(min.getBonus()>b.getBonus()) min=b;
if(max.getBonus()<b.getBonus()) max=b;
result.setFirst(min);
result.setSecond(max);
}
}
public static void printBuddies(Pair<? extends Employee> p)
{
Employee first=p.getFirst();
Employee second=p.getSecond();
System.out.println(first.getName()+" and "+second.getName()+" are buddies!");
}
public static void maxminBonus(Manger[] a,Pair<? super Manger> result)
{
minmaxBonus(a,result);
PairAlg.swapHelper(result);
}
}
class PairAlg{
public static boolean hasNulls(Pair<?> p)
{
return p.getFirst()==null||p.getSecond()==null;
}
public static void swap(Pair<?> p) {swapHelper(p);}
public static <T> void swapHelper(Pair<T> p)
{
T t=p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
}