澳门新浦京8455comGson通过借助TypeToken获取泛型参数的类型的方法

引言

Java反射机制是一个非常强大的功能,在很多大型项目比如Spring,
Mybatis都可以看见反射的身影。通过反射机制我们可以在运行期间获取对象的类型信息,利用这一特性我们可以实现工厂模式和代理模式等设计模式,同时也可以解决Java泛型擦除等令人苦恼的问题。本文我们就从实际应用的角度出发,来应用一下Java的反射机制。

 泛型是Java SE
1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
Java语言引入泛型的好处是安全简单。

 

反射基础

p.s:
本文需要读者对反射机制的API有一定程度的了解,如果之前没有接触过的话,建议先看一下官方文档的Quick
Start。

在应用反射机制之前,首先我们先来看一下如何获取一个对象对应的反射类Class,在Java中我们有三种方法可以获取一个对象的反射类。

在Java SE
1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。

最近在使用Google的Gson包进行Json和Java对象之间的转化,对于包含泛型的类的序列化和反序列化Gson也提供了很好的支持,感觉有点意思,就花时间研究了一下。

通过getClass方法

在Java中,每一个Object都有一个getClass方法,通过getClass方法我们可以获取到这个对象对应的反射类:

String s = "ziwenxie";
Class<?> c = s.getClass();

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

由于Java泛型的实现机制,使用了泛型的代码在运行期间相关的泛型参数的类型会被擦除,我们无法在运行期间获知泛型参数的具体类型(所有的泛型类型在运行时都是Object类型)。

通过forName方法

我们也可以调用Class类的静态方法forName

Class<?> c = Class.forName("java.lang.String");

1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。

但是有的时候,我们确实需要获知泛型参数的类型,比如将使用了泛型的Java代码序列化或者反序列化的时候,这个时候问题就变得比较棘手。

使用.class

或者我们也可以直接使用.class

Class<?> c = String.class;

2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

?

获取类型信息

在文章开头我们就提到反射的一大好处就是可以允许我们在运行期间获取对象的类型信息,下面我们通过一个例子来具体看一下。

首先我们在typeinfo.interfacea包下面新建一个接口A

package typeinfo.interfacea;
public interface A { void f(); }

接着我们在typeinfo.packageaccess包下面新建一个接口C,接口C继承自接口A,并且我们还另外创建了几个用于测试的方法,注意下面几个方法的权限都是不同的。

package typeinfo.packageaccess;
import typeinfo.interfacea.A;
class C implements A {
    public void f() { System.out.println("public C.f()"); }
    public void g() { System.out.println("public C.g()"); }
    protected void v () { System.out.println("protected C.v()"); }
    void u() { System.out.println("package C.u()"); }
    private void w() { System.out.println("private C.w()"); }
}
public class HiddenC {
    public static A makeA() { return new C(); }
}

callHiddenMethod()方法中我们用到了几个新的API,其中getDeclaredMethod()根据方法名用于获取Class类指代对象的某个方法,然后我们通过调用invoke()方法传入实际的对象就可以触发对象的相关方法:

package typeinfo;
import typeinfo.interfacea.A;
import typeinfo.packageaccess.HiddenC;
import java.lang.reflect.Method;
public class HiddenImplementation {
    public static void main(String[] args) throws Exception {
        A a = HiddenC.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        // Oops! Reflection still allows us to call g():
        callHiddenMethod(a, "g");
        // And even methods that are less accessible!
        callHiddenMethod(a, "u");
        callHiddenMethod(a, "v");
        callHiddenMethod(a, "w");
    }
    static void callHiddenMethod(Object a, String methodName) throws Exception {
        Method g = a.getClass().getDeclaredMethod(methodName);
        g.setAccessible(true);
        g.invoke(a);
    }
}

从输出结果我们可以看出来,不管是publicdefaultprotect还是pricate方法,通过反射类我们都可以自由调用。当然这里我们只是为了显示反射的强大威力,在实际开发中这种技巧还是不提倡。

public C.f()
typeinfo.packageaccess.C
public C.g()
package C.u()
protected C.v()
private C.w()

3、泛型的类型参数可以有多个。

1
2
3
4
5
6
7
8
class Foo<T> {
  T value;
}
Gson gson = new Gson();
Foo<Bar> foo = new Foo<Bar>();
gson.toJson(foo); // May not serialize foo.value correctly
 
gson.fromJson(json, foo.getClass()); // Fails to deserialize foo.value as Bar

应用实践

我们有下面这样一个业务场景,我们有一个泛型集合类List<Class<? extends Pet>>,我们需要统计出这个集合类中每种具体的Pet有多少个。由于Java的泛型擦除,注意类似List<? extends Pet>的做法肯定是不行的,因为编译器做了静态类型检查之后,到了运行期间JVM会将集合中的对象都视为Pet,但是并不会知道Pet代表的究竟是Cat还是Dog,所以到了运行期间对象的类型信息其实全部丢失了。p.s:
关于泛型擦除:我在上一篇文章里面有详细解释,感兴趣的朋友可以看一看。

为了实现我们上面的例子,我们先来定义几个类:

public class Pet extends Individual {
    public Pet(String name) { super(name); }
    public Pet() { super(); }
}
public class Cat extends Pet {
    public Cat(String name) { super(name); }
    public Cat() { super(); }
}
public class Dog extends Pet {
    public Dog(String name) { super(name); }
    public Dog() { super(); }
}
public class EgyptianMau extends Cat {
    public EgyptianMau(String name) { super(name); }
    public EgyptianMau() { super(); }
}
public class Mutt extends Dog {
    public Mutt(String name) { super(name); }
    public Mutt() { super(); }
}

上面的Pet类继承自IndividualIndividual类的的实现稍微复杂一点,我们实现了Comparable接口,重新自定义了类的比较规则,如果不是很明白的话,也没有关系,我们已经将它抽象出来了,所以不理解实现原理也没有关系。

public class Individual implements Comparable<Individual> {
    private static long counter = 0;
    private final long id = counter++;
    private String name; // name is optional
    public Individual(String name) { this.name = name; }
    public Individual() {}
    public String toString() {
        return getClass().getSimpleName() + (name == null ? "" : " " + name);
    }
    public long id() { return id; }
    public boolean equals(Object o) {
        return o instanceof Individual && id == ((Individual)o).id;
    }
    public int hashCode() {
        int result = 17;
        if (name != null) {
            result = 37 * result + name.hashCode();
        }
        result = 37 * result + (int) id;
        return result;
    }
    public int compareTo(Individual arg) {
        // Compare by class name first:
        String first = getClass().getSimpleName();
        String argFirst = arg.getClass().getSimpleName();
        int firstCompare = first.compareTo(argFirst);
        if (firstCompare != 0) {
            return firstCompare;
        }
        if (name != null && arg.name != null) {
            int secendCompare = name.compareTo(arg.name);
            if (secendCompare != 0) {
                return secendCompare;
            }
        }
        return (arg.id < id ? -1 : (arg.id == id ? 0 : 1));
    }
}

下面创建了一个抽象类PetCreator,以后我们通过调用arrayList()方法便可以直接获取相关Pet类的集合。这里使用到了我们上面没有提及的newInstance()方法,它会返回Class类所真正指代的类的实例,这是什么意思呢?比如说声明new Dog().getClass().newInstance()和直接new Dog()是等价的。

public abstract class PetCreator {
    private Random rand = new Random(47);
    // The List of the different getTypes of Pet to create:
    public abstract List<Class<? extends Pet>> getTypes();
    public Pet randomPet() {
        // Create one random Pet
        int n = rand.nextInt(getTypes().size());
        try {
            return getTypes().get(n).newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
    public Pet[] createArray(int size) {
        Pet[] result = new Pet[size];
        for (int i = 0; i < size; i++) {
           result[i] = randomPet();
        }
        return result;
    }
    public ArrayList<Pet> arrayList(int size) {
        ArrayList<Pet> result = new ArrayList<Pet>();
        Collections.addAll(result, createArray(size));
        return result;
    }
}

接下来我们来实现上面这一个抽象类,解释一下下面的代码,在下面的代码中,我们声明了两个集合类,allTypestypes,其中allTypes中包含了我们呢上面所声明的所有类,但是我们具体的类型实际上只有两种即MuttEgypianMau,所以我们真正需要new出来的宠物只是types中所包含的类型,以后我们通过调用getTypes()便可以得到types中所包含的所哟类型。

public class LiteralPetCreator extends PetCreator {
    @SuppressWarnings("unchecked")
    public static final List<Class<? extends Pet>> allTypes = Collections.unmodifiableList(
        Arrays.asList(Pet.class, Dog.class, Cat.class, Mutt.class, EgyptianMau.class));
    private static final List<Class<? extends Pet>> types = allTypes.subList(
        allTypes.indexOf(Mutt.class), allTypes.size());
    public List<Class<? extends Pet>> getTypes() {
        return types;
    }
}

总体的逻辑已经完成了,最后我们实现用来统计集合中相关Pet类个数的TypeCounter类。解释一下isAssignalbeFrom()方法,它可以判断一个反射类是某个反射类的子类或者间接子类。而getSuperclass()顾名思义就是得到某个反射类的父类了。

public class TypeCounter extends HashMap<Class<?>, Integer> {
    private Class<?> baseType;
    public TypeCounter(Class<?> baseType) {
        this.baseType = baseType;
    }
    public void count(Object obj) {
        Class<?> type = obj.getClass();
        if (!baseType.isAssignableFrom(type)) {
            throw new RuntimeException(
                obj + " incorrect type " + type + ", should be type or subtype of " + baseType);
        }
        countClass(type);
    }
    private void countClass(Class<?> type) {
        Integer quantity = get(type);
        put(type, quantity == null ? 1 : quantity + 1);
        Class<?> superClass = type.getSuperclass();
        if (superClass != null && baseType.isAssignableFrom(superClass)) {
            countClass(superClass);
        }
    }
    @Override
    public String toString() {
        StringBuilder result = new StringBuilder("{");
        for (Map.Entry<Class<?>, Integer> pair : entrySet()) {
            result.append(pair.getKey().getSimpleName());
            result.append("=");
            result.append(pair.getValue());
            result.append(", ");
        }
        result.delete(result.length() - 2, result.length());
        result.append("} ");
        return result.toString();
    }
}

4、泛型的参数类型可以使用extends语句,例如<T extends
superclass>。习惯上称为“有界类型”。 

 

5、泛型的参数类型还可以是通配符类型。例如Class<?> classType =
Class.forName(“java.lang.String”);

对于上面的类Foo<T>,由于在运行期间无法得知T的具体类型,对这个类的对象进行序列化和反序列化都不能正常进行。Gson通过借助TypeToken类来解决这个问题。

泛型擦除以及相关的概念

?

Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

1
2
3
4
5
6
7
8
TestGeneric<String> t = new TestGeneric<String>();
  t.setValue("Alo");
  Type type = new TypeToken<TestGeneric<String>>(){}.getType();
   
  String gStr = GsonUtils.gson.toJson(t,type);
  System.out.println(gStr);
  TestGeneric t1 = GsonUtils.gson.fromJson(gStr, type);
  System.out.println(t1.getValue());

 

 

类型擦除引起的问题及解决方法

TypeToken的使用非常简单,如上面的代码,只要将需要获取类型的泛型类作为TypeToken的泛型参数构造一个匿名的子类,就可以通过getType()方法获取到我们使用的泛型类的泛型参数类型。

澳门新浦京8455com, 1、先检查,在编译,以及检查编译的对象和引用传递的问题

下面来简单分析一下原理。

2、自动类型转换

要获取泛型参数的类型,一般的做法是在使用了泛型的类的构造函数中显示地传入泛型类的Class类型作为这个泛型类的私有属性,它保存了泛型类的类型信息。

3、类型擦除与多态的冲突和解决方法

?

4、泛型类型变量不能是基本数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Foo<T>{
  
 public Class<T> kind;
  
 public Foo(Class<T> clazz){
  this.kind = clazz;
 }
  
 public T[] getInstance(){
  return (T[])Array.newInstance(kind, 5);
 }
  
 public static void main(String[] args){
  Foo<String> foo = new Foo(String.class);
  String[] strArray = foo.getInstance();
 }
 
}

5、运行时类型查询

 

6、异常中使用泛型的问题

这种方法虽然能解决问题,但是每次都要传入一个Class类参数,显得比较麻烦。Gson库里面对于这个问题采用了了另一种解决办法。

7、数组(这个不属于类型擦除引起的问题)

同样是为了获取Class的类型,可以通过另一种方式实现:

9、类型擦除后的冲突

?

10、泛型在静态方法和静态类中的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class Foo<T>{
  
 Class<T> type;
  
 public Foo(){
  this.type = (Class<T>) getClass();
 }
 
        public static void main(String[] args) {
   
  Foo<String> foo = new Foo<String>(){};
  Class mySuperClass = foo.getClass();
 
 }
  
}

 

 

1. 问:Java 的泛型是什么?有什么好处和优点?JDK 不同版本的泛型有什么区别? 

答:泛型是 Java SE 1.5
的新特性,泛型的本质是参数化类型,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。在
Java SE 1.5 之前没有泛型的情况的下只能通过对类型 Object
的引用来实现参数的任意化,其带来的缺点是要做显式强制类型转换,而这种强制转换编译期是不做检查的,容易把问题留到运行时,所以
泛型的好处是在编译时检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率,避免在运行时出现
ClassCastException。

JDK 1.5 引入了泛型来允许强类型在编译时进行类型检查;JDK 1.7
泛型实例化类型具备了自动推断能力,譬如 List<String> list = new
ArrayList<String>(); 可以写成 List<String> llist = new
ArrayList<>(); 了,JDK
具备自动推断能力。下面几种写法可以说是不同版本的兼容性了:

//JDK 1.5 推荐使用的写法
List<String> list = new ArrayList<String>();
//JDK 1.7 推荐使用的写法
List<String> llist = new ArrayList<>();

//可以使用,但不推荐,是为了兼容老版本,IDE 会提示警告,可以通过注解屏蔽警告

List<String> list = new ArrayList();

//可以使用,但不推荐,是为了兼容老版本,IDE 会提示警告,可以通过注解屏蔽警告

List list = new ArrayList<String>(); 

声明一个抽象的父类Foo,匿名子类将泛型类作为Foo的泛型参数传入构造一个实例,再调用getClass方法获得这个子类的Class类型。

 

这里虽然通过另一种方式获得了匿名子类的Class类型,但是并没有直接将泛型参数T的Class类型传进来,那又是如何获得泛型参数的类型的呢,
这要依赖Java的Class字节码中存储的泛型参数信息。Java的泛型机制虽然在运行期间泛型类和非泛型类都相同,但是在编译java源代码成
class文件中还是保存了泛型相关的信息,这些信息被保存在class字节码常量池中,使用了泛型的代码处会生成一个signature签名字段,通过
签名signature字段指明这个常量池的地址。

2. **问:Java 泛型是如何工作的?什么是类型擦除?**

答:泛型是通过类型擦除来实现的,编译器在编译时擦除了所有泛型类型相关的信息,所以在运行时不存在任何泛型类型相关的信息,譬如
List<Integer> 在运行时仅用一个 List 来表示,这样做的目的是为了和
Java 1.5
之前版本进行兼容。泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

 

关于class文件中存储泛型参数类型的具体的详细的知识可以参考这里:

3. 问:Java 泛型类、泛型接口、泛型方法有什么区别?

 答:泛型类是在实例化类的对象时才能确定的类型,其定义譬如 class
Test<T> {},在实例化该类时必须指明泛型 T 的具体类型。

泛型接口与泛型类一样,其定义譬如 interface Generator<E> { E dunc(E
e); }。

泛型方法所在的类可以是泛型类也可以是非泛型类,是否拥有泛型方法与所在的类无关,所以在我们应用中应该尽可能使用泛型方法,不要放大作用空间,尤其是在
static 方法时 static 方法无法访问泛型类的类型参数,所以更应该使用泛型的
static 方法(声明泛型一定要写在 static
后返回值类型前)。泛型方法的定义譬如 <T> void func(T val) {}。 

 

JDK里面提供了方法去读取这些泛型信息的方法,再借助反射,就可以获得泛型参数的具体类型。同样是对于第一段代码中的foo对象,通过下面的代码可以得到foo<T>中的T的类型:

4. 问:Java 如何优雅的实现元组?

 答:元组其实是关系数据库中的一个学术名词,一条记录就是一个元组,一个表就是一个关系,纪录组成表,元组生成关系,这就是关系数据库的核心理念。很多语言天生支持元组,譬如
Python 等,在语法本身支持元组的语言中元组是用括号表示的,如 (int, bool,
string) 就是一个三元组类型,不过在 Java、C
等语言中就比较坑爹,语言语法本身不具备这个特性,所以在 Java
中我们如果想优雅实现元组就可以借助泛型类实现,如下是一个三元组类型的实现:

Triplet<A, B, C> {

   private A a;
   private B a;
   private C a;
   public Triplet(A a, B b, C c) {
       this.a = a;
       this.b = b;
       this.c = c;
   }
}

?

 

1
2
3
Type mySuperClass = foo.getClass().getGenericSuperclass();
  Type type = ((ParameterizedType)mySuperClass).getActualTypeArguments()[0];
System.out.println(type);

5. 问:下面程序块的运行结果是什么,为什么?

Class c1 = new
ArrayList<String>().getClass();

Class c2 = new ArrayList<String>().getClass();

System.out.println(c1 == c2);

答:上面代码段结果为 true,解释如下。

因为 load 的是同一个 class 文件,存在 ArrayList.class 文件但是不存在
ArrayList<String>.class 文件,即便是通过 class.getTypeParameters()
方法获取类型信息也只能获取到 [T]
一样的泛型参数占位符。泛型是通过擦除来实现的,所以编译后任何具体的泛型类型都被擦除了(替换为非泛型上边界,如果没有指定边界则为
Object 类型),泛型类型只有在静态类型检查期间才出现,上面都被擦除成了
ArrayList 类型,所以运行时加载的是同一个 class 文件。

 

 

6. 问:为什么 Java 泛型要通过擦除来实现?擦除有什么坏处或者说代价?

 答:可以说 Java
泛型的存在就是一个不得已的妥协,正因为这种妥协导致了 Java
泛型的混乱,甚至说是 JDK 泛型设计的失败。Java
之所以要通过擦除来实现泛型机制其实是为了兼容性考虑,只有这样才能让非泛化代码到泛化代码的转变过程建立在不破坏现有类库的实现上。正是因为这种兼容也带来了一些代价,譬如泛型不能显式地引用运行时类型的操作之中(如向上向下转型、instanceof
操作等),因为所有关于参数的信息都丢失了,所以任何时候使用泛型都要提醒自己背后的真实擦除类型到底是什么;此外擦除和兼容性导致了使用泛型并不是强制的(如
List<String> list = new ArrayList();
等写法);其次擦除会导致我们在编写代码时十分谨慎(如不想被擦除为 Object
类型时不要忘了添加上边界操作等)。

 

运行结果是class java.lang.String。

7. 问:下面三个 funcX 方法有问题吗,为什么?

class Product<T> {
   private void func1(Object arg) {
       if (arg instanceof T) {}
   }
   private void func2() {
       T var = new T();
   }
   private void func3() {
       T[] vars = new T[3];
   }

}

答:func1、func2、func3 三个方法均无法编译通过。

因为泛型擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道确切类型信息的操作都将无法工作。

 

分析一下这段代码,Class类的getGenericSuperClass()方法的注释是:

 8. 问:下面代码段有问题吗,运行效果是什么,为什么?

ArrayList<Integer> arrayList = new ArrayList<Integer>();

arrayList.add(1);

arrayList.getClass().getMethod(“add”,
Object.class).invoke(arrayList,
“abc”);

for (int i=0; i<arrayList.size(); i++) {

  System.out.println(arrayList.get(i));

}

答:由于在程序中定义的 ArrayList 泛型类型实例化为 Integer
的对象,如果直接调用 add 方法则只能存储整形数据,不过当我们利用反射调用
add 方法时就可以存储字符串,因为 Integer
泛型实例在编译之后被擦除了,只保留了原始类型 Object,所以自然可以插入。

 

Returns the Type representing the direct superclass of the entity
(class, interface, primitive type or void) represented by thisClass.

9. 问:请比较深入的谈谈你对 Java 泛型擦除的理解和带来的问题认识?

答:Java
的泛型是伪泛型,因为在编译期间所有的泛型信息都会被擦除掉,譬如
List<Integer> 在运行时仅用一个 List 来表示(所以我们可以通过反射
add 方法来向 Integer 的泛型列表添加字符串,因为编译后都成了
Object),这样做的目的是为了和 Java 1.5
之前版本进行兼容。泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法,如果类型变量有限定则原始类型就用第一个边界的类型来替换,譬如
class Prd<T extends Comparable & Serializable> {} 的原始类型就是
Comparable),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

先检查再擦除的类型检查是针对引用的,用引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象。可以说这是为了兼容带来的问题,如下:

ArrayList<String> arrayList1 = new ArrayList<String>();  

arrayList1.add("123");    //编译通过
arrayList1.add(123);    //编译错误
String str1 = arrayList1.get(0);    //返回类型是 String
ArrayList<String> arrayList2 = new ArrayList();
arrayList2.add("123");    //编译通过
arrayList2.add(123);    //编译错误
String object2 = arrayList2.get(0);    //返回类型是 String

ArrayList arrayList3 = new ArrayList<String>();

arrayList3.add("123");    //编译通过

arrayList3.add(123);    //编译通过  

Object object3 = arrayList3.get(0);    //返回类型是 Object

所以说擦除前的类型检查是针对引用的,用这个引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象

先检查再擦除带来的另一个问题就是泛型中参数化类型无法支持继承关系,因为泛型的设计初衷就是为了解决
Object
类型转换的弊端而存在,如果泛型中参数化类型支持继承操作就违背了设计的初衷而继续回到原始的
Object 类型转换弊端。也同样可以说这是为了兼容带来的问题,如下:

ArrayList<Object> arrayList1 = new ArrayList<Object>();  

arrayList1.add(new Object());  

arrayList1.add(new Object());  
ArrayList<String> arrayList2 = arrayList1;    //编译错误

ArrayList<String> arrayList3 = new ArrayList<String>();  
arrayList3.add("abc");  

arrayList3.add(new String());  

ArrayList<Object> arrayList4 = arrayList3;    //编译错误

ArrayList<String> arrayList5 = new ArrayList<Object>();    //编译错误

ArrayList<Object> arrayList6 = new ArrayList<String>();    //编译错误

之所以这样我们可以从反面来论证,假设编译不报错则当通过 arrayList2 调用
get() 方法取值时返回的是 String
类型的对象(因为类型检测是根据引用来决定的),而实际上存放的是 Object
类型的对象,这样 get 出来就会 ClassCastException
了,所以这违背了泛型的初衷。对于 arrayList4 同样假设编译不报错,当调用
arrayList4 的 get() 方法取出来的 String 变成了 Object 虽然不会出现
ClassCastException,但是依然没有意义啊,泛型出现的原因就是为了解决类型转换的问题,其次如果我们通过
arrayList4 的 add()
方法继续添加对象则可以添加任意类型对象实例,这就会导致我们 get()
时更加懵逼不知道加的是什么类型了,所以怎么说都是个死循环。

擦除带来的另一个问题就是泛型与多态的冲突,其通过子类中生成桥方法解决了多态冲突问题,这个问题的验证也很简单,可以通过下面的例子说明:

class Creater<T> {  

   private T value;
   public T getValue() {  
       return value;  
   }
   public void setValue(T value) {  
       this.value = value;  
   }  
}
class StringCreater extends Creater<String> {  
   @Override  
   public void setValue(String value) {  
       super.setValue(value);  
   }
   @Override  
   public String getValue() {  
       return super.getValue();  
   }  

}
StringCreater stringCreater =new StringCreater();  
stringCreater.setValue("abc");                  
stringCreater.setValue(new Object());    //编译错误

上面代码段的运行情况很诧异吧,按理来说 Creater 类被编译擦除后 setValue
方法的参数应该是 Object 类型了,子类 StringCreater 的 setValue
方法参数类型为
String,看起来父子类的这组方法应该是重载关系,所以调用子类的 setValue
方法添加字符串和 Object
类型参数应该都是合法才对,然而从编译来看子类根本没有继承自父类参数为
Object 类型的 setValue 方法,所以说子类的 setValue
方法是对父类的重写而不是重载(从子类添加 @Override
注解没报错也能说明是重写关系)。关于出现上面现象的原理其实我们通过 javap
看下两个类编译后的本质即可:

class StringCreater extends Creater<java.lang.String> {

 ......

 public void setValue(java.lang.String);    //重写的setValue方法

   Code:
      0: aload_0
      1: aload_1
      2: invokespecial #2    // Method Creater.setValue:(Ljava/lang/Object;)V
      5: return


 public java.lang.String getValue();    //重写的getValue方法  

   Code:
      0: aload_0
      1: invokespecial #3    // Method Creater.getValue:()Ljava/lang/Object;
      4: checkcast     #4    // class java/lang/String
      7: areturn

 public void setValue(java.lang.Object);    //编译器生成的桥方法,调用我们重写的setValue方法
   Code:
      0: aload_0
      1: aload_1
      2: checkcast     #4    // class java/lang/String
      5: invokevirtual #5    // Method setValue:(Ljava/lang/String;)V
      8: return

 public java.lang.Object getValue();    //编译器生成的桥方法,调用我们重写的getValue方法
   Code:
      0: aload_0
      1: invokevirtual #6    // Method getValue:()Ljava/lang/String;
      4: areturn
}

通过编译后的字节码我们可以看到 Creater 泛型类在编译后类型被擦除为
Object,而我们子类的本意是进行重写实现多态,可类型擦除后子类就和多态产生了冲突,所以编译后的字节码里就出现了桥方法来实现多态。可以看到桥方法的参数类型都是
Object,也就是说子类中真正覆盖父类方法的是桥方法,而子类 String 参数
setValue、getValue 方法上的 @Oveerride
注解只是个假象,桥方法的内部实现是直接调用了我们自己重写的那两个方法;不过上面的
setValue 方法是为了解决类型擦除与多态之间的冲突生成的桥方法,而 getValue
是一种协变,之所以子类中 Object getValue() 和 String getValue()
方法可以同时存在是虚拟机内部的一种区分(我们自己写的代码是不允许这样的),因为虚拟机内部是通过参数类型和返回类型来确定一个方法签名的,所以编译器为了实现泛型的多态允许自己做这个看起来不合法的实现,实质还是交给了虚拟机去区别。

先检查再擦除带来的另一个问题就是泛型读取时会进行自动类型转换问题,所以如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

关于这个可以通过 javap 去查看使用 List 的 add、get
方法后的字节码指令,你会发现 checkcast 指令不是在 get
方法里面强转的(虽然 get 方法里面返回值在代码里面是被转换成了
T,实际编译擦除了),而是在调用处强转的。

擦除带来的另一个问题是泛型类型参数不能是基本类型,比如
ArrayList<int> 是不合法的,只有 ArrayList<Integer>
是合法的,因为当类型擦除后 ArrayList 的原始类型是 Object,而 Object
是引用类型而不是基本类型。

擦除带来的另一个问题是无法进行具体泛型参数类型的运行时类型检查,譬如
arrayList instanceof ArrayList<String> 是非法的,Java
对于泛型运行时检查的支持仅限于 arrayList instanceof ArrayList<?>
方式。 

擦除带来的另一个问题是我们不能抛出也不能捕获泛型类的对象,因为异常是在运行时捕获和抛出的,而在编译时泛型信息会被擦除掉,擦除后两个
catch 会变成一样的东西。也不能在 catch
子句中使用泛型变量,因为泛型信息在编译时已经替换为原始类型(譬如
catch(T) 在限定符情况下会变为原始类型 Throwable),如果可以在 catch
子句中使用则违背了异常的捕获优先级顺序。

这一个题目能说明白和全面泛型基本就掌握百分之九十了。

 

If the superclass is a parameterized type, the Type object returned must
accurately reflect the actual type parameters used in the source code.
The parameterized type representing the superclass is created if it had
not been created before. See the declaration of ParameterizedType for
the semantics of the creation process for parameterized types. If
thisClass represents either theObject class, an interface, a primitive
type, or void, then null is returned. If this object represents an array
class then theClass object representing theObject class is returned

10. 问:为什么 Java 的泛型数组不能采用具体的泛型类型进行初始化?

答:这个问题可以通过一个例子来说明。

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Unsound, but passes run time store check    
String s = lsa[1].get(0); // Run-time error: ClassCastException.

由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList
也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现
ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭
ClassCastException,所以如果 Java
支持泛型数组初始化操作就是搬起石头砸自己的脚。而对于下面的代码来说是成立的:

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Correct.    
Integer i = (Integer) lsa[1].get(0); // OK

所以说采用通配符的方式初始化泛型数组是允许的,因为对于通配符的方式最后取出数据是要做显式类型转换的,符合预期逻辑。综述就是说 Java
泛型数组初始化时数组类型不能是具体的泛型类型,只能是通配符的形式,因为具体类型会导致可存入任意类型对象,在取出时会发生类型转换异常,会与泛型的设计思想冲突,而通配符形式本来就需要自己强转,符合预期。 

关于这道题的答案其 Oracle
官方文档给出了原因:

 

概括来说就是对于带有泛型的class,返回一个ParameterizedType对象,对于Object、接口和原始类型返回null,对于数
组class则是返回Object.class。ParameterizedType是表示带有泛型参数的类型的Java类型,JDK1.5引入了泛型之
后,Java中所有的Class都实现了Type接口,ParameterizedType则是继承了Type接口,所有包含泛型的Class类都会实现
这个接口。

11. 问:下面语句哪些是有问题,哪些没有问题?

List<String>[] list1 = new ArrayList<String>[10];    //编译错误,非法创建
List<String>[] list2 = new ArrayList<?>[10];    //编译错误,需要强转类型
List<String>[] list3 = (List<String>[]) new ArrayList<?>[10];    //OK,但是会有警告

List<?>[] list4 = new ArrayList<String>[10];    //编译错误,非法创建
List<?>[] list5 = new ArrayList<?>[10];    //OK
List<String>[] list6 = new ArrayList[10];    //OK,但是会有警告

答:上面每个语句的问题注释部分已经阐明了,因为在 Java
中是不能创建一个确切的泛型类型的数组的,除非是采用通配符的方式且要做显式类型转换才可以。 

 

实际运用中还要考虑比较多的情况,比如获得泛型参数的个数避免数组越界等,具体可以参看Gson中的TypeToken类及ParameterizedTypeImpl类的代码。

12. 问:如何正确的初始化泛型数组实例?

答:这个无论我们通过 new ArrayList[10]
的形式还是通过泛型通配符的形式初始化泛型数组实例都是存在警告的,也就是说仅仅语法合格,运行时潜在的风险需要我们自己来承担,因此那些方式初始化泛型数组都不是最优雅的方式,我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用
java.lang.reflect.Array.newInstance(Class<T> componentType, int
length) 方法来创建一个具有指定类型和维度的数组,如下:

public class ArrayWithTypeToken<T> {  

   private T[] array;
   public ArrayWithTypeToken(Class<T> type, int size) {  
      array = (T[]) Array.newInstance(type, size);  
   }
   public void put(int index, T item) {  
       array[index] = item;  
   }

   public T get(int index) {  
       return array[index];  
   }

   public T[] create() {  
       return array;  
   }

}

ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();

所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T
在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java
运行时想办法,而运行时能起作用的技术最好的就是反射了。

 

13. 问:Java 泛型对象能实例化 T t = new T() 吗,为什么?

答:不能,因为在 Java
编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于
T 被擦除为 Object,如果可以 new T() 则就变成了 new
Object(),失去了本意。如果要实例化一个泛型 T
则可以通过反射实现(实例化泛型数组也类似),如下:

static <T> T newTclass(Class<T> clazz) throws InstantiationException, IllegalAccessException {  

   T obj = clazz.newInstance();  

   return obj;  

}

原因就不解释了,姑且可以认为和上面泛型数组创建一个原因,至于本质深层次原因请关注后边关于泛型反射面试题的推送。

 

14. 问:什么是 Java 泛型中的限定通配符和非限定通配符?有什么区别?

 答:限定通配符对类型进行限制,泛型中有两种限定通配符,一种是 <?
extends T> 来保证泛型类型必须是 T
的子类来设定泛型类型的上边界,另一种是 <? super T>
来保证泛型类型必须是 T
的父类来设定类型的下边界,泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。非限定通配符
<?>
表示可以用任意泛型类型来替代,可以在某种意义上来说是泛型向上转型的语法格式,因为
List<String> 与 List<Object> 不存在继承关系。

 

15. 问:简单说说 List<Object> 与 List 原始类型之间的区别?

答:主要区别有两点。

  • 原始类型和带泛型参数类型 <Object>
    之间的主要区别是在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用
    Object 作为类型可以告知编译器该方法可以接受任何类型的对象(比如
    String 或 Integer)。

  • 我们可以把任何带参数的类型传递给原始类型 List,但却不能把
    List<String> 传递给接受 List<Object>
    的方法,因为会产生编译错误。

 

16. 问:简单说说 List<Object> 与 List<?> 类型之间的区别?

答:这道题跟上一道题看起来很像,实质上却完全不同。List<?>
是一个未知类型的 List,而 List<Object> 其实是任意类型的
List,我们可以把 List<String>、List<Integer> 赋值给
List<?>,却不能把 List<String> 赋值给
List<Object>。譬如:

List<?> listOfAnyType;

List<Object> listOfObject = new ArrayList<Object>();

List<String> listOfString = new ArrayList<String>();

List<Integer> listOfInteger = new ArrayList<Integer>();

listOfAnyType = listOfString; //legal

listOfAnyType = listOfInteger; //legal

listOfObjectType = (List<Object>) listOfString; //compiler error 

所以通配符形式都可以用类型参数的形式来替代,通配符能做的用类型参数都能做。
通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以能用通配符的就用通配符。
如果类型参数之间有依赖关系或者返回值依赖类型参数或者需要写操作则只能用类型参数。

 

17. 问:List<? extends T>和List <? super T>之间有什么区别?

答:有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符,这两个
List 的声明都是限定通配符的例子,List<? extends T>
可以接受任何继承自 T 的类型的 List,而 List<? super T>
可以接受任何 T 的父类构成的 List。例如 List<? extends Number>
可以接受 List<Integer> 或 List<Float>。Java
容器类的实现中有很多这种用法,比如  Collections 中就有如下一些方法:

public static <T extends Comparable<? super T>> void sort(List<T> list)

public static <T> void sort(List<T> list, Comparator<? super T> c)

public static <T> void copy(List<? super T> dest, List<? extends T> src)

public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)

 

18. 问:说说 <T extends E> 和 <? extends E> 有什么区别?

答:它们用的地方不一样,<T extends E>
用于定义类型参数,声明了一个类型参数
T,可放在泛型类定义中类名后面、接口后面、泛型方法返回值前面。 <?
extends E>
用于实例化类型参数,用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是
E 或 E
的某个子类型。虽然它们不一样,但两种写法经常可以达到相同的目的,譬如:

public void addAll(Bean<? extends E> c)

public <T extends E> void addAll(Bean<T> c)

 

19. 问:说说 List<String> 与 List<Object> 的关系和区别?

答:这两个东西没有关系只有区别。

因为也许很多人认为 String 是 Object 的子类,所以 List<String>
应当可以用在需要 List<Object>
的地方,但是事实并非如此,泛型类型之间不具备泛型参数类型的继承关系,所以
List<String> 和 List<Object> 没有关系,无法转换。

 

20. 问:下面两个代码片段有问题吗,为什么?

//Part
1

List<Object> obj = new ArrayList<Long>(); obj.add(“I love Android!”);

//Part
2

Object[] objArray = new Long[1]; objArray[0] = “I love Android!”;

 答:上面 Part 1 编译出错,Part 2 编译 OK,运行出错。

因为 List<Object> 和 ArrayList<Long> 没有继承关系,而 Java
的数组是在运行时类型检查的。

 

21. 问:如何把 int 值放入 ArrayList<String> list = new ArrayList<String>(); 的 list 列表中?

答:本题实质为泛型的擦除,不过答案比较多,常见的一种是通过兼容性,一种是通过反射的特性来处理。

通过泛型擦除兼容性实现如下:

ArrayList<String> list = new ArrayList<String>();

ArrayList list1 = list;

list1.add(12);
System.out.println(list1.get(0)); 

通过反射实现如下:

ArrayList<String> list = new ArrayList<String>();

Class clazz = list.getClass();

Method m = clazz.getMethod("add", Object.class);

m.invoke(list, 100); 

 

22. 问:泛型擦除到底擦除了哪些信息?

答:这道题就比较有意思和深度了,很多没有深入了解泛型的人可能听到这道题就觉得题出的有问题,因为在他们的认识里泛型信息都被擦除了,怎么还分擦除了哪些信息?难道还分情况?答案是确定的,泛型擦除其实是分情况擦除的,不是完全擦除,一定要消除这个误区。

Java
在编译时会在字节码里指令集之外的地方保留部分泛型信息,泛型接口、类、方法定义上的所有泛型、成员变量声明处的泛型都会被保留类型信息,其他地方的泛型信息都会被擦除。

感兴趣的可以自己编写各种场景的泛型代码然后编译后反编译查看即可发现。

 

23. 问:既然泛型类型在编译时就被擦除了,那类似 Gson 这种 json 解析框架是如何解析数据到泛型类型 Bean 结构的呢? 

答:本题其实一箭双雕,即考察了对于 Gson 框架是否熟悉又考察了 Java
泛型与反射的关系及泛型的实质。

由于在运行期间无法通过 getClass() 得知 T 的具体类型,所以 Gson 通过借助
TypeToken 类来解决这个问题,使用样例如下:

ArrayList<String> list = new ArrayList<String>();

list.add("java");

Type type = new TypeToken<ArrayList<String>>(){}.getType();

String gStr = new Gson().toJson(list, type);

ArrayList<String> gList = new Gson().fromJson(gStr, type);

可以看到 TypeToken 的使用非常简单,只用将需要获取类型的泛型类作为
TypeToken 的泛型参数构造一个匿名的子类就可以通过 getType()
方法获取到我们使用的泛型类的泛型参数类型。

通过上面的使用样例我们会发现使用 Gson 解析转换的 Bean
不存在特殊的构造方法,因此可以排除在泛型类的构造方法中显示地传入泛型类的
Class
类型作为这个泛型类的私有属性来保存泛型类的类型信息的实现方案,所以通过源码分析发现
Gson 使用了另一种方式来获取泛型的类型参数,其方法依赖 Java 的 Class
字节码中存储的泛型参数信息,Java
的泛型机制虽然在编译期间进行了擦除,但是在编译 Java 源代码成 class
文件中还是保存了泛型相关的信息,这些信息被保存在 class
字节码的常量池中,使用了泛型的代码处会生成一个 signature
签名字段,通过签名 signature 字段指明这个常量池的地址,JDK
提供了方法去读取这些泛型信息的方法,然后再借助反射就可以获得泛型参数的具体类型,具体实现原理如下:

Type mySuperClass = new ArrayList<String>(){}.getClass().getGenericSuperclass();
Type type = ((ParameterizedType) mySuperClass).getActualTypeArguments()[0];
System.out.println(type);

所以获取泛型参数类型的实质就是通过 Class 类的 getGenericSuperClass()
方法返回一个 ParameterizedType 对象(对于 Object、接口和原始类型返回
null,对于数组 class 返回 Object.class),ParameterizedType
表示带有泛型参数类型的 Java 类型,JDK1.5 引入泛型后 Java 中所有的 Class
都实现了 Type 接口,ParameterizedType 继承了 Type 接口,所有包含泛型的
Class 类都会自动实现这个接口。 

关于 class
文件中存储泛型参数类型的详细信息可以参考: 

 

24. 问:下面程序的输出是什么?为什么?

public class Demo {

   public static void main(String[] args) throws Exception {

       ParameterizedType type = (ParameterizedType) Bar.class.getGenericSuperclass();
       System.out.println(type.getActualTypeArguments()[0]);
       ParameterizedType fieldType = (ParameterizedType) Foo.class.getField("children").getGenericType();
       System.out.println(fieldType.getActualTypeArguments()[0]);
       ParameterizedType paramType = (ParameterizedType) Foo.class.getMethod("foo", List.class).getGenericParameterTypes()[0];

       System.out.println(paramType.getActualTypeArguments()[0]);
       System.out.println(Foo.class.getTypeParameters()[0].getBounds()[0]);

   }

   class Foo<T extends CharSequence> {

       public List<Bar> children = new ArrayList<Bar>();

       public List<StringBuilder> foo(List<String> foo) {
           return null;
       }
       public void bar(List<? extends String> param) {
           //empty
       }
   }
   class Bar extends Foo<String> {}
}

答:其运行结果如下。

class java.lang.String
class Demo$Bar
class java.lang.String
interface java.lang.CharSequence 

通过上面例子会发现泛型类型的每一个类型参数都被保留了,而且在运行期可以通过反射机制获取到,因为泛型的擦除机制实际上擦除的是除结构化信息外的所有东西(结构化信息指与类结构相关的信息,而不是与程序执行流程有关的,即与类及其字段和方法的类型参数相关的元数据都会被保留下来通过反射获取到)。

 

25. 问:请说说下面代码片段中注释行执行结果和原因?

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a);        //这三行add现象?
numbers.add((Number)a);
numbers.add((Object)a);

public void copyTo(DynamicArray<? super E> dest){

   for(int i=0; i<size; i++){

       dest.add(get(i));    //这行add现象?
   }
} 

答:上面代码段注释行执行情况解释如下。

三个 add 方法都是非法的,无论是 Integer,还是 Number 或
Object,编译器都会报错。因为 ? 表示类型安全无知,? extends Number 表示是
Number 的某个子类型,但不知道具体子类型, 如果允许写入,Java
就无法确保类型安全性,所以直接禁止。

最后方法的 add 是合法的,因为 <? super E> 形式与 <? extends
E> 正好相反,超类型通配符表示 E
的某个父类型,有了它我们就可以更灵活的写入了。

本题特别重要:一定要注意泛型类型声明变量 ?时写数据的规则。

 

26. 问:请说说下面代码片段中注释行执行结果和原因?

Vector<? extends Number> x1 = new Vector<Integer>();    //正确
Vector<? extends Number> x2 = new Vector<String>();    //编译错误

Vector<? super Integer> y1 = new Vector<Number>();    //正确
Vector<? super Integer> y2 = new Vector<Byte>();    //编译错误

答:上面代码编译运行情况如注释所示,本题主要考察泛型中的 ?
通配符的上下边界扩展问题。

通配符对于上边界有如下限制:Vector<? extends 类型1> x = new
Vector<类型2>();
中的类型1指定一个数据类型,则类型2就只能是类型1或者是类型1的子类。

通配符对于下边界有如下限制:Vector<? super 类型1> x = new
Vector<类型2>();
中的类型1指定一个数据类型,则类型2就只能是类型1或者是类型1的父类。

 

27. 问:下面程序合法吗?

class Bean<T super Student> { //TODO }

答:编译时报错,因为 Java 类型参数限定只有 extends 形式,没有 super
形式。

 

28. 问:下面程序有什么问题?该如何修复?

public class Test {  

   public static void main(String[] args) throws Exception{  
       List<Integer> listInteger = new ArrayList<Integer>();  
       printCollection(listInteger);    
   }  
   public static void printCollection(Collection<Object> collection) {  
       for(Object obj:collection){  
           System.out.println(obj);  
       }    
   }  
}

答:语句 printCollection(listInteger);
编译报错,因为泛型的参数是没有继承关系的。修复方式就是使用
?通配符,printCollection(Collection<?> collection),因为在方法
printCollection(Collection<?> collection)
中不可以出现与参数类型有关的方法,譬如
collection.add(),因为程序调用这个方法的时候传入的参数不知道是什么类型的,但是可以调用与参数类型无关的方法,譬如

 collection.size()。 

 

29. 问:请解释下面程序片段的执行情况及原因?

public class Test{
   public static <T> T add(T x, T y){  
       return y;
   }
   public static void main(String[] args) {
       int t0 = Test.add(10, 20.8);
       int t1 = Test.add(10, 20);

       Number t2 = Test.add(100, 22.2);
       Object t3 = Test.add(121, "abc");
       int t4 = Test.<Integer>add(10, 20);

       int t5 = Test.<Integer>add(100, 22.2);
       Number t6 = Test.<Number>add(121, 22.2);
   }
}

答:t0 编译直接报错,add 的两个参数一个是 Integer,一个是
Float,所以取同一父类的最小级为 Number,故 T 为 Number 类型,而 t0
类型为 int,所以类型错误。

  • t1 执行赋值成功,add 的两个参数都是 Integer,所以 T 为 Integer
    类型。

  • t2 执行赋值成功,add 的两个参数一个是 Integer,一个是
    Float,所以取同一父类的最小级为 Number,故 T 为 Number 类型。 

  • t3 执行赋值成功,add 的两个参数一个是 Integer,一个是
    Float,所以取同一父类的最小级为 Object,故 T 为 Object 类型。

  • t4 执行赋值成功,add 指定了泛型类型为 Integer,所以只能 add 为
    Integer 类型或者其子类的参数。

  • t5 编译直接报错,add 指定了泛型类型为 Integer,所以只能 add 为
    Integer 类型或者其子类的参数,不能为 Float。 

  • t6 执行赋值成功,add 指定了泛型类型为 Number,所以只能 add 为 Number
    类型或者其子类的参数,Integer 和 Float 均为其子类,所以可以 add
    成功。 

t0、t1、t2、t3 其实演示了调用泛型方法不指定泛型的几种情况,t4、t5、t6
演示了调用泛型方法指定泛型的情况。
在调用泛型方法的时可以指定泛型,也可以不指定泛型;在不指定泛型时泛型变量的类型为该方法中的几种类型的同一个父类的最小级(直到
Object),在指定泛型时该方法中的几种类型必须是该泛型实例类型或者其子类。切记,java
编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。

 

30. 问:下面两个方法有什么区别?为什么?

public static <T> T get1(T t1, T t2) {  

   if(t1.compareTo(t2) >= 0);

   return t1;  

}  



public static <T extends Comparable> T get2(T t1, T t2) {

   if(t1.compareTo(t2) >= 0);  

   return t1;  

}

答:get1
方法直接编译错误,因为编译器在编译前首先进行了泛型检查和泛型擦除才编译,所以等到真正编译时
T 由于没有类型限定自动擦除为 Object 类型,所以只能调用 Object 的方法,而
Object 没有 compareTo 方法。

get2 方法添加了泛型类型限定可以正常使用,因为限定类型为 Comparable
接口,其存在 compareTo 方法,所以 t1、t2
擦除后被强转成功。所以类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过不管该限定是类还是接口都使用
extends 和 &
符号,如果限定类型既有接口也有类则类必须只有一个且放在首位,如果泛型类型变量有多个限定则原始类型就用第一个边界的类型变量来替换。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图