Set接口概述
我们下面来学习Collection
接口的另一个最重要的子类接口————Set
接口。Set
接口与List
接口最大的不同在于,实现Set
接口的类(HashSet,TreeSet
)等都是没有下标的。也就是不能用以前我们常用的for
遍历,必须用迭代器或者增强for
遍历(建议用迭代器,灵活)。最有意思的是,我们java
中的Set
模仿了数学中Set
的概念,数学中Set
是什么,就是集合啊,数学中集合最重要的特性之一就是集合内元素不能重复,所以我们java
中的实现Set
接口的实现类也不能有重复元素,其实我们之前就区分过实现List
接口的集合和实现Set
接口的集合的区别,主要的两个就是(List
有下标而且可以重复,Set
没下标而且不能重复(其实这个不能重复的意思并不是不能存储重复的元素,而是存储了也没用,输出的时候只有一个))
下面我们先从实现Set
接口的HashSet
实现类开始讲起。
HashSet类
HashSet
接口的底层的是一个哈希表结构,对于哈希表,我们目前只需要知道它的查询速度非常的快。当然,记住了,所有Collection
后代都是在java.util
包里面,我们先来看一段有意思的代码
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
public class Main{
public static void main(String[] args){
Set<String> mySet=new HashSet<String>();
mySet.add("小龙女");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
Iterator<String> it=mySet.iterator();
while(it.hasNext()){
System.out.println(it.next()+"爱"+it.next());
}
}
}
然后这样改代码
public class Main{
public static void main(String[] args){
Set<String> mySet=new HashSet<String>();
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("小龙女");
Iterator<String> it=mySet.iterator();
while(it.hasNext()){
System.out.println(it.next()+"爱"+it.next());
}
}
}
运行结果还是:
然后这样改代码
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
public class Main{
public static void main(String[] args){
Set<String> mySet=new HashSet<String>();
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("小龙女");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
Iterator<String> it=mySet.iterator();
while(it.hasNext()){
System.out.println(it.next()+"爱"+it.next());
}
}
}
然后结果还是:
这就告诉我们两件事
1.Set
接口的实现类确实是存储时不重复,哪怕很多次add
,只要值一样,那么只存储一次。
2.Set
接口遍历是的顺序与存储顺序无关,我们看到了,不管我们怎么改变小龙女与尹志平的存储先后,结果都是尹志平爱小龙女.
哈希值
在继续往下学习之前我们必须引入一个哈希值的概念。所谓哈希值,实质上就是对象的逻辑地址。
我们都知道,一个对象都有物理地址,而他的索引存储的就是这个物理地址。而且使用System.out.println(对象索引);
就可以吧索引中的物理地址打印出来,但有的对象直接打印对象索引又不是物理地址,这究竟是为什么呢?
其实都是因为,Object
类作为所有对象的祖宗类,他有两个重要的与地址相关方法,一个叫toString()
一个叫hashCode()
,这两个函数,第一个是返回物理地址的值,第二个是返回逻辑地址的值(哈希值),打印索引之所以会打印出物理地址,其实就是因为默认调用了toString()
方法,有的类打印索引不是物理地址(如 ArrayList LinkedList
),是因为这个子类重写了toString()
方法。
哈希表
学完了哈希值,就要引出哈希表的概念了,学过数据结构的同学知道哈希表的底层有点类似于一个索引数组。实际就是数组+链表(jdk1.8以后有加入了红黑树,链表长度大于8的时候,转成红黑树,查询速度更快!)
数组用来存储哈希值(逻辑地址)
链表用来把哈希值相同的元素串在一起
实现原理如图:
正是因为这样,哈希表的查询速度非常快,但这也解释了为什么多次存储重复元素只相当于存储了一次,因为Set
接口的add
重写过,会自动调用hashCode()
和equals()
.
这也向我们传达了一个重要的信息!如果我们要用HashSet
存储自定义类型元素,那么我们必须重写hashCode()
函数和equals()
函数。
现在还有一个问题,就是我们刚才插入的元素进入HashSet
后都是无顺序的,这就让我们很苦恼,能不能变有序呢?当然能,这就要用到HashSet
的子类,LinkedHashSet
.
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.Iterator;
public class Main{
public static void main(String[] args){
Set<String> mySet=new LinkedHashSet<String>();
mySet.add("小龙女");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
mySet.add("尹志平");
Iterator<String> it=mySet.iterator();
while(it.hasNext()){
System.out.println(it.next()+"爱"+it.next());
}
}
}
这样就有序啦!