Compare commits

...

13 Commits

Author SHA1 Message Date
Looly
bd42686a65 CombinationArrangement 重构避免数组频繁拷贝,并避免溢出(pr#4144@Github) 2025-11-24 16:05:43 +08:00
Golden Looly
e8eec73125
Merge pull request #4144 from CherryRum/fix-combination
Enhance Mathematical Correctness and Performance in Combination and Arrangement Modules
2025-11-24 16:04:03 +08:00
yulin
04314cdd4a fix(Arrangement): improve count and select methods with overflow checks and enhance documentation 2025-11-23 23:37:30 +08:00
Looly
4d04d5daf9 修复TypeUtil.getClass无法识别GenericArrayType问题(pr#4138@Github) 2025-11-23 23:27:45 +08:00
Golden Looly
1a7d522dff
Merge pull request #4138 from sunshineflymeat/hutool-1120
Fix issue 4137
2025-11-23 23:23:10 +08:00
Looly
eec6056876 修复FileNameUtil.extName在特殊后缀判断逻辑过于宽松导致误判问题(pr#4142@Github) 2025-11-23 23:17:01 +08:00
Golden Looly
c6cbaeabff
Merge pull request #4142 from ZhonglinGui/v5-dev
修复FileNameUtil.extName 方法对特殊后缀判断逻辑过于宽松导致误判
2025-11-23 23:14:42 +08:00
Golden Looly
9dad6fc4d5
Merge pull request #4143 from CherryRum/v5-dev
修改部分描述
2025-11-23 23:11:00 +08:00
yulin
5e1110426b feat(Combination): add BigInteger-based combination calculation methods and deprecate old count method 2025-11-23 22:41:39 +08:00
yulin
edb4401e47 fix(LocalPortGenerater): update class documentation and plan for future name correction 2025-11-23 19:26:36 +08:00
yulin
7258a5b946 fix(URLUtil): correct spelling of "occurred" in exception messages 2025-11-23 19:12:35 +08:00
Will
17f78f8cd4 修复FileNameUtil.extName 方法对特殊后缀判断逻辑过于宽松导致误判 2025-11-21 14:42:07 +08:00
zwm
ed27c637b2 feat: 解决TypeUtil.getClass方法传入泛型数组类型时方法返回错误值null问题 2025-11-20 23:18:42 +08:00
11 changed files with 540 additions and 62 deletions

View File

@ -1,13 +1,14 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
# 5.8.42(2025-11-20)
# 5.8.42(2025-11-24)
### 🐣新特性
* 【core 】 `ListUtil`增加`zip`方法pr#4052@Github
* 【http 】 增加`JakartaSoapClient`issue#4103@Github
* 【ai 】 增加代理支持pr#4107@Github
* 【core 】 `CharSequenceUtil`增加`builder`方法重载pr#4107@Github
* 【core 】 `Combination``Arrangement `重构避免数组频繁拷贝并避免溢出pr#4144@Github
### 🐞Bug修复
* 【jwt 】 修复verify方法在定义alg为`none`时验证失效问题issue#4105@Github
@ -21,6 +22,8 @@
* 【core 】 修复`ImgUtil.write`没有释放BufferedImage可能导致内存泄露issue#ID6VNJ@Gitee
* 【core 】 修复`VersionUtil.matchEl`如果输入的版本范围表达式右边界为空时会抛出数组越界访问错误的问题pr#4130@Github
* 【core 】 修复`Validator.isBetween`在高精度Number类型下存在精度丢失问题pr#4136@Github
* 【core 】 修复`FileNameUtil.extName`在特殊后缀判断逻辑过于宽松导致误判问题pr#4142@Github
* 【core 】 修复`TypeUtil.getClass`无法识别`GenericArrayType`问题pr#4138@Github
-------------------------------------------------------------------------------------------------------------
# 5.8.41(2025-10-12)

View File

@ -238,7 +238,7 @@ public class FileNameUtil {
// issue#I4W5FS@Gitee
final int secondToLastIndex = fileName.substring(0, index).lastIndexOf(StrUtil.DOT);
final String substr = fileName.substring(secondToLastIndex == -1 ? index : secondToLastIndex + 1);
if (StrUtil.containsAny(substr, SPECIAL_SUFFIX)) {
if (StrUtil.equalsAny(substr, SPECIAL_SUFFIX)) {
return substr;
}

View File

@ -1,12 +1,9 @@
package cn.hutool.core.math;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.NumberUtil;
import java.io.Serializable;
import java.util.*;
/**
* 排列A(n, m)<br>
@ -47,10 +44,23 @@ public class Arrangement implements Serializable {
* @return 排列数
*/
public static long count(int n, int m) {
if (n == m) {
return NumberUtil.factorial(n);
if (m < 0 || m > n) {
throw new IllegalArgumentException("n >= 0 && m >= 0 && m <= n required");
}
return (n > m) ? NumberUtil.factorial(n, n - m) : 0;
if (m == 0) {
return 1;
}
long result = 1;
// n n-m+1 逐个乘
for (int i = 0; i < m; i++) {
long next = result * (n - i);
// 溢出检测
if (next < result) {
throw new ArithmeticException("Overflow computing A(" + n + "," + m + ")");
}
result = next;
}
return result;
}
/**
@ -77,26 +87,85 @@ public class Arrangement implements Serializable {
}
/**
* 排列选择从列表中选择m个排列
* 从当前数据中选择 m 个元素生成所有不重复的排列Permutation
*
* @param m 选择个数
* @return 所有排列列表
* <p>
* 说明
* <ul>
* <li>不允许重复选择同一个元素即经典排列 A(n, m)</li>
* <li>结果中不会出现 ["1","1"] 这种重复元素的情况</li>
* <li>顺序敏感因此 ["1","2"] ["2","1"] 都会包含</li>
* </ul>
*
* 数量公式
* <pre>
* A(n, m) = n! / (n - m)!
* </pre>
*
* 举例
* <pre>
* datas = ["1","2","3"]
* m = 2
* 输出
* ["1","2"]
* ["1","3"]
* ["2","1"]
* ["2","3"]
* ["3","1"]
* ["3","2"]
* 6 A(3,2)=6
* </pre>
*
* @param m 选择的元素个数
* @return 所有长度为 m 的不重复排列列表
*/
public List<String[]> select(int m) {
final List<String[]> result = new ArrayList<>((int) count(this.datas.length, m));
select(this.datas, new String[m], 0, result);
if (m < 0 || m > datas.length) {
return Collections.emptyList();
}
if (m == 0) {
// A(n,0) = 1唯一一个空排列
return Collections.singletonList(new String[0]);
}
long estimated = count(datas.length, m);
int capacity = estimated > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) estimated;
List<String[]> result = new ArrayList<>(capacity);
boolean[] visited = new boolean[datas.length];
dfs(new String[m], 0, visited, result);
return result;
}
/**
* 排列所有组合即A(n, 1) + A(n, 2) + A(n, 3)...
* 生成当前数据的全部不重复排列长度为 1 n 的所有排列
*
* @return 全排列结果
* <p>
* 说明
* <ul>
* <li>不允许重复选择元素 ["1","1"] ["2","2","3"] 这种</li>
* <li>包含所有长度 m=1..n 的排列</li>
* <li>总数量为 A(n,1) + A(n,2) + ... + A(n,n)</li>
* </ul>
*
* 举例datas = ["1","2","3"]
* <pre>
* m=1: ["1"], ["2"], ["3"] 3
* m=2: ["1","2"], ["1","3"], ["2","1"], ... 6
* m=3: ["1","2","3"], ["1","3","2"], ["2","1","3"], ... 6
*
* 总共3 + 6 + 6 = 15
* </pre>
*
* @return 所有不重复排列列表
*/
public List<String[]> selectAll() {
final List<String[]> result = new ArrayList<>((int) countAll(this.datas.length));
for (int i = 1; i <= this.datas.length; i++) {
result.addAll(select(i));
List<String[]> result = new ArrayList<>();
for (int m = 1; m <= datas.length; m++) {
result.addAll(select(m));
}
return result;
}
@ -124,4 +193,113 @@ public class Arrangement implements Serializable {
select(ArrayUtil.remove(datas, i), resultList, resultIndex + 1, result);
}
}
/**
* 返回一个排列的迭代器
*
* @param m 选择的元素个数
* @return 排列迭代器
*/
public Iterable<String[]> iterate(int m) {
return () -> new ArrangementIterator(datas, m);
}
/**
* 排列迭代器
*
* @author CherryRum
*/
private static class ArrangementIterator implements Iterator<String[]> {
private final String[] datas;
private final int m;
private final boolean[] visited;
private final String[] buffer;
private final Deque<Integer> stack = new ArrayDeque<>();
boolean end = false;
ArrangementIterator(String[] datas, int m) {
this.datas = datas;
this.m = m;
this.visited = new boolean[datas.length];
this.buffer = new String[m];
// 初始化 dfs
stack.push(0);
}
@Override
public boolean hasNext() {
return !end;
}
@Override
public String[] next() {
while (!stack.isEmpty()) {
int depth = stack.size() - 1;
int idx = stack.pop();
if (idx >= datas.length) {
// 这一层遍历结束
if (!stack.isEmpty()) {
int prev = stack.pop();
stack.push(prev + 1);
}
continue;
}
// 如果该元素未使用
if (!visited[idx]) {
visited[idx] = true;
buffer[depth] = datas[idx];
if (depth == m - 1) {
// 输出一个排列
visited[idx] = false;
// 下一次从 idx+1 继续
stack.push(idx + 1);
return Arrays.copyOf(buffer, m);
} else {
// 继续下一层
stack.push(idx + 1); // 当前层下一个起点
stack.push(0); // 下一层起点
continue;
}
}
// 已访问则跳过
stack.push(idx + 1);
}
end = true;
return null;
}
}
/**
* 核心递归方法回溯算法
* * @param current 当前构建的排列数组
*
* @param depth 当前递归深度填到了第几个位置
* @param visited 标记数组记录哪些索引已经被使用了
* @param result 结果集
*/
private void dfs(String[] current, int depth, boolean[] visited, List<String[]> result) {
if (depth == current.length) {
result.add(Arrays.copyOf(current, current.length));
return;
}
for (int i = 0; i < datas.length; i++) {
if (!visited[i]) {
visited[i] = true;
current[depth] = datas[i];
dfs(current, depth + 1, visited, result);
visited[i] = false;
}
}
}
}

View File

@ -1,13 +1,13 @@
package cn.hutool.core.math;
import cn.hutool.core.util.StrUtil;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
/**
* 组合即C(n, m)<br>
* 排列组合相关类 参考http://cgs1999.iteye.com/blog/2327664
@ -32,17 +32,70 @@ public class Combination implements Serializable {
/**
* 计算组合数即C(n, m) = n!/((n-m)! * m!)
*
* <p>注意此方法内部使用 BigInteger 修复了旧版 factorial 的计算错误
* 但最终仍以 long 返回因此当结果超过 long 范围时仍会溢出</p>
* <p>建议使用 {@link #countBig(int, int)} 获取精确结果或使用
* {@link #countSafe(int, int)} 获取安全 long 版本</p>
* @param n 总数
* @param m 选择的个数
* @return 组合数
*/
@Deprecated
public static long count(int n, int m) {
if (0 == m || n == m) {
return 1;
BigInteger big = countBig(n, m);
return big.longValue();
}
return (n > m) ? NumberUtil.factorial(n, n - m) / NumberUtil.factorial(m) : 0;
/**
* 计算组合数 C(n, m) BigInteger 精确版本
* 使用逐步累乘除法非阶乘保证不溢出性能好
* <p>
* 数学定义
* C(n, m) = n! / (m! (n - m)!)
* <p>
* 优化方式
* 1. 利用对称性 m = min(m, n-m)
* 2. 每一步先乘 BigInteger再除以当前 i保证数值不暴涨
*
* @param n 总数 n必须 >= 0
* @param m 取出 m必须 >= 0
* @return C(n, m) BigInteger 精确值 m > n 时返回 BigInteger.ZERO
*/
public static BigInteger countBig(int n, int m) {
if (n < 0 || m < 0) {
throw new IllegalArgumentException("n and m must be non-negative. got n=" + n + ", m=" + m);
}
if (m > n) {
return BigInteger.ZERO;
}
if (m == 0 || n == m) {
return BigInteger.ONE;
}
// 使用对称性C(n, m) = C(n, n-m)
m = Math.min(m, n - m);
BigInteger result = BigInteger.ONE;
// 1 m 累乘
for (int i = 1; i <= m; i++) {
int numerator = n - m + i;
result = result.multiply(BigInteger.valueOf(numerator))
.divide(BigInteger.valueOf(i));
}
return result;
}
/**
* 安全组合数 long 版本
*
* @param n 总数 n必须 >= 0
* @param m 取出 m必须 >= 0
* <p>若结果超出 long 范围会抛 ArithmeticException而非溢出</p>
*/
public static long countSafe(int n, int m) {
BigInteger big = countBig(n, m);
return big.longValueExact();
}
/**
* 计算组合总数即C(n, 1) + C(n, 2) + C(n, 3)...
@ -104,4 +157,5 @@ public class Combination implements Serializable {
select(i + 1, resultList, resultIndex + 1, result);
}
}
}

View File

@ -4,14 +4,34 @@ import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 本地端口生成器<br>
* 用于生成本地可用未被占用的端口号<br>
* 注意多线程甚至单线程访问时可能会返回同一端口例如获取了端口但是没有使用
* 本地端口生成器LocalPortGenerator
* <p>
* 当前类名中Generater为拼写错误正确应为 Generator为保持兼容性暂未更改
* 该问题将在后续大版本中以重命名方式修复并保留旧类名的弃用@Deprecated兼容层
* <p>
*
* 用于从指定起点开始递增探测一个当前可用的本地端口探测通过短暂绑定
* {@link java.net.ServerSocket}以及可选 UDP DatagramSocket完成但不会真正占用端口
* <p>注意</p>
* <ul>
* <li>该方法执行的是端口探测分配返回端口不保证实际使用时仍然可用</li>
* <li>存在 TOCTOU检测到使用之间竞态多线程下可能返回同一端口</li>
* <li>UDP 探测可能导致误判TCP 可用但 UDP 被占用</li>
* <li>不适合作为生产级端口分配策略推荐使用 {@code new ServerSocket(0)}</li>
* </ul>
*
* <p>未来版本计划</p>
* <ul>
* <li>修复类名拼写问题Generater更名为 Generator</li>
* <li>提供真正可靠的端口获取实现绑定即占用避免竞态</li>
* <li>优化探测策略减少不必要的 UDP 检测</li>
* <li>提供更安全的随机端口生成 API</li>
* </ul>
* @author looly
* @since 4.0.3
*
*/
public class LocalPortGenerater implements Serializable{
private static final long serialVersionUID = 1L;

View File

@ -34,6 +34,12 @@ public class TypeUtil {
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
return (Class<?>) ((ParameterizedType) type).getRawType();
} else if (type instanceof GenericArrayType) {
final Type componentType = ((GenericArrayType) type).getGenericComponentType();
final Class<?> componentClass = getClass(componentType);
if (componentClass != null) {
return Array.newInstance(componentClass, 0).getClass();
}
} else if (type instanceof TypeVariable) {
Type[] bounds = ((TypeVariable<?>) type).getBounds();
if (bounds.length == 1) {

View File

@ -270,7 +270,7 @@ public class URLUtil extends URLEncodeUtil {
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw new UtilException(e, "Error occured when get URL!");
throw new UtilException(e, "Error occurred when get URL!");
}
}
@ -288,7 +288,7 @@ public class URLUtil extends URLEncodeUtil {
urls[i] = files[i].toURI().toURL();
}
} catch (MalformedURLException e) {
throw new UtilException(e, "Error occured when get URL!");
throw new UtilException(e, "Error occurred when get URL!");
}
return urls;

View File

@ -19,4 +19,12 @@ public class FileNameUtilTest {
final String s = FileNameUtil.mainName("abc.tar.gz");
assertEquals("abc", s);
}
@Test
public void extNameAndMainNameBugTest() {
// 正确输出前缀为 "app-v2.3.1-star"
assertEquals("app-v2.3.1-star",FileNameUtil.mainName("app-v2.3.1-star.gz"));
// 当前代码会失败预期后缀结果 "gz"但是输出 "star.gz"
assertEquals("gz", FileNameUtil.extName("app-v2.3.1-star.gz"));
}
}

View File

@ -1,20 +1,22 @@
package cn.hutool.core.math;
import cn.hutool.core.lang.Console;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import cn.hutool.core.lang.Console;
/**
* 排列单元测试
* @author looly
*
*/
public class ArrangementTest {
// ----------------------------------------------------
// 基础测试
// ----------------------------------------------------
@Test
public void arrangementTest() {
long result = Arrangement.count(4, 2);
@ -30,37 +32,117 @@ public class ArrangementTest {
assertEquals(64, resultAll);
}
// ----------------------------------------------------
// select 基础测试
// ----------------------------------------------------
@Test
public void selectTest() {
Arrangement arrangement = new Arrangement(new String[] { "1", "2", "3", "4" });
Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3", "4"});
List<String[]> list = arrangement.select(2);
assertEquals(Arrangement.count(4, 2), list.size());
assertArrayEquals(new String[] {"1", "2"}, list.get(0));
assertArrayEquals(new String[] {"1", "3"}, list.get(1));
assertArrayEquals(new String[] {"1", "4"}, list.get(2));
assertArrayEquals(new String[] {"2", "1"}, list.get(3));
assertArrayEquals(new String[] {"2", "3"}, list.get(4));
assertArrayEquals(new String[] {"2", "4"}, list.get(5));
assertArrayEquals(new String[] {"3", "1"}, list.get(6));
assertArrayEquals(new String[] {"3", "2"}, list.get(7));
assertArrayEquals(new String[] {"3", "4"}, list.get(8));
assertArrayEquals(new String[] {"4", "1"}, list.get(9));
assertArrayEquals(new String[] {"4", "2"}, list.get(10));
assertArrayEquals(new String[] {"4", "3"}, list.get(11));
// 校验数量一致
assertEquals(Arrangement.count(4, 2), list.size());
// 逐项严格校验顺序是否一致 DFS 顺序
assertArrayEquals(new String[]{"1", "2"}, list.get(0));
assertArrayEquals(new String[]{"1", "3"}, list.get(1));
assertArrayEquals(new String[]{"1", "4"}, list.get(2));
assertArrayEquals(new String[]{"2", "1"}, list.get(3));
assertArrayEquals(new String[]{"2", "3"}, list.get(4));
assertArrayEquals(new String[]{"2", "4"}, list.get(5));
assertArrayEquals(new String[]{"3", "1"}, list.get(6));
assertArrayEquals(new String[]{"3", "2"}, list.get(7));
assertArrayEquals(new String[]{"3", "4"}, list.get(8));
assertArrayEquals(new String[]{"4", "1"}, list.get(9));
assertArrayEquals(new String[]{"4", "2"}, list.get(10));
assertArrayEquals(new String[]{"4", "3"}, list.get(11));
// 测试 selectAll
List<String[]> selectAll = arrangement.selectAll();
assertEquals(Arrangement.countAll(4), selectAll.size());
// m=0应该返回一个空排列
List<String[]> list2 = arrangement.select(0);
assertEquals(1, list2.size());
assertEquals(0, list2.get(0).length);
}
// ----------------------------------------------------
// 扩展测试边界错误处理
// ----------------------------------------------------
@Test
@Disabled
public void selectTest2() {
List<String[]> list = MathUtil.arrangementSelect(new String[] { "1", "1", "3", "4" });
for (String[] strings : list) {
Console.log(strings);
public void boundaryTest() {
Arrangement arr = new Arrangement(new String[]{"A", "B", "C"});
// m = n
List<String[]> full = arr.select(3);
assertEquals(6, full.size());
// m = 1
List<String[]> one = arr.select(1);
assertEquals(3, one.size());
assertArrayEquals(new String[]{"A"}, one.get(0));
// m > n empty list
assertTrue(arr.select(10).isEmpty());
// m < 0 empty list
assertTrue(arr.select(-1).isEmpty());
}
// ----------------------------------------------------
// 扩展测试空数组
// ----------------------------------------------------
@Test
public void emptyTest() {
Arrangement arrangement = new Arrangement(new String[]{});
assertEquals(1, arrangement.select(0).size());
assertTrue(arrangement.select(1).isEmpty());
assertTrue(arrangement.selectAll().isEmpty()); // A(0,m) = 0 for m>0A(0,0)=1 全排列 = 1 个空排列
}
// ----------------------------------------------------
// 扩展测试重复元素用于验证去重算法
// 默认 Arrangement 不去重因此应该包含重复排列
// ----------------------------------------------------
@Test
@Disabled("默认 Arrangement 不支持去重;启用后手动检查")
public void duplicateElementTest() {
Arrangement arrangement = new Arrangement(new String[]{"1", "1", "3"});
List<String[]> list = arrangement.select(2);
// 应该有 A(3,2) = 6
assertEquals(6, list.size());
for (String[] s : list) {
Console.log(s);
}
}
// ----------------------------------------------------
// 扩展测试selectAll 覆盖全部不重复排列A(n,1..n)
// ----------------------------------------------------
@Test
public void selectAllTest() {
Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3"});
List<String[]> all = arrangement.selectAll();
// 打印用于观测
for (String[] s : all) {
Console.log(s);
}
// A(3,1) + A(3,2) + A(3,3) = 3 + 6 + 6 = 15
assertEquals(Arrangement.countAll(3), all.size());
assertEquals(15, all.size());
// spot check 不重复排列
assertArrayEquals(new String[]{"1"}, all.get(0));
assertArrayEquals(new String[]{"1", "2"}, all.get(3));
assertArrayEquals(new String[]{"1", "2", "3"}, all.get(9));
}
}

View File

@ -1,10 +1,12 @@
package cn.hutool.core.math;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* 组合单元测试
*
@ -51,4 +53,96 @@ public class CombinationTest {
List<String[]> list2 = combination.select(0);
assertEquals(1, list2.size());
}
// -----------------------------
// countBig() 正确性测试
// -----------------------------
@Test
void testCountBig_basicCases() {
assertEquals(BigInteger.ONE, Combination.countBig(5, 0));
assertEquals(BigInteger.ONE, Combination.countBig(5, 5));
assertEquals(BigInteger.valueOf(10), Combination.countBig(5, 3));
assertEquals(BigInteger.valueOf(10), Combination.countBig(5, 2));
}
@Test
void testCountBig_mGreaterThanN() {
assertEquals(BigInteger.ZERO, Combination.countBig(5, 6));
}
@Test
void testCountBig_negativeInput() {
assertThrows(IllegalArgumentException.class, () -> Combination.countBig(-1, 3));
assertThrows(IllegalArgumentException.class, () -> Combination.countBig(5, -2));
}
@Test
void testCountBig_symmetry() {
assertEquals(Combination.countBig(20, 3), Combination.countBig(20, 17));
}
@Test
void testCountBig_largeNumbers() {
// C(50, 3) = 19600
assertEquals(new BigInteger("19600"), Combination.countBig(50, 3));
// C(100, 50) 的确切值重要测试
BigInteger expected = new BigInteger(
"100891344545564193334812497256"
);
assertEquals(expected, Combination.countBig(100, 50));
}
@Test
void testCountBig_veryLargeCombination() {
// 不比较具体值只断言不要抛错
BigInteger result = Combination.countBig(2000, 1000);
assertTrue(result.signum() > 0);
}
// -----------------------------
// count(long) 兼容性测试
// -----------------------------
@Test
void testCount_basic() {
assertEquals(10L, Combination.count(5, 3));
assertEquals(1L, Combination.count(5, 0));
assertEquals(0L, Combination.count(5, 6));
}
@Test
void testCount_overflowBehavior() {
// C(100, 50) 远超 long 范围但旧版行为要求不抛异常
long r = Combination.count(100, 50);
// longValue() 不抛异常并且可能溢出
assertNotNull(r);
}
@Test
void testCount_noException() {
assertDoesNotThrow(() -> Combination.count(5000, 2500));
}
// -----------------------------
// countSafe() 安全 long 版本测试
// -----------------------------
@Test
void testCountSafe_exactFitsLong() {
// C(50, 3) = 19600 fits long
assertEquals(19600L, Combination.countSafe(50, 3));
}
@Test
void testCountSafe_overflowThrows() {
// C(100, 50) 超出 long 应抛 ArithmeticException
assertThrows(ArithmeticException.class, () -> Combination.countSafe(100, 50));
}
@Test
void testCountSafe_invalidInput() {
assertThrows(IllegalArgumentException.class, () -> Combination.countSafe(-1, 3));
assertThrows(IllegalArgumentException.class, () -> Combination.countSafe(3, -1));
}
}

View File

@ -1,5 +1,7 @@
package cn.hutool.core.util;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
@ -38,7 +40,7 @@ public class TypeUtilTest {
public void getClasses() {
Method method = ReflectUtil.getMethod(Parent.class, "getLevel");
Type returnType = TypeUtil.getReturnType(method);
Class clazz = TypeUtil.getClass(returnType);
Class<?> clazz = TypeUtil.getClass(returnType);
assertEquals(Level1.class, clazz);
method = ReflectUtil.getMethod(Level1.class, "getId");
@ -47,6 +49,37 @@ public class TypeUtilTest {
assertEquals(Object.class, clazz);
}
/**
* 测试getClass方法对泛型数组类型T[]的处理
* 验证未绑定泛型参数的数组类型会被正确解析为Object[]
*/
@Test
public void getClassForGenericArrayTypeTest() throws NoSuchFieldException {
// 获取T[]类型字段的泛型类型
Field levelField = GenericArray.class.getDeclaredField("level");
Type genericArrayType = levelField.getGenericType();
// 调用getClass方法处理GenericArrayType
Class<?> clazz = TypeUtil.getClass(genericArrayType);
// 验证返回Object[]类型
assertNotNull(clazz, "getClass方法返回null");
assertTrue(clazz.isArray(), "返回类型不是数组");
assertEquals(Object.class, clazz.getComponentType(), "数组组件类型应为Object");
}
/**
* 测试getClass方法对参数化类型数组{@code List<String>[]}的处理
* 验证数组组件类型能正确解析为原始类型
*/
@Test
public void getClassForParameterizedArrayTypeTest() {
// 创建List<String>[]类型引用
Type genericArrayType = new TypeReference<List<String>[]>() {}.getType();
// 调用getClass方法处理GenericArrayType
Class<?> clazz = TypeUtil.getClass(genericArrayType);
// 验证返回List[]类型
assertEquals(Array.newInstance(List.class, 0).getClass(), clazz);
}
public static class TestClass {
public List<String> getList() {
return new ArrayList<>();