《疯狂Java讲义(第4版)》-----第8章【Java集合】(Collection、Iterator、Set)
Java集合概述
Java集合本身是封装的几种常见的数据结构,只要学习过基本的数据结构课程,便可理解清楚底层的实现细节。由于Java提供封装好的集合众多,每个集合暴露的方法众多,一般记住常用的,其他的知道有就行了,用的时候查询官方API即可。
Java集合里存放的只能是对象(严格的说是对象的引用变量)。如果不使用泛型的话,丢进集合内的所有对象都是Object类型的。泛型知识后续会专门一章介绍,本章暂时不用泛型
Collectioiin接口和Iterator接口
Collection接口的演示代码(参考官方API中的Collection书写):
import java.util.Collection;
import java.util.ArrayList;
public class Hello{
public static void main(String[] args){
Collection c = new ArrayList();
System.out.println(c.isEmpty());//true
c.add("Tom");
c.add(666);
System.out.println(c.size());//2
System.out.println(c.contains(666));//true
System.out.println(c.remove("Tom"));//true
System.out.println(c);//[666]
c.clear();
System.out.println(c.isEmpty());//true
}
}
编译提示这个警告,因为没有使用泛型,说白了就是没有指定Collection里面存放的到底是哪种类型的对象。现在集合里放入的有666整数类型,还有Tom字符串类型。
使用Lambda表达式遍历集合
Collection接口继承了Iterable接口,Iterable接口提供了forEach(Consumer<? super T> action) 方法,Consumer是函数式接口,可以用Lambda来遍历集合。
官方文档提供的Iterable接口的forEach方法,参数是接口Consumer
Consumer接口只含有一个抽象方法accept,是函数式接口:
import java.util.Collection;
import java.util.HashSet;
public class Hello{
public static void main(String[] args){
Collection c = new HashSet();
c.add(1);
//System.out.println(c.add(1));//false,不能放重复元素
c.add("Tom");
c.add('c');
c.forEach(obj->System.out.println(obj));
}
}
使用Iterator遍历集合元素
Iterator接口。Iterator对象又成为迭代器,Iterator对象依赖于Collection对象,专门为遍历集合而存在的。Iterator官方文档里提供了下面几个方法:
使用Iterator里的forEachRemaining方法遍历集合。这个方法的参数Consumer,是函数式接口,只有一个抽象方法accept,因此可以用Lambda表达式。
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class Hello{
public static void main(String[] args){
Collection c = new HashSet();
c.add("Jack");
c.add("Tom");
c.add("Marry");
Iterator it = c.iterator();
while(it.hasNext()){
String student = (String)(it.next());
System.out.println(student);
if(student.equals("Marry")){
it.remove();//可以把Marry从集合c中删除
//c.remove("Marry");//这样也可以把Marry从集合中删除,不要这么干,很危险
//c.remove(student);//这样也可以把Marry从集合中删除,不要这么干,很危险
}
student = "test";//student仅仅是一个中间变量,这不会改变集合元素的值
}
//使用Iterator的forEachRemaining方法遍历集合,
//forEachRemaining方法的参数Consumer是函数式接口,可以用Lambda表达式
it = c.iterator();
it.forEachRemaining(obj->System.out.print(obj+" "));
System.out.println();
System.out.println(c);//[Tom, Jack]
}
}
使用增强for循环遍历集合
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class Hello{
public static void main(String[] args){
Collection c = new HashSet();
c.add("Jack");
c.add("Tom");
c.add("Marry");
//使用增强for循环遍历
for(Object obj : c){
String student = (String)obj;
System.out.println(student);
if(student.equals("Tom")){
//c.remove("Tom");//这么做会抛异常java.util.ConcurrentModificationException
}
}
}
}
Java8新增的Predicate集合Lamb操作集合
Collection接口提供了下面的一个方法,里面参数Predicate是一个函数式接口,只有一个抽象方法boolean test(T t),因此可以结合Lambda表达式,批量删除集合中的元素,也可以自定义一些方法,批量处理集合中的元素。
示例代码见该书297~298页
Java8新增了Stream、IntStream、LongStream、DoubleStream等流式API
这几个接口,可以自行搞集合,存元素,对元素处理操作;也可以去操作集合。用法示例代码见该书299~300页。
Set集合
Set集合元素不允许重复。
HashSet
- HashSet不能保证元素的排列顺序
- HashSet元素可以是null
- HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或两个以上线程同时修改了HashSet集合时,必须通过代码来保证同步。
**HashSet集合判断两个元素相等的标准是hashCode()相等并且equals相等。**下面代码重写父类的Object的equals方法或hashCode方法:
import java.util.HashSet;
class A{
public boolean equals(Object obj){
return true;
}
}
class B{
public int hashCode(){
return 1;
}
}
class C{
public boolean equals(Object obj){
return true;
}
public int hashCode(){
return 2;
}
}
public class Hello{
public static void main(String[] args){
HashSet hs = new HashSet();
hs.add(new A());
hs.add(new A());
hs.add(new B());
hs.add(new B());
hs.add(new C());
hs.add(new C());
System.out.println(hs);
}
}
输出:
说明第2个new C()没有添加进去,因为两个C对象equals和hashCode都是相等的。底层实现上,HashSet是根据元素的HashCode值来决定存放的位置的,万一两个元素的equals不等,而HashCode相等,只能在同一个位置用链式存起来,这样效率就下降了。
在重写equals和hashCode方法的时候,要保证两个元素通过equals比较是true的时候,他们的hashCode也是相等的,逆命题亦然。
下面展示一个用对象成员变量计算hashCode并决定equals的返回值的代码,也就是说HashSet传入了一个可变的元素,如果试图修改这个可变元素的参与计算hashCode和equals比较的成员变量的值,可能会导致两个元素的equals相等而hashCode不等,或者hashCode相等而equals不等。**所以不要去修改集合中参与计算hashCode、equals的实例变量。**具体见下面代码:
import java.util.HashSet;
import java.util.Iterator;
class R{
private int cnt;
public R(int cnt){
this.cnt = cnt;
}
public String toString(){
return "R[cnt:"+this.cnt+"]";
}
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == R.class){
R r = (R)obj;
return this.cnt == r.cnt;
}
return false;
}
public int hashCode(){
return this.cnt;
}
public void setCnt(int cnt){
this.cnt = cnt;
}
public int getCnt(){
return this.cnt;
}
}
public class Hello{
public static void main(String[] args){
HashSet hs = new HashSet();
hs.add(new R(1));
hs.add(new R(2));
hs.add(new R(3));
hs.add(new R(4));
System.out.println(hs);
Iterator it = hs.iterator();
R first = (R)it.next();
first.setCnt(2);
System.out.println(hs);
hs.remove(new R(2));
System.out.println(hs);
//注意:现在,第1个数是2,equals用2比较,但刚开始存放他的时候hashCode是1
//由于修改成员变量,导致了equals和hashCode不一致
System.out.println(hs.contains(new R(2)));//false
System.out.println(hs.contains(new R(1)));//false
}
}
LinkedHashSet
LinkedHashSet继承HashSet,与HashSet不同的是,LinkedHashSet底层用链表维护,元素按照插入先后有序排列。由于要维护这个顺序,性能略低于HashSet,但在遍历的时候,LinkedHashSet性能更好。
import java.util.LinkedHashSet;
public class Hello{
public static void main(String[] args){
LinkedHashSet hs = new LinkedHashSet();
hs.add(new R(1));
hs.add(new R(3));
hs.add(new R(6));
hs.add(new R(4));
System.out.println(hs);
}
}
输出(按照插入顺序打印):
TreeSet
不允许插入null,否则报异常:java.lang.NullPointerException
TreeSet实现了SortedSet接口,利用红黑树算法保证元素的有序性。TreeSet支持自然排序和定制排序,默认采用自然排序。下面将详细说明这两种排序方式,在此之前,先写一段代码,认识下TreeSet,还是那句老话,具体用法看官方API。
import java.util.TreeSet;
public class Hello{
public static void main(String[] args){
TreeSet ts = new TreeSet();
ts.add(34);
ts.add(6);
ts.add(4);
ts.add(3);
System.out.println(ts);//[3, 4, 6, 34]
//返回比6小的所有元素组成的集合
System.out.println(ts.headSet(4));//[3]
//返回大于等于4的所有元素组成的集合
System.out.println(ts.tailSet(4));//[4, 6, 34]
System.out.println(ts.first());//第一个元素3
System.out.println(ts.last());//最后一个元素34
}
}
(1)自然排序
当把一个元素插入到TreeSet集合的时候,TreeSet就会调用这个元素的compareTo方法和集合中已经存在的其他元素比较,保证每次插入后,集合的元素总是有序的。插入到TreeSet集合的元素必须实现Comparable接口实现compareTo方法,TreeSet判断两个对象是否相等的唯一标准是两个对象通过compareTo方法比较返回值是否是0。Comparable接口只有一个方法并且是抽象方法compareTo,所以定制排序的时候可以借助Lambda表达式。可比较大小的常见对象已经实现了compareTo方法,在实现过程中保证了参与比较的两个对象是同类型的,即同一个类的实例。如果非要往TreeSet中添加自定义的不同类对象,可以让这些类都去实现Comparable接口,且compareTo方法没有强制类型转换,但是当从TreeSet取对象的时候就会报错ClassCastException,这是因为TreeSet要按照顺序取,但无法排序啊!所以,TreeSet中加入的对象必须是同类型的。
【代码实例】
import java.util.TreeSet;
class A implements Comparable{
private int x;
public A(int x){
this.x = x;
}
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == A.class){
A a = (A)obj;
return this.x == a.x;
}
return false;
}
public int compareTo(Object obj){
return 1;//认为设定永远比obj大,每次都能插入TreeSet
}
public void setX(int x){
this.x = x;
}
public int getX(){
return this.x;
}
}
public class Hello{
public static void main(String[] args){
TreeSet ts = new TreeSet();
A a = new A(99);
ts.add(a);
System.out.println(ts.add(a));//true
((A)(ts.first())).setX(23);
System.out.println(((A)(ts.last())).getX());
System.out.println(ts.size());//2
}
}
上面代码中,由于人为重写的compareTo,导致每次插入的对象即使和之前的值一样,也会被判成不一样的,可以插入。虽然TreeSet保存两个元素,实质上他们指向同一个对象。内存情况:
从上面的例子,我们可以给出这样的建议:在自定义compareTo的时候,要保证和equals有一致的判定结果。
在学习HashSet的时候,演示了插入可变对象,并试图修改用于计算equals和hashCode的成员变量的值,导致了equals和hashCode判断不一致的情况。类似地,对TreeSet来说,插入可变对象,并试图修改用于计算compareTo的成员变量的值,导致元素顺序混乱,但TreeSet不会重新排序,最终可能会引发无法删除的现象,具体示例见该书308~309页,通过这个示例,建议不要修改用于计算compareTo的关键成员变量的值。
import java.util.TreeSet;
class A implements Comparable{
private int x;
public A(int x){
this.x = x;
}
public String toString(){
return "A[x:" + this.x + "]";
}
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == A.class){
A a = (A)obj;
return this.x == a.x;
}
return false;
}
public int compareTo(Object obj){
A a = (A)obj;
return this.x > a.x ? 1 :
this.x < a.x ? -1 : 0;
}
public void setX(int x){
this.x = x;
}
public int getX(){
return this.x;
}
}
public class Hello{
public static void main(String[] args){
TreeSet ts = new TreeSet();
ts.add(new A(5));
ts.add(new A(-3));
ts.add(new A(9));
ts.add(new A(-2));
System.out.println(ts);
A first = (A)ts.first();
A last = (A)ts.last();
first.setX(20);//修改了第一个位置的数,导致元素顺序混乱
last.setX(-2);//修改成-2,和已有的-2重复。
System.out.println(ts);
System.out.println(ts.remove(new A(-2)));
System.out.println(ts);//无法删除,修改的-2和已有-2相同
System.out.println(ts.remove(new A(5)));//执行完这个代码后,又重新索引(不是排序)
//之后就能删除所有元素了,笔者通过下面的实验大致可以说明
//重新的索引就是按照原来插入的顺序来索引的。
System.out.println(ts);
System.out.println(ts.remove(new A(-2)));
System.out.println(ts);
System.out.println(ts.first());//第一个元素是20,表明这个顺序还是刚开始插入的顺序
}
}
(2)定制排序
定制排序,就是在利用TreeSet的构造器,创建TreeSet的时候传入Comparator对象。Comparator是个接口,传入Comparator对象的方式有三种:Lambda表达式、匿名内部类、自定义一个Comparator的实现类。
import java.util.TreeSet;
class A{
private int x;
public A(int x){
this.x = x;
}
public int getX(){
return this.x;
}
public String toString(){
return "A[x:" + this.x + "]";
}
}
public class Hello{
public static void main(String[] args){
TreeSet ts = new TreeSet((obj1, obj2)->{
A a1 = (A)obj1;
A a2 = (A)obj2;
return a1.getX() > a2.getX() ? -1 :
a1.getX() < a2.getX() ? 1 : 0;
});
ts.add(new A(5));
ts.add(new A(-3));
ts.add(new A(9));
ts.add(new A(-2));
System.out.println(ts);
}
}
输出:
EnumSet
不允许插入null,否则报异常:java.lang.NullPointerException
只能存储同属于一个枚举类的枚举值。
import java.util.EnumSet;
enum Season{
SPRING, SUMMER, AUTUMN, WINTER;
}
public class Hello{
public static void main(String[] args){
//Creates an enum set containing all of the elements in the specified element type.
EnumSet es1 = EnumSet.allOf(Season.class);
System.out.println(es1);
//Creates an empty enum set with the specified element type.
EnumSet es2 = EnumSet.noneOf(Season.class);
es2.add(Season.AUTUMN);
es2.add(Season.SPRING);
System.out.println(es2);
EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
System.out.println(es3);
//Creates an enum set initially containing all of the elements
//in the range defined by the two specified endpoints.
EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
System.out.println(es4);
//Creates an enum set with the same element type as the specified enum set,
//initially containing all the elements of this type that are not contained in the specified set.
EnumSet es5 = EnumSet.complementOf(es4);
System.out.println(es5);
}
}
要复制Collection集合中的所有元素来创建EnumSet集合时,要求Collection集合中的素有元素必须是同一个枚举类的枚举值。
import java.util.EnumSet;
import java.util.HashSet;
enum Season{
SPRING, SUMMER, AUTUMN, WINTER;
}
public class Hello{
public static void main(String[] args){
HashSet hs = new HashSet();
hs.clear();
hs.add(Season.SUMMER);
hs.add(Season.SPRING);
//java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Enum
//hs.add("hello");//如果写这句代码会引发上面的注释异常。
EnumSet es = EnumSet.copyOf(hs);
System.out.println(es);
}
}
HashSet、TreeSet、EnumSet都是线程不安全的
如果又多个线程同时访问了一个Set集合,并且有超过一个线程修改了该Set集合,必须手动保证该Set集合的同步性。通常可以通过Collections工具类的 一个方法在创建的时候进行封装Set集合。具体见该书312页、339页。或者见下篇博文的Collections工具类的相关内容。