Java-面试-基础-1

本文主要汇总常见的 Java 面试基础内容,后续可能有集合、并发部分。

本文来源 Java基础常见面试题总结(上) | JavaGuide

基础概念与常识

Java 语言的特点

面向对象、平台无关、网络编程、编译与解释并存、最重要的是平台无关性,一次编写,处处运行。

JVM JDK JRE JIT

JVM:Java 虚拟机,并不只有一个标准。是运行字节码的虚拟机。

JDK:Java 开发工具包,包含 JRE,以及编译器 javac 和一些其他工具。

JRE:Java 运行环境,包含 JVM 和一些基础 Java 类库。

但是在 Java 9 之后,JDK与JRE不再区分,JDK被组织为94个模块与jlink工具。

Java 代码的运行流程:

1
.java -> javac 编译 -> .class -> 解释器&JIT(热点代码JIT,非则解释器) -> 机器代码执行

.class -> 机器码 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行。后续针对热点代码引进了 JIT编译器,JIT 属于运行时编译,一次编译后,机器码保存。

热点代码采用惰性评估的方式,JIT 仅对热点代码进行编译,JVM 根据代码每次执行情况收集信息进行优化,所以执行次数越多越快。

总之,四者的包含关系是:

1
JDK > JRE > JVM > JIT

AOT

AOT: Ahead of Time compilation ,静态编译。

好处:避免了 JIT 的预热开销,提高启动速度,减少内存占用,增强安全性(不容易反汇编)。

对比:

  1. JIT: 更高的极限处理能力、降低请求的最大延迟
  2. AOT:启动时间、内存占用、打包体积

缺点:AOT 更适合云原生场景,对微服务友好,但是无法支持 Java 的动态特性,比如 反射、动态代理、动态加载、JNI 等

OpenJDK

与 Oracle JDK 不同,OpenJDK 属于开源版本。内容上差距不会太大。

与 C++ 对比

相同之处:

  1. 面向对象
  2. 封装 继承 多态

不同之处:

  1. Java 不提供指针访问内存
  2. Java 类不可以多继承,C++可以。但是注意 Java的接口是可以多继承的
  3. Java 有 GC(自动垃圾回收),不需要手动释放
  4. Java 支持方法重载,C++ 支持方法和操作符重载。
  5. ……

基本语法

标识符 关键字 字面值

标识符:名字

关键字:特殊的,被Java预定使用的标识符

字面值:true false null

default,同时兼顾:

  1. 程序控制:switch 中的默认匹配
  2. 类、方法、变量修饰符:定义默认实现
  3. 访问控制:如果一个方法前面没有任何的修饰符,默认有一个 default,但是加上会报错。

移位运算符

注意 >>> 表示无符号右移。

移位运算符的作用:

  1. 快速乘2或除以2
  2. 位字段管理,存储操作多个布尔值
  3. 哈希算法与加解密
  4. 数据压缩,如霍夫曼编码
  5. 数据校验,如CRC 循环冗余校验码
  6. 内存对齐,用于计算调整数据对齐地址

只能对 int long 类型进行移位,short byte char 会先做自动类型转换。

超过 32 位的移位 %32.

Java 数据类型

基本数据类型

8种基本数据类型:

基本类型 位数 字节 默认值 取值范围
byte 8 1 0 -128 ~ 127
short 16 2 0 -32768(-2^15) ~ 32767(2^15 - 1)
int 32 4 0 -2147483648 ~ 2147483647
long 64 8 0L -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1)
char 16 2 'u0000' 0 ~ 65535(2^16 - 1)
float 32 4 0f 1.4E-45 ~ 3.4028235E38
double 64 8 0d 4.9E-324 ~ 1.7976931348623157E308
boolean 1 false true、false

注意:

  1. Java 的 boolean 没有明确定义,看 JVM 怎么实现。
  2. Java 的数据类型占用位数不会随着硬件而变化。
  3. long 后面 需要加上 L
  4. float 后面需要加上 F\f
  5. char 使用单引号,字符串类型才使用双引号,与Python不同。另外 String 不属于基本类型

包装数据类型

额外学习内存模型 【java】jvm内存模型全面解析_哔哩哔哩_bilibili

基本数据类型与包装数据类型的区别:

  1. 包装类型可以用于泛型,对象属性中大都用包装类型
  2. 包装类型存在JVM堆中,基本数据类型的局部变量存在JVM的局部变量表中
  3. 包装类型没有默认值,直接是 null
  4. == 比较的是值,对于包装类型比较的是地址。包装类之间的比较,需要使用 equals() 方法

关于逃逸分析:

逃逸分析(Escape Analysis)是一种编译器优化技术,它分析程序中的对象分配,以确定对象的作用域和生命周期。具体来说,逃逸分析要确定一个对象是否会逃逸出它被创建的方法或者作用域,换句话说,就是判断对象的引用是否会被传递到当前方法或作用域之外。

逃逸分析的主要目的是为了优化内存分配和提高程序性能。以下是逃逸分析可以带来的一些优化:

栈上分配:如果逃逸分析确定某个对象不会逃逸出方法,那么这个对象可以在栈上分配内存,而不是在堆上。栈上分配的好处是当方法执行完毕后,对象的内存可以立即被释放,这样可以避免垃圾收集器的介入,减少垃圾回收的开销。

同步消除:如果逃逸分析发现对象仅在单线程内部使用,那么对该对象的同步操作可以被消除,因为不存在竞争条件。

锁消除:如果逃逸分析确定某段代码中的锁不会被其他线程所需,那么这个锁可以被消除。

基本数据类型存储在栈上,对象实例存储在堆中属于误区,需要通过逃逸分析判断是否逃逸到方法外部,对象如果是局部的,通过变量替换存放在栈上,基本数据类型如果是成员变量,存放在堆/方法去/元空间中。

包装类型会采用缓存与装箱机制。

1
2
3
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);

上述代码中,第一行装箱,采用内部缓存的值,第二行新建对象,所以两者是不一样的。

自动装箱与拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;
1
2
Integer i = 10;  //装箱
int n = i; //拆箱

浮点数精度丢失的解决

浮点数精度丢失是因为计算机内部存储不了那么多精度。

采用 BigDecimal 可以实现堆浮点数的运算。

超过long 类型的整数

BigInteger 类型,内部采用 int [] 数组存储任意大小的整数

变量

成员变量与局部变量

成员变量有默认值,自动赋值,如果是 final 修饰的内容,需要显式赋予初始值。

静态变量

  1. 类变量
  2. 通过类名访问
  3. 一般,静态变量会被 final 关键字修饰为常量

字符型常量与字符串常量

  1. 前者两个字节,后者若干
  2. 前者是单引号,后者双引号
  3. 前者存值,能参与运算,后者实际上存一个地址

方法

静态方法不能调用非静态成员

  1. 静态方法属于类,非静态成员属于实例
  2. 静态方法在类加载时就存在,此时内存中还不存在非静态成员(因为还没有实例化)

实例方法是可以访问静态成员与非静态成员的。

重写与重载

  1. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  2. 重写的参数列表一定不能修改,子类方法返回值范围小于等于父类方法类型【意思是:如果父类是 void 或者基本数据类型,那子类不能变,如果父类的返回值类型是一个引用类型,重写时可修改未引用类型的子类】

重写 遵循 ”两同两小一大

“两同”即方法名相同、形参列表相同;

“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;

“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等

可变长参数

  1. public static void method1(int n, String... args)

  2. 遇到方法重载时,优先匹配定长参数的方法

  3. Java 的可变参数编译后实际会被转换成一个数组

面向对象基础

  1. 对象实例/实体在堆内存中,对象引用指向对象实例,在栈内存中。
  2. 对象相等比较内存中存放的内容是否相等,引用相等指的是指向的内存地址是否相等
  3. 构造方法没有返回值,不能被重写,但是可以被重载

面向对象三大特征

关于继承的误区:注意!子类用于父类的所有属性与方法,包括私有属性与私有方法,只是子类无法访问继承来的父类的私有属性与私有方法,只是拥有

接口与抽象类

区别:

  1. 设计目的:接口是为了约束类的行为;抽象类是为了复用
  2. 接口可以多继承;抽象类只能单继承
  3. 成员变量:接口 public static final 一定要初始值,不能修改。抽象类成员变量任意。
  4. 方法:
    1. Java 8 之前接口方法类型 public abstract
    2. Java 8 中 default 方法用于提供接口方法的默认实现
    3. Java 8 中 static 方法类似于类中的静态方法,只能通过接口名调用,不能被实现类覆盖。很少用
    4. Java 9 中 private 方法仅用于在接口内部进行代码的复用共享,不对外暴露。

深拷贝 浅拷贝 引用拷贝

shallow&deep-copy

一图胜千言:

  1. 引用拷贝:只拷贝引用变量的内容,也就是拷贝最外面一层的地址。两个引用变量,指向堆上的同一个对象
  2. 浅拷贝:往里走一层,创建一个新的对象,拷贝过来,但是至于对象里面的属性是不是引用的,不继续深究了
  3. 深拷贝:一层一层直到最深,把所有对象全部新创建一遍

Object

所有类的父类,提供了11个方法:

这部分暂时跳过吧。

String

三种字符串

  1. String 不可变,内部采用数组存储字符串,类和数组都是用final 修饰,没提供接口也不能继承也不能修改,所以字符串就不能修改了。
  2. String 常量、线程安全。StringBuffer 加同步锁,线程安全。StringBuilder 没有同步锁,线程不安全。
  3. String 改动时生成新的对象,改变指针指向。StringBuffer 对对象本身操作。

对于三者使用的总结:

  • 操作少量的数据: 适用 String
  • 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  • 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

字符串拼接

Java 中仅有的两个重载运算符 ++= 是为 String 类重载的,注意Java 不支持运算符重载。

Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是字符串的值是否相等。

注意循环体内使用 + 做字符串拼接不要使用 String,而是应该使用 StringBuilder,因为JVM内部是创建了 StringBuilder对象,如果采用循环体外定义 String而后循环体内 +,JVM会在循环内隐式不断创建 StringBuilder,所以我们应该先在循环体外显式创建一个 StringBuilder,而后在循环体内进行拼接。

1
2
3
4
5
6
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {
s.append(value);
}
System.out.println(s);

JDK 9 中 更新了字符串拼接方法。

String s1 = new String("abc");这句话创建了几个字符串对象?

先说答案:会创建 1 或 2 个字符串对象。

  1. 字符串常量池中不存在 "abc":会创建 2 个 字符串对象。一个在字符串常量池中,由 ldc 指令触发创建。一个在堆中,由 new String() 创建,并使用常量池中的 "abc" 进行初始化。
  2. 字符串常量池中已存在 "abc":会创建 1 个 字符串对象。该对象在堆中,由 new String() 创建,并使用常量池中的 "abc" 进行初始化。

String.intern()

  • 用于保证字符串引用在常量池中的唯一性
  • 如果常量池中已经存在相同内容的,返回已有对象的引用,斗则,将该字符串添加到常量池并返回引用

分析拼接操作后的比较可能

1
2
3
4
5
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";

str1str2: 这两个字符串是直接赋值的字面量 "str""ing",因此它们会被放入字符串池中。

str3: str3 被赋值为 "str" + "ing"。因为这两个字符串都是字面量,编译器在编译期间会对它们进行常量折叠,直接将它们拼接成 "string"。因此,str3 指向字符串池中的 "string"

str4: str4 被赋值为 str1 + str2,由于 str1str2 是变量而不是字面量,编译器无法在编译期确定 str4 的值。因此,str4 的拼接会在运行时完成,会生成一个新的 String 对象在堆上,而不是字符串池中的 "string"

str5: str5 是直接赋值的字面量 "string",它也会被放入字符串池中。

在 Java 中,== 比较的是对象引用(内存地址),而不是字符串的值。字符串池中的相同字面量字符串会共享引用,而运行时通过变量拼接产生的新字符串会生成新的对象。因此,只有当字符串引用的是同一个字符串池对象时,== 比较才会返回 true

注意:str4 是变量。还有一种办法,我们可以将它描述为 final,这样编译器会把他当作常量值来处理,结果就相同了。

异常

Java 异常类层次结构图
  1. 异常:程序本身可以处理的,可以通过 catch 捕获的
    1. 受检查异常:必须处理
    2. 不受检查异常:可以不处理
  2. 错误:程序无法处理,不建议使用 catch 进行捕获,一般会导致线程终止。

RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):

  • NullPointerException(空指针错误)
  • IllegalArgumentException(参数错误比如方法入参类型错误)
  • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
  • ArrayIndexOutOfBoundsException(数组越界错误)
  • ClassCastException(类型转换错误)
  • ArithmeticException(算术错误)
  • SecurityException (安全错误比如权限不够)
  • UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)

Throwable

常用方法:

  1. String getMessage() 获取异常发生时的详细信息。
  2. String toString() 返回简要信息
  3. String getLocalizedMessage() 获取异常的本地化信息。
  4. printStackTrace() 控制台打印对象封装的异常信息。

try catch finally

try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。

catch块:用于处理 try 捕获到的异常。

finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行. 不要在这这里使用 return

1
2
3
4
5
6
7
8
try{
System.out.println("Try doing...");
throw new RuntimeException("RuntimeException");
} catch(Exception e){
System.out.println("catch : " + e.getMessage());
}finally {
System.out.println("Finally");
}

两种情况,finally不会被执行:

  1. 程序所在线程死亡
  2. 关闭CPU
  3. 总之是 JVM GG了

try with resources

有需要关闭的资源时建议使用

注意事项

  1. 不要将异常定义为静态变量,每次 new 一个丢出
  2. 抛出具体的异常,而不是父类
  3. 避免日志记录的膨胀

泛型

JDK 5 引入,本质上时参数化类型,也就是将所操作的数据类型指定为一个参数。(很像是C++中的函数模板)

有三种应用方式:

  1. 泛型类
  2. 泛型接口(实现泛型接口,可以指定类也可以不指定)
  3. 泛型方法

反射

详见另一个博客了。

注解

看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。

本质上继承 Annotation的特殊接口:

1
2
3
4
5
6
7
8
9
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

public interface Override extends Annotation{

}

解析方法:

  1. 编译时 @override
  2. 框架的一般都是运行时进行反射处理。

SPI

简而言之,就是 Java 提供接口,其他外部框架去实现,Java 愿意调用哪个动态选择。

序列化

持久化存储。

  • transient 用来修饰变量不进行序列化
  • static 属于类不属于对象,不会序列化

常见的序列化协议

JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。

像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。

IO

详见其他

语法糖

JVM 本身并不支持语法糖,是 javac 编译器支持语法糖,将内部转换为原生的JDK实现的。

详见其他