Java Collections API被许多Java开发人员所接受,作为标准Java数组的一个非常需要的替代和所有的缺点。主要与关联集合ArrayList
并不是一个错误,但是有更多的收集为那些谁去寻找。
类似地,虽然Map
(和它的选择的实现, HashMap
)是伟大的做名称值或键值对,没有理由限制自己这些熟悉的工具。你可以使用正确的API,甚至是正确的集合来修复很多容易出错的代码。
5系列的第二篇文章是几篇关于Collections的第一篇文章,因为它们对我们在Java编程中的作用非常重要。我会在开始时一看最快的(但可能不是最常见)的方式来完成日常的事情,比如换出启动 Array
S代表List
秒。之后,我们将深入了解一些不太了解的东西,比如编写一个自定义Collections类并扩展Java Collections API。
集合王牌阵列
刚开始使用Java技术的开发人员可能不知道数组最初包含在语言中,以避免20世纪90年代早期C ++开发人员的性能批评。那么,从那以后,我们已经走了很长的路,当与Java集合库相比时,该阵列的性能优势通常很短。
例如,将数组内容转换为字符串需要遍历数组并将内容连接在一起 String
; 而Collections实现都有一个可行的toString()
实现。
除了少见的情况,最好的做法是尽可能快地转换到您的方式到集合的任何数组。然后乞求这个问题,什么是最简单的方法来切换?事实证明,Java Collections API使它变得容易,如清单1所示:
清单1. ArrayToList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import java.util.*;
public class ArrayToList
{
public static void main(String[] args)
{
// This gives us nothing good
System.out.println(args);
// Convert args to a List of String
List<
String
> argList = Arrays.asList(args);
// Print them out
System.out.println(argList);
}
}
|
请注意,返回的List
是不可修改的,因此尝试向其添加新元素将抛出 UnsupportedOperationException
。
并且,因为对要添加到元素中的元素Arrays.asList()
使用varargs参数List
,您还可以使用它来轻松地创建List
出new
ed对象。
2.迭代效率低下
将一个集合(特别是一个由数组生成的集合)的内容移动到另一个集合中或从较大的集合中删除一小部分对象并不罕见。
你可能会试图简单地遍历集合,添加或删除每个元素,因为它找到,但不是。
在这种情况下,迭代具有主要的缺点:
- 使用每个添加或删除来调整集合大小是无效的。
- 在获取锁,执行操作和每次释放锁时都有潜在的并发噩梦。
- 在添加或删除正在进行时,其他线程在您的集合上引发的竞争条件。
您可以通过使用addAll
或 removeAll
传入包含要添加或删除的元素的集合来避免所有这些问题。
3.通过任何Iterable循环
增强型for循环是Java 5中添加到Java语言的最大便利之一,它消除了使用Java Collections的最后一个障碍。
之前,开发人员必须手动获取一个Iterator
,使用 next()
获取从指向的对象 Iterator
,并检查是否有更多的对象可用hasNext()
。Post Java 5,我们可以自由使用一个for循环变体,静默处理所有上面的。
实际上,此增强适用于实现接口的任何对象Iterable
,而不仅仅是 Collections
。
清单2展示了一种从一个Person
对象中获取子对象的列表的方法 Iterator
。而不是给内部的引用List
(这将使外面的呼叫者Person
添加孩子给你的家人 - 大多数父母会发现Person
uncol ), 类型实现Iterable
。这种方法还使增强的for循环遍历孩子们。
清单2. Ehanced for循环:显示你的孩子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
// 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 child : kids)
children.add(child);
}
public String getFirstName() { return this.firstName; }
public String getLastName() { return this.lastName; }
public int getAge() { return this.age; }
public Iterator<
Person
> iterator() { return children.iterator(); }
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 String toString() {
return "[Person: " +
"firstName=" + firstName + " " +
"lastName=" + lastName + " " +
"age=" + age + "]";
}
private String firstName;
private String lastName;
private int age;
private List<
Person
> children = new ArrayList<
Person
>();
}
// App.java
public class App
{
public static void main(String[] args)
{
Person ted = new Person("Ted", "Neward", 39,
new Person("Michael", "Neward", 16),
new Person("Matthew", "Neward", 10));
// Iterate over the kids
for (Person kid : ted)
{
System.out.println(kid.getFirstName());
}
}
}
|
使用Iterable
在域建模时有一些明显的缺点,因为只有一个这样的对象集合可以通过该iterator()
方法“隐式”支持。然而,对于子集合是明显和明显的情况, Iterable
使得针对域类型的编程更容易和更明显。
4.经典和自定义算法
你有没有想走路Collection
,但反过来?这就是经典的Java集合算法派上用场。
的儿童Person
在上市2 以上,因为它们在被传递的顺序列出; 但是,现在你想以相反的顺序列出他们。虽然你可以写另一个for循环以ArrayList
相反的顺序将每个对象插入一个新对象,但编码在第三或第四次之后会变得乏味。
这是清单3中未充分利用的算法:
清单3. ReverseIterator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class ReverseIterator
{
public static void main(String[] args)
{
Person ted = new Person("Ted", "Neward", 39,
new Person("Michael", "Neward", 16),
new Person("Matthew", "Neward", 10));
// Make a copy of the List
List<
Person
> kids = new ArrayList<
Person
>(ted.getChildren());
// Reverse it
Collections.reverse(kids);
// Display it
System.out.println(kids);
}
}
|
的Collections
类有许多的这些“算法”,即实施采取静态方法Collections
作为参数,并在集合作为一个整体提供实现无关的行为。
此外,Collections
类中存在的算法肯定不是伟大的API设计中的最后一个词 - 例如,我喜欢不直接修改传入的集合的内容的方法。因此,你可以编写自己的自定义算法是一件好事,如清单4所示:
清单4. ReverseIterator更简单
1
2
3
4
5
6
7
8
9
|
class MyCollections
{
public static <
T
> List<
T
> reverse(List<
T
> src)
{
List<
T
> results = new ArrayList<
T
>(src);
Collections.reverse(results);
return results;
}
}
|
5.扩展Collections API
上面的自定义算法说明了关于Java Collections API的最后一点:它总是旨在被扩展和变形以适合开发者的特定目的。
所以,例如,说你需要的类中的孩子的列表 Person
总是按年龄排序。虽然你可以编写代码来重复排序孩子(使用Collections.sort
方法,或许),它将是更好的有一个Collection
类为你排序。
事实上,你甚至可能不关心保留对象被插入的顺序Collection
(这是a的主要理由List
)。你可能只是想按照排序顺序保持它们。
没有Collection
类java.util
可以满足这些要求,但写一个很简单。所有你需要做的是创建一个接口,描述Collection
应提供的抽象行为 。在a的情况下 SortedCollection
,意图是完全行为的。
清单5. SortedCollection
1
2
3
4
5
|
public interface SortedCollection<
E
> extends Collection<
E
>
{
public Comparator<
E
> getComparator();
public void setComparator(Comparator<
E
> comp);
}
|
编写这个新接口的实现几乎是前瞻性的:
清单6. ArraySortedCollection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
import java.util.*;
public class ArraySortedCollection<
E
>
implements SortedCollection<
E
>, Iterable<
E
>
{
private Comparator<
E
> comparator;
private ArrayList<
E
> list;
public ArraySortedCollection(Comparator<
E
> c)
{
this.list = new ArrayList<
E
>();
this.comparator = c;
}
public ArraySortedCollection(Collection<?
extends
E> src, Comparator<
E
> c)
{
this.list = new ArrayList<
E
>(src);
this.comparator = c;
sortThis();
}
public Comparator<
E
> getComparator() { return comparator; }
public void setComparator(Comparator<
E
> cmp) { comparator = cmp; sortThis(); }
public boolean add(E e)
{ boolean r = list.add(e); sortThis(); return r; }
public boolean addAll(Collection<?
extends
E> ec)
{ boolean r = list.addAll(ec); sortThis(); return r; }
public boolean remove(Object o)
{ boolean r = list.remove(o); sortThis(); return r; }
public boolean removeAll(Collection<?> c)
{ boolean r = list.removeAll(c); sortThis(); return r; }
public boolean retainAll(Collection<?> ec)
{ boolean r = list.retainAll(ec); sortThis(); return r; }
public void clear() { list.clear(); }
public boolean contains(Object o) { return list.contains(o); }
public boolean containsAll(Collection <?> c) { return list.containsAll(c); }
public boolean isEmpty() { return list.isEmpty(); }
public Iterator<
E
> iterator() { return list.iterator(); }
public int size() { return list.size(); }
public Object[] toArray() { return list.toArray(); }
public <
T
> T[] toArray(T[] a) { return list.toArray(a); }
public boolean equals(Object o)
{
if (o == this)
return true;
if (o instanceof ArraySortedCollection)
{
ArraySortedCollection<
E
> rhs = (ArraySortedCollection<
E
>)o;
return this.list.equals(rhs.list);
}
return false;
}
public int hashCode()
{
return list.hashCode();
}
public String toString()
{
return list.toString();
}
private void sortThis()
{
Collections.sort(list, comparator);
}
}
|
这种快速和肮脏的实现,没有考虑到优化,显然可以经受一些重构。但关键是,Java Collections API从来不是要在所有与collection相关的事情中的最后一个词。它需要和鼓励扩展。
当然,一些扩展将是“重型”品种,如引入的那些java.util.concurrent
。但其他人将像写一个自定义算法或对现有Collection
类的简单扩展一样简单。
扩展Java Collections API可能看起来不堪重负,但一旦你开始这样做,你会发现它没有你想象的那么难。
结论是
像Java序列化一样,Java Collections API充满了未开发的角落和核心 - 这就是为什么我们没有完成这个主题。5物系列的下一篇文章将给你五种更多的方法来使用Java Collections API。
Collections类java.util
旨在帮助,即通过替换数组,从而提高Java性能。正如你在前一篇文章中所学到的,它们也是可塑的,愿意以各种方式定制和扩展,以服务于良好,干净的代码。
集合也是强大的,但是,mutable:使用它们,小心和滥用他们自己的风险。
列表与数组不同
Java开发人员经常犯错误,认为 ArrayList
这只是Java数组的替代。集合由数组支持,这导致在集合中随机查找项目时的良好性能。并且,像数组一样,集合使用整数序数来获取特定项。尽管如此,一个集合不是一个数组的替代。
区分集合和数组的技巧是知道顺序和位置之间的区别。例如, List
是一个保留项目放置到集合中的顺序的接口,如清单1所示:
清单1.可变键
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
- 从数组中删除一个项意味着用一个新的引用或空值覆盖它的索引槽)。
2. Iterator
,你惊讶我!
毫无疑问,Java开发人员喜欢Java Collections Iterator
,但是什么时候是最后一次真正看着 Iterator
界面呢?大多数时候,我们只是Iterator
在一个for()
循环或增强 for()
循环中,然后继续前进。
但是,对于那些谁去挖,Iterator
有两个惊喜在商店。
首先,Iterator
支持从源集合安全地删除对象,通过调用的能力remove()
上 Iterator
本身。这里的要点是避免一个 ConcurrentModifiedException
,它准确地说明它的名字暗示:一个集合被修改,而一个 Iterator
打开它。有些藏品会让你得逞的删除或添加元素,以一个Collection
同时在它迭代,但调用remove()
的 Iterator
是一个更安全的做法。
第二,Iterator
支持一个派生的(可以说是更强大的)表兄弟。ListIterator
,只有List
s 可用 ,支持List
在迭代期间添加和删除 ,以及通过List
s的双向滚动。
双向滚动对于诸如无处不在的“滑动结果集”的场景可以是特别强大的,其示出了从数据库或其他集合检索的许多结果中的10个。它也可以用于“向后”通过集合或列表,而不是试图从前面做的一切。删除a ListIterator
比使用向下计数整数参数List.get()
来“向后”通过a 更容易List
。
不是所有Iterable
的都来自集合
Ruby和Groovy开发人员喜欢炫耀他们如何迭代一个文本文件,并用一行代码将其内容打印到控制台。大多数时候,他们说,在Java编程中做同样的事情需要几十行代码:打开一个FileReader
,然后一个 BufferedReader
,然后创建一个while()
循环,getLine()
直到它回到null。当然,你必须在一个try/catch/finally
块中处理异常并在完成时关闭文件句柄。
它可能看起来像一个愚蠢和讽刺的论点,但它有一些优点。
他们(和很多Java开发人员)不知道的是,不是所有 Iterable
的都必须来自集合。相反,一个人 Iterable
可以创造一个Iterator
知道如何用稀薄的空气制造下一个元素,而不是盲目地把它从一个预先存在的Collection
:
清单2.迭代文件
// 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>迭代器</ 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; 尝试{ line = br.readLine(); }} catch(IOException ioEx){ line = null; }} 回线; }} String line = getLine(); }; }} }; }} }} //DumpApp.java import java.util。*; public class DumpApp { public static void main(String [] args) throws异常 { for(String line:FileUtils.readlines(args [0])) System.out.println(line); }} }}
这种方法的优点是不把文件的整个内容保存在内存中,但是有一点需要注意,就像写的一样,它不是 close()
底层文件句柄。(你可以通过在readLine()
返回null时关闭来解决这个问题,但这不会解决Iterator
不会运行到完成的情况。)
4.小心变量 hashCode()
Map
是一个精彩的集合,带给我们在其他语言如Perl常常发现的键/值对集合的敏捷。JDK为我们提供了一个很好Map
的实现HashMap
,它在内部使用散列表来支持快速键查找相应的值。但这里面有一个微妙的问题:支持根据可变字段的内容的哈希代码的密钥容易受到一个错误的影响,甚至将驱动最有耐心的Java开发人员batty。
假设Person
清单3中的对象具有典型的 hashCode()
(使用firstName
, lastName
和age
字段-所有的非决赛-来计算hashCode()
),则get()
调用Map
会失败并返回null
:
清单3. Mutable hashCode()
驱动我的bug
// Person.java import java.util。*; public class Person 实现Iterable <Person> { public Person(String fn,String ln,int a,Person ... kids) { this.firstName = fn; this.lastName = ln; this.age = a; for(人孩子:孩子) 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 List <Person> children = new ArrayList <Person>(); }} // MissingHash.java import java.util。*; public class MissingHash { public static void main(String [] args) { 人员p1 =新人员(“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
是提供任何排序行为Collection
的java.util
包中唯一 的,开发人员经常开始使用它,而不太仔细地询问细节。清单4演示:
清单4. SortedSet
我很高兴我找到你了!
import java.util。*; public class UsingSortedSet { public static void main(String [] args) { 列表<Person> persons = Arrays.asList( 新人(“Ted”,“Neward”,39), 新人(“Ron”,“Reynolds”,39), 新人(“Charlotte”,“Neward”,38), 新人(“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中进行了说明。A Set
是不包含重复元素的集合。更正式地,集合不包含元素e1和e2对,使得e1.equals(e2)和最多一个空元素。
但是,这并不实际上似乎是的情况下-虽然没有的 Person
在对象清单4相等(根据本equals()
实施 Person
),只有三个对象是内本 TreeSet
打印时。
与集合的所述性质相反TreeSet
,要求对象Comparable
直接实现或者Comparator
在构建时传递equals()
的对象不用于比较对象; 它使用compare
或compareTo
方法 Comparator/Comparable
。
因此,存储在a中的对象Set
将有两种确定等式的可能equals()
方法:期望的方法和 Comparable/Comparator
方法,取决于谁在请求的上下文。
更糟的是,简单地声明两者应该是相同的是不够的,因为用于排序目的的比较与用于平等目的的比较不同:Person
在排序时考虑两个相等是完全可以接受的按姓氏,但不相等的内容。
始终确保在实现时,equals()
和返回-0之间的 Comparable.compareTo()
差异是清楚的 Set
。通过扩展,差异应该在您的文档中清楚。
结论是
Java Collections库分散有tidbits,可以使你的生活更容易,更有成效,如果只有你知道他们。发布tidbits通常涉及一些复杂性,但是,像发现,你可以有你的方式HashMap
,只要你从来没有使用可变对象类型作为其关键。
到目前为止,我们已经在Collections的表面下挖掘,但是我们还没有达到金矿:并发集合,在Java 5中引入。本系列的下面五个提示将关注java.util.concurrent
。