科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道体验JAVA 5的新增语言特性(2)

体验JAVA 5的新增语言特性(2)

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

Java 5.0发布了,许多人都将开始使用这个JDK版本的一些新增特性。从增强的for循环到诸如泛型(generic)之类更复杂的特性,都将很快出现在您所编写的代码中。我们刚刚完成了一个基于Java 5.0的大型任务,而本文就是要介绍我们使用这些新特性的体验。

作者:Jess Garms, Tim Hanson 来源:IT专家网 2008年5月4日

关键字: 特性 Java 5 体验 java

  • 评论
  • 分享微博
  • 分享邮件
使用泛型

  我们将从两个角度来了解泛型:使用泛型和构造泛型。我们不讨论List、Set和Map的显而易见的用法。知道泛型集合是强大的并且应该经常使用就足够了。

  我们将讨论泛型方法的使用以及编译器推断类型的方法。通常这些都不会出问题,但是当出问题时,错误信息会非常令人费解,所以需要了解如何修复这些问题。

  泛型方法

  除了泛型类型,Java 5还引入了泛型方法。在这个来自java.util.Collections的例子中,构造了一个单元素列表。新的List的元素类型是根据传入方法的对象的类型来推断的:

static <T> List<T> Collections.singletonList(T o)
示例用法:
public List<Integer> getListOfOne() {
    return Collections.singletonList(1);
}

  示例用法:

  在示例用法中,我们传入了一个int。所以方法的返回类型就是List。编译器把T推断为Integer。这和泛型类型是不同的,因为您通常不需要显式地指定类型参数。

  这也显示了自动装箱和泛型的相互作用。类型参数必须是引用类型:这就是为什么我们得到的是List而不是List

  不带参数的泛型方法

  emptyList()方法与泛型一起引入,作为java.util.Collections中EMPTY_LIST字段的类型安全置换:

static <T> List<T> Collections.emptyList()
示例用法:
public List<Integer> getNoIntegers() {
    return Collections.emptyList();
}

  与先前的例子不同,这个方法没有参数,那么编译器如何推断T的类型呢?基本上,它将尝试使用一次参数。如果没有起作用,它再次尝试使用返回或赋值类型。在本例中,返回的是List,所以T被推断为Integer。

  如果在返回语句或赋值语句之外的位置调用泛型方法会怎么样呢?那么编译器将无法执行类型推断的第二次传送。在下面这个例子中,emptyList()是从条件运算符内部调用的:

public List<Integer> getNoIntegers() {
    return x ? Collections.emptyList() : null;
}

  因为编译器看不到返回上下文,也不能推断T,所以它放弃并采用Object。您将看到一个错误消息,比如:“无法将List<Object>转换为List<Integer>。” 为了修复这个错误,应显式地向方法调用传递类型参数。这样,编译器就不会试图推断类型参数,就可以获得正确的结果:

return x ? Collections.<Integer>emptyList() : null;

  这种情况经常发生的另一个地方是在方法调用中。如果一个方法带一个List<String>参数,并且需要为那个参数调用这个传递的emptyList(),那么也需要使用这个语法。
集合之外

  这里有三个泛型类型的例子,它们不是集合,而是以一种新颖的方式使用泛型。这三个例子都来自标准的Java库:

• Class<T>
Class在类的类型上被参数化了。这就使无需类型强制转换而构造一个newInstance成为可能。
• Comparable<T>
Comparable被实际的比较类型参数化。这就在compareTo()调用时提供了更强的类型化。例如,String实现Comparable<String>。对除String之外的任何东西调用compareTo(),都会在编译时失败。
• Enum<E extends Enum<E>>
Enum被枚举类型参数化。一个名为Color的枚举类型将扩展Enum<Color>。getDeclaringClass()方法返回枚举类型的类对象,在这个例子中就是一个Color对象。它与getClass()不同,后者可能返回一个无名类。

      通配符

  泛型最复杂的部分是对通配符的理解。我们将讨论三种类型的通配符以及它们的用途。

  首先让我们了解一下数组是如何工作的。可以从一个Integer[]为一个Number[]赋值。如果尝试把一个Float写到Number[]中,那么可以编译,但在运行时会失败,出现一个ArrayStoreException:

Integer[] ia = new Integer[5];
Number[] na = ia;
na[0] = 0.5; // compiles, but fails at runtime

如果试图把该例直接转换成泛型,那么会在编译时失败,因为赋值是不被允许的:

List<Integer> iList = new ArrayList<Integer>();
List<Number> nList = iList; // not allowed
nList.add(0.5);

  如果使用泛型,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException。

上限通配符

  我们想要的是一个确切元素类型未知的列表,这一点与数组是不同的。

List<Number>是一个列表,其元素类型是具体类型Number。
List<? extends Number>是一个确切元素类型未知的列表。它是Number或其子类型。

上限

  如果我们更新初始的例子,并赋值给List<? extends Number>,那么现在赋值就会成功了:

List<Integer> iList = new ArrayList<Integer>();
List<? extends Number> nList = iList;
Number n = nList.get(0);
nList.add(0.5); // Not allowed

  我们可以从列表中得到Number,因为无论列表的确切元素类型是什么(Float、Integer或Number),我们都可以把它赋值给Number。

  我们仍然不能把浮点类型插入列表中。这会在编译时失败,因为我们不能证明这是安全的。如果我们想要向列表中添加浮点类型,它将破坏iList的初始类型安全——它只存储Integer。

  通配符给了我们比数组更多的表达能力。

为什么使用通配符

  在下面这个例子中,通配符用于向API的用户隐藏类型信息。在内部,Set被存储为CustomerImpl。而API的用户只知道他们正在获取一个Set,从中可以读取Customer。
此处通配符是必需的,因为无法从Set<CustomerImpl>向Set<Customer>赋值:

public class CustomerFactory {
    private Set<CustomerImpl> _customers;
    public Set<? extends Customer> getCustomers() {
        return _customers;
    }
}

通配符和协变返回

  通配符的另一种常见用法是和协变返回一起使用。与赋值相同的规则可以应用到协变返回上。如果希望在重写的方法中返回一个更具体的泛型类型,声明的方法必须使用通配符:
public interface NumberGenerator {
    public List<? extends Number> generate();
}
public class FibonacciGenerator extends NumberGenerator {
    public List<Integer> generate() {
        ...
    }
}

  如果要使用数组,接口可以返回Number[],而实现可以返回Integer[]。

下限

  我们所谈的主要是关于上限通配符的。还有一个下限通配符。List<? super Number>是一个确切“元素类型”未知的列表,但是可能是Mnumber,或者Number的超类型。所以它可能是一个List<Number>或一个List<Object>。

  下限通配符远没有上限通配符那样常见,但是当需要它们的时候,它们就是必需的。

下限与上限

List<? extends Number> readList = new ArrayList<Integer>();
Number n = readList.get(0);

List<? super Number> writeList = new ArrayList<Object>();
writeList.add(new Integer(5));

  第一个是可以从中读数的列表。
  第二个是可以向其写数的列表。

无界通配符

  最后,List<?>列表的内容可以是任何类型,而且它与List<? extends Object>几乎相同。可以随时读取Object,但是不能向列表中写入内容。
公共API中的通配符

  总之,正如前面所说,通配符在向调用程序隐藏实现细节方面是非常重要的,但即使下限通配符看起来是提供只读访问,由于remove(int position)之类的非泛型方法,它们也并非如此。如果您想要一个真正不变的集合,可以使用java.util.Collection上的方法,比如unmodifiableList()。

  编写API的时候要记得通配符。通常,在传递泛型类型时,应该尝试使用通配符。它使更多的调用程序可以访问API。

  通过接收List<? extends Number>而不是List<Number>,下面的方法可以由许多不同类型的列表调用:

void removeNegatives(List<? extends Number> list);

构造泛型类型

  现在我们将讨论构造自己的泛型类型。我们将展示一些例子,其中通过使用泛型可以提高类型安全性,我们还将讨论一些实现泛型类型时的常见问题。
集合风格(Collection-like)的函数

  第一个泛型类的例子是一个集合风格的例子。Pair有两个类型参数,而且字段是类型的实例:

public final class Pair<A,B> {
    public final A first;
    public final B second;

    public Pair(A first, B second) {
        this.first = first;
        this.second = second;
    }
}

  这使从方法返回两个项而无需为每个两种类型的组合编写专用的类成为可能。另一种方法是返回Object[],而这样是类型不安全或者不整洁的。

在下面的用法中,我们从方法返回一个File和一个Boolean。方法的客户端可以直接使用字段而无需类型强制转换:

public Pair<File,Boolean> getFileAndWriteStatus(String path){
    // create file and status
    return new Pair<File,Boolean>(file, status);
}

Pair<File,Boolean> result = getFileAndWriteStatus("...");
File f = result.first;
boolean writeable = result.second;

集合之外

  在下面这个例子中,泛型被用于附加的编译时安全性。通过把DBFactory类参数化为所创建的Peer类型,您实际上是在强制Factory子类返回一个Peer的特定子类型:

public abstract class DBFactory<T extends DBPeer> {
    protected abstract T createEmptyPeer();
    public List<T> get(String constraint) {
        List<T> peers = new ArrayList<T>();
        // database magic
        return peers;
    }
}
通过实现DBFactory<Customer>,CustomerFactory必须从createEmptyPeer()返回一个Customer:
public class CustomerFactory extends DBFactory<Customer>{

    public Customer createEmptyPeer() {
        return new Customer();
    }
}

泛型方法

  不管想要对参数之间还是参数与返回类型之间的泛型类型施加约束,都可以使用泛型方法:

  例如,如果编写的反转函数是在位置上反转,那么可能不需要泛型方法。然而,如果希望反转返回一个新的List,那么可能会希望新List的元素类型与传入的List的类型相同。在这种情况下,就需要一个泛型方法:

<T> List<T> reverse(List<T> list)

具体化

  当实现一个泛型类时,您可能想要构造一个数组T[]。因为泛型是通过擦除(erasure)实现的,所以这是不允许的。

  您可以尝试把Object[]强制转换为T[]。但这是不安全的。

具体化解决方案

  按照泛型教程的惯例,解决方案使用的是“类型令牌”,通过向构造函数添加一个Class<T>参数,可以强制客户端为类的类型参数提供正确的类对象:

public class ArrayExample<T> {
    private Class<T> clazz;

    public ArrayExample(Class<T> clazz) {
        this.clazz = clazz;
    }

    public T[] getArray(int size) {
        return (T[])Array.newInstance(clazz, size);
    }
}

  为了构造ArrayExample<String>,客户端必须把String.class传递给构造函数,因为String.class的类型是Class<String>。
拥有类对象使构造一个具有正确元素类型的数组成为可能。

结束语

  总而言之,新的语言特性有助于从根本上改变Java。通过了解在什么场景下使用以及如何使用这些新特性,您将会编写出更好的代码。

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章