Java 八股面试基础之数据类型详解篇
前提补充知识点:原码、反码、补码详解
可以把它看成计算机为"如何用二进制表示负数,并让加减法容易计算"走过的三步。
先说结论:
原码最直观,但计算麻烦;反码让减法能转成加法,但有两个零;补码同时解决运算统一和零重复的问题,所以现代计算机的有符号整数通常使用补码。Java 的
byte、short、int、long都是补码表示。
以 4 位二进制表示整数为例。
| 表示法 | +3 |
-3 |
范围 | 主要问题/意义 |
|---|---|---|---|---|
| 原码 | 0011 |
1011 |
-7 ~ +7 |
最直观,但加减法要分情况 |
| 反码 | 0011 |
1100 |
-7 ~ +7 |
可将减法转加法,但有 +0、-0 |
| 补码 | 0011 |
1101 |
-8 ~ +7 |
加减法统一、零唯一,现代计算机使用 |
1. 原码:最符合人的直觉
原码(sign-magnitude)用最高位表示正负:
0:正数1:负数- 后面的位表示绝对值
所以:
+3 = 0011
-3 = 1011
它的意义是容易理解:把最高位当"正负号",后面直接写数值。
但问题是,计算机不能直接把原码相加当作普通加法:
+3 + (-3)
0011 + 1011 = 1110
结果并不是 0。CPU 必须先判断两个数的符号,再决定做"绝对值加法"还是"绝对值相减",硬件逻辑更复杂。
而且原码有两个零:
+0 = 0000
-0 = 1000
这会浪费一个编码,也增加比较与判断的复杂度。
2. 反码:让负数更适合做加法
反码的规则是:
- 正数的反码与原码相同;
- 负数的反码:保留符号位,其余位按位取反。
因此:
+3 的原码/反码:0011
-3 的原码: 1011
-3 的反码: 1100
这样,3 + (-3) 的结果更接近正确答案:
0011 // +3
+ 1100 // -3 的反码
------
1111 // -0
反码的历史意义是:将减法尽量转化为二进制加法。不过如果产生最高位进位,还要把进位"绕回去"加到最低位(end-around carry),规则仍不够自然。
它也仍有两个零:
+0 = 0000
-0 = 1111
3. 补码:在反码基础上加 1
补码的规则是:
- 正数的补码与原码相同;
- 负数的补码 = 对其绝对值的二进制按位取反,再加
1。
所以 -3:
+3: 0011
按位取反: 1100
加 1: 1101 // -3 的补码
现在做计算:
0011 // +3
+ 1101 // -3 的补码
------
1 0000
固定为 4 位时,最左侧溢出的 1 直接丢弃,得到:
0000 // 0
于是 CPU 可以用同一个加法器处理:
a - b 等价于 a + (-b)
补码还消除了 -0:
0000 = 0
原本表示 -0 的那个编码被利用起来表示最小负数:
1000 = -8
因此,4 位补码范围是 -8 ~ +7;32 位 int 的范围就是:
-2^31 ~ 2^31 - 1
一句适合面试的总结:
原码通过符号位直接表示正负,直观但运算要区分符号;反码通过按位取反让减法可以更多地转为加法,但仍有正零和负零;补码在反码基础上加一,使加减法能统一使用二进制加法器、零只有一种表示,并能充分利用全部二进制编码,因此成为现代计算机有符号整数的标准表示方式。
一、八种基本的数据类型
Java 支持数据类型分为两类:基本数据类型和引用数据类型。
基本数据类型共有 8 种,可分为三类:
- 数值型:整数类型(
byte、short、int、long)和浮点类型(float、double)。 - 字符型:
char。 - 布尔型:
boolean。
| 数据类型 | 占用大小(字节) | 位数 | 取值范围 | 默认值 | 描述 |
|---|---|---|---|---|---|
byte |
1 | 8 | -128 (-2^7) 到 127 (2^7 - 1) |
0 |
最小的整数类型,适合节省内存,例如处理文件或网络流时存储小范围整数。 |
short |
2 | 16 | -32768 (-2^15) 到 32767 (2^15 - 1) |
0 |
较少使用,通常在需要节省内存且数值范围在该区间时使用。 |
int |
4 | 32 | -2147483648 (-2^31) 到 2147483647 (2^31 - 1) |
0 |
最常用的整数类型,可满足大多数日常整数计算。 |
long |
8 | 64 | -9223372036854775808 (-2^63) 到 9223372036854775807 (2^63 - 1) |
0L |
表示很大的整数;当 int 范围不够时使用,字面量后需加 L 或 l。 |
float |
4 | 32 | -3.4028235E38 到 3.4028235E38 |
0.0f |
单精度浮点数,精度相对较低;字面量后需加 F 或 f。 |
double |
8 | 64 | -1.7976931348623157E308 到 1.7976931348623157E308 |
0.0d |
双精度浮点数,精度比 float 高,是 Java 中表示小数的默认类型。 |
char |
2 | 16 | '\u0000' (0) 到 '\uffff' (65535) |
'\u0000' |
表示单个字符,采用 Unicode 编码,可表示各种语言的字符。 |
boolean |
无明确字节大小(理论上 1 位) | 无明确位数 | true 或 false |
false |
用于逻辑判断,只有两个取值,常用于条件判断和循环控制。 |
Float 和 Double 的最大值会以科学记数法输出。结尾的 E+数字 表示 E 前的数字乘以 10 的相应次方:3.14E3 为 3.14 × 1000 = 3140,3.14E-3 为 3.14 / 1000 = 0.00314。Float.MIN_VALUE / Double.MIN_VALUE 表示最小正非零值(如 1.4E-45),不是负的最小值;浮点类型的最小负值是 -MAX_VALUE。
注意:
- 浮点数默认类型为
double;声明float常量时,末尾必须加f或F。 - 整数默认类型为
int;声明long字面量时,末尾加l或L。 - 基本类型的包装类中,
char对应Character,int对应Integer,其余通常为首字母大写。 char是无符号类型,不能为负数,范围从 0 开始。
二、int 和 long 是多少位、多少字节?
int是 32 位(bit),占 4 个字节(byte),是有符号整数,取值范围-2^31到2^31 - 1。最大正数为2,147,483,647;超出范围会溢出。long是 64 位,占 8 个字节,也是有符号整数,取值范围-2^63到2^63 - 1。处理很大的整数(例如文件大小)时,若int范围不够,应使用long。
三、long 和 int 可以互转吗?
可以。long 的范围大于 int,因此 int → long 是安全的;long → int 可能导致数据丢失或溢出。
java
int intValue = 10;
long longValue = intValue; // 自动转换,安全
long longValue = 100L;
int intValue = (int) longValue; // 强制类型转换,可能会有数据丢失或溢出
当 longValue 超出 int 范围时,转换结果是截断后的低位部分。转换前应检查值是否位于 int 的范围内,以避免数据丢失或溢出。
四、数据类型转换方式你知道哪些?
- 自动类型转换(隐式转换) :当目标类型范围大于源类型时,Java 自动转换,无需显式转换。例如
int → long、float → double。 - 强制类型转换(显式转换) :当目标类型范围小于源类型时,需要使用强制转换,可能导致数据丢失或溢出。例如
long → int、double → int。语法:目标类型 变量名 = (目标类型) 源类型;。 - 字符串转换 :例如
Integer.parseInt()将字符串转为int,Double.parseDouble()将字符串转为double。 - 数值之间的转换 :可以通过包装类提供的方法进行,例如
Character、Integer。
五、类型互转会出现什么问题吗?
基本数据类型转换的问题
小范围数据类型赋给大范围数据类型时,Java 会自动转换,通常安全:
java
int num = 100;
long bigNum = num; // 自动将 int 转换为 long
大范围数据类型赋给小范围数据类型时,可能发生数据溢出或精度损失。
数据溢出:目标类型无法容纳原数据时发生。byte 类型范围是 -128 ~ 127。300 的二进制为 00000001 00101100,强制转换为 byte 时会丢弃高位字节,只保留低 8 位 00101100,十进制为 44。
java
int largeNum = 300;
byte b = (byte) largeNum; // b 的值为 44
精度损失:进行浮点数类型转换时可能发生。double 转 int 时也会损失精度:
java
double d = 3.14;
int i = (int) d; // i 的值为 3,小数部分 0.14 被舍弃
对象引用转换的问题
向上转型自动进行,通常安全:
java
class Animal {}
class Dog extends Animal {}
Dog dog = new Dog();
Animal animal = dog; // 自动向上转型
向下转型需要手动进行,有风险。如果父类对象真实类型不是目标子类实例,转型时会抛出异常:
java
Animal animal = new Animal();
Dog dog = (Dog) animal; // 运行时抛出 ClassCastException
Java 对象在运行时记录真实类型;向下转型时会检查真实类型是否与目标类型兼容。可使用 instanceof 检查:
java
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // 只有确认 animal 是 Dog 的实例时才进行转换
}
### 为什么用 BigDecimal,不用 double?
`double` 会出现精度丢失。`double` 执行二进制浮点运算,而二进制无法准确表示某些小数;类似十进制无法准确表示 `1/3 = 0.3333...`,二进制只能表示若干 `1 / (2^n)` 之和,`0.1` 不能精确表示为这种有限组合。
```java
System.out.println(0.05 + 0.01);
System.out.println(1.0 - 0.42);
System.out.println(4.015 * 100);
System.out.println(123.3 / 100);
// 输出:
// 0.060000000000000005
// 0.5800000000000001
// 401.49999999999994
// 1.2329999999999999
商品价格计算出现上述误差时,可能造成无法下单或对账问题。因此牵涉金钱计算时通常使用 BigDecimal:
java
import java.math.BigDecimal;
public class BigDecimalExample {
public static void main(String[] args) {
BigDecimal num1 = new BigDecimal("0.1");
BigDecimal num2 = new BigDecimal("0.2");
BigDecimal sum = num1.add(num2);
BigDecimal product = num1.multiply(num2);
System.out.println("Sum: " + sum);
System.out.println("Product: " + product);
}
}
// 输出:
// Sum: 0.3
// Product: 0.02
该代码创建两个 BigDecimal 对象,分别表示 0.1 和 0.2,使用 add() 求和、multiply() 求积。BigDecimal 可确保精确十进制计算,避免 double 的舍入误差。创建 BigDecimal 时应使用字符串参数,而不要直接使用浮点数值,以避免浮点精度问题。
六、装箱和拆箱是什么?
装箱(Boxing)和拆箱(Unboxing)是基本数据类型和对应包装类之间的转换过程:
java
Integer i = 10; // 装箱
int n = i; // 拆箱
自动装箱主要发生在赋值和方法调用两种情况。
赋值时
Java 1.5 以前需要手动转换;现在由编译器完成:
java
// before autoboxing
Integer iObject = Integer.valueOf(3);
int iPrimitive = iObject.intValue();
// after Java 5
Integer iObject = 3; // autoboxing - primitive to wrapper conversion
int iPrimitive = iObject; // unboxing - object to primitive conversion
方法调用时
方法调用时可传入原始数据值或对象,编译器会进行转换:
java
public static Integer show(Integer iParam) {
System.out.println("autoboxing example - method invocation i: " + iParam);
return iParam;
}
// autoboxing and unboxing in method invocation
show(3); // autoboxing
int result = show(3); // unboxing because return type of method is Integer
show 接受 Integer 参数;调用 show(3) 时,int 会转成 Integer,这就是自动装箱。show 返回 Integer,赋给 int result 时发生自动拆箱。
自动装箱的弊端
循环中自动装箱可能创建多余对象、影响性能:
java
Integer sum = 0;
for (int i = 1000; i < 5000; i++) {
sum += i;
}
sum += i 可看成 sum = sum + i。+ 不适用于 Integer 对象,因此先拆箱、进行数值相加,再装箱。其内部变化如下:
java
int result = sum.intValue() + i;
Integer sum = Integer.valueOf(result);
因为 sum 为 Integer,自动装箱会由编译器替换为 Integer.valueOf(...)。命中 IntegerCache(默认 -128 ~ 127)时可复用对象;本例循环结果超出缓存范围,约会创建 4000 个 Integer 对象,降低性能并加重 GC 负担。应正确声明变量类型,避免自动装箱造成的性能问题。new Integer(int) 自 JDK 9 起被 @Deprecated,应使用 Integer.valueOf(int) 或直接自动装箱。
七、Java 为什么要有 Integer?
Integer 对应 int 的包装类,即把 int 包装成 Object 对象。对象封装可将数据与处理数据的方法结合,例如 Integer 有 parseInt() 等处理整数的方法。
另一重要原因是 Java 中许多方法或类处理的是对象。ArrayList 集合只能以类作为存储对象;若要将 int 存入 List,必须包装成 Integer。
泛型中的应用
泛型只能使用引用类型,不能使用基本类型。因此泛型中使用整数必须使用 Integer:
java
List<Integer> list = new ArrayList<>();
list.add(3);
list.add(1);
list.add(2);
Collections.sort(list);
System.out.println(list);
转换中的应用
java
int i = 10;
Integer integer = Integer.valueOf(i);
String str = integer.toString();
System.out.println(str);
集合中的应用
Java 集合只能存储对象,不能存储基本数据类型。例:
java
List<Integer> list = new ArrayList<>();
list.add(3);
list.add(1);
list.add(2);
int sum = list.stream().mapToInt(Integer::intValue).sum();
System.out.println(sum);
八、Integer 相比 int 有什么优点?
int 是 Java 的原始数据类型,Integer 是 int 的包装类。
- 基本类型和引用类型:
int是预定义的基本数据类型,不需要实例化即可使用;Integer是引用类型,以对象形式使用。使用int存储整数通常不需要额外对象分配,基本类型的操作通常比对应引用类型快。 - 自动装箱和拆箱:
Integer作为int包装类,可实现自动装箱和拆箱。int值赋给Integer时 Java 自动装箱;Integer值赋给int时 Java 自动拆箱。 - 空指针异常:
int成员变量默认值为 0,Integer作为引用类型默认值为null。对值为null的Integer自动拆箱会抛出空指针异常,因为null无法拆箱为基本类型。
九、为什么还要保留 int 类型?
包装类是引用类型,对象的引用和对象本身分开存储;基本类型数据的变量直接存储数据本身。因此基本类型在读写效率和存储效率上通常比包装类高。
在 64 位 JVM、开启引用压缩的情况下,一个 Integer 对象占用 16 字节,而一个 int 类型数据占用 4 字节;前者是后者的 4 倍。
十、说一下 Integer 的缓存
Java 的 Integer 类内部实现静态缓存池,用于存储特定范围内整数对应的 Integer 对象。
默认范围是 -128 到 127。通过 Integer.valueOf(int) 创建处于该范围内的整数对象时,不会每次创建新对象,而会复用缓存中的已有对象。
Integer 缓存是指:Java 会提前创建并复用一部分常用整数对应的 Integer 对象,避免每次装箱都新建对象。
默认缓存范围是:
-128 ~ 127
例如:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
因为 100 在缓存范围内,自动装箱时大致都会调用:
Integer.valueOf(100)
它们拿到的是缓存中的同一个 Integer 对象。
但超出范围:
Integer x = 200;
Integer y = 200;
System.out.println(x == y); // false
200 默认不在缓存范围内,通常会得到两个不同的对象;虽然数值相同,但引用不同。
因此,比较两个 Integer 的数值时,不要依赖 ==,应该用:
System.out.println(x.equals(y)); // true
或者在确定不为 null 时自动拆箱比较:
System.out.println(x.intValue() == y.intValue()); // true
缓存主要是性能优化:小整数在循环、集合、计数等场景出现非常频繁,复用对象能减少创建对象和垃圾回收的开销。
还有一个易错点:
Integer a = new Integer(100);
Integer b = new Integer(100);
System.out.println(a == b); // false
new Integer(...) 强制创建新对象,不会使用缓存;而且这个构造器在现代 Java 中已废弃。
面试可以这样答:
Java 的
Integer默认缓存-128到127的对象。通过自动装箱或Integer.valueOf()创建该范围内的整数时,会复用缓存对象,因此==可能返回true;超出范围通常创建不同对象。由于==比较引用,比较Integer数值应使用equals(),并注意空指针问题。