title: 集合
tag: Java集合
categories: 分类
comment: 是否允许评论(true or false)
description: 描述
top_img: https://z3.ax1x.com/2021/10/06/4xq2s1.png
cover: https://z3.ax1x.com/2021/10/06/4xq2s1.png
Java集合框架
集合接口与实现分离
队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。
队列接口的最简形式
public interface Queue<E>
{
void add(E element);
E remove();
int size();
}
队列实现的方式有两种:一种是使用循环数组,另一种是使用链表。
每一个实现都可以用一个实现了Queue接口的类表示:
public class CircularArrayQueue<E> implements Queue<E>
{
private int head;
private int tail;
CircularArrayQueue(int capacity){
...
}
public void add(E element){...}
public E remove(){...}
public int size(){...}
private E[] elements;
}
public class LinkedListQueue<E> implements Queue<E>{
private Link head;
private Link tail;
LinkedListQueue(){...}
public void add(E element){...}
public E remove(){...}
public int size(){...}
}
当在程序中使用队列时,一旦已经构造了集合,就不需要知道究竟使用了哪种实现。因此,只有在构造集合对象时,才会使用具体的类。可以使用接口类型存放集合引用。
Queue<Customer> expressLane = new LinkedListQueue<>();
expressLane.add(new Customer("Harry"));
Collection接口
在Java类库中,集合类的基本接口是Collection接口。这个接口中有两个基本方法:
public interface Collection<E>{
boolean add(E element);
Iterator<E> iterator();
...
}
add方法用于向集合中添加元素,如果添加元素确实改变了集合就返回true;如果集合没有发生变化就返回false.如果试图向set中添加一个对象,而这个对象在set中已经存在,这个add请求就没有实效,因为set中不允许存放有重复的对象。
迭代器
Iterator接口中包含4个方法:
public interface Iterator<E>{
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<? super E> action);
}
通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next方法将抛出一个NoSuchElementException。因此,需要在调用next之前调用hasNext方法。如果迭代器对象还有多个可以访问的元素,这个方法就返回true。
Collection<String> c =...;
Iterator<String> iter = c.iterator();
while(iter.hasNext()){
String element = iter.next();
do something with element;
}
用"for each"同样可以更加简练地表示同样的循环操作:
for(String elemet: c){
do something with element;
}
编译器简单地将"for each"循环转换为带有迭代器的循环。
"for each"循环可以处理任何实现了Iterator接口的对象,这个接口只包含了一个抽象方法:
public interface Iterable<E>{
Iterator<E> iterator();
...
}
Collection接口扩展了Iterator接口。因此,对于标准类库中的任何集合都可以使用"for each"循环。
也可以调用forEachRemaining方法并提供一个lambda表达式。将对迭代器的每一个元素调用这个lambda表达式,直到再没有元素为止。
iterator.forEachRemaining(element -> do something with element);
可以认为Java迭代器位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
Iterator接口的remove方法将会删除上次调用next方法时返回的元素。在大多数情况下,这是有道理的,在决定删除某个元素之前应该先看下这个元素。不过如果想要删除指定位置上的元素,仍然需要越过这个元素。如,可以删除一个字符集合中的第一个元素:
Iterator<String> it = c.iterator();
it.next(); // skip over the first element
it.remove(); // now remove it
更重要的是,next方法和remove方法调用之间存在依赖性。如果调用remove之前没有调用next,将是不合法的。这样做,会抛出一个IllegalStateException异常。
it.remove();
it.remove(); // ERROR
// 实际上,必须先调用next越过将要删除的元素
it.remove();
it.next();
it.remove(); //OK
泛型实用方法
由于Collection与Iterator都是泛型接口,这意味着你可以编写处理任何集合类型的实用方法。下面是一个检测任意集合是否包含指定元素的泛型方法:
public static <E> boolean contains(Collection<E> c,Object obj){
for(E element : c){
if(element.equals(obj))
return true;
return false;
}
}
事实上,Collection接口声明了很多有用的方法,所有的实现类都必须提供这些方法。为了能够让实现者更容易地实现这个接口,Java类库提供了一个类AbstractCollection,它保持基本方法size和iterator仍为抽象方法,但是为实现者实现了其他例行方法。如:
public abstract class AbstractCollection<E> implements Collection<E>
{
...
public abstract Iterator<E> iterator();
public boolean contains(Object obj){
for(E element: this){
if(element.equals(obj)){
return true;
}else{
return false;
}
}
...
}
}
java.util.Collection.
Iterator<E> iterator()
// 返回一个用于访问集合中各个元素的迭代器
int size()
// 返回当前存储在集合中的元素个数
boolean isEmpty()
// 如果集合中没有元素,返回true
boolean contains(Object obj)
// 如果集合中包含了一个与obj相等的对象,返回true
boolean containsAll(Collection<?> other)
// 如果这个集合包含other集合中的所有元素,返回true
boolean add(E element)
// 将一个元素添加到集合中。如果由于这个调用改变了结合,返回true。
boolean addAll(Collection<? extends E> other)
// 将other集合中的所有元素添加到这个集合。如果由于这个调用改变了集合,则返回true.
boolean remove(Object obj)
// 从这个集合中删除等于Obj的对象,如果有匹配的对象被删除,就返回true
void clear()
// 从这个集合中删除所有的元素
boolean retainAll(Colleation<?> other)
// 从这个集合中删除所有与other集合中元素不同的元素。如果由于这个调用改变了集合,返回true.
Object[] toArray()
// 返回这个集合中的对象的数组
<T> T[] toArray(T[] arrayToFill)
// 返回这个集合中的对象的数组。如果arrayToFill足够大,就将集合中的元素填入这个数组中。剩余空间填补null;否则,分配一个数组,其成员类型与arrayToFill的成员类型相同,其长度等于集合的大小,并填充集合元素。
Java.util.Iterator
boolean hasNext()
// 如果存在另一个可访问的对象,则返回true
E next()
// 返回将要访问的下一个对象。如果已经到达了集合的末尾,将抛出一个NoSuchElementException.
void remove()
// 删除上次访问的对象。这个方法必须紧跟在访问一个元素之后执行。如果上次访问之后集合已经发生了变化,这个方法将抛出一个IllegalStateException.
default void forEachRemaining(Consumer<? super E> action)
// 访问元素,并传递到指定的动作,直到再没有更多元素,或者这个动作抛出一个异常。
集合框架中的接口
集合中有两个基本接口:Collection和Map。我们可以用以下方法在集合中插入元素:
boolean add(E element)
Set接口等同于Collection接口,不过其方法的行为有更严谨的定义。set的add方法不允许有重复的元素。要适当地定义set的equals方法:只要两个set中包含同样的元素就认为它们是相等的,而不要求这些元素有同样的问题。hashCode方法的定义要保证包含相同元素的两个set会得到相同的散列码。
具体集合
链表
使用了数组以及动态的ArrayList类。不过。数组和数组列表都有一个重大的缺陷。这就是从数组中删除一个元素开销很大,其原因是数组中位于被删除元素之后的所有元素都要向数组的前端移动。在数组中间插入一个元素也是如此。
另外的一个数据结构-链表解决了这个问题。数组在连续的存储位置上存放对象引用,而链表则是将每个对象存放在单独的链接中。每个链接还存放着序列中下一个链接的引用。在Java程序设计语言中,所有的链表实际上都是双向链接的–即每个链接还存放着其前驱的引用。
从链表中间删除一个元素是一个很轻松的操作,只需要更新所删除元素周围的链接即可。
数组列表
List接口用于描述一个有序集合,并且集合中每个元素的位置很重要。有两种访问元素的协议:一种是通过迭代器,另一种是通过get和set方法随机地访问每个元素。后者不适合链表,但是get和set方法对数组很有用。集合类库提供了一种大家熟悉的ArrayList类,这个类也实现了List接口。ArrayList封装了一个动态在分配的对象数组。
散列集
如果想要查看某个指定得到元素,却又不记得它的位置,就需要访问所有元素,直到找到为止。如果集合中包含的元素很多,这将会需要很长时间。如果不在意元素的顺序,有几种能够快速查找元素的数据结构。缺点是无法控制元素的次序。
有一个众所周知的数据结构,可以用于快速地查找对象,这就是散列表。散列表为每个对象计算一个整数,称为散列码。散列码是由对象的实例字段得出的一个整数。
在Java中,散列表应链表数组实现。每个列表被称为桶。要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。如果某个对象的散列码为76268,并且有128个桶,那么这个对象就应该保存在108号桶中。在这个桶中没有其他元素,此时将元素直接插入到桶中就可以了。当然,有时候会遇到桶已经被填充的情况。这种现象被称为**散列冲突。**这时候需要将新对象与桶中的所有对象进行比较。查看这个对象是否已经存在。如果散列码合理地随机分布,桶的数目也足够大,需要比较的次数就会很少。
通常,将桶数设置为预计元素个数的75%~150%。最好将桶数设置为一个素数,以防止键的聚集。
并不是总能够知道需要存储多少个元素,也有可能最初的估计过低。如果散列表太满,就需要再散列。如果堆散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。装填因子可以确定何时对散列表进行再散列。例如:如果装填因子是0.75(默认值),说明表中已经填满了75%以上,就会自动再散列,新表的桶数是原来的两倍。对于大多数应用程序来说,装填因子为0.75是合理的。
散列表可以用于实现很多重要的数据结构。最简单的是集类型。集是没有重复元素的元素集合。集(Set)的add方法首先要在这个集中查找要添加的对象,如果不存在,就添加这个对象。
散列集迭代器将依次访问所有的桶。由于散列将元素分散在表中,所以会以一种看起来随机的顺序访问元素。
Java.util.HashSet
HashSet()
// 构造一个空散列集
HashSet(Collection<? extends E> elements)
// 构造一个散列集,并将集合中的所有元素添加到这个散列集中
HashSet(int initicalCapacity)
// 构造一个空的具有指定容量的散列集
HashSet(int initialCapacity,float loadFactor)
// 构造一个有指定容量和装填因子的空散列集
树集
TreeSet类与散列集十分类似,但是,它比散列集有所改进。树集是一个有序集合。可以任意顺序将元素插入到集合中。在对集合进行遍历时,值将自动地按照排序后的顺序呈现。
队列与双端队列
队列允许你高效地在尾部添加元素,并在头部删除元素。双端队列允许在头部和尾部都高效地添加或删除元素。
Java6中引入了Deque接口,ArrayDeque和LinkedList类实现了这个接口。这两个类都可以提供双端队列,其大小可以根据需要扩展。
java.util.Queue<E>
boolean add(E element)
boolean offer(E element)
// 如果队列没有满,将给定的元素添加到这个队列的队尾并返回true.如果队列已满,第一个方法将抛出一个IllegalStateException,而第二个方法返回false.
E remove()
E poll()
// 假如队列不为空,删除并返回这个队列队头的元素。如果队列是空的,第一个方法抛出NoSuchElementException,而第二个方法返回null。
E element()
E peek()
// 如果队列不为空,返回这个队列队头的元素,但不删除。如果队列空,第一个方法将抛出一个NoSuchElementException,而第二个方法返回null。
java.util.Deque<E>
void addFirst(E element)
void addLast(E element)
boolean offerFirst(E element)
boolean offerLast(E element)
// 将给定的对象添加到双端队列的队头或队尾。如果这个双端队列已满。前面两个方法将抛出一个IllegalStateException,而后面两个方法返回false.
E removeFirst()
E removeLast()
E pollFirst()
E pollLast()
// 如果这个双端队列不为空,删除并返回双端队列头的元素。如果双端队列为空,前面两个方法将抛出一个NoSuchElementException,而后面两个方法将返回null.
优先队列
优先队列中的元素可以按照任意的顺序插入,但会按照有序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先队列中最小的元素。优先队列并没有对所有元素进行排序。如果迭代处理这些元素,并不需要对它们进行排序。优先队列使用了一个精巧而高效的数据结构,称为堆。堆是一个可以自组织的二叉树,其添加和删除操作可以让最小的元素移动到根,而不必花费时间对元素进行排序。
优先队列的典型用法是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除。
映射
基本映射操作
Java类库为映射提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索数。散列或比较函数只应用于键。与键关联的值不进行散列或比较。
如果不需要按照有序的顺序访问键,最好选择散列映射。
下面的代码将建立一个散列映射来存储员工信息:
var staff = new HashMap<String,Employee>();
var harry = new Employee("Harry Hacker");
staff.put("9887-98-9996",harry);
...
每当往映射中添加一个对象时,必须同时提供一个键。在这里,键是一个字符串,对应的值是Employee对象。
要想检索一个对象,必须使用键
var id = "987-98-9996";
Employee e = staff.get(id);
如果映射中没有存储与给定键对应的信息,get将返回null。
null返回值可能并不方便。有时对应没有出现在映射中的键,可以使用一个好的默认值。然后使用getOrDefault方法。
Map<String,Integer> scores = ...;
int score = scores.getOrDefault(id,0); // 如果id不存在就会返回0
键必须是唯一的。不能对同一个键存放两个值。如果对同一个键调用两次put方法,第二个值就会取代第一个值。
remove方法从映射中删除给定键对应的元素。size方法返回映射中的元素数。
要迭代处理映射的键和直,最容易的方法是使用forEach方法。可以提供一个接收键和值的lambda表达式。映射中的每一项会依序调用这个表示式。
scores.forEach((k,v)->
System.out.println("key=" + k +", value = "+v));
java.util.Map
V get(Object key)
// 获取与键关联的值;返回与键关联的对象,如果映射中没有这个对象,则返回null.实现类可以禁止键为Null。
default V getOrDefault(Object key,V defaultValue)
// 获得与键关联的值,返回与键关联的对象,或者如果未在映射中找到这个键,则返回defaultValue.
V put(K key,V value)
// 将关联的一对键和值放到映射中。如果这个键已经存在,新的对象将取代与这个键关联的旧对象。这个方法将返回关联的旧值。
void putAll(Map<? extends K,? extends V> entries)
// 将给定映射中的所有映射条目添加到这个映射中
boolean containsKey(Object key)
// 如果在映射中已经有这个键,返回true
boolean containsValue(Object value)
// 如果映射中已经有这个值,返回true。
java.util.HashMap
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity,float loadFactor)
// 用给定的容量和装填因子构造一个空散列映射,装填因子是一个0.0~1.0之间得到数。默认装填因子是0.75.
java.util.SortedMap
Comparator<? super K> comparator()
// 返回对键进行排序的比较器。如果键是用Comparable接口的compareTo方法进行比较,则返回null.
K firstKey()
K lastKay()
// 返回映射中的最小或最大键
更新映射条目
考虑使用映射统计一个单词在文件中出现的频度。看到一个单词是,我们将计数器增1
counts.put(word,counts.get(word) + 1);
// 第一个看到word时,在这种情况下,get会返回null,因此会出现一个NullPointException异常。
// 一种补救的方法就是使用getOrDefault方法:
counts.put(word,counts.getOrDefault(word,0)+1);
// 另一种方法是首先调用putIfAbsent方法,只有当键存在时才会放入一个值。
counts.putIfAbsent(word,0);
counts.put(word,counts.getOrDefault(word,0)+1);
java.util.Map<K,V>
default V merge(K key,V value,BiFunction<? super V,? super V ,? extends V> remappingFunction)
// 如果key与一个非null值V关联,将函数应用到V和value,将key与结果关联,或者如果结果为null,则删除这个键。否则,将Key与value关联,返回get(key)
default V computeIfPresent(K key,BiFunction<? super V,? super V ,? extends V> remappingFunction)
// 如果key与一个非null值V关联,将函数应用到key和V,将key与结果关联,或者如果结果为null,则删除这个键。返回get(key).
映射视图
集合框架不认为映射本身是一个集合。不过,可以得到映射的视图–这是实现了Collection接口的某个子接口的对象。
有3种视图:键集、值集合以及键/值对集。键和键/值对可以构成一个集,因为映射中一个键只能有一个副本。
需要说明的是,keySet不是HashSet或TreeSet,而是实现了Set接口的另外某个类的对象。Set接口扩展了Collention接口。因此,可以像使用任何集合一样使用keySet.
Set<String> keys = map.keySet();
for(String key : keys){
do something with key
}
如果想同时查看键和值,可以通过枚举映射条目来避免查找值。使用下面的代码:
for(Map.Entry<String,Employee> entry:staff.entrySet()){
String k = entry.getKey();
Employee v = entry.getValue();
do something with k,v
}
java.util.Map<K,V>
Set<Map.Entry<K,V>> entrySet()
// 返回Map.Entry对象的一个集视图。可以从这个集中删除元素,它们将从映射中删除,但是不能添加任何元素
Set<K> keySet()
// 返回映射中所有键的一个集视图。可以从这个集中删除元素,键和相关联的值将从映射中删除,但是不能添加任何元素。
Collection<V> values()
// 返回映射中所有值的一个集合视图。可以从这个集合中删除元素,所删除的值及相应的键将从映射中删除,不过不能添加任何元素。
java.util.Map.Entry
K getKey()
V getValue()
// 返回这个映射条目的键和值
V setValue(V newValue)
// 将相关映射中的值改为新值,并返回原来的值
枚举集与映射
EnumSet是一个枚举类型元素的高效实现。由于枚举类型只有有限个实例,所有EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置1.
EnumSet类没有公共的构造器。要使用静态工厂方法构造这个集:
enum Weekday{MONDAY ,TUESDAY,WENDESDAY,THURSDAY,FRIDAY,STATUDAY,SUNDAY};
EnumSet<Weekday> always = EnumSet.allOf(Weekday.class);
EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);
可以使用Set接口的常用方法来修改EnumSet.
EnumMap是一个键类型为枚举类型的映射,它可以直接且高效地实现一个值为数组。需要在构造器中指定键类型。
var personInCharge = new EnumMap<Weekday,Employee>(Weekday,class);
视图与包装器
小集合
Java 9 引入了一些静态方法,可以生产给定元素的集或列表,以及给定键/值对的映射
List<String> names = List.of("Peter","Paul","Mary");
Set<Integer> numbers = Set.of(2,3,5);
会分别生成一个包含3个元素的一个列表和一个集。对于映射,需要指定键和值,如下所示:
Map<String,Integer> scores = Map.of("Peter",2."Paul",3,"Mary",5);
元素、键或值不能为null.
List和Set接口有11个方法,分别为0到10个参数,另外还有一个参数可变的of方法。提供这种特定性是为了提高效率。
如果需要一个可更改的集合,可以把这个不可修改的集合传递到构造器:
var names = new ArrayList<>(List.of("Peter","Paul","Mary"));
子范围
可以为很多集合建立子范围视图。假如有一个列表staff,想从中取出第10个~第19个元素。可以使用subList方法来获得这个列表子范围的视图。
List<Employee> group2 = staff.subList(10,20);
// 第一个索引包含在内,第二个索引则不包含在内。
Set numbers = Set.of(2,3,5);
会分别生成一个包含3个元素的一个列表和一个集。对于映射,需要指定键和值,如下所示:
```java
Map<String,Integer> scores = Map.of("Peter",2."Paul",3,"Mary",5);
元素、键或值不能为null.
List和Set接口有11个方法,分别为0到10个参数,另外还有一个参数可变的of方法。提供这种特定性是为了提高效率。
如果需要一个可更改的集合,可以把这个不可修改的集合传递到构造器:
var names = new ArrayList<>(List.of("Peter","Paul","Mary"));
子范围
可以为很多集合建立子范围视图。假如有一个列表staff,想从中取出第10个~第19个元素。可以使用subList方法来获得这个列表子范围的视图。
List<Employee> group2 = staff.subList(10,20);
// 第一个索引包含在内,第二个索引则不包含在内。