Java泛型 各类问题(Java编程思想)
任何基本类型都不能作为类型参数
虽然基本类型不能作为类型参数的具体类型,但通过基本类型对应的包装类,可以通过同样的效果,这样就会用到java的自动拆装箱。
import java.util.*;
public class ListOfInt {
public static void main(String[] args) {
List<Integer> li = new ArrayList<Integer>();//出现在尖括号里的,只能是包装类
for(int i = 0; i < 5; i++)
li.add(i);//自动装箱
for(int i : li)//自动拆箱
System.out.print(i + " ");
}
} /* Output:
0 1 2 3 4
*///:~
自动拆装箱必然会产生性能问题,有开源库的容器类,能够适配基本类型,比如:Org.apache.commons.collectiions.primitives。但咱没用过,咱也不敢问呢。
import java.util.*;
public class ByteSet {
Byte[] possibles = { 1,2,3,4,5,6,7,8,9 };
Set<Byte> mySet =
new HashSet<Byte>(Arrays.asList(possibles));
// But you can't do this:
// Set<Byte> mySet2 = new HashSet<Byte>(
// Arrays.<Byte>asList(1,2,3,4,5,6,7,8,9));
} ///:~
注意,Byte
是包装类。asList是个静态的泛型方法,如果泛型方法没有显式地类型说明,那么自动拆装箱会起作用,把int装箱成Integer,这样泛型方法的类型参数就推断为了Integer。但如果你显式地类型说明了(尖括号里的,只能是包装类),那么不好意思,按照类型检查,传入的参数也必须是包装类,此时自动拆装箱不起作用。
import net.mindview.util.*;
//此句一般可以不加,如果你的工程里之前已经导入过java编程思想的例子,而其中的一个例子也有Generator泛型接口
//但Generator的包位置肯定不是net.mindview.util,所以加下面这句,以免编译器误会。不然前两次fill调用编译报错
import net.mindview.util.Generator;
// Fill an array using a generator:
class FArray {
public static <T> T[] fill(T[] a, Generator<T> gen) {
for(int i = 0; i < a.length; i++)
a[i] = gen.next();
return a;
}
public static <T> T testWrap(T t1, T t2) {
return t2;
}
}
public class PrimitiveGenericTest {
public static void main(String[] args) {
String[] strings = FArray.fill(new String[7], new RandomGenerator.String(10));
for(String s : strings)
System.out.println(s);
Integer[] integers = FArray.fill(new Integer[7], new RandomGenerator.Integer());
for(int i: integers)
System.out.println(i);
// Autoboxing won't save you here. This won't compile:
int[] b = FArray.fill(new int[7], new RandomGenerator.Integer());//编译报错
Integer c = FArray.testWrap(1,Integer.valueOf(2));
}
}
int[] b = FArray.fill(new int[7], new RandomGenerator.Integer())
此句报错,是因为T因为第二个实参已经推断为了Integer了,虽然第一个实参貌似也能推断出int进而通过自动装箱变成Integer,但由于第一个形参的类型是数组,即T[]
。编译器需要第一个形参推断出的int[]
装箱成Integer[]
,但这是编译器不允许的,所以编译器报错。下图为编译报错:
注意,这里是类型推断就已经出错了,跟变量b的类型没关系的。
为了形成对比,我写了testWrap
静态泛型方法,可见非数组的变量进行推断,是可以对变量进行装箱的。Integer c = FArray.testWrap(1,Integer.valueOf(2))
这里,是将第一个实参推断出来的int自动装箱成Integer的。
实现参数化接口
interface Payable<T> {}
class Employee implements Payable<Employee> {}
// 编译报错:无法使用以下不同的参数继承Payable: <Hourly> 和 <Employee>
class Hourly extends Employee
implements Payable<Hourly> {}
在继承泛型类或者泛型接口时,如果有两次继承,那么这两次继承的指定类型不一样时,编译会报错。这个很好理解,毕竟指定的类型不同,编译器要做的工作也不一样(比如进行的类型检查、隐式加的类型转换)。
interface Payable<T> {}
class Employee implements Payable {}
class Hourly extends Employee
implements Payable {} ///:~
如果改成,两次继承泛型接口时,都没有指定类型,那么编译不会报错。毕竟,编译器不会做什么工作了,反正两次继承都认为是类型参数是Object,必然也不会冲突了。
转型和警告
在泛型代码内,使用类型参数T进行强制转换没有实际效果(指编译器运行到这里并没有执行强转)。
class FixedSizeStack<T> {
private int index = 0;
private Object[] storage;
public FixedSizeStack(int size) {
storage = new Object[size];
}
public void push(T item) { storage[index++] = item; }
@SuppressWarnings("unchecked")
public T pop() { return (T)storage[--index]; }//这里实际没有发生强转,从字节码可以看到
}
public class GenericCast {
public static final int SIZE = 10;
public static void main(String[] args) {
FixedSizeStack<String> strings =
new FixedSizeStack<String>(SIZE);
for(String s : "A B C D E F G H I J".split(" "))
strings.push(s);
for(int i = 0; i < SIZE; i++) {
String s = strings.pop();
System.out.print(s + " ");
}
}
} /* Output:
J I H G F E D C B A
*///:~
截取汇编:57: invokevirtual #9 // Method FixedSizeStack.pop:()Ljava/lang/Object;
,可以看到,返回的是一个Object。强转实际发生在String s = strings.pop();
即泛型代码的出口。
虽然用类型参数来强转是假的,但return (T)storage[--index]
这里编译器还是会报一个unchecked cast警告。(该配合你演出的我,没有演视而不见)
import java.io.*;
import java.util.*;
public class ClassCasting {
//@SuppressWarnings("unchecked")
public void f(String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]));
List<Integer> shapes = (List<Integer>)in.readObject();
// Won't Compile:
//List<Integer> lw1 = List<Integer>.class.cast(in.readObject());
List lw2 = List.class.cast(in.readObject());
List<Integer> lw3 = List.class.cast(in.readObject());//unchecked assigned warning
}
public static void main(String[] args) { }
} ///:~
首先这个in.readObject()
返回的对象类型是Object,很明显它需要被强转后再使用。(List<Integer>)in.readObject()
这里会报一个unchecked cast警告。除了直接强转,还可以使用Class对象的cast方法来强转。
List<Integer>.class.cast(in.readObject())
,获得泛型类的Class对象,可以通过类名.class
获得,但不可以在类名后面确定具体类型作为类型参数。原因是泛型只存在于编译期,泛型后面加类型参数只是为了让编译器帮忙做点编译期的工作。再换个角度, 不管是List<Integer>
,还是List<String>
,实际上编译生成的 .class 文件,永远只有一份。- cast函数的源码其实很简单,它只是帮我们加了一个
@SuppressWarnings("unchecked")
,它和直接强转其实没什么区别。如下,cast里面不能强转的话,就帮我们抛出异常;return (T) obj;
也是假的,只是隐式帮你加了强制类型转换。甚至编译器会提示你改成(List)(in.readObject())
。
@SuppressWarnings("unchecked")
public T cast(Object obj) {
if (obj != null && !isInstance(obj))
throw new ClassCastException(cannotCastMsg(obj));
return (T) obj;
}
重载
函数重载无法区分泛型类的形参,即使你指定了不同的类型参数,因为类型擦除的原因,两个函数的形参的类型在编译器看来是一样的。
class UseList<W,T> {
void f(List<T> v) {}//编译报错
void f(List<W> v) {}
} ///:~
基类劫持了接口
其实在前面的“实现参数化接口”章节里面的第一个例子,就体现了基类劫持接口。
public class ComparablePet
implements Comparable<ComparablePet> {
public int compareTo(ComparablePet arg) { return 0; }
}
class Cat extends ComparablePet implements Comparable<Cat>{
// Error: Comparable cannot be inherited with
// different arguments: <Cat> and <Pet>
public int compareTo(Cat arg) { return 0; }
} ///:~
编译必然报错,ComparablePet已经实现了Comparable<ComparablePet>
,但cat又要实现Comparable<Cat>
。