Java高级特性—泛型

更新时间:2020-01-15 10:39:09 点击次数:278次
一、什么是泛型?
泛型支持在定义类、接口和方法时将类型作为参数。泛型参数与方法声明中使用的形式参数非常相似,泛型参数使我们可以在不同的输入中重用相同的代码。但与形式参数不同之处在于,形式参数的输入是值,而类型参数的输入是类型。
我们可以将泛型 < T > 理解为占位符,在定义类时不清楚它会是什么类型,但是类中的代码逻辑是通用的,无论类在使用时< T >会被什么参数类型替代。

比如我们经常使用的List集合,在创建时通常都要指定其类型:

//指定泛型类型为String
List<String> stringList = new ArrayList<>();
  stringList.add("hello");
  stringList.add("world");
  //试图添加非String类型元素,编译会报错
  stringList.add(100);

当然我们也可以不指定集合泛型E,但是它会带来隐患:

//未指定元素泛型类型,可以存储任何object类型
List frulist  = new ArrayList();
 frulist .add("apple");
 frulist .add("banana");
 //隐患1:可以加入其它类型元素
 frulist.add(100);
 //隐患2:取出元素时,必须进行类型转换,容易出错
 String str = (String)frulist.get(0);

二、泛型的定义
上面已经提到,泛型支持在定义类、接口和方法时将类型作为参数。下面我们通过例子来看下具体泛型具体使用方式。

(1)泛型类
泛型类的定义方式如下,类型参数 T由< >包裹,紧跟在类名之后,类型参数可以有多个,以英文逗号分割。

class name<T1, T2, ..., Tn> { 
   /* ... */ 
}

知道了泛型类的格式,我们来具体实践下,先定义一个非泛型的类Box,它只有一个Object类型成员变量,同时提供简单的set\get方法

class Box{
    private Object content;
    
    public void set(Object object) {
        this.content = object;
    }
    public Object get() {
        return content;
    }
}

Box类的成员属性content是Object类型,所以我们可以自由的存放任何类型数据,在使用时可能会像下面这样:

       public static void main(String[] args) {
            //创建一个box类,来存放String类型数据
        Box box = new Box();
        box.set("hello world");
        //取值时,都要进行强制类型转换
        String content = (String) box.get();
     
        //.....很多行代码后,不小心存了boolean类型
        box.set(false);
        
        //...很多行代码后,又一个疏忽,带来了ClassCastException
        Integer count = (Integer) box.get();
    }

可以看到,在使用非泛型的Box类时,虽然存放的元素类型非常自由,但也存在很多严重问题,比如我们创建Box类对象,本来是想存放String类型数据,却可能不小心使用box的set()方法存了boolean类型,另外每次使用box.get()取值时,都要进行强制类型转换,很容易遇见java.lang.ClassCastException

这时候我们就可以使用泛型类了,对Box类进行改造,在类的声明时加入泛型参数 < T >,然后在Box类内部就可以使用泛型参数 T 代替原来的Object,set\get方法所使用的参数类型,也都使用 T 来代替,此时我们的Box类就是泛型类了。

class Box <T> {
    private T content;
    
    public void set(T object) {
        this.content = object;
    }
    public T get() {
        return content;
    }
}

这时我们在使用泛型类Box时,指定类型参数< T >的实际类型即可

 public static void main(String[] args) {
        //创建Box类时,指定泛型参数为String
        Box<String> box = new Box();
        box.set("hello world");
        
        //由于指定了泛型,不需要在进行强制类型转换
        String content = box.get();
      
        //不小心存了boolean类型,IDE在编译时会报错
        box.set(false);
    }

到了这里你是否联想到,我们经常使用的集合List< T >,Map < K,V>,例如HashMap的源码中类声明部分:

//HashMap类源码
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
  
   //........../
}    

(2)泛型方法
泛型方法是引入自己的类型参数的方法。类似于声明泛型类型,但是类型参数的范围仅限于声明它的方法。允许使用静态和非静态泛型方法,以及泛型类构造函数。
泛型方法的语法包括尖括号< >内的类型参数列表,该列表出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。
泛型方法长什么样子呢,下面看个例子:

/**
*泛型类Pair,用于创建key-value类型键值对
*类似与JDK中Map内部Entry<K,V>
*/
public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

public class Util {
    /**
    * 泛型方法compare
    * 泛型参数列表<T>出现在必须位于方法的返回值之前。
    * 泛型参数<T>在声明后,才能在方法内部使用       
    * 泛型类中的返回值为T的方法不是泛型方法
    **/
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
   
   /**
   *泛型方法,
   *计算数组T[]中大于指定元素elem的元素数量
   * <T extends Comparable<T> > 是泛型的继承,extends 限定了方法
   * 中使用的<T>必须是Comparable<T>的子类,这样才能在方法里使用compareTo方法
   */
   public static <T extends Comparable<T> > int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

    public static void main(String[] args) {
         Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
//使用泛型方法,并指定参数类型
boolean same = Util.<Integer, String>compare(p1, p2);
    }
}

三、泛型规范
(1)类型参数命名约定
按照惯例,类型参数名称是单个大写字母。便于区分类型变量和普通类或接口名称。最常用的类型参数名有:

T 类型,常用在泛型类、泛型接口上,如java 中的 Comparable< T >
E 元素,在Java集合类中广泛使用,如List< E > , Set< E >
N 数值类型,主要用在数字相关的
K 键,典型的就是Map< K ,V>
V 值,同Map< K ,V>
以上是官方推荐的几种,除此之外,还可以使用S、U等等

(2)泛型通配符与泛型限定
在泛型里代码中,使用 <?> 作为通配符,表示一个未知的类型。
那为什么要使用通配符呢?主要是因为在java中,数组是可以协变的,比如Cat extends Animal,那么Animal[] 与Cat[]是兼容的。而集合是不能协变的,也就是说List < Animal >不是List< Cat >的父类,二者不能相互赋值,这就导致了一个逻辑上问——能够存放父类Animal元素的集合,却不能存放它的子类Cat。

abstract class Animal {
    public abstract void eat();
}
class Cat extends Animal{
    @Override
    public void eat() {}
}
public class TestC{
    public static void main(String[] args) {
        //Animal是Cat父类,数组可以赋值
        Animal[] animal = new Cat[5];
        
        //Animal是Cat父类,但是不意味着List<Animal>集合是List<Cat>父类,
        List<Animal> animals = new ArrayList<>();
        List<Cat> cats = new ArrayList<>();
        
        //下面这行代码会编译失败,编译器无法推断出List<Cat>是List<Animal>的子类
        animals = cats; // incompatible types
    }
}

为了解决上面描述的问题,泛型的通配符就派上用途了。泛型通配符分为三种类型:

无边界配符(Unbounded Wildcards)
< ? >就是无边界通配符,比如List<?> list表示持有某种特定类型对象的List,但是不知道是哪种类型,所以不能add任何类型的对象。它与List list并不相同,List list是表示持有Object类型对象的List,可以add任何类型的对象。

上边界限定的通配符(Upper Bounded wildcards)

<? extends E>, E指是就是该泛型的上边界,表示泛型的类型只能是E类或者E类的子类 ,这里虽然用的是extends关键字, 却不仅限于继承了E的子类, 也可以代指接口E的实现类。
   public static void main(String[] args) {

        // <? extends Animal>限定了泛型类只能是Animal或其子类对象的List
        List<? extends Animal> animals = new ArrayList<>();
        List<Cat> cats = new ArrayList<>();
        //下面代码不会报错,二者引用可以赋值
        animals = cats;
        //但是集合中无法add元素,因为无法确定持有的实际类型,
        animals.add(new Cat());//error
        //从集合中获取对象是是可以的,因为在这个List中,不管实际类型是什么,肯定都能转型为Animal
        Animal animal = animals.get(0);
    }

下边界限定的通配符(Lower Bounded wildcards)

<? super E>,表示泛型的类型只能是E类或者E类的父类,List<? super Integer> list表示某种特定类型(Integer或者Integer的父类)对象的List。可以确定这个List持有的对象类型肯定是Integer或者其父类。
        //某种特定类型(Integer或者Integer的父类)对象的List
        List<? super Integer> list = new ArrayList<>();
        //往list里面add一个Integer或者其子类的对象是安全的,
        //因为Integer或者其子类的对象,都可以向上转型为Integer的父类对象
        list.add(new Integer(1));
        //下面代码编译错误,因为无法确定实际类型,所以往list里面add一个Integer的父类对象是不被允许的
        list.add(new Object());

所以从上面上边界限定的通配符和下边界限定的通配符的特性,可以知道:

对于上边界限定的通配符 <? extends E>,无法向其中加入任何对象,但是可以从中正常取出对象。
对于下边界限定的通配符 <? super E>,,可以存入subclass对象或者subclass的子类对象,但是取出时只能用Object类型变量指向取出的对象。
四、类型擦除
Java 中的的泛型是伪泛型,这是因为泛型信息只存在于代码编译阶段,编译后与泛型相关的信息会被擦除掉,称为类型擦除(type erasure)。
编译器在编译期,会将泛型转化为原生类型。并在相应的地方插入强制转型的代码。什么是原生类型呢?原生类型就是删去类型参数后泛型类的类型名,比如:

   List<String> stringList = new ArrayList<String>();
   List<Integer> integerList = new ArrayList<Integer>();
   //下面的结果为true,因为类型擦除后,二者原生类型都是List
   System.out.println(stringList.getClass() == integerList.getClass());

如果泛型参数中,有限定符则会使用 第一个限定符的类型来替换,比如

class Box <T extends Number> {
    private T content;
   
}

类型擦除后的原生类型变为其限定符类型:

class Box{
    private Number content;
}

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!