-
- 1.1. 迭代器
- 1.2. 实现foreach循环
- 1.3. 泛型实用方法
1. collection接口
在java类库中,集合类的基本接口是Collection接口。这个接口有两个基本方法:
1 | public interface Collection<E>{ |
1.1. 迭代器
Iterator接口包含4个方法
1 | public interface Iterator<E> { |
- 调用next方法可以访问集合的下一个元素,但在此之前需要调用hasNext方法确定是否还有下一个元素,否则会抛出一个异常。
- java的迭代器和c++不同,c++是根据数组索引建模的;而java迭代器可以看成位于两个元素中间,当调用next方法,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
- Iterator的remove方法会删除上一次调用next方法时返回的元素,如果在调用next方法之前没有调用next,会抛出异常,即remove对next有依赖性。
1.2. 实现foreach循环
要想使用foreach循环,需要实现Iterable接口
1 | public interface Iterable<T> { |
接口中的方法返回一个实现类的迭代器,因此还必须先实现这个迭代器,下面以自己实现的LIFO栈为例
1 | public class stack<T> implements Iterable<T> { |
在该类中,我编写了实现了Iterator接口的内部类,其能够按照后进先出的顺序遍历堆栈。Iterable中的方法返回该类的引用,因此foreach循环能够调用此迭代器来遍历堆栈。
Collection接口扩展了Iterable接口,因此对于标准库中的任何集合都能够使用foreach循环。
总之,对于自己实现的类,要想使用foreach循环,要经历以下步骤:
- 实现一个适用于该类的迭代器。
- 实现Iterable接口并重写其方法,返回你所写的迭代器。
1.3. 泛型实用方法
Colllection接口中还有其他实用的方法
1 | public interface Collection<E> extends Iterable<E> { |
所有的实现类都必须要提供这些方法,这会非常麻烦,因此java类库中有一个AbstractCollection类,其实现Collection接口中大部分的方法。但是仍保持size和iterator仍为抽象方法。
因此具体集合类能够扩展AbstractCollection类,只需提供size方法和iterator方法即可。
2. 集合框架中的接口
集合有两个基本接口:Collection和Map。第二个为映射。
图片转载自网络
因为映射包含键值对,因此在映射中插入对象要用put方法:
1 | V put(K key,V value) |
要从集合中读取元素,可以使用迭代器访问元素。不过,从映射中读取值则需要使用get方法:
1 | V get(K key) |
2.1. List接口
List是个有序集合,继承自Collection。元素会增加到容器中的特定位置。可以采用两种方式访问元素:
- 使用迭代器访问。
- 使用一个整数索引来访问,该方式为随机访问。
List接口定义了多个用于随机访问的方法:
1 | void add(int index,E element) |
它也有个抽象类AbstractList,其实现了List接口中的大部分方法。
实现类中最常用的是数组列表(ArrayList)和链表(LinkList),第一个是以数组为基准实现的,而第二个是基于双向链表实现的。
链表的插入和删除效率高于数组,但是查找元素的效率却很低。链表中的get和set方法的效率是非常低的,并不推荐使用,因此有一个专门用于链表的迭代器——listIterator,可以使用listIterator()方法返回
1 | LinkedList linkedList = new LinkedList(); |
ListIterator接口中提供了许多有用的方法:
1 | public interface ListIterator<E> extends Iterator<E> { |
注意:集合中的remove方法不能连续调用,需要调用一次next或previous在进行remove操作。如果上一次操作是previous,remove会删除右侧的元素。
如果一个迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的某个方法修改了,就会抛出一个异常。
因此,为了避免并发修改异常,请遵循这样一个原则:
- 可以根据需要为一个集合关联多个迭代器,前提是这些迭代器只能读取集合,或者可以在关联一个能同时读写的迭代器。
2.2. 散列集
先看一下散列表的概念:
有一种数据结构可以用于快速的查找对象,这就是散列表。
散列表为每个对象计算一个整数,称为散列码,散列码是由对象的实例字段得出的一个整数,有不同数据的对象将产生不同的散列码。
Object类中有一个hashCode方法来产生一个散列码,如果定义自己的类,需要重写hashCode,使之与equals方法相容(即两个相同的对象产生的散列码也要相同)
在java中,散列表用链表数组实现,每个列表称为桶。要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。
java集合类库中提供了一个HashSet类,它实现了基于散列表的集(set)。
而对于Set接口,其等同于Collection接口,但是它的add方法不允许增加重复的元素,并且不在意元素之间的顺序,只要两个集具有相同的元素,就可以认为他们是相等的。
HashSet只是不保证有序,并不是保证无序!!!
1 | HashSet<String> strings = new HashSet<>(); |
2.3. 树集
TreeSet类与散列集十分类似,不过树集是一个有序集合。
可以将任意顺序的元素插入到集合中。在对集合进行遍历时,值将自动地按照排序后的顺序呈现。
1 | TreeSet<String> strings = new TreeSet<>(); |
排序是用红黑树完成的,因此将一个数据元素添加到树中要比添加到散列表中慢。
2.4. 队列与双端队列
双端队列不同于队列,其允许在头部和尾部都高效地添加或删除元素,不支持在队列中间添加元素。如果要使用双端队列,需要实现Deque接口。
ArrayDeque和LinkedList类都实现了这个接口,因此能在队头和队尾进行操作。
Deque中的方法
1 | public interface Deque<E> extends Queue<E> { |
2.5. 优先队列
优先队列(PriorityQueue)中的元素可以按照任意的顺序插入,但会按照有序的顺序进行检索。无论何时调用remove方法,总会获得当前优先队列中最小的元素。
但是:优先队列并没有对所有的元素进行排序! 其只是使用了堆这个数据结构来找出最小的元素。
3. 映射
映射用来存放键值对。如果提供了键,就能找到值。
3.1. 基本映射操作
java类库为映射提供了两个通用的实现:HashMap和TreeMap,这两个类都实现了Map接口。
每当往映射中添加一个对象时,必须同时提供一个键。在这里键是String对象,对应的值为Integer对象。
1 | HashMap<Integer, String> hash = new HashMap<>(); |
要想检索一个对象,必须使用键!
1 | System.out.println(hash.get(1));//输出 小明 |
如果映射中没有存储与给定键对应的信息,get将返回null。可以使用默认值来代替null。
1 | System.out.println(hash.getOrDefault(3,"空缺"));//第二个参数为默认值 |
键必须是唯一的! 不能对同一个键存放两个值,如果对同一个键调用两次put方法,第二个值就会取代第一个值。但是put会返回第一个值。
1 | System.out.println(hash.put(1,"小王"));//输出 小明 |
3.2. 更新映射条目
如果要统计一个单词在文件中的出现数目,看到一个单词时计数器加1
1 | counts.put(word,counts.get(word)+1);//counts是一个映射,键为单词,值为出现数 |
但是在第一次看见这个单词时,其数目为null,对null操作会产生一个异常。
有三种方法来更新这个值:
- 使用默认值
1 | counts.put(word,counts.getOrDefault(word,0)+1); |
- putIfAbsent方法
1 | counts.putIfAbsent(word,0);//如果word不存在或与null关联,则将其与0关联 |
- merge方法
1 | counts.merge(word,1,Integer::sum); |
3.3. 映射视图
集合框架不认为映射本身是一个集合。不过,可以得到映射的视图(view)——这是实现了Collection接口或某个子接口的对象。
有三种视图:
- 键集
- 值集合(不是一个集)
- 键/值对集
下面的方法会返回上面所列的视图
1 | Set<K> KeySet()//键集 |
其中KeySet不是HashSet或TreeSet,而是实现了Set接口的另外类的对象。
由于都扩展了Collection接口,所以他们都可以用foreach循环来遍历
1 | HashMap<Integer, String> hash = new HashMap<>(); |
注意:
如果在视图上删除某个值或者键,映射中的键值对都会被删除!
不能向视图中添加元素!
3.4. 弱散列映射
如果有一个值,其对应的键已经不再在程序中的任何地方使用,因为没有任何途径可以引用这个值的对象,所以无法从映射中删除这个这个键值对。而垃圾回收器也不会回收它(其中所有的桶是活动的)。因此,需要程序负责从长期存活的映射表中删除那些无用的值。
或者使用WeakHashMap。
WeakHashMap使用弱引用保存键,对于这种类型的对象,垃圾回收器会回收。
3.5. 连接散列集和映射
LinkedHashSet 和 LinkedHashMap 类会记住插入元素项的顺序。这样就可以避免散列表中的项看起来顺序是随机的。
3.6. 标识散列映射
类IdentityHashMap 有特殊的用途。在这个类中,键的散列值不是用hashCode计算的,而是用System.IdentityHashCode方法计算的。这是根据对象的内存地址计算散列码时所使用的方法。而且,在对两个对象进行比较时,其使用”==”而不是equals。
4. 视图与包装器
4.1. 小集合
java9中引入了一些静态方法,可以生成给定的元素的集或列表,以及给定键值对的映射。
1 | List<String> names = List.of("a","b","c"); |
会分别生成包含3个元素的一个列表、一个集和一个映射。
Map接口有一个静态方法ofEntries,能接受任意多个Map.Entry<K,V>对象。
1 | Map<String, Integer> s = Map.ofEntries( |
这些集合对象都是不可修改的! 如果需要一个可更改的集合,可以把这个不可修改的集合传递到构造器:
1 | List<String> hello = List.of("hello", "java"); |
4.2. 子范围
可以为很多集合建立子范围视图。
1 | ArrayList<Integer> integers = new ArrayList<>(); |
第一个索引包含在内,第二个则不包含。这与String类的substring操作中的参数情况相同。
可以对子范围应用任何操作,而且操作会自动反映到整个列表!
对于有序集合映射,可以使用排序顺序而不是元素位置建立子范围。具体在SortedSet接口中。
4.3. 不可修改的视图
Collections类(不是Collection接口)有几个可以生成不可修改视图的方法
1 | public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) { |
每个方法都定义处理一个接口。例如,Collections.unmodifiableList处理ArrayList、LinkedList或者实现了List接口的其他类。这个方法会返回一个实现了List接口的类对象,他们是不可更改的。
4.4. 检查型视图
将错误的类型混入泛型集合中的情况极有可能发生
1 | ArrayList<String> strings = new ArrayList<>(); |
这个错误的命令在运行时检查不出来,这时候可以用检查型视图,如果插入的对象不属于指定的类,就会抛出一个ClassCastException异常
1 | ArrayList<String> strings = new ArrayList<>(); |
5. 算法
在java集合框架中还有一些有用的算法。
5.1. 排序与混排
Collections类的sort方法实现了对List接口的集合进行排序,这个方法假定列表元素实现了Comparable接口。
1 | var staff = new LinkedList<String>(); |
或者可以使用List接口的sort方法,不过这个方法需要传进一个Comparator对象。
1 | staff.sort(Comparator a); |
如果想按降序对列表进行排序,可以使用静态的遍历方法Collections.reverseOrder()。这个方法将返回一个比较器
1 | staff.sort(Comparator.reverseOeder()); |
注意:集合类库中的排序不是快速排序!
Collections类中还有一个算法shuffle,其会打乱集合中的顺序,作用与排序相反。
5.2. 二分查找
Collections类中还有个二分查找的方法,不过只适用于随机访问的集合,而对于链表,它会自动地退化为线性查找
Collections类中还有许多简单算法,例如求最大值,最小值,拷贝,交换位置等等。
5.3. 批操作
很多操作会“成批”复制或删除元素。
- a.removeAll(b); 会从a中删除b中出现的所有元素
- a.retainAll(b); 会从a中删除所有未在b中出现的元素
上面的方法是定义在Collection接口中的,Map中没有此方法,不过可以对Map的视图进行操作。
5.4. 集合与数组的转换
如果需要把一个数组转换为一个集合,可以使用List.of
1 | String[] values = ...; |
将一个集合转换为数组会麻烦一些,需要使用toArray方法
1 | staff.toArray(new String[0]); |