在Java中如何将字节大小转换为易读的格式?

681

我该如何在Java中将字节大小转换为人类可读的格式?

比如1024应该变成“1 Kb”,而1024*1024应该变成“1 Mb”。

我已经厌烦了每个项目都要写这个实用程序方法。在Apache Commons中有没有静态方法可以完成这个任务?


42
如果您使用标准单位,1024应该变成“1KiB”,而1024*1024则应该变成“1MiB”。参见http://en.wikipedia.org/wiki/Binary_prefix。 - Pascal Cuoq
4
@ Pascal Cuoq:谢谢提供参考。在阅读这篇文章之前,我不知道在欧盟我们必须按法律要求使用正确的前缀。请允许我翻译为:“感谢您提供的参考。我直到阅读它之前才意识到,在欧盟,按法律要求我们必须使用正确的前缀。” - JeremyP
2
@DerMike,你曾经提到过“直到这样的库存在”。现在它已经成为现实了。:-)https://dev59.com/XG865IYBdhLWcg3wi_Q2#38390338 - Christian Esken
1
@AaronDigulla 你说得对。为什么那个比这个问题早两个月的问题被关闭为重复,而不是这个呢? - hc_dev
1
@hc_dev 我想早两个月提出的那个问题被关闭是因为这个问题有更好的答案。这两个问题都是在2010年发布的,另一个问题一直到2013年才关闭。(现在想想,SO真应该有一个“合并问题”的功能,把两个问题的答案合并到一个地方。) - FeRD
显示剩余3条评论
31个回答

1492
趣闻: 这里最初发布的代码片段是 Stack Overflow 上被复制最多的 Java 代码片段,但它存在缺陷。虽然已经修复,但变得混乱了。

完整故事请看这篇文章:Stack Overflow 历史上被复制最多的代码片段存在缺陷!

来源:如何将字节大小格式化为易于阅读的格式 | Programming.Guide

SI(1 k = 1,000)

public static String humanReadableByteCountSI(long bytes) {
    if (-1000 < bytes && bytes < 1000) {
        return bytes + " B";
    }
    CharacterIterator ci = new StringCharacterIterator("kMGTPE");
    while (bytes <= -999_950 || bytes >= 999_950) {
        bytes /= 1000;
        ci.next();
    }
    return String.format("%.1f %cB", bytes / 1000.0, ci.current());
}

二进制(1 Ki = 1,024)

public static String humanReadableByteCountBin(long bytes) {
    long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);
    if (absB < 1024) {
        return bytes + " B";
    }
    long value = absB;
    CharacterIterator ci = new StringCharacterIterator("KMGTPE");
    for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) {
        value >>= 10;
        ci.next();
    }
    value *= Long.signum(bytes);
    return String.format("%.1f %ciB", value / 1024.0, ci.current());
}

示例输出:

                             SI     BINARY

                  0:        0 B        0 B
                 27:       27 B       27 B
                999:      999 B      999 B
               1000:     1.0 kB     1000 B
               1023:     1.0 kB     1023 B
               1024:     1.0 kB    1.0 KiB
               1728:     1.7 kB    1.7 KiB
             110592:   110.6 kB  108.0 KiB
            7077888:     7.1 MB    6.8 MiB
          452984832:   453.0 MB  432.0 MiB
        28991029248:    29.0 GB   27.0 GiB
      1855425871872:     1.9 TB    1.7 TiB
9223372036854775807:     9.2 EB    8.0 EiB   (Long.MAX_VALUE)

1
我唯一不喜欢的是,1.0 KB 可以更漂亮地显示为 1 KB。(这就是为什么我在我的答案中使用 DecimalFormat) - Sean Patrick Floyd
14
我更喜欢1.0 KB。这样可以清楚地知道输出包含多少有效数字。 (这似乎也是Linux中du命令的行为,例如。) - aioobe
28
请注意,项目中客户希望以2为底(除以1024)的形式显示值,但使用常见的前缀而非KiB、MiB、GiB等。请使用KB、MB、GB和TB。请记住不要改变原意。 - Borys
6
对于iOS开发者,你可以使用NSByteCountFormatter。例如(在Swift中):let bytes = 110592 NSByteCountFormatter.stringFromByteCount(Int64(bytes), countStyle: NSByteCountFormatterCountStyle.File)将产生"111 KB"的结果。 - duthen
1
最好使用相应区域设置的 "String.format",例如:String.format(Locale.US, "%.1f %sB", bytes / Math.pow(unit, exp), pre) - Bo Lu
显示剩余3条评论

380

FileUtils.byteCountToDisplaySize(long size) 可以解决您的问题,如果您的项目可以依赖于 org.apache.commons.io

该方法的JavaDoc


22
我已经在我的项目中使用了commons-io,但最终使用了aioobe的代码,因为它有更好的舍入行为(请参阅JavaDoc链接)。 - Iravanchi
3
有没有工具可以执行相反的操作,即从人类可读的字节计数获取字节数? - arunmoezhi
7
很遗憾,这个函数不支持地域感知;例如,在法语中,字节总被称为“octets”,所以如果你要向法国用户显示一个100KB的文件,正确的标签应该是100Ko。 - Tacroy
7
当数值大于1GB时,该值会四舍五入到最接近的GB,这意味着你获得的精度是可变的。 - tksfz
对于使用Zendesk Util APIs (com.zendesk.util)的任何人,您应该有以下API作为选项:FileUtils.humanReadableFileSize(bytesSize) - Thiengo
显示剩余2条评论

89

我们可以完全避免使用缓慢的 Math.pow()Math.log() 方法而不牺牲简洁性,因为单位之间的因子(例如 B、KB、MB 等)是 1024,即 2^10。Long 类有一个方便的 numberOfLeadingZeros() 方法,我们可以使用它来确定大小值属于哪个单位。

关键点:大小单位距离 10 位比特(1024 = 2^10),意味着最高有效比特的位置,或者说前导零的数量,相差 10 位(字节 = KB * 1024,KB = MB * 1024 等)。

前导零数量和大小单位之间的对应关系:

前导零数量 大小单位
>53 B (字节)
>43 KB
>33 MB
>23 GB
>13 TB
>3 PB
<=3 EB

最终代码:

public static String formatSize(long v) {
    if (v < 1024) return v + " B";
    int z = (63 - Long.numberOfLeadingZeros(v)) / 10;
    return String.format("%.1f %sB", (double)v / (1L << (z*10)), " KMGTPE".charAt(z));
}

1
请注意,您可以使用Math.scalb(v, z * -10)代替(double)v / (1L << (z*10)) - Holger
在某些情况下,会收到除以零的错误。我在 Kotlin 中的分母中使用了 coerceAtLeast(1)。 - Razi Kallayi

29

最近我也问了同样的问题:

如何将文件大小格式化为MB、GB等

虽然没有现成的答案,但是我可以接受以下解决方案:

private static final long K = 1024;
private static final long M = K * K;
private static final long G = M * K;
private static final long T = G * K;

public static String convertToStringRepresentation(final long value){
    final long[] dividers = new long[] { T, G, M, K, 1 };
    final String[] units = new String[] { "TB", "GB", "MB", "KB", "B" };
    if(value < 1)
        throw new IllegalArgumentException("Invalid file size: " + value);
    String result = null;
    for(int i = 0; i < dividers.length; i++){
        final long divider = dividers[i];
        if(value >= divider){
            result = format(value, divider, units[i]);
            break;
        }
    }
    return result;
}

private static String format(final long value,
    final long divider,
    final String unit){
    final double result =
        divider > 1 ? (double) value / (double) divider : (double) value;
    return new DecimalFormat("#,##0.#").format(result) + " " + unit;
}

测试代码:

public static void main(final String[] args){
    final long[] l = new long[] { 1l, 4343l, 43434334l, 3563543743l };
    for(final long ll : l){
        System.out.println(convertToStringRepresentation(ll));
    }
}

输出结果(在我的德语环境下):

1 B
4,2 KB
41,4 MB
3,3 GB

我已经为Google Guava开启了一个功能请求问题。也许有人愿意支持它。


3
为什么0是无效的文件大小? - aioobe
@aioobe 这在我的使用情况中(显示上传文件的大小)很有用,但可以说这并不是普遍适用的。 - Sean Patrick Floyd
如果你将最后一行改为 return NumberFormat.getFormat("#,##0.#").format(result) + " " + unit;,它在GWT中也可以工作!感谢您的帮助,这仍然不在Guava中。 - tom

18
private String bytesIntoHumanReadable(long bytes) {
    long kilobyte = 1024;
    long megabyte = kilobyte * 1024;
    long gigabyte = megabyte * 1024;
    long terabyte = gigabyte * 1024;

    if ((bytes >= 0) && (bytes < kilobyte)) {
        return bytes + " B";

    } else if ((bytes >= kilobyte) && (bytes < megabyte)) {
        return (bytes / kilobyte) + " KB";

    } else if ((bytes >= megabyte) && (bytes < gigabyte)) {
        return (bytes / megabyte) + " MB";

    } else if ((bytes >= gigabyte) && (bytes < terabyte)) {
        return (bytes / gigabyte) + " GB";

    } else if (bytes >= terabyte) {
        return (bytes / terabyte) + " TB";

    } else {
        return bytes + " Bytes";
    }
}

我喜欢这个,因为它易于跟随和理解。 - Joshua Pinter
1
@Joshua Pinter:是的,但也有很多冗余。它需要一个循环和一个(静态)字符串列表。 - Peter Mortensen
2
你总是可以让事情变得更“高效”,但在某些时候,这可能会以人类读者的清晰度为代价。我认为这是一个很好的权衡。现在,如果您需要支持2倍或3倍的单位(例如“PB”,“EB”,“ZB”,“YB”),就像其他答案所做的那样,那么我认为DRYing是一个不错的方法。值得庆幸的是,在我们的应用程序中,我们永远不会超过“GB”,更不用说“TB”了。 - Joshua Pinter
虽然这可能非常快,但它不会产生十进制输出。1.01 GB大小的文件和1.99 GB大小的文件之间有很大的差异。使用此方法,它们都将显示为“1 GB”。 - peterh

11

这是aioobe的回答的修改版。

更改内容:

  • 添加了Locale参数,因为一些语言使用.而其他语言使用,作为小数点。
  • 人性化的代码

private static final String[] SI_UNITS = { "B", "kB", "MB", "GB", "TB", "PB", "EB" };
private static final String[] BINARY_UNITS = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" };

public static String humanReadableByteCount(final long bytes, final boolean useSIUnits, final Locale locale)
{
    final String[] units = useSIUnits ? SI_UNITS : BINARY_UNITS;
    final int base = useSIUnits ? 1000 : 1024;

    // When using the smallest unit no decimal point is needed, because it's the exact number.
    if (bytes < base) {
        return bytes + " " + units[0];
    }

    final int exponent = (int) (Math.log(bytes) / Math.log(base));
    final String unit = units[exponent];
    return String.format(locale, "%.1f %s", bytes / Math.pow(base, exponent), unit);
}

1
将Locale参数仅用于分隔符号有点混合的结果,但是不考虑使用不同字节符号的语言(例如法语)来本地化单位。 - Nzall
@Nzall 你是指八位组吗?维基百科上说它已经不常见了。否则,你有参考资料吗? - Christian Strempfer
作为一名法国人,我确认“octet”仍然被广泛使用;法国人会期望“Ko”,“Mo”,“Go”等。无论如何,i18n似乎超出了OP的范围。如果您真的需要i18n,您可能需要使用一些属性文件。 - user1075613

10

通过扩展属性获取Kotlin版本

如果您正在使用Kotlin,使用这些扩展属性对文件大小进行格式化非常容易。它是无循环的,并完全基于纯数学。


HumanizeUtils.kt

import java.io.File
import kotlin.math.log2
import kotlin.math.pow

/**
 * @author aminography
 */

val File.formatSize: String
    get() = length().formatAsFileSize

val Int.formatAsFileSize: String
    get() = toLong().formatAsFileSize

val Long.formatAsFileSize: String
    get() = log2(coerceAtLeast(1).toDouble()).toInt().div(10).let {
        val precision = when (it) {
            0 -> 0; 1 -> 1; else -> 2
        }
        val prefix = arrayOf("", "K", "M", "G", "T", "P", "E", "Z", "Y")
        String.format("%.${precision}f ${prefix[it]}B", toDouble() / 2.0.pow(it * 10.0))
    }

用法:

println("0:          " + 0.formatAsFileSize)
println("170:        " + 170.formatAsFileSize)
println("14356:      " + 14356.formatAsFileSize)
println("968542985:  " + 968542985.formatAsFileSize)
println("8729842496: " + 8729842496.formatAsFileSize)

println("file: " + file.formatSize)

抱歉,我无法按照您的要求进行翻译。
0:          0 B
170:        170 B
14356:      14.0 KB
968542985:  923.67 MB
8729842496: 8.13 GB

file: 6.15 MB

1
尝试了这种方法,它有效! - Kevin Germain

9

private static final String[] Q = new String[]{"", "K", "M", "G", "T", "P", "E"};

public String getAsString(long bytes)
{
    for (int i = 6; i > 0; i--)
    {
        double step = Math.pow(1024, i);
        if (bytes > step) return String.format("%3.1f %s", bytes / step, Q[i]);
    }
    return Long.toString(bytes);
}

这里使用了循环,而不是许多其他答案中的粗略复制粘贴重用。然而,它缺少一个解释。它是否有效? - Peter Mortensen
魔数“6”是必要的吗?它不是与Q的长度有关吗? - Peter Mortensen
好的,OP已经离开了这个场所:"最后一次出现是在6年前"。 - undefined

9
如果您使用Android系统,您可以简单地使用android.text.format.Formatter.formatFileSize()。它的优点是易于使用,并且根据本地化设置向用户显示。缺点是它不支持EB,仅适用于公制单位(每千为1000字节),不能作为1024字节使用。

另外,这里有一个基于这个流行帖子的解决方案:


interface BytesFormatter {
    /**called when the type of the result to format is Long. Example: 123KB
     * @param unitPowerIndex the unit-power we need to format to. Examples: 0 is bytes, 1 is kb, 2 is mb, etc...
     * available units and their order: B,K,M,G,T,P,E
     * @param isMetric true if each kilo==1000, false if kilo==1024
     * */
    fun onFormatLong(valueToFormat: Long, unitPowerIndex: Int, isMetric: Boolean): String

    /**called when the type of the result to format is Double. Example: 1.23KB
     * @param unitPowerIndex the unit-power we need to format to. Examples: 0 is bytes, 1 is kb, 2 is mb, etc...
     * available units and their order: B,K,M,G,T,P,E
     * @param isMetric true if each kilo==1000, false if kilo==1024
     * */
    fun onFormatDouble(valueToFormat: Double, unitPowerIndex: Int, isMetric: Boolean): String
}

/**
 * formats the bytes to a human readable format, by providing the values to format later in the unit that we've found best to fit it
 *
 * @param isMetric true if each kilo==1000, false if kilo==1024
 * */
fun bytesIntoHumanReadable(
    @IntRange(from = 0L) bytesToFormat: Long, bytesFormatter: BytesFormatter,
    isMetric: Boolean = true
): String {
    val units = if (isMetric) 1000L else 1024L
    if (bytesToFormat < units)
        return bytesFormatter.onFormatLong(bytesToFormat, 0, isMetric)
    var bytesLeft = bytesToFormat
    var unitPowerIndex = 0
    while (unitPowerIndex < 6) {
        val newBytesLeft = bytesLeft / units
        if (newBytesLeft < units) {
            val byteLeftAsDouble = bytesLeft.toDouble() / units
            val needToShowAsInteger =
                byteLeftAsDouble == (bytesLeft / units).toDouble()
            ++unitPowerIndex
            if (needToShowAsInteger) {
                bytesLeft = newBytesLeft
                break
            }
            return bytesFormatter.onFormatDouble(byteLeftAsDouble, unitPowerIndex, isMetric)
        }
        bytesLeft = newBytesLeft
        ++unitPowerIndex
    }
    return bytesFormatter.onFormatLong(bytesLeft, unitPowerIndex, isMetric)
}

Sample usage:

// val valueToTest = 2_000L
// val valueToTest = 2_000_000L
// val valueToTest = 2_000_000_000L
// val valueToTest = 9_000_000_000_000_000_000L
// val valueToTest = 9_200_000_000_000_000_000L
val bytesToFormat = Random.nextLong(Long.MAX_VALUE)
val bytesFormatter = object : BytesFormatter {
    val numberFormat = NumberFormat.getNumberInstance(Locale.ROOT).also {
        it.maximumFractionDigits = 2
        it.minimumFractionDigits = 0
    }

    private fun formatByUnit(formattedNumber: String, threePowerIndex: Int, isMetric: Boolean): String {
        val sb = StringBuilder(formattedNumber.length + 4)
        sb.append(formattedNumber)
        val unitsToUse = "B${if (isMetric) "k" else "K"}MGTPE"
        sb.append(unitsToUse[threePowerIndex])
        if (threePowerIndex > 0)
            if (isMetric) sb.append('B') else sb.append("iB")
        return sb.toString()
    }

    override fun onFormatLong(valueToFormat: Long, unitPowerIndex: Int, isMetric: Boolean): String {
        return formatByUnit(String.format("%,d", valueToFormat), unitPowerIndex, isMetric)
    }

    override fun onFormatDouble(valueToFormat: Double, unitPowerIndex: Int, isMetric: Boolean): String {
        //alternative for using numberFormat :
        //val formattedNumber = String.format("%,.2f", valueToFormat).let { initialFormattedString ->
        //    if (initialFormattedString.contains('.'))
        //        return@let initialFormattedString.dropLastWhile { it == '0' }
        //    else return@let initialFormattedString
        //}
        return formatByUnit(numberFormat.format(valueToFormat), unitPowerIndex, isMetric)
    }
}
Log.d("AppLog", "formatting of $bytesToFormat bytes (${String.format("%,d", bytesToFormat)})")
Log.d("AppLog", bytesIntoHumanReadable(bytesToFormat, bytesFormatter))
Log.d("AppLog", "Android:${android.text.format.Formatter.formatFileSize(this, bytesToFormat)}")


@aioobe 但这意味着循环可以在i == unitsCount时停止,这意味着i == 6,这意味着“charAt”将失败... - android developer
@aioobe 正确。我会修复它。顺便说一下,你的算法也可能提供奇怪的结果。尝试将"999999,true"作为参数输入。它将显示"1000.0 kB",所以它被四舍五入了,但当人们看到它时,他们可能会想:为什么它不能显示1MB,因为1000KB=1MB...你认为这应该如何处理?这是由于String.format引起的,但我不确定应该如何修复它。 - android developer
@aioobe 如果您使用Android,我还添加了一种简短的方法来完成它。 - android developer
@aioobe 看起来这个解决方案对于太大的值不起作用,因为Double无法处理它们。我认为“E”单位有问题(我使用每千字节1024字节)。在那之前,我认为它运行良好。如果您发送Long.MAX_VALUE,它将不会显示正确的值。你怎么看? - android developer
好的,我已经修复了,并更新了我的答案。 - android developer
显示剩余4条评论

8

Byte Units可以让你像这样做:

long input1 = 1024;
long input2 = 1024 * 1024;

Assert.assertEquals("1 KiB", BinaryByteUnit.format(input1));
Assert.assertEquals("1 MiB", BinaryByteUnit.format(input2));

Assert.assertEquals("1.024 KB", DecimalByteUnit.format(input1, "#.0"));
Assert.assertEquals("1.049 MB", DecimalByteUnit.format(input2, "#.000"));

NumberFormat format = new DecimalFormat("#.#");
Assert.assertEquals("1 KiB", BinaryByteUnit.format(input1, format));
Assert.assertEquals("1 MiB", BinaryByteUnit.format(input2, format));

我编写了另一个名为 storage-units 的库,它可以让你像这样做:

String formattedUnit1 = StorageUnits.formatAsCommonUnit(input1, "#");
String formattedUnit2 = StorageUnits.formatAsCommonUnit(input2, "#");
String formattedUnit3 = StorageUnits.formatAsBinaryUnit(input1);
String formattedUnit4 = StorageUnits.formatAsBinaryUnit(input2);
String formattedUnit5 = StorageUnits.formatAsDecimalUnit(input1, "#.00", Locale.GERMAN);
String formattedUnit6 = StorageUnits.formatAsDecimalUnit(input2, "#.00", Locale.GERMAN);
String formattedUnit7 = StorageUnits.formatAsBinaryUnit(input1, format);
String formattedUnit8 = StorageUnits.formatAsBinaryUnit(input2, format);

Assert.assertEquals("1 kB", formattedUnit1);
Assert.assertEquals("1 MB", formattedUnit2);
Assert.assertEquals("1.00 KiB", formattedUnit3);
Assert.assertEquals("1.00 MiB", formattedUnit4);
Assert.assertEquals("1,02 kB", formattedUnit5);
Assert.assertEquals("1,05 MB", formattedUnit6);
Assert.assertEquals("1 KiB", formattedUnit7);
Assert.assertEquals("1 MiB", formattedUnit8);

如需强制使用某个单位,请按照以下步骤操作:

String formattedUnit9 = StorageUnits.formatAsKibibyte(input2);
String formattedUnit10 = StorageUnits.formatAsCommonMegabyte(input2);

Assert.assertEquals("1024.00 KiB", formattedUnit9);
Assert.assertEquals("1.00 MB", formattedUnit10);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接