It's likely that at some point in time you'll need to read a file full of little-endian data, especially if you're working on Intel hardware or with data written by native code on such a platform. Java has essentially no support for little-endian numbers. The LittleEndianOutputStream class in Example 8-5 and the LittleEndianInputStream class in Example 8-6 provide the support you need to do this. These classes are closely modeled on the java.io.DataInputStream and java.io.DataOutputStream classes. Some of the methods in these classes do exactly the same thing as the same methods in the DataInputStream and DataOutputStream classes. After all, a big-endian byte is no different from a little-endian byte. In fact, these two classes come very close to implementing the java.io.DataInput and java.io.DataOutput interfaces. Actually doing so would have been a bad idea, however, because client programmers expect objects implementing DataInput and DataOutput to use big-endian numbers, and it's best not to go against such common assumptions.
Example 8-5. The LittleEndianOutputStream class
package com.elharo.io; import java.io.*; public class LittleEndianOutputStream extends FilterOutputStream { protected int written; public LittleEndianOutputStream(OutputStream out) { super(out); } public void write(int b) throws IOException { out.write(b); written++; } public void write(byte[] data, int offset, int length) throws IOException { out.write(data, offset, length); written += length; } public void writeBoolean(boolean b) throws IOException { if (b) this.write(1); else this.write(0); } public void writeByte(int b) throws IOException { out.write(b); written++; } public void writeShort(int s) throws IOException { out.write(s & 0xFF); out.write((s >>> 8) & 0xFF); written += 2; } public void writeChar(int c) throws IOException { out.write(c & 0xFF); out.write((c >>> 8) & 0xFF); written += 2; } public void writeInt(int i) throws IOException { out.write(i & 0xFF); out.write((i >>> 8) & 0xFF); out.write((i >>> 16) & 0xFF); out.write((i >>> 24) & 0xFF); written += 4; } public void writeLong(long l) throws IOException { out.write((int) l & 0xFF); out.write((int) (l >>> 8) & 0xFF); out.write((int) (l >>> 16) & 0xFF); out.write((int) (l >>> 24) & 0xFF); out.write((int) (l >>> 32) & 0xFF); out.write((int) (l >>> 40) & 0xFF); out.write((int) (l >>> 48) & 0xFF); out.write((int) (l >>> 56) & 0xFF); written += 8; } public final void writeFloat(float f) throws IOException { this.writeInt(Float.floatToIntBits(f)); } public final void writeDouble(double d) throws IOException { this.writeLong(Double.doubleToLongBits(d)); } public void writeBytes(String s) throws IOException { int length = s.length( ); for (int i = 0; i < length; i++) { out.write((byte) s.charAt(i)); } written += length; } public void writeChars(String s) throws IOException { int length = s.length( ); for (int i = 0; i < length; i++) { int c = s.charAt(i); out.write(c & 0xFF); out.write((c >>> 8) & 0xFF); } written += length * 2; } public void writeUTF(String s) throws IOException { int numchars = s.length( ); int numbytes = 0; for (int i = 0 ; i < numchars ; i++) { int c = s.charAt(i); if ((c >= 0x0001) && (c <= 0x007F)) numbytes++; else if (c > 0x07FF) numbytes += 3; else numbytes += 2; } if (numbytes > 65535) throw new UTFDataFormatException( ); out.write((numbytes >>> 8) & 0xFF); out.write(numbytes & 0xFF); for (int i = 0 ; i < numchars ; i++) { int c = s.charAt(i); if ((c >= 0x0001) && (c <= 0x007F)) { out.write(c); } else if (c > 0x07FF) { out.write(0xE0 | ((c >> 12) & 0x0F)); out.write(0x80 | ((c >> 6) & 0x3F)); out.write(0x80 | (c & 0x3F)); written += 2; } else { out.write(0xC0 | ((c >> 6) & 0x1F)); out.write(0x80 | (c & 0x3F)); written += 1; } } written += numchars + 2; } public int size( ) { return this.written; } } |
Notice how all writing is done by passing byte values to the underlying output stream out (set in the constructor and inherited from the superclass, FilterOutputStream). The primary purpose of these methods is to convert the Java data type to bytes and then write them in a little-endian order. In general, the conversions are accomplished by shifting the bits of interest into the low-order eight bits and then masking it off. For example, consider the writeInt( ) method:
public void writeInt(int i) throws IOException { out.write(i & 0xFF); out.write((i >>> 8) & 0xFF); out.write((i >>> 16) & 0xFF); out.write((i >>> 24) & 0xFF); written += 4; }
A Java int is composed of four bytes in big-endian order. Thus, the low-order byte is in the last eight bits. This byte needs to be written first in a little-endian scheme. The mask 0xFF has one bit in the low-order eight bits and zero bits everywhere else. By bitwise ANDing 0xFF with i, we select the low-order eight bits of i. The second-lowest order bytethat is, bits 16 to 23is selected by first shifting the bits right without sign extension into the low-order bits. That's the purpose of (i >>> 8). Then this byte can be retrieved with the same 0xFF mask used before. The same is done for the second-to-lowest-order byte and the highest-order byte. Here, however, it's necessary to shift by 16 and 24 bits, respectively.
floats and doubles are written by first converting them to ints and longs using Float.floatToIntBits( ) and Double.doubleTolongBits( ) and then invoking writeInt( ) or writeLong( ) to write those bits in little-endian order.
Each method increments the protected field written by the number of bytes actually written. This tracks the total number of bytes written onto the output stream at any one time.
Example 8-6 shows the corresponding LittleEndianInputStream class, based on the DataInputStream class.
Example 8-6. The LittleEndianInputStream class
package com.elharo.io; import java.io.*; public class LittleEndianInputStream extends FilterInputStream { public LittleEndianInputStream(InputStream in) { super(in); } public boolean readBoolean( ) throws IOException { int bool = in.read( ); if (bool == -1) throw new EOFException( ); return (bool != 0); } public byte readByte(int b) throws IOException { int temp = in.read( ); if (temp == -1) throw new EOFException( ); return (byte) temp; } public int readUnsignedByte( ) throws IOException { int temp = in.read( ); if (temp == -1) throw new EOFException( ); return temp; } public short readShort( ) throws IOException { int byte1 = in.read( ); int byte2 = in.read( ); // only need to test last byte read // if byte1 is -1 so is byte2 if (byte2 == -1) throw new EOFException( ); return (short) (((byte2 << 24) >>> 16) + (byte1 << 24) >>> 24); } public int readUnsignedShort( ) throws IOException { int byte1 = in.read( ); int byte2 = in.read( ); if (byte2 == -1) throw new EOFException( ); return ((byte2 << 24) >> 16) + ((byte1 << 24) >> 24); } public char readChar( ) throws IOException { int byte1 = in.read( ); int byte2 = in.read( ); if (byte2 == -1) throw new EOFException( ); return (char) (((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24)); } public int readInt( ) throws IOException { int byte1 = in.read( ); int byte2 = in.read( ); int byte3 = in.read( ); int byte4 = in.read( ); if (byte4 == -1) { throw new EOFException( ); } return (byte4 << 24) + ((byte3 << 24) >>> 8) + ((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24); } public long readLong( ) throws IOException { long byte1 = in.read( ); long byte2 = in.read( ); long byte3 = in.read( ); long byte4 = in.read( ); long byte5 = in.read( ); long byte6 = in.read( ); long byte7 = in.read( ); long byte8 = in.read( ); if (byte8 == -1) { throw new EOFException( ); } return (byte8 << 56) + ((byte7 << 56) >>> 8) + ((byte6 << 56) >>> 16) + ((byte5 << 56) >>> 24) + ((byte4 << 56) >>> 32) + ((byte3 << 56) >>> 40) + ((byte2 << 56) >>> 48) + ((byte1 << 56) >>> 56); } public String readUTF( ) throws IOException { int byte1 = in.read( ); int byte2 = in.read( ); if (byte2 == -1) throw new EOFException( ); int numbytes = (byte1 << 8) + byte2; char result[] = new char[numbytes]; int numread = 0; int numchars = 0; while (numread < numbytes) { int c1 = readUnsignedByte( ); // The first 4 bits of c1 determine how many bytes are in this char int test = c1 >> 4; if (test < 8) { // one byte numread++; result[numchars++] = (char) c1; } else if (test == 12 || test == 13) { // 2 bytes numread += 2; if (numread > numbytes) throw new UTFDataFormatException( ); int c2 = readUnsignedByte( ); if ((c2 & 0xC0) != 0x80) throw new UTFDataFormatException( ); result[numchars++] = (char) (((c1 & 0x1F) << 6) | (c2 & 0x3F)); } else if (test == 14) { // three bytes numread += 3; if (numread > numbytes) throw new UTFDataFormatException( ); int c2 = readUnsignedByte( ); int c3 = readUnsignedByte( ); if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) { throw new UTFDataFormatException( ); } result[numchars++] = (char) (((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F)); } else { // malformed throw new UTFDataFormatException( ); } } // end while return new String(result, 0, numchars); } public final double readDouble( ) throws IOException { return Double.longBitsToDouble(this.readLong( )); } public final float readFloat( ) throws IOException { return Float.intBitsToFloat(this.readInt( )); } public final int skipBytes(int n) throws IOException { for (int i = 0; i < n; i += (int) skip(n - i)); return n; } } |
This class is used later in this chapter to view files containing little-endian numbers.
Basic I/O
Introducing I/O
Output Streams
Input Streams
Data Sources
File Streams
Network Streams
Filter Streams
Filter Streams
Print Streams
Data Streams
Streams in Memory
Compressing Streams
JAR Archives
Cryptographic Streams
Object Serialization
New I/O
Buffers
Channels
Nonblocking I/O
The File System
Working with Files
File Dialogs and Choosers
Text
Character Sets and Unicode
Readers and Writers
Formatted I/O with java.text
Devices
The Java Communications API
USB
The J2ME Generic Connection Framework
Bluetooth
Character Sets