本文部分内容节选自Java Guide, 地址: https://javaguide.cn/java/basis/java-basic-questions-01.html
🚀 基础(上) → 🚀 基础(中) → 🚀基础(下) → 🤩集合(上) → 🤩集合(下) → 🤗JVM专题1 → 🤗JVM专题2 → 🤗JVM专题3 → 🤗JVM专题4 →😋JUC专题1 → 😋JUC专题2
过段时间打算投简历开始找实习了, 得开始复习一点基础知识. 这几天在看Java Guide, 感觉写得不错, 看的时候做点笔记, 把输入转为输出, 帮助消化知识
基本数据类型
Java中的几种基本数据类型了解吗?
Java有 8 种基本数据类型, 分别为:
- 6 种数字类型:
- 4 种整数型:
byte
,short
,int
,long
- 2 种浮点型:
float
,double
- 4 种整数型:
- 1 种字符类型:
char
- 1 种布尔类型:
boolean
基本类型 | 位数 | 字节 | 默认值 |
---|---|---|---|
byte | 8 | 1 | 0 |
short | 16 | 2 | 0 |
int | 32 | 4 | 0 |
long | 64 | 8 | 0L |
char | 16 | 2 | ‘u0000’ |
float | 32 | 4 | 0f |
double | 64 | 8 | 0d |
boolean | 1 | false |
Java基本类型的存储空间不会随着机器硬件架构的改变而改变
注意:
-
Java 里使用
long
类型的数据一定要在数值后面加上 L,否则将作为整型解析 -
(这一条是我自己加的) 根据阿里巴巴Java开发准则, 在使用
long
类型数据时, 数值后面不能用 l , 而必须得用 L为什么? 请看如下的代码
long a = 121l; long b = 1211; // a的最后一位其实是l, b的最后一位才是数字1, 你发现了吗?是不是傻傻分不清 // 如果写成l, 那么很容易会和数字1混淆
这些基本类型也有它们的包装类: Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
.
讲讲包装类型和基本类型的区别
- 除了常量或者局部变量, 在其他地方诸如函数参数、对象属性上都用包装类型, 而且包装类型可以用于泛型, 基本类型不行
- 基本类型的局部变量存储在虚拟机栈的局部变量表中, 基本类型的成员变量存储在虚拟机的堆中. 包装类型属于对象类型, 自然是存在堆里面
- 基本类型的占用空间比包装类型占用空间更小
- 包装类型不赋值就是null, 基本类型有初始值且不是null
- 对于基本类型而言,
==
比较的是值; 对于包装类型而言,==
比较的是内存地址,equal()
才是比较的值
⚠️ 注意:**基本数据类型存放在栈中是一个常见的误区! ** 基本数据类型的存储位置取决于它们的作用域和声明方式. 如果它们是局部变量, 那么它们会存放在栈中; 如果它们是成员变量, 那么它们会存放在堆中.
public class Test {
// 成员变量,存放在堆中
int a = 10;
// 被 static 修饰,也存放在堆中,但属于类,不属于对象
// JDK1.7 静态变量从永久代移动了 Java 堆中
static int b = 20;
public void method() {
// 局部变量,存放在栈中
int c = 30;
static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量
}
}
包装类型的缓存机制了解吗
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据, Character
创建了数值在 [0,127] 范围的缓存数据, Boolean
直接返回 True
or False
.
浮点数的包装类 Float
和 Double
没有实现缓存机制
根据阿里巴巴Java开发准则, 所有整型包装类在比较大小时, 必须全部使用 equal() 进行比较
为什么? 请看下面的代码
Integer a = 10;
Integer b = new Integer(10);
System.out.println(a == b); // 结果是什么?
正确答案是 false.
Integer a = 10
这一行代码会发生装箱, 也就是说这行代码等价于 Integer a = Integer.valueOf(10)
. 因此, a
直接使用的是缓存中的对象. 而Integer b = new Integer(10)
会直接创建新的对象.
对于 Integer var = ?
在 -128 ~ 127 之间的赋值, Integer对象在 IntegerCache.cache产生, 会复用已有对象, 因此可以用 ==
判断, 但是这个区间以外的赋值都会在堆上产生, 不会复用已有对象
自动拆装箱了解吗
什么是自动装箱/拆箱机制?
装箱: 基本类型用对应的包装类型包装起来
拆箱: 包装类型转换成对应的基本类型
实际上, 装箱是调用了包装类的 valueOf()
方法, 而拆箱是调用了 xxxValue()
方法
频繁地拆装箱会严重影响系统的性能
为什么浮点数运算会有精度丢失的风险
这个是计组的内容, Java Guide里面讲的不是很详细, 我从CSAPP里面再详细讲一下
目前我们使用的浮点数标准是 IEEE浮点标准 , IEEE浮点标准用 V = ( − 1 ) s × M × 2 E V = (-1)^s \times M \times 2^E V=(−1)s×M×2E 的形式表示一个数
-
符号 s s s 决定这个数是正数( s = 0 s = 0 s=0)还是负数( s = 0 s=0 s=0) , 对于数值 0 的符号位作为特殊情况处理
-
尾数 M M M 是一个二进制小数, 它的范围是 1 ∼ 2 − ε 1 \sim 2-\varepsilon 1∼2−ε , 或者是 0 ∼ 1 − ε 0\sim 1-\varepsilon 0∼1−ε
-
阶码 E E E 的作用是对浮点数加权, 这个权重是 2 2 2 的 E E E 次幂
将浮点数的位表示划分为三个字段, 分别对这些值进行编码
- 一个单独的符号位的符号位直接编码符号 s s s
- k k k 位的阶码字段 exp = e k − 1 ⋯ e 1 e 0 \exp = e_{k-1}\cdots e_1e_0 exp=ek−1⋯e1e0 编码阶码 E E E
- n n n 位小数字段 frac = f n − 1 ⋯ f 1 f 0 f_{n-1}\cdots f_1f_0 fn−1⋯f1f0 编码尾数 M M M , 但是编码出来的值也依赖于阶码字段的值是否等于 0
根据 exp \exp exp 的值, 被编码的值可以分成三种不同的情况
- 情况1: 规格化的值
当 exp \exp exp 的位模式不全为 0, 也不全为 1(单精度数值为255, 双精度数值为2047)
在这种情况下, 阶码字段被解释为以 偏置 表示的有符号整数, 也就是说, 阶码的值 E = e − B i a s E = e - Bias E=e−Bias , 其中 e e e 是无符号数, 其位表示是 e k − 1 ⋯ e 1 e 0 e_{k-1}\cdots e_1e_0 ek−1⋯e1e0 , 而 B i a s Bias Bias 是一个等于 2 k − 1 − 1 2^{k-1}-1 2k−1−1 (单精度是127, 双精度是1023) 的偏置值.
小数字段 frac 被解释为描述小数值 f f f , 其中 0 ≤ f < 1 0\le f < 1 0≤f<1, 其二进制表示为 0. f n − 1 ⋯ f 1 f 0 0.f_{n-1}\cdots f_1f_0 0.fn−1⋯f1f0 , 也就是二进制小数点在最高有效位的左边.
尾数定义为 M = 1 + f M = 1 + f M=1+f .
- 情况2: 非规格化的值
当阶码域全为 0 时, 所表示的数是非规格化形式
非规格化有两个用途:
- 它提供了一种表示数值0的方法. +0.0的浮点表示的位模式全为0 : 符号位为0, 阶码字段全为0, 而小数域也全为0. 当符号位为1, 阶码和小数域全为0时, 就得到了值 -0.0. 值 +0.0 和 -0.0发在某些方面认为是不同了, 而在其他一些方面认为是相同的.
- 非规格化数另一个功能是表示那些非常接近0的数, 它们提供了一种属性, 称为 逐渐下溢
- 情况3: 特殊值
最后一类数值是当阶码全为1的时候出现的, 当小数域全为0时, 得到的值表示无穷, 当 s = 0 s = 0 s=0 时是 + ∞ +\infty +∞ , 或者当 s = 0 s = 0 s=0 时是 − ∞ -\infty −∞ . 当小数域非 0 时, 结果值被称为 “NAN”, 即 “不是一个数(Not a Number)”
关于浮点数运算, IEEE 标准指定了一个简单的规则, 来确定诸如加法和乘法这样的算术运算的结果
把浮点值 x x x 和 y y y 看成实数, 而某个运算 ⊙ \odot ⊙ 定义在实数上, 计算将产生 R o u n d ( x ⊙ y ) Round(x\odot y) Round(x⊙y) , 这是对实际运算的精确结果进行舍入之后的结果
浮点加法不具有结合性
假设编译器给定了如下代码片段
x = a + b + c;
y = b + c + d;
编译器可能会通过产生下列代码来省去一个浮点加法
t = b + c;
x = a + t;
y = t + d;
然而, 对于 x 来说, 这个运算可能会产生与原始值不同的值, 因为它使用了和加法运算不同的结合方式.
编译器无法知道在效率和忠于原始程序的确切行为之间, 使用者愿意做出什么选择, 结果是, 编译器倾向于保守, 避免任何对功能产生影响的优化, 即使是很轻微的影响
浮点加法满足了单调性属性
如果 a ≥ b a \ge b a≥b , 那么对于任何 a a a, b b b 以及 x x x 的值, 除了 N a N NaN NaN , 都有 x + a ≥ x + b x + a \ge x + b x+a≥x+b . 无符号或补码加法不具有这个实数加法的属性
浮点乘法 也遵循乘法所具有的许多属性. 定义 x ∗ f y x *^f y x∗fy 为 R o u n d ( x × y ) Round(x \times y) Round(x×y) . 这个运算在乘法中是封闭的, 它是可交换的, 另一方面, 由于可能发生溢出, 或者由于舍入失去精度, 它不具有可结合性
既然浮点数运算有精度丢失问题, 如何规避?
简单回答: 用大数类( BigDecimal
) 来实现
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
接下来简单讲讲BigDecimal
的使用
- 创建
我们在使用 BigDecimal
时, 为了防止精度丢失, 推荐使用它的BigDecimal(String val)
构造方法或者BigDecimal.valueOf(double val)
静态方法来创建对象.
根据阿里巴巴Java开发准则, 禁止使用构造方法 BigDecimal(double)
的方式将double变量转为BigDecimal对象
究其原因还是上面已经提及的精度丢失问题, 这里不再赘述
- 加减乘除
add
方法用于将两个 BigDecimal
对象相加, subtract
方法用于将两个 BigDecimal
对象相减. multiply
方法用于将两个 BigDecimal
对象相乘, divide
方法用于将两个 BigDecimal
对象相除.
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.add(b));// 1.9
System.out.println(a.subtract(b));// 0.1
System.out.println(a.multiply(b));// 0.90
System.out.println(a.divide(b));// 无法除尽,抛出 ArithmeticException 异常
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));// 1.11
- 大小比较
a.compareTo(b)
返回 -1表示 a
小于 b
, 0 表示相等, 1 表示 a
大于 b
根据阿里巴巴Java开发准则, BigDecimal的等值比较应当使用compareTo()
方法, 而不应该是 equal()
方法. 因为 equal()
会比较值和精度, 而 compareTo()
会忽略精度进行比较
变量
成员变量和局部变量的区别?
- 成员变量是属于类的, 而局部变量是在方法或者代码块中定义的变量或者是方法的参数. 成员变量可以被
public
,private
,static
等修饰符修饰, 但是局部变量不行. 局部变量和成员变量都能被final
修饰 - 从变量在内存中的存储方式来看, 如果成员变量是使用
static
修饰的, 那么这个成员变量是属于类的, 如果没有使用static
修饰, 这个成员变量是属于实例的. 而对象存在于堆内存, 局部变量则存在于栈内存 - 成员变量的生存时间是跟着类生成的对象的, 对象生成, 它就产生, 对象被销毁, 它也就消失. 局部变量的生存时间主要是看代码运行的位置有没有跳出它的作用域(也就是它所处的代码块)
- 成员变量如果没有被赋初始值, 则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值), 而局部变量则不会自动赋值
为什么成员变量要有默认值
-
先不考虑变量类型, 如果没有默认值会怎样? 变量存储的是内存地址对应的任意随机值, 程序读取该值运行会出现意外
-
默认值有两种设置方式: 手动和自动, 根据第一点, 没有手动赋值一定要自动赋值. 成员变量在运行时可借助反射等方法手动赋值, 而局部变量不行
-
对于编译器(javac)来说, 局部变量没赋值很好判断, 可以直接报错. 而成员变量可能是运行时赋值, 无法判断, 误报“没默认值”又会影响用户体验, 所以采用自动赋默认值
public class VariableExample {
// 成员变量
private String name;
private int age;
// 方法中的局部变量
public void method() {
int num1 = 10; // 栈中分配的局部变量
String str = "Hello, world!"; // 栈中分配的局部变量
System.out.println(num1);
System.out.println(str);
}
// 带参数的方法中的局部变量
public void method2(int num2) {
int sum = num2 + 10; // 栈中分配的局部变量
System.out.println(sum);
}
// 构造方法中的局部变量
public VariableExample(String name, int age) {
this.name = name; // 对成员变量进行赋值
this.age = age; // 对成员变量进行赋值
int num3 = 20; // 栈中分配的局部变量
String str2 = "Hello, " + this.name + "!"; // 栈中分配的局部变量
System.out.println(num3);
System.out.println(str2);
}
}
静态变量有什么用?
静态变量, 也就是被 static
修饰的变量, 它可以被类的所有实例共享, 也就是说无论一个类创建了多少对象, 它们都共享一个静态变量. 因为静态变量只会被分配一次内存, 即使创建多个对象, 所以静态变量可以节省内存
静态变量一般通过类名访问, 如下所示
class StaticVariableExample {
public static int example = 0;
}
public class main {
public static void main(String[] args) {
System.out.println(StaticVariableExample.example);
}
}
其实, 对象也可以访问静态变量, 但是很容易和成员变量混淆, 所以建议用类名访问 (我之前有一次刷牛客的Java客观题, 就搞错了这一点)
静态变量如果再被 final
修饰, 就会变成常量
class StaticVariableExample {
public static final int example = 0;
}
public class main {
public static void main(String[] args) {
System.out.println(StaticVariableExample.example);
}
}
字符型常量和字符串常量有什么区别?
-
形式 : 字符常量是单引号引起的一个字符, 字符串常量是双引号引起的 0 个或若干个字符
-
含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
-
占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节
方法
静态方法为什么不能调用非静态成员?
- 静态方法是属于类的, 在类加载的时候就会分配内存, 可以通过类名直接访问, 而非静态成员需要对象实例化之后才存在, 需要通过类的实例对象去访问
- 在非静态成员不存在的时候静态方法就已经存在了, 如果此时去调用不存在的非静态成员, 是非法操作(试图读取未知的内存)
静态方法和实例方法有什么不同?
- 调用方式
在外部调用静态方法时, 可以使用 类名.方法名
的方式, 也可以使用 对象.方法名
的方式, 而实例方法只有后面这种方式. 也就是说, 调用静态方法可以无需创建对象
但是和前面提到的静态变量调用同理, 不建议用 对象.方法名
的方式进行调用
- 访问类成员是否存在限制
静态方法在访问本类的成员时, 只允许访问静态成员 (即静态成员变量和静态方法) , 不允许访问实例成员 (即实例成员变量和实例方法) , 而实例方法不存在这个限制
重载和重写有什么区别?
重载: 发生在同一个类中 (或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同, 方法返回值和访问修饰符可以不同
重写: 重写发生在运行期, 是子类对父类的允许访问的方法的实现过程进行重新编写
- 方法名、参数列表必须相同, 子类方法返回值类型应比父类方法返回值类型更小或相等, 抛出的异常范围小于等于父类, 访问修饰符范围大于等于父类
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法, 但是被static
修饰的方法能够被再次声明 - 构造方法无法被重写
区别 | 重载 | 重写 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能改 |
返回类型 | 可以改 | 子类返回类型应当比父类的更小或者相等 |
异常 | 可以改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等 |
访问修饰符 | 可以改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译 | 运行 |