Перейти к основному содержимому

Преобразование размера байта в удобочитаемый формат в Java

· 10 мин. чтения

1. Обзор

Когда мы получаем размер файла в Java , обычно мы получаем значение в байтах. Однако, как только файл становится достаточно большим, например, 123456789 байт, просмотр длины, выраженной в байтах, становится проблемой для нас, пытающихся понять, насколько велик файл.

В этом руководстве мы рассмотрим, как преобразовать размер файла в байтах в удобочитаемый формат в Java.

2. Введение в проблему

Как мы говорили ранее, когда размер файла в байтах велик, человеку нелегко его понять. Поэтому, когда мы представляем объем данных людям, мы часто используем правильный двоичный префикс , такой как КБ, МБ, ГБ и т. д., чтобы сделать большое число удобочитаемым для человека. Например, «270 ГБ» гораздо легче понять, чем «282341192 байта».

Однако когда мы получаем размер файла через стандартный API Java, обычно он указывается в байтах. Итак, чтобы иметь удобочитаемый формат, нам нужно динамически преобразовать значение из единицы байта в соответствующий двоичный префикс, например, преобразовать «282341192 байта» в «207 ГБ» или преобразовать «2048 байт» в «2 КБ». .

Стоит отметить, что существует два варианта префиксов единиц измерения:

  • Двоичные префиксы – степени числа 1024; например, 1 МБ = 1024 КБ, 1 ГБ = 1024 МБ и т. д.
  • SI ( Международная система единиц ) Префиксы – это степени числа 1000; например, 1 МБ = 1000 КБ, 1 ГБ = 1000 МБ и т. д.

В большинстве случаев в промышленности используются двоичные префиксы. Поэтому наше руководство также будет посвящено двоичным префиксам.

3. Решение проблемы

Возможно, мы уже поняли, что ключом к решению проблемы является динамический поиск подходящей единицы измерения.

Например, если вход меньше 1024, скажем, 200, то нам нужно взять единицу байта, чтобы иметь «200 байт». Однако, когда вход больше 1024, но меньше 1024 * 1024, например, 4096, мы должны использовать единицу измерения КБ, поэтому у нас есть «4 КБ».

Но давайте решать задачу поэтапно. Прежде чем мы углубимся в логику определения юнитов, давайте сначала определим все необходимые юниты и их границы.

3.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 — это два в степени 10 .

3.1. Определение числового формата

Предполагая, что мы определили правильную единицу измерения и хотим выразить размер файла с точностью до двух знаков после запятой, мы можем создать метод для вывода результата:

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.

Параметр делителя — это базовое значение выбранного юнита, а строковый аргумент unitName — это имя юнита. Например, если мы выбрали КБ в качестве подходящей единицы измерения, делитель=1024 и unitName = «КБ».

Этот метод централизует расчет деления и преобразование числового формата.

Теперь пришло время перейти к основной части решения: поиску подходящего модуля.

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) к младшим (Byte). Как только мы обнаружим, что входной размер больше или равен базовому значению текущей единицы, текущая единица измерения будет правильной.

Как только мы найдем нужную единицу, мы можем вызвать ранее созданный метод formatSize , чтобы получить окончательный результат в виде String .

3.3. Тестирование решения

Теперь давайте напишем метод модульного тестирования, чтобы проверить, работает ли наше решение должным образом. Чтобы упростить тестирование метода, давайте инициализируем Map <Long, String>, содержащий входные данные и соответствующие ожидаемые результаты:

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 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 содержит как unitBase , так и name .

Кроме того, поскольку мы хотим позже проверить единицы измерения в «убывающем» порядке, мы создали вспомогательный метод, unitInDescending, для возврата всех единиц измерения в требуемом порядке.

С этим перечислением нам не нужно кодировать имена вручную.

Далее, давайте посмотрим, сможем ли мы улучшить набор операторов if .

4.2. Использование цикла для определения единицы измерения

Поскольку наше перечисление SizeUnit может предоставить все единицы в списке в порядке убывания, мы можем заменить набор операторов 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

Мы решили задачу, проверив блоки один за другим и выбрав первый, удовлетворяющий нашему условию.

В качестве альтернативы мы можем использовать метод Long.numberOfLeadingZeros из стандартного API Java, чтобы определить, к какой единице относится данное значение размера.

Далее давайте подробнее рассмотрим этот интересный подход.

5.1. Введение в метод Long.numberOfLeadingZeros

Метод Long.numberOfLeadingZeros возвращает количество нулевых битов, предшествующих самому левому единичному биту в двоичном представлении заданного значения Long .

Поскольку тип Long в Java представляет собой 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 ). Следовательно, если мы посчитаем количество начальных нулей базового значения каждой единицы, разница между двумя соседними единицами всегда будет равна 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», чтобы построить полное имя модуля.

Стоит отметить, что мы намеренно оставляем первый символ в строке « KMGTPE » пустым . Это связано с тем, что единица « Байт » не соответствует шаблону « *B », и мы обработали ее отдельно: if (size < 1024) return size + «Bytes»;

Опять же, давайте напишем тестовый метод, чтобы убедиться, что он работает должным образом:

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

6. Использование Apache Commons IO

До сих пор мы реализовали два разных подхода к преобразованию значения размера файла в удобочитаемый формат.

Собственно, какая-то внешняя библиотека уже предоставила способ решения проблемы: Apache Commons-IO .

FileUtils Apache Commons-IO позволяет нам преобразовывать размер байта в удобочитаемый формат с помощью метода 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 .