ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java数据类型与运算符:从基本类型到隐式转换与优先级

Java数据类型与运算符:从基本类型到隐式转换与优先级 数据类型和运算符很多人在入门编程时都会背一遍但真正能把这两块吃透的人真不多。我见过不少工作两三年的开发写代码时遇到隐式转换、短路逻辑、自增自减还是靠猜。这篇文章就以Java为主线把数据类型和运算符从头到尾捋一遍包括8大基本类型到底占多少字节、引用类型和值类型差在哪儿、常见运算符的底层行为、自动提升和强制转换的规则以及那些让你栽过跟头的优先级问题。适合刚学编程的朋友也适合准备面试或者写代码总被同事吐槽的老伙计。看完你至少能回答清楚为什么0.1加0.2不等于0.3为什么i和i经常把人绕晕为什么字符串拼接的加号和数字相加的加号不是同一个加号。1. 数据类型的本质为什么一门语言要区分类型1.1 类型在底层管的三件事很多初学者会觉得数据类型就是“int存整数String存字符串”这么简单但如果你只停在这一层后面遇到类型转换、泛型、序列化问题会非常难受。数据类型在一门语言里本质上是三件事的约定第一这个数据在内存里占多大空间第二这个空间的二进制位应该怎么解释第三这个数据能参与哪些运算。用生活里的事打比方数据类型就像是不同规格的收纳箱。byte是一个小药盒只能装下几颗药int是一个书包能装课本long是一个大行李箱能装更多东西但占地方。编译器看到数据类型才知道该给变量分配多大的抽屉抽屉里的二进制数按什么规则读取。同样是8个二进制位的“11111111”如果按byte解释是-1按char解释是65535差别巨大。这里还要补充一个关键点类型其实还决定了“你能对这个值做什么”。数字可以加减乘除字符串能拼接、截取、比较长度boolean只能参与逻辑判断。如果一门语言没有类型约束那么“1加a”这种操作就只能靠运行时猜非常容易出bug。所以“类型系统”不是故意给你找麻烦而是在编译阶段就把不合法的操作拦下来。1.2 基本类型和引用类型的核心差异Java里的类型表面上看有很多但底层只有两个阵营基本类型primitive type和引用类型reference type。基本类型就是8个byte、short、int、long、float、double、char、boolean。引用类型包括类、接口、数组、枚举等比如String、ArrayList这些都是。这两类最关键的区别一是存的位置二是赋值的语义。基本类型变量里直接存的就是值本身而引用类型变量里存的是一个地址相当于“指向堆内存某个对象的定位信息”。最直观的例子就是赋值操作int a 10; int b a; b 20; System.out.println(a); // 输出 10a没有被影响 StringBuilder x new StringBuilder(hello); StringBuilder y x; y.append( world); System.out.println(x); // 输出 hello worldx跟着变了这个例子我第一次写的时候也愣了一下为什么int赋值之后各走各的StringBuilder却像“联动”一样因为int赋值是拷贝值两个变量各是一份StringBuilder赋值拷贝的是引用x和y指向堆里同一个对象不管通过谁改对象都变了。理解这一点后再看方法传参就非常通透Java方法传参对基本类型是值传递对引用类型传的也是引用地址的副本所以方法内部改引用类型对象的属性会影响外部但给形参重新new一个对象则不影响外部。这套机制贯穿整个Java开发建议一定搞懂。2. Java的8大基本数据类型逐个拆解2.1 整数家族byte、short、int、longJava的整数类型有四个按字节数从小到大排byte占1字节short占2字节int占4字节long占8字节。字节数不同取值范围也不同。byte范围是-128到127short是-32768到32767int约正负21亿long约正负9千亿亿。日常开发int基本够用但遇到时间戳、金额分这种大数就要考虑long。有几个字面量的坑值得说。给long赋值时数值后面要加大写L或小写llong big 9999999999L; // 不加L会直接编译报错因为9999999999默认按int解析小写l视觉上和1太像我强烈建议统一用大写L。另外Java 7之后支持在数字里用下划线分隔比如1_000_000_000可读性提升很多但注意下划线不能放在数字开头、结尾或小数点附近。还要警惕一个经典溢出场景int max Integer.MAX_VALUE; // 2147483647 int result max 1; System.out.println(result); // 输出 -2147483648这不是bug是溢出。整数运算超出范围后会发生回绕正数加着加着变成负数。很多人第一眼看懵但只要你记得int是4字节定长存储最高位是符号位就不难理解。2.2 浮点家族float和double浮点类型只有两个float占4字节double占8字节。float的精度约7位有效数字double约15到16位有效数字。Java里所有带小数点的字面量默认都是double所以给float赋值必须加F或f后缀float price 1.1F; double total 2.5;如果写成float price 1.1;编译直接报错。这个很多人刚开始写程序都遇到。浮点数的经典问题就是精度丢失。0.1 0.2 0.3的结果是false因为十进制小数转成二进制时绝大多数小数是无限循环的比如0.1转二进制是无限循环小数计算机只能按52位尾数截断存储所以运算结果并不是数学意义的0.3。这事不是Java独有Python、JavaScript、C都一个样。所以我给的经验是凡是涉及金额、利率、物理测量值等需要精确计算的场景别用float和double直接用BigDecimal。如果不是精确计算只是做展示或图形坐标那么float/double完全够用。判断两个浮点数是否相等也别用而是看差的绝对值是否小于一个极小阈值比如Math.abs(a - b) 1e-6。2.3 char与boolean两个容易被忽略的类型char在Java里是16位无符号整数表示一个Unicode字符。它和C语言里的char不一样C的char是1字节Java的char是2字节所以一个char可以直接存中文字符char c 中;。因为char本质是数字它也能参与算术运算比如char c A;那么c 1等于66。这里有个很多新手都踩过的输出坑char c A; System.out.println(c 1); // 输出 66因为c被提升为int然后加1 System.out.println( c 1); // 输出 A1因为字符串拼接从左到右同样是加号第一种是先做数值运算第二种是字符串拼接结果完全不同。我后来习惯判断“表达式里有没有String类型出现”有就大概率是拼接没有就是数值运算。boolean更简单只有true和false两个值。但要注意Java的boolean不能和整数互相转换比如if (1)这种写法在Java里编译不过。这是Java和C/C很大的区别C里非0就是trueJava必须写if (flag)或if (flag true)。2.4 8大基本数据类型速查表类型字节数默认值取值范围/说明byte10-128 ~ 127short20-32768 ~ 32767int40-2147483648 ~ 2147483647long80L-9223372036854775808 ~ 9223372036854775807float40.0F约 ±3.4E387位有效数字double80.0D约 ±1.7E30815位有效数字char2\u00000 ~ 65535表示Unicode字符boolean不确定falsetrue / false这张表我在面试前背得滚瓜烂熟但真正开发时不用死记只要记住几个关键点long是8字节、float是4字节、char是2字节无符号、boolean默认false。值得留意的是boolean到底占几个字节没有硬性规定JVM规范没强制HotSpot里通常按1字节或4字节处理所以表里我直接写了“不确定”。3. 运算符体系从算术到位运算一次拆透3.1 算术运算符加减乘除、取余、自增自减算术运算符有 、-、*、/、%以及一元的和--。多数人觉得简单但有几个行为必须心里有数。第一整数除法直接截断小数部分不会四舍五入。7 / 2结果是3-7 / 2结果是-3注意这里是向零截断不是向下取整。很多需要四舍五入的场景直接写除号是错的。第二取余运算%的符号和左操作数保持一致。7 % 3结果是1-7 % 3结果是-17 % -3结果是1。这个规则在做循环下标、分页计算时一定要注意否则负数场景会得到意料之外的结果。第三自增自减的语义。i是“先用后加”i是“先加后用”。比如int i 1; int a i; // a1, i2 int b i; // i先变成3然后b3单独写i和i没区别但一旦放进表达式里就容易乱。我的原则是不要在复杂表达式里混合自增自减宁可拆成两行写清楚。代码是给人读的不是用来炫技的。3.2 赋值运算符与复合赋值藏在里的隐式转换赋值运算符是从右往左结合的这个容易理解。更要关注的是复合赋值运算符、-、*、/、%以及位运算的、|、^、、、。复合赋值看起来只是简写但有一个隐式强转行为很多人不知道short s 1; s s 1; // 编译报错s1结果是int不能直接赋给short s 1; // 编译通过为什么s 1不报错因为Java规范规定E1 op E2等价于E1 (T) ((E1) op (E2))也就是自动做了强转。所以s 1本质是s (short)(s 1)。这带来一个隐患如果算出来的结果超出short范围会静默溢出。比如short s 30000; s 30000; // 不报错但s的结果是负数因为溢出了我在实际代码审查里见过不少类似的坑建议大家使用复合赋值时多想想类型范围。再说一个容易被忽略的点a * b c展开是a a * (b c)不是a a * b c。复合赋值运算符的结合性决定了右侧整个表达式先算。3.3 比较运算符比较的是值还是地址比较运算符包括、、、、、!。前四个只能用于数值类型和!既可以用于基本类型也可以用于引用类型但语义完全不同基本类型比较的是值引用类型比较的是栈里的地址也就是“两个引用是否指向同一个对象”。最典型的坑就是字符串比较String a hello; String b hello; String c new String(hello); System.out.println(a b); // true因为字面量字符串在字符串常量池里复用了同一个对象 System.out.println(a c); // false因为c是new出来的新对象地址不同 System.out.println(a.equals(c)); // true内容相同所以判断字符串内容是否相同永远用equals不要用。包装类型也有类似问题Integer在-128到127之间有缓存Integer a 100; Integer b 100; a b是true但超过127就new了不同对象会变成false。这种“有时真有时假”的诡异问题最让新手头疼。统一做法是包装类型用equals比较或者转成int再比。3.4 逻辑运算符短路与不短路逻辑运算符有、||、!以及不怎么常用的、|、^用于boolean时。和||支持短路求值如果左边已经能确定整个表达式的真假右边就不再执行。短路在空判断上特别实用if (obj ! null obj.length() 0) { // 如果obj为null右边不会执行不会抛NullPointerException }如果把换成右边照样会执行obj.length()就会空指针。反过来也有场景必须用不短路的当右边表达式有副作用比如必须执行某个赋值或计数操作时但这种情况太少见了而且代码可读性差我的经验是有这种需求就拆开写一行一个操作。还有一个容易忽略的点的优先级高于||。表达式a || b c等价于a || (b c)。实际写的时候我会加括号避免别人跟我抬杠。3.5 位运算符与移位运算符看着高级但很实用位运算符处理的是整数类型的二进制位包括按位与、|按位或、^按位异或、~按位取反以及左移、右移、无符号右移。位运算最经典的实际用途有几个。一是判断奇偶(n 1) 0是偶数(n 1) 1是奇数。二是乘除2的幂x 1等价于x * 2x 1等价于x / 2但注意这些只对正整数无副作用负数右移会保留符号位。三是从一个整数里提取某几位比如处理ARGB颜色时用(argb 16) 0xFF取红色通道。移位这里有个容易踩的坑移位量会对位数取模。对int来说实际移动的位数是右边操作数对32取模对long来说是对64取模。所以int x 1; System.out.println(x 35); // 实际等价于 1 (35 % 32) 1 3 8如果你确实想移动35位得先把x转成long再移位。这个坑我在协议解析的代码里踩过排查半天才找到原因。还有一个频繁被人问的位运算用法不用临时变量交换两个整数。int a 5, b 7; a a ^ b; b a ^ b; a a ^ b;原理是异或的“对称性”x ^ y ^ y等于x。这个技巧面试偶尔考但日常开发不建议用因为可读性太差直接写临时变量更清楚。3.6 三元运算符简单但也有类型规则三元运算符条件 ? 表达式1 : 表达式2本质是个表达式有返回值。用得好代码很简洁比如int max a b ? a : b;。但用不好就是坑。第一个坑是三元表达式有类型提升规则。如果两个分支类型不同结果会取两者之间的更大类型。比如int x true ? 1 : 2.0; // 编译报错因为结果类型是double第二个坑是拆箱空指针。类似Integer val null; int y val ! null ? val : 0; // 这个还好 int z flag ? null : 1; // 如果flag为truenull拆箱成int时抛NullPointerException三元表达式的结果若是包装类型赋给基本类型时会自动拆箱null一拆就炸。业务上遇到这种写法我会直接拆成if-else至少一眼能看出风险。4. 类型转换自动提升、强制转换与经典陷阱4.1 自动类型转换谁都能往大范围转吗自动类型转换发生在“小范围转大范围”时编译器偷偷帮你转了。Java的自动转换方向是byte - short - int - long - float - double char - int - long - float - double注意long虽然是8字节float只有4字节但long依然可以自动转成float因为float的取值范围远比long大。代价是可能丢失精度这是语言规范允许的。在表达式中类型提升规则是两个操作数中范围大的那个决定结果类型。比如int long结果是longint double结果是doublelong float结果是float。如果参与运算的是byte、short、char它们会先提升为int再计算所以byte byte结果是int。这带来的最大影响就是小类型变量运算后的赋值问题byte b1 10; byte b2 20; byte b3 b1 b2; // 编译报错因为b1b2结果是int byte b4 10 20; // 编译通过因为10和20是常量编译器常量折叠后知道结果是30在byte范围内这个例子特别经典。常量之间的运算编译器能算出结果并判断是否溢出所以可以赋给byte变量之间的运算编译器不做运行时判断直接按提升后的类型处理。我经常用这个例子教新同事理解“编译期常量”和“运行期变量”的差别。4.2 强制类型转换强转一时爽精度火葬场强制类型转换也叫窄化转换语法是在变量或表达式前面加括号类型。当你确定要“大范围转小范围”时必须显式强转比如int x (int) someDouble;。但强转有几个后果必须清楚。第一是整型溢出int big 128; byte small (byte) big; // 结果是-128因为128超出byte范围二进制回绕了第二是浮点转整型会直接截断小数部分不是四舍五入int x (int) 3.99; // 结果是3不是4第三是double转float可能得到无穷大float f (float) 1e300; // 超出了float范围结果是Infinity所以强转之前最好先做范围判断特别是处理外部传入的数值时尽量不要直接转完才后悔。还有一点容易搞错强转的优先级。(int) x y只强转了x然后x和y做加法(int)(x y)是先把x和y相加再强转。括号差一个结果可能天差地远。位运算里也常见(byte)(value 8)这种写法括号一定要配好。4.3 字符串拼接的隐式转换加号的重载Java里号比较“人格分裂”两边都是数值时做加法任何一边出现String时做字符串拼接。拼接的过程会把另一边转成字符串这就是隐式转换。经典面试题System.out.println(1 2 a); // 输出 3a System.out.println(a 1 2); // 输出 a12原因是表达式从左到右求值。第一个先算123再算3a变成3a第二个先算a1变成a1再算a12变成a12。所以不是所有数字都会被拼成字符串关键看执行顺序。这个特性我在拼日志、拼SQL条件时经常用到。但我不建议用“字符串加数字”实现复杂的格式化因为可读性太差。用String.format或者Java 15的文本块更清晰。另外把一个数字转成字符串的快速写法 num能用但性能不咋地高频循环里我更推荐String.valueOf(num)。4.4 包装类型与parse方法别把强转当万能有人会把(int) obj和Integer.parseInt(str)混在一起理解其实完全不是一回事。强转作用于数值类型之间parse方法作用在字符串和包装类型之间。例如字符串123转成int(int)123是不行的必须用Integer.parseInt(123)。想要包装类型则用Integer.valueOf(123)这个方法还会走缓存-128到127范围内的对象是复用同一个实体的。还有一个高频bug包装类型和基本类型之间自动拆箱null拆箱必崩。Integer num null; int n num; // 编译能过运行抛NullPointerException这个崩溃发生在赋值那一行但根因往往是上游把null塞进Integer对象了。我的建议是如果方法可能返回null包装类型先判空再拆箱别图省事直接塞给基本类型变量。5. 运算符优先级与结合性从死记到理解5.1 一张优先级速查表看懂大多数代码很多开发对优先级的态度是“遇到过再说”但真遇到时往往已经晚了。我整理了Java主要运算符的从高到低优先级优先级运算符结合性1后缀、后缀--、()、[]从左到右2前缀、前缀--、!、~、一元、一元-、(强转)从右到左3*、/、%从左到右4、-从左到右5、、从左到右6、、、、instanceof从左到右7、!从左到右8从左到右9^从左到右10|从左到右11从左到右12||从左到右13?:从右到左14、、-等复合赋值从右到左看完这张表你会发现移位运算优先级低于加减法位与低于相等判断逻辑与又低于位或。这里最反直觉的是移位1 2 3会先算23再移位结果是1532而不是8。很多人第一眼都会错。我的习惯是凡是混合了位运算、移位、算术的表达式一律加括号。括号不丢人丢了优先级判断出错才丢人。5.2 表达式求值中的几个致命陷阱优先级只是一个维度求值顺序是另一个维度。Java规定了操作数从左到右求值但复杂表达式依然可能被玩坏。先看这个int i 0; int j (i) (i); System.out.println(j); // 1i变成2解释是按顺序执行第一个i先返回0i变1第二个i返回1i变2最后011。看着合理但如果你写成j i i结果依赖左右操作数的求值顺序代码可读性极差。我的原则是一个表达式里最多出现一次自增或自减最好干脆独立成语句。再看两个很经典的判空判断// 错误str可能为null直接调用equals会NPE if (str.equals(ok)) { } // 正确常量字符串调equals反过来写str为null也不会NPE if (ok.equals(str)) { }这个虽然和优先级无关但属于表达式写法里最常见的坑之一。另外if (a 1)这类错误在C语言里能编译但逻辑错在Java里直接编译不过因为赋值表达式的结果是int而if要求boolean。这对Java程序员来说反而是保护。5.3 优先级记忆经验别背表用习惯我从来不背整张优先级表就记一句话单目运算 算术 移位 关系 相等 位运算 逻辑 三元 赋值。然后用括号解决所有不确信的场景。有些人会觉得加括号“不够专业”其实恰恰相反。代码最重要的读者是人和三个月后的自己。复杂表达式不加括号等于逼着下一任维护者去查表。代码审查时我看到类似if (flag 0x1 1)这样的写法会直接要求改成分步判断不是说它一定会错而是这种代码没法让人一眼确认意图。6. 跨语言对照与面试高频考点6.1 其他语言的数据类型和Java有什么不同离开Java很多语言对类型的处理方式差异很大。JavaScript里只有number一种数值类型统一按64位浮点存储所以没有int溢出问题但位运算会把number先转成32位整数结果再转回number所以JS的位运算没法处理超过32位的数值。Python的int是任意精度整数跑几千位的数都不会溢出代价是性能比不上Java的int。Python的True和False本质上是int的子类True等于1、False等于0所以True 1是合法的这在Java里完全不可想象。PHP的弱类型特性导致比较运算符有很多自动转换的“惊喜”比如10 10是true但10 10是false所以PHP里必须用做强比较。MySQL里的数据类型则侧重存储和索引INT占4字节BIGINT占8字节DECIMAL用于金额高精度场景选错类型可能让索引膨胀甚至计算出错。Redis的数据类型更像“数据结构”String、List、Hash、Set、ZSet每一种底层编码方式都不同存大对象之前要搞清楚它到底用的是raw还是ziplist/quicklist编码。这里多说一句Modbus工业协议里经常看到“保持寄存器默认长度是16位”的说法两个寄存器拼一个32位浮点或长整型很常见。数据类型长度在不同的技术领域里都是底层关键不是Java独有的问题。6.2 高频面试题看你会不会踩坑面试里关于数据类型和运算符的问题翻来覆去就是那几道但答对率并不高。最常见的有第一float f 1.1;错在哪。答案是1.1是double字面量不能隐式转成float要写1.1F。第二short s 1; s s 1;和s 1;哪个对。答案是前者编译报错后者编译通过原因是自带强转。第三0.1 0.2 0.3结果是true还是false。答案是false浮点数二进制存储导致精度丢失。第四字符串拼接的System.out.println(a 1 2)输出什么。答案是a12因为从左到右执行先拼接成字符串。第五Integer a 100; Integer b 100; a b结果是什么。答案是true因为有缓存改成1000就是false。遇到这种题直接点出包装类型缓存机制和比较地址的本质基本就稳了。第六位运算判断一个数是不是2的n次幂(n (n - 1)) 0。原理是2的n次幂二进制只有一位是1减一之后后面全变1与原数按位与必然为0。这些题目看着简单但背后考的都是对类型存储和运算符语义的底层理解建议认真过一遍。7. 我在实战中的几条避坑心得最后分享几条我踩过坑后总结出来的实操经验。第一条金额计算一律用BigDecimal或者整数存储的最小单位不要用double。我在一个账单系统里见过两位小数对不上账查了一天最后发现是double累加误差改成长整型“分”存储后问题消失。第二条做数值运算前先想清楚结果类型。比如int a 1000000000; long b a * 2;你以为结果是2000000000?不20亿还在int范围内所以能算出结果但如果再乘个10结果直接溢出变成负的。想要正确就写long b (long) a * 2;先把a转long再做乘法。第三条字符串和包装类型判空后再用。equals用常量调Integer拆箱前先判断null这是我写工具方法时的固定习惯。第四条位运算表达式一定加括号。我见过flag 0xF 0xF这种写法因为优先级高于实际执行的是flag (0xF 0xF)最后变成flag 1逻辑完全跑偏。这种bug极其隐蔽代码审查都不容易发现。类型和运算符是编程的基石但恰恰是最容易被“自以为懂”的部分。这篇文章偏基础先把数据类型和运算符的各种行为讲透下一部分可以接着聊表达式求值、泛型擦除和包装类型的深水区。如果你在项目里遇到过类似的诡异问题欢迎交流我踩过的坑可能比你想的还多。
返回列表