JavaSE面试题
编程题:写一个Singleton(单例模式)示例
Singleton:在Java中即指单例设计模式,它是软件开发中最常用的设计模式之一。
单:唯一
例:实例
单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。
例如:代表JVM运行环境的Runtime类
要点
一、是某个类只能有一个实例;
1.构造器私有化
二、是它必须自行创建这个实例;
2.含有一个该类的静态变量来保存这个唯一的实例
三、是它必须自行向整个系统提供这个实例;
3.对外提供获取该实例对象的方式:
(1)直接暴露(2)用静态变量的get方法获取
几种常见的形式
饿汉式:直接创建对象,不存在线程安全问题
- 直接实例化饿汉式(简洁直观)
/*
* 饿汉式:
* 在类初始化时直接创建实例对象,不管你是否需要这个对象都会创建
*
* (1)构造器私有化
* (2)自行创建,并且用静态变量保存
* (3)向外提供这个实例
* (4)强调这是一个单例,我们可以用final修改
*/
public class Singleton1 {
public static final Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
- 枚举式(最简洁)
/*
* 枚举类型:表示该类型的对象是有限的几个
* 我们可以限定为一个,就成了单例
*/
public enum Singleton2 {
INSTANCE
}
- 静态代码块饿汉式(适合复杂实例化)
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 + "]";
}
}
懒汉式:延迟创建对象
- 多线程不安全(适用于单线程)
/*
* 懒汉式:
* 延迟创建这个实例对象
*
* (1)构造器私有化
* (2)用一个静态变量保存这个唯一的实例
* (3)提供一个静态方法,获取这个实例对象
*/
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
public static Singleton4 getInstance(){
if(instance == null){
instance = new Singleton4();
}
return instance;
}
}
- 线程安全(适用于多线程)
/*
* 懒汉式:
* 延迟创建这个实例对象
*
* (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){
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;
}
}
如下代码的运行结果
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);
}
运行步骤
1.执行到 i=i++
①由于++在后面,要先把i的值压入操作数栈
局部变量i | 操作数栈 |
---|---|
1 | 1 |
②i变量自增1
局部变量i | 操作数栈 |
---|---|
2 | 1 |
③把操作数栈中的值赋值给i,所以此时i=1
局部变量i | 操作数栈 |
---|---|
1 |
2.执行到int j = i++;
①由于++在后面,要先把i的值压入操作数栈
局部变量i | 局部变量j | 操作数栈 |
---|---|---|
1 | 1 |
②i变量自增1
局部变量i | 局部变量j | 操作数栈 |
---|---|---|
2 | 1 |
③把操作数栈中的值赋值给j,所以此时i=2,j=1;
局部变量i | 局部变量j | 操作数栈 |
---|---|---|
2 | 1 |
3.执行到int k = i + ++i * i++;
①把i的值压入操作数栈
局部变量i | 局部变量k | 操作数栈 |
---|---|---|
2 | 2+ ++i * i++ |
②i变量自增1,由于++在前面,所以i要先自增再压入操作数栈,此时int k = 2+ 3 * i++;
局部变量i | 局部变量k | 操作数栈 |
---|---|---|
3 | 2+ 3 * i++ |
③把i的值压入操作数栈
局部变量i | 局部变量k | 操作数栈 |
---|---|---|
3 | 2+ 3 * 3 |
④i变量自增1
局部变量i | 局部变量k | 操作数栈 |
---|---|---|
4 | 2+ 3 * 3 |
⑥把操作数栈中前两个弹出求乘积结果再压入栈
局部变量i | 局部变量k | 操作数栈 |
---|---|---|
4 | 2+9 |
⑦把操作数栈中的值弹出求和再赋值给k
局部变量i | 局部变量k | 操作数栈 |
---|---|---|
4 | 11 |
所以结果为:i=4,j=1,k=11;
小结
1.赋值=,最后计算
2.=右边的从左到右加载值依次压入操作数栈
3.实际先算哪个,看运算符优先级
4.自增、自减操作都是直接修改变量的值,不经过操作数栈
5.最后的赋值之前,临时结果也是存储在操作数栈中
类初始化和实例初始化等
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(){
// 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 s1 = new Son();
System.out.println();
Son s2 = new Son();
}
}
1.类初始化过程
要点:
1、一个类要创建实例需要先加载并初始化该类
- main方法所在的类需要先加载和初始化
2、一个子类要初始化需要先初始化父类
3、一个类初始化就是执行clinit()方法,clinit是在字节码
- clinit()方法由静态类变量显示赋值代码和静态代码块组成
- 类变量显示赋值代码和静态代码块代码从上到下顺序执行
- clinit()方法只执行一次
结论:
1.要先初始化父类,所以先执行父类的clint()方法,所以结果为(5)(1)
2.然后初始话子类,所以执行子类的clint()方法,所以结果为(10)(6)
2.实例初始化过程
实例初始化就是执行init()方法,init()方法是字节码中的
- init()方法可能重载有多个,有几个构造器就有几个init方法
- init()方法由非静态实例变量显示赋值代码和非静态代码块、对应构造器代码组成
- 非静态实例变量显示赋值代码和非静态代码块代码从上到下顺序执行,而对应构造器的代码最后执行
- 每次创建实例对象,调用对应构造器,执行的就是对应的init方法
- init方法的首行是super()或super(实参列表),即对应父类的init方法
3.方法的重写
1.哪些方法不可以被重写
- final方法
- 静态方法
- private等子类中不可见方法
2.对象的多态性
- 子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过的代码
- 非静态方法默认的调用对象是this
- this对象在构造器或者说方法中就是正在创建的对象
结论:
子类的实例化方法init:
1.先执行super(),所以先执行父类的init方法,由于方法的重写,所以结果为(9)(3)(2)
2.然后执行子类的init方法,所以结果为(9)(8)(7)
3.由于创建了两个Son对象,因此实例化方法init方法执行了两次,所以结果为(9)(3)(2)(9)(8)(7)
此代码的执行结果
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
方法的参数传递机制
如下的运行结果为:
public class Test{
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 += "world";
n += 1;
a[0] += 1;
m.a += 1;
}
}
class MyData{
int a = 10;
}
考点
1.方法的参数传递机制
- 传递数据值
2.String、包装类等对象的不可变性
- 传递地址值
- 特殊的类型:String、包装类等对象不可变性
图解
结果
i=1 str = hello num=201 arr=[2,2,3,4,5] my.a=11
递归与迭代
方法调用自身称为递归,利用变量的原值推出新值称为迭代。
递归
- 优点:大问题转化为小问题,可以减少代码量,同时代码精简,可读性好;
- 缺点:递归调用浪费了空间,而且递归太深容易造成堆栈的溢出。
代码
public class TestStep{
@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);
}
}
迭代
- 优点:代码运行效率好,因为时间只因循环次数增加而增加,而且没有额外的空间开销;
- 缺点:代码不如递归简洁,可读性好
代码
public class TestStep2 {
@Test
public void test(){
long start = System.currentTimeMillis();
System.out.println(loop(100));//165580141
long end = System.currentTimeMillis();
System.out.println(end-start);//<1ms
}
public int loop(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;
}
}
成员变量和局部变量
public class Exam5 {
static int s;//成员变量,类变量
int i;//成员变量,实例变量
int j;//成员变量,实例变量
{
int i = 1;//非静态代码块中的局部变量 i
i++;
j++;
s++;
}
public void test(int j){//形参,局部变量,j
j++;
i++;
s++;
}
public static void main(String[] args) {//形参,局部变量,args
Exam5 obj1 = new Exam5();//局部变量,obj1
Exam5 obj2 = new Exam5();//局部变量,obj1
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);
}
}
考点
就近原则
变量的分类
- 成员变量:类变量、实例变量
- 局部变量
非静态代码块的执行:每次创建实例对象都会执行
方法的调用规则:调用一次执行一次
局部变量与成员变量的区别
1、声明的位置
(1).局部变量:方法体{}中,形参,代码块{}中
(2).成员变量:类中方法外
- 类变量:有static修饰
- 实例变量:没有static修饰
2、修饰符
局部变量:final
成员变量:public、protected、private、final、static、volatile、transient
3、值存储的位置
局部变量:栈
实例变量:堆
类变量:方法区
4、作用域
局部变量:从声明处开始,到所属的}结束
实例变量:在当前类中“this.”(有时this.可以缺省),在其他类中“对象名.”访问
类变量:在当前类中“类名.”(有时类名.可以省略),在其他类中“类名.”或“对象名.”访问
5、生命周期
局部变量:每一个线程,每一次调用执行都是新的生命周期
实例变量:随着对象的创建而初始化,随着对象的被回收而消亡,每一个对象的实例变量是独立的
类变量:随着类的初始化而初始化,随着类的卸载而消亡,该类的所有对象的类变量是共享的
当局部变量与xx变量重名时,如何区分
局部变量与实例变量重名
- 在实例变量前面加“this.”
局部变量与类变量重名
- 在类变量前面加“类名.”
结果
2,1,5
1,1,5