一、Set接口的框架:

1.Collection接口:单列集合,用来存储一个一个的对象
2.Set接口:存储无序的,不可重复的数据 ,说白了就是高中讲的"集合"
3.HashSet接口:作为Set接口的主要实现类,线程不安全的,可以存储null值
4.LinkedHashSet:作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序进行遍历。
对于频繁的遍历操作,LinkedHashSet效率高于HashSet
5.TreeSet接口:可以按照添加对象的指定属性,进行排序。

二、Set集合的无序性与不可重复性的理解:

  1. 无序性:不等于随机性,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
  2. 不可重复性:保证添加的元素按照equals()判断时,不能返回true,即:相同的元素只能添加一个
  3. Set接口中没有额外定义新方法,使用的都是Collection中声明过的方法
  4. 要求:向Set中添加的数据,其所在的类一定要重写equals()方法和hashCode()方法
    要求:重写的hashCode和equals()方法尽可能保持一致性:相等的对象必须具有相等的散列码(hash值)
    重写两个方法的小技巧:对象中用作equals()方法比较的Field,都应该用来计算hashCode值.用快捷键直接生成就行了。

Set接口和常用方法如下

Java Collection接口的子接口之Set接口及其Set接口的主要实现类HashSet,LinkedHashSet,TreeSet详解_实现类HashSet

public class SetMethod {
public static void main(String[] args) {
//1.以Set接口的实现类,HashSet来讲解Set接口的方法
//2.set接口的实现类的对象(Set接口对象),不能存放重复的元素,可以添加一个null
//3.set接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
//4.注意:取出的顺序虽然不是添加的顺序,但是它是固定的
Set set = new HashSet();
set.add("john");
set.add("lucy");
set.add("john");
set.add("jack");
set.add(null);
set.add(null);
set.add("ly");
for (int i = 0; i < 10; i++) {
System.out.println("set=" + set);//取出的顺序,它是固定的
}

//遍历
//方式一:使用迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
set.remove(null); //删除null
System.out.println("===增强for循环===");
//方式二:增强for循环
for (Object obj : set) {
System.out.println("obj=" + obj);
}

//set接口对象,不能通过索引来获取
}
}

输出结果如下

set=[null, john, lucy, ly, jack]
set=[null, john, lucy, ly, jack]
set=[null, john, lucy, ly, jack]
set=[null, john, lucy, ly, jack]
set=[null, john, lucy, ly, jack]
set=[null, john, lucy, ly, jack]
set=[null, john, lucy, ly, jack]
set=[null, john, lucy, ly, jack]
set=[null, john, lucy, ly, jack]
set=[null, john, lucy, ly, jack]
obj=null
obj=john
obj=lucy
obj=ly
obj=jack
===增强for循环===
obj=john
obj=lucy
obj=ly
obj=jack

三、HashSet的全面说明

Java Collection接口的子接口之Set接口及其Set接口的主要实现类HashSet,LinkedHashSet,TreeSet详解_java_02


对应的代码讲解如下

public class HashSet_ {
public static void main(String[] args) {
//1.构造器走的代码
/*
public HashSet() {
map = new HashMap<>();
}
2.HashSet可以存放null,但是只能有一个null,即元素不能重复

*/

Set hashSet = new HashSet();
hashSet.add(null);
hashSet.add(null);
System.out.println("hashSet=" + hashSet);
}
}

输出结果如下

hashSet=[null]

HashSet的案例说明

public class HashSet01 {
public static void main(String[] args) {
HashSet set = new HashSet();
//说明
//1.在执行add方法时,会返回一个boolean值
//2.如果添加成功,返回true,否则返回false
//3.可以通过remove 指定删除哪个对象
System.out.println(set.add("john"));//true
System.out.println(set.add("lucy"));//true
System.out.println(set.add("john"));//false
System.out.println(set.add("jack"));//true
System.out.println(set.add("Rose"));//false

set.remove("john");
System.out.println("set=" + set);

set = new HashSet();
System.out.println(set);//[]
set.add("lucy");//添加成功
set.add("lucy");//加入不了
set.add(new Dog("tom"));//添加成功
set.add(new Dog("tom"));//添加成功
System.out.println("set=" + set);

//经典面试题
set.add(new String("ly"));//添加成功
set.add(new String("ly"));//加入不了
System.out.println("set=" + set);
}
}

class Dog {
private String name;

@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}

public Dog(String name) {
this.name = name;
}
}

输出结果

true
true
false
true
true
set=[Rose, lucy, jack]
[]
set=[Dog{name='tom'}, Dog{name='tom'}, lucy]
set=[Dog{name='tom'}, Dog{name='tom'}, lucy, ly]

HashSet底层机制说明
1、HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)

下面模拟简单的数组+链表结构代码如下

public class HashSetStructure {
public static void main(String[] args) {
//模拟一个HashSet的底层(HashMap的底层结构)
Node[] table = new Node[16];
System.out.println("table=" + Arrays.toString(table));

//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
System.out.println("table="+table[2]);

Node lucy = new Node("lucy", null);
table[3] = lucy;
System.out.println("table="+table[3]);
}

}

class Node {//结点,存储数据,可以指向下一个结点,从而形成链表
Object item;//存放数据
Node next;//指向下一个结点

public Node(Object item, Node next) {
this.item = item;
this.next = next;
}

@Override
public String toString() {
return "Node{" +
"item=" + item +
", next=" + next +
'}';
}
}

输出结果如下

table=[null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
table=Node{item=john, next=Node{item=jack, next=Node{item=Rose, next=null}}}
table=Node{item=lucy, next=null}

四、添加元素的过程:以HashSet为例:

我们向HashSet中添加元素a,首先调用元素a所在类的HashCode()方法,计算元素a的哈希值,此哈希值通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素。
如果此位置上没有其他元素,则元素a添加成功 ----情况1
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值。
如果hash值不同,则元素a添加成功 -----情况2
如果hash值相同,进而需要调用元素a所在类的equals()方法,equals()返回true,则元素a添加失败,
equals()返回false,则元素a添加成功… ------情况3

注:对于添加成功的情况2和情况3而言,元素a与已经存在指定索引位置上数据以链表的形式存储
JDK7:元素a放到数组中,指向原来的元素
JDK8:原来的元素在数组中,指向元素a

总结:七上八下
HashSet底层:数组+链表的结构

五、HashSet的源码剖析

Java Collection接口的子接口之Set接口及其Set接口的主要实现类HashSet,LinkedHashSet,TreeSet详解_LinkedHashSet_03


剖析代码如下

public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println("hashSet=" + hashSet);

/*
HashSet源码解读:
1.执行HashSet()
public HashSet() {
map = new HashMap<>();
}
2.执行add方法
public boolean add(E e) {//e="java"
return map.put(e, PRESENT)==null; //PRESENT 就是 private static final Object 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;
//根据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个结点,如果达到8个结点(是从0开始的)
//就调用treefyBin()对当前这个链表进行树化(转成红黑树)
//注意,在转成红黑树时,要进行判断
// if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 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;
}
*/
}
}

HashSet底层机制二

Java Collection接口的子接口之Set接口及其Set接口的主要实现类HashSet,LinkedHashSet,TreeSet详解_实现类HashSet_04


可以debug以下代码,进行分析

public class HashSetIncrement {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
// for (int i = 1; i < 100; i++) {
// hashSet.add(i);
// }

/*
在java8中,如果一条链表的元素个数达到TREEIFY_THRESHOLD(默认是8)
并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
否则仍然采用数组扩容机制
*/
for (int i = 1; i < 12; i++) {
hashSet.add(new A(i));
}
}
}

class A {
private int n;

public A(int n) {
this.n = n;
}

@Override
public int hashCode() {
return 100;
}
}

HashSet底层机制三

当我们向hashset增加一个元素时,->Node ->加入到table,就算是增加了一个size++

可以debug以下源码进行分析

public class HashSetIncrement {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
/*
当我们向hashset增加一个元素时,->Node ->加入到table,就算是增加了一个size++

*/
for (int i = 1; i <=7 ; i++) {//在table的某一条链表上添加了 7个A对象
hashSet.add(new A(i));
}

for (int i = 0; i <=7 ; i++) {//在table的某一条链表上添加了 7个A对象
hashSet.add(new B(i));
}


}
}
class B{
private int n;

public B(int n) {
this.n = n;
}

@Override
public int hashCode() {
return 200;
}
}

class A {
private int n;

public A(int n) {
this.n = n;
}

@Override
public int hashCode() {
return 100;
}
}

HashSet的课堂练习题如下

Java Collection接口的子接口之Set接口及其Set接口的主要实现类HashSet,LinkedHashSet,TreeSet详解_TreeSet_05


具体代码如下

public class HashSetExercise {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("tom", 20));
hashSet.add(new Employee("jack", 19));
hashSet.add(new Employee("tom", 20));
System.out.println(hashSet);
}
}

class Employee {
private String name;
private int age;

@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public Employee(String name, int age) {
this.name = name;
this.age = age;
}

//如果name和age相同,则返回相同的hash值
@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{name='tom', age=20}, Employee{name='jack', age=19}]

六、LinkedHashSet的使用

1、LinkedHashSet的全面说明

Java Collection接口的子接口之Set接口及其Set接口的主要实现类HashSet,LinkedHashSet,TreeSet详解_LinkedHashSet_06


2、LinkedHashSet底层机制示意图

Java Collection接口的子接口之Set接口及其Set接口的主要实现类HashSet,LinkedHashSet,TreeSet详解_实现类HashSet_07


3、LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据

优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet

@Test
public void SetTest(){
Set hashSet = new LinkedHashSet();
hashSet.add("hello");
hashSet.add("AA");
hashSet.add(12);
hashSet.add(12);
hashSet.add("cc");
hashSet.add(new User("小红",13));
hashSet.add(new User("小红",13));
Iterator iterator = hashSet.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}

遍历的结果:是按照添加的顺序,进行遍历的。

hello
AA
12
cc
www.entity.User@168edbb

LinkedHashSet底层源码分析

@SuppressWarnings({"all"})
public class LinkedHashSetResource {
public static void main(String[] args) {
//分析一下LinkedHashSet的底层机制
Set set = new LinkedHashSet();
set.add(new String("AA"));
set.add(456);
set.add(456);
set.add(new Customer("刘", 1001));
set.add(123);
set.add("ly");
System.out.println("set=" + set);

//1.LinkedHashSet 加入顺序和取出元素/数据的顺序一致
//2.LinkedHashSet底层维护的是一个LinkedHashMap(是HashMap的子类)
//3.LinkedHashSet底层结构(数组+双向链表)
//4.添加第一次时,直接将数组table扩容到16,存放的结点类型是LinkedHashMap$Entry
//5.数组是HashMap$Node[]存放的元素/数据是LinkedHashMap$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);
}
}
*/

}
}

class Customer {
private String name;
private int no;

public Customer(String name, int no) {
this.name = name;
this.no = no;
}

@Override
public String toString() {
return "Customer{" +
"name='" + name + '\'' +
", no=" + no +
'}';
}
}

输出结果如下

set=[AA, 456, Customer{name='刘', no=1001}, 123, ly]

LinkedHashSet对应的练习题

Java Collection接口的子接口之Set接口及其Set接口的主要实现类HashSet,LinkedHashSet,TreeSet详解_Set接口_08


具体代码如下

public class LinkedHashSetExercise {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(new Car("奥迪", 10000));
linkedHashSet.add(new Car("保时捷", 1000000));
linkedHashSet.add(new Car("奥迪", 10000));
System.out.println("linkedHashSet=" + linkedHashSet);
}
}

class Car {
private String name;
private double price;

public Car(String name, double price) {
this.name = name;
this.price = price;
}

@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}

//重写equals方法,和hashCode
//当name和price相同时,就返回相同的hashCode,equals返回true

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Double.compare(car.price, price) == 0 &&
Objects.equals(name, car.name);
}

@Override
public int hashCode() {
return Objects.hash(name, price);
}
}

输出结果如下

linkedHashSet=[Car{name='奥迪', price=10000.0}, Car{name='保时捷', price=1000000.0}]

TreeSet类的详解

TreeSet最大的特点就是可以排序,具体案例代码如下

public class TreeSet_ {
public static void main(String[] args) {
//1.当我们使用无参构造器,创建TreeSet时,仍然是无序的
//2.希望添加的元素,按照字符串大小来排序
//3.使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类)
//并制定排序规则
//简单看看源码

//源码解读
/*
1.构造器把传入的比较器对象,赋给了TreeSet的底层的TreeMap的属性this.comparator

public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2.在调用add方法时treeSet.add("tom");,在底层会执行到
if (cpr != null) {//cpr 就是我们的匿名内部类(对象)
do {
parent = t;
//动态绑定到我们的匿名内部类(对象)compare
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果相等,即返回0,这个Key就没有加入
return t.setValue(value);
} while (t != null);
}
*/

// TreeSet treeSet = new TreeSet();
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//下面调用String的compareTo方法 进行字符串大小比较,从小到大排序
// return ((String) o1).compareTo((String) o2);
//下面调用String的compareTo方法 进行字符串大小比较,从大到小排序
// return ((String) o2).compareTo((String) o1);
//要求加入的元素,按照长度从小到大排序
// return ((String) o1).length() - ((String) o2).length();
//要求加入的元素,按照长度从大到小排序
return ((String) o2).length() - ((String) o1).length();
}
});
//添加数据
treeSet.add("jack");
treeSet.add("tom");//字符串长度为3
treeSet.add("sp");
treeSet.add("a");
// treeSet.add("abc");//字符串长度为3 加入不进去 因为长度与 treeSet.add("tom");一致,所以加不进去
System.out.println("treeset=" + treeSet);


}
}

输出结果如下

treeset=[jack, tom, sp, a]

四、TreeSet的使用

  1. 向TreeSet中添加的数据,要求是相同类的对象
  2. 两种排序方式:自然排序和定制排序
  3. 自然排序中,比较两个对象是否相同的标准为:CompareTo()返回0,不再是equals
@Test
public void TreeSetTest(){
TreeSet set = new TreeSet();
//失败:不能添加不同类的对象
// set.add("hello");
// set.add("AA");
// set.add(12);
// set.add(12);
// set.add("cc");
// set.add(new User("Tom",13));
//举例1. Integer类型 按照从小到大的顺序排列
// set.add(23);
// set.add(77);
// set.add(-53);
// set.add(12);
// set.add(56);

//举例2. String类型 按照从小到大的顺序排列
// set.add("23");
// set.add("abe");
// set.add("android");
// set.add("JAVA");
// set.add("IOS");
//举例3. 对象 按照从小到大的顺序排列
set.add(new User("Tom",13));
set.add(new User("Jack",23));
set.add(new User("Mary",15));
set.add(new User("Jerry",20));
set.add(new User("Jim",33));
set.add(new User("Jim",33));
Iterator iterator = set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}

User类:

package www.entity;

import java.util.Objects;

public class User implements Comparable {
private String name;
private int age;

@Override
public boolean equals(Object o) {
System.out.println("执行了");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
Objects.equals(name, user.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}

public User(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 int compareTo(Object o) {
if (o instanceof User){
User user = (User) o;
int compare = -this.name.compareTo(user.name);
if (compare!=0){
return compare;
}else {
return Integer.compare(this.age, user.age);
}
}else {
throw new RuntimeException("输入的类型不匹配!");
}
}
}

4.定制排序:比较两个对象是否相同的标准为:CompareTo()返回0,不再是equals

@Test
public void test4() {
Comparator comparator = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//按照年龄从小到大排序
if (o1 instanceof User && o2 instanceof User) {
User u1 = (User) o1;
User u2 = (User) o2;
return Integer.compare(u1.getAge(), u2.getAge());
} else {
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(comparator);
set.add(new User("Tom", 13));
set.add(new User("Jack", 23));
set.add(new User("Mary", 15));
set.add(new User("Jerry", 20));
set.add(new User("Jim", 33));
set.add(new User("Toke", 33));
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}

输出结果:年龄从小到大排序,如果有相同年龄的,按自上而下的顺序,输出,后面哪个就不再输出。