Java的Set接口java.util.Set表示一组唯一的对象,换句话说,同一个对象在Set中不能多次出现。Set接口是java.util.Collection接口的子类型,即Set继承自Collection。
可以将任何Java对象添加到JavaSet。如果Set没有使用Java泛型进行类型化,那么甚至可以在同一个Set中混合不同类型的对象,不过通常情况下我们不会这么使用Set。
这篇文章我们详细地把Set在Java中的应用梳理一下,文章大纲如下:
Set和List的区别
Set和List接口彼此非常相似。两个接口都表示一个元素的集合。存在一些显著差异。这些差异反映在Set和List接口包含的方法中。Set和List接口之间的第一个区别是,相同的元素在Set中不能出现多次,List则是可以有重复元素。Set和List接口之间的第二个区别是,Set中的元素没有可保证的内部顺序。List中的元素具有内部顺序,并且元素可以按该顺序进行迭代。
初识Set
首先来一个简单示例,让你了解Set是怎么工作的:
package com.example.learnset;
import java.util.HashSet;
public class SetExample {
public static void main(String[] args) {
Set setA = new HashSet();
String element = "demo element";
setA.add(element);
System.out.println( setA.contains(element) );
}
}
复制代码
此示例创建一个HashSet,它是Java提供的Set接口实现类,紧接着向Set中添加一个字符串对象,最后检查Set是否包含刚刚添加的元素。
Java提供的Set实现类
作为Collection的子类型,Collection接口中的所有方法在Set接口中也包含。由于Set是一个接口,需要实例化该接口的具体实现才能使用。不过Java的集合框架中的已经提供了一些非常优秀的实现,我们可以在以下Set实现中进行选择使用:
这些Set实现中的每一个在迭代Set时的元素顺序以及插入和访问Set中的元素的时间复杂度都略有不同。
HashSet底层由HashMap实现。当对HashSet进行迭代时,它不保证元素的顺序。
LinkedHashSet与HashSet的不同之处在于,它保证了迭代期间元素的顺序与它们插入LinkedHashSet的顺序相同。重新插入一个已经在LinkedHashSet中的元素不会改变这个顺序。
TreeSet也保证了迭代时元素的顺序,但元素的排序顺序由它们的自然顺序或由特定的Comparator实现确定。
创建Set实例
以下是如何创建Set实例的几个示例:
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
public class SetExample {
public static void main(String[] args) {
Set setA = new HashSet();
Set setB = new LinkedHashSet();
Set setC = new TreeSet();
}
}
复制代码
默认情况下,可以将任何对象放入Set,但是从Java5开始,Java引入泛型后让限制可插入到Set中的对象类型成为可能。下面是一个例子:
Set<MyObject> set = new HashSet<MyObject>();
复制代码
现在只能将MyObject实例插入这个Set中。然后,无需进行强制类型转换就可以访问和迭代其元素。
for(MyObject anObject : set){
//do someting to anObject...
}
复制代码
除非有充分的理由不对Set使用泛型约束,否则创建Set实例时应始终使用泛型约束。
不可变的Set
下面是一个与我国行情不符的功能点,虽然用不上,也先学一下。
从Java9开始,Set接口包含一个静态工厂方法,可以创建不可修改的Set实例。Set静态工厂方法of接收零个或多个参数创建不可变的Set。
Set set = Set.of(); // 创建一个空的,不可变的Set
Set<String> emptyStringSet = Set.<String>of();
Set<String> immutableSet = Set.<String>of("val1", "val2", "val3");
复制代码
添加元素到Set
添加单个元素
要向Set添加元素,需要调用其add方法。该方法继承自Collection接口。下面一些例子:
Set<String> setA = new HashSet<>();
setA.add("element 1");
setA.add("element 2");
setA.add("element 3");
复制代码
求两个Set的并集
与List接口一样,Set也有一个名为addAll的方法,将另一个集合对象中的所有元素添加到Set中。
Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
set.add("three");
Set<String> set2 = new HashSet<>();
set2.add("four");
set2.addAll(set);
复制代码
执行此代码示例后,set2将包含四个字符串元素"one","two","three","four"。
从Set中删除元素
从Set中删除指定元素
可以通过调用remove方法从Set中删除指定元素:
set.remove("object-to-remove");
复制代码
List接口中还提供了根据索引删除元素的remove方法,但是无法根据Set中的索引删除对象,因为元素的顺序取决于Set具体的实现。
清空Set
可以使用clear方法从Set中删除所有元素:
set.clear();
复制代码
删除在另外一个集合中也存在的元素
Set的removeAll方法,它删除Set中的所有也存在于另一个Collection中的元素。在集合论中,这被称为求该Set与其他集合的差集。下面是一个从Set中删除同样也存在于另一个Set中的元素的示例:
Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
set.add("three");
Set set2 = new HashSet();
set2.add("three");
set.removeAll(set2);
复制代码
运行例程,set中包含字符串元素"one"和"two"。元素"three"被删除,因为它也存在于set2中。
求两个Set的交集
跟List一样Set也有RetainAll方法,继承自Collection接口,该方法保留Set中的所有元素,这些元素也存在于另一个Collection中。存在与Set中但在其他Collection中不存在的所有元素都将被删除。在集合论中,这被称为两个Set之间的交集。
Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
set.add("three");
Set<String> set2 = new HashSet<>();
set2.add("three");
set2.add("four");
set.retainAll(set2);
复制代码
运行这段Java代码后,该Set将只包含字符串元素"three"。它是set和set2都存在的元素。
获取Set的尺寸
使用size方法检查Set的大小。Set的大小是Set中包含的元素的数量。
et<String> set = new HashSet<>();
set.add("123");
set.add("456");
set.add("789");
int size = set.size(); // size = 3
复制代码
检查Set是否为空
通过调用Set上的isEmpty方法来检查JavaSet是否为空,为空意味着Set不包含任何元素。
Set<String> set = new HashSet<>();
boolean isEmpty = set.isEmpty();
复制代码
检查Set是否包含指定元素
可以通过调用contains方法检查JavaSet是否包含给定元素:
Set<String> set = new HashSet<>();
set.add("123");
set.add("456");
boolean contains123 = set.contains("123");
复制代码
为了确定Set是否包含元素,contains方法将在内部迭代Set元素并将每个元素与作为参数传递进来的对象进行比较。比较使用元素的Javaequals方法来检查元素是否等于参数。由于可以向Set添加空值,因此也可以检查Set是否包含空值。以下是检查Set是否包含空值的方法:
set.add(null);
containsElement = set.contains(null);
System.out.println(containsElement);
复制代码
显然,如果contains的输入参数为null,则contains方法不会使用equals方法来比较每个元素,而是使用==运算符进行比较。
把Set转成List
可以通过创建List并调用其addAll方法,将Set作为参数传递给addAll方法,这样能将Set转换为List。下面是一个将Set转换为List的示例:
Set<String> set = new HashSet<>();
set.add("123");
set.add("456");
List<String> list = new ArrayList<>();
list.addAll(set);
复制代码
迭代Set
有两种方法可以迭代Set的元素:
从Set获取Iterator进行迭代使用for-each循环进行迭代
使用Iterator迭代
跟List一样,可以通过调用iterator方法从Set中获取Iterator。
Set<String> setA = new HashSet<>();
setA.add("element 1");
setA.add("element 2");
setA.add("element 3");
Iterator<String> iterator = set.iterator();
while(iterator.hasNext(){
String element = iterator.next();
}
复制代码
使用foreach循环迭代
Set接口实现了Java的Iterable接口,这就是为什么可以使用for-each循环迭代Set的元素。
Set<String> set = new HashSet<>();
for(String str : set) {
System.out.println(str);
}
复制代码
迭代Set还可以通过StreamAPI实现。用Set创建一个Stream,调用forEach方法进行元素迭代
Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
set.add("three");
Stream<String> stream = set.stream();
stream.forEach((element) -> { System.out.println(element); });
复制代码
Stream的具体用法,等到了Stream章节再详细学习。
有序的Set
上面操作都是通过HashSet给大家演示的,因为都是实现自JavaCollection的Set接口,所以其他几种Set--LinkedSet、TreeSet也都可以使用与HashSet相同的操作方法进行操作。
不过Set的几种实现类除了操作的相似性之外,也具有不同的地方,不然Java提供这么多实现类也就没啥意义了,在这些不同中最明显能让使用者感觉出来的就是他们的元素排列顺序、或者叫迭代时元素的顺序是不同的。
HashSet因为底层是由HashMap实现的,所以它是个无序的集合,而LinkedHashSet是可以保证其元素的排列顺序与元素的插入顺序保持一致的,其实这点跟List很像。重新插入一个已经在LinkedHashSet中的元素不会改变这个元素已有的顺序。
另外一种有序的Set类--TreeSet中的元素是按自然排序或者用户指定比较器排序的Set,就是说插入元素到TreeSet时,是按照排序规则放到某个位置的,这点跟数据库的索引有点像,实际上也确实是这样,TreeSet依赖的底层数据结构是红黑树,每次往树中插入节点是会排列树的。
比如现在我们有这样一个保存字符串的TreeSet实例
import java.util.TreeSet;
public class TreeSetDemoApp {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add("ccc");
ts.add("aaa");
ts.add("ddd");
ts.add("bbb");
System.out.println(ts); // [aaa, bbb, ccc, ddd]
}
}
复制代码
虽然它的元素ccc第一个插入,可以在输出元素的时候,确实按照字符串的排序规则进行排列的,把它排在了第三位。
那么怎么给TreeSet给元素排序的规则是什么呢?如果元素的类型本身是可比较的类型,就按照类型的比较规则进行排序,什么是可比较类型呢?即实现了Comparable接口的类型都是可比较类型,这个接口里只有一个compareTo方法。我们用的Java自带的内置类都已经实现过这个接口。
假如你把自定义Class的实例,放在TreeSet中后,只要这个Class实现了Comparable接口,那么TreeSet就会按照实现里的规则对它持有的元素进行排序。
比如下面的Person类,就是通过CompareTo方法指定了按照对象的age属性值进行排序,那么放入到TreeSet后,TreeSet就会按照这个规则对元素进行排序。
class Person implements Comparable {
private String name;
private int age;
private String gender;
public Person() {
}
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// 省略 Getter 和 Setter
@Override
public int compareTo(Object obj) {
Person p = (Person) obj;
System.out.println(this+" compareTo:"+p);
if (this.age > p.age) {
return 1;
}
if (this.age < p.age) {
return -1;
}
return this.name.compareTo(p.name);
}
}
复制代码
除此之外,还有一种方法是给TreeSet容器设置指定一个排序器,比如上面的例子,不让Person实现Comparable接口的话也可以通过给TreeSet指定这样一个排序起达到同样的效果。
class MyComparator implements Comparator {
public int compare(Person o1, Person o2) {
Person p = (Person) obj;
System.out.println(this+" compareTo:"+p);
if (this.age > p.age) {
return 1;
}
if (this.age < p.age) {
return -1;
}
return this.name.compareTo(p.name);
}
}
public class TreeSetDemoApp {
public static void main(String[] args) {
TreeSet<Person> ts2 = new TreeSet<>(new MyComparator());
ts2.add(new Person("cc", 17, "男"));
ts2.add(new Person("dd", 17, "女"));
ts2.add(new Person("aa", 15, "女"));
ts2.add(new Person("dd", 15, "女"));
System.out.println(ts2);
}
}
复制代码
上面示例中的TreeSet在排列元素的时候,会先按照元素的年龄大小进行排序,相等了再按照名字进行排序,所以元素的排列的顺序会是这样。
[Person [name=aa, age=15, gender=女], Person [name=dd, age=15, gender=女], Person [name=cc, age=17, gender=男], Person [name=dd, age=17, gender=女]]
复制代码
Set跟List的主要区别是不允许重复对象,这在有些场景会减少复杂度,提高程序的效率。Set中的HashSet是无序集合,不能像List一样使元素的排列顺序和插入顺序保存一致,想要使用有序Set可以选择LinkedHashSet和TreeSet,TreeSet更是能让我们指定比较器,让元素在插入时就进行好排序。
文章为作者独立观点,不代表 股票程序化软件自动交易接口观点