文章目录
五、集合
1、集合简介
在Java中,如果一个Java对象可以在内部持有若干其他Java对象,并对外提供访问接口,我们把这种Java对象称为集合。很显然,Java的数组可以看作是一种集合。
String[] ss = new String[10]; // 可以持有10个String对象
ss[0] = "Hello"; // 可以放入String对象
String first = ss[0]; // 可以获取String对象
为什么在又了数组这种数据类型后,还需要其他集合类?
因为数组有如下的限制:
- 数组初始化后的大小不能改变;
- 数组只能按索引顺序存取。
因此,我们需要各种不同的集合来满足不同的需求。例如:
- 可变大小的顺序链表;
- 保证无重复元素的集合;
- …
Collection
Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口。
Java的java.util包主要提供了以下三种类型的集合:
List:一种有序列的集合;Set:一种保证没有重复元素的集合;Map:一种通过键值查找的映射表集合。
Java集合的设计有几个特点:
- 实现了接口和实现类相分离,例如,有序表的接口是
List,具体的实现类有ArrayList,LinkedList等; - 支持泛型,可以限制在一个集合中只放入同一种数据类型的元素。
- 访问集合时总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。
由于Java的集合设计非常久远,中间经历过大规模改进,我们要注意到有一小部分集合类是遗留类,不应该继续使用:
Hashtable:一种线程安全的Map实现;Vector:一种线程安全的List实现;Stack:基于Vector实现的LIFO的栈。还有一小部分接口是遗留接口,也不应该继续使用:
Enumeration:已被Iterator取代。
2、List
List是最基础的一种集合:它是一种有序链表。
List 的行为和数组几乎完全相同:List 内部按照放入元素的先后顺序存放,每个元素都可以通过索引值确定自己的位置。
2.1 ArrayList
在实际应用中,需要增删元素的有序列表,因此我们使用最多的是ArrayList。
实际上,ArrayList 在内部使用了数组来存储所有元素。它的处理和数组的使用类似,例如,一个ArrayList拥有5个元素,实际数组大小为6(即有一个空位):
size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ │
└───┴───┴───┴───┴───┴───┘
当添加一个元素并指定索引到ArrayList时,ArrayList自动移动需要移动的元素:
size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘
然后,往内部指定索引的数组位置添加一个元素,然后把size加1:
size=6
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘
继续添加元素,但是数组已满,没有空闲位置的时候,ArrayList先创建一个更大的新数组,然后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组:
size=6
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
现在,新数组就有了空位,可以继续添加一个元素到数组末尾,同时size加1:
size=7
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │ G │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
2.2 LinkedList
LinkedList通过“链表”也实现了List接口。在LinkedList中,它的内部每个元素都指向下一个元素:
┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐
HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │ │
└───┴───┘ └───┴───┘ └───┴───┘ └───┴───┘
2.3 区别
| ArrayList | LinkedList | |
|---|---|---|
| 获取指定元素 | 速度很快 | 需要从头开始查找元素 |
| 添加元素到末尾 | 速度很快 | 速度很快 |
| 在指定位置添加/删除 | 需要移动元素 | 不需要移动元素 |
| 内存占用 | 少 | 较大 |
通常情况下,我们总是优先使用
ArrayList。
2.4 遍历List
@Test
public void m0() {
List<String> list = new ArrayList<>();
list.add("apple");
list.add("pear");
list.add("orange");
//方法一:通过索引遍历,不推荐
for (int i = 0; i < list.size(); ++i) {
String str = list.get(i);
System.out.println(str);
}
//方法二:通过Iterator遍历
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
//方法三:for earch方法遍历
for (String str : list) {
System.out.println(str);
}
}
上面通过三种方法遍历了List:
- 第一种方法并不推荐,一是因为代码复杂,二是因为
get(i)方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢。 - 第二种方法:使用迭代器
Iterator来访问List。Iterator本身也是一个对象,但它是由List的实例调用iterator()方法的时候创建的。Iterator对象知道如何遍历一个List,并且不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率。 - 第三种方法:Java编译器会自动把
for each循环变成Iterator的调用。
需要记住,通过
Iterator遍历List永远是最高效的方式。
2.5 List和Array转换
List -> Array
Array -> List
对于JDK 11之前的版本,可以使用Arrays.asList(T...)方法把数组转换成List。但返回的是一个只读 List 。
@Test
public void m1() {
Integer[] array = { 1, 2, 3 };
List<Integer> list = Arrays.asList(array);
for (int i : list) {
System.out.println(i);
}
list.add(5); //java.lang.UnsupportedOperationException
}
对于高版本的,可以通过List.of(T...)方法转换,返回的也是只读 List 。
Integer[] array = { 1, 2, 3 };
List<Integer> list = List.of(array);
3、编写equals方法
在 List 中提供了有两个方法:
boolean contains(Object o)方法判断List是否包含某个指定元素;int indexOf(Object o)方法返回某个元素的索引,如果元素不存在,返回-1。
下面是一个例子:
@Test
public void m3() {
List<String> list = new ArrayList<>();
list.add("apple");
list.add("pear");
list.add("orange");
System.out.println(list.contains(new String("apple"))); //true
System.out.println(list.contains(new String("banana"))); //false
System.out.println(list.indexOf(new String("apple"))); //0
System.out.println(list.indexOf(new String("banana"))); //-1
}
虽然传入的 new String("apple") 和List 中的 apple 是不同的实例。但是仍然得到了true。这是因为在 List 内部并不是通过 == 来判断两个元素是否相等,而是通过 equals() 方法判断两个元素是否相等。源码如下:
/*ArrayList.java*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
因此,要正确使用 List 的contains()、indexOf()这些方法,必须要覆写对象的equals()方法。
之所以放入
String、Integer这些对象能够得到预期结果,是因为Java标准库定义的这些类已经正确实现了equals()方法。
例如,在 Student 类没有编写 equals 方法时,不能得到正确结果。
@Test
public void m3() {
List<Student> students = new ArrayList<>();
Student stu1 = new Student("zhangsan");
students.add(stu1);
Student stu2 = new Student("lisi");
students.add(stu2);
Student stu3 = new Student("wangwu");
students.add(stu3);
System.out.println(students.contains(new Student("zhangsan"))); //false
}
编写
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。
根据规则,编写Student类的 equals() 方法:
@Override
public boolean equals(Object obj) {
if (obj instanceof Student) {
Student student = (Student) obj;
return Objects.equals(this.name, student.name) && this.id == student.id;
}
return false;
}
再来运行上一个测试代码,便能得到正确结果。
使用
Objects.equals()比较两个引用类型是否相等的目的是省去了判断null的麻烦。两个引用类型都是null时也返回true。
4、Map
Map这种键值(key-value)映射表的数据结构,作用是能高效地通过key快速查找value(元素)。比如,通过name 查询某个 Student 。
Map 和 List 一样,也是一个接口,常用的方法包括:
put(K key, V value)方法:把key和value做了映射并放入MapV get(K key):通过key获取到对应的value。如果key不存在,则返回null。boolean containsKey(K key):查询某个key是否存在。
对于 Map 来说,常用的实现类是 HashMap 。
4.1 遍历Map
对 Map 来说,要遍历 key 可以使用 for each 循环遍历 Map 实例的 keySet() 方法返回的Set集合,它包含不重复的key的集合。
如果要遍历key和value,可以使用for each循环遍历Map对象的entrySet()集合,它包含每一个key-value映射。
具体如下:
@Test
public void m3() {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 456);
map.put("banana", 789);
//方法一
for (String key : map.keySet()) {
System.out.println(key + " " + map.get(key));
}
//方法二
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
Map和List不同:
Map存储的是key-value的映射关系,- 它不保证顺序。
Map的遍历顺序没有逻辑可言,甚至不同的JDK版本,相同的代码输出顺序都不同。
5、编写equals和hashCode
HashMap 之所以能够根据 key 直接拿到 value ,原因是它在内部通过空间换时间的方法,用一个大数组存储所有的 value ,并根据 key 直接计算出来 value 应该存储的位置。
┌───┐
0 │ │
├───┤
1 │ ●─┼───> Student("Xiao Ming")
├───┤
2 │ │
├───┤
3 │ │
├───┤
4 │ │
├───┤
5 │ ●─┼───> Student("Xiao Hong")
├───┤
6 │ ●─┼───> Student("Xiao Jun")
├───┤
7 │ │
└───┘
上述行为就是本科学的 哈希表 ,根据 key 通过一个算法得到一个索引值,将 value 放到相应的位置。这个算法得到的索引值应该尽可能减少冲突,也就是说,对于两个 key:a 和b ,该算法得到的索引应该尽量不一样,如果发生冲突,也会有冲突解决办法。在 HashMap 中采用的办法是:如果发生冲突,在数组中,实际存储的就不是一个Student 实例,而是一个 List ,如下所示:
┌───┐
0 │ │
├───┤
1 │ │
├───┤
2 │ │
├───┤
3 │ │
├───┤
4 │ │
├───┤
5 │ ●─┼───> List<Entry<String, Student>>
├───┤
6 │ │
├───┤
7 │ │
└───┘
在查找时,先通过 a 得到索引5,再得到List<Entry<String, Student>>,接着它还需要遍历这个 List ,才能返回对应的Student 实例。所以如果冲突的概率越大,这个List 就越长,Map 的get() 方法查找效率就越低。
由上分析,得到结论:
- 作为
key的类必须正确覆写equals()和hashCode()方法; - 一个类如果覆写了
equals(),就必须覆写hashCode(),并且覆写规则是:- 如果
equals()返回true,则hashCode()返回值必须相等; - 如果
equals()返回false,则hashCode()返回值尽量不要相等。
- 如果
- 实现
hashCode()方法可以通过Objects.hashCode()辅助方法实现
6、EnumMap
如果Map传入的 key 对象是 enum 类型,那么可以使用 EnumMap ,它在内部以一个非常紧凑的数组存储value,并且根据 enum 类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。
我们以 DayOfWeek (import java.time.DayOfWeek;)枚举来做一个“翻译”功能:
@Test
public void m4() {
Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
map.put(DayOfWeek.MONDAY, "星期一");
map.put(DayOfWeek.TUESDAY, "星期二");
map.put(DayOfWeek.WEDNESDAY, "星期三");
map.put(DayOfWeek.THURSDAY, "星期四");
map.put(DayOfWeek.FRIDAY, "星期五");
map.put(DayOfWeek.SATURDAY, "星期六");
map.put(DayOfWeek.SUNDAY, "星期日");
System.out.println(map);
System.out.println(map.get(DayOfWeek.MONDAY));
}
7、TreeMap
Map 接口的一个实现HashMap 是一种以空间换时间的映射表,它的实现原理决定了内部的 key 是无序的,导致了在遍历时的输出顺序是不可预测的。
如果要实现对 key 进行排序,就需要用到 SortedMap 接口,它的实现类是 TreeMap 。
┌───┐
│Map│
└───┘
▲
┌────┴─────┐
│ │
┌───────┐ ┌─────────┐
│HashMap│ │SortedMap│
└───────┘ └─────────┘
▲
│
┌─────────┐
│ TreeMap │
└─────────┘
要使用 TreeMap 实现key的排序,放入Map 中的key 必须实现 Comparable 接口。当然对于 String 、Integer 这些类来说,已经实现了 Comparable 接口,因此可以直接使用。但对于自定义类,比如Student 类,则需要自己实现。
如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时指定一个自定义排序算法。
实现的例子如下:
//方法一
//修改 Student.java
public class Student implements Comparable<Student> {
//...
@Override
public int compareTo(Student o) {
if (Objects.equals(this.name, o.getName())) {
return 0;
}
return this.getName().compareTo(o.getName());
}
}
//测试方法
@Test
public void m5() {
Map<Student, Integer> map = new TreeMap<>();
map.put(new Student("zhangsan"), 111);
map.put(new Student("lisi"), 222);
map.put(new Student("asan"), 333);
map.put(new Student("wangwu"), 444);
for (Student stu : map.keySet()) {
System.out.println(stu);
}
}
//方法二
@Test
public void m5() {
Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
if(Objects.equals(o1.getName(), o2.getName())) {
return 0;
}
return o1.getName().compareTo(o2.getName());
}
});
map.put(new Student("zhangsan"), 111);
map.put(new Student("lisi"), 222);
map.put(new Student("asan"), 333);
map.put(new Student("wangwu"), 444);
for (Student stu : map.keySet()) {
System.out.println(stu);
}
}
8、Properties
实现对配置文件的读写需要用到Properties 。由于历史遗留原因,Properties内部本质上是一个Hashtable,但我们只需要用到Properties自身关于读写配置的接口。
9、Set
如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set。在应该中,我们常用Set 来去除重复元素。
Set用于存储不重复的元素集合,它主要提供以下几个方法:
- 将元素添加进
Set:boolean add(E e) - 将元素从
Set删除:boolean remove(Object e) - 判断是否包含元素:
boolean contains(Object e)
@Test
public void m5() {
Set<String> set = new HashSet<>();
System.out.println(set.add("abc")); // true
System.out.println(set.add("xyz")); // true
System.out.println(set.add("xyz")); // false,添加失败,因为元素已存在
System.out.println(set.contains("xyz")); // true,元素存在
System.out.println(set.contains("XYZ")); // false,元素不存在
System.out.println(set.remove("hello")); // false,删除失败,因为元素不存在
System.out.println(set.size()); // 2,一共两个元素
}
Set实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。因为放入
Set的元素和Map的key类似,都要正确实现equals()和hashCode()方法,否则该元素无法正确地放入Set。最常用的
Set实现类是HashSet,实际上,HashSet仅仅是对HashMap的一个简单封装,它的核心代码如下:public class HashSet<E> implements Set<E> { private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; } public boolean contains(Object o) { return map.containsKey(o); } public boolean remove(Object o) { return map.remove(o) == PRESENT; } }
9.1 TreeSet
和Map 的TreeMap 类似, Set 接口也有一个 TreeSet 。关系如下:
┌───┐
│Set│
└───┘
▲
┌────┴─────┐
│ │
┌───────┐ ┌─────────┐
│HashSet│ │SortedSet│
└───────┘ └─────────┘
▲
│
┌─────────┐
│ TreeSet │
└─────────┘
用法也是一样,添加的元素必须要正确的实现 Comparable 接口;如果没有实现,那么创建TreeSet时必须传入一个Comparator对象。
10、Queue
队列(Queue)是一种经常使用的集合。Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。它和 List 的区别就是:限制了添加和取出的方向。Queue 只能从末尾添加元素、从头部取出元素。
在Java的标准库中,队列接口Queue定义了以下几个方法:
int size():获取队列长度;boolean add(E)/boolean offer(E):添加元素到队尾;E remove()/E poll():获取队首元素并从队列中删除;E element()/E peek():获取队首元素但并不从队列中删除。
对于具体的实现类,有的Queue有最大队列长度限制,有的Queue没有。
注意到添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,这两个方法的行为是不同的。我们用一个表格总结如下:
| throw Exception | 返回false或null | |
|---|---|---|
| 添加元素到队尾 | add(E e) | boolean offer(E e) |
| 取队首元素并删除 | E remove() | E poll() |
| 取队首元素但不删除 | E element() | E peek() |
注意:不要把
null添加到队列中,否则poll()方法返回null时,很难确定是取到了null元素还是队列为空。
10.1 LinkedList
LinkedList类即实现了List接口,又实现了Queue接口,但是,在使用的时候,如果我们把它当作List,就获取List的引用,如果我们把它当作Queue,就获取Queue的引用:
// 这是一个List:
List<String> list = new LinkedList<>();
// 这是一个Queue:
Queue<String> queue = new LinkedList<>();
11、PriorityQueue
PriorityQueue 是一种允许插队的 Queue 。它的出队顺序与元素的优先级有关。对PriorityQueue调用remove()或poll()方法,返回的总是优先级最高的元素。
元素的优先级是通过排序得到的,因此和TreeMap 、TreeSet 一样,放入PriorityQueue的元素,必须实现Comparable接口,如果没有实现,那么创建PriorityQueue 时必须传入一个Comparator对象。
//方法一
@Test
public void m6() {
Queue<Student> queue = new PriorityQueue<>();
queue.add(new Student("zhangsan"));
queue.add(new Student("lisi"));
queue.add(new Student("asan"));
queue.add(new Student("wangwu"));
System.out.println(queue.remove()); //asan
System.out.println(queue.remove()); //lisi
System.out.println(queue.remove()); //wangwu
System.out.println(queue.remove()); //zhangsan
}
12、Deque
Deque 是对 Queue 的一种变体,它允许两端都进,两端都出,叫做双端队列(Double Ended Queue)
Queue和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 是Queue 的扩展,Queue 提供的add()/offer()方法也可以使用,但是使用Deque时,最好不要调用offer(),而是调用offerLast()。
Deque是一个接口,它的实现类有ArrayDeque和LinkedList。
LinkedList是一个全能选手,它即是List,又是Queue,还是Deque。但是我们在使用的时候,总是要用特定的接口来引用它。
13、Stack
栈(Stack)是一种后进先出(LIFO:Last In First Out)的数据结构。
Stack的操作:
- 把元素压栈:
push(E); - 把栈顶的元素“弹出”:
pop(E); - 取栈顶元素但不弹出:
peek(E)。
在Java中,没有单独的
Stack接口。
14、Collections
Collections是JDK提供的工具类,同样位于java.util包中。它提供了一系列静态方法,能更方便地操作各种集合。
注意Collections结尾多了一个s,不是Collection!
14.1 创建空集合
Collections提供了一系列方法来创建空集合:
- 创建空List:
List emptyList() - 创建空Map:
Map emptyMap() - 创建空Set:
Set emptySet()
但是返回的空集合时不可变集合,无法向其中添加或删除元素。
@Test
public void m8() {
List<String> list = Collections.emptyList();
list.add("111"); //java.lang.UnsupportedOperationException
}
14.2 创建单元素集合
Collections提供了一系列方法来创建一个单元素集合:
- 创建一个元素的List:
List singletonList(T o) - 创建一个元素的Map:
Map singletonMap(K key, V value) - 创建一个元素的Set:
Set singleton(T o)
同样,返回的单元素集合也是不可变集合,无法向其中添加或删除元素。
14.3 排序
Collections可以对List进行排序。因为排序会直接修改List元素的位置,因此必须传入可变List:
@Test
public void m8() {
List<String> list = new ArrayList<>();
list.add("apple");
list.add("pear");
list.add("orange");
// 排序前:
System.out.println(list);
Collections.sort(list);
// 排序后:
System.out.println(list);
}
//输出结果
[apple, pear, orange]
[apple, orange, pear]
14.4 洗牌
Collections提供了洗牌算法,即传入一个有序的List,可以随机打乱List内部元素的顺序,效果相当于让计算机洗牌:
@Test
public void m8() {
List<Integer> list = new ArrayList<>();
for (int i=0; i<10; i++) {
list.add(i);
}
// 洗牌前:
System.out.println(list);
Collections.shuffle(list);
// 洗牌后:
System.out.println(list);
}
//输出结果
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[5, 1, 4, 9, 2, 0, 7, 6, 8, 3]
14.5 不可变集合
Collections还提供了一组方法把可变集合封装成不可变集合:
- 封装成不可变List:
List unmodifiableList(List list) - 封装成不可变Set:
Set unmodifiableSet(Set set) - 封装成不可变Map:
Map unmodifiableMap(Map m)
这种封装实际上是通过创建一个代理对象,拦截掉所有修改的方法。
@Test
public void m9() {
List<String> mutable = new ArrayList<>();
mutable.add("apple");
mutable.add("pear");
// 变为不可变集合:
List<String> immutable = Collections.unmodifiableList(mutable);
immutable.add("orange"); // UnsupportedOperationException
}
但是继续对原始的可变List进行增删是可以的,并且,还会影响到封装后的“不可变”List:
@Test
public void m8() {
List<String> mutable = new ArrayList<>();
mutable.add("apple");
mutable.add("pear");
// 变为不可变集合:
List<String> immutable = Collections.unmodifiableList(mutable);
mutable.add("orange");
System.out.println(immutable); //[apple, pear, orange]
}
所以在返回不可变 List 后,最好立刻将可变List 等于 null ,如下:
@Test
public void m8() {
List<String> mutable = new ArrayList<>();
mutable.add("apple");
mutable.add("pear");
// 变为不可变集合:
List<String> immutable = Collections.unmodifiableList(mutable);
// 立刻扔掉mutable的引用:
mutable = null;
System.out.println(immutable);
}
来源:CSDN
作者:不知道改什么昵称
链接:https://blog.csdn.net/weixin_40971059/article/details/104759537