进击的Java面试

1. 自增自减问题

1.1 i++

对于i++,在程序执行时会先将i赋值给变量,等语句执行完以后i的值再加1。如:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int a;
int i = 1;
// 先将i的值1赋给a,所以a=1。然后i再加1,i = 2
a = i++;
System.out.println("a的值为:" + a);
System.out.println("i的值为:" + i);
}

运行结果:

1
2
a的值为:1
i的值为:2

1.2 ++i

对于++i,程序运行时先将 i的值加1再赋值给变量,当然语句执行结束后i的值。如:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int b;
int i = 1;
// 先将i的值加1然后再赋给b,所以b=2;i=2
b = ++i;
System.out.println("b的值为:" + b);
System.out.println("i的值为:" + i);
}

运行结果:

1
2
b的值为:2
i的值为:2

1.3 综合问题

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
int i = 1;
i = i++; // i=1
int j = i++; // j=1,i=2
int k = i + ++i * i++; // k=2+3*3=11,i=4

System.out.println("i的值为:"+ i);
System.out.println("j的值为:"+ j);
System.out.println("k的值为:"+ k);
}

运行结果:

1
2
3
i的值为:4
j的值为:1
k的值为:11

分析:

通过javap命令对字节码进行反汇编:javap -c MyCode.class,得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Method descriptor #15 ([Ljava/lang/String;)V
// Stack: 4, Locals: 4
public static void main(java.lang.String[] args);
0 iconst_1
1 istore_1 [i]
2 iload_1 [i]
3 iinc 1 1 [i]
6 istore_1 [i]
7 iload_1 [i]
8 iinc 1 1 [i]
11 istore_2 [j]
12 iload_1 [i]
13 iinc 1 1 [i]
16 iload_1 [i]
17 iload_1 [i]
18 iinc 1 1 [i]
21 imul
22 iadd
23 istore_3 [k]

其中,0 iconst_11 istore_1 [i]表示源码中的int i = 1;语句:首先静态变量1被初始化,随后赋值给了变量i

istore_x表示将操作树栈的结果赋值给对应的局部变量。

iload_x表示将局部变量的值压入操作数的栈中。

iinc表示局部变量自身自增。

imul表示求乘积。

iadd表示求和。

02

03

1.4 小结

  • 表达式中,赋值(=)最后计算
  • = 右边的从左到右依次加载值,并压入操作数栈
  • 实际算哪个,看运算符的优先级
  • 自增、自减操作都是直接修改变量的值,不经过操作数栈
  • 最后的赋值操作之前,临时结果也是存储在操作数栈中

建议参考《JVM虚拟机规范》关于指令的部分。

2. 单例模式

Singleton:在Java中即指单例设计模式,它是软件开发中最常用的设计模式之一。

  • 单:唯一

  • 例:实例

单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。例如:代表JVM运行环境的Runtime类。

2.1 代码实现要点

实现单例模式的重要点:

  1. 某个类只能有一个实例;

    • 代码实现:构造器私有化
  2. 它必须自行创建这个实例

    • 代码实现:含有一个该类的静态变量来保存这个唯一的实例
  3. 必须自行向整个系统提供这个实例

    代码实现:对外提供获取该实例对象的方式:1)直接暴露。2)用静态变量的get方法获取。

2.2 单例类型

2.2.1 饿汉式

直接创建对象,不存在线程安全问题

  • 直接实例化饿汉式(简洁直观)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* 饿汉式:
* 在类初始化时直接创建实例对象,不管你是否需要这个对象都会创建
*
* (1)构造器私有化
* (2)自行创建,并且用静态变量保存
* (3)向外提供这个实例
* (4)强调这是一个单例,我们可以用final修改
*/
public class Singleton1 {
public static final Singleton1 INSTANCE = new Singleton1();
private Singleton1(){

}
}
  • 枚举式(最简洁)
1
2
3
4
5
6
7
/*
* 枚举类型:表示该类型的对象是有限的几个
* 我们可以限定为一个,就成了单例
*/
public enum Singleton2 {
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
26
27
28
29
30
31
32
33
public class Singleton3 {
public static final Singleton3 INSTANCE;
private String info;

static{
try {
Properties pro = new Properties()
// 从配置文件中加载类名 pro.load(Singleton3.class.getClassLoader().getResourceAsStream("single.properties"));

INSTANCE = new Singleton3(pro.getProperty("info"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private Singleton3(String info){
this.info = info;
}

public String getInfo() {
return info;
}

public void setInfo(String info) {
this.info = info;
}

@Override
public String toString() {
return "Singleton3 [info=" + info + "]";
}

}

2.2.2 懒汉式

延迟创建对象

  • 线程不安全(适用于单线程)
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
/*
* 懒汉式:
* 延迟创建这个实例对象
*
* (1)构造器私有化
* (2)用一个静态变量保存这个唯一的实例
* (3)提供一个静态方法,获取这个实例对象
*/
public class Singleton4 {
private static Singleton4 instance;

private Singleton4(){
}

public static Singleton4 getInstance() {
if(instance == null) {

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

instance = new Singleton4();
}
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
26
27
28
29
30
31
/*
* 懒汉式:
* 延迟创建这个实例对象
*
* (1)构造器私有化
* (2)用一个静态变量保存这个唯一的实例
* (3)提供一个静态方法,获取这个实例对象
*/
public class Singleton5 {
private static Singleton5 instance;

private Singleton5() {
}

public static Singleton5 getInstance() {
if(instance == null){
synchronized (Singleton5.class) {
if(instance == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

instance = new Singleton5();
}
}
}
return instance;
}
}
  • 静态内部类形式(适用于多线程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 在内部类被加载和初始化时,才创建INSTANCE实例对象
* 静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载和初始化的。
* 因为是在内部类加载和初始化时,创建的,因此是线程安全的
*/
public class Singleton6 {
private Singleton6() {
}

private static class Inner {
private static final Singleton6 INSTANCE = new Singleton6();
}

public static Singleton6 getInstance() {
return Inner.INSTANCE;
}
}

3. 走台阶算法(循环实现)

编程题

有n步台阶,一次只能上1步或2步,共有多少种走法?

3.1 思路分析

走第 1 个台阶

  • 走第 1 个台阶

    只有 1 种走法,即:走1步。

  • 走第 2 个台阶

    有 2 种走法,即:可以俩个 1 步,也可以走一个 2 步。

  • 走第 3 个台阶

    因为只能走 1 步或者 2 步,所以能走到最后一个台阶,一定是走到了前面某个台阶,再跨一步就到了最后一个台阶。

    因此,能走到第 3 个台阶的之前一个台阶,只能以下俩种可能:

    • 走到第 1 个台阶, 再走 2 步。
    • 走到第 2 个台阶,再走 1 步。
  • 依次类推

  • 走第 n 个台阶(n >= 3)

    因为只能走 1 步或者 2 步,所以只能走到最后一个台阶的情况,只能是下面这俩种可能:

    走到第 n-2 个台阶了,再走 2 步。

    走到第 n-1 个台阶了,再走 1 步。

从上述推理过程可以,推导出数学函数公式:

1
2
3
f(1) = 1
f(2) = 2
f(n) = f(n-1) + f(n-2), n>=3

这里的f(n)函数表示走到第 n 个台阶能走到的可能数。

3.2 递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 递归实现
public class TestStep1 {
@Test
public void test(){
long start = System.currentTimeMillis();
System.out.println(f(100));//165580141
long end = System.currentTimeMillis();
System.out.println(end-start);//586ms
}

//实现f(n):求n步台阶,一共有几种走法
public int f(int n) {
if(n<1){
throw new IllegalArgumentException(n + "不能小于1");
}

if(n==1 || n==2) {
return n;
}

return f(n-2) + f(n-1);
}
}

递归实现的最大弊端在于,递归太深可能会导致栈溢出,同时递归调用很耗费CPU且耗时。

3.3 循环实现

在递归中,如:

1
2
3
4
5
6
7
8
9
10
f(1) = 1
f(2) = 1
f(3) = f(1) + f(2)

// f(4) = f(2) + f(3), f(3) 需要用到上一步的 f(3),所以
f(4) = f(2) + f(1) + f(2)

// f(5) = f(3) + f(4), f(3) f(4) 需要用到上面的 f(3) 和 f(4),所以
f(5) = f(1) + f(2) + f(2) + f(1) + f(2)
……

从上面的分析可以出:f(5)中的f(1)+f(2)已经计算了2 次。

这算是一种不必要的浪费,可以将f(1)+f(2)的计算结果保存下来,在计算f(4)的使用这个结果能更快。

所以每次都到的台阶的前 2 种可能记住,那么就省去再次计算。

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
// 循环实现
public class TestStep2 {
@Test
public void test() {
long start = System.currentTimeMillis();
System.out.println(f(100));//165580141
long end = System.currentTimeMillis();
System.out.println(end-start);//<1ms
}

public int f(int n) {
if(n<1) {
throw new IllegalArgumentException(n + "不能小于1");
}
if(n==1 || n==2) {
return n;
}

int one = 2;//初始化为走到第二级台阶的走法
int two = 1;//初始化为走到第一级台阶的走法
int sum = 0;

for(int i=3; i<=n; i++) {
//最后跨2步 + 最后跨1步的走法
sum = two + one;
two = one;
one = sum;
}
return sum;
}
}
updated updated 2024-01-05 2024-01-05
本文结束感谢阅读

本文标题:进击的Java面试

本文作者:

微信公号:木鲸鱼 | woodwhales

原始链接:https://woodwhales.cn/2019/07/02/038/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%