.集合Set,HashSet,TreeSet及其底层实现HashMap和红黑树;Collection总结

ONE.Set集合

one.Set集合的特点 
无序,唯一

TWO.HashSet集合

1.底层数据结构是哈希表(是一个元素为链表的数组)

2.那么HashSet如何来实现元素的唯一性的呢?

通过一HashSet添加字符串的案例查看HashSet中add()的源码,看为什么相同的字符串没有被加入HashSet中

interface Collection {
...
}

interface Set extends Collection {
...
}

class HashSet implements Set {
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
//1.从这一步我们可以看出来,HashSet()其实使用HashMap()实现的
public HashSet() {
map = new HashMap<>();
}

public boolean add(E e) { //e=hello,world
//2.add()方法的内部也是HashMap的实例对象调用方法,这里e是被添加的对象,而PRESENT是一个
private static final Object PRESENT = new Object();对象。
  return map.put(e, PRESENT)==null;
}
}

class HashMap implements Map {
//3.来到HashMap实现的put方法   
public V put(K key, V value) { //key=e=hello,world

//4.看哈希表是否为空,如果空,就开辟空间
  if (table == EMPTY_TABLE) {
      inflateTable(threshold);
  }

  //5.判断对象是否为null
  if (key == null)
      return putForNullKey(value);

  //6_1.调用hash()方法,通过查看这个方法我们知道这个方法的返回值和对象的hashCode()方法相关
  int hash = hash(key); 

  //7.在哈希表中查找hash值
  int i = indexFor(hash, table.length);
  //8.这里for循环的的初始条件是把table[i]赋值给e,如果在哈希表中找不到这个hash值得话,
    就不会进入for循环比较,如果有的话,就会进入比较
  for (Entry<K,V> e = table[i]; e != null; e = e.next) {
      Object k;
      //9.如果hash值一样(说实话,这一步每太看懂,既然能够查询到,说明两者的hash值必然相等),
        并且地址值或者equls一样的话,不会添加进来
      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
          V oldValue = e.value;
          e.value = value;
          e.recordAccess(this);
          return oldValue;
          //走这里其实是没有添加元素
      }
  }

  modCount++;
  //9.把元素添加
  addEntry(hash, key, value, i); 
  return null;
}

transient int hashSeed = 0;

//6_2.这是HashMap中的hash方法,从这个方法的实现我们可以看出,这个方法的唯一变量是
hashCode()

final int hash(Object k) { //k=key=e=hello,
  int h = hashSeed;//这个值默认是0
  if (0 != h && k instanceof String) {
      return sun.misc.Hashing.stringHash32((String) k);
  }

  h ^= k.hashCode(); //这里调用的是对象的hashCode()方法

  // This function ensures that hashCodes that differ only by
  // constant multiples at each bit position have a bounded
  // number of collisions (approximately 8 at default load factor).
  h ^= (h >>> 20) ^ (h >>> 12);
  return h ^ (h >>> 7) ^ (h >>> 4);
}

HashSet其实是用HashMap()来实现的,HashMap()是Map接口的实现类 
调用HashSet的add()方法其实就是调用HashMap()中的put(),put()中主要涉及到两个方面

1).对象的hash值,通过调用hash()得到,这个方法是由hashCode()经过操作实现的,由hashCode()的值控制

2).创建了哈希表,哈希表会将每一个hash值收入 
然后比较的方式是

A.先在hash表中查找看是否有这个hash值(第一次比较,看hash表中是否有当前元素的hash值(这个值有hashcode操作的得到)),如果没有,直接将这个hash值对应的对象添加到HashSet中,如果有还要进行第二次比较

B.如果hash表中有这个hash值,那么获取表中的这个hash对应的对象,如果这两个对象的地址值(e.key == key)或者key.equal(e.key)。(第二次比较,如果两个对象的hash值相同,还是不能认为是同一个对象,还要比较两个对象的地址值,或者是equals(),这里是一个||,只要有一个满足相等,就可以认为是同一个元素,不添加

3.现在可以回答开始的问题,为什么存储字符串的时候,字符串相同的时候,只存储了一个了,原因是String类中重写了hashCode()和equals()而String类的hashcode值和equals结果都是由字符串的内容决定的

我们来看一下String类中的hashCode()和equals()方法

1) public int hashCode()返回此字符串的哈希码。 
String 对象的哈希码根据以下公式计算: 
s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1] 
使用 int 算法,这里 s[i] 是字符串的第 i 个字符,n 是字符串的长度,^ 表示求幂。(空字符串的哈希值为 0。)

2)public boolean equals(Object anObject)将此字符串与指定的对象比较。 
当且仅当该参数不为 null,并且是与此对象表示相同字符序列的 String 对象时,结果才为 true。

所以当对象是String类型的时候,地址值(可能相等(字符串常量池知识点)),equals方法必然相等,所以不会加入

4.如果HashSet中收入的是自定义对象,那如何实现唯一呢? 
1)通过上面String类的分析,我们知道,HashSet中实现唯一的两个比较,先是比较hash值(这个值由hashcode值控制),如果这个值相同,那么再比较地址值或者equals(),所以我们可以通过重写hashCode()和equals(),来实现对自定义对象的唯一性判断 
2)这里开始的想法是,其实只要重写了equals,让这个方法比较的是对象的内容,那么就可以排除掉相同的对象 
所以让hashCode()返回一个常数,这样hash就相等了,然后通过equals判断 
但是这样会导致每一个新对象都要和老对象进行比较,太麻烦,所以我们可以仿照String类中对hashCode()重写的方式,同样的有对象的成员变量来决定这个对象的hashcode值,equals方法也是通过比较对象的成员变量 
是否相等。这样我们就得到了最后的结论,其实eclipse提供了自定义类重写这两个方法的最终版本

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)
            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;
        return true;
    }

3)开发的时候,代码非常的简单,自动生成即可。

THREE.TreeSet集合

1:底层数据结构是红黑树(是一个自平衡的二叉树) 
这里写图片描述

2:TreeSet底层是如何保证元素的排序方式,和元素的唯一性呢 
同样的我们来看看看TreeSet的源码

interface Collection {...}

interface Set extends Collection {...}

interface NavigableMap {

}

class TreeMap implements NavigableMap {
     public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = 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);
        }
        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;
    }
}

class TreeSet implements Set {
    private transient NavigableMap<E,Object> m;

    public TreeSet() {
         this(new TreeMap<E,Object>());
    }

    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
}

真正的比较是依赖于元素的compareTo()方法,而这个方法是定义在 Comparable里面的。
所以,你要想重写该方法,就必须是先 Comparable接口。这个接口表示的就是自然排序。

根据源码,我们知道,TreeSet的底层代码可以通过两种方式来实现其元素的唯一性和有序

a:自然排序(元素具备比较性) 
让元素所属的类实现Comparable接口,重写其中的compareTo方法

package cn.itcast_03;

public class Student implements Comparable<Student>{
    private String name;
    private int age;
    public Student() {
        super();
        // TODO Auto-generated constructor stub
    }
    public Student(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public int compareTo(Student s) {
        int num = 0;
        num = this.age - s.age;
        int num2 = num == 0 ? this.name.compareTo(s.name) : num;
        return num2;

    }

}

b:比较器排序(集合具备比较性) 
让集合构造方法接收Comparator的实现类对象,在实现类中重写compare()方法

这种方法通过调用集合的带参构造来实现比较 
public TreeSet(Comperator comparator)//比较器排序

Comperator 是一个接口,把接口作为参数其实就是需要这个接口的实现类的对象 
通过API我们可以得知这个接口中方法的格式,另外,这种需要一个接口的实现类的对象作为参数的情况,开发中我们常常使用匿名内部类的格式

TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
            public int compare(Student s1,Student s2) {
                //注意这个三目比较,赋值的优先级最小,==最大,三目中间
                int num = s1.getName().length() - s2.getName().length();
                int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
                int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
                return num3;                    
            }
        });

3.需要注意的问题

1)根据对源码和底层数据结构红黑树的理解,我们知道无论是通过自然排序还是通过比较器排序,数据结构核心是通过将根节点和子节点比较,在红黑树的默认方法中,小的为左儿子,大的为右儿子,实现一颗平衡二叉树,而代码的体现是将成员变量进行某种比较得到一个int值,如果值大于1,就放右边,如果值小于1就放左边,如果值相等就不放入,通过这个int值来完成元素的收集

2)我们在考虑这个值的时候,对于大于,小于,相等的条件设定常常使用三目运算符来做判断,熟练使用这一三目运算符

//如果按照正常的二叉排序,那么会先输出小的,而我们要求先输出分高的,所以,可以将全部的s1,s2交换顺序
                //就可以达到总分大的放左边(先输出),然后单科权重是语文——数学——英语
                int num = s2.getSum() - s1.getSum();
                int num2 = num ==0 ? s2.getChineseScore() - s1.getChineseScore() : num;
                int num3 = num2 == 0 ? s2.getMathScore() - s1.getMathScore() : num2;
                int num4 = num3 == 0 ? s2.getEnglishScore() - s1.getEnglishScore() : num3;
                return num4;

FOUR.Collection集合总结(掌握) Collection

    |--List 有序,可重复
        |--ArrayList
            底层数据结构是数组,查询快,增删慢。
            线程不安全,效率高
        |--Vector
            底层数据结构是数组,查询快,增删慢。
            线程安全,效率低
        |--LinkedList
            底层数据结构是链表,查询慢,增删快。
            线程不安全,效率高
    |--Set  无序,唯一
        |--HashSet
            底层数据结构是哈希表。
            如何保证元素唯一性的呢?
                依赖两个方法:hashCode()和equals()
                开发中自动生成这两个方法即可
            |--LinkedHashSet
                底层数据结构是链表和哈希表
                由链表保证元素有序
                由哈希表保证元素唯一
        |--TreeSet
            底层数据结构是红黑树。
            如何保证元素排序的呢?
                自然排序
                比较器排序
            如何保证元素唯一性的呢?
                根据比较的返回值是否是0来决定

4:针对Collection集合我们到底使用谁呢?(掌握) 
唯一吗? 
是:Set 
排序吗? 
是:TreeSet 
否:HashSet 
如果你知道是Set,但是不知道是哪个Set,就用HashSet。

否:List 
要安全吗? 
是:Vector 
否:ArrayList或者LinkedList 
查询多:ArrayList 
增删多:LinkedList 
如果你知道是List,但是不知道是哪个List,就用ArrayList。

如果你知道是Collection集合,但是不知道使用谁,就用ArrayList。

如果你知道用集合,就用ArrayList。

5:在集合中常见的数据结构(掌握) 
ArrayXxx:底层数据结构是数组,查询快,增删慢 
LinkedXxx:底层数据结构是链表,查询慢,增删快 
HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals() 
TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序

猜你喜欢

转载自blog.csdn.net/qq_38384440/article/details/81739778