java集合之Set
写在前面
集合类是java语言对数据结构
的实现
Set接口介绍
java.util.Set
接口和java.util.List
接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格。
Set接口特点:
-
元素无序。
-
元素不能重复。
HashSet
java.util.HashSet
底层采用哈希表存储数据,实际上是一个HashMap
中的Key值。哈希表传送门java.util.HashSet
允许使用 null 元素(只能有一个)。java.util.HashSet
是不同步的。(多线程)
private transient HashMap<E,Object> map;
如何存储元素
Set集合元素不重复的原理
源码如下:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 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;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
图解:
简单的面试题:
- (问)为什么重写equals一定要重写hashcode?
- (答)因为HashSet或Map等集合中判断元素是否相等时,会先判断hashcode是否相等,如果相等,再通过equals判断两个元素是否相等。所以为了避免我们认为相等但是逻辑判断却不相等的情况出现,自定义类重写equals时,也必须同时重写hashcode方法
代码演示:
public class Demo03 {
public static void main(String[] args) {
Set<Person> set = new HashSet();
Person p1 = new Person("钢铁侠",28);
Person p2 = new Person("黑寡妇",28);
Person p3 = new Person("黑寡妇",28);
Person p4 = new Person("蜘蛛侠",18);
set.add(p1);
set.add(p2);
set.add(p3);
set.add(p4);
//[Person(name=黑寡妇, age=28), Person(name=黑寡妇, age=28), Person(name=钢铁侠, age=28), Person(name=蜘蛛侠, age=18)]
System.out.println(set);
}
}
@AllArgsConstructor
@ToString
class Person{
private String name;
private Integer age;
}
p2和p3是两个完全不同的对象,所以它们的地址值不同,所以它们的哈希值不同,所以Set集合判定它们并不重复。
但是实际上,它们肯定是重复的元素(姓名相同,年龄相同)。
修改代码如下:
public class Demo03 {
public static void main(String[] args) {
Set<Person> set = new HashSet();
Person p1 = new Person("钢铁侠",28);
Person p2 = new Person("黑寡妇",28);
Person p3 = new Person("黑寡妇",28);
Person p4 = new Person("蜘蛛侠",18);
set.add(p1);
set.add(p2);
set.add(p3);
set.add(p4);
//[Person(name=黑寡妇, age=28), Person(name=钢铁侠, age=28), Person(name=蜘蛛侠, age=18)]
System.out.println(set);
}
}
@AllArgsConstructor
@ToString
class Person{
private String name;
private Integer age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name) &&
Objects.equals(age, person.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
LinkedHashSet
java.util.LinkedHashSet
是哈希表和链表(记录元素顺序)组合的一个数据存储结构。java.util.LinkedHashSet
是有序的。java.util.LinkedHashSet
迭代访问元素的性能优于java.util.HashSet
,插入元素性能稍微逊色于java.util.HashSet
简单代码实例:(略)
TreeSet
java.util.TreeSet
是基于TreeMap的NavigableSet 实现,底层的数据结构是红黑树(平衡二叉树)。java.util.TreeSet
使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。java.util.TreeSet
是不同步的(多线程)。
TreeSet如何存储元素
- 存储的第一个元素作为根节点。
- 从第二个开始,每个元素从根节点开始比较,大于根节点,右边走好,小于根节点,左边走好。
- 相等元素,拜拜了您内,下一个。
TreeSet的两种排序方式
- 自然排序
- 比较器排序
简单代码演示
public class Demo04 {
public static void main(String[] args) {
//自定义排序规则第一种方式:传递Comparator匿名对象
TreeSet<Integer> ts = new TreeSet<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//默认升序排序
//return o1 - o2; //[-27, -4, 3, 15, 69]
//降序排序
return o2 - o1; //[69, 15, 3, -4, -27]
}
});
ts.add(15);
ts.add(3);
ts.add(-27);
ts.add(69);
ts.add(-4);
System.out.println(ts);
//自定义排序规则第二种方式:集合内存储的对象实现Comparable接口,并重写其compareTo方法
TreeSet<Hero> ts2 = new TreeSet<>();
Hero h1 = new Hero("钢铁侠",38);
Hero h2 = new Hero("蜘蛛侠",18);
Hero h3 = new Hero("雷神",1500);
ts2.add(h1);
ts2.add(h2);
ts2.add(h3);
System.out.println(ts2);
}
}
@Data
@AllArgsConstructor
class Hero implements Comparable<Hero>{
private String name;
private Integer age;
//实现自定义排序规则
@Override
public int compareTo(Hero o) {
//升序
//return this.age - o.age; //[Hero(name=蜘蛛侠, age=18), Hero(name=钢铁侠, age=38), Hero(name=雷神, age=1500)]
//降序
return o.age - this.age; //[Hero(name=雷神, age=1500), Hero(name=钢铁侠, age=38), Hero(name=蜘蛛侠, age=18)]
}
}
补充01(可变参数)
在JDK1.5之后,如果一个方法需要接受多个参数,且参数类型一致,我们可以使用可变参数,格式如下:
修饰符 返回值类型 方法名(参数类型... 形参名){ }
等价于:
修饰符 返回值类型 方法名(参数类型[] 形参名){ }
只是后面这种定义,在调用时必须传递数组,而前者可以直接传递数据即可。
可变参数:参数类型确定,个数不确定。
可变参数的便捷之处在于,传递参数时不用创建数组,直接将数组中的元素作为实际参数进行传递。
实际上在java代码编译时,会先将传递的参数封装到一个数组中,然后再进行传递。
public class Demo06 {
public static void main(String[] args) {
int sum = Demo06.getSum(1,2,3,4,5,6,7,8,9,10);
System.out.println(sum);
sum = Demo06.getSum(10,20,30,40,50);
System.out.println(sum);
}
public static int getSum(int...arr){
System.out.println(arr);
System.out.println(arr.length);
int sum = 0;
for (int i : arr) {
sum += i;
}
return sum;
}
}
注意:
如果某个java方法拥有多参数,且参数中包含可变参数,那么可变参数一定要写在参数列表的末尾。
补充02(Collections)
java.utils.Collections
是集合工具类,用来对集合进行操作。部分方法如下:
public static <T> boolean addAll(Collection<T> c, T... elements)
:往集合中添加元素。public static void shuffle(List<?> list)
:随机打乱集合顺序。public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序。