集合,有时候称之为容器,是一个简单对象,用来将多个元素组合到一个单一的单元。集合用来存储,检索,操作和传递聚合数据,典型的,他们可以表示自然数据的数据项,例如扑克牌(一个卡片集合),邮件文件夹(信的集合),电话簿(名字和号码的映射)。
集合框架是表示和操作集合的统一体系结构,所有的集合框架都包含以下内容:
接口:接口是表示集合的抽象数据类型,接口允许对结合进行独立于其表示细节的操作,在面向对象的语言中,接口通常形成层次结构。
实现:实现是集合接口的具体实现,实质上他们是可重用的数据结构。
算法:算法是一些对实现了集合接口的对象执行有用计算的方法,例如搜索和排序。这些算法被称之为多态,这就意味着相同的方法可以在恰当的集合接口的不同实现上使用。本质上算法是可重用的方法。
除了java集合框架,最著名的集合框架示例是c++标准模板库(STL)和Smalltalk的集合结构,从历史上来看,集合框架非常复杂,它以陡峭的学习曲线而闻名。java集合框架将打破这一传统。
减少工作量:通过有效的数据结构和算法,集合框架可以让你专注于程序的重要部分,而不是关注让其工作的底层的“管道”上,通过促进无关API之间的互操作性,java集合框架使你不用编写适配器对象或转换代码来连接API.
提高编程速度和质量:集合框架提供了高性能,高质量的数据结构和算法实现,每个接口的各种实现都是可以互换的,因此可以通过切换集合实现来轻松的实现程序调整,由于你无需编写自己的数据结构,所以你可以将更多的时间致力于提高程序的质量和性能。
允许无关API之间互操作性:集合接口是API通过其来回传递集合的本地语言。如果我的网络管理API提供了一组节点名称,并且您的GUI工具包需要一组列标题,那么我们的API将无缝地互操作,即使它们是独立编写的。
减少学习和使用新API的工作量:许多API自然地在输入上收集集合并将它们作为输出提供。过去,每个这样的API都有一个专门用于操作其集合的小型子API。这些特殊的集合子API之间几乎没有一致性,因此您必须从头开始学习每一个,并且在使用它们时很容易出错。随着标准集合接口的出现,问题就消失了。
减少设计新API的工作量:这是之前优势的另一面。设计人员和实施人员每次创建依赖于集合的API时都不必重新发明轮子; 相反,他们可以使用标准的集合接口。
促进软件重用:符合标准集合接口的新数据结构本质上是可重用的。对于实现这些接口的对象进行操作的新算法也是如此
核心集合接口封装了不同类型的集合,如下图所示,这些接口允许对集合进行独立于其表示细节的操作,核心集合接口是java集合框架的基础,如下图所示,核心集合接口形成了一个层次结构。
Set是一种特殊的Collection,SortedSet是一种特殊的Set,这里需要注意的是,层次结构由两个不同的树组成, Map不是真正的Collection。
注意:所有的核心接口都是通用的,例如,这就是Collection接口的声明
public interface Collection<E>...
为了保证核心集合接口的数量可管理,java平台不为每个集合类型的每个变体提供单独的接口(此类变形可能包含不可变,固定大小和追加的属性),相反的,每个接口的修改操作都被指定为可选,给定的实现可以选择不支持所有的操作,如果一个不支持的操作被调用,集合将抛出一个UnsupportedOperationException异常,实现负责记录他们支持的可选操作,所有的java平台的通用实现都支持所有可选操作
下面的列表描述了核心集合接口:
Collection:集合层次结构的根,集合代表一组被称之为集合元素的对象,Collection接口是所有集合的最小公分母,所有的集合都实现了该接口,用于传递集合,并在需要最大通用性时对其进行操作。某些类型的集合允许重复的元素,而其他的不允许,有些是有序的,有些是无序的。java平台不提供该接口的任何直接实现,但提供了更具体的子接口(如Set和List)的实现。
Set: 一种不包含重复元素的集合,这个接口是对数学中的集合进行抽象建模,并用于表示集合,例如包含扑克手的卡片,构成学生日程的课程或在机器上运行的进程。
List: 一种顺序集合(有时称之为序列sequence),list可以包含重复元素,List的用户通常可以精确控制列表中每个元素的插入位置,并可以通过整数索引(位置)访问元素。
Queue 一种用来保存处理之前的多个元素的集合,除了基本的集合操作外,Queue还提供了额外的插入,获取和检查操作。队列通常但不一定以FIFO(先进先出)方式对元素进行排序。除了优先级队列之外,优先级队列根据提供的比较器或元素的自然顺序对元素进行排序,无论使用什么顺序,队列的头部是即将通过调用删除或轮询删除的元素。在FIFO队列中,所有新元素都插入队列的尾部,其他类型的队列可能使用不同的放置规则,每个Queue实现都必须指定其排序顺序。
Deque - 用于在处理之前保存多个元素的集合。 除了基本的集合操作外,Deque还提供额外的插入,获取和检查操作。Deques可用作FIFO(先进先出)和LIFO(后进先出)。在双端队列中,所有新元素均可在两端插入,检索和删除。
Map:一个键值映射的对象,map不能包含重复的键,每个key能够映射至少一个value,如果你使用过hasTable,你应该已经数据了基本的map。
最后两个核心集合接口仅仅是Set和Map的排序版本。
SortedSet : 一个按照升序保存元素的set,为了利用排序提供了几个额外的操作,排序集合用于自然顺序集合,例如单词表和成员名册。
SortedMap : 一种按照升序存储映射关系的map,这是Map对于SortedSet的衍生,有序映射通常用来对集合的键值对进行自然排序,例如字典,电话簿。
注:如果想了解排序接口如何维护元素的顺序,请参阅“对象排序”部分。
集合代表的是一组称之为集合元素的对象,Collection接口用于传递需要最大通用性的集合对象。例如,按照惯例,所有通用的集合接口都有一个包含Collection参数的构造器,此构造器称之为转换构造器,无论给定集合的子接口或实现类型是什么,该构造器都能初始化一个新的集合以包含一个特定集合的所有元素。换句话说,他允许你转换集合类型。
实例:假如你有一个Collection c, 这个c可能是一个List, Set 或者其他类型的集合,下面的语句创建了一个新的 ArrayList(一个实现了List接口的对象),一开始就包含了所有的元素c
List<String> list = new ArrayList<>();
Collection接口包含了一些基本的集合操作方法,例如
int size()
boolean isEmpty()
boolean contains(Object element)
boolean add(E element)
boolean remove(Object elment)
Iterator<E> iterator()
同时在整个集合中还包含如下操作方法
boolean containAll(Collection<?> c)
boolean addAll(Collection<? extends E> c)
boolean romoveAll(Collection<?> c)
boolean retainAll(Collection<?> c)
void clear()
另外也包含一些数组操作(例如 Object[] toArray(), T[] toArray(T[] a))
在JDK8和更高版本中,Collection接口增加了Stream stream() 和 Stream parallelStream()方法,用于从底层集合中获取顺序或并行流(关于流的操作可以关注我的关于java8函数式编程部分大博客)
Collection接口能够在你给定的集合(集合代表的是一组对象)上执行你所期望的操作,它有方法告诉你在集合中有多少元素(size, isEmpty),有方法检查你给定的对象是否在集合中已存在(contains),有方法从集合中增加和删除一个元素(add, remove),有方法提供一个迭代器遍历集合(iterator)。
add方法定义的足够通用,因此对于允许重复和不允许重复的集合都有意义。它保证在方法调用完成之后集合将包含插入的元素,并在调用导致集合有变更时返回true。类似的,remove方法旨在从集合中移除一个指定元素的单个实例,假设集合中包含元素去执行remove方法,当集合被变更的时候会返回true。
以下介绍三中集合遍历的方式
在JDK8和更高版本中,遍历集合的首选方式是获取流并对流进行聚合操作。聚合操作通常与lambda表达式配合使用,借此你的代码将更加精简,意图也更加明显,以下代码实现顺序遍历形状集合并输出红色对象
myShapesCollection.stream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));
同样,你可以很容易得到一个并行流,当你的计算机资源有限但是集合足够大的时候这是很有意义的。
myShapeCollection.parallelStream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));
使用collect这个API可以有很多种不同的方式来收集数据,例如你可以将一个集合中的元素转换成String对象,然后使用逗号分隔连接起来。
String joined = elements.stream()
.map(Object::toString)
.collect(Collectors.joining(","));
或者计算下所有员工的工资
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
以上只是使用流和聚合操作的一些示例,虽然只有一点,但是已经可以看出它的强大(关于函数编程和lambda请关注我的博客)。
集合框架总是会提供许多所谓的“批量操作”作为其部分API,这些API包含对整个集合操作的方法,例如containAll, addAll, removeAll等等,不要将JDK8中的聚合操作和这些批量操作所混淆,新的聚合操作和批量操作最主要的区别在于批量操作都是可变的,意味着他们(批量操作)都修改了基础集合。相反的,新的聚合操作没有修改基础集合,在使用新的聚合操作和lambda表达式时,如果你的代码稍后会从并行流中运行,必须避免突变,以免在将来引入问题。
for-each构造器允许你使用for循环简洁的遍历数组和集合。以下大麦使用for-each构造器换行输出集合的每个元素。
for(Object o : collection){
System.out.println(o);
}
Iterator是一个对象,他使你能够遍历集合并且有选择性的从集合中删除元素。通过调用集合的iterator方法你可以得到该集合的Iterator,如下展示Iterator接口
public interface Iterator<E>{
boolean hasNext();
E next();
void remove(); //optional
}
调用hasNext方法时,如果集合中还有更多的元素会返回true,next方法返回迭代器中的下一个元素,一次remove方法对应一次next方法,也就是说每调用一次next只能调用一次remove,如果违反此规则则抛出异常。
注意:Iterator.remove方法是在迭代期间唯一安全的修改集合的方式,如果在迭代过程中以其他任何方式修改基础集合,都是不被指定的。这里不被指定的意思是在迭代期间使用Iterator.remove的时候不允许对基础集合进行修改。以下代码展示了这种安全性;
运行报错
Iterator iterator = collection.iterator();
while (iterator.hasNext()){
iterator.remove();
collection.add(e);
}
正常运行
Iterator iterator = collection.iterator();
while (iterator.hasNext()){
collection.remove(0);
collection.add(e);
}
当你需要如下操作的时候就需要使用迭代器来代替forEach操作:
下面的方法展示了如何使用迭代器过滤任意集合,即遍历集合删除特定元素
static void filter(Collection<?> c) {
for (Iterator<?> it = c.iterator(); it.hasNext(); )
if (!cond(it.next()))
it.remove();
}
这段简单的代码是多态的,意味着它适用于任何集合而不用关注实现,这个例子也展示了使用javaCollection FrameWork 编写多态算法是多么的容易。
批量操作是对整个集合执行的操作,你可以使用基本操作来实现这些快速操作,但大多数情况下,此类实现效率低下。以下是批量操作:
containAll :如果目标集合包含指定集合的所有元素则返回true。
addAll:把指定集合的所有元素增加到目标集合中
removeAll :从目标集合中删除目标集合和指定集合共同包含的元素
retainAll: 从目标集合中删除目标集合包含但指定集合不包含的元素(交集)
clear:删除集合的所有元素
addAll, removeAll, 和 retainAll方法在执行中改变了集合都会返回true。
作为批量操作功能的一个简单例子,思考以下习惯用法,从Collection中删除指定元素的所有实例e, 注意这里是删除所有的e。
c.removeAll(Collections.singleton(e));
更加特殊的操作,猜测你想删除集合中所有的null
c.removeAll(Collections.singleton(null));
Collections.singleton这个惯用语法是一个静态工厂方法,他会返回只包含特定元素的不可变集合。
toArray方法是作为集合和旧的API(期望输入数组的api)之间的桥梁而提供的,数组操作允许集合内容被转换到一个数组中。这是一个简单的结构,使用无参函数创建了一新的对象数组。更加复杂的结构允许调用者提供一个数组或者选择输出数组的运行时类型。
例如:假设c是一个集合,以下代码将集合c的内容装进一个新的Object数组,数组的长度与集合中元素的个数相同。
Object[] a = c.toArray();
假设:已知c只包含String类型(也许c的类型是Collection), 下面的代码将集合c的内容装进一个新分配的String数组,同样数组的长度和集合元素的个数相同。
String[] a = c.toArray(new String[0]);
Set是一个不能包含重复元素的集合,它模拟了数学集合的抽象,Set接口仅包含从Collection接口继承成来的方法并且增加了禁止重复的限制。Set还为equals和hashCode操作增加了更强的契约,允许Set实例进行有意义的比较,即使他们的实现类型不同。两个Set实例如果包含的元素都相同则两个实例相等。
这里有一个简单但是有用的Set惯用语法,假设你有一个集合c,并且你想创建另外一个删除了重复元素且和c包含相同的元素的集合,那么下面一行代码就够了:
Collection<Type> noDups = new HashSet<Type>(c);
它的工作原理是创建一个Set(根据定义,不能包含重复元素), 初始化为包含在c中的所有元素,上面的代码使用了标准的Collection接口中转换构造器。
或者你也可以使用JDK8及以后版本的语法,你可以更加容易的使用聚合操作收集到一个Set
c.stream()
.collect(Collectors.toSet());
这里有个略微长的示例将集合的人的姓名累积到一个treeSet
Set<String> set = people.stream()
.amp(Person :: getName())
.collect(Collectors.toCollection(TreeSet::new));
下面的例子是第一个惯用语法的变形,他在删除重复元素的时候保持了原始集合的顺序。
Collection<Type> noDups = new LinkedHashSet<Type>(c);
下面的对上面第一个惯用语法更通用的封装,返回一个跟传递的集合泛型一致的Set:
public static <E> Set<E> removeDups(Collection<E> c){
return new LinkedHashSet<E>(c);
}
size操作返回集合中元素的个数(它的基数),isEmpty跟你认为的它能干吗一样。add方法添加一个指定的元素到Set(如果他尚不存在)并返回一个布尔值用来表示它是否被添加。同样的,remove方法从Set中删除一个指定的元素(如果它存在)同时返回一个布尔值用来表示它是否存在(这里的是否存在是指删除之前),iterator方法通过集合调用返回一个迭代器。
以下 程序打印出其参数列表中的所有不同单词。提供了该程序的两个版本。第一个使用JDK 8聚合操作。第二个使用for-each构造。
使用JDK 8聚合操作:
import java.util.*;
import java.util.stream.*;
public class FindDups {
public static void main(String[] args) {
Set<String> distinctWords = Arrays.asList(args).stream()
.collect(Collectors.toSet());
System.out.println(distinctWords.size()+
" distinct words: " +
distinctWords);
}
}
使用for-each构造:
import java.util.*;
public class FindDups {
public static void main(String[] args) {
Set<String> s = new HashSet<String>();
for (String a : args)
s.add(a);
System.out.println(s.size() + " distinct words: " + s);
}
}
现在来运行第一版:
java FindDups i came i saw i left
处理完的输出:
4 distinct words: [left, came, saw, i]
注意:这两段代码始终引用的是Collection的接口类(Set)而不是它的实现类,这是一个强烈推荐的编程实践,因为它使你可以灵活的通过构造函数更改实现。如果用来存储变量的集合或者用来传递的参数被声明成接口的实现类而不是接口类,为了改变它的实现类型,所有的这些变量和参数必须被改变。此外,无法保证程序的正常运行, 如果程序使用了任何原始实现类型存在但是在新的实现中不存的非标准操作,程序会立马崩溃。所以仅通过接口引用集合可以防止你使用任何非标准操作。
在前面的例子中Set的实现类型是HashSet(不能保证Set中元素的顺序),如果你想按照字母顺序打印单词列表,仅需将Set的实现类型从HashSet变成TreeSe即可,修改这一行代码之后就有如下输出:
java FindDups i came i saw i left
4 distinct words: [came, i, left, saw]
批量操作特别适合Set, 使用时,他们执行标准的集合代数运算, 假设有两个Set分别是s1和s2,以下是批量操作的作用:
为了避免在计算集合的交集,并集和差集的时候对另外一个集合的修改,调用者在调用批量操作之前必须对一个集合进行拷贝。下面是一些惯用语法:
Set<Type> union = new HashSet<Type>(s1);
union.addAll(s2);
Set<Type> intersection = new HashSet<Type>(s1);
intersection.retainAll(s2);
Set<Type> difference = new HashSet<Type>(s1);
difference.removeAll(s2);
前面的结果Set的实现是HashSet,前面已经提到过,在java平台中HashSet是Set的最佳实现,然而,任何的通用的Set实现都可被替代。
让我们重新审视下FindBugs程序,假设您想知道参数列表中的哪些单词只出现一次,哪些出现多次,但您不希望重复打印任何重复项。这种效果可以通过生成两个集合来实现, 一个集合包含参数列表中的每个单词,另一个集合仅包含重复项目。仅出现一次的单此是这两个集合的差集,以下是具体实现:
import java.util.*;
public class FindDups2 {
public static void main(String[] args) {
Set<String> uniques = new HashSet<String>();
Set<String> dups = new HashSet<String>();
for (String a : args)
if (!uniques.add(a))
dups.add(a);
// Destructive set-difference
uniques.removeAll(dups);
System.out.println("Unique words: " + uniques);
System.out.println("Duplicate words: " + dups);
}
}
当使用跟前面相同的参数列表 (i came i saw i left)运行时,会有以下输出:
Unique words: [left, saw, came]
Duplicate words: [i]
一些不太常见的集合代数运算是取出两个集合中的公共部分,以下代码实现了这个功能:
Set<Type> symmetricDiff = new HashSet<Type>(s1);
symmetricDiff.addAll(s2);
Set<Type> tmp = new HashSet<Type>(s1);
tmp.retainAll(s2);
symmetricDiff.removeAll(tmp);
Set的数组操作除了执行跟Collection数组操作一样的操作之外不会对集合执行任何特殊的操作,具体参见上面介绍的Collection接口的数组操作。
注意:其他的接口后续持续更新