1. 概述
在 Java 中处理文件时,我们常常需要操作文件名。比如,有时候我们希望从一个完整的文件名中提取出不带扩展名的部分。换句话说,就是要去掉文件名的后缀。
本文将介绍一种通用的方式,来实现从文件名中移除扩展名的功能。
2. 文件名去扩展名的不同场景
初看这个问题,你可能会觉得很简单:不就是去掉最后一个点(.
)后面的内容吗?
但如果你仔细想想,其实没那么简单。
先来看看常见的几种文件名类型:
✅ 无扩展名:例如 "baeldung"
✅ 单个扩展名:最常见的形式,例如 "baeldung.txt"
✅ 多个扩展名:例如 "baeldung.tar.gz"
✅ 以点开头的隐藏文件(dotfile)且无扩展名:例如 ".baeldung"
✅ dotfile 带单个扩展名:例如 ".baeldung.conf"
✅ dotfile 带多个扩展名:例如 ".baeldung.conf.bak"
下面列出了针对这些文件名,去掉扩展名后的预期结果:
❌ "baeldung"
→ 不变 → "baeldung"
✅ "baeldung.txt"
→ "baeldung"
⚠️ "baeldung.tar.gz"
→ 只去最后一个扩展名是 "baeldung.tar"
;全去掉则是 "baeldung"
❌ ".baeldung"
→ 不变 → ".baeldung"
✅ ".baeldung.conf"
→ ".baeldung"
⚠️ ".baeldung.conf.bak"
→ 去一个扩展名是 ".baeldung.conf"
;去全部则是 ".baeldung"
接下来我们会测试 Guava 和 Apache Commons IO 这两个常用库提供的工具方法是否能应对所有情况,并最终给出一个通用的解决方案。
3. 使用 Guava 库处理
从 Guava 14.0 版本开始,它提供了一个静态方法:
Files.getNameWithoutExtension(String file)
你可以直接通过 Maven 引入依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
看看它的源码实现:
public static String getNameWithoutExtension(String file) {
...
int dotIndex = fileName.lastIndexOf('.');
return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex);
}
逻辑非常简单粗暴:找到最后一个点的位置,然后截取前面的部分。如果没找到点,就原样返回。
❌ 但它对 dotfile 处理不友好:
@Test
public void givenDotFileWithoutExt_whenCallGuavaMethod_thenCannotGetDesiredResult() {
assertNotEquals(".baeldung", Files.getNameWithoutExtension(".baeldung"));
}
⚠️ 而且无法处理多个扩展名的情况:
@Test
public void givenFileWithoutMultipleExt_whenCallGuavaMethod_thenCannotRemoveAllExtensions() {
assertNotEquals("baeldung", Files.getNameWithoutExtension("baeldung.tar.gz"));
}
4. 使用 Apache Commons IO 库处理
Apache Commons IO 同样提供了类似的方法:
FilenameUtils.removeExtension(String filename)
添加依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
实现方式也差不多:
public static String removeExtension(final String filename) {
...
final int index = indexOfExtension(filename); // 实际也是 lastIndexOf('.')
if (index == NOT_FOUND) {
return filename;
} else {
return filename.substring(0, index);
}
}
同样地,它也踩了同样的坑:
❌ 对 dotfile 支持不好:
@Test
public void givenDotFileWithoutExt_whenCallApacheCommonsMethod_thenCannotGetDesiredResult() {
assertNotEquals(".baeldung", FilenameUtils.removeExtension(".baeldung"));
}
⚠️ 不支持一次性清除所有扩展名:
@Test
public void givenFileWithoutMultipleExt_whenCallApacheCommonsMethod_thenCannotRemoveAllExtensions() {
assertNotEquals("baeldung", FilenameUtils.removeExtension("baeldung.tar.gz"));
}
5. 通用解决方案:自己动手丰衣足食
既然两个主流库都有短板,那我们就自己写一个通用的方法吧。
public static String removeFileExtension(String filename, boolean removeAllExtensions) {
if (filename == null || filename.isEmpty()) {
return filename;
}
String extPattern = "(?<!^)[.]" + (removeAllExtensions ? ".*" : "[^.]*$");
return filename.replaceAll(extPattern, "");
}
这个方法支持两个参数:
filename
: 待处理的文件名removeAllExtensions
: 是否移除所有扩展名
核心在于正则表达式:
(?<!^)[.]
:负向后瞻断言,确保这个点不是文件名的第一个字符(避免误伤 dotfile)(removeAllExtensions ? ".*" : "[^.]*$")
:- 如果是 true,则匹配到最后一个点之后的所有内容(即所有扩展名)
- 如果是 false,则只匹配最后一个扩展名
✅ 测试用例覆盖各种场景:
@Test
public void givenFilenameNoExt_whenCallFilenameUtilMethod_thenGetExpectedFilename() {
assertEquals("baeldung", MyFilenameUtil.removeFileExtension("baeldung", true));
assertEquals("baeldung", MyFilenameUtil.removeFileExtension("baeldung", false));
}
@Test
public void givenSingleExt_whenCallFilenameUtilMethod_thenGetExpectedFilename() {
assertEquals("baeldung", MyFilenameUtil.removeFileExtension("baeldung.txt", true));
assertEquals("baeldung", MyFilenameUtil.removeFileExtension("baeldung.txt", false));
}
@Test
public void givenDotFile_whenCallFilenameUtilMethod_thenGetExpectedFilename() {
assertEquals(".baeldung", MyFilenameUtil.removeFileExtension(".baeldung", true));
assertEquals(".baeldung", MyFilenameUtil.removeFileExtension(".baeldung", false));
}
@Test
public void givenDotFileWithExt_whenCallFilenameUtilMethod_thenGetExpectedFilename() {
assertEquals(".baeldung", MyFilenameUtil.removeFileExtension(".baeldung.conf", true));
assertEquals(".baeldung", MyFilenameUtil.removeFileExtension(".baeldung.conf", false));
}
@Test
public void givenDoubleExt_whenCallFilenameUtilMethod_thenGetExpectedFilename() {
assertEquals("baeldung", MyFilenameUtil.removeFileExtension("baeldung.tar.gz", true));
assertEquals("baeldung.tar", MyFilenameUtil.removeFileExtension("baeldung.tar.gz", false));
}
@Test
public void givenDotFileWithDoubleExt_whenCallFilenameUtilMethod_thenGetExpectedFilename() {
assertEquals(".baeldung", MyFilenameUtil.removeFileExtension(".baeldung.conf.bak", true));
assertEquals(".baeldung.conf", MyFilenameUtil.removeFileExtension(".baeldung.conf.bak", false));
}
6. 总结
今天我们聊了聊怎么从文件名中去掉扩展名。
- 先分析了几种典型场景
- 然后看了 Guava 和 Apache Commons IO 的现成方法,虽然好用但各有局限
- 最后给出了一个更通用、兼容性更强的自定义实现
如你所见,有时候“轮子”现成的不一定能满足你的需求,关键时刻还得靠自己动手解决。
完整代码示例可以在这里找到 👉 GitHub 项目地址