一、简介
Set集合中的元素是无序的且不可重复, 如果试图把两个相同元素加入同一个Set集合中,则添加操作失败,add()方法返回false,且新元素不会被加入。
二、HashSet类
HashSet底层数据结构是哈希表
,因此具有很好的存取和查找性能。
哈希表
:一个元素为链表的数组,综合了链表(存储速度快)和数组(查询速度快)的优点。
哈希表的存取原理:
1. 调用对象的hashCode()方法,获得要存储元素的哈希值。
2. 将哈希值与表的长度(即数组的长度)进行求余运算得到一个整数值,该值就是新元素要存放的位置(即是索引值)。
- 如果索引值对应的位置上没有存储任何元素,则直接将元素存储到该位置上。
- 如果索引值对应的位置上已经存储了元素,则执行第3步。
3.遍历该位置上的所有旧元素,依次比较每个旧元素的哈希值和新元素的哈希值是否相同。
- 如果有哈希值相同的旧元素,则执行第4步。
- 如果没有哈希值相同的旧元素,则执行第5步。
4.比较新元素和旧元素的地址是否相同
如果地址值相同则用新的元素替换老的元素。停止比较。
如果地址值不同,则新元素调用equals方法与旧元素比较内容是否相同。
- 如果返回true,用新的元素替换老的元素,停止比较。
- 如果返回false,则回到第3步继续遍历下一个旧元素。
5.说明没有重复,则将新元素存放到该位置上并让新元素记住之前该位置的元素。
HashSet特点:
- 无序
- 集合中的元素值可以是null
- hashSet不是同步的,如果多个线程同时访问一个Set,只要有一个线程修改了Set中的值,就必须进行同步处理,通常通过同步封装这个Set对象来完成同步,如果不存在这样的对象,可以使用Collections.synchronizedSet()方法完成。
实体类:
public class Person {
String name;
int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
测试代码:
@Test
public void testHashSet(){
Person p1 = new Person("钟梅", 25);
Person p2 = new Person("王兴", 34);
Person p3 = new Person("张三", 18);
Person p4 = new Person("李四", 21);
Person p5 = new Person("李四", 21);
HashSet<Person> hashSet = new HashSet<>();
hashSet.add(p1);
hashSet.add(p2);
hashSet.add(p3);
hashSet.add(p4);
hashSet.add(p5);
for (Person person : hashSet) {
System.out.println(person.getName()+ "----------" + person.getAge());
}
}
输出结果:
由上可以看到,结果中出现重复元素。在实体类Person中重写hashCode和equals方法:
//判断判断两个对象是否相等,对象是否存在,对象的name和age是否相等
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
//返回对象的name和age的hash值
@Override
public int hashCode() {
return Objects.hash(name, age);
}
重写之后不是判断两个对象hashCode是否相等,而是判断对象的name和age是否同时相等,如果同时相等则判断为同一对象,不能重复出现在集合中。
再次遍历集合,运行结果:
可以看到重复的元素已经被覆盖,保证了集合中元素的唯一性。
为什么不直接使用数组,而用HashSet呢?
因为数组的索引是连续的而且数组的长度是固定的,无法自由增加数组的长度。而HashSet就不一样了,HashCode表用每个元素的hashCode值来计算其存储位置,从而可以自由增加HashCode的长度,并根据元素的hashCode值来访问元素。而不用一个个遍历索引去访问,这就是它比数组快的原因。
LinkedHashSet类
LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的,也就是说当遍历集合LinkedHashSet集合里的元素时,集合将会按元素的添加顺序来访问集合里的元素。
输出集合里的元素时,元素顺序总是与添加顺序一致。但是LinkedHashSet依然是HashSet,因此它不允许集合重复。
三、TreeSet类
TreeSet可以确保集合元素处于排序状态。
内部存储机制
TreeSet内部实现的是红黑树,默认整形排序为从小到大。
与HashSet集合相比,TreeSet还提供了几个额外方法:
//如果TreeSet采用了定制顺序,则该方法返回定制排序所使用的Comparator,如果TreeSet采用自然排序,则返回null;
Comparator comparator();
//返回集合中的第一个元素;
Object first();
//返回集合中的最后一个元素;
Object last();
//返回指定元素之前的元素。
Object lower(Object e);
//返回指定元素之后的元素。
Object higher(Object e);
//返回此Set的子集合,含头不含尾;
SortedSet subSet(Object fromElement,Object toElement);
//返回此Set的子集,由小于toElement的元素组成;
SortedSet headSet(Object toElement);
//返回此Set的子集,由大于fromElement的元素组成;
SortedSet tailSet(Object fromElement);
用法示例:
@Test
public void testTreeSet(){
TreeSet<Integer> nums = new TreeSet<>();
//向集合中添加元素
nums.add(5);
nums.add(2);
nums.add(15);
nums.add(-4);
//输出集合,可以看到元素已经处于排序状态
System.out.println(nums);//[-4, 2, 5, 15]
System.out.println("集合中的第一个元素:"+nums.first());//集合中的第一个元素:-4
System.out.println("集合中的最后一个元素:"+nums.last());//集合中的最后一个元素:15
System.out.println("集合小于4的子集,不包含4:"+nums.headSet(4));//集合小于4的子集,不包含4:[-4, 2]
System.out.println("集合大于5的子集:"+nums.tailSet(2));//集合大于5的子集:[2, 5, 15]
System.out.println("集合中大于等于-3,小于4的子集:"+nums.subSet(-3,4));//集合中大于等于-3,小于4的子集:[2]
}
输出结果:
从上面的运行结果可以看出输出的集合已经按从小到大排好了,但是问题来了,只能从小到大排序吗?如果是字符对象应按该怎样的顺序排序?如果是一个对象又按怎样的顺序排序呢?遵循怎样的排序规则呢?
针对这个问题,TreeSet支持两种排序方法:自然排序和定制排序,在默认情况下,采用的是自然排序。
自然排序
TreeSet会调用集合元素的compareTo(Objec obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这就是自然排序。
拓展:
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类必须实现该方法,实现接口的类就可以比较大小了。当调用一个一个对象调用该方法与另一个对象进行比较时, compareTo(Object obj)如果返回0表示两个对象相等;如果返回正整数则表明obj1大于obj2,如果是负整数则相反。
案例:
实现存储Person类的集合,排序方式,按年龄大小,如果年龄相等,则按name字符串长度,如果长度相等则比较字符。如果name和age都相等则视为同一对象。
public class Person implements Comparable<Person>{
String name;
int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
@Override
public int compareTo(Person o) {
//比较age
int num=this.age-o.age;
//如果age相等则比较name长度
int num1=num==0?this.name.length()-o.name.length():num;
//如果前两者都相等则比较name字符串
int num2=num1==0?this.name.compareTo(o.name):num1;
return num2;
}
}
测试:
@Test
public void testTreeSet(){
TreeSet<Person> tree = new TreeSet<>();
//向集合中添加元素
tree.add(new Person("孙悟空",16));
tree.add(new Person("孙悟空",17));
tree.add(new Person("孙悟空",16));
tree.add(new Person("唐僧",16));
tree.add(new Person("沙悟净",23));
tree.add(new Person("唐僧",30));
//遍历
System.out.println(tree);
}
输出:
从运行结果可以看到满足定义的排序规则。
当把一个对象添加进集合时,集合调用该对象的CompareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树结构中找到它的存储位置。如果两个对象相等则新对象无法加入到集合中。
定制排序
TreeSet的自然排序是根据集合元素的大小,TreeSet将它们以升序排列。如果需要实现定制排序,例如降序排序,则可通过Comparator接口的帮助。该接口里包含一个int compare(T o1,T o2)方法,用于比较o1和o2的大小。由于Comparator是一个函数式接口,因此还可以使用Lambda表达式来代替Comparator子类对象。
@Test
public void testTreeSet2(){
TreeSet<Integer> nums = new TreeSet<>((a,b)->-(a-b));
//向集合中添加元素
nums.add(5);
nums.add(2);
nums.add(15);
nums.add(-4);
//输出集合,可以看到元素已经处于排序状态
System.out.println(nums);//[15, 5, 2, -4]
}
以上就是我查看资料后的一些整理,有什么问题请评论区留言。
转载自: https://blog.csdn.net/mashaokang1314/article/details/83721792