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

Преобразование между байтовыми массивами и шестнадцатеричными строками в Java

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

1. Обзор

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

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

2. Преобразование между байтами и шестнадцатеричными числами

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

2.1. Байт в шестнадцатеричный

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

Например, мы можем записать 45 как 0010 1101 в двоичном формате, а шестнадцатеричный эквивалент будет «2d»:

0010 = 2 (base 10) = 2 (base 16)
1101 = 13 (base 10) = d (base 16)

Therefore: 45 = 0010 1101 = 0x2d

Давайте реализуем эту простую логику на Java:

public String byteToHex(byte num) {
char[] hexDigits = new char[2];
hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16);
hexDigits[1] = Character.forDigit((num & 0xF), 16);
return new String(hexDigits);
}

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

char[] hexDigits = new char[2];

Затем мы изолировали биты более высокого порядка, сдвинув вправо 4 бита. А затем мы применили маску, чтобы изолировать младшие 4 бита. Маскировка необходима, потому что отрицательные числа внутренне представлены как два дополнения положительного числа:

hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16);

Затем мы преобразуем оставшиеся 4 бита в шестнадцатеричный формат:

hexDigits[1] = Character.forDigit((num & 0xF), 16);

Наконец, мы создаем объект String из массива char. А затем вернул этот объект как преобразованный шестнадцатеричный массив.

Теперь давайте разберемся, как это будет работать для отрицательного байта -4:

hexDigits[0]:
1111 1100 >> 4 = 1111 1111 1111 1111 1111 1111 1111 1111
1111 1111 1111 1111 1111 1111 1111 1111 & 0xF = 0000 0000 0000 0000 0000 0000 0000 1111 = 0xf

hexDigits[1]:
1111 1100 & 0xF = 0000 1100 = 0xc

Therefore: -4 (base 10) = 1111 1100 (base 2) = fc (base 16)

Также стоит отметить, что Character. Метод forDigit () всегда возвращает символы нижнего регистра.

2.2. Шестнадцатеричный в байт

Теперь давайте преобразуем шестнадцатеричную цифру в байт. Как известно, байт содержит 8 бит. Следовательно, нам нужно две шестнадцатеричных цифры для создания одного байта .

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

Затем нам нужно соединить два четырехбитовых сегмента, чтобы получить байтовый эквивалент:

Hexadecimal: 2d
2 = 0010 (base 2)
d = 1101 (base 2)

Therefore: 2d = 0010 1101 (base 2) = 45

Теперь давайте напишем операцию на Java:

public byte hexToByte(String hexString) {
int firstDigit = toDigit(hexString.charAt(0));
int secondDigit = toDigit(hexString.charAt(1));
return (byte) ((firstDigit << 4) + secondDigit);
}

private int toDigit(char hexChar) {
int digit = Character.digit(hexChar, 16);
if(digit == -1) {
throw new IllegalArgumentException(
"Invalid Hexadecimal Character: "+ hexChar);
}
return digit;
}

Давайте разберемся с этим, по одной операции за раз.

Прежде всего, мы преобразовали шестнадцатеричные символы в целые числа:

int firstDigit = toDigit(hexString.charAt(0));
int secondDigit = toDigit(hexString.charAt(1));

Затем мы сдвинули старшую цифру влево на 4 бита. Следовательно, двоичное представление имеет нули в четырех младших битах.

Затем мы добавили к нему младшую значащую цифру:

return (byte) ((firstDigit << 4) + secondDigit);

Теперь давайте внимательно рассмотрим метод toDigit() . Мы используем метод Character.digit() для преобразования. Если значение символа, переданное этому методу, не является допустимой цифрой в указанной системе счисления, возвращается -1.

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

3. Преобразование массивов байтов в шестнадцатеричные строки

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

3.1. Массив байтов в шестнадцатеричную строку

Нам нужно перебрать массив и сгенерировать шестнадцатеричную пару для каждого байта:

public String encodeHexString(byte[] byteArray) {
StringBuffer hexStringBuffer = new StringBuffer();
for (int i = 0; i < byteArray.length; i++) {
hexStringBuffer.append(byteToHex(byteArray[i]));
}
return hexStringBuffer.toString();
}

Как мы уже знаем, вывод всегда будет в нижнем регистре.

3.2. Шестнадцатеричная строка в массив байтов

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

Теперь мы пройдемся по массиву и преобразуем каждую шестнадцатеричную пару в байт:

public byte[] decodeHexString(String hexString) {
if (hexString.length() % 2 == 1) {
throw new IllegalArgumentException(
"Invalid hexadecimal String supplied.");
}

byte[] bytes = new byte[hexString.length() / 2];
for (int i = 0; i < hexString.length(); i += 2) {
bytes[i / 2] = hexToByte(hexString.substring(i, i + 2));
}
return bytes;
}

4. Использование класса BigInteger

Мы можем создать объект типа BigInteger , передав знак и массив байтов .

Теперь мы можем сгенерировать шестнадцатеричную строку с помощью формата статического метода, определенного в классе String :

public String encodeUsingBigIntegerStringFormat(byte[] bytes) {
BigInteger bigInteger = new BigInteger(1, bytes);
return String.format(
"%0" + (bytes.length << 1) + "x", bigInteger);
}

Предоставленный формат будет генерировать заполненную нулями строчную шестнадцатеричную строку. Мы также можем сгенерировать строку в верхнем регистре, заменив «x» на «X».

В качестве альтернативы мы могли бы использовать метод toString() из BigInteger . Тонкое отличие использования метода toString() заключается в том, что вывод не дополняется ведущими нулями :

public String encodeUsingBigIntegerToString(byte[] bytes) {
BigInteger bigInteger = new BigInteger(1, bytes);
return bigInteger.toString(16);
}

Теперь давайте посмотрим на преобразование шестнадцатеричной строки в байтовый массив:

public byte[] decodeUsingBigInteger(String hexString) {
byte[] byteArray = new BigInteger(hexString, 16)
.toByteArray();
if (byteArray[0] == 0) {
byte[] output = new byte[byteArray.length - 1];
System.arraycopy(
byteArray, 1, output,
0, output.length);
return output;
}
return byteArray;
}

Метод toByteArray() создает дополнительный знаковый бит . Мы написали специальный код для обработки этого дополнительного бита.

Следовательно, мы должны знать об этих деталях, прежде чем использовать класс BigInteger для преобразования.

5. Использование класса DataTypeConverter

Класс DataTypeConverter поставляется с библиотекой JAXB. Это часть стандартной библиотеки до Java 8. Начиная с Java 9, нам нужно явно добавить модуль java.xml.bind в среду выполнения.

Давайте посмотрим на реализацию с использованием класса DataTypeConverter :

public String encodeUsingDataTypeConverter(byte[] bytes) {
return DatatypeConverter.printHexBinary(bytes);
}

public byte[] decodeUsingDataTypeConverter(String hexString) {
return DatatypeConverter.parseHexBinary(hexString);
}

Как показано выше, очень удобно использовать класс DataTypeConverter . Вывод метода printHexBinary() всегда в верхнем регистре . Этот класс предоставляет набор методов печати и синтаксического анализа для преобразования типа данных.

Прежде чем выбрать этот подход, нам нужно убедиться, что класс будет доступен во время выполнения.

6. Использование библиотеки Apache Commons-Codec

Мы можем использовать класс Hex , поставляемый с библиотекой Apache commons-codec:

public String encodeUsingApacheCommons(byte[] bytes) 
throws DecoderException {
return Hex.encodeHexString(bytes);
}

public byte[] decodeUsingApacheCommons(String hexString)
throws DecoderException {
return Hex.decodeHex(hexString);
}

Вывод encodeHexString всегда в нижнем регистре .

7. Использование библиотеки Google Guava

Давайте посмотрим, как можно использовать класс BaseEncoding для кодирования и декодирования массива байтов в шестнадцатеричную строку:

public String encodeUsingGuava(byte[] bytes) {
return BaseEncoding.base16().encode(bytes);
}

public byte[] decodeUsingGuava(String hexString) {
return BaseEncoding.base16()
.decode(hexString.toUpperCase());
}

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

8. Заключение

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

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

Наконец, полный исходный код этого руководства доступен на GitHub .