JDK、JRE、JVM
- JDK是Java开发工具包,包括JRE和编译工具。
- JRE是Java运行环境,包括Java虚拟机和核心类库。
- JVM是Java虚拟机,是Java跨平台的核心。
编译型语言和解释型语言
- 解释型语言如Python,JavaScript,运行时解释器逐行解释并执行源代码,跨平台性好但是效率低。
- 编译型语言如C,C++,Rust,运行前编译器将源代码转化为机器码,生成可执行文件,跨平台性差但效率高。
- Java是混合型语言,同时具有解释型和编译型语言的特点。
- Java会先将源代码编译为字节码文件(.class文件),再由虚拟机去解释执行字节码文件。
- 混合型语言性能介于编译型与解释型之间,Java通过字节码缓解了解释型语言效率低的问题,同时保留了解释型语言可移植的特点。
- 字节码可以被反编译,即把.class文件反编译为java源代码,常用的反编译工具如JD-GUI,反编译可以查看第三方库或框架的实现细节。
数据类型
- 基本数据类型:byte,char,short,int,long,float,double,boolean
- 字节数分别为1,2,2,4,8,4,8,1,1个字节有8比特位,按照位数计算表示范围,例如byte的范围是-128到127,即-(2^7)到2^7-1
- 不同数据类型运算时,容量小的类型自动转化为容量大的类型(byte,char,short先转化为int)
- 引用数据类型:类,接口,数组
- 基本数据类型直接存储数据,引用数据类型存储数据在内存中的地址。
- 基本数据类型不是对象,所以每个基本数据类型都对应一个包装类(对象),包装类就可以拥有对象的特性(赋为null,用于泛型,存储于堆)
- 自动装箱:自动将基本数据类型转化为对象,例如Integer num = 9。自动拆箱反之,如num--操作。
Static关键字
- static关键字用于修饰类的成员(变量,方法,代码块和内部类),表明这些成员属于类本身而非类的实例(对象)。
- 静态变量(属性)被所有实例共享,在类加载时分配内存。
- 静态方法通过类名直接调用,不能访问非静态成员,不能使用this和super,不能被重写(静态方法不属于类的实例,不参与多态性)。
- 静态代码块在类加载时执行,且只执行一次,可以用于初始化操作。
- 静态内部类不能访问外部类实例,只能访问外部类静态成员。
类中各成员的执行顺序
- 类被加载时,静态变量最先初始化,然后静态代码块按顺序执行。只执行一次。
- 类被实例化时,先执行代码块(执行初始化操作),然后执行构造方法。每次创建对象时执行。
- 如果存在继承关系,先执行父类的静态变量/代码块,再执行子类静态变量/代码块,然后创建子类实例时会去访问父类构造器,最后初始化子类代码块和构造方法。
访问权限
- public:同一个工程内可以访问。
- protected:同一个包内和所有子类(不同包)可以访问。
- default:同一个包内可以访问。
- private:只有类内部可以访问。
- 注意只有public和default可以修饰类。
面向对象
- 封装
- 把客观事物封装成抽象的类,隐藏了不需要对外暴露的细节,使用者只能通过类对外界暴露的方法来访问数据,提高了安全性。
- 继承
- 多个类中存在相同的属性或行为时,无需重复定义,而是将这些重复内容抽取到父类中,通过继承父类获得这些属性和行为,并且子类可以有自己的功能扩展,提高了代码的复用性。
- Java只支持单继承,且子类不能继承父类的私有属性和方法。
- 子类不能继承父类的构造器(每个类都有自己的构造器),但是可以调用父类的构造器,若子类的构造器没有显式调用父类构造器则编译器会自动插入super()调用父类的无参构造器。
- 多态
- 多态是指同一个方法调用可以根据对象的不同而表现出不同的行为,多态增加了代码复用性,灵活性,可扩展性。
- 多态有两种实现方式分别是编译时多态(方法重载,编译时确定调用)和运行时多态(方法重写,运行时确定调用),一般说的多态指运行时多态。
- 实现运行时多态需要3个必要条件:继承、重写和向上转型(父类引用指向子类对象,父类为编译时类型,子类为运行时类型)。
多态底层原理
多态的底层原理依赖于JVM的方法调用机制和动态绑定机制。
- JVM为每个类维护了一个方法表,其中存储了该类所有方法的入口地址。
- 子类方法表包含父类方法,若子类重写父类方法,子类方法表中对应条目指向子类方法实现。
- 每个对象有对象头,其中包含指向方法表的指针,通过父类引用调用方法时,JVM根据对象实际类型(子类)找到对应方法表,查找方法入口地址并调用(这就是动态绑定,通过JVM的invokevirtual指令实现)。
重载和重写
- 重载是同一个类中存在多个同名方法,只要参数列表(个数或类型)不同即可,与返回值类型无关,根据不同参数输入调用对应的方法,常用于构造器重载
- 重写是子类对从父类中继承的方法的覆盖,方法名、参数列表必须相等。
- 子类不能重写父类中的私有方法/构造器(继承不到),静态方法(属于类,不参与多态),final方法。
- 重写的方法的返回值和异常覆盖范围比父类小,访问权限范围比父类大。
接口和抽象类
接口和抽象类中主要是抽象方法,抽象方法只有声明没有实现(即一个模板),所以接口和抽象类需要被实现或继承从而实现抽象方法,所以abstract与final,static,private不兼容(无法继承)。
- 接口
- 接口是抽象方法和常量值定义的集合,变量默认由public static final修饰,方法默认由public abstract修饰。
- 类可以实现多个接口。
- 接口是对类局部行为进行抽象,作为一个标准。
- 接口中不能有静态代码块和静态方法。
- 抽象类
- 抽象类中可以包含抽象方法和非抽象方法,成员变量和方法可以是各种类型的。
- 类只能继承一个父类。
- 抽象类是对类整体进行抽象,是一种模板式设计。
- 抽象类中可以有静态代码块和静态方法。
值传递和引用传递
- 值传递指调用函数时将实际参数复制一份到函数中,若函数修改此参数,不会对实际参数造成影响。
- 引用传递指调用函数时将实际参数的引用传递到函数中,函数对此参数的修改会影响到实际参数。
- Java只有值传递,但是在将对象作为值传递时,可以修改对象的引用来达到引用传递的效果,这是因为虽然拷贝了对象,但是没有拷贝对象的引用,即不是深拷贝。
// 值传递,输出0
public static void main(String[] args) {
int val = 0;
add(val);
System.out.println(val);
}
public static int add(int i){
return i+1;
}
// 修改对象的引用,实现引用传递,输出5 10
public static void main(String[] args) {
MyObject obj = new MyObject(5);
System.out.println(obj.value);
changeObject(obj);
System.out.println(obj.value);
}
public static void changeObject(MyObject obj) {
obj.value = 10;
}
static class MyObject {
int value;
public MyObject(int value) {
this.value = value;
}
}
// 单纯修改对象不会改变,输出5 5
public static void main(String[] args) {
MyObject obj1 = new MyObject(5);
MyObject obj2 = new MyObject(10);
System.out.println(obj1.value);
changeObject(obj1, obj2);
System.out.println(obj1.value);
}
public static void changeObject(MyObject obj1, MyObject obj2) {
obj1 = obj2;
}
hashcode和equals
- equals的底层实现为==(未重写),比较基本数据类型的时候比较的是值是否相等,比较对象时比较的是地址是否相等。
- hashcode是对某个对象使用hash算法计算出来的,可用于快速判断对象是否相同。
- HashSet插入时先计算hashcode,如果映射到同一位置且hashcode相同,再用equals比较,若相同则插入失败,若不同则重新散列到其他地方。重写equals必须重写hashcode方法,否则会出现hashcode值不同,而equals判断返回true的问题,即equals的标准与hashcode的标准不一致。如果hashset把两个不同hashcode但是相同equals返回值的元素都加入容器中,就会出问题。
String类
- String类被声明为final,底层是char数组。
- 不可变即当要修改String字符串时必须重新指定内存区域,不能修改原内存区域,声明为不可变的优点:
- 线程安全
- 方便实现字符串常量池(字面量都会放在常量池中,多个String指向同一个字面量,如果String可变就会改变这个字面量,就会影响其他String对象)
- 便于作为kv型容器的key(Map,redis,因为可以缓存hashcode)
- String不可变,线程安全;StringBuffer可变,线程安全(加同步锁),效率低于StringBuilder;StringBuilder可变,线程不安全。三者底层都是char数组。
字符串常量池
- 字符串常量池在堆内存中,保存的是字符串对象的引用,优化内存使用,避免重复创建相同的字符串对象。
- 使用字面量创建时,如String s = "aaa",会先在常量池中找aaa,若找到将其引用赋值给变量s;若没有则创建对象aaa,放入常量池并赋值给s。
- 使用new关键字创建时,如String s = new String("aaa"),会在堆中创建对象s,不会放入常量池中。
- intern()方法:若字符串(的引用)在池中直接返回,若不在则将字符串的引用放入池中。
- 两个字符串拼接,若都是字面量则拼接后的结果在常量池中,若有变量(对象)参与则会放到堆空间中。
反射
- 反射是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法,使用步骤如下
获取Class对象的三种方法
// Class.forName("完整类名")
Class<?> clazz = Class.forName("java.lang.String");
// 类名.class
Class<?> clazz = String.class;
// 对象名.getClass()
String str = "hello";
Class<?> clazz = str.getClass();
获取类字段
// 获取所有字段
Field[] fields = clazz.getDeclaredFields();
// 获取指定字段
Field field = clazz.getDeclaredField("fieldName");
获取类方法
// 获取指定方法
Method method = clazz.getDeclaredMethod("methodName", parameterTypes);
// 调用方法
method.setAccessible(true); // 设置可访问
Object result = method.invoke(object, args); // 调用方法
获取类构造器
// 获取指定构造器
Constructor<?> constructor = clazz.getDeclaredConstructor(parameterTypes);
// 创建对象
constructor.setAccessible(true); // 设置可访问
Object instance = constructor.newInstance(args); // 创建对象
-
反射的应用
- 动态加载类:在运行时根据配置或用户输入加载类,例如JDBC数据库连接时加载驱动类。
// 使用反射加载MySQL驱动类(注意JDBC4.0以上版本无需显示加载驱动类) Class.forName("com.mysql.cj.jdbc.Driver"); // 获取数据库连接 Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "user", "password");- ORM框架:将数据库查询到的结果映射到实体对象上。
ResultSet resultSet = statement.executeQuery("SELECT id FROM users"); while (resultSet.next()) { User user = new User(); // 使用反射设置属性 Field idField = User.class.getDeclaredField("id"); idField.setAccessible(true); idField.set(user, resultSet.getInt("id")); }- Spring框架:依赖注入,面向切面编程,Bean的初始化与销毁,动态代理
// 依赖注入,Spring通过反射创建MyService和MyRepository实例,并将后者注入到前者中 @Service public class MyService { @Autowired private MyRepository myRepository; } // 面向切面编程主要依赖动态代理,动态代理使用反射创建代理对象 // Spring使用反射调用Bean的初始化方法和销毁方法 -
反射的原理
- JVM类加载时会在堆内存中产生该类的一个Class对象(每个类唯一),这个Class对象包含了类的完整结构信息,反射就是去获取这个Class对象。
-
反射的缺点
- 反射可以获取类的私有属性和方法,会破坏封装性。
- 反射性能较低,因为它需要解析字节码,可以通过关闭JDK安全检查来提升反射速度。
- 代码可读性差。
泛型
- 泛型就是将类型参数化,在编译时确定具体的类型,可以用在类,接口和方法的创建中。
- 使用泛型可以解决元素存储的安全性问题,避免强制类型转换问题,且泛型的校验在编译器中完成不需要依靠JVM。
- 泛型只存在于编译阶段,编译器校验完成后将类型擦除为Object(除了使用extends,会把类型擦除为其父类)
序列化
- 序列化指把对象转化为字节流(反之就是反序列化),序列化是为了在网络上传输或在文件中保存对象。
- 要使类的对象可序列化,类需要实现Serializable接口(一个标记接口),并且类中所有字段都要是可序列化的,若某些字段不需要序列化可使用transient关键字标记。
- 序列化时需要指定serialVersionUID,用来确保序列化和反序列化的类版本一致,若没有显示指定JVM会自动生成,但是后续类结构更改可能导致反序列化失败,所以开发中必须显示指定序列号。
- 实现Externalizable接口可以自定义序列化过程,需要实现writeExternal()和readExternal()方法。
- Externalizable需要重写读写方法,由程序员决定存储哪些信息,Serializable无需重写,由系统自动存储必要信息
- 序列化是针对对象的,故静态变量不会被序列化。
// 序列化
public class SerializationExample {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
try (FileOutputStream fileOut = new FileOutputStream("person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(person); // 序列化对象
System.out.println("Serialized data is saved in person.ser");
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 反序列化
public class DeserializationExample {
public static void main(String[] args) {
try (FileInputStream fileIn = new FileInputStream("person.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)) {
Person person = (Person) in.readObject(); // 反序列化对象
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 常用序列化方法包括:
- Java原生的序列化(比较耗时)。
- Json序列化,使用Json字符串格式,可读性较强但性能较差。
- Protobuf序列化,使用二进制格式,没有可读性但体积更小,效率更高。
异常
- Error和Exception
- Error是程序无法处理的错误,例如JVM内部错误,堆栈溢出,内存不足
- Exception是程序本身可以处理的异常,又分为运行时异常和编译时异常
- 运行时异常包括类型转化异常,数据下标越界,空指针,算数异常
- 编译时异常包括IO异常,SQL异常,文件未找到异常(要求调用者必须处理)
- 捕获异常:try-catch-finally
- 尝试执行try中的代码,若有异常则执行catch中的代码,最后一定会执行finally中的操作(即使catch中return了)。
- 三部分中catch和finally可以省略,只有try时可以处理运行时异常(捕获后丢弃),加了catch可以处理编译时异常(捕获后进一步处理),编译时异常捕获到后一定要处理。
- 抛出异常:throws和throw
- throws用在方法声明上表示可能抛出的异常,将异常抛出给调用者处理并终止运行,可以抛出多个异常。(注意子类不能抛出比父类还大的异常,若父类中没有throws异常,则子类只能用try-catch方法处理异常)
- throw用在方法内部,手动声明一个异常并抛出,终止运行。
public void checkAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("Age must be at least 18");
}
}
public void readFile(String filePath) throws IOException {
...
}
- JVM是如何处理异常的
- 代码出现异常时,JVM会先创建一个异常对象(new Exception()),包含异常类型、消息和堆栈跟踪信息,然后JVM检查当前方法是否有try-catch块处理异常,若无则抛给调用者方法,沿着调用栈向上传播,直到main方法,若还是未能捕获异常则终止程序并打印堆栈跟踪信息。
IO流
- IO流可分为
- 输入流,输出流(inputStream, outputStream)
- 节点流,处理流(FileReader, BufferedReader):节点流只负责基本的读写文件功能,而处理流可以叠加在节点流上,为其加上指定的功能。
- 字节流,字符流(inputStream,Reader):字节流适合所有类型文件的数据传输,字符流只适合纯文本数据,字节流和字符流可以通过InputStreamReader进行转化。
- IO的两个阶段(以读数据为例)
- 阶段1(数据准备):将数据从网卡处理好后放到socket缓冲区。
- 阶段2(数据拷贝):将数据从内核态的socket缓冲区拷贝到用户态的应用程序缓冲区。
- 同步与异步
- 同步指程序先等待IO操作完成后才能继续执行后续代码。
- 异步指程序不需要等待IO操作完成,可以继续执行后续代码。
- 同步和异步强调代码执行的先后关系。
- 阻塞与非阻塞
- 阻塞指程序需要等待IO时,会一直等待直到数据准备好。
- 非阻塞指程序执行IO操作时,若数据未准备好会立刻返回,不等待。
- 阻塞与非阻塞强调需要等待时不同的行为。
- BIO,NIO,AIO
- BIO:同步并阻塞,程序等待IO操作完成,期间线程被挂起。
- NIO:同步非阻塞,程序等待IO操作完成,期间线程可以做其他事。
- 轮询等待IO:线程询问OS内核数据是否准备好,没准备好就返回,过一段时间继续询问,轮询过程不做其他事
- IO多路复用:使用了事件机制,只有当数据准备好时内核才会去通知线程,期间线程可以去处理其他连接(Netty和Redis使用到了NIO)
- AIO:异步非阻塞,程序不需要等待IO操作完成,期间线程可以做其他事。
- Java的IO模块使用了哪些设计模式
- 适配器模式:把一个类的接口转换成另一种接口,使原本因接口不匹配而无法一起工作的两个类能够一起工作。例如字节流和字符流的转换
- 装饰器模式:动态地往一个类中添加新的行为,比生成子类更加灵活。例如节点流上可以加处理流。
Java8新特性
- 函数式接口与Lambda表达式:只包含一个抽象方法的接口为函数式接口,Lambda表达式本质是函数式接口的实例,它可以简化匿名内部类的写法。
// 函数式接口
@FunctionalInterface
interface MyFunction {
void apply(String s);
}
// Lambda表达式
MyFunction func = s -> System.out.println(s);
func.apply("Hello");
- Stream API:支持函数式操作,使处理集合数据变得简单高效,包括map,filter,reduce,collect等操作,使用parallerl()方法可以将其转化为并行操作,但要注意线程安全问题。
List<String> list = Arrays.asList("a", "b", "c");
list.stream()
.filter(s -> s.startsWith("a"))
.forEach(System.out::println);
- 方法引用:简化Lambda表达式,直接引用已有方法。
System.out::println
- Optional类:用于封装可能为空的对象,避免了空指针的风险。
// 创建Optional对象,若值为null,抛出NullPointerException
Optional<String> optional = Optional.of("Hello");
// 创建可能包含null值的Optional对象
Optional<String> optional = Optional.ofNullable(null);
// 判断是否存在
if (optional.isPresent()) {
...
}