java-集合2

一、Set

(1)Set接口是Collection接口的子接口,Collection的方法Set也能用
(2)Set接口是无序的,添加和取出的元素顺序不一致(依然是一个固定顺序但是与添加顺序不一致),并且没有索引
(3)Set接口不允许有重复元素,可以有null但只能有一个
(4)Set 体系最常用的是 HashSet、TreeSet和 LinkedHashSet 三个集合类。
(5)Set是Collection接口的子接口,所以可以用迭代器和增强for循环遍历,由于没有索引,所以不能用索引获取元素

二、HashSet

1、简介

(1)HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
(2)HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
(3)HashSet实际是HashMap,HashSet线程不安全。

案例:

public static void main(String[] args) {
        Set hs = new HashSet();
        //添加以下元素
        hs.add("123123");
        hs.add("abcde");
        hs.add("nihao");
        hs.add("hello123");
        hs.add("abcde");
        System.out.println(hs);
    }

在这里插入图片描述
在HashSet中添加元素,可以看到,取出顺序与添加顺序不一致,而且添加了两次 “abcde” 却只打印了一次。

2、HashSet遍历

(1)迭代器遍历:

public static void main(String[] args) {
        Set hs = new HashSet();
        hs.add("123123");
        hs.add("abcde");
        hs.add("nihao");
        hs.add("hello123");
        hs.add("abcde");
        Iterator iterator = hs.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(next);
        }
    }

在这里插入图片描述

(2)增强for循环遍历(本质还是迭代器)

public static void main(String[] args) {
        Set hs = new HashSet();
        hs.add("123123");
        hs.add("abcde");
        hs.add("nihao");
        hs.add("hello123");
        hs.add("abcde");
        for (Object o :hs) {
            System.out.println(o);
        }
    }

在这里插入图片描述

3、HashSet原理

(1)HashSet底层是HashMap(查看源代码):

下断点,Debug,F7进入HashSet无参构造器底层,可以看到底层创建了HashMap,所以HashSet实际是HashMap
在这里插入图片描述
在这里插入图片描述
注意:如果F7进入不了底层,可以配置一下idea,在Setting中搜索Stepping,把java. *和javax. *取消勾选
在这里插入图片描述
最好再将Data Views——>Java下的Enable alternative ····· 和Hide null···· 取消勾选,这样debug时,可以查看控制台上的详细信息
在这里插入图片描述

(2)之前说过HashSet是单列集合,HashMap是双列集合。为什么HashSet底层创建HashMap,HashSet还是单列集合呢?
HashMap的确是双列集合,底层有一组键值对(key-value),但HashSet创建的HashMap中的value永远地被一个静态的Object类型对象占用了,这是系统设定的,所以HashSet只用到了HashMap的key。

HashSet.add()方法底层:
在这里插入图片描述
在这里插入图片描述

可以看到e是map的key,也就是你当前添加的元素。而PRESENT是map的value,这里PRESENT是系统的一个静态Object类对象,虽然这个Object类对象没有任何数据,但是在这起到了占位作用,占据了value的位置,这样调用hashset.add()方法时只用传一个参数。

(3)HashSet扩容机制
HashSet底层是HashMap,HashMap有一个存储数据表table,专门存放数据。当创建一个HashSet时,table表为空。在这里插入图片描述
当第一次执行add()方法时,map中的table表会扩容到16。在这里插入图片描述
可以看到第一次添加元素并没有添加到下标为0的位置,而是添加到下标为5的位置。这时因为hashset和hashmap都是通过调用hashCode() 方法来计算当前元素的hashCode 值,再通过hashCode值与table表容量大小取余,余数就是元素在表中存储的位置。

何时扩容?
在这里插入图片描述
threshold为临界值,loadFactor为临街因子(临界值=临街因子*当前容量)
也就是如果不断添加元素,直到超过当前临界值,就会发生扩容,新容量是原容量的2倍,然后新临界值=新容量 * 临界因子

前面说过,元素具体存储位置是通过计算得到的。那么有可能两个元素通过计算得到相同的hashCode值,就会存储到相同的位置,这时就发生了哈希碰撞。
发生hash碰撞后,系统会调用equals方法再对两个元素进行判断,如果判断结果为true,系统就认定两个元素为同一个元素,则添加失败。如果判断结果为false,那么新元素和旧元素就会形成一个链表,同时存储在当前位置上。

假设agea和afgc的hashCode相同,那么就会出现这种情况:
在这里插入图片描述
示例:

public class HashSetTest01{
    @SuppressWarnings("all")
    public static void main(String[] args) {
        Set hs = new HashSet();
        //添加4个Person类对象到集合中
        hs.add(new Person("java",10));
        hs.add(new Person("python",11));
        hs.add(new Person("c",12));
        hs.add(new Person("go",13));
    }
}

class Person{
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //重写Person类中的hashCode()方法,使Person类所有对象hashCode均为一个固定值
    @Override
    public int hashCode() {
        return 88;
    }
}

添加三个元素时,集合size大小已经到了3
在这里插入图片描述
但是只有8号位置有元素,因为创建对象hashCode都相同,全都存在了8号位置,并在8号位置上形成了链表。
在这里插入图片描述
next代表链表的下一个元素
在这里插入图片描述

(4)关于hashcode的一些规定:
两个对象相等,hashcode一定相等
两个对象不等,hashcode不一定不等
hashcode相等,两个对象不一定相等
hashcode不等,两个对象一定不等

为什么不直接equals比较值是否相同,而先要计算hashCode?
hash算法是二进制算法,计算式本质是二进制,所以hash算法速度很快。具体的存储位置看hashCode,如若hashCode不同且其他位置为空则可直接存储不用equlas比较。如果先进行equals比较,再进行hashCode比较这样就多了一步equals比较,效率慢很多。所以先计算hashCode大大加快了存储速率。

4、HashMap的树化

(1)java1.7及之前,hashmap存储结构是(数组+链表)。java1.8时,hashmap扩容到一定程度链表可能会变为红黑树(数组+ (链表 |红黑树))

红黑树:
在这里插入图片描述

(2)形成红黑树条件:
jdk1.8开始,hashmap底层定义了常量TREEIFY_THRESHOLD,默认为8。当数组上某个位置的链表长度大于TREEIFY_THRESHOLD,就会扩容一次数组。当某个链表长度大于8,并且数组长度扩容到64时,这个链表就会树化,其他没有大于8长度的链表不会树化。

比如:目前hashmap长度16,某个位置的链表长度为8。当这个位置的链表添加一个元素时,hashmap就会发生一次扩容,长度扩容为32,链表长度变为9。当这个位置链表再添加一个元素时,hashmap再发生一次扩容,长度扩容为64,链表长度变为10。当这个位置链表继续添加一个元素时,hashmap底层判断当前数组长度到了64,并且链表长度大于8,数组不会发生扩容,并且这个链表会树化,变成一个红黑树。

Node链表树化:
在这里插入图片描述
HashMap$Node变成了HashMap$TreeNode,同时里面的存储结构也发生了巨大变化
在这里插入图片描述

(3)为什么jdk1.8引入了链表树化

hashmap引入链表是为了解决哈希碰撞问题,将链表树化简单说是为了提高查询效率。如果继续使用链表来查询的话,查询时可能会遍历链表的所有元素才会找到。但红黑树查询是基于特定操作保持二叉查找树的平衡,效率要比链表高很多。

(4)红黑树链表化

我们知道长度大于8的链表会树化,但是通过底层代码发现,长度小于等于6的红黑树也会链表化。为什么红黑树不是长度回到8链表化,而是长度小于等于6才会链表化?可以简单理解6和8之间是一个缓冲区,如果不这样设计,将来在长度为8时频繁地进行插入和删除,那么链表也会频繁的树化和链化,效率会降低很多。

三、LinkedHashSet

1、简介

(1)LinkedHashSet是HashSet子类,所以HashSet有的特点它也会有。
(2)创建LinkedHashSet时,底层调用的是HashSet构造函数,实际创建的是LinkedHashMap。
(3)HashSet存储结构是数组+链表,LinkedHashSet存储结构是数组+双向链表
(4)LinkedHashSet不允许元素重复

2、双向链表

(1)双向链表是链表的一种,他的每个数据节点都有两个指针分别指向直接后继和直接前驱,所以从双向链表的任意一个节点开始都可以很方便的访问它的前驱节点和后继节点,这保证了LinkedHashSet是有序的。这是双向链表的优点,那么有优点就有缺点,缺点是每个节点都需要保存当前节点的next和prev两个属性,这样才能保证优点。所以需要更多的内存开销,并且删除和添加也会比较费时间。
(2)双向链表的首个元素的前驱和最后元素的后继节点都为null
(3)结构图:
在这里插入图片描述
案例:

public static void main(String[] args) {
        Set set = new LinkedHashSet();
        set.add("nihao");
        set.add("123123");
        set.add("hello");
        set.add("tom");
        set.add("jack");
        System.out.println(set);
    }

在这里插入图片描述
可以看到,存入顺序和取出顺序一致

四、TreeSet

1、简介

(1)TreeSet底层是TreeMap
(2)与HashSet不同,TreeSet是对元素排序的。与HashSet相同,TreeSet也不允许元素重复
(3)TreeSet底层使用红黑树结构存储数据,特点:有序,查询速度比List快
(4)TreeSet底层既然是红黑树,那么它对元素就有一定的排序方式,所以TreeSet 两种排序方法: 自然排序和定制排序。默认情况下, TreeSet 采用自然排序。

2、自然排序

(1)如果想通过自然排序的方式对TreeSet排序,那么添加对象时,这个对象必须实现Comparable接口。否则编译不通过。实现了接口,需要重写接口中的compareTo(Object obj) 方法来比较元素之间的大小关系,这时底层就会调用你重写的compareTo方法。
(2)compareTo方法是Object类的一个方法,也就是所有类都有一个compareTo()方法。系统自带的类型底层就有默认的排序方式,只不过自己创建的类需要重写这个方法,然后按你的方式来排序。

注意:compareTo(Object obj)方法返回的是int值,底层通过返回的正数还是负数来确定元素放到左子树还是右子树位置

底层判断方式:
在这里插入图片描述

示例:

public static void main(String[] args) {
        Set set = new TreeSet();
        set.add(new AA("niuniu",5));
        set.add(new AA("maoqiu",5));
        set.add(new AA("huahua",5));
        for (Object o :set) {
            System.out.println(o);
        }
    }

class AA implements Comparable{
    private String name;
    private int age;

    public AA(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "AA{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Object o) {
        if (o instanceof AA){      
            return this.name.compareTo(((AA) o).name); 
        //这里的compareTo方法和AA类重写的不是同一个方法
        //这里调用的是name的compareTo,也就是String的compareTo
        //添加AA对象时,调用AA的compareTo判断对象o是否和AA是同一种类型
        //对象o在底层是指上一次添加的元素,与这一次添加的AA进行compareTo
        //这里设置的逻辑是,如果o和AA是同一种类型,就按name的大小排序
        }else {
            System.out.println("二者不是同一类型");
            return 0;
            //如果不是同一种类型就返回0,添加失败
        }

    }
}

3、定制排序

(1)自然排序添加的对象必须实现Comparable接口,如果不想让添加的类实现Comparable接口,或希望按照其它属性大小进行排序,则考虑使用定制排序。
(2)定制排序通过Comparator接口实现,并且需要从写compare(T o1,T o2)方法
(3)要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。在这里插入图片描述
Comparator接口的实例作为形参传递给TreeSet的构造器
在这里插入图片描述
(4)定制排序添加的元素是对象时,那么添加的所有元素最好是你指定的某一类型对象,这样才能向下转型,对这个类或类的某一个元素进行比较。如果添加的元素不是同一类型,向下转型时会抛出类转换异常。这时你需要另做判断,判断如果不是同一类型该怎样处理。
(5)使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。

public static void main(String[] args) {
        Set set = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) { 
                CC c1 = (CC) o1;
                CC c2 = (CC) o2;
                if (c1.getAge() > c2.getAge()){  //按年龄排序
                    return 1;
                }
                if (c1.getAge() < c2.getAge()){
                    return -1;
                }
                return 0;
            }
        });
        set.add(new CC("niuniu",5));
        set.add(new CC("maoqiu",4));
        set.add(new CC("huahua",3));
        for (Object o : set) {
            System.out.println(o);
        }
    }

定制排序底层的排序规则,compare方法已经被我们重写,所以会按照我们的规则来返回值。
在这里插入图片描述
结果:按年龄从小到大排序,如果想从大到小排序,只用把返回值1和-1位置互换即可
在这里插入图片描述

五、Map

1、简介

(1)Map用于保存具有映射关系的数据:Key-Value
(2)Map中的Key和Value可以是任何引用类型的数据,会封装到HashMap$Node对象中
(3)Map中的Key不允许重复,Value允许重复。原因和HashSet相同。当有相同的key时,新key的value值会替换旧key的value值
(4)Map中的key可以是null只能有一个,value也可以是null可以有多个
(5)Map关系体系图:
在这里插入图片描述

2、map常用方法

(1)put(Object key,Object value):给map集合中添加键值对
(2)containsKey():底层调用了equals方法,查询key中是否包含某个元素
(3)containsValue():底层调用了equals方法,查询value中是否包含某个元素
(4)get():查询key对应的value值,返回value值
(5)isEmpty():判断map集合中是否为空,若为空则返回true
(6)keySet():获取map集合中所有的key,并将所有的key中存入set集合中
(7)clear():移除map里所有的映射关系
(8)remove():利用key删除map集合中的元素
(9)size():获取map集合中键值对的个数
(10)values():获取map集合中所有的value值,并将value值存入Collection集合中返回

3、entrySet

entrySet()也是Map集合的一个方法,这里单独说一下:
使用entrySet()将map集合转换为set集合,set集合中元素的类型是Map.Entry<K,V>,一个Map.Entry<K,V>对应map中的一个HashMap$Node(就是对应一个位置的元素),Map.Entry是Map内部的一个接口,接口中提供了getKey()和getValues()方法用于得到key和value的值。

keySet()、values()、entrySet()三个方法示意图:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

4、map集合的几种遍历方式

public static void main(String[] args) {
		Map map = new HashMap();
		map.put("n1","hk");
		map.put("n2","dz");
		map.put("n1","xz");
		map.put("n3","kk");
		map.put(null,null);
		map.put(null,"123");
		System.out.println(map);
		Set keyset = map.keySet(); 
		//第一种:先取出所有的key,在通过key取出对应的value
		//增强for循环遍历
		for (Object key :keyset) {
		    System.out.println(key+"-"+map.get(key));
		}
		//迭代器遍历
		Iterator iterator = keyset.iterator();
		while (iterator.hasNext()){
		    Object obj=iterator.next();
		    System.out.println(obj+"-"+map.get(obj));
		}
		System.out.println("----------------------------------------");
		//第二种:只取value
		Collection values = map.values();
		//增强for循环遍历
		for (Object value :values) {
		    System.out.println(value);
		}
		//迭代器遍历
		Iterator iterator1 = values.iterator();
		while (iterator1.hasNext()){
		    Object value1=iterator1.next();
		    System.out.println(value1);
		}
		System.out.println("---------------------------------------------");
		//第三种:通过EntrySet来获取k-v
		Set entryset = map.entrySet();
		for (Object entry:entryset) {
		    Map.Entry m=(Map.Entry)entry; 
		//向下转型,将set中元素遍历后转成Map.Entry,方便调用getKey()和getValues()方法取值
		    System.out.println(m.getKey()+"-"+m.getValue());
		}
		Iterator iterator2 = entryset.iterator();
		while (iterator2.hasNext()){
		    Object obj2 = iterator2.next();
		    Map.Entry m1=(Map.Entry)obj2;
		    System.out.println(m1.getKey()+"-"+m1.getValue());
		}
}

六、HashMap

1、简介

(1)允许使用null键和null值,与HashSet一样,不保证映射的顺序。
(2)所有的key构成的集合是Set:无序的、不可重复的。所以, key所在的类要重写:equals()和hashCode()
(3)所有的value构成的集合是Collection:无序的、可以重复的。所以, value所在的类要重写: equals()
(4)所有的entry构成的集合是Set:无序的、不可重复的
(5)HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等
(6)HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true

2、HashMap底层

前面提到HashSet时,其实已经把HashMap底层也顺便讲完了,与HashSet不同,HashMap的value是可以存储元素的。
补充一点:计算HashMap的容量时,n为什么必须是2的n次幂?
在这里插入图片描述
HashMap使用n-1 & hash得到槽位地址,这个运算n必须是2的n次幂,结论当容量是2的n次幂的时候(16,32…),hash % n = (n-1) & hash 的位运算性能高**
如果初始容量不是2的n次幂,HashMap调用tableSizeFor自动转换成大于这个数最小的2的n次幂

3、LinkedHashMap

(1)同样的LinkedHashMap几乎和LinkedHashSet一模一样,只是LinkedHashSet使用LinkedHashMap实现,只有key,而value是一个静态的空对象。而LinkedHashMap有一组key-value
(2)LinkedHashMap底层使用双向链表实现,有顺序(插入顺序),没有重复的集合
在这里插入图片描述

七、TreeMap

(1)TreeSet使用TreeMap实现,只是value使用静态空对象,只是用key实现TreeSet
(2)TreeMap存储 Key-Value 对时, 需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
(3)TreeMap底层使用红黑树结构存储数据
(4)TreeMap也是用自然排序或定制排序对元素进行排序,定制排序器和自然排序都存在时,定制优先
● TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0,1,-1
○ 0:对象相等,添加失败
○ -1:比对象小,添加到左边
○ 1:比对象打,添加到右边

自然排序案例:

public class User implements Comparable {

    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        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 String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //按照姓名从大到小排列,年龄从小到大排列
    @Override
    public int compareTo(Object o) {
        if(o instanceof User){
            User user = (User)o;
//            return -this.name.compareTo(user.name);
//            int compare = -this.name.compareTo(user.name);
            int compare = this.name.compareTo(user.name);
            if(compare != 0){
                return compare;
            }else{
                return Integer.compare(this.age,user.age);
            }
        }else{
            throw new RuntimeException("输入的类型不匹配");
        }

    }

}

public class TreeMapDemo1 {
	public static void main(String[] args) {
        User u1 = new User("ggg", 35);
        User u2 = new User("ggg", 35);
        User u3 = new User("ccc", 83);
        User u4 = new User("ccc", 30);
        User u5 = new User("ccc", 74);
        User u6 = new User("eee", 39);
        User u7 = new User("fff", 40);
        User u8 = new User("aaa", 40);
        User u9 = new User("bbb", 40);

        TreeMap map = new TreeMap();
        map.put(u1, "a");
        map.put(u2, "a");
        map.put(u3, "a");
        map.put(u4, "a");
        map.put(u5, "a");
        map.put(u6, "a");
        map.put(u7, "a");
        map.put(u8, "a");
        map.put(u9, "a");


        for (Object o : map.entrySet()) {
            System.out.println(o);
       	 }
  	  }
    }

定制排序案例:

public class User{

    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        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 String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
   		 }
    }
}


public class TreeMapDemo1 {
    public static void main(String[] args) {
        User u1 = new User("ggg", 35);
        User u2 = new User("ggg", 35);
        User u3 = new User("ccc", 83);
        User u4 = new User("ccc", 30);
        User u5 = new User("ccc", 74);
        User u6 = new User("eee", 39);
        User u7 = new User("fff", 40);
        User u8 = new User("aaa", 40);
        User u9 = new User("bbb", 40);

        Comparator com = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof User && o2 instanceof User){
                    User u1 = (User) o1;
                    User u2 = (User) o2;
                    return -Integer.compare(u1.getAge(), u2.getAge());           
                }else{
                    throw new RuntimeException("输入的类型不匹配");
                }
            }
        };

        TreeMap map = new TreeMap(com);
        map.put(u1, "a");
        map.put(u2, "a");
        map.put(u3, "a");
        map.put(u4, "a");
        map.put(u5, "a");
        map.put(u6, "a");
        map.put(u7, "a");
        map.put(u8, "a");
        map.put(u9, "a");

        for (Object o : map.entrySet()) {
            System.out.println(o);
        }
    }
}

八、Hashtable

hashtable是一个很古老的类,现在基本都用hashmap

hashtable特点:
1、hashtable的键和值都不能为null,否则会抛出空指针异常
2、hashtable使用方法和hashmap基本一致。hashtable线程安全,hashmap线程不安全
3、与HashMap一样, Hashtable 也不能保证其中 Key-Value 对的顺序
4、Hashtable判断两个key相等、两个value相等的标准, 与HashMap一致。
5、hashtable底层有数组hashtable$Entry[ ]初始化大小为11,临界值threshold为11* 0.75
6、hashtable数组大小达到临界值时,按照int newCapacity=(oldCapacity* 2)+1; 的大小进行扩容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值