Java范型那些事(一)

参考资料:http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html

oracle官网介绍:https://docs.oracle.com/javase/tutorial/extra/generics/intro.html

在JDK1.5 加入了范型,范型可以增加代码的稳定性,使得在编译时能检查出更多的潜在bug,尤其是为集合类增加了编译时的类型安全,并减少了类型转换的苦差事。

目录

1. 简介

2. 定义简单的范型

3. 泛型和子类型

4. 通配符

5. 范型方法


1. 简介

泛型允许您抽象类型。最常见的示例是容器类型,例如Collections层次结构中的容器类型。

以下是此类的典型用法:

List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3  

第3行的类型转换有点烦人。通常,程序员知道将哪种数据放入特定列表中。但是,类型转换是必不可少的。编译器只能保证迭代器返回一个Object。为确保赋值为Integer类型的变量是类型安全的,需要强制转换。

当然,类型强转不仅引入了混乱,它还引入了运行时错误的可能性,因为程序员可能会弄错。

如果程序员实际上可以表达他们的意图,并将列表标记为限制包含特定的数据类型,该怎么办?这是范型背后的核心理念。以下是使用泛型给出的上述程序片段的一个版本:

List<Integer> 
    myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'

注意变量myIntList的类型声明,它指定这不仅是一个任意List,而是一个数据类型是Integer的List,写成List <Integer>。我们说List是一个带有类型参数的通用接口 - 在本例中是Integer。我们还在创建列表对象时指定了类型参数。这时候,在取出元素时,不用在强制类型转化。

通过这种方式,编译器现在可以在编译时检查程序的类型正确性。当我们说使用类型List <Integer>声明myIntList时,这告诉我们关于变量myIntList的一些信息,无论何时何地使用它都保持类型不变,并且编译器将保证它。相比之下,强制类型转换只是程序员认为在某一时间点时是正确的。所以,特别是在大型程序中,使用范型是提高了可读性和稳健性。

 

2. 定义简单的范型

以下是java.util包中接口List和Iterator的定义的一小段摘录:


public interfacep  List <E> {
    void add(E x);
    Iterator<E> iterator();
}

public interface Iterator<E> {
    E next();
    boolean hasNext();
}

在简介中,我们看到了泛型类型声明List的调用,例如List <Integer>。在调用(通常称为参数化类型)中,所有出现的形式类型参数(在本例中为E)都被实际类型参数(在本例中为Integer)替换。

⚠️注意:范型的声明类似于接口和类一样,都只有一个文件,不会增加多个代码副本。可以将类型参数理解成方法或者构造函数中的普通形式参数,在调用范型方法时,实际的类型参数将替换形式类型参数。

关于类型参数的命名,建议使用简洁的单个大写字符,在集合类中通常使用 E 来表示元素的类型,List <E>, Map中通常使用Map<K , V>  来表示,再次,请注意形式类型参数的命名约定 - 键为K,值为V

3. 泛型和子类型

通常,如果Foo是Bar的子类型(子类或子接口),并且G是一些泛型类型声明,则G <Foo>不是G <Bar>的子类型。这可能是您需要了解范型最难的事情,因为它违背了我们深刻的直觉。

 所以如果将子类型的范型对象赋值给父类型的范型对象,将会引起编译错误。

官网举了以下这个例子:

List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2 
lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!

编译器会在其中第二行报错,解决办法见下一节 通配符

 

4. 通配符

Java中会遇到类似List< ? extends CustomClass>的上界通配符和List<? super CustomClass>的下界通配符,用以限定类型参数的范围,以下介绍以下为什么会要这样来做。

在JDK 1.5以前的集合遍历代码中,大概是以下样式:

void printCollection(Collection c) {
    Iterator i = c.iterator();
        forfor (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
} 

在JDK1.5引入范型后,可以这样来写:

void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

但这并没有什么比旧版本好到哪去,因为旧版本中,可以使用任意的集合类型作为参数,新版本中,只能使用Collection<Object>作为参数,如上一节中所说,它不是各种集合的超类型!

那么各种集合类的超类型是什么?它写成Collection <?>(发音为“Collection of unknown”),即元素类型与任何东西匹配的集合。由于显而易见的原因,它被称为通配符类型。我们可以写:

void printCollection(Collection <?> c){
    for(Object e:c){
        的System.out.println(E);
    }
}

现在,我们可以用任何类型的集合来调用它。请注意,在printCollection()内部,我们仍然可以从c中读取元素并为它们指定类型Object。这总是安全的,因为无论集合的实际类型如何,它都包含对象。然而,向它添加任意对象是不安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error

由于我们不知道c的元素类型代表什么,我们无法向其添加对象。 add()方法接受类型为E的参数,即集合的元素类型。当实际类型参数是?时,它代表某种未知类型。我们传递给add的任何参数都必须是这种未知类型的子类型。因为我们不知道它是什么类型,所以我们无法传递任何内容。唯一的例外是null,它是每种类型的成员。

另一方面,给定List <?>,我们可以调用get()并使用结果。结果类型是未知类型,但我们始终知道它是一个对象。因此,可以安全地将get()的结果赋给Object类型的变量,或者将其作为期望Object类型的参数传递。

但是在我们的项目中,经常需要限定该集合中可以存放哪些有某一共同特征的类的对象,但又不能使用List<Object>  或者List<?>, 于是就有了有界通配符,如:List <?extends Shape> shapes

其中的?代表一种未知的类型,就像我们之前看到的通配符一样。但是,在这种情况下,我们知道这种未知类型实际上是Shape的子类型。 (注意:它可能是Shape本身,或者是某些子类;它不需要字面上扩展Shape。)我们说Shape是通配符的上限

但是这里使用通配符后,在灵活性上却付出了代价,就是在方法体中添加这样的代码是非法的:

public void addRectangle(List <?extends Shape> shapes){
    //编译时错误!
    shapes.add(0,new Rectangle());
}

⚠️注意:您应该能够弄清楚为什么不允许上面的代码。 shapes.add()的第二个参数的类型是? extends Shape-- Shape的未知子类型。由于我们不知道它是什么类型,我们不知道它是否是Rectangle的超类型;它可能是也可能不是这样的超类型,所以在那里传递一个Rectangle是不安全的,但有界通配符却是可以解决传递进来的参数被限定为某些类型的解决方案。

5. 范型方法

先从一个例子说起,从一个数组中读取对象,存放到集合中,第一版代码如下:

static voidstatic void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) { 
        c.add(o); // compile-time error
    }
}     

现在你应该不会像初学者一样,再使用 Collection<Object>作为集合的类型参数,但是使用Collection<?>同样也不行,如之前所说,unknown类型的集合,无法向其中添加元素。

这时候就可以使用范型方法来解决该问题,同范型类的声明一样,方法声明也可以是范型的,即使用一个或多个参数化的类型

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

按照上面的范型方法,然后我们可以使用任何元素类型是数组元素类型的超类型的集合来调用该方法。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();

// T 被推断为 Object 类型
fromArrayToCollection(oa, co); 

String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();

// T 被推断为 String
fromArrayToCollection(sa, cs);

// T 被推断为 Object
fromArrayToCollection(sa, co);

Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();

// T 被推断为 Number
fromArrayToCollection(ia, cn);

// T 被推断为 Number
fromArrayToCollection(fa, cn);

// T 被推断为 Number
fromArrayToCollection(na, cn);

// T 被推断为 Object
fromArrayToCollection(na, co);

// 编译报错
fromArrayToCollection(na, cs);

⚠️注意:我们不必将实际类型参数传递给泛型方法。编译器根据实际参数的类型为我们推断类型参数。它通常会推断出使得调用时类型安全的最具体的类型参数。

出现的一个问题是:何时应该使用泛型方法,何时应该使用通配符类型?为了理解答案,我们来看一下Collection库中的一些方法。

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

我们也可以使用范型方法替代上面的代码:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // 嘿,类型变量也可以有界限!
}

但是,在containsAll和addAll中,类型参数T仅使用一次。返回类型不依赖于类型参数,也不依赖于方法的任何其他参数(在上述情况下,只有一个参数)。这告诉我们类型参数用于多态;它唯一的作用是允许在不同的调用站点使用各种实际的参数类型。如果是这种情况,则应使用通配符。通配符旨在支持灵活的子类型,这是我们在此尝试表达的内容。

范型方法允许使用类型参数来表示方法和/或其返回类型的一个或多个参数的类型之间的依赖关系。如果没有这种依赖关系,则不应使用范型方法。可以串联使用范型方法和通配符。这是方法Collections.copy():

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

注意两个参数类型之间的依赖关系。从源列表src复制的任何对象必须可分配给目标列表的元素类型T,dst。所以src的元素类型可以是T的任何子类型 - 我们不关心哪个。copy方法使用类型参数表示依赖关系,但对第二个参数的元素类型使用了通配符。

我们可以用另一种方式为这种方法编写签名,而不使用通配符:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

这很好,但是第一个类型参数T既在dst的类型中使用,也在第二个类型参数S的边界中使用,且S本身只使用一次(即在src的类型中 - 没有别的依赖它)。这表明我们可以用通配符替换S。使用通配符比声明明确的类型参数更清晰,更简洁,因此应尽可能优先使用。

通配符还具有以下优点:它们可以在方法签名之外使用,如字段类型,局部变量和数组。如:

static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>();

public void drawAll(List<? extends Shape> shapes) {
    history.addLast(shapes);
    for (Shape s: shapes) {
        s.draw(this);
    }
}

最后,再次让我们注意用于类型参数的命名约定。我们使用T作为类型,只要没有更具体的类型来区分它。范型方法通常就是这种情况。如果有多个类型参数,我们可能会使用字母表中与T相邻的字母,例如S。如果泛型方法出现在泛型类中,最好避免对方法的类型参数使用相同的名称,以避免混淆。这同样适用于嵌套泛型类。

猜你喜欢

转载自blog.csdn.net/unicorn97/article/details/81813712