Convierte un Array de Bytes a una Cadena Hexadecimal en Java

Cómo Convertir un Array de Bytes a una Cadena Hexadecimal en Java

1. Overview

En este tutorial, vamos a explorar diferentes maneras de convertir un array de bytes a una cadena hexadecimal, así como hacer la conversión inversa. Además, entenderemos el mecanismo de conversión y desarrollaremos nuestra propia implementación para lograrlo.

La conversión entre bytes y representaciones hexadecimales es un procedimento común en la programación, especialmente en aplicaciones que involucran criptografía, redes y procesamiento de archivos binarios. Java proporciona varias formas de llevar a cabo estas conversiones, lo que lo convierte en un tema esencial para cualquier programador que trabaje con datos a nivel bajo.

2. Convirtiendo entre Byte y Hexadecimal

Primero, examinemos la lógica detrás de la conversión entre números en byte y hexadecimal.

2.1. Byte a Hexadecimal

Los bytes son enteros con signo de 8 bits en Java. Por lo tanto, necesitamos convertir cada segmento de 4 bits a hexadecimal por separado y concatenarlos. Como resultado, obtendremos dos caracteres hexadecimales tras la conversión.

Por ejemplo, podemos escribir 45 como 0010 1101 en binario, y el equivalente hexadecimal será “2d”:

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

Por lo tanto: 45 = 0010 1101 = 0x2d

Ahora implementemos esta lógica simple en 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);
}

Entendamos el código paso a paso. Primero, creamos un array de caracteres de longitud 2 para almacenar el resultado:

char[] hexDigits = new char[2];

Luego, aislamos los bits de mayor orden aplicando un desplazamiento a la derecha de 4 bits, y usamos una máscara para obtener los 4 bits de menor orden. La enmascarado es necesario porque los números negativos están representados de manera interna como el complemento a dos del número positivo:

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

Después convertimos los 4 bits restantes a hexadecimal:

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

Finalmente, creamos un objeto de String a partir del array de caracteres y lo retornamos como resultado de la conversión hexadecimal.

Ahora, consideremos cómo funcionará esto para un byte negativo, por ejemplo, el byte -4:

hexDigits[0]:
1111 1100 >> 4 = 1111 (equivalente a f en hexadecimal)

De modo que al final:

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

2.2. Hexadecimal a Byte

Ahora, vamos a convertir un dígito hexadecimal a byte. Como sabemos, un byte contiene 8 bits; por lo tanto, necesitamos dos dígitos hexadecimales para crear un byte.

Primero, convertimos cada dígito hexadecimal a su equivalente binario de forma separada. Luego, concatenamos los segmentos de cuatro bits para obtener el byte equivalente:

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

Por lo tanto: 2d = 0010 1101 (base 2) = 45

Ahora implementaremos la operación en 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("Carácter hexadecimal inválido: " + hexChar);
    }
    return digit;
}

Primero, convertimos los caracteres hexadecimales en enteros:

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

Luego, desplazamos a la izquierda el dígito más significativo 4 bits:

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

El método toDigit() convierte a un entero y lanza una excepción si se pasa un carácter no válido.

3. Convirtiendo entre Arrays de Bytes y Cadenas Hexadecimales

Ahora que sabemos cómo convertir un byte a hexadecimal y viceversa, escalaremos nuestro algoritmo para convertir un array de bytes de/ a una cadena hexadecimal.

3.1. Array de Bytes a Cadena Hexadecimal

Necesitamos recorrer el array y generar pares hexadecimales para cada byte:

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

Como hemos mencionado, la salida siempre será en minúsculas.

3.2. Cadena Hexadecimal a Array de Bytes

Primero, debemos verificar si la longitud de la cadena hexadecimal es un número par. Esto es porque una cadena hexadecimal con longitud impar resultará en una representación de byte incorrecta.

Ahora, iteraremos a través de la cadena y convertiremos cada par hexadecimal a un byte:

public byte[] decodeHexString(String hexString) {
    if (hexString.length() % 2 == 1) {
        throw new IllegalArgumentException("Cadena hexadecimal no válida.");
    }

    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. Usando la Clase BigInteger

Podemos crear un objeto de tipo BigInteger pasando un signo y un array de bytes. Posteriormente, podemos generar la cadena hexadecimal con la ayuda de un método estático en la clase String:

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

El formato proporcionado generará una cadena hexadecimal en minúsculas con ceros a la izquierda. También podemos generar una cadena en mayúsculas reemplazando “x” con “X”.

Alternativamente, podríamos haber usado el método toString() de BigInteger. La sutil diferencia de usar el método toString() es que la salida no está padded con ceros a la izquierda:

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

Ahora, echemos un vistazo a la conversión de cadena hexadecimal a array de bytes:

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

El método toByteArray() produce un bit de signo adicional. Hemos escrito código específico para manejar este bit adicional.

5. Usando la Clase DataTypeConverter

La clase DataTypeConverter es suministrada con la biblioteca JAXB. Esta es parte de la biblioteca estándar hasta Java 8. A partir de Java 9, necesitamos añadir el módulo java.xml.bind al entorno de ejecución de forma explícita.

Veamos la implementación utilizando la clase DataTypeConverter:

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

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

Es muy conveniente usar la clase DataTypeConverter. La salida del método printHexBinary() está siempre en mayúsculas. Esta clase proporciona un conjunto de métodos para imprimir y analizar conversiones de tipo de datos.

Antes de optar por este enfoque, debemos asegurarnos de que la clase estará disponible en tiempo de ejecución.

6. Usando la Biblioteca Commons-Codec de Apache

Podemos usar la clase Hex que proporciona la biblioteca 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);
}

La salida de encodeHexString está siempre en minúsculas.

7. Usando la Biblioteca Guava de Google

Veamos cómo la clase BaseEncoding puede ser utilizada para codificar y decodificar un array de bytes a la cadena hexadecimal:

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

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

La clase BaseEncoding codifica y decodifica usando caracteres en mayúsculas por defecto. Si necesitamos usar caracteres en minúsculas, deberíamos crear una nueva instancia de codificación usando el método estático lowercase().

8. Usando HexFormat de Java 17

Además de los métodos discutidos anteriormente para convertir entre arrays de bytes y cadenas hexadecimales, Java 17 introdujo una alternativa conveniente a través de la clase java.util.HexFormat. La clase HexFormat proporciona una manera estandarizada de realizar operaciones de codificación y decodificación hexadecimal, ofreciendo una solución más robusta y concisa en comparación con las implementaciones personalizadas.

8.1. Array de Bytes a Cadena Hexadecimal

Para convertir un array de bytes a una cadena hexadecimal usando HexFormat, simplemente podemos usar el método HexFormat.of().formatHex():

public String encodeUsingHexFormat(byte[] bytes) {
    HexFormat hexFormat = HexFormat.of();
    return hexFormat.formatHex(bytes);
}

8.2. Cadena Hexadecimal a Array de Bytes

De manera similar, para convertir una cadena hexadecimal a un array de bytes, podemos utilizar el método HexFormat.of().parseHex():

public byte[] decodeUsingHexFormat(String hexString) {
    HexFormat hexFormat = HexFormat.of();
    return hexFormat.parseHex(hexString);
}

9. Conclusion

En este artículo, aprendimos el algoritmo de conversión entre un array de bytes y una cadena hexadecimal. También discutimos diversos métodos para codificar arrays de bytes en cadenas hexadecimales y viceversa.

No se recomienda añadir una biblioteca para utilizar solo un par de métodos de utilidad. Por lo tanto, si no estamos utilizando ya las bibliotecas externas, deberíamos optar por el algoritmo que hemos discutido. La clase DataTypeConverter es otro método efectivo para codificar/decodificar entre varios tipos de datos.

Con el conocimiento adquirido, ahora puedes implementar y utilizar conversiones de bytes a hexadecimales en tus aplicaciones Java con facilidad. ¡Feliz codificación!