Bootstrap

Java基础|泛型使用姿势和原理你真的搞懂了吗?

1 使用泛型的好处

  • 泛型设计程序的主要目的是:使编写的程序代码可以被多种不同类型的对象所重用。
  • 采取一定的语法规则对这个类型进行适当的限定。
  • 采用适当的限定语法规则,在编译期进行类型转换的安全检查(没有泛型时,强制类型转换安全检查是在运行时期),既提升安全性,也提升了性能。

2 泛型类

  • 泛型类:具有一个或多个类型变量的类。
  • 声明: 声明泛型。(一个类型变量 T ,用尖括号 <> 括起来,放在类名后面,可以有多个类型变量,如 BookStore<T,U>。)
  • 类型变量:T 代表泛型的类型变量。形式上使用大写字母,用具体类型进行替换,就可以实例化泛型类型。
  • 类中定义的泛型类型变量意义:泛型类的类型变量 T,可以是该类某个成员方法的返回类型,以及域或是局部变量类型。

3 泛型方法

  • 泛型方法:具有一个或多个类型变量的方法。
  • 存在于什么类中:泛型方法既可以定义在普通类中,也可以定义在泛型类中。
  • 声明符号的位置: ,位置在修饰符之后,方法返回类型之前。

4 类型变量的限定

  • 类型变量限定:类或方法需要对类型变量加以约束,如限定类型变量 T 为实现了Comparable 接口的类,。
  • 一个类型变量或通配符可以有多个限定,用 & 分隔(T extends Father&Comparable&Serializable)。遵循继承规则,可以拥有至多一个类,和多个接口限定。类必须是限定列表中的第一个。

5 泛型原理(泛型擦除)

  • 虚拟机没有泛型类型对象,只有普通类的对象。无论何时定义一个泛型类,都自动提供一个相应的原始类型。原始类型的名字就是删除类型参数后的泛型类型名。
  • 原始类型的名字确定:泛型类型变量擦除后,用限定类型列表中的第一个类型进行替换;如果没有限定类型,用 Object 替换,注意类的名字还原为原始类的名字
  • 泛型的内部机制原理:泛型变量会被擦除替换;为了保证类型安全性,编译器会在必要时插入强制类型转换;当类型擦除后,导致与多态发生冲突时,编译器会生成一个桥方法来保持多态,桥方法里其实就是做一些强制类型转换。

eg:

public class Book<T extends Comparable & Serializable> implements Serializable{
 private T first;
 private T second;
}

擦除后:

//类的名字还原为原始类的名字
public class Book implements Serializable{
//泛型变量擦除后,按规则替换为对应的原始类型名字
private Comparable firt;
private Comparable second;
}

如果调整顺序为:

<T extends Serializable &  Comparable>
  • 这样做,原始类型用 Serializable 替换 T。编译器会在必要时,加入Comparable 进行强制类型转换。

  • 为了提高效率,应将标记接口(标记接口没有方法。常见有:Serializable->支持序列化标记,Cloneable->可深度拷贝标记,RandomAccess->集合元素可通过索引快速访问标记,数组的数据结构有,链表的就没有。)放在限制列表的末尾。

  • 虚拟机中,用方法的签名和方法的返回类型二者确定一个方法。

  • 编写的程序,根据方法签名和方法入参确定一个方法。

6 通配符类型

  • 通配符类型解决固定的泛型类型(指泛型变量被单一类型实例化)使用的局限性。
  • 通配符类型中,允许类型参数变化。
  • 通配符 ? 同样可以对类型进行限定。可以分为子类型限定;超类型限定;无限定。通配符不是类型变量,因此不能在代码中使用 ? 作为一种类型。
 <? super Book>//指定下限(超类型限定);安全写,指可安全写入Book,以及Book的子类;因为限定最低泛型类型是Book类型
 <? extends Book>//指定上限(子类型限定);安全读,指可把读取的值赋值给Book;因为限定读取到值的最高类型是Book类型,即是Book或Book的子类型
 ? obj;//error ,'?'不能作为一种类型
 
  • 带有超类型限定的通配符可以向泛型对象写入,即可作为入参类型,为方法提供参数,但不益作为返回参数类型(返回的类型只能赋给Object)。(如示例中,可以写入 Book 及其子类型)
  • 带有子类型限定的通配符可以从泛型对象中读取,即可以作为返回参数类型。(如示例中,可以读取 Book 及其子类型)
  • 无限定不等于可以传任何值,相反作为方法的参数时,只能传递 null,作为方法的返回时,只能赋给 Object。

7 泛型局限

  • 1 不能用基本类型实例化类型参数。因为类型擦除之后,一个类对象如Object,不能存储一个基本类型的值如 double 类型。
  • 2 运行时类型查询只适用于原始类型。
//类型查询只适用于原始类型,故下面的示例都错误
 if(b instanceof BookStore<T>)//error
 if(b instanceof BookStore<MathBook>)//error
 //warinning->实际转换为了BookStore原始类型
 BookStore<MathBook> book = (BookStore<MathBook>)b;
 /***.getClass()方法也是返回的是原始类型。**/
 BookStore<ArithmeticBook> aritBook = new BookStore<>();
 BookStore<MathBook> mathBook = new BookStore<>();
 if(aritBook.getClass().equals(mathBook.getClass())) {
  //都是得到原始类型,故一定是相等的。
 }
  • 3 不能创建参数化类型数组(可以创建参数化类型数组,再进行强制转换);
  • 4 可向参数个数可变的方法传递一个泛型类型实例。但会得到一个警告。(可变长度参数实际上就是数组,此时泛型规则对此有所放松。)
  • 5 泛型类型变量,不能被实例化;也不能在静态域或方法中引用类型变量;也不能捕获或抛出泛型变量实例。
  obj = new T();//error
  private static T obj;//error
  try{...}
  catch(T e)//error
  {
      
  }

代码示例-泛型类:

/**
 * 
 * 泛型类:具有一个或多个类型变量的类。
 * 声明:<T> 声明泛型。(一个类型变量T,用尖括号<>括起来,放在类名后面。可以有多个类型变量,eg:BookStore<T,U>。)
 * 类型变量:T 代表泛型的类型变量。形式上使用大写字母。用具体类型进行替换,就可以实例化泛型类型。
 * 类中定义的泛型类型变量意义:泛型类的类型变量T,可以是该类某个成员方法的返回类型,以及域或是局部变量类型。
 */
public class BookStore<T> {
    //T 泛型类型变量作为域变量类型
    private T first;
    private T second;

    /**
     * 构造器
     */
    public BookStore() {
        super();
        first = null;
        second = null;
    }

    /**构造器
     * T 泛型类型变量作为成员方法的局部变量类型
     * @param first
     * @param second
     */
    public BookStore(T first, T second) {
        super();
        this.first = first;
        this.second = second;
    }
    //T 泛型类型变量作为成员方法的返回类型
    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

通用泛型方法工具类:

import java.util.Collection;
import org.apache.commons.collections4.CollectionUtils;
/**
 * @version $Id: BookStoreUtil.java, v 0.1 2017年8月14日 下午12:42:24
 */
public class BookStoreUtil {
    /**
     * @param b
     * @return
     */
    public static boolean hasNulls(BookStore<?> b) {
        return b.getFirst() == null || b.getSecond() == null;
    }

    /**
     * @param b
     */
    public static void swap(BookStore<?> b) {
        swapHelper(b);
    }

    /**
     * 泛型方法
     * @param b
     */
    public static <T> void swapHelper(BookStore<T> b) {
        T t = b.getFirst();
        b.setFirst(b.getSecond());
        b.setSecond(t);
    }

    /**
     * 向参数个数可变的方法,传递一个泛型类型实例。
     * 实际args是一个泛型数组。虚拟机也必须对应建立一个泛型数组。泛型对此有所放松。会给出一个警告。可使用@SafeVarargs标注。
     * @param collection
     * @param args
     * @return
     */
    @SafeVarargs
    public static <T> Collection<T> addAll(Collection<T> collection, T... args) {
        if (CollectionUtils.isNotEmpty(collection) && args.length > 0) {
            //添加
            for (T item : args) {
                collection.add(item);
            }
            return collection;
        }
        return null;
    }
}

测试类

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * @version $Id: Test.java, v 0.1 2017年8月15日 下午12:24:46
 */
public class Test {
    @org.junit.Test
    public void test1() {
        @SuppressWarnings("unchecked")
        /**
         * 创建泛型数组。
         * 直接创建不允许。
         * BookStore<String>[] books = new BookStore<String>[2];///error
         * 可声明通配类型数组,然后进行类型转换。从而达到创建泛型数组。
         * 建议最好使用:ArrayList<T> 集合来满足泛型数组的需求。
        */
        BookStore<String>[] books = (BookStore<String>[]) new BookStore<?>[2];
        books[0] = new BookStore<>("1", "2");
        books[1] = new BookStore<>("3", "4");

        //泛型数组转换为对应的List集合
        List<BookStore<String>> arrays = Arrays.asList(books);
        arrays.forEach(item -> System.out.println(item.getFirst() + "," + item.getSecond()));
        System.out.println("---------");
    }

    @org.junit.Test
    public void test2() {
        Collection<BookStore<String>> collection = new ArrayList<>();
        collection.add(new BookStore<>("lala", "la"));
        @SuppressWarnings("unchecked")
        BookStore<String>[] books = (BookStore<String>[]) new BookStore<?>[2];
        books[0] = new BookStore<>("gaga", "ga");
        books[1] = new BookStore<>("haha", "ha");
        Collection<BookStore<String>> resultCollection = BookStoreUtil.addAll(collection, books[0], books[1]);
        resultCollection.forEach(item -> System.out.println(item.getFirst() + "," + item.getSecond()));
    }
}

测试结果

1,2
3,4
------
lala,la
gaga,ga
haha,ha

8 泛型不可协变

会遇到一个问题:子类 List 不能转换为父类 List。

具体示例:

public static void main(String[] args) {
     OtherClass otherClass = new OtherClass();
     Child child = new Child();
     List<Child> childList = new ArrayList<>();
     childList.add(child);

     otherClass.setParent(child);//ok
     otherClass.setParentList(childList);//报错
     otherClass.setParents(childList);//ok
 }

//
public class OtherClass {
    private Parent parent;
    private List<Parent> parentList;
    private List<? extends Parent> parents;

    public Parent getParent() {
        return parent;
    }
    public void setParent(Parent parent) {
        this.parent = parent;
    }
    public List<Parent> getParentList() {
        return parentList;
    }
    public void setParentList(List<Parent> parentList) {
        this.parentList = parentList;
    }
    public List<? extends Parent> getParents() {
        return parents;
    }
    public void setParents(List<? extends Parent> parents) {
        this.parents = parents;
    }
}

Java 中的泛型是不可协变的,即使子类是父类的子类型,泛型参数类型也不能向上转型
如果要实现这个功能,可以使用通配符来表示一个未知类型,例如 List<?>,或者使用泛型通配符上界 List<? extends 父类类型>,这样就可以接收子类的对象了。

例如,如果有一个父类Animal和一个子类Dog,则可以这样定义一个父类List接收子类List的方法:

public void processList(List<? extends Animal> animals) {
    // 处理animals列表
}

调用方法时,可以传递父类List或子类List:

List<Animal> animals1 = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();

processList(animals1);
processList(dogs);

请注意,processList方法中的参数类型使用了泛型通配符上界<? extends Animal>,这样可以接收Animal及其子类的List对象。但是,在processList方法内部,我们只能使用父类Animal的方法和属性,不能使用子类Dog的特有方法和属性。

;