Java 基础面试题之一

Java 基础

自增变量

代码案例:

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) {
int a = 10;
int varNum = 66;
varNum = varNum++;
System.out.println(varNum);
}

// 运行结果: 66
}

知识点:

  • 局部变量表 (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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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: bipush 10
2: istore_1
3: bipush 66
5: istore_2
6: iload_2
7: iinc 2, 1
10: istore_2
11: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
14: iload_2
15: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
18: return
}

结合上面铺垫的虚拟机指令,这里讲解 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
2
3
4
5
6
7
8
9
10
11
12
public class Test {

public static void main(String[] args) {
int i = 1;
i = i++;
int j = i++;
int k = i + ++i * i++;
System.out.println("i=" + i); // 4
System.out.println("j=" + j); // 1
System.out.println("k=" + k); // 11
}
}
★展开完整的 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();
}
}