集合--Set、HashSet、LinkedHashSet
3.7 Set接口、常用方法及遍历方式
Set接口基本介绍:
- 无序(添加和取出顺序不一致),没有索引
- 不允许重复元素,所以最多一个 null
- JDK API 中 Set 接口的实现类
Set接口常用方法:
和List接口一样,Set接口也是Collection的子接口,因此,常用方法和 Collection 接口—样
//以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法
// Set 接口的实现类的对象(Set接口对象),不能存放重复的元素可以添加一个null
// Set 接口对象存放数据是无序的(即添加的顺序和取出的顺序不一致)
//注意:取出的顺序虽然不是添加的顺序,但取出顺序是固定的
Set set = new HashSet();
set.add("john");
set.add("luck");
set.add("john");//重复
set.add("jack");
set.add(null);
set.add(null);//再次添加null
Set接口的遍历方式
同Collection的遍历方式一样,因为Set接口是Collection接口的子接口。
-
可以使用选代器
Set set = new HashSet(); Iterator iterator = set.iterator(); while(iterator.hasNext()){ Object obj = iterator.next(); System.out.println(obj); } -
增强for
Set set = new HashSet(); for(Object o : set){ System.out.println(o); } -
不能使用索引的方式来获取
注意: set 接口对象,不能通过索引来获取,所以不能用普通 for 循环
3.8 HashSet底层结构和源码分析
HashSet 全面说明:
-
HashSet 实现了 Set接口
-
HashSet 实际上是 HashMap
public HashSet(){ map = new HashMap<>(); } -
可以存放null 值,但只能有一个 null ,即元素不能重复
-
HashSet 不保证元素是有序的,取决于 hash 后,在确定索引的结果(即,不保证存放元素顺序和取出顺序不一致)
-
不能有重复元素/对象
案例说明:
HashSet set =new HashSet();
//说明
//1.在执行add方法后,会返回一个boolean值
//2.如果添加成功,返回 true ,否则返回 false
//3.可以通过 remove 指定删除某个元素
System.out.println(set.add("john")); //T
System.out.println(set.add("lucy")); //T
System.out.println(set.add("john")); //F
System.out.println(set.add("jack")); //T
System.out.println(set.add("Rose")); //T
set.remove("john");
System.out.println("set="+ set);
HashSet set = new HashSet();
//4. HashSet 不能存放相同的元素/数据
set.add("lucy"); //添加成功
set.add("lucy"); //加入不了
set.add(new Dog("tom")); //ok
set.add(new Dog("tom")); //ok,两个Dog只是同名,但是两个元素
System.out.println("set="+ set);
//再加深一下,经典面试题
//看 add 的源码,做分析 ==> 底层机制
set.add(new String("hsp")); //ok
set.add(new String("hsp")); //加入不了
class Dog{//定义了一个Dog类
private String name;
private Dog(String name){
this.name = name;
}
}
HashSet 底层机制说明:
HashSet 底层是 HashMap ,HashMap 底层是(数组+链表+红黑树)
//模拟一个 HashSet 的底层(HashMap 的底层结构)
//1.创建一个数组,数组类型是 Node[]
//2.有些人,直接把 Node[] 数组称为表
Node[] table = new Node[16];
//3.创建结点
Node john = new Node("john",null);
table[2] = john;
Node jack = new Node("jack",null);
john.next = jack;//将 jack 结点挂载到 john
Node rose = new Node("rose",null);
jack.next = rose;//将 rose 结点挂载到 jack
Node luck = new Node("luck",null);
table[3] = luck;//将luck 放到 table 表 索引为3的位置
class Node{//结点,用来存储数据,可以指向下一个结点,从而形成链表
Object item; //存放数据
Node next; //指向下一个结点
public Node(Object item,Node next){
this.item = item;
this.next = next;
}
}
HashSet 扩容机制:(hash() + equals())
(一)结论
-
HashSet底层是HashMap,第一次添加时,table 数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor,是0.75)= 12
如果 table 数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,依次类推
-
添加一个元素时,先得到hash值 -会转成->索引值
-
找到存储数据表table,看这个索引位置是否已经存放的有元素
-
如果没有,直接加入
-
如果有,调用equals 比较,如果相同,就放弃添加,如果不相同,则添加到最后
-
在 Java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树),否则仍采用数组扩容机制
(二)源码
HashSet hashSet = new HashSet();
hashSet.add("java"); //到此,第1次add分析完毕
hashSet.add("php");//第2次add分析完毕
hashSet.add("java");
System.out.println("set=" + hashSet);
/*
对HashSet的源码解读
1.执行 HashSet()
public HashSet(){
map = new HashMap<>();
}
2.执行 add()
public boolean add(E e){ //e = "java"
return map.put(e,PRESENT)==null; //(static) PRESENT = new Object()
}
3.执行 put(),该方法会执行 hash(key) 得到key对应的hash值 算法h = key.hashCode()) ^ (h >>> 16)
public V put(K key,V value){ //key = "java" value = PRESENT 共享
return putVal(hash(key), key, value, false, true);
}
4.执行 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[]
//if 语句表示如果当前 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)如果 p 为 null,表示还没有存放元素,就创建一个 Node(key = "java",value = PRESENT)
//(2.2)就放在该位置 tab[i] = newNode(hash, key, value, null)
if((p = tab[i = (n -1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else{
Node<K,V> e;K k;
//如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
//并且满足下面两个条件之一:
//(1)准备加入的key 和 p 指向的Node 结点的 key 是同一对象
//(2)p 指向的Node 结点的 key 的equals() 和准备加入的key比较后相同
//就不能加入
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);
else{ //如果table对应索引位置,已经是一个链表,就使用for循环比较
//(1)依次和该链表每个元素比较后,都不相同,则加入到该链表的最后
// 注意在把元素添到链表后,立即判断 该链表是否已经达到8个结点
// 如果达到,就调用 treeifyBin() 对当前的链表进行树化(转成红黑树)
// 注意,在转成红黑树时,要进行判断,条件如下
// if(tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
// resize();
// 如果上面条件成立,就先对table 扩容
// 只有上面条件不成立时,才转成红黑树
//(2)在依次比较的过程中,如果有相同情况,就直接break
for(int binCount = 0; ; ++binCount){
if((e = p.next) == null){
p.next = newNode(hash, key, value, null);
if(binCount >= TREEIFY_THRESHOLD(8) - 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;
//size 就是我们每加入一个结点 Node(k,v,h,next),size++(无论这个结点在哪)
if(++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
*/
练习:
定义一个Employee类,该类包含:private成员属性name,age。要求:
1.创建3个Employee 对象放入HashSet中
2.当 name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
public class HashSetExcrise{
public static void main(String[] args){
HashSet hashSet = new HashSet();
hashSet.add(new EmpLoyee("milan",18));
hashSet.add(new Employee("smith",28));
hashSet.add(new Employee("milan",18));
//若没有重写方法,此时加入了几个对象?
//3个,只是内容同名,但是属于不同的Employee 对象
}
}
//创建 Employee 对象
class Employee{
private String name;
private int age;
...//构造器和get/set方法省略
//如果name 和 age 值相同,则返回相同的hash值
//alt + insert 快捷键
@Override
public boolean equals(Object o){
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EmpLoyee employee = (EmpLoyee) o;
return age == employee.age &&
Objects.equals(name, employee.name);
}
@Override
public int hashCode(){
return Objects.hash(name,age);
}
}
作业:
定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中 birthday 为 MyDate类型(属性包括:year,,month,day),要求:
1.创建3个Employee 放入HashSet中
2.当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
3.9 LinkedHashSet底层结构和源码分析
LinkedHashSet全面说明:
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个数组+双向链表
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的
- LinkedHashSet 不允许添重复元素
LinkedHastSet 底层机制说明:
- 在 LinkedHastSet 中维护了一个hash表和双向链表(LinkedHashSet 有 head 和 tail )
- 每一个节点有 pre 和 next 属性,这样可以形成双向链表在添加一个元素时,先求hash值,再求索引。确定该元素在 hashtable 的位置,然后将添加的元素加入到双向链表(如果已经存在,就不添加【原则和 hashset 一样】)
- tail.next = newElement // 简单指定
- newElement.pre = tail
- tail= newEelment;
- 这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致

浙公网安备 33010602011771号