尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java进制转换全解析:从二进制到十六进制的原理与实战

Java进制转换全解析:从二进制到十六进制的原理与实战 写了好多年Java面试过别人也被别人面试过几乎每次聊到基础都绕不开一个看起来人畜无害的话题进制。你以为你懂问一句“十进制255转二进制等于多少”能秒答再问“十六进制字符串怎么转成浮点数”就开始卡壳再问“为什么Integer.toHexString(-1)输出的是ffffffff”很多人当场就懵了。这篇就把Java四种进制——二进制、八进制、十进制、十六进制——从底层原理讲到实操代码再把你可能在面试和线上日志里踩过的坑挨个扒一遍。适合正在学Java基础的同学也适合准备跳槽面试的开发者文章里所有示例都是可以直接复制跑起来的看完你就能把进制这块彻底焊死。1. 先搞清楚四个进制到底在解决什么问题1.1 计算机为什么偏偏选中了二进制很多同学第一次接触进制时最大的疑问是人明明用的是十进制为什么计算机偏偏要选二进制这个问题的答案不在软件层而在硬件层。工程实践里最稳定的电子器件只有两种状态高电平和低电平对应1和0。你可以把一根电路想象成一个开关要么开要么关不存在“半开半关”这种模糊状态。晶体管也好存储器也罢本质都是在维护这样的一堆0和1。如果非要造一个能稳定表示0到9十种状态的电子元器件带来的直接后果是制造精度要求指数级上升、成本暴涨、可靠性大幅下降。电子领域有个基本判断标准状态越少越不容易出错这是二进制胜出的根本原因。再从逻辑层面看布尔代数里的“与、或、非”天然就建立在真假之上而真假恰好可以映射成1和0。也就是说二进制的运算规则可以直接用电路实现不需要额外的翻译层。计算机里最底层的加法器、计数器、移位寄存器全部是围绕0和1设计的。所以说白了二进制不是“被选中”而是物理世界和逻辑世界交汇处唯一的最优解其他进制都是为了让人类读起来更舒服才存在的。1.2 八进制和十六进制是来“翻译”二进制的有人会问既然计算机只认二进制那为什么Java代码里还要有八进制、十六进制它们存在的意义可以概括成一句话二进制对人类太不友好了八进制和十六进制是二进制的“速记符号”。举个例子十进制255写成二进制是11111111看着还行但如果是65535呢二进制是111111111111111116个1人眼扫过去就晕了。在早期的汇编和底层调试场景里程序员的寄存器值、内存地址、机器指令全是这种长串01出错概率极高。这时候八进制和十六进制的作用就显现了因为8等于2的三次方16等于2的四次方所以二者跟二进制之间存在“一位对多位的直接映射”。每1位八进制数正好对应3位二进制数每1位十六进制数正好对应4位二进制数。比如二进制11111111从右往左按4位分组就是1111 1111每组对应十六进制F F所以就是0xFF按3位分组就是11 111 111对应3 7 7也就是八进制的0377。这种映射不需要计算直接查表就能翻译比“先把二进制转成十进制再转成八进制”快得多。那为什么不用十进制来当二进制的翻译官呢原因很简单10不是2的整数次幂一位十进制数无法固定对应整数个二进制位转换必须经过真实的数学运算没法一眼看出来。所以工程上基本形成了共识面向人的底层数据展示要么用八进制要么用十六进制调试效率高得多。你在Java源码里看到0x7fffffff、在日志里看到0xCAFEBABE这种魔数全是十六进制的功劳。1.3 Java中的十进制只是“人的习惯”你可能会想Java代码里到处写的都是int a 10;这不是十进制吗没错源码层面的字面量是十进制但那只是给人看的。Java编译器负责把你的十进制字面量翻译成二进制补码JVM运行时内存里存的也全部是二进制位序列根本不存在“十进制存储”这个概念。所以你可以把十进制理解成一层“显示协议”System.out.println(255)能输出255是因为println内部把内存里二进制的11111111转成了十进制字符串再打到屏幕上。Integer.toString、字符串拼接、日志框架都在做同样的翻译工作。理解这一点非常重要尤其是在分析网络协议、二进制文件、加密数据时你面对的本质是一堆字节怎么把它们解释成十进制、十六进制、浮点数完全取决于你调用的转换工具。后面讲的所有坑基本都是这层“解释”出了偏差。2. Java里四种进制的表示法与核心API2.1 字面量怎么写0b / 0 / 0x 前缀Java里写整数字面量时可以用前缀直接指定进制这是最基础的语法但也是很多人写错的第一站。int bin 0b1010; // 二进制值是10 int oct 012; // 八进制值是10注意这个“0”不是数字零后面的字母o int dec 10; // 十进制 int hex 0xA; // 十六进制值是10这里有几个非常容易踩的点。第一八进制字面量是以数字0开头不是小写字母o开头所以012是十进制的10不是12。第二从Java 7开始才支持0b前缀的二进制字面量如果你在维护一些老代码或者面试时写伪代码务必记清这个时间点。第三八进制里永远不能出现8和9这两个数字因为八进制只有0到7所以int x 08;会直接编译报错。我在网上看过不少新手把日期里的月份“08”直接拼进八进制字面量编译一报错就开始懵其实就是这个原因。十六进制前缀0x大小写都可以0XA和0xa合法。二进制前缀0b同理。另外补充一句Java里浮点数字面量没有进制前缀这回事0x1.8p1这种写法是十六进制浮点字面量属于Java 5引入的特殊语法日常开发极少用到新手可以先不理会免得混淆。2.2 进制转换的官方API清单很多同学一遇到进制转换就想着自己写算法其实JDK已经提供了相当完整的一组工具方法。下面这张表我建议直接收藏方法作用示例Integer.toBinaryString(int)转成二进制字符串Integer.toBinaryString(10)返回1010Integer.toOctalString(int)转成八进制字符串Integer.toOctalString(10)返回12Integer.toHexString(int)转成十六进制字符串Integer.toHexString(10)返回aInteger.parseInt(String, int radix)按指定进制把字符串解析成intInteger.parseInt(1010, 2)返回10Integer.valueOf(String, int radix)功能同上返回包装类型Integer.valueOf(A, 16)返回10Integer.toString(int, int radix)按指定进制把整数转成字符串Integer.toString(10, 2)返回1010这里有几个细节值得注意toHexString输出的字母是小写的a到f而且不带0x前缀toOctalString、toBinaryString同样不带前缀。反过来parseInt要求字符串里不能带前缀它只认纯数字序列。比如说Integer.parseInt(0xFF, 16)一定会抛NumberFormatException因为0x里的x不是合法的十六进制数字。这一点很多人踩过坑后面专门展开。另外Long类也有一套完全对称的方法Long.toBinaryString、Long.parseLong(s, radix)等处理64位数据时用得上。如果你做协议解析经常会把8个字节拼成long再转十六进制这套API要熟。2.3 从源码理解toString与负数的真相如果只看API说明你会以为Integer.toBinaryString(-1)输出的是11111111111111111111111111111111是因为“负数取绝对值转进制前面加负号”那就全错了。实际上Integer.toBinaryString、toOctalString、toHexString这三个方法是把int当作一个无符号的32位二进制序列来格式化它们根本不会输出负号。看JDK源码会发现toBinaryString(int i)内部调用的是toUnsignedString0(i, 1)第二个参数代表每次取几位。toHexString对应参数4toOctalString对应参数3。它的实现思路是先算出这个int在无符号视角下有多少位有效长度然后每次从最低位取对应位数的二进制值映射成0到15之间的数字再转成字符。所以-1在内存里本来就是32个1无符号视角下它就是4294967295这个数输出自然就是32个1。这一点背后的原理是补码。Java里的int是32位有符号整数负数以补码存储-1的补码就是所有位全是1。补码的存在让-1 1 0可以顺畅地通过硬件加法器完成不需要单独设计减法逻辑。面试时如果被问到“-1的二进制是什么”标准的答法是32位补码全部为1即11111111111111111111111111111111。后面第四大节我还会结合byte转hex的坑再讲一遍因为这是实际项目里最高频的翻车现场。3. 进制转换实操手写算法与工具方法3.1 十进制转二进制手写除2取余虽然Java提供了现成API但面试时经常要求手写“十进制转二进制”算法。原理就是教科书上的“除2取余余数逆序”不断用2去除把余数记录下来直到商为0最后把余数从下往上拼起来就是二进制。public static String toBinary(int n) { if (n 0) { return 0; } StringBuilder sb new StringBuilder(); while (n 0) { sb.append(n % 2); n / 2; } return sb.reverse().toString(); }比如toBinary(13)13除以2商6余16除以2商3余03除以2商1余11除以2商0余1余数序列是1、0、1、1反转后就是1101正好等于13。这个版本只处理非负数负数的情况后面说因为负数的补码不能简单靠“除2取余”得到。代码里有三个容易被忽略的细节。第一n 0要特判否则循环一次都不执行最后返回空字符串。第二用StringBuilder接余数再reverse()比每次往字符串前面拼字符的性能好得多。第三如果追求极致速度可以把n % 2和n / 2替换成n 1和n 1因为对二进制来说取余就是取最低位除以2就是右移一位这在原理上完全等价。3.2 二进制转十进制位运算实现反过来把二进制字符串转成十进制整数除了按权展开累加更推荐直接用位运算逻辑清晰还不会出错。public static int parseBinary(String s) { int res 0; for (int i 0; i s.length(); i) { char c s.charAt(i); if (c ! 0 c ! 1) { throw new IllegalArgumentException(非法二进制字符: c); } res (res 1) | (c - 0); } return res; }这段代码的思路是从左往右扫描字符串每读到一个字符就把当前结果左移一位再按位或上当前字符对应的0或1。举个例子字符串1101初始res为0读到第一个字符1得到(0 1) | 1 1读到第二个1得到(1 1) | 1 3读到第三个0得到(3 1) | 0 6读到第四个1得到(6 1) | 1 13。整个过程等价于按权展开的1*8 1*4 0*2 1*1。为什么要强调字符校验因为parseInt(10a, 2)会抛异常但你自己手写循环时如果不校验c - 0会把字母也算出负数最终结果莫名其妙。面试时写出校验这一步会显得你考虑问题更周全。如果要支持更大的二进制字符串比如超过32位那就不能用int了改用long.parseBinary或BigInteger。3.3 十六进制转浮点数日志与协议解析的硬骨头这是整个主题里最有实战价值的一节。很多人见过十六进制字符串但不知道怎么转成浮点数面试题也经常从这个角度出。为什么需要这种转换因为在网络协议、串口通信、蓝牙SDK、嵌入式上报数据里浮点数经常是以IEEE 754标准编码成4个字节float或8个字节double传过来的。日志、抓包工具里看到的通常是一串十六进制比如40490FDB这就是浮点数3.1415927的十六进制表示。IEEE 754单精度浮点数的内存布局是第31位是符号位第30到23位共8位是指数位第22到0位共23位是尾数位。双精度则把指数位扩展到11位、尾数扩展到52位。Java早就把底层的位解释成浮点数的方法封装好了直接用就行String hex 40490FDB; long bits Long.parseLong(hex, 16); float value Float.intBitsToFloat((int) bits); System.out.println(value); // 输出 3.1415927注意这里为什么先用Long.parseLong(hex, 16)而不是Integer.parseInt因为40490FDB这个十六进制值对应十进制的1078530011已经接近int类型的边界更稳妥的做法是用long先接住再强制转成int传给intBitsToFloat。如果你要转的是double对应方法就是Double.longBitsToDouble(Long.parseLong(hex, 16))示例3FF0000000000000解析出来就是1.0。反过来的操作同样常用已知一个float想拿到它对应的十六进制字符串用Float.floatToRawIntBits拿到原始位模式对应的int再转hexfloat f 3.14f; int raw Float.floatToRawIntBits(f); String hex Integer.toHexString(raw); System.out.println(hex); // 输出 4048f5c3这里有个高频坑Float.parseFloat只认识形如3.14这样的十进制字符串不认识40490FDB直接传十六进制字符串一定会抛NumberFormatException。所以必须先解析成整数位模式再交给intBitsToFloat。再补一个实战细节协议数据在字节流里的顺序可能是小端。比如对方按照小端顺序把0x40490FDB的四个字节发成了DB 0F 49 40你要是直接把整个字节数组按顺序拼起来转float结果会完全不对。常见做法是先把字节数组按大端重排再转换成float。手动拼int时注意每个字节都要 0xFF否则字节的符号扩展会把高位全部填成1这个细节在后面第四大节还会遇到。3.4 任意进制互转BigInteger 兜底面试和开发中还有一种更“野”的需求十六进制转八进制、三进制转七进制、甚至三十六进制的互转。Java的Integer.parseInt支持2到36进制字母a到z分别表示10到35但int能表示的范围有限字符串一旦很长就溢出。这时候用BigInteger最省心因为它可以表示任意长度的整数。import java.math.BigInteger; String hex 1A; String oct new BigInteger(hex, 16).toString(8); System.out.println(oct); // 输出 32原理其实很简单先以源进制为基数把字符串解析成BigInteger实例也就是拿到这个数在内存里的十进制表示再调用toString(targetRadix)转换成目标进制的字符串。两步走源进制和目标进制都支持2到36负数如果不是带负号规则和parseInt一样。为什么说它兜底因为Integer方法只能处理32位以内的值而协议解析、加密算法、大数计算里动不动就是几十位的十六进制字符串只有BigInteger能无脑处理。当然toString(36)还有个实用场景生成短编号。比如把自增ID转换成三十六进制字符串能显著缩短URL里参数的位数很多短视频链接里的短码就是这么来的。4. 那些年我们一起踩过的进制坑高频面试实战排查4.1 负数为什么显示成 ffffffff先看一段代码byte b (byte) 0xFF; System.out.println(Integer.toHexString(b)); // 输出 ffffffff System.out.println(Integer.toHexString(b 0xFF)); // 输出 ff第一次遇到这个问题的同学多半是在解析网络数据、读取二进制文件时想把byte数组打印成十六进制。明明读出来的是0xFF用Integer.toHexString(b)一打却是ffffffff还以为是数据错了。其实原因很简单byte在Java里是有符号类型范围是-128到1270xFF按有符号看待就是-1。当把一个byte赋值或提升成int时JVM会做符号扩展负数的最高位补1所以-1变成了32位全1的int即0xFFFFFFFF打印出来就成了一串f。解决办法就是在转换前把byte按无符号方式扩展成intb 0xFF。 0xFF会把除了低8位以外的所有位清零相当于把-1“洗”成255这样转出来的十六进制就是ff。实际开发里我建议封装一个工具方法以后统一使用避免每个人都重复踩坑private static final char[] HEX 0123456789abcdef.toCharArray(); public static String toHex(byte[] bytes) { StringBuilder sb new StringBuilder(bytes.length * 2); for (byte b : bytes) { int v b 0xFF; sb.append(HEX[v 4]).append(HEX[v 0x0F]); } return sb.toString(); }这里高四位用v 4取低四位用v 0x0F取正好对应字节转十六进制的标准写法。面试时如果被问到“为什么字节转十六进制要先0xFF”答案就是消除符号扩展带来的前导f。这个知识点还可以延伸成“十六进制转byte”的反向坑byte b (byte) Integer.parseInt(FF, 16);必须强转否则编译不过因为255超出byte范围。4.2 parseInt 的三种姿势带不带前缀差很多很多资料会告诉你“用parseInt做进制转换”但不会告诉你它和decode之间有个大坑parseInt不认前缀。// 这行代码会抛 NumberFormatException Integer.parseInt(0xFF, 16); // 正确写法之一是 Integer.decode(0xFF);parseInt(String s, int radix)要求字符串内容必须是纯数字字符正负号可以带但十六进制数字串里不能混进0x这两个字符。所以0xFF里的x会被当成非法十六进制字符直接抛异常。如果你拿到的字符串自带0x前缀要么手动去掉前缀要么用Integer.decode。decode的智能之处在于它会自动识别前缀0x开头按十六进制解析0开头按八进制解析#开头也按十六进制解析其他情况按十进制。但decode不支持你显式传radix参数这是它和parseInt的使用场景差异。真正容易在日常代码里踩的坑是把用户输入或配置文件里的进制数字直接丢给parseInt。比如配置文件写的是port0x1F你用Integer.parseInt(config, 16)解析一定会挂正确的做法是先判断是否带0x前缀再走不同分支。这个排查思路我写进过很多次日报每次原因都一样。4.3 进制字符串的合法性与大小写处理外部数据时十六进制字符串经常带着各种“脏东西”空格、换行、0x前缀、甚至#。在调parseInt或decode之前先做好校验和清洗能省去大量排查时间。校验十六进制字符串的合法性最简单的方式是正则public static boolean isHex(String s) { if (s null || s.isEmpty()) { return false; } return s.matches([0-9a-fA-F]); }要注意的是matches判断的是整个字符串所以别写漏符号。如果字符串可能是负数比如-1A需要先处理掉开头的负号再校验。如果来源是日志文件记得先trim()因为parseInt对空格非常敏感 ff这种带空格的字符串都会抛异常。很多语言会自动忽略空白Java不会这是个印象深刻的差异。大小写问题也值得提一句Integer.toHexString生成的是小写字母但十六进制数字解析时大小写都能识别。不过在字符串比较、日志去重、签名验签这类场景里同一个值可能来自不同来源一个有前导零一个没有一个是大写一个是小写直接equals就会误判。稳妥的做法是先统一格式再比较比如全部转成小写并且按固定长度补齐前导零否则很容易出现ff和0xFF明明代表同一个数却对不上的尴尬。4.4 位运算里藏着最实用的进制直觉进制和位运算是不分家的。理解了二进制位很多看似高级的技巧其实就是一行代码。判断整数奇偶最经典(n 1) 1就是奇数因为二进制最低位是1代表奇数是0代表偶数比n % 2 1更贴近计算机思维。判断一个正整数是不是2的整数次幂n 0 (n (n - 1)) 0比如8的二进制是10008-17即0111两者按位与为0所以8是2的幂。这个技巧在很多框架源码里用来判断扩容前容量是否合法。取模优化是另一个高频写法n (2的幂次 - 1)等价于n % 2的幂次但仅限非负整数。HashMap计算桶下标的时候就是这么干的前提是容量是2的幂。RGB颜色值也是典型的十六进制位操作红色0xFF0000、绿色0x00FF00、蓝色0x0000FF你要提取某个颜色分量只需要右移再与0xFF相与。int color 0x8A2BE2; // 一种紫色 int red (color 16) 0xFF; int green (color 8) 0xFF; int blue color 0xFF;这种位操作的直觉在开发权限系统、状态标志位时特别有用。比如用一个int的32个bit表示32个开关状态flags | 1 3表示打开第3位(flags (1 3)) ! 0表示检查第3位本质就是在二进制位层面做增删查。面试时如果你能顺手把这些位运算落地点出来比单纯背结论强得多。4.5 面试高频题速查最后整理一个进制和位运算相关的面试速查表都是我在面试中真实问到过的点题目考点速答要点十进制转二进制手写除基取余循环取余逆序拼接判断整数是否是2的幂位运算n 0 (n (n - 1)) 0不借助第三个变量交换两个int异或a ^ b; b ^ a; a ^ b;int最大值、最小值的十六进制补码最大值0x7FFFFFFF最小值0x80000000整数二进制里有多少个1位运算Integer.bitCount(n)手写用n (n - 1)循环十六进制字符串转floatIEEE 754Float.intBitsToFloat((int) Long.parseLong(hex, 16))Integer.toHexString(-1)结果补码与无符号视角输出32个1即ffffffffInteger.parseInt(0xFF, 16)能否成功前缀解析不能会抛NumberFormatException应使用decode面试时除了答对结果最好能把背后的补码原理说清楚负数在计算机里以补码形式存储-1就是所有位为1toHexString从无符号视角格式化所以看到的是一串f。能顺带补一句“byte转int会有符号扩展取 0xFF可以按无符号处理”面试官通常都会觉得你底层理解扎实。我个人在实际项目里的体会是进制问题说难不难说简单也不简单真正容易翻车的地方全在“符号”和“前缀”这两个点上。很多线上日志排查半天最后发现就是byte转hex忘了 0xFF或者parseInt时混入了前缀。所以我的建议很直接把这几个工具方法背下来遇到二进制数据先统一按字节dump成十六进制再分析比对着控制台乱猜高效十倍。最后再分享一个小技巧用IDEA调试时变量面板可以直接切换进制显示右键变量选择View as可以选二进制、十六进制排查位运算和进制转换问题时肉眼可见地舒服。
返回列表