Day29_HashMap

一、概述

我们先来看一张图,回顾一下之前学习的ArrayList、LinkedList、HashMap
在这里插入图片描述

(1)数组的优势/劣势

采用一段连续的存储单元来存储数据。它是由相同类型的元素的集合所组成,并且被分配一块连续的内存来存储(与链表对比),利用元素的索引可以计算出该元素对应的存储地址。它的特点是提供随机访问并且容量有限。

缺点:java中内存申请之后大小是固定的,如果数组满了还想再插入数据,就需要重新再建一个容量更大数组,把数据导进去之后再插入新的数据,这个过程很浪费时间。

优点查找速度很快,对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。

(2)链表的优势/劣势

链表是一种线性表,不像顺序表那样连续存储元素,而是在每一个节点里存到下一个节点的指针。

优点:由于不用连续存储,对于链表的新增、删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),比顺序表快得多;

缺点:但查找操作需要遍历链表逐一进行比对,复杂度为O(n),而顺序表相应的时间复杂度分别是O(logn)和O(1)。

(3)散列表的特点

散列表,又叫哈希表(Hash Table),是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。也就是说,把关键字映射到一个表中的位置来直接访问记录,以加快访问速度。

(4)什么是Hash

哈希:Hash
基本原理就是把任意长度输入,转化为固定长度输出
这个映射的规则就是Hash算法,而原始数据映射的二进制串就是Hash值

Hash的特点

  • 1.从Hash值不可以反向推导出原始数据
  • 2.输入数据的微小变化会得到完全不同的Hash值,相同的数据一定可以得到相同的值
  • 3.哈希算法的执行效率要高效,长的文本也能快速计算Hash值
  • 4.Hash算法的冲突概率要小

由于Hash原理就是将输入空间映射成Hash空间,而Hash空间远远小于输入空间,根据抽屉原理,一定存在不同输出有相同的映射

抽屉原理
由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成相同输出的情况。

抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。

二、HashMap原理讲解

(1)的继承体系

HashMap继承了AbstractMap,实现了Cloneable接口、Serializable接口、Map<K,V>接口
img

(2)Node数据结构分析

Node节点主要有四个变量:
hash:哈希值,用于路由寻址公式计算出对应的索引位置:index = (table.length - 1) & node.hash
<k,v>:键值对
next:指针

static class Node<K,V> implements Map.Entry<K,V>{
   
   
    final int hash;//K.hash--->f(k.hash)(扰动函数,让hash值更散列)--->hash
    final K key;
    V value;
    Node<K,V> next;
}
(3)底层存储结构介绍

在这里插入图片描述

哈希表,整体是一个数组,不过Node数组的每一项都是由一个单链表组成,下标由hash值与数组长度与运算(路由寻址)构成。在jdk8中,当数组长度达到64(有些文章里也叫数组容量)并且单链表的长度达到8之后,链表会转化成红黑树的结构

为什么选择8:当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值(树化门槛)。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。

(4)put数据原理分析

在这里插入图片描述
这幅图路由寻址公式index=(table.length - 1) & node.hash你可能不太理解,它的意思就是index=(表的长度减一)& (哈希值)
表的长度一定是2的幂方次,本例中是16,先16减去1得到15,把15转为二进制为(0000 00000 1111),然后用这个二进制和哈希值做一下与运算得到0010,转为十进制为2,所以下标为2。

(5)什么是Hash碰撞

哈希碰撞(冲突):要把一个对象插入散列表,由路由寻址公式,计算出来的值有可能是一样的。找到这个对象应该插入散列表的位置时,已经存放了其他的对象。
原因:哈希就是做一个映射,为的是查找快。因为映射毕竟是有一个范围的,这个范围可能会小于你原来的那个范围,所以可能好多个值映射了之后成为一个值,即产生哈希冲突。

(6)什么是链化

因为哈希碰撞,插入一个对象时,就会在链表最后面插入(jdk7 头插,jdk8 尾插)。导致链表越来越长,查询会变慢,最后接近为o(n)。

(7)jdk8为什么引入红黑树

解决链化很长的情况,因为红黑树是一个自平衡的二叉查找树,查找效率比链表高。

(8)HashMap扩容原理

16位的数组桶位bin被装满的时候,离散性就变得很差,bin中链表长度会达到阈值(树化门槛),又会导致查询效率变慢。扩容即用空间换时间的思想,提高查找的性能。

三、部分方法的源码:

0.HashMap几个常量

//定义了数组默认大小是2^4
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//定义了数组最大大小是2^30
static final int MAXIMUM_CAPACITY = 1<<30;

//定义了默认的负载因子大小是0.75
static final int DEFAULT_LOAD_FACTOR = 0.75f;

//定义了树化阈值(有一个链长度大于8,并且链表所有元素之和超过64,那么链表结构就变为红黑树)
static final int TREEIFY_THRESHOLD = 8//定义了树化阈值(有一个链长度大于8,并且链表所有元素之和超过64,那么链表结构就变为红黑树)
static final int MIN_TREEIFY_CAPACITY = 64;

1.HashMap核心属性(threshold、loadFactory、size、modCount)

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
   
   
    
    // 这就是我们的HashMap散列表表,长度始终是2的幂。
    transient Node<K,V>[] table;
    transient Set<Map.Entry<K,V>> entrySet;
    
    // 当前哈希表中元素个数
    transient int size;
    
    // 当前哈希表结构修改次数(增、删时才算结构修改)
    transient int modCount;
    
    // 负载因子就是0.75
    final float loadFactor;
    
    // 扩容阈值,当你的哈希表中的元素超过阈值时,触发扩容。
    // 计算方式:threshold = capacity * loadFactor,也就是说“扩容阈值=(表的大小*0.75)”
    int threshold;
}

2.HashMap的构造方法

构造方法做了什么呢?
首先人家不知道该创建一个多大的HashMap,所以构造方法先判断调用者有没有把(表的大小Capacity)和(负载因子loadFactor)传进来,
①如果传进来了就用调用者传过来的值(前提是调用者传进来的值可能不规范,我们做一系列加工处理)计算出扩容阈值+负载因子
②如果调用者只传进来(表的大小Capacity)那么(负载因子loadFactor)就使用默认的值计算出扩容阈值+负载因子
③如果调用者只传来一个Map,那么经过一系列操做计算出扩容阈值+负载因子
④如果是空参构造方法,那么只会计算出负载因子(扩容阈值为空!!!)

//构造方法
public HashMap(int initialCapacity, float loadFactor) {
   
   

    //做一些校验,capacity必须大于0,最大值也就是MAX_CAP
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
        
    //lodaFactor负载因子必须大于0
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
                                           
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);//调用了下面的函数
}

public HashMap(int initialCapacity) {
   
   
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
   
   
	this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

public HashMap(Map<? extends K, ? extends V> m) {
   
   
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

// 此方法核心功能就是求出“大于等于输入长度的2次幂的值”
// 如输出:8,输出为8
// 如输出:9,输出为16
static final int tableSizeFor(int cap) {
   
   
    int n = cap - 1;
    n |= n >>> 1; //  n = n | (n >>> 1);
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY
public class CronExpressionBuilderUtil { private static final Map<String, String> DAY_OF_WEEK_MAP = new HashMap<>(); private static final Map<String, String> DAY_OF_WEEK_NUM_MAP = new HashMap<>(); static { DAY_OF_WEEK_MAP.put("SUNDAY", "1"); DAY_OF_WEEK_MAP.put("MONDAY", "2"); DAY_OF_WEEK_MAP.put("TUESDAY", "3"); DAY_OF_WEEK_MAP.put("WEDNESDAY", "4"); DAY_OF_WEEK_MAP.put("THURSDAY", "5"); DAY_OF_WEEK_MAP.put("FRIDAY", "6"); DAY_OF_WEEK_MAP.put("SATURDAY", "7"); DAY_OF_WEEK_MAP.forEach((k, v) -> DAY_OF_WEEK_NUM_MAP.put(v, k)); } public static String buildCronExpression(String periodTime, int periodHour, int periodMinute) { if (periodHour < 0 || periodHour > 23) { throw new IllegalArgumentException("小时数必须在0-23之间"); } if (periodMinute < 0 || periodMinute > 59) { throw new IllegalArgumentException("分钟数必须在0-59之间"); } String normalizedTime = periodTime.trim().toUpperCase(); // 根据周期类型生成cron switch (normalizedTime) { case "MONTH": // 格式: 秒 分 时 日(每月1号) 月(每月) 星期(不指定) return String.format("0 %d %d 1 * ?", periodMinute, periodHour); case "DAY": // 格式: 秒 分 时 日(每天) 月(每月) 星期(不指定) return String.format("0 %d %d * * ?", periodMinute, periodHour); default: // 星期类型处理 (如MONDAY) String cronDay = DAY_OF_WEEK_MAP.get(normalizedTime); if (cronDay == null) throw new IllegalArgumentException("Invalid periodTime: " + periodTime); // 格式: 秒 分 时 日(不指定) 月(每月) 星期(具体值) return String.format("0 %d %d ? * %s", periodMinute, periodHour, cronDay); } } /** * 从cron表达式反解析参数 * * @param cronExpression Quartz cron表达式 * @return Map包含periodTime, periodHour, periodMinute * @throws IllegalArgumentException cron格式无效时抛出异常 */ public static Map<String, Object> parseCron(String cronExpression) { if (cronExpression == null || cronExpression.trim().isEmpty()) { throw new IllegalArgumentException("cronExpression cannot be empty"); } String[] fields = cronExpression.trim().split("\\s+"); if (fields.length != 6) { throw new IllegalArgumentException("Invalid cron format (expected 6 fields)"); } // 验证固定字段 if (!"0".equals(fields[0]) || !"*".equals(fields[4]) || (!"?".equals(fields[5]) && !isValidDayOfWeek(fields[5]))) { throw new IllegalArgumentException("Unsupported cron structure"); } // 解析分钟和小时 int minute = Integer.parseInt(fields[1]); int hour = Integer.parseInt(fields[2]); if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { throw new IllegalArgumentException("Invalid time values in cron"); } Map<String, Object> result = new HashMap<>(); result.put("periodMinute", minute); result.put("periodHour", hour); // 处理特殊字符情况 if ("?".equals(fields[5])) { if ("*".equals(fields[3])) { result.put("periodTime", "DAY"); } else if ("1".equals(fields[3])) { result.put("periodTime", "MONTH"); } else { throw new IllegalArgumentException("Unsupported day pattern"); } } else { // 处理星期字段 String dayField = fields[5]; if (DAY_OF_WEEK_NUM_MAP.containsKey(dayField)) { result.put("periodTime", DAY_OF_WEEK_NUM_MAP.get(dayField)); } else if (DAY_OF_WEEK_MAP.containsValue(dayField)) { result.put("periodTime", getKeyByValue( dayField)); } else { throw new IllegalArgumentException("Invalid day-of-week format"); } } return result; } private static String getKeyByValue(String value) { return CronExpressionBuilderUtil.DAY_OF_WEEK_MAP.entrySet().stream() .filter(e -> e.getValue().equals(value)) .map(Map.Entry::getKey) .findFirst() .orElse(null); } private static boolean isValidDayOfWeek(String field) { return DAY_OF_WEEK_MAP.containsValue(field); // 检查是否在1-7范围内 } } 优化一下
最新发布
10-15
<think>我们正在优化一个用于构建和解析cron表达式的工具类。原代码有两个主要方法:`buildCronExpression`(用于根据参数构建cron表达式字符串)和`parseCron`(用于将cron表达式字符串解析为Map结构)。用户要求优化这两个方法。 原代码中`parseCron`方法存在以下问题: 1. 对cron表达式的结构限制过于严格:要求秒字段必须为"0",月份字段必须为"*",星期字段必须为"?"或特定的星期值(1-7或英文缩写)。这导致无法解析合法的cron表达式(如"0 45 15 * * ?"),因为原代码中星期字段索引为5(即第6个字段)要求必须是"?"或有效的星期值,而原代码在验证时对月份字段(索引4)要求必须是"*",但实际表达式中的月份字段是"*",星期字段是"?",所以理论上应该通过。但原代码中有一个条件:`(!"?".equals(fields[5]) && !isValidDayOfWeek(fields[5]))`,这意味着如果星期字段不是"?",则必须是有效的星期值。所以表达式"0 45 15 * * ?"是满足条件的。然而,问题可能是出在字段长度判断上?原代码要求6个字段,而实际表达式也是6个字段(秒、分、时、日、月、周)。 2. 原解析方法只解析了分钟、小时和星期,没有处理日期(日)字段。实际上,在标准的Quartz cron表达式中,日期(日)和星期(周)字段是互斥的,通常一个指定时另一个用"?"表示。 优化目标: 1. 构建方法(buildCronExpression)需要能够根据不同的调度类型(每天、每周、每月)生成正确的cron表达式。 2. 解析方法(parseCron)需要能够正确处理各种合法的cron表达式,特别是日期和星期字段的互斥关系,并返回一个包含调度信息的Map。 改进思路: 一、构建方法优化(buildCronExpression): 输入参数:一个Map,包含periodTime(调度时间,格式如"15:45")、periodType(调度类型:1-每天,2-每周,3-每月)、periodTimeDay(当periodType为2时,表示星期几,如"1"或"MON";当periodType为3时,表示日期,如"15") 输出:cron表达式字符串。 根据periodType构建cron表达式: 1. 每天:秒=0,分钟和小时取自periodTime,日期字段为"*",月份字段为"*",星期字段为"?" -> "0 分钟 小时 * * ?" 2. 每周:秒=0,分钟和小时取自periodTime,日期字段为"?",月份字段为"*",星期字段为星期几(如"1"或"MON") -> "0 分钟 小时 ? * 星期几" 3. 每月:秒=0,分钟和小时取自periodTime,日期字段为具体日期(如"15"),月份字段为"*",星期字段为"?" -> "0 分钟 小时 具体日期 * ?" 注意:星期字段的值,如果输入是数字(1-7),则使用数字;如果输入是字符串(如"MON"),则直接使用。但在构建时,我们统一使用数字表示星期(因为Quartz cron表达式支持数字1-7,其中1表示星期日,7表示星期六)?或者也可以支持英文缩写?根据原代码,似乎有一个DAY_OF_WEEK_MAP,用于将数字映射为英文缩写。但构建时,我们可以允许输入为数字或英文缩写,然后统一转换为数字(或者统一转换为英文缩写?),但Quartz同时支持这两种形式。为了简单,我们可以规定构建时,periodTimeDay如果是星期,则使用数字(1-7)或英文缩写(如"MON")都可以,但我们在构建时直接使用它(不转换),因为Quartz支持。但是,在解析时,我们可能需要统一转换为数字。 二、解析方法优化(parseCron): 步骤: 1. 拆分字段,检查字段数量(6个)。 2. 验证秒字段必须为0(因为我们只支持整点任务,不支持秒级调度)。 3. 解析分钟和小时字段(必须是整数,且在有效范围内)。 4. 处理日期和星期字段: - 日期字段(第4个字段,索引3)和星期字段(第6个字段,索引5)应该满足:其中一个为具体值,另一个为"?"(互斥)。 - 如果日期字段为"*"且星期字段为"?",则表示每天(即periodType=1)。 - 如果日期字段为"?"且星期字段为具体值(数字1-7或英文缩写),则表示每周(periodType=2),并解析星期值(将英文缩写转换为数字,或者直接使用数字)。 - 如果日期字段为具体数字(1-31)且星期字段为"?",则表示每月(periodType=3),日期值就是日期字段的值。 5. 如果遇到不满足上述互斥的情况,则抛出异常。 注意:月份字段(索引4)必须为"*"(因为我们不支持指定月份)。 返回的Map结构: { "periodMinute": 45, "periodHour": 15, "periodType": 1, // 1-每天,2-每周,3-每月 "periodTime": "3" // 当periodType=2时,为星期几(数字1-7);当periodType=3时,为日期(字符串形式的数字,如"15");当periodType=1时,这个字段可以不存在,或者设为null?或者也可以不返回这个字段?根据原解析方法,它只返回periodMinute, periodHour, periodTime(这个periodTime在每天的情况下不存在,所以原代码可能有问题)。因此,我们需要调整返回结构。 优化后的返回Map: - 对于每天:periodType=1,不需要periodTime(即不包含periodTime字段,或者包含但为null?) - 对于每周:periodType=2,periodTime为星期几(数字字符串,如"2"表示星期一) - 对于每月:periodType=3,periodTime为日期(字符串,如"15") 但是,原解析方法返回的Map中,periodTime在每天的情况下会抛出异常(因为每天时星期字段是"?",而原代码中要求星期字段必须是有效的星期值或"?",但在解析星期字段时,它试图将"?"映射为periodTime,这显然不对)。因此,我们需要重新设计返回结构。 我们可以这样设计返回的Map: - periodHour: 小时(整数) - periodMinute: 分钟(整数) - periodType: 整数(1,2,3) - periodDay: 当periodType=2或3时,表示具体的星期或日期(字符串)。对于星期,我们统一使用数字(1-7)表示。 注意:在构建cron表达式时,每周的periodTimeDay参数可以是数字也可以是英文缩写,但在解析时,我们统一转换为数字(1-7)返回。 三、代码重构: 1. 构建方法(buildCronExpression)优化: 根据periodType构建不同的cron表达式。 2. 解析方法(parseCron)优化: 根据日期和星期字段的取值情况确定periodType,并解析出相应的periodDay(星期或日期)。 四、辅助方法: 需要有一个将星期英文缩写转换为数字的方法,以及将数字转换为英文缩写的方法(构建时可以选择使用数字还是缩写,但解析时统一返回数字)。 五、常量定义: 定义星期映射关系:数字1-7与英文缩写的映射。 具体代码实现: 首先,定义常量映射: private static final Map<String, String> DAY_OF_WEEK_MAP = new HashMap<>(); static { DAY_OF_WEEK_MAP.put("1", "SUN"); DAY_OF_WEEK_MAP.put("2", "MON"); DAY_OF_WEEK_MAP.put("3", "TUE"); DAY_OF_WEEK_MAP.put("4", "WED"); DAY_OF_WEEK_MAP.put("5", "THU"); DAY_OF_WEEK_MAP.put("6", "FRI"); DAY_OF_WEEK_MAP.put("7", "SAT"); // 反向映射:英文缩写到数字(用于解析) DAY_OF_WEEK_MAP.put("SUN", "1"); DAY_OF_WEEK_MAP.put("MON", "2"); DAY_OF_WEEK_MAP.put("TUE", "3"); DAY_OF_WEEK_MAP.put("WED", "4"); DAY_OF_WEEK_MAP.put("THU", "5"); DAY_OF_WEEK_MAP.put("FRI", "6"); DAY_OF_WEEK_MAP.put("SAT", "7"); } 注意:这样定义的一个Map包含了两个方向的映射,但要注意键不能重复,因为英文缩写和数字字符串不会重复(数字字符串是"1"~"7",英文缩写是三个字母)。 但是,这样定义会有一个问题:数字字符串和英文缩写混合在一个Map中,在构建时,如果我们输入的是数字,则直接使用数字;如果输入的是英文缩写,则也是直接使用(因为Quartz都支持)。在解析时,如果星期字段是英文缩写,我们就通过这个Map转换为数字字符串。 然而,我们也可以单独建立一个英文缩写到数字的映射用于解析。 为了清晰,我们可以定义两个Map: private static final Map<String, String> NUMBER_TO_WEEK_ABBR = Map.of( "1", "SUN", "2", "MON", "3", "TUE", "4", "WED", "5", "THU", "6", "FRI", "7", "SAT" ); private static final Map<String, String> WEEK_ABBR_TO_NUMBER = Map.of( "SUN", "1", "MON", "2", "TUE", "3", "WED", "4", "THU", "5", "FRI", "6", "SAT", "7" ); 构建方法中,如果periodType=2,且periodTimeDay是数字,则直接使用;如果是英文缩写,则也可以直接使用(因为Quartz支持)。但为了统一,我们可以要求输入periodTimeDay时,如果是星期,则必须传入数字(1-7)?或者我们可以允许传入英文缩写,然后在构建时转换为数字?这里我们不做强制,因为Quartz两者都支持。但是,解析时我们统一返回数字。 因此,在解析方法中,遇到星期字段时,判断: 如果该字段是数字(1-7),则直接作为periodDay; 如果该字段是英文缩写,则通过WEEK_ABBR_TO_NUMBER转换为数字; 否则,报错。 构建方法中,我们不对periodTimeDay做转换,直接使用传入的值(因为Quartz支持数字和缩写)。但是,如果构建时传入的数字,解析出来也是数字,这是没问题的。如果构建时传入的是缩写,解析出来会转换为数字返回。这样,构建和解析可能会不对称(构建时用缩写,解析返回数字)。为了对称,我们可以在构建方法中将星期统一转换为数字?或者统一转换为缩写?或者不做处理,由调用方决定。 考虑到调用方可能传入数字也可能传入缩写,而我们的解析方法总是返回数字,所以构建方法中,我们可以尝试转换:如果periodTimeDay是英文缩写(即WEEK_ABBR_TO_NUMBER中包含这个键),则将其转换为对应的数字?或者不转换,直接使用。这里建议直接使用,因为Quartz两者都支持,而且解析时也能正确转换回来。 但是,为了保持一致性,我们可以在构建方法中统一使用数字表示星期。这样,如果调用方传入的是英文缩写,我们在构建方法中将其转换为数字。这样,构建的cron表达式中的星期字段就是数字。解析时,我们返回的也是数字,这样就不会有不对称的问题。 因此,在构建方法中,如果periodType=2(每周),且periodTimeDay是英文缩写(即WEEK_ABBR_TO_NUMBER的key中包含这个字符串),我们就将其转换为对应的数字字符串。如果periodTimeDay是数字(1-7),则直接使用。如果都不是,则抛出异常。 同理,对于periodType=3(每月),periodTimeDay必须是1-31之间的数字字符串(注意,这里日期没有英文缩写,所以直接使用)。 具体步骤: 优化后的buildCronExpression方法: 步骤: 1. 验证参数:periodTime(时间字符串)必须符合HH:mm格式,periodType必须是1,2,3。 2. 解析periodTime得到小时和分钟。 3. 根据periodType构建cron表达式: - 每天:返回 "0 [分钟] [小时] * * ?" - 每周:先处理periodTimeDay(星期): - 如果periodTimeDay在WEEK_ABBR_TO_NUMBER的keySet中(即传入的是英文缩写),则将其转换为数字(通过WEEK_ABBR_TO_NUMBER映射) - 如果periodTimeDay是数字字符串(1-7),则直接使用 - 否则,抛出异常 然后返回 "0 [分钟] [小时] ? * [星期]" - 每月:验证periodTimeDay必须是1-31之间的数字字符串(注意:不考虑月份的不同天数),如果不是,抛出异常。 然后返回 "0 [分钟] [小时] [日期] * ?" 解析方法(parseCron)优化: 步骤: 1. 拆分字段,检查是否为6个字段。 2. 验证秒字段必须为"0"(否则不支持)。 3. 验证月份字段(索引4)必须为"*"(因为我们不支持指定月份)。 4. 解析分钟(字段1)和小时(字段2)为整数,并验证范围(分钟0-59,小时0-23)。 5. 处理日期字段(索引3)和星期字段(索引5): a. 如果日期字段等于"*"且星期字段等于"?",则periodType=1(每天)。 b. 如果日期字段等于"?"且星期字段不等于"?"(即具体值),则periodType=2(每周): - 解析星期字段:如果是数字(1-7),则直接作为periodDay; - 如果是英文缩写(如SUN),则转换为数字(通过WEEK_ABBR_TO_NUMBER映射); - 否则,抛出异常。 c. 如果日期字段是数字(1-31)且星期字段等于"?",则periodType=3(每月),periodDay就是日期字段的值(字符串形式)。 d. 其他情况(如日期字段为"?"且星期字段也为"?",或者日期字段为具体值且星期字段也为具体值,或者日期字段为"*"且星期字段也为具体值等)均不支持,抛出异常。 6. 构建返回Map: Map<String, Object> result = new HashMap<>(); result.put("periodMinute", minute); result.put("periodHour", hour); result.put("periodType", periodType); // 整数1,2,3 if (periodType == 2 || periodType == 3) { result.put("periodDay", periodDay); // 注意:每周返回的是星期的数字字符串(如"1"),每月返回的是日期的数字字符串(如"15") } 注意:原解析方法返回的是periodTime,现在改为periodDay,并且只当periodType为2或3时存在。 六、代码实现: 重构后的CronExpressionBuilderUtil类: </think>以下是针对 `CronExpressionBuilderUtil` 类的优化方案,包含构建和反解析 cron 表达式的完整重构代码: ### 优化后的工具类实现 ```java import java.util.HashMap; import java.util.Map; public class CronExpressionBuilderUtil { // 星期映射表(数字 -> 英文缩写) private static final Map<String, String> DAY_OF_WEEK_MAP = new HashMap<>(); // 星期反向映射表(英文缩写 -> 数字) private static final Map<String, String> DAY_OF_WEEK_REVERSE_MAP = new HashMap<>(); static { DAY_OF_WEEK_MAP.put("1", "SUN"); DAY_OF_WEEK_MAP.put("2", "MON"); DAY_OF_WEEK_MAP.put("3", "TUE"); DAY_OF_WEEK_MAP.put("4", "WED"); DAY_OF_WEEK_MAP.put("5", "THU"); DAY_OF_WEEK_MAP.put("6", "FRI"); DAY_OF_WEEK_MAP.put("7", "SAT"); // 初始化反向映射 DAY_OF_WEEK_MAP.forEach((num, abbr) -> DAY_OF_WEEK_REVERSE_MAP.put(abbr, num)); } /** * 构建 cron 表达式 * @param periodType 调度类型:1=每天, 2=每周, 3=每月 * @param hour 小时 (0-23) * @param minute 分钟 (0-59) * @param dayValue 日期值(根据类型不同:每周=星期几, 每月=日期) * @return 符合 Quartz 规范的 cron 表达式 */ public static String buildCronExpression(int periodType, int hour, int minute, String dayValue) { validateTime(hour, minute); return switch (periodType) { case 1 -> // 每天 String.format("0 %d %d * * ?", minute, hour); case 2 -> // 每周 String.format("0 %d %d ? * %s", minute, hour, normalizeDayOfWeek(dayValue)); case 3 -> // 每月 String.format("0 %d %d %s * ?", minute, hour, validateDayOfMonth(dayValue)); default -> throw new IllegalArgumentException("Unsupported periodType: " + periodType); }; } /** * 解析 cron 表达式 * @param cronExpression Quartz cron 表达式 * @return 包含解析结果的 Map 结构 */ public static Map<String, Object> parseCron(String cronExpression) { if (cronExpression == null || cronExpression.trim().isEmpty()) { throw new IllegalArgumentException("cronExpression cannot be empty"); } String[] fields = cronExpression.trim().split("\\s+"); if (fields.length != 6) { throw new IllegalArgumentException("Invalid cron format. Expected 6 fields"); } // 验证固定约束 if (!"0".equals(fields[0]) || !"*".equals(fields[4])) { throw new IllegalArgumentException("Unsupported cron structure"); } // 解析时间字段 int minute = Integer.parseInt(fields[1]); int hour = Integer.parseInt(fields[2]); validateTime(hour, minute); // 解析调度类型和日期/星期 int periodType = determinePeriodType(fields[3], fields[5]); String periodDay = parsePeriodDay(periodType, fields[3], fields[5]); // 构建结果 Map<String, Object> result = new HashMap<>(); result.put("periodMinute", minute); result.put("periodHour", hour); result.put("periodType", periodType); if (periodDay != null) { result.put("periodDay", periodDay); } return result; } // ================ 辅助方法 ================ private static void validateTime(int hour, int minute) { if (hour < 0 || hour > 23) throw new IllegalArgumentException( "Invalid hour value: " + hour + " (must be 0-23)"); if (minute < 0 || minute > 59) throw new IllegalArgumentException( "Invalid minute value: " + minute + " (must be 0-59)"); } private static String validateDayOfMonth(String dayValue) { int day = Integer.parseInt(dayValue); if (day < 1 || day > 31) throw new IllegalArgumentException( "Invalid day of month: " + day + " (must be 1-31)"); return dayValue; } private static String normalizeDayOfWeek(String dayValue) { // 已经是数字直接返回 if (dayValue.matches("[1-7]")) return dayValue; // 英文缩写转换为数字 String normalized = DAY_OF_WEEK_REVERSE_MAP.get(dayValue.toUpperCase()); if (normalized == null) throw new IllegalArgumentException( "Invalid day of week: " + dayValue); return normalized; } private static int determinePeriodType(String dayField, String weekField) { boolean isDayWildcard = "*".equals(dayField) || "?".equals(dayField); boolean isWeekWildcard = "?".equals(weekField); if ("*".equals(dayField) && isWeekWildcard) return 1; // 每天 if ("?".equals(dayField) && !isWeekWildcard) return 2; // 每周 if (!isDayWildcard && "?".equals(weekField)) return 3; // 每月 throw new IllegalArgumentException("Unsupported cron pattern"); } private static String parsePeriodDay(int periodType, String dayField, String weekField) { return switch (periodType) { case 2 -> // 每周:解析星期字段 DAY_OF_WEEK_MAP.getOrDefault(weekField, weekField); case 3 -> // 每月:直接返回日期字段 dayField; default -> // 每天:无日期信息 null; }; } } ``` ### 关键优化点说明 1. **双向星期映射优化**: - 新增 `DAY_OF_WEEK_REVERSE_MAP` 实现英文缩写到数字的快速转换 - 消除原 `getKeyByValue()` 的流操作开销,时间复杂度从 $O(n)$ 降至 $O(1)$ 2. **构建方法增强**: ```java public static String buildCronExpression(int periodType, int hour, int minute, String dayValue) ``` - 明确参数类型和含义(避免原 Map 结构的模糊性) - 支持三种调度类型: - 每天:`0 m H * * ?` - 每周:`0 m H ? * D`(D=星期数字) - 每月:`0 m H D * ?` 3. **解析逻辑重构**: ```mermaid graph TD A[拆分字段] --> B{验证基本结构} B --> C[解析时间字段] C --> D{分析日期/星期字段} D -->|日=* 周=?| E[每天] D -->|日=? 周≠?| F[每周] D -->|日=数字 周=?| G[每月] E --> H[返回结果] F --> H G --> H ``` 4. **错误处理强化**: - 分离时间验证 `validateTime()` - 独立日期验证 `validateDayOfMonth()` - 明确异常消息(包含具体错误值) 5. **性能优化**: - 消除不必要的字符串操作 - 使用 switch 替代 if-else 链 - 预处理星期映射关系 ### 使用示例 **构建 cron 表达式**: ```java // 每天 15:45 执行 String dailyCron = buildCronExpression(1, 15, 45, null); // 每周一 9:30 执行(支持数字或缩写) String weeklyCron = buildCronExpression(2, 9, 30, "2"); // 使用数字 String weeklyCron2 = buildCronExpression(2, 9, 30, "MON"); // 使用缩写 // 每月 15 日 8:00 执行 String monthlyCron = buildCronExpression(3, 8, 0, "15"); ``` **解析 cron 表达式**: ```java Map<String, Object> result = parseCron("0 30 8 15 * ?"); // 返回结构: // { // "periodMinute": 30, // "periodHour": 8, // "periodType": 3, // "periodDay": "15" // } ``` ### 相关数学表达 对于 cron 字段的验证逻辑,使用以下数学约束: - 小时约束:$0 \leq H \leq 23$ - 分钟约束:$0 \leq m \leq 59$ - 日期约束:$1 \leq D \leq 31$ - 星期约束:$1 \leq W \leq 7$(其中 $1$=SUN, $7$=SAT) ### 相关问题 1. 如何处理包含秒级精度的 cron 表达式?[^1] 2. 如何扩展支持月份指定的 cron 表达式(如季度任务)? 3. 在分布式系统中如何保证 cron 解析的一致性?[^2] 4. 如何验证 cron 表达式的有效性而不实际执行?[^3] [^1]: Quartz cron 支持秒级精度,需扩展字段处理逻辑 [^2]: 建议使用 ISO 标准时间格式和协调世界时(UTC) [^3]: 可通过状态机验证 cron 语法结构
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BlackTurn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值