JavaSE面试题记录

2021/6/11 20:24:25

本文主要是介绍JavaSE面试题记录,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Java SE面试题记录

(author/jadexu)

目录
  • Java SE面试题记录
    • 单例模式
    • 自增变量
    • 面向对象的三大特征?
    • 类加载初始化和实例初始化题
    • 哪些方法不能被重写?
    • 为什么静态方法不能被重写?
    • Override和Overload的区别?
    • Overload重写有什么要求?
    • JVM 的 () 和 ()的区别?
    • 类的初始化时机有?
    • 实例化类的途径有?
    • 方法的参数传递机制

单例模式

单例,即单个实例,即某个类在整个系统中只能有一个实例对象可被获取和使用。

例如:代表JVM运行环境的Runtime类

所谓单例,那就得:

1、某个类只能有一个实例,即不能被外界new出来。所以构造器需私有

2、这个类必须自行创建这单个实例

3、这个类需要自行向外界提供这个实例,所以需要有个静态变量或静态方法去调用

几种常见实现形式:

总结有两大种:
	饿汉式:直接创建对象,不存在线程安全问题
	-	直接实例化(简洁直观)
	-	枚举式(最简洁,JDK1.5才开始有枚举)
	-	静态代码块(适合初始化时需要传参的类)
	
	懒汉式:只有调用对象才会创建对象,即延迟创建对象
	-	线程不安全(适合单线程)
	-	线程安全(适合多线程)
	-	静态内部类(适合多线程)

1、饿汉式

  1. 直接实例化
/**
 * @author jadexu
 * 饿汉式1:直接实例化
 * 1、构造器私有化
 * 2、类加载时就创建
 * 3、向外提供这个实例
 * 4、用final修饰体现单例不能被修改
 *
 * 适合简单直接,初始化时不需要有参构造的场景
 */
public class Singleton1 {
    /**
     * 公共的,静态,final
     */
    public final static Singleton1 INSTANCE = new Singleton1();

    /**
     * 构造函数私有化
     */
    private Singleton1(){

    }
}

  1. 枚举式
/**
 * @author jadexu
 * 饿汉式2:枚举
 * JDK1.5开始出现枚举类型,枚举表示某类型的有限个对象
 * 枚举默认不能实例化,变量是final修饰的
 * 所以又不能构造,又不能修改
 * 那么限定为一个,就相当于单例了
 */
public enum Singleton2 {
    INSTANCE
}

测试

    @Test
    public void singleton2Test(){
        /**
         * 报错,枚举类型不能被实例化
         */
//        Singleton2 singleton2 = new Singleton2();

        /**
         * 报错,枚举类型是final的,不能被修改
         */
//        Singleton2.INSTANCE = null;

        /**
         * 只能直接调用唯一实例
         */
        Singleton2 singleton2 = Singleton2.INSTANCE;
    }
  1. 静态内部块
/**
 * @author jadexu
 * 饿汉式3:静态代码块
 * 和直接实例化类似
 * 就是会在静态代码块里创建实例
 *
 * 适合初始化时需要有参构造的场景
 */
public class Singleton3 {
    public final static Singleton3 INSTANCE;

    static {
        //假装这是初始化时需要有参构造
        String info = "test";
        INSTANCE = new Singleton3(info);
    }

    private Singleton3(String info){

    }
}

2、懒汉式

  1. 线程不安全
/**
 * @author jadexu
 * 懒汉式1:线程不安全
 * 1、构造器私有化
 * 2、用一个私有的静态变量保存唯一实例
 * 3、提供一个静态方法,获得这个实例
 *
 * 适合单线程
 */
public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4(){

    }

    public static Singleton4 getInstance() throws InterruptedException {
        //判断静态变量是否为空,如果为null就创建一个,不为null就直接返回
        if (instance == null){

            instance = new Singleton4();
        }
        return instance;
    }
}

测试

    @Test
    public void singleton4Test() throws ExecutionException, InterruptedException {
        Callable<Singleton4> callable = new Callable<Singleton4>() {
            @Override
            public Singleton4 call() throws Exception {
                return Singleton4.getInstance();
            }
        };

        ExecutorService service = Executors.newFixedThreadPool(2);
        Future<Singleton4> f1 = service.submit(callable);
        Future<Singleton4> f2 = service.submit(callable);

        Singleton4 s1 = f1.get();
        Singleton4 s2 = f2.get();

        System.out.println(s1 == s2);

        service.shutdown();

    }
  1. 线程安全
/**
 * @author jadexu
 * 懒汉式2:线程安全(双检锁)
 * 适合多线程场景,但是效率低
 */
public class Singleton5 {
    private static volatile Singleton5 instance;

    public static Singleton5 getInstance(){

        if (instance == null){

            synchronized (Singleton5.class){
                if (instance == null){
                    instance = new Singleton5();
                }
            }

        }

        return instance;
    }

}

测试

	@Test
    public void singleton5Test() throws ExecutionException, InterruptedException {
        Callable<Singleton5> callable = new Callable<Singleton5>() {
            @Override
            public Singleton5 call() throws Exception {
                return Singleton5.getInstance();
            }
        };

        ExecutorService service = Executors.newFixedThreadPool(2);
        Future<Singleton5> f1 = service.submit(callable);
        Future<Singleton5> f2 = service.submit(callable);

        Singleton5 s1 = f1.get();
        Singleton5 s2 = f2.get();

        System.out.println(s1 == s2);

        service.shutdown();
    }
  1. 静态内部类
/**
 * @author jadexu
 * 懒汉式3:静态内部类
 * 1、构造器私有
 * 2、私有静态内部类
 * 3、提供一个公共的静态方法
 *
 * 静态内部类不会随着外部类的加载而加载
 * 当调用公共静态方法getInstance时,才会加载Inner静态内部类
 * 然后创建实例
 * 因为是在内部类加载和初始化时创建的,因此是线程安全的
 *
 * 适合于多线程的场景
 */
public class Singleton6 {

    private Singleton6(){

    }

    private static class Inner{
        private static final Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance(){
        return Inner.INSTANCE;
    }
}

自增变量

题目:

/**
 * @author jadexu
 */
public class Main {
    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);
    }
}

输出结果:

image

解析:

分析:
i = i++:先把i的值压入栈中,i 自身再进行自增得 i = 2,再把操作数栈中的值赋给 i ,最终 i = 1
int j = i++:先把i的值压入栈中,i自身再进行自增得2,再把操作数栈的值赋给 j,最终 j = 1,此时i为2
int k = i + ++i * i++:
从左到右存入操作数,且按运算符的优先级进行计算:
首先把i的值存入操作数栈,此时栈中有操作数2
++i:i先自增得3,再存入栈中,此时栈中有操作数2、3
i++:先把i的值压入栈中,i再自增得4,此时栈中有操作数2、3、3
计算 ++i * i++:即将操作数栈中 3*3=9,此时栈中有操作数2、9
计算i + ++i * i++:即 2+9 = 11,此时栈中有操作数11
最后把栈中结果赋给k,最终 k = 11,此时 i=4,j=1
故结果为:i=4,j=1,k=11

知识:

image

面向对象的三大特征?

面向对象的三大特征是封装、继承、多态

-	封装就是把类的一些你不想让别人看到的变量或方法通过更改它的访问权限,即把它们隐藏起来,然后只给外界提供公共的数据访问接口。这主要是为了防止恶意或无意的行为对类造成影响。
-	继承就是子类继承父类的所有对其可见的变量和方法,就比如我们大中国的一个传统,父母的财产后面会被自己的孩子给继承。
-	多态就是对于继承同一父类,不同子类对象表现出不同的行为。实现多态需要先继承父类,再重写父类的方法。比如不同类型的动物具有不同的行为方式,有的是天上飞,有的是水里游,父类就定义一个行动方式的通用方法,子类则通过重写这个方法去展示自己特有的行为方式。

类加载初始化和实例初始化题

题目:

/**
 * @author jadexu
 * 父类
 */
public class Father {
    /**
     * 局部变量
     */
    private int i = test();

    /**
     * 成员变量
     */
    private static int j = method();

    /**
     * 静态代码块
     */
    static {
        System.out.println("(1)");
    }

    /**
     * 构造方法
     */
    Father(){
        System.out.println("(2)");
    }

    /**
     * 匿名代码块
     */
    {
        System.out.println("(3)");
    }

    /**
     * 局部方法
     * @return
     */
    public int test(){
        System.out.println("(4)");
        return 1;
    }

    /**
     * 静态方法
     * @return
     */
    public static int method(){
        System.out.println("(5)");
        return 1;
    }
}
/**
 * @author jadexu
 * 子类
 */
public class Son extends Father {
    /**
     * 局部变量
     */
    private int i = test();
    /**
     * 成员变量
     */
    private static int j = method();

    /**
     * 静态代码块
     */
    static {
        System.out.println("(6)");
    }

    /**
     * 构造方法
     */
    Son(){
        System.out.println("(7)");
    }

    /**
     * 匿名代码块
     */
    {
        System.out.println("(8)");
    }

    /**
     * 重写父类的方法
     * @return
     */
    @Override
    public int test(){
        System.out.println("(9)");
        return 1;
    }

    /**
     * 静态方法
     * @return
     */
    public static int method(){
        System.out.println("(10)");
        return 1;
    }
}
/**
 * @author jadexu
 * 测试
 */
public class Test {

    public static void main(String[] args) {
        Son s1 = new Son();
        System.out.println();
        Son s2 = new Son();
    }
}

输出结果:

image

解析:

解题过程:

由于首次在虚拟机启动含有main方法的类,所以先加载Test类,但是Test类里没有什么静态变量和静态代码块,所以不进行初始化。
而后执行main方法,main方法是类的程序入口。

Son s1 = new Son();

第一步:先加载并初始化类,先初始化父类,再子类,初始化时调用clinit方法,包含静态变量显式赋值代码和静态代码块,
从上到下执行二者,即
1、父类的静态变量 -- 输出5
2、父类的静态代码块 -- 输出1
3、子类的静态变量 -- 输出10
4、子类的静态代码块 -- 输出6

第二步:实例初始化,每次创建对象,调用对应构造器,就是执行对应的init方法。
这是jvm生成的方法,由非静态实例变量显式赋值代码、非静态代码块和构造器组成,前两者按从上到下顺序执行,构造器最后执行。
在init方法首行是super()或带参的super(),即父类的构造器,写不写都会去调用。
所以在实例化子类时,要先调用父类构造器。
而又要注意到,非静态方法前面其实有一个默认的this对象,这个对象指的是当前正在创建的对象,
所以调用的是当前正在创建的对象的方法,即

1、父类的非静态变量:因为当前正在创建的对象是Son,所以调用子类的test() -- 输出 9
2、父类的非静态代码块 -- 输出 3
3、父类的构造器 -- 输出2
4、子类的非静态变量:因为当前正在创建的对象是Son,所以调用子类的test() -- 输出9
5、子类的非静态代码块 -- 输出 8
6、子类的构造器 -- 7

---------------------------------------------------------------------

Son s2 = new Son();

创建第二个Son实例,因为只有首次创建类的实例时才需要类加载并初始化,所以这里直接进行实例初始化过程,即

1、父类的非静态变量:因为当前正在创建的对象是Son,所以调用子类的test() -- 输出 9
2、父类的非静态代码块 -- 输出 3
3、父类的构造器 -- 输出2
4、子类的非静态变量:因为当前正在创建的对象是Son,所以调用子类的test() -- 输出9
5、子类的非静态代码块 -- 输出 8
6、子类的构造器 -- 7

知识:

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

实例初始化过程
实例初始化就是执行<init>()方法
	init方法可能重载有多个,有几个构造器就有几个init方法
	init方法由非静态实例变量显式赋值代码、非静态代码块和对应构造器代码组成
	非静态实例变量显式赋值代码和非静态代码块从上到下顺序执行,而构造器的代码最后执行
	每次创建对象,调用对应构造器,执行的方法就是对应的init方法
	init的方法的首行是super()或super(实参列表),即对应父类的init方法.
	(子类构造器里一定会有super(),即父类的空构造器,写或不写都会先调用父类的构造器)

哪些方法不能被重写?

-	final方法
-	静态方法
-	private等对子类不可见方法

为什么静态方法不能被重写?

重写的目的在于对于继承相同的父类,不同的子类对象表现出不同的行为。

所谓静态,就是在运行时,虚拟机已经认定这个方法属于哪个类了。
对于实例方法可以重写,而对于静态方法只能说是隐藏。
就算子类有和父类一样的静态方法,这也是两个毫不相干的方法,只是名字什么的一样而已。
(重写的方法上要加上@Override注解,而静态方法上如果加@Override注解编译器会报错,提示这里不能加这个注解)

静态方法是属于类的,不需要创建对象就能调用,这就不符合“不同的子类对象表现出不同的行为“这一目的,不符合重写的概念。

Override和Overload的区别?

-	Override是重写,Overload是重载
-	重写指的是,对于继承同一父类的不同子类,它们的对象通过重写父类的方法而表现出不同的行为。简单打个比方,鸟类和鱼类都继承动物类,动物类都有同一个行为那就是行动方式,而不同类型的动物又会表现出不同的行动方式,比如鸟类对象就是飞行,鱼类对象就是在水里游
(可能比喻的不精准,但差不多就是这意思)
-	重载指的是,在同一个类里,方法名相同而参数列表不同的方法。常见的就是构造方法的重载,但是构造函数相对特殊些,它默认是public的,也可以用其它访问修饰符修饰,它没有返回值,但不能用void

Overload重写有什么要求?

-	方法名相同
-	参数列表不同

JVM 的 <clinit>() 和 <init>()的区别?

-	执行时机不同
	clinit方法是类构造器方法,在jvm进行类加载-验证-解析-初始化中的初始化阶段会调用
	init方法是创建对象实例时执行
-	执行目的不同
	clinit方法是class类构造器,对静态变量和静态代码块进行初始化
	init方法是instance实例构造器,对非静态实例变量解析初始化
-	clinit方法由静态类变量显式赋值代码和静态代码块组成,二者是从上往下进行执行
	init方法由非静态实例变量显式赋值代码、非静态代码块以及构造方法组成,前二者是从上往下执行,构造方法最后执行

clinit是由Javac编译器自动生成和命名的,它是一个不含参数的静态方法。不能通过程序直接编码的方式实现,只能由编译器根据静态类变量的显式赋值语句或静态代码块自动插入到Class文件中。此外,clinit方法没有任何虚拟机字节码指令可以调用,它只能在类初始化阶段被虚拟机隐式调用。

注意:
1、并非所有类都会有一个clinit方法
	-	该类既没有声明任何静态类变量,也没有静态初始化语句
	-	该类声明了静态类变量,但是没有明确使用类变量初始化语句或静态初始化语句进行初始化
	-	该类仅包含静态final变量的初始化语句,并且类变量初始化语句是编译时常量表达式
2、在静态代码块中使用静态变量时,要将该静态变量的声明语句方法静态代码块的前面,否则会发送illegal forward reference的编译错误

类的初始化时机有?

-	首次创建某个类的新实例时,即new,反射,克隆或反序列化
-	首次在虚拟机启动某个含有main()方法的类时
-	首次调用某个类的静态方法时
-	首次使用某个类或接口的静态字段或对该字段赋值时
-	首次调用Java的某些反射方法时
-	首次初始化某个类的子类时

实例化类的途径有?

-	调用new操作符
-	调用Class或 java.lang.reflect.Constructor对象的 newInstance()方法
-	调用任何现有对象的clone()方法
-	通过 java.io.ObjectInputStream类的 getObject()方法反序列化

方法的参数传递机制

题目:

import java.util.Arrays;

/**
 * @author jadexu
 * 方法的传参机制
 */
public class Main {

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

输出结果:
image

解析:

1、局部变量i,str,num,arr,myData放在栈里
-	i是基本数据类型,存的是值 -- 1
-	str是不可变的引用数据类型,存的是地址值,字符串hello在常量池,str的地址值指向常量池的hello
-	num是不可变的引用数据类型,存的是地址值,但是值是放在堆里的,地址值指向堆的值
-	arr是引用数据类型,存的是地址值,值放在堆里,地址值指向
-	myData是引用数据类型,存的是地址值,值放在堆里,地址值指向

2、change方法的局部变量,j,s,n,a,m
-	j是基本数据类型,存的是实参i的值 -- 1(只是单纯存这个值)
-	s是不可变的引用数据类型,存的是实参str的地址值,指向常量池里的“hello”
-	n是不可变的引用数据类型,存的是实参num的地址值,指向堆里的num对象
-	a是引用数据类型,存的是实参arr的地址值,指向堆里的arr数组对象
-	m是引用数据类型,存的是实参myData的地址值,指向堆里的myData对象

change()方法里的是形参列表,调用的change()方法里的是实参列表

分析图:

image

知识:

方法的参数传递机制

形参给实参传值:	
-	基本数据类型,传递数据值
-	引用类型,传递地址值(对于特殊类型:String、包装类等对象具有不可变性)
			
方法放在栈,局部变量放在对应方法里,字符串放常量池,引用对象放堆

对于引用数据类型,栈里放地址值,堆里放对象

对于不可变的引用数据类型,每次操作都会生成一个新的对象


这篇关于JavaSE面试题记录的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程