一、set集合中元素的特点
set系列集合中添加元素的特点是无序,不重复,无索引,
无序:是指集合内的元素没有顺序,存入和取出的顺序可以不同
不重复:集合内的元素都是唯一的
无索引:不能根据索引对集合内的元素进行操作
与set集合对应的是list集合,list集合的特点是添加的元素是有序,可重复,有索引
二、set接口的实现类
set是一个接口,在实际使用时通常使用set的实现类,包括HashSet,TreeSet,LinkedHashSet
HashSet:无序,不重复,无索引
LinkedHashSet:有序,不重复,无索引
TreeSet:可排序,不重复,无索引
1.HashSet
(1)哈希表
HashSet采用哈希表来存储数据
哈希表的组成:
jdk8之前:数组+链表
jdk8之后:数组+链表+红黑树
(2)哈希值
在哈希表中哈希值是一个非常重要的数据,是对象的整数表现形式,用来确定元素在数组中的存储位置
哈希值是根据hashCode方法计算出来的int型的整数,默认使用地址值进行计算(不同对象的哈希值不同),在实际应用中一般都会重写hashCode方法,利用对象内部的属性来计算哈希值(不同对象如果属性相同,则哈希值相同)
在创建实体类时重写hashCode方法和equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
在测试类中测试两个对象的哈希值
public class Test {
public static void main(String[] args) {
Student s1 = new Student(12,"张三");
Student s2 = new Student(12,"张三");
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
}
但是在一些情况下不同的属性或不同的地址计算出来的哈希值是相同的(哈希碰撞)
(3)数据存储过程
① 创建一个HashSet集合
HashSet<String> h = new HashSet<>();
在这个过程中底层会默认创建一个长度为16,加载因子为0.75的数组,数组名为table
(加载因子的作用是当数组已经被使用了16*0.75=12个存储空间时数组会自动扩充,扩充为原 来的两倍)
② 根据哈希值和数组长度的计算结果决定元素存储的位置
int index = (数组长度-1)&哈希值;进行与运算
③判断当前位置是否为null,如果为null则直接存入
④当前位置不为null,已经有元素存在,则会调用equals方法进行比较
如果两个元素的属性值相同,不存入
如果两个元素的属性值不同
jdk8之前:新元素存入数组中,原来的元素挂在新元素的下面形成链表
jdk8之后:新元素直接挂在原来元素的下面
现在数组中下标3已经有元素存在,元素b经过计算之后下标也是3并且与a的属性值不同(如果下标为3的位置上的链表有多个元素,需要依次和每个元素进行比较,来判断是否存储)
jdk8之前:
jdk8之后:
当链表的长度>8,而且数组的长度>=64时,链表会自动转成红黑树,提高了查找效率(jdk8之后)
注意:如果集合中存储的是自定义的对象,则必须要重写hashCode方法和equals方法,因为比较对象的地址值没有意义,如果不是自定义的类,如:String,Integer等不需要重新,因为这些类中已经重写过了hashCode和equals方法
(4)实际应用
创建一个Student类,并重写hashCode和equals方法
public class Student {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
创建测试类
public class Test {
public static void main(String[] args) {
Student s1 = new Student(12,"张三");
Student s2 = new Student(17,"李四");
Student s3 = new Student(15,"王五");
Student s4 = new Student(12,"张三");
HashSet<Student> hs = new HashSet<>();
System.out.println(hs.add(s1));
System.out.println(hs.add(s2));
System.out.println(hs.add(s3));
System.out.println(hs.add(s4));
for(Student s:hs){
System.out.println(s);
}
}
}
运行结果
2.LinkedHashSet
LinkedHashSet与HashSet的用法没有太大差别,只是在底层原理上有一些不同,HashSet的特点是无序,不重复,无索引,LinkedHashSet的特点是有序,不重复,无索引。
LinkedHashSet中每个元素上添加了一个双向链表,在新加入元素后,上一个元素的尾结点指向新加入的元素,新加入的元素的头节点指向上一个元素,再有新的元素加入时重复上面操作。在取出元素时会按照顺序获取。
3.TreeSet
TreeSet集合是通过红黑树实现的,能够对存储的数据进行排序
①对于数值类型:默认按照从小到大的方式进行排序
②对于字符,字符串类型:按照ASCII进行排序
例如:有下列字符串:"aaa" "ab" "aba" "cd" "qwer"(已经排序)
在排序时首先看第一个字符,如果第一个字符相同看后面的字符
如果第一个字符能够确定大小则不看后面的字符,直接得出结果,如“aaa”和“ab”,第一个字 符相同,“ab”的第二个字符比“aaa”的第二个字符大,所以不看后面的字符,“ab”直接排在 “aaa”的后面
“ab”和“aba”的前两个字符相同,“ab”只有两个字符,“aba”有三个字符,所以“aba”排在“ab”的 后面
TreeSet的两种排序方式
(1)方式1(默认排序/自然排序)
在创建的类中实现Comparable接口,实现compareTo方法,指定排序的内容和方式
创建一个学生类,在这个类中实现Comparable接口,重写方法
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
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;
}
public String toString() {
return "Student{name = " + name + ", age = " + age + "}";
}
@Override
public int compareTo(Student o) {
return this.getAge()-o.getAge();
}
}
创建测试类
public class Test {
public static void main(String[] args) {
Student s1 = new Student("zhangsan",21);
Student s2 = new Student("lisi",20);
Student s3 = new Student("wangwu",23);
TreeSet<Student> ts = new TreeSet<>();
ts.add(s1);
ts.add(s2);
ts.add(s3);
System.out.println(ts);
}
}
输出结果
[Student{name = lisi, age = 20}, Student{name = zhangsan, age = 21}, Student{name = wangwu, age = 23}]
对于compareTo的解释:
TreeSet的底层是按照红黑树来存储数据的,这个方法中的参数o表示的是已经在红黑树中存 在的数据,通过判断返回结果的正负来决定存左边或右边(按照红黑树存储的相关规则),如果返回的是0则说明已经存在相同的元素,不会存入集合中
(2)方式2(比较器排序)
创建TreeSet对象的时候,传递比较器Comparator指定规则
使用方法:
如果要比较"a","abc","de","f"这几个字符串,先比较字符串的长度,如果长度相同再按照默认的排序方式进行排序
public class Test1 {
public static void main(String[] args) {
TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
int i = o1.length()-o2.length();
if(i==0)i=o1.compareTo(o2);
return i;
}
});
ts.add("a");
ts.add("abc");
ts.add("de");
ts.add("f");
System.out.println(ts);
}
}
运行结果
[a, f, de, abc]
如果第一种方式能够满足要求就用第一种方式排序,如果不能满足要求再使用第二种方式
三、应用场景
1.如果允许集合中的元素可重复:用ArrayList集合
2.如果允许集合中的元素可重复,并且经常进行增删操作:用LinkedList集合
3.如果不允许集合中的元素重复:用HashSet集合
4.如果不允许集合中的元素重复,并且想要存和取的顺序一致:用LinkedHashSet集合
5.如果不允许集合中的元素重复,并且想对元素进行排序:用TreeSet集合