java中的Set集合

上两片博客已经基本的介绍了一下java中的集合分类,list集合和泛型等知识,这篇再介绍一下单列集合中的另外一种集合,Set集合。
在这里插入图片描述
看到这篇但是没有看过前两篇的小伙伴可以点一下这里,看一下我之前写的博客。
java中的集合:Collection的简介,子接口List,以及两个实现类
java中的泛型:泛型的的概述,泛型的定义,泛型方法,泛型类,泛型接口,通配符等

好接下来我们进入正题:
java中的Set集合:

一.概述

在这里插入图片描述

如图所示,Set是Collection的另一个子接口

  1. 特点:

无序,没有前后顺序的分别(这里指添加的时候的顺序),所有的元素没有位置的概念,所有的元素都在集合中。(好比一个罐子里面装东西一样,不分顺序,都装在这个罐子里)
无索引,每个元素没有特定的编号
不可重复,元素只有值得区别,没有位置的区别,如果重复,无法区分。

2.实现类:
HashSet,使用哈希表来存储元素。
3.方法:
没有特别的功能,完全使用Collection中的方法。
4.存储特点:
(1)没有顺序,存储的顺序和取出的顺序不一致。
(2)不可重复。

代码示例:

package set集合;

import java.util.HashSet;
import java.util.Set;

public class Demo1_Set的特点 {
	
	public static void main(String[] args) {
		Set<Integer> s = new HashSet<Integer>();
		s.add(123);
		s.add(456);
		s.add(666);
		s.add(888);
		s.add(888);
		s.add(-222);
		s.add(0);
		System.out.println(s);
	}
}
--------------------------------------------------------------------
打印结果:[0, 456, 888, 666, 123, -222]
输出时和添加时的顺序不一致(无序),并且只输出了一次888,证明了set集合的不可重复性。

练习:生成十个不重复的1-100的随机数,存储在合适的集合中,并输出

package set集合;

import java.util.HashSet;
import java.util.Random;
import java.util.Set;

public class Demo2_练习1 {
//生成十个不重复的1-100的随机数,存储在合适的集合中,并遍历集合
	public static void main(String[] args) {
		Random r = new Random();//Random类用来生成随机数
		Set<Integer> s = new HashSet<Integer>();
		int count = 0;
		while (s.size() < 10) {//当已经存入HashSet集合中的数字小于10的时候循环(如果重复则元素数不变)。
			s.add(r.nextInt(100) + 1);
			count++;

		}

		System.out.println(s);
		System.out.println("共添加过" + count + "个数字");//打印一下执行了多少次看看是否能保证不可重复
	}
}

---------------------------------------------
打印结果:
[65, 66, 50, 53, 37, 54, 88, 57, 42, 75]
共添加过12个数字

二.Set集合的遍历

没有特别的遍历方式,全部都是用Collection集合中的遍历方式。
一共有四种:

  1. 转成不带泛型的数组
  2. 转成带泛型的数组
  3. 使用迭代器
  4. 增强型for循环

第一种和第二种遍历方式类似,所以放在一起说一下:

package set集合;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class Demo3_Set集合的4中遍历方式的前两种 {

	public static void main(String[] args) {
		Set<Integer> s = new HashSet<Integer>();
		s.add(123);
		s.add(456);
		s.add(666);
		s.add(888);
		s.add(-222);
		s.add(0);
		System.out.println("第一种遍历方式");
		Frist(s);
		System.out.println("第二种遍历方式1");
		Second1(s);
		System.out.println("第一种遍历方式2");
		Second2(s);
		System.out.println("第一种遍历方式3");
		Second3(s);

	}

	private static void Second3(Set<Integer> s) {
		// 第三种情况:自己创建的数组大于集合元素的个数,这样会把前几个位置填充,剩下的返回null。
		Integer[] ss = new Integer[10];
		s.toArray(ss);
		for (int i = 0; i < ss.length; i++) {
			System.out.print(ss[i] + " ");
		}
	}

	private static void Second2(Set<Integer> s) {
		// 第二种情况:自己创建的数组大小正好等于集合的元素个数,这样就不用创建新的数组,返回的和创建的是同一个数组。
		Integer[] ss = new Integer[6];
		s.toArray(ss);
		for (int i = 0; i < ss.length; i++) {
			System.out.print(ss[i] + " ");
		}
		System.out.println();
	}

	private static void Second1(Set<Integer> s) {
		// 第二种遍历方式,含泛型的数组
		// 第一种情况,自己创建的数组大小<小于集合的元素个数,这种情况会在集合内部产生一个新的数组,将之返回
		Integer[] ss = new Integer[2];
		Integer[] i1 = s.toArray(ss);
		for (int i = 0; i < ss.length; i++) {
			System.out.print(ss[i] + "  ");
		}
		System.out.println("\n" + Arrays.toString(i1));
	}

	private static void Frist(Set<Integer> s) {
		// 第一种遍历方式:
		Object[] o = s.toArray();
		for (int i = 0; i < o.length; i++) {

			System.out.print(o[i] + " ");
		}
		System.out.println();
	}

}
-----------------------------------------------
打印结果:
第一种遍历方式
0 456 888 666 123 -222 
第二种遍历方式1
null  null  
[0, 456, 888, 666, 123, -222]
第一种遍历方式2
0 456 888 666 123 -222 
第一种遍历方式3
0 456 888 666 123 -222 null null null null 

注意事项:
一.转数组不带泛型:Object[] toArray()

1.返回值:无论集合是否带泛型的,返回的都是Object[]。
2.必须对数组中的每个元素做强转。

二.转数组带泛型:T[] toArray(T[] arr )在参数中传入一个指定类型的数组返回该类型的一个数组
1.理解:
参数表示传入一个数组容器,返回值表示装满了元素的数组容器。
2.三种情况:
(1)参数数组的容量较小,集合中元素个数较多,就会在方法内部生成一个新的数组,装集合中的元素,将新数组返回。
(2)参数数组的容量较大,集合中元素个数较少,就不需要创建新的数组,返回的数组和参数数组是同一个数组。空余的空间,使用null填充。
(3)参数数组的容量和集合元素个数一样多,就不需要创建新数组,返回的数组和参数数组是同一个数组。没有剩余空间。

第三种遍历方式:迭代器遍历
迭代器遍历已经是老生常谈了,这里也没什么特别之处,直接上代码:
示例代码:

Iterator it = s.iterator();
		while(it.hasNext()) {
			System.out.print(it.next() + " ");
		}
------------------------------------------------
打印结果:
0 456 888 666 123 -222 

第四种遍历方式:增强型for循环遍历(foreach循环)
1.格式:

for (数据类型 变量名称 :  集合或者数组) {
}

2.说明:
(1)数据类型:集合或者数组中的元素的数据类型。
(2)变量名称:每次循环过程中元素的值,虽然每次循环使用相同的变量名称,但是表示的是每次不同的元素。
(3)集合或者数组:除了Collection集合意外,还可以遍历数组。
3.注意事项:
(1)增强for循环底层就是迭代器:在使用增强for遍历集合的同时,使用集合对象进行增删,就会出现并发修改异常
(2)增强for无法对元素进行修改:

1、无法增加:并发修改异常
2、无法删除:不能使用集合对象、没有获取迭代器的引用
3、无法修改:没有获取到元素的位置,只能拿到元素的值

代码示例:

public class Demo3_Set集合的4中遍历方式的第四种增强型for循环 {

	public static void main(String[] args) {
		Set<Integer> s = new HashSet<Integer>();
		s.add(123);
		s.add(456);
		s.add(666);
		s.add(888);
		s.add(-222);
		s.add(0);
		for (Integer i : s) {
			System.out.print(i + " ");
		}

	}
}

练习: 键盘录入一个字符串,输出其中的字符,相同字符只输出一次

public class Demo4_练习2 {
	//键盘录入一个字符串,输出其中的字符,相同字符只输出一次
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		String s = sc.next();
		Set<Character> set = new HashSet<Character>();
		
		for (int i = 0; i < s.length(); i++) {
			set.add(s.charAt(i));//使用charAt方法,每次存到set集合中一个字符。
		}
		
		for (Character character : set) {
			System.out.print(character + " ");//增强型for循环遍历set集合
		}
		
	}

}

三.Set的实现类,HashSet

1、是Set接口的实现类
2、通过哈希存储的方式实现的
3、哈希存储:
顺序存储和链式存储的中和情况
4、没有特有的功能,全都使用Set接口中实现的方法

HashSet保证元素唯一性的探究(重要)
我们先定义一个Person类,里面有年龄,姓名两个属性。

public class Person {

	private int age;
	private String name;
	
	public Person(int age, String name) {
		super();
		this.age = age;
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return "Person [age=" + age + ", name=" + name + "]";
	}	
}

现在我们想在HashSet中存储多个Person类型的数据。

public class Demo5_HashSet唯一性 {
	
	public static void main(String[] args) {
		HashSet<Person> hs = new HashSet<Person>();
		hs.add(new Person(20, "xiaohong"));
		hs.add(new Person(20, "xiaohong"));
		hs.add(new Person(22, "xiaoming"));
		hs.add(new Person(23, "xiaolan"));
		hs.add(new Person(21, "xiaobai"));
		
		System.out.println(hs.toString());
	}

}

打印结果如下:
在这里插入图片描述
很神奇的发现,说好的Set集合的唯一性不存在了,里面竟然存储了两个年龄,姓名都相同的Person对象,这是为什么呢?

猜测如下:

  1. 怀疑在比较元素的时候使用的是没有重写的Object类中的equals方法,比较的是两个对象的地址值,虽然对象里面的数据相同,但是地址值不同,造成HashSet没能去重成功。

那么来重写一下equals方法

@Override
		public boolean equals(Object obj) {
			if(this != obj)//如果比较的类型根本就不是Person类,直接返回false。
				return false;
			Person other = (Person) obj;
			
			if(age == other.age && name.equals(other.name)) {//比较其中的内容是否相同
				return true;
			}
			return false;
		}

2.重写equals后运行发现:
在这里插入图片描述
两个相同的元素还是存在,并没有保证HashSet的唯一性,并且在equals方法中添加了一句:

System.out.println("调用了equals方法");

再次执行发现,根本就没有调用equals方法。
在这里插入图片描述
3.怀疑HashSet使用哈希表来存储元素,考虑和hashCode方法有关,通过hashCode代码发现,hashCode调用的是super.hashCode方法,其中super指的是Object类,因为Object类可以保证根据不同的对象生成不同的hashCode值,也就是代表不同的对象,这样HashSet就无法去重,所以我们重写一下hashCode方法,让所有的Person对象都返回相同的hashCode值(相同的对象必须返回有的哈希值,不同的对象可以有相同的哈希值),相同的数字不一定能表示相同的对象。

@Override
		public int hashCode() {
			// TODO Auto-generated method stub
			return 666;
		}

重写后测试一下
在这里插入图片描述
这回不仅调用了equals方法,也去重了。

总结一下

1、计算即将存储到集合中的元素的hashCode值,分别和集合中已经存在的元素的哈希值比较
2、如果没有任何一个已存在的元素的哈希值,和当前即将存储的元素的哈希值相等,那么直接存储到集合中
3、如果有一部分已经存在的元素的哈希值,和当前即将存储元素的哈希值相等,那么就需要使用equals方法比较当前对象和这些元素是否相等
4、任意一个已存在的元素和即将存储元素equals比较返回true,则不存储了
5、所有已存在的元素和即将存储元素equals比较返回都是false,说明当前元素没有存在于集合中,就可以直接存储
在这里插入图片描述

以上的重写步骤可以通过快捷键实现 Alt + Shift + S ,h 来重写两个方法
在这里插入图片描述

四.哈希存储的内存解释

1、哈希存储就是顺序存储和链式存储的结合使用
2、本质上还是一个数组,数组中可以存储节点,节点还可以链上其他的节点

在这里插入图片描述

五.LinkedHashSet

1、内存特点:
在每个元素所在的节点中,另外记录了一个逻辑顺序的后一个节点的地址
2、使用特点:
可以根据存储的顺序,将集合中的元素遍历
3、应用场景:
如果既需要去重,也需要保证原有顺序,就可以考虑使用LinkedHashSet

代码示例:

import java.util.LinkedHashSet;

public class Demo02_LinkedHashSet {
	public static void main(String[] args) {
		LinkedHashSet<Integer> hs = new LinkedHashSet<>();
		hs.add(-123);
		hs.add(-123);
		hs.add(666);
		hs.add(0);
		hs.add(888);
		System.out.println(hs);
	}
}

以上就是java中的单列集合。

感谢您能看到这里,如果喜欢请点个赞谢谢!

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值