Java操作符、进制转换与补码详解:从基础到面试避坑

发布时间:2026/10/13 11:13:03
Java操作符、进制转换与补码详解:从基础到面试避坑
很多刚开始学Java的朋友看到“操作符”这三个字就觉得简单结果一写代码照样翻车。不是加号算错而是不知道整数除法会丢精度不是不会写if而是搞不懂和到底差在哪更别提进制转换和原反补码简直是面试恐惧症的起点。更麻烦的是不少人是带着C语言的基础来学Java的总觉得语法差不多踩坑之后才发现这俩语言在操作符底层的思路其实有很多关键差异。我写Java十几年也带过不少新人发现一个规律操作符分类、进制转换、原反补码这三块东西看似基础但没吃透的人后面学位运算、网络协议、数据序列化、阅读JDK源码都会吃力。所以这篇文章就把这三部分集中讲透并把Java和C的操作符差异放在一起对照让你不是“背概念”而是真正理解底层逻辑顺便把面试笔试里最常出现的坑一起趟平。1. 操作符分类先搞清楚Java到底有哪些“零件”可以用1.1 按功能拆解不只有加减乘除很多初学者会把操作符简单理解成“加减乘除”但实际写代码时就会发现为什么a b有时候是逻辑判断有时候又是位运算为什么i和i结果不一样原因很简单Java的操作符种类很多而且同一个符号在不同场景下会有完全不同的含义。Java操作符按功能可以分成这么几大类类型包含操作符典型用途算术操作符 - * / %基本数值计算取余数一元操作符 - -- ! ~ (type)正负号、自增自减、逻辑取反、按位取反、强制类型转换关系操作符 ! instanceof比较大小、判断相等、判断类型逻辑操作符 || !布尔逻辑与、或、非位操作符 | ^ ~ 按位与、或、异或、取反、移位赋值操作符 - * / % | ^ 赋值以及复合赋值条件操作符? :三目运算简洁的if-else注意一个问题和|既是位操作符也能作为逻辑操作符使用。如果两边是boolean类型它们就是非短路逻辑运算如果两边是整数它们就是按位运算。很多人在这里栽过跟头后边我会专门讲。至于一元操作符是后来Java面试里特别爱考的一类。比如i是先加后用i是先用后加但如果在表达式里连续两次使用同一个变量结果就会变得非常微妙。先把这些分类记住后面拆解才不容易乱。1.2 Java和C在操作符上的先天差异说实话Java操作符和C操作符表面上看挺像但仔细对照你会发现Java做了一堆“环境净化”。我用一个表格列出最常见的差异能力JavaC获取类型大小没有sizeof因为类型大小固定有sizeof类型大小依赖平台字符串连接可以拼接字符串不可以字符串本质是字符数组布尔类型boolean是独立类型不能当整数用没有真正的布尔类型0/非0就是真假取地址和解引用没有和*这类指针操作有指针取地址*解引用运行时类型判断有instanceof没有无符号右移有没有移位距离越界位移数对int取低5位对long取低6位移位距离大于等于类型位数是未定义行为看到这里你应该理解一件事Java是“故意”把C里容易出问题的地方改掉了。比如C的int在32位平台上是4字节在16位远古平台可能是2字节所以C程序员至少要会sizeof来确保内存布局。而Java为了跨平台一致性规定int永远4字节long永远8字节所以直接取消sizeof这类操作符。这带来的影响不只是“少了个关键字”那么简单。比如C里你写char c 65;然后if(c)它是合法的因为C把非0整数当真。但Java里if(c)直接编译失败必须写成if(c ! 0)。这种差异会直接影响你从C转到Java时的编码习惯也反过来解释了为什么Java面试题总喜欢拿“短路与”和“位与”来考基础。2. 进制转换从二进制的世界观重新看数字2.1 为什么工程师一定要懂进制计算机内部所有数字最终都是二进制的0和1。十进制只是给人看的“外壳”。你可能觉得“我平时写int a 10;就行管它底层是啥”但一旦涉及位运算、网络报文解析、文件格式、内存对齐甚至调试一个奇怪的异常不懂二进制就会两眼一抹黑。举个最常见的场景你要把一个IP地址192.168.1.10转成32位整数就要把每个十进制段转成8位二进制然后拼起来。再比如颜色值#FF7F50本质上就是三个字节的十六进制表示。这些如果平时没接触过进制转换压根不知道从哪下手。所以我的建议是不要把进制转换当成考试内容而是当成一种“翻译能力”。二进制的位长虽然长但借助十六进制和八进制可以大幅缩短表达。一个16进制位正好对应4个二进制位所以0x1F一眼就能看出是0001 1111这比写一堆0011111舒服多了。2.2 手工转换和Java/C里的字面量表示先说手工转换这是必须练的。二进制转十进制最简单按权展开就行1101 1×2³ 1×2² 0×2¹ 1×2⁰ 8 4 0 1 13十进制转二进制用“除2取余倒序排列”。比如4545 ÷ 2 22 余 122 ÷ 2 11 余 011 ÷ 2 5 余 15 ÷ 2 2 余 12 ÷ 2 1 余 01 ÷ 2 0 余 1倒过来读就是101101所以45的二进制是101101。这个方法虽然笨但一定要亲手推几遍因为在推导过程中你会慢慢感知到“位”的重量。二进制和十六进制转换更简单每4位二进制对应1位十六进制。比如1101 0101从右往左每四位分组1101是D0101是5结果就是0xD5。反过来十六进制的0x9A拆成1001 1010直接展开即可。再看代码里的表示方式。Java里这样写int a 0b1010; // 二进制Java 7开始支持 int b 012; // 八进制前导0 int c 0x1F; // 十六进制前导0xC语言里传统标准只保证八进制和十六进制int a 012; // 八进制 int b 0x1F; // 十六进制 int c 0b1010; // C23之前是编译器扩展GCC/Clang支持MSVC不支持这里就有一个经典坑System.out.println(012);输出的是10不是12。因为012在Java里是八进制字面量等于十进制的10。C语言同样如此。凡是看到数字开头带0都要立刻警惕不要把它当普通十进制看。Java 7还有一个很实用的特性数字可以加下划线方便阅读int money 1_000_000; int flag 0b1010_1100;这个下划线只在数字字面量里合法不能放在开头和结尾也不能连续放两个。用它来分隔十进制长数字和二进制位段非常舒服。2.3 实用转换工具和踩坑提醒我平时写代码最常用的转换工具就是Integer.toBinaryString和Integer.parseInt。比如System.out.println(Integer.toBinaryString(5)); // 101 System.out.println(Integer.parseInt(1101, 2)); // 13 System.out.println(Integer.parseInt(FF, 16)); // 255Integer.parseInt可以把指定进制的字符串转成int第二个参数就是进制基数。这个在面试题里经常出现比如“把二进制字符串转成十进制整数”这种题本质上就是在考parseInt和进制原理。还有一个小提醒Integer.toBinaryString(-1)会输出32个1因为int的-1在内存里是全1。这并不是bug而是补码机制造成的。说到补码接下来这部分是真正的重头戏。3. 原码、反码、补码整数在计算机里到底怎么存的3.1 先从原码说起我们知道一个整数在计算机里需要用二进制表示但要区分正负最朴素的想法就是“加一个符号位”。原码就是这么来的最高位是符号位0表示正1表示负其余位表示数值绝对值。拿8位二进制举例5的原码0000 0101-5的原码1000 0101这样看起来很好理解但实际用起来全是问题。第一0是0000 0000-0是1000 0000同一个0有两种表示方法很浪费。第二做减法的时候很麻烦因为计算机没有“减法器”只有“加法器”你必须把减法转成加法而直接用原码相加结果往往是错的。比如1 - 1直接用原码0000 0001 1000 0001 1000 0010结果等于-2完全错误。所以需要一套更聪明的编码方案这就是反码和补码存在的意义。3.2 反码与补码的推导逻辑反码的出现是为了解决原码做减法问题。正数的反码和原码一样负数的反码是“符号位不变其余位按位取反”。5反码0000 0101-5反码1111 1010但反码还是没解决0和-0重复的问题而且用反码做减法结果如果出现“溢出进位”还需要额外加回来麻烦。真正一步到位的是补码。补码的规则也很简单正数的补码不变负数的补码是“反码加1”。用8位举例-5的补码就是1111 1010 1 1111 1011。但你如果只是背这个规则很快会忘。我更推荐你用“模运算”的角度去理解。8位二进制最多能表示256个数从0000 0000到1111 1111这就相当于一个只有256个刻度的时钟。在数学里一个数的相反数可以理解为256 - 这个数。所以-1的补码就是256 - 1 255转成二进制是1111 1111。-5的补码就是256 - 5 251二进制是1111 1011。这样一解释你再看补码运算5 (-5)也就是0000 0101 1111 1011 1 0000 0000丢到最高位的进位后8位结果是0000 0000刚好是0。这个思路特别重要因为补码并不是“为了和原码对着干”而是为了让加减法统一成加法运算并且让0只有一种表示。补码表示下8位能表示的范围是-128 ~ 127多出来的那一个负数就是-128。3.3 溢出和边界值为什么byte的范围是-128到127很多Java面试题喜欢问“byte取值范围为什么是-128到127而不是-127到127”答案就在补码里。8位无符号整数能表示0~255。如果我们要同时表示正负就要把256个编码拆成两半。前面说了补码方案中0用0000 0000表示正数从0000 0001到0111 1111也就是1~127剩下从1000 0000到1111 1111表示负数。1111 1111是-11000 0001是-127那1000 0000呢它不能白白浪费于是规定它就是-128。所以在Java里byte b (byte) 0b10000000; // -128 System.out.println(b); // -128而补码溢出的表现也很有名byte b 127; b 1; System.out.println(b); // -128这个结果不是“乱来”而是补码加法定律0111 1111加1变成1000 0000正好是-128。C语言里char类型如果char被实现为signed char也会有同样的表现但char到底有无符号由平台决定这就是Java明确byte是带符号类型的价值。理解补码之后再回头看位运算很多问题就豁然开朗。比如~5为什么等于-6因为5的补码是0000...0101按位取反变成1111...1010这个位模式对应的补码值就是-6。看不懂位操作时先老老实实画一遍补码比死记结果好一百倍。4. Java与C的操作符对比代码行为差异的根源4.1 类型系统如何影响运算结果Java是强类型语言且每种整数类型的字节数固定。byte是1字节short是2字节int是4字节long是8字节。这带来一个重要规则byte、short、char在做算术运算时会先提升到int再进行计算。所以Java里这么写会编译报错byte a 1; byte b 2; byte c a b; // 编译错误a b 的结果是int必须写成强转byte c (byte)(a b);C语言里其实也有“整数提升”integer promotion但规则比较宽松。char参与运算时会被提升到int但赋值回去时不强制要求强转最多产生一个警告。这是Java比C严格得多的地方目的就是防止精度丢失。这个差异看起来只是“编译过不过”实际上会直接影响你的编码习惯。Java新手最容易犯的错之一就是明明算的是byte结果中途被提升成int最后赋值失败然后一脸懵。4.2 短路逻辑、移位、自增自减这些操作符的“脾性”不一样先看短路逻辑。Java的和||是短路操作符左边的结果已经能决定整个表达式的真假时右边的表达式不会执行。和|则不然它们会老老实实把两边都算完而且如果操作数是整数那做的是位运算。C语言里也有和区别类似但因为C没有独立boolean类型所以你能写出if(a 1)这种代码而Java里1不能直接当true用。这又回到了类型系统的差异。再看移位。Java有无符号右移C语言没有。对有符号数Java的是算术右移也就是高位补符号位C标准并没有强制规定高位补什么但主流平台几乎都是算术右移。这个“几乎”就是很多跨平台C代码的隐患。Java为了抹平平台差异直接提供了补0移动非常省心。还有一个经典坑Java里1 32的结果不是0而是1。因为Java对int类型的移位距离只取低5位32 0x1F等于0所以相当于1 0。如果换成1L 32long类型取低6位结果就是正确的0x100000000。C语言里1 32是未定义行为很多编译器会警告但结果完全看平台心情。这个区别面试里只要考到就是送分或者送命题。再说自增自减。Java和C的i/i在使用上整体区别不大但有一个经典表达式行为完全不同int i 0; i i; System.out.println(i); // Java中是0为什么是0因为Java会把i的先使用值0存起来然后i变成1最后再把存起来的0赋给i。C语言里i i;这个表达式直接是未定义行为编译器输出什么都是合法的。这种题在Java面试中很常见你要做的不是“猜”而是理解“先使用再自增”的求值顺序。4.3 字符串、引用与Java比C多出来的“身份问题”Java里能连接字符串这是C语言做不到的。更值得警惕的是。Java对引用类型比较的是“是不是同一个对象”而不是“内容是否相等”。所以String s1 new String(hello); String s2 new String(hello); System.out.println(s1 s2); // false System.out.println(s1.equals(s2)); // trueC语言里你如果写char* s1 hello; char* s2 hello;然后if (s1 s2)比较的也是指针地址而不是字符串内容。这一点Java和C倒是一致但很多从C转Java的人会因为“字符串就是数组”的惯性误以为能比内容结果踩坑。Java还多了一个instanceof操作符用于判断一个对象是不是某种类型。C语言没有这种安全类型检查只能靠各种“手工判断”。这一类操作符差异本质上反映了两门语言在设计哲学上的不同Java把更多“基建”做进语言里宁可牺牲一点自由度也要让你少出错。5. 常见问题与“一眼看出错”的排查技巧5.1 面试和笔试高频坑直接背结论也要懂原理这里整理几个我在带新人过程中反复被问到的题目每道都是经典中的经典。第一题System.out.println(012 0x10 0b10);输出多少先拆解012是八进制等于100x10是十六进制等于160b10是二进制等于2。总数是28。丢分的人基本都是把012当成了12。第二题-5 % 2结果是几Java里取余结果的符号跟被除数相同所以-5 % 2的结果是-1不是1。C语言从C99开始也是-1但C89实现相关。如果你写算法题时假设结果永远是正数会出大问题。第三题int i 0; i i;最终i是多少Java答案是0。记住一句话后置自增在表达式里返回的是“自增前的旧值”然后才把自增后的新值写回变量但紧接着整个变量又被旧值覆盖所以等于没自增。第四题1 32等于多少Java里int的左移只看低5位所以1 32等于1 0等于1。要移满32位得用long或者分两次移。C语言里这种写法是未定义行为面试时如果对方考你C记得先说“这是未定义行为不能依赖”。第五题byte b 127; b 1;和b b 1;有什么不一样b 1;是复合赋值Java编译器会做隐式强转所以可以编译通过运行结果为-128。而b b 1;中b 1是int类型直接赋给byte会编译失败。这个陷阱特别适合区分新手到底有没有理解“类型提升”。第六题1 2 3和1 2 3结果一样吗不一样。第一个是123因为从左往右先拼接字符串后续全是字符串连接。第二个是1 2先算出3再拼成33。这题考的是操作符结合性和字符串连接的优先级。第七题和到底什么区别是短路逻辑与左边为false右边不执行。如果操作数是boolean是非短路逻辑与两边都会执行如果操作数是整数是按位与。面试官问你“写出a b和a b的区别”你要把“短路”和“按位”这两个维度都答出来才算完整。5.2 排查技巧用补码眼光看位运算结果有一个特别实用的排查套路如果你发现某个位运算结果跟自己预期不符不要瞪着十进制数字发呆先把十进制数字转成二进制补码再看一遍运算过程。我举个高频例子System.out.println(~5); // -6如果你不知道补码会觉得“~5”大概是“取反后变成-5”这完全错了。5的二进制补码是0000...0101取反后是1111...1010按照补码解读最高位是1负数先减1得到1111...1001再取反得到0000...0110绝对值6所以结果是-6。这个“减1再取反”反推绝对值的方法解决位运算问题特别好用。遇到负数相关的位运算第一步永远是写补码不要直接口算。另外用javap -c反编译字节码也能看到底层细节。比如你对i和i有点含糊写个小类然后反编译会看到它们底层字节码指令的区别一个先赋值加一一个先加一再赋值。看得多了很多概念就不再是死记硬背了。最后再分享一个我自己的习惯每学一个新操作符都拿Integer.toBinaryString把结果打出来看一眼。尤其在二进制、十六进制、补码之间来回倒的时候这种“眼见为实”的训练特别培养直觉。看懂打印结果的那一刻很多基础题都不用死记因为底层逻辑你已经握在手里了。