如果一个程序只包含固定数量的对象且对象的生命周期都是已知的,那么这是一个非常简单的程序。
通常,程序总是根据运行时才知道的某些条件去创建新的对象。在此之前,无法知道所需对象的数量甚至确切类型。为了解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。
大多数编程语言都提供了某种方法来解决这个基本问题。例如之前学习过的数组,它是编译器支持的类型。数组是保存一组对象的最有效的方式,如果想要保存一组基本类型数据,也推荐使用数组。但是数组具有固定的大小尺寸,而且在更一般的情况下,在写程序的时候并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,因此数组长度固定这一限制就显得太不方便了。
因此集合框架就应运而生了,让我们先来看一张整个 Java 集合框架的结构图:
是不是让人看得眼花缭乱?确实如此,我第一次见到这张图的时候也被吓了一跳,没想到Java的集合框架比C++ 的 STL 要复杂这么多。不过没事,让我们慢慢来。
而在这次课程中我们主要介绍图中用红框标记的接口及其主要子类。
基本概念
首先Java集合框架分为两大类,如上图所示,分别是 Collection 和 Map。
Collection 顾名思义,就是集合的意思,不过和数学上的集合不太一样,根据官方文档,这里的集合既可以有重复元素,也可以没有重复元素,既可以有序,也可以无序,是用来实现多态的一个接口。我们可以把它简单理解成长度可变的数组。List 会以插入顺序存储元素,而 Set 则不能包含重复元素,Queue 则按照指定的排队规则来确定元素存储的顺序(Queue的部分就留给你们自学啦)。
Map 则是指一个一个的键值对,可以把它简单的理解成元素类型是键值对的 Collection(但不允许一个键对应多个值)。通过给定 Map 一个键,就可以得到其对应的值,因此它又叫做字典(就像我们查字典的时候,根据单词拼写可以找到对应的单词意思一样)。
在大多数情况下,你编写的大部分代码都在与这些接口打交道,并且唯一需要指定具体类型的地方就是在创建它们的时候。
因此,可以像下面这样创建一个 List ,尖括号里面是集合里面元素的类型,同样右边尖括号里的范型类型可以省略。
List<Apple> apples = new ArrayList<>();
请注意, ArrayList 已经被向上转型为了 List 。使用这些接口的目的是:如果想要改变具体实现,只需在创建时修改它就行了,就像下面这样:
List<Apple> apples = new LinkedList<>();
因此,应该创建一个具体类的对象,将其向上转型为对应的接口,然后在其余代码中都是用这个接口,这是面向对象的一种重要思想。
这种方式并非总是有效的,因为某些具体类有额外的功能。例如, LinkedList 具有 List 接口中未包含的额外方法,而 TreeMap 也具有在 Map 接口中未包含的方法。如果需要使用这些方法,就不能将它们向上转型为更通用的接口。
Collection
作为所有集合的根接口,它定义了所有集合都能进行的操作,这里就只介绍几个比较重要的方法:
- int size() 获取集合大小
- boolean isEmpty() 集合是否为空
- boolean contains(Object) 集合是否包含某个元素
- boolean add(E) 将元素加入集合,成功则返回true
- boolean remove(E) 将指定元素移除集合,成功则返回true
- clear() 清空整个集合
- boolean addAll(Collection<?>)
将指定集合中所有元素加入此集合,如果集合改变了则返回true- boolean removeAll(Collection<?>)
删除所有指定集合中有的元素,如果集合改变了则返回true- <T> T[] toArray(T[] a)
返回一个包含集合中所有元素的数组,运行时根据集合元素的类型指定数组的类型
值得注意的最后这个方法的参数数组a,如果数组a有足够空间可以放下所有的元素,就会把生成的数组放入a里面,反之则会生成一个新的数组,所以我一般的用法是这样的:
String[] array=list.toArray(new String[0]);
列表 List
List 将元素保存在特定的序列(Sequence)中。 List 接口在 Collection 的基础上添加了许多方法,允许在 List 的中间插入和删除元素。
有下面两种类型的 List :
- ArrayList :实现原理是数组,随机访问元素(就是不按照元素的顺序进行依次访问)速度较快,但在 List 中间插入和删除元素时速度较慢。
-
LinkedList :实现原理是链表,它在 List 中间进行的插入和删除操作速度比较快,但对于随机访问来说相对较慢。由于是用链表实现的,提供了很方便获取开头和末尾元素的方法。
集合 Set
Set 是不允许重复元素的集合,也就是数学意义上真正的集合, 如果试图将相同对象的多个实例添加到 Set 中,它会阻止这种行为。Set 具有与 Collection 一模一样的接口,因此没有任何额外的功能,不像前面两种不同类型的 List 那样。所以实际上, Set 就是一个 Collection ,只是行为不同。
由于 Set 和 Collection 的接口完全相同,所以没有方便的获取中间元素的方法,一般访问元素采用的是 for-in 语句(或者是迭代器),如下所示:
public class SetOfString {
public static void main(String[] args) {
Set<String> colors = new HashSet<>();
for(int i = 0; i < 100; i++) {
colors.add("Yellow");
colors.add("Blue");
colors.add("Red");
colors.add("Red");
colors.add("Orange");
colors.add("Yellow");
colors.add("Blue");
colors.add("Purple");
}
for(String color: colors){
System.out.println(color);
}
}
}
细心的小伙伴们运行完上面的代码以后可能会发现不对的地方,怎么输出的顺序和添加的顺序不同呢?
对,这就是 HashSet 的一个特性之一,由于它是用散列函数决定元素存储的顺序,所以无法保证输出顺序和添加的顺序保持一致,它内部是用后面要讲到的 HashMap 实现的。
那如果就是要保持一致怎么办呢?那就要用到 LinkedHashSet 了,可以试试把上面代码中的 HashSet 换成 LinkedHashSet 然后输出结果。
最后就是 TreeSet,可以将元素按照给定的 Comparator 进行排序,而它内部也是用 TreeMap 实现的。
迭代器 Iterator
Iterator 的中文名称是迭代器,在 C++ 的 STL 里也有类似的东西。
迭代器是什么?试想一下,我们在所有集合中是不是都会需要访问里面的元素,而迭代器则是将对元素的访问和集合本身隔离开来,也就是说迭代器就是专门用来对集合中的元素进行访问的类。
为什么要有迭代器?想象一下下面的场景:你有一家餐厅,有一个存储菜单项的集合,需要对集合的元素进行访问从而显示菜单,刚开始你可以打算用 List,于是用了 get() 函数访问元素;但过几天以后老板觉得不满意,说要你改用Set,之前所有的代码都需要改,这可麻烦了。
而一开始就用迭代器就不一样了,所有的 Collection 都有一个方法 iterator(),可以返回一个迭代器对象,不管你集合的底层怎么变化,你都不需要改很多的代码。
迭代器主要有如下的方法:
- boolean hasNext() 判断是否有下一个元素
- T next() 返回下一个元素
- void remove() 删除上一次调用next()方法所返回的对象,如果之前没有执行过next()则会抛出错误
值得注意的是remove()这个方法并不是抽象方法,而是有默认实现的:
default void remove() {
throw new UnsupportedOperationException("remove");
}
也就是说不是所有的集合返回的迭代器对象都可以在中途删除元素,只要子类不重写这个方法就可以了。
映射 Map
Map用起来比较简单,它有两个范型参数,分别表示键的类型和值的类型。
它的几个核心方法如下:
- V put(K key, V value) 将一个键和一个值进行对应,返回值是这个键之前对应的值,如果这个键之前没有对应的值则返回null
- V get(K key) 返回一个键对应的值
- V remove(Object key) 删除一个键对应的值,返回值是这个键之前对应的值
- void clear() 清除所有键值对的对应关系
- boolean containsKey(Object key)/containsValue(Object value) 是否存在某个键/值
- Set<Map.Entry<K,V>> entrySet() 返回包含所有键值对的 Set
然后就是几个主要子类:
HashMap 通过散列函数使得查找速度非常快,但不保存键的顺序(和 HashSet 差不多)。
TreeMap 始终按照指定排序规则的升序来保存键,所以查找速度比 HashMap 要慢。
LinkedHashMap 在保持 HashMap 的查找速度的同时按照键的插入顺序保存键。