Java集合架构图
点击放大查看
List , Set, Map都是接口,List , Set继承至Collection接口(Collection继承至Iterable),Map为独立接口
Set接口简介
- Set是一种不包括重复元素的Collection。
- 与List一样,允许添加null的元素
Set接口直接或者间接实现类
Set集合 | 说明 |
---|---|
HashSet | 无序 并且性能比TreeSet高效,底层数据结构是哈希表(底层由 HashMap 实现) 。(无序,唯一) ,依赖两个方法: hashCode()和equals() |
TreeSet | 底层数据结构是红黑树(平衡二叉树) 。(唯一,有序),需要采用红黑树算法 维护数据的排序,对Set的数据类型有要求(添加元素 需要实现Comparable 接口或者是在TreeSet构造方法上自定义排序Comparator ),性能较HashSet低效,比较适用于需要排序 的场景。 |
LinkedHashSet | 底层数据结构是链表和哈希表 。(FIFO插入有序,唯一) 由链表保证元素有序 ,由哈希表保证元素唯一,其他与HashSet无太大差异,性能较HashSet略低 (链表开销) |
EnumSet | 这四种常见Set实现类中最高效的,采用的是位向量 的数据结构存储元素(存储高效、紧凑),要求存储元素必须是一个枚举类Enum (约束大),适用于枚举值 执行批量操作的场景 |
上面4种Set都是非线程安全
的,即需要使用代码同步机制保证多线程访问的安全性。想使用线程安全的Set,可采用Collections.synchronizedSortedSet()
HashSet
- 实现原理,基于
哈希表(HashMap)
实现 不允许重复
,最多可以有一个null
元素不保证顺序
恒久不变- 添加元素时把元素作为
HashMap的key
保存,HashMap的value
使用一个固定的Object对象
- 排除重复是通过
hashCode和equals
方法来检查对象是否相等 - 判断两个对象是否相同,先判断==内存地址是否相同, 内存地址不同判断两个对象的
hashCode
是否相同(两个对象的hashCode相等不一定是同一个对象,但如果不同,一定不是同一个对象),若不同,则两个对象不是同一个对象;若相同,还要进行equals
判断。equals方法返回true则为同一个对象,返回false则不是同一个对象。 - 自定义类存入HashSet时,建议重写类的
hashCode和equals
方法
哈希表的存储结构:
1.7 数组+链表 1.8数组+链表+红黑树
,数组里的每个元素以链表的形式存储
如何把对象存储到哈希表中?
- 先计算对象的hashCode值再对数组的长度求余数,来决定对象要存储在数组中的哪个位置。
示例代码
public class TestSet {
@Test
public void testHashSet(){
//Demo1类没有重写equals方法和HashCode方法, 内存地址相同的对象才能认定为同一个对象(equals比较的是内存地址,hashCode比较的也是内存地址(10位整数))
Set<Demo1> demo1Set = new HashSet<>();
demo1Set.add(new Demo1(1));
demo1Set.add(new Demo1(1));
demo1Set.add(new Demo1(1));
System.out.println("demo1Set=>"+demo1Set.size());// 3
//Demo2类重写了equals方法和HashCode方法,内存地址不同但是num值相同的Demo2可以判断为同一对象
Set<Demo2> demo2Set = new HashSet<>();
demo2Set.add(new Demo2(1));
demo2Set.add(new Demo2(1));
demo2Set.add(new Demo2(1));
System.out.println("demo2Set=>"+demo2Set.size());// 1
//String类重写equals方法和HashCode方法,值相同的对象HashCode和equals返回相同
Set<String> demo3Set = new HashSet<>();
demo3Set.add("111");
demo3Set.add("111");
demo3Set.add("111");
demo3Set.add("222");
demo3Set.add("333");
demo3Set.add(null);
demo3Set.add(null);
System.out.println("demo3Set=>"+demo3Set.size());// 4
}
class Demo1 {
private Integer num;
public Demo1(Integer num) {
this.num = num;
}
}
class Demo2 {
private Integer num;
public Demo2(Integer num) {
this.num = num;
}
/**
* 重写了equals方法,只要值相同就可以认为是同一个对象
*
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj instanceof Demo2) {
Demo2 demo = (Demo2) obj;
if (num.equals(demo.num)) {
return true;
}
}
return false;
}
/**
* 重写hashCode方法,返回当前num值
*
* @return
*/
@Override
public int hashCode() {
return num;
}
}
}
执行结果:
Integer和String类
都重写
了equals()和hashCode()
,值相同的对象, hashCode和equals返回值都相同
,因此使用HashSet可以自动去重- 使用HashSet保存
自定义类Demo1
时,由于Demo1没有重写equasl和hashCode方法
,因此默认使用的是父类Object
的方法, equals判断内存地址, hashCode返回整型内存地址, 因此会值相同
的对象都由于内存地址不同
,添加进HashSet
Java1.8中Set不允许元素重复的原理
- 重写了hashCode方法和equals方法,set加入一个对象,首先查看该对象的哈希值,然后看哈希表对应的哈希值是否有对象存储了,如果还没有那么就直接插入哈希表中;如果已经存储着相同哈希值的对象,先判断内存地址 或者 equals方法比较是不是相同,如果不相同则插入。如果相同则覆盖旧值。
注意哈希表在解决冲突问题的时候采用的是拉链法,在同一个哈希值存储的的元素个数小于8时使用链表,大于等于8就改成了红黑树(红黑树是一棵自平衡的二叉树)
LinkedHashSet
- LinkedHashSet底层是由
哈希表和链表(链接列表)
实现的一个唯一有序的Set。 - 由
链表
保证元素有序
- 由
哈希表
保证元素唯一
- 继承于 HashSet,并且其内部是通过
LinkedHashMap
来实现的。
示例代码
public class TestSet {
@Test
public void testLinkedHashSet(){
//Demo1类没有重写equals方法和HashCode方法, 内存地址相同的对象才能认定为同一个对象(equals比较的是内存地址,hashCode比较的也是内存地址(10位整数))
Set<Demo1> demo1Set = new LinkedHashSet<>();
demo1Set.add(new Demo1(1));
demo1Set.add(new Demo1(1));
demo1Set.add(new Demo1(1));
System.out.println("demo1Set=>"+demo1Set.toString());// 3
//Demo2类没有重写equals方法和HashCode方法,内存地址不同但是num值相同的Demo2可以判断为同一对象
Set<Demo2> demo2Set = new LinkedHashSet<>();
demo2Set.add(new Demo2(1));
demo2Set.add(new Demo2(1));
demo2Set.add(new Demo2(1));
System.out.println("demo2Set=>"+demo2Set);// 1
Set<String> demo3Set = new LinkedHashSet<>();
demo3Set.add(null);
demo3Set.add(null);
demo3Set.add("111");
demo3Set.add("111");
demo3Set.add("111");
demo3Set.add("222");
demo3Set.add("333");
System.out.println("demo3Set=>"+demo3Set);// 4
}
@Test
public void testHashSet(){
//Demo1类没有重写equals方法和HashCode方法, 内存地址相同的对象才能认定为同一个对象(equals比较的是内存地址,hashCode比较的也是内存地址(10位整数))
Set<Demo1> demo1Set = new HashSet<>();
demo1Set.add(new Demo1(1));
demo1Set.add(new Demo1(1));
demo1Set.add(new Demo1(1));
System.out.println("demo1Set=>"+demo1Set.size());// 3
//Demo2类重写了equals方法和HashCode方法,内存地址不同但是num值相同的Demo2可以判断为同一对象
Set<Demo2> demo2Set = new HashSet<>();
demo2Set.add(new Demo2(1));
demo2Set.add(new Demo2(1));
demo2Set.add(new Demo2(1));
System.out.println("demo2Set=>"+demo2Set.size());// 1
Set<String> demo3Set = new HashSet<>();
demo3Set.add("333");
demo3Set.add(null);
demo3Set.add("222");
demo3Set.add(null);
demo3Set.add("111");
demo3Set.add("111");
demo3Set.add("111");
System.out.println("demo3Set=>"+demo3Set.size());// 3
}
class Demo1 {
private Integer num;
public Demo1(Integer num) {
this.num = num;
}
@Override
public String toString() {
return num.toString();
}
}
class Demo2 {
private Integer num;
public Demo2(Integer num) {
this.num = num;
}
/**
* 重写了equals方法,只要值相同就可以认为是同一个对象
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj instanceof Demo2) {
Demo2 demo = (Demo2) obj;
if (num.equals(demo.num)) {
return true;
}
}
return false;
}
/**
* 重写hashCode方法,返回当前num值
* @return
*/
@Override
public int hashCode() {
return num;
}
@Override
public String toString() {
return num.toString();
}
}
}
执行结果:
- LinkedHashSet是带有有
去重
, 带有插入顺序
的Set, 插入自定义对象是必须重写equals和hashCode方法
, 否则因为默认的equals和hashCode判断内存地址相同而插入相同对象
TreeSet
- TreeSet基于
TreeMap
的NavigableSet(继承自SortedSet接口)
实现。默认
使用元素的自然顺序
对元素进行排序(自定义对象需要实现Comparable接口
),或者创建 set 时在构造方法
中指定Comparator
进行自定义排序 - TreeSet保证元素的
排序
和唯一性
的 - 底层数据结构是
红黑树(自平衡二叉树)
如何保证元素排序的呢?
添加元素实现自然排序Comparable接口/构造方法中创建自定义比较器排序Comparator
如何保证元素唯一性的呢
在比较器中根据比较的返回值是否是0来决定,两个元素相比如果为0则表示对象相等
示例代码
public class TestSet {
@Test
public void testTreeSet() {
//Demo类实现了Comparable接口,并重写了compareTo方法,去重的方式与equals和hashCode无关,通过比较器返回0来判断元素相等
Set<Demo> demo1Set = new TreeSet<>();
demo1Set.add(new Demo(1));
demo1Set.add(new Demo(2));
demo1Set.add(new Demo(3));
System.out.println("demo1Set=>" + demo1Set);// 1
//因为String已经实现了Comparable接口并重写了comparTo方法,默认只能按自然顺序升序 可以在类外部使用Comparator自定义排序规则
Set<String> demo2Set = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1.compareTo(o2) > 0) {
return -1;
} else if (o1.compareTo(o2) < 0) {
return 1;
} else {
return 0;
}
}
});
demo2Set.add("CCC");
demo2Set.add("EEE");
demo2Set.add("FFF");
demo2Set.add("DDD");
demo2Set.add("AAA");
System.out.println("demo3Set=>" + demo2Set);// 3
}
class Demo implements Comparable<Demo> {
private Integer num;
public Demo(Integer num) {
this.num = num;
}
/**
* 重写了equals方法,只要值相同就可以认为是同一个对象
*
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj instanceof Demo) {
Demo demo = (Demo) obj;
if (num.equals(demo.num)) {
return true;
}
}
return false;
}
/**
* 重写hashCode方法,返回当前num值
*
* @return
*/
@Override
public int hashCode() {
return num;
}
@Override
public String toString() {
return num.toString();
}
@Override
public int compareTo(Demo obj) {
if ((this.num - obj.num) > 0) {
return -1;
} else if ((this.num - obj.num) < 0) {
return 1;
} else {
return 0;
}
}
}
}
- 可以看出有
两种
排序方式,一种是在类内部实现Comparable接口,重写CompareTo方法, 一种是在构造方法
或者Collections.sort(set,comparator)
自定义构造器Comparator
实现排序 不需要重写equals和hashCode方法
,因此是通过在比较器中返回 "0"
,来判断来两个元素是否相同
,从而实现去重
的.
TreeSet接口间接的实现了SortedSet接口,具有以下方法
方法 | 说明 |
---|---|
public Comparator<? super E> | 返回排序有关联的比较器 |
public E first() | 返回集合中的第一个元素 |
public SortedSet<E> headSet(E toElement) |
返回从开始到指定元素的集合 |
public E last() | 返回最后一个元素 |
public SortedSet<E> subSet(E fromElement,E toElement) |
返回指定对象间的元素 |
public SortedSet<E> tailSet( 从指定元素到最后) |
返回排序有关联的比较器 |
EnumSet
- EnumSet 是一个专为枚举设计的集合类,EnumSet中的
所有元素
都必须是指定枚举类型
的枚举值,该枚举类型
在创建
EnumSet时显式或隐式
地指定 - EnumSet以在Enum类内枚举值的定义顺序来决定
- EnumSet在内部以
位向量
的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll()和retainAll()方法)时,如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。 - EnumSet集合
不允许添加null
元素,否则将抛出NullPointerException异常。 - EnumSet类
没有暴露任何构造器来创建该类的实例
,程序应该通过它提供的类方法来创建EnumSet对象。 - 如果只是想判断EnumSet是否包含null元素或试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。
常用方法
方法 | 说明 |
---|---|
EnumSet allOf(Class elementType): | 创建一个包含指定枚举类里所有枚举值的EnumSet集合。 |
EnumSet complementOf(EnumSet e): | 创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,新EnumSet集合包含原EnumSet集合所不包含的、此类枚举类剩下的枚举值(即新EnumSet集合和原EnumSet集合的集合元素加起来是该枚举类的所有枚举值)。 |
EnumSet copyOf(Collection c): | 使用一个普通集合来创建EnumSet集合。 |
EnumSet copyOf(EnumSet e): | 创建一个指定EnumSet具有相同元素类型、相同集合元素的EnumSet集合。 |
EnumSet noneOf(Class elementType): | 创建一个元素类型为指定枚举类型的空EnumSet。 |
EnumSet of(E first,E…rest): | 创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举类。 |
EnumSet range(E from,E to): | 创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。 |
示例代码
public class TestEnumSet {
//创建一个枚举
enum WeatherEnum{
SPRING("春天"),
SUMMER("夏天"),
FALL("秋天"),
WINTER("冬天");
private String value;
WeatherEnum(String value) {
this.value = value;
}
}
@Test
public void testEnumSet(){
//1.创建一个包含Session(枚举类)里所有枚举值的EnumSet集合
EnumSet e1 = EnumSet.allOf(WeatherEnum.class);
System.out.println("EnumSet.allOf=>"+e1);//[SPRING, SUMMER, FAIL, WINTER]
Iterator<WeatherEnum> iterator = e1.iterator();
while (iterator.hasNext()){
WeatherEnum weatherEnum = iterator.next();
System.out.println("-----iterator=>"+weatherEnum.value);
}
//2.创建一个空EnumSet
EnumSet e2 = EnumSet.noneOf(WeatherEnum.class);
System.out.println("e2.noneOf=>"+e2);//[]
//3. add()空EnumSet集合中添加枚举元素
e2.add(WeatherEnum.SPRING);
e2.add(WeatherEnum.SUMMER);
System.out.println("e2.add=>"+e2);//[SPRING, SUMMER]
//4. 以指定枚举值创建EnumSet集合
EnumSet e3 = EnumSet.of(WeatherEnum.SPRING,WeatherEnum.FALL);
System.out.println("EnumSet.of=>"+e3);//[SPRING, FAIL]
//5.创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。
EnumSet e4 = EnumSet.range(WeatherEnum.SPRING,WeatherEnum.FALL);
System.out.println("EnumSet.range=>"+e4);//[SPRING, SUMMER, FAIL]
//6.创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,
// 新EnumSet集合包含原EnumSet集合所不包含的枚举值
EnumSet e5 = EnumSet.complementOf(e4);
System.out.println("EnumSet.complementOf(e4)=>"+e5);//[WINTER]
//7.创建一个集合
Set set = new HashSet();
set.clear();
set.add(WeatherEnum.SPRING);
set.add(WeatherEnum.WINTER);
//复制Collection集合中的所有元素来创建EnumSet集合
EnumSet es = EnumSet.copyOf(set);
System.out.println("EnumSet.copyOf=>"+es);
}
}
执行结果: