Рубрики
Без рубрики

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

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

Автор оригинала: baeldung.

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)

Также стоит отметить, что символ . 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);

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

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

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

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

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 с помощью статического формата метода, определенного в классе 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);
}

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

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 class. Вывод метода printexbinary() всегда находится в верхнем регистре . Этот класс предоставляет набор методов печати и анализа для преобразования типов данных.

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

6. Использование библиотеки общих кодеков Apache

Мы можем использовать класс 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

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

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

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

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

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

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

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

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