首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java IO 數據流:DataInputStream 基本型別二進制讀取完整指南

     編輯:Java 相關知識

核心定位:機器無關的基本型別二進制讀取

DataInputStream 實作 DataInput 介面,繼承自 FilterInputStream。它包裝任意 InputStream,提供基本型別的固定長度、大端序二進制讀取,配合 DataOutputStream 實現跨平台二進制資料交換。核心特性:大端序讀取、修改版 UTF-8 解碼、readFully 保證讀滿、拋出 IOException/EOFException。

基本型別讀取對應表

方法位元組數回傳型別解碼規則
readBoolean()1boolean非 0 為 true
readByte()1byte有號位元組
readUnsignedByte()1int (0-255)無號位元組
readShort()2short大端序有號
readUnsignedShort()2int (0-65535)大端序無號
readChar()2char大端序 Unicode 碼點
readInt()4int大端序有號
readLong()8long大端序有號
readFloat()4floatreadInt → Float.intBitsToFloat
readDouble()8doublereadLong → Double.longBitsToDouble
readUTF()2+長度String修改版 UTF-8 解碼

大端序讀取核心邏輯


// readInt() 範例:從 4 位元組重建 int
public final int readInt() throws IOException {
    int ch1 = in.read();  // 高 24-31 位
    int ch2 = in.read();  // 高 16-23 位
    int ch3 = in.read();  // 高 8-15 位
    int ch4 = in.read();  // 低 0-7 位
    if ((ch1 | ch2 | ch3 | ch4) < 0) throw new EOFException();
    return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
}

// readLong() 使用 8 位元組暫存陣列
private byte readBuffer[] = new byte[8];
public final long readLong() throws IOException {
    readFully(readBuffer, 0, 8);
    return (((long)readBuffer[0] << 56) +
            ((long)(readBuffer[1] & 255) << 48) +
            ((long)(readBuffer[2] & 255) << 40) +
            ((long)(readBuffer[3] & 255) << 32) +
            ((long)(readBuffer[4] & 255) << 24) +
            ((readBuffer[5] & 255) << 16) +
            ((readBuffer[6] & 255) << 8) +
            ((readBuffer[7] & 255) << 0));
}

readFully:保證讀滿的關鍵方法


// 讀滿位元組陣列,否則拋出 EOFException
public final void readFully(byte[] b) throws IOException {
    readFully(b, 0, b.length);
}

public final void readFully(byte[] b, int off, int len) throws IOException {
    if (len < 0) throw new IndexOutOfBoundsException();
    int n = 0;
    while (n < len) {
        int count = in.read(b, off + n, len - n);
        if (count < 0) throw new EOFException();
        n += count;
    }
}

關鍵差異:InputStream.read() 可能讀不滿(返回實際讀取數),readFully() 循環讀取直到滿足長度或遇到 EOF,適合結構化二進制解析。

readUTF 修改版 UTF-8 解碼完整邏輯

解碼流程

  1. 讀取無符號短整數 utflen(2 位元組)作為資料長度
  2. 分配/複用位元組緩衝區 bytearr 和字元緩衝區 chararr
  3. readFully(bytearr, 0, utflen) 讀滿資料位元組
  4. 掃描 bytearr 解碼為 chararr:
    • 單位元組 (0x00-0x7F,但 0x00 特殊處理):直接轉字元
    • 雙位元組 (0xC0-0xDF 開頭):((b1&0x1F)<<6) | (b2&0x3F)
    • 三位元組 (0xE0 開頭):((b1&0x0F)<<12) | ((b2&0x3F)<<6) | (b3&0x3F)
    • 補字元:以 Surrogate Pair 形式編碼,解碼為兩個 char
  5. 返回 new String(chararr, 0, chararr_count)

關鍵位運算解析


// 取首位元組高 4 位判斷 UTF-8 序列長度
int c = bytearr[count] & 0xFF;  // 轉無號 0-255
switch (c >> 4) {
    case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
        // 0xxxxxxx (0-127) → 1 位元組
        chararr[chararr_count++] = (char) c;
        count++;
        break;
    case 12: case 13:
        // 110xxxxx (192-223) → 2 位元組
        // 驗證第 2 位元組為 10xxxxxx
        char2 = bytearr[count+1] & 0xFF;
        if ((char2 & 0xC0) != 0x80) throw UTFDataFormatException;
        chararr[chararr_count++] = (char)(((c & 0x1F) << 6) | (char2 & 0x3F));
        count += 2;
        break;
    case 14:
        // 1110xxxx (224-239) → 3 位元組
        char2 = bytearr[count+1] & 0xFF;
        char3 = bytearr[count+2] & 0xFF;
        if ((char2 & 0xC0) != 0x80 || (char3 & 0xC0) != 0x80) throw UTFDataFormatException;
        chararr[chararr_count++] = (char)(((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | (char3 & 0x3F));
        count += 3;
        break;
    default:
        // 10xxxxxx (延續位元組) 或 1111xxxx (無效) → 異常
        throw new UTFDataFormatException("malformed input around byte " + count);
}

實戰範例:二進制協定解析與健壯讀取


import java.io.*;
import java.nio.charset.StandardCharsets;

public class DataInputStreamDemo {
    public static void main(String[] args) {
        demoBasicReading();
        demoProtocolParsing();
        demoRobustReading();
    }
    
    // 基本型別讀取驗證
    private static void demoBasicReading() {
        File file = new File("datatypes.bin");
        
        try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
            System.out.println("Boolean: " + dis.readBoolean());
            System.out.printf("Byte: 0x%02X%n", dis.readByte());
            System.out.printf("UnsignedByte: %d%n", dis.readUnsignedByte());
            System.out.printf("Char: 0x%04X%n", dis.readChar());
            System.out.printf("Short: %d (0x%04X)%n", dis.readShort(), dis.readShort());
            System.out.printf("UnsignedShort: %d%n", dis.readUnsignedShort());
            System.out.printf("Int: %d (0x%08X)%n", dis.readInt(), dis.readInt());
            System.out.printf("Long: %d (0x%016X)%n", dis.readLong(), dis.readLong());
            System.out.println("Float: " + dis.readFloat());
            System.out.println("Double: " + dis.readDouble());
            System.out.println("UTF: " + dis.readUTF());
        } catch (IOException e) { e.printStackTrace(); }
    }
    
    // 解析自定義二進制協定
    private static void demoProtocolParsing() {
        // 協定:[Magic:2] [Version:1] [Type:1] [Length:4] [Payload:N] [Checksum:1]
        try (DataInputStream dis = new DataInputStream(new FileInputStream("protocol.bin"))) {
            int magic = dis.readUnsignedShort();
            if (magic != 0xCAFE) throw new IOException("Magic 不符: " + Integer.toHexString(magic));
            
            int version = dis.readUnsignedByte();
            int type = dis.readUnsignedByte();
            int length = dis.readInt();
            
            if (length < 0 || length > 1_000_000) 
                throw new IOException("長度異常: " + length);
            
            byte[] payload = new byte[length];
            dis.readFully(payload);  // 保證讀滿
            
            int checksum = dis.readUnsignedByte();
            
            // 驗證校驗和(簡單加總)
            int calcSum = 0;
            calcSum += (magic >> 8) & 0xFF;
            calcSum += magic & 0xFF;
            calcSum += version;
            calcSum += type;
            calcSum += (length >> 24) & 0xFF;
            calcSum += (length >> 16) & 0xFF;
            calcSum += (length >> 8) & 0xFF;
            calcSum += length & 0xFF;
            for (byte b : payload) calcSum += b & 0xFF;
            calcSum &= 0xFF;
            
            if (calcSum != checksum) {
                System.err.println("校驗和失敗: 計算=" + calcSum + " 文件=" + checksum);
            }
            
            String text = new String(payload, StandardCharsets.UTF_8);
            System.out.printf("Ver: %d, Type: 0x%02X, Len: %d, Checksum: %s%n", 
                              version, type, length, calcSum == checksum ? "OK" : "FAIL");
            System.out.println("Payload: " + text);
        } catch (IOException e) { e.printStackTrace(); }
    }
    
    // 健壯讀取模式:處理截斷、損壞、超大資料
    private static void demoRobustReading() {
        File file = new File("corrupted.bin");
        // 建立損壞檔案:長度聲稱 100 但只有 10 位元組
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) {
            dos.writeInt(100);  // 長度
            dos.write(new byte[10]);  // 只有 10 位元組資料
        } catch (IOException e) { e.printStackTrace(); }
        
        // 健壯讀取
        try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
            int claimedLen = dis.readInt();
            System.out.println("聲稱長度: " + claimedLen);
            
            if (claimedLen < 0 || claimedLen > 10_000_000) {
                throw new IOException("長度明顯異常,拒絕分配記憶體");
            }
            
            byte[] data = new byte[claimedLen];
            try {
                dis.readFully(data);
                System.out.println("讀取成功: " + data.length + " 位元組");
            } catch (EOFException e) {
                System.err.println("檔案截斷: 預期 " + claimedLen + " 位元組,實際讀取 " + (claimedLen - data.length) + " 位元組缺失");
                // 可選:讀取可用部分、記錄錯誤、跳過、重試等
            }
        } catch (IOException e) { e.printStackTrace(); }
    }
}

位元組序轉換工具:有號/無號顯示


// 正確顯示位元組/短整數的十六進制
private static String byteToHexString(byte val) {
    return String.format("0x%02X", val & 0xFF);  // &0xFF 移除符號擴展
}

private static String shortToHexString(short val) {
    return String.format("0x%04X", val & 0xFFFF);
}

private static String charToHexString(char val) {
    return String.format("0x%04X", (int) val);  // char 本身無號
}

// 使用範例
System.out.println(byteToHexString((byte) 0x8F));   // 0x8F (而非 0xFFFFFF8F)
System.out.println(shortToHexString((short) 0x8FCF)); // 0x8FCF

DataInputStream vs 其他輸入流

特性DataInputStreamBufferedReaderObjectInputStream
資料格式基本型別二進制文字行導向物件序列化
端序大端序N/A大端序
字串編碼修改版 UTF-8指定編碼修改版 UTF-8
讀取保證readFully 讀滿readLine 讀行完整物件圖
跨語言相容高低低
適用場景二進制協定、檔案格式、網路封包文字日誌、配置檔、CSVJava 物件持久化、RMI

最佳實踐與避坑指南

  1. 必須配對使用:寫入端用 DataOutputStream,讀取端用 DataInputStream,嚴格對應型別順序。
  2. 防禦性長度檢查:讀取陣列/字串前驗證長度合理性,防止惡意/損壞資料導致 OOM。
  3. 使用 readFully 而非 read:結構化解析時確保讀滿預期位元組數。
  4. 處理 EOFException:檔案截斷、網路中斷會拋出,需區分正常結束與異常中斷。
  5. 修改版 UTF-8 限制:補字元 (Emoji) 解碼為 Surrogate Pair,長度計算與標準 UTF-8 不同,跨語言需注意。
  6. 緩衝加速:new DataInputStream(new BufferedInputStream(new FileInputStream(file))) 大幅提升小塊讀取效能。
  7. 無號型別顯示:readUnsignedByte/Short 返回 int 避免符號誤解,十六進制顯示務必 & 0xFF/0xFFFF。

現代替代:ByteBuffer 與 NIO Channel


// 使用 ByteBuffer 進行相同解析,支援大小端切換、記憶體映射
try (FileChannel channel = FileChannel.open(Path.of("data.bin"), StandardOpenOption.READ)) {
    ByteBuffer buf = ByteBuffer.allocate(1024).order(ByteOrder.BIG_ENDIAN);
    channel.read(buf);
    buf.flip();
    
    int magic = buf.getShort() & 0xFFFF;
    byte version = buf.get();
    byte type = buf.get();
    int length = buf.getInt();
    
    byte[] payload = new byte[length];
    buf.get(payload);
    // ...
}

DataInputStream 是 Java 二進制資料解析的基石,掌握其大端序讀取、readFully 語義、修改版 UTF-8 解碼與異常處理,能構建健壯、高效、跨平台的二進制協定解析器。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved