一、Map集合

双列集合,每个元素都包含一个Key值和Value值,键对象和值对象之间的关系称为映射。通过key找到value,

Map不能存基本数据类型,只能存引用数据类型

1、Map的实现类

Map的实现类

2、Map接口的常用方法

map常用方法

重点

put()方法,可以添加和修改内容使用该方法时会覆盖原有的值。

replace()的修改和put()有些类似,但是replace()只有替换的功能,如果key()不存在,则不替换。

重点掌握:get、put、remove

/**
 * @document: Map集合的常用方法
 * @Author:SmallG
 * @CreateTime:2023/8/7+11:29
 */

public class Demo01 {
    public static void main(String[] args) {
        //创建一个Map集合对象 key是整型,value是字符串
        Map<Integer, String> map = new HashMap<>();
        //存数据,以键值对的形式存储
        //key不能重复 ,value值可以重复
        map.put(1, "Hello");
        map.put(3, "Tom");
        map.put(5, "SmallG");

        //查看集合中键值对的个数(集合中数据的个数)
        System.out.println("Map集合中元素的个数为:" + map.size());

        //从map集合中取出一个数据
        System.out.println(map.get(1)); //Hello
        System.out.println(map.get(2)); //null 没有这个key,返回null值

        //存放已经存在的key,用put方法可以覆盖之前的内容并返回替换的内容
        String s = map.put(1, "World");
        System.out.println("之前的value值为" + s); //之前的value值为Hello
        System.out.println(map.get(1)); //World

        //删除集合中的数据,返回删除掉的数据
        String remove = map.remove(1);
        System.out.println("删除的数据是:" + remove); //删除的数据是:World
        System.out.println(map.get(1));  //null
    }
}

3、哈希表

也称散列表,是一种常见的数据结构,用于存储和检索键值对。基于hash函数将key映射到数组索引,以便快速访问和操作数据。

(1)哈希冲突

两个不同的输入值,根据同一哈希函数计算出的索引相同的现象称为哈希冲突,也叫哈希碰撞,任何Hash函数都无法避免哈希冲突,常见的解决哈希冲突的方法有:

  • 开放地址法:一旦发生了冲突,就去寻找下一个空的哈希地址,只要哈希表足够大,总能找到空的哈希地址,并将元素存入
  • 再Hash法:当Hash地址发生冲突时使用其他函数计算另一个Hash函数地址,直到不再产生冲突为止
  • 建立公共溢出区:将Hash表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表
  • 链地址法:将Hash表的每个单元作为链表的头结点,所有Hash地址为i的元素构成一个同义词链表,即发生冲突时就把该元素链接在该单元为头节点的链表的尾部

(2)HashMap

内部基于哈希表存储键值对数据,应用于缓存、索引、数据存储和快速查找等场景,内部使用了一个Node类存储在哈希桶里,map集合本身是一种无序的结构。

  • final int hash:存储的哈希码,用于确定键值对在桶数组中的位置
  • final K key:存储键的值
  • V value:存储与键相关联的值
  • Node next:用于处理哈希冲突,存储下一个Node节点的引用,形成链表或红黑树结构
/**
 * @document: HashMap遍历数组
 * @Author:SmallG
 * @CreateTime:2023/8/7+14:29
 */

public class Demo02 {
    public static void main(String[] args) {
        //创建一个map对象
        Map<Integer, String> map = new HashMap<>();
        //往map中加入数据
        map.put(5, "Tom");
        map.put(3, "Jerry");
        map.put(9, "SmallG");
        map.put(4, "hhhh");
        map.put(1, "hhh");

        //遍历方式一:先获取所有的key,然后用key找value
        Set<Integer> integers = map.keySet();//获取map集合所有的key
        for (Integer key : integers) {
            //根据key获取value
            String value = map.get(key);
            System.out.println(key + " : " + value);
        }
        System.out.println("-------------------------------");

        //遍历方式二:通过map集合的entrySet()方法,把map中的所有数据一次查询出来
        //entrySet是map的一个内部类
        Set<Map.Entry<Integer, String>> entries = map.entrySet();
        for (Map.Entry<Integer, String> entry : entries) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

(3)HashCode方法

用于返回当前对象的hash值,返回值类型为int

(4)put方法的执行流程

  1. 计算Key的哈希值,如果key为null,则哈希值为0。如果key不为null,调用key的hashCode方法,计算Key的哈希值
  2. 如果内部数组没有被初始化,会先初始化内部数组
  3. 通过key的哈希值计算key在桶(数组)中的位置
  4. 如果桶中的目标位置没有元素,则创建Node对象,存储键值对数据,并将Node对象保存到桶中目标位置
  5. 如果桶中目标位置有元素(注意可能有多个),则将key与这些元素的key进行比较,如果key与某个元素的key相等,则使用新存入的value覆盖旧的value,如果不相等则创建新的node对象,并追加到链表中。
/**
 * @document: put()方法
 * @Author:SmallG
 * @CreateTime:2023/8/7+15:12
 */

public class Demo03 {
    public static void main(String[] args) {
        //创建一个Key为包装类的map集合
        Map<Integer, String> map1 = new HashMap<>();
        map1.put(1, "Tom");
        //相同的key会把之前的key替换掉
        map1.put(1, "Jerry");
        System.out.println(map1.get(1)); //Jerry

        //创建一个使用自定义类作为key的map集合
        Map<Student, String> map2 = new HashMap<>();
        Student student1 = new Student("Tom", 12);
        Student student2 = new Student("Tom", 12);
        map2.put(student1, "Tom");
        map2.put(student2, "Jerry");
        System.out.println(map2.get(student1)); //Tom
        System.out.println(map2.get(student2)); //Jerry

        //两个key之所以不同,不是调用了equals方法
        //是因为两个对象的hashCode值不同,导致判断两个对象不是同一个对象
        System.out.println("student1 的hashCode值"+student1.hashCode());
        //student1 的hashCode值990368553
        System.out.println("student2 的hashCode值"+student2.hashCode());
        //student2 的hashCode值1096979270

    }
}

class Student {
    String name;
    Integer age;

    public Student() {
    }

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

(5)重写hashCode()方法

/**
 * @document: put()方法
 * @Author:SmallG
 * @CreateTime:2023/8/7+15:12
 */

public class Demo03 {
    public static void main(String[] args) {
        //创建一个Key为包装类的map集合
        Map<Integer, String> map1 = new HashMap<>();
        map1.put(1, "Tom");
        //相同的key会把之前的key替换掉
        map1.put(1, "Jerry");
        System.out.println(map1.get(1)); //Jerry

        //创建一个使用自定义类作为key的map集合
        Map<Student, String> map2 = new HashMap<>();
        Student student1 = new Student("Tom", 12);
        Student student2 = new Student("Tom", 12);
        map2.put(student1, "Tom");
        map2.put(student2, "Jerry");
        System.out.println(map2.get(student1)); //Tom
        System.out.println(map2.get(student2)); //Jerry

        //两个key之所以不同,不是调用了equals方法
        //是因为两个对象的hashCode值不同,导致判断两个对象不是同一个对象
        System.out.println("student1 的hashCode值" + student1.hashCode());
        //student1 的hashCode值990368553
        System.out.println("student2 的hashCode值" + student2.hashCode());
        //student2 的hashCode值1096979270

    }
}

class Student {
    String name;
    Integer age;

    public Student() {
    }

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("equals方法被调用");  //默认情况下equals()方法没有被调用
        Student stu = (Student) o;
        if (this.name.equals(stu.name) && this.age.equals(stu.age)) {
            return true;
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        //重写了hashCode方法,map集合在判断对象是否相等时,会调用equals方法
        //如果比较相等,就认为是同一个key
        return Objects.hash(name, age);
    }
}

(6)HashMap原理

  • HashMap的容量

HashMap的内部数组不是在创建HashMap对象时初始化,而是在首次存入元素时进行初始化,以减少对内存的占用。

官方规定table的长度总是2的N次方:目的是为了保证HashMap的速度足够快。

不论存操作还是取操作,HashMap都使用除留取余法通过key的哈希值计算key在桶中的索引(位运算)

  • HashMap的初始容量

默认初始容量手动指定容量两种情况,用无参构造器创建HashMap对象时,table的初始化长度为16(默认长度)

  • HashMap的扩容

自动将桶的长度扩容到原来的两倍,容量(HashMap的内部数组长度,默认为16),大小(HashMap中实际存储的元素个数,默认为0),负载因子(用来衡量HashSet"满”的程度,默认值为0.75f),临界值(当size超过临界值时,HashMap将扩容,threshold=容量×负载因子)。

(7)树化和退化

  • HashMap中使用链地址法处理Hash冲突,当桶中的某个位置的链表过长时,会影响查询效率的情况。当链表长度大于等于阈值(默认为8),同时HashMap容器已达到64时,链表------->红黑树,从而减少查询操作的时间复杂度,这个过程叫树化。
  • 当红黑树中的节点数量减少到一定程度(默认为6)时,HashMap将红黑树----->链表,这个过程称为退化。

红黑树

4、LinkedHashMap

  • HashMap无法保持元素添加顺序,而LinkedHashMap就能解决这一问题,但性能上没HashMap高。
  • LinkedHashMap在HashMap的基础上维护了一个entry的双向链表,用来记录顺序。

LinkedHashMap

/**
 * @document: LinkedHashMap集合,相比于HashMap的无序集合,linkedList是个有序集合
 * @Author:SmallG
 * @CreateTime:2023/8/8+10:07
 */

public class Demo05 {
    public static void main(String[] args) {
        //创建对象
        LinkedHashMap<Integer, String> linkedHashMap = new LinkedHashMap<>();
        //存入数据
        linkedHashMap.put(5, "Tom");
        linkedHashMap.put(3, "Jerry");
        linkedHashMap.put(9, "SmallG");

        //存入相同的数据
        linkedHashMap.put(3, "SmallY");
        //删除一个数据
        linkedHashMap.remove(5);
        //查询一个数据
        System.out.println(linkedHashMap.get(3));

        //遍历LinkedHashMap集合
        Set<Map.Entry<Integer, String>> entries = linkedHashMap.entrySet();
        for (Map.Entry<Integer, String> entry : entries) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

二、Set集合

1、Set接口概述

  • 继承Collection接口,与Collection接口中的方法基本一致,比Collection接口更加严格。
  • 无序且不可重复
  • 可以使用set不可重复的特性实现去重操作
/**
 * @document: set集合,无序不可重复
 * @Author:SmallG
 * @CreateTime:2023/8/8+10:26
 */

public class Demo06 {
    public static void main(String[] args) {
        //创建对象
        Set<Integer> set = new HashSet<>();
        LinkedHashSet linkedHashSet = new LinkedHashSet();
        //存数据
        linkedHashSet.add(15);
        linkedHashSet.add(30);
        linkedHashSet.add(20);
        linkedHashSet.add(10);
        //存入数据
        set.add(15);
        set.add(30);
        set.add(20);
        set.add(10);
        //尝试存入重复的数据
        boolean add = set.add(20);//被忽略
        System.out.println("是否添加成功:" + add);  //false


        System.out.println(set);
        //set集合没有get方法,只能通过迭代器的方法才能遍历
        //1、将set集合变成迭代器
        Iterator<Integer> iterator = set.iterator();
        //2、遍历迭代器
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        System.out.println("------------------");

        //增强for循环
        for (Integer set1 : set) {
            System.out.println(set1);
        }
        System.out.println("------------------");
        System.out.println(linkedHashSet);
        System.out.println("------------------");

        //利用set集合中的数据不可重复 可以去掉List集合中重复的数据,达到去重的目的
        List<Integer> list = Arrays.asList(7, 8, 10, 9, 9, 21); //快速构建成一个list集合
        System.out.println(list);
        //利用set集合去重
        Set<Integer> set1 = new HashSet<>(list);
        System.out.println(set1);
    }
}

2、Set常用方法

set常用方法

/**
 * @document: 集合运算,求交集、并集、差集(补集)
 * @Author:SmallG
 * @CreateTime:2023/8/8+10:48
 */

public class Demo07 {
    public static void main(String[] args) {
        List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
        List<Integer> list2 = Arrays.asList(3, 4, 5, 6);

        //转换为set集合
        Set<Integer> set1 = new HashSet<>(list1);
        Set<Integer> set2 = new HashSet<>(list2);

        //查看原始数据
        System.out.println(set1);
        System.out.println(set2);

        //求交集
        set1.retainAll(set2);
        System.out.println("set1和set2的交集是" + set1);

        //求并集
        //重置set集合
        set1 = new HashSet<>(list1);
        set1.addAll(set2);
        System.out.println("set1和set2的并集是" + set1);

        //求差集
        set1 = new HashSet<>(list1);
        set1.removeAll(set2);
        System.out.println("set1和set2的差集是" + set1);
    }
}

3、HashSet

底层用哈希表实现,用于存储唯一的元素

HashSet特点

  • 基于哈希表,使用哈希函数将元素映射到对应的存储位置
  • 无序(元素的插入顺序与遍历顺序不一致)
  • 不允许重复元素(如果尝试添加重复元素会被忽略)
  • HashSet允许使用null作为元素,但只能有一个,HashMap中key值只允许有一个null
  • HashSet的插入、删除和查找操作时间复杂度最低(O(1))。
/**
 * @document: HashSet集合
 * @Author:SmallG
 * @CreateTime:2023/8/8+11:24
 */

public class Demo08 {
    public static void main(String[] args) {
        HashSet<Student1> set1 = new HashSet<>();
        //创建学生类对象
        Student1 student1 = new Student1("Tom", 18);
        Student1 student2 = new Student1("Tom", 18);

        //将两个学生存入到set中,内容相同地址不同,可以存进去
        set1.add(student1);
        set1.add(student2);
        System.out.println(set1); //两个对象通过equals方法判断两个不想等

        //重写equals方法和hashCode方法,equals判断相等,只能成功保存一个数据
        HashSet<Student2> set2 = new HashSet<>();
        Student2 student3 = new Student2("Tom", 18);
        Student2 student4 = new Student2("Tom", 18);
        set2.add(student3);
        set2.add(student4);
        System.out.println(set2);

        //尝试存入null值
        set2.add(null);
        set2.add(null);
        set2.add(null);
        set2.add(null);
        set2.add(null);
        System.out.println(set2); //只能存入一个null值
    }
}

class Student1 {
    String name;
    int age;

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

    public Student1() {
    }

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

class Student2 {
    String name;
    int age;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student2 student2 = (Student2) o;
        return age == student2.age && Objects.equals(name, student2.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

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

三、练习

1 统计一句话中各个字符的个数

现有字符串"good good study, day day up.",统计其中各个字符出现的次数,统计结果要求按字母在语句中出现的顺序输出。

程序运行效果如下所示:

练习1

/**
 * @document: 统计一句话中各个字符的个数
 * @Author:SmallG
 * @CreateTime:2023/8/7+14:12
 */

public class StringCount {
    public static void main(String[] args) {
        System.out.println("请输入字符串:");
        Scanner scanner = new Scanner(System.in);
        String s = scanner.nextLine();
        //创建hashmapkey存放key,value存放出现次数
        LinkedHashMap<Character, Integer> hashMap = new LinkedHashMap<>();
        //遍历字符串
        for (int i = 0; i < s.length(); i++) {
            if (hashMap.containsKey(s.charAt(i))) {
                int a = hashMap.get(s.charAt(i));
                hashMap.put(s.charAt(i), a + 1);
            } else if (Character.isLetter(s.charAt(i))) { //判断字符是否为英文
                hashMap.put(s.charAt(i), 1);
            } else {
                continue;
            }
        }
        System.out.println(hashMap);
    }
}

2 分析用户行为数据

电商平台会记在用户的行为数,用于隐式反馈推荐问题的研究。

UserBehavior.csv是阿里巴巴提供的一个淘宝用户行为数据集。

用户的行为数据字段描述如下:

练习2

其中,“行为类型”字段的描述如下所示:

练习3

UserBehavior-1000.csv是该数据集的一个子集,筛选了2017年11月26日中的1000行记录,数据内容如下所示(数据集中的字段顺序与上图中的数据字段说明顺序一致):

练习4

比如,上图中的第一行数据表示:用户ID为 1000228的客户对商品ID为2410458的商品(商品类目ID为4145813)进行了pv操作(浏览),操作时间戳为1511625804。

请综合运用之前所学知识,完成以下分析需求:

1、统计数据集中出现次数前5的商品类目,在控制台输出商品类目ID和出现次数,按出现次数降序排列;

2、统计11月26日每小时的用户行为数据量,在控制台输出时间区间和对应的行为数据数量,按时间区间升序排列。

提示:

1、将每一条用户数据封装成一个对象,可降低整体编码复杂度

2、借助AI工具学习如何实现集合中数据的排序

程序运行效果如下所示:

练习5

/**
 * @document: 用户类
 * @Author:SmallG
 * @CreateTime:2023/8/8+7:48
 */

public class User {
    private Integer userId; //用户ID
    private Integer commodityId; //商品ID
    private Integer comClassId; //商品类别Id
    private String type; //行为类型
    private long time; //时间戳

    public User(Integer userId, Integer commodityId, Integer comClassId, String type, long time) {
        this.userId = userId;
        this.commodityId = commodityId;
        this.comClassId = comClassId;
        this.type = type;
        this.time = time;
    }

    public User() {
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public Integer getCommodityId() {
        return commodityId;
    }

    public void setCommodityId(Integer commodityId) {
        this.commodityId = commodityId;
    }

    public Integer getComClassId() {
        return comClassId;
    }

    public void setComClassId(Integer comClassId) {
        this.comClassId = comClassId;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }

    @Override
    public String toString() {
        return "User{" +
                "userId=" + userId +
                ", commodityId=" + commodityId +
                ", comClassId=" + comClassId +
                ", type='" + type + '\'' +
                ", time=" + time +
                '}';
    }
}

主方法类:

/**
 * @document:
 * @Author:SmallG
 * @CreateTime:2023/8/8+7:56
 */

public class Extended01 {
    public static void main(String[] args) {
        String path = "src/day02/homework/UserBehavior-1000.csv";
        //读取文件,用arraylist接收
        ArrayList<User> users = readUserBehavior(path);
        //用hashmap存储出现次数key-商品id ,value-出现次数
        HashMap<Integer, Integer> counts = counts(users);
        //对存储的counts进行输出,按照value的降序进行排列,求前五
        num5(counts);

        //统计每天的行为次数,key--对应的小时数,value--该小时数的数据量
        HashMap<Integer, Integer> hourCounts = hourCounts(users);

        //对数据进行排序输出
        printCount(hourCounts);

    }

    /**
     * 输出行为次数
     * @param hourCounts
     */
    public static void printCount(HashMap<Integer, Integer> hourCounts) {
        TreeMap<Integer, Integer> treeMap = new TreeMap<>(hourCounts);
        //输出统计信息
        System.out.println("----------每小时数据数量----------");
        for (Map.Entry<Integer, Integer> entry : treeMap.entrySet()) {
            int startHour = entry.getKey();
            int endHour = startHour + 1;
            System.out.println(startHour + "-" + endHour + ":" + entry.getValue());
        }
    }

    /**
     * 返回每天的行为次数
     *
     * @param users 传入参数为读取的用户数据
     * @return 返回一个hashmap用于存储每天的行为次数
     */
    public static HashMap<Integer, Integer> hourCounts(ArrayList<User> users) {
        HashMap<Integer, Integer> hourCountMap = new HashMap<>();
        //遍历用户行为数据
        for (User user : users) {
            //将时间戳转换为本地日期时间对象
            LocalDateTime ldt = LocalDateTime.ofEpochSecond(user.getTime(), 0, ZoneOffset.ofHours(8));
            //获取小时信息
            int hour = ldt.getHour();
            if (hourCountMap.containsKey(hour)) {
                int newHourCount = hourCountMap.get(hour);
                hourCountMap.put(hour, newHourCount + 1);
            } else {
                hourCountMap.put(hour, 1);
            }
        }
        return hourCountMap;

    }

    /**
     * 输出前五的商品(理解上存在问题)
     *
     * @param counts 传入一个hashMap存储的信息是商品id出现的次数
     */
    public static void num5(HashMap<Integer, Integer> counts) {
        //将出现的次数放到arraylist数组
        ArrayList<Map.Entry<Integer, Integer>> cidCountList = new ArrayList<>(counts.entrySet());
        //对数组进行排列
        cidCountList.sort(new Comparator<Map.Entry<Integer, Integer>>() {
            @Override
            public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2) {
                return o2.getValue() - o1.getValue();
            }
        });
        System.out.println("----------商品类目Top5----------");
        //对前五进行输出
        for (Map.Entry<Integer, Integer> entry : cidCountList.subList(0, 5)) {
            System.out.println(entry);
        }
    }

    /**
     * 统计商品出现次数的方法
     *
     * @param users 传入参数为arraylist的数组
     * @return 返回一个HashMap,key--商品id,value--该商品出现的次数
     */
    public static HashMap<Integer, Integer> counts(ArrayList<User> users) {
        //创建HashMap对象,用于存储商品出现的次数,当作返回值
        HashMap<Integer, Integer> counts = new HashMap<>();
        for (User user : users) {
            if (counts.containsKey(user.getCommodityId())) {
                //商品id已经存在
                int a = counts.get(user.getCommodityId());
                //将商品的出现次数加一
                counts.put(user.getCommodityId(), a + 1);
            } else {//首次出现,则将hashmap中key对应的value值设为1
                counts.put(user.getCommodityId(), 1);
            }
        }
        return counts;
    }

    /**
     * 处理读取的数据方法
     *
     * @param str 读取的一行数据
     * @return 返回一个User对象
     */
    public static User readUser(String str) {
        String[] strings = str.split(",");
        User user = new User(Integer.parseInt(strings[0]),
                Integer.parseInt(strings[1]),
                Integer.parseInt(strings[2]),
                strings[3], Long.parseLong(strings[4]));
        return user;
    }

    /**
     * 读取文件方法
     *
     * @param path 传入文件路径
     * @return 返回封装好的用户信息
     */
    public static ArrayList<User> readUserBehavior(String path) {
        //存储读取信息,用于返回
        ArrayList<User> users = new ArrayList<>();
        try (
                //字符输入流 读取数据
                FileReader fr = new FileReader(path);
                //字符输入缓冲流
                BufferedReader br = new BufferedReader(fr)
        ) {
            String line = br.readLine();
            while (line != null) {
                users.add(readUser(line));
                line = br.readLine();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return users;
    }
}
最后修改:2023 年 08 月 08 日
如果觉得我的文章对你有用,请随意赞赏