使用教材:java核心技术 卷一 第十版
8.1
1.在 Java 中增加范型类之前, 泛型程序设计是用继承实现的。
2.ArrayList类出现于泛型类之前,其中有一个Object引用的数组,取出值时必须进行强制类型转换,且其中没有错误检查,因此可以向数组列表中添加任意类的对象(反正都是Object的子类,而且都是引用,不用担心存贮问题)
3.在引入泛型后,ArrayList 类有一个类型参数用来指示元素的类型:
ArrayList<String> files = new ArrayList<String>():
//在 Java SE 7 及以后的版本中, 构造函数中可以省略泛型类型: ArrayList<String>files = new ArrayList<>();省略的类型,可以从变量的类型推断得出,事实上,当这个file实例调用方法时,也不需强制类型转换,而且编译器将进行类型检查。
8.2
public class Pair<T>
{
private T first;
private T second;
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;}
}
1.语法:定义类时:public class Pair<T, U> { . . . }
//C++中是Template<class T, class U> class Pair{…}
2.类定义中的类型变量可以指定方法的返回类型以及域和局部变量的类型
3.用具体的类型替换类型变量就可以实例化泛型类型:Pair<String>,其结果可看成一个普通类。
//在new一个泛型类的变量时,似乎不用声明<>里的东西,编译器会根据其中的参数类型进行推算。
//接上,当定义一个引用时,必须要声明出类型。
8.3泛型方法
1.泛型方法可以定义在普通类中,类型变量放在修饰符(这里是 public static) 的 后面,返回类型(这里是T)的前面。
public static <T> T getMiddle(T... a) { return a[a.length / 2]; }
2.当调用一个泛型方法时’在方法名前的尖括号中放人具体的类型: String middle = ArrayAlg.<String>getMiddle("]ohnM, "Q.n, "Public");
在这种情况(实际也是大多数情况)下,方法调用中可以省略类型参数。编译器有足够的信息能够推断出所调用的方法。它用 names 的类型(即String[ ])(函数参数) 与泛型类型 T[ ] 进行匹配并推断出 T 一定是 String。也就是说,可以调用 String middle = ArrayAlg.getHiddle("]ohn", "Q.", "Public");
3.要注意二义性的问题:double middle = ArrayAlg.getMiddle(B.14, 1729, 0);函数参数里面有int和double, 编译器将会自动打包参数为1个Double和2个Integer对象,而后寻找这些类的共同超类型。事实上;找到2个这样的超类 型:Number 和 Comparable 接口,而这些超类其本身也是一个泛型类型。
//C++是会直接不允许二义性的出现,而不会向上自动寻找超类。
8.4 类型参数的限定
1.有时需要对于类型变量加以约束,比如说我们使用了一个方法,但是类型变量并不能保障一定具有这个方法,比如说可能代码要依赖于某个接口,即:
class ArrayAIg
{
public static <T> T min(T[] a)
{
if (a null || a.length = 0) return null;
T smallest = a[0];
for (int i = 1 ; i < a.length; i++)
if (smallest.compareTo(a[i]) > 0) smallest = a[i];
return smallest;
}
}
无法保证T在使用时对应的类型一定会有compareTo方法。可以通过对类型变量 T 设置限定(bound) 实现这一点: public static <T extends Comparable> T min(T[] a) . . .
// Comparable 接口本身就是一个泛型类型???实现此方法说明以对其做了约束。
//在 C++ 中不能对模板参数的类型加以限制。如果程序员用一个不适当的类型 实例化一个模板,将会在模板代码中报告一个(通常是含糊不清的)错误消息。
2.限制关键字使用extends,无论对象是类还是接口。一个类型变量或通配符可以有多个限定, 例如: T extends Comparable & Serializable 限定类型用“ &” 分隔,而逗号用来分隔类型变量。
3.在限定中,可以有多个接口,但只能有一个类,而且如果用一个类作为限定,他必须时限定列表中的第一个。
8.5 泛型代码和虚拟机
1.无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased) 类型变量, 并替换为限定类型(无限定的变量用 Object)。
2.java中的泛型只在程序源码中存在,当编译成字节码之后,就会变为原始类型,java实现泛型的方法是类型擦除。
3.当程序调用泛型方法时,如果擦除返回类型, 编译器插入强制类型转换。例如,下面这个语句序列
Pair<Employee> buddies = . .
Employee buddy = buddies.getFirst();
擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插人 Employee 的强制类型转换,将返回的Object类型转换为Employee类型。当存取一个泛型域时也要插入强制类型转换。假设 Pair 类的 first 域和 second 域都是公 有的。表达式:
Employee buddy = buddies.first;
就会在结果字节码中插入强制类型转换。
4.类型擦除也会出现在泛型方法中,由此产生一些问题:在一个子类中,如果对于父类的一个函数进性了重载,则在擦除之后就会破坏重载。比如说:
class Datelnterval extends Pair<LocalDate>
{
public void setSecond(LocalDate second)
{
if (second.compareTo(getFirstO) >= 0)
super.setSecond(second);
}
}
在被擦除之后,就会变为:
class Datelnterval extends Pair
{
public void setSecond(LocalDate second)
{……}
}
而在父类中,则有一个public void setSecond(Object second)方法,原本这个方法应该作为模板方法被实例化为public void setSecond(LocalDate second)而后被子类中的同名方法所重载,但是此时由于擦除,重载被破坏了,父类的方法与子类中的这个方法变为了两个方法,public void setSecond(Object second)和public void setSecond(LocalDate second)共同出现在子类中。
因此,编译器的解决方法是在作为子类的Datelnterval类中,生成一个桥方法,即为:
public void setSecond(Object second) { setSecond((Date) second); },这个方法使用了与父类方法在擦除之后相同的函数名,返回值,与参数列表,从而完全将父类方法覆盖,而这个桥方法的作用则是,将变量强制类型转换之后,传入子类方法,而在实际使用时,如果一个Pair的引用引用了一个Datelnterval类的变量,在接收参数时,即使调用桥函数,也会和以前一样运行。
5.假设Datelnterval类也重载了getSecond()方法,比如说:
class Datelnterval extends Pair
{
public LocalDate getSecond()
{
return (Date) super.getSecond().clone();
}
……
}
那么,在擦除之后,这个类中就会有两个函数:
public LocalDate getSecond()
public Object getSecond()/*这是一个桥方法,它将Pair类中的方法给覆盖掉了*/
由于虚拟机中,是使用参数类型和返回类型唯一确定一个方法,因此虽然在源码中无法写出如此形式,但是在最后生成的字节码中,却会出现以上这种情况。
6.这也导致了其他问题,比如说:
public class TestTheBug
{
public static void method (Pair<String> pairex) { System.out.println("Pair string pairex"); }
public static void method (Pair<Integer> pairex) { System.out.println("Pair int pairex"); }
}
这段代码表面上没有问题,但事实上是无法进行编译的,因为在进性擦除之后,这两个函数的函数头部已经完全一致了,由此造成错误。但是,如果你进行这样的修改:
public class TestTheBug
{
public static string method (Pair<String> pairex)
{
System.out.println("Pair string pairex");
return " "
}
public static int method (Pair<Integer> pairex)
{
System.out.println("Pair int pairex");
return 1;
}
}
这两个方法就可以照常运行,因为通过不同的参数列表,通过了编译器,而后又通过不同的返回值,使得虚拟机也可以区分。
7.桥方法不仅用于泛型类型。 在一个方法覆盖另一个方法时可以指定一个更严格的返回类型。例如:
public class Employee implements Cloneable
{
public Employee clone() throws CloneNotSupportedException { ...}
}
Object.clone 和 Employee.clone 方法被说成具有协变的返回类型(covariant returntypes)。 实际上,Employee 类有两个克隆方法:
Employee clone() // defined above
Object clone() // 合成的桥方法,覆盖了原本的Object.clone方法
合成的桥方法中调用了新定义的方法。