1. Обзор

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

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

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

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

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

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

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

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

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

Therefore: 45 = 0010 1101 = 0x2d

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

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

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

char[] hexDigits = new char[2];

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

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

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

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

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

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

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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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. Шестнадцатеричная строка в массив байтов

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

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

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

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

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

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

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

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

В качестве альтернативы мы могли бы использовать метод 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.

«