From 7bd0585a39c0d9f7896752d0f657cc1027968b41 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 25 Nov 2025 17:07:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96`EscapeUtil`=EF=BC=8C?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E4=B8=8D=E8=A7=84=E8=8C=83=E7=9A=84=E8=BD=AC?= =?UTF-8?q?=E4=B9=89=EF=BC=88pr#4150@Github=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hutool/v7/core/collection/CollUtil.java | 7 +- .../v7/core/collection/iter/IterUtil.java | 4 +- .../cn/hutool/v7/core/date/CalendarUtil.java | 33 ++-- .../cn/hutool/v7/core/date/StopWatch.java | 6 +- .../java/cn/hutool/v7/core/io/IoUtil.java | 4 +- .../java/cn/hutool/v7/core/map/BiMap.java | 87 ++++++++++- .../java/cn/hutool/v7/core/map/MapProxy.java | 18 +++ .../cn/hutool/v7/core/map/multi/Table.java | 4 +- .../v7/core/reflect/ConstructorUtil.java | 4 +- .../hutool/v7/core/reflect/FieldReflect.java | 6 +- .../cn/hutool/v7/core/reflect/FieldUtil.java | 8 +- .../v7/core/reflect/method/MethodReflect.java | 2 +- .../v7/core/reflect/method/MethodUtil.java | 10 +- .../hutool/v7/core/text/TextSimilarity.java | 37 +++-- .../v7/core/text/escape/EscapeUtil.java | 60 +++++-- .../v7/core/text/escape/EscapeUtilTest.java | 146 ++++++++++++++++-- 16 files changed, 357 insertions(+), 79 deletions(-) diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/collection/CollUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/collection/CollUtil.java index 0e6f60ebb..87b8758fe 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/collection/CollUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/collection/CollUtil.java @@ -1790,12 +1790,13 @@ public class CollUtil { } /** - * 获取集合中指定多个下标的元素值,下标可以为负数,例如-1表示最后一个元素 + * 获取集合中指定多个下标的元素值,下标可以为负数,例如-1表示最后一个元素
+ * 如果下标越界(转换负数索引后仍然越界),则抛出异常 * * @param 元素类型 * @param collection 集合 - * @param indexes 下标,支持负数 - * @return 元素值列表 + * @param indexes 下标列表,支持负数(负数表示从末尾倒数,-1为最后一个元素,-2为倒数第二个,以此类推) + * @return 元素值列表,返回 {@link ArrayList},只包含有效下标对应的元素 * @since 4.0.6 */ @SuppressWarnings("unchecked") diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/collection/iter/IterUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/collection/iter/IterUtil.java index a1f18ace3..dd9ad88e5 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/collection/iter/IterUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/collection/iter/IterUtil.java @@ -243,7 +243,7 @@ public class IterUtil { * @param 对象类型 * @param iterable 对象列表 * @param fieldName 字段名(会通过反射获取其值) - * @return 某个字段值与对象对应Map + * @return 字段值列表 * @since 4.6.2 */ public static List fieldValueList(final Iterable iterable, final String fieldName) { @@ -257,7 +257,7 @@ public class IterUtil { * @param 对象类型 * @param iter 对象列表 * @param fieldName 字段名(会通过反射获取其值) - * @return 某个字段值与对象对应Map + * @return 字段值列表 * @since 4.0.10 */ @SuppressWarnings("unchecked") diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/date/CalendarUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/date/CalendarUtil.java index 9ff00adaa..fea7a4ad3 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/date/CalendarUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/date/CalendarUtil.java @@ -512,10 +512,11 @@ public class CalendarUtil { } /** - * 获取指定日期字段的最小值,例如分钟的最小值是0 + * 获取指定日期字段的最小值,例如分钟的最小值是0
+ * 对于 {@link DateField#DAY_OF_WEEK} 字段,会返回一周的第一天,详见 {@link #getBeginValue(Calendar, int)} * * @param calendar {@link Calendar} - * @param dateField {@link DateField} + * @param dateField {@link DateField} 日期字段 * @return 字段最小值 * @see Calendar#getActualMinimum(int) * @since 5.4.2 @@ -525,7 +526,10 @@ public class CalendarUtil { } /** - * 获取指定日期字段的最小值,例如分钟的最小值是0 + * 获取指定日期字段的最小值,例如分钟的最小值是0
+ * 对于 {@link Calendar#DAY_OF_WEEK} 字段,需要特殊处理:
+ * 由于 DAY_OF_WEEK 的值范围是 1-7,取决于一周的第一天设置(周日或周一),
+ * 本方法会直接返回 {@link Calendar#getFirstDayOfWeek()} 的值作为一周的第一天。 * * @param calendar {@link Calendar} * @param dateField {@link DateField} @@ -535,18 +539,21 @@ public class CalendarUtil { */ public static int getBeginValue(final Calendar calendar, final int dateField) { if (Calendar.DAY_OF_WEEK == dateField) { + // DAY_OF_WEEK 的值范围是 1-7,直接返回一周的第一天 return calendar.getFirstDayOfWeek(); } return calendar.getActualMinimum(dateField); } /** - * 获取指定日期字段的最大值,例如分钟的最大值是59 + * 获取指定日期字段的最大值,例如分钟的最大值是59
+ * 对于 {@link DateField#DAY_OF_WEEK} 字段,会根据一周的第一天设置计算最后一天,详见 {@link #getEndValue(Calendar, int)} * * @param calendar {@link Calendar} - * @param dateField {@link DateField} + * @param dateField {@link DateField} 日期字段 * @return 字段最大值 * @see Calendar#getActualMaximum(int) + * @see #getEndValue(Calendar, int) * @since 5.4.2 */ public static int getEndValue(final Calendar calendar, final DateField dateField) { @@ -554,16 +561,22 @@ public class CalendarUtil { } /** - * 获取指定日期字段的最大值,例如分钟的最大值是59 + * 获取指定日期字段的最大值,例如分钟的最大值是59
+ * 对于 {@link Calendar#DAY_OF_WEEK} 字段,需要特殊处理:
+ * 由于 DAY_OF_WEEK 的值范围是 1-7,取决于一周的第一天设置(周日或周一),
+ * 本方法会根据 {@link Calendar#getFirstDayOfWeek()} 计算出一周的最后一天。
+ * 计算逻辑:最后一天 = 第一天 + 6,如果结果 > 7,则减去 7 以保持在 1-7 范围内。
+ * 例如:如果第一天是周一(2),则最后一天是周日(1);如果第一天是周日(1),则最后一天是周六(7)。 * * @param calendar {@link Calendar} - * @param dateField {@link DateField} + * @param dateField {@link DateField} 日期字段,使用 {@link Calendar} 中的字段常量,如 {@link Calendar#MINUTE}、{@link Calendar#DAY_OF_WEEK} 等 * @return 字段最大值 * @see Calendar#getActualMaximum(int) * @since 4.5.7 */ public static int getEndValue(final Calendar calendar, final int dateField) { if (Calendar.DAY_OF_WEEK == dateField) { + // DAY_OF_WEEK 的值范围是 1-7,根据一周的第一天计算最后一天 return (calendar.getFirstDayOfWeek() + 6) % 7; } return calendar.getActualMaximum(dateField); @@ -584,10 +597,10 @@ public class CalendarUtil { } /** - * Calendar{@link Instant}对象 + * 将 {@link Calendar} 转换为 {@link Instant} 对象 * - * @param calendar Date对象 - * @return {@link Instant}对象 + * @param calendar {@link Calendar} 对象 + * @return {@link Instant} 对象,如果 calendar 为 {@code null} 则返回 {@code null} * @since 5.0.5 */ public static Instant toInstant(final Calendar calendar) { diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/date/StopWatch.java b/hutool-core/src/main/java/cn/hutool/v7/core/date/StopWatch.java index a0c3298d4..b58f5293d 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/date/StopWatch.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/date/StopWatch.java @@ -165,7 +165,7 @@ public class StopWatch { /** * 开始默认的新任务 * - * @throws IllegalStateException 前一个任务没有结束 + * @throws IllegalStateException 当前已有任务正在运行时抛出此异常,必须先调用{@link #stop()}停止当前任务 */ public void start() throws IllegalStateException { start(StrUtil.EMPTY); @@ -175,7 +175,7 @@ public class StopWatch { * 开始指定名称的新任务 * * @param taskName 新开始的任务名称 - * @throws IllegalStateException 前一个任务没有结束 + * @throws IllegalStateException 当前已有任务正在运行时抛出此异常,必须先调用{@link #stop()}停止当前任务 */ public void start(final String taskName) throws IllegalStateException { if (null != this.currentTaskName) { @@ -188,7 +188,7 @@ public class StopWatch { /** * 停止当前任务 * - * @throws IllegalStateException 任务没有开始 + * @throws IllegalStateException 当前没有正在运行的任务时抛出此异常,必须先调用{@link #start()}或{@link #start(String)}开始任务 */ public void stop() throws IllegalStateException { if (null == this.currentTaskName) { diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/io/IoUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/io/IoUtil.java index 0870fa246..d17fceb0b 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/io/IoUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/io/IoUtil.java @@ -229,7 +229,7 @@ public class IoUtil extends NioUtil { /** * 获得一个Writer,默认编码UTF-8 * - * @param out 输入流 + * @param out 输出流 * @return OutputStreamWriter对象 * @since 5.1.6 */ @@ -240,7 +240,7 @@ public class IoUtil extends NioUtil { /** * 获得一个Writer * - * @param out 输入流 + * @param out 输出流 * @param charset 字符集 * @return OutputStreamWriter对象 */ diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/map/BiMap.java b/hutool-core/src/main/java/cn/hutool/v7/core/map/BiMap.java index 62ff33620..0d43550d9 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/map/BiMap.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/map/BiMap.java @@ -49,6 +49,15 @@ public class BiMap extends MapWrapper { super(raw); } + /** + * 将指定的键值对放入Map中,同时同步更新反向Map + *

+ * 如果key已存在但value不同,会在反向Map中移除旧的value到key的映射,并建立新的value到key的映射 + * + * @param key 键 + * @param value 值 + * @return 与key关联的旧值,如果key不存在则返回null + */ @Override public V put(final K key, final V value) { final V oldValue = super.put(key, value); @@ -63,6 +72,11 @@ public class BiMap extends MapWrapper { return oldValue; } + /** + * 将指定Map中的所有映射关系复制到此Map中,同时同步更新反向Map + * + * @param m 要存储在此Map中的映射关系 + */ @Override public void putAll(final Map m) { super.putAll(m); @@ -71,6 +85,12 @@ public class BiMap extends MapWrapper { } } + /** + * 从Map中移除指定键的映射关系,同时同步从反向Map中移除对应的反向映射 + * + * @param key 要从Map中移除其映射关系的键 + * @return 与key关联的旧值,如果key不存在则返回null + */ @Override public V remove(final Object key) { final V v = super.remove(key); @@ -80,11 +100,21 @@ public class BiMap extends MapWrapper { return v; } + /** + * 仅当指定键当前映射到指定值时才移除该键的映射关系,同时同步从反向Map中移除对应的反向映射 + * + * @param key 与指定值关联的键 + * @param value 与指定键关联的值 + * @return 如果删除成功则返回true + */ @Override public boolean remove(final Object key, final Object value) { return super.remove(key, value) && null != this.inverse && this.inverse.remove(value, key); } + /** + * 从Map中移除所有映射关系,同时清空反向Map + */ @Override public void clear() { super.clear(); @@ -113,14 +143,34 @@ public class BiMap extends MapWrapper { return getInverse().get(value); } + /** + * 如果指定的键尚未与值关联(或映射到null),则将其与给定值关联,同时同步更新反向Map + *

+ * 只有当键成功添加到主Map时(即键之前不存在),才会同步更新反向Map,确保双向映射的一致性 + * + * @param key 与指定值关联的键 + * @param value 与指定键关联的值 + * @return 与指定键关联的当前值,如果没有映射则返回null + */ @Override public V putIfAbsent(final K key, final V value) { - if (null != this.inverse) { - this.inverse.putIfAbsent(value, key); + final V oldValue = super.putIfAbsent(key, value); + // 只有当oldValue为null时(即key之前不存在),才更新反向Map + if (null == oldValue && null != this.inverse) { + this.inverse.put(value, key); } - return super.putIfAbsent(key, value); + return oldValue; } + /** + * 如果指定的键尚未与值关联(或映射到null),则使用给定的映射函数计算其值并放入此Map中 + *

+ * 由于此操作可能会修改Map的映射关系,因此在操作完成后会重置反向Map,下次访问时会重新构建 + * + * @param key 与计算值关联的键 + * @param mappingFunction 用于计算值的映射函数 + * @return 与指定键关联的当前(现有的或计算的)值,如果计算的值为null则返回null + */ @Override public V computeIfAbsent(final K key, final Function mappingFunction) { final V result = super.computeIfAbsent(key, mappingFunction); @@ -128,6 +178,16 @@ public class BiMap extends MapWrapper { return result; } + /** + * 如果指定键的值存在且非null,则使用给定的重新映射函数计算新值 + *

+ * 如果重新映射函数返回null,则移除该映射关系。由于此操作会修改Map的映射关系, + * 因此在操作完成后会重置反向Map,下次访问时会重新构建 + * + * @param key 与指定值关联的键 + * @param remappingFunction 用于计算值的重新映射函数 + * @return 与指定键关联的新值,如果不存在则返回null + */ @Override public V computeIfPresent(final K key, final BiFunction remappingFunction) { final V result = super.computeIfPresent(key, remappingFunction); @@ -135,6 +195,16 @@ public class BiMap extends MapWrapper { return result; } + /** + * 尝试计算指定键及其当前映射值的映射关系(如果当前映射不存在,则为null) + *

+ * 此方法用于为指定的键计算新的值。如果重新映射函数返回null,则移除该映射关系(如果存在)。 + * 由于此操作会修改Map的映射关系,因此在操作完成后会重置反向Map,下次访问时会重新构建 + * + * @param key 与指定值关联的键 + * @param remappingFunction 用于计算值的重新映射函数 + * @return 与指定键关联的新值,如果不存在则返回null + */ @Override public V compute(final K key, final BiFunction remappingFunction) { final V result = super.compute(key, remappingFunction); @@ -142,6 +212,17 @@ public class BiMap extends MapWrapper { return result; } + /** + * 如果指定的键尚未与值关联或关联的值为null,则将其与给定的非null值关联 + *

+ * 否则,使用给定的重新映射函数的结果替换关联的值,如果结果为null则移除该映射关系。 + * 由于此操作会修改Map的映射关系,因此在操作完成后会重置反向Map,下次访问时会重新构建 + * + * @param key 与指定值关联的键 + * @param value 要与键关联的非null值 + * @param remappingFunction 用于重新计算值的重新映射函数 + * @return 与指定键关联的新值,如果不存在则返回null + */ @Override public V merge(final K key, final V value, final BiFunction remappingFunction) { final V result = super.merge(key, value, remappingFunction); diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/map/MapProxy.java b/hutool-core/src/main/java/cn/hutool/v7/core/map/MapProxy.java index 9413f25cb..8a18646d4 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/map/MapProxy.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/map/MapProxy.java @@ -139,6 +139,24 @@ public class MapProxy implements Map, TypeGetter, Invoca return map.entrySet(); } + /** + * 实现InvocationHandler接口的调用处理方法 + *

+ * 此方法用于动态代理,支持以下功能: + *

    + *
  • 将getXXX方法调用转换为从Map中获取对应key的值
  • + *
  • 将isXXX方法调用(针对boolean类型)转换为从Map中获取对应key的值
  • + *
  • 将setXXX方法调用转换为向Map中设置对应key的值
  • + *
  • 支持驼峰命名和下划线命名的自动转换
  • + *
  • 自动进行类型转换,将Map中的值转换为方法返回类型
  • + *
+ * + * @param proxy 代理实例 + * @param method 被调用的方法 + * @param args 方法参数 + * @return 方法调用的结果 + * @throws UnsupportedOperationException 如果方法不支持代理 + */ @Override public Object invoke(final Object proxy, final Method method, final Object[] args) { final Class[] parameterTypes = method.getParameterTypes(); diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/map/multi/Table.java b/hutool-core/src/main/java/cn/hutool/v7/core/map/multi/Table.java index 188a6c5f7..e3dbd76fa 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/map/multi/Table.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/map/multi/Table.java @@ -56,7 +56,7 @@ public interface Table extends Iterable> { /** * 行是否存在 * - * @param rowKey 行键 + * @param rowKey 行键set * @return 行是否存在 */ default boolean containsRow(final R rowKey) { @@ -124,7 +124,7 @@ public interface Table extends Iterable> { /** * 返回所有列的key,列的key如果实现Map是可重复key,则返回对应不去重的List。 * - * @return 列set + * @return 列List * @since 5.8.0 */ default List columnKeys() { diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/ConstructorUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/ConstructorUtil.java index fc500b360..7d537916c 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/ConstructorUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/ConstructorUtil.java @@ -77,7 +77,7 @@ public class ConstructorUtil { * @param 构造的对象类型 * @param beanClass 类,非{@code null} * @return 字段列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ @SuppressWarnings("unchecked") public static Constructor[] getConstructors(final Class beanClass) throws SecurityException { @@ -90,7 +90,7 @@ public class ConstructorUtil { * * @param beanClass 类 * @return 字段列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public static Constructor[] getConstructorsDirectly(final Class beanClass) throws SecurityException { return beanClass.getDeclaredConstructors(); diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/FieldReflect.java b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/FieldReflect.java index d96612303..b13373690 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/FieldReflect.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/FieldReflect.java @@ -76,7 +76,7 @@ public class FieldReflect { * * @param predicate 过滤器 * @return 字段数组 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public Field[] getDeclaredFields(final Predicate predicate) { if (null == declaredFields) { @@ -94,7 +94,7 @@ public class FieldReflect { * * @param predicate 过滤器 * @return 字段数组 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public Field[] getAllFields(final Predicate predicate) { if (null == allFields) { @@ -113,7 +113,7 @@ public class FieldReflect { * * @param withSuperClassFields 是否包括父类的字段列表 * @return 字段列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public Field[] getFieldsDirectly(final boolean withSuperClassFields) throws SecurityException { Field[] allFields = null; diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/FieldUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/FieldUtil.java index eae7e4392..148210b98 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/FieldUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/FieldUtil.java @@ -158,7 +158,7 @@ public class FieldUtil { * * @param beanClass 类 * @return 字段列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public static Field[] getFields(final Class beanClass) throws SecurityException { return getFields(beanClass, null); @@ -172,7 +172,7 @@ public class FieldUtil { * @param beanClass 类 * @param fieldPredicate field过滤器,过滤掉不需要的field,{@link Predicate#test(Object)}为{@code true}保留,null表示全部保留 * @return 字段列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 * @since 5.7.14 */ public static Field[] getFields(final Class beanClass, final Predicate fieldPredicate) throws SecurityException { @@ -186,7 +186,7 @@ public class FieldUtil { * @param beanClass 类 * @param fieldPredicate field过滤器,过滤掉不需要的field,{@link Predicate#test(Object)}为{@code true}保留,null表示全部保留 * @return 字段列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 * @since 6.0.0 */ public static Field[] getDeclaredFields(final Class beanClass, final Predicate fieldPredicate) throws SecurityException { @@ -201,7 +201,7 @@ public class FieldUtil { * @param beanClass 类 * @param withSuperClassFields 是否包括父类的字段列表 * @return 字段列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public static Field[] getFieldsDirectly(final Class beanClass, final boolean withSuperClassFields) throws SecurityException { return FieldReflect.of(beanClass).getFieldsDirectly(withSuperClassFields); diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/method/MethodReflect.java b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/method/MethodReflect.java index c3f9fcdcc..749ffa0fe 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/method/MethodReflect.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/method/MethodReflect.java @@ -149,7 +149,7 @@ public class MethodReflect { * @param withSupers 是否包括父类或接口的方法列表 * @param withMethodFromObject 是否包括Object中的方法 * @return 方法列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public Method[] getMethodsDirectly(final boolean withSupers, final boolean withMethodFromObject) throws SecurityException { final Class clazz = this.clazz; diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/method/MethodUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/method/MethodUtil.java index 268145075..ddcd934c9 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/reflect/method/MethodUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/reflect/method/MethodUtil.java @@ -286,7 +286,7 @@ public class MethodUtil { * * @param clazz 类,非{@code null} * @return 方法列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public static Method[] getMethods(final Class clazz) throws SecurityException { return getMethods(clazz, null); @@ -298,7 +298,7 @@ public class MethodUtil { * @param clazz 类,非{@code null} * @param predicate 方法过滤器,{@code null}表示无过滤 * @return 方法列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public static Method[] getMethods(final Class clazz, final Predicate predicate) throws SecurityException { return METHODS_CACHE.computeIfAbsent(Assert.notNull(clazz), MethodReflect::of).getAllMethods(predicate); @@ -330,7 +330,7 @@ public class MethodUtil { * * @param clazz 类,非{@code null} * @return 方法列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public static Method[] getDeclaredMethods(final Class clazz) throws SecurityException { return getDeclaredMethods(clazz, null); @@ -342,7 +342,7 @@ public class MethodUtil { * @param clazz 类,非{@code null} * @param predicate 方法过滤器,{@code null}表示无过滤 * @return 方法列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public static Method[] getDeclaredMethods(final Class clazz, final Predicate predicate) throws SecurityException { return METHODS_CACHE.computeIfAbsent(Assert.notNull(clazz), MethodReflect::of).getDeclaredMethods(predicate); @@ -363,7 +363,7 @@ public class MethodUtil { * @param withSupers 是否包括父类或接口的方法列表 * @param withMethodFromObject 是否包括Object中的方法 * @return 方法列表 - * @throws SecurityException 安全检查异常 + * @throws SecurityException 当安全管理器存在且拒绝访问时抛出 */ public static Method[] getMethodsDirectly(final Class beanClass, final boolean withSupers, final boolean withMethodFromObject) throws SecurityException { return MethodReflect.of(Assert.notNull(beanClass)).getMethodsDirectly(withSupers, withMethodFromObject); diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/text/TextSimilarity.java b/hutool-core/src/main/java/cn/hutool/v7/core/text/TextSimilarity.java index b56cda95c..741297962 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/text/TextSimilarity.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/text/TextSimilarity.java @@ -34,12 +34,23 @@ public class TextSimilarity { *
  • 只比较两个字符串字母、数字、汉字部分,其他符号去除
  • *
  • 计算出两个字符串最大子串,除以最长的字符串,结果即为相似度
  • * + * 空值处理: + *
      + *
    • 如果两个字符串都为{@code null},返回1(视为完全相同)
    • + *
    • 如果其中一个字符串为{@code null},返回0(视为完全不同)
    • + *
    * * @param strA 字符串1 * @param strB 字符串2 - * @return 相似度 + * @return 相似度,取值范围为[0, 1],其中0表示完全不同,1表示完全相同 */ public static double similar(final String strA, final String strB) { + if (null == strA && null == strB) { + return 1; + } + if (null == strA || null == strB) { + return 0; + } final String newStrA; final String newStrB; if (strA.length() < strB.length()) { @@ -52,7 +63,7 @@ public class TextSimilarity { // 用较大的字符串长度作为分母,相似子串作为分子计算出字串相似度 final int temp = Math.max(newStrA.length(), newStrB.length()); - if(0 == temp) { + if (0 == temp) { // 两个都是空串相似度为1,被认为是相同的串 return 1; } @@ -64,8 +75,8 @@ public class TextSimilarity { /** * 利用莱文斯坦距离(Levenshtein distance)算法计算相似度百分比 * - * @param strA 字符串1 - * @param strB 字符串2 + * @param strA 字符串1 + * @param strB 字符串2 * @param scale 保留小数 * @return 百分比 */ @@ -79,9 +90,12 @@ public class TextSimilarity { * * @param strA 字符串1 * @param strB 字符串2 - * @return 最长公共子串 + * @return 最长公共子串,如果任一参数为{@code null}则返回{@code null},如果没有公共子串则返回空字符串"" */ public static String longestCommonSubstring(final String strA, final String strB) { + if (null == strA || null == strB) { + return null; + } // 初始化矩阵数据,matrix[0][0]的值为0, 如果字符数组chars_strA和chars_strB的对应位相同,则matrix[i][j]的值为左上角的值加1, // 否则,matrix[i][j]的值等于左上方最近两个位置的较大值, 矩阵中其余各点的值为0. final int[][] matrix = generateMatrix(strA, strB); @@ -108,6 +122,7 @@ public class TextSimilarity { } // --------------------------------------------------------------------------------------------------- Private method start + /** * 将字符串的所有数据依次写成一行,去除无意义字符串 * @@ -121,7 +136,7 @@ public class TextSimilarity { char c; for (int i = 0; i < length; i++) { c = str.charAt(i); - if(isValidChar(c)) { + if (isValidChar(c)) { sb.append(c); } } @@ -137,9 +152,9 @@ public class TextSimilarity { */ private static boolean isValidChar(final char charValue) { return (charValue >= 0x4E00 && charValue <= 0X9FFF) || // - (charValue >= 'a' && charValue <= 'z') || // - (charValue >= 'A' && charValue <= 'Z') || // - (charValue >= '0' && charValue <= '9'); + (charValue >= 'a' && charValue <= 'z') || // + (charValue >= 'A' && charValue <= 'Z') || // + (charValue >= '0' && charValue <= '9'); } /** @@ -162,9 +177,9 @@ public class TextSimilarity { for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (strA.charAt(i - 1) == strB.charAt(j - 1)) { - currLine[j] = lastLine[j-1] + 1; + currLine[j] = lastLine[j - 1] + 1; } else { - currLine[j] = Math.max(currLine[j-1], lastLine[j]); + currLine[j] = Math.max(currLine[j - 1], lastLine[j]); } } temp = lastLine; diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/text/escape/EscapeUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/text/escape/EscapeUtil.java index 41d3917eb..917723b4c 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/text/escape/EscapeUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/text/escape/EscapeUtil.java @@ -131,9 +131,10 @@ public class EscapeUtil { return StrUtil.toStringOrNull(content); } - final StringBuilder tmp = new StringBuilder(content.length() * 6); + final int len = content.length(); + final StringBuilder tmp = new StringBuilder(len * 6); char c; - for (int i = 0; i < content.length(); i++) { + for (int i = 0; i < len; i++) { c = content.charAt(i); if (!filter.test(c)) { tmp.append(c); @@ -156,7 +157,26 @@ public class EscapeUtil { } /** - * Escape解码 + * Escape解码支持两种转义格式的解码: + *
      + *
    • %XX - 两位十六进制数字,用于表示ASCII字符(0-255)
    • + *
    • %uXXXX - 四位十六进制数字,用于表示Unicode字符
    • + *
    + *

    + * 对于不完整的转义序列,本方法会将其原样保留而不抛出异常: + *

      + *
    • 字符串末尾的单独"%"字符会被原样保留
    • + *
    • "%u"后面不足4位十六进制数字时,整个不完整序列会被原样保留
    • + *
    • "%"后面不足2位十六进制数字时(非%u格式),整个不完整序列会被原样保留
    • + *
    + * 例如: + *
    +	 * unescape("test%")      = "test%"     // 末尾的%被保留
    +	 * unescape("test%u12")   = "test%u12"  // 不足4位,原样保留
    +	 * unescape("test%2")     = "test%2"    // 不足2位,原样保留
    +	 * unescape("test%20")    = "test "     // 正常解码空格
    +	 * unescape("test%u4E2D") = "test中"    // 正常解码中文字符
    +	 * 
    * * @param content 被转义的内容 * @return 解码后的字符串 @@ -166,26 +186,40 @@ public class EscapeUtil { return content; } - final StringBuilder tmp = new StringBuilder(content.length()); + final int len = content.length(); + final StringBuilder tmp = new StringBuilder(len); int lastPos = 0; int pos; char ch; - while (lastPos < content.length()) { + while (lastPos < len) { pos = content.indexOf("%", lastPos); if (pos == lastPos) { - if (content.charAt(pos + 1) == 'u') { - ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); - tmp.append(ch); - lastPos = pos + 6; + if (pos + 1 < len && content.charAt(pos + 1) == 'u') { + if (pos + 6 <= len) { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } else { + // Not enough characters, append as-is + tmp.append(content.substring(pos)); + lastPos = len; + } } else { - ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); - tmp.append(ch); - lastPos = pos + 3; + // Check if there's enough characters for hex escape (%XX) + if (pos + 3 <= len) { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } else { + // Not enough characters, append as-is + tmp.append(content.substring(pos)); + lastPos = len; + } } } else { if (pos == -1) { tmp.append(content.substring(lastPos)); - lastPos = content.length(); + lastPos = len; } else { tmp.append(content, lastPos, pos); lastPos = pos; diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/text/escape/EscapeUtilTest.java b/hutool-core/src/test/java/cn/hutool/v7/core/text/escape/EscapeUtilTest.java index 171365477..b5dcc24ff 100644 --- a/hutool-core/src/test/java/cn/hutool/v7/core/text/escape/EscapeUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/v7/core/text/escape/EscapeUtilTest.java @@ -16,31 +16,33 @@ package cn.hutool.v7.core.text.escape; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + public class EscapeUtilTest { @Test public void escapeHtml4Test() { final String escapeHtml4 = EscapeUtil.escapeHtml4("你好"); - Assertions.assertEquals("<a>你好</a>", escapeHtml4); + assertEquals("<a>你好</a>", escapeHtml4); final String result = EscapeUtil.unescapeHtml4("振荡器类型"); - Assertions.assertEquals("振荡器类型", result); + assertEquals("振荡器类型", result); final String escape = EscapeUtil.escapeHtml4("*@-_+./(123你好)"); - Assertions.assertEquals("*@-_+./(123你好)", escape); + assertEquals("*@-_+./(123你好)", escape); } @Test public void escapeTest(){ final String str = "*@-_+./(123你好)ABCabc"; final String escape = EscapeUtil.escape(str); - Assertions.assertEquals("*@-_+./%28123%u4f60%u597d%29ABCabc", escape); + assertEquals("*@-_+./%28123%u4f60%u597d%29ABCabc", escape); final String unescape = EscapeUtil.unescape(escape); - Assertions.assertEquals(str, unescape); + assertEquals(str, unescape); } @Test @@ -48,24 +50,24 @@ public class EscapeUtilTest { final String str = "*@-_+./(123你好)ABCabc"; final String escape = EscapeUtil.escapeAll(str); - Assertions.assertEquals("%2a%40%2d%5f%2b%2e%2f%28%31%32%33%u4f60%u597d%29%41%42%43%61%62%63", escape); + assertEquals("%2a%40%2d%5f%2b%2e%2f%28%31%32%33%u4f60%u597d%29%41%42%43%61%62%63", escape); final String unescape = EscapeUtil.unescape(escape); - Assertions.assertEquals(str, unescape); + assertEquals(str, unescape); } /** - * https://gitee.com/chinabugotech/hutool/issues/I49JU8 + * issue#I49JU8 */ @Test public void escapeAllTest2(){ final String str = "٩"; final String escape = EscapeUtil.escapeAll(str); - Assertions.assertEquals("%u0669", escape); + assertEquals("%u0669", escape); final String unescape = EscapeUtil.unescape(escape); - Assertions.assertEquals(str, unescape); + assertEquals(str, unescape); } @Test @@ -73,21 +75,135 @@ public class EscapeUtilTest { // 单引号不做转义 final String str = "'some text with single quotes'"; final String s = EscapeUtil.escapeHtml4(str); - Assertions.assertEquals("'some text with single quotes'", s); + assertEquals("'some text with single quotes'", s); } @Test public void unescapeSingleQuotesTest(){ final String str = "'some text with single quotes'"; final String s = EscapeUtil.unescapeHtml4(str); - Assertions.assertEquals("'some text with single quotes'", s); + assertEquals("'some text with single quotes'", s); } @Test public void escapeXmlTest(){ final String a = "<>"; final String escape = EscapeUtil.escapeXml(a); - Assertions.assertEquals("<>", escape); - Assertions.assertEquals("中文“双引号”", EscapeUtil.escapeXml("中文“双引号”")); + assertEquals("<>", escape); + assertEquals("中文“双引号”", EscapeUtil.escapeXml("中文“双引号”")); + } + + @Test + void testUnescapeNull() { + assertNull(EscapeUtil.unescape(null)); + } + + @Test + void testUnescapeEmpty() { + assertEquals("", EscapeUtil.unescape("")); + } + + @Test + void testUnescapeBlank() { + assertEquals(" ", EscapeUtil.unescape(" ")); + } + + @Test + void testUnescapeAsciiCharacters() { + // 测试ASCII字符转义 + assertEquals("hello", EscapeUtil.unescape("hello")); + assertEquals("test space", EscapeUtil.unescape("test%20space")); + assertEquals("A", EscapeUtil.unescape("%41")); + assertEquals("a", EscapeUtil.unescape("%61")); + assertEquals("0", EscapeUtil.unescape("%30")); + assertEquals("!", EscapeUtil.unescape("%21")); + assertEquals("@", EscapeUtil.unescape("%40")); + assertEquals("#", EscapeUtil.unescape("%23")); + } + + @Test + void testUnescapeUnicodeCharacters() { + // 测试Unicode字符转义 + assertEquals("中", EscapeUtil.unescape("%u4E2D")); + assertEquals("文", EscapeUtil.unescape("%u6587")); + assertEquals("测", EscapeUtil.unescape("%u6D4B")); + assertEquals("试", EscapeUtil.unescape("%u8BD5")); + assertEquals("😊", EscapeUtil.unescape("%uD83D%uDE0A")); // 笑脸表情 + } + + @Test + void testUnescapeMixedContent() { + // 测试混合内容 + assertEquals("Hello 世界!", EscapeUtil.unescape("Hello%20%u4E16%u754C%21")); + assertEquals("测试: 100%", EscapeUtil.unescape("%u6D4B%u8BD5%3A%20100%25")); + assertEquals("a+b=c", EscapeUtil.unescape("a%2Bb%3Dc")); + } + + @Test + void testUnescapeIncompleteEscapeSequences() { + // 测试不完整的转义序列 + assertEquals("test%", EscapeUtil.unescape("test%")); + assertEquals("test%u", EscapeUtil.unescape("test%u")); + assertEquals("test%u1", EscapeUtil.unescape("test%u1")); + assertEquals("test%u12", EscapeUtil.unescape("test%u12")); + assertEquals("test%u123", EscapeUtil.unescape("test%u123")); + assertEquals("test%1", EscapeUtil.unescape("test%1")); + assertEquals("test%2", EscapeUtil.unescape("test%2")); + } + + @Test + void testUnescapeEdgeCases() { + // 测试边界情况 + assertEquals("%", EscapeUtil.unescape("%")); + assertEquals("%u", EscapeUtil.unescape("%u")); + assertEquals("%%", EscapeUtil.unescape("%%")); + assertEquals("%u%", EscapeUtil.unescape("%u%")); + assertEquals("100% complete", EscapeUtil.unescape("100%25%20complete")); + } + + @Test + void testUnescapeMultipleEscapeSequences() { + // 测试多个连续的转义序列 + assertEquals("ABC", EscapeUtil.unescape("%41%42%43")); + assertEquals("中文测试", EscapeUtil.unescape("%u4E2D%u6587%u6D4B%u8BD5")); + assertEquals("A 中 B", EscapeUtil.unescape("%41%20%u4E2D%20%42")); + } + + @Test + void testUnescapeSpecialCharacters() { + // 测试特殊字符 + assertEquals("\n", EscapeUtil.unescape("%0A")); + assertEquals("\r", EscapeUtil.unescape("%0D")); + assertEquals("\t", EscapeUtil.unescape("%09")); + assertEquals(" ", EscapeUtil.unescape("%20")); + assertEquals("<", EscapeUtil.unescape("%3C")); + assertEquals(">", EscapeUtil.unescape("%3E")); + assertEquals("&", EscapeUtil.unescape("%26")); + } + + @Test + void testUnescapeComplexScenario() { + // 测试复杂场景 + final String original = "Hello 世界! 这是测试。Email: test@example.com"; + final String escaped = "Hello%20%u4E16%u754C%21%20%u8FD9%u662F%u6D4B%u8BD5%u3002Email%3A%20test%40example.com"; + assertEquals(original, EscapeUtil.unescape(escaped)); + } + + @Test + void testUnescapeWithIncompleteAtEnd() { + // 测试末尾有不完整转义序列 + assertEquals("normal%", EscapeUtil.unescape("normal%")); + assertEquals("normal%u", EscapeUtil.unescape("normal%u")); + assertEquals("normal%u1", EscapeUtil.unescape("normal%u1")); + assertEquals("normal%1", EscapeUtil.unescape("normal%1")); + } + + @Test + void testUnescapeUppercaseHex() { + // 测试大写十六进制 + assertEquals("A", EscapeUtil.unescape("%41")); + assertEquals("A", EscapeUtil.unescape("%41")); + assertEquals("中", EscapeUtil.unescape("%u4E2D")); + assertEquals("中", EscapeUtil.unescape("%u4E2D")); } }