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 项目地址


原始标题:Get a Filename Without the Extension in Java