Skip to main content
2 of 2
Apparently decompression was causing confusion; so I added a section on how it works. File is not AutoCloseable but Scanner is.
mdfst13
  • 21.7k
  • 6
  • 33
  • 68

StringBuilder

            str = "" + ch;

If you find yourself doing a lot of string addition, consider using a StringBuilder (or StringBuffer if you need thread support).

            builder.setLength(0);
            builder.append(ch);

This saves creating a new string object with each new character.

                if (dictionary.containsKey(str + ch)) {
                    str = str + ch;
                } else {

becomes

                builder.append(ch);
                if (!dictionary.containsKey(builder.toString())) {

which simplifies things.

Use bitwise operations

                    String s12 = to12bit(dictionary.get(str));
                    // Store the 12 bits into an array and then write it to the
                    // output file
                    if (onleft) {
                        buffer[0] = (byte) Integer.parseInt(
                                s12.substring(0, 8), 2);
                        buffer[1] = (byte) Integer.parseInt(
                                s12.substring(8, 12) + "0000", 2);
                    } else {
                        buffer[1] += (byte) Integer.parseInt(
                                s12.substring(0, 4), 2);
                        buffer[2] = (byte) Integer.parseInt(
                                s12.substring(4, 12), 2);
                        for (int b = 0; b < buffer.length; b++) {
                            out.writeByte(buffer[b]);
                            buffer[b] = 0;
                        }
                    }

This is clever but more complicated than is necessary.

                    int compressed = dictionary.get(str));

                    // Store the 12 bits into an array and then write it to the
                    // output file
                    if (onleft) {
                        buffer[0] = (byte) (compressed & 0xff);

This only includes the last eight bits of the compressed valued. Note that 0xff is the same as binary 11111111. So you AND compressed and 0xff which leaves all of the last eight bits that were set still set. But it zeroes out bits outside that area and leaves cleared those bits that were already cleared.

                        buffer[1] = (byte) ((compressed >> 8) << 4);

We right shift eight bits, which clears those bits (what we put in buffer[0]). Then we left shift four bits, which has the same effect as appending "0000" does in the original code. This relies on compressed never being greater than or equal to 4096. Otherwise the conversion to byte will drop some of the information.

                    } else {
                        buffer[1] += (byte) (compressed & 0xf);

This masks out everything but the last four bits.

                        buffer[2] = (byte) (compressed >> 4);

Remove the four bits that we put in buffer[1] and put the rest in buffer[2]. Again, this relies on compressed never being greater than or equal to 4096. Otherwise the conversion to byte will drop some of the information.

                        for (int b = 0; b < buffer.length; b++) {
                            out.writeByte(buffer[b]);
                            buffer[b] = 0;
                        }
                    }

By using the bitwise operators, we save the entire to12bit method. We also avoid creating a String just so that we could use substring.

You also may want to put this into its own method. Then when you do it again in the catch block, you could just call the method.

Decompression

Undoing this is more complicated. Because byte is a signed type in Java, we have to promote it to int and mask off the excess bits to make the shifts work right. E.g. ((int) b & 255) will produce the correct eight bit pattern for a byte b.

public int getvalue(byte b1, byte b2, boolean onleft) {
    int value;

    if (onleft) {
        value = ((int) b1 & 0xFF) + ((((int) b2 & 0xFF) >> 4) << 8);
    } else {
        value = ((int) b1 & 0xF) + (((int) b2 & 0xFF) << 4);
    }

    return value;
}

With the second byte, we rotate right four places to clear the four least significant bits and then left eight to align it properly with the other byte. Then for the other half, we can just mask off all but the four least significant bits and add to the third byte shifted four places to the left.

We have to cast to int and mask each value so that it doesn't get treated as a negative number.

I'm not crazy about the name getvalue, which I would expect to be a getter. But I kept it for consistency's sake. Consider changing to something like toIntValue instead, which better reflects what it actually does.

HashMap tracks its own size

                        dictionary.put(str + ch, dictSize++);

You don't need a dictSize variable. You could just say

                        dictionary.put(str + ch, dictionary.size());

This saves managing the dictSize variable.

catch EOFException

Your code runs forever until it encounters an IOException. Then it assumes that the IOException is an EOFException and writes out whatever is waiting to be written. Instead, consider catching just the EOFException. Then an IOException will crash the program. Which is what an IOException thrown in the catch block would do anyway.

Use buffered I/O

You are using RandomAccessFile, which works but is unnecessary. You only do sequential operations. You don't use the random access capability at all. You could just use a buffered I/O method.

You could even write your own wrapper for it. Then you could say something like

            out.write12bits(compressed);

and let your wrapper handle the details.

Use descriptive variable names if you can

Some variables don't have descriptive names that make sense, so we just call them string or something like that. But some do.

            String str = input.nextLine();

In this case, the String represents a file name. So call it that.

            String filename = input.nextLine();

Use try with resources

        try {
            LZWCompression lzw = new LZWCompression();
            Scanner input = new Scanner(System.in);
            System.out.println("Enter the name of your (input.txt) file.");
            String str = input.nextLine();
            File file = new File(str);
            Scanner fileScanner = new Scanner(file);
            String line = "";
            while (fileScanner.hasNext()) {
                line = fileScanner.nextLine();
                System.out.println("Contents of your file being compressed: \n"
                        + line);
            }
            lzw.compress(str);
            System.out.println("\nCompression of your file is complete!");
            System.out.println("Your new file is named: " + str.concat(".lzw"));

But the early part of this can't throw a FileNotFoundException, so it doesn't need to be in the try block.

        LZWCompression lzw = new LZWCompression();

        Scanner input = new Scanner(System.in);
        System.out.println("Enter the name of your (input.txt) file.");
        String filename = input.nextLine();

        try (Scanner = new Scanner(new File(filename))) {
            

            while (fileScanner.hasNext()) {
                String line = fileScanner.nextLine();
                System.out.println("Contents of your file being compressed: \n"
                        + line);
            }

            lzw.compress(filename);

            System.out.println("\nCompression of your file is complete!");
            System.out.println("Your new file is named: " + filename.concat(".lzw"));

Now the fileScanner will be managed by the try statement.

Confirm what we should know

        } catch (FileNotFoundException e) {
            System.out.println("File was not found!");
        }

Which file was not found?

        } catch (FileNotFoundException e) {
            System.out.println("File '" + filename + "' was not found!");
        }

Now we know what the program thought it wanted to find.

mdfst13
  • 21.7k
  • 6
  • 33
  • 68