当当当当当当当,本来打算出去浪来着,想想还是把这个先一起写完吧,毕竟这篇的主角跟我一样是一个超级偷懒的角色——LinkedHashSet,有多偷懒?看完你就知道了。
本篇将从以下几个方面对LinkedHashSet进行介绍:
1、LinkedHashSet中的特性
2、LinkedHashSet源码分析
3、LinkedHashSet应用场景
本篇预计需要食用10分钟,快的话五分钟也够了,完全取决于各位看官心情。
LinkedHashSet中的特性
前面已经介绍过了HashSet,本篇要介绍的LinkedHashSet正是它的儿子,作为HashSet的唯一法定继承人,可以说是继承了HashSet的全部优点——懒,并且将其发挥到了极致,这一点在之后的源码分析里可以看到。
LinkedHashSet继承了HashSet的全部特性,元素不重复,快速查找,快速插入,并且新增了一个重要特性,那就是有序,可以保持元素的插入顺序,所以可以应用在对元素顺序有要求的场景中。
先来看一个小栗子:
public class LinkedHashSetTest { public static void main(String[] args){ LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>(); HashSet<String> hashSet = new HashSet<>(); for (int i = 0; i < 10; i++) { linkedHashSet.add("I" + i); hashSet.add("I" + i); } System.out.println("linkedHashSet遍历:"); for (String string : linkedHashSet){ System.out.print(string + " "); } System.out.println(); System.out.println("hashSet遍历:"); for (String string : hashSet){ System.out.print(string + " "); } } }
linkedHashSet遍历:
I0 I1 I2 I3 I4 I5 I6 I7 I8 I9
hashSet遍历:
I9 I0 I1 I2 I3 I4 I5 I6 I7 I8
可以看到,在HashSet中存储的元素遍历是无序的,而在LinkedHashSet中存储的元素遍历是有序的。嗯,它和HashSet就这唯一的区别了。
LinkedHashSet源码分析
那么问题来了,LinkedHashSet中的元素为什么会是有序的呢?难道也跟LinkedHashMap一样用了链表把元素都拴起来了?别着急,让我们一起来看看源码。
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { private static final long serialVersionUID = -2851667679971038690L; /** * 使用指定初始容量和装载因子构造一个空的LinkedHashSet实例 */ public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); } /** * 使用指定的初始容量和默认的装载因子构造一个空的LinkedHashSet实例 */ public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); } /** * 使用默认的初始容量和默认的装载因子构造一个空的LinkedHashSet实例 */ public LinkedHashSet() { super(16, .75f, true); } /** * 构造一个与指定集合有相同元素的空LinkedHashSet实例,使用默认的装载因子和能够容纳下指定集合所有元素的合适的容量。 */ public LinkedHashSet(Collection<? extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); addAll(c); } /** * 可分割式迭代器 */ @Override public Spliterator<E> spliterator() { return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED); } }
你没看错,这应该是所有容器类中最短小精悍的了,这也就是开头为什么说这家伙懒到家的原因了。
可是,LinkedHashSet中并没有覆盖add方法,只是加了几个构造函数和一个迭代器,其他全部和HashSet一毛一样,为什么它就能有序呢??
玄机就藏在这个构造函数中,这几个构造函数其实都是调用了它父类(HashSet)的一个构造函数:
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
嗯,这个构造函数跟其他构造函数唯一的区别就在于,它创建的是一个LinkedHashMap对象,所以元素之所以有序,完全是LinkedHashMap的功劳。该构造函数是默认访问权限的,所以在HashSet中是不能直接调用的,留给子类去调用或覆盖(讲道理使用protected权限不是更合理吗)。
LinkedHashSet应用场景
现在假设这样的场景,现在我有一堆商品,商品有名称和价格,但是里面有重复商品,我希望把重复的商品(名称和价格都一样的)过滤掉,只保留一个,并且希望输出后的顺序跟原来的顺序一致。嗯,这时候LinkedHashSet就派上用场了。(废话,那是你特意给主角加的戏)
商品的结构是这样的:
public class Commodity { private String name; private Double price; public Commodity(String name, Double price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } @Override public String toString() { return "Commodity{" + "name='" + name + '\'' + ", price=" + price + '}'; } }
来用LinkedHashSet解决一下这个需求:
public class CommodityTest { public static void main(String[] args){ //有7个商品,A和E、D和G信息完全一样,希望能过滤掉,只保留一个,C和F虽然名称一样,但是价格不同,希望保留 Commodity commodityA = new Commodity("Iphone6S", 6666.66); Commodity commodityB = new Commodity("Iphone7", 7777.77); Commodity commodityC = new Commodity("Iphone8", 8888.88); Commodity commodityD = new Commodity("IphoneX", 9999.99); Commodity commodityE = new Commodity("Iphone6S", 6666.66); Commodity commodityF = new Commodity("Iphone8", 6666.66); Commodity commodityG = new Commodity("IphoneX", 9999.99); LinkedHashSet<Commodity> commodities = new LinkedHashSet<>(); commodities.add(commodityA); commodities.add(commodityB); commodities.add(commodityC); commodities.add(commodityD); commodities.add(commodityE); commodities.add(commodityF); commodities.add(commodityG); for (Commodity commodity : commodities){ System.out.println(commodity); } } }
输出如下:
Commodity{name='Iphone6S', price=6666.66} Commodity{name='Iphone7', price=7777.77} Commodity{name='Iphone8', price=8888.88} Commodity{name='IphoneX', price=9999.99} Commodity{name='Iphone6S', price=6666.66} Commodity{name='Iphone8', price=6666.66} Commodity{name='IphoneX', price=9999.99}
翻...翻...翻车了?虽然输出的顺序与插入的顺序是一致的最后一个IphoneX和Iphone6S并没有被去掉,怎么回事呢?说好的可以去重呢?
嗯,别慌,我既然可以让车翻过来,那就有办法让它再翻回去。
想要利用LinkedHashSet自动去重性质,那么我们就要先理解它是怎样去重的,其实和HashSet是一样的,往里面添加元素的时候,其实是这样的:
public boolean add(E e) { return map.put(e, PRESENT)==null; }
所以当该元素在map中存在的时候,map.put方法就会返回旧值,此时add方法会返回false,在查找map中put元素的时候,会先调用hashCode方法得到该元素的hashCode值,然后查看table中是否存在该hashCode值,如果存在则调用equals方法重新确定是否存在该元素,如果存在,则更新value值,否则将新的元素添加到HashMap中,如果没有覆盖过hashcode方法,那么就会使用对象默认的hashcode,这个值跟对象成员变量的具体值就没有直接关联了,所以我们需要覆盖hashcode方法和equals方法。
public class Commodity { private String name; private Double price; public Commodity(String name, Double price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } @Override public String toString() { return "Commodity{" + "name='" + name + '\'' + ", price=" + price + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Commodity commodity = (Commodity) o; return Objects.equals(name, commodity.name) && Objects.equals(price, commodity.price); } @Override public int hashCode() { return Objects.hash(name, price); } }
这里用的equals方法和hashCode方法是很通用的,在其他地方也可以使用类似的写法,现在再来重新跑一下程序看下:
Commodity{name='Iphone6S', price=6666.66} Commodity{name='Iphone7', price=7777.77} Commodity{name='Iphone8', price=8888.88} Commodity{name='IphoneX', price=9999.99} Commodity{name='Iphone8', price=6666.66}
好的,现在已经达到我们想要的效果了。任务完成,午饭加个蛋。