JavaSE面试题
Java基础知识面试
此部分为Java基础面试题,包括自增变量、单例模式、类的初始化与实例化、方法的参数传递机制、递归与迭代、成员变量与局部变量。
一、自增变量
习题练习
public static void main(String[] args){
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=1;
① 创建局部变量,赋值为1
序号 | 局部变量表(i) | 操作数 |
---|---|---|
① | 1 |
i=i++;
①把i的值压入操作数栈
②i变量自增1
③把操作数栈中的值赋值给i
序号 | 局部变量表(i) | 操作数 |
---|---|---|
① | 1 | 1 |
② | 2 | 1 |
③ | 1 |
int j=i++;
①把i的值压入操作数栈
②i变量自增1
③把操作数栈中的值赋值给j
序号 | 局部变量表(i) | 局部变量表(j) | 操作数 |
---|---|---|---|
① | 1 | 1 | |
② | 2 | 1 | |
③ | 2 | 1 | 1 |
int k=i + ++i * i++;
①把i的值压入操作数栈
②i变量自增1
③把i的值压入操作数栈
④把i的值压入操作数栈
⑤i变量自增1
⑥把操作数栈中前两个弹出求乘积结果再压入栈
⑦把操作数栈中的值弹出求和再赋值给k
序号 | 局部变量表(i) | 局部变量表(j) | 局部变量表(k) | 操作数 |
---|---|---|---|---|
① | 2 | 1 | 2 | |
② | 3 | 1 | 2 | |
③ | 3 | 1 | 2,3 | |
④ | 3 | 1 | 2,3,3 | |
⑤ | 4 | 1 | 2,3,3 | |
⑥ | 4 | 1 | 2,9 | |
⑦ | 4 | 1 | 11 |
总结
- 赋值=,最后计算
- =右边的从左到右加载值,一次压入操作数栈
- 时间先算哪个,看运算符优先级
- 自增、自减操作都是直接修改变量的值,不经过操作数栈
- 最后的赋值之前,临时结果也是存储在操作数栈中。
- i++先赋值,后自增,++1先自增,后赋值
二、单例模式(Singleton)
单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。
例如:代表JVM运行环境的Runtime类。
要点:
- 某个类只能有一个实例;
构造器私有化- 必须自行创建这个实例;
含有一个该类的静态变量来保存这个唯一的实例- 必须自行向整个系统提供这个实例;
对外提供获取该实例对象的方式:
(1)直接暴露(2)用静态变量的get方法获取
常见形式:饿汉式、懒汉式
饿汉式:直接创建对象,不存在线程安全问题
- 直接实例化饿汉式(简洁直观)
- 枚举式(最简洁)
- 静态代码块饿汉式(适合复杂实例化)
/**
* 直接实例化饿汉式
* 直接创建实例对象,不管是否需要这个对象
*/
public class Singleton1{
//构造器私有化
private Singleton1(){
}
//自行创建,用静态变量保存
//向外提供这个实例
//强调这是一个单例,可以用final修饰
public static final Singleton1 INSTANCE = new Singleton1();
}
/**
* 枚举类型,表示该类型的对象是有限的几个
* 我们可以限定为一个,就成了单例
*/
public enum Singleton2(){
INSTANCE
}
/**
* 静态代码块饿汉式
* 适用于对象实例化复杂的情况
*/
public class Singleton3(){
//构造器私有化
private Singleton3(){}
//创建静态变量
public static final Singleton3 INSTANCE;
//代码块实例化对象
static{
INSTANCE = new Singleton3();
}
}
懒汉式:延迟创建对象
- 线程不安全(适用于单线程)
- 线程安全(适用于多线程)
- 静态内部类形式(适用于多线程)
/**
* 懒汉式:
* 延迟创建这个实例对象
* 存在线程安全问题
*/
public class Singleton4(){
//构造器私有化
private Singleton4(){
}
//用一个静态变量保存这个唯一的实例
private static Singleton4 instance;
//提供一个静态方法,获取这个实例对象
public static Singleton4 getInstance(){
if(instance == null){
instance = new Singleton4();
}
return instance;
}
}
/**
* 懒汉式:
* 延迟创建这个实例对象
* 通过同步锁控制,不存在线程安全问题
*/
public class Singleton5(){
//构造器私有化
private Singleton5(){
}
//用一个静态变量保存这个唯一的实例
private static Singleton5 instance;
//提供一个静态方法,获取这个实例对象
public static Singleton5 getInstance(){
//提高效率,先判断是否初始化
if(instance == null){
//加同步锁,保证线程安全
synchronized(Singleton5.class){
if(instance == null){
/*
代码段
*/
instance = new Singleton5();
}
return instance;
}
}
}
}
/**
* 在内部类被加载和初始化时,才创建INSTANCE实例对象
* 静态内部类不会自动随着外部类加载初始化而初始化,它是要单独加载和初始化的。
* 因为是在内部类加载和初始化时创建的,因此线程安全
*/
public class Singleton6(){
//构造器私有化
private Singleton6(){
}
//静态内部类
private static class Inner{
private static final Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance(){
return Inner.INSTANCE;
}
}
三、 类的初始化和实例化
习题练习
父类代码
/**
* 父类初始化\<clinit>:
* (1)静态类变量显示赋值代码:j = method();
* (2)父类静态代码块
*
* 父类的实例化方法\<init>:
* (1)super()(最前)
* (2)非静态变量:i = test();
* (3)父类非静态代码块
* (4)父类的无参构造(最后)
*
* 非静态方法前面有一个默认对象this
* this在构造器(或<init>)它表示的是正在创建的对象,因为这里是在创建Son对象,所以test()执行的是子类重写的代码(面向对象多态)
* 这里i=test()执行的是子类重写
*/
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)");
}
pricate int test(){
System.out.print("(4)");
}
private static int method(){
System.out.print("(5)");
return 1;
}
}
子类代码
/**
* 子类的初始化\<clinit>:
* (1)静态类变量显示赋值代码:j = method();
* (2)子类的静态代码块
*
* 先初始化父类:(5)(1)
* 初始化子类:(10)(6)
*
* 子类的实例化方法\<init>:
* (1)super()(最前) (9)(3)(2)
* (2)非静态变量:i = test(); (9)
* (3)子类非静态代码块 (8)
* (4)子类的无参构造(最后) (7)
*
* 因为创建了两个Son对象,因此实例化方法执行两次
* (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)");
}
pricate int test(){
System.out.print("(9)");
}
private 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)
总结
类初始化过程
- 一个类要创建实例,需要先加载并初始化该类
main方法所在的类,需要先加载和初始化
- 一个子类要初始化,需要先初始化父类
- 一个类初始化,就是执行 <clinit>() 方法(类初始化方法)
- <clinit>() 方法由静态类变量显示赋值代码和静态代码块组成
- 类变量显示赋值代码和静态代码块代码从上到下顺序执行
- <client>() 方法只执行一次
实例初始化过程
- 实例初始化就是执行 <init>() 方法
- <init>() 方法可能重载有多个,有几个构造器就有几个 <init>() 方法
- <init>() 方法由非静态实例变量显示赋值代码和非静态代码块、对应构造器代码组成
- 非静态实例变量显示赋值代码和非静态代码块代码从上到下顺序执行,而对应构造器代码最后执行
- 每次创建实例对象,调用对应构造器,执行的就是对应的**<init>()** 方法
- <init>() 方法的首行是super()或者super(实参列表),即对应父类的 <init>() 方法
方法重写
- 哪些方法不可以被重写
- final方法
- 静态方法
- private等子类中不可见方法
- 对象的多态性
- 子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过的代码
- 非静态方法默认的调用对象是this
- this对象在构造器或者说 <init> 方法中就是正在创建的对象
四、方法的参数传递机制
习题练习
public class Exam4{
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 m){
j += 1;
s += "word";
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]
my.a = 11
过程分析
总结
- 形参是基本数据类型
传递数据值
- 实参是引用数据类型
传递地址值
特殊的类型:String、包装类等对象不可变性
五、递归与迭代
习题练习
编程题:有n步台阶,一次只能上1步或2步,共有多少种走法
- 递归
- 循环迭代
递归解法
过程分析
台阶 | 过程 | 结果 |
---|---|---|
1 | 一步 | f(1) = 1 |
2 | (1)一步一步(2)直接2步 | f(2) = 2 |
3 | 先到达f(1),然后从f(1)直接跨2步;先到达f(2),然后从f(2)跨1步 | f(3) = f(1)+f(2) |
4 | 先到达f(2),然后从f(2)直接跨2步;先到达f(3),然后从f(3)跨1步 | f(4) = f(2)+f(3) |
… | ||
x | 先到达f(x-2),然后从f(x-2)直接跨2步;先到达f(x-1),然后从f(x-1)跨一步 | f(x) = f(x-2)+f(x-1) |
public class TestStep{
public int f(int n){
if(n<1){
return 0;
}
if(n == 1 || n==2){
return n;
}
return f(n-2) + f(n-1);
}
}
循环迭代
public class TestStep2{
public int loop(int n){
if(n<1){
return 0;
}
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;
}
}
总结
- 方法调用自身称为递归,利用变量的原值推出新值成为迭代。
- 递归
- 优点:大问题转化为小问题,可以减少代码量,同时代码精简,可读性好;
缺点:递归调用浪费了空间,而且递归太深容易造成堆栈的溢出。
- 迭代
- 优点:代码运行效率好,因为时间只因循环次数增加而增加,而且没有额外的空间开销;
缺点:代码不如递归简洁,可读性好