wildcard-matcher
高性能通配符字符串匹配工具(Java,零依赖单文件)。输入:待匹配字符串 + 匹配模式 + 长度。
*匹配任意长度(含 0)的任意字符序列,包含/(不做”路径不跨目录”特例)?匹配恰好一个字符- 其余字符按字面量精确匹配
- 模式必须完整匹配整个字符串(等价于正则的
Matcher.matches())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
package com.example.wildmatch;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
/**
* 高性能通配符字符串匹配工具(零依赖、无正则、无锁、可跨线程共享)。
*
* <h2>语义</h2>
* <ul>
* <li>{@code '*'} 匹配任意长度(含 0)的任意字符序列;</li>
* <li>{@code '?'} 匹配恰好一个任意字符;</li>
* <li>其余字符按字面量精确匹配;</li>
* <li>整个模式必须<b>完整匹配</b>整个待匹配字符串(等价于正则的 {@code Matcher.matches()})。</li>
* </ul>
*
* <p>注意:{@code '*'} 匹配<b>任意</b>字符,包括 {@code '/'}、{@code '\'} 等分隔符,
* 不做"路径 glob 不跨目录"的特例(与"*号匹配任意字符"的语义一致)。
* 若需要"不跨目录"的路径语义,请按分隔符切分后逐段匹配,或把每段的未知字符用 {@code '?'} 约束长度。
*
* <h2>长度匹配</h2>
* 第三个参数 {@code expectedLength} 为 {@code null} 时不做长度校验;非 null 时要求
* {@code input.length() == expectedLength},不等则立即返回 false(在扫描前就短路,是最便宜的加速手段)。
* 负数长度视为不匹配(返回 false,不抛异常,便于直接处理脏数据)。
* {@code input == null} 一律返回 false。
*
* <h2>用法</h2>
* <pre>
* // 1) 复用模式(推荐:模式编译一次,匹配千万次)
* private static final WildcardMatcher M = WildcardMatcher.compile("order_*_2024?");
* M.matches("order_10086_2024A");
* M.matches("order_10086_2024A", 17); // 顺带校验长度
* M.matches("order_10086_2024A", null); // 不校验长度
*
* // 2) 一次性调用(内部有模式缓存,重复模式不会重复编译)
* WildcardMatcher.isMatch("order_10086_2024A", "order_*_2024?", null);
*
* // 3) 零拷贝匹配已有缓冲区(日志/报文解析场景,避免 substring 产生新对象)
* matcher.matches(buffer, offset, len, len);
* </pre>
*
* <p>命名说明:一次性调用的静态方法叫 {@code isMatch(...)} 而不是 {@code matches(...)},
* 是为了避免与实例方法 {@code matches(String, Integer)} 在传 {@code null}(即"不校验长度")
* 时产生重载二义性。
*
* <h2>为什么快</h2>
* <ol>
* <li><b>预编译</b>:`compile()` 时完成模式扫描、连续 {@code '*'} 折叠、按 {@code '*'} 切分片段、
* 计算最短可能长度,匹配阶段不再解析模式。</li>
* <li><b>按模式形态分流</b>:纯字面量走 {@link String#equals};只有 {@code '?'} 走一趟线性比较;
* 只有 {@code '*'} 直接 true;通用情况才进入片段匹配。</li>
* <li><b>锚定前后缀 + 片段贪心</b>:把模式按 {@code '*'} 切成片段后,首片段若不以 {@code '*'} 开头即锚定在头部,
* 末片段若不以 {@code '*'} 结尾即锚定在尾部(于是中间片段的搜索窗口被收窄到 {@code [pos, n-suffixLen-segLen]}),
* 中间片段从左到右取<b>最早</b>出现位置。匹配过程严格 O(n) 量级、无递归、无回溯爆炸,
* 不像正则 {@code .*} 那样可能指数退化。</li>
* <li><b>字面量"锚"跳转</b>:每个片段预编译出"最长连续非 {@code '?'} 字面量"当锚,
* 匹配时用 {@link String#indexOf(String, int)}(可被 JIT 内建/向量化)直接跳到候选位置,
* 再回推片段起点做一次完整校验;纯字面量片段的锚就是整段,一次 indexOf 到位;
* 全 {@code '?'} 的片段只校验长度。避免逐字符暴力扫描(实测这一优化让含 {@code '?'} 的多段模式快约 5 倍)。</li>
* <li><b>匹配路径零分配</b>:不 substring、不 toCharArray、不做正则编译;{@link StringText} 包装对象会被
* 逃逸分析栈上消除。</li>
* <li><b>长度前置过滤</b>:长度参数 + 模式最短长度(非 {@code '*'} 字符个数)先做 O(1) 拒绝。</li>
* </ol>
*
* <p>本类不可变、线程安全:可作为 {@code static final} 常量共享。
*/
public final class WildcardMatcher {
/** 通配任意长度(含空串)。 */
public static final char STAR = '*';
/** 通配恰好一个字符。 */
public static final char QUESTION = '?';
/** 一次性调用 API 的模式缓存上限,超过后整体清空(防止动态模式把内存撑爆)。 */
private static final int MAX_CACHED_PATTERNS = 2048;
private static final ConcurrentHashMap<String, WildcardMatcher> CACHE = new ConcurrentHashMap<>();
private static final char[] EMPTY_CHARS = new char[0];
private static final char[][] EMPTY_SEGS = new char[0][];
private static final String[] EMPTY_ANCHORS = new String[0];
private static final int[] EMPTY_OFFSETS = new int[0];
private final String pattern;
/** 非 '*' 字符个数,即模式能匹配的最短长度。 */
private final int minLength;
private final boolean hasStar;
private final boolean hasQuestion;
/** 模式只由 '*' 组成(如 "*"、"***"):匹配任意字符串。 */
private final boolean starOnly;
/** 模式以 '*' 开头:头部无锚定。 */
private final boolean leadingStar;
/** 模式以 '*' 结尾:尾部无锚定。 */
private final boolean trailingStar;
/** 无 '*' 时使用:完整模式字符(含 '?')。 */
private final char[] full;
/** 有 '*' 时使用:按 '*' 切分出的非空片段(顺序即模式顺序,连续 '*' 已折叠)。 */
private final char[][] segs;
/**
* 每个片段的"锚":片段内最长的一段连续非 '?' 字面量(片段全为 '?' 时为 null)。
* 匹配时先用 {@link String#indexOf} 快速跳到锚的位置,再回推片段起点做完整校验,
* 避免逐字符暴力扫描。纯字面量片段的锚就是它自身(offset 为 0)。
*/
private final String[] segAnchors;
/** 锚在片段内的起始下标。 */
private final int[] segAnchorOffsets;
// ------------------------------------------------------------------ 构造 / 编译
private WildcardMatcher(String pattern) {
this.pattern = pattern;
final int n = pattern.length();
boolean hasStar = false;
boolean hasQuestion = false;
int starCount = 0;
for (int i = 0; i < n; i++) {
final char c = pattern.charAt(i);
if (c == STAR) {
hasStar = true;
starCount++;
} else if (c == QUESTION) {
hasQuestion = true;
}
}
this.hasStar = hasStar;
this.hasQuestion = hasQuestion;
this.minLength = n - starCount;
this.leadingStar = hasStar && pattern.charAt(0) == STAR;
this.trailingStar = hasStar && pattern.charAt(n - 1) == STAR;
this.starOnly = hasStar && this.minLength == 0;
if (!hasStar) {
this.full = pattern.toCharArray();
this.segs = EMPTY_SEGS;
this.segAnchors = EMPTY_ANCHORS;
this.segAnchorOffsets = EMPTY_OFFSETS;
} else {
this.full = EMPTY_CHARS;
// 片段数最多为 starCount + 1(连续 '*' 会产生空片段,需要丢弃)
final char[][] tmpSegs = new char[starCount + 1][];
final String[] tmpAnchors = new String[starCount + 1];
final int[] tmpOffsets = new int[starCount + 1];
int segCount = 0;
int from = 0;
for (int i = 0; i <= n; i++) {
if (i == n || pattern.charAt(i) == STAR) {
if (i > from) {
final char[] seg = new char[i - from];
pattern.getChars(from, i, seg, 0);
tmpSegs[segCount] = seg;
// 找片段内最长的一段连续非 '?' 字面量作为"锚":
// 纯字面量片段 -> 锚就是整段(offset 0);
// 含 '?' 的片段 -> 锚是内部最长字面量串,匹配时先 indexOf 跳到锚再回推校验;
// 全 '?' 片段 -> 无锚(null),只需长度够即可。
int bestLen = 0;
int bestOff = -1;
int runStart = -1;
for (int k = 0; k <= seg.length; k++) {
final boolean literalChar = k < seg.length && seg[k] != QUESTION;
if (literalChar) {
if (runStart < 0) {
runStart = k;
}
} else if (runStart >= 0) {
final int runLen = k - runStart;
if (runLen > bestLen) {
bestLen = runLen;
bestOff = runStart;
}
runStart = -1;
}
}
tmpAnchors[segCount] = bestLen == 0 ? null : new String(seg, bestOff, bestLen);
tmpOffsets[segCount] = bestOff < 0 ? 0 : bestOff;
segCount++;
}
from = i + 1;
}
}
this.segs = Arrays.copyOf(tmpSegs, segCount);
this.segAnchors = Arrays.copyOf(tmpAnchors, segCount);
this.segAnchorOffsets = Arrays.copyOf(tmpOffsets, segCount);
}
}
/**
* 编译模式。返回的实例不可变、线程安全,建议在静态字段里复用。
*
* @param pattern 模式串,{@code '*'} 匹配任意长度,{@code '?'} 匹配单个字符
* @throws IllegalArgumentException pattern 为 null
*/
public static WildcardMatcher compile(String pattern) {
if (pattern == null) {
throw new IllegalArgumentException("pattern must not be null");
}
return new WildcardMatcher(pattern);
}
/**
* 取(带缓存的)编译结果,适合模式数量有限且重复出现的场景。
* 缓存上限 {@value #MAX_CACHED_PATTERNS},超限整体清空。
*/
public static WildcardMatcher matcherFor(String pattern) {
if (pattern == null) {
throw new IllegalArgumentException("pattern must not be null");
}
WildcardMatcher cached = CACHE.get(pattern);
if (cached != null) {
return cached;
}
if (CACHE.size() >= MAX_CACHED_PATTERNS) {
CACHE.clear();
}
return CACHE.computeIfAbsent(pattern, WildcardMatcher::new);
}
/** 清空一次性调用 API 的模式缓存。 */
public static void clearCache() {
CACHE.clear();
}
// ------------------------------------------------------------------ 静态便捷 API
/**
* 一次性匹配(内部复用模式缓存)。
*
* @param input 待匹配字符串,null 返回 false
* @param pattern 模式,不能为 null
* @param expectedLength 期望长度;为 null 表示不校验长度
*/
public static boolean isMatch(String input, String pattern, Integer expectedLength) {
return matcherFor(pattern).matches(input, expectedLength);
}
/** 一次性匹配,不校验长度。 */
public static boolean isMatch(String input, String pattern) {
return matcherFor(pattern).matches(input, null);
}
// ------------------------------------------------------------------ 实例 API
/** 完整匹配,不校验长度。 */
public boolean matches(String input) {
return matches(input, (Integer) null);
}
/**
* 完整匹配。
*
* @param input 待匹配字符串
* @param expectedLength 期望长度;为 null 表示不校验长度,负数视为不匹配
* @return 是否匹配
*/
public boolean matches(String input, Integer expectedLength) {
if (input == null) {
return false;
}
final int n = input.length();
if (expectedLength != null) {
final int expected = expectedLength; // 自动拆箱一次即可,避免重复拆箱
if (expected < 0 || n != expected) {
return false;
}
}
if (n < minLength) {
return false;
}
if (starOnly) {
return true;
}
return match(new StringText(input), n);
}
/** 零拷贝匹配字符数组缓冲区(等价于对 {@code new String(text)} 做匹配,但不产生任何对象)。 */
public boolean matches(char[] text, int offset, int length) {
return matches(text, offset, length, null);
}
/**
* 零拷贝匹配字符数组缓冲区,并校验长度。
*
* @param text 缓冲区
* @param offset 起始下标
* @param length 参与匹配的长度
* @param expectedLength 期望长度;为 null 表示不校验(非 null 时必须等于 length)
*/
public boolean matches(char[] text, int offset, int length, Integer expectedLength) {
if (text == null) {
throw new IllegalArgumentException("text must not be null");
}
if (offset < 0 || length < 0 || offset + length > text.length) {
throw new IndexOutOfBoundsException("offset=" + offset + ", length=" + length
+ ", text.length=" + text.length);
}
if (expectedLength != null && expectedLength != length) {
return false;
}
if (length < minLength) {
return false;
}
if (starOnly) {
return true;
}
return match(new CharArrayText(text, offset, length), length);
}
/** 模式原文。 */
public String pattern() {
return pattern;
}
/** 模式能匹配的最短长度(非 '*' 字符个数)。 */
public int minLength() {
return minLength;
}
/** 模式只由 '*' 组成,匹配任意字符串。 */
public boolean isStarOnly() {
return starOnly;
}
/** 模式不含通配符,等价于字符串相等判断。 */
public boolean isLiteral() {
return !hasStar && !hasQuestion;
}
@Override
public String toString() {
return "WildcardMatcher{" + pattern + '}';
}
// ------------------------------------------------------------------ 匹配核心
private boolean match(Text text, int n) {
if (!hasStar) {
// 无 '*': 长度必须完全相等
if (n != minLength) {
return false;
}
if (!hasQuestion) {
return text.equalsString(pattern); // 纯字面量:走 String.equals
}
final char[] p = full;
for (int i = 0; i < n; i++) {
final char pc = p[i];
if (pc != QUESTION && pc != text.charAt(i)) {
return false;
}
}
return true;
}
// 有 '*': 首尾可锚定,中间片段贪心取最早出现位置
final char[][] segs = this.segs;
final String[] anchors = this.segAnchors;
final int[] anchorOffsets = this.segAnchorOffsets;
final int segCount = segs.length;
final int suffixReserve = trailingStar ? 0 : segs[segCount - 1].length;
int pos = 0; // 已匹配消费到的位置
int idx = 0; // 下一个待匹配片段下标
if (!leadingStar) {
final char[] first = segs[0];
if (n < first.length || !regionMatches(text, 0, first)) {
return false;
}
pos = first.length;
idx = 1;
}
final int lastMiddle = trailingStar ? segCount : segCount - 1;
for (; idx < lastMiddle; idx++) {
final char[] seg = segs[idx];
final int segLen = seg.length;
final int limit = n - suffixReserve - segLen; // 该片段允许的最大起始位置
if (limit < pos) {
return false; // 剩余空间不够,直接失败
}
final String anchor = anchors[idx];
if (anchor == null) {
// 片段全是 '?': 任意 segLen 个字符都能匹配,取最早位置即可
pos += segLen;
continue;
}
final int anchorOffset = anchorOffsets[idx];
if (anchor.length() == segLen) {
// 纯字面量片段:锚就是整段,一次 indexOf(JIT 内建/向量化)到位
final int at = text.indexOf(anchor, pos);
if (at < 0 || at > limit) {
return false;
}
pos = at + segLen;
} else {
// 含 '?' 的片段:跳到锚的最早位置,再回推片段起点做完整校验
int searchFrom = pos + anchorOffset;
int start = -1;
while (true) {
final int at = text.indexOf(anchor, searchFrom);
if (at < 0) {
return false;
}
final int candidate = at - anchorOffset;
if (candidate > limit) {
return false; // 更靠后的候选只会更超界
}
if (candidate >= pos && regionMatches(text, candidate, seg)) {
start = candidate;
break;
}
searchFrom = at + 1;
}
pos = start + segLen;
}
}
if (trailingStar) {
return true; // 尾巴由最后一个 '*' 吸收
}
final char[] suffix = segs[segCount - 1];
final int suffixStart = n - suffix.length;
return suffixStart >= pos && regionMatches(text, suffixStart, suffix);
}
/** 判断 text 的 [start, start+seg.length) 是否与片段匹配(片段内 '?' 匹配任意单字符)。 */
private static boolean regionMatches(Text text, int start, char[] seg) {
for (int i = 0; i < seg.length; i++) {
final char pc = seg[i];
if (pc != QUESTION && pc != text.charAt(start + i)) {
return false;
}
}
return true;
}
// ------------------------------------------------------------------ 文本访问抽象
// 让匹配核心同时服务于 String(走 String.indexOf 内建)与 char[](零拷贝)两条路径。
private interface Text {
int length();
char charAt(int index);
/** 从 fromIndex 起查找 needle 首次出现位置,语义同 {@link String#indexOf(String, int)}。 */
int indexOf(String needle, int fromIndex);
boolean equalsString(String other);
}
private static final class StringText implements Text {
private final String value;
StringText(String value) {
this.value = value;
}
@Override
public int length() {
return value.length();
}
@Override
public char charAt(int index) {
return value.charAt(index);
}
@Override
public int indexOf(String needle, int fromIndex) {
return value.indexOf(needle, fromIndex);
}
@Override
public boolean equalsString(String other) {
return value.equals(other);
}
}
private static final class CharArrayText implements Text {
private final char[] buf;
private final int off;
private final int len;
CharArrayText(char[] buf, int off, int len) {
this.buf = buf;
this.off = off;
this.len = len;
}
@Override
public int length() {
return len;
}
@Override
public char charAt(int index) {
return buf[off + index];
}
@Override
public int indexOf(String needle, int fromIndex) {
final int needleLen = needle.length();
final int max = len - needleLen;
if (max < fromIndex) {
return -1;
}
final char first = needle.charAt(0);
for (int i = fromIndex; i <= max; i++) {
if (buf[off + i] != first) {
continue;
}
int j = 1;
while (j < needleLen && buf[off + i + j] == needle.charAt(j)) {
j++;
}
if (j == needleLen) {
return i;
}
}
return -1;
}
@Override
public boolean equalsString(String other) {
if (other.length() != len) {
return false;
}
for (int i = 0; i < len; i++) {
if (buf[off + i] != other.charAt(i)) {
return false;
}
}
return true;
}
}
}
1. API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.example.wildmatch.WildcardMatcher;
// ① 编译一次、复用千万次(推荐;实例不可变、线程安全,可做 static final)
private static final WildcardMatcher ORDER = WildcardMatcher.compile("order_*_2024?");
ORDER.matches("order_10086_2024A"); // true,不校验长度
ORDER.matches("order_10086_2024A", 17); // true,校验长度 == 17
ORDER.matches("order_10086_2024A", null); // true,null = 不校验长度
ORDER.matches("order_10086_2024A", 18); // false,长度不符(扫描前就短路)
// ② 一次性调用(内部有模式缓存,重复模式不重复编译;上限 2048,超限整体清空)
WildcardMatcher.isMatch(input, "user_*_v?", null);
WildcardMatcher.isMatch(input, "user_*_v?");
// ③ 零拷贝匹配 char[] 缓冲区(日志/报文解析,避免 substring/toCharArray 产生对象)
ORDER.matches(buffer, offset, len, len); // 校验长度
ORDER.matches(buffer, offset, len); // 不校验长度
// ④ 元信息
ORDER.pattern(); // "order_*_2024?"
ORDER.minLength(); // 13(非 '*' 字符个数 = 能匹配的最短长度)
ORDER.isLiteral(); // false(纯字面量模式会走 String.equals 快路径)
ORDER.isStarOnly(); // false("*"、"***" 直接返回 true)
命名说明:一次性调用的静态方法叫
isMatch(...),而不是matches(...)。 因为实例方法matches(String, Integer)与静态matches(String, String)在传字面量null(也就是”不校验长度”这个用法)时会产生重载二义性,Java 编译不过。 这个坑是本次真实编译时抓到的,不是设计偏好。
返回 / 异常约定
| 输入 | 行为 |
|---|---|
input == null |
返回 false(不抛异常,便于直接处理脏数据) |
pattern == null |
抛 IllegalArgumentException(编程错误,早失败) |
expectedLength == null |
不校验长度 |
expectedLength < 0 |
返回 false(Integer.MIN_VALUE 同样安全,不会溢出) |
expectedLength != input.length() |
返回 false,且在扫描前短路 |
char[] 越界 |
抛 IndexOutOfBoundsException |
2. 为什么快
- 预编译:
compile()阶段完成模式扫描、连续*折叠、按*切分片段、计算最短长度;匹配阶段不再解析模式。 - 按模式形态分流:纯字面量 →
String.equals;只含?→ 一趟线性比较;只含*→ 直接true;通用情况才进片段匹配。 - 首尾锚定 + 片段贪心:首个片段若不以
*开头即锚在头部;末片段若不以*结尾即锚在尾部, 于是中间片段的搜索窗口被收窄到[pos, n - suffixLen - segLen];中间片段从左到右取最早出现位置。 全程无递归、无回溯爆炸(正则的.*在退化输入上可能指数级),复杂度 O(n) 量级。 - 字面量”锚”跳转:每个片段预编译出”最长连续非
?字面量”作为锚,用String.indexOf(JIT 内建/向量化) 跳到候选位置再回推校验;纯字面量片段的锚就是整段;全?片段只校验长度。 - 匹配路径零分配:不 substring、不 toCharArray、不编译正则;
StringText包装对象会被逃逸分析栈上消除。 - 长度前置过滤:长度参数 +
minLength()先做 O(1) 拒绝,脏数据/不匹配长度几乎零成本。
3. 实测(本机 JDK 17,粗测非 JMH,仅供量级对比)
正确性:592,631 条断言全部通过(穷举 1365 个模式 × 63 个输入全组合 + 20 万随机用例 + 2 万 char[] 对照), 用两个互相独立的 oracle 交叉验证(递归回溯参考实现 + glob 转正则)。
| 场景 | 本工具 ns/op | java.util.regex ns/op |
加速比 |
|---|---|---|---|
| 字面量 32 字符 · 命中 | 12.2 | 60.5 | 4.9x |
日志行 *level=ERROR* · 未命中 |
26.9 | 1311.9 | 48.7x |
order_*_2024? · 命中 |
28.1 | 115.0 | 4.1x |
a?c*d?f* · 512 字符命中 |
58.4 | 743.5 | 12.7x |
*a*b*c*d* · 未命中 |
56.9 | 4779.3 | 84.0x |
| 长度参数快速拒绝(长度不符) | 5.6 | — | — |
第 4 行是优化前后的关键对比:最初的逐字符扫描版是 2122 ns(比正则还慢 3 倍), 改成”锚 + indexOf”后降到 58 ns,快 36 倍。这也是本工具值得自己写而不是用正则的理由。
4. 构建与验证
1
2
3
4
5
6
7
8
# 方式一:Maven
mvn -q clean package
mvn -q exec:java # 跑自测 + 微基准(失败时 exit 1)
# 方式二:只要 JDK
javac -d out src/main/java/com/example/wildmatch/WildcardMatcher.java \
src/test/java/com/example/wildmatch/WildcardMatcherSelfTest.java
java -cp out com.example.wildmatch.WildcardMatcherSelfTest
源码兼容 Java 8+(已用 javac --release 8 编译并跑通自测)。
5. 文件
| 文件 | 说明 |
|---|---|
src/main/java/com/example/wildmatch/WildcardMatcher.java |
全部实现(单文件、零依赖,可直接拷进任何项目) |
src/test/java/com/example/wildmatch/WildcardMatcherSelfTest.java |
自包含自测 + 微基准(两个 oracle 交叉验证、穷举 + 随机) |
pom.xml |
可选构建文件(无第三方依赖) |
6. 已知边界与可选项
*跨/:按”*匹配任意字符”实现。需要路径语义(不跨目录)时按/切分后逐段匹配,或用?约束每段长度。- 仅支持
*和?:不提供字符集[abc]、转义\*、大小写不敏感等扩展。若需要,可加WildcardMatcher.compile(pattern, EnumSet<Option>)选项。 - 中文/emoji:按 UTF-16 code unit 处理,与
String.length()一致(一个 emoji 占 2 个 char), 所以长度参数与?的计数都以String.length()为准。 - 模式缓存:
isMatch用的静态缓存上限 2048 个模式,超限整体清空;模式种类无界时建议自己compile后复用。
7. WildcardMatcherSelfTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
package com.example.wildmatch;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.regex.Pattern;
/**
* 自包含的正确性 + 性能验证程序(零依赖,直接 {@code java} 运行):
*
* <pre>
* javac -d out src/main/java/com/example/wildmatch/WildcardMatcher.java src/test/java/com/example/wildmatch/WildcardMatcherSelfTest.java
* java -cp out com.example.wildmatch.WildcardMatcherSelfTest
* </pre>
*
* <p>正确性用两个互相独立的 oracle 交叉验证:
* <ol>
* <li>递归回溯的参考实现 {@link #referenceMatch}(写法与主实现完全不同);</li>
* <li>glob 转 Java 正则后 {@code Pattern.matches}。</li>
* </ol>
* 覆盖:穷举小规模全组合 + 大规模随机 + 长度参数的等价性 + char[] 零拷贝重载一致性。
*/
public final class WildcardMatcherSelfTest {
private static int checks;
private static int failures;
public static void main(String[] args) {
long start = System.currentTimeMillis();
basicCases();
lengthCases();
edgeCases();
exhaustiveDifferential();
randomizedDifferential();
charArrayOverload();
cacheAndApiChecks();
System.out.println();
System.out.println("==================================================");
System.out.printf("断言总数: %d, 失败: %d, 耗时: %d ms%n",
checks, failures, System.currentTimeMillis() - start);
if (failures > 0) {
System.out.println("结果: 失败");
System.exit(1);
}
System.out.println("结果: 全部通过");
microBenchmark();
}
// ------------------------------------------------------------------ 用例
private static void basicCases() {
section("基本语义");
check(true, WildcardMatcher.isMatch("hello", "hello", null), "字面量相等");
check(false, WildcardMatcher.isMatch("hello", "hellO", null), "字面量不等(大小写敏感)");
check(false, WildcardMatcher.isMatch("hell", "hello", null), "前缀不算匹配");
check(true, WildcardMatcher.isMatch("hello", "*", null), "'*' 匹配任意串");
check(true, WildcardMatcher.isMatch("", "*", null), "'*' 匹配空串");
check(true, WildcardMatcher.isMatch("", "", null), "空模式匹配空串");
check(false, WildcardMatcher.isMatch("a", "", null), "空模式不匹配非空串");
check(true, WildcardMatcher.isMatch("hello", "h*o", null), "中间 '*'");
check(true, WildcardMatcher.isMatch("ho", "h*o", null), "'*' 匹配空串(中间)");
check(true, WildcardMatcher.isMatch("hello", "*hello", null), "前导 '*'");
check(true, WildcardMatcher.isMatch("hello", "hello*", null), "尾随 '*'");
check(true, WildcardMatcher.isMatch("hello", "*ell*", null), "两侧 '*'");
check(false, WildcardMatcher.isMatch("hello", "*ell", null), "尾部锚定失败");
check(true, WildcardMatcher.isMatch("hello", "h?llo", null), "'?' 匹配单字符");
check(false, WildcardMatcher.isMatch("hello", "h?lo", null), "'?' 不吃长度");
check(true, WildcardMatcher.isMatch("hello", "?????", null), "全 '?'");
check(false, WildcardMatcher.isMatch("hello", "????", null), "'?' 数量不足");
check(true, WildcardMatcher.isMatch("abc", "a**c", null), "连续 '*' 折叠");
check(true, WildcardMatcher.isMatch("abc", "***", null), "纯 '*' 模式");
check(true, WildcardMatcher.isMatch("aXbYc", "a*b*c", null), "多段 '*'");
check(false, WildcardMatcher.isMatch("aXbY", "a*b*c", null), "多段 '*' 尾部失败");
check(false, WildcardMatcher.isMatch("ab", "a*a", null), "'a*a' 不匹配 'ab'");
check(true, WildcardMatcher.isMatch("aa", "a*a", null), "'a*a' 匹配 'aa'");
// 文件/日志场景
WildcardMatcher orderMatcher = WildcardMatcher.compile("order_*_2024?");
check(true, orderMatcher.matches("order_10086_2024A"), "订单号模式命中");
check(false, orderMatcher.matches("order_10086_2024AB"), "订单号模式长度不符");
check(true, WildcardMatcher.isMatch("/var/log/app-2024-01-02.log", "/var/log/*.log", null), "路径通配");
// 语义说明:'*' 匹配任意字符,包含 '/',不做"路径不跨目录"的特例(与需求"*号匹配任意字符"一致)
check(true, WildcardMatcher.isMatch("/var/log/app-2024/app.log", "/var/log/*.log", null), "'*' 跨 '/'");
check(true, WildcardMatcher.isMatch("a/b/c", "a*c", null), "'*' 跨分隔符");
// 真要不跨目录,用 '?' 约束每段长度即可
check(false, WildcardMatcher.isMatch("/var/log/app-2024/app.log", "/var/log/????????.log", null),
"用 '?' 约束单段长度实现不跨目录");
}
private static void lengthCases() {
section("长度匹配(第三个参数)");
WildcardMatcher m = WildcardMatcher.compile("order_*_2024?");
check(true, m.matches("order_10086_2024A", 17), "长度命中");
check(true, m.matches("order_10086_2024A", (Integer) null), "null 表示不校验长度");
check(true, m.matches("order_10086_2024A", 17), "长度命中(装箱)");
check(false, m.matches("order_10086_2024A", 18), "长度不符 -> false");
check(false, m.matches("order_10086_2024A", 0), "长度 0 -> false");
check(false, m.matches("order_10086_2024A", -1), "负数长度 -> false 且不抛异常");
check(false, m.matches("order_10086_2024A", Integer.MIN_VALUE), "Integer.MIN_VALUE -> false");
// 长度参数必须与"不校验长度再补一次判断"完全等价
String[] inputs = {"", "a", "ab", "abc", "abcd", "*", "?", "a*b", "order_1_2024X"};
String[] patterns = {"", "*", "?", "a*", "*b", "a?a", "a**b", "order_?_2024?", "***"};
for (String p : patterns) {
WildcardMatcher mc = WildcardMatcher.compile(p);
for (String in : inputs) {
boolean plain = mc.matches(in, null);
for (int len = -1; len <= in.length() + 2; len++) {
boolean withLen = mc.matches(in, len);
boolean expected = len >= 0 && len == in.length() && plain;
check(expected, withLen, "长度参数等价性 pattern=" + p + " input=" + in + " len=" + len);
}
}
}
}
private static void edgeCases() {
section("边界与 null");
check(false, WildcardMatcher.isMatch(null, "*", null), "null 输入 -> false");
check(false, WildcardMatcher.isMatch(null, "abc", null), "null 输入(字面量模式) -> false");
check(false, WildcardMatcher.isMatch(null, "*", 0), "null 输入 + 长度 -> false");
try {
WildcardMatcher.compile(null);
check(false, true, "compile(null) 应抛 IllegalArgumentException");
} catch (IllegalArgumentException e) {
check(true, true, "compile(null) 抛 IllegalArgumentException");
}
try {
WildcardMatcher.isMatch("a", null, null);
check(false, true, "isMatch(input, null, len) 应抛异常");
} catch (IllegalArgumentException e) {
check(true, true, "isMatch(input, null, len) 抛 IllegalArgumentException");
}
check(0, WildcardMatcher.compile("***").minLength(), "'*' 的最短长度 0");
check(3, WildcardMatcher.compile("a?c*").minLength(), "最短长度 = 非 '*' 字符数");
check(true, WildcardMatcher.compile("***").isStarOnly(), "isStarOnly");
check(true, WildcardMatcher.compile("abc").isLiteral(), "isLiteral");
check(false, WildcardMatcher.compile("a?c").isLiteral(), "含 '?' 不是 literal");
// 中文 / Unicode 按 UTF-16 code unit 处理(与 String.length() 一致)
check(true, WildcardMatcher.isMatch("订单_2024年", "订单_*年", null), "中文匹配");
check(true, WildcardMatcher.isMatch("订单_2024年", "订单_????年", 8), "中文按 char 计数");
check(false, WildcardMatcher.isMatch("订单_2024年", "订单_????年", 9), "中文长度不符 -> false");
check(true, WildcardMatcher.isMatch("emoji😀test", "emoji*test", null), "代理对(emoji)匹配");
// 长串与极端模式,防止栈溢出/回溯爆炸(本实现无递归)
String longText = repeat("ab", 100_000);
check(true, WildcardMatcher.isMatch(longText, "*ab", null), "10 万字符 + 尾随 '*'");
check(false, WildcardMatcher.isMatch(longText, "*ababababababababababababababX", null), "长串未命中");
check(true, WildcardMatcher.isMatch(longText, "ab*ab", null), "长串 + 中段 '*'");
}
/** 穷举:字母表 {a,b} 的文本(长度 0..5) × 字母表 {a,b,?,*} 的模式(长度 0..5),与两个 oracle 逐一比对。 */
private static void exhaustiveDifferential() {
section("穷举交叉验证(模式的全部组合)");
char[] textAlpha = {'a', 'b'};
char[] patternAlpha = {'a', 'b', '?', '*'};
List<String> texts = allStrings(textAlpha, 5);
List<String> patterns = allStrings(patternAlpha, 5);
int cases = 0;
for (String p : patterns) {
WildcardMatcher m = WildcardMatcher.compile(p);
Pattern regex = toRegex(p);
for (String in : texts) {
boolean actual = m.matches(in, null);
boolean expected = referenceMatch(in, p);
if (actual != expected || regex.matcher(in).matches() != expected) {
failures++;
System.out.printf(" [FAIL] pattern='%s' input='%s' actual=%s reference=%s regex=%s%n",
p, in, actual, expected, regex.matcher(in).matches());
}
// 长度参数等价性
boolean withLen = m.matches(in, in.length());
if (withLen != expected) {
failures++;
System.out.printf(" [FAIL] length pattern='%s' input='%s' actual=%s expected=%s%n",
p, in, withLen, expected);
}
checks += 2;
cases++;
}
}
System.out.printf(" 穷举组合 %d 组(模式 %d × 文本 %d),共 %d 次比对%n",
cases, patterns.size(), texts.size(), cases * 2);
}
/** 随机大规模交叉验证(长模式、长文本,含长度参数与 char[] 重载)。 */
private static void randomizedDifferential() {
section("随机大规模交叉验证");
Random random = new Random(20240607L);
char[] textAlpha = {'a', 'b', 'c'};
final int iterations = 200_000;
int lengthChecked = 0;
for (int it = 0; it < iterations; it++) {
String p = randomString(random, patternAlphaWithWildcard(), 0, 24);
String in = randomString(random, textAlpha, 0, 48);
boolean expected = referenceMatch(in, p);
boolean actual = WildcardMatcher.matcherFor(p).matches(in, null);
if (actual != expected) {
failures++;
System.out.printf(" [FAIL] pattern='%s' input='%s' actual=%s reference=%s%n", p, in, actual, expected);
}
// 随机抽查长度参数
if ((it & 7) == 0) {
Integer len = random.nextBoolean() ? in.length() : random.nextInt(in.length() + 3);
boolean withLen = WildcardMatcher.matcherFor(p).matches(in, len);
boolean expectedLen = len == in.length() && expected;
if (withLen != expectedLen) {
failures++;
System.out.printf(" [FAIL] length pattern='%s' input='%s' len=%d actual=%s expected=%s%n",
p, in, len, withLen, expectedLen);
}
lengthChecked++;
}
checks += 2;
}
System.out.printf(" 随机用例 %d 组(其中长度校验 %d 组)%n", iterations, lengthChecked);
}
private static void charArrayOverload() {
section("char[] 零拷贝重载");
WildcardMatcher m = WildcardMatcher.compile("a*b?c");
check(true, m.matches("a--bXc".toCharArray(), 0, 6), "char[] 全量匹配");
check(true, m.matches("###a--bXc###".toCharArray(), 3, 6), "char[] 带 offset 匹配");
check(false, m.matches("###a--bXc###".toCharArray(), 0, 6), "char[] 窗口不对 -> false");
check(true, m.matches("###a--bXc###".toCharArray(), 3, 6, 6), "char[] + 长度命中");
check(false, m.matches("###a--bXc###".toCharArray(), 3, 6, 7), "char[] + 长度不符");
check(false, m.matches("abc".toCharArray(), 2, 0, 0), "char[] 空窗口 vs 非空模式");
// 与 String 重载随机对比
Random random = new Random(7L);
for (int i = 0; i < 20_000; i++) {
String p = randomString(random, patternAlphaWithWildcard(), 0, 12);
String in = randomString(random, new char[]{'a', 'b', 'c'}, 0, 20);
char[] buf = ("[" + in + "]").toCharArray();
boolean viaString = WildcardMatcher.matcherFor(p).matches(in, null);
boolean viaChars = WildcardMatcher.matcherFor(p).matches(buf, 1, in.length(), null);
if (viaString != viaChars) {
failures++;
System.out.printf(" [FAIL] char[] pattern='%s' input='%s' string=%s chars=%s%n",
p, in, viaString, viaChars);
}
checks++;
}
System.out.println(" char[] 与 String 路径随机对比 20000 组");
try {
m.matches(new char[4], 2, 5, null);
check(false, true, "越界应抛 IndexOutOfBoundsException");
} catch (IndexOutOfBoundsException e) {
check(true, true, "越界抛 IndexOutOfBoundsException");
}
}
private static void cacheAndApiChecks() {
section("缓存与 API 细节");
WildcardMatcher.clearCache();
WildcardMatcher a = WildcardMatcher.matcherFor("x*y");
WildcardMatcher b = WildcardMatcher.matcherFor("x*y");
check(true, a == b, "相同模式返回同一实例(缓存生效)");
check("x*y", a.pattern(), "pattern() 返回原文");
check(true, WildcardMatcher.matcherFor("*").isStarOnly(), "matcherFor('*').isStarOnly()");
check(true, a.toString().contains("x*y"), "toString 可读");
// 动态模式大量涌入时缓存不会无限增长
for (int i = 0; i < 5000; i++) {
WildcardMatcher.matcherFor("dyn_" + i + "_*");
}
check(true, true, "5000 个动态模式未 OOM(缓存有上限)");
}
// ------------------------------------------------------------------ 微基准(仅供参考,非 JMH)
private static void microBenchmark() {
System.out.println();
System.out.println("==================================================");
System.out.println("微基准(粗测,非 JMH;数值受 JIT/机器影响,仅用于量级对比)");
System.out.println("==================================================");
System.out.printf("%-46s %14s %14s %10s%n", "场景", "本工具 ns/op", "正则 ns/op", "加速比");
bench("字面量 32 字符 · 命中", repeat("abcdefgh", 4), repeat("abcdefgh", 4), 2_000_000);
bench("日志行 '*level=ERROR*' · 未命中", buildLogLine(), "*level=ERROR*", 500_000);
bench("订单号 'order_*_2024?' · 命中", "order_10086_2024A", "order_*_2024?", 2_000_000);
bench("多段 'a?c*d?f*' · 512 字符命中", buildText("abc", "def", 512), "a?c*d?f*", 500_000);
bench("星号密集 '*a*b*c*d*' · 未命中", repeat("xyz", 200), "*a*b*c*d*", 200_000);
benchLengthReject("长度参数快速拒绝(长度不符)", buildLogLine(), "*level=ERROR*", 999, 4_000_000);
}
/** 长度参数为"已知但不等"时的早退路径(正则在这一点上无法等价对比)。 */
private static void benchLengthReject(String name, String input, String pattern, int length, int iterations) {
WildcardMatcher matcher = WildcardMatcher.compile(pattern);
for (int i = 0; i < 200_000; i++) {
sink ^= matcher.matches(input, length) ? 1 : 0;
}
long t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
sink ^= matcher.matches(input, length) ? 1 : 0;
}
long t1 = System.nanoTime();
System.out.printf("%-46s %14.1f %14s %10s%n", name, (t1 - t0) / (double) iterations, "—", "—");
}
private static void bench(String name, String input, String pattern, int iterations) {
WildcardMatcher matcher = WildcardMatcher.compile(pattern);
Pattern regex = toRegex(pattern);
// 正确性 + 预热
for (int i = 0; i < 200_000; i++) {
sink ^= matcher.matches(input, null) ? 1 : 0;
sink ^= regex.matcher(input).matches() ? 1 : 0;
}
long t0 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
sink ^= matcher.matches(input, null) ? 1 : 0;
}
long t1 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
sink ^= regex.matcher(input).matches() ? 1 : 0;
}
long t2 = System.nanoTime();
double ours = (t1 - t0) / (double) iterations;
double theirs = (t2 - t1) / (double) iterations;
System.out.printf("%-46s %14.1f %14.1f %9.1fx%n", name, ours, theirs, theirs / ours);
}
private static volatile int sink;
// ------------------------------------------------------------------ oracle / 工具
/** 递归回溯参考实现:与主实现算法完全不同,用于交叉验证。 */
private static boolean referenceMatch(String s, String p) {
return referenceMatch(s, 0, p, 0);
}
private static boolean referenceMatch(String s, int i, String p, int j) {
while (true) {
if (j == p.length()) {
return i == s.length();
}
char pc = p.charAt(j);
if (pc == '*') {
while (j < p.length() && p.charAt(j) == '*') {
j++; // 折叠连续 '*'
}
if (j == p.length()) {
return true;
}
for (int k = i; k <= s.length(); k++) {
if (referenceMatch(s, k, p, j)) {
return true;
}
}
return false;
}
if (i == s.length()) {
return false;
}
if (pc != '?' && pc != s.charAt(i)) {
return false;
}
i++;
j++;
}
}
/** glob -> 正则('*' => ".*",'?' => ".",其余字符转义)。 */
private static Pattern toRegex(String glob) {
StringBuilder sb = new StringBuilder(glob.length() * 2 + 2);
for (int i = 0; i < glob.length(); i++) {
char c = glob.charAt(i);
if (c == '*') {
sb.append(".*");
} else if (c == '?') {
sb.append('.');
} else {
sb.append(Pattern.quote(String.valueOf(c)));
}
}
return Pattern.compile(sb.toString());
}
private static char[] patternAlphaWithWildcard() {
return new char[]{'a', 'b', 'c', '?', '*'};
}
private static List<String> allStrings(char[] alpha, int maxLen) {
List<String> result = new ArrayList<>();
for (int len = 0; len <= maxLen; len++) {
int total = 1;
for (int i = 0; i < len; i++) {
total *= alpha.length;
}
for (int code = 0; code < total; code++) {
StringBuilder sb = new StringBuilder(len);
int v = code;
for (int i = 0; i < len; i++) {
sb.append(alpha[v % alpha.length]);
v /= alpha.length;
}
result.add(sb.toString());
}
}
return result;
}
private static String randomString(Random random, char[] alpha, int minLen, int maxLen) {
int len = minLen + (maxLen > minLen ? random.nextInt(maxLen - minLen + 1) : 0);
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
sb.append(alpha[random.nextInt(alpha.length)]);
}
return sb.toString();
}
private static String repeat(String unit, int times) {
StringBuilder sb = new StringBuilder(unit.length() * times);
for (int i = 0; i < times; i++) {
sb.append(unit);
}
return sb.toString();
}
private static String buildLogLine() {
return "2024-06-07T10:15:30.123Z [http-nio-8080-exec-7] INFO c.e.OrderService - "
+ "userId=10086 orderId=order_10086_2024A amount=199.00 status=PAID traceId=abc123def456";
}
private static String buildText(String head, String tail, int totalLen) {
StringBuilder sb = new StringBuilder(totalLen);
sb.append(head);
while (sb.length() < totalLen - tail.length()) {
sb.append('x');
}
sb.append(tail);
return sb.toString();
}
private static void section(String title) {
System.out.println();
System.out.println("---- " + title + " ----");
}
private static void check(boolean expected, boolean actual, String what) {
checks++;
if (expected != actual) {
failures++;
System.out.printf(" [FAIL] %s: 期望 %s, 实际 %s%n", what, expected, actual);
}
}
private static void check(long expected, long actual, String what) {
checks++;
if (expected != actual) {
failures++;
System.out.printf(" [FAIL] %s: 期望 %d, 实际 %d%n", what, expected, actual);
}
}
private static void check(String expected, String actual, String what) {
checks++;
if (!expected.equals(actual)) {
failures++;
System.out.printf(" [FAIL] %s: 期望 '%s', 实际 '%s'%n", what, expected, actual);
}
}
}