《疯狂Java讲义(第4版)》-----第8章【Java集合】(Collection、Iterator、Set)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ccnuacmhdu/article/details/82929185

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工具类的相关内容。

猜你喜欢

转载自blog.csdn.net/ccnuacmhdu/article/details/82929185
今日推荐