详解Set集合中实现类:HashSet
前面我们学习完Collection接口中子接口List中的所有内容,今天来学习下Collection集合中另外一个子接口:Set接口。
1. Set集合的概述
前面我们了解到List接口中存储的为有序的、可以重复的数据。
而Set接口中存储无序的、不可重复的元素。 Set接口中有三个具体实现类:HashSet,LinkedHashSet以及TreeSet。
下面总结这三个具体实现类的特点:
Set接口中三个具体实现类 | 特点 |
---|---|
HashSet | 作为Set接口的主要实现类,线程不安全的,可以存储null值 |
LinkedHashSet | 作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序进行遍历 |
TreeSet | 可以按照添加对象的指定属性,进行排序 |
在这里需要注意的是Set集合中没有定义额外的新的方法,使用的都是Collection接口中的方法。
2.对于Set集合中存储无序的、不可重复的数据理解
我们应经直到Set集合的特点是:存放无序的,不可重复的元素。对于无序性与不可重复性该如何进行理解呢?
这里以HashSet(HashSet为Set接口的主要实现类)为例说明:
小编是这样认为的:
无序性:不等于随机性。HashSet底层的数据结果为数组加链表加二叉树(JDK1.8)。存储的数据在底层数组中并非按照数组索引的顺序进行添加,而是根据数据的哈希值进行添加的
不可重复性:保证添加的元素按照equals()判断时,不能返回true,即:相同的元素只能添加一个。
//自定义的Student类
public class 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;
}
@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(name, age);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
//测试类
import java.util.HashSet;
import java.util.Set;
public class MyTest {
public static void main(String[] args) {
/* Set中存放的是无序的、不可重复的数据
以HashSet为例说明:
1.无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引
的顺序进行添加,而是根据数据的哈希值进行添加的
2.不可重复性:保证添加的元素按照equals()判断时,不能返回true,即:
相同的元素只能添加一个
*/
Set set= new HashSet();
//向set集合中添加元素
set.add(100);
set.add(200);
set.add(100);
set.add("aaa");
set.add("bbb");
set.add("bbb");
//向set集合中添加自定义类对象时,想要实现元素没有重复性,需要重写hashCode方法以及equals方法
//如果不重写equals方法,每new一个对象,比较的为地址值,始终为不同的
set.add(new Student("Mike",23));
set.add(new Student("Kite",24));
set.add(new Student("Jack",25));
set.add(new Student("Mike",23));
//对set集合进行遍历
for (Object o : set) {
System.out.println(o);
}
}
}
运行后的结果为:
3.HashSet底层详解(面试常问)
HashSet是Set集合的主要实现类,HashSet按照Hash算法来存储集合中元素。存在以下特点:
- HashSet不能保证元素的顺序,元素是无序的
- HashSet集合允许存放集合元素值为null
- Hashset是不同步的,即线程不安全的
继承关系:
java.util.Collection
| java.util.AbstractCollection<E>
| java.util.AbstractSet<E>
| java.util.HashSet<E>
实现接口:
Set,Cloneable,Serializable,
HahSet的构造方法:
//无参构造方法,完成HashMap的创建
public HashSet() {
map = new HashMap<>();
}
//指定集合转换为HashSet,完成HashMap的创建
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
//指定初始化大小和负载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
通过构造方法可以看出,HashSet的底层实现是利用HashMap来实现的
HashSet的add()方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashSet的add()方法调用HashMap的put()方法实现。
如果键已经存在,map.put()放回的是旧值,添加失败。如果添加成功,map.put方法返回的是null,HashSet.add()方法返回的true,则添加的元素可以作为map中的key。
通过例子阐述HashSet是无序的,而且不存在重复的元素:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class MyTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("詹姆斯");
set.add("科比");
set.add("杜兰特");
set.add("安东尼");
set.add("欧文");
set.add("欧文");
set.add("詹姆斯");
//使用迭代器进行遍历
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
System.out.print(next+" ");
}
}
}
运行后的结果为:
注意:HashSet存储元素的顺序并不是按照存入的顺序(与List集合不同),是按照哈希值来进行存放的,所以取数据也是按照哈希值来取的。
HashSet不存入重复元素的规则:使用hashCode()方法和equals()方法。
Hashset添加元素的过程:
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值。
此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即索引位置)。
判断数组该位置是否已经有元素:
如果此位置上没有其他元素,则元素a添加成功。---->情况1
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
如果hash值不相同,则元素添加成功。---->情况2
如果hash值相同,进而需要调用元素所在类的equals()方法:
equals()返回true,元素a添加失败
equals()返回false,则元素a添加成功---->情况3
对于添加成功的情况2与情况3而言:元素a与已经存在指定索引位置上数据以索引的方式存储
JDK1.7:元素a放在数组中,指向原来的数组
JDK1.8:原来的元素在数组中,指向元素a
图1:hashCode值不相同的情况
图2:hashCode值相同,但equals不相同的情况
HashSet:通过hashCode值来确定元素在内存中的位置。一个hashCode位置上可以存放多个元素。当hashcode() 值相同equals() 返回为true 时,hashset 集合认为这两个元素是相同的元素.只存储一个(重复元素无法放入)。调用原理:先判断hashcode 方法的值,如果相同才会去判断equals 如果不相同,是不会调用equals方法的。
使用HashSet存储自定义对象,并尝试添加重复对象(对象的重复的判定):此时需要重写hashCode()方法以及equals()方法
//自定义的Person类
import java.util.Objects;
public class Person {
private String name;
private int age;
public Person() {
}
public Person(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;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + 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);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
//测试类:
import java.util.HashSet;
import java.util.Iterator;
public class MyTest {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
set.add(new Person("张三",23));
set.add(new Person("赵六",24));
set.add(new Person("王五",25));
set.add(new Person("张三",23));
set.add(new Person("李四",24));
set.add(new Person("李四",24));
Iterator<Person> iterator = set.iterator();
while (iterator.hasNext()) {
Person p = iterator.next();
System.out.println(p);
}
}
}
运行后的结果为:
在判断自定义类对象是否相等时,是通过计算两者的hashCode值,和equals()方法进行的,这是作为Set集合判断不存在重复元素的根本原因。
总结
本节了解到了Set集合的特点:无序以及不允许元素的重复。对于HashSet底层实现原理进行探索,进而从底层更加深刻理解Set集合特点。