个人总结的一些关于安卓面试中java语言的八股文。(由于本人是菜鸡,水平有限,如有错漏欢迎留言)
1. Java访问权限(public protected default private区别)
Private只在当前类内可以访问;default在同一个包内都可以访问;protected在同个包内可访问,其他包中的子类也可以继承到protected变量进行访问;public在不同包,非子类都可以访问
2. java final关键字
final关键字的核心实现首先依赖编译期的静态检查,编译器会检查是否有多次赋值,若final变量是编译期可确定的值,编译器会将其替换为实际值;
final修饰的类、方法会在字节码层面通过访问标志(access flags)标记,这是JVM识别其不可变性的关键。JVM会在类的访问标志中设置ACC_FINAL,表示该类不能被继承。方法的访问标志会被标记为ACC_FINAL,表示该方法不能被子类重写。
3. 泛型
3.1 Java泛型、应用场景
泛型主要有三种使用形式:在类定义时声明类型参数,如ArrayList<E>;在接口定义时声明类型参数,如Comparable<T>;在方法返回值前声明类型参数,独立于类。最典型应用就是集合类,像hashmap之类,还有就是通用类型的算法实现。
3.2 Java泛型通配符
上界通配符(? extends T),?需要是T的子类,(Integer extends Number),不可以添加
下界通配符(? super T),?需要是T的父类,(food super rice),读取只能用object接收
无界通配符(?),表示完全未知的类型,通常用于方法只依赖Object类中定义的功能时
3.3 类型擦除
Java泛型是通过类型擦除实现的,编译器在编译时会擦除泛型类型信息,无界类型参数(<T>)擦除为Object,有界类型参数(<T extends Number>)擦除为边界类型(Number)。由于类型擦除,创建泛型数组不安全,替代方案使用List等容器
3.4 泛型限制
不能实例化泛型类型,基本类型不能作为类型参数(int), 静态成员不能使用类型参数
4. 比较==和equals和hashcode区别
" == "在基本数据类型比较的是值内容, 引用类型时比较地址
equals只在对象中有,equals不重写时比较地址,重写后比较值内容 。
在所有没有重写equals()方法的类中,调用equals()方法其实和使用==的效果一样,也是比较的地址值,然而,Java提供的所有类中,绝大多数类都重写了equals()方法,equals方法进行了重写则是用来比较指向的对象所存储的内容是否相等
hashcode实际是计算元素的哈希值,可以起到一定判重作用,例如我们使用equals判断集合是否有存在某元素,复杂度O(n),但是如果在map和set这种结构,底层是哈希表,我们先计算hashcode找到对应位置,看看有没有元素,有的话再进行equals判重,可以加快效率。
重写equals后必须重写hashcode,Java 规定,如果两个对象通过 equals() 方法比较相等,它们的 hashCode() 必须返回相同的值。
5. try-catch-finally
5.1 实现原理:
java
try{
Block1;
} catch{
Block2;
} finally{
Block3;
};
编译时会生成两块代码 Block1 block3 block2 block3,同时有个异常表。首先顺序执行block1 block3部分,如果block1部分有异常,根据异常表会goto跳到block2 block3的部分继续执行,没有异常就执行完block1 block2退出。
5.2 在过程中有return的执行逻辑
如果try或者catch中有return,先计算并暂存 try/catch中的返回值,然后执行 finally,最后返回暂存值,如果finally也有return,以finally的值作为最终返回值。
对于try/catch中return的变量,在finally会有一份副本,如果返回变量是基本数据类型,那么修改这个变量,不会影响到变量实际值,因为只是副本;如果返回变量是引用类型,那么修改变量的成员会影响到实际值
5.3 在极端条件下finally不执行
在 try/ catch块中调用了 System.exit(int status)方法,导致Java虚拟机退出;
执行 try/ catch块的线程被突然终止(例如调用 Thread.stop()方法,但该方法不安全)
try/catch执行时Java虚拟机或操作系统进程因崩溃等非正常原因退出
6. 类加载
6.1 Java类加载过程
通过classloader加载一个类时,先看本类是否已经加载过,每个类加载器内部维护一个已加载类的缓存(通常通过哈希表或类似数据结构实现)。当收到类加载请求时,首先检查缓存中是否存在目标类。
如果没有加载,根据双亲委派机制一步一步往上请求父类加载器加载,一直到bootstrap;如果都没有加载,由bootstrap尝试加载,加载不了就往下传递,直到最后一个子类如果没法加载,尝试自定义加载器,还不行就报class-not-found异常。(双亲委派机制也是重点、不了解的话需要自己学习一下)
6.2 类加载失败
如果你的类在加载时,父类加载过同名类,你的类会加载失败,因为在父类加载器查到缓存就返回了,而父类加载的同名类并不是你这个类。一般是通过自定义classloader加载外部的类,但是本身项目文件也有同名类的实现,当创建自定义加载器时,若未显式指定父加载器,Java 会通过 ClassLoader的构造函数自动将 AppClassLoader设为其父加载器。AppClassloader专门加载编译好的class文件,所以项目内的同名类先加载并返回了。
6.3 自定义classloader
有时候项目可能自己实现一个类,但是引用外部库发现有同名类,导致加载失败;或者需要加载的类是经过加密的;又或者需要进行动态替换;会用到自定义classloader进行类加载。自定义classloader时,如果我们不想打破双亲委派模型,就重写ClassLoader类中的findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
6.4 类加载步骤
具体加载过程主要有三步:
|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1加载 | 通过全类名获取定义此类的二进制字节流,将字节流所代表的静态存储结构转换为方法区的运行时数据结构,在堆内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口 |
| 2链接 | 验证确保字节码合法且安全,包括:文件格式验证(如魔数 0xCAFEBABE)。元数据验证(继承关系、方法重载等是否符合规范)。字节码验证(方法体逻辑是否合法)。符号引用验证(引用的类、方法是否存在)。 为静态变量分配内存并设置默认值(如 int初始化为 0,引用类型为 null)。若静态变量是 final常量且值在编译期确定,则直接赋实际值。 将常量池中的符号引用(如类名、方法名)转换为直接引用(内存地址或偏移量)。此阶段可能延迟到首次使用时(如方法调用时) |
| 3初始化 | 执行静态代码块和静态变量的显式赋值。 |
6.5 静态加载和动态加载
|--------|-------------------------------------|--------------------------------------------|
| 特性 | 静态加载 | 动态加载 |
| 加载时机 | 编译时加载 | 运行时加载 |
| 实现方式 | 使用 new关键字实例化对象 | 使用 Class.forName()或类加载器的 loadClass()方法 |
| 灵活性 | 较低,依赖关系在编译时即确定,无法在运行时改变 | 较高,可根据运行时条件或配置动态决定加载哪个类 |
| 性能特点 | 加载速度快,运行时无额外类查找开销 | 相对较慢,因为存在运行时的类查找和解析开销 |
| 依赖检查时机 | 编译时检查。若类不存在,编译将无法通过 | 运行时检查。即使目标类不存在,只要代码未执行到该处,程序仍可运行 |
| 异常/错误 | 类找不到时,运行时抛出 NoClassDefFoundError | 类找不到时,抛出ClassNotFoundException,必须捕获处理 |
| 典型应用场景 | 类固定不变、对性能要求高的小型应用或稳定模块 | 框架(如Spring)、插件系统、模块化开发等需要高扩展性的复杂应用 |
动态加载依赖反射机制,在运行时才进行类加载与查找和解析,所以效率上较弱。另外由于动态加载是运行时才加载,而有些事情是编译期做的,所以能做到一些静态加载不能完成的事情。(例如绕过泛型检查,但不建议使用)
7. 反射
重要特性,很多源码、框架、插件等都会用到反射。
7.1 为什么要有反射
java的代码需要编译成字节码,在虚拟机运行,为了赋予程序在运行时动态获取信息、操作对象的能力,从而突破静态语言在编译期就必须确定一切的限制,提升灵活性和扩展性,才有了反射。用到反射的场景往往是运行时才能确定程序接下来走向的情况,例如根据配置文件决定逻辑、动态加载、热更新、框架、插件、ide的代码提示等。
7.2 反射的原理
反射本质上就是把java类中的各种成分映射成一个个的Java对象。例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把各个组成部分映射成一个个对象。通过读取类的.class对象得到类的结构,然后将各个成分构造对象。
通过反射可以做到动态构造对象,调用函数,访问成员等操作,通过爆破还可以访问到private成员。在普通的应用程序开发中应谨慎使用反射,除非确实需要其提供的动态能力,但是对于理解一些代码、框架来说是必备能力。
8. 多态
8.1 Java多态的实现原理
多态需要继承父类并重写方法,例如Animal a = new Dog(); 编译时只检查Animal里有没有speak函数,运行时会根据a的实际类型去查找Dog是否重写了speak。(这就是JVM的动态绑定机制)。
Java 中所有"非 static、非 final 的方法"都是虚方法。JVM 会为每个类维护一个 虚方法表(vtable),这个表记录了每个方法在内存中的实际地址。a.speak实际是根据a的运行类型,找到a的vtable,查找speak方法的入口地址去执行。
static 方法:属于类本身,编译时绑定,final 方法:不能被重写,JVM 会直接绑定,private 方法:只在类内部使用,也不会多态,这些都属于"静态绑定",在编译期就决定了。
8.2 子类是否可以重写父类静态方法
不可以,静态方法在编译时通过类名直接绑定,与实例无关,因此不具备多态性。即使子类定义了同名静态方法,调用时仍取决于引用类型(编译时类型),而非实际对象类型(运行时类型)。子类定义与父类同名的静态方法时,父类方法会被隐藏(而非覆盖)。调用时,通过父类引用调用父类方法,通过子类引用调用子类方法。
9. 代理
代理是一种强大的设计模式,它允许你通过一个代理对象来控制对原始对象(目标对象)的访问。借助代理,你可以在不修改原始代码的情况下,为方法调用添加额外的逻辑,例如日志记录、性能监控、事务管理或访问控制。
9.1 静态代理
静态代理需要你手动创建一个代理类,该代理类必须实现与目标对象相同的接口。代理类内部持有目标对象的引用,并在其方法调用前后添加自定义逻辑。
java
// 1. 定义接口
public interface SmsService {
String send(String message);
}
// 2. 实现目标类
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("发送消息: " + message);
return message;
}
}
// 3. 创建静态代理类
public class SmsProxy implements SmsService {
private final SmsService target; // 持有目标对象
public SmsProxy(SmsService target) {
this.target = target;
}
@Override
public String send(String message) {
// 调用目标方法前增强
System.out.println("Before method send()");
// 调用目标对象的方法
target.send(message);
// 调用目标方法后增强
System.out.println("After method send()");
return null;
}
}
// 4. 使用代理
public class Main {
public static void main(String[] args) {
SmsService target = new SmsServiceImpl();
SmsProxy proxy = new SmsProxy(target); // 创建代理对象
proxy.send("Hello"); // 通过代理调用方法
}
}
9.2 动态代理
9.2.1 基于接口的 JDK 动态代理
jdk动态代理的核心在于在运行时动态生成代理类,用户调用Proxy.newProxyInstance()。JVM根据传入的接口数组生成一个新的字节码类(代理类)。所有接口方法都会被转发到用户自定义的InvocationHandler的invoke()方法。invoke()方法中可以实现方法增强、参数修改、权限校验、日志记录等操作.
JDK动态代理在JVM层内部通过以下几个步骤实现:生成代理类的字节码,加载字节码到内存(通过定义类加载器将生成的字节码加载为一个真正的Java 类),创建代理对象实例(使用反射调用构造函数,传入InvocationHandler实例)。
动态性强,运行时生成代码。要求目标类必须实现接口。无法代理类本身的函数,只能代理接口中定义的 public方法。
java
// 1. 定义接口和目标类(同上)
public interface SmsService {
String send(String message);
}
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("发送消息: " + message);
return message;
}
}
// 2. 实现 InvocationHandler
public class DebugInvocationHandler implements InvocationHandler {
private final Object target; // 目标对象
public DebugInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法调用前增强
System.out.println("Before method " + method.getName());
// 调用目标方法
Object result = method.invoke(target, args);
// 方法调用后增强
System.out.println("After method " + method.getName());
return result;
}
}
// 3. 获取代理对象工厂
public class JdkProxyFactory {
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载器
target.getClass().getInterfaces(), // 目标类实现的接口
new DebugInvocationHandler(target) // 自定义的 InvocationHandler
);
}
}
// 4. 使用代理
public class Main {
public static void main(String[] args) {
SmsService target = new SmsServiceImpl();
// 通过工厂获取代理对象
SmsService proxy = (SmsService) JdkProxyFactory.getProxy(target);
proxy.send("Hello JDK Dynamic Proxy");
}
}
9.2.2 CGLIB库动态代理
通过继承目标类并生成其子类的方式来实现代理,可代理普通类,但不能代理 final 类或 final 方法、static方法、private方法。(和java的多态性有关)
java
// 1. 定义目标类(未实现接口)
public class AliSmsService {
public String send(String message) {
System.out.println("发送消息: " + message);
return message;
}
}
// 2. 实现方法拦截器
public class DebugMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 方法调用前增强
System.out.println("Before method " + method.getName());
// 调用目标类的方法
Object result = proxy.invokeSuper(obj, args);
// 方法调用后增强
System.out.println("After method " + method.getName());
return result;
}
}
// 3. 获取代理对象工厂
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(clazz.getClassLoader()); // 设置类加载器
enhancer.setSuperclass(clazz); // 设置父类(即目标类)
enhancer.setCallback(new DebugMethodInterceptor()); // 设置回调(拦截器)
return enhancer.create(); // 创建代理对象
}
}
// 4. 使用代理
public class Main {
public static void main(String[] args) {
AliSmsService proxy =
(AliSmsService)CglibProxyFactory.getProxy(AliSmsService.class);
proxy.send("Hello CGLIB Proxy");
}
}
10. hook
Hook(钩子)是一种强大的技术,它允许你在程序执行的特定点插入自定义代码,从而在不修改原始代码的情况下改变或增强程序的行为。
10.1 基于动态代理的hook
这是最灵活和常见的方式,通过 Java 的反射 API 和动态代理机制在运行时拦截并修改方法的行为。流程是:1.明确需要 Hook 的类和方法;2.利用 InvocationHandler在目标方法调用前后插入逻辑(如日志记录、性能监控)3.通过反射或其他机制,让程序后续调用的是代理对象的方法。
10.2 基于java运行时的关闭钩子
Java 运行时环境提供了 Runtime.getRuntime().addShutdownHook(Thread hook)方法,用于注册一个在 JVM 正常关闭(如调用 System.exit()或收到 SIGTERM 信号)时执行的线程 。这在需要在程序退出前执行清理工作(如释放资源、保存临时数据)时非常有用。
关闭钩子本质上是一个已初始化但尚未启动的线程。可以创建一个或多个线程,通过 Runtime.getRuntime().addShutdownHook(Thread)方法注册后,当JVM开始关闭流程时,这些钩子线程会被启动并执行其 run方法中的代码,顺序不确定
10.3 基于字节码层面的hook
字节码层面的Hook是一种通过直接修改Java字节码来拦截和改变程序行为的技术。它主要利用Java Agent和字节码操作库,在类加载时或运行时动态修改类的字节码,从而实现方法拦截、逻辑插入等功能。
字节码Hook的核心在于拦截JVM的类加载过程,对字节码进行修改后再交给JVM执行。这主要通过以下机制实现:
Java Agent与Instrumentation API:Java Agent是JDK提供的标准机制,允许在JVM启动时(通过-javaagent参数)或运行时(通过Attach API)加载代理程序。代理程序会获取一个Instrumentation实例,通过它注册ClassFileTransformer(字节码转换器)。
类加载拦截:当JVM加载某个类时,所有注册的ClassFileTransformer会依次被调用。它们可以检查当前加载的类,并返回修改后的字节码,JVM则会使用这个修改后的版本来定义类。
字节码操作库:直接编写字节码指令非常复杂,因此通常使用字节码操作库来简化这一过程。常见的库包括:ASM:一个轻量级、高性能的库,提供接近底层的API,适合对字节码进行精细控制。Javassist:提供了更高级的API,允许使用类似Java的源代码字符串来修改字节码,上手更容易.
11. 注解
注解是给程序添加声明性信息的手段,这些信息可以被编译器、构建工具或运行时环境读取和处理,从而简化开发流程、增强代码可读性并减少冗余代码;注解本身不改变程序逻辑,而是通过附加信息(如@Override)提示编译器或框架执行特定操作(如检查方法重写);
11.1 元注解
Java中用于修饰其他注解的注解,它们定义了被修饰注解的基本行为和适用范围。
通过@Retention指定注解生命周期
|--------------------------------------------------------------------------------------------------------------------------------------------------|
| source源码级,仅源码阶段有效,如@NonNull用于编译时检查。 |
| class保留到字节码但运行时不可见,主要作用有字节码增强(字节码工具在编译后读取这些注解并进行相应处理,如AOP切面织入、性能监控代码插入等),辅助代码生成(某些框架在编译时根据CLASS级别注解生成辅助代码),为编译器或构建工具提供优化指示,如标记某些类需要特殊处理(内联、缓存等)。 |
| Runtime级别,运行时可通过反射读取注解并处理,如果注解需要在运行时读取并执行,效率可能会下降,也有部分注解虽然是runtime级别,但是在运行时没有实际处理逻辑或者其运行逻辑直接调用在编译时处理注解生成的代码,这种情况不会由于反射影响效率。 |
@Target限定注解可以应用的目标范围(如类、方法、字段等)。
|----------------------------------------------|
| ElementType.TYPE:类、接口、枚举。 |
| ElementType.METHOD:方法。 |
| ElementType.FIELD:字段。 |
| ElementType.PARAMETER:方法参数。 |
| ElementType.ANNOTATION_TYPE:其他注解(用于自定义注解的嵌套) |
@Documented标记注解是否包含在生成的Javadoc文档中。若注解被@Documented修饰,则使用该注解的元素会在API文档中显示注解信息。
@Inherited允许子类继承父类的注解(仅对类级注解有效)。例如,父类使用@Inherited修饰的注解,子类若无显式覆盖,则自动继承该注。
11.2 如何定义注解
通过@interface类型代表注解,通过"类型"加"名称();" 定义参数,通过元注解指定行为。
java
@Retention(RetentionPolicy.RUNTIME) // 指定注解生命周期
@Target(ElementType.METHOD) // 指定注解可应用的位置
public @interface MyCustomAnnotation {
String value() default "default value"; // 定义属性
int priority() default 0; // 可以定义多个属性
}
11.3 如何使用注解
java
public class AnnotationDemo {
@MyCustomAnnotation (value = "Hello Annotation", priority = 3)
public void myMethod() {
// 方法体
}
}
12. String
12.1 JAVA String不可变性
Java 使用字符串常量池来存储字符串字面量,以节省内存,多个相同String变量可以复用一份数据。(jdk7之后字符串常量池也在堆中,如果String s= "123";那就是在字符串常量池创建一个字符串对象,如果是String s = new String("123");那么字符串常量池和堆区存放对象的地方各有一个字符串对象)
实现原理:
|------------------------------------------------------------------------------------------------------------------------------------------------|
| final 类:String 类被声明为 final,无法被继承,防止子类修改其行为。 |
| 私有字符数组:String 内部使用 private final 的char(java8和以前)或者Byte数组存储字符数据,且没有提供修改数组内容的公共方法。(char占16字节,对于ASCII字符有一半内存浪费,后面jdk9后默认使用byte,节省空间,非ASCII采用char) |
| 操作返回新对象:String 的所有修改操作(如 toUpperCase()、replace()、substring())都会返回一个新的 String 对象,而不是修改原对象。 |
| 无 Set方法:String 类不提供任何修改内容的方法,所有操作都是只读的。 |
12.2 String StringBuilder StringBuffer区别
String创建后不可变,因为final;其它两个不是final所以可变;
StringBuilder非线程安全,可能有多线程安全问题;StringBuffer对公有方法加synchronized保证线程安全;String天生线程安全;
频繁修改时StringBuilder性能最高,StringBuffer次之,String最差
13. List
List是自动扩容的数组,顺序存储,支持下标访问;一般有两种实现,ArrryList底层是动态数组,适合查询多,增删少的场景,非线程安全;LinkedList是基于双向链表,适合增删多,查询少的场景,非线程安全。使用不当经常出现ConcurrentModificationException,一般是使用for遍历时删除数据,或者遍历时其它线程改动了list。需要在遍历时删除,使用迭代器的 remove()方法,这是唯一安全的方式,可以避免 ConcurrentModificationException
另外CopyOnWriteArrayList这种实现,线程安全,写操作时复制,读操作完全无锁,性能极高,迭代器不会抛出ConcurrentModificationException,写操作性能较差,适合读多写少的场景。
另一种实现线程安全方法是Collections.synchronizedList包装,所有方法都使用synchronized同步,遍历时仍需要手动加锁
ArrryList扩容默认是原先1.5倍容量。
14. Map集合
一般Map有三种实现:Hashmap实现、基于红黑树的Map实现、链表哈希表实现(LinkedHashMap)
基于红黑树的Map实现原理:使用平衡二叉搜索树存储键值对,时间复杂度:基本操作都是O(log n),键按自然顺序或自定义顺序排序,不允许null键
链表哈希表是HashMap的子类,保持插入顺序,在哈希表基础上维护一个双向链表记录插入顺序,遍历顺序可预测,性能略低于HashMap
14.1 hashMap底层原理
数组+链表+红黑树,当链表长度超过阈值(默认8)且数组长度≥64时,链表转换为红黑树,当红黑树节点数减少到阈值(默认6)以下时,会退化为链表。
计算索引:先获取元素的哈希值,然后高16位与低16位异或优化哈希分布,最后是将哈希值与哈希数组长度-1进行与操作。
当数组长度是2的幂次,哈希值与数组长度减一等效于哈希值对数组长度取模,但效率更高;同时也可能使哈希值分布均匀减少冲突,当n-1的二进制形式全是1时(如15=01111),哈希值的低k位能够完全参与索引计算,使得不同哈希值大概率映射到不同位置;扩容时元素在新数组中的位置要么保持原索引,要么是原索引+原容量,只需检查哈希值的某一位即可确定位置变化,无需重新计算整个哈希值,原先n=16,计算哈希值&16如果是0就不移动,否则新位置=原位置+16。
14.2 Weakhashmap
使用弱引用存储key,配合引用队列实现自动清理。当弱键被垃圾回收后,对应的弱引用会被加入引用队列,WeakHashMap在执行操作(如put/get)时会检查队列,移除已被回收的键值对。
14.3 Hashtable
比较古老的线程安全哈希表,key和value都非空,使用数组+链表的实现,所有public方法都用synchronized关键字加锁保证线程安全。
14.4 ConcurrentHashMap
比起hashmap提供了线程安全功能,CAS用于无竞争情况下的节点插入/更新,通过原子操作保证线程安全,无需加锁;synchronized当发生哈希冲突时,仅锁定冲突的桶(bucket)节点,锁粒度从段缩小到单个节点,并发度更高
14.5 hashset和hashmap区别
HashSet:底层基于HashMap实现,内部维护一个HashMap实例,将元素作为HashMap的key,而value固定为一个常量对象,因此,HashSet本质上是"简化版"的HashMap,仅关心键的唯一性。