文章目录
一、set接口和常用方法
1.Set接口基本介绍
- 无序(添加和取出的顺序不一致)、没有索引。
- 不允许重复元素,所以最多包含一个null
2.Set接口常用方法
- add:添加单个元素
- remove:删除指定元素
- contains:查找元素是否存在
- size:获取元素个数
- isEmpty:判断是否为空
- clear:清空
- addAll:添加多个元素
- containsAll:查找多个元素是否都存在
- removeAll:删除多个元素
3.Set接口遍历方法
- 迭代器
- 增强for
- 不能使用索引方式
二、HashSet
1.说明
- HashSet实现了Set接口
- HashSet实际上是HashMap
- 可以存放null值,但只能存放一个
- HashSet不保证元素是有序的,取决于hash后,在确定索引的结果。
- 不能有重复的元素/对象,
"lucy"字符串放在常量池,第二个显然放不进去了,因为已经存在了。而在new Dog()的时候,每次调用new Dog()方法都是在内存中开辟一块新内存,所以都是不同的对象,即使名字一样。
但是:(原因看add()方法的源码)
2.HashSet底层机制说明
HashSet的底层是HashMap,而HashMap的底层是(数组+链表+红黑树)
这里是数组加链表的形式,当该表的规模超过一定大小后,链表会变为红黑树形式。
不用数组的原因是数组效率太低。
3.HashSet扩容机制
添加元素——hash()+equals()
补充:可以通过在自己写的类中重写hashCode()方法和equals()方法来实现一些东西: 可以通过重写hashCode()让该类的对象都返回一眼的哈希值,这样该类的所有结点都会存放在同一链表中。 也可以通过重写equals()方法,指定比较类中某个属性来确定是不是一样的结点。(源码解读中可以看到,调用了传入的值key的hashCode()方法和equals()方法)
要注意的是,虽然在Object类中的equals()方法是用‘==’来比较内存地址。但是在String类中,重写了equals()方法来比较两个字符串的内容是否一致。所以 set.add(“lisi1”); 和 set.add(new String(“lisi”)); 其实在equals方法中相等了,也就不能放进去了
- HashSet底层是HashMap
- 添加一个元素时,使用hashCode()方法先得到hash值——运算后会转成->索引值(数组下标)
- 找到存储数据表table,看这个索引位置是否有已经存放的元素
- 如果没有,直接加入(挂在后面)
- 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后。注意这里equals()方法不局限于单纯比较内容,可以由程序员重写。
- 在JDK8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN TREEIFY_CAPACITY(默认64),就会进行树化(红黑树);如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小<MIN TREEIFY_CAPACITY(默认64),那么会先对数组进行扩容(扩容的方法是数组大小变为原来二倍)
扩容详细过程:
1.HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16加载因子(LoaFactor)是0.75=12
2.如果table表大小达到了临界值12,就会扩容到162 = 32,新的临界值就是32*0.75=24,依次类推(补充:这里是指表中所有元素个数,不仅指在数组中的,还要算上链表和数中的元素个数)
3.在JDK8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN TREEIFY_CAPACITY(默认64),就会进行树化(红黑树);如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小<MIN TREEIFY_CAPACITY(默认64),那么会先对数组进行扩容(扩容的方法是数组大小变为原来二倍),如果在继续在该链表加上一个元素,并且table的大小<MIN TREEIFY_CAPACITY(默认64),那么数组继续扩容依次(扩容的方法是数组大小变为原来二倍)。
4.HashSet源码解读
重点:putval()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;//定义辅助变量
//table是HashMap的一个数组,类型是Node[]
//第一次进入(开始时)table应该为空或长度为0
//如果当前table是null,或者大小=0,则进行第一次扩容到16个空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(1)根据key得到的hash值,去计算该key应该存放到table表的哪个索引位置
// 并把这个位置的对象赋给p
//(2)判断p是否为null
// (2.1)如果为null,表示在表中这一行还没存放元素,就创建一个Node(key="要添加的值",value=PRESENT)
// 把这个Node放在tab[i]
//
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果当前索引位置对应的第一个元素(数组里的元素)的hash值和准备加入的key的hash一样
//且满足以下两个条件之一:
//(1)准备加入的key和p指向的Node结点的key指向同一个对象
//(2)p指向的Node结点的key与准备加入的key 执行equals()后相等(不知道有没有重写成比较值的方法???????)
//就不能加入
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//在判断p是不是一颗红黑树
//如果是一颗红黑树,就执行putTreeVal()方法进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果table对应索引位置已经是一个链表,就使用for循环来比较
//(1)依次和链表中每个元素比较后,都不相同,则加入到链表的最后
// 注意这里加入后,立即判断当前链表是否超过8个结点
// 超过则调用treeifyBin()方法,进一步判断table数组大小
// 如果table表大小小于64,则进行扩容操作
// 否则,转该链表为红黑树。
//(2)依次比较过程中发现有相同的情况,直接break;
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;
//如果所存东西的数量超过了临界值(容量的0.75),就进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//HashMap中有更详细简介
三、LinkedHashSet
LinkedHashSet 继承了 HashSe类, 实现了Set接口
1.LinkedHashSet的全面说明
- LinkedHashSet是 HashSet的子类
- LinkedHashSet底层是一个 LinkedHashMap,底层维护了一个数组+双向链表
- LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的.
- LinkedHashSet 不允许添重复元素
2.LinkedHashSet底层机制示意图
3.LinkedHashSet底层代码解读
- LinkedHashSet 加入顺序和取出元素/数据的顺序一致
- LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
- LinkedHashSet 底层结构(数组table+双向链表),每个节点还是有next,可以形成自己的单链。
- 添加第一次时,直接将数组table扩容到16,存放的结点类型是 LinkedHashMap的Entry子类
- 数组是 HashMap的Node[] 。 存放的元素/数据是LinkedHashMap的Entry子类类型。 其实就是子类和父类(多态)。
//HashMap的静态内部类Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//LinkedHashSet的静态内部类Entry
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
四、TreeSet
代码讲解:
public static void main(String[] args) {
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//调用String的compareTo方法进行字符串比较
return ((String)o1).compareTo((String)o2);
}
});
treeSet.add("jack");
treeSet.add("tom");
treeSet.add("a");
treeSet.add("rose");
}
-
当我们使用无参构造器创建 TreeSet时,仍然是无序的
-
如果希望添加的元素有序,可以使用TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类),并指定排序规则(重写compare方法)
public TreeSet(Comparator<? super E> comparator) { //TreeSet底层实现就是TreeMap this(new TreeMap<>(comparator)); } //继续step Into public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
-
添加元素时,先调用 add()方法, 然后调用put()方法
public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // 这里会使用传入的比较器,调用比较器的compare方法 Comparator<? super K> cpr = comparator; if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } else { if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); //cmp为比较结果 if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else //如果比较结果为0,则不加入 return t.setValue(value); } while (t != null); } Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); size++; modCount++; return null; }