图源:
本篇文章是的下篇。
Set
也是一种常见的数据类型,很多编程语言都会提供这种容器。它的主要用途有两个:
-
提供一个可供查询的去重容器。
-
进行集合运算。
在Java中,Set
是一个继承自Collection
的接口:
public interface Set<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
...
"varargs")
( static <E> Set<E> of(E... elements) {
switch (elements.length) { // implicit null check of elements
case 0:
"unchecked")
( var set = (Set<E>) ImmutableCollections.EMPTY_SET;
return set;
case 1:
return new ImmutableCollections.Set12<>(elements[0]);
case 2:
return new ImmutableCollections.Set12<>(elements[0], elements[1]);
default:
return new ImmutableCollections.SetN<>(elements);
}
}
"unchecked")
( static <E> Set<E> copyOf(Collection<? extends E> coll) {
if (coll instanceof ImmutableCollections.AbstractImmutableSet) {
return (Set<E>)coll;
} else {
return (Set<E>)Set.of(new HashSet<>(coll).toArray());
}
}
}
如果仔细对比Set
与中介绍的Collection
就能发现,其实Set
与Collection
的方法几乎一摸一样,它并没有像List
那样添加额外方法。所以Set
与Collection
在用法上是完全相同的,不同的是Set
被定义为”一种去重容器“。
标准库主要提供三种实现了Set
接口的容器:
package ch9;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new TreeSet<>();
Set<Integer> set3 = new LinkedHashSet<>();
}
}
它们的区别是:
-
HashSet
,最常用的Set
,采用哈希算法实现,有最快的查询效率。 -
TreeSet
,采用树作为底层数据结构,会将元素以一定规则进行排序,查询效率不如HashSet
。 -
LinkedHashSet
,同样用哈希算法实现,有较快查询效率,同时用链表保留元素添加顺序。
用一个简单的例子说明这几种Set
的区别:
package ch9.set2;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
public class Main {
private static Random random = new Random();
private static void fillSet(Set<Integer> set) {
for (int i = 0; i < 10; i++) {
set.add(random.nextInt(100));
}
}
private static void printSet(Set<Integer> set) {
for (Integer integer : set) {
System.out.print(integer + " ");
}
System.out.println();
}
public static void main(String[] args) {
Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new TreeSet<>();
Set<Integer> set3 = new LinkedHashSet<>();
fillSet(set1);
printSet(set1);
fillSet(set2);
printSet(set2);
set3.add(1);
set3.add(2);
set3.add(3);
printSet(set3);
// 96 64 33 97 82 2 8 41 42 79
// 38 47 49 53 55 57 66 70 92
// 1 2 3
}
}
TreeSet
默认会以自然排序由大大小排列元素。之所以要进行排序,是因为排序后的元素可以利用类似二分查找的方式进行查找,这样查询效率不会太慢。
TreeSet
还支持一个接收Comprator
类型参数的构造函数,可以通过这个构造函数来改变排序规则:
package ch9.set3;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.Collections;
public class Main {
private static Random random = new Random();
private static void fillSet(Set<Integer> set) {
for (int i = 0; i < 10; i++) {
set.add(random.nextInt(100));
}
}
private static void printSet(Set<Integer> set) {
for (Integer integer : set) {
System.out.print(integer + " ");
}
System.out.println();
}
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>(new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
int a = o1.intValue();
int b = o2.intValue();
if (a < b) {
return 1;
} else if (a == b) {
return 0;
} else {
return -1;
}
}
});
fillSet(set);
printSet(set);
// 83 79 73 72 67 58 29 17 5
set = new TreeSet<>(Collections.reverseOrder());
fillSet(set);
printSet(set);
// 72 69 67 59 55 42 23 21 20 12
}
}
上边示例中分别通过两种方式指定Comparator
,分别是用匿名类自己实现和使用Collections.reverseOrder()
方法返回的预定义Comparator
。两者效果是一样的,都实现了对Integer
类型Set
的逆序排列。
对于内置类型,如Integer
、Character
,都实现了Comparable
接口,所以可以直接使用TreeSet
,但对于自定义类型,则必须实现Comparable
接口或者给TreeSet
指定一个Comparator
:
package ch9.set4;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import util.Fmt;
import java.util.Collection;
import java.util.Comparator;
import java.util.Collections;
class Student {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public String toString() {
return Fmt.sprintf("Student(name:%s,age:%d)", name, age);
}
}
public class Main {
public static void main(String[] args) {
// class ch9.set4.Student cannot be cast to class java.lang.Comparable ...
Set<Student> students = new TreeSet<>();
students.add(new Student(19, "Li Lei"));
students.add(new Student(15, "Zhang San"));
students.add(new Student(20, "Han Meimei"));
}
}
上面代码无法通过编译,原因很明确:Student
没有实现Comparable
接口。
package ch9.set5;
import java.util.Set;
import java.util.TreeSet;
import util.Fmt;
class Student implements Comparable<Student> {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public String toString() {
return Fmt.sprintf("Student(name:%s,age:%d)", name, age);
}
public int compareTo(Student s) {
return Integer.compare(this.age, s.age);
}
}
public class Main {
public static void main(String[] args) {
Set<Student> students = new TreeSet<>();
students.add(new Student(19, "Li Lei"));
students.add(new Student(15, "Zhang San"));
students.add(new Student(20, "Han Meimei"));
for (Student s : students) {
System.out.print(s + " ");
}
System.out.println();
// Student(name:Zhang San,age:15) Student(name:Li Lei,age:19) Student(name:Han
// Meimei,age:20)
}
}
当然,换用Comparator
效果也是一样的:
package ch9.set6;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
import util.Fmt;
class Student {
...
public static Comparator<Student> comparator() {
return new Comparator<Student>() {
public int compare(Student o1, Student o2) {
return Integer.compare(o1.age, o2.age);
}
};
}
}
public class Main {
public static void main(String[] args) {
Set<Student> students = new TreeSet<>(Student.comparator());
students.add(new Student(19, "Li Lei"));
students.add(new Student(15, "Zhang San"));
students.add(new Student(20, "Han Meimei"));
for (Student s : students) {
System.out.print(s + " ");
}
System.out.println();
// Student(name:Zhang San,age:15) Student(name:Li Lei,age:19) Student(name:Han
// Meimei,age:20)
}
}
Map
Map
是一种映射类型,可以保存“键值对”,其用途与其它语言中的映射类型几乎一致。不过因为Java不像Python或者Go那样,直接在语言层面支持映射,无法通过类似map[key] = value;
这样的方式方便地赋值或者遍历。
Java中Map
是一个接口,下面是摘抄自Java源码的Map
定义:
public interface Map<K, V> {
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
V put(K key, V value);
V remove(Object key);
void putAll(Map<? extends K, ? extends V> m);
void clear();
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
interface Entry<K, V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K, V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K, V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
}
boolean equals(Object o);
int hashCode();
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
// ise thrown from function is not a cme.
v = function.apply(k, v);
try {
entry.setValue(v);
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
}
}
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
return false;
}
remove(key);
return true;
}
default boolean replace(K key, V oldValue, V newValue) {
Object curValue = get(key);
if (!Objects.equals(curValue, oldValue) ||
(curValue == null && !containsKey(key))) {
return false;
}
put(key, newValue);
return true;
}
default V replace(K key, V value) {
V curValue;
if (((curValue = get(key)) != null) || containsKey(key)) {
curValue = put(key, value);
}
return curValue;
}
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
}
return v;
}
default V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue;
if ((oldValue = get(key)) != null) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null) {
put(key, newValue);
return newValue;
} else {
remove(key);
return null;
}
} else {
return null;
}
}
default V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue = get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue == null) {
// delete mapping
if (oldValue != null || containsKey(key)) {
// something to remove
remove(key);
return null;
} else {
// nothing to do. Leave things as they were.
return null;
}
} else {
// add or replace old mapping
put(key, newValue);
return newValue;
}
}
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if (newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
"varargs")
( static <K, V> Map<K, V> ofEntries(Entry<? extends K, ? extends V>... entries) {
if (entries.length == 0) { // implicit null check of entries array
"unchecked")
( var map = (Map<K,V>) ImmutableCollections.EMPTY_MAP;
return map;
} else if (entries.length == 1) {
// implicit null check of the array slot
return new ImmutableCollections.Map1<>(entries[0].getKey(),
entries[0].getValue());
} else {
Object[] kva = new Object[entries.length << 1];
int a = 0;
for (Entry<? extends K, ? extends V> entry : entries) {
// implicit null checks of each array slot
kva[a++] = entry.getKey();
kva[a++] = entry.getValue();
}
return new ImmutableCollections.MapN<>(kva);
}
}
static <K, V> Entry<K, V> entry(K k, V v) {
// KeyValueHolder checks for nulls
return new KeyValueHolder<>(k, v);
}
"rawtypes","unchecked"})
({ static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map) {
if (map instanceof ImmutableCollections.AbstractImmutableMap) {
return (Map<K,V>)map;
} else {
return (Map<K,V>)Map.ofEntries(map.entrySet().toArray(new Entry[0]));
}
}
}
整理一下,主要方法有:
-
int size();
,返回键值对个数。 -
boolean isEmpty();
,映射是否为空。 -
boolean containsKey(Object key);
,是否包含某个key
。 -
boolean containsValue(Object value);
,是否包含某个值。 -
V get(Object key);
,根据给定的键获取对应的值,如果不存在,返回null
。 -
V put(K key, V value);
,设置键和值。 -
V remove(Object key);
,将指定的键移除,并返回对应的值。 -
void putAll(Map<? extends K, ? extends V> m);
,将指定映射的键值对添加到当前映射。 -
void clear();
,清空映射。 -
Set<K> keySet();
,返回由键组成的Set
。 -
Collection<V> values();
,返回由值组成的Collection
。 -
Set<Map.Entry<K, V>> entrySet();
,返回由Map.Entry
组成的Set
。 -
boolean equals(Object o);
,比较。 -
int hashCode();
,返回哈希值。 -
V getOrDefault(Object key, V defaultValue)
,如果匹配到给定的key
,返回对应的值,否则返回指定的默认值。 -
void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
,用指定的函数对所有的键值对进行处理(这应当是JavaSE8引入函数式编程后添加的方法)。 -
V putIfAbsent(K key, V value)
,如果指定的键不存在或者对应的值为null
,用给定的键值对进行设置。 -
boolean remove(Object key, Object value)
,如果Map
存在指定的键和值,则删除。 -
boolean replace(K key, V oldValue, V newValue)
,如果Map
存在指定的键和值(oldValue),则用指定的新值(newValue)替换。 -
V replace(K key, V value)
,如果指定的key
存在,使用value
进行替换。 -
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
,如果指定的键不存在或者对应的值为null
,用给定的函数处理后生成对应的值。 -
V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
,如果指定的键存在,且值不为null
,使用给定的函数处理后生成新值。
类似于List
,Map
同样包含两个用于生成不可修改的映射的静态方法:
-
static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map)
-
static <K, V> Map<K, V> of(K k1, V v1)
生成的“不可修改的映射”中,键和值都不能包含null
。
实际上
of
函数同样有多组重载方法,但因为是键值对这种形式的参数,所以没法使用可变参数列表。
Map
接口包含一个嵌套接口Entry<k,V>
,其作用是充当一种键值对类型,作为Map.entrySet
方法返回的Set
的元素类型,用于对Map
的遍历。这个接口包含以下主要方法:
-
K getKey();
,返回键。 -
V getValue();
,返回值。 -
V setValue(V value);
,设置值。 -
boolean equals(Object o);
,比较。 -
int hashCode();
,返回当前Entry
的哈希值。
还包含几个静态方法:
-
comparingByKey()
,返回对Entry
键进行自然排序的comparator
。 -
comparingByValue()
,返回对Entry
值进行自然排序的comparator
。 -
comparingByKey(Comparator<? super K> cmp)
,指定一个Comparator
,并返回对Entry
的键进行排序的Comparator
。 -
comparingByValue(Comparator<? super V> cmp)
,指定一个Comparator
,并返回对Entry
的值进行排序的Comparator
。
篇幅关系,省略返回值类型。
Map
最常见的用途是统计键出现的频率,用一个简单示例说明:
package ch9.map2;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> counter = new HashMap<>();
Random random = new Random();
for (int i = 0; i < 100; i++) {
int num = random.nextInt(10) + 1;
int oldTimes = counter.getOrDefault(num, 0);
counter.put(num, oldTimes + 1);
}
System.out.println(counter);
// {1=12, 2=11, 3=11, 4=8, 5=6, 6=8, 7=8, 8=13, 9=5, 10=18}
}
}
这个程序循环100次,每次产生1~10的随机数,并利用Map
统计随机数出现的频率。
需要注意的是,Java的Map
其键和值都必须是对象,所以如果尝试访问一个不存在的键,Map.get
返回的值是null
,而非你期望的相应“零值”,这点和Go或Python之类的语言有很大不同。
所以在上面的示例中使用counter.getOrDefault(num, 0)
这样的方式来避免额外的初始化语句,如果不这么做的话,可能需要进行额外的初始化工作:
package ch9.map3;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> counter = new HashMap<>();
Random random = new Random();
for (int i = 0; i < 100; i++) {
int num = random.nextInt(10) + 1;
if (!counter.containsKey(num)) {
counter.put(num, 0);
}
int oldTimes = counter.get(num);
counter.put(num, oldTimes + 1);
}
System.out.println(counter);
// {1=12, 2=11, 3=11, 4=8, 5=6, 6=8, 7=8, 8=13, 9=5, 10=18}
}
}
当然也可以使用putIfAbsent
方法,这样可以省略一个if
语句,但依然不如getOrDefault
简洁。
Map
可以和其它容器结合使用,以构建更复杂的数据结构:
package ch9.map;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ch8.collection.Collection;
import util.Fmt;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Person) {
Person p = (Person) obj;
if (this.name == p.name && this.age == p.age) {
return true;
}
}
return super.equals(obj);
}
@Override
public String toString() {
return Fmt.sprintf("Person(name:%s,age:%d)", name, age);
}
}
class Pet {
private String type;
public Pet(String type) {
this.type = type;
}
@Override
public String toString() {
return Fmt.sprintf("Pet(%s)", type);
}
}
public class Main {
public static void main(String[] args) {
Map<Person, List<Pet>> persons = new HashMap<>();
List<Pet> pets1 = new ArrayList<Pet>();
Collections.addAll(pets1, new Pet("bird"), new Pet("dog"));
persons.put(new Person("Li Lei", 19), pets1);
List<Pet> pets2 = new ArrayList<Pet>();
Collections.addAll(pets2, new Pet("cat"));
persons.put(new Person("Han Meimei", 15), pets2);
for (Person p : persons.keySet()) {
System.out.println(p);
for (Pet pet : persons.get(p)) {
System.out.print(pet + " ");
}
System.out.println();
}
// Person(name:Han Meimei,age:15)
// Pet(cat)
// Person(name:Li Lei,age:19)
// Pet(bird) Pet(dog)
}
}
在这个示例中,使用了先获取键组成的Set
,再遍历这个Set
以获取值。这种方式不仅“笨拙”,而且因为需要再次利用Map.get()
查询索引,性能也较差。
所以,如果需要在遍历Map
的时候同时访问键和值,通常需要用以下方式:
...
public class Main {
public static void main(String[] args) {
...
for (Map.Entry<Person, List<Pet>> entry : persons.entrySet()) {
Person person = entry.getKey();
List<Pet> pets = entry.getValue();
System.out.println(person);
for (Pet pet : pets) {
System.out.print(pet + " ");
}
System.out.println();
}
// Person(name:Han Meimei,age:15)
// Pet(cat)
// Person(name:Li Lei,age:19)
// Pet(bird) Pet(dog)
}
}
这正是利用了之前我们所说的Map.Entry
这个嵌套接口。
除了常用的HashMap
之外,和List
类似,还有TreeMap
和LinkedHashMap
两种标准库的Map
实现,这里不再详细说明。
Queue
队列(queue)同样是种非常有用的数据结构,尤其是在多线程编程中,利用队列可以安全地在多个线程之间传递消息。对此,熟悉Go的开发者一定不会陌生,在Go中,通道(channel)就是类似的用途。
队列本身的规则很简单:FIFO(first in first out,即先进先出)。
Queue
在Java中是一个继承自Collection
的接口:
public interface Queue<E> extends Collection<E> {
...
}
先来看Queue
接口包含哪些主要方法:
-
boolean add(E e);
,如果允许的话,向队列中立即添加一个元素。成功返回true
,如果空间不够,抛出IllegalStateException
异常。 -
boolean offer(E e);
,如果队列的空间允许,向队列中添加一个元素。成功返回true
,失败返回false
。对有限空间的队列,该方法比add
更好用。 -
E remove();
,从队列头部移除一个元素并返回,如果队列为空,抛出一个NoSuchElementException
异常。 -
E poll();
,从队列头部移除一个元素并返回,如果队列为空,返回null
。 -
E element();
,将队列头部的元素返回(不移除)。如果队列为空,抛出NoSuchElementException
异常。 -
E peek();
,将队列头部的元素返回。如果队列为空,返回null
。
之前在中我们也提到了,LinkedList
类提供了栈和队列等需要的方法,所以利用LinkedList
可以实现队列。不过不同的是,因为Java提供了一个队列接口Queue
,并且LinkedList
实现了该接口,所以我们可以直接将一个LinkedList
实例向上转型为Queue
来使用,而不需要像Stack
那样单独创建类:
package ch9.queue;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Queue;
import util.Fmt;
public class Main {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
for (int i = 1; i <= 10; i++) {
queue.offer(i);
Fmt.printf("%d in queue\n", i);
Fmt.printf("now queue: %s\n", queue);
}
System.out.println("starting queue out operation.");
do {
try {
int num = queue.remove();
Fmt.printf("%d is queue out.\n", num);
Fmt.printf("now queue: %s\n", queue);
} catch (NoSuchElementException e) {
break;
}
} while (true);
System.out.println("queue is cleared.");
// 1 in queue
// now queue: [1]
// 2 in queue
// now queue: [1, 2]
// 3 in queue
// now queue: [1, 2, 3]
// 4 in queue
// now queue: [1, 2, 3, 4]
// 5 in queue
// now queue: [1, 2, 3, 4, 5]
// 6 in queue
// now queue: [1, 2, 3, 4, 5, 6]
// 7 in queue
// now queue: [1, 2, 3, 4, 5, 6, 7]
// 8 in queue
// now queue: [1, 2, 3, 4, 5, 6, 7, 8]
// 9 in queue
// now queue: [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 10 in queue
// now queue: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// starting queue out operation.
// 1 is queue out.
// now queue: [2, 3, 4, 5, 6, 7, 8, 9, 10]
// 2 is queue out.
// now queue: [3, 4, 5, 6, 7, 8, 9, 10]
// 3 is queue out.
// now queue: [4, 5, 6, 7, 8, 9, 10]
// 4 is queue out.
// now queue: [5, 6, 7, 8, 9, 10]
// 5 is queue out.
// now queue: [6, 7, 8, 9, 10]
// 6 is queue out.
// now queue: [7, 8, 9, 10]
// 7 is queue out.
// now queue: [8, 9, 10]
// 8 is queue out.
// now queue: [9, 10]
// 9 is queue out.
// now queue: [10]
// 10 is queue out.
// now queue: []
// queue is cleared.
}
}
这个示例可以说明Queue
的确是按照FIFO的方式运行。
PriorityQueue
优先级队列(PriorityQueue
)也是我们可能会用到的一种队列。普通的队列是完全按照FIFO的方式运行,这样会产生一个后果,在某些时候,我们可能需要一个后加入的元素能够“优先”出队列,就无法实现。而优先级队列就是为了解决这一问题。
优先级队列可以按照给定的优先级规则,将进入队列的元素进行排序,并按照排序后的结果执行“出队列”的操作。
下面是一个优先级队列的示例:
package ch9.queue2;
import java.util.Collections;
import java.util.NoSuchElementException;
import java.util.PriorityQueue;
import java.util.Queue;
enum MsgPriority {
HIGH, MIDDLE, LOW;
public static int compare(MsgPriority o1, MsgPriority o2) {
if (o1 == o2) {
return 0;
} else if (o1 == HIGH) {
return 1;
} else if (o2 == HIGH) {
return -1;
} else if (o1 == MIDDLE) {
return 1;
} else if (o2 == MIDDLE) {
return -1;
} else {
;
}
return 0;
}
}
class Message implements Comparable<Message> {
private String msg;
private MsgPriority priority;
public Message(String msg, MsgPriority priority) {
this.msg = msg;
this.priority = priority;
}
public String toString() {
return msg;
}
public int compareTo(Message o) {
return MsgPriority.compare(this.priority, o.priority);
}
}
public class Main {
public static void main(String[] args) {
Queue<Message> msgs = new PriorityQueue<>();
msgs.offer(new Message("hello", MsgPriority.LOW));
msgs.offer(new Message("world", MsgPriority.MIDDLE));
msgs.offer(new Message("How are you.", MsgPriority.HIGH));
msgs.offer(new Message("middle msg", MsgPriority.MIDDLE));
do {
try {
Message msg = msgs.remove();
System.out.println(msg);
} catch (NoSuchElementException e) {
break;
}
} while (true);
// hello
// middle msg
// world
// How are you.
}
}
可以看到,输出结果的确是经过排序了,midle msg
这条消息被插入到了第二条,但是结果并不是如我们期望的那样按照Msg
的priority
属性的优先级从大到小排列。这是因为Java标准库中所有的容器排序(如果有的话)都是按照从小到大的方式进行排序。而在创建MsgPriority
枚举的比较规则的时候我很自然地用了优先级大小比较的方式,所以结果与预期不符。
难道要达到预期要修改MsgPriority
的比较,算法?其实是不必要的,只要使用一个预定义比较器初始化优先级队列即可:
...
public class Main {
public static void main(String[] args) {
Queue<Message> msgs = new PriorityQueue<>(Collections.reverseOrder());
...
// How are you.
// middle msg
// world
// hello
}
}
只修改了一行代码,结果就符合预期了。
在枚举类型
MsgPriority
中,我并没有让其实现Comparable
接口,原因是enum
作为一种特殊的类,其能实现的接口相当有限,这其中并不包含Comparable
接口。所以选择直接让其实现一个compare
静态方法。
foreach和迭代器
在中我们已经看到了迭代器模式在容器遍历中的威力,因为Collection
扩展了Iterable
接口,所以拥有iterator
方法并可以返回一个迭代器。而Java的foreach
语法可以接受任何实现了Iterable
接口的对象,并进行遍历。
但是比较奇怪的是,foreach
并不能接受一个迭代器(实现了Iterator
接口的对象),这导致对于Iterator
和Iterable
我们要区别对待:
package ch9.foreach;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class Main {
private static void foreachIterator(Iterator iterator) {
while (iterator.hasNext()) {
System.out.print(iterator.next() + ", ");
}
System.out.println();
}
private static void foreachIterable(Iterable iterable) {
for (Object object : iterable) {
System.out.print(object + ", ");
}
System.out.println();
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5, 6);
foreachIterable(list);
foreachIterator(list.iterator());
// 1, 2, 3, 4, 5, 6,
// 1, 2, 3, 4, 5, 6,
}
}
之所以会有这样的问题,是因为Java的Iterator
接口没有扩展Iterable
接口。
或许这样说很抽象,很难理解。实际上Iterator
的定义中应当包含iterator
方法:
interface MyIterator<T> extends Iterable<T>, Iterator<T> {
default Iterator<T> iterator() {
return this;
}
}
内容也很简单,就是返回自己,毕竟自己就是一个Iterator
。
这样做的好处在于,一个Iterator
对象必然是一个Iterable
对象,所以自然可以用foreach
语法进行迭代。
这在道理上也说的通,毕竟
Iterator
拥有全部迭代所需的功能。
事实上Python的迭代机制就是这么设计的,关于Python的迭代器相关内容可以阅读。
还有需要说明的是,Java中的数组虽然同样是类,但是并没有实现Iterable
接口,自然也没有一个返回Iterator
对象的iterator
方法,之所以可以用foreach
语法进行遍历,是因为语言底层的支持,所以是不能将一个数组作为Iterable
类型进行参数传递的:
...
public class Main {
...
public static void main(String[] args) {
...
// The method foreachIterable(Iterable) in the type Main is not applicable for
// the arguments (Integer[])
Integer[] array = new Integer[] { 1, 2, 3 };
foreachIterable(array);
}
}
这里的代码无法通过编译。
asList
在上篇和本篇笔记中,大量使用Arrays.asList
方法产生List
来填充相关容器。需要注意的是,Arrays.asList
虽然返回的是一个List
对象,但其底层的存储依然是使用的原数组,所以对该List
的修改会影响到原始数组:
package ch9.aslist;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Main {
public static void main(String[] args) {
Integer[] array = new Integer[] { 1, 2, 3, 4, 5, 6 };
List<Integer> list = Arrays.asList(array);
Collections.shuffle(list);
System.out.println(list);
System.out.println(Arrays.toString(array));
// [2, 4, 3, 1, 6, 5]
// [2, 4, 3, 1, 6, 5]
array = new Integer[] { 1, 2, 3, 4, 5, 6 };
list = new ArrayList<>(Arrays.asList(array));
Collections.shuffle(list);
System.out.println(list);
System.out.println(Arrays.toString(array));
// [6, 2, 3, 5, 1, 4]
// [1, 2, 3, 4, 5, 6]
}
}
如果查看底层Arrays.asList
的实现就能发现,其实其调用的是内部类Arrays$ArrayList<>(T[] array)
构造函数,这个构造函数会利用传入的数组创建一个固定大小并共享底层数组的ArrayList
。
这里的
ArrayList
是一个Arrays
的内部类,而非java.util.ArrayList
。
而java.util.ArrayList
的构造方法ArrayList(Collection c)
则是通过拷贝的方式利用传入的Collection
对象创建一个全新的ArrayList
,所以自然不会对原始数据产生影响。
最后我用EA绘制了上篇和本篇笔记涉及到的相关接口和类的UML类图,已上传到Github仓库,可以通过连接进行下载查阅。
如果有安装EA的话,可以在同目录找到工程文件,可以直接用EA加载。
谢谢阅读。
文章评论