Java集合框架之 Collection(1.2)
Collection
JDK
中在java.util
包下提供了一组类库,用以实现集合这一数据存储结构,作为容器,用以存储、处理大量对象。这些由JDK
提供的集合主要围绕着Collection
接口和Map
接口实现,由此也分为两类,本节主要介绍由Collection
接口实现的集合。
1.1、Collection接口相关继承关系
Collection
接口下,主要由List接口
、Set接口
和Queue接口
对其进行了继承。
List接口
主要由ArrayList类
、Vector类
、LinkedList类
等对其进行实现Set接口
主要由HashSet类
、TreeSet类
等对其进行实现
它们都在java.util
包下,它们之间简单的继承关系图可以概括如上。
解释:
1、java.util.Collection
接口是一个泛型接口,主要规范的方法:
add(E e)
、contains(Object o)
、toArray()
、remove(Object o)
、size()
、clear()
2、java.util.List
接口是一个泛型接口,继承了java.util.Collection
接口,规范了有序(这里的有序指元素具有下标,可以通过下标索引来访问元素)集合的行为,在java.util.Collection
接口的基础上,又规范了get(int index)、set(int index, E element)、add(int index, E element)、remove(int index)、indexOf(Object o)、lastIndexOf(Object o)
等方法。
3、java.util.Set
接口是一个泛型接口,继承了java.util.Collection接口,规范了无序(这里的无序指元素不具有下标,无法通过下标索引来访问元素)集合的行为,该接口覆盖了java.util.Collection接口的部分方法。
4、java.util.ArrayList
类、java.util.Vector类、java.util.LinkedList类均分别实现了java.util.List接口;
5、java.util.HashSet
类、java.util.TreeSet类均分别实现了java.util.Set接口。
开发人员在学习、使用这些集合相关的类库时,需要熟悉这些类库中提供的常用API,进而掌握如何向集合中添加元素、如何从集合中获取某一个元素,如何获取集合中所有的元素,如何将一个元素从集合中移除,如何判断集合中是否包含了某个元素等常用操作。
1.2、List接口
实现了java.util.List
接口的集合也被称为列表,它们和数组比较相似,使用这类集合存储的对象有序,可以重复。常见的实现类如java.util.ArrayList
类、java.util.Vector
类、java.util.LinkedList
类等,它们的功能与用法几乎完全相同,只是内部实现不同。
java.util.List
接口中的API
如下:
参考API: JDK 1.8
序号 | 返回值 | 方法 | 方法说明 |
---|---|---|---|
01 | boolean | add(E e) | 将指定的元素追加到此列表的末尾(可选操作) |
02 | void | add(int index, E element) | 在列表中指定的位置上插入指定的元素(可选操作) |
03 | boolean | addAll(Collection<? extends E> c) | 追加指定集合的所有元素到这个列表的末尾,按他们的指定集合的迭代器返回(可选操作) |
04 | boolean | addAll(int index, Collection<? extends E> c) | 将指定的集合中的所有元素插入到指定位置的列表中(可选操作) |
05 | void | clear() | 从这个列表中移除所有的元素(可选操作)。 |
06 | boolean | contains(Object o) | 返回 true如果这个列表包含指定元素。 |
07 | boolean | containsAll(Collection<?> c) | 返回 true如果这个列表包含指定集合的所有元素。 |
08 | boolean | equals(Object o) | 将指定的对象与此列表进行比较,以进行相等性。 |
09 | E | get(int index) | 返回此列表中指定位置的元素。 |
10 | int | hashCode() | 返回此列表的哈希代码值。 |
11 | int | indexOf(Object o) | 返回此列表中指定元素的第一个出现的索引,或-如果此列表不包含元素,或- 1。 |
12 | boolean | isEmpty() | 返回 true如果此列表不包含元素 |
13 | Iterator<E> | iterator() | 在这个列表中的元素上返回一个正确的顺序。 |
14 | int | lastIndexOf(Object o) | 返回此列表中指定元素的最后一个发生的索引,或-如果此列表不包含元素,或- 1。 |
15 | ListIterator<E> | listIterator() | 返回列表元素的列表迭代器(在适当的顺序)。 |
16 | ListIterator<E> | listIterator(int index) | 在列表中的元素上返回列表迭代器(在适当的顺序),从列表中的指定位置开始 |
17 | E | remove(int index) | 移除此列表中指定位置的元素(可选操作)。 |
18 | boolean | remove(Object o) | 从该列表中移除指定元素的第一个发生,如果它是存在的(可选操作)。 |
19 | boolean | removeAll(Collection<?> c) | 从这个列表中移除包含在指定集合中的所有元素(可选操作)。 |
20 | default void | replaceAll(UnaryOperator<E> operator) | 用将运算符应用到该元素的结果替换此列表中的每个元素。 |
21 | boolean | retainAll(Collection<?> c) | 仅保留包含在指定集合中的列表中的元素(可选操作)。 |
22 | E | set(int index, E element) | 用指定元素替换此列表中指定位置的元素(可选操作)。 |
23 | int | size() | 返回此列表中元素的数目。 |
24 | default void | sort(Comparator<? super E> c) | 分类列表使用提供的 Comparator比较元素。 |
25 | default Spliterator | spliterator() | 创建此列表中的元素的 Spliterator。 |
26 | List<E> | subList(int fromIndex, int toIndex) | 返回一个视图之间的指定 fromIndex,包容,和 toIndex这份名单的部分,独家。 |
27 | Object[] | toArray() | 返回一个数组,包含在这个列表中的所有元素在适当的顺序(从第一个到最后一个元素)。 |
28 | <T> T[] | toArray(T[] a) | 返回一个数组,包含在这个列表中的所有元素在适当的顺序(从第一到最后一个元素);返回数组的运行时类型是指定的数组的运行时类型。 |
JDK 1.8比JDK 1.6多了三个方法。
sort(Comparator<? super E> c)
spliterator()
replaceAll(UnaryOperator<E> operator)
1.2.1、ArrayList类
Java.util.ArrayList
类借助数组实现java.util.List
接口,该类是一个泛型类,存储的对象有序且可重复。
由于java.util.ArrayList
类基于数组,在内存中分配的空间连续,因此访问元素速度相对较快,添加和删除元素速度相对较慢。
例1:
Student类的源码:
package com.bennett.test;
public class Student {
private String name; // 姓名
// 构造方法
public Student(String name) {
this.name = name;
}
// 学习的方法
public void study(){
System.out.println("student" + this.name + "在学习");
}
}
Teacher类的源码:
package com.bennett.test;
public class Teacher {
private String name; // 姓名
// 构造方法
public Teacher(String name) {
this.name = name;
}
// 授课的方法
public void teach(){
System.out.println("teacher" + this.name + "在授课");
}
测试类Test类的源码:
package com.bennett.test;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
// 声明并实例化集合对象list
List list = new ArrayList();
// 向集合中添加不同类型的元素
Teacher t = new Teacher("卢俊义");
Student s = new Student("燕青");
list.add("Hello");
list.add(t);
list.add(s);
// 判断集合中是否包含"Hello"
if (list.contains("Hello")) {
System.out.println("集合中包含Hello");
} else {
System.out.println("集合中不包含Hello");
}
// 显示集合中所有的元素
System.out.println("集合中有以下元素");
for (int i = 0; i < list.size(); i++) {
if (list.get(i) instanceof java.lang.String) {
System.out.println(list.get(i));
} else if (list.get(i) instanceof Teacher) {
Teacher tea = (Teacher) list.get(i);
tea.teach();
} else if (list.get(i) instanceof Student) {
Student stu = (Student) list.get(i);
stu.study();
}
}
// 从集合中移除下标为0的元素
list.remove(0);
// 显示移除后集合中所有的元素
System.out.println("移除后集合中还有" + list.size() + "个元素");
}
}
执行输出结果:
集合中包含Hello
集合中有以下元素
Hello
teacher卢俊义在授课
student燕青在学习
移除后集合中还有2个元素
说明:
本例的测试类main方法中,声明了java.util.List类型的变量list,并引用了一个新实例化的java.util.ArrayList类对象,由于在此过程中没有指定类型参数所代表的具体类型,故类型参数所代表的具体类型就是java.lang.Object。随后,使用add(Object e)方法向集合list中添加了String类型的元素"Hello"、Teacher类型的元素t、Student类型的元素s。再之后,还使用了集合list调用了contains(Object o)、get(int index)、remove(int index)、size()等方法。
需要注意的是,由于没有指定类型参数具体代表的类型,get(int index)方法返回的集合元素的类型均为java.lang.Object,如果要赋值给其他类型的变量,需要强制类型转换。
例2:
Person类的源码:
package com.bennett.test;
public class Person {
private String name; // 姓名
// 构造方法
public Person(String name) {
this.name = name;
}
// 工作的方法
public void work() {
System.out.println("person" + this.name + "在工作");
}
}
测试类Test类的源码:
package com.bennett.test;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
// 指定集合的类型参数为Person,该集合中只允许存储Person类型的对象
List<Person> list = new ArrayList<Person>();
// 实例化几个Person对象
Person p1 = new Person("鲁智深");
Person p2 = new Person("武松");
Person p3 = new Person("林冲");
// 向集合中添加元素
list.add(p1); // add方法的参数是Person类型
list.add(p2); // add方法的参数是Person类型
list.add(p3); // add方法的参数是Person类型
list.add(p3); // 将对象p3再添加一遍
// 判断集合中是否包含p1
if (list.contains(p1)) {
System.out.println("集合中包含p1");
} else {
System.out.println("集合中不包含p1");
}
// 显示集合中所有的元素
for (int i = 0; i < list.size(); i++) {
list.get(i).work(); // get方法的返回值是Person类型
}
// 从集合中移除p1元素
list.remove(p1);
// 显示移除p1元素后集合中剩余的元素个数
System.out.println("移除后集合中还有" + list.size() +"个元素");
}
}
执行输出结果:
集合中包含p1
person鲁智深在工作
person武松在工作
person林冲在工作
person林冲在工作
移除后集合中还有3个元素
说明:
本例在使用java.util.List接口声明变量时和实例化java.util.ArrayList类对象时,指定了类型参数为Person,因此,集合list中只能储存Person类型的对象,add(E e)方法的参数只能是Person类型,get(int index)方法的返回值类型也是Person类型,不需要强制类型转换。
1.2.2、Vector类
java.util.Vector
类同java.util.ArrayList
类非常相似,同样借助数组实现java.util.List
接口,该类也是一个泛型类,存储的对象有序且可重复;与java.util.ArrayList
类最核心的不同是,java.util.Vector
类是线程同步(Thread Synchronized
)的,所以它也是线程安全的,而java.util.ArrayList
类是线程异步(Thread ASynchronized
)的,在多线程的场景下不能保证线程安全。
例1:
Person类的源码同上例。
测试类Test类的源码:
package com.bennett.test;
import java.util.List;
import java.util.Vector;
public class Test {
public static void main(String[] args) {
// 声明集合变量list,并引用一个新实例化的Vector对象
List<Person> list = new Vector<Person>();
// 实例化几个Person对象
Person p1 = new Person("鲁智深");
Person p2 = new Person("武松");
Person p3 = new Person("林冲");
//向集合中添加元素
list.add(p1); // add方法的参数是Person类型
list.add(p2); // add方法的参数是Person类型
list.add(p3); // add方法的参数是Person类型
list.add(p3); // 将对象p3再添加一遍
// 判断集合中是否包含p1
if (list.contains(p1)) {
System.out.println("集合中包含p1");
} else {
System.out.println("集合中不包含p1");
}
// 显示集合中所有的元素
for (int i = 0; i < list.size(); i++) {
list.get(i).work();
}
// 从集合中移除p1元素
list.remove(p1);
// 显示移除p1元素后集合中剩余的元素个数
System.out.println("移除后集合中还有" + list.size() +"个元素");
}
}
执行输出结果:
集合中包含p1
person鲁智深在工作
person武松在工作
person林冲在工作
person林冲在工作
移除后集合中还有3个元素
说明:
java.util.Vector
类的使用与java.util.ArrayList
类使用方法几乎完全相同,这里不再赘述。
1.2.3、LinkedList类
java.util.LinkedList
类借助链表实现java.util.List
接口,该类是一个泛型类,存储的对象有序且可重复。该类的使用方法和java.util.ArrayList
类基本相同,而与java.util.ArrayList
类不同的是,java.util.LinkedList
类基于链表,在内存中分配的空间不必连续,因此添加和删除元素速度相对较快,而访问元素速度相对较慢。
例1:
Person类的源码同上例。
测试类Test类的源码:
package com.bennett.test;
import java.util.LinkedList;
import java.util.List;
public class Test {
public static void main(String[] args) {
// 声明集合变量list,并引用一个新实例化的LinkedList对象
List<Person> list = new LinkedList<Person>();
// 实例化几个Person对象
Person p1 = new Person("鲁智深");
Person p2 = new Person("武松");
Person p3 = new Person("林冲");
//向集合中添加元素
list.add(p1); // add方法的参数是Person类型
list.add(p2); // add方法的参数是Person类型
list.add(p3); // add方法的参数是Person类型
list.add(p3); // 将对象p3再添加一遍
// 判断集合中是否包含p1
if (list.contains(p1)) {
System.out.println("集合中包含p1");
} else {
System.out.println("集合中不包含p1");
}
// 显示集合中所有的元素
for (int i = 0; i < list.size(); i++) {
list.get(i).work();
}
// 从集合中移除p1元素
list.remove(p1);
// 显示移除p1元素后集合中剩余的元素个数
System.out.println("移除后集合中还有" + list.size() +"个元素");
}
}
执行输出结果:
集合中包含p1
person鲁智深在工作
person武松在工作
person林冲在工作
person林冲在工作
移除后集合中还有3个元素
说明:
java.util.LinkedList
类的是使用和java.util.Vector
类、java.util.ArrayList
类几乎完全相同,这里不再赘述。
1.3、Set接口
实现了java.util.Set
接口的集合,存储的对象无序且不可重复。常见的实现类如java.util.HashSet
类、java.util.TreeSet
类等,它们的功能与用法几乎完全相同,只是内部实现不同。java.util.Set
接口中的一些常用API
如下:
方法 返回值 方法说明
add(E e) boolean 如果指定的元素不存在,则添加该元素
clear() void 从此集合中删除所有元素
contains(Object o) boolean 如果此集合包含指定的元素,则返回true,否则返回false
isEmpty() boolean 如果此集合不包含元素,则返回true,否则返回false
remove(Object o) boolean 如果存在,则从该集合中删除指定的元素
size() int 返回此集合中的元素数
toArray() Object[] 返回一个包含此集合中所有元素的数组
1.3.1、HashSet类
java.util.HashSet
类借助散列算法实现java.util.Set
接口,该类是一个泛型类。该类的对象可以储存无序、唯一的对象,由于储存的元素是无序的,故无法基于下标进行操作。
下面是一个示例:
Person类的源码同上例。
测试类Test类的源码:
package com.bennett.test;
import java.util.HashSet;
import java.util.Set;
public class Test {
public static void main(String[] args) {
// 声明并实例化Set集合,指定类型参数为Person
Set<Person> set = new HashSet<Person>();
// 实例化几个Person对象
Person p1 = new Person("鲁智深");
Person p2 = new Person("武松");
Person p3 = new Person("林冲");
// 向集合中添加元素
set.add(p1); // add方法的参数是Person类型
set.add(p2); // add方法的参数是Person类型
set.add(p3); // add方法的参数是Person类型
set.add(p3); // 将对象p3再添加一遍
// 显示集合中的元素个数
System.out.println("集合中有"+ set.size() +"个元素");
// 显示集合中所有的元素
for (Person person : set) {
person.work();
}
// 从集合中删除p1元素
set.remove(p1);
// 显示移除p1元素后集合中剩余的元素个数
System.out.println("移除后集合中还有" + set.size() +"个元素");
}
}
执行输出结果:
集合中有3个元素
person林冲在工作
person武松在工作
person鲁智深在工作
移除后集合中还有2个元素
说明:
本例使用java.util.Set接口声明变量set,实例化java.util.HashSet类对象并赋值给变量set。由于set集合中的元素不可重复,故本例中两次执行语句set.add(p3),集合中只会保存一个对象p3的引用。又由于set集合是无序的,故无法使用普通的for循环通过下标遍历集合中的元素,不过set集合仍然可以使用For-Each循环(加强型循环)遍历。
下面是另一个示例:
Person类的源码同上例。
测试类Test类的源码:
package com.codeke.java.test;
import java.util.HashSet;
import java.util.Set;
public class Test {
public static void main(String[] args) {
// 声明并实例化Set集合,指定类型参数为Integer
Set<Integer> integerSet = new HashSet<Integer>();
// 实例化两个Integer对象
Integer num1 = new Integer(108);
Integer num2 = new Integer(108);
// 向集合中添加元素
integerSet.add(num1);
integerSet.add(num2);
// 打印集合长度
System.out.println("integerSet的长度是 = " + integerSet.size());
// 声明并实例化Set集合,指定类型参数为Person
Set<Person> personSet = new HashSet<Person>();
// 实例化两个Person对象
Person p1 = new Person("吴用");
Person p2 = new Person("吴用");
// 向集合中添加元素
personSet.add(p1);
personSet.add(p2);
// 打印集合长度
System.out.println("personSet的长度是 = " + personSet.size());
}
}
执行输出结果:
integerSet的长度是 = 1
personSet的长度是 = 2
说明:
前文提到,java.util.HashSet类实例化的集合中元素是不可重复的,观察本例中的代码,java.lang.Integer类的对象num1、num2在内存中的地址不同,但所包装的字面值相同,在将对象num1、num2添加到java.util.HashSet类实例化的集合integerSet中后,集合integerSet的长度为1,显然该集合认为对象num1、num2重复了;之后,两个Person类的对象p1和p2,它们在内存中的地址也不同,但属性字面值相同,在将对象p1和p2添加到java.util.HashSet类实例化的集合personSet中后,集合personSet的长度为2,显然该集合认为对象p1、p2没有重复。
本例的代码便提出了这样一个问题,由java.util.HashSet类实例化的集合中元素是否重复是如何界定的?
实际上,由java.util.HashSet类实例化的集合通过元素的hashCode()方法和equals(Object obj)方法来判断元素是否重复,其逻辑大致为,当向集合中添加一个对象时,先调用该对象的hashCode()方法,获得该对象的哈希值,通过哈希值分析元素在集合中的存储位置,得出的存储位置没有被其他元素占据时,存储该对象,而当得出存储位置上已经存储了另一元素时,使用该对象的equals(Object obj)方法与另一元素比较,equals(Object obj)方法返回false则继续存储,equals(Object obj)方法返回true则认为对象重复,不再存储。本例中,集合integerSet集合认为对象num1、num2重复,是因为java.lang.Integer类中重写了hashCode()方法和equals(Object obj)方法,保证了在java.lang.Integer类的对象封装的整数字面值相同时,hashCode()方法返回值相同且equals(Object obj)方法比较后返回的结果为true。
前面的章节提到,在实际开发中,有时需要当两个对象的属性值完全对应相同时即认为两个对象相同,此时,equals(Object obj)方法需要被重写;而在这种情况下,如果对象还需要存储到本质是哈希表的数据结构中(Java中常用的有java.util.HashSet、java.util.HashMap、java.util.HashTable等),并要确保属性值完全对应相同的对象在该数据结构中唯一,仅仅重写equals(Object obj)方法是不够的,还需要重写hashCode()方法,确保属性值完全对应相同的对象调用hashCode()方法时返回的哈希值完全相同。下面重写Person
类中的hashCode()
方法和equals(Object obj)
方法,Person
类修改后的源码如下:
package com.codeke.java.test;
public class Person {
private String name; // 姓名
// 构造方法
public Person(String name) {
this.name = name;
}
// 工作的方法
public void work() {
System.out.println("person" + this.name + "在工作");
}
// 重写后的equals(Object o)方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return name.equals(person.name);
}
// 重写后的hashCode()方法
@Override
public int hashCode() {
return name.hashCode();
}
}
再次执行测试类Test类中的main方法,执行输出结果如下:
integerSet的长度是 = 1
personSet的长度是 = 1
可以看到,在将对象p1和p2添加到java.util.HashSet类实例化的集合personSet中后,集合personSet的长度为1,显然此时该集合认为对象p1、p2重复了。
1.3.2、TreeSet类
java.util.TreeSe
t类借助二叉树(红黑树
)实现java.util.Set
接口,该类也是一个泛型类。该类的使用方法与java.util.HashSet
类几乎完全相同,不同的是,java.util.TreeSet
集合在保存元素时会对元素进行比较并按照自然的方式对元素进行排序(注意这里的排序和前文中“有序”二字的区别,有序指的是元素按照进入集合的顺序具有下标,而这里的排序值的是按照元素本身的值比较之后进行排序,和元素的下标没有关系),java.util.TreeSet
集合中的元素仍然无法通过下标访问。
下面是一个示例:
package com.codeke.java.test;
import java.util.HashSet;
import java.util.TreeSet;
import java.util.Set;
public class Test {
public static void main(String[] args) {
// 声明变量hashSet,引用一个HashSet实例
Set<Integer> hashSet = new HashSet<Integer>(5);
hashSet.add(7);
hashSet.add(25);
hashSet.add(199);
hashSet.add(78);
hashSet.add(957);
hashSet.add(3);
// 遍历hashSet集合中的元素
for (Integer num : hashSet) {
System.out.print(num + " ");
}
System.out.println();
// 声明变量treeSet,引用一个TreeSet实例
Set<Integer> treeSet = new TreeSet<Integer>();
treeSet.add(7);
treeSet.add(25);
treeSet.add(199);
treeSet.add(78);
treeSet.add(957);
treeSet.add(3);
// 遍历treeSet集合中的元素
for (Integer num : treeSet) {
System.out.print(num + " ");
}
}
}
执行输出结果:
25 3 957 78 7 199
3 7 25 78 199 957
说明:
本例比较了java.util.TreeSet
类的实例和java.util.HashSet
类的实例在存储完全相同的若干个java.lang.Integer
实例时会有什么不同,观察输出结果可以看到,集合treeSet
中存储的java.lang.Integer
对象已经按照其所包装的整型数值的大小进行了排序。