聊聊 Java 泛型

我正在参加「掘金·启航计划」

概述

什么是泛型?为什么需要泛型?如何使用?是什么原理?如何改进? 这基本上就是我们学习一项技术的正确套路,本文将按照以上顺序展开阐述,介绍我所了解的泛型。

什么是泛型

泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

为什么需要泛型

Java中引入泛型最主要的目的是将类型检查工作提前到编译时期,将**类型强转(cast)**工作交给编译器,从而让你在编译时期就获得类型转换异常以及去掉源码中的类型强转代码。例如

泛型作用的对象

泛型有三种使用方式,分别为:泛型类、泛型接口和泛型方法。

泛型类

在类的申明时指定参数,即构成了泛型类,例如下面代码中就指定T为类型参数,那么在这个类里面就可以使用这个类型了。例如申明T类型的变量name,申明T类型的形参param等操作。

public class Generic<T> {
    public T name;
    public Generic(T param){
        name=param;
    }
    public T m(){
        return name;
    }
}

那么在使用类时就可以传入相应的类型,构建不同类型的实例,如下面代码分别传入了String,Integer,Boolean3个类型:

private static void genericClass()
 {
     Generic<String> str=new Generic<>("总有刁民想害朕");
     Generic<Integer> integer=new Generic<>(110);
     Generic<Boolean> b=new Generic<>(true);

     System.out.println("传入类型:"+str.name+"  "+integer.name+"  "+b.name);
}

输出结果为:传入类型:总有刁民想害朕 110 true

如果没有泛型,我们想要达到上面的效果需要定义三个类,或者一个包含三个构造函数,三个取值方法的类。

泛型接口

泛型接口与泛型类的定义基本一致

public interface Generator<T> {
    public T produce();
}

泛型方法

这个相对来说就比较复杂,当我首次接触时也是一脸懵逼,抓住特点后也就没有那么难了。

public class Generic<T> {
    public T name;
    public  Generic(){}
    public Generic(T param){
        name=param;
    }
    public T m(){
        return name;
    }
    public <E> void m1(E e){ }
    public <T> T m2(T e){ }
}

重点看public <E> void m1(E e){ }这就是一个泛型方法,判断一个方法是否是泛型方法关键看方法返回值前面有没有使用<>标记的类型,有就是,没有就不是。这个<>里面的类型参数就相当于为这个方法声明了一个类型,这个类型可以在此方法的作用块内自由使用。 上面代码中,m()方法不是泛型方法,m1()m2()都是。值得注意的是m2()方法中声明的类型T与类申明里面的那个参数T不是一个,也可以说方法中的T隐藏了类型中的T。下面代码中类里面的T传入的是String类型,而方法中的T传入的是Integer类型。

Generic<String> str=new Generic<>("总有刁民想害朕");
str.m2(123);

通配符?

?代表任意类型,例如有如下函数:

public void m3(List<?>list){
    for (Object o : list) {
        System.out.println(o);
    }
}

其参数类型是,那么我们调用的时候就可以传入任意类型的List,如下

str.m3(Arrays.asList(1,2,3));
str.m3(Arrays.asList("总有刁民","想害","朕"));

但是说实话,单独一个意义不大,因为大家可以看到,从集合中获取到的对象的类型是Object 类型的,也就只有那几个默认方法可调用,几乎没什么用。如果你想要使用传入的类型那就需要强制类型转换,这是我们接受不了的,不然使用泛型干毛。其真正强大之处是可以通过设置其上下限达到类型的灵活使用,且看下面分解重点内容

通配符上界

通配符上界使用<? extends T>的格式,意思是需要一个T类型或者T类型的子类,一般T类型都是一个具体的类型,例如下面的代码。

public void printIntValue(List<? extends Number> list) {  
    for (Number number : list) {  
        System.out.print(number.intValue()+" ");   
    }  
}

这个意义就非凡了,无论传入的是何种类型的集合,我们都可以使用其父类的方法统一处理。

通配符下界

通配符下界使用<? super T>的格式,意思是需要一个T类型或者T类型的父类,一般T类型都是一个具体的类型,例如下面的代码。

public void fillNumberList(List<? super Number> list) {  
    list.add(new Integer(0));  
    list.add(new Float(1.0));  
}

至于什么时候使用通配符上界,什么时候使用下界,在**《Effective Java》中有很好的指导意见:遵循PECS原则,即producer-extends,consumer-super.** 换句话说,如果参数化类型表示一个生产者,就使用 <? extends T>;如果参数化类型表示一个消费者,就使用<? super T>

Java泛型原理解析

为什么人们会说Java的泛型是伪泛型呢,就是因为Java在编译时擦除了所有的泛型信息,所以Java根本不会产生新的类型到字节码或者机器码中,所有的泛型类型最终都将是一种原始类型,那样在Java运行时根本就获取不到泛型信息。

擦除

Java编译器编译泛型的步骤:

  1. 检查泛型的类型 ,获得目标类型
  2. 擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换)
  3. 调用相关函数,并将结果强制转换为目标类型。

如何擦除: 当擦除泛型类型后,留下的就只有原始类型了,例如上面的代码,原始类型就是ArrayList。擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换),如下所示

擦除之前:

//泛型类型  
class Pair<T> {    
    private T value;    
    public T getValue() {    
        return value;    
    }    
    public void setValue(T  value) {    
        this.value = value;    
    }    
}

擦除之后:

//原始类型  
class Pair {    
    private Object value;    
    public Object getValue() {    
        return value;    
    }    
    public void setValue(Object  value) {    
        this.value = value;    
    }    
}

因为在Pair<T>中,T是一个无限定的类型变量,所以用Object替换。如果是Pair<T extends Number>,擦除后,类型变量用Number类型替换。

Gson是如何把泛型转换成目标类型的

TypeToken

/**
 * Represents a generic type {@code T}. Java doesn't yet provide a way to
 * represent generic types, so this class does. Forces clients to create a
 * subclass of this class which enables retrieval the type information even at
 * runtime.
 *
 * <p>For example, to create a type literal for {@code List<String>}, you can
 * create an empty anonymous inner class:
 *
 * <p>
 * {@code TypeToken<List<String>> list = new TypeToken<List<String>>() {};}
 *
 * <p>This syntax cannot be used to create type literals that have wildcard
 * parameters, such as {@code Class<?>} or {@code List<? extends CharSequence>}.
 *
 * @author Bob Lee
 * @author Sven Mawson
 * @author Jesse Wilson
 */
public class TypeToken<T> {
  final Class<? super T> rawType;
  final Type type;
  final int hashCode;

  /**
   * Constructs a new type literal. Derives represented class from type
   * parameter.
   *
   * <p>Clients create an empty anonymous subclass. Doing so embeds the type
   * parameter in the anonymous class's type hierarchy so we can reconstitute it
   * at runtime despite erasure.
   */
  @SuppressWarnings("unchecked")
  protected TypeToken() {
    this.type = getSuperclassTypeParameter(getClass());
    this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
    this.hashCode = type.hashCode();
  }
  ......
}
public <T> T fromJson(JsonElement json, Type typeOfT) throws JsonSyntaxException {
  if (json == null) {
    return null;
  }
  return (T) fromJson(new JsonTreeReader(json), typeOfT);
}
//只要调用fromJson传入一个TypeToken的子类就能获取到泛型的类型
new Gson().fromJson(json,new TypeToken<ArrayList<String>>(){}.getType());

原理

子类如果实例化这个泛型,则编译时会保留实例化类型,可以通过反射获取,如:

Generic<String> generic = new Generic<String>(){ };
System.out.println(generic.getClass().getGenericSuperclass());

以上类型会输出:com.hencoder.layoutlayout.Generic<java.lang.String>

猜你喜欢

转载自juejin.im/post/7233720284708044859
今日推荐