首页 > 代码库 > Java基础-集合框架之Set
Java基础-集合框架之Set
ps:本人小菜一枚,所以本文是自我学习后的一篇总结,大虾请您飘过
Set类概述
Set是最简单的集合,集合中的对象不按照特定的方式排序,并且没有重复的对象。
Set集合里多个对象之间没有明显的顺序,基本与Collection方法相同。只是行为不同(Set不允许包含重复元素)。
HashSet
HashSet按Hash算法来存储集合的元素,因此具有很好的存取和查找性能。
特点:
HashSet不是同步的,多个线程访问是需要通过代码保证同步
集合元素值可以使null。
注意:HashSet存储自定义对象必须重写hashCode()和equals()方法
重写hashCode基本的指导:
1.初始化一个整形变量,为此变量赋予一个非零的常数值,比如下文:int result = 1;
2.为对象内每个有意义的域f(即每个可以做equals()操作的域)计算出一个int散列码c
boolean c=(f?0:1) byte,char,short,int c=(int)f long c=(int)(f^(f>>>32)) float c=Float.floatToIntBits(f) double c=Double.doubleToLongBits(f) 若结果为long类型则用上述规则处理long得到int Object c=f.hashCode() 数组 对每个元素应用上述规则3.合并计算得到散列码:
result = 31 * result + c;
4.返回 result
5.检查hashCode() 最后生成的结果,确保相同的对象有相同的散列码
class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } /* * 为什么选择31? * 1,31是一个质数,质数是能被1和自己本身整除的数,减少了相乘出现同值的情况 * 2,31这个数既不大也不小 * 3,31这个数好算,2的五次方-1,2向左移动5位 */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + age; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj)//调用的对象和传入的对象是同一个对象 return true; if (obj == null)//传入的对象为null return false; if (getClass() != obj.getClass())//判断两个对象对应的字节码文件是否是同一个字节码 return false; Person other = (Person) obj; if (age != other.age)//以下比较对象的域 return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } }
测试类:
public static void main(String[] args) { HashSet<Person> hashSet = new HashSet<>(); hashSet.add(new Person("张三", 23)); hashSet.add(new Person("张三", 23)); hashSet.add(new Person("李四", 24)); hashSet.add(new Person("李四", 23)); hashSet.add(new Person("王五", 25)); hashSet.add(new Person("赵六", 25)); for (Person person : hashSet) { System.out.println(person); } }
结果
Person [name=赵六, age=25] Person [name=张三, age=23] Person [name=李四, age=24] Person [name=李四, age=23] Person [name=王五, age=25]看到了吧,上面的结果是不是去除了重复,而且是无序的
HashSet如何保证元素的唯一性呢?
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{ private transient HashMap<E,Object> map; private static final Object PRESENT = new Object(); public HashSet() { map = new HashMap<>(); } public Iterator<E> iterator() { return map.keySet().iterator(); } public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean contains(Object o) { return map.containsKey(o); } public boolean add(E e) { return map.put(e, PRESENT)==null; } public boolean remove(Object o) { return map.remove(o)==PRESENT; } public void clear() { map.clear(); } }
看其源码(JDK1.8)可知,其底层依靠的是HashMap,由于未涉及到HashMap,便对其原理做简单概述,后面会详细介绍
使用Set集合都是需要去掉重复元素的, 如果在存储的时候逐个equals()比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用equals()方法的次数
当存储对象的时候, 先调用对象的hashCode()方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象
如果没有哈希值相同的对象就直接存入集合
如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行equals()比较,比较结果为false就存入, true则不存
自定义类的对象存入HashSet去重复
类中必须重写hashCode()和equals()方法
hashCode(): 属性相同的对象返回值必须相同, 属性不同的返回值尽量不同(提高效率)
equals(): 属性相同返回true, 属性不同返回false,返回false的时候存储
LinkedHashSet
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); } } public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { public LinkedHashSet() { super(16, .75f, true); } }
看其源码(JDK1.8)可知,其底层依靠的是LinkedHashMap,然而LinkedHashMap也是继承于HashMap的,所以归根结底他还是依赖HashMap
看其名字便可知,其与HashSet的不同之处是,他是有序的,所以他可以保证怎么存就怎么取
在其LinkedHashMap维持了一个双向链表
transient LinkedHashMap.Entry<K,V> head; transient LinkedHashMap.Entry<K,V> tail; static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; } private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null) head = p; else { p.before = last; last.after = p; } }
简单使用
public static void main(String[] args) { LinkedHashSet<Person> Set = new LinkedHashSet<>(); Set.add(new Person("张三", 23)); Set.add(new Person("张三", 23)); Set.add(new Person("李四", 24)); Set.add(new Person("李四", 23)); Set.add(new Person("王五", 25)); Set.add(new Person("赵六", 25)); for (Person person : Set) { System.out.println(person); } }
结果
Person [name=张三, age=23] Person [name=李四, age=24] Person [name=李四, age=23] Person [name=王五, age=25] Person [name=赵六, age=25]
和上面的HashSet的结果比较,是不是存入的顺序与取出的顺序一致了
TreeSet
TreeSet是SortedSet接口的唯一实现,TreeSet可以确保集合元素处于排序状态(元素是有序的)。
TreeSet提供的几个额外方法:
Comparator comparttor(): 返回当前Set使用的Compara投入,或者返回null,表示以自然方式排序。 Object first():返回集合中的第一个元素。 Object last():返回集合中的最后一个元素。 Objiect lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素可以不是TreeSet的元素)。 Object higher(Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素可以不需要TreeSet的元素)。 SortedSet subSet(fromElement, toElement):返回此Set的子集,范围从fromElement(包含大于等于)到toElement(不包含小于)。 SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成。 SortedSet tailSet(fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。
来简单使用下:
public static void main(String[] args) { TreeSet<String> treeSet = new TreeSet<>(); treeSet.add("efg"); treeSet.add("abc"); treeSet.add("abd"); treeSet.add("abc"); treeSet.add("bcd"); treeSet.add("cde"); System.out.println(treeSet); } //outPut:[abc, abd, bcd, cde, efg]
看其结果既去除了重复,又进行了自然排序
使用自定义类会如何?
public static void main(String[] args) { TreeSet<Person> treeSet = new TreeSet<>(); treeSet.add(new Person("张三", 23)); treeSet.add(new Person("张三", 23)); treeSet.add(new Person("李四", 23)); } //Exception in thread "main" java.lang.ClassCastException: Person cannot be cast to java.lang.Comparable
很遗憾,报错了,Person这个类不能转化为Comparable
我们看下String类的声明:
public final class Stringextends Objectimplements Serializable, Comparable<String>, CharSequence
我们看到了String这个类实现了Comparable这个接口,查看API可得,此接口强行对实现它的每个类的对象进行整体排序,这意味着我们的Person类必须实现其Comparable接口吗?
答案当然是不一定的,查看TreeSet的API
public TreeSet(Comparator<? super E> comparator)
在其构造方法内可以传入一个比较器:Comparator
大家可能会混淆了Comparator和Comparable,先来看看代码的实现(以年龄排序从大到小排序,年龄相同比较姓名自然排序)
先来Comparator,在构造器内传入我们的比较器
public static void main(String[] args) { TreeSet<Person> treeSet = new TreeSet<>(new Comparator<Person>() { @Override//匿名内部类,在其内部实现其如何比较 public int compare(Person per1, Person per2) { int num = per2.getAge() - per1.getAge(); return num == 0 ? per1.getName().compareTo(per2.getName()) : num; } }); treeSet.add(new Person("张三", 23)); treeSet.add(new Person("张三", 23)); treeSet.add(new Person("李四", 24)); treeSet.add(new Person("李四", 23)); treeSet.add(new Person("王五", 25)); treeSet.add(new Person("赵六", 25)); for (Person person : treeSet) { System.out.println(person); } }
结果
Person [name=王五, age=25] Person [name=赵六, age=25] Person [name=李四, age=24] Person [name=张三, age=23] Person [name=李四, age=23]
你可能对中文的字符的排序有疑问,你可以将其替换为英文字符串,或者看其下面代码,你就知道了如何比较
System.out.println("王:"+('王'+0));//char字符对应的Unicode码表值 System.out.println("赵:"+('赵'+0)); //王:29579 //赵:36213
再来看看Comparable实现:
改造我们的Person
class Person implements Comparable<Person> { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } @Override public int compareTo(Person per) { //这部分代码是和Comparator.compare方法中内容是一样的 int num = per.getAge() - this.getAge(); return num == 0 ? getName().compareTo(per.getName()) : num; } }
测试:
public static void main(String[] args) { TreeSet<Person> treeSet = new TreeSet<>(); treeSet.add(new Person("张三", 23)); treeSet.add(new Person("张三", 23)); treeSet.add(new Person("李四", 24)); treeSet.add(new Person("李四", 23)); treeSet.add(new Person("王五", 25)); treeSet.add(new Person("赵六", 25)); for (Person person : treeSet) { System.out.println(person); } }
可以想象,这个结果和上面代码的结果是一样的。通俗点的方式说Comparable是将比较的代码写在了自己的内部,而Comparator需单独定义一个类对其进行比较。
Comparable & Comparator
Comparator位于包java.util下,而Comparable位于包 java.lang下
都是用来实现集合中元素的比较、排序的,只是 Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序,所以,如想实现排序,就需要在集合外定义 Comparator 接口的方法或在集合内实现 Comparable 接口的方法。
两种方法各有优劣, 用Comparable 简单, 只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码, 用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator 里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。
两种比较小结一下
a.自然顺序(Comparable)
TreeSet类的add()方法中会把存入的对象提升为Comparable类型
调用对象的compareTo()方法和集合中的对象比较
根据compareTo()方法返回的结果进行存储
b.比较器顺序(Comparator)
创建TreeSet的时候可以制定 一个Comparator
如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的顺序排序
add()方法内部会自动调用Comparator接口中compare()方法排序
调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数
两种方式的区别
TreeSet构造函数什么都不传, 默认按照类中Comparable的顺序(没有就报错ClassCastException)
TreeSet如果传入Comparator, 就优先按照Comparator
TreeSet其底层依靠的是TreeMap,其内部维护着一颗二叉排序树,之后会详细讲解
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable{ private transient NavigableMap<E,Object> m; //TreeMap实现的接口 private static final Object PRESENT = new Object(); public TreeSet() { this(new TreeMap<E,Object>()); } public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); } public int size() { return m.size(); } public boolean isEmpty() { return m.isEmpty(); } public boolean contains(Object o) { return m.containsKey(o); } public boolean add(E e) { return m.put(e, PRESENT)==null; } public boolean remove(Object o) { return m.remove(o)==PRESENT; } public void clear() { m.clear(); } }
对List和Set迭代进行一下总结
List
a.普通for循环, 使用get()逐个获取
b.调用iterator()方法得到Iterator, 使用hasNext()和next()方法
c.增强for循环, 只要可以使用Iterator的类都可以用
d.Vector集合可以使用Enumeration的hasMoreElements()和nextElement()方法
Set
a.调用iterator()方法得到Iterator, 使用hasNext()和next()方法
b.增强for循环, 只要可以使用Iterator的类都可以用
Java基础-集合框架之Set