1. 概述

在Java中获取文件大小时,通常得到的是字节数。但当文件很大时(例如123456789字节),直接看字节数很难直观理解文件的实际大小。

本文将探讨如何在Java中将文件大小从字节转换为人类可读格式(如KB、MB、GB等)。

2. 问题描述

当文件大小的字节数很大时,人类难以直观理解。因此,我们通常使用适当的SI前缀(如KB、MB、GB)来使大数字更易读。例如"270GB"比"282341192字节"更易理解。

但Java标准API返回的文件大小通常是字节值。要转换为人类可读格式,需要动态将字节值转换为对应的二进制前缀单位(如将"282341192字节"转为"207MiB")。

需要特别注意两种单位前缀体系:

  • 二进制前缀:基于1024的幂次(如1MiB = 1024 KiB)
  • SI前缀:基于1000的幂次(如1MB = 1000 KB)

本文将同时处理这两种前缀体系。

3. 解决方案

核心在于动态选择合适的单位。例如:

  • 输入200字节 → 输出"200 Bytes"
  • 输入4096字节 → 输出"4 KiB"

3.1 定义单位常量

二进制前缀(单位间乘以1024):

private static long BYTE = 1L;
private static long KiB = BYTE << 10;
private static long MiB = KiB << 10;
private static long GiB = MiB << 10;
private static long TiB = GiB << 10;
private static long PiB = TiB << 10;
private static long EiB = PiB << 10;

使用左移运算符<<计算:x << 10等价于x * 1024(因为1024=2^10)

SI前缀(单位间乘以1000):

private static long KB = BYTE * 1000;
private static long MB = KB * 1000;
private static long GB = MB * 1000;
private static long TB = GB * 1000;
private static long PB = TB * 1000;
private static long EB = PB * 1000;

3.2 定义数字格式化

确定单位后,使用DecimalFormat格式化输出(保留两位小数):

private static DecimalFormat DEC_FORMAT = new DecimalFormat("#.##");

private static String formatSize(long size, long divider, String unitName) {
    return DEC_FORMAT.format((double) size / divider) + " " + unitName;
}

divider是单位基值(如KiB的1024),unitName是单位名称(如"KiB")

3.3 动态确定单位

二进制前缀版

public static String toHumanReadableBinaryPrefixes(long size) {
    if (size < 0)
        throw new IllegalArgumentException("Invalid file size: " + size);
    if (size >= EiB) return formatSize(size, EiB, "EiB");
    if (size >= PiB) return formatSize(size, PiB, "PiB");
    if (size >= TiB) return formatSize(size, TiB, "TiB");
    if (size >= GiB) return formatSize(size, GiB, "GiB");
    if (size >= MiB) return formatSize(size, MiB, "MiB");
    if (size >= KiB) return formatSize(size, KiB, "KiB");
    return formatSize(size, BYTE, "Bytes");
}

SI前缀版

public static String toHumanReadableSIPrefixes(long size) {
    if (size < 0)
        throw new IllegalArgumentException("Invalid file size: " + size);
    if (size >= EB) return formatSize(size, EB, "EB");
    if (size >= PB) return formatSize(size, PB, "PB");
    if (size >= TB) return formatSize(size, TB, "TB");
    if (size >= GB) return formatSize(size, GB, "GB");
    if (size >= MB) return formatSize(size, MB, "MB");
    if (size >= KB) return formatSize(size, KB, "KB");
    return formatSize(size, BYTE, "Bytes");
}

逻辑:从最大单位(EB)向最小单位(Byte)检查,当size≥单位基值时即选中该单位

3.4 测试验证

二进制前缀测试数据

private static Map<Long, String> DATA_MAP_BINARY_PREFIXES = new HashMap<Long, String>() {{
    put(0L, "0 Bytes");
    put(1023L, "1023 Bytes");
    put(1024L, "1 KiB");
    put(12_345L, "12.06 KiB");
    put(10_123_456L, "9.65 MiB");
    put(10_123_456_798L, "9.43 GiB");
    put(1_777_777_777_777_777_777L, "1.54 EiB");
}};

SI前缀测试数据

private final static Map<Long, String> DATA_MAP_SI_PREFIXES = new HashMap<Long, String>() {{
    put(0L, "0 Bytes");
    put(999L, "999 Bytes");
    put(1000L, "1 KB");
    put(12_345L, "12.35 KB");
    put(10_123_456L, "10.12 MB");
    put(10_123_456_798L, "10.12 GB");
    put(1_777_777_777_777_777_777L, "1.78 EB");
}};

测试执行

DATA_MAP.forEach((in, expected) -> 
    Assert.assertEquals(expected, FileSizeFormatUtil.toHumanReadable(in))
);

所有测试用例均通过

4. 优化方案:使用枚举和循环

4.1 创建单位枚举

二进制前缀枚举

enum SizeUnitBinaryPrefixes {
    Bytes(1L),
    KiB(Bytes.unitBase << 10),
    MiB(KiB.unitBase << 10),
    GiB(MiB.unitBase << 10),
    TiB(GiB.unitBase << 10),
    PiB(TiB.unitBase << 10),
    EiB(PiB.unitBase << 10);

    private final Long unitBase;

    public static List<SizeUnitBinaryPrefixes> unitsInDescending() {
        List<SizeUnitBinaryPrefixes> list = Arrays.asList(values());
        Collections.reverse(list);
        return list;
    }
    // 省略getter和构造函数
}

SI前缀枚举

enum SizeUnitSIPrefixes {
    Bytes(1L),
    KB(Bytes.unitBase * 1000),
    MB(KB.unitBase * 1000),
    GB(MB.unitBase * 1000),
    TB(GB.unitBase * 1000),
    PB(TB.unitBase * 1000),
    EB(PB.unitBase * 1000);

    private final Long unitBase;

    public static List<SizeUnitSIPrefixes> unitsInDescending() {
        List<SizeUnitSIPrefixes> list = Arrays.asList(values());
        Collections.reverse(list);
        return list;
    }
    // 省略getter和构造函数
}

枚举封装了单位基值和名称,unitsInDescending()提供降序排列的单位列表

4.2 使用循环确定单位

public static String toHumanReadableWithEnum(long size) {
    List<SizeUnit> units = SizeUnit.unitsInDescending();
    if (size < 0) {
        throw new IllegalArgumentException("Invalid file size: " + size);
    }
    String result = null;
    for (SizeUnit unit : units) {
        if (size >= unit.getUnitBase()) {
            result = formatSize(size, unit.getUnitBase(), unit.name());
            break;
        }
    }
    return result == null ? 
        formatSize(size, SizeUnit.Bytes.getUnitBase(), SizeUnit.Bytes.name()) : 
        result;
}

优势:避免硬编码单位名称、消除多个if语句、代码更简洁

测试验证

DATA_MAP.forEach((in, expected) -> 
    Assert.assertEquals(expected, FileSizeFormatUtil.toHumanReadableWithEnum(in))
);

5. 使用Long.numberOfLeadingZeros方法

5.1 方法原理

Long.numberOfLeadingZeros()返回long值二进制表示中最高位1前面的零位数:

  • Long.numberOfLeadingZeros(0L) = 64
  • 1L(二进制...0001)→ 63个前导零
  • 1024L(二进制...10000000000)→ 53个前导零

关键发现:相邻单位的前导零数差值为10(因为1024=2^10)

单位 前导零数
Byte 63
KiB 53
MiB 43
GiB 33
TiB 23
PiB 13
EiB 3

5.2 实现方案

public static String toHumanReadableByNumOfLeadingZeros(long size) {
    if (size < 0) {
        throw new IllegalArgumentException("Invalid file size: " + size);
    }
    if (size < 1024) return size + " Bytes";
    int unitIdx = (63 - Long.numberOfLeadingZeros(size)) / 10;
    return formatSize(size, 1L << (unitIdx * 10), " KMGTPE".charAt(unitIdx) + "iB");
}

技巧:使用字符串" KMGTPE"索引获取单位字母(首字符留空处理Byte单位)

测试验证

DATA_MAP.forEach((in, expected) -> 
    Assert.assertEquals(expected, FileSizeFormatUtil.toHumanReadableByNumOfLeadingZeros(in))
);

6. 使用Apache Commons IO

Apache Commons IO的FileUtils.byteCountToDisplaySize()可直接转换:

DATA_MAP.forEach((in, expected) -> 
    System.out.println(in + " bytes -> " + FileUtils.byteCountToDisplaySize(in))
);

输出示例

0 bytes -> 0 bytes
1024 bytes -> 1 KB
1777777777777777777 bytes -> 1 EB
12345 bytes -> 12 KB
10123456 bytes -> 9 MB
10123456798 bytes -> 9 GB
1023 bytes -> 1023 bytes

注意:此方法会自动向上取整(如12.35KB显示为12KB)

7. 总结

本文提供了三种将文件大小转换为人类可读格式的方案:

  1. 基础方案:使用单位常量和if-else判断
  2. 优化方案:通过枚举和循环简化代码
  3. 高效方案:利用Long.numberOfLeadingZeros()的位运算特性

所有代码示例均可在GitHub仓库中获取。根据实际需求选择合适方案——简单场景可用基础方案,追求代码简洁可选枚举方案,追求性能可选位运算方案。


原始标题:Convert Byte Size Into a Human-Readable Format in Java