本文已收录到AndroidFamily ,技术和职场问题,请关注公众号 [彭旭锐] 提问。
前言
大家好,我是小彭。
在前面的文章里,我们聊到了计算机的冯·诺依曼架构的 3 个基本原则。其中第 1 个原则是计算机中所有信息都是采用二进制格式的编码。也就是说,在计算机中程序的数据和指令,以及用户输入的所有数据,计算机都需要把它们转换为二进制的格式,才能进行识别和运算。
*[En]*
**
小彭的 Android 交流群 02 群已经建立啦,扫描文末二维码进入~
思维导图:

1. 为什么计算机要使用二进制数制?
所谓数制其实就是一种 “计数的进位方式”。
常见的数制有十进制、二进制、八进制和十六进制:
- 十进制是我们日常生活中最熟悉的进位方式,它一共有 0、1、2、3、4、5、6、7、8 和 9 十个符号。在计数的过程中,当某一位满 10 时,就需要向它临近的高位进一,即逢十进一;
*[En]*
**
- 八进制和十六进制同理。
*[En]*
**
2.有符号数与无符号数
*[En]*
**
好在我们发现 “正 / 负” 是两种截然不同的状态,正好可以映射到计算机能够理解的 “0 / 1” 上。因此,我们可以直接 “将符号数字化”,将 “正+” 数字化为 “0”,将 “负-” 数字化为 “1”,并将数字化后的符号和数值共同组成数字编码。
另外,为了计算方便,我们额外再规定将 “符号位” 放在数字编码的 “最高位”。例如, +1110
和 -1110
用 8 位二进制表示就是:
- 0000, 1110(符号作为编码的一部分,最高位 0 表示正数)
- *1000, 1110(符号作为编码的一部分,最高位 1 表示负数)
从中我们也可以看出无符号数和有符号数的区别:
- 1、最高位功能不同: 无符号数的编码中的每一位都可以用来存放数值信息,而有符号数需要在编码的最高位留出一位符号位;
- 2、数值范围不同: 相同位数下有符号数和无符号数表示的数值范围不同。以 16 位数为例,无符号数可以表示 0~65536,而有符号数可以表示 -32768~32768。
提示: 无符号数和有符号数表示的数值范围大小是一样大的,n 位二进制最多只能表示 $2^n$ 个信息量,这是无法被突破的。
3. 机器数的运算效率问题
在计算机中,我们会把带 “正 / 负” 符号的数称为真值(True Value),而把符号化后的数称为机器数(Computer Number)。
机器数才是数字在计算机中的二进制表示。 例如在前面的数字中, +1110
是真值,而 0000, 1110
是机器数。新的问题来了:将符号数字化后的机器数,在运算的过程中符号位是否与数值参与运算,又应该如何运算呢?
我们先举几个加法运算的例子:
- *两个正数相加:
0000, 1110 + 0000, 0001 = 0000, 1111 // 14 + 1 = 15 正确
^ ^ ^
符号位 符号位 符号位
- *两个负数相加:
1000, 1110 + 1000, 0001 = 0000, 1111 // (-14) + (-1) = 15 错误
^ ^ ^
符号位 符号位 符号位(最高位的 1 溢出)
- *正负数相加:
0000, 1110 + 1000, 0001 = 1001, 1111 // 14 + (-1) = -15 错误
^ ^ ^
符号位 符号位 符号位
可以看到,在对机器数进行 “按位加法” 运算时,只有两个正数的加法运算的结果是正确的,而包含负数的加法运算的结果却是错误的,会出现 -14 - 1 = 15
和 14 - 1 = -15
这种错误结果。
*[En]*
**
- 两个正数相加:
- 直接做按位加法。
- 两个负数相加:
- 1、用较大的绝对值 + 较小的绝对值(加法运算);
- 2、最终结果的符号为负。
- 正负数相加:
- 1、判断两个数的绝对值大小(数值部分);
- 2、用较大的绝对值 – 较小的绝对值(减法运算);
- 3、最终结果的符号取绝对值较大数的符号。
哇🤩?好好的加法运算给整成减法运算? 运算器的电路设计不仅要多设置一个减法器,而且运算步骤还特别复杂。那么,有没有不需要设置减法器,而且步骤简单的方案呢?
4. 原码、反码、补码
*[En]*
**
机器数 正数 负数 原码 符号位表示符号
数值位表示真值的绝对值 符号位表示数字的符号
数值位表示真值的绝对值 反码 无(或者认为是原码本身) 符号位为 1
数值位是对原码数值位的 “按位取反” 补码 无(或者认为是原码本身) 在负数反码的基础上 + 1
- 1、原码: 原码是最简单的机器数,例如前文提到从
+1110
和-1110
转换得到的0000, 1110
和1000, 1110
就是原码表示法,所以原码在进行数字运算时会存在前文提到的效率问题; - 2、反码: 反码一般认为是原码和补码转换的中间过渡;
- 3、补码: 补码才是解决机器数的运算效率的关键, 在计算机中所有 “整型类型” 的负数都会使用补码表示法;
- 正数的补码是原码本身;
- 零的补码是零;
- 负数的补码是在反码的基础上再加 1。
*[En]*
**
提示: 为了便于你理解,小彭后文会继续用 “正数的补码是原码本身” 这个观点阐述。
5. 使用补码消除减法运算
理解补码表示法后,似乎还是不清楚补码有什么用❓
我们重新计算上一节的加法运算试试:
举例 真值 原码 反码 补码 +14 +1110 0000, 1110 0000, 1110 0000, 1110 +13 +1101 0000, 1101 0000, 1101 0000, 1101 -14 +1110 1000, 1110 1111, 0001 1111, 0010 -15 -1110 1000, 1111 1111, 0000 1111, 0001 +1 +0001 0000, 0001 0000, 0001 0000, 0001 -1 -0001 1000, 0001 1111, 1110 1111, 1111
- *两个正数相加:
// 补码表示法
0000, 1110 + 0000, 0001 = 0000, 1111 // 14 + 1 = 15 正确
^ ^ ^
符号位 符号位 符号位
- *两个负数相加:
// 补码表示法
1111, 0010 + 1111, 1111 = 1111, 0001 // (-14) + (-1) = -15 正确
^ ^ ^
符号位 符号位 符号位(最高位的 1 溢出)
- *正负数相加:
// 补码表示法
0000, 1110 + 1111, 1111 = 0000, 1101 // 14 + (-1) = 13 正确
^ ^ ^
符号位 符号位 符号位(最高位的 1 溢出)
*[En]*
**
除了消除减法运算外,补码表示法还实现了 “0” 的机器数的唯一性:
在原码表示法中,”+0″ 和 “-0” 都是合法的,而在补码表示法中 “0” 只有唯一的机器数表示,即 0000, 0000
。换言之补码能够比原码多表示一个最小的负数 1000, 0000
。
最后提供按照不同表示法解释二进制机器数后得到的真值对比:
二进制数 无符号真值 原码真值 反码真值 补码真值 0000, 0000 0 +0 +0 +0 0000, 0001 1 +1 +1 +1 … … … … … 1000, 0000 128 -0(负零,无意义) -127 -128(多表示一个数) 1000, 0001 129 -1 -126 -127 … … … … … 1111, 1110 254 -126 -1 -2 1111, 1111 255 -127 -0(负零) -1
6. 补码我懂了,但是为什么?
*[En]*
**
这就要提到数学中的 “补数” 概念:
- 1、当一个正数和一个负数互为补数时,它们的绝对值之和就是模;
- 2、一个负数可以用它的正补数代替。
6.1 时钟里的补数
听起来很抽象对吧❓其实生活中,就有一个更加形象的例子 —— 时钟,时钟里就蕴含着补数的概念!
比如说,现在时钟的时针刻度指向 6 点,我们想让它指向 3 点,应该怎么做:
- 方法 1 : 逆时针地拨动 3 个点数,让时针指向 3 点,这相当于做减法运算 -3;
- 方法 2: 顺时针地拨动 9 个点数,让时针指向 3 点,这相当于做加法运算 +9。
可以看到,对于时钟来说 -3 和 +9 竟然是等价的! 这是因为时钟只能 12 个小时,当时间点数超过 12 时就会自动丢失,所以 15 点和 3 点在时钟看来是都是 3 点。如果我们要在时钟上进行 6 - 3
减法运算,我们可以将 -3
等价替换为它的正补数 +9
后参与计算,从而将减法运算替换为 6 + 9
加法运算,结果都是 3。

6.2 十进制的例子
理解了补数的概念后,我们再多看一个十进制的例子:我们要计算十进制 354365 - 95937 =
的结果,怎么做呢?
- 方法 1 – 借位做减法: 常规的做法是利用连续向前借位做减法的方式计算,这没有问题;
- 方法 2 – 减模加补: 使用补数的概念后,我们就可以将减法运算消除为加法运算。
具体来说,如果我们限制十进制数的位长最多只有 6 位,那么模就是 1000000, -95937
对应的正补数就是 1000000 - 95937 = 904063
。此时,我们可以直接用正补数代替负数参与计算,则有:
354365 - 95937 // = 258428
= 354365 - (1000000 - 904063)
= 354365 - 1000000 + 904063 【减整加补】
= 258428
可以看到,把 -95937
等价替换为 +904063
后,就把减法运算替换为加法运算。细心的你可能要举手提问了,还是需要减去 1000000
呀?🙋🏻♀️
其实并不用,因为 1000000
是超过位数限制的,所以减去 1000000
这一步就像时针逆时针拨动一整圈一样是无效的。所以实际上需要计算的是:
// 实际需要计算的是:
354365 + 904063
= 1258428 = 258428
^
最高位 1 超出位数限制,直接丢弃
6.3 为什么要使用补码?
继续使用前文提到的 14 + (-1)
正负数相加的例子:
// 原码表示法
0000, 1110 + 1000, 0001 = 1001, 1111 // 14 + (-1) = -15 错误
^ ^ ^
符号位 符号位 符号位
// 补码表示法
0000, 1110 + 1111, 1111 = 1, 0000, 1101 // 14 + (-1) = 13 正确
^ ^ ^
符号位 符号位 最高位 1 超出位数限制,直接丢弃
如果我们限制二进制数字的位长最多只有 8 位,那么模就是 1, 0000, 0000
,此时, -1
的二进制数 1000, 0001
的正补数就是 1111, 1111
。
我们使用正补数 1111, 1111
代替负数 1000, 0001
参与运算,加法运算后的结果是 1, 0000, 1101
。其中最高位 1 超出位数限制,直接丢弃,所以最终结果是 0000, 1101
,也就是 13,计算正确。
补码示意图

到这里,相信补码的设计原理已经很清楚了。
补码的关键在于: 找到一个与负数等价的正补数,使用该正补数代替负数,从而将减法运算替换为两个正数加法运算。 补码的出现与运算器的电路设计有关,从设计者的角度看,希望尽可能简化电路设计和计算复杂度。而使用正补数代替负数就可以消除减法器,实现简化电路的目的。
*[En]*
**
7. 总结
- 1、无符号数的编码中的每一位都可以用来存放数值信息,而有符号数需要在最高位留出一位符号位;
- 2、在有符号数的机器数运算中,需要对正数和负数采用不同的计算方法,而且需要引入减法器;
- 3、为了解决有符号机器数运算效率问题,计算机科学家们提出多种机器数的表示法:原码、反码、补码和移码;
- 4、使用补码表示法后,运算器可以消除减法运算,而且实现了 “0” 的机器数的唯一性;
- 5、补码的关键是找到一个与负数等价的正补数,使用该正补数代替负数参与计算,从而将减法运算替换为加法运算。
在前文讲补码的地方,我们提到计算机所有 “整型类型” 的负数都会使用补码表示法,刻意强调 “整数类型” 是什么原因呢,难道浮点数和整数在计算机中的表示方法不同吗?这个问题我们在 下一篇文章 里讨论,请关注。
参考资料
- 计算机组成原理教程(第 2、6 章) —— 尹艳辉 王海文 邢军 著
- 深入浅出计算机组成原理(第 11 ~ 16 讲) —— 徐文浩 著,极客时间 出品
- 10分钟速成课 计算机科学 —— Carrie Anne 著
- Binary number —— Wikipedia
小彭的 Android 交流群 02 群

Original: https://www.cnblogs.com/pengxurui/p/16942787.html
Author: pengxurui
Title: 为什么计算机中的负数要用补码表示?
相关阅读
Title: Kafka自定义分区器
1.定义类实现 Partitioner 接口,重写 partition()方法
package com.kafka.producer;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
/**
* 1. 实现接口 Partitioner
* 2. 实现 3 个方法:partition,close,configure
* 3. 编写 partition 方法,返回分区号
*/
public class MyPartitioner implements Partitioner {
/*** 返回信息对应的分区
* @param topic 主题
* @param key 消息的 key
* @param keyBytes 消息的 key 序列化后的字节数组
* @param value 消息的 value
* @param valueBytes 消息的 value 序列化后的字节数组
* @param cluster 集群元数据可以查看分区信息
* @return
*/
@Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { // 获取消息 String msgValue = value.toString(); // 创建 partition int partition; // 判断消息是否包含 Kafka if (msgValue.contains("Kafka")){ partition = 0; }else { partition = 1; } // 返回分区号 return partition; } // 关闭资源 @Override public void close() { } // 配置方法 @Override public void configure(Map configs) { }}
undefined
2.使用分区器的方法,在生产者的配置中添加分区器参数。
复制之前写的CustomProducerCallBack类,改成CustomProducerCallBackPartition

红色代码为添加部分
package com.kafka.producer; import org.apache.kafka.clients.producer.*; import org.apache.kafka.common.serialization.StringSerializer; import java.util.Properties; public class CustomProducerCallBackPartition { public static void main(String[] args) { //配置 Properties properties = new Properties(); //连接集群 properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092"); //指定对应的key和value序列化类型 properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.kafka.producer.MyPartitioner"); //创建Kafka生产者对象 KafkaProducerkafkaProducer = new KafkaProducer<>(properties); //发送数据 for (int i = 0; i < 5; i++) { kafkaProducer.send(new ProducerRecord<>("first", "Kafka" + i), new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception exception) { if (exception == null) { System.out.println("主题:" + metadata.topic() + " 分区:" + metadata.partition()); } } }); } //关闭资源 kafkaProducer.close(); } } ,>
3.在 hadoop102 上开启 Kafka 消费者。
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

4.在IDEA运行代码
查看hadoop102信息


同时IDEA打印信息,指定分区为0,因为消息包含Kafka
修改发送的信息为hello

运行代码
查看hadoop102信息

同时IDEA打印信息,指定分区为1,因为消息不包含Kafka

Original: https://www.cnblogs.com/hz-Master/p/16273069.html
Author: hz15968199950
Title: Kafka自定义分区器
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/222819/
转载文章受原作者版权保护。转载请注明原作者出处!