本文是Java基础课程的第十三课。Java中的集合分两类,分别是Collection和Map,本文主要介绍Java中集合的概念,java.util.Collection接口下常见的集合实现类及使用,最后还会涉及到Java中的泛型、迭代器、比较器等内容
计算机的优势在于处理大量的数据,在编程开发中,为存储、处理大量的数据,必须具备相应的存储结构,之前章节中学习的数组可以完成这一任务,但是数组长度固定、操作不方便。
在实际开发过程中,为了使大量数据的存储和处理更加方便,便衍生了集合这一概念。集合是一种存储数据个数不受限制、操作方式更灵活的数据存储结构。集合内部持有若干其他对象的引用,并对外提供访问接口。对开发人员来说,集合就像一个容器,内部存放了若干对象,这些集合中的对象也被称作元素。
下面运用之前章节中的知识,通过编程实现一个简易的集合。 下面是一个示例: MyList接口的源码:
package com.codeke.java.test; /** * 简易的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类的源码:
package com.codeke.java.test; 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类的源码:
package com.codeke.java.test; /** * 测试类 */ 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(1),实际上进行了自动装箱,虚拟机执行的代码为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类等内容,将在后文中介绍。观察前面小节中简易版集合的案例,虽然该简易版集合不再有元素个数的限制,但是该简易集合中存储的元素类型都是Object,在使用时不可避免的需要进行强制类型转换,这在使用时仍然不方便,而且存在发生类型转换异常的风险。在Java中,使用泛型这一特性便可以避免这些问题,这里先来简单了解一下泛型。
泛型,即参数化类型,泛型是一种代码模板技术,它允许开发人员在编写代码时将类、接口或方法中要操作的类型参数化,到使用时再指明要操作的具体类型。
使用泛型带来的作用大致有以下几点:
可以统一数据类型,便于操作;避免了强制类型转换,降低出现类型转换异常的风险;实现代码的模板化,把数据类型当作参数传递,提高了代码重用性。在编写代码的过程中使用泛型稍显抽象但仍然比较容易,这里介绍一些最基本的泛型的使用方式,关于泛型更复杂的内容这里暂不涉及。
首先,开发人员可以在编写一个方法时应用泛型。语法格式如下:
[修饰符] <T1[, T2, ..., Tn]> 返回值类型 方法名称([参数列表]) { // 方法体 }下面是一个示例: MyArrayUtils类源码:
package com.codeke.java.test; 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类源码:
package com.codeke.java.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, "^"); } }说明:
声明方法时,在方法返回类型之前增加类型参数声明部分(由尖括号分隔,如本例中的<E>),这样的方法便成为一个泛型方法。表示类型参数的标识符一般使用一个大写的英文字母。类型参数声明部分,可以包含一个或多个类型参数 ,中间使用,隔开。类型参数能作为泛型方法得到的形参数据类型的占位符,在方法被调用时,传入的实际参数类型决定了类型参数代表的具体类型,如果形参数据类型中没有使用方法的某类型参数,那么,该类型参数代表的具体类型为java.lang.Object。除了被用作方法形参的数据类型外,类型参数也可以被用来声明返回值类型,也可以被用来声明方法中局部变量的类型等。注意类型参数只能代表引用类型,不能代表基本数据类型。类型参数不能和new关键字一起用来构造对象,即new T()是不合法的。注意泛型方法要防止重复定义方法,例如:public <E> boolean equals(E obj)方法,因为可能和基类或接口中的方法声明冲突。另外,开发人员也可以针对类或接口应用泛型。语法格式如下:
class 类名<T1[, T2, ..., Tn]> { // 成员变量声明部分,即属性 // 成员方法声明部分,即行为 }下面是一个示例: MyArrayUtils类源码:
package com.codeke.java.test; 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类源码:
package com.codeke.java.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, "^"); } }说明:
声明类时,在类名后增加类型参数声明部分(由尖括号分隔,如本例中的<T>),这样的类便成为一个泛型类。和泛型方法一样,泛型类的类型参数声明部分,可以包含一个或多个类型参数 ,中间使用,隔开。泛型类在使用时,一般需要指定类型参数代表的具体类型;如果不指定,类型参数代表的类型即是java.lang.Object。泛型类的类型参数可以被用来声明实例成员变量的类型,可以被用来声明实例成员方法的参数类型、返回值类型,可以被用来声明局部变量的类型,等等。泛型类中的类型参数无法应用到类中的静态成员上。请注意本例中的方法和上例中的泛型方法之间的区别。和泛型方法一样,类型参数只能代表引用类型,不能代表基本数据类型;类型参数不能和new关键字一起用来构造对象;泛型类中也要注意防止重复定义方法。泛型接口和泛型类基本相同,即在声明接口时,在接口名后增加类型参数声明部分,形成泛型接口,这里不再赘述。下面,在前文中编程实现的简易集合里应用泛型的知识。 下面是一个示例: MyList接口修改后的源码:
package com.codeke.java.test; /** * 简易的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类修改后的源码:
package com.codeke.java.test; 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类修改后的源码:
package com.codeke.java.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类型的,不用再进行强制类型转换。JDK中在java.util包下提供了一组类库,用以实现集合这一数据存储结构,作为容器,用以存储、处理大量对象。这些由JDK提供的集合主要围绕着Collection接口和Map接口实现,由此也分为两类,本节主要介绍由Collection接口实现的集合。
Collection接口下,主要由List接口、Set接口对其进行了继承,List接口主要由ArrayList类、Vector类、LinkedList类等对其进行实现,而Set接口主要由HashSet类、TreeSet类等对其进行实现,它们都在java.util包下,它们之间简单的继承关系图可以概括如下: 说明:
java.util.Collection接口是一个泛型接口,主要规范了add(E e)、contains(Object o)、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接口。开发人员在学习、使用这些集合相关的类库时,需要熟悉这些类库中提供的常用API,进而掌握如何向集合中添加元素、如何从集合中获取某一个元素,如何获取集合中所有的元素,如何将一个元素从集合中移除,如何判断集合中是否包含了某个元素等常用操作。
实现了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,否则返回falseget(int index)E返回此列表中指定位置的元素isEmpty()boolean如果此列表不包含元素,则返回true,否则返回falseindexOf(Object o)int返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1lastIndexOf(Object o)int返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1remove(int index)E删除该列表中指定位置的元素,同时返回该元素remove(Object o)boolean从列表中删除指定元素的第一个出现(如果存在)size()int返回此列表中的元素数toArray()Object[]返回一个包含此列表中所有元素的数组java.util.ArrayList类借助数组实现java.util.List接口,该类是一个泛型类。由于java.util.ArrayList类基于数组,在内存中分配的空间连续,因此访问元素速度相对较快,添加和删除元素速度相对较慢。
下面是一个示例: Student类的源码:
package com.codeke.java.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.codeke.java.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.codeke.java.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,如果要赋值给其他类型的变量,需要强制类型转换。下面是另一个示例: 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 + "在工作"); } }测试类Test类的源码:
package com.codeke.java.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类型,不需要强制类型转换。java.util.Vector类同java.util.ArrayList类非常相似,同样借助数组实现java.util.List接口,该类也是一个泛型类;与java.util.ArrayList类最核心的不同是,java.util.Vector类是线程同步(Thread Synchronized)的,所以它也是线程安全的,而java.util.ArrayList类是线程异步(Thread ASynchronized)的,在多线程的场景下不能保证线程安全。
下面是一个示例: Person类的源码同上例。 测试类Test类的源码:
package com.codeke.java.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类使用方法几乎完全相同,这里不再赘述。java.util.LinkedList类借助链表实现java.util.List接口,该类是一个泛型类。该类的使用方法和java.util.ArrayList类基本相同,而与java.util.ArrayList类不同的是,java.util.LinkedList类基于链表,在内存中分配的空间不必连续,因此添加和删除元素速度相对较快,而访问元素速度相对较慢。
下面是一个示例: Person类的源码同上例。 测试类Test类的源码:
package com.codeke.java.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类几乎完全相同,这里不再赘述。实现了java.util.Set接口的集合,存储的对象无序且不可重复。常见的实现类如java.util.HashSet类、java.util.TreeSet类等,它们的功能与用法几乎完全相同,只是内部实现不同。java.util.Set接口中的一些常用API如下:
方法返回值类型方法说明add(E e)boolean如果指定的元素不存在,则添加该元素clear()void从此集合中删除所有元素contains(Object o)boolean如果此集合包含指定的元素,则返回true,否则返回falseisEmpty()boolean如果此集合不包含元素,则返回true,否则返回falseremove(Object o)boolean如果存在,则从该集合中删除指定的元素size()int返回此集合中的元素数toArray()Object[]返回一个包含此集合中所有元素的数组java.util.HashSet类借助散列算法实现java.util.Set接口,该类是一个泛型类。该类的对象可以储存无序、唯一的对象,由于储存的元素是无序的,故无法基于下标进行操作。
下面是一个示例: 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集合,指定类型参数为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重复了。java.util.TreeSet类借助二叉树实现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对象已经按照其所包装的整型数值的大小进行了排序。在编写代码的过程中,常常需要将数组或集合中的元素逐个访问一遍,即遍历,这个过程也被称作迭代,迭代器即是在这一过程中使用的一种工具。
对于集合的迭代,除了前文中使用的for循环或For-Each循环(加强型循环)外,Java还提供了java.util.Iterator接口作为迭代器,同样可以完成遍历集合的任务。 java.util.Iterator接口是一个泛型接口,该接口中一些常用API如下:
方法返回值类型方法说明hasNext()boolean如果具有更多元素,则返回truenext()E返回下一个元素remove()void从底层集合中删除此迭代器返回的最后一个元素在常见的集合类中,大多都使用了内部类的方式实现了java.util.Iterator接口,并对外提供了获得该内部类实例的方法iterator()。
下面是一个示例: 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 + "在工作"); } // name属性的属性访问器 public String getName() { return name; } }测试类Test类的源码:
package com.codeke.java.test; import java.util.*; public class Test { public static void main(String[] args) { // 实例化几个Person对象 Person p1 = new Person("鲁智深"); Person p2 = new Person("武松"); Person p3 = new Person("林冲"); Person p4 = new Person("吴用"); Person p5 = new Person("公孙胜"); // 声明并实例化ArrayList集合 List<Person> list = new ArrayList<Person>(); // 向集合中添加元素 list.add(p1); list.add(p2); list.add(p3); list.add(p4); list.add(p5); // 使用iterator()方法获得迭代器 Iterator<Person> itr1 = list.iterator(); // 使用迭代器和while循环遍历集合 while(itr1.hasNext()){ Person p = itr1.next(); p.work(); } // 声明并实例化HashSet集合 Set<Person> set = new HashSet<Person>(); // 向集合中添加元素 set.add(p1); set.add(p2); set.add(p3); set.add(p4); set.add(p5); // 使用iterator()方法获得迭代器 Iterator<Person> itr2 = set.iterator(); // 使用迭代器和while循环遍历集合 while (itr2.hasNext()){ Person p = itr2.next(); p.work(); } } }说明:
本例中演示了通过集合的iterator()方法获得迭代器,并通过while循环语句配合迭代器的hasNext()方法、next()方法遍历集合。下面是另一个示例: Person类的源码同上例。 测试类Test类的源码:
package com.codeke.java.test; import java.util.*; public class Test { public static void main(String[] args) { // 实例化几个Person对象 Person p1 = new Person("鲁智深"); Person p2 = new Person("武松"); Person p3 = new Person("林冲"); Person p4 = new Person("吴用"); Person p5 = new Person("公孙胜"); // 声明并实例化ArrayList集合 List<Person> list = new ArrayList<Person>(); // 向集合中添加元素 list.add(p1); list.add(p2); list.add(p3); list.add(p4); list.add(p5); // 删除集合中name属性值为“林冲”或“吴用”的Person对象 for (int i = 0; i < list.size(); i++) { if ("林冲".equals(list.get(i).getName()) || "吴用".equals(list.get(i).getName())) { list.remove(i); } } // 遍历集合 for (Person p : list) { p.work(); } // 声明并实例化HashSet集合 Set<Person> set = new HashSet<Person>(); // 向集合中添加元素 set.add(p1); set.add(p2); set.add(p3); set.add(p4); set.add(p5); // 删除集合中name属性值为“林冲”或“吴用”的Person对象 for (Person p : set) { if ("林冲".equals(p.getName()) || "吴用".equals(p.getName())) { set.remove(p); } } // 遍历集合 for (Person p : set) { p.work(); } } }执行输出结果:
person鲁智深在工作 person武松在工作 person吴用在工作 person公孙胜在工作 Exception in thread "main" java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445) at java.util.HashMap$KeyIterator.next(HashMap.java:1469) at com.codeke.java.test9.Test.main(Test.java:43)说明:
本例中的代码试图在循环过程中,将集合中name属性值为“林冲”或“吴用”的Person对象移出集合,但是观察执行输出结果发现,对于有序集合,name属性值为“吴用”的Person对象并没有被移出集合,这是因为将一个元素移出有序集合时,后续元素的下标会随之发生变化(前移),而for循环的循环变量并没有随之发生变化,导致后续元素没有被遍历到;而对本例中的集合set,无法在For-Each循环中移除元素,会抛出ConcurrentModificationException异常,事实上,集合list也无法在For-Each循环中移除元素。当遍历集合,并涉及到集合元素删除操作时,使用迭代器是更好的选择,测试类Test类修改后的源码如下:package com.codeke.java.test; import java.util.*; public class Test { public static void main(String[] args) { // 实例化几个Person对象 Person p1 = new Person("鲁智深"); Person p2 = new Person("武松"); Person p3 = new Person("林冲"); Person p4 = new Person("吴用"); Person p5 = new Person("公孙胜"); // 声明并实例化ArrayList集合 List<Person> list = new ArrayList<Person>(); // 向集合中添加元素 list.add(p1); list.add(p2); list.add(p3); list.add(p4); list.add(p5); // 删除集合中name属性值为“林冲”或“吴用”的Person对象 Iterator<Person> itr1 = list.iterator(); while (itr1.hasNext()){ Person p = itr1.next(); if("林冲".equals(p.getName()) || "吴用".equals(p.getName())){ itr1.remove(); } } // 遍历集合 System.out.println("list集合中的元素:"); for (Person p : list) { p.work(); } // 声明并实例化HashSet集合 Set<Person> set = new HashSet<Person>(); // 向集合中添加元素 set.add(p1); set.add(p2); set.add(p3); set.add(p4); set.add(p5); // 删除集合中name属性值为“林冲”或“吴用”的Person对象 Iterator<Person> itr2 = set.iterator(); while (itr2.hasNext()){ Person p = itr2.next(); if("林冲".equals(p.getName()) || "吴用".equals(p.getName())){ itr2.remove(); } } // 遍历集合 System.out.println("set集合中的元素:"); for (Person p : set) { p.work(); } } } 执行输出结果:list集合中的元素: person鲁智深在工作 person武松在工作 person公孙胜在工作 set集合中的元素: person公孙胜在工作 person武松在工作 person鲁智深在工作比较器,顾名思义,是在比较的时候需要用到的一种工具。其最主要的作用是用来实现对象的比较逻辑,解决对象在需要排序的情况下孰先孰后这一问题。
之前的内容中介绍了java.util.TreeSet类,了解到java.util.TreeSet集合在保存元素时会对元素进行比较并按照自然的方式对元素进行排序,并且演示了在一个java.util.TreeSet集合中存储多个java.lang.Integer类的对象后元素是怎样排序的,如果在java.util.TreeSet集合中存储多个自定义引用类型的对象,会怎样排序呢?
下面是一个示例: Student类的源码:
package com.codeke.java.test; 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); } }测试类Test类的源码:
package com.codeke.java.test; 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(); } } }执行输出结果:
Exception in thread "main" java.lang.ClassCastException: com.codeke.java.test.Student cannot be cast to java.lang.Comparable at java.util.TreeMap.compare(TreeMap.java:1294) at java.util.TreeMap.put(TreeMap.java:538) at java.util.TreeSet.add(TreeSet.java:255) at com.codeke.java.test10.Test.main(Test.java:16)说明:
观察本例,当treeSet集合中添加自定义引用类型的对象时,抛出了ClassCastException异常,异常的信息中提示Student类的对象并不能转换成java.lang.Comparable接口类型。实际上,java.util.TreeSet集合保证元素唯一,并且能够对元素进行排序,均是因为集合中的元素所属的类型实现了java.lang.Comparable接口。下面介绍java.lang.Comparable接口,该接口是一个泛型接口,其中只声明了一个compareTo(T o)方法,方法的返回值类型为int,该方法可以由实现类自定义对象如何比较,当方法返回值大于0时,表示调用方法的对象比方法参数对象大,当方法返回值等于0时,表示调用方法的对象与方法参数对象相等,当方法返回值小于0时,表示调用方法的对象比方法参数对象小。
下面节选了java.lang.Integer类和java.lang.String类中的部分代码,可以看到这两个类均实现了java.lang.Comparable接口。 java.lang.Integer类是如何实现java.lang.Comparable接口的:
public final class Integer implements Comparable<Integer> { public int compareTo(Integer anotherInteger) { return compare(this.value, anotherInteger.value); } public static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); } }java.lang.String类是如何实现java.lang.Comparable接口的:
public final class String implements Comparable<String> { public int compareTo(String anotherString) { int len1 = value.length; int len2 = anotherString.value.length; int lim = Math.min(len1, len2); char v1[] = value; char v2[] = anotherString.value; int k = 0; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; if (c1 != c2) { return c1 - c2; } k++; } return len1 - len2; } }下面是另一个示例: Student类修改后的源码:
package com.codeke.java.test; 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); } }测试类Test类的源码同上例。 执行输出结果:
student公孙胜在学习,数学成绩是90,语文成绩是85 student吴用在学习,数学成绩是95,语文成绩是81 student林冲在学习,数学成绩是76,语文成绩是87 student武松在学习,数学成绩是73,语文成绩是80 student鲁智深在学习,数学成绩是70,语文成绩是75说明:
本例中Student类实现了java.lang.Comparable接口,并重写了compareTo(T o)方法,于是,集合treeSet中可以添加Student类的对象并对集合中的元素进行排序。对于java.lang.Comparable接口,一个类只能实现一次。但如果对某一类的若干实例需要使用不同的比较方式进行比较,比如上例中的若干个Student类对象,数学老师希望看到按数学成绩排序的结果,语文老师则希望看到按语文成绩排序的结果,此时,还需要用到实现了java.util.Comparator接口的比较器。
java.util.Comparator接口是一个泛型接口,其中声明compare(T o1, T o2)方法,该方法与java.lang.Comparable接口中的compareTo(T o)方法非常相似,方法的返回值类型也是int,比较逻辑也是相同的,不同的是,参与比较的两个对象均是通过方法参数传入的。
下面是一个示例: Student类修改后的源码:
package com.codeke.java.test; 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; } }测试类Test类修改后的源码:
package com.codeke.java.test; 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(); } } }说明:
本例中,实例化java.util.treeSet集合时,通过构造方法传入了不同的、实现了java.lang.Comparator接口的比较器,在实现比较器时还用到了匿名内部类。通过比较本例和上例,可以看到两种比较器在使用方式上的不同。需要注意的是,实现了java.util.Comparator接口的比较器优先级较高,当同时存在分别实现了java.lang.Comparable接口和java.util.Comparator接口的比较器时,会优先按照后者实现的比较逻辑进行比较。