数据类型与包装类 — 一个新手学Java的数据类型笔记

数据类型与包装类 — 一个新手学Java的数据类型笔记 写在前面数据类型这个东西我刚开始学Java的时候觉得很简单——不就是整数、小数、字符、布尔嘛。但后来发现远远没我想的那么简单。八种基本数据类型各有各的范围和用途还有包装类、自动装箱拆箱、Integer缓存、BigDecimal精度……学到后面我甚至有点怀疑自己之前是不是真的懂了。这篇文章算是我把这些概念重新捋一遍的笔记从一个新手的角度来理解这些看起来基础但实际很深的Java数据类型体系。八种基本数据类型一开始我记不住后来发现有个规律Java的数据类型分为两类基本数据类型和引用数据类型。基本数据类型一共有8种分三类数值型整数类型byte、short、int、long和浮点类型float、double字符型char布尔型boolean刚接触的时候我老记不住这8种分别占多少字节、范围是多少。后来发现其实有个规律——按字节数来记就好记多了1字节byte、boolean2字节short、char4字节int、float8字节long、double你看刚好两两一组每个组内一个整数一个浮点或布尔/字符。这么一整理就清晰多了。另外还有一个记忆方法除了boolean和char比较特殊剩下六种数值类型刚好是从小到大排列的——byte两字节翻到shortshort两字节翻到intint两字节翻到long。浮点类型float四字节、double八字节刚好对应int和long的字节数。这么理解之后我后面就再也没记混过了。各类型的默认值和取值范围我放在下面这张表里方便查数据类型占用大小位数取值范围默认值byte1字节8-128 到 1270short2字节16-32768 到 327670int4字节32-2147483648 到 21474836470long8字节64-9223372036854775808 到 92233720368547758070Lfloat4字节32±3.4028235E380.0fdouble8字节64±1.7976931348623157E3080.0dchar2字节16‘\u0000’ 到 ‘\uffff’0~65535‘\u0000’boolean1字节位宽未明确定义—true 或 falsefalse有几个注意点也是我一开始犯过迷糊的第一Float.MIN_VALUE和Double.MIN_VALUE不是它们能表示的最小负数而是最小的正非零值非常非常接近0的一个数。我一开始以为MIN_VALUE是-3.4E38这种后来查了才知道不是。浮点类型真正的最小负值其实是-MAX_VALUE。第二整数的默认类型是int。如果你写了一个很大的数字没加L后缀它就是int类型。如果这个数字超出了int的范围编译阶段就直接报错了。我之前遇到过——定义一个常量的数值很大忘了加L编译器就报integer number too large还纳闷了好久。第三浮点数的默认类型是double。所以要声明float类型的时候必须在末尾加f或F不然编译器会报错。我刚学的时候写float pi 3.14;编译器报错我还想3.14不就是小数吗为什么不让我赋值后来才知道3.14默认是double赋值给float会有精度损失风险所以必须显式加f。int和long到底占多少位多少字节int是32位占4个字节long是64位占8个字节。这么说好像挺简单的但真正理解它们的边界还是靠实践。有次我写一个计数器程序用int来存计数值。我以为用户再怎么用也超不过几十万吧结果那个程序跑了几个月计数值累加到了二十多亿……直接溢出了变成了负数。当时排查了好久才发现是int溢出了。后来我才真正理解int的范围上限是2147483647——这看起来很大但在某些场景下真的不够用。long就好多了范围大到基本上不会溢出。比如文件传输程序里文件的大小用int可能存不下但long可以。在实际用到long的场景中我还没遇到过long不够用的情况。long和int可以互相转吗可以但是有讲究。int转long是安全的。因为long的范围比int大把int赋值给long会自动进行类型转换不会有数据丢失。intintValue10;longlongValueintValue;// 自动转换安全的long转int就不行了。因为int的范围比long小如果long的值超过了int的范围强制转过去会截断数据就丢了。必须用强制类型转换如果值超出范围转换结果是截断后的低位部分一般不是你想要的。longlongValue100L;intintValue(int)longValue;// 强制类型转换可能有数据丢失所以实际写代码的时候如果你要把long转成int最好先检查一下值是不是在int的范围内。我一般是加一个if判断再强制转这样更安全。数据类型转换有哪几种方式Java的类型转换分为几种情况我一开始也搞不太清楚什么时候自动转、什么时候要手动转。自动类型转换隐式转换小范围转大范围的时候Java会自动帮你转。比如int赋值给long、float赋值给double。这很直观——往大了转肯定安全。强制类型转换显式转换大范围转小范围的时候必须用强制转换。语法是(目标类型) 变量名。但这种转换可能有数据丢失或溢出的风险前面已经说了。字符串转数值这个很常用。比如你从用户输入拿到一个字符串123要当数字用就得用Integer.parseInt(123)或者Double.parseDouble(3.14)。这个我写代码的时候经常用到。数值之间的其他转换比如char和int之间可以互转——char其实本质上是Unicode编码的数字。这个我之前理解char的时候花了一些时间。类型互转可能出什么问题这个我踩过坑。数据溢出大范围转小范围的时候如果值放不下就会溢出。比如300转成bytebyte最大只能到127所以300转过去变成了44——因为高位被截断了。intlargeNum300;byteb(byte)largeNum;// b的值是44不是300我当时看到44的时候一脸懵后来才去查了二进制怎么截断的。精度损失浮点数转整数的时候小数部分会被直接舍弃不会四舍五入。doubled3.14;inti(int)d;// i是30.14被丢了这个其实好理解但我在实际写代码的时候经常忘记检查精度问题。对象引用转换的问题这个是关于引用类型的跟基本类型不太一样。子类转父类向上转型是自动且安全的。但父类转子类向下转型需要手动转换而且如果父类对象实际上并不是目标子类的实例会抛出ClassCastException。AnimalanimalnewAnimal();Dogdog(Dog)animal;// 运行时报错解决办法是用instanceof先检查一下if(animalinstanceofDog){Dogdog(Dog)animal;}这个知识点我记得在面试题里很常见我准备面试的时候专门背过。为什么不用double要用BigDecimal这个知识点我第一次知道的时候还挺震惊的——double做浮点数运算居然会不精确System.out.println(0.050.01);// 0.060000000000000005System.out.println(1.0-0.42);// 0.5800000000000001System.out.println(4.015*100);// 401.49999999999994System.out.println(123.3/100);// 1.2329999999999999我第一次跑这段代码的时候看到输出结果第一反应是我代码写错了吧“后来查了才知道这不是bug这是二进制浮点数的天性”。原因说起来也不复杂double做的是二进制浮点运算而二进制在某些情况下没办法精确表示一个小数。就好比十进制没办法精确表示1/30.33333…循环一样二进制也没办法精确表示0.1。因为0.1不能写成1/(2^n)的和的形式。所以在涉及金钱计算的场景——比如电商、支付系统——用double就会出现大问题。我举个例子假设你手里有0.06元想买一个0.05元的商品和一个0.01元的商品。用double计算的话0.050.01的结果是0.060000000000000005比实际多了那么一丢丢。表面上看没什么但如果一个电商网站的订单量很大这些微小的误差累积起来就是一笔不小的数目。可能导致对账对不上、用户下单失败等等。所以Java提供了BigDecimal来做精确的十进制计算。使用方式是用字符串来构造BigDecimal对象importjava.math.BigDecimal;BigDecimalnum1newBigDecimal(0.1);BigDecimalnum2newBigDecimal(0.2);BigDecimalsumnum1.add(num2);// 0.3BigDecimalproductnum1.multiply(num2);// 0.02注意构造BigDecimal的时候一定要用字符串传参不要直接传double值。比如new BigDecimal(0.1)和new BigDecimal(0.1)结果是不一样的——前者因为0.1本身在二进制下就不精确所以构造出来的结果也不精确。我之前就犯过这个错。装箱和拆箱是什么这个概念刚接触的时候觉得有点绕。“装箱”“拆箱”——这词听着像搬家。简单来说装箱把基本数据类型转成对应的包装类对象拆箱把包装类对象转回基本数据类型Java 5之前这些转换需要手动写代码。Java 5之后编译器能自动帮你做就叫自动装箱和自动拆箱。Integeri10;// 自动装箱 —— 编译器替换成 Integer.valueOf(10)intni;// 自动拆箱 —— 编译器替换成 i.intValue()我第一次看到Integer i 10;这行代码的时候心想这不是在赋值一个数字给一个对象吗这也能行后来才知道是编译器在背后帮我调用了Integer.valueOf(10)。现在习惯了倒觉得挺方便的但理解背后的机制还是很重要的。自动装箱在方法调用的时候也会发生publicstaticIntegershow(IntegeriParam){System.out.println(autoboxing example - method invocation i: iParam);returniParam;}show(3);// 自动装箱 —— int 3 转成 Integerintresultshow(3);// 自动拆箱 —— Integer 返回值转成 int自动装箱的弊端自动装箱虽然方便但用不对会出性能问题。比如下面这个循环Integersum0;for(inti1000;i5000;i){sumi;}你看这段代码表面上没什么问题。但实际上每次循环都发生了拆箱sum转int做加法→ 装箱结果转回Integer。而且因为循环值在-128到127范围之外不能复用缓存对象所以要新建将近4000个Integer对象。如果你在一个高并发的系统里这么写GC垃圾回收会频繁触发性能影响很大。所以正确的写法应该是用int而不是Integer来累加intsum0;for(inti1000;i5000;i){sumi;}这个坑我是在看别人代码reivew的时候学到的后来自己也注意了。Java为什么要有Integer直接用int不好吗这个问题我也想过——既然int就能表示整数了为什么还要搞一个Integer当时我学到这里的时候第一反应是这不是多此一举吗后来慢慢理解了主要有几个原因包装类的功能Integer能把数据和操作数据的方法封装在一起。比如Integer.parseInt()能直接把字符串转成整数Integer.toString()能把整数转成字符串。这些方法如果不用包装类就得放到别的工具类里。集合框架需要对象Java的集合类ArrayList、HashMap等只能存对象不能存基本数据类型。比如你有一个ArrayListint这是编译不过的。必须用ArrayListInteger。这点我刚学集合的时候踩过坑——想都没想就写了一个Listint然后编译器报错了才想起来要改成Integer。泛型中的应用泛型也只支持引用类型不支持基本类型。所以泛型容器里用int必须包装成Integer。ListIntegerlistnewArrayList();list.add(3);list.add(1);list.add(2);Collections.sort(list);// 排序没问题集合中的应用因为集合只能存对象所以需要的数据存在集合里的时候要转成包装类。然后可以用Stream API做各种操作ListIntegerlistnewArrayList();list.add(3);list.add(1);list.add(2);intsumlist.stream().mapToInt(Integer::intValue).sum();// 求和Integer相比int有什么优点这个问题算是前面那个问题的延续。Integer相对于int主要有这么几个方面类型不同int是基本类型Integer是引用类型。int不用实例化就能直接用Integer需要创建对象。自动装箱拆箱Integer支持自动装箱拆箱方便在基本类型和对象之间转换。写代码的时候不用手动调用valueOf和intValue了。默认值不同int的默认值是0而Integer的默认值是null。这一点在实际开发中挺重要的。比如在数据库里一个字段可能没有值NULLJava里用Integer来对应就很自然——null表示没有值。但用int的话0到底是真的值为0还是没有值就区分不出来了。但是这里有一个坑空指针异常。如果一个Integer是null你对它做自动拆箱操作——比如赋值给int——就会抛出NullPointerException。我自己就遇到过从数据库查出来一个字段是null然后直接赋值给int变量程序就崩了。Integernullablenull;intvaluenullable;// 运行时抛出 NullPointerException那为什么还要保留int类型既然Integer有这么多好处那为什么Java不干脆去掉int全部用Integer算了主要原因是性能和内存。基本类型直接存储数据本身读写效率高。而引用类型的对象引用和对象本身是分开存储的操作起来多了一层间接访问。在64位JVM上开启引用压缩的情况下一个Integer对象占用16字节的内存空间而一个int类型只占4个字节。也就是说Integer占的空间是int的4倍。如果是大量数据的运算和存储这个差距就很明显了。所以一般来说需要对象特性的时候用Integer单纯做数值运算和存储的时候用int。各取所需互不冲突。Integer的缓存机制这个应该是面试里经常被问到的一个点了。Java的Integer类内部维护了一个静态缓存池默认情况下缓存了-128到127之间的Integer对象。当你通过Integer.valueOf()方法创建这个范围内的整数时不会创建新对象而是直接从缓存池里取。什么意思呢看这段代码Integera100;Integerb100;System.out.println(ab);// true —— 因为都是从缓存里取的同一个对象Integerc200;Integerd200;System.out.println(cd);// false —— 超出缓存范围创建了两个不同的对象我第一次看到这个输出的时候很诧异——为什么100的时候相等200的时候就不相等了后来知道是缓存机制之后才恍然大悟。这个缓存范围可以通过JVM参数来调整但一般默认就是-128到127。设计这个缓存的原因也很简单-128到127是日常编程中最常用的整数范围复用对象可以减少内存分配提高性能。所以写代码的时候要注意比较两个Integer是否相等不要用要用equals()。因为比较的是对象引用而equals()比较的是数值。上面例子也说明了超出缓存范围之后的结果就不靠谱了。关于数据类型转换的一个小总结把上面说的整理一下其实数据类型转换的核心就几条原则小转大自动安全int转long、float转double范围扩大了不会丢东西Java自动帮你处理。大转小强制有风险long转int、double转int可能溢出或丢精度必须手动强制转换转换前最好检查范围。字符串转数值用包装类的parse方法。对象引用转换向上转型自动安全向下转型需要instanceof检查。我做题的时候遇到最多的坑就是大转小那块要么忘记检查范围直接强转要么没注意到double转int会丢掉小数部分。写在后头数据类型这部分内容看起来是Java最基础的东西但学进去之后发现每一个知识点背后都涉及到很深的设计考量。为什么要有包装类为什么要有缓存为什么double不精确这些问题背后都有设计者的权衡和取舍。我觉得学基础知识和学框架不一样——框架追求快能跑起来就行但基础知识需要慢需要多问几个为什么。数据类型这一块我前前后后看了好几遍每次都有新的理解。比如第一次学的时候只记住了八种类型分别占多少字节第二次看的时候才开始关注包装类的设计意图第三次才把自动装箱拆箱和Integer缓存串起来理解。数据类型是所有后续学习的基础——面向对象、泛型、集合、甚至多线程里的volatile都和类型系统有关系。虽然这篇写得很长但我感觉自己也只是摸到了冰山一角。以后学到更深的内容可能还会回来翻这些笔记。写这篇文章的过程也是重新整理了一遍自己的理解希望对新学的朋友有一点帮助。