13、集合(一)
一、初识集合
1、集合的概念
计算机的优势在于处理大量的数据,在编程开发中,数组是可以存储、处理大量的数据,必须具备相应的存储结构,但是数组长度固定、操作不方便。为了弥补这一缺陷,便衍生了集合这一概念。集合是一种存储数据个数不受限制、操作方式更灵活的数据存储结构。集合内部持有若干其他对象的引用,并对外提供访问接口。对开发人员来说,集合就像一个容器,内部存放了若干对象,这些集合中的对象也被称为元素。
2、编程实现简易集合
通过编程实现一个简易的集合:
Mylist
接口的源码:
/**
* 简易的List接口
*/
public interface MyList {
/**
* 向集合中添加元素
* @param o object
* @return 添加成功则返回true
*/
boolean add(Object o);
/**
* 获取指定下标处的元素
* @param index 下标
* @return 指定下标处的元素
*/
Object get(int index);
/**
* 移除并返回集合中指定下标处的元素
* @param index 下标
* @return 指定下标处的元素
*/
Object remove(int index);
/**
* 获取集合中元素的个数
* @return 集合中元素的个数
*/
int size();
}
MyArrayList
类的源码:
import java.util.Arrays;
/**
* 简易的ArrayList类
*/
public class MyArrayList implements MyList {
private Object[] elementData = {}; // 用来存储元素的数组
private int size = 0; // 容器大小
/**
* 向集合中添加元素
* @param o object
* @return 添加成功则返回true
*/
@Override
public boolean add(Object o) {
int capacity = size + 1;
if (capacity - elementData.length > 0){
elementData = Arrays.copyOf(elementData, capacity);
}
elementData[size++] = o;
return true;
}
/**
* 获取指定下标处的元素
* @param index 下标
* @return 指定下标处的元素
*/
@Override
public Object get(int index) {
return elementData[index];
}
/**
* 移除并返回集合中指定下标处的元素
* @param index 下标
* @return 指定下标处的元素
*/
@Override
public Object remove(int index) {
Object oldValue = elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null;
return oldValue;
}
/**
* 获取集合中元素的个数
* @return 集合中元素的个数
*/
@Override
public int size() {
return size;
}
}
测试类Test
的源码:
/**
1. 测试类
*/
public class Test {
public static void main(String[] args) {
// 实例化一个自定义实现的集合对象
MyList myList = new MyArrayList();
// 添加元素
myList.add(1);
myList.add(2);
myList.add(3);
myList.add(4);
// 打印集合的尺寸
System.out.println("myList.size() = " + myList.size());
// 移除下标为2的元素
Integer num = (Integer) myList.remove(2);
// 打印刚才移除的元素
System.out.println("num = " + num);
// 再次打印集合的尺寸
System.out.println("myList.size() = " + myList.size());
// 遍历集合中的元素并打印
for(int i = 0; i < myList.size(); i ++){
Integer n = (Integer) myList.get(i);
System.out.println(n);
}
}
}
说明:
- 本例通过编程实现了一个简易版的集合,包含
MyList
接口和MyArrayList
类,基本实现了数据的存储、获取、删除,判断集合大小等功能。但该简易版的集合只是实现了最基本、最简单的功能,没有考虑在内存开销、计算性能等方面进行优化。 - 在本例测试类中的代码“
在这里插入代码片
,实际上进行了自动装箱,虚拟机执行的代码为myList.add(Integer.valueOf(1))
,在之前章节中介绍包装类时,提到包装类的作用之一便是作为和基本数据类型对应的引用类型存在,方便涉及到对象的操作,这里便是一个典型的场景。 - 在本例简易版集合的实现过程中,使用了
java.lang.System
类arraycopy(Object src
,int srcPos
,Object dest
,int destPos
,int length)
方法,及java.util.Arrays
类中的copyOf(T[] original, int newLength)
方法。java.lang.System
类中的arraycopy(Object src, int srcPos,Object dest, int destPos, int length)
方法可以将固定数量的数组元素从原数组中的指定位置拷贝到目标数组的指定位置;java.util.Arrays
类中的copyOf(T[]original, int newLength)
方法可以将原数组中的元素拷贝到一个指定长度的新数组中,事实上,该方法底层仍然是在调用java.lang.System
类中的arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
方法,另外,该方法涉及到泛型的知识,关于泛型及java.util.Array
类等内容,将在后文中介绍。
3、简单了解泛型
虽然简易版集合不再有元素个数的限制,但是该集合中存储的数据类型都是Object
,在使用时不可避免的需要进行强制类型转换,这在使用时仍不方便,而且存在发生类型转换异常的风险。。在Java中,使用泛型这一特性便可以避免这一问题。更多关于泛型
3.1、泛型的概念及其作用
泛型,即参数化类型,泛型是一种代码模板技术,它允许开发人员在编写代码时将类,接口或方法中要操作的类型参数化,到使用时,再指明要操作的具体类型。
使用泛型的优点大概有以下几点:
- 可以统一数据类型,便于操作。
- 避免了强制类型转换,降低出现类型转换异常的风险。
- 实现代码模板化,把数据类型当作参数传递,提高了代码的
重用性[^footnote]。
3.2、编写泛型代码
首先,开发人员可以在编写方法时,应用泛型。
语法格式入下:
[修饰符] <T1[, T2, ..., Tn]> 返回值类型 方法名称([参数列表]) {
// 方法体
}
下面是一个示例:
MyArrayUtils
类源码:
public class MyArrayUtils {
/**
* 打印数组元素
* @param array 需要被打印元素的数组
* @param separator 打印时元素间的分割符
* @param <E> 类型参数
*/
public static <E> void printArray(E[] array, String separator)
{
System.out.print("{ ");
for (int i = 0; i < array.length; i++) {
E element = array[i];
System.out.print(element + " " + (i == array.length - 1 ? "" : separator+ " "));
}
System.out.println("}");
}
}
Test
类源码:
public class Test {
public static void main(String[] args) {
// 实例化一些不同类型的数组
Integer[] integerArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] characterArray = { 'H', 'E', 'L', 'L', 'O' };
// 打印数组
MyArrayUtils.printArray(integerArray, ",");
MyArrayUtils.printArray(doubleArray, "|");
MyArrayUtils.printArray(characterArray, "^");
}
}
说明:
- 声明方法时,在方法返回类型之前增加类型参数声明部分(由尖括号分隔,如本例中的),这样的方法便成为一个泛型方法。表示类型参数的标识符一般使用一个大写的英文字母。
- 类型参数声明部分,可以包含一个或多个类型参数 ,中间使用,隔开。
- 类型参数能作为泛型方法得到的形参数据类型的占位符,在方法被调用时,传入的实际参数类型决定了类型参数代表的具体类型,如果形参数据类型中没有使用方法的某类型参数,那么,该类型参数代表的具体类型为
java.lang.Object
。 - 除了被用作方法形参的数据类型外,类型参数也可以被用来声明返回值类型,也可以被用来声明方法中局部变量的类型等。
- 注意类型参数只能代表引用类型,不能代表基本数据类型。类型参数不能和new关键字一起用来构造对象,即
new T()
是不合法的。 - 注意泛型方法要防止重复定义方法,例如:
public <E> boolean equals(E obj)
方法,因为可能和基类或接口中的方法声明冲突。
另外,开发人员也可以针对类或接口应用泛型。语法格式如下:
class 类名<T1[, T2, ..., Tn]> {
// 成员变量声明部分,即属性
// 成员方法声明部分,即行为
}
下面是一个示例:
MyArrayUtils
类源码:
public class MyArrayUtils<T> {
/**
* 打印数组元素
* @param array 需要被打印元素的数组
* @param separator 打印时元素间的分割符
*/
public void invertPrintArray(T[] array, String separator) {
System.out.print("{ ");
for (int i = array.length - 1; i >= 0; i--) {
T element = array[i];
System.out.print(element + " " + (i == 0 ? "" : separator + " "));
}
System.out.println("}");
}
}
Test
类源码:
public class Test {
public static void main(String[] args) {
// 实例化一些不同类型的数组
Integer[] integerArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] characterArray = { 'H', 'E', 'L', 'L', 'O' };
// 打印数组
MyArrayUtils<Integer> myArrayUtils1 = new MyArrayUtils<>();
myArrayUtils1.invertPrintArray(integerArray, ",");
MyArrayUtils<Double> myArrayUtils2 = new MyArrayUtils<>();
myArrayUtils2.invertPrintArray(doubleArray, "|");
MyArrayUtils<Character> myArrayUtils3 = new MyArrayUtils<>();
myArrayUtils3.invertPrintArray(characterArray, "^");
}
}
说明:
- 声明类时,在类名后增加类型参数声明部分(由尖括号分隔,如本例中的),这样的类便成为一个泛型类。
和泛型方法一样,泛型类的类型参数声明部分,可以包含一个或多个类型参数 ,中间使用,隔开。- == 泛型类==在使用时,一般需要指定类型参数代表的 具体类型;如果不指定,类型参数代表的 类型即是
java.lang.Object
。 - 泛型类的类型参数可以被用来声明实例成员变量的类型,可以被用来声明实例成员方法的参数类型、返回值类型,可以被用来声明局部变量的类型,等等。
- 泛型类中的类型参数无法应用到类中的静态成员上。请注意本例中的方法和上例中的泛型方法之间的区别。
- 和泛型方法一样,类型参数只能代表引用类型,不能代表基本数据类型;类型参数不能和new关键字一起用来构造对象;泛型类中也要注意防止重复定义方法。
- 泛型接口和泛型类基本相同,即在声明接口时,在接口名后增加类型参数声明部分,形成泛型接口,这里不再赘述。
4 、在简易集合中应用泛型
在前文中编程实现的简易集合里应用泛型的知识。
下面是一个实例:
MyList
接口修改后的源码:
**
* 简易的List接口
*/
public interface MyList<T> {
/**
* 向集合中添加元素
* @param t 要添加的元素
* @return 添加成功则返回true
*/
boolean add(T t);
/**
* 获取指定下标处的元素
* @param index 下标
* @return 指定下标处的元素
*/
T get(int index);
/**
* 移除并返回集合中指定下标处的元素
* @param index 下标
* @return 指定下标处的元素
*/
T remove(int index);
/**
* 获取集合中元素的个数
* @return 集合中元素的个数
*/
int size();
}
MyArrayList
类修改后的源码:
import java.util.Arrays;
/**
* 简易的ArrayList类
*/
public class MyArrayList<E> implements MyList<E> {
private Object[] elementData = {}; // 用来存储元素的数组
private int size = 0; // 容器大小
/**
* 向集合中添加元素
* @param e 要添加的元素
* @return 添加成功则返回true
*/
@Override
public boolean add(E e) {
int capacity = size + 1;
if (capacity - elementData.length > 0){
elementData = Arrays.copyOf(elementData, capacity);
}
elementData[size++] = e;
return true;
}
/**
* 获取指定下标处的元素
* @param index 下标
* @return 指定下标处的元素
*/
@Override
public E get(int index) {
return (E) elementData[index];
}
/**
* 移除并返回集合中指定下标处的元素
* @param index 下标
* @return 指定下标处的元素
*/
@Override
public E remove(int index) {
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null;
return oldValue;
}
/**
* 获取集合中元素的个数
* @return 集合中元素的个数
*/
@Override
public int size() {
return size;
}
}
测试类Test
类修改后的源码:
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
// 实例化一个自定义实现的集合对象
MyList<Integer> myList = new MyArrayList<Integer>();
// MyList<Integer> myList = new MyArrayList<>(); 上一句代码可以简写成本行的形式
// 添加元素
myList.add(1);
myList.add(2);
myList.add(3);
myList.add(4);
// myList.add(5.0); 这句代码会报错,myList中只能添加Integer类型的对象
// 打印集合的尺寸
System.out.println("myList.size() = " + myList.size());
// 移除下标为2的元素
Integer num = myList.remove(2);
// 打印刚才移除的元素
System.out.println("num = " + num);
// 再次打印集合的尺寸
System.out.println("myList.size() = " + myList.size());
// 遍历集合中的元素并打印
for(int i = 0; i < myList.size(); i ++){
Integer n = myList.get(i);
System.out.println(n);
}
}
}
说明:
- 本例在简易集合中应用了泛型,在本例的测试类中,代码
MyList<Integer> myList = new MyArrayList<Integer>()
指定了类型参数代表的具体类型是Integer
,于是,变量myList
所引用的简易集合对象中便只能保存Integer类型的对象,从该集合对象中取出的元素自然也是Integer
类型的,不用再进行强制类型转换。
二、集合框架之Collection
JDK中在Java.until
包下提供了一组类库,用以实现集合这一数据存储结构,作为容器,用以存储、处理大量对象。这些jdk提供的集合主要围绕 着Collection
接口和Map
接口实现,由此也分为两类,本节主要介绍Collection
接口实现的集合。
1、Collection接口相关继承关系
Collcetion
接口下,主要由List
接口,Set
接口对其进行了继承,List
接口主要由ArrayList
类、Vector
类、LinkedList
类等对其进行实现,而Set
接口主要由HashSet
类、TreeSet
类等对其进行实现,它们都在java.util
包下,它们之间简单的继承关系图可以概括如下:
说明:
java.util.Collection
接口是一个泛型接口,主要规范了add(E e)
、contains(Object0)
、toArray()
、remove(Object o)
、size()
、clear()
等方法。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)
等方法。java.util.Set
接口是一个泛型接口,继承了java.util.Collection
接口,规范了无序(这里的无序指元素不具有下标,无法通过下标索引来访问元素)集合的行为,该接口覆盖了java.util.Collection
接口的部分方法。java.util.ArrayList
类、java.util.Vector
类、java.util.LinkedList
类均分别实现了java.util.List
接口;java.util.HashSet
类、java.util.TreeSet
类均分别实现了java.util.Set
接口。
2、List接口
实现了java.util.List
接口的集合也被称为列表,它们和数组比较相似,使用这类集合存储的对象有序,可以重复。常见的实现类如 java.util.ArrayList
类、java.util.Vector
类、java.util.LinkedList
类等,它们的功能与用法几乎完全相同,只是内部实现不同。java.util.List
接口中的一些常用API如下:
方法 | 返回值类型 | 方法说明 |
---|---|---|
add(E e) | boolean | 将指定的元素追加到此列表的末尾(可选操作) |
add(int index, E element) | void | 将指定的元素插入此列表中的指定位置 |
clear() | void | 从此列表中删除所有元素 |
contains(Object o) | boolean | 如果此列表包含指定的元素,则返回true ,否则返回false |
get(int index) | E | 返回此列表中指定位置的元素 |
isEmpty() | boolean | 如果此列表不包含元素,则返回true ,否则返回false |
indexOf(Object o) | int | 返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1 |
lastIndexOf(Object o) | int | 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1 |
remove(int index) | E | 删除该列表中指定位置的元素,同时返回该元素 |
remove(Object o) | boolean | 从列表中删除指定元素的第一个出现(如果存在) |
size() | int | 返回此列表中的元素数 |
toArray() | Object[] | 返回一个包含此列表中所有元素的数组 |
2.1ArrayList类
java.util.ArrayList
类借助数组实现java.util.List
接口,该类是一个泛型类。由于java.util.ArrayList
类基于数组,在内存中分配的空间连续,因此访问元素速度相对较快,添加和删除元素速度相对较慢。
下面是一个示例:
Student
类的源码:
public class Student {
private String name; // 姓名
// 构造方法
public Student(String name) {
this.name = name;
}
// 学习的方法
public void study(){
System.out.println("student" + this.name + "在学习");
}
}
Teacher
类的源码:
public class Teacher {
private String name; // 姓名
// 构造方法
public Teacher(String name) {
this.name = name;
}
// 授课的方法
public void teach(){
System.out.println("teacher" + this.name + "在授课");
}
}
测试类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() + "个元素");
}
}
说明:
- 本例的测试类
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.2、Vector类
java.util.Vector
类同java.util.ArrayList
类非常相似,同样借助数组实现 java.util.List
接口,该类也是一个泛型类;与java.util.ArrayList
类最核心的不同是,java.util.Vector
类是线程同步(Thread Synchronized)的,所以它也是线程安全的,而java.util.ArrayList
类是线程异步(Thread ASynchronized)的,在多线程的场景下不能保证线程安全。
下面是一个示例:
Person
类的源码同上例。
测试类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() +"个元素");
}
}
说明:
java.util.Vector
类的使用与java.util.ArrayList
类使用方法几乎完全相同。
2.3、LikedList类
java.util.LinkedList
类借助链表实现 java.util.List
接口,该类是一个泛型类。该类的使用方法和java.util.ArrayList
类基本相同,而与java.util.ArrayList
类不同的是,java.util.LinkedList
类基于链表,在内存中分配的空间不必连续,因此添加和删除元素速度相对较快,而访问元素速度相对较慢。
下面是一个示例:
Person
类的源码同上例。
测试类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() +"个元素");
}
}
说明:
java.util.LinkedList
类的是使用和java.util.Vector
类、java.util.ArrayList
类几乎完全相同,这里不再赘述。
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[] | 返回一个包含此集合中所有元素的数组 |
3.1、HashSet类
java.util.HashSet
类借助散列算法实现 java.util.Set
接口,该类是一个泛型类。该类的对象可以储存无序、唯一的对象,由于储存的元素是无序的,故无法基于下标进行操作。
下面是一个示例:
Person
类的源码同上例。
测试类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() +"个元素");
}
}
说明:
- 本例使用
java.util.Set
接口声明变量set
,实例化java.util.HashSet
类对象并赋值给变量set
。由于set
集合中的元素不可重复,故本例中两次执行语句set.add(p3)
,集合中只会保存一个对象p3
的引用。又由于set
集合是无序的,故无法使用普通的for循环通过下标遍历集合中的元素,不过set
集合仍然可以使用For-Each
循环(加强型循环)遍历。
下面是另一个示例:
Person
类的源码同上例。
测试类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());
}
}
说明:
- 前文提到,
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
类修改后的源码如下:
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();
}
}
3.2、TreeSet类
java.util.TreeSet
类借助二叉树实现java.util.Set
接口,该类也是一个泛型类。该类的使用方法与java.util.HashSet
类几乎完全相同,不同的是,java.util.TreeSet
集合在保存元素时会对元素进行比较并按照自然的方式对元素进行排序(注意这里的排序和前文中“有序”二字的区别,有序指的是元素按照进入集合的顺序具有下标,而这里的排序值的是按照元素本身的值比较之后进行排序,和元素的下标没有关系),java.util.TreeSet
集合中的元素仍然无法通过下标访问。
三、迭代器 Iterator
1、迭代器的概念
在编写代码的过程中,常常需要将数组或集合中的元素逐个访问一遍,即遍历,这个过程也被称作迭代,迭代器即是在这一过程中使用的一种工具。
2、Iterator接口
对于集合的迭代,除了前文中使用的for
循环或For-Each
循环(加强型循环)外,Java还提供了java.util.Iterator
接口作为迭代器,同样可以完成遍历集合的任务。
java.util.Iterator
接口是一个泛型接口,该接口中一些常用API如下:
方法 | 返回值类型 | 方法说明 |
---|---|---|
hasNext() | boolean | 如果具有更多元素,则返回true |
next() | E | 返回下一个元素 |
remove() | void | 从底层集合中删除此迭代器返回的最后一个元素 |
在常见的集合类中,大多都使用了内部类的方式实现了java.util.Iterator
接口,并对外提供了获得该内部类实例的方法iterator()
。
四、比较器 Comparable和Comparator
1、比较器的概念
比较器,顾名思义,是在比较的时候需要用到的一种工具。其最主要的作用是用来实现对象的比较逻辑,解决对象在需要排序的情况下孰先孰后这一问题。
2、Comparable
接口
之前的内容中介绍了java.util.TreeSet
类,了解到java.util.TreeSet
集合在保存元素时会对元素进行比较并按照自然的方式对元素进行排序,并且演示了在一个java.util.TreeSet
集合中存储多个java.lang.Integer
类的对象后元素是怎样排序的,如果在java.util.TreeSet
集合中存储多个自定义引用类型的对象,会怎样排序呢?
public class Student {
private String name; // 姓名
private Integer mathScore; // 数学成绩
private Integer ChineseScore; // 语文成绩
// 构造方法
public Student(String name, Integer mathScore, Integer chineseScore) {
this.name = name;
this.mathScore = mathScore;
ChineseScore = chineseScore;
}
// 学习的方法
public void study(){
System.out.println("student" + this.name + "在学习,数学成绩是"
+ this.mathScore + ",语文成绩是" + this.ChineseScore);
}
}
测试类:
import java.util.Set;
import java.util.TreeSet;
public class Test {
public static void main(String[] args) {
// 实例化几个Student对象
Student stu1 = new Student("鲁智深", 70, 75);
Student stu2 = new Student("武松", 73, 80);
Student stu3 = new Student("林冲", 76, 87);
Student stu4 = new Student("吴用", 95, 81);
Student stu5 = new Student("公孙胜", 90, 85);
// 声明变量treeSet,引用一个TreeSet实例
Set<Student> treeSet = new TreeSet<Student>();
treeSet.add(stu1);
treeSet.add(stu2);
treeSet.add(stu3);
treeSet.add(stu4);
treeSet.add(stu5);
// 遍历treeSet集合中的元素
for (Student stu : treeSet) {
stu.study();
}
}
}
3、Comparator
接口
对于java.lang.Comparable
接口,一个类只能实现一次。但如果对某一类的若干实例需要使用不同的比较方式进行比较,比如上例中的若干个Student
类对象,数学老师希望看到按数学成绩排序的结果,语文老师则希望看到按语文成绩排序的结果,此时,还需要用到实现了java.util.Comparator
接口的比较器。
java.util.Comparator
接口是一个泛型接口,其中声明compare(T o1, T o2)
方法,该方法与java.lang.Comparable
接口中的compareTo(T o)
方法非常相似,方法的返回值类型也是int
,比较逻辑也是相同的,不同的是,参与比较的两个对象均是通过方法参数传入的。
Student
类:
public class Student implements Comparable<Student>{
private String name; // 姓名
private Integer mathScore; // 数学成绩
private Integer ChineseScore; // 语文成绩
// 构造方法
public Student(String name, Integer mathScore, Integer chineseScore) {
this.name = name;
this.mathScore = mathScore;
ChineseScore = chineseScore;
}
// 学习的方法
public void study(){
System.out.println("student" + this.name + "在学习,数学成绩是"
+ this.mathScore + ",语文成绩是" + this.ChineseScore);
}
// 重写Comparable接口中的compareTo(T o)方法
@Override
public int compareTo(Student o) {
return name.compareTo(o.name);
}
// 获取数学成绩
public Integer getMathScore() {
return mathScore;
}
// 获取语文成绩
public Integer getChineseScore() {
return ChineseScore;
}
}
测试类:
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class Test {
public static void main(String[] args) {
// 实例化几个Student对象
Student stu1 = new Student("鲁智深", 70, 75);
Student stu2 = new Student("武松", 73, 80);
Student stu3 = new Student("林冲", 76, 87);
Student stu4 = new Student("吴用", 95, 81);
Student stu5 = new Student("公孙胜", 90, 85);
// 声明变量mathScoreTreeSet,引用一个TreeSet实例,构造方法传入一个实现了Comparator接口的比较器
Set<Student> mathScoreTreeSet = new TreeSet<Student>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
// 按数学成绩进行比较
return s1.getMathScore().compareTo(s2.getMathScore());
}
});
mathScoreTreeSet.add(stu1);
mathScoreTreeSet.add(stu2);
mathScoreTreeSet.add(stu3);
mathScoreTreeSet.add(stu4);
mathScoreTreeSet.add(stu5);
// 遍历treeSet集合中的元素
System.out.println("按数学成绩排序:");
for (Student stu : mathScoreTreeSet) {
stu.study();
}
// 声明变量chineseScoreTreeSet,引用一个TreeSet实例,构造方法传入一个实现了Comparator接口的比较器
Set<Student> chineseScoreTreeSet = new TreeSet<Student>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
// 按语文成绩进行比较
return s1.getChineseScore().compareTo(s2.getChineseScore());
}
});
chineseScoreTreeSet.add(stu1);
chineseScoreTreeSet.add(stu2);
chineseScoreTreeSet.add(stu3);
chineseScoreTreeSet.add(stu4);
chineseScoreTreeSet.add(stu5);
// 遍历treeSet集合中的元素
System.out.println("按语文成绩排序:");
for (Student stu : chineseScoreTreeSet) {
stu.study();
}
}
}