加载中...

Java面试题总结


自增变量

主要是对 i++++i 的理解


public class Test01 {
    public static void main(String[] args) {
        // 变量自增 i++ ++i的理解
        int i = 1;
        i = i++; // 
        int j = i++;
        int k = i + ++i * i++;
        System.out.println("i=" + i);
        System.out.println("j=" + j);
        System.out.println("k=" + k);
    }
}
i=4
j=1
k=11

分析i = i++i的初始值是1,然后执行i = i++,先算等号后面的i++++i的后面,所以先把1压入操作数栈,然后i++,现在i在局部变量表中=2,再将操作数栈中的1赋值给i,即将2给覆盖掉了,i最终还是1

分析int j = i++,与上个步骤同理。先把1压入操作数栈,i++i在局部变量表中的值变为2,然后将操作数栈中的1赋值给j,最终j=1,而i的2没有被覆盖此时i的值为2

分析int k = i + ++i * i++;:先算等号后面的,i目前的值是2,++i的话先赋值,i变为3,,再将i=3压入栈中,i++先把i=3压入操作数栈,然后i++,i此时在局部变量表中等于4,运算是在操作数栈中运算完后再赋值给局部变量,此时k=2+3*3即11

总结

  • 赋值=,最后计算

  • =右边的从左到右加载值依次压入操作数栈

  • 实际先算哪个,看运算符优先级

  • 自增、自减操作都是直接修改变量的值,不经过操作数栈

  • 最后的赋值之前,临时结果也是存储在操作数栈中

类初始化和实例初始化


// 类初始化和实例初始化

/**
 * 先父类初始化
 * (1): j = method(); (5)
 * (2): 父类静态代码块 (1)
 *
 * 子类的实例化方法
 * (1) super()(最前)
 * (2) i = test(); 子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过的代码: (9)
 * (3) 子类的非静态代码块 (3)
 * (4) 子类的无参构造(最后) (2)
  */
public class Father {
    private int i = test();
    private static int j = method();

    static {
        System.out.print("(1)");
    }

    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;
    }

}

/**
 * 先初始化父类: (5) (1)
 * 再子类初始化
 * (1): j = method(); (10)
 * (2): 子类静态代码块 (6)
 *
 * 子类的实例化方法
 * (1) super()(最前) (9) (3) (2)
 * (2) i = test(); (9)
 * (3) 子类的非静态代码块 (8)
 * (4) 子类的无参构造(最后) (7)
 *
 * 因为创建了两个对象,因此实例化方法<init>创建了两次 (9)(3)(2)(9)(8)(7)
 */
public class Son extends Father{
    private int i = test();
    private static int j = method();

    static {
        System.out.print("(6)");
    }

    Son() {
        // super(); // 子类构造器中一定会调用父类的构造器
        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 son1 = new Son();
        System.out.println();
        Son son2 = new Son();
    }
}
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
  • 类初始化过程

    • 一个类要创建实例需要先加载并初始化该类
      • main方法所在的类需要先加载和初始化
    • 一个子类要初始化需要先初始化父类
    • 一个类初始化就是执行<clinit>()方法
      • <clinit>()方法由静态类变量显示赋值代码和静态代码块组成
      • 类变量显示赋值代码和静态代码块代码从上到下顺序执行
      • <clinit>()方法只执行一次
  • 实例初始化过程

  • 实例初始化就是执行<init>()方法

    • <init>()方法可能重载有多个,有几个构造器就有几个<init>方法
  • <init>()方法由非静态实例变量显示赋值代码和非静态代码块、对应构造器代码组成

    • 非静态实例变量显示赋值代码和非静态代码块代码从上到下顺序执行,而对应构造器的代码最后执行
  • 每次创建实例对象,调用对应构造器,执行的就是对应的<init>方法

    • <init>方法的首行是super()或super(实参列表),即对应父类的<init>方法
  • 方法的重写

    • 哪些方法不可以被重写

      • final方法
      • 静态方法
      • private等子类中不可见方法
    • 对象的多态性

    • 子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过的代码

      • 非静态方法默认的调用对象是this
    • this对象在构造器或者说<init>方法中就是正在创建的对象

方法的参数传递机制

import java.util.Arrays;

// 方法的参数传递机制
public class Test02 {
    public static void main(String[] args) {
        int i = 1;
        String str = "hello";
        Integer num = 200;
        int[] arr = {1, 2, 3, 4, 5};
        MyData myData = new MyData();
        change(i, str, num, arr, myData);
        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("myData.a = " + myData.a);
    }

    public static void change(int j, String s, Integer n, int[] a, MyData m) {
        j += 1; 
        s += "world";
        n += 1;
        a[0] += 1;
        m.a += 1;
    }

}

class MyData {
    int a = 10;
}
i = 1
str = hello
num = 200
arr = [2, 2, 3, 4, 5]
myData.a = 11
  • 形参是基本数据类型
    • 传递数据值

形式参数如果是基本数据类型,形式参数改变对实际参数没有影响!(传递的时具体数值)

形式参数如果是引用类型(数组,类,接口),形式参数的改变对实际参数有很大影响(传递的是地址)

  • 实参是引用数据类型
    • 传递地址值
    • 特殊的类型:String、包装类等对象不可变性

i是基本数据类型单独的一份,(地址没有告诉我们,不会变),j变成了2,而i还是1、str和num是引用数据类型,因为他们的不可变性,导致一变换会产生新的对象,因此原来的对象没有变化,数组是属性修改或者说是元素修改,是会变化的

再看一个案例

形参为基本类型

当形式参数为基本类型时,传递过来的是一个值.方法在调用后,会在栈空间开辟一个空间,创建一个局部变量,然后将接受到的值复制到形式参数的变量上,然后对其进行操作.在方法结束时,创建的局部变量也会消失.同时其原始数据并没有收到影响.

class Demo{
	public static void main (String[] args ){
		int a=3;
		int b=4;
		demo(a,b);
		System.out.println("a="+a+",b="+b); // a=3,b=4
	}
	public static void demo( int a,int b){
		a=a*2;
		b=a+b;
		System.out.println("a="+a+",b="+b);//a=6,b=10
	}
}
a=6,b=10
a=3,b=4

形参为引用类型

当形式参数为引用类型时,传递过来的值是一个堆内存的地址.调用方法后,系统会在栈空间开辟一个空间,创建一个对象,当接收到地址值后,会将刚创建的对象指向地址,然后方法对引用类型的操作实际上操作的是在堆空间存放的原始数据.当方法结束后,方法中创建的对象排队释放,而原始的引用类型依然指向堆空间,所以原始数据发生了变化.

class Demo2{
	public static void main (String[] args ){
		
		int[] arr={1,2,3};
		System.out.println("调用前的数组元素"+arr[0]+","+arr[1]+","+arr[2]);
		demo2(arr);
		System.out.println("调用后的数组元素"+arr[0]+","+arr[1]+","+arr[2]); 
		
	}
	public static void demo2( int[] arr){
		arr[0]+=100;
		arr[1]+=100;
		arr[2]+=100;
		System.out.println("调用中的数组元素"+arr[0]+","+arr[1]+","+arr[2]);
	}	
}
调用前的数组元素1,2,3
调用中的数组元素101,102,103
调用后的数组元素101,102,103

递归和迭代

上台阶问题递归分析示意图


// 递归
public class Test03 {
    @Test
    public void test() {
        System.out.println("10个台阶上去的方式有 "+step(10)+" 种");
    }

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

}
10个台阶上去的方式有89

上台阶问题迭代分析示意图


// 迭代
public class Test04 {
    @Test
    public void test() {
        System.out.println("10个台阶上去的方式有 " + step(4) + " 种");
    }

    public int step(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步和+最后跨一步的走法
            sum = two + one;
            two = one;
            one = sum;
        }
        return sum;
    }

}
10个台阶上去的方式有89

成员变量与局部变量

package interview;

/**
 * @author shaoshao
 * @version 1.0
 * @date 2022/8/4 12:20
 */
public class Test05 {
    static int s;
    int i;
    int j;

    {
        int i = 1;
        i++;
        j++;
        s++;
    }

    public void test(int j){
        j++;
        i++;
        s++;
    }

    public static void main(String[] args) {
        Test05 obj1 = new Test05();
        Test05 obj2 = new Test05();
        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
这个结果只要理解了不同的值存储的位置就好理解了
视频地址:https://www.bilibili.com/video/BV1xt411S7xy?p=6&vd_source=5a24abffb167a75d9836a8ee2c24dc7e    
  • 成员变量:类变量、实例变量

  • 局部变量

非静态代码块的执行:每次创建实例对象都会执行

方法的调用规则:调用一次执行一次

局部变量成员变量的区别

  • 声明的位置:
    • 局部变量:方法体{}中,形参中,代码块中
    • 成员变量:类中方法外
      • 类变量:有static修饰
      • 实例变量:无static修饰
  • 修饰符:
    • 局部变量final
    • 成员变量publicprotectedprivatefinalstaticvolatiletransient
  • 值存储的位置:
    • 局部变量:栈
    • 实例变量:堆 new对象
    • 类变量:方法区
  • 作用域:
    • 局部变量:从声明处开始,到所属的}结束
    • 实例变量:在当前类中“this.”(有时this.可以缺省),在其他类中“对象名.”访问
    • 类变量:在当前类中“类名.”(有时类名.可以省略),在其他类中“类名.”或“对象名.”访问
  • 声明周期:
    • 局部变量:每一个线程,每一次调用执行都是新的生命周期
    • 实例变量:随着对象的创建而初始化,随着对象的被回收而消亡,每一个对象的实例变量是独立的
    • 类变量:随着类的初始化而初始化,随着类的卸载而消亡,该类的所有对象的类变量是共享的
  • 局部变量与实例变量重名:在实例变量前面加“this.”
  • 局部变量与类变量重名:在类变量前面加“类名.”

文章作者: shaoshaossm
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 shaoshaossm !
评论
  目录