集合简介
- 集合(Collection)就是“由若干个确定的元素所构成的整体”。为了便于处理一组类似的数据,在计算机中引入集合。
- 在Java中,如果一个Java对象可以在内部持有若干其他Java对象,并对外提供访问接口,我们把这种Java对象称为集合。显然,Java的数组可以看作是一种集合:
String[] ss = new String[10]; // 可以持有10个String对象
ss[0] = "Hello"; // 可以放入String对象
String first = ss[0]; // 可以获取String对象
- 数组有如下限制:1)数组初始化后大小不可变;2)数组只能按索引顺序存取。因此,我们需要各种不同类型的集合类来处理不同的数据。
- Java标准库自带的
java.util
包提供了集合类:Collection
,它是除Map
外所有其他集合类的根接口。Java的java.util
包主要提供了以下三种类型的集合:
- List:一种有序列表的集合,例如,按索引排列的Student的List;
- Set:一种保证没有重复元素的集合,例如,所有无重复名称的Student的Set;
- Map:一种通过键值(key-value)查找的映射表集合,例如,根据Student的name查找对应Student的Map。
- Java集合的设计有几个特点:一是实现了接口和实现类相分离,二是支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素。
- Java访问集合总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。
注意:有一小部分集合类是遗留类,不应该继续使用:
-
Hashtable
:一种线程安全的Map
实现; -
Vector
:一种线程安全的List
实现; -
Stack
:基于Vector
实现的LIFO
的栈; -
Enumeration<E>
:已被Iterator<E>
取代。
使用List
- 在集合类中,
List
是最基础的一种集合:它是一种有序列表。List
的行为和数组几乎完全相同,在添加和删除元素的时候,会非常不方便。因此,在实际应用中,需要增删元素的有序列表,我们使用最多的是ArrayList
。实际上,ArrayList
在内部使用了数组来存储所有元素。 -
ArrayList
把添加和删除的操作封装起来,让我们操作List
类似于操作数组,却不用关心内部元素如何移动。 - 我们考察
List<E>
接口,可以看到几个主要的接口方法:
- 在末尾添加一个元素:
boolean add(E e)
- 在指定索引添加一个元素:
boolean add(int index, E e)
- 删除指定索引的元素:
int remove(int index)
- 删除某个元素:
int remove(Object e)
- 获取指定索引的元素:
E get(int index)
- 获取链表大小(包含元素的个数):
int size()
- 实现
List
接口并非只能通过数组(即ArrayList
的实现方式)来实现,另一种LinkedList
通过“链表”也实现了List接口。在LinkedList
中,它的内部每个元素都指向下一个元素:
- 我们来比较一下
ArrayList
和LinkedList
:
ArrayList | LinkedList | |
获取指定元素 | 速度很快 | 需要从头开始查找元素 |
添加元素到末尾 | 速度很快 | 速度很快 |
在指定位置添加/删除 | 需要移动元素 | 不需要移动元素 |
内存占用 | 少 | 较大 |
通常情况下,我们总是优先使用ArrayList
。
- List的特点
- 允许添加重复的元素
- 允许添加null
public class ListTest {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("apple");//size=1
list.add("pear"); //size=2
list.add("apple"); //size=3
list.add(null); //size=4
System.out.println(list.size());
System.out.println(list);
}
}
- 创建LIst的方法
- 使用
ArrayList
和LinkedList
- 通过
List
接口提供的of()
方法(JDK9新特性)
List<Integer> list = List.of(1, 2, 5);
//但是List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常。
- 遍历List的方法
- for循环+索引
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple");//size=1
list.add("pear"); //size=2
list.add("banana"); //size=3
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
}
}
但这种方式并不推荐,一是代码复杂,二是因为get(int)
方法只有ArrayList
的实现是高效的,换成LinkedList
后,索引越大,访问速度越慢。
- 使用迭代器
Iterator
来访问List
Iterator对象有两个方法:boolean hasNext()判断是否有下一个元素,E next()返回下一个元素。
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple");//size=1
list.add("pear"); //size=2
list.add("banana"); //size=3
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
}
}
由于Iterator
遍历是如此常用,所以,Java的for each
循环本身就可以帮我们使用Iterator
遍历。
for (String s : list) {
System.out.println(s);
}
- List和Array的转换
a. List–>Array
i. 调用toArray()
方法直接返回一个Object[]
数组,这种方法会丢失类型信息,所以实际应用很少。
Object[] array=list.toArray();
for (Object s:array){
System.out.println(s);
}
ii. 给toArray(T[])
传入一个类型相同的Array
,List
内部自动把元素复制到传入的Array
中。
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Integer[] array = list.toArray(new Integer[3]);
for (Integer n : array) {
System.out.println(n);
}
}
}
注意:如果传入的数组不够大,那么List
内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List
元素还要多,那么填充完元素后,剩下的数组元素一律填充null
。
b. Array–>List
i. 通过List.of(T...)
方法(JDK9)
ii. 对于JDK 11之前的版本,可以使用Arrays.asList(T...)
方法把数组转换成List
Integer[] array1 = { 1, 2, 3 };
List<Integer> list2 = Arrays.asList(array);
System.out.println(list2);
注意,如果我们调用List.of()
,它返回的是一个只读List,对只读List
调用add()
、remove()
方法会抛出UnsupportedOperationException
。
编写equals方法
- 要正确使用
List
的contains()
、indexOf()
这些方法,放入的实例必须正确覆写equals()
方法,否则,放进去的实例,查找不到。我们之所以能正常放入String
、Integer
这些对象,是因为Java标准库定义的这些类已经正确实现了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为例,定义“相等”的逻辑含义。对于
Person
类,如果name
相等,并且age
相等,我们就认为两个Person
实例相等。
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"))); // TRUE
}
}
class Person {
public String name;
public int age;
public Person(String name) {
this.name = name;
}
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
boolean nameEquals = false;
if (this.name == null && p.name == null) {
nameEquals = true;
}
if (this.name != null) {
nameEquals = this.name.equals(p.name);
}
return nameEquals && this.age == p.age;
}
return false;
}
}
如果Person
有好几个引用类型的字段,上面的写法就太复杂了。要简化引用类型的比较,我们使用Objects.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;
}
使用Objects.equals()
比较两个引用类型是否相等的目的是省去了判断null
的麻烦。两个引用类型都是null
时它们也是相等的。
使用Map
-
Map
这种键值(key-value)映射表的数据结构,作用就是能高效通过key
快速查找value
(元素)。 -
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,value可重复,因为放入相同的key,只会把原有的key-value对应的value给替换掉。
public class MapTest {
public static void main(String[] args) {
Person p = new Person("liu", 22);
Map<String, Person> map = new HashMap<>();
map.put("liu", p);//将“liu”和Person实例映射并关联
Person target = map.get("liu"); //通过key查找并返回映射的Person实例
System.out.println(target == p); //true,同一个实例
System.out.println(target.age); //22
Person another=map.get("Bob");
System.out.println(another); //未找到,返回null
System.out.println(map.containsKey("liu"));
}
}
- 遍历Map
- 键找值方式:要遍历
key
可以使用for each
循环遍历Map
实例的keySet()
方法返回的Set
集合,它包含不重复的key
的集合
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);
}
}
- 键值对方式:同时遍历
key
和value
可以使用for each
循环遍历Map
对象的entrySet()
集合,它包含每一个key-value
映射
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 (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}
-
Map
和List
不同的是,Map
存储的是key-value
的映射关系,并且,它不保证顺序。在遍历的时候,遍历的顺序既不一定是put()
时放入的key
的顺序,也不一定是key
的排序顺序。
HashMap&HashCOde
-
HashMap
之所以能根据key
直接拿到value
,原因是它内部通过空间换时间的方法,用一个大数组存储所有value
,并根据key直接计算出value
应该存储在哪个索引。 - HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
- 在
Map
的内部,对key
做比较是通过equals()
实现的,正确使用Map
必须保证:作为key
的对象必须正确覆写equals()
方法。通过key
计算索引的方式就是调用key
对象的hashCode()
方法,它返回一个int
整数。HashMap
正是通过这个方法直接定位key
对应的value
的索引,继而直接返回value
。 - 正确使用
Map
必须保证:
a. 作为key
的对象必须正确覆写equals()
方法,相等的两个key
实例调用equals()
必须返回true
;
b. 作为key
的对象还必须正确覆写hashCode()
方法,且hashCode()
方法要严格遵循以下规范:
- 如果两个对象相等,则两个对象的
hashCode()
必须相等; - 如果两个对象不相等,则两个对象的
hashCode()
尽量不要相等。
即对应两个实例a
和b
:
- 如果
a
和b
相等,那么a.equals(b)
一定为true
,则a.hashCode()
必须等于b.hashCode()
; - 如果
a
和b
不相等,那么a.equals(b)
一定为false
,则a.hashCode()
和b.hashCode()
尽量不要相等。
- 我们把不同的
key
具有相同的hashCode()
的情况称之为哈希冲突。在冲突的时候,一种最简单的解决办法是用List
存储hashCode()
相同的key-value
。显然,如果冲突的概率越大,这个List
就越长,Map
的get()
方法效率就越低。
使用EnumMap
- 因为HashMap是一种通过对key计算hashCode(),通过空间换时间的方式,直接定位到value所在的内部数组的索引,因此,查找效率非常高。
- 如果作为key的对象是enum类型,那么,还可以使用Java集合库提供的一种EnumMap,它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。
- 以DayOfWeek这个枚举类型为例,为它做一个“翻译”功能:
public class EnumMapTest {
public static void main(String[] args) {
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));
}
}
使用TreeMap
- 还有一种
Map
,它在内部会对Key进行排序,这种Map
就是SortedMap
。注意到SortedMap
是接口,它的实现类是TreeMap
。
-
SortedMap
保证遍历时以Key的顺序来进行排序。例如,放入的Key是"apple"
、"pear"
、"orange"
,遍历的顺序一定是"apple"
、"orange"
、"pear"
,因为String
默认按字母排序:
public class TreeMapTest {
public static void main(String[] args) {
Map<String, Integer> map = new TreeMap<>();
map.put("orange", 1);
map.put("apple", 2);
map.put("pear", 3);
for (String key:map.keySet()){
System.out.println(key);
}
}
}
- 使用
TreeMap
时,放入的Key必须实现Comparable
接口。如果作为Key的class没有实现Comparable
接口,那么,必须在创建TreeMap
时同时指定一个自定义排序算法:
public class Main {
public static void main(String[] args) {
Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() {
public int compare(Person p1, Person p2) {
return p1.name.compareTo(p2.name);
}
});
map.put(new Person("Tom"), 1);
map.put(new Person("Bob"), 2);
map.put(new Person("Lily"), 3);
for (Person key : map.keySet()) {
System.out.println(key);
}
// {Person: Bob}, {Person: Lily}, {Person: Tom}
System.out.println(map.get(new Person("Bob"))); // 2
}
}
class Person {
public String name;
Person(String name) {
this.name = name;
}
public String toString() {
return "{Person: " + name + "}";
}
}
注意到Person
类并未覆写equals()
和hashCode()
,因为TreeMap
不使用equals()
和hashCode()
。
使用Set
- 如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用
Set
。 -
Set
用于存储不重复的元素集合,它主要提供以下几个方法:
- 将元素添加进
Set<E>
:boolean add(E e)
- 将元素从
Set<E>
删除:boolean remove(Object e)
- 判断是否包含元素:
boolean contains(Object e)
- 因为放入
Set
的元素和Map
的key类似,都要正确实现equals()
和hashCode()
方法,否则该元素无法正确地放入Set
。 - 最常用的
Set
实现类是HashSet
,实际上,HashSet
仅仅是对HashMap
的一个简单封装。Set
接口并不保证有序,而SortedSet
接口则保证元素是有序的:
-
HashSet
是无序的,因为它实现了Set
接口,并没有实现SortedSet
接口; -
TreeSet
是有序的,因为它实现了SortedSet
接口。
public class Main {
public static void main(String[] args) {
Set<String> set = new TreeSet<>();
set.add("apple");
set.add("banana");
set.add("pear");
set.add("orange");
for (String s : set) {
System.out.println(s);
}
}
}
使用TreeSet
和使用TreeMap
的要求一样,添加的元素必须正确实现Comparable
接口,如果没有实现Comparable
接口,那么创建TreeSet
时必须传入一个Comparator
对象。