一.Collection
Java标准库自带的java.util
包提供了集合类:Collection
,它是除Map
外所有其他集合类的根接口。Java的java.util
包主要提供了以下三种类型的集合:
List
:一种有序列表的集合,例如,按索引排列的Student
的List
;Set
:一种保证没有重复元素的集合,例如,所有无重复名称的Student
的Set
;Map
:一种通过键值(key-value)查找的映射表集合,例如,根据Student
的name
查找对应Student
的Map
。
Java集合的设计有几个特点:一是实现了接口和实现类相分离,例如,有序表的接口是List
,具体的实现类有ArrayList
,LinkedList
等,二是支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素,例如:
List<String> list = new ArrayList<>(); // 只能放入String类型
最后,Java访问集合总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。
由于Java的集合设计非常久远,中间经历过大规模改进,我们要注意到有一小部分集合类是遗留类,不应该继续使用:
Hashtable
:一种线程安全的Map
实现;Vector
:一种线程安全的List
实现;Stack
:基于Vector
实现的LIFO
的栈。
还有一小部分接口是遗留接口,也不应该继续使用:
Enumeration<E>
:已被Iterator<E>
取代。
Java的集合类定义在java.util
包中,支持泛型,主要提供了3种集合类,包括List
,Set
和Map
。Java集合使用统一的Iterator
遍历,尽量不要使用遗留接口。
二.List
1.创建List
List<String> list = new ArrayList<>();
除了使用ArrayList
和LinkedList
,我们还可以通过List
接口提供的of()
方法,根据给定元素快速创建List
:
List<Integer> list = List.of(1, 2, 5);
但是List.of()
方法不接受null
值,如果传入null
,会抛出NullPointerException
异常。
2.遍历List
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (String s : list) {
System.out.println(s);
}
}
}
import java.util.*;
public class Main {
public static void main(String[] args) {
// 构造从start到end的序列:
final int start = 10;
final int end = 20;
List<Integer> list = new ArrayList<>();
for (int i = start; i <= end; i++) {
list.add(i);
}
// 洗牌算法shuffle可以随机交换List中的元素位置:
Collections.shuffle(list);
// 随机删除List中的一个元素:
int removed = list.remove((int) (Math.random() * list.size()));
int found = findMissingNumber(start, end, list);
System.out.println(list.toString());
System.out.println("missing number: " + found);
System.out.println(removed == found ? "测试成功" : "测试失败");
}
static int findMissingNumber(int start, int end, List<Integer> list) {
final int start1 = 10;
final int end1 = 20;
List<Integer> list1 = new ArrayList<>();
for (int i = start1; i <= end1; i++) {
list1.add(i);
}
for(Integer s : list1){
if (!list.contains(s.intValue())) {
return s.intValue();
}
}
return 0;
}
}
三.equals
List
内部并不是通过==
判断两个元素是否相等,而是使用equals()
方法判断两个元素是否相等。
要正确使用List
的contains()
、indexOf()
这些方法,放入的实例必须正确覆写equals()
方法,否则,放进去的实例,查找不到。我们之所以能正常放入String
、Integer
这些对象,是因为Java标准库定义的这些类已经正确实现了equals()
方法。
public class Main {
public static void main(String[] args) {
List<Person> list = List.of(
new Person("Xiao Ming"),
new Person("Xiao Hong"),
new Person("Bob")
);
System.out.println(list.contains(new Person("Bob"))); // false
}
}
class Person {
String name;
public Person(String name) {
this.name = name;
}
}
//虽然放入了new Person("Bob"),但是用另一个new Person("Bob")查询不到,原因就是Person类没有覆写equals()方法。
编写equals
equals()
方法的正确编写方法:
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用
instanceof
判断传入的待比较的Object
是不是当前类型,如果是,继续比较,否则,返回false
; - 对引用类型用
Objects.equals()
比较,对基本类型直接用==
比较。
使用Objects.equals()
比较两个引用类型是否相等的目的是省去了判断null
的麻烦。两个引用类型都是null
时它们也是相等的。
如果不调用List
的contains()
、indexOf()
这些方法,那么放入的元素就不需要实现equals()
方法。
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;
}
import java.util.Objects;
public class Main {
public static void main(String[] args) {
// List<Person> list = List.of(
// new Person("Xiao", "Ming", 18),
// new Person("Xiao", "Hong", 25),
// new Person("Bob", "Smith", 20)
// );
List<Person> list = new ArrayList<>();
list.add(new Person("Xiao", "Ming", 18));
list.add(new Person("Xiao", "Hong", 25));
list.add(new Person("Bob", "Smith", 20));
boolean exist = list.contains(new Person("Bob", "Smith", 20));
System.out.println(exist ? "测试成功!" : "测试失败!");
}
}
class Person {
String firstName;
String lastName;
int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
return Objects.equals(this.firstName, p.firstName) &&Objects.equals(this.lastName, p.lastName)&& this.age == p.age;
}
return false;
}
}
在
List
中查找元素时,List
的实现类通过元素的equals()
方法比较两个元素是否相等,因此,放入的元素必须正确覆写equals()
方法,Java标准库提供的String
、Integer
等已经覆写了equals()
方法;编写
equals()
方法可借助Objects.equals()
判断。如果不在
List
中查找元素,就不必覆写equals()
方法。
四.Map
Map<K, V>
是一种键-值映射表,当我们调用put(K key, V value)
方法时,就把key
和value
做了映射并放入Map
。当我们调用V get(K key)
时,就可以通过key
获取到对应的value
。如果key
不存在,则返回null
。和List
类似,Map
也是一个接口,最常用的实现类是HashMap
。
如果只是想查询某个key
是否存在,可以调用boolean containsKey(K key)
方法。
始终牢记:Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。
遍历Map
对Map
来说,要遍历key
可以使用for each
循环遍历Map
实例的keySet()
方法返回的Set
集合,它包含不重复的key
的集合:
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 456);
map.put("banana", 789);
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + " = " + value);
}
}
}
class Students {
List<Student> list;//定义了一个Student的List,名为list,可能十分巨大
Map<String, Integer> cache;//定义了一个Map,key为String类型,value为Integer类型
//作为缓存来存放那些被查询频率很高的学生,比如成绩差爱惹事的学生,下次查询直接从这里面找就会很快
Students(List<Student> list) {
this.list = list;
cache = new HashMap<>();
}
int getScore(String name) {
// a.有个娃犯事了,先看看重点关注对象(cache)里有没有这娃
Integer score = this.cache.get(name);//试图从cache中查找这娃对应的成绩
if (score == null) {//b.如果成绩为null,说明没有重点关注他(cache里没有他的名字)
// TODO:
score = findInList(name);//c.我们再在学校的超级学生名册里来查找一下
if (score!= null) {//d.如果有成绩记录,score不为null,看来确实是我们学校的学生
cache.put(name, score);//e.立即把他放到重点关注对象里来,下次犯事了很快就能查到他所有信息
}
}
return score == null ? -1 : score.intValue();//f.这里不太好理解,为什么会再次判断score是否为null,我们来分析一下:
//这里有两种情况:
//1.当在执行步骤b的判断时如果score!=null,说明重点关注对象里有他,直接跳过if循环,return时score==null为假,返回他的成绩
//2.当在执行步骤b的判断时如果score==null,if循环里的程序会被执行,这里步骤c的语句 score = findInList(name)会重新对score进行赋值
//根据下面findInList(String name)方法我们知道它可能返回null,也可能返回分数,所以最后return的时候会再次判断score是否为null
//如果score为null则返回-1,表示这个学生不在超级学生名单里,不是学校里的娃
}
Integer findInList(String name) {
for (var ss : this.list) {
if (ss.name.equals(name)) {
return ss.score;
}
}
return null;//表示查询的人不在名单内
}
}
Map
是一种映射表,可以通过key
快速查找value
。可以通过
for each
遍历keySet()
,也可以通过for each
遍历entrySet()
,直接获取key-value
。最常用的一种
Map
实现是HashMap
。
五.equals和hashCode
在Map
的内部,对key
做比较是通过equals()
实现的,这一点和List
查找元素需要正确覆写equals()
是一样的,即正确使用Map
必须保证:作为key
的对象必须正确覆写equals()
方法。
我们经常使用String
作为key
,因为String
已经正确覆写了equals()
方法。但如果我们放入的key
是一个自己写的类,就必须保证正确覆写了equals()
方法。
正确使用Map
必须保证:
-
作为
key
的对象必须正确覆写equals()
方法,相等的两个key
实例调用equals()
必须返回true
; -
作为
key
的对象还必须正确覆写hashCode()
方法,且hashCode()
方法要严格遵循以下规范:
- 如果两个对象相等,则两个对象的
hashCode()
必须相等; - 如果两个对象不相等,则两个对象的
hashCode()
尽量不要相等。
public class Person {
String firstName;
String lastName;
int age;
@Override
int hashCode() {
int h = 0;
h = 31 * h + firstName.hashCode();
h = 31 * h + lastName.hashCode();
h = 31 * h + age;
return h;
}
//编写equals()和hashCode()遵循的原则是:equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算;equals()中没有使用到的字段,绝不可放在hashCode()中计算。
int hashCode() {
return Objects.hash(firstName, lastName, age);
}
}
//引用类型使用Objects.equals()比较,基本类型使用==比较。
package test.MapDemo;
import java.util.*;
public class equalsDemo2 {
public static void main(String[] args) {
Person p1 = new Person("zhang",12);
Map<Person,Integer> map = new HashMap<>();
map.put(p1,123);
Person p2 = new Person("zhang",12);
System.out.println(p1==p2);
System.out.println(p1.equals(p2));
System.out.println(map.get(p1));
System.out.println(map.get(p2));
}
}
class Person{
public String name;
public int age;
public Person(){}
public Person(String n,int a){
name = n;
age = a;
}
@Override public boolean equals(Object obj) {
if(obj instanceof Person){
Person p = (Person) obj;
return Objects.equals(this.name,p.name) && this.age == p.age;
}
return false;
}
public int hashCode(){
return Objects.hash(name,age);
}
}
false
true
123
123
p1和p2都是Person类的对象,虽然地址值不一样,但是内容相同
所以map.get()获取的value值是相同的
要正确使用
HashMap
,作为key
的类必须正确覆写equals()
和hashCode()
方法;一个类如果覆写了
equals()
,就必须覆写hashCode()
,并且覆写规则是:
如果
equals()
返回true
,则hashCode()
返回值必须相等;如果
equals()
返回false
,则hashCode()
返回值尽量不要相等。实现
hashCode()
方法可以通过Objects.hashCode()
辅助方法实现
六.set
Map
用于存储key-value的映射,对于充当key的对象,是不能重复的,并且,不但需要正确覆写equals()
方法,还要正确覆写hashCode()
方法。
如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set
。
Set
用于存储不重复的元素集合,它主要提供以下几个方法:
- 将元素添加进
Set<E>
:boolean add(E e)
- 将元素从
Set<E>
删除:boolean remove(Object e)
- 判断是否包含元素:
boolean contains(Object e)
Set
实际上相当于只存储key、不存储value的Map
。我们经常用Set
用于去除重复元素。
因为放入Set
的元素和Map
的key类似,都要正确实现equals()
和hashCode()
方法,否则该元素无法正确地放入Set
。
Set
接口并不保证有序,而SortedSet
接口则保证元素是有序的:
HashSet
是无序的,因为它实现了Set
接口,并没有实现SortedSet
接口;TreeSet
是有序的,因为它实现了SortedSet
接口。
使用TreeSet
和使用TreeMap
的要求一样,添加的元素必须正确实现Comparable
接口,如果没有实现Comparable
接口,那么创建TreeSet
时必须传入一个Comparator
对象。
在聊天软件中,发送方发送消息时,遇到网络超时后就会自动重发,因此,接收方可能会收到重复的消息,在显示给用户看的时候,需要首先去重。请练习使用Set
去除重复的消息:
public class Main {
public static void main(String[] args) {
// List<Message> received = List.of(
// new Message(1, "Hello!"),
// new Message(2, "发工资了吗?"),
// new Message(2, "发工资了吗?"),
// new Message(3, "去哪吃饭?"),
// new Message(3, "去哪吃饭?"),
// new Message(4, "Bye")
// );
List<Message> received = new ArrayList<>();
received.add(new Message(1, "Hello!"));
received.add(new Message(2, "发工资了吗?"));
received.add(new Message(2, "发工资了吗?"));
received.add(new Message(3, "去哪吃饭?"));
received.add(new Message(3, "去哪吃饭?"));
received.add(new Message(4, "Bye"));
List<Message> displayMessages = process(received);
for (Message message : displayMessages) {
System.out.println(message.text);
}
}
//方法一:用到sequence来判断,而做这个判断最好的就是用Set来判断要加到List中的Message对象的sequence字段是不是已经存在了
static List<Message> process(List<Message> received) {
// TODO: 按sequence去除重复消息
Set<Integer> map = new HashSet<>();
List<Message> receiveds = new ArrayList<>();
for(Message message: received){
if(!map.contains(message.sequence)){
map.add(message.sequence);
receiveds.add(message);
}
}
return receiveds;
}
//方法二:重写message对象的equals和hashMap使message类可以满足放入set,然后用以message泛型的set就可以自动去重。
//用list和set的相互转换
static List<Message> process(List<Message> received) {
HashSet<Message> messageSet = new HashSet<>(received);
ArrayList<Message> messageList = new ArrayList<>(messageSet);
return messageList;
}
//写在message类中
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Message message = (Message) o;
return sequence == message.sequence && Objects.equals(text, message.text);
}
@Override
public int hashCode() {
return Objects.hash(sequence, text);
}
}
class Message {
public final int sequence;
public final String text;
public Message(int sequence, String text) {
this.sequence = sequence;
this.text = text;
}
}
七.Queue
Queue
实际上是实现了一个先进先出(FIFO:First In First Out)的有序表.
注意:不要把null
添加到队列中,否则poll()
方法返回null
时,很难确定是取到了null
元素还是队列为空。(调用poll()
方法来取出队首元素,当获取失败时,它不会抛异常,而是返回null
)
队列
Queue
实现了一个先进先出(FIFO)的数据结构:
- 通过
add()
/offer()
方法将元素添加到队尾;- 通过
remove()
/poll()
从队首获取元素并删除;- 通过
element()
/peek()
从队首获取元素但不删除。要避免把
null
添加到队列。