面试总结
类的初始化过程和实例初始化
父类
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;
}
}
子类
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(); // 5 1 10 6 9 3 2 9 8 7
System.out.println();
Son s2 = new Son(); // 9 3 2 9 8 7
}
}
类初始化
- 一个类要创建实例需要先加载并初始化该类
- main方法所在的类需要先加载和初始化
- 一个子类要初始化需要先初始化父类
- 一个类初始化就是执行
<clinit>()
方法<clinit>()
方法由静态类变量显示复制代码和静态代码块组成- 类变量显示赋值代码和静态代码块代码从上到下的顺序执行
<clinit>()
方法只执行一次
分析上述的方法 假设子类方法中mian方法没有内容
当执行main方法时 先初始化父类中的静态变量和静态代码块,按顺序初始化
1. Father初始化时 private static int j = method();输出(5) 再初始化静态代码块输出(1)
2. Son类初始化时 初始化 private static int j = method(); 输出(10) 再输出静态代码块,输出(6)所以类初始化时输出(5)(1)(10)(6)
实例初始化
- 实例初始化就是执行
<init>()
方法<init>()
方法可能重载有多个,有几个构造器就有几个<init>
方法<init>()
方法由非静态实例变量显示赋值代码和非静态代码块、对应构造器代码组成- 非静态实例变量显示赋值代码和非静态代码块代码从上到下顺序执行,而对应的构造器代码最后执行
- 每次创建实例对象,调用对应构造器,执行的就是对应的
<init>
方法 <init>
方法首行是super()或super(实参列表),即对应父类的<init>
方法(子类的构造器,写或不写都存在super())
分析上述代码
子类的实例化方法
1. super() (最前) --> 执行父类的实例初始化 (9)(3)(2)
2. 非静态实例变量 i = test() (9)
3. 子类的非静态代码块 (8)
4. 子类的无参构造(最后) (7)
因为创建了两个实例 ,所以<init>
执行两次 (9)(3)(2)(9)(8)(7)(9)(3)(2)(9)(8)(7)父类的实例化
1. super() (最前)
2. 非静态实例变量 i = test() —> 执行的是子类重写的test()方法。输出 (9)
非静态方法前有一个默认的对象this
this 在构造器(或者<init>
)中表示正在创建的对象,因为this()执行的是子类,所以使用重写的代码(面向对象多态)
3. 父类的非静态代码块 (3)
4. 父类的无参构造(最后). (2)
综上所述,输出为:
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
- 补充:
- 哪些方法不能被重写
- final方法
- 静态方法
- private等子类中不可见的方法
- 对象的多态性
- 子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过的代码
- 非静态方法默认调用的对象是this
- this对象在构造器或者说
<init>
方法中就是正在创建的对象
- 哪些方法不能被重写
方法的传参机制,对象的不可变性
class Exam {
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);
}
public static void change(int j, String s, Integer n, int[] a, MyData myData) {
j += 1;
s += "world";
n += 1;
a[0] += 1;
myData.a += 1;
}
}
class MyData {
int a = 10;
}
方法的传参机制
-
实参给形参赋值
-
形参是基本数据类型
- 传递数据值
-
形参是引用数据类型
- 传递地址值
- 特殊的对象类型String、包装类等对象不可变性
-
上述程序分析
- 将方法传递到chenge时的分析如下,基本数据类型直接将值复制过去,引用数据对象,将自己的内存地址复制过去,两个方法栈中的情况如下
- 当方法执行时分析如下,change()中字符串类型重新生成新的字符串对象并指向新生成的对象,同时包装类型Integer对象生成新的对象并指向新的对象,但是数组和MyData中的对象并没有生成新的对象,所以指向如下
- 所以输出结果为
i=1
str = hello
num = 200
arr = [2, 2, 3, 4, 5]
my.a = 11
算法:台阶问题
- 问题描述: 有n步台阶,一次只能上2步或者1步,共有多少种走法?
方法一 递归
public int f(int n){
if (n < 1 ){
throw new IllegalArgumentException("n不能小于0");
}
if (n==1 || n ==2){
return n;
}
return f(n-1) + f(n-2);
}
方法二 循环迭代
public int f2(int n) {
if (n < 1) {
throw new IllegalArgumentException("n不能小于0");
}
if (n == 1 || n == 2) {
return n;
}
int one = 2; //一层台阶 一种走法,n的前两层台阶的走法
int two = 1; //两层台阶 , 两种走法 , n的前一阶台阶的走法
int sum = 0; // 总和
for (int i = 3; i <= n; i++) {
// 最后跨一步的 + 最后跨两步的
sum = two + one;
two = one ;
one = sum ;
}
return sum;
}
总结
- 递归:方法调用自身,迭代: 利用变量的原值推出新值。
- 递归
- 优点:大问题转化为小问题,可以减少代码量,同时代码精简,可读性好;
- 缺点:递归调用浪费了空间,而且递归太深容易造成堆栈的溢出。
- 迭代
- 优点:代码运行效率好,因为时间复杂度为 O(n),而且没有额为空间的开销;
- 缺点:代码不如递归简洁。
成员变量与局部变量
class LocalAndMemberVariable {
public 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) {
LocalAndMemberVariable obj1 = new LocalAndMemberVariable();
LocalAndMemberVariable obj2 = new LocalAndMemberVariable();
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);
}
}
局部变量与成员变量
区别
-
声明的位置
- 局部变量:方法体{}中,形参,代码块{}中
- 成员变量:类中,方法外
- 类变量:有static修饰
- 实例变量:没有static修饰
-
修饰符
- 局部变量:final
- 成员变量: public 、protected、private、final、static、volatile、transient
-
值存储的位置
- 局部变量 栈中
- 实例变量:堆
- 类变量:方法区
-
作用域
- 局部变量:从声明处开始,到所属的}结束
- 实例变量: 在当前类中this.(有时this.可以缺省),在其他类中(对象名.)访问
- 类变量: 在当前类中 类名.(有时类名.可以省略),在其他类中 类名. 或者对象名.访问
-
声明周期
- 局部变量:每一个线程,每一次调用执行都是新的生命周期
- 实例变量: 随着对象的创建而初始化,随着对象的回收而消亡,每一个对象的实例变量是独立的
- 类变量:随着类的初始化而初始化,随着类的回收而消亡,该类的所有对象的类变量是共享的
分析上述代码
- 当刚执行main方法时,内存结构如下
-
根据上述实例的初始化
-
首先执行非静态代码块,根据就近原则,i++中的i为该静态代码块中的i,}结束后无效,j为对应实例中的j,j++后该对象中的j+1,类变量s++变量s+1,s变量共享(初始化了两个实例,所以s最后的结果为2),当对象实例化完成后,对象的内存结构如下
{ int i = 1; // 非静态代码块中的局部变量 i i++; j++; s++; }
-
两个对象执行test方法,根据就近原则,j++执行的是方法体中的j,i为this.i,即当前实例对象的i,s为类变量,所以共享,所以当执行完test后的内存结构如下
obj1.test(10); obj1.test(20); obj2.test(30); public void test(int j) { // 形参 局部变量 j++; i++; s++; }
-
结果为
2,1,5 1,1,5
-
补充:
- 局部变量与实例变量重名
- 在成员变量前加this
- 局部变量与类变量重名
- 在类变量前加 类名.