2. Numeric Data
输入流读入字节,输出流写出字节,而reader读入字符,writer写出字符。因此要理解JavaIO首先要深入理解Java如何处理字节、整数、字符和其他的基本数据类型,它们之间什么时候并且为什么发生转型,很多情况下这些转型都不明显。
1.2.1. Integer Data
Java中int是4个字节前端高位后端低位由二进制补码表示的整型数据类型,,int的范围-2,147,483,648 到 2,147,483,647。对于数字7、-8345和
3,000,000,000编译器都处理成int数据,但是编译器对3,000,000,000这样大的数据会产生数据溢出错误。
而long数据是8个字节的,其前端是较高位,后端是最低位,由二进制补码组成,它的范围自-9,223,372,036,854,775,808到9,223,372,036,854,775,807。这样的大数据前加L或l后缀,例如7L, -8345L和3000000000L都是64位的长整型数据。为了不造成混淆,大写L比小写l要常用。
另外两种数据类型short和byte在Java中也可能见到,short是2字节前端高位后端低位由二进制补码表示的短整型数据类型,其范围-32,768 到32,767,short在Java中可能用到,并且对C语言有兼容性。
但是byte在Java中很常见,尤其是在JavaIO中,一个byte数据即一个字节,由二进制补码表示的整数,其范围-128 到 127。注意像Java中的所有数据类型一样,byte是有符号的。最大的值是127,对于byte比这大的数不合法。像42和24000这样的数字,编译器会当做int数据,而不是short或者byte数据,即使像下面显式声明byte和short数据,Java内部也是作为int来处理:
byte b = 42;
short s = 24000;
这些int数据在编译时时编译器理解为常量,但编译器允许其转型为byte和short窄类型的数据,并且是高效的。但是像下面这样简单的赋值来却达不到转型的目的:
int i = 42;
byte b = i;
编译器理论上知道这样转型并不会丢失数据,但编译这样的代码会报错:
Error: Incompatible type for declaration.
Explicit cast needed to convert int to short.
ByteTest.java line 6
要改正这个错误,需要这样显式转型
int i = 42;
byte b = (byte) i;
即使是两个byte数据产生的新数据赋值给byte也需要显式转型:
byte b1 = 22;
byte b2 = 23;
byte b3 = b1 + b2;
所以直接操作byte非常不方便。但JavaIO的某些方法的确是基于byte的,这些方法返回值或参数是一个无符号的字节表示的整数(0-255),这个范围说明它并不满足Java的基本数据类型,这就意味着需要内部转型。
例如根据Java类库文档的描述:the read( ) method of java.io.InputStream returns "the next byte of data, or -1 if the end of the stream is reached."但怎样区分数据中的-1和标志流尾的-1呢?有疑惑了吧,仔细思考一下(参看原文档源代码)
事实上,read方法返回的并不是一个byte,而是一个int:
public abstract int read( ) throws IOException
很显然这里的int不是byte数据,可以看作一个无符号的字节表示的整数(0-255)。因此,流尾的-1很容易和合法数据区分开。(合法数据-1被转型到0-255之间?)
java.io.OutputStream 中的write方法有相同的问题,它无返回值并且以一个int数据为参数
public abstract void write(int b) throws IOException
同样,这个int意味着一个无符号字节表示的整数(0-255),这个方法并不能防止因为程序员的疏忽传了超过这个范围的int数据。那么就需要去掉超出这个范围的三个字节:
b = b & 0x000000FF;
这在Java语言规范中有描述。
因为write是抽象的,具体的实现在子类中都有实现,但是如果自定义子类,粗心的程序员可能忽略这点。
byte在IO中还体现在另一方面,某些读写方法使用byte数组,例如下面这两个方法:
public int read(byte[] data) throws IOException
public int read(byte[] data, int offset, int length) throws IOException
操作一个单独的8位的byte数据和一个32位的int数据没有多大差别,但是如果很成百上千的数据需要读写的话就有很大区别。事实上,一个byte数据在Java虚拟机中占有4个字节空间,但是byte数组只占实际需要的大小空间。虚拟机对byte数组执行特殊的操作,但对单独的byte数据并没有操作,它仅仅是转型为int。
尽管byte数组使用的是有符号的值(-127-128),但IO操作时它们都有对应的无符号byte值,这种对应使用的下面这种方法:
int unsignedByte = signedByte >= 0 ? signedByte : 256 + signedByte;
1.2.2. Conversions and Casts转变和转型
byte数据范围很小,它们在计算和方法调用时都宽转变为int数据,通常它们需要窄转型回来,深入理解这种变化时非常有用的。
通过抹掉数据高位来达到int数据向byte数据的转型,其值并不会发生改变。int数据127转型为byte数据其值还是127。当然,如果int值太大的话,转型时数据值可能前后不一,int数据128转型为byte是-128。原因在于转型执行的二进制算法,128是用十六进制表示的,为0x00000080,转型为byte时需要抹掉高位结果为0x80。128用二进制表示为10000000,如果无符号是128,但是Java会看做是带符号的,这个1是符号位,通过补码加1得到其绝对的值。因此Ox80代表-128,诸如类推129代表-127,130代表-126,直到255代表-1。
所以,int数据256转型为byte是0,然后循环轮回。这个算法很简单,如下所示:
int byteValue;
int temp = intValue % 256;
if ( intValue < 0) {
byteValue = temp < -128 ? 256 + temp : temp;
}
else {
byteValue = temp > 127 ? temp - 256 : temp;
}