概要:你可以在任何地方使用Java集合框架,但不要想当然地使用它们。集合框架有神秘之处,如果你不能正确地对待它,它就会为你惹麻烦。Ted Neward探索了Java集合框架API中复杂且可变的部分,还给出了一些帮助你更好地利用Iterable,HashMap和SortedSet的窍门,这些窍门将会使你的代码不会产生Bug。
设计java.util包中集合框架类的目的就是帮助,也即替代数组,这也就提高了Java的能力。如你在上一篇文章中所学习到的,它们仍具可塑性,它们希望以不同的途径,好的方式,整洁的代码去进行定制和扩展。
集合框架仍然强大,但它是可变的:要小心使用之,若滥用之则会使你陷入危机中。
1. List不同于数组
Java开发者经常错误地猜想ArrayList只是Java数组的替代品。集合框架的背后就是数组,这就使得在集合对象中随机地查找元素时能有好的性能。另外,如同数组那样,集合对象使用整数序数去获取特定元素。即便如此,集合对象仍不是数组的简易替代品。
将集合对象与数组区分开来的技巧就是要知道顺序与位置之间的区别。例如,List是一个接口,它为置入集合中的元素维护了顺序,如清单1所示:
清单1. Mutable keys
import java.util.*;
public class OrderAndPosition
{
public static <T> void dumpArray(T[] array)
{
System.out.println("=============");
for (int i=0; i<array.length; i++)
System.out.println("Position " + i + ": " + array[i]);
}
public static <T> void dumpList(List<T> list)
{
System.out.println("=============");
for (int i=0; i<list.size(); i++)
System.out.println("Ordinal " + i + ": " + list.get(i));
}
public static void main(String[] args)
{
List<String> argList = new ArrayList<String>(Arrays.asList(args));
dumpArray(args);
args[1] = null;
dumpArray(args);
dumpList(argList);
argList.remove(1);
dumpList(argList);
}
}
当删除上面List中的第三个元素时,该元素"下面"的其它元素会向上移动以填补空位。很清楚,集合对象的行为不同于数组。(事实上,从数组中删除一个元素与从List中删除一个元素大为不同--从数组中"删除"一个元素就是用一个新的引用变量或null去覆盖该元素所处的位置。)
2. 迭代器,令我大为吃惊!
毫无疑问,Java开发者喜欢Java集合框架中的Iterator,但你最后一次看到Iterator接口是在什么时候呢?可以这么说,多数时候,我们只是将Iterator置入for循环或改进的for循环中。
但对于那些善于挖掘的人,Iterator内藏两大惊人之处:
第一,通过调用Iterator本身的remove()方法,Iterator拥有了从来源集合对象中安全地删除元素的能力。此处的关键点在于避免了 ConcurrentModifiedException,顾名思意:当迭代器正在遍历集合对象时,又正在修改该集合。一些集合对象不会让你向正在被遍历的集合中删除或添加元素,但调用Iterator的remove()方法是一个安全的实践方式。
第二,Iterator支持派生出的(且功能更强大的)兄弟。ListIterator,它只存在于List实例中,支持在遍历过程中向List中添加和删除元素,并且能双向滚动(bidirectional scrolling)List对象。
双向滚动(bidirectional scrolling)在某些场景下有特别强大的功能,例如无处不在的"结果集滑动",即,从数据库或其它集合对象的众多结果中展示其中的10个。它还可以被用于"向后遍历"一个集合或列表,而不用试图从前向后地访问每个元素。使用ListIterator要比利用向下计数的整数参数的List.get() 方法去"向后遍历"一个List容易得多。
3. 并不是所有的Iterable实例都来自于集合对象
Ruby和Groovy开发者喜欢炫耀他们怎样使用一行代码就遍历了整篇文本,并将其中的内容打印到控制台上。多数时候,他们会说,使用Java来做同样的事情需要编写许多代码:打开一个FileReader,再创建一个BufferedReader,然后创建一个while()循环去调用 getLine()方法,直到返回null为止。当然,你还必须得在一个try/catch/finally语句块中做上述事情,这个语句块用于处理异常且在结束时关闭文件句柄。
看起来这像是一个微不足道,学究式的争论,但它还是有些意义的。
他们(包括一些Java开发者)不知道并不是所有Iterable实例都要来自于集合对象。相反地,一个Iterable实例可以创建一个 Iterator实例,这个Iterator知道如何去凭空地造出下一个元素,而不是在一个预先已存在集合对象的内部默默地进行处理。
清单2 Iterating a file
// FileUtils.java
import java.io.*;
import java.util.*;
public class FileUtils
{
public static Iterable<String> readlines(String filename)
throws IOException
{
final FileReader fr = new FileReader(filename);
final BufferedReader br = new BufferedReader(fr);
return new Iterable<String>() {
public <code>Iterator</code><String> iterator() {
return new <code>Iterator</code><String>() {
public boolean hasNext() {
return line != null;
}
public String next() {
String retval = line;
line = getLine();
return retval;
}
public void remove() {
throw new UnsupportedOperationException();
}
String getLine() {
String line = null;
try {
line = br.readLine();
}
catch (IOException ioEx) {
line = null;
}
return line;
}
String line = getLine();
};
}
};
}
}
//DumpApp.java
import java.util.*;
public class DumpApp
{
public static void main(String[] args)
throws Exception
{
for (String line : FileUtils.readlines(args[0]))
System.out.println(line);
}
}
该方法的优点在于不需要在内存中处理整个文件的内容,但有一个告诫,如上面所编写的代码,它不能关闭下层的文件句柄。(当readLing()方法返回 null时就关闭文件句柄,通过该方法可以修正这一问题,但当Iterator未能遍历完整个文件时,该方法也解决不了这个问题。)
4. 意识到可变的hashCode()方法
Map是很好的集合对象,它带给我们只有在其它编程语言,如Perl,中才能体会到的键-值对集合的乐趣。并且JDK为我们提供了一个很棒的Map实现,HashMap,该实现在内部使用散列表,这使得快速地通过键来查找对应的值。但在那儿就会出现一个细微的问题:支持散列码的键会依赖内容可变的字段,这很容易就产生Bug。即使对那些最有耐心的Java开发者,这样的Bug也会使他们发疯。
想像清单3中的Person对象,它有一个典型的hashCode()方法(该方法使用firstName,lastName和age字段--所有的字段都不是final的--去计算散列码),调用Map的get()方法将可能失败并返回null。
清单3 可变的hashCode()使人犯错
// Person.java
import java.util.*;
public class Person
implements Iterable<Person>
{
public Person(String fn, String ln, int a, Person kids)
{
this.firstName = fn; this.lastName = ln; this.age = a;
for (Person kid : kids)
children.add(kid);
}
//
public void setFirstName(String value) { this.firstName = value; }
public void setLastName(String value) { this.lastName = value; }
public void setAge(int value) { this.age = value; }
public int hashCode() {
return firstName.hashCode() & lastName.hashCode() & age;
}
//
private String firstName;
private String lastName;
private int age;
private List<Person> children = new ArrayList<Person>();
}
// MissingHash.java
import java.util.*;
public class MissingHash
{
public static void main(String[] args)
{
Person p1 = new Person("Ted", "Neward", 39);
Person p2 = new Person("Charlotte", "Neward", 38);
System.out.println(p1.hashCode());
Map<Person, Person> map = new HashMap<Person, Person>();
map.put(p1, p2);
p1.setLastName("Finkelstein");
System.out.println(p1.hashCode());
System.out.println(map.get(p1));
}
}
更明确地说,上述方法令人痛楚,但解决方法却很简单:HashMap的键永远不要使用可变对象。
5. equals() vs Comparable
浏览Javadoc时,Java开发者们常会遇到SortedSet类型(在JDK中,它的唯一实现是TreeSet)。因为SortedSet是 java.util包中唯一提供了某种指定排序行为的集合类,所以开发者们在一开始使用它时并没有仔细地考究其中的细节。清单4证明了这一点:
清单4 SortedSet,很高兴发现你
import java.util.*;
public class UsingSortedSet
{
public static void main(String[] args)
{
List<Person> persons = Arrays.asList(
new Person("Ted", "Neward", 39),
new Person("Ron", "Reynolds", 39),
new Person("Charlotte", "Neward", 38),
new Person("Matthew", "McCullough", 18)
);
SortedSet ss = new TreeSet(new Comparator<Person>() {
public int compare(Person lhs, Person rhs) {
return lhs.getLastName().compareTo(rhs.getLastName());
}
});
ss.addAll(perons);
System.out.println(ss);
}
}
在用了上述代码一段时间之后,你可能会发现Set的核心特性之一:它不允许重复。这一特性在Set的Javadoc中有明确的描述。Set是"不包含重复元素的集合"。更准确地说,对于元素e1和e2,如果有e1.eqauls(e2),那么Set就不能同时包含它们,并且最多只能包含一个null元素。
但这似乎不是实际情况--虽然清单4没有Person对象是相等的(根据Person所实现的equals()方法),但当打印该TreeSet时,只展示了三个Person对象。
与Set的天然状态相反,TreeSet要求对象要么实现Comparable接口,要么向构造器中直接传入一个Comparator实现,不用 equals()方法相比较对象;而是使用Comparator/Comparable中的compare/comparaTo方法。
存储在Set中的对象有两种潜在的方法来判定相等性:期望中的equals()方法;Comparable/Comparator方法,这依赖于调用这些方法的上下文。
更糟的是,如此简单的描述还不足以表明这二者是不同的,因为以排序为目的的比较不同于以等价性为目的的比较:当按姓氏进行排序时,某两个Person对象是相等的,但它们的内容却是不等的。
总是要明确equals()与Comparable.compareTo()方法的区别--当实现Set时,返回零必须是清晰的。甚至于,应该在你的文档中清晰地描述这一区别。
结论
Java集合框架遍布有用之物,只要知道它们,就能使你的生活更简单也更富有成效。然而,挖掘出的这些有用之物经常伴随着一定的复杂度,例如,你会发现只要不在键中使用可变对象,就可以按你自己的方式去使用HashMap。
到目前为止,我们已经对集合框架进行了深入挖掘,但我们还未触及这其中的"金矿":由Java 5引入的并发集合。本系列的后5个窍门将关注包java.util.concurrent。
分享到:
相关推荐
2. 遵循单一职责原则,每个方法只做一件事情。 3. 方法签名应简洁,避免过长或复杂的参数列表。 六、集合与泛型 1. 使用泛型来保证类型安全,避免在运行时出现ClassCastException。 2. 避免使用`Collection.addAll...
- **定义**:JUnit是一种针对Java编程语言的单元测试框架。它在测试驱动开发(TDD)的发展中扮演了重要角色,并且是XUnit家族的一员,这个家族起源于JUnit。 - **作用**:通过JUnit进行单元测试有助于确保代码质量,...
Java的集合框架提供了丰富的容器类,用于存储和操作对象集合。然而,默认情况下这些集合类并不是线程安全的。为了确保线程安全性,可以使用`Collections.synchronizedList()`等方法包装集合,或者使用专门设计为线程...
七、集合框架 1. 使用泛型,提高代码类型安全。 2. 避免在循环中调用`size()`、`isEmpty()`等方法,提前计算并存储结果。 3. 使用`try-with-resources`处理`AutoCloseable`类型的集合,确保资源释放。 八、IO流 1. ...
五、集合框架Java集合框架是用于存储和管理对象的一组接口和类,包括List、Set、Map等接口和ArrayList、HashSet、HashMap等实现类。 1. List接口 有序、可重复元素,如ArrayList和LinkedList。 2. Set接口 无序、...
- **基础知识:** Java集合框架提供了存储和操作对象的集合类,如List、Set和Map等。这些集合类根据数据结构的不同特性提供了不同的行为。例如,`ArrayList`提供快速的随机访问,而`LinkedList`则提供高效的插入和...
Java集合框架包括数组、列表、集合、映射等。书中介绍了如何选择合适的集合类型,以及如何使用泛型来增强类型安全。 九、多线程 Java支持多线程编程,书中详细解释了如何创建和管理线程,以及如何同步线程以避免竞...
1. 单一职责原则(SRP):一个类或方法应只做一件事情,避免职责过多导致复杂性增加。 2. 开闭原则(OCP):对扩展开放,对修改关闭。设计时应考虑未来的需求变化,使代码易于扩展而难以修改。 3. 里氏替换原则...
七、集合框架 1. 使用`List`而非`Vector`,`Map`而非`Hashtable`,因为它们有更好的性能和线程安全特性。 2. 避免在循环中调用`Collection`的`size()`方法,可以先存储在局部变量中。 3. 使用`Set`而非`List`存储...
Java集合框架的基本分类 - **知识点**: 集合框架按照存储结构的不同分为单列集合和双列集合。 - **单列集合**: 根接口为 `Collection`。 - **双列集合**: 根接口为 `Map`。 - **应用场景**: 用于存储不同类型的...
第五章:数组与集合框架 数组是存储固定数量同类型数据的数据结构,而集合框架则提供了更灵活的数据存储方式。我们将学习如何声明、初始化和操作数组,以及ArrayList、LinkedList、HashSet和HashMap等集合类的使用。...
### Java企业级开发编程学习资料Java EE教程J2ee教程Spring概述 ...Spring作为Java企业级开发的重要框架之一,不仅提供了强大的功能支持,还极大地提升了开发效率和代码质量,是Java开发者不可或缺的工具之一。
由于文档内容过长,这里将...以上知识点覆盖了Java面试中常见的问题,包括Java基础、集合框架、多线程、IO流、网络通信、异常处理、设计模式、Java Web技术以及JVM相关问题。掌握这些知识点对于通过Java面试至关重要。
Java集合框架提供了一组接口和类,如List、Set、Map,它们用于存储和操作对象。ArrayList和LinkedList是List接口的实现,HashSet和HashMap对应Set和Map。 尽管"Java基础课堂笔记"中未提及"中国邮政储蓄银行网上...
以下是从给定文件内容中提取出的关于Java学习各阶段的书籍指南以及各书所对应的知识点。 一、基础类书籍 1. 《Thinking in Java》:作为Java入门书籍,这本书着重于建立正确的概念,它能够帮助初学者理解Java语言的...
通过上述分析,我们可以看到这个Java代码示例提供了一个完整的邮件发送框架。开发者可以通过简单的配置即可实现邮件发送、批量发送以及附件添加等功能。此外,对于调试和错误处理也提供了一定的支持,使得开发过程...
**四、集合框架** 1. **集合初始化**:避免使用`new ArrayList()`,推荐使用`Collections.emptyList()`, `Collections.emptyMap()`等静态工厂方法。 2. **遍历方式**:推荐使用`for-each`循环遍历集合,避免修改...
### Java软件开发规范知识点 #### 一、概述 在标题为“【技(制)字20160321】JAVA软件开发规范V1 0.docx”的文档中,主要内容围绕Java软件开发规范展开,旨在为财务系统研发群NonSAP系统的开发团队提供一套标准化...
Java技术面试题涉及到的领域广泛,包括Java基础、高级开发、数据库等关键知识点。下面是对这些知识点的详细解释: **一、Java基础** 在设计接口时,需要考虑的因素主要包括: 1. **业务定位**:接口应明确其在系统...
Java集合框架是另一个重要的主题,包括ArrayList、LinkedList、HashMap等数据结构。这些数据结构在实际编程中非常常见,能够有效地存储和操作数据。源代码中可能包含使用这些集合的例子,比如如何添加、删除和查找...