上两片博客已经基本的介绍了一下java中的集合分类,list集合和泛型等知识,这篇再介绍一下单列集合中的另外一种集合,Set集合。
看到这篇但是没有看过前两篇的小伙伴可以点一下这里,看一下我之前写的博客。
java中的集合:Collection的简介,子接口List,以及两个实现类
java中的泛型:泛型的的概述,泛型的定义,泛型方法,泛型类,泛型接口,通配符等
好接下来我们进入正题:
java中的Set集合:
一.概述
如图所示,Set是Collection的另一个子接口
- 特点:
无序,没有前后顺序的分别(这里指添加的时候的顺序),所有的元素没有位置的概念,所有的元素都在集合中。(好比一个罐子里面装东西一样,不分顺序,都装在这个罐子里)
无索引,每个元素没有特定的编号
不可重复,元素只有值得区别,没有位置的区别,如果重复,无法区分。
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集合中的遍历方式。
一共有四种:
- 转成不带泛型的数组
- 转成带泛型的数组
- 使用迭代器
- 增强型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对象,这是为什么呢?
猜测如下:
- 怀疑在比较元素的时候使用的是没有重写的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中的单列集合。