温故而知新—— Java 的泛型

  |   0 评论   |   0 浏览

JDK1.5 增加泛型支持在很大程度上都是为了让集合能记住其元素的数据类型。在没有泛型之前,一旦把一个对象“丢进”Java集合中,集合就会忘记对象的类型,把所有的对象当成 Object 类型处理。

泛型入门

Java 集合有个缺点 —— 把一个对象 “丢进”集合之后,集合就会"忘记"这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了 Object 类型(其运行时类型没有变)。

这样做带来如下两个问题:

  • 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存 Dog 对象的集合,但程序也可以轻易地将 Cat 对象 "丢"进去,所以可能引发异常。

  • 由于把对象 "丢进" 集合时,集合丢失了对象的状态信息,集合只知道它盛装的 Object,因此取出集合元素后通常还需要强制类型转换。这种强制类型转换既增加了变成的复杂度,也可能引发 ClassCastException 异常。

使用泛型

在Java 5 以后,Java 引入 "参数化类型"的概念,允许程序在创建集合时指定集合元素的类型。Java 的参数化类型被称为 泛型。

image.png

上面代码不仅更加健壮,程序再也不能 "不小心"地把其他对象 "丢进" strList 集合中;而程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。

Java7 泛型的 "菱形" 语法

在Java 7 以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这显得有些多余了。

List<String> strList =  new ArrayList<String>();
Map<String,Integer> scores = new HashMap<String,Integer>();

从 Java 7开始,Java允许在构造器不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java 可以推断尖括号应该是什么泛型信息。

List<String> strList = new ArrayList<>();
Map<String,Integer> scores = new HashMap<>();

"菱形"语法对原有泛型没有改变,只是更好的简化了泛型编程。

深入泛型

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。

定义泛型接口、类

下面是 Java 5 改写后List 接口、Iterator 接口、Map的代码片段

//定义接口时指定了一个类型形参,该形参名为E
public interface List<E>{
    //在该接口里,E 可作为类型是哟个
    // 下面方法可以使用E 作为参数类型
    void add(E x);
    Iterator<E> iterator();  //①
    ...
}

//定义接口时指定了一个类型形参,该形参名 为 E
public interface Iterator<E>{
    //在该接口里E完全可以作为类型使用
   E next();
   boolean hasNext();
   ...
}

//定义该接口时指定了两个类型形参,其形参名为 K、V
public interface Map<K,V>{
   //在该接口里 K、V 完全可以作为类型使用
   Set<K> keySet();    //②
   V put(K key,V value)
   ...
}

泛型允许在定义接口、类是声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方可以使用这种类型形参。

除此之外 ,①②处方法声明返回值类型是 Iterator、Set,这表明 Set 形式是一种特殊数据类型,是一种 与Set不同的数据类型 ——可以认为是 Set 类型的子类。

例如使用 List 类型是,如果为E 形参传入S同日那个类型实参,则产生了一个新的类型: List 类型,可以把List想象成 E被全部替换成 String 的特殊List 子接口。

//List<String> 等同于如下接口
public interface ListString extends List{
 
   //原来的E形参全部变成 String 类型实参
   void add(String x);
   Interator<String> iterator();   
   ...
}

可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)。

//定义 Apple 类时 使用了泛型声明
public class Apple<T>{
   
    //使用T 类型形参定义实例变量
    private T info;
    public Apple(){
  
    }   

    //下面方法中使用T 类型形参来定义构造器
   public Apple(T info){
       this.info = info;
   }

   public void setInfo(T info){
      this.info = info;
   }

    public T getInfo(){
        return this.Info;
    }

    public static void main(String[] args){
        //由于传给T 形参的是String,所以构造器参数只是String
      Apple<String> a1 = new Apple<>("苹果");
      System.out.println(a1.getInfo());
      // 由于 传给 T形参的是Double,所以构造器参数只是Double 或double
      Apple<Double> a2 = new Apple<>(5.67);
      System.out.println(a2.getInfo());
    }
}

从泛型类派生子类

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口、父类时不能再包含类型形参。

//定义类 A 继承Apple 类,Apple 类不能跟类型形参
public class A extends Apple<T>{

}

如果想从 Apple类派生一个子类,则可以改为如下代码

//使用 Apple 类时为T 形参传入String类型
public class A extends Apple<String>{

}

调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时也可以不为类型形参传入实际的类型参数,即下面代码也是正确的。

//使用 Apple 类时,没有为T 形参传入实际的类型参数
public class A extends Apple{

}

类型通配符

正如前面讲的,当使用一个泛型类是,都应该为这个泛型类型传入一个类型实参。如果没有传入类型实参,编译器就会提出泛型警告。例如现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎样定义呢?

image.png

上面程序当然没有问题:这是一段最普通的遍历List 集合的代码。问题是上面程序中 List 是一个有泛型声明的接口,此处使用List 接口时没有传入实际类型参数,这将引起泛型警告。为此,考虑为 List接口传入实际的类型参数 —— 因为 List 集合里的元素类型是不确定的,将上面方法改为如下形式:

public void test(List<Object > c){
  
   for(int i = 0 ; i < c.size();i ++){
         System.out.println(c.get(i));
  }

}

image.png

使用类型通配符

为了表示各种泛型 List 的父类,可以使用类型通配符,类型通配符是一个问号 (?),将一个问号作为类型实参传给 List 集合,写作 :List<?> (意思是元素类型未知的 List)。这个问号(?) 被称为通配符,它的元素类型可以匹配任何类型。可以将上面方法写为如下形式:

public void test(List<?> c){
    for(int i = 0 ; i < c.size();i++){
       System.out.println(c.get(i));
    }
}

现在使用任何类型的List 来调用它,程序依然可以访问集合 c 中的元素,其类型时 Object,这永远是安全的,因为 不管List 的真是类型时什么,它包含的都是 Object。

但这种带通配符的 List 仅表示它是各种泛型List 的父类,并不能把元素加入其中。例如,如下代码将引起编译错误。

List<?> c = new ArrayList<String>();
//下面程序引起编译错误
c.add(new Object());

因为程序无法确定 c 集合中元素的类型,所以不能向其中添加对象。根据前面的List 接口定义的代码可以返现:add() 方法有类型参数E 作为 集合的元素类型,所以传给add 的参数必须是E
类的对象或者子类的对象。

设置类型通配符的上限

被限制的泛型通配符表示如下:

//它表示所有Shape 泛型 List 的父类
List<? extends Shape>

由于无法确定这个受限制的通配符的具体类型,所以不能把 Shape 对象或其子类的对象加入这个泛型集合中。例如,下面代码就是错误的。

image.png

设置类型参数的上限

Java 泛型不仅允许在使用通配符形参时设置上限,而且可以在定义类型形参时设置上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。

image.png

泛型方法

前面介绍了在定义类、接口时可以使用类型形参,在类的方法定义和成员变量定义、接口的方法定义中,这些类型形参可以被当成普通类型来用。在另外一些情况下,定义类、接口时没有使用类型形参,但定义方法时想自己定义类型形参,这也是可以的。

定义泛型方法

假设需要实现这样一个方法 —— 该方法负责将一个 Object 数组的所有元素添加到一个 Collection 集合中。

static void  fromArrayToCollection(Object[] a,Collection<Object > c){
  
   for(Object o : a){
       c.add(o);
   }  

}

上面定义的方法没有任何问题,关键在于方法中的 c形参,它的数据类型是 Collection。Collection 不是Collection的子类型 ——所以这个方法的功能非常有限,它只能将 Object[] 数组的元素复制到元素为 Object(Object 的子类不行)的Collection集合中,即下面 代码将引起编译错误。

String[] strArr = {"a","b"};
List<String> strList = new ArrayList<>();
//Collection<String> 对象不能当成 Collection<Object > 使用,下面代码出现编译错误
fromArrayToCollection(strArr,strList);

为了解决这个问题,可以使用 Java5 提供的泛型方法。所谓泛型方法,就是在声明方法时定义一个或多个类型形参。泛型方法的用法格式如下:

修饰符 <T,S> 返回值类型 方法名(形参列表)
{

}

采用支持泛型的方法,就可以讲上面的 fromArrayToCollection方法改为如下形式:

static <T> void fromArrayToCollection(T[] a,Collection<T> c){
   for(T o : a){
        c.add(o);
   }
}

下面示范了完整的用法。

public class GenericMethodTest {



    //声明一个泛型方法

    static <T> void fromArrayToCollection(T[] a, Collection<T> c){

        for(T o : a){
           c.add(o);
        }
    }

    public static void main(String[] args){
        Object[] oa = new Object[100];
        Collection<Object> co = new ArrayList<>();
        //下面代码中T代表Object 类型
        fromArrayToCollection(oa,co);

        String[] sa = new String[100];
        Collection<String> cs = new ArrayList<>();
        //下面代码中T代表String 类型
        fromArrayToCollection(sa,cs);
        //下面代码中T代表Object 类型
        fromArrayToCollection(sa,co);
    }

}

泛型方法和类型匹配符的区别

大多数时候都可以使用泛型方法来替代类型匹配符。例如,对于Java 的 Collection 接口中两个方法定义:

public interface Collection<E>{
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E>  c);
}

上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式,如下所示。

public interface Collection<E>{
    <T> boolean containsAll(Collection<T> C);
    <T extends E> boolean addAll(Collection<T> c);
}

上面两个方法中类型形参 T 只使用了一次,类型形参T产生的唯一效果是可以在不同的调用点传入不同的实例类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。

泛型方法允许类型形参被用来表示方法的一个或多个桉树之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

Java 7 的 "菱形"语法与泛型构造器

正如泛型方法允许在方法签名中声明类型形参一样,Java 也允许在构造器中声明类型形参,这样就产生了所谓的泛型构造器。
一旦定义了泛型构造器,接下来在调用构造器是,就不仅可以让 Java 根据数据参数的类型来"推断"类型形参的类型,而且程序员也可以显式地为构造器中的类型形参执行实际的类型。如下程序所示.

class Foo{
   public <T> Foo(T t){
      System.out.println(t);
   }
}

public class GenericConstructor{
   public static void main(String[] args){
      //泛型构造器中的 T 参数为String
      new Foo("疯狂 Java 讲义");
      //泛型构造器中的T 参数为 Integer
      new Foo(200);
      //显式指定泛型构造器中的T 参数为String
      //传给 Foo 构造器的实参也是String 对象,完全正确
      new <String> Foo("疯狂 Android 讲义");    //①
       //显式指定泛型构造器中的T 参数为 String,
       // 但传给 Foo 构造器的实参是Double 对象,下面代码出错
       new <String> Foo(12.3);   //②
   }
}

上面程序中① 号代码不仅显示指定泛型构造器中的类型形参T的类型应该是 String,而且程序传给构造器的参数值也是String 类型,因此程序完全正常。但在 ②号代码处,程序显式指定了泛型构造器中的类型形参 T 的类型应该是String,但实际传给该构造器的参数值是 Double 类型,因此这行代码将会出现错误。

前面介绍过 Java 7 新增的 "菱形" 语法,它允许调用该构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛型构造器中声明的类型形参的实际类型,则不可以使用"菱形" 语法。如下程序所示。

class MyClass<E>{
     public <T> MyClass(T t){
        System.out.println("t参数的值为:" + t);
     }
}

public class GenericDiamondTest{
    public static void main(String[] args){
          //MyClass 类声明中的E 形参时 String 类型
          //泛型构造器中声明的T形参时 Integer 类型
         MyClass<String> mc1 = new MyClass<>(5);
         //显式指定泛型构造器中声明的 T 形参是Integer 类型
          MyClass<String> mc2 = new <Integer> MyClass<String>(5);

         //MyClass 类声明中的 E形参是String 类型
         //如果显式指定泛型构造器中声明的T形参时Integer 类型
        // 此时就不能使用"菱形"语法,下面代码是 错的
        // MyClass<String> mc3 = new <Integer> MyClass<>(5);
     }
}

设置 通配符下限

假设自己实现了一个工具方法: 实现将src集合里的元素复制到 dest 集合里的功能,因为 dest 集合可以保存src集合里的所有元素,所以 dest集合元素的类型应该是 src集合元素类型的父类。为了表示两个参数之间的类型依赖,考虑同时使用通配符、泛型参数来实现该方法。

public static <T> void copy(Collection<T> dest,Collection<? extends T > src){
    for(T ele:src){
         dest.add(ele);
    }
}

现在假设该方法需要一个返回值,返回最后一个被复制的元素,则可以把上面方法改为如下形式:

public static <T> T copy(Collection<T> dest,Collection<? extends T> src){
    T last = null;
    for(T ele:src){
         last = ele;
         dest.add(ele);
    }
   return last;
}

表面上看起来,上面方法实现了这个功能,实际上有一个问题:当遍历 src 集合的元素时,src元素的类型是不确定的(只可以肯定它是T 的子类,程序只能用T 来笼统地表示各种 src集合的元素类型。)

List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
//下面代码将引起编译错误
Integer last = copy(ln,li);

对于上面的 copy() 方法,可以这样理解 两个集合参数之间的依赖关系:不管 src集合元素的类型是什么,只要 dest集合元素的类型与前者相同或是前者的父类即可。为了表达这种约束关系,Java 允许设定通配符的下限:<? super Type>,这个通配符表示它必须是 Type本身,或是 Type 的父类。

public class MyUtils{
   //下面  dest 集合元素的类型必须与 src 参数元素的类型相同,或是其父类
    public static <T> T copy(Collection<? super T> dest,Collection<T> src){
       
         T last = null; 
         for(T ele : src){
             last = ele;
             dest.add(ele);
         }
         return last;
    }

    public static void main(String[] args){
         List<Number> ln = new ArrayList<>();
         List<Integer> li = new ArrayList<>();
         li.add(5);
         //此处可准确 地知道最后一个被复制的缘故是Integer 类型
        // 与 src 集合元素的类型相同
        Integer last = copy(ln,li);
        System.out.println(ln);      
   }
}

使用这种语句,就可以保证程序的 ①处调用后推断出 最后一个被复制的元素类型是Integer,而不是笼统的 Number 类型。

泛型方法与方法重载

因为泛型既允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法定义。

public class MyUtils{
     public static<T> void copy(Collection<T> dest,Collection<? extends T> src){...} //①

     public static <T> T copy(Collection<? super T> dest,Collection<T> src){...}   //②

 }

上面的 MyUtils 类中包含两个 copy() 方法,这两个方法的参数列表存在一定的区别,但这种区别不是很明确: 这两个方法的两个参数都是 Collection 对象,前一个集合里的集合元素类型是后一个集合里集合元素类型的父类。如果只是在该类中定义这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。例如,对于如下代码:

List<Number > ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
copy(ln,li);

Java 8 改进的类型推断

Java 8 改进了泛型方法的类型推断能力,类型推断主要有如下两方面。

  • 可通过调用方法的上下文来推断类型参数的目标类型。

  • 可在方法调用链中,将推断得到的类型参数传递到最后一个方法。

如下程序示范了 Java 8 对泛型方法的类型推断。

class MyUtil<E>{
    public static <Z> MyUtil<Z> nil(){
        return null;
    }
    public static <Z>  MyUtil<Z> cons(Z head,MyUtil<Z> tail){
          return null;
     }
     
      E head(){  
          return  null;
      }
    
}

public class InferenceTest{

     public static void main(String[] args){

           //可以通过方法赋值的目标参数来推断类型参数为 String
           // 无须使用下面语句在 调用 nil() 方法时指定类型参数的类型
          // 可调用  cons() 方法所需的参数类型来推断类型参数为 Integer
          MyUtil.cons(42,MyUtil.nil());
          //无须使用下面语句在调用 nil() 方法时指定类型参数的类型
          MyUtil.cons(42.MyUtil.<Integer>nil());
     } 
}

泛型与数组

Java 泛型有一个很重要的设计原则 —— 如果一段代码在编译时没有提出 "[unchecked] 未经检查的 转换警告",则程序在运行时不会引发 ClassCastException 异常。正是基于这个原因,所以数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素类型包含类型变量或类型形参的数组。也就是说,只能声明List[] 形式的数组,但不能创建ArrayList[10]这样的数组对象。


标题:温故而知新—— Java 的泛型
作者:zh847707713
地址:http://lovehao.cn/articles/2019/07/08/1562578511648.html