面试总结一:Java 基础


一、基本概念

1.1 JVM / JDK / JRE

JVM 是 Java 虚拟机,JDK 是 Java 开发工具,JRE 是 Java 运行时环境。

1.2 什么是字节码

狭义:在 Java 中可以被 JVM 理解的代码就叫做字节码,即被编译后生成的 .class 文件。

1.3 为什么说 Java 是 “编译与解释并存”

Java 语言既具有编译语言的特性,也具有解释语言的特性。Java 程序需要先经过编译,生成 .class 文件,然后由 Java 解释器来解释执行。

1.4 静态方法为什么不能调用非静态成员

静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。非静态成员是属于实例对象的,只有在对象实例化之后才会存在,需要类的实例对象才可以访问。在类的非静态成员不存在时,静态成员就已经存在了,所以不能调用。

1.5 == 和 equals() 区别
  • == 对于基本数据类型来说比较的是值,对于引用数据类型来说比较的是对象的地址值。
  • equals() 对于没有覆盖 equals() 方法时等价于 ==,对于已经覆盖了 equals() 方法时比较的是对象的属性是否相等。
1.6 包装类使用
  • 装箱:基本数据类型转换为对应的包装类型;
  • 拆箱:包装类型转换为对应的基本数据类型;
    装箱和拆箱都是自动进行的,如果频繁的进行装箱拆箱操作,也会严重影响系统性能(自测相差10~12倍左右)。
1.7 成员变量和局部变量

成员变量可以不用初始化,局部变量必须进行初始化。

  • 成员变量属于实例对象的一部分,而实例对象都是在堆内存中分配的,在 JVM 分配存储空间的时候,就会把对象的成员变量初始化为零值。
  • 局部变量是位于方法中,每一个方法都是存放在栈帧中,调用方法前必须确定方法栈帧需要分配内存空间,所以局部变量在创建时就必须进行初始化以确定分配内存大小。
1.8 值传递与引用传递
  • 值传递指的是在方法调用时,传递的参数是实参值的拷贝。
  • 引用传递指的是在方法调用时,传递的参数是实参的引用,也可以理解为实参的内存空间地址。
  • 从本质上来说,在Java语言中的引用传递实际上都是值传递,传递的是地址的值。
1.9 main() 方法为什么必须是静态的

Java 平台调用 main() 方法时不会创建这个类的实例,因此这个方法必须声明为 static,如果没有声明为static 时,程序能正常编译但运行时会抛 NoSuchMethodError 异常。

1.10 泛型的本质

Java 在 JDK1.5 中引入泛型这一新特性,泛型的本质是参数化类型,即可以把数据类型指定为一个参数,这个参数类型可以用在类、接口和方法的创建中。在 Java 中泛型只存在于编译阶段,而不存在于运行阶段,也就是通常所说的类型擦除。

1.11 Error 和 Exception 区别是什么

Error 和 Exception 都是 Throwable 的子类,在Java中只有Throwable类型的实例才可以被抛出或者捕获,它是异常处理机制的基本类型。

  • Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应的处理。
  • Error是指正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序处于不可用状态。
  • Exception又分为可检查(checked)异常和不可检查(unchecked)异常。
    在这里插入图片描述

二、String 对象

2.1 什么叫不可变类
  • 是指当一个对象被创建出来以后,它的值就不能被修改了,也就是说当对象被创建出来后,在其整个生命周期中,它的成员变量就不能被修改了。
  • 在 Java 类库中,所有基本类型的包装类和 String 都是不可变类。不可变类具有使用简单、线程安全、节省内存等优点。
2.2 String 的特性

String 是标准的不可变类,对它的任何改动,其实就是创建了一个新对象,再把引用指向该对象。String 对象赋值之后就会在常量池中缓存,如果下次创建会判定常量池是否已经有缓存对象,如果有则直接返回该引用给创建者。

2.3 new String(“test”) 创建了几个对象

创建了一个或两个对象,如果在字符串常量池中已经存在"test",则会在堆中创建对象并指向常量池中的"test",如果常量池中不存在"test",则会在常量池中会创建一个”test“,并且在堆中创建一个对象指向常量池的对象。

2.4 String 拼接字符串效率低原因

String 拼接字符串效率低是指在循环中调用字符串拼接,这是因为字符串在拼接时底层会创建一个StringBuilder 对象,然后调用 append 方法进行拼接,如果在循环中调用字符串拼接,则每次循环都会创建一个新的对象,造成效率低下。

2.5 String、StringBuffer、StringBuilder 的区别
  • 线程安全方面:String 和 StringBuilder 都是线程不安全的,StringBuffer 是线程安全的,因为 StringBuffer 被 synchronized 修饰。
  • 执行效率方面:StringBuilder > StringBuffer > String,因为 StringBuffer 是被 synchronized 进行修饰的,创建的 String 对象都是不可变的。
  • 应用场景方面:如果要操作少量的数据用 String,单线程操作字符串缓冲区 StringBuilder,多线程操作字符串缓冲区 StringBuffer。
2.6 String 的 intern() 方法

使用引号声明的字符串都会在字符串常量池中生成,而 new 出来的 String 对象是放在堆区域。intern() 方法用于查找常量池中是否存在该字符串,如果已经存在则直接返回当前字符串,如果常量池中不存在则先在常量池中创建对应的字符串,然后返回此字符串对应的引用。

String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str4 = str1 + str2;
String str5 = new String("ab");
System.out.println(str5.equals(str3));//true
System.out.println(str5 == str3);//false
System.out.println(str5.intern() == str3);//true
System.out.println(str5.intern() == str4);//false
2.7 String 的 intern() 方法底层实现

Java 使用 JNI 调用 C++ 实现的 StringTable 的 intern 方法,intern 方法跟 Java 中的 HashMap 的实现类似,只是不能自动扩容,默认大小是1009。

2.8 字符串常量池

字符串常量池是存储在 Java 堆内存中的字符串池,是为防止每次新建字符串时的时间和空间消耗的一种解决方案。在创建字符串时 JVM 会首先检查字符串常量池,如果字符串已经存在池中,就返回池中的实例引用,如果字符串不在池中,就会实例化一个字符串放到池中并把当前引用指向该字符串。


三、反射基础

3.1 反射机制

反射是指能够获取到处于运行状态的类或对象的所有属性和方法,它主要实现了以下功能:

  • 获取类的访问能修饰符、方法、属性以及父类信息。
  • 在运行时根据类的名字创建对象。在运行时调用任意一个对象的方法。
  • 在运行时判断一个对象属于哪个类。
  • 生成动态代理。
3.2 获取Class对象
  • 通过 className.class 来获取,不执行静态块和动态构造块。
  • 通过 Class.forName() 来获取,只执行静态块、而不执行动态构造块。
  • 通过 Object.getClass() 来获取,因为需要创建对象,所以会执行静态块和动态构造块。
3.3 动态代理

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的(动态代理的实现依赖于反射)。

3.4 基于 JDK 动态代理

在 Java 动态代理机制中 InvocationHandler 接口和 Proxy 类是核心,通过 Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现 InvocationHandler 接口的类的 invoke()方法,可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(), // 目标类的类加载
            target.getClass().getInterfaces(),  // 代理需要实现的接口,可指定多个
            (proxy, method, args) -> method.invoke(target, args) // 代理对象对应的自定义 InvocationHandler
        );
    }
}
3.5 基于 CGLIB 动态代理
public class CglibProxyFactory {
    public static Object getProxy(Class<?> clazz) {
        // 创建动态代理增强类
        Enhancer enhancer = new Enhancer();
        // 设置类加载器
        enhancer.setClassLoader(clazz.getClassLoader());
        // 设置被代理类
        enhancer.setSuperclass(clazz);
        // 设置方法拦截器
        enhancer.setCallback(MethodInterceptor.intercept());
        // 创建代理类
        return enhancer.create();
    }
}

四、IO 模型


五、未归类问题

5.1 switch 语句问题

JDk1.5 以前 switch() 语句只支持 byte、short、char、int类型(或其包装类)的常量表达式,从 JDk1.5 开始支持枚举类型,从 JDk1.7 开始支持 String 类型。
JVM 并没有增加新的指令来处理 String 类型,而是通过调用 switch (string.hashCode) 的方式,将string转换为int从而进行判断。

5.2 try-catch 为什么比较耗费性能

JVM 在构造异常实例的时候需要生成该异常的栈轨迹,这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息,这就会导致使用异常捕获会比较耗费性能。

5.3 finally 为什么总能被执行

因为编译器在编译 Java 代码时,会复制 finally 代码块的内容,然后分别放在 try-catch 代码块所有的正常执行路径及异常执行路径的出口中,这样 finally 才会不管发生什么情况都会执行。

5.4 finalize() 方法的作用

finalize() 是在 Object 类中定义的一个方法,所有的类都继承了它,子类可以覆盖 finalize() 方法来整理系统资源或者执行其他清理工作,Java 允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

5.5 finalize()方法的调用时机

fnalize() 方法是由垃圾收集器在确定这个对象没有被引用时自动调用的,但 JVM 不保证此方法总被调用,当对象不可达时,会判断该对象是否覆盖了 finalize 方法,若未覆盖则直接将其回收,若已覆盖且未执行过该方法时,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的 finalize 方法,执行finalize方法完毕后,GC会再次判断该对象是否可达,若对象不可达进行回收,若对象可达则对象"复活"。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值