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. 总结
本文提供了三种将文件大小转换为人类可读格式的方案:
- 基础方案:使用单位常量和if-else判断
- 优化方案:通过枚举和循环简化代码
- 高效方案:利用
Long.numberOfLeadingZeros()
的位运算特性
所有代码示例均可在GitHub仓库中获取。根据实际需求选择合适方案——简单场景可用基础方案,追求代码简洁可选枚举方案,追求性能可选位运算方案。