Set接口
Set集合是无序的、元素不可重复的结合
常用集合类有HashSet和TreeSet
HashSet类
常用的两种List集合各有各的优点,那么有没有同时具备这两种List集合的优点的集合呢?答案是肯定的,就是Set集合。
实例: package collection.set.hashSet; import java.util.HashSet; import java.util.Iterator; /** * 演示HashSet * @author 学霸联盟 - 赵灿 */ public class HashSetDemo { public static void main(String[] args) { //创建HashSet对象 HashSet hs = new HashSet(); //循环向集合hs中添加元素 for (int i = 0; i < 10; i++) { //创建Person对象 Person p = new Person(); //为Person对象的name属性赋值 //有此处可知,加入的顺序是赵0、赵1、赵2......赵9 p.name = "赵" + i; //将Person对象p加入集合 hs.add(p); } /* * Iterator(迭代器):提供一种访问Collection集合中每个元素的方法 * Iterator是一个接口 * 其中定义了hasNext()方法,用于判断集合中是否含有下一个元素 * 如果还有下一个元素,返回true;反之,返回false * 还定义了next()方法,获取下一个元素 * Collection中定义了一个iterator()方法,用于获取Iterator对象 * * 由于Set集合无法使用下标访问元素,无法使用下标的方式遍历集合元素 * 所以java为我们提供了Iterator用于访问集合中的各个元素 */ Iterator it = hs.iterator(); //调用hasNext方法判断集合中是否还有下一个元素 while(it.hasNext()){ //调用next方法获取下一个元素 Object obj = it.next(); //强制类型转换 Person p = (Person)obj; //输出name属性值,和加入的顺序不同 System.out.print(p.name + " "); } } } //Person类 class Person{ public String name; } 我的机器上运行的结果: 赵4 赵7 赵8 赵0 赵6 赵1 赵2 赵3 赵5 赵9
从上面的结果可以看到,输出的顺序并不是加入时的顺序(未必一次就能运行出乱序的效果,可以瞬间连续多次点击eclipse上的运行按钮,这样看到乱序的效果几率比较大);所以称Set是无序的集合。
那么Set是如何同时实现两种List集合的优点,又为什么没有顺序呢?
首先Set同时使用了两种List集合的结构,即数组 + 链表(Set中使用的是单项链表:一个节点中只存储下一个节点,而不存储上一个节点);其中数组中的每一个位置均用于存储一个链表第一个元素。
这里不得不提到hashCode和equals。在创建Set集合的子类对象是,会创建一个Node[] table(节点数组)用于存储Node对象,Node中存储下一个Node和加入集合的元素。
再向集合中添加元素A时,Set会创建一个Node对象nodeA(Node nodeA = new Node()),将元素A赋值给nodeA中key属性(nodeA.key = A),然后根据元素A的hashCode值计算出下标(假如得到的下标是1),最后将nodeA的地址赋值到下标为1的位置中。
再次向集合中添加元素B时,又会创建Node对象nodeB,使用元素B的hashCode值计算出的下标也是1;此时会用equals方法判断元素A和元素B这两个对象是否相同。
如果equals返回true,则表示A和B内容相同,此时不会再将nodeB加入集合。
如果equals方法返回false,说明元素A和元素B是不同的两个对象,此时会将nodeB赋值给nodeA中的next属性(nodeA.next = nodeB),这样就形成了单项链表结构。
由以上描述可知,Set中不能添加重复的元素,而且无法从Set中快速获取某一个指定的元素,只能使用Iterator为我们提供的遍历集合的方法获取Set中的每一个元素。
示例图
上述中创建的table称作散列表或哈希表,默认初始长度为16(如果创建集合对象时自定义初始容量,自定义的容量值必须是2的整倍数),加载因子默认为0.75(当集合中添加的元素个数等于当前长度乘以加载因子时,集合会自动将容量增加至原来的2倍)。
拓展阅读:《I学霸官方免费教程三十七:Java数据结构之单向链表结构》
在使用key的hashCode值计算下标(计算方法:int h = key.hashCode(); int hash = h^(h>>>16); int index = (table.length - 1) & hash),这样可以保证不越界,但无法保证顺序,所以Set集合是无序的。
这样做带来的优点是当加入和删除元素时,都无需将前后元素在数组中位移,只需根据计算出的下标和equals方法得到的结果,进行加入和删除即可。
缺点是没有顺序,无法快速获取某个元素,必须采用遍历的方式获取Set集合中的元素。
另外,可以利用重写Object类中的hashCode方法,来控制什么样的对象可以生成一致的(节点对象在hash表中的)下标。此时再利用重写Object类的equals方法,来控制对象是否属于重复的对象。而且必须两个方法得到的结果同时满足要求时,才认为两个对象时同一对象;任一方法不满足要求都认为是不同的对象。
实例: package collection.set.hashSet; import java.util.HashSet; import java.util.Set; /** * 演示重写hashCode方法和equals方法 * * @author 学霸联盟 - 赵灿 */ public class HashCodeEqualsDemo { public static void main(String[] args) { /* * 在堆内存中创建了一个PersonHE类型的对象 * 对象中保存的是"张三"和"123456789"两个字符串 */ PersonHE p1 = new PersonHE("张三", "123456789", 10); /* * 又在堆内存中创建了一个PersonHE类型的对象 * 对象中保存的也是"张三"和"123456789"两个字符串 */ PersonHE p2 = new PersonHE("张三", "123456789", 20); /* * 此时在内存中有两个PersonHE类型的对象 * 如果PersonHE类没有重写父类Object的equals方法 * 那么使用的则是Object类中的equals方法比较 * 和使用==比较一样,结果是:false */ System.out.println(p1 == p2); /* * 现在已经重写了equals方法,按照姓名和***号比较 * 创建的时候姓名和***号赋的值都一样 * 所以此时使用equals比较得到的结果是:true */ System.out.println(p1.equals(p2)); //创建HashSet对象(父类引用指向子类对象) Set setHE = new HashSet(); //向集合中添加元素 setHE.add(p1); /* * PersonHE重写了hashCode方法和equals方法 * 使得对象p1和p2的hashCode相等,equals的结果为true * 所以不会将对象p2加入集合 */ setHE.add(p2); /* * 可以使用Iterator + while循环获取Set集合中的元素 * 也可以使用增强for循环 * 语法格式:for(数据类型 变量名 : 继承Collection接口的集合或数组){} * 作用:循环一次从集合或数组中取出一个元素赋值变量 * 注意:声明变量的数据类型,必须能够接收集合或数组中取出的元素的类型 * 反例:String[] str = {"abc", "xyz"}; * for(int i : str){}; * 执行第一次循环,取出字符串abc,赋值给int类型的变量i * 这很明显是错误的 */ for(Object obj:setHE){ PersonHE pHE = (PersonHE)obj; /* * 由于加入前一个对象p1被覆盖 * 所以这里只会输出一次,而且输出的是对象p1 * 输出结果:姓名:张三 ***号:123456789 年龄:10 */ System.out.println(pHE); } } } /** * 创建人类PersonHE * @author 学霸联盟 - 赵灿 */ class PersonHE{ //姓名 private String name; //***号 private String id; //年龄 public int age; //声明带参构造方法 public PersonHE(String name, String id, int age){ this.name = name; this.id = id; this.age = age; } /** * 重写父类的hashCode * 目的:自定义计算hash值的算法 */ @Override public int hashCode() { /* * 这里自定义的计算规则是,姓名的hash值亦或***号的hash值 * 当然算法不是固定的,只要能符合需求即可 * 使用亦或运算只是为了尽可能少的出现相同情况 */ return name.hashCode() ^ id.hashCode(); } /** * 重写父类的equals方法 * 目的:自定义比较两个对象内容是否相同的规则 */ @Override public boolean equals(Object obj) { //将Object强制类型转换为Person PersonHE p = (PersonHE)obj; //这里自定义的比较规则是,当姓名和***号相同时,就表示是同一个人 boolean result = this.name.equals(p.name) && this.id.equals(p.id); return result; } /** * 重写父类的toString方法 * 目的:自定义将对象转换成字符串的规则 */ @Override public String toString() { //这里自定义的规则是 ,返回姓名和***号的字符串 return "姓名:" + name + " ***号:" + id + " 年龄:" + age; } } 运行结果: false true 姓名:张三 省份证号:123456789 年龄:10
总结:
Set集合是无序的,不可重复的集合,采用散列(Hash)存储(数组 + 单向链表),遍历时可以采用Iterator + 循环和增强for循环,Iterator(迭代器)主要方法:hasNext()和next(),判断加入集合中的两个对象是否重复:
1、判断hashCode是否相同;
当hashCode相同,但equals比较结果为false时,两个对象会被保存在一个单向链表上,这个单向链表称为“桶”
2、使用equals判断内容是否相同;
当equals比较结果为true,但hashCode不同时,两个对象会被保存在不同的“桶”中
TreeSet类
既然Set集合是无序的,那么可不可以另外给Set增加排序的方法呢?答案是肯定的。
java为我们提供了TreeSet类,实现了自动对加入Set集合的元素进行排序。但是要求加入到TreeSet集合的元素类型必须实现Comparable接口,并实现接口中的compareTo()方法,在该方法中自定义排序方式。
实例: package collection.set.treeSet; import java.util.TreeSet; /** * 演示泛型TreeSet集合 * @author 学霸联盟 - 赵灿 */ public class TreeSetDemo { public static void main(String[] args) { //创建三个树枝对象 Branch b1 = new Branch("树枝10", 10); Branch b2 = new Branch("树枝5", 5); Branch b3 = new Branch("树枝8", 8); //创建TreeSet集合对象 TreeSet ts = new TreeSet(); //向集合中添加元素;加入TreeSet集合的元素类型必须实现Comparable接口 ts.add(b1); ts.add(b2); ts.add(b3); //增强for循环遍历集合ts for (Object obj : ts) { //强制类型转换 Branch b = (Branch) obj; //输出 System.out.println("名称:" + b.getName() + " 年轮" + b.getAnnualRing()); } } } /** * 树枝类Branch * 加入TreeSet集合的元素类型必须实现Comparable接口 * 否则程序运行的时候会出现异常 * @author 学霸联盟 - 赵灿 */ class Branch implements Comparable{ //标识名称 private String name; //年轮:用于排序的依据 private int annualRing; //带参构造方法 public Branch(String name, int annualRing){ this.name = name; this.annualRing = annualRing; } //重写接口中的方法 @Override public int compareTo(Object o) { //强制类型转换 Branch b = (Branch)o; /* * 使用年轮作为排序比较的依据 * 当前对象年轮减去参数传入的Branch对象的年龄 * 结果等于0表示:两个树枝年轮相等,排序不分先后 * 结果大于0表示:当前对象的年轮大,排在后面 * 结果小于0表示:参数对象的年轮大,排在后面 */ int result = this.annualRing - b.annualRing; return result; } //获取名称 public String getName(){ return name; } //获取年轮 public int getAnnualRing(){ return annualRing; } } 运行结果: 名称:树枝5 年轮5 名称:树枝8 年轮8 名称:树枝10 年轮10