JAVA学习笔记20-(集合③)

本文深入解析Java中集合类如HashSet、TreeSet、HashMap、TreeMap的特点与使用,涵盖哈希表、二叉树数据结构及Collections工具类的高级应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


集合③

HashSet && TreeSet
Map
哈希表
二叉树
Collections工具类


HashSet && TreeSet

HashSet集合:
    无序不可重复。

TreeSet集合存储元素特点:
    1、无序不可重复的,但是存储的元素可以自动按照大小顺序排序!称为:可排序集合。
    2、无序:这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。

Map

Map作为一个父接口,其特点如下:
1、Map集合和Collection集合没有关系。
2、Map集合以key和value的方式存储数据:
	键值对key和value都是引用数据类型。
	key和value都是存储对象的内存地址。
	key起到主导的地位,value是key的一个附属品。
3、key和value都是存储java对象的内存地址。
4、所有Map集合的key特点:无序不可重复的。
Map集合的key和Set集合存储元素特点相同。

Map接口常用方法

在Map接口中有许多方法,详情查询API文档。
下面介绍Map中的常用方法:

V put(K key, V value) 向Map集合中添加键值对

V get(Object key) 通过key获取value

void clear()    清空Map集合

boolean containsKey(Object key) 判断Map中是否包含某个key

boolean containsValue(Object value) 判断Map中是否包含某个value

boolean isEmpty()   判断Map集合中元素个数是否为0

V remove(Object key) 通过key删除键值对

int size() 获取Map集合中键值对的个数。

Collection<V> values() 获取Map集合中所有的value,返回一个Collection

Set<K> keySet() 获取Map集合所有的key(所有的键是一个set集合)

Set<Map.Entry<K,V>> entrySet()
将Map集合转换成Set集合
假设现在有一个Map集合,如下所示:
map1集合对象
key             value
----------------------------
1               zhangsan
2               lisi
3               wangwu
4               zhaoliu

Set set = map1.entrySet();
set集合对象
1=zhangsan 【注意:Map集合通过entrySet()方法转换成的这个Set集合,Set集合中元素的类型是 Map.Entry<K,V>2=lisi     【Map.Entry和String一样,都是一种类型的名字,只不过:Map.Entry是静态内部类,是Map中的静态内部类】
3=wangwu
4=zhaoliu ---> 这个东西是个什么?Map.Entry
下面是对这些常用方法的代码演示:

public class MapTest01 {
    public static void main(String[] args) {
    
        创建Map集合对象
        Map<Integer, String> map = new HashMap<>();
        
        向Map集合中添加键值对
        map.put(1, "zhangsan"); // 1在这里进行了自动装箱。
        map.put(2, "lisi");
        map.put(3, "wangwu");
        map.put(4, "zhaoliu");
        
        通过key获取value
        String value = map.get(2);
        System.out.println(value);
        
        获取键值对的数量
        System.out.println("键值对的数量:" + map.size());
        
        通过key删除key-value
        map.remove(2);
        System.out.println("键值对的数量:" + map.size());
        
        判断是否包含某个key
        contains方法底层调用的都是equals进行比对的,所以自定义的类型需要重写equals方法。
        System.out.println(map.containsKey(new Integer(4))); // true
        
        判断是否包含某个value
        System.out.println(map.containsValue(new String("wangwu"))); // true

        获取所有的value
        Collection<String> values = map.values();
        // foreach
        for(String s : values){
            System.out.println(s);
        }

        清空map集合
        map.clear();
        System.out.println("键值对的数量:" + map.size());
        
        判断是否为空
        System.out.println(map.isEmpty()); // true
    }
}

Map集合的遍历【非常重要】
对于Map的遍历,有两种思路方法:

1、获取所有的key,然后通过key去获得value完成遍历。

2、Set<Map.Entry<K,V>> entrySet()
这个方法是把Map集合直接全部转换成Set集合。
Set集合中元素的类型是:Map.Entry
下面是对这两种方法的代码实现:

首先创建集合:
Map<Integer, String> map = new HashMap<>();
map.put(1, "zhangsan");
map.put(2, "lisi");
map.put(3, "wangwu");
map.put(4, "zhaoliu");

---------------------------------------------------------------
方法1:
获取所有的key,通过遍历key,来遍历value。所有的key是一个Set集合

先通过keySet()方法取出所有key存储在keys集合中:
Set<Integer> keys = map.keySet();

迭代器完成遍历:
Iterator<Integer> it = keys.iterator();
while(it.hasNext()){
	取出其中一个key
	Integer key = it.next();
	通过key获取value
	String value = map.get(key);
	System.out.println(key + "=" + value);
}

foreach也可以完成遍历:
for(Integer key : keys){
	System.out.println(key + "=" + map.get(key));
}

---------------------------------------------------------------
方法2:
这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值。
比较适合于大数据量。
Set<Map.Entry<K,V>> entrySet()通过entrySet()方法去获得:

首先通过entrySet()方法来转换:
Map.Entry其实是个内部类,是个类名,后面带<>是泛型,set是集合名(变量名)
Set<Map.Entry<Integer,String>> set = map.entrySet();

迭代器完成遍历:
Iterator<Map.Entry<Integer,String>> it2 = set.iterator();
while(it2.hasNext()){
	Map.Entry<Integer,String> node = it2.next();
	Integer key = node.getKey();
	String value = node.getValue();
	System.out.println(key + "=" + value);
}

foreach完成遍历:
for(Map.Entry<Integer,String> node : set){
	System.out.println(node.getKey() + "--->" + node.getValue());
}
---------------------------------------------------------------

哈希表/散列表 数据结构

首先要知道哪些集合底层是哈希表?
HashMap
HashSet(他的底层是HashMap,进而是哈希表)
HashTable
哈希表/散列表是一个怎样的数据结构?
初步理解:

哈希表是一个将数组和链表结合起来的结构,他结合了他们各自的优点
数组:在查询方面效率很高,随机增删方面效率很低。
单向链表:在随机增删方面效率较高,在查询方面效率很低。
形成了一个新的数据结构。

HashMap集合底层的源代码:
public class HashMap{
	HashMap底层实际上就是一个数组。(一维数组)
	Node<K,V>[] table;
	静态的内部类HashMap.Node
	static class Node<K,V> {
	final int hash;  哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标。)
	final K key;  存储到Map集合中的那个key
	V value;  存储到Map集合中的那个value
	Node<K,V> next;  下一个节点的内存地址。
	}
}
哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。(数组和链表的结合体。)

HashMap集合的key部分特点:
无序,不可重复。
为什么无序? 因为不一定挂到哪个单向链表上。
不可重复是怎么保证的? equals方法来保证HashMap集合的key不可重复。如果key重复了,value会覆盖。

哈希表HashMap使用不当时无法发挥性能!
假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了纯单向链表。这种情况我们成为:散列分布不均匀。
什么是散列分布均匀?
假设有100个元素,10个单向链表,那么每个单向链表上有10个节点,这是最好的,是散列分布均匀的。

假设将所有的hashCode()方法返回值都设定为不一样的值,可以吗,有什么问题?
	不行,因为这样的话导致底层哈希表就成为一维数组了,没有链表的概念了。也是散列分布不均匀。

散列分布均匀需要你重写hashCode()方法时有一定的技巧。

重点:放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。

HashMap集合的默认初始化容量是16,默认加载因子是0.75
这个默认加载因子是当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。

重点,记住:HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是因为达到散列均匀,为了提高HashMap集合的存取效率,所必须的。
下面是对于哈希表的图片详解:

在这里插入图片描述

对于哈希表,重点要掌握的是:
map.put(k,v)
v = map.get(k)
以上这两个方法的实现原理!

在上图已经写了这两个方法的原理了。
map.put(k,v)原理:
首先将k,v封装到Node对象当中。
然后底层会调用k的hashCode()方法去得出其哈希值
然后通过哈希函数/哈希算法去将hash值转换为数组的下标
如果下标所指的位置上没有任何元素,就把Node添加到这个位置上
如果此下标对应的位置有链表,就会拿着k去跟链表上每个节点中的k进行equals
如果所有的equals方法都返回false(都不相等),则在链表末尾新添这个节点
如果其中有一个equals方法反悔了true,那么这个节点的value将被覆盖(不可重复)
v = map.get(k)原理:
先调用k的hashCode()方法得出哈希值,然后通过哈希算法转换成数组下标
然后通过数组下标去快速定位到位置
如果这个位置什么也没有,返回null
如果这个位置有单向链表,那么会拿着参数k和单向链表上的每个节点中的k进行equals
如果所有equals方法返回false,那么get方法返回null
只要其中有一个节点的k和参数k equals的时候返回true
那么这个节点的value就是我们要找的value,get方法最终返回这个要找的value
从上面的原理可以看出,如果用哈希表的话,必须去重写两个方法!
放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法!
具体重写使用IDEA工具生成即可
1、向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,然后再调用equals方法!
equals方法有可能调用,也有可能不调用。
    拿put(k,v)举例,什么时候equals不会调用?
        k.hashCode()方法返回哈希值,
        哈希值经过哈希算法转换成数组下标。
        数组下标位置上如果是null,equals不需要执行。
    拿get(k)举例,什么时候equals不会调用?
        k.hashCode()方法返回哈希值,
        哈希值经过哈希算法转换成数组下标。
        数组下标位置上如果是null,equals不需要执行。

2、注意:如果一个类的equals方法重写了,那么hashCode()方法必须重写。
并且equals方法返回如果是truehashCode()方法返回的值必须一样。
    equals方法返回true表示两个对象相同,在同一个单向链表上比较。
    那么对于同一个单向链表上的节点来说,他们的哈希值都是相同的。
    所以hashCode()方法的返回值也应该相同。

3hashCode()方法和equals()方法不用研究了,直接使用IDEA工具生成,但是这两个方法需要同时生成。

4、终极结论:
    放在HashMap集合key部分的,以及放在HashSet集合中的元素,需要同时重写hashCode方法和equals方法。

5、对于哈希表数据结构来说:
    如果o1和o2的hash值相同,一定是放到同一个单向链表上。
    当然如果o1和o2的hash值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生“哈希碰撞”。
小TIPS:

HashMap集合key部分允许null吗?
    允许
    但是要注意:HashMap集合的key null值只能有一个。

Hashtable的key可以为null吗?
    Hashtable的key和value都是不能为null的。
    HashMap集合的key和value都是可以为null的。

Hashtable方法都带有synchronized:线程安全的。
线程安全有其它的方案,这个Hashtable对线程的处理
导致效率较低,使用较少了。

Hashtable和HashMap一样,底层都是哈希表数据结构。
Hashtable的初始化容量是11,默认加载因子是:0.75f
Hashtable的扩容是:原容量 * 2 + 1

Properties

目前只看到了Properties属性类对象的相关方法。
Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。
Properties被称为属性类对象。
Properties是线程安全的。

创建一个Properties对象
Properties pro = new Properties();

需要掌握Properties的两个方法,一个存,一个取。
pro.setProperty("url", "jdbc:mysql://localhost:3306/bjpowernode");
pro.setProperty("driver","com.mysql.jdbc.Driver");
pro.setProperty("username", "root");
pro.setProperty("password", "123");

通过key获取value
String url = pro.getProperty("url");
String driver = pro.getProperty("driver");
String username = pro.getProperty("username");
String password = pro.getProperty("password");

System.out.println(url);
System.out.println(driver);
System.out.println(username);
System.out.println(password);

二叉树 数据结构

首先要知道哪些集合底层是二叉树?
TreeMap
TreeSet(他的底层是TreeMap,进而是二叉树)
关于二叉树

初步理解:
(基于TreeSet,因为其实现了SortedSet接口,故可排序,TreeMap同理实现SortedMap接口,可排序)

TreeSet集合存储元素特点:
    1、无序不可重复的,但是存储的元素可以自动按照大小顺序排序!
    称为:可排序集合。

    2、无序:这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。
    
TreeSet集合底层实际上是一个TreeMap
TreeMap集合底层是一个二叉树。
放到TreeSet集合中的元素,等同于放到TreeMap集合key部分了。
TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。
称为:可排序集合。
下面图解释了关于二叉树的数据结构

在这里插入图片描述

关于此图的解释:

1、TreeSet/TreeMap是自平衡二叉树。遵循左小右大的原则存放。(存放的时候就会进行大小比较)
2、遍历二叉树有三种方式:
	前序遍历:根 左 右
	中序遍历:左 根 右
	后序遍历:左 右 根
	
	前中后说的是 根 的位置。
3、TreeSet集合/TreeMap集合采用的是:中序遍历方式。
Iterator迭代器也是,左 根 右。

例如将一组数据:
100,200,5060,80120,140,130,135,180,666,40,50放入自平衡二叉树
流程如下:
先放100,第一个 根
然后200100比较,200>100,故放在100的右边
然后再50100比较,50<100,故放在100的左边
之后每次都是跟100开始比较,然后循环,直到找到一个自己的空位为止

由于存放中已经排了序,故取出的时候自动按照了大小顺序排序。
可自行模拟中序遍历,不再赘述,可以看出是自动排序了。

比较规则

当我们将一些JAVA自带的类型放进TreeSet时,是可以自动排序的。
那如果我们将自定义的类型放入TreeSet,可以自动排序吗?

下面的代码开始试验:
public class TreeSetTest03 {
    public static void main(String[] args) {
        Person p1 = new Person(32);
        Person p2 = new Person(20);
        Person p3 = new Person(30);
        Person p4 = new Person(25);

        创建TreeSet集合
        TreeSet<Person> persons = new TreeSet<>();
        
        添加元素
        persons.add(p1);
        persons.add(p2);
        persons.add(p3);
        persons.add(p4);

        遍历
        for (Person p : persons){
            System.out.println(p);
        }
    }
}

class Person {
    int age;
    public Person(int age){
        this.age = age;
    }

    重写toString()方法
    public String toString(){
        return "Person[age="+age+"]";
    }
}
很遗憾的是,不行!
程序运行的时候出现了这个异常:
java.lang.ClassCastException:class com.bjpowernode.javase.collection.Personcannot be cast to class java.lang.Comparable
程序中对于Person类型来说,无法排序。因为没有指定Person对象之间的比较规则。
谁大谁小并没有说!我们需要给程序一个自定义类的排序规则!

如何给一个比较规则?通过翻阅源代码我们可以知道,JAVA自带的类型之所以能够实现排序
是因为他们实现了java.lang.Comparable接口。
或者在创建对象时通过构造方法传入一个比较器Comparator。

下面的代码实现了Comparable接口

public class TreeSetTest04 {
    public static void main(String[] args) {
        Customer c1 = new Customer(32);
        Customer c2 = new Customer(20);
        Customer c3 = new Customer(30);
        Customer c4 = new Customer(25);

        创建TreeSet集合
        TreeSet<Customer> customers = new TreeSet<>();
        添加元素
        customers.add(c1);
        customers.add(c2);
        customers.add(c3);
        customers.add(c4);

        遍历
        for (Customer c : customers){
            System.out.println(c);
        }
    }
}

放在TreeSet集合中的元素需要实现java.lang.Comparable接口。
并且实现compareTo方法。equals可以不写。
class Customer implements Comparable<Customer>{

    int age;
    public Customer(int age){
        this.age = age;
    }

    需要在这个方法中编写比较的逻辑,或者说比较的规则,按照什么进行比较!
    拿着参数k和集合中的每一个k进行比较,返回值可能是>0 <0 =0
    比较规则最终还是由程序员指定的:例如按照年龄升序。或者按照年龄降序。
    
    @Override
    public int compareTo(Customer c) { 
    	// 例如调用方法时写c1.compareTo(c2);
        // this是c1
        // c是c2
        // c1和c2比较的时候,就是this和c比较。
        可以这么写
        int age1 = this.age;
        int age2 = c.age;
        if(age1 == age2){
            return 0;
        } else if(age1 > age2) {
            return 1;
        } else {
            return -1;
        }
        当然简单的写法是这个
        return this.age - c.age;
        or:
        return c.age - this.age;
    }

    public String toString(){
        return "Customer[age="+age+"]";
    }
}
TreeSet集合中元素可排序的第二种方式:使用比较器的方式。

结论:
    放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:
        第一种:放在集合中的元素实现java.lang.Comparable接口。
        第二种:在构造TreeSet或者TreeMap集合的时候给它传一个比较器对象。
        
Comparable和Comparator怎么选择呢?
    当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。
    如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。

    Comparator接口的设计符合OCP原则。

例如下面的代码:

public class TreeSetTest06 {
    public static void main(String[] args) {
    
        创建TreeSet集合的时候,需要使用这个比较器。
        这样不行,没有通过构造方法传递一个比较器进去。
        TreeSet<WuGui> wuGuis = new TreeSet<>();

        给构造方法传递一个比较器。
        TreeSet<WuGui> wuGuis = new TreeSet<>(new WuGuiComparator());

        也可以使用匿名内部类的方式(这个类没有名字。直接new接口。)
        TreeSet<WuGui> wuGuis = new TreeSet<>(new Comparator<WuGui>() {
            @Override
            public int compare(WuGui o1, WuGui o2) {
                return o1.age - o2.age;
            }
        });

        wuGuis.add(new WuGui(1000));
        wuGuis.add(new WuGui(800));
        wuGuis.add(new WuGui(810));

        for(WuGui wuGui : wuGuis){
            System.out.println(wuGui);
        }
    }
}

乌龟类
class WuGui{

    int age;

    public WuGui(int age){
        this.age = age;
    }

    @Override
    public String toString() {
        return "小乌龟[" +
                "age=" + age +
                ']';
    }
}

单独在这里编写一个比较器
比较器实现java.util.Comparator接口。(Comparable是java.lang包下的。Comparator是java.util包下的。)

class WuGuiComparator implements Comparator<WuGui> {

    @Override
    public int compare(WuGui o1, WuGui o2) {
        指定比较规则
        按照年龄排序
        return o1.age - o2.age;
    }
}


Collections工具类

java.util.Collections 集合工具类,方便集合的操作。

1、线程安全
ArrayList集合不是线程安全的。
List<String> list = new ArrayList<>();

将其变成线程安全的
Collections.synchronizedList(list);

2、排序
Collections.sort(list);
注意:对List集合中元素排序,需要保证List集合中的元素实现了:Comparable接口。

对Set集合怎么排序呢?
将Set集合转换成List集合
Set<String> set = new HashSet<>();
List<String> myList = new ArrayList<>(set);

这种方式也可以排序。(不实现Comparable接口,而是通过比较器)
Collections.sort(list集合, 比较器对象)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值