文章目录
1. Set
Set接口是Collection接口的一个子接口,Set集合中不允许存储重复的元素,而且没有索引。如果要判断两个对象是否相同根据的是equals(),而不是 == 。Set接口主要的实现类有:
- HashSet
- LinkedHashSet
2. Hastset
2.1 概念
HashSet具有如下的:
- 不允许存储重复的元素
- 没有索引,没有带索引的方法
- 无序集合
- 底层实现是哈希表,因此查询非常快
- 线程不安全
- 集合元素可以是null
import java.util.HashSet;
import java.util.Iterator;
public class SetMain {
public static void main(String[] args) {
HashSet<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);
System.out.println(set); // [1, 2, 3]
// 遍历元素
Iterator<Integer> iter = set.iterator();
while (iter.hasNext()){
System.out.println(iter.next());
}
for(Integer ele : set){
System.out.println(ele );
}
}
}
哈希值:系统随即给出的一个十进制的整数(对象的逻辑地址,而不是数据实际存储的物理地址)。在Object类中可以通过
int hashCode()方法得到对象的哈希值public native int hashCode(){};public class Hash { public static void main(String[] args) { String s1 = "Forlogen"; String s2 = "kobe"; System.out.println(s1.hashCode()); // 538205156 System.out.println(s2.hashCode()); // 3297447 } }
2.2 底层实现
HashSet存储数据的结构为哈希表:
- JDK1.8之前哈希表通过数组 + 链表实现
- JDK1.8之后哈希表通过数组 + 红黑树实现
其中数组存储数据的哈希值,链表和红黑树存储相同哈希值的数据。HashSet如何实现存储的数据不重复呢?原理是HashSet重写了hashCode()和equals()。当需要判断两个对象是否相等时,不仅需通过hashCode()判断,还需要equals()的返回值也相同。下面给出一个HashSet如何存储元素的例子。
示意图:
如上所示,list中包含有三个元素 [ F o r l o g e n , F o r l o g e n , k o b e ] [Forlogen, Forlogen, kobe] [Forlogen,Forlogen,kobe],那么在将其添加到HashSet中会有如下过程:
- 添加第一个"Forlogen":调用
hashCode()计算它对应的哈希值538205156,在数组找发现没有这个值,那么将其存储到链表中 - 添加第二个"Forlogen":调用
hashCode()计算它对应的哈希值538205156,在数组找发现已经这个值,然后是使用equals()判断它和相应哈希值存储的元素是否相同,发现相同,那么不存储 - 添加"kobe":调用
hashCode()计算它对应的哈希值3297447,在数组找发现没有这个值,那么将其存储到链表中 - 添加"重地":调用
hashCode()计算它对应的哈希值1179395,在数组找发现没有这个值,那么将其存储到链表中 - 添加"通话":调用
hashCode()计算它对应的哈希值1179395,在数组找发现已经这个值,然后是使用equals()判断它和相应哈希值存储的元素是否相同,发现不相同,故将其存储到链表中
源码分析:HaseSet底层实现依赖于HashMap,依赖于Map中键不能重复的特点来检验元素。
public HashSet() {
map = new HashMap<>();
}
HashSet中的add()使用了HashMap中的put()
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
我们找到HashMap中的put(),发现它又调用了putval()方法进行判断
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putval()的源码为:
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过hash值计算元素的位置,然后判断该位置是否有值
// 如果没有则直接插入,最后返回null
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果该位置上已经有其他的元素
// 则使用hash和equals方法进行判断是否是重复元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
如何利用HashSet来存储不重复的自定义类数据呢?同样我们需要重写hashCode()和equals(),假设现有Person类如下所示,如果我们不重写hashCode()和equals():
package Set;
import java.util.Objects;
public class Person{
private int age;
private String name;
public Person() {
}
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
那么在往HashSet中添加元素时就无法去重:
import java.util.HashSet;
public class SetMain {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
set.add(new Person(18, "Forlogen"));
set.add(new Person(20, "kobe"));
set.add(new Person(20, "kobe"));
System.out.println(set); // [Person{age=20, name='kobe'}, Person{age=20, name='kobe'}, Person{age=18, name='Forlogen'}]
}
}
而如果我们重写hashCode()和equals():
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return getAge() == person.getAge() &&
Objects.equals(getName(), person.getName());
}
@Override
public int hashCode() {
return Objects.hash(getAge(), getName());
}
那么HashSet就可以正常的进行重复元素的去除:
import java.util.HashSet;
public class SetMain {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
set.add(new Person(18, "Forlogen"));
set.add(new Person(20, "kobe"));
set.add(new Person(20, "kobe"));
System.out.println(set); // [Person{age=20, name='kobe'}, Person{age=18, name='Forlogen'}]
}
}
3. LinkedhashSet
LinkedhashSet的底层实现是哈希表(数组 + 链表/红黑树) + 链表,多加的双向链表用于记录元素的添加顺序,从而保证元素有序。
4. TreeSet
TreeSet是SortedSet接口的实现类,它可以确保集合中的元素处于排序状态,TreeSet具有如下的特点:
-
能够保证元素唯一性(根据返回值是否是0来决定的),并且按照某种规则排序
-
自然排序,无参构造方法(元素具备比较性)
-
按照compareTo()方法排序,让需要比较的元素所属的类实现自然排序接口Comparable,并重写compareTo()
-
底层是自平衡二叉树结构
- 二叉树有前序遍历、后序遍历、中序遍历
- TreeSet类是按照从根节点开始,按照从左、中、右的原则依此取出元素
-
当使用无参构造方法,也就是自然排序,需要根据要求重写compareTo()方法,这个不能自动生成