Java泛型 各类问题(Java编程思想)

我会带着你远行 2023-06-05 11:21 81阅读 0赞

任何基本类型都不能作为类型参数

虽然基本类型不能作为类型参数的具体类型,但通过基本类型对应的包装类,可以通过同样的效果,这样就会用到java的自动拆装箱。

  1. import java.util.*;
  2. public class ListOfInt {
  3. public static void main(String[] args) {
  4. List<Integer> li = new ArrayList<Integer>();//出现在尖括号里的,只能是包装类
  5. for(int i = 0; i < 5; i++)
  6. li.add(i);//自动装箱
  7. for(int i : li)//自动拆箱
  8. System.out.print(i + " ");
  9. }
  10. } /* Output: 0 1 2 3 4 *///:~

自动拆装箱必然会产生性能问题,有开源库的容器类,能够适配基本类型,比如:Org.apache.commons.collectiions.primitives。但咱没用过,咱也不敢问呢。

  1. import java.util.*;
  2. public class ByteSet {
  3. Byte[] possibles = { 1,2,3,4,5,6,7,8,9 };
  4. Set<Byte> mySet =
  5. new HashSet<Byte>(Arrays.asList(possibles));
  6. // But you can't do this:
  7. // Set<Byte> mySet2 = new HashSet<Byte>(
  8. // Arrays.<Byte>asList(1,2,3,4,5,6,7,8,9));
  9. } ///:~

注意,Byte是包装类。asList是个静态的泛型方法,如果泛型方法没有显式地类型说明,那么自动拆装箱会起作用,把int装箱成Integer,这样泛型方法的类型参数就推断为了Integer。但如果你显式地类型说明了(尖括号里的,只能是包装类),那么不好意思,按照类型检查,传入的参数也必须是包装类,此时自动拆装箱不起作用。

  1. import net.mindview.util.*;
  2. //此句一般可以不加,如果你的工程里之前已经导入过java编程思想的例子,而其中的一个例子也有Generator泛型接口
  3. //但Generator的包位置肯定不是net.mindview.util,所以加下面这句,以免编译器误会。不然前两次fill调用编译报错
  4. import net.mindview.util.Generator;
  5. // Fill an array using a generator:
  6. class FArray {
  7. public static <T> T[] fill(T[] a, Generator<T> gen) {
  8. for(int i = 0; i < a.length; i++)
  9. a[i] = gen.next();
  10. return a;
  11. }
  12. public static <T> T testWrap(T t1, T t2) {
  13. return t2;
  14. }
  15. }
  16. public class PrimitiveGenericTest {
  17. public static void main(String[] args) {
  18. String[] strings = FArray.fill(new String[7], new RandomGenerator.String(10));
  19. for(String s : strings)
  20. System.out.println(s);
  21. Integer[] integers = FArray.fill(new Integer[7], new RandomGenerator.Integer());
  22. for(int i: integers)
  23. System.out.println(i);
  24. // Autoboxing won't save you here. This won't compile:
  25. int[] b = FArray.fill(new int[7], new RandomGenerator.Integer());//编译报错
  26. Integer c = FArray.testWrap(1,Integer.valueOf(2));
  27. }
  28. }

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的。

实现参数化接口

  1. interface Payable<T> { }
  2. class Employee implements Payable<Employee> { }
  3. // 编译报错:无法使用以下不同的参数继承Payable: <Hourly> 和 <Employee>
  4. class Hourly extends Employee
  5. implements Payable<Hourly> { }

在继承泛型类或者泛型接口时,如果有两次继承,那么这两次继承的指定类型不一样时,编译会报错。这个很好理解,毕竟指定的类型不同,编译器要做的工作也不一样(比如进行的类型检查、隐式加的类型转换)。

  1. interface Payable<T> { }
  2. class Employee implements Payable { }
  3. class Hourly extends Employee
  4. implements Payable { } ///:~

如果改成,两次继承泛型接口时,都没有指定类型,那么编译不会报错。毕竟,编译器不会做什么工作了,反正两次继承都认为是类型参数是Object,必然也不会冲突了。

转型和警告

在泛型代码内,使用类型参数T进行强制转换没有实际效果(指编译器运行到这里并没有执行强转)。

  1. class FixedSizeStack<T> {
  2. private int index = 0;
  3. private Object[] storage;
  4. public FixedSizeStack(int size) {
  5. storage = new Object[size];
  6. }
  7. public void push(T item) { storage[index++] = item; }
  8. @SuppressWarnings("unchecked")
  9. public T pop() { return (T)storage[--index]; }//这里实际没有发生强转,从字节码可以看到
  10. }
  11. public class GenericCast {
  12. public static final int SIZE = 10;
  13. public static void main(String[] args) {
  14. FixedSizeStack<String> strings =
  15. new FixedSizeStack<String>(SIZE);
  16. for(String s : "A B C D E F G H I J".split(" "))
  17. strings.push(s);
  18. for(int i = 0; i < SIZE; i++) {
  19. String s = strings.pop();
  20. System.out.print(s + " ");
  21. }
  22. }
  23. } /* 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警告。(该配合你演出的我,没有演视而不见)

  1. import java.io.*;
  2. import java.util.*;
  3. public class ClassCasting {
  4. //@SuppressWarnings("unchecked")
  5. public void f(String[] args) throws Exception {
  6. ObjectInputStream in = new ObjectInputStream(
  7. new FileInputStream(args[0]));
  8. List<Integer> shapes = (List<Integer>)in.readObject();
  9. // Won't Compile:
  10. //List<Integer> lw1 = List<Integer>.class.cast(in.readObject());
  11. List lw2 = List.class.cast(in.readObject());
  12. List<Integer> lw3 = List.class.cast(in.readObject());//unchecked assigned warning
  13. }
  14. public static void main(String[] args) { }
  15. } ///:~

首先这个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”)

    1. public T cast(Object obj) {
    2. if (obj != null && !isInstance(obj))
    3. throw new ClassCastException(cannotCastMsg(obj));
    4. return (T) obj;
    5. }

重载

函数重载无法区分泛型类的形参,即使你指定了不同的类型参数,因为类型擦除的原因,两个函数的形参的类型在编译器看来是一样的。

  1. class UseList<W,T> {
  2. void f(List<T> v) { }//编译报错
  3. void f(List<W> v) { }
  4. } ///:~

基类劫持了接口

其实在前面的“实现参数化接口”章节里面的第一个例子,就体现了基类劫持接口。

  1. public class ComparablePet
  2. implements Comparable<ComparablePet> {
  3. public int compareTo(ComparablePet arg) { return 0; }
  4. }
  5. class Cat extends ComparablePet implements Comparable<Cat>{
  6. // Error: Comparable cannot be inherited with
  7. // different arguments: <Cat> and <Pet>
  8. public int compareTo(Cat arg) { return 0; }
  9. } ///:~

编译必然报错,ComparablePet已经实现了Comparable<ComparablePet>,但cat又要实现Comparable<Cat>

发表评论

表情:
评论列表 (有 0 条评论,81人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java编程:类型安全问题

    在Java的泛型编程中,类型安全是一个重要的考虑因素。以下是一些可能引发类型安全问题的情况: 1. 泛型擦除:编译器会在运行时将泛型参数替换为具体的类型。这种行为可能导致类型