Java 集合 Collection
Java的集合类定义在java.util
包中,支持泛型,主要提供了3种集合类,包括List
,Set
和Map
。Java集合使用统一的Iterator
遍历,尽量不要使用遗留接口。
List 有序集合
- 在末尾添加一个元素:
void add(E e)
- 在指定索引添加一个元素:
void add(int index, E e)
- 删除指定索引的元素:
int remove(int index)
- 删除某个元素:
int remove(Object e)
- 获取指定索引的元素:
E get(int index)
- 获取链表大小(包含元素的个数):
int size()
普通数组 与 ArrayList
数组的增删方式:
从一个已有的数组{'A', 'B', 'C', 'D', 'E'}中删除索引为2的元素:
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │
└───┴───┴───┴───┴───┴───┘
│ │
┌───┘ │
│ ┌───┘
│ │
▼ ▼
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ D │ E │ │ │
└───┴───┴───┴───┴───┴───┘
这个“删除”操作实际上是把'C'后面的元素依次往前挪一个位置,而“添加”操作实际上是把指定位置以后的元素都依次向后挪一个位置,腾出来的位置给新加的元素。
ArrayList的增删方式:
一个ArrayList拥有5个元素,实际数组大小为6(即有一个空位):
size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │
└───┴───┴───┴───┴───┴───┘
当添加一个元素并指定索引到ArrayList时,ArrayList自动移动需要移动的元素:
size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘
增加一个元素后 size +1
如果数组已满 ArrayList先创建一个更大的新数组,然后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组:
LinkedList "链表"实现的List接口
,它的内部每个元素都指向下一个元素:
┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │ │
└───┴───┘ └───┴───┘ └───┴───┘ └───┴───┘
ArrayList 与 LinkedList 比较
ArrayList | LinkedList | |
---|---|---|
获取指定元素 | 速度很快 | 需要从头开始查找元素 |
添加元素到末尾 | 速度很快 | 速度很快 |
在指定位置添加/删除 | 需要移动元素 | 不需要移动元素 |
内存占用 | 少 | 较大 |
遍历多 使用ArrayList,修改操作多 使用 LinkedList
创建List
除了使用ArrayList
和LinkedList
,我们还可以通过List
接口提供的of()
方法,根据给定元素快速创建List
:
List<Integer> list = List.of(1, 2, 5);
但是List.of()
方法不接受null
值,如果传入null
,会抛出NullPointerException
异常。
遍历List
我们要始终坚持使用迭代器Iterator来访问List。Iterator本身也是一个对象,但它是由List的实例调用iterator()方法的时候创建的。Iterator对象知道如何遍历一个List,并且不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率。
List<String> list = List.of("apple", "pear", "banana");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
Java的for each
循环本身就可以帮我们使用Iterator
遍历
List<String> list = List.of("apple", "pear", "banana");
for (String s : list ) {
System.out.println(s);
}
List和Array(数组)转换
List to Array
Integer[] array = list.toArray(new Integer[list.size()]);
Integer[] array = list.toArray(Integer[]::new); # h函数式 写法拓展
Array to List
Interger [] array = {1, 2, 3};
List<Interger> list = List.of(array)
返回的List
不一定就是ArrayList
或者LinkedList
,因为List
只是一个接口,如果我们调用List.of()
,它返回的是一个只读List
(即调用add() remove()会抛出异常)
编写equal()方法
要正确使用List
的contains()
、indexOf()
这些方法,放入的实例必须正确覆写equals()
方法,否则,放进去的实例,查找不到。****
如何正确编写equals()
方法?equals()
方法要求我们必须满足以下条件:
- 自反性(Reflexive):对于非
null
的x
来说,x.equals(x)
必须返回true
; - 对称性(Symmetric):对于非
null
的x
和y
来说,如果x.equals(y)
为true
,则y.equals(x)
也必须为true
; - 传递性(Transitive):对于非
null
的x
、y
和z
来说,如果x.equals(y)
为true
,y.equals(z)
也为true
,那么x.equals(z)
也必须为true
; - 一致性(Consistent):对于非
null
的x
和y
来说,只要x
和y
状态不变,则x.equals(y)
总是一致地返回true
或者false
; - 对
null
的比较:即x.equals(null)
永远返回false
。
以Person 类例
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
return Objects.equals(this.name, p.name) && this.age == p.age;
}
return false;
}
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用
instanceof
判断传入的待比较的Object
是不是当前类型,如果是,继续比较,否则,返回false
; - 对引用类型用
Objects.equals()
比较,对基本类型直接用==
比较。
Map 键值对
Map
是一种键-值映射表
Map
也是一个接口,最常用的实现类是HashMap
。
public static void main(String[] args) {
Person p = new Person("Xiao", "Ming", 25)
Map<String, Person> map = new HashMap<>();
map.put("Xiao Ming", p);
Person map_v = map.get("Xiao Ming");
}
Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。
遍历Map
通过 key 遍历 keySet()
与 通过 条目集 遍历 enrtySet ()
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Xiao Ming", 11);
map.put("Xiao Wang", 12);
map.put("Xiao Ying", 13);
// 通过 Key遍历 map.KeySey()
for (String key : map.keySet()) {
Integer v = map.get(key);
System.out.println(v);
}
// 通过 条目集遍历 map.entrySet()
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String k = entry.getKey();
Integer v = entry.getValue();
System.out.println(k + "=" + v);
}
}
}
Map
和List
不同的是,Map
存储的是key-value
的映射关系,并且,它不保证顺序。
HashMap
之所以能根据key
直接拿到value
,原因是它内部通过空间换时间的方法,用一个大数组存储所有value
,并根据key直接计算出value
应该存储在哪个索引:
┌───┐
0 │ │
├───┤
1 │ ●─┼───> Person("Xiao Ming")
├───┤
2 │ │
├───┤
3 │ │
├───┤
4 │ │
├───┤
5 │ ●─┼───> Person("Xiao Hong")
├───┤
6 │ ●─┼───> Person("Xiao Jun")
├───┤
7 │ │
└───┘
编写equeals 和 hashCode
我们放入Map
的key
是字符串"a"
,但是,当我们获取Map
的value
时,传入的变量不一定就是放入的那个key
对象。
两个key
应该是内容相同,但不一定是同一个对象
相同的key
对象(使用equals()
判断时返回true
)必须要计算出相同的索引,否则,相同的key
每次取出的value
就不一定对。 所以作为key
的对象必须正确覆写equals()
方法。
因此,正确使用Map
必须保证:
- 作为
key
的对象必须正确覆写equals()
方法,相等的两个key
实例调用equals()
必须返回true
; - 作为
key
的对象还必须正确覆写hashCode()
方法,且hashCode()
方法要严格遵循以下规范:
- 如果两个对象相等,则两个对象的
hashCode()
必须相等; - 如果两个对象不相等,则两个对象的
hashCode()
尽量不要相等。
hashCode编写
int hashCode() {
return Objects.hash(firstName, lastName, age);
}
HashMap 内部数组 默认长度为 16 即 0~15
通过
int index = key.hashCode() & 0xf; // 0xf = 15
计算内部数组索引
添加超过一定数量的key-value
时,HashMap
会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度32,相应地,需要重新确定hashCode()
计算的索引位置。例如,对长度为32的数组计算hashCode()
对应的索引,计算方式要改为:
int index = key.hashCode() & 0x1f; // 0x1f = 31
频繁扩容对HashMap
的性能影响很大。如果我们确定要使用一个容量为10000
个key-value
的HashMap
,更好的方式是创建HashMap
时就指定容量:
Map<String, Integer> map = new HashMap<>(10000);
哈希冲突
如果不同的两个key
,例如"a"
和"b"
,它们的hashCode()
恰好是相同的由于计算出的数组索引相同,后面放入的"Xiao Hong"
会不会把"Xiao Ming"
覆盖了?
我们就假设"a"
和"b"
这两个key
最终计算出的索引都是5,那么,在HashMap
的数组中,它包含两个Entry
,一个是"a"
的映射,一个是"b"
的映射
┌───┐
0 │ │
├───┤
1 │ │
├───┤
2 │ │
├───┤
3 │ │
├───┤
4 │ │
├───┤
5 │ ●─┼───> List<Entry<String, Person>>
├───┤
6 │ │
├───┤
7 │ │
└───┘
HashMap内部通过"a"
找到的实际上是List>
,它还需要遍历这个List
,并找到一个Entry
,它的key
字段是"a"
,才能返回对应的Person
实例。
我们把不同的key
具有相同的hashCode()
的情况称之为哈希冲突 如果冲突的概率越大,这个List
就越长,Map
的get()
方法效率就越低
EnumMap 枚举Map
因为HashMap
是一种通过对key计算hashCode()
,通过空间换时间的方式,直接定位到value所在的内部数组的索引,因此,查找效率非常高。
如果作为key的对象是enum
类型,那么,还可以使用Java集合库提供的一种EnumMap
,它在内部以一个非常紧凑的数组存储value,并且根据enum
类型的key直接定位到内部数组的索引,并不需要计算hashCode()
,不但效率最高,而且没有额外的空间浪费。
如果Map
的key是enum
类型,推荐使用EnumMap
,既保证速度,也不浪费空间。
使用EnumMap
的时候,根据面向抽象编程的原则,应持有Map
接口。
TreeMap 有序Map
还有一种Map
,它在内部会对Key进行排序,这种Map
就是SortedMap
。注意到SortedMap
是接口,它的实现类是TreeMap
。
┌───┐
│Map│
└───┘
▲
┌────┴─────┐
│ │
┌───────┐ ┌─────────┐
│HashMap│ │SortedMap│
└───────┘ └─────────┘
▲
│
┌─────────┐
│ TreeMap │
└─────────┘
SortedMap
保证遍历时以Key的顺序来进行排序。例如,放入的Key
使用TreeMap
时,放入的Key必须实现Comparable
接口。
Comparator
接口要求实现一个比较方法,它负责比较传入的两个元素a
和b
,
如果a<b则返回负数,通常是
-1,如果
a==b,则返回
0,如果
a>b,则返回正数,通常是
1。
TreeMap`内部根据比较结果对Key进行排序。
Properties 读取配置文件
Java集合库提供的Properties
用于读写配置文件.properties
。.properties
文件可以使用UTF-8编码。
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
可以从文件系统、classpath或其他任何地方读取.properties
文件。
读写Properties
时,注意仅使用getProperty()
和setProperty()
方法,不要调用继承而来的get()
和put()
等方法。
Set 无重复元素集合
Set
用于存储不重复的元素集合,它主要提供以下几个方法:
- 将元素添加进
Set
:boolean add(E e)
- 将元素从
Set
删除:boolean remove(Object e)
- 判断是否包含元素:
boolean contains(Object e)
Set
接口并不保证有序,而SortedSet
接口则保证元素是有序的:
HashSet
是无序的,因为它实现了Set
接口,并没有实现SortedSet
接口; 需求实现HashCode
equals
接口TreeSet
是有序的,因为它实现了SortedSet
接口。 需要实现Comparable 接口
┌───┐
│Set│
└───┘
▲
┌────┴─────┐
│ │
┌───────┐ ┌─────────┐
│HashSet│ │SortedSet│
└───────┘ └─────────┘
▲
│
┌─────────┐
│ TreeSet │
└─────────┘
Queue 队列
队列(Queue
)是一种经常使用的集合。Queue
实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。它和List
的区别在于,List
可以在任意位置添加和删除元素,而Queue
只有两个操作:
-
把元素添加到队列末尾;
-
从队列头部取出元素。
即 单向操作
超市的收银台就是一个队列:
队列Queue
实现了一个先进先出(FIFO)的数据结构:
- 通过
add()
/offer()
方法将元素添加到队尾; - 通过
remove()
/poll()
从队首获取元素并删除; - 通过
element()
/peek()
从队首获取元素但不删除。
要避免把null
添加到队列。
PriorityQueue 优先级队列
PriorityQueue
和Queue
的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue
调用remove()
或poll()
方法,返回的总是优先级最高的元素。
PriorityQueue
实现了一个优先队列:从队首获取元素时,总是获取优先级最高的元素。
PriorityQueue
默认按元素比较的顺序排序(必须实现Comparable
接口),也可以通过Comparator
自定义排序算法(元素就不必实现Comparable
接口)
Deque 双向队列
Java集合提供了接口Deque
来实现一个双端队列,它的功能是:
- 既可以添加到队尾,也可以添加到队首;
- 既可以从队首获取,又可以从队尾获取。
Queue | Deque | |
---|---|---|
添加元素到队尾 | add(E e) / offer(E e) | addLast(E e) / offerLast(E e) |
取队首元素并删除 | E remove() / E poll() | E removeFirst() / E pollFirst() |
取队首元素但不删除 | E element() / E peek() | E getFirst() / E peekFirst() |
添加元素到队首 | 无 | addFirst(E e) / offerFirst(E e) |
取队尾元素并删除 | 无 | E removeLast() / E pollLast() |
取队尾元素但不删除 | 无 | E getLast() / E peekLast() |
Deque
是一个接口,它的实现类有ArrayDeque
和LinkedList
。
// 不推荐的写法:
LinkedList<String> d1 = new LinkedList<>();
d1.offerLast("z");
// 推荐的写法:
Deque<String> d2 = new LinkedList<>();
d2.offerLast("z");
可见面向抽象编程的一个原则就是:尽量持有接口,而不是具体的实现类。
Stack 栈
栈(Stack)是一种后进先出(LIFO:Last In First Out)的数据结构。
───────────────────────────────┐
(\(\ (\(\ (\(\ (\(\ (\(\ │
(='.') <─> (='.') (='.') (='.') (='.')│
O(_")") O(_")") O(_")") O(_")") O(_")")│
───────────────────────────────┘
Stack
只有入栈和出栈的操作:
- 把元素压栈:
push(E)
; - 把栈顶的元素“弹出”:
pop(E)
; - 取栈顶元素但不弹出:
peek(E)
。
在Java中,我们用Deque
可以实现Stack
的功能:
-
把元素压栈:
push(E)
/addFirst(E)
; -
把栈顶的元素“弹出”:
pop(E)
/removeFirst()
; -
取栈顶元素但不弹出:
peek(E)
/peekFirst()
。避免调用
Deque
的其他方法。
Iterator 迭代器
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
Iterator
是一种抽象的数据访问模型。使用Iterator
模式进行迭代的好处有:
- 对任何集合都采用同一种访问模型;
- 调用者对集合内部结构一无所知;
- 集合类返回的
Iterator
对象知道如何迭代。
Java提供了标准的迭代器模型,即集合类实现java.util.Iterable
接口,返回java.util.Iterator
实例。
package test_throws;
import java.util.*;
public class Main {
public static void main(String[] args) {
ReverseList<String> rlist = new ReverseList<>();
rlist.add("Apple");
rlist.add("Orange");
rlist.add("Pear");
for (String s : rlist) {
System.out.println(s);
}
}
}
class ReverseList<T> implements Iterable<T> {
private List<T> list = new ArrayList<>();
public void add(T t) {
this.list.add(t);
}
// Iterable 接口 重写iterator 返回一个迭代器
@Override
public Iterator<T> iterator() {
return new ReverseIterator(this.list.size());
}
// 内部 Iterator 迭代器
class ReverseIterator implements Iterator<T> {
public int index;
ReverseIterator(int index) {
this.index = index;
}
@Override
public boolean hasNext() {
return index > 0;
}
@Override
public T next() {
this.index--;
return ReverseList.this.list.get(index);
}
}
}
Collections 集合类
Collections
是JDK提供的工具类,同样位于java.util
包中。它提供了一系列静态方法,能更方便地操作各种集合。
addAll()
方法可以给一个Collection
类型的集合添加若干元素。因为方法签名是Collection
,所以我们可以传入List
,Set
等各种集合类型。
创建空集合
Collections
提供了一系列方法来创建空集合:
- 创建空List:
List emptyList()
- 创建空Map:
Map emptyMap()
- 创建空Set:
Set emptySet()
创建单元素集合
Collections
提供了一系列方法来创建一个单元素集合:
- 创建一个元素的List:
List singletonList(T o)
- 创建一个元素的Map:
Map singletonMap(K key, V value)
- 创建一个元素的Set:
Set singleton(T o)
要注意到返回的单元素集合也是不可变集合,无法向其中添加或删除元素。
排序、洗牌
Collections.sort() 排序
Collections.shuffle() 洗牌
不可变集合
Collections
还提供了一组方法把可变集合封装成不可变集合:
- 封装成不可变List:
List<T> unmodifiableList(List<? extends T> list)
- 封装成不可变Set:
Set unmodifiableSet(Set<? extends T> set)
- 封装成不可变Map:
Map unmodifiableMap(Map<? extends K, ? extends V> m)
Java 集合 Collection
Java的集合类定义在java.util
包中,支持泛型,主要提供了3种集合类,包括List
,Set
和Map
。Java集合使用统一的Iterator
遍历,尽量不要使用遗留接口。
List 有序集合
- 在末尾添加一个元素:
void add(E e)
- 在指定索引添加一个元素:
void add(int index, E e)
- 删除指定索引的元素:
int remove(int index)
- 删除某个元素:
int remove(Object e)
- 获取指定索引的元素:
E get(int index)
- 获取链表大小(包含元素的个数):
int size()
普通数组 与 ArrayList
数组的增删方式:
从一个已有的数组{'A', 'B', 'C', 'D', 'E'}中删除索引为2的元素:
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │
└───┴───┴───┴───┴───┴───┘
│ │
┌───┘ │
│ ┌───┘
│ │
▼ ▼
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ D │ E │ │ │
└───┴───┴───┴───┴───┴───┘
这个“删除”操作实际上是把'C'后面的元素依次往前挪一个位置,而“添加”操作实际上是把指定位置以后的元素都依次向后挪一个位置,腾出来的位置给新加的元素。
ArrayList的增删方式:
一个ArrayList拥有5个元素,实际数组大小为6(即有一个空位):
size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │
└───┴───┴───┴───┴───┴───┘
当添加一个元素并指定索引到ArrayList时,ArrayList自动移动需要移动的元素:
size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘
增加一个元素后 size +1
如果数组已满 ArrayList先创建一个更大的新数组,然后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组:
LinkedList "链表"实现的List接口
,它的内部每个元素都指向下一个元素:
┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │ │
└───┴───┘ └───┴───┘ └───┴───┘ └───┴───┘
ArrayList 与 LinkedList 比较
ArrayList | LinkedList | |
---|---|---|
获取指定元素 | 速度很快 | 需要从头开始查找元素 |
添加元素到末尾 | 速度很快 | 速度很快 |
在指定位置添加/删除 | 需要移动元素 | 不需要移动元素 |
内存占用 | 少 | 较大 |
遍历多 使用ArrayList,修改操作多 使用 LinkedList
通过使用ArrayList
创建List
除了使用ArrayList
和LinkedList
,我们还可以通过List
接口提供的of()
方法,根据给定元素快速创建List
:
List<Integer> list = List.of(1, 2, 5);
但是List.of()
方法不接受null
值,如果传入null
,会抛出NullPointerException
异常。
遍历List
我们要始终坚持使用迭代器Iterator来访问List。Iterator本身也是一个对象,但它是由List的实例调用iterator()方法的时候创建的。Iterator对象知道如何遍历一个List,并且不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率。
List<String> list = List.of("apple", "pear", "banana");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
Java的for each
循环本身就可以帮我们使用Iterator
遍历
List<String> list = List.of("apple", "pear", "banana");
for (String s : list ) {
System.out.println(s);
}
List和Array(数组)转换
List to Array
Integer[] array = list.toArray(new Integer[list.size()]);
Integer[] array = list.toArray(Integer[]::new); # h函数式 写法拓展
Array to List
Interger [] array = {1, 2, 3};
List<Interger> list = List.of(array)
返回的List
不一定就是ArrayList
或者LinkedList
,因为List
只是一个接口,如果我们调用List.of()
,它返回的是一个只读List
(即调用add() remove()会抛出异常)
编写equal()方法
要正确使用List
的contains()
、indexOf()
这些方法,放入的实例必须正确覆写equals()
方法,否则,放进去的实例,查找不到。****
如何正确编写equals()
方法?equals()
方法要求我们必须满足以下条件:
- 自反性(Reflexive):对于非
null
的x
来说,x.equals(x)
必须返回true
; - 对称性(Symmetric):对于非
null
的x
和y
来说,如果x.equals(y)
为true
,则y.equals(x)
也必须为true
; - 传递性(Transitive):对于非
null
的x
、y
和z
来说,如果x.equals(y)
为true
,y.equals(z)
也为true
,那么x.equals(z)
也必须为true
; - 一致性(Consistent):对于非
null
的x
和y
来说,只要x
和y
状态不变,则x.equals(y)
总是一致地返回true
或者false
; - 对
null
的比较:即x.equals(null)
永远返回false
。
以Person 类例
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
return Objects.equals(this.name, p.name) && this.age == p.age;
}
return false;
}
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用
instanceof
判断传入的待比较的Object
是不是当前类型,如果是,继续比较,否则,返回false
; - 对引用类型用
Objects.equals()
比较,对基本类型直接用==
比较。
Map 键值对
Map
是一种键-值映射表
Map
也是一个接口,最常用的实现类是HashMap
。
public static void main(String[] args) {
Person p = new Person("Xiao", "Ming", 25)
Map<String, Person> map = new HashMap<>();
map.put("Xiao Ming", p);
Person map_v = map.get("Xiao Ming");
}
Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。
遍历Map
通过 key 遍历 keySet()
与 通过 条目集 遍历 enrtySet ()
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Xiao Ming", 11);
map.put("Xiao Wang", 12);
map.put("Xiao Ying", 13);
// 通过 Key遍历 map.KeySey()
for (String key : map.keySet()) {
Integer v = map.get(key);
System.out.println(v);
}
// 通过 条目集遍历 map.entrySet()
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String k = entry.getKey();
Integer v = entry.getValue();
System.out.println(k + "=" + v);
}
}
}
Map
和List
不同的是,Map
存储的是key-value
的映射关系,并且,它不保证顺序。
HashMap
之所以能根据key
直接拿到value
,原因是它内部通过空间换时间的方法,用一个大数组存储所有value
,并根据key直接计算出value
应该存储在哪个索引:
┌───┐
0 │ │
├───┤
1 │ ●─┼───> Person("Xiao Ming")
├───┤
2 │ │
├───┤
3 │ │
├───┤
4 │ │
├───┤
5 │ ●─┼───> Person("Xiao Hong")
├───┤
6 │ ●─┼───> Person("Xiao Jun")
├───┤
7 │ │
└───┘
编写equeals 和 hashCode
我们放入Map
的key
是字符串"a"
,但是,当我们获取Map
的value
时,传入的变量不一定就是放入的那个key
对象。
两个key
应该是内容相同,但不一定是同一个对象
相同的key
对象(使用equals()
判断时返回true
)必须要计算出相同的索引,否则,相同的key
每次取出的value
就不一定对。 所以作为key
的对象必须正确覆写equals()
方法。
因此,正确使用Map
必须保证:
- 作为
key
的对象必须正确覆写equals()
方法,相等的两个key
实例调用equals()
必须返回true
; - 作为
key
的对象还必须正确覆写hashCode()
方法,且hashCode()
方法要严格遵循以下规范:
- 如果两个对象相等,则两个对象的
hashCode()
必须相等; - 如果两个对象不相等,则两个对象的
hashCode()
尽量不要相等。
hashCode编写
int hashCode() {
return Objects.hash(firstName, lastName, age);
}
HashMap 内部数组 默认长度为 16 即 0~15
通过
int index = key.hashCode() & 0xf; // 0xf = 15
计算内部数组索引
添加超过一定数量的key-value
时,HashMap
会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度32,相应地,需要重新确定hashCode()
计算的索引位置。例如,对长度为32的数组计算hashCode()
对应的索引,计算方式要改为:
int index = key.hashCode() & 0x1f; // 0x1f = 31
频繁扩容对HashMap
的性能影响很大。如果我们确定要使用一个容量为10000
个key-value
的HashMap
,更好的方式是创建HashMap
时就指定容量:
Map<String, Integer> map = new HashMap<>(10000);
哈希冲突
如果不同的两个key
,例如"a"
和"b"
,它们的hashCode()
恰好是相同的由于计算出的数组索引相同,后面放入的"Xiao Hong"
会不会把"Xiao Ming"
覆盖了?
我们就假设"a"
和"b"
这两个key
最终计算出的索引都是5,那么,在HashMap
的数组中,它包含两个Entry
,一个是"a"
的映射,一个是"b"
的映射
┌───┐
0 │ │
├───┤
1 │ │
├───┤
2 │ │
├───┤
3 │ │
├───┤
4 │ │
├───┤
5 │ ●─┼───> List<Entry<String, Person>>
├───┤
6 │ │
├───┤
7 │ │
└───┘
HashMap内部通过"a"
找到的实际上是List>
,它还需要遍历这个List
,并找到一个Entry
,它的key
字段是"a"
,才能返回对应的Person
实例。
我们把不同的key
具有相同的hashCode()
的情况称之为哈希冲突 如果冲突的概率越大,这个List
就越长,Map
的get()
方法效率就越低
EnumMap 枚举Map
因为HashMap
是一种通过对key计算hashCode()
,通过空间换时间的方式,直接定位到value所在的内部数组的索引,因此,查找效率非常高。
如果作为key的对象是enum
类型,那么,还可以使用Java集合库提供的一种EnumMap
,它在内部以一个非常紧凑的数组存储value,并且根据enum
类型的key直接定位到内部数组的索引,并不需要计算hashCode()
,不但效率最高,而且没有额外的空间浪费。
如果Map
的key是enum
类型,推荐使用EnumMap
,既保证速度,也不浪费空间。
使用EnumMap
的时候,根据面向抽象编程的原则,应持有Map
接口。
TreeMap 有序Map
还有一种Map
,它在内部会对Key进行排序,这种Map
就是SortedMap
。注意到SortedMap
是接口,它的实现类是TreeMap
。
┌───┐
│Map│
└───┘
▲
┌────┴─────┐
│ │
┌───────┐ ┌─────────┐
│HashMap│ │SortedMap│
└───────┘ └─────────┘
▲
│
┌─────────┐
│ TreeMap │
└─────────┘
SortedMap
保证遍历时以Key的顺序来进行排序。例如,放入的Key
使用TreeMap
时,放入的Key必须实现Comparable
接口。
Comparator
接口要求实现一个比较方法,它负责比较传入的两个元素a
和b
,
如果a<b则返回负数,通常是
-1,如果
a==b,则返回
0,如果
a>b,则返回正数,通常是
1。
TreeMap`内部根据比较结果对Key进行排序。
Properties 读取配置文件
Java集合库提供的Properties
用于读写配置文件.properties
。.properties
文件可以使用UTF-8编码。
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
可以从文件系统、classpath或其他任何地方读取.properties
文件。
读写Properties
时,注意仅使用getProperty()
和setProperty()
方法,不要调用继承而来的get()
和put()
等方法。
Set 无重复元素集合
Set
用于存储不重复的元素集合,它主要提供以下几个方法:
- 将元素添加进
Set
:boolean add(E e)
- 将元素从
Set
删除:boolean remove(Object e)
- 判断是否包含元素:
boolean contains(Object e)
Set
接口并不保证有序,而SortedSet
接口则保证元素是有序的:
HashSet
是无序的,因为它实现了Set
接口,并没有实现SortedSet
接口; 需求实现HashCode
equals
接口TreeSet
是有序的,因为它实现了SortedSet
接口。 需要实现Comparable 接口
┌───┐
│Set│
└───┘
▲
┌────┴─────┐
│ │
┌───────┐ ┌─────────┐
│HashSet│ │SortedSet│
└───────┘ └─────────┘
▲
│
┌─────────┐
│ TreeSet │
└─────────┘
Queue 队列
队列(Queue
)是一种经常使用的集合。Queue
实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。它和List
的区别在于,List
可以在任意位置添加和删除元素,而Queue
只有两个操作:
-
把元素添加到队列末尾;
-
从队列头部取出元素。
即 单向操作
超市的收银台就是一个队列:
队列Queue
实现了一个先进先出(FIFO)的数据结构:
- 通过
add()
/offer()
方法将元素添加到队尾; - 通过
remove()
/poll()
从队首获取元素并删除; - 通过
element()
/peek()
从队首获取元素但不删除。
要避免把null
添加到队列。
PriorityQueue 优先级队列
PriorityQueue
和Queue
的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue
调用remove()
或poll()
方法,返回的总是优先级最高的元素。
PriorityQueue
实现了一个优先队列:从队首获取元素时,总是获取优先级最高的元素。
PriorityQueue
默认按元素比较的顺序排序(必须实现Comparable
接口),也可以通过Comparator
自定义排序算法(元素就不必实现Comparable
接口)
Deque 双向队列
Java集合提供了接口Deque
来实现一个双端队列,它的功能是:
- 既可以添加到队尾,也可以添加到队首;
- 既可以从队首获取,又可以从队尾获取。
Queue | Deque | |
---|---|---|
添加元素到队尾 | add(E e) / offer(E e) | addLast(E e) / offerLast(E e) |
取队首元素并删除 | E remove() / E poll() | E removeFirst() / E pollFirst() |
取队首元素但不删除 | E element() / E peek() | E getFirst() / E peekFirst() |
添加元素到队首 | 无 | addFirst(E e) / offerFirst(E e) |
取队尾元素并删除 | 无 | E removeLast() / E pollLast() |
取队尾元素但不删除 | 无 | E getLast() / E peekLast() |
Deque
是一个接口,它的实现类有ArrayDeque
和LinkedList
。
// 不推荐的写法:
LinkedList<String> d1 = new LinkedList<>();
d1.offerLast("z");
// 推荐的写法:
Deque<String> d2 = new LinkedList<>();
d2.offerLast("z");
可见面向抽象编程的一个原则就是:尽量持有接口,而不是具体的实现类。
Stack 栈
栈(Stack)是一种后进先出(LIFO:Last In First Out)的数据结构。
───────────────────────────────┐
(\(\ (\(\ (\(\ (\(\ (\(\ │
(='.') <─> (='.') (='.') (='.') (='.')│
O(_")") O(_")") O(_")") O(_")") O(_")")│
───────────────────────────────┘
Stack
只有入栈和出栈的操作:
- 把元素压栈:
push(E)
; - 把栈顶的元素“弹出”:
pop(E)
; - 取栈顶元素但不弹出:
peek(E)
。
在Java中,我们用Deque
可以实现Stack
的功能:
-
把元素压栈:
push(E)
/addFirst(E)
; -
把栈顶的元素“弹出”:
pop(E)
/removeFirst()
; -
取栈顶元素但不弹出:
peek(E)
/peekFirst()
。避免调用
Deque
的其他方法。
Iterator 迭代器
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
Iterator
是一种抽象的数据访问模型。使用Iterator
模式进行迭代的好处有:
- 对任何集合都采用同一种访问模型;
- 调用者对集合内部结构一无所知;
- 集合类返回的
Iterator
对象知道如何迭代。
Java提供了标准的迭代器模型,即集合类实现java.util.Iterable
接口,返回java.util.Iterator
实例。
package test_throws;
import java.util.*;
public class Main {
public static void main(String[] args) {
ReverseList<String> rlist = new ReverseList<>();
rlist.add("Apple");
rlist.add("Orange");
rlist.add("Pear");
for (String s : rlist) {
System.out.println(s);
}
}
}
class ReverseList<T> implements Iterable<T> {
private List<T> list = new ArrayList<>();
public void add(T t) {
this.list.add(t);
}
// Iterable 接口 重写iterator 返回一个迭代器
@Override
public Iterator<T> iterator() {
return new ReverseIterator(this.list.size());
}
// 内部 Iterator 迭代器
class ReverseIterator implements Iterator<T> {
public int index;
ReverseIterator(int index) {
this.index = index;
}
@Override
public boolean hasNext() {
return index > 0;
}
@Override
public T next() {
this.index--;
return ReverseList.this.list.get(index);
}
}
}
Collections 集合类
Collections
是JDK提供的工具类,同样位于java.util
包中。它提供了一系列静态方法,能更方便地操作各种集合。
addAll()
方法可以给一个Collection
类型的集合添加若干元素。因为方法签名是Collection
,所以我们可以传入List
,Set
等各种集合类型。
创建空集合
Collections
提供了一系列方法来创建空集合:
- 创建空List:
List emptyList()
- 创建空Map:
Map emptyMap()
- 创建空Set:
Set emptySet()
创建单元素集合
Collections
提供了一系列方法来创建一个单元素集合:
- 创建一个元素的List:
List singletonList(T o)
- 创建一个元素的Map:
Map singletonMap(K key, V value)
- 创建一个元素的Set:
Set singleton(T o)
要注意到返回的单元素集合也是不可变集合,无法向其中添加或删除元素。
排序、洗牌
Collections.sort() 排序
Collections.shuffle() 洗牌
不可变集合
Collections
还提供了一组方法把可变集合封装成不可变集合:
- 封装成不可变List:
List<T> unmodifiableList(List<? extends T> list)
- 封装成不可变Set:
Set unmodifiableSet(Set<? extends T> set)
- 封装成不可变Map:
Map unmodifiableMap(Map<? extends K, ? extends V> m)