HOWTO · Java

Java 中将字节数组转换为整数

本教程演示如何在 Java 中将字节数组转换为 int

本页内容

在 Java 中处理网络协议、文件 I/O 或二进制数据格式时,将字节数组转换为整数是一项常见任务。正确的方法取决于这些字节代表什么:有时是几个字节编码一个 int,有时每个字节都是独立的值。本文介绍 Java 的核心方法——ByteBuffer、DataInputStream、手动位运算和 BigInteger——以及决定结果是否正确的字节序和有符号性规则。

使用 ByteBuffer 将四个字节转换为一个 int

ByteBuffer(位于 java.nio 包中)是完成此转换最直接的 Java 核心工具。wrap 方法创建一个与数组共享存储空间的缓冲区,getInt() 从缓冲区当前位置开始读取四个字节,并将其解释为一个 int。运行下面的示例(代码中打印的标签保持英文):

本节中的代码以及显示的标签或输出仍为英文。

import java.nio.ByteBuffer;

public class ByteArrayToInt {
  public static void main(String[] args) {
    byte[] byteArray = {0x00, 0x01, 0x03, 0x10};

    int intValue = ByteBuffer.wrap(byteArray).getInt();

    System.out.println("Converted Integer Value: " + intValue);
  }
}

输出:

Converted Integer Value: 66320

ByteBuffer 在所有平台上默认都是大端序:数组中的第一个字节被视为最高有效字节。如果数据是以小端序写入的,请在读取之前使用 ByteOrder 显式选择该顺序:

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class ByteArrayToIntOrder {
  public static void main(String[] args) {
    byte[] byteArray = {0x00, 0x01, 0x03, 0x10};

    int bigEndianValue = ByteBuffer.wrap(byteArray).getInt();
    int littleEndianValue =
        ByteBuffer.wrap(byteArray).order(ByteOrder.LITTLE_ENDIAN).getInt();

    System.out.println("big-endian: " + bigEndianValue);
    System.out.println("little-endian: " + littleEndianValue);
  }
}

输出:

big-endian: 66320
little-endian: 268632320

这里有两个细节很重要。第一,getInt() 在缓冲区的当前位置读取并将其向前推进四个字节,因此在同一个缓冲区上调用两次会读取接下来的四个字节;而新的 wrap 总是从位置 0 开始。第二,当剩余字节少于四个时,getInt() 会抛出 BufferUnderflowException,因此读取之前要先检查输入长度。

使用 DataInputStream 读取四个字节

当字节通过流到达时——或者当你需要按顺序读取多个基本类型值时——DataInputStream 是合适的选择。将数组包装在 ByteArrayInputStream 中,再将其包装在 DataInputStream 中,然后调用 readInt()(示例输出中的标签保持英文):

本节中的代码以及显示的标签或输出仍为英文。

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;

public class ByteArrayToIntWithDataInputStream {
  public static void main(String[] args) throws IOException {
    byte[] byteArray = {0x00, 0x01, 0x03, 0x10};

    try (DataInputStream input =
        new DataInputStream(new ByteArrayInputStream(byteArray))) {
      int intValue = input.readInt();
      System.out.println("Converted Integer Value: " + intValue);
    }
  }
}

输出:

Converted Integer Value: 66320

无论平台如何,readInt() 始终按大端序解释接下来的四个字节。当流中剩余字节少于四个时,它会抛出 EOFException,你可以捕获该异常来报告输入过短,而不是让程序崩溃:

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;

public class ByteArrayToIntShortInput {
  public static void main(String[] args) throws IOException {
    byte[] byteArray = {0x03, 0x10};

    try (DataInputStream input =
        new DataInputStream(new ByteArrayInputStream(byteArray))) {
      int intValue = input.readInt();
      System.out.println("Converted Integer Value: " + intValue);
    } catch (EOFException e) {
      System.out.println("readInt() needs four bytes, but only " + byteArray.length + " remain");
    }
  }
}

输出:

readInt() needs four bytes, but only 2 remain

通过字节移位构建 int

自己对字节进行移位可以清楚地展示转换的工作原理,并让你完全控制顺序和长度。对每个字节,将累积值左移 8 位,再与下一个字节进行或运算(示例输出中的标签保持英文):

本节中的代码以及显示的标签或输出仍为英文。

public class ByteArrayToIntManually {
  public static void main(String[] args) {
    byte[] byteArray = {0x00, 0x01, 0x03, 0x10};

    int intValue = 0;

    for (byte b : byteArray) {
      intValue = (intValue << 8) | (b & 0xFF);
    }

    System.out.println("Converted Integer Value: " + intValue);
  }
}

输出:

Converted Integer Value: 66320

掩码 b & 0xFF 是必需的,因为 Java 的 byte 类型是有符号的:0x80 被存储为 -128。如果没有掩码,| 会合并 0xFFFFFF80 而不是 0x00000080,从而破坏所有更高位的字节。这个循环按大端序组装字节——第一个数组元素是最高有效字节——并假设数组恰好包含四个字节。如果需要不同的顺序,就反向遍历数组;如果字节更多,就累积到 long 而不是 int 中。

使用 BigInteger 解释字节数组

BigInteger 的构造函数直接接受字节数组,并将其解释为任意长度的有符号、大端序、二进制补码数字。然后 intValue() 将结果收窄为 int(示例输出中的标签保持英文):

本节中的代码以及显示的标签或输出仍为英文。

import java.math.BigInteger;

public class ByteArrayToIntWithBigInteger {
  public static void main(String[] args) {
    byte[] byteArray = {0x00, 0x01, 0x03, 0x10};

    int intValue = new BigInteger(byteArray).intValue();

    System.out.println("Converted Integer Value: " + intValue);
  }
}

输出:

Converted Integer Value: 66320

由于解释是有符号的,前导 1 位会产生小于 0 的值;将符号值 1 作为构造函数的第一个参数传入,即可得到无符号解释。请注意,intValue() 只保留低 32 位——它会静默截断无法放入 int 的值:

import java.math.BigInteger;

public class BigIntegerSignAndRange {
  public static void main(String[] args) {
    byte[] allOnes = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF};

    System.out.println("signed: " + new BigInteger(allOnes));
    System.out.println("unsigned: " + new BigInteger(1, allOnes));
    System.out.println("narrowed to int: " + new BigInteger(1, allOnes).intValue());
  }
}

输出:

signed: -1
unsigned: 4294967295
narrowed to int: -1

当数组长度超过四个字节,或者在收窄之前需要无符号数值时,BigInteger 是自然的选择。

分别转换每个字节

上面的所有方法都是把多个字节组合成一个数字。如果每个字节都是独立的值——传感器读数、颜色通道、协议字段——则应逐个转换元素。注意两者的区别:对两个字节使用 getShort() 或 readShort() 得到的是一个组合的 short,而不是分别转换每个字节。

还要记住,Java 的 byte 类型是有符号的,范围为 -128 到 127。当意图是 0 到 255 的无符号值时,使用 Byte.toUnsignedInt(自 Java 8 起可用)或等效的 b & 0xFF 掩码(示例输出中的标签保持英文):

本节中的代码以及显示的标签或输出仍为英文。

public class PrintEachByte {
  public static void main(String[] args) {
    byte[] byteArray = {0x03, 0x04, (byte) 0x80, (byte) 0xFF};

    for (byte b : byteArray) {
      System.out.println("signed: " + b + " unsigned: " + Byte.toUnsignedInt(b));
    }
  }
}

输出:

signed: 3 unsigned: 3
signed: 4 unsigned: 4
signed: -128 unsigned: 128
signed: -1 unsigned: 255

选择方法并处理边界情况

每种方法适用于不同的场景:

  • ByteBuffer 是从数组读取四字节 int 的首选:它紧凑,通过 ByteOrder 支持两种字节序,并提供 getLong()、getShort() 等方法处理其他宽度。
  • DataInputStream 适合已经在使用流、并需要按顺序读取多个类型化值的代码;它的读取始终是大端序。
  • 手动移位要多写几行代码,但能精确控制顺序、偏移量和宽度,且不需要缓冲区或流对象。
  • BigInteger 可以处理长于四个字节的数组,并在收窄之前进行显式的有符号或无符号解释。
  • 当每个元素本身就是独立的值、而不是一个数字的组成部分时,使用 Byte.toUnsignedInt 逐字节转换。

无论选择哪种方法,转换之前请检查以下几点:

  • **输入长度:**对短于四个字节的数组进行四字节读取会失败——ByteBuffer 抛出 BufferUnderflowException,DataInputStream 抛出 EOFException,而手动代码则会抛出 ArrayIndexOutOfBoundsException 或产生静默错误的结果。
  • **字节序:**读取端的顺序必须与字节写入时的顺序一致;ByteBuffer 默认为大端序,可以用 ByteOrder 切换,而 DataInputStream 的读取始终是大端序,没有顺序选项。
  • 有符号性和范围:getInt() 和 readInt() 返回有符号 int,因此超过 Integer.MAX_VALUE 的无符号 32 位值会显示为小于 0 的数。当源数据是无符号时,使用 Integer.toUnsignedLong(intValue) 或累积到 long 中。
  • **缓冲区位置:**重用 ByteBuffer 会从其当前位置继续读取,而不是从头开始;调用 rewind() 或再次 wrap 以重新读取数组。