本篇主要记录Java的核心技术点,其他知识点另外写文章记录(字数多很卡)。
回答问题抓重点,要和面试官保持在同一纬度。抛出锚点,看他对什么有兴趣。按照总-分-总的顺序回答问题,先做总结,再做分项描述。不会的就绕过去
持续更新,用于个人记录整理,包括后续的面试总结也会更新在这里。记得一定要有自己的回答思路和理解,不要全背。
一:面向对象
Java遵循面向对象设计原则,面向对象编程(OOP)是一种以对象为中心的编程风格,相比于面向过程编程(专注这个流程的实现)更加模块化,具有灵活可扩展性强的优点。
注重对象的模块化和可复用性,面向对象的三大特性是封装、继承、多态,提供面向对象实现。
封装:将数据和行为封装在对象内部,对外提供访问方法,隐藏实现细节,提高安全性和扩展。
继承:子类可以继承父类的属性和方法,实现代码复用和增强扩展。
多态:对象可以通过父类或接口调用不同子类,实现不同的行为,降低耦合度,提高可扩展性。
新增子类或实现类时,不用修改原有代码(同Jdk8接口默认方法),只需要父类或接口调用即可。
多态分为:编译时多态和运行时多态,在不同阶段决定方法的绑定。
编译时多态:通过方法重载实现,在编译阶段完成绑定,编译器通过方法名+参数列表,直接确定调用的具体方法。
运行时多态:通过方法重写实现,在运行阶段完成绑定,通过子类的实例动态查找并调用重写后的方法。
重载:在一个类或接口中,允许存在多个方法名相同的方法,只要其参数列表(类型,数量,顺序)不同。在调用时,会根据传参动态选择调用的具体方法,适用于同一个类下定义不同场景下的行为。
重写:发生在继承或接口实现场景,子类可以重新父类或接口的方法,为该方法提供新的个性化或增强的实现,实现运行时多态性,增强系统可扩展性。需要注意重写的方法访问修饰符需大于或等于父类,否则会编译报错,添加@Override注解,可帮助编译器验证重写是否符合规范(非强制但推荐使用)。
另外子类重写方法抛出的异常类型需与父类一致或是其子类,不能抛出更宽泛的检查异常。
二:String字符串
总:String,StringBuilder,StringBuffer 都是Java中处理字符串的类。区别主要体现在是否可变,线程安全以及性能上。
String:String是final修饰的不可变类,字符串一旦创建,其内容就无法更改。后续对其的修改截取操作,都会重新创建字符串对象。因其不可变特性,在JVM中被用作常量池。
在Java8及以后,编译器会对字符串的常量拼接做优化,将字符串常量拼接替换为StringBuilder,提高了代码性能。但是开发中,最好还是根据业务场景确定使用类型。
StringBuffer:跟随Java1.0发布存在,对象是可变的,可以进行字符串的追加、删除等操作。所有方法使用 Synchronized 方法修饰,保证线程安全。同样由于同步会导致性能开销,适用于多线程环境下进行字符串操作。
StringBuilder:Java5发布的性能优化,对象也是可变的,并提供了与StringBuffer类似的方法,可对字符串进行处理。非线程安全,所以性能更好。平时使用较多,适用于单线程环境下频繁修改字符串或修改大量字符串的场景。
底层:
StringBuffer 和 StringBuilder 都继承了 AbstractStringBuilder 抽象类,底层都是利用可修改的char数组实现(Jdk 9是byte数组),其大致流程为:
1:使用字符数组存储字符序列 char[],(String底层也是char数组,但也是不可变的)。
2:使用方法append()、insert()等操作,是直接修改内部的字符数组,不会创建新的对象。
3:操作字符串时,如果当前数组容量不足,会优先以2倍+2容量扩展数组长度,减少扩展次数,提供性能。
扩展:
StringBuilder 底层是使用char数组存储数据。而数组是连续内存结构,为了避免频繁扩容复制和申请内存,可以在创建时传入capacity(数组大小,默认16),减少扩容次数,提升效率。
具体新增逻辑:在调用append方法时,先判断转成char需要占数组的几位,然后计算现在的数组容量是否足够,如果不够就扩容。随后将数据转换成char放到数组中,并更新字符数。
扩容就是使用 Arrays.copyOf(),进行拷贝加扩容,扩容后的数组容量为:原容量的两倍 + 2。但当扩容后也不够时,则会直接扩容到恰好能容纳所有数据的最小值。
另外,在Jdk 9中,底层的char数组优化为 byte数组 和一个 coder 标志位,更加节省内存。其中coder=0 表示字符串仅包含 Latin-1 字符(单字节编码),coder=1 表示包含 UTF-16 字符(双字节编码)。
追加字符时,若遇到无法用当前编码表示的字符(如 Latin-1 模式下追加中文字符),会触发编码转换:将整个 byte[] 转换为 UTF-16 格式(双字节),并更新 coder=1。此过程可能伴随扩容和内存复制。
Latin-1 编码下每个字符占用 1 字节,相比 char[](每个字符 2 字节)可减少 50% 的内存占用,但对非 Latin-1 字符会回退到双字节。
三:异常
总:Java中的异常程序是管理程序运行时错误的重要机制,在日常开发中,也经常遇到代码异常的问题(空指针,微服务找不到,数组越界,sql语句有问题等),以及指定业务抛出错误提示等。
Throwable是所有异常和错误的基类,其下有分为 Error 和 Exception 两个子类。异常的处理格式为:try-catch-finally,finally代码块为最后执行(无论是否异常)。
finally这个可能会问:是否总会被执行?正常情况下是的,除非有以下几种特殊情况:
1:在try/catch中调用 System.exit(),终止Java的虚拟机。2:线程被终止。3:JVM崩溃。
1:Error
指程序无法处理的报错,严重错误,例如OutOfMemoryError,出现这个报错时,程序将被迫停止运行。
2:Exception
指程序可处理的异常,也是平时遇到比较多的。其下又分为 CheckedException(检查异常) 和 RuntimeException(运行时异常),发生在不同的环节:
CheckedException:异常发生在代码编译阶段,Java编译器会去强制程序对此类异常进行处理: try-catch 或 throws 上抛,但是注意,如果子类重写父类的方法上没有抛出,则子类必须自己处理。常见的异常有:
- SQLException:数据库操作失败(如连接断开、SQL 语法错误)。
- IOException:文件流相关异常,文件流操作失败。
- FileNotFoundException:IO异常子类,文件路径不存在或无法访问。
- ClassNotFoundException:尝试通过字符串类名加载不存在的类。
- InterruptedException:线程在等待、睡眠时被中断。
RuntimeException:发生在程序运行过程中,会导致程序当前线程运行失败。无须显式处理,但出现该异常,则表示代码逻辑错误,程序需要优化处理。常见异常有:
- NullPointerException:空指针异常,尝试访问空对象的方法或属性,处理是代码添加判空。
- IndexOutOfBoundsException:集合或字符串下标越界,获取长度外的索引数据。
- ClassCastException:对象类型强制转换不兼容,如强转接收类。
- NumberFormatException:转数值格式不合法,例如String转int,处理是可使用工具类。
- ArithmeticException:数学运算错误(如除以零),int result = 5 / 0。
3:异常优化
Java在后续版本中对异常也做了优化,主要是资源释放和多个异常捕获。
在Java1.7及以后,可使用 try-with-resources 语法,自动关闭资源,不用再手动设置finally块释放资源,减少了冗余代码,简化操作。注意需实现 AutoCloseable 接口。
try (FileInputStream file = new FileInputStream("file.txt")) {
// 使用资源
} catch (IOException e) {
e.printStackTrace();
}
同样在1.7版本,优化了多异常捕捉。不用像之前写多个catch,相同处理逻辑的可放在一起。
catch (IOException | SQLException e) {
e.printStackTrace();
}
这是系统层面的优化,而对于我们开发中,对异常的处理也要规范运用和优化。
1:首先是尽量避免异常,例如空指针这些,提前判空处理,就不会发生。开发人员平时应该写出高质量代码,避免一些逻辑bug。
2:尽量捕捉具体的异常,而不是直接用Exception顶级异常。会造成代码逻辑不清晰,且可能捕捉到预期之外的异常,不好排查问题。
3:不要吞了异常,捕获到异常之后要做相应处理,例如打个日志,抛出一个报错提示等。如果什么都不做,则线上不能及时地发现问题(还不如不捕捉),最后可能造成更严重地数据问题。且还不好排查(无日志)。
4:try-catch范围尽量小,只需要圈住关键逻辑代码,而不是整个方法。因为try-catch会影响JVM对代码地优化,例如重排序等。
5:不要在 finally 代码块处理返回值和直接return,因为这里会涉及JVM的栈的执行指令,会覆盖返回值或吞掉异常(可在JVM详讲)。
4:Throw 和 throws 的区别
首先,一个是用来具体使用,一个是用来声明可能抛出的异常。
throw:在代码中手动抛出一个异常,需要写在方法内部。抛出后可对齐进行捕获,也可向上传递,由调用者处理。开发中,可根据业务场景抛出指定异常或自定义异常提示(将底层异常转为业务异常)。
throws:在创建方法时声明可能抛出的异常,需要写在方法参数列表之后。声明后由调用者处理。子类重写父类的含异常声明的方法时,不能抛出比父类异常的上级或其他无关联的异常。
class Parent {
public void doSomething() throws IOException { }
}
class Child extends Parent {
@Override
public void doSomething() throws Exception { // ❌ 编译错误:Exception是IOException的父类
// ...
}
@Override
public void doSomething() throws SQLException { // ❌ 编译错误:SQLException是新的Checked异常
// ...
}
}
5:自定义异常
在Java中,自定义异常是通过继承现有异常类来实现的,通常用于表示特定业务场景或系统逻辑中的错误。统一异常规范,将系统错误和业务报错区分开来,提高系统可维护性,简化开发流程,便于排查问题。
自定义异常的创建步骤:
1:选择继承的父类,继承父类Exception 或 RuntimeException,常使用运行时异常。
2:命名规范推荐以 Exception 结尾,如果安装有阿里规范插件,会标黄提示。
3:提供构造器,可传递参数并使用super调用父类的构造器实现。
4:添加业务数据,如单号,错误代码,时间戳等,便于记录及后续排查。
public class PaymentFailedException extends RuntimeException {
private String orderId; // 自定义字段:订单ID
private int errorCode; // 自定义字段:错误码
// 构造器
public PaymentFailedException(String message, String orderId, int errorCode) {
super(message);
this.orderId = orderId;
this.errorCode = errorCode;
}
// Getter方法
public String getOrderId() { return orderId; }
public int getErrorCode() { return errorCode; }
}
开发中,避免自定义异常过多,会再次导致异常混乱。
保持简洁,避免添加过多字段,导致异常类臃肿。
可根据场景传递并保留原始异常,便于后续排查。
通过合理设计自定义异常,可以显著提升代码的可读性和可维护性,同时为调用者提供清晰的错误处理入口。
四:注解
Java注解是Java 5引入的一种元数据处理机制,用于为代码添加额外的信息,这些信息可以被编译器、工具或者运行时框架读取和处理。说白了就是在引入注解的地方做一些事情,简化代码。注解本身不直接影响代码逻辑,但可以用于生成代码、配置框架行为、静态检查等场景。
注解有Java自带的,框架中提供的,或自定义的。在代码中随处可见,非常简便,使用频率很高。
1:注解概念
注解是一种以 @注解名 形式,依附在类、方法、字段、参数等代码元素上的标记,用以做一些查验和处理。注解的处理形式可总体分为三个阶段:
- 编译时处理:生成代码。例如Lombok注解(@Data、@Getter、@Setter)。
- 运行时处理:通过反射获取注解信息。例如Spring注解(@Autowired、@Component、@RequestMapping等),Jackson实例化注解(@JsonProperty),Swagger注解(@ApiModel、@ApiOperation),JUnit单元测试注解(@Test)等。
- 静态检查:编译器验证代码是否符合规范。例如验证重写方法(@Override),过时方法标记(@Deprecated),参数判空(@NonNull)等。
以上都为简化代码配置,如果让我们代码实现也很容易做,但是调用多了,就会造成代码冗余,重复逻辑过多。并且不如注解清晰,便于代码维护和扩展。
2:元注解
元注解是用于定义其他注解的行为,需写在其他注解的上面。
常见的有:
1:@Retention:指定注解的保留策略。有三个枚举可选,可做了解。
//RetentionPolicy.SOURCE:表示仅源码保留(编译后丢弃,例如Lombok注解)。
//RetentionPolicy.CLASS:保留到字节码(默认,运行时不可见)。
//RetentionPolicy.RUNTIME:运行时保留(可以通过反射读取)。
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation { ... }
2:Target:指定注解可以应用的目标元素类型。
//ElementType.TYPE:类、接口、枚举
//ElementType.METHOD:方法
//ElementType.FIELD:字段
//ElementType.PARAMETER:参数
//ElementType.CONSTRUCTOR:构造函数
//其他如ElementType.ANNOTATION_TYPE(注解类型)等。
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation { ... }
包括上面说的@Documented,以及@Inherited标注类是否可被继承等。
3:自定义注解
根据开发中的业务场景,也可以把用的频繁的或者特殊业务配置成自定义注解的形式,在指定位置添加使用。创建时选择annotation可带出,同样使用 @interface 关键字,可在内部设置属性。
//1:创建(注意Target设置使用权限)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface annotationTest {
//该种方式在使用时不能传递参数赋值
String message = null;
//可设置默认值,参数后需设置括号
String name() default "张三";
//可设置默认值,参数后需设置括号
String address();
}
//2:使用
@annotationTest(name = "里斯", address = "北京")
public class StudentEntity {
//...
}
在开发中,还可以通过反射获取注解信息,但是不推荐,影响性能。
Method method = obj.getClass().getMethod("testMethod");
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
System.out.println("Name: " + annotation.name());
}
自定义注解结合反射或注解处理器可实现灵活的功能扩展,处理一些相同的业务场景。
五:泛型
Java中的泛型是比较重要的一个特性,泛型的作用是在编译时检查类型安全,允许开发人员编写更通用和灵活的代码,避免在运行期出现类型转换错误。
泛型是Java 5引入的,在这之前是没有这个特性的,例如 List 以前的使用:
List list = new ArrayList();
list.add("yes"); // 加入string
list.add(233); // 加入int
String str = list.get(0);
String str = list.get(1); //报错
这样插入数据时不会报错,都是当作Object类型。但是在获取数据时,会要求我们强转数据。那么此时可能就会发生类型转换异常 ClassCaseException 。
所以Java引入泛型,约束参数类型,在编译期就可以识别不符合类型的数据,提前抛出错误提示。并且在获取数据时无需再显式转换。
注意:Java的泛型只在编译时生效,JVM运行时泛型被擦除了,获取不到泛型信息(可在运行中使用反射插入不符合泛型的数据验证)。
1:泛型类
在开发中,有时需要考虑代码通用,例如请求接口或接口返回的报文格式。或业务场景推送数据时,传入不同的对象。此时我们也可以自己定义泛型类,在创建类时传入类型并使用。
括号内时泛型参数,这个参数名字是自己定义的,下面可以使用。
public class Response<T> {
private T content;
public void setContent(T content) { this.content = content; }
public T getContent() { return content; }
}
//使用
Response<String> response = new Response<>();
response.setContent("Hello");
例如我之前有一个推送需求,根据场景不同要传入不同的对象。那么我在创建类的时候,可以定义泛型为 Object 类型即可,很灵活。
2:泛型方法
不仅仅可以定义泛型类,还可以定义泛型方法,使得方法能够处理多种不同的数据类型。使用格式是:在方法返回类型前声明类型参数。
例如一个方法需要返回不同对象。但是注意实际使用时,这样反而不利于获取对象,要先判断返回的对象类型,然后强转。这里用class做一个演示,根据传入参数可以确定返回类型。
public <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
// 使用示例
String str = createInstance(String.class); // 新建空字符串
Date date = createInstance(Date.class); // 新建Date对象
包括可以设置键值格式泛型。
public <K, V> void printMapEntries(Map<K, V> map) {
for (Map.Entry<K, V> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
// 使用示例
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
printMapEntries(scores); // 输出: Alice: 95
3:通配符
指在泛型中添加符号,用于对泛型类型参数进行范围限制。包含无界限定符、上界限定符和下界限定符。
无界限定符:表示未知类型,使用格式为:在泛型内添加 ? ,该种类型只支持读取为Object,为最大的类型。
public void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
上界限定符:使用格式为 <? extends T> ,表示只接受T及其子类型。通常用于读取操作,确保可以读取为T或T的子类的对象。一般直接使用T进行读取,这是比较稳妥安全的。
void processNumbers(List<? extends Number> list) {
Number num = list.get(0); // 安全读取
}
上界限定符一般用来读取,因为即使限制了指定类及其子类,但在添加元素时,会违反类型安全性(不知道哪个子类),例如往LIst添加元素。
List<? extends Number> numberList = new ArrayList<>();
//会编译错误:不能向上界类型中添加元素
//numberList.add(10);
下界限定符:使用格式为 <? super T> ,接受T及其父类型。通常用以写入操作,确保可以安全地向泛型集合中插入指定类型的对象。
void addIntegers(List<? super Integer> list) {
list.add(42); // 安全写入
}
下界限定符一般只用来写入,因为虽然限制了添加元素时必须是T或其父类。但在读取时,无法确定是哪种具体类型,也就不能用上面传入的类型接收(T),可用Object接收并判断类型,再强转,但是这样相对麻烦一点。
List<? super Integer> integerList = new ArrayList<>();
integerList.add(10); // 正确,可以添加 Integer 类型
integerList.add(20);
//会编译错误:虽然是 Integer 的父类,但不能保证是 Integer 类型,因此不能读取为 Integer
//Integer num = integerList.get(0);