很多刚开始学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做了一堆“环境净化”。我用一个表格列出最常见的差异:
| 能力 | Java | C |
|---|---|---|
| 获取类型大小 | 没有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取余,倒序排列”。比如45:
45 ÷ 2 = 22 余 1
22 ÷ 2 = 11 余 0
11 ÷ 2 = 5 余 1
5 ÷ 2 = 2 余 1
2 ÷ 2 = 1 余 0
1 ÷ 2 = 0 余 1
倒过来读就是101101,所以45的二进制是101101。这个方法虽然笨,但一定要亲手推几遍,因为在推导过程中你会慢慢感知到“位”的重量。
二进制和十六进制转换更简单:每4位二进制对应1位十六进制。比如1101 0101,从右往左每四位分组,1101是D,0101是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是-1,1000 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 << 32,long类型取低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是八进制,等于10;0x10是十六进制,等于16;0b10是二进制,等于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把结果打出来看一眼。尤其在二进制、十六进制、补码之间来回倒的时候,这种“眼见为实”的训练特别培养直觉。看懂打印结果的那一刻,很多基础题都不用死记,因为底层逻辑你已经握在手里了。