一、 JavaSE面试题
1.1 自增变量
如下程序应该输出什么:
public static void main(){
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的值为1,先把1压入操作数栈,然后i自增变为2;然后执行赋值语句,将操作数栈中的值1赋值给i,此时i的值为1,操作数栈为空。
第四行时先执行赋值运算符右边的语句i++,i的值为1,先把1压入操作数栈,然后i自增变为2;然后执行赋值语句,将操作数栈中的值1赋值给j,此时j的值为1,操作数栈为空。
第五行时先执行赋值运算符右边的语句i+++i*i++,从左往右入栈,i的值为2,先把2压入操作数栈;然后执行乘法运算++i*i++,首先执行++i,i自增变为3,把3压入操作数栈;再执行i++,先把3压入操作数栈,然后i自增变为4;然后执行乘法,取栈顶两个数3和3相乘得9,再执行加法运算9+2=11,最后执行赋值运算,将11赋值给k。
所以最后的结果为i=4,j=1,k=11;
小结:
- 赋值最后运算;
- 赋值右边的语句从左至右加载值以此压入操作数栈;
- 实际先算哪个,看运算符优先级;
- 自增、自减操作都是直接修改变量的值,不经过操作数栈;
- 最后赋值之前,临时结果都是存储在操作数栈中。
- 建议相关阅读《JVM虚拟机规范》关于指令部分。
1.2 单例设计模式
编程题:写一个Singleton实例。
什么是Singleton?
- 在java中即指单例设计模式,它是软件开发中最常用的模式之一。
- 单例即唯一实例。
- 单例设计模式即某个类在整个系统中只能有一个实例对象可被获取或使用的代码模式。
- 例如:代表JVM运行环境的Runtime类。
Singleton的要点?
- 一是某个类只能有一个实例。
- 构造器私有化。
- 二是他必须自行创建这个实例。
- 用含有一个该类的静态变量来保存这个实例。
- 三是它必须自行向整个系统提供这个实例。
- 直接暴露。
- 用静态变量的get方法获取。
Singleton的几种常见模式?
- 饿汉式:直接创建对象,不存在线程安全问题。
- 直接实例化饿汉式(简洁直观)。
- 枚举式(最简洁)。
- 静态代码块饿汉式(适合复杂实例化)。
- 懒汉式:延迟创建对象。
- 线程不安全式(适用于单线程)。
- 线程安全式(适用于多线程)。
- 静态内部类形式(适用于多线程)。
饿汉式创建法一:
/**
* 在类初始化时直接创建,不管是否需要该实例。
*/
public class Singleton01 {
//用final强调这是单例模式
public static final Singleton01 INSTANCE=new Singleton01();
private Singleton01(){
}
}
饿汉式创建法二:
/**
* 枚举类型:表示该类型的对象是有限的几个。
* 限定一个,就成了单例。
*/
public enum Singleton01{
INSTANCE
}
饿汉式创建法三:
/**
* 一般用来初始化时传递参数时用
* 比如在静态代码块中从文件中读取属性。
*/
public class Singleton03{
private static final Singleton03 INSTANCE;
static{
INSTANCE=new Singleton03();
}
private Singleton03(){
}
}
懒汉式创建法一:
public class Singleton04{
private static Singleton04 instance;
private Singleton04(){
}
//调用该方法时再创建实例(线程不安全)
public Singleton04 getInstance(){
if(instance==null){
instance=new Singleton04();
}
return instance;
}
}
懒汉式创建法二:
public class Singleton05{
private static Singleton05 instance;
private Singleton05(){
}
public Singleton05 getInstance(){
if(instance==null){
//加同步锁,线程安全
synchronized(Singleton05.class){
instance=new Singleton05();
}
}
return instance;
}
}
懒汉式创建法三:
/**
* 静态内部类不会随者外部类的加载和初始化而初始化,
* 它需要单独进行加载和初始化。
*/
public class Singleton06{
private Singleton06(){}
private static class Inner(){
private static final Singleton06 INSTANCE=new Singleton06();
}
public static getInstance(){
return Inner.INSTANCE;
}
}
1.3 类初始化和实例初始化
以下代码运行结果是什么?
代码一:
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;
}
}
代码二:
public class Son extends Father{
private int i=test();
private static int j=method();
static {
System.out.print("(6)");
}
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();
}
}
分析类的初始化过程:
- 一个类要创建实例需要先加载并初始化该类。
- main方法所在的类需要先加载并初始化。
- 一个子类要初始化需要先初始化父类。
- 一个类初始化就是执行<clinit>()方法,该方法由编译器自动生成。
- <clinit>()方法由静态类变量显式赋值代码和静态代码块组成。
- 以上两块代码从上到下按顺序执行。
- <clinit>()方法只执行一次。
当main方法中为空时,执行程序会发生类的初始化,执行顺序如下:
System.out.println("(5)");
System.out.println("(1)");
System.out.println("(10)");
System.out.println("(6)");
此时输入结果为:(5)(1)(10)(6)
—
分析实例初始化过程:- 实例初始化就是执行<init>()方法。
- 该方法可能有多个重载方法,有几个构造器就有几个<init>()方法。
- <init>()方法由非静态实例变量显示赋值代码和非静态代码块、对应构造器代码组成。
- 前两种代码块从上到下按顺序执行,其对应构造器代码最后执行。
- 每次创建实例对象调用对应构造器时,执行的就是对应的<init>()方法。
- <init>()方法首行是super()或super(实参列表),即对应父类的<init>()方法。
当类实例化时,执行顺序如下:
super()
(不管是否写了都会调用,并一定在最前)- 非静态实例变量显示赋值代码。
- 非静态代码块。
- 构造器代码。(一定最后执行)
注:2、3步按实际顺序由上到下执行。
在以上的的代码程序中,还涉及到方法的重写(Override):
- 哪些方法不可以被重写?
- final方法。
- 静态方法。
- private等子类中不可见的方法。
- 对象的多态性是什么?
- 子类如果重写了父类的方法,通过子类调用的一定是子类重写过的方法。
- 非静态对象默认的调用对象是this。
- this对象在构造器中或者说在<init>()方法中就是正在创建的对象。
综上所述,该程序最后执行的结果为:
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
由于创建了两个实例对象,所以<init>()执行了两次。
1.4 方法的参数传递机制
以下代码的运行结果是什么:
public class exam01 {
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[] ar,MyData m){//形参列表
j+=1;
s+="world";
n+=1;
ar[0]+=1;
m.a+=1;
}
}
class MyData{
int a=10;
}
先说明方法的传参机制:
- 形参是基本数据类型?
- 传递数据值。
- 实参是引用数据类型?
- 传递地址值。
- 特殊类型String、包装类等对象具有不可变性。
局部变量存储在各自的方法栈中,例如main方法的实参列表存储在栈1中,change方法中的形参列表存储在栈2中,分析每个由实参到形参之间的传递:
- i传递值给j,j的值改变,i值照旧。
- str值存储在常量池中,str指向常量池"hello",当str传递地址值给s,s也指向常量池"hello";s产生了字符串拼接,此时常量池中产生新的常量"world"和"helloworld",s重新指向"helloworld",str照旧。
- num的值存储在堆中,num传递地址值给n,num和n都指向200;n值改变,重新指向堆中的201,num值照旧。
- arr传递地址值给ar,ar根据地址改变ar[0]的值,地址不变,则指向同一地址的arr中的arr[0]改变。
- my传递地址值给m,m根据地址改变a的值,地址不变,则指向同一地址的my中的a属性值改变。
综上所述,该程序的结果为:
i=1
str=hello
num=200
arr=[2, 2, 3, 4, 5]
my.a=11
1.5 递归和迭代
编程题:有n步台阶,一次只能上一步或两步,共有多少种走法?
首先分析规律:
- 当n为1和2时,走法有n种;
- 大于2时,设走法为f(n),则f(n)=f(n-1)+f(n-2)。
据此一般有两种做法:递归和循环迭代。
递归:
public int steps(int n){
if(n==1||n==2){
return n;
}
return steps(n-1)+steps(n-2);
}
递归是在重复某件工作,考虑将重复的步骤提取进行循环,有“现状态=前一步状态+前两步状态”,可以用one保存前一步状态,用two保存前两步状态,循环加即可。
迭代:
public int steps(int n){
if(n==1||n==2){
return n;
}
int one=2;
int two=1;
int sum=0;
for(int i=3;i<=n;i++){
sum=one+two;
two=one;//保存前两步状态
one=sum;//保存前一步状态
}
return sum;
}
小结:
- 方法调用自身叫做递归,利用变量的原值推出新值称为迭代。
- 递归:
- 优点:大问题化小问题,代码量少而精简,可读性好。
- 缺点:递归调用浪费空间,递归太深容易造成堆栈溢出。
- 迭代:
- 优点:运行效率高,时间复杂度只因循环次数增加而增加,没有额外的空间开销。
- 缺点:代码不简洁,可读性较差。
1.6 成员变量与局部变量
下面程序的运行结果是什么:
public class Exam02 {
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) {
Exam02 obj1=new Exam02();
Exam02 obj2=new Exam02();
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."访问(有时可省略),其它类中用"对象名."访问。
- 类变量:在当前类中用"类名."访问(有时可省略),其它类中用"类名."或"对象名."访问。
- 生命周期不同。
- 局部变量:对于每一个线程,每一次调用执行都是新的生命周期。
- 实例变量:随着对象的创建而初始化,随着对象的被回收而消亡,每一个实例变量都是独立的。
- 类变量:随者类的初始化而初始化,随者类的卸载而消亡,该类的所有类变量是共享的。
再分析程序的执行过程:
- 首先obj1变量被实例化,执行<init>()方法(见1.3节),非静态代码块执行。其中i为局部变量,i++是局部变量的自增(同名变量且没有加this.),在代码块结束后便消亡;j++是成员变量的自增,j值为1;s是类变量的自增,在整个类中是共享的,s值为1。
- obj2变量被实例化,执行<init>()方法,非静态代码块执行。i值同上;j++是成员变量的自增,j值为1;s是类变量的自增,在整个类中是共享的,s值为2。
- obj1调用test方法,将10传给j,j为局部变量,在方法结束后消亡;i++是成员变量的自增(隐式this.),i值为1;s是类变量的自增,在整个类中是共享的,s值为3。
- obj1调用test方法,将20传给j,j为局部变量,在方法结束后消亡;i++是成员变量的自增(隐式this.),i值为2;s是类变量的自增,在整个类中是共享的,s值为4。
- obj2调用test方法,将30传给j,j为局部变量,在方法结束后消亡;i++是成员变量的自增(隐式this.),i值为1;s是类变量的自增,在整个类中是共享的,s值为5。
综上所述,代码运行结果为:
2,1,5
1,1,5
小考点:当局部变量与xx变量重名时,如何区分?
- 局部变量与实例变量重名:
- 在实例变量前面加"this."。
- 局部变量与类变量重名:
- 在类变量前面加"类名."。