Set体系结构
HashSet
特点
- HashSet基于HashCode、equals实现元素不重复
- JDK1.8:使用数组+链表+红黑树
HashSet常用方法
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* @author 张宝旭
*/
public class SetTest {
public static void main(String[] args) {
// 创建集合
Set<String> set = new HashSet<>();
// 添加元素
set.add("北京");
set.add("上海");
set.add("沈阳");
set.add("东戴河");
set.add("北京");
// 判断是否为空
System.out.println(set.isEmpty());
// 获取集合大小
System.out.println(set.size());
// 删除集合元素
set.remove("东戴河");
// for增强遍历
for (String s : set) {
System.out.print(s + " ");
}
System.out.println();
// 迭代器遍历
// 迭代器允许在遍历过程中删除元素,但是不能使用集合的删除方法,
// 否则会出现异常ConcurrentModificationException
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
System.out.println();
// 判断元素是否存在
System.out.println(set.contains("北京"));
}
}
HashSet源码分析
构造方法
使用HashMap实现,在构造方法里new了一个HashMap
public HashSet() {
map = new HashMap<>();
}
add()方法
同样是调用HashMap的put方法
将元素存储在map的键中,而将map的值存储一个伪值
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
PRESENT的定义
private static final Object PRESENT = new Object();
具体添加细节,查看HashMap源码:HashMap常用方法与源码分析 JDK1.8
LinkedHashSet
特点
- 有序、没有下标、不能重复
- 和HashSet相同的去重原理,多加了链表结构,保证了保存添加元素的顺序
源码分析
构造方法
LinkedHashSet继承了HashSet:在构造方法中调用了父类HashSet的构造方法
public LinkedHashSet() {
super(16, .75f, true);
}
而在父类的构造方法中又new了一个LinkedHashMap
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
在LinkedHashMap中又调用了HashMap的构造方法
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
其实LinkedHashSet的本质还是一个map
add()方法
在其内部直接使用父类HashMap的add()方法,将值存储到了map的键中
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
TreeSet
特点
- 不能有重复元素,使用compareTo方法实现去重
- 实现了SortedSet接口,对集合元素自动排序
- 元素对象的类型必须实现Comparable接口,指定排序规则
- 底层使用红黑树实现
compareTo方法
以Integer方法为例,查看Integer中的compareTo方法
public int compareTo(Integer anotherInteger) {
// 比较当前值,和传入的值
return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
// 如果当前值小,则返回-1;如果两个值相等,则返回0;如果当前值大,则返回1
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
重写compareTo方法
创建Student类
/**
* @author 张宝旭
*/
public class Student {
private String name;
private Integer age;
public Student() {
}
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
创建TreeSet,将Student对象存入TreeSet中
public class TreeSetTest {
public static void main(String[] args) {
Set<Student> students = new TreeSet<>();
students.add(new Student("小张", 20));
students.add(new Student("小李", 18));
students.add(new Student("小哥", 21));
students.add(new Student("小哥", 21));
for (Student student : students) {
System.out.println(student);
}
}
}
运行时发现会报错 java.lang.ClassCastException
,因为TreeSet中保证有序使用的是compareTo方法,所以我们需要重写compareTo方法
在Student类中重写compareTo:以年龄比较,当年龄相同时,再以姓名比较
public class Student implements Comparable<Student>{
//...
@Override
public int compareTo(Student o) {
int n1 = this.getAge() - o.getAge();
// String字符串默认实现了compareTo方法
int n2 = this.getName().compareTo(o.getName());
return n1 == 0 ? n2 : n1;
}
}
迭代器遍历
- TreeSet是红黑树实现,迭代器遍历是中序遍历,所以也就是按升序输出结果
将以上程序使用迭代器遍历
Iterator<Student> iterator = students.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
输出结果
Student{name='小李', age=18}
Student{name='小张', age=20}
Student{name='小哥', age=21}
哈希表
- JDK1.8时使用数组+链表+红黑树
- 初始大小为16
插入元素过程
解决哈希冲突-链地址法
将所有哈希地址值相同的元素,链接在同一链表中
- 先计算哈希值,如果发生冲突,则将冲突的那个元素与它相同的那个元素链接成一个链表,存到同一地址中
重写HashCode
如果不重写HashCode
-
将所有哈希地址值相同的元素,链接在同一链表中
-
哈希地址值相同,但是哈希值不同
public class HashSetTest {
public static void main(String[] args) {
Set<Student> students = new HashSet<>();
Student s1 = new Student("Java", 18);
Student s2 = new Student("C++", 21);
Student s3 = new Student("Java", 18);
students.add(s1);
students.add(s2);
students.add(s3);
for (Student student : students) {
System.out.println(student);
}
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
}
}
结果
Student{name='C++', age=21}
Student{name='Java', age=18}
Student{name='Java', age=18}
1625635731
1580066828
491044090
因为new出的是两个不同的对象,两个对象在堆中的地址是不同的,哈希值默认是根据地址进行计算,所以哈希值不同
如果重写HashCode
- 当new出两个对象,且两个对象值相同时,重写的equals会判断值是否相等,值相等就不会再添加相同的元素,不会形成链表
重写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 Objects.equals(name, student.name) &&
Objects.equals(age, student.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
在执行查看结果
Student{name='Java', age=18}
Student{name='C++', age=21}
71347665
2039635
71347665
发现相同值的对象没有添加进去,而且s1和s3的哈希值也相同,那是因为重写了hashCode与equals方法,使其根据姓名和年龄来判断哈希值,因为s1和s3的姓名、年龄相同,所以两者的哈希值也相同
如果重写equals方法,则必须要重写hashCode方法