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>

posted @ 2019-10-10 23:46  allMayMight  阅读(384)  评论(0编辑  收藏  举报