Java集合 HashSet 和 TreeSet的理解

来源:互联网 发布:nginx mysql 安装 编辑:程序博客网 时间:2024/06/14 05:00

HashSet

HashSet实现了 Set 接口,底层是一个HashMap。源码如下:

public class HashSet<E>    {        private transient HashMap<E,Object> map;        public HashSet() {            map = new HashMap<>();        }    }

通过以上源码可以发现,创建一个HashSet对象实际上是创建了一个HashMap对象。

HashSet中的元素是无序的。这里的无序指的是进出集合的顺序。如下:

public class HashSetTest {    public static void main(String[] args) {        HashSet<String> hashSet = new HashSet<>();        hashSet.add("1.Java");        hashSet.add("2.HashSet");        hashSet.add("3.元素");        hashSet.add("4.进出");        hashSet.add("5.无序");        // 使用增强for循环变量元素        for(String str : hashSet){            System.out.println(str);        }    }}

遍历结果为:

4.进出2.HashSet3.元素1.Java5.无序

从遍历的结果来看,元素遍历时的顺序和添加时的顺序不同。

HashSet中的元素是唯一的。多次添加相同的元素,只会有一个元素加入HashSet。通过查看HashSet中add方法的源码发现,这依赖于底层的hashCode方法和equals方法。如下:

// HashSet中的add方法,其中又调用了HashMap中的put方法public boolean add(E e) {        return map.put(e, PRESENT)==null;}

从以上可以看出: add方法只是作了一个判断,添加元素真正起作用的还是HashMap中的put方法,元素是作为key存储的。

// HashMap中的put方法,这里的key是HashSet中的元素 public V put(K key, V value) {    if (table == EMPTY_TABLE) {        inflateTable(threshold);    }    if (key == null)        return putForNullKey(value);    int hash = hash(key);    int i = indexFor(hash, table.length);    for (Entry<K,V> e = table[i]; e != null; e =  e.next) {        Object k;        if (e.hash==hash && ((k=e.key)==key||key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;         }    }    modCount++;    addEntry(hash, key, value, i);    return null;}// put方法中调用的hash方法,返回值与key(即元素)的hashCode方法有关final int hash(Object k) {    int h = hashSeed;    if (0 != h && k instanceof String) {        return sun.misc.Hashing.stringHash32((String) k);    }    h ^= k.hashCode();    h ^= (h >>> 20) ^ (h >>> 12);    return h ^ (h >>> 7) ^ (h >>> 4);}

从put方法的源码中可以知道,判断元素是否唯一,取决于表达式e.hash==hash && ((k=e.key)==key||key.equals(k))的值。其中hash值的计算与元素的hashCode方法有关;而((k=e.key)==key||key.equals(k)) 与元素的equals方法有关。如果元素的hash值相同,且元素相等(对于基础数据类型而言)或内容相同(对于引用类型而言),表达式结果为true,说明HashSet中已有这个元素,则不会继续添加该元素。综上,说明HashSet中元素的唯一性与元素中的hashCode方法和equals方法有关。要保证HashSet中元素的唯一性,就需要保证这两个方法正常判断。因此,当HashSet中的元素为自定义类的对象时,自定义类就需要重写hashCode方法和equals方法。如下:

public class Student {    private String name;    private int age;    private String sex;    Student(){    }    Student(String name, int age, String sex){        this.name = name;        this.age = age;        this.sex = sex;    }    public int hashCode() {        final int prime = 31;        int result = 1;        result = prime * result + age;        result = prime * result +                  ((name == null) ? 0 : name.hashCode());        result = prime * result +                  ((sex == null) ? 0 : sex.hashCode());        return result;    }    public boolean equals(Object obj) {        if (this == obj)            return true;        if (obj == null)            return false;        if (getClass() != obj.getClass())            return false;        Student other = (Student) obj;        if (age != other.age)            return false;        if (name == null) {            if (other.name != null)                return false;        } else if (!name.equals(other.name))            return false;        if (sex == null) {            if (other.sex != null)                return false;        } else if (!sex.equals(other.sex))            return false;        return true;    }}

TreeSet

TreeSet的底层是TreeMap。如下:

public class TreeSet<E> {    private transient NavigableMap<E,Object> m;    // 这里的NavigableMap是接口    TreeSet(NavigableMap<E,Object> m) {        this.m = m;    }    //TreeMap实现了NavigableMap接口    public TreeSet() {        this(new TreeMap<E,Object>());    }}

TreeSet的元素很有特点,元素是唯一的,而且集合会根据元素的自然顺序对元素排序,但元素的进出仍然是无序的。如下:

public class TreeSetTest {    public static void main(String[] args) {        TreeSet<Integer> treeSet = new TreeSet();        // 添加元素        treeSet.add(5);        treeSet.add(4);        treeSet.add(4);        treeSet.add(7);        treeSet.add(1);        treeSet.add(1);        treeSet.add(3);        // 遍历元素        for(Integer number : treeSet){            System.out.print(number + );        }    }}

遍历结果为:1 3 4 5 7
这说明TreeSet中的元素不重复,按自然顺序排序,且进出无序。实际上,TreeSet中元素的自然排序和元素的唯一性依赖于compareTo或compare方法。如下:

//TreeSet 中的add方法,元素仍然是作为key 存储的public boolean add(E e) {        return m.put(e, PRESENT)==null;}// TreeMap 中的put方法public V put(K key, V value) {    Entry<K,V> t = root;    if (t == null) {        compare(key, key);         root = new Entry<>(key, value, null);        size = 1;        modCount++;        return null;    }    int cmp;    Entry<K,V> parent;    Comparator<? super K> cpr = comparator;    // TreeSet中的构造TreeSet(Comparator<? super E> comparator)    // 实际上是调用了TreeMap中的同参构造方法,即:    // TreeMap(Comparator<? super K> comparator)    // 此时,指定了comparator(排序比较器),就按比较器的规则进行比较    if (cpr != null) {        do {            parent = t;            cmp = cpr.compare(key, t.key);            if (cmp < 0)                t = t.left;            else if (cmp > 0)                t = t.right;            else                return t.setValue(value);        } while (t != null);    }    // TreeSet中的无参构造方法,实际上是调用TreeMap的空参构造方法    // 此时没有指定comparator(排序比较器),comparator 为null,    // 比较时调用元素中的compareTo方法    else {        if (key == null)            throw new NullPointerException();        Comparable<? super K> k = (Comparable<? super K>) key;        do {            parent = t;            cmp = k.compareTo(t.key);            if (cmp < 0)                t = t.left;            else if (cmp > 0)                t = t.right;            else                return t.setValue(value);        } while (t != null);    }    Entry<K,V> e = new Entry<>(key, value, parent);    if (cmp < 0)        parent.left = e;    else        parent.right = e;    fixAfterInsertion(e);    size++;    modCount++;    return null;}

从以上源码可知:TreeSet在添加元素时,会对元素进行自然顺序的比较。如果TreeSet的构造方法中指定了排序比较器comparator,元素的比较就使用排序比较器中的compare方法;如果没有指定,元素的比较就使用元素中的compareTo方法。因此,要在TreeSet 中添加自定义类的对象作为元素时,要么在TreeSet的构造方法中指定排序比较器;要么在自定义类中实现Comparable接口,重写compareTo方法。如下:

// 重写compareTo方法public class Person implements Comparable<Person>{    private String name;    private String sex;    private int age;    Person(String name, String sex, int age){        this.name = name;        this.sex = sex;        this.age = age;    }    public int compareTo(Person p) {        int result1 = this.name.compareTo(p.name);        int result2 = this.sex.compareTo(p.sex);        int result3 = this.age - p.age;        return result1==0 ?              (result2==0 ?               (result3==0 ? result3 : result3)              : result2 ) : result1;    }}

或者指定比较器comparator

public class TreeSetTest {    public static void main(String[] args) {    // 创建TreeSet对象时,在构造方法中使用匿名类的方式指定comparator        TreeSet<Person> treeSet =             new TreeSet<>(new Comparator<Person>(){            public int compare(Person p1, Person p2) {               int result1 =   p1.getName().compareTo(                               p2.getName());                int result2 = p1.getSex().compareTo(p2.getSex());                int result3 = p1.getAge() - p2.getAge();                return result1==0 ?                      (result2==0 ?                       (result3==0 ? result3 : result3)                      : result2 ) : result1;            }        });        treeSet.add(new Person("John","male",10));        treeSet.add(new Person("Michle","male",20));        treeSet.add(new Person("Lucy","female",22));        treeSet.add(new Person("John","male",10));        treeSet.add(new Person("Lucy","female",22));        treeSet.add(new Person("Adam","male",19));        for(Person person : treeSet){            System.out.println(person + " ");        }    }}
0 0