系列文章目录
前言
本文介绍了Java中常用的集合框架HashSet和TreeSet,本文为系列文章,接下来还会介绍Map,如果喜欢的话,可以点赞,收藏,关注。
一、Set类型的特点
- 无序的:Set中的元素没有特定的顺序。添加到Set的元素不会保持原来的顺序。
- 无重复的元素:Set的一个重要特性是它不允许重复元素。如果尝试添加一个已经存在的元素,Set将不会接受它。
- 线程不安全:Set接口的实现类(例如HashSet和TreeSet)不是线程安全的。如果你在多线程环境中使用,需要额外的同步措施。
- 不允许null元素:Set不允许包含null元素。
- 没有父/子关系:与List不同,Set没有子列表的概念。
- 基于Map实现:大多数Set的实现(如HashSet和LinkedHashSet)是基于Map实现的。这意味着添加和删除操作通常具有O(1)的复杂度,而查找操作具有O(n)的复杂度(在最佳情况下,如在TreeSet中,查找操作具有O(log n)的复杂度)。
- 用于存储单例:由于Set不允许重复元素,所以它是存储单例的理想结构。
- 支持泛型:从Java 5开始,Set接口的实现类(如HashSet和TreeSet)支持泛型,这使得在编译时检查类型错误成为可能。
二、HashSet案例
HashSet
public class HashSetTest {
public static void main(String[] args) {
Set set = new HashSet();
set.add("苹果");
set.add("苹果");
set.add("香蕉");
set.add("椰子");
set.add("桃子");
//因为set是无序的,因为没有下标,所以只能使用增强型for循环
for (Object o : set) {
System.out.println(o);
}
}
}
/**
运行结果:不可重复,且无序的
桃子
苹果
香蕉
椰子
*/
三、Set的去重原理
先创建一个复杂类型Dog
public class Dog {
private String name;
private String type;
//省略了get和set方法以及构造函数
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dog dog = (Dog) o;
return Objects.equals(name, dog.name) && Objects.equals(type, dog.type);
}
}
HashSet
public class HashSetTest {
public static void main(String[] args) {
Set set = new HashSet();
set.add(new Dog("a","a"));
//与第一条相同
set.add(new Dog("a","a"));
set.add(new Dog("b","b"));
set.add(new Dog("c","c"));
set.add(new Dog("d","d"));
set.add(new Dog("e","e"));
//因为set是无序的,因为没有下标,所以只能使用增强型for循环
for (Object o : set) {
System.out.println((Dog)o);
}
}
}
/*
运行结果:
Dog{name='b', type='b'}
Dog{name='e', type='e'}
Dog{name='a', type='a'}
Dog{name='d', type='d'}
Dog{name='a', type='a'}
Dog{name='c', type='c'}
**/
此时,复杂类型并没有被去重!!!
所以,此时需要在Dog类中重写hashCode方法
@Override
public int hashCode() {
super.hashCode();
return Objects.hash(name, type);
}
再执行,成功去重
Dog{name='a', type='a'}
Dog{name='b', type='b'}
Dog{name='c', type='c'}
Dog{name='d', type='d'}
Dog{name='e', type='e'}
Set去重的原理主要基于对象的hashCode()和equals()方法。当添加元素到Set时,首先会计算元素的hashCode,然后在Set内部查找具有相同hashCode的元素。如果找到了具有相同hashCode的元素,那么就会调用equals()方法来比较这个元素和Set内部存储的元素是否相同。如果equals()返回true,那么就会忽略新添加的元素,实现去重,简而言之,Set去重一定要重写hashCode()和equals()方法。
四、TreeSet算法依赖于一个比较接口
代码案例
public class TreeSetTest {
public static void main(String[] args) {
Set set = new TreeSet();
set.add("a");
set.add("a");
set.add("b");
set.add("c");
set.add("d");
set.add("e");
for (Object o:set){
System.out.println(o);
}
}
}
/**
添加字符串已经去重
a
b
c
d
e
*/
当添加的是对象时
public class TreeSetTest {
public static void main(String[] args) {
Set set = new TreeSet();
set.add(new Dog("a","a"));
set.add(new Dog("a","a"));
set.add(new Dog("b","b"));
set.add(new Dog("c","c"));
set.add(new Dog("d","d"));
set.add(new Dog("e","e"));
for (Object o : set) {
System.out.println((Dog)o);
}
}
}
报错
出现这个问题是因为,TreeSet在添加遍历复杂类型时,必须实现一个比较接口,Comparable,同时重写compareTo(T o)方法。
public class Dog implements Comparable<Dog>{
private String name;
private String type;
//省略了get和set方法已经全参构造函数
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dog dog = (Dog) o;
return Objects.equals(name, dog.name) && Objects.equals(type, dog.type);
}
@Override
public int hashCode() {
return Objects.hash(name, type);
}
@Override
public int compareTo(Dog o) {
return name.compareTo(o.name)+type.compareTo(o.type);
}
}
此时再运行,没有报错
Dog{name='a', type='a'}
Dog{name='b', type='b'}
Dog{name='c', type='c'}
Dog{name='d', type='d'}
Dog{name='e', type='e'}
为什么直接添加字符串不需要实现Comparable接口
原因:String类型中已经自动实现了Comparable了。
总结
Set集合对开发产生了重要的影响。首先,它提供了一种有效的方式来存储和处理不重复的数据元素,这有助于减少数据冗余和不一致性。其次,Set集合框架提高了代码的可读性和可维护性,因为它提供了一组标准的方法来操作集合,如添加、删除和检查元素等。
此外,Set集合也使得代码更加健壮,因为它能够将运行时期的异常转换成编译时的错误,这有助于提早发现并解决潜在的错误。例如,如果你试图将一个重复的元素添加到Set中,编译器将会报错,这避免了在运行时期出现重复元素的错误。