1. 概要

ファイルのサイズをJavaで取得すると、通常、値はバイト単位で取得されます。 ただし、ファイルが十分に大きくなると(たとえば、123456789バイト)、バイトで表された長さを確認することは、ファイルの大きさを理解しようとする私たちにとって課題になります。

このチュートリアルでは、バイト単位のファイルサイズをJavaで人間が読める形式に変換する方法について説明します。

2. 問題の紹介

先ほどお話ししたように、ファイルのサイズ(バイト単位)が大きいと、人間にはわかりにくいです。 したがって、大量のデータを人間に提示する場合、KB、MB、GBなどの適切なバイナリプレフィックスを使用して、多数の人間が判読できるようにすることがよくあります。 たとえば、「270GB」は「282341192バイト」よりもはるかに理解しやすいです。

ただし、標準のJava APIを介してファイルサイズを取得する場合、通常はバイト単位です。 したがって、人間が読める形式にするには、値をバイト単位から対応するバイナリプレフィックスに動的に変換する必要があります。たとえば、「282341192バイト」を「207GB」に変換したり、「2048バイト」を「2KB」に変換したりします。 。

単位接頭辞には2つのバリエーションがあることに言及する価値があります。

  • バイナリプレフィックス–1024の累乗です。 たとえば、1MB = 1024 KB、1GB =1024MBなどです。
  • SI(国際単位系)接頭辞–1000の累乗です。 たとえば、1MB = 1000 KB、1GB =1000MBなどです。

ほとんどの場合、業界ではバイナリプレフィックスが使用されます。 したがって、このチュートリアルでは、バイナリプレフィックスにも焦点を当てます。

3. 問題の解決

問題を解決するための鍵は、適切なユニットを動的に見つけることであることにすでに気付いているかもしれません。

たとえば、入力が1024未満、たとえば200の場合、バイト単位を「200バイト」にする必要があります。 ただし、入力が1024より大きく1024 * 1024より小さい場合、たとえば4096の場合、KB単位を使用する必要があるため、「4KB」になります。

しかし、問題を段階的に解決していきましょう。 ユニット決定ロジックに飛び込む前に、まず必要なすべてのユニットとその境界を定義しましょう。

3.1. 必要な単位の定義

ご存知のように、 1ユニットに1024を掛けると、次のレベルのユニットに移行します。 したがって、必要なすべてのユニットとその基本値を示す定数を作成できます。

private static long BYTE = 1L;
private static long KB = BYTE << 10;
private static long MB = KB << 10;
private static long GB = MB << 10;
private static long TB = GB << 10;
private static long PB = TB << 10;
private static long EB = PB << 10;

上記のコードが示すように、バイナリを使用しました左シフト演算子 (<<)基本値を計算します。 ここ、 「x<<10」は、「x * 1024」と同じように機能します。これは、1024が2の10の累乗であるためです。

3.1. 数値形式の定義

適切な単位を決定し、ファイルサイズを小数点以下2桁で表現したい場合は、結果を出力するメソッドを作成できます。

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;
}

次に、メソッドが何をするのかを簡単に理解しましょう。 上記のコードで見たように、最初に、数値形式DEC_FORMAT。を定義しました。

divider パラメーターは選択されたユニットの基本値であり、String引数unitNameはユニットの名前です。 たとえば、適切な単位としてKBを選択した場合、divider =1024およびunitName=“ KB”。

この方法は、除算の計算と数値形式の変換を一元化します。

次に、ソリューションのコア部分である適切なユニットを見つけることに移ります。

3.2. ユニットの決定

まず、単位決定方法の実装を見てみましょう。

public static String toHumanReadable(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)から低(バイト)の方向にチェックします。 入力が見つかったらサイズ現在の単位の基本値以上の場合、現在の単位が正しい単位になります。

適切なユニットが見つかるとすぐに、以前に作成した formatSize メソッドを呼び出して、Stringとして最終結果を取得できます。

3.3. ソリューションのテスト

それでは、ソリューションが期待どおりに機能するかどうかを確認するための単体テストメソッドを作成しましょう。 メソッドのテストを簡素化するために、 マップを初期化する入力と対応する期待される結果を保持します。

private static Map<Long, String> DATA_MAP = new HashMap<Long, String>() {{
    put(0L, "0 Bytes");
    put(1023L, "1023 Bytes");
    put(1024L, "1 KB");
    put(12_345L, "12.06 KB");
    put(10_123_456L, "9.65 MB");
    put(10_123_456_798L, "9.43 GB");
    put(1_777_777_777_777_777_777L, "1.54 EB");
}};

次に、 Map DATA_MAP を調べて、各キー値を入力として取得し、期待される結果が得られるかどうかを確認します。

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

単体テストを実行すると合格です。

4. 列挙型とループを使用したソリューションの改善

これまでのところ、問題は解決しました。 解決策は非常に簡単です。 toHumanReadable メソッドでは、単位を決定するために複数のifステートメントを記述しました。

解決策を注意深く考えると、いくつかの点でエラーが発生しやすい可能性があります。

  • これらのifステートメントの順序は、メソッド内でそのまま固定する必要があります。
  • ifステートメントでは、単位定数と対応する名前をStringオブジェクトとしてハードコーディングしています。

次に、ソリューションを改善する方法を見てみましょう。

4.1. SizeUnit列挙型の作成

実際には、単位定数を enum に変換できるため、メソッドに名前をハードコーディングする必要はありません。

enum SizeUnit {
    Bytes(1L),
    KB(Bytes.unitBase << 10),
    MB(KB.unitBase << 10),
    GB(MB.unitBase << 10),
    TB(GB.unitBase << 10),
    PB(TB.unitBase << 10),
    EB(PB.unitBase << 10);

    private final Long unitBase;

    public static List<SizeUnit> unitsInDescending() {
        List<SizeUnit> list = Arrays.asList(values());
        Collections.reverse(list);
        return list;
    }
   
    //getter and constructor are omitted
}

上記の列挙型SizeUnitが示すように、SizeUnitインスタンスはunitBasenameの両方を保持します。

さらに、後でユニットを「降順」でチェックしたいので、必要な順序ですべてのユニットを返すヘルパーメソッド unitsInDescending、を作成しました。

この列挙型を使用すると、名前を手動でコーディングする必要がありません。 

次に、ifステートメントのセットを改善できるかどうかを見てみましょう。

4.2. ループを使用して単位を決定する

SizeUnit列挙型List内のすべてのユニットを降順で提供できるため、ifステートメントのセットをforに置き換えることができます。ループ:

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メソッドの使用

ユニットを1つずつチェックし、条件を満たす最初のユニットを取得することで、問題を解決しました。

または、Java標準APIの Long.numberOfLeadingZeros メソッドを使用して、指定されたサイズ値がどの単位に該当するかを判別することもできます。

次に、この興味深いアプローチを詳しく見てみましょう。

5.1. Long.numberOfLeadingZerosメソッドの概要

Long.numberOfLeadingZeros メソッドは、指定されたLong値のバイナリ表現の左端の1ビットに先行するゼロビットの数を返します。 

として JavaのLong型は、64ビット整数、Long.numberOfLeadingZeros(0L)=64です。 いくつかの例は、メソッドをすばやく理解するのに役立つ場合があります。

1L  = 00... (63 zeros in total) ..            0001 -> Long.numberOfLeadingZeros(1L) = 63
1024L = 00... (53 zeros in total) .. 0100 0000 0000 -> Long.numberOfLeadingZeros(1024L) = 53

これで、Long.numberOfLeadingZerosメソッドについて理解できました。 しかし、なぜそれがユニットを決定するのに役立つのでしょうか?

それを理解しましょう。

5.2. 問題を解決するためのアイデア

ユニット間の係数は1024で、2の10の累乗( 2 ^ 10 )であることがわかっています。 したがって、各ユニットの基本値の先行ゼロの数を計算すると、2つの隣接するユニット間の差は常に10になります。

Index  Unit	numberOfLeadingZeros(unit.baseValue)
----------------------------------------------------
0      Byte	63
1      KB  	53
2      MB  	43
3      GB  	33
4      TB  	23
5      PB  	13
6      EB        3

さらに、入力値の先行ゼロの数を計算し、結果がどのユニットの範囲に収まるかを確認して、適切なユニットを見つけることができます。

次に、例を見てみましょう–単位を決定し、サイズ4096の単位ベース値を計算する方法:

if 4096 < 1024 (Byte's base value)  -> Byte 
else:
    numberOfLeadingZeros(4096) = 51
    unitIdx = (numberOfLeadingZeros(1) - 51) / 10 = (63 - 51) / 10 = 1
    unitIdx = 1  -> KB (Found the unit)
    unitBase = 1 << (unitIdx * 10) = 1 << 10 = 1024

次に、このロジックをメソッドとして実装しましょう。

5.3. アイデアの実装

今説明したアイデアを実装するためのメソッドを作成しましょう。

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) + "B");
}

ご覧のとおり、上記の方法はかなりコンパクトです。 単位定数や列挙型は必要ありません。 その代わり、 単位を含む文字列を作成しました:” KMGTPE”。 次に、計算されたunitIdxを使用して正しいユニット文字を選択し、「B」を追加して完全なユニット名を作成します。

String ” KMGTPE”の最初の文字を意図的に空のままにしておくことは言及する価値があります。 これは、ユニット「 バイト 」はパターン「 * B 「そして、私たちはそれを別々に扱いました: if(size <1024)return size +” Bytes”;

繰り返しになりますが、期待どおりに機能することを確認するためのテストメソッドを作成しましょう。

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

6. ApacheCommonsIOの使用

これまで、ファイルサイズの値を人間が読める形式に変換するための2つの異なるアプローチを実装してきました。

実際、一部の外部ライブラリは、問題を解決するためのメソッドをすでに提供しています: ApacheCommons-IO

ApacheCommons-IOのFileUtilsを使用すると、byteCountToDisplaySizeメソッドを使用してバイトサイズを人間が読める形式に変換できます。

ただし、このメソッドは小数部を自動的に切り上げます

最後に、 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

7. 結論

この記事では、バイト単位のファイルサイズを人間が読める形式に変換するさまざまな方法について説明しました。

いつものように、この記事で紹介するコードは、GitHubから入手できます。