避免在 Java 接口中使用数组的 3 个理由

jopen 10年前

避免在 Java 接口中使用数组的 3 个理由

如果你发现在一个接口使用有如下定义方法:

public String[] getParameters();

那么你应该认真反思。数组不仅仅老式,而且我们有合理的理由避免暴露它们。在这篇文章中,我将试图总结在Java API中使用数组的缺陷。首先从最出人意料的一个例子开始。

数组导致性能不佳

你可能认为使用数组是最快速的,因为数组是大多数collection实现的底层数据结构。使用一个纯数组怎么会比使用一个包含数组的对象性能更低?

让我们先从这个看起来很熟悉的普遍的习惯用法开始:

public String[] getNames() {    return namesList.toArray( new String[ namesList.size() ] );  }

这个方法从一个用来在其内部保存数据的可变集合处创建了一个数据. 它通过提供一个确切大小的数组来尝试优化数组的创建. 有趣的是,这一“优化”使得其比下面的更简单的版本速度还要慢(请看图表中绿色VS橘色条):

public String[] getNames() {    return namesList.toArray( new String[ 0 ] );  }

不过,如果方法返回的是一个List, 创建防御式的副本又更加的快了 (红条):

public List<String> getNames() {    return new ArrayList( namesList );  }

不同之处在于一个ArrayList将它的数据项放在一个Object[]数组中,并且使用的是无类型的toArray方法,其比有类型的方法要快很多(蓝条). 这是类型安全的,因为无类型的数组时封装在由编译器检查的泛型类型ArrayList<T>中的.

toArray 3 Good Reasons to Avoid Arrays in Java Interfaces

这个图标展示了一个在Java 7上n=5的参考标准. 不过,更多的数据项或者是另外一个VM情况系啊,这幅图片并不会改变太多. CPU的开销可能并不会太剧烈,但是会有增长. 机会有一个数组的使用者应该将其转换到一个集合中去,以便利用它做任何事情, 然后将结果转换回一个数组,来送进另外一个接口的方法中,诸如此类做法.

是用一个简单的ArrayList,而不是一个数组来提升性能,无需再动太多的手脚. ArrayList 为封装的数组增加了32字节的恒定开销. 例如,一个有十个对象的数组需要104字节,一个ArrayList 136字节.

使用 集合,你甚至可能决定返回内部列表的一个不可修改的版本:

public List<String> getNames() {    return Collections.unmodifiableList( namesList );  }

此操作会在固定的市价运行,因此他比任何上述其它的方法都要快很多(黄条). 其同一个防御式的拷贝不同。一个不可修改的集合将会在你的内部数据变化时跟着变化。如果变化发生了,客户端会在迭代数据项时运行到一个ConcurrentModificationException中. 可以认为它是一个糟糕的设计,接口提供了一个在运行时抛出一个UnsupportedOperationException. 不过,至少对于内部的使用,这个方法对于一个防御式的拷贝而言,会是一个高性能的选择 - 一些不可能使用数组实现的东西.

数组定义一个结构,而不是一个接口

Java 是一门面向对象的语言。面向对象的核心概念就是提供一些方法来访问和操作它们的数据,而不是直接对数据域进行操作. 这些方法创建一个接口来描述你可以在对象上面做的事情.

由于java已经对性能做了设计,原生类型和数组已经被融合进了类型系统之中. 对象可以使用数组来在内容高效地存储数据. 然而,即使通过数组来呈现一个可变集合的元素,它们也不会提供任何方法来访问和操作这些元素. 事实上,除了直接访问的替换元素之外,在数组上你没有多少其它事情可以做. 数组甚至连toString 和 equals 都没有一个有意义的实现, 而集合却有:

String[] array = { "foo", "bar" };  List<String> list = Arrays.asList( array );     System.out.println( list );  // -> [foo, bar]  System.out.println( array );  // -> [Ljava.lang.String;@6f548414     list.equals( Arrays.asList( "foo", "bar" ) )  // -> true  array.equals( new String[] { "foo", "bar" } )  // -> false

不同于数组,集合的 API 提供了许多有用的方法来访问元素. 用户可以检查包含的元素,提取子列表或者计算交集. 集合可以向数据层添加特定的特性, 诸如线程安全,同时将实现原理保持在内部可见.

通过使用一个数据,你定义了数据被保存在内存中的哪个地方. 通过使用一个集合,你定义了用户可以在数据上做的操作.

数组不是类型安全的

如果你依赖于编译器检查的类型安全,小心对象数组. 下面的代码会在运行时奔溃,但是编译器找不出问题所在:

Number[] numbers = new Integer[10];  numbers[0] = Long.valueOf( 0 ); // throws ArrayStoreException

原因是数组是“协变式”的, 比如,如果 T 是S 的一个子类型, 那么 T[] 就会是 S[] 的一个子类型. Joshua Bloch 在其著作 Effective Java 涵盖了所有的理论, 每一个Java开发者必读.

归因于这个行为,暴露数组类型的接口允许返回声明数组类型的一个子类型, 导致了一个怪异的运行时异常.

Bloch 同时也解释说,数组与泛型类型不兼容. 因为数组会在运行时强制要求有类型信息,而泛型则会在编译时被检查,泛型类型不能被放到数组中.

一般而言,数组和泛型不能很好的融合。如果你发现自己在融合它们而得到了一个编译时错误或者警告,那你的第一反应应该是用list去替换数组.

- Joshua Bloch, Effective Java (第二版), 第29条

总结

数组底层的语言构造、它们会被用在实现中,但是它们不应该想其它的类暴露. 在一个接口方法中使用数组违背了面向对象的原则,它会导致违和的API,并且它也可能给类型安全和性能造成短板.