8

I have the following method for my android program, written in Java.

The method takes in a string of hex and returns a string of the same text written in ascii.

public static String hexToString(String hex)
{
    StringBuilder sb = new StringBuilder();

    for (int count = 0; count < hex.length() - 1; count += 2)
    {
        String output = hex.substring(count, (count + 2));    //grab the hex in pairs

        int decimal = Integer.parseInt(output, 16);    //convert hex to decimal

        sb.append((char)decimal);    //convert the decimal to character
    }

    return sb.toString();
}

The method works fine, however my program is very time critical and this method is potentially being called tens of thousands of times. When analysing the slow bits of my program, this method takes up far too much time because of:

Integer.parseInt(output, 16);

and

hex.substring(count, (count + 2));

In the order of slowest first.

Does anyone know of a faster method of achieving the same thing?

Pippa Rose Smith
  • 1,377
  • 5
  • 18
  • 42

4 Answers4

9

Don't create a new String on each iteration. One way to improve performance would be using a char array and applying math operations per character.

public static String hexToString(String hex) {
    StringBuilder sb = new StringBuilder();
    char[] hexData = hex.toCharArray();
    for (int count = 0; count < hexData.length - 1; count += 2) {
        int firstDigit = Character.digit(hexData[count], 16);
        int lastDigit = Character.digit(hexData[count + 1], 16);
        int decimal = firstDigit * 16 + lastDigit;
        sb.append((char)decimal);
    }
    return sb.toString();
}

More info about this method:

Also, if you're parsing the hex string in pairs, you can use a look up table as @L7ColWinters suggests:

private static final Map<String, Character> lookupHex = new HashMap<String, Character>();

static {
    for(int i = 0; i < 256; i++) {
        String key = Integer.toHexString(i);
        Character value = (char)(Integer.parseInt(key, 16));
        lookupHex.put(key, value);
    }
}

public static String hexToString(String hex) {
    StringBuilder sb = new StringBuilder();
    for (int count = 0; count < hex.length() - 1; count += 2) {
        String output = hex.substring(count, (count + 2));
        sb.append((char)lookupHex.get(output));
    }
    return sb.toString();
}
Luiggi Mendoza
  • 85,076
  • 16
  • 154
  • 332
  • This one is a lot faster, averages around `0.026819ms` while authors averages around `0.172089ms`. But, instead of `Integer.parseInt(..)` which expects a String as a 1st parameter, use `Character.digit(..)`. – nullpotent Aug 20 '12 at 14:49
  • Do you mean the top one or the lookup table bottom one is the fastest? – Pippa Rose Smith Aug 20 '12 at 14:54
  • @PippaRoseSmith it would be better if you do the benchmark between all the methods posted here. Also, to measure the time correctly, use [System#nanoTime](http://docs.oracle.com/javase/6/docs/api/java/lang/System.html#nanoTime()). An explanation of this advice: [System.currentTimeMillis vs System.nanoTime](http://stackoverflow.com/questions/351565/system-currenttimemillis-vs-system-nanotime) – Luiggi Mendoza Aug 20 '12 at 15:09
  • @PippaRoseSmith how many tests are you running to measure the effectiveness of the codes? – Luiggi Mendoza Aug 20 '12 at 15:23
  • @LuiggiMendoza `lookupHex.get(hex)` should be `lookupHex.get(output)`. – nullpotent Aug 20 '12 at 15:32
  • @iccthedral You're right again. I' kinda sleepy and didn't tested the code itself - I have no Java editor in the PC I'm using right now :( -. – Luiggi Mendoza Aug 20 '12 at 15:41
  • These are great methods! The top one doesn't run much faster than mine but the second one does slightly, so I'm going to upgrade to that. – Pippa Rose Smith Aug 20 '12 at 15:45
  • I'm using a profiler to see where most of the time is being spent when running it. – Pippa Rose Smith Aug 20 '12 at 15:46
  • The HashMap approach as shown doesn't work at all for hex values 00 01 ... 0f because it creates mappings for 0 ... f instead. Use String.format ("%02x", i) instead of toHexString(). After that it only works for input hex using lowercase a ... f not uppercase; it's not hard to add 1F F1 and FF entries, but Ff and fF is trickier. The Character.digit approach handles both cases, the c>='A' approach can easily be changed to do so, and both are twice as fast on my systems. – dave_thompson_085 Mar 14 '14 at 00:05
3

What about this one...

public static String hexToString(final String str) {
 return new String(new BigInteger(str, 16).toByteArray());
}
nullpotent
  • 9,162
  • 1
  • 31
  • 42
  • That method doesn't produce the same result on a hex string as my original method... does it take into account 01 instead of 1 and things like that? – Pippa Rose Smith Aug 20 '12 at 14:36
  • On what input? On `"6162636465"` they both give the `abcde`. Anyhow, I profiled my method and it took a lot longer than yours did. I'll leave my answer, someone might consider it. – nullpotent Aug 20 '12 at 14:42
1

Another alternative would be to just do some simple arithmetics:

public static int hexCharToInt(char c)
{
    int result = 0;
    if(c >= 'A' && c <= 'F')
    {
        result += (c - 'A' + 10);
    }
    else if( c >= '0' && c <= '9')
    {
            result += (c - '0');
    }
        return result;
    }

public static String hexToString(String hex)
{
    StringBuilder sb = new StringBuilder();

    for (int count = 0; count < hex.length() - 1; count += 2)
    {
        char c1 = hex.charAt(count);
        char c2 = hex.charAt(count + 1);

        int decimal = hexCharToInt(c1) * 16 + hexCharToInt(c2);

        sb.append((char)decimal);    //convert the decimal to character
    }

    return sb.toString();
}

Try it out and see which solution that works best on your system!

ekholm
  • 2,543
  • 18
  • 18
  • In this case, it would result even faster if you use a look up to store the char/int key/value pair. – Luiggi Mendoza Aug 20 '12 at 15:10
  • @Luiggi Mendoza: Are you sure? Maybe a lookup in form of a simple array would be slightly faster, but with such few values in a map, I think it is hard to predict which way will be fastest. I think the sum of all answers here give a pretty good idea of what to try and then I guess some measurements on the target system will show the best way to go. – ekholm Aug 20 '12 at 16:14
1

This code was taken from Hex class of Apache Commons Codec and simplified a bit. (Removed some range checks etc. which is not necessary for understanding here. In practice you want to use the original implementation.)

/**
 * Converts an array of characters representing hexadecimal values into an array of bytes of those same values. The
 * returned array will be half the length of the passed array, as it takes two characters to represent any given
 * byte. An exception is thrown if the passed char array has an odd number of elements.
 * 
 * @param data
 *            An array of characters containing hexadecimal digits
 * @return A byte array containing binary data decoded from the supplied char array.
 * @throws DecoderException
 *             Thrown if an odd number or illegal of characters is supplied
 */
public static byte[] decodeHex(char[] data) throws DecoderException {

    int len = data.length;

    byte[] out = new byte[len >> 1];

    // two characters form the hex value.
    for (int i = 0, j = 0; j < len; i++) {
        int f = Character.digit(data[j], 16) << 4;
        j++;
        f = f | Character.digit(data[j], 16);
        j++;
        out[i] = (byte) (f & 0xFF);
    }

    return out;
}

You can use the returned byte[] to construct a String object afterwards.

So when using Apache Commons Codec then your method looks like this:

public static String hexToString(String hex) throws UnsupportedEncodingException, DecoderException {
    return new String(Hex.decodeHex(hex.toCharArray()), "US-ASCII");
  }
Fabian Barney
  • 14,219
  • 5
  • 40
  • 60
  • The only problem here is that OP is sending the hex string in substrings of 2 chars. – Luiggi Mendoza Aug 21 '12 at 17:19
  • @LuiggiMendoza Sry, don't get it. What's the problem? What char sequence works with yours or OPs code but not with "mine"? – Fabian Barney Aug 21 '12 at 18:08
  • @LuiggiMendoza My tests with your two methods and the original Apache Codec method are producing the same result where your first method takes about 8,900 millis, your 2nd method takes about 21,100 millis and Apache's method takes about 6,700 millis for decoding a 1182 chars long hex string resulting in 591 chars long decoded ASCII string each done a million times. So Apache's method is much faster than yours. – Fabian Barney Aug 21 '12 at 18:50