Java 基础面试题之一
自增变量
代码案例:
1 | public class Test { |
知识点:
- 局部变量表 (Local Variable Table):一组变量值的存储空间,用于存放方法参数和方法内定义的局部变量
- 操作数栈:在内存分析的时候,都被放入了栈中,栈的特点是先进后出(LIFO),意味着先放进去的数,会被放在下面,后进去的数,一个一个垒在上面
- iconst 虚拟机指令:将常量加载到操作数栈上(入栈),可以用来将 int 类型的数字、取值在 -1 到 5 之间的整数压入栈中
- push 虚拟机指令:主要包括 bipush 和 sipush,它们的区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数,它们都可以将参数压入栈
- istore_n 虚拟机指令:从操作数栈中弹出一个整数,并把它赋值给第 n 个局部变量
- xload_n 虚拟机指令:表示将第 n 个局部变量压入操作数栈,比如 iload_1、fload_0、aload_0 等指令,其中 aload_n 表示将一个对象引用压栈
- iinc 虚拟机指令:对给定的局部变量做自增操作,这条指令是少数几个执行过程中完全不修改操作数栈的指令;它接收两个操作数:第 1 个局部变量表的位置,第 2 个位累加数。比如常见的 i++ 就会产生这条指令
问题分析:
下面将使用 javap
工具来分析问题,javap
是 JDK 自带的反汇编器,可以查看 Java 编译器生成的字节码。通过它,可以对照源代码和字节码,从而更了解编译器内部的工作过程。执行以下命令:
- 编译命令:
javac Test.java
- 反汇编命令:
javap -c Test
javap
反汇编后,会输出以下内容,其中main()
里是varNum = varNum++
的执行过程
1 | Compiled from "Test.java" |
结合上面铺垫的虚拟机指令,这里讲解一下 main()
里的 0 -11 步骤的工作流程:
- 0: bipush 10 将参数 10 压入操作数栈
- 2: istore_1 从操作数栈中弹出一个数,赋给第一个局部变量(a)
- 3: bipush 66 将参数 66 压入操作数栈
- 5: istore_2 从操作数栈中弹出一个数,赋给第二个局部变量(varNum)
- 6: iload_2 将第二个局部变量(varNum)的值入栈,此时操作数栈的栈顶值为 66
- 7: iinc 2, 1 对第二个局部变量(varNum)做自增 1 操作,意味着局部变量 varNum 的值变为 67;特别注意,这里并没有修改操作数栈里的任何内容
- 10: istore_2 从操作数栈的栈顶弹出一个数(即 66)赋给第二个局部变量(varNum),意味局部变量 varNum 的值又变回 66 了
- 11: iload_2 将第二个局部变量(varNum)的值入栈,此时操作数栈的栈顶值为 66
- 最终打印结果就是:66
Java Lang Spec 在文中也提到:
- 自增运算符
++
的优先级大于赋值运算符=
- 原文描述为
The result of the postfix increment expression is not a variable, but a value
,翻译过来就是:后++
符表达式的结果是个值而不是一个变量
进阶案例代码:
1 | public class Test { |
★展开完整的 Java 字节码★
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_1
7: iload_1
8: iinc 1, 1
11: istore_2
12: iload_1
13: iinc 1, 1
16: iload_1
17: iload_1
18: iinc 1, 1
21: imul
22: iadd
23: istore_3
24: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
27: new #3 // class java/lang/StringBuilder
30: dup
31: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
34: ldc #5 // String i=
36: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: iload_1
40: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
43: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
46: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
49: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
52: new #3 // class java/lang/StringBuilder
55: dup
56: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
59: ldc #10 // String j=
61: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
64: iload_2
65: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
68: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
71: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
74: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
77: new #3 // class java/lang/StringBuilder
80: dup
81: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
84: ldc #11 // String k=
86: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
89: iload_3
90: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
93: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
96: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
99: return
}
案例小结:
- 赋值操作符
=
最后计算 - 赋值操作符
=
右边的从左到右加载值依次压入操作数栈 - 实际先算哪个,看运算符的优先级
- 自增、自减操作都是直接修改变量的值,不经过操作数栈
- 在最后的赋值之前,临时结果都是存储在操作数栈中
方法参数传递
知识点:
- 形参是基本数据类型时
- 传递数据值
- 实参是引用数据类型
- 传递地址值:类对象类型、数组
- 特殊的类型:String、包装类(如 Integer)等对象拥有不可变性
代码案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Example {
public static void main(String[] args) {
int i = 1;
String str = "hello";
Integer num = 200;
int[] arr = { 1, 2, 3, 4, 5 };
MyData my = new MyData();
change(i, str, num, arr, my);
System.out.println("i = " + i);
System.out.println("str = " + str);
System.out.println("num = " + num);
System.out.println("arr = " + Arrays.toString(arr));
System.out.println("my.a = " + my.a);
// 运行结果
// i = 1
// str = hello
// num = 200
// arr = [2, 2, 3, 4, 5]
// my.a = 11
}
public static void change(int j, String s, Integer n, int[] a, MyData data) {
j += 1;
s += "world";
n += 1;
a[0] += 1;
data.a += 1;
}
}
class MyData {
int a = 10;
}
分析过程: 点击图解查看分析过程
类与实例初始化
类初始化
类的初始化过程:
- 一个类要创建实例需要先加载并初始化该类(
main()
方法所在的类需要先加载和初始化) - 一个子类要初始化需要先初始化父类
- 一个类初始化,本质就是执行
<clinit>()
方法<clinit>()
方法由静态类变量赋值代码和静态代码块组成- 静态类变量赋值代码和静态代码块代码从上到下顺序执行
<clinit>()
方法只会执行一次
实例初始化
实例的初始化过程,本质就是执行 <init>()
方法:
<init>()
方法可能重载有多个,有几个构造器就有几个 <init>()
方法<init>()
方法由非静态实例变量赋值代码和非静态代码块、对应构造器代码组成- 非静态实例变量赋值代码和非静态代码块代码从上到下顺序执行,而构造器的代码永远最后执行
- 每次创建实例对象,调用对应构造器,执行的就是对应的
<init>()
方法 <init>
方法的首行是 super()
或 super(实参列表)
,即对应父类的 <init>
方法
重写与重载
两者的区别:
- 重写(Override)也称覆盖,它是父类与子类之间多态性的一种表现,而重载(Overload)是一个类中多态性的一种表现
- 重写(Override 它是覆盖了父类的一个方法并且对其重写,以求达到不同作用
- 重载(Overload)它是指定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法
重写(Override)的规则
- 参数列表必须与被重写方法的相同
- 非抽象子类中必须重写父类中的 abstract 方法
- 访问修饰符的限制一定不能不小于被重写方法的访问修饰符
- 不能重写被标识为 final、private、static 的方法(子类中不可见的方法)
- 子类直接再写一个同名的方法,这并不是对父类方法进行重写(Override),而是重新生成一个新的方法
- 重写的方法不能抛出新的异常,或者超过了父类范围的异常,但是可以抛出更少、更有限的异常,或者不抛出异常
重载(Overload)的规则:
- 重载是针对于一个类而言的
- 不能重载只有返回值类型不同的方法
- 方法的异常类型和数目不会对重载造成影响
- 不能通过访问权限、返回类型、抛出的异常进行重载
- 与被重载的方法比较,参数的类型、个数、顺序至少有一个不相同
对象的多态性
- 非静态方法默认的调用对象是
this
this
对象在构造器或者说 <init>()
方法中就是正在创建的对象- 子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过的代码
面试案例分析
问答互动:
- 为什么在实例化子类的对象时,会先调用父类的构造器?
- 子类继承父类后,获取到父类的属性和方法,这些属性和方法在使用前必须先初始化,所以须先调用父类的构造器进行初始化
- 在哪里调用父类的构造器?
- 父类的构造器是不能被继承的,但可以用
super()
来调用 - 在子类构造器的第一行会隐式的调用
super()
,即调用父类的默认无参构造器 - 如果父类中没有定义无参的构造器,则必须在子类的构造器的第一行显式地调用
super(参数)
,以此调用父类中的有参构造器
代码案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Father {
private int i = test();
private static int j = method();
static {
System.out.print("(1)");
}
public Father() {
System.out.print("(2)");
}
{
System.out.print("(3)");
}
public int test() {
System.out.print("(4)");
return 1;
}
public static int method() {
System.out.print("(5)");
return 1;
}
public static void main(String[] args) {
Father f1 = new Father();
System.out.println();
Father f2 = new Father();
}
// 运行结果:
// (5)(1)(4)(3)(2)
// (4)(3)(2)
// 分析结果:
// 静态类变量赋值代码和静态代码块代码从上到下顺序执行 (5)(1)
// 非静态实例变量赋值代码和非静态代码块代码从上到下顺序执行 (4)(3)
// 构造器的代码永远最后执行 (2)
// 由于创建了两个 Father 对象,因此实例化方法 <init>() 执行了两次 (4)(3)(2)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Son extends Father{
public Son() {
}
public static void main(String[] args){
Son test = new Son();
}
// 运行结果:
// (5)(1)(4)(3)(2)
// 分析过程:
// 实例化子类的对象时,默认会先通过 super() 调用父类的构造器方法,即先创建父类对象
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Son extends Father {
private int i = test();
private static int j = method();
static {
System.out.print("(6)");
}
public Son() {
System.out.print("(7)");
}
{
System.out.print("(8)");
}
public int test() {
System.out.print("(9)");
return 1;
}
public static int method() {
System.out.print("(10)");
return 1;
}
public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
}
// 运行结果:
// (5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
// (9)(3)(2)(9)(8)(7)
// 分析过程:
// 初始化父类和子类:
// 1) 先初始化父类:(5)(1)
// 2) 初始化子类:(10)(6)
// 调用子类的实例化方法 <init>():
// 1) super(),期间调用了子类中被重写的 test() 方法 (9)(3)(2)
// 2) i= test() (9)
// 3) 子类的非静态代码 (8)
// 4) 子类的无参构造方法 (7)
// 由于创建了两个 Son 对象,因此实例化方法 <init>() 执行了两次
// (9)(3)(2)(9)(8)(7)
}
局部变量与成员变量
知识点:
- 就近原则
- 变量的分类
- 局部变量
- 成员变量:包括类变量、实例变量
- 局部变量与成员变量的区别
- 声明的地方
- 局部变量声明的地方:方法体 {} 中、形参、代码块 {} 中
- 成员变量声明的地方:类中方法外
- 类变量:有 static 修饰
- 实例变量:没有 static 修饰
- 修饰符的使用
- 局部变量:final
- 成员变量:public、protected、private、final、static、volatile、transient
- 值存储的位置
- 局部变量:栈
- 成员变量
- 类变量:方法区
- 实例变量:堆
- 作用域
- 局部变量:从声明处开始,到所属的
}
结束 - 成员变量
- 类变量:在当前类中通过
类名.
访问,在其他类中通过 类名.
或者 对象名.
访问 - 实例变量:在当前类中通过
this.
,在其他类中通过 对象名.
访问
- 生命周期
- 局部变量:每一个线程,每一次调用执行都是新的生命周期
- 成员变量
- 类变量:随着类的初始化而初始化,随着类的卸载而消亡,该类的所有对象的类变量是共享的
- 实例变量:随着对象的创建而初始化,随着对象的被回收而消亡,每一个对象的实例变量都是互相独立的
- 当局部变量与成员变量重名时,如何区分?
- 局部变量与类变量重名:在类变量前加
类名.
- 局部变量与实例变量重名:在实例变量前加
this.
- 非静态代码块的执行:每次创建实例对象时都会执行
代码案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Example {
static int s;
int i;
int j;
{
int i = 1;
i++;
j++;
s++;
}
public void test(int j) {
i++;
j++;
s++;
}
public static void main(String[] args) {
Example obj1 = new Example();
Example obj2 = new Example();
obj1.test(10);
obj1.test(20);
obj2.test(30);
System.out.println(obj1.i + ", " + obj1.j + ", " + obj1.s);
System.out.println(obj2.i + ", " + obj2.j + ", " + obj2.s);
// 运行结果:
// 2, 1, 5
// 1, 1, 5
}
}
分析过程: 点击截图查看分析过程
设计模式
单例模式
知识点:
单例的特点
- 一是某个类只能有一个实例,即构造器私有化
- 二是必须自行创建这个实例,即含有一个该类的静态变量来保存唯一的实例
- 三是必须自行向整个系统提供这个实例,对外提供获取实例对象的方式一般是:直接通过静态变量暴露或者使用静态 Get 方法来获取
单例模式常见的几种形式
饿汉式(在类初始化时,直接创建对象,不存在线程安全的问题)
- 直接实例化饿汉式(简洁直观)
- 枚举式(最简洁)
- 静态代码块饿汉式(适合复杂的实例化)
懒汉式(延迟创建对象,可能存在线程安全的问题)
- 线程不安全(适用于单线程)
- 线程安全(适用于多线程)
- 静态内部类形式(适用于多线程)
饿汉式代码案例:
1
2
3
4
5
6
7
8
9
10
11
/**
* 饿汉式 - 直接实例化饿汉式(简洁直观)
*/
public class HungrySingleton {
public static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {
}
}
1
2
3
4
5
6
/**
* 饿汉式 - 枚举式(最简洁)
*/
public enum HungrySingleton {
INSTANCE
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 饿汉式 - 静态代码块饿汉式(适合复杂的实例化)
*/
public class HungrySingleton {
public static final HungrySingleton INSTANCE;
static {
// do anything
INSTANCE = new HungrySingleton();
}
private HungrySingleton() {
}
}
懒汉式代码案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 懒汉式 - 线程不安全(适用于单线程)
*/
public class LazySingleton01 {
private static LazySingleton01 instance;
private LazySingleton01() {
}
public static LazySingleton01 getInstance() {
if (instance == null) {
instance = new LazySingleton01();
}
return instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 懒汉式 - DCL(双端检锁),适用于多线程
* 1) DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排序的存在,加入 volatile 可以禁止指令重排
* 2) 原因在多线程环境下,某一个线程执行到第一个检测,读取到的 instance 不为 null 时,instance 的引用对象可能没有完成初始化
* 3) 指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问 instance 不为 null 时,由于 instance 实例未必已初始化完成,也就造成了线程安全问题
*/
public class LazySingleton02 {
private static volatile LazySingleton02 instance;
private LazySingleton02() {
}
public static LazySingleton02 getInstance() {
if (instance == null) {
synchronized (LazySingleton02.class) {
if (instance == null) {
instance = new LazySingleton02();
}
}
}
return instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 懒汉式 - 静态内部类形式(适用于多线程)
* 1)静态内部类不会自动随着外部类的加载和初始化而初始化,它是单独去加载和初始化的
* 2)因为是在静态内部类加载和初始化时,才创建单例对象,因此是线程安全的
*/
public class LazySingleton03 {
private LazySingleton03() {
}
public static LazySingleton03 getInstance() {
return Singleton.instance;
}
private static class Singleton {
private static final LazySingleton03 instance = new LazySingleton03();
}
}