Java中正数 负数位运算与解析
先说下出现的背景
最近和一位资深嵌入式开发的大佬一起开发一个一栈式(设备-应用项目), 大佬精通底层汇编,负责解析设备侧的数据,
考虑了数据量他改造了数据库的各个字段排开主键和时间,算是每个字段都死扣了,这样可以使cpu计算更快,硬盘耗费更低
CREATE TABLE `gk_report_gps` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`device_id` bigint(20) NOT NULL COMMENT '设备内部ID',
`battery_volt` smallint(2) NOT NULL COMMENT '电压',
`battery_power` tinyint(1) NOT NULL COMMENT '电量100%',
`state` tinyint(1) NOT NULL COMMENT '充电、运动状态',
`latitude` int(4) NOT NULL COMMENT '纬度0.00001',
`longitude` int(4) NOT NULL COMMENT '经度0.00001',
`altitude` smallint(2) NOT NULL COMMENT '高度0.1m',
`speed` tinyint(1) NOT NULL COMMENT '速度',
`gps_flag` tinyint(1) NOT NULL COMMENT 'debug',
`satellite_cal1` tinyint(1) NOT NULL COMMENT 'debug',
`satellite_cal2` tinyint(1) NOT NULL COMMENT 'debug',
`satellite_view1` tinyint(1) NOT NULL COMMENT 'debug',
`satellite_view2` tinyint(1) NOT NULL COMMENT 'debug',
`pdop` smallint(2) NOT NULL COMMENT 'debug',
`hdop` smallint(2) NOT NULL COMMENT 'debug',
`vdop` smallint(2) NOT NULL COMMENT 'debug',
`utc` char(4) NOT NULL COMMENT 'utc时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9573 DEFAULT CHARSET=utf8 COMMENT='gk'
然而入库的时候出现了问题
Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Out of range value for column ‘altitude’ at row 1
设备数据到应用侧是Base64格式,我依据标准Base64解析成byte数组,再参照数据库类型(数据按各字节转换10进制)解析入库
altitude在应用侧就是2个字节数据,而数据库对应的smallint也是2字节,但是却抛异常了
数据分析原因
debug下先下解析的数据
而我这边采用的是无符号解析
new BigInteger(1, data).toString(10);
解析altitude为 60765 但是数据库中smallint是带sign的范围为 -2^15 (-32,768) 到 2^15 - 1 (32,767) 显然是存不下这数据量
怎么办呢?
要么把数据库smallint改成4字节int,要么就存入负数,但是大佬直接否定了前者,那就在应用当带符号二进制解析
负数在java中是以补码方式存储的(正数补码是其本身, 负数补码先去反码再末尾+1)
按照当时byte[]{-19, 93}
- 源码[10010011, 01011101]
- 补码[11101101, 01011101]
java中二进制化整形采用位运算:&, |
- &: 按位与(同为1时为1,否则为0)
- |: 按位或(同为0时为0,否则为1)
而altitude 是一个2字节数据那么拼凑可以这样:
(byte[0] << 8) | byte[1]
但那事实是不行,前者(byte[0] << 8) 左移8为结果为int,负数的话其高位会全部补1也就是说在内存中是: 1111 1111 1111 1111 1110 1101 0000 0000
那么如果byte[1]也是负数, 必然其高位结果会受影响,所以按位与应该要截断高位是这样(byte[0] << 8) | (byte[1] & 0xFF)
0xFF十六进制 为1111 1111,按位与上一个8为的byte其结果是其本身,但是是这样么? 0xFFint型补齐: 0000 0000 0000 0000 1111 1111 1111 1111
& 0xFF会直接截取一个8位的正数
- byte数组内存存在形式:[11101101, 01011101]
- 按位或执行完结果: 1111 1111 1111 1111 1110 1101 0101 1101
- 结果(源码显示): 1111 1111 1111 1111 1001 0010 1010 0011
- 结果(十进制显示): -4771
将这-4771存入数据库中
其读取到java内存顺序:
- 源码显示: 1001 0010 1010 0011
- java中负数以补码存入: 1111 1111 1111 1111 1110 1101 0101 1101
- 无符号十进制显示(&FFFF): 60765
这样讲解析后的带符号数还原了原始数据