真值与机器值
真值很好理解,就是十进制的数字前面再加上正负号,这是人类可以简单识别的数字,比如 0、±16、±1084、±10.34、±100.453 等,而正数前面的+符号可以省略。机器值从字面理解就是机器(计算机)识别的值,实际上也确实是这个意思。
计算机中通过高低电平表示1或者0,这样就可以表示一个二进制的数值。一个1或者0表示的数值位称为一个bit,而计算机中存储和传输数据的最小单位是一个字节(byte)也就是8个bit,所以说计算机所有计算本质上都是基于二进制。
在计算机中,我们可以使用1个或者多个字节存储一个数,但无论是多少个字节,其大小肯定是固定的,同时其所能表示的数值的范围也是固定的。比如说对使用1个字节存储的数进行计算或者传输,那么这个数所能表示的最小值为00000000最大值为11111111,转换为十进制为0 ~ 255。那么无论对这个数做了什么计算,无论计算之后的结果为多少都不能超出这个范围,同理使用2个字节存储的数范围为0 ~ 65535。
由于很多时候一个数据需要使用2个或者2个以上的字节表示,那么这种数据无论是存储还是传输的时候都会有一个顺序的问题,也就是大小端对齐(字节序)问题。在存储时高位字节在前为大端对齐,反之为小端对齐。在数据传输时先传输高位字节为大端字节序,反之为小端字节序。目前绝大多数平台内部都是小端对齐的方式存储数据,而大多数通信协议却都是用大端字节序传输数据,所以这一点值得注意一下。
符号位与数值位
计算机中使用二进制存储传输和计算数值,但是不能只有数值,计算的时候还得有正负之分。在计算机中使用最高bit位的数值来表示正负号,这个bit位称作符号位。
计算机中符号位的值为0表示这个数为正数,符号位值为1表示这个树为负数。由于符号位表示符号所以其不表示具体的值,除开符号位剩余的bit位用来表示数值也就是数值位。比如1个字节的整数00000001,其中最高bit(最左边)位的0为符号位,表示这个数为正数,数值位为1,所以其真值为1。同理2个字节的整数00000000_0000001,其真值也是1。
原码、反码和补码
计算机只识别机器码,其实也就是二进制数,并且使用最高bit位表示符号位。那么两个真值为8和-8的8位整数,它们在计算机内部的机器值是否就分别是00001000和10001000?其实并不是,这只是8和-8的原码,而机器算计中的机器值是使用补码存储和计算的。
计算机中,正数的原码、反码和补码是一样的,所以上面那个例子中,真值为8的8位整数的机器值确实是00001000,但是-8就不是这么回事了。负数的首先将原码数值位按位取反得到反码,然后再将反码数值位加1之后则得到补码。我们来看一下-8这个例子,其原码为10001000,数值位按位取反之后的反码为11110111,然后数值位加1之后的补码为11111000。所以真值为-8的8位整数在计算机中的机器值为11111000,我们来看下面这张表
原码 | 反码 | 补码 | |
---|---|---|---|
int8 | 00000001 | 00000001 | 00000001 |
int8 | 10000001 | 11111110 | 11111111 |
int16 | 00000000_00000001 | 00000000_00000001 | 00000000_00000001 |
int16 | 10000000_00000001 | 11111111_11111110 | 11111111_11111111 |
… | … | … | … |
注:int8为8bit位整数占用1byte,int16为16bit位整数占用2byte。
刚说的是原码转补码的步骤,其实补码转原码的步骤是一样的。首先正数的原码补码是一样的不需要转换,我们看负数11111000,首先将数值位按位取反得到10000111,然后再将数值位加1得到10001000。我们再来看一个8位的整数10000000,是不是发现这个数原码和补码是一样的,那么这个看起来像是“-0”的数是怎么回事呢?其实可以将这个数看成是一个特殊值,它的真实含义就是最小值。8位的这种“-0”的真值为-128,16位的这种“-0”真值为-32768。所以只需要记住100…000这种补码就是最小值就行,我们看下面的这张表
二进制补码 | 十进制 | |
int8 | 10000000 | -128 |
int16 | 10000000_00000000 | -32768 |
int32 | 10000000_00000000_00000000_00000000 | -2147483648 |
int64 | 10000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000 | -9223372036854775808 |
有两对8bit位的整数4、8和4、-8,我们分别看一下他们在计算机中是怎么做加法计算的。首先看4和8的补码分别为00000100和00001000,只需要将每个bit位相加就行,结果为00001100,其真值为12。我们再来4和-8的计算,它们补码分别为00000100和11111000,然后将它们按位相加(注意符号位也要做加法)得到11111100,其原码为10000100,真值为-4。
再来看一下减法计算,比如8bit位的整数-8减去4,首先可以将4处理一下可以变为(-8) + (-4),这样是不是就又变为了加法了?-8和-4的补码分别为11111000和11111100,将它们按位相加得到补码11110100(注意这是8位的整数,超出部分发生了溢出),转换成原码为10001100,真值为-12。
再来看一下乘法,比如8bit位的整数-8乘以13,他们的补码分别为11111000和00001101。其中-8为被乘数,13为乘数,并且乘数有8个bit位,需要将被乘数按位与和位计算8次然后将结果相加,看如下分析:
- 1、被乘数的第0个bit位值为1,将被乘数乘以1然后左移0位得到:11111000;
- 2、被乘数的第1个bit位值为0,将被乘数乘以0然后左移1位得到:00000000;
- 3、被乘数的第2个bit位值为1,将被乘数乘以1然后左移2位得到;11100000;
- 4、被乘数的第3个bit位值为1,将被乘数乘以1然后左移3位得到;11000000;
- 5、被乘数的第4个bit位值为0,将被乘数乘以0然后左移4位得到;00000000;
- 6、被乘数的第5个bit位值为0,将被乘数乘以0然后左移5位得到;00000000;
- 7、被乘数的第6个bit位值为0,将被乘数乘以0然后左移6位得到;00000000;
- 8、被乘数的第7个bit位值为0,将被乘数乘以0然后左移7位得到;00000000;
由此可以得计算得到8组补码(注意上面做位移涉及到的整数溢出,只能是8个bit位),然后将它们做加法得到10011000(也存在整数溢出)转换为原码为11101000,真值为-104。
至于除法则是使用交替加减法的方式,本文只是对计算原理做一下扩展,这里不再继续深入做介绍,如果有想了解的可以自行上网查询。
通过上面的分析可以知道,使用补码可以将所有计算都转化为加法计算,这样可以让计算机底层对于整数(浮点数再此不做讲解,有时间会再单独写一篇文章作介绍)计算变得简单,反码属于历史遗留,因为其存在±0的问题。
Java中的基本数据类型
在计算机编程语言中都会有数据类型的概念,数据类型是用来修饰变量的。不同数据类型所修饰的变量,其指代的数据在内存中占用空间的大小(基本类型变量使用的空间、指针或引用变量指向的地址空间等,后面简单称数据类型占用的内存空间)是固定的。即使在一些弱类型语言中,虽然变量可以不用显示地声明数据类型,但当第一次为变量赋值时,还是会隐式地为其附上数据类型属性。
对于java来说,由于其具有跨平台的特性,所以基本数据类型所占用的内存空间大小(字节数)是固定的。我们来看一下java中的几个基本数据类型:
byte | char | short | int | float | long | double | boolean | |
---|---|---|---|---|---|---|---|---|
字节数 | 8bit / 1byte | 16bit / 2byte | 16bit / 2byte | 32bit / 4byte | 32bit / 4byte | 64bit / 8byte | 64bit / 8byte | / |
取值范围 | -27 ~ 27 - 1 | 0 ~ 216 - 1 | -215 ~ 215 - 1 | -231 ~ 231 - 1 | - | -263 ~ 263 - 1 | - | false | true |
默认值 | 0 | 0 | 0 | 0 | 0.0 | 0 | 0.0 | false |
后缀 | / | / | / | / | f | F | l | L | d | D | / |
注:jvm规范并没有指明boolean类型占用几个字节的空间,所以根据jvm产品的不同,实现的方式也可能不同。最广泛的说法是,jvm内部使用int代替boolean类型,也就是占用4个字节。另外这里没有列出浮点数的大小范围,由于本文只介绍整数,后面如果有时间则会单独出一篇介绍浮点数的博文。
我们来看下面几个例子
// 案例1,下面的10进制真值的写法,但是当编译器编译完成之后,在内存中还是会以补码的形式存在
int value1 = 10;
int value2 = -10;
System.out.printf("value1=%d, value2=%d\n", value1, value2); // 结果为: value1=10, value2=-10
// 案例2,下面是2进制补码的写法,在数字前面加上'0b'或者'0B','_'只是一个分隔符
int value3 = 0b00000000_00000000_00000000_00001010; // 2进制int类型10的补码
int value4 = 0B11111111_11111111_11111111_11110110; // 2进制int类型-10的补码
System.out.printf("value3=%d, value4=%d\n", value3, value4); // 结果为: value3=10, value4=-10
// 案例3,下面是16进制补码的写法,在数字前面加上'0x'或者'0X',大于9的数值使用a~f或A-F表示
int value5 = 0x0000000a; // 16进制int类型10的补码
int value6 = 0Xfffffff6; // 16进制int类型-10的补码
System.out.printf("value5=%d, value6=%d\n", value5, value6); // 结果为: value5=10, value6=-10
// 案例4,下面是8进制补码的写法,在数字前面加上'0'
int value7 =