从基础到高级!JavaSE核心知识点全整理(面试/复习必备)
JavaSE 八股文
目录
一、Java概述
1. 什么是Java?
Java是一种跨平台、面向对象的编程语言,由Sun Microsystems(现在是Oracle公司)于1995年推出。它的设计理念是"一次编写,到处运行"(Write Once, Run Anywhere,WORA),意味着Java程序可以在任何支持Java虚拟机(JVM)的平台上运行,而不需要重新编译。
2. Java的主要特点是什么?
- 简单性:Java语法基于C++,但去除了指针、多继承等复杂特性,更易于学习和使用。
- 面向对象:Java是一种纯面向对象的编程语言,支持封装、继承和多态等特性。
- 跨平台性:通过Java虚拟机(JVM)实现跨平台运行,只要安装了相应平台的JVM,Java程序就可以运行。
- 安全性:Java提供了安全管理器(Security Manager)和字节码验证器(Bytecode Verifier)等安全机制,防止恶意代码的执行。
- 健壮性:Java具有强类型检查、异常处理机制和自动内存管理(垃圾回收)等特性,减少了程序错误和内存泄漏。
- 多线程:Java内置了多线程支持,使程序可以同时执行多个任务,提高了程序的并发性能。
- 高性能:虽然Java程序需要通过JVM解释执行,但现代JVM采用了即时编译(JIT)等技术,使Java程序的性能接近本地代码。
3. JVM、JDK、JRE有什么区别?
JVM(Java Virtual Machine)是Java虚拟机,它是Java跨平台的核心。JVM负责将Java字节码解释或编译为特定平台的机器码并执行。
JRE(Java Runtime Environment)是Java运行时环境,它包含了JVM和Java类库,以及其他运行Java程序所需的组件。用户只需要安装JRE就可以运行Java程序。
JDK(Java Development Kit)是Java开发工具包,它包含了JRE、Java编译器(javac)、调试器(jdb)等开发工具。开发者需要安装JDK来开发Java程序。
简单来说:JDK包含JRE,JRE包含JVM。
4. Oracle JDK 和 OpenJDK 的区别?
Oracle JDK是Oracle公司提供的Java开发工具包,它包含了Oracle的商业特性和支持服务。Oracle JDK在2019年开始对商用用户收费,而个人用户可以免费使用。
OpenJDK是Java的开源实现,由Oracle和其他社区成员共同维护。OpenJDK包含了Java SE平台的核心功能,与Oracle JDK在功能上几乎相同,但不包含Oracle的商业特性和支持服务。
5. 为什么说 Java 是跨平台的?
Java之所以能够跨平台,是因为它使用了Java虚拟机(JVM)。Java程序在编译时会被编译成一种中间格式,称为字节码(bytecode),而不是直接编译成特定平台的机器码。然后,不同平台上的JVM会将字节码解释或编译成特定平台的机器码并执行。
这样,同一份Java字节码可以在任何安装了JVM的平台上运行,而不需要重新编译,实现了"一次编写,到处运行"的目标。
二、基础语法
1. Java 有哪些数据类型?
Java的数据类型分为两大类:基本数据类型和引用数据类型。
基本数据类型:也称为内置类型,它们的值直接存储在内存中。Java有8种基本数据类型:
- 整型:byte(1字节)、short(2字节)、int(4字节)、long(8字节)
- 浮点型:float(4字节)、double(8字节)
- 字符型:char(2字节)
- 布尔型:boolean(1字节)
引用数据类型:引用数据类型指向对象的地址,而不是对象本身。主要包括:
- 类(Class):如String、Integer等
- 接口(Interface):如List、Map等
- 数组(Array):如int[]、String[]等
- 枚举(Enum)
2. 自动拆装箱了解吗?原理是什么?
自动拆装箱(Autoboxing and Unboxing)是Java 5引入的特性,它允许基本数据类型和对应的包装类之间自动转换。
自动装箱:将基本数据类型自动转换为对应的包装类对象。例如:
Integer i =10;// 自动装箱,相当于 Integer i = Integer.valueOf(10);自动拆箱:将包装类对象自动转换为对应的基本数据类型。例如:
int j = i;// 自动拆箱,相当于 int j = i.intValue();原理:
- 自动装箱通过调用包装类的valueOf()方法实现
- 自动拆箱通过调用包装类的xxxValue()方法实现(如intValue()、doubleValue()等)
3. Java中的方法参数传递机制是什么?
Java中只有值传递(Pass by Value),没有引用传递。
- 对于基本数据类型,传递的是值的副本,修改参数不会影响原始值。
- 对于引用数据类型,传递的是对象引用的副本,而不是对象本身。修改参数指向的对象内容会影响原始对象,但修改参数的引用(如重新赋值)不会影响原始引用。
4. 重载和重写的区别?
重载(Overloading):
- 发生在同一个类中
- 方法名相同,但参数列表不同(参数类型、个数或顺序不同)
- 与返回值类型无关
- 与访问修饰符无关
重写(Overriding):
- 发生在父类和子类之间
- 方法名相同,参数列表相同
- 返回值类型相同或为父类返回值类型的子类(协变返回类型)
- 访问修饰符不能比父类更严格
- 不能抛出比父类更多的检查异常
5. Java 中的关键字 final、static、this、super 各有什么作用?
final:
- 修饰类:表示该类不能被继承
- 修饰方法:表示该方法不能被重写
- 修饰变量:表示该变量是常量,一旦赋值就不能修改
static:
- 修饰变量:表示该变量是静态变量,属于类,不属于实例
- 修饰方法:表示该方法是静态方法,属于类,可以通过类名直接调用
- 修饰代码块:表示静态代码块,在类加载时执行,只执行一次
- 修饰内部类:表示静态内部类,与外部类实例无关
this:
- 引用当前对象的实例
- 区分成员变量和局部变量
- 调用当前类的构造方法
super:
- 引用父类的实例
- 访问父类的成员变量和方法
- 调用父类的构造方法
6. 构造器的作用与注意事项
构造器的作用:
- 创建对象时初始化对象的状态
- 可以设置对象的初始值
注意事项:
- 构造器的名称必须与类名相同
- 构造器没有返回值类型,也不能写void
- 一个类可以有多个构造器(重载)
- 如果没有显式定义构造器,编译器会自动生成一个无参构造器
- 如果显式定义了构造器,编译器不会自动生成无参构造器
- 构造器可以调用其他构造器(使用this()或super()),但必须放在第一行
7. 成员变量与局部变量的区别
- 定义位置:成员变量定义在类中,方法外;局部变量定义在方法内或方法参数中
- 作用域:成员变量的作用域是整个类;局部变量的作用域是定义它的方法或代码块
- 初始值:成员变量有默认初始值;局部变量没有默认初始值,必须先赋值后使用
- 内存位置:成员变量存储在堆内存中;局部变量存储在栈内存中
- 生命周期:成员变量的生命周期与对象一致;局部变量的生命周期与方法或代码块的执行一致
8. static关键字的作用与用法
static关键字可以用来修饰变量、方法、代码块和内部类:
- 静态变量:也称为类变量,属于类,不属于实例。所有实例共享同一个静态变量的值。
- 静态方法:也称为类方法,属于类,可以通过类名直接调用。静态方法不能访问非静态成员变量和非静态方法。
- 静态代码块:在类加载时执行,只执行一次。用于初始化静态变量或执行一些需要在类加载时完成的操作。
- 静态内部类:与外部类绑定,但独立于外部类实例。静态内部类可以访问外部类的静态成员,但不能访问外部类的非静态成员。
9. final关键字的作用与用法
final关键字可以用来修饰类、方法和变量:
- final类:不能被继承。例如:String、Integer等
- final方法:不能被重写。可以防止子类修改父类的关键方法
- final变量:一旦赋值就不能修改。如果是基本数据类型,其值不能修改;如果是引用数据类型,其引用不能修改,但引用指向的对象内容可以修改
10. 什么是字节码?采用字节码的好处是什么?
什么是字节码?
所谓的字节码,就是Java程序经过编译后产生的.class文件。
Java程序从源代码到运行需要经过三步:
• 编译:将源代码文件.java编译成JVM可以识别的字节码文件.class
• 解释:JVM执行字节码文件,将字节码翻译成操作系统能识别的机器码
• 执行:操作系统执行二进制的机器码
采用字节码的好处是什么?
- 跨平台性:Java字节码是平台无关的,同一个字节码文件可以在不同的操作系统上运行,实现了"一次编写,到处运行"。
- 安全性:JVM在执行字节码前会进行字节码验证,确保字节码是安全的,不会对系统造成危害。
- 高效性:虽然Java是解释执行的,但现代JVM采用了JIT(即时编译)技术,将热点字节码编译成机器码,提高了执行效率。
11. 为什么有人说Java是“编译与解释并存”的语言?
编译型语言是指编译器针对特定的操作系统,将源代码一次性翻译成可被该平台执行的机器码。
解释型语言是指解释器对源代码进行逐行解释,解释成特定平台的机器码并执行。
Java程序的执行过程结合了编译和解释两种方式:
- 编译阶段:Java源代码通过javac编译器编译成字节码文件(.class),这是一个编译过程。
- 解释执行阶段:JVM加载字节码文件,然后通过解释器将字节码逐行解释成机器码并执行,这是一个解释过程。
- JIT编译:现代JVM还会将频繁执行的热点字节码编译成机器码并缓存,提高执行效率,这也是一个编译过程。
因此,Java被称为"编译与解释并存"的语言。
12. 自动类型转换、强制类型转换了解吗?
当把一个范围较小的数值或变量赋给另外一个范围较大的变量时,会进行自动类型转换;反之,需要强制转换。
自动类型转换方向: char -> byte -> short -> int -> long -> float -> double
这就好像,小杯里的水倒进大杯没问题,但大杯的水倒进小杯就可能会溢出。
①、float f=3.4,对吗?
不正确。3.4 默认是双精度,将双精度赋值给浮点型属于下转型(down-casting,也称窄化)会造成精度丢失,因此需要强制类型转换 float f =(float)3.4; 或者写成 float f =3.4F
②、short s1 = 1; s1 = s1 + 1; 对吗?short s1 = 1; s1 += 1; 对吗?
short s1 = 1; s1 = s1 + 1; 会编译出错,由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。
而 short s1 = 1; s1 += 1; 可以正确编译,因为 s1+= 1; 相当于 s1 = (short(s1 + 1); 其中有隐含的强制类型转换。
13. &和&&有什么区别?
& 是 逻辑与。
&& 是短路与运算。逻辑与跟短路与的差别是非常大的,虽然二者都要求运算符左右两端的布尔值都是 true,整个表达式的值才是 true。
&& 之所以称为短路运算是因为,如果 && 左边的表达式的值是 false,右边的表达式会直接短路掉,不会进行运算。
例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为 username != null && !username.equals(“”),二者的顺序不能交换,更不能用 & 运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,会抛出 NullPointerException 异常。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是类似。
14. switch 语句能否用在 byte/long/String 类型上?
Java 5 以前 switch(expr) 中,expr 只能是 byte、short、char、int。
从 Java 5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型。
从 Java 7 开始,expr 还可以是字符串,但是长整型在目前所有的版本中都是不可以的。
15. break,continue,return 的区别及作用?
• break 跳出整个循环,不再执行循环(结束当前的循环体)
• continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
• return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
16. 用效率最高的方法计算 2 乘以 8?
2 << 3。位运算,数字的二进制位左移三位相当于乘以 2 的三次方。
17. 说说自增自减运算?
在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(–)。
++和–运算符可以放在变量之前,也可以放在变量之后。
当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。
例如,当 b = ++a 时,先自增(自己增加 1),再赋值(赋值给 b);当 b = a++ 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。
用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。
18. float 是怎么表示小数的?
float 类型的小数在计算机中是通过 IEEE 754 标准的单精度浮点数格式来表示的。
V = (-1)^S imes M imes 2^E
- S: 符号位,0 代表正数,1 代表负数;
- M: 尾数部分,用于表示数值的精度;比如说 ( 1.25 ∗ 2 2 ) (1.25 * 2^2) (1.25∗22);1.25 就是尾数;
- R: 基数,十进制中的基数是 10,二进制中的基数是 2;
E:指数部分,例如 10 − 1 10^{-1} 10−1 中的 -1 就是指数。
这种表示方法可以将非常大或非常小的数值用有限的位数表示出来,但这也意味着可能会有精度上的损失。
单精度浮点数占用 4 字节(32 位),这 32 位被分为三个部分:符号位、指数部分和尾数部分。
- 符号位(Sign bit):1 位
- 指数部分(Exponent):10 位
- 尾数部分(Mantissa,或 Fraction):21 位
19. 讲一下数据准确性高是怎么保证的?
在金融计算中,保证数据准确性有两种方案,一种使用 BigDecimal ,一种将浮点数转换为整数 int 进行计算。
肯定不能使用 float 和 double 类型,它们无法避免浮点数运算中常见的精度问题,因为这些数据类型采用二进制浮点数来表示,无法准确地表示,例如 0.1。
使用 BigDecimal 保证数据准确性:
BigDecimal num1 =newBigDecimal("0.1");BigDecimal num2 =newBigDecimal("0.2");BigDecimal sum = num1.add(num2);System.out.println("Sum of 0.1 and 0.2 using BigDecimal: "+ sum);// 输出 0.3, 精确计算转换为整数计算保证数据准确性:
在处理小额支付或计算时,通过转换为较小的货币单位(如分),这样不仅提高了运算速度,还保证了计算的准确性。
int priceInCents =199;// 商品价格199分int quantity =3;int totalInCents = priceInCents * quantity;// 计算总价System.out.println("Total price in cents: "+ totalInCents);// 输出597分三、面向对象(OOP)
1. 类与对象
如何创建一个对象?有哪些方式?
Java 有四种创建对象的方式:
序列化机制创建:通过序列化将对象转换为字节流,再通过反序列化从字节流中恢复对象。需要实现 Serializable 接口。
Person person =newPerson();ObjectOutputStream oos =newObjectOutputStream(newFileOutputStream("person.txt")); oos.writeObject(person);ObjectInputStream ois =newObjectInputStream(newFileInputStream("person.txt"));Person person2 =(Person) ois.readObject();clone 拷贝创建:通过 clone 方法创建对象,需要实现 Cloneable 接口并重写 clone 方法。
Person person =newPerson();Person person2 =(Person) person.clone();反射机制创建:反射机制允许在运行时创建对象,并且可以访问类的私有成员,在框架和工具类中比较常见。
Class clazz =Class.forName("Person");Person person =(Person) clazz.newInstance();new 关键字创建:这是最常见和直接的方式,通过调用类的构造方法来创建对象。
Person person =newPerson();new 关键字创建对象的过程是怎样的?
使用 new 关键字创建对象的过程如下:
- 类加载检查:JVM 遇到
new指令时,首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,必须先执行相应的类加载过程。 - 内存分配:在类加载检查通过后,接下来 JVM 为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。内存分配方式有两种:
- 指针碰撞 (Bump the Pointer):如果 Java 堆中内存是规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
- 空闲列表 (Free List):如果 Java 堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,那么虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
- 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用了 TLAB,这一工作过程也可以提前至 TLAB 分配时进行。这步保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 设置对象头:接下来,JVM 会设置对象的对象头(Object Header)信息,包括这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等。
- 执行 init 方法:执行
new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
使用 Class.newInstance() 和 Constructor.newInstance() 有什么区别?(注意:Class.newInstance() 已被弃用)
Class.newInstance():只能调用类的无参公共构造器。如果类没有无参构造器或构造器不是 public,会抛出InstantiationException或IllegalAccessException。此方法已被弃用。Constructor.newInstance():可以调用任何构造器(包括私有构造器),只要通过getDeclaredConstructor()获取到对应的Constructor对象。它提供了更大的灵活性。
对象创建过程中的内存分配流程(堆、栈、对象头等)?
- 栈 (Stack):存储局部变量(包括对象的引用变量)和方法调用信息。当一个方法被调用时,会在虚拟机栈中创建一个栈帧,方法执行完毕后,栈帧被弹出。
- 堆 (Heap):存储所有通过
new关键字创建的对象实例以及数组。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。对象的实例数据(成员变量)都存储在堆上。 - 方法区 (Method Area):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK 8 之后,这部分被元空间(Metaspace)取代。
- 对象头 (Object Header):每个对象在堆内存中都包含一个对象头,它包含两部分信息:
- Mark Word:存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
- 类型指针 (Klass Pointer):指向它的类元数据的指针,JVM 通过这个指针来确定这个对象是哪个类的实例。
- 程序计数器 (Program Counter Register):记录当前线程所执行的字节码行号。
补充:JVM 内存模型与对象创建
- 栈:线程私有,存储局部变量和方法调用。
new指令执行时,对象的引用(如person)存储在栈上。 - 堆:所有线程共享,存储对象实例。
new指令分配的内存和创建的对象实例都位于堆中。 - 方法区:存储类的元数据、常量池、静态变量等。
new指令的参数(类的符号引用)在常量池中。 - 程序计数器:记录当前线程执行的字节码指令地址,每个线程独立。
2. 面向对象三大特性
封装:如何实现?访问修饰符有哪些?作用范围?
- 实现:封装是将对象的属性和行为(数据和代码)结合成一个整体,并通过访问修饰符隐藏对象的内部细节,仅对外提供公共方法(getter/setter)来访问和修改数据。
- 访问修饰符:
private:仅在本类内部可见。default(包私有):在本类和同一个包中的其他类可见。protected:在本类、同一个包中的其他类以及所有子类(即使子类在不同包中)可见。public:在所有类中可见。
继承:extends 和 super 的使用?方法重写(@Override)规则?
extends:关键字用于一个类继承另一个类。子类会继承父类的非私有成员(属性和方法)。super:关键字用于在子类中调用父类的构造器、属性或方法。super()必须是子类构造器中的第一条语句。- 方法重写规则:
- 子类方法的返回类型、方法名、参数列表必须与父类方法完全相同。
- 子类方法的访问权限不能比父类方法更严格(例如,父类是
protected,子类不能是private,但可以是public)。 - 子类方法不能抛出比父类方法更多或更宽泛的检查异常。
- 静态方法不能被重写,只能被隐藏。
多态:实现方式?向上转型和向下转型?动态绑定机制?
- 实现方式:多态是同一个行为具有多个不同表现形式的能力。在 Java 中,多态主要通过继承和方法重写来实现。父类的引用变量可以指向子类的对象。
- 向上转型 (Upcasting):将子类对象赋值给父类引用变量。这是自动的、安全的。例如:
Animal a = new Dog();。 - 向下转型 (Downcasting):将父类引用变量强制转换为子类类型。这是不安全的,需要使用
instanceof检查类型,否则会抛出ClassCastException。例如:Dog d = (Dog) a;。 - 动态绑定 (Dynamic Binding):在运行时,JVM 根据对象的实际类型(而非引用类型)来决定调用哪个重写的方法。这是实现多态的关键机制。
3. 构造器
构造器是否可以重写?是否可以重载?
- 构造器不能重写,因为子类和父类的构造器名称不同,无法满足重写条件。
- 构造器可以重载,一个类可以有多个构造器,只要它们的参数列表不同(参数类型、数量或顺序不同)。
默认构造器、无参构造器、私有构造器的作用?
- 默认构造器 (Default Constructor):当一个类没有显式定义任何构造器时,编译器会自动为该类生成一个无参的公共构造器,称为默认构造器。
- 无参构造器 (No-arg Constructor):一个不带任何参数的构造器。如果类中定义了有参构造器,编译器将不再生成默认构造器,此时如果需要无参构造器,必须显式定义。
- 私有构造器 (Private Constructor):将构造器声明为
private,可以防止外部类直接通过new创建该类的实例。常用于单例模式或工具类。
构造代码块、静态代码块的执行顺序?
执行顺序为:静态代码块 -> 构造代码块 -> 构造器。
- 静态代码块 (Static Block):使用
static {}定义,用于初始化类的静态成员。它在类被加载时执行,且只执行一次。 - 构造代码块 (Instance Block):使用
{}定义,用于初始化对象的实例成员。它在每次创建对象时执行,且在构造器之前执行。
构造器的典型应用场景(联想补充)
1. Spring 中的构造器注入
Spring 框架推荐使用构造器注入来实现依赖注入(DI),因为它能保证依赖不可变且不为 null。
@ServicepublicclassUserService{privatefinalUserMapper userMapper;privatefinalOrderService orderService;publicUserService(UserMapper userMapper,OrderService orderService){this.userMapper = userMapper;this.orderService = orderService;}}- 优势:
- 依赖强制注入,避免 null 指针。
- 字段可声明为
final,保证不可变性。 - 易于单元测试,可直接
new注入模拟对象。
- 与 Java 构造器关系:Spring 容器通过反射调用该构造器,传入已创建的 Bean 实例,完成依赖装配。
2. 私有构造器的典型用途
将构造器设为 private 是控制实例化的重要手段。
(1) 工具类防止实例化
publicclassMathUtils{privateMathUtils(){thrownewAssertionError("工具类不能实例化");}publicstaticintadd(int a,int b){return a + b;}}- 防止通过
new MathUtils()创建实例。 - 构造器私有,且抛出异常,进一步防止反射攻击。
(2) 单例模式(懒汉式线程安全)
publicclassSingleton{privatestaticSingleton instance;privateSingleton(){}publicstaticsynchronizedSingletongetInstance(){if(instance ==null){ instance =newSingleton();}return instance;}}- 私有构造器防止外部
new。 - 通过静态方法控制唯一实例的创建。
(3) 防止继承
publicclassUtilityClass{privateUtilityClass(){}publicstaticvoiddoSomething(){...}}- 子类无法调用
super(),因此不能继承此类。
(4) Java 标准库示例
java.lang.Math:所有方法静态,无需实例。java.util.Collections:集合工具类。LocalDateTime.of(...):静态工厂方法创建实例。
注意事项:
- 私有构造器仍可在类内部调用,用于单例或静态工厂。
- 防反射攻击:可在构造器中检查实例状态并抛出异常。
- 反序列化破坏单例:可通过
readResolve()方法防止。
对比总结
| 场景 | 构造器访问 | 是否允许外部 new | 是否依赖 Java 构造器 | 推荐程度 |
|---|---|---|---|---|
| Spring 构造器注入 | public | 是(由容器调用) | 是 | 高 |
| 工具类 / 单例 | private | 否 | 是 | 高 |
4. 对象复制
浅拷贝 vs 深拷贝的区别?
- 浅拷贝 (Shallow Copy):创建一个新对象,但新对象的属性(字段)和原对象的属性完全相同。如果属性是基本数据类型,拷贝的是值;如果属性是引用类型,拷贝的是引用地址,因此新旧对象共享同一个引用对象。
- 深拷贝 (Deep Copy):创建一个新对象,并且会递归地复制所有的引用对象,确保新对象和原对象完全独立。新对象与原对象的任何更改都不会相互影响。
如何实现浅拷贝?(实现 Cloneable 接口,重写 clone() 方法)
实现浅拷贝需要:
- 实现
Cloneable接口(一个标记接口)。 - 重写
Object类的clone()方法,并将其访问权限改为public。
classPersonimplementsCloneable{String name;int age;Address address;@OverrideprotectedObjectclone()throwsCloneNotSupportedException{returnsuper.clone();}}如何实现深拷贝?(序列化、手动复制、第三方工具如 Apache Commons BeanUtils)
实现深拷贝的方法有:
- 手动复制:在重写
clone()方法时,对所有引用类型的字段也进行clone()操作。 - 序列化与反序列化:将对象序列化为字节流,然后再反序列化为新对象。新对象与原对象完全独立。
- 第三方工具:使用如 Apache Commons BeanUtils 等工具库。
clone() 方法属于浅拷贝吗?为什么?Object 类的 clone() 方法是浅拷贝。因为它只是简单地复制对象的内存,对于引用类型的字段,它复制的是引用地址,而不是创建一个新的引用对象。
5. this 和 super
this 可以用在哪些场景?调用构造器、成员变量、成员方法?
- 调用成员变量:当方法的局部变量与成员变量同名时,使用
this.变量名来指代成员变量。 - 调用成员方法:在类的内部调用本类的其他方法,可以使用
this.方法名(),通常可以省略this。 - 调用构造器:在构造器中调用本类的其他构造器,使用
this(参数),且必须是构造器中的第一条语句。
super 调用父类构造器时必须是第一行吗?
是的,super() 或 super(参数) 必须是子类构造器中的第一条语句。这是为了确保父类在子类之前被正确初始化。
6. equals() 与 hashCode()
为什么重写 equals() 必须重写 hashCode()?
因为 HashMap、HashSet 等基于哈希的集合依赖于 hashCode() 和 equals() 方法。hashCode() 决定了对象在哈希表中的存储位置,equals() 用于在哈希冲突时比较对象是否相等。如果两个对象 equals() 返回 true,但 hashCode() 返回不同的值,它们会被存储在不同的哈希桶中,导致 HashMap 无法正确识别它们是相等的,从而破坏集合的正常功能。
== 与 equals() 的区别?
==:对于基本数据类型,比较的是值;对于引用类型,比较的是两个引用是否指向内存中的同一个对象。equals():是Object类的一个方法,用于比较两个对象的内容是否相等。默认实现是return (this == obj);,但通常会被重写以提供更合理的比较逻辑(如String类)。
equals() 的通用约定(自反性、对称性、传递性、一致性)?equals() 方法必须满足以下通用约定:
- 自反性 (Reflexive):对于任何非
null的引用值x,x.equals(x)应该返回true。 - 对称性 (Symmetric):对于任何非
null的引用值x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。 - 传递性 (Transitive):对于任何非
null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也应该返回true。 - 一致性 (Consistent):对于任何非
null的引用值x和y,多次调用x.equals(y)始终返回true或始终返回false,前提是对象上equals比较中所用的信息没有被修改。 - 非空性 (Non-null):对于任何非
null的引用值x,x.equals(null)应该返回false。
什么是 hashCode 方法?
hashCode() 方法的作用是获取哈希码,它会返回一个 int 整数,定义在 Object 类中,是一个本地方法。
public native int hashCode();
为什么要有 hashCode 方法?
hashCode 方法主要用来获取对象的哈希码,哈希码是由对象的内存地址或者对象的属性计算出来的,它是一个 int 类型的整数,通常是不会重复的,因此可以用来作为键值对的键,以提高查询效率。
为什么两个对象有相同的 hashcode 值,它们也不一定相等?
这主要是由于哈希码(hashCode)的本质和目的所决定的。
哈希码是通过哈希函数将对象中映射成一个整数值,其主要目的是在哈希表中快速定位对象的存储位置。
由于哈希函数将一个较大的输入域映射到一个较小的输出域,不同的输入值(即不同的对象)可能会产生相同的输出值(即相同的哈希码)。
这种情况被称为哈希冲突。当两个不相等的对象发生哈希冲突时,它们会有相同的 hashCode。
hashCode 和 equals 方法的关系?
如果两个对象通过 equals 相等,它们的 hashCode 必须相等。否则会导致哈希表数据结构(如 HashMap、HashSet)的行为异常。
在哈希表中,如果 equals 相等但 hashCode 不相等,哈希表可能无法正确处理这些对象,导致重复元素或键值冲突等问题。
7. final 关键字
final 修饰类、方法、变量分别意味着什么?
- final 修饰类:该类不能被继承。
- final 修饰方法:该方法不能被子类重写。
- final 修饰变量:该变量一旦被初始化,其值就不能被修改。对于基本类型,值不能变;对于引用类型,引用不能变,但引用指向的对象内容可以修改。
String 为什么被设计为 final?String 被设计为 final 主要出于安全和性能考虑:
- 安全性:字符串常被用作网络连接、文件路径等,如果
String可变,这些参数可能在不经意间被修改,导致安全漏洞。 - 不可变性:
final保证了String类不能被继承,从而防止了子类破坏其不可变性。 - 字符串常量池:
String的不可变性使其可以安全地放入字符串常量池中,多个引用可以指向同一个对象,节省内存。 - 哈希值缓存:因为
String不可变,其哈希值可以被缓存,提高在HashMap等集合中的性能。
8. 面向对象和面向过程的区别?
面向过程是以过程为核心,通过函数完成任务,程序结构是函数+步骤组成的顺序流程。
面向对象是以对象为核心,通过对象交互完成任务,程序结构是类和对象组成的模块化结构,代码可以通过继承、组合、多态等方式复用。
在技术派实战项目中,像 VO、DTO 都是业务抽象后的对象实体类,而 Service、Controller 则是业务逻辑的实现,这其实就是面向对象的思想。
9. 面向对象编程有哪些特性?
面向对象编程有三大特性:封装、继承、多态。
封装是什么?
封装是指将数据(属性,或者叫字段)和操作数据的方法(行为)捆绑在一起,形成一个独立的对象(类的实例)。
所以,封装是把一个对象的属性私有化,同时提供一些可以被外界访问的方法。
继承是什么?
继承允许一个类(子类)继承现有类(父类或者基类)的属性和方法。以提高代码的复用性,建立类之间的层次关系。
同时,子类还可以重写或者扩展从父类继承来的属性和方法,从而实现多态。
什么是多态?
多态允许不同类的对象对同一消息做出响应,但表现出不同的行为(即方法的多样性)。
多态其实是一种能力——同一个行为具有不同的表现形式;换句话说就是,执行一段代码,Java 在运行时能根据对象类型的不同产生不同的结果。
多态的前置条件有三个:
- 子类继承父类
- 子类重写父类的方法
- 父类引用指向子类的对象
为什么Java里面要多组合少继承?
继承适合描述"is-a"的关系,但继承容易导致类之间的强耦合,一旦父类发生改变,子类也要随之改变,违背了开闭原则(尽量不修改现有代码,而是添加新的代码来实现)。
组合适合描述"has-a"或"can-do"的关系,通过在类中组合其他类,能够更灵活地扩展功能。组合避免了复杂的类继承体系,同时遵循了开闭原则和松耦合的设计原则。
10. 多态解决了什么问题?
多态指向一个接口或方法在不同的类中有不同的实现,比如说动态绑定,父类引用指向子类对象,方法的具体调用会延迟到运行时决定。
答案是在运行时根据对象的类型进行后期绑定,编译器在编译阶段并不知道对象的类型,但是 Java 的方法调用机制能找到正确的方法体,然后执行,得到正确的结果,这就是多态的作用。
多态的实现原理是什么?
多态通过动态绑定实现,Java 使用虚方法表存储方法指针,方法调用时根据对象实际类型从虚方法表查找具体实现。
11. 访问修饰符 public、private、protected、以及默认时的区别?
Java 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
- default(即默认,什么也不写):在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
- private:在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)
- public:对所有类可见。可以修饰类、接口、变量、方法
- protected:对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)。
12. this 关键字有什么作用?
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法在 Java 中大体可以分为 3 种:
- 普通的直接引用,this 相当于是指向当前对象本身
- 形参与成员变量名字重名,用 this 来区分:
public Person(String name,int age){
this.name=name;
this.age=age;
} - 引用本类的构造方法
13. 抽象类和接口有什么区别?
一个类只能继承一个抽象类;但一个类可以实现多个接口。所以我们在新建线程类的时候一般推荐使用实现 Runnable 接口的方式,这样线程类还可以继承其他类,而不单单是 Thread 类。
抽象类符合 is-a 的关系,而接口更像是 has-a 的关系,比如说一个类可以序列化的时候,它只需要实现 Serializable 接口就可以了,不需要去继承一个序列化类。
抽象类更多地用来为多个相关的类提供一个共同的基础框架,包括状态的初始化,而接口则是定义一套行为标准,让不同的类可以实现同一接口,提供行为的多样化实现。
抽象类可以定义构造方法吗?
可以,抽象类可以有构造方法。
接口可以定义构造方法吗?
不能,接口主要用于定义一组方法规范,没有具体的实现细节。
Java支持多继承吗?
Java不支持多继承,一个类只能继承一个类,多继承会引发菱形继承问题。
接口可以多继承吗?
接口可以多继承,一个接口可以继承多个接口,使用逗号分隔。
继承和抽象的区别?
继承是一种允许子类继承父类属性和方法的机制。通过继承,子类可以重用父类的代码。
抽象是一种隐藏复杂性和只显示必要部分的技术。在面向对象编程中,抽象可以通过抽象类和接口实现。
抽象类和普通类的区别?
抽象类使用 abstract 关键字定义,不能被实例化,只能作为其他类的父类。普通类没有 abstract 关键字,可以直接实例化。
抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须由子类实现。普通类只能包含非抽象方法。
14. static 关键字了解吗?
static 关键字可以用来修饰变量、方法、代码块和内部类,以及导入包。
| 修饰对象 | 作用 |
|---|---|
| 变量 | 静态变量,类级别变量,所有实例共享同一份数据。 |
| 方法 | 静态方法,类级别方法,与实例无关。 |
| 代码块 | 在类加载时初始化一些数据,只执行一次。 |
| 内部类 | 与外部类绑定但独立于外部类实例。 |
| 导入 | 可以直接访问静态成员,无需通过类名引用,简化代码书写,但会降低代码可读性。 |
静态变量和实例变量的区别?
静态变量:是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个副本。
实例变量:必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
静态方法和实例方法有何不同?
静态方法:static 修饰的方法,也被称为类方法。在外部调用静态方法时,可以使用“类名.方法名”的方式,也可以使用“对象名.方法名”的方式。静态方法里不能访问类的非静态成员变量和方法。
实例方法:依存于类的实例,需要使用“对象名.方法名”的方式调用;可以访问类的所有成员变量和方法。
15. final、finally、finalize 的区别?
①、final 是一个修饰符,可以修饰类、方法和变量。当 final 修饰一个类时,表明这个类不能被继承;当 final 修饰一个方法时,表明这个方法不能被重写;当 final 修饰一个变量时,表明这个变量是个常量,一旦赋值后,就不能再被修改了。
②、finally 是 Java 中异常处理的一部分,用来创建 try 块后面的 finally 块。无论 try 块中的代码是否抛出异常,finally 块中的代码总是会被执行。通常,finally 块被用来释放资源,如关闭文件、数据库连接等。
③、finalize 是Object 类的一个方法,用于在垃圾回收器将对象从内存中清除出去之前做一些必要的清理工作。
这个方法在垃圾回收器准备释放对象占用的内存之前被自动调用。我们不能显式地调用 finalize 方法,因为它总是由垃圾回收器在适当的时间自动调用。
16. == 和 equals 的区别?
在 java 中,== 操作符和 equals() 方法用于比较两个对象:
①、:用于比较两个对象的引用,即它们是否指向同一个对象实例。
如果两个变量引用同一个对象实例,返回 true,否则返回 false。
对于基本数据类型(如 int, double, char 等),== 比较的是值是否相等。
②、equals() 方法:用于比较两个对象的内容是否相等。默认情况下,equals() 方法的行为与 == 相同,即比较对象引用,如在超类 Object 中。然而,equals() 方法通常被各种类重写。例如,string 类重写了 equals() 方法,以便它可以比较两个字符串的字符内容是否完全一样。
17. Java 是值传递,还是引用传递?
Java 是值传递,不是引用传递。
当一个对象被作为参数传递到方法中时,参数的值就是该对象的引用。引用的值是对象在堆中的地址。
对象是存储在堆中的,所以传递对象的时候,可以理解为把变量存储的对象地址给传递过去。
引用类型的变量有什么特点?
引用类型的变量存储的是对象的地址,而不是对象本身。因此,引用类型的变量在传递时,传递的是对象的地址,也就是说,传递的是引用的值。
18. 说说深拷贝和浅拷贝的区别?
在 Java 中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是两种拷贝对象的方式,它们在拷贝对象的方式上有很大不同。
浅拷贝会创建一个新对象,但这个新对象的属性(字段)和原对象的属性完全相同。如果属性是基本数据类型,拷贝的是基本数据类型的值;如果属性是引用类型,拷贝的是引用地址,因此新旧对象共享同一个引用对象。
浅拷贝的实现方式为:实现 Cloneable 接口并重写 clone() 方法。
深拷贝也会创建一个新对象,但会递归地复制所有的引用对象,确保新对象和原对象完全独立。新对象与原对象的任何更改都不会相互影响。
深拷贝的实现方式有:手动复制所有的引用对象,或者使用序列化与反序列化。
19. Java 创建对象有哪几种方式?
Java有四种创建对象的方式:
①、new关键字创建,这是最常见和直接的方式,通过调用类的构造方法来创建对象。
②、反射机制创建,反射机制允许在运行时创建对象,并且可以访问类的私有成员,在框架和工具类中比较常见。
③、clone拷贝创建,通过clone方法创建对象,需要实现Cloneable接口并重写clone方法。
④、序列化机制创建,通过序列化将对象转换为字节流,再通过反序列化从字节流中恢复对象。需要实现Serializable接口。
new子类的时候,子类和父类静态代码块,构造方法的执行顺序
在 Java 中,当创建一个子类对象时,子类和父类的静态代码块、构造方法的执行顺序遵循一定的规则。这些规则主要包括以下几个步骤:
- 首先执行父类的静态代码块(仅在类第一次加载时执行)。
- 接着执行子类的静态代码块(仅在类第一次加载时执行)。
- 再执行父类的构造方法。
- 最后执行子类的构造方法。
- 静态代码块:在类加载时执行,仅执行一次,按父类-子类的顺序执行。
- 构造方法:在每次创建对象时执行,按父类-子类的顺序执行,先初始化后构造方法。
四、String 类
1. 不可变性
为什么 String 是不可变的?如何实现?String 是不可变的,因为:
final修饰:String类被声明为final,不能被继承。final字符数组:String内部使用一个private final char value[]来存储字符串内容,数组引用是final的,不能指向其他数组。- 无修改方法:
String类没有提供任何可以修改其内部字符数组内容的公共方法。所有看似修改字符串的方法(如concat,substring)都返回一个新创建的String对象。
不可变的好处和代价?
- 好处:
- 线程安全:不可变对象是线程安全的,无需同步。
- 安全性:防止参数被意外修改。
- 字符串常量池:可以被共享,节省内存。
- 哈希值缓存:
hashCode可以被缓存,提高性能。
- 代价:频繁修改字符串会产生大量中间对象,浪费内存和性能。
StringBuilder 和 StringBuffer 的区别?
StringBuilder:非线程安全,但性能更高。适用于单线程环境。StringBuffer:线程安全,因为其方法都使用synchronized关键字修饰,但性能较低。适用于多线程环境。
2. 常量池
字符串常量池(String Pool)是什么?存放在哪里?(JDK 6 vs JDK 7 vs JDK 8)
- 是什么:字符串常量池是 JVM 为了优化内存使用而维护的一个特殊区域,用于存储字符串字面量(如
"abc")和通过intern()方法加入的字符串。 - 存放位置:
- JDK 6:存放在永久代 (PermGen) 中。
- JDK 7:字符串常量池被移到了堆 (Heap) 中。这是一个重要的优化,因为永久代大小有限且容易导致
OutOfMemoryError。 - JDK 8:永久代被元空间 (Metaspace) 取代,字符串常量池仍然在堆中。
new String(“abc”) 创建几个对象?new String("abc") 会创建两个对象:
- 一个在堆上的
String对象。 - 一个在字符串常量池中的
"abc"字符串对象(如果常量池中还没有"abc")。
intern() 方法的作用?不同 JDK 版本的行为差异?
- 作用:
intern()方法会检查字符串常量池中是否已经存在一个与该字符串equals()相等的字符串。如果存在,则返回常量池中的字符串引用;如果不存在,则将该字符串放入常量池中,并返回其引用。 - JDK 6:
intern()会将字符串的拷贝放入永久代的常量池。 - JDK 7+:
intern()可以将字符串的引用直接放入堆中的常量池,避免了不必要的拷贝,性能更好。
3. 常用方法
split()、substring()、replace()、trim()、indexOf() 等方法细节?
split(String regex):根据正则表达式regex分割字符串,返回一个字符串数组。substring(int beginIndex, int endIndex):返回从beginIndex到endIndex(不包括)的子字符串。replace(char oldChar, char newChar)/replace(CharSequence target, CharSequence replacement):替换所有匹配的字符或字符序列。trim():去除字符串两端的空白字符(ASCII值小于等于32的字符)。indexOf(int ch)/indexOf(String str):返回指定字符或子字符串第一次出现的索引,如果未找到则返回 -1。
split(“.”) 为什么返回空数组?(正则表达式问题)
因为 split() 方法的参数是一个正则表达式。. 在正则表达式中是一个元字符,表示匹配任意单个字符。因此,split(".") 会尝试用“任意字符”作为分隔符,导致字符串被完全分割,返回一个空数组。要按字面量的点号分割,需要转义:split("\\.")。
4. 性能与优化
用 + 拼接字符串的性能问题?编译器优化?
- 性能问题:在循环中使用
+拼接字符串时,每次+操作都会创建一个新的String对象,导致大量临时对象,影响性能和内存。 - 编译器优化:对于编译期常量的拼接(如
String s = "a" + "b" + "c";),编译器会将其优化为单个字符串字面量。但对于运行时变量的拼接,编译器会将其转换为StringBuilder的append操作,但这在循环中仍然不够高效。
什么时候用 String、StringBuilder、StringBuffer?
String:字符串内容不经常改变,或作为HashMap的 key。StringBuilder:单线程环境下,需要频繁修改字符串。StringBuffer:多线程环境下,需要频繁修改字符串,且需要线程安全。
5. String 是 Java 基本数据类型吗?可以被继承吗?
不是,string 是一个类,属于引用数据类型。Java 的基本数据类型包括/种:四种整型(byte、short、int、long)、两种浮点型(float、double)、一种字符型(char)和一种布尔型(boolean)。
String 类可以被继承吗?
不行。String 类使用 final 修饰,是所谓的不可变类,无法被继承。
String 有哪些常用方法?
我自己常用的有:
- length() - 返回字符串的长度。
- charAt(int index) - 返回指定位置的字符。
- substring(int beginIndex, int endIndex) - 返回字符串的一个子串,从 beginIndex 到 endIndex-1。
- contains(CharSequence s) - 检查字符串是否包含指定的字符序列。
- equals(object anotherObject) - 比较两个字符串的内容是否相等。
- indexOf(int ch) 和 indexOf(String str) - 返回指定字符或字符串首次出现的位置。
- replace(char oldChar, char newChar) 和 replace(CharSequence target, CharSequence replacement) - 替换字符串中的字符或字符序列。
- trim() - 去除字符串两端的空白字符。
- split(String regex) - 根据给定正则表达式的匹配拆分此字符串。
6. intern 方法有什么作用?
JDK 源码里已经对这个方法进行了说明:
意思也很好懂:
- 如果当前字符串内容存在于字符串常量池(即 equals()方法为 true,也就是内容一样),直接返回字符串常量池中的字符串
- 否则,将此 String 对象添加到池中,并返回 String 对象的引用
五、Integer类
1. Integer缓存机制
Integer a=127, Integer b=127; Integer c=128, Integer d=128; 相等吗?
a 和 b 相等,c 和 d 不相等。
这个问题涉及到 Java 的自动装箱机制以及 Integer 类的缓存机制。
对于第一对:
Integer a = 127;
Integer b = 127;
a 和 b 是相等的。这是因为 Java 在自动装箱过程中,会使用 Integer.valueOf() 方法来创建 Integer 对象。Integer.valueOf() 方法会针对数值在-128 到 127 之间的 Integer 对象使用缓存。因此,a 和 b 实际上引用了常量池中相同的 Integer 对象。
对于第二对:
Integer c = 128;
Integer d = 128;
c 和 d 不相等。这是因为 128 超出了 Integer 缓存的范围(-128 到 127)。因此,自动装箱过程会为 c 和 d 创建两个不同的 Integer 对象,它们有不同的引用地址。
可以通过 == 运算符来检查它们是否相等:
System.out.println(a == b); // 输出true
System.out.println(c == d); // 输出false
要比较 Integer 对象的数值是否相等,应该使用 equals 方法,而不是 == 运算符:
System.out.println(a.equals(b)); // 输出true
System.out.println(c.equals(d)); // 输出true
使用 equals 方法时,c 和 d 的比较结果为 true,因为 equals 比较的是对象的数值,而不是引用地址。
什么是 Integer 缓存?
就拿 Integer 的缓存来说吧。根据实践发现,大部分的数据操作都集中在值比较小的范围,因此 Integer 搞了个缓存池,默认范围是 -128 到 127。
当我们使用自动装箱来创建这个范围内的 Integer 对象时,Java 会直接从缓存中返回一个已存在的对象,而不是每次都创建一个新的对象。这意味着,对于这个值范围内的所有 Integer 对象,它们实际上是引用相同的对象实例。
Integer 缓存的主要目的是优化性能和内存使用。对于小整数的频繁操作,使用缓存可以显著减少对象创建的数量。
可以在运行的时候添加 -Djava.lang.Integer.IntegerCache.high=1000 来调整缓存池的最大值。
引用是 Integer 类型,= 右侧是 int 基本类型时,会进行自动装箱,调用的其实是 Integer.valueOf() 方法,它会调用 IntegerCache。
new Integer(10) == new Integer(10) 相等吗
在 Java 中,使用 new Integer(10) == new Integer(10) 进行比较时,结果是 false。
这是因为 new 关键字会在堆(Heap)上为每个 Integer 对象分配新的内存空间,所以这里创建了两个不同的 Integer 对象,它们有不同的内存地址。
当使用==运算符比较这两个对象时,实际上比较的是它们的内存地址,而不是它们的值,因此即使两个对象代表相同的数值(10),结果也是 false。
2. String 转 Integer
String 怎么转成 Integer 的?原理?
String 转成 Integer,主要有两个方法:
● Integer.parseInt(String s)
● Integer.valueOf(String s)
不管哪一种,最终还是会调用 Integer 类内中的 parseInt(String s, int radix) 方法。
抛去一些边界之类的看看核心代码:
去掉枝枝蔓蔓(当然这些枝枝蔓蔓可以去看看,源码 cover 了很多情况),其实剩下的就是一个简单的字符串遍历计算,不过计算方式有点反常规,是用负的值累减。
六、Object类
1. Object类的常见方法
Object类的常见方法?
在Java中,经常提到一个词“万物皆对象”,其中的“万物”指的是Java中的所有类,而这些类都是Object类的子类。
Object主要提供了11个方法,大致可以分为六类:对象比较、对象拷贝、对象转字符串、多线程调度、反射、垃圾回收。
对象比较:
①、public native int hashCode():native 方法,用于返回对象的哈希码。
public native int hashCode();
按照约定,相等的对象必须具有相等的哈希码。如果重写了 equals 方法,就应该重写 hashCode 方法。可以使用 Objects.hash() 方法来生成哈希码。
②、public boolean equals(object obj):用于比较 2 个对象的内存地址是否相等。
public boolean equals(object obj) {
return (this == obj);
}
如果比较的是两个对象的值是否相等,就要重写该方法,比如 String 类、Integer 类等都重写了该方法。
对象拷贝:
protected native Object clone() throws CloneNotSupportedException:native 方法,返回此对象的一个副本。默认实现只做浅拷贝,且类必须实现 Cloneable 接口。
Object 本身没有实现 Cloneable 接口,所以在不重写 clone 方法的情况下直接直接调用该方法会发生 CloneNotSupportedException 异常。
对象转字符串:
public String toString():返回对象的字符串表示。默认实现返回类名@哈希码的十六进制表示,但通常会被重写以返回更有意义的信息。
public String toString() {
return getClass().getName() + “@” + Integer.toHexString(hashCode());
}
数组也是一个对象,所以通常我们打印数组的时候,会看到诸如 [T@1b6d3586] 这样的字符串,这个就是 int 数组的哈希码。
多线程调度:
每个对象都可以调用 Object 的 wait/notify 方法来实现等待/通知机制。
①、public final void wait() throws InterruptedException:调用该方法会导致当前线程等待,直到另一个线程调用此对象的 notify() 方法或 notifyAll() 方法。
②、public final native void notify(): 唤醒在此对象监视器上等待的单个线程。如果有多个线程等待,选择一个线程被唤醒。
③、public final native void notifyAll(): 唤醒在此对象监视器上等待的所有线程。
④、public final native void wait(long timeout) throws InterruptedException:等待 timeout 毫秒,如果在 timeout 毫秒内没有被唤醒,会自动唤醒。
⑤、public final void wait(long timeout, int nanos) throws InterruptedException:更加精确了,等待 timeout 毫秒和 nanos 纳秒,如果在 timeout 毫秒和 nanos 纳秒内没有被唤醒,会自动唤醒。
反射:
public final native Class<?> getClass():用于获取对象的类信息,如类名。
垃圾回收:
protected void finalize() throws Throwable:当垃圾回收器决定回收对象占用的内存时调用此方法。用于清理资源,但 Java 不推荐使用,因为它不可预测且容易导致问题,Java 9 开始已被弃用。
七、异常处理
1. 异常分类
Error vs Exception vs RuntimeException?
Error:表示严重的系统级错误,如OutOfMemoryError、StackOverflowError。程序通常无法处理,只能终止。Exception:表示程序可以处理的异常。RuntimeException:是Exception的子类,表示运行时异常,如NullPointerException、ArrayIndexOutOfBoundsException。它们是非检查异常 (unchecked exception),编译器不要求必须处理。
检查异常(checked exception)和非检查异常(unchecked exception)的区别?
- 检查异常 (Checked Exception):
Exception的子类(不包括RuntimeException)。编译器强制要求必须处理(捕获或声明抛出),否则编译失败。例如IOException、SQLException。 - 非检查异常 (Unchecked Exception):
RuntimeException和Error。编译器不要求必须处理。
2. try-catch-finally
finally 一定会执行吗?return 在 try 或 catch 中时,finally 的执行顺序?
finally块几乎总是会执行,即使在try或catch块中使用了return、break或continue。- 执行顺序:当
try或catch中有return时,finally会在return语句执行后、方法返回前执行。如果finally中也有return,它会覆盖try/catch中的return值。
try-with-resources 是如何实现自动关闭的?(实现 AutoCloseable 接口)try-with-resources 语句要求资源必须实现 AutoCloseable 接口(或其子接口 Closeable)。在 try 语句块执行完毕后(无论是否发生异常),JVM 会自动调用资源的 close() 方法来释放资源,无需手动 finally 块。
3. throw vs throws
抛出异常 vs 声明异常的区别?
throw:用于在方法内部抛出一个具体的异常实例。throws:用于在方法签名中声明该方法可能抛出的异常类型,由调用者来处理。
4. 常见异常
NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException、NumberFormatException、ArithmeticException 等常见原因和解决?
NullPointerException:尝试访问或调用一个null对象的成员。解决:在使用前进行null检查。ArrayIndexOutOfBoundsException:访问数组时索引超出范围。解决:检查数组长度和索引。ClassCastException:尝试将一个对象强制转换为不兼容的类型。解决:使用instanceof检查类型。NumberFormatException:尝试将一个非数字字符串转换为数字。解决:使用try-catch或正则表达式验证。ArithmeticException:例如除以零。解决:检查除数是否为零。
5. 自定义异常
如何自定义异常?什么时候需要自定义异常?
- 何时需要:当 Java 提供的异常类型不足以描述特定的业务错误时,例如业务规则验证失败。
如何自定义:继承 Exception(检查异常)或 RuntimeException(非检查异常)。
publicclassMyExceptionextendsException{publicMyException(String message){super(message);}}6. 三道经典异常处理代码题
题目1
publicclassTryDemo{publicstaticvoidmain(String[] args){System.out.println(test());}publicstaticinttest(){try{return1;}catch(Exception e){return2;}finally{System.out.print("3");}}}在test()方法中,首先有一个try块,接着是一个catch块(用于捕获异常),最后是一个finally块(无论是否捕获到异常,finally块总会执行)。
①、try 块中包含一条 return 1;语句。正常情况下,如果 try 块中的代码能够顺利执行,那么方法将返回数字 1。在这个例子中,try 块中没有任何可能抛出异常的操作,因此它会正常执行完毕,并准备返回 1。
②、由于 try 块中没有异常发生,所以 catch 块中的代码不会执行。
③、无论前面的代码是否发生异常,finally 块总是会执行。在这个例子中,finally 块包含一条 System.out.print(“3”); 语句,意味着在方法结束前,会在控制台打印出 3。
当执行 main 方法时,控制台的输出将会是:31
这是因为 finally 块确保了它包含的 System.out.print(“3”); 会执行并打印 3,随后 test() 方法返回 try 块中的值 1,最终结果就是 31。
题目2
publicclassTryDemo{publicstaticvoidmain(String[] args){System.out.println(test1());}publicstaticinttest1(){try{return2;}finally{return3;}}}执行结果:3。
try 返回前先执行 finally,结果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。
注意:finally 里面使用 return 仅存在于面试题中,实际开发这么写要挨揍的( )。
题目3
publicclassTryDemo{publicstaticvoidmain(String[] args){System.out.println(test1());}publicstaticinttest1(){int i =0;try{ i =2;return i;}finally{ i =3;}}}执行结果:2。
大家可能会以为结果应该是 3,因为在 return 前会执行 finally,而 i 在 finally 中被修改为 3 了,那最终返回 i 不是应该为 3 吗?
但其实,在执行 finally 之前,JVM 会先将 i 的结果暂存起来,然后 finally 执行完毕后,会返回之前暂存的结果,而不是返回 i,所以即使 i 已经被修改为 3,最终返回的还是之前暂存起来的结果 2。
八、I/O 流
1. 流的分类
字节流 vs 字符流?InputStream/OutputStream vs Reader/Writer?
- 字节流:以字节(8位)为单位处理数据,适用于所有类型的数据(如图片、音频)。基类是
InputStream和OutputStream。 - 字符流:以字符(16位)为单位处理数据,专门用于处理文本数据,能自动处理字符编码。基类是
Reader和Writer。
节点流 vs 处理流(包装流)?
- 节点流 (Node Stream):直接连接到数据源或目的地,如
FileInputStream、FileReader。 - 处理流 (Processing Stream):对一个已存在的流进行包装,以提供更高级的功能,如缓冲、数据转换。如
BufferedInputStream、InputStreamReader。处理流不能独立存在,必须连接到一个节点流。
2. 常用类
FileInputStream / FileOutputStream
用于读写文件的字节流。
BufferedInputStream / BufferedOutputStream
为字节流提供缓冲功能,提高读写效率。
InputStreamReader / OutputStreamWriter(桥接字节与字符)InputStreamReader:将字节输入流转换为字符输入流,可以指定字符编码。OutputStreamWriter:将字符输出流转换为字节输出流,可以指定字符编码。
BufferedReader / BufferedWriter(高效读写文本)
为字符流提供缓冲功能,BufferedReader 提供了 readLine() 方法,方便读取文本行。
PrintStream / PrintWriter(格式化输出)
提供 print、println、printf 等方法,方便格式化输出。
3. NIO vs IO
BIO、NIO、AIO 的区别?
- BIO (Blocking I/O):传统的阻塞 I/O 模型。线程在执行 I/O 操作时被阻塞,直到数据准备好。适用于连接数少、数据量大的场景。
- NIO (Non-blocking I/O):基于通道(Channel)和缓冲区(Buffer),使用选择器(Selector)实现多路复用,一个线程可以管理多个通道。适用于连接数多、数据量小的场景。
- AIO (Asynchronous I/O):异步非阻塞 I/O。I/O 操作由操作系统完成,完成后通过回调通知应用程序。适用于连接数多、数据量大的场景。
FileChannel、ByteBuffer、Selector、Path、Files 工具类?
FileChannel:NIO 中用于读写文件的通道。ByteBuffer:NIO 中用于存储数据的缓冲区。Selector:NIO 中用于监控多个通道的事件(如连接、读就绪)。Path:NIO.2 中表示文件路径的接口。Files:NIO.2 中的工具类,提供了丰富的静态方法来操作文件和目录。
零拷贝(transferTo)原理?
零拷贝技术(如 FileChannel.transferTo())允许数据直接从文件系统缓存传输到网络套接字,而无需经过用户空间的缓冲区,减少了数据拷贝次数和上下文切换,极大地提高了大文件传输的性能。
4. 编码问题
读取文件时如何指定编码?(如 UTF-8)
使用 InputStreamReader 或 FileReader 的构造函数指定编码。
InputStreamReader isr =newInputStreamReader(newFileInputStream("file.txt"),"UTF-8");BufferedReader br =newBufferedReader(isr);乱码问题如何解决?
乱码问题通常是由于读取和写入时使用的字符编码不一致导致的。解决方法是确保在整个 I/O 流程中使用一致的编码(如 UTF-8)。
九、序列化
1. Serializable 接口
什么是序列化?用途?(RMI、网络传输、深拷贝)
- 序列化 (Serialization):将对象转换为字节流的过程。
- 反序列化 (Deserialization):将字节流转换回对象的过程。
- 用途:持久化对象(保存到文件或数据库)、通过网络传输对象(如 RMI)、实现深拷贝。
Serializable 是标记接口吗?为什么?
是的,Serializable 是一个标记接口,它没有定义任何方法。它的存在只是告诉 JVM 这个类的对象可以被序列化。
serialVersionUID 的作用?不声明会怎样?
- 作用:
serialVersionUID是一个版本号,用于在反序列化时验证序列化者和反序列化者是否加载了与序列化兼容的类。如果对象的serialVersionUID与目标类的不匹配,会抛出InvalidClassException。 - 不声明:如果不显式声明,JVM 会根据类的细节(如类名、接口、方法等)动态生成一个
serialVersionUID。如果类的结构发生变化(如添加字段),生成的serialVersionUID也会改变,导致反序列化失败。因此,建议显式声明一个static final long serialVersionUID。
2. 序列化机制
static 和 transient 字段是否被序列化?
static字段:不会被序列化。因为static属于类,不属于对象实例。transient字段:不会被序列化。transient关键字用于标记不希望被序列化的字段。
如何自定义序列化过程?(writeObject / readObject 方法)
可以通过在类中定义 private void writeObject(ObjectOutputStream out) 和 private void readObject(ObjectInputStream in) 方法来控制序列化和反序列化过程。这些方法可以调用 defaultWriteObject() 和 defaultReadObject() 来处理默认的序列化,然后添加自定义逻辑。
readResolve() 方法的作用?(防止反序列化破坏单例)readResolve() 方法用于在反序列化时返回一个指定的对象。在单例模式中,可以返回单例实例,从而保证反序列化后仍然是同一个实例,防止破坏单例。
3. 常见问题
反序列化时会调用构造函数吗?
不会。反序列化是通过从字节流中重建对象,而不是通过构造器。因此,构造器不会被调用。
子类序列化,父类是否也必须实现 Serializable?如果父类没有实现会怎样?
- 如果父类没有实现
Serializable,那么父类的字段不会被序列化。 - 在反序列化时,会调用父类的无参构造器来初始化父类的字段。
Externalizable 接口与 Serializable 的区别?
Serializable:Java 的默认序列化机制,简单但性能较低。Externalizable:是Serializable的子接口,提供了writeExternal(ObjectOutput out)和readExternal(ObjectInput in)方法,允许完全控制序列化过程,性能更高,但需要手动实现。
十、网络编程
1. 基本概念
OSI 七层模型 vs TCP/IP 四层模型?
- OSI 七层模型:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
- TCP/IP 四层模型:应用层、传输层、网络层、网络接口层。TCP/IP 模型更贴近实际的网络协议。
TCP vs UDP 的区别?
- TCP:传输控制协议,面向连接、可靠、基于字节流。保证数据按序到达,有重传机制。适用于 HTTP、FTP、SMTP。
- UDP:用户数据报协议,无连接、不可靠、基于数据报。速度快,开销小。适用于 DNS、视频流、在线游戏。
三次握手、四次挥手?
- 三次握手 (Three-way Handshake):建立 TCP 连接。
- 客户端发送
SYN包。 - 服务端回复
SYN-ACK包。 - 客户端回复
ACK包。
- 客户端发送
- 四次挥手 (Four-way Wave):终止 TCP 连接。
- 一端发送
FIN包。 - 另一端回复
ACK包。 - 另一端发送自己的
FIN包。 - 第一端回复
ACK包。
- 一端发送
2. Java 网络 API
InetAddress 类的使用?InetAddress 类用于表示互联网协议 (IP) 地址。可以用来获取主机名、IP 地址等信息。
InetAddress address =InetAddress.getByName("www.example.com");System.out.println(address.getHostAddress());URL / URLClassLoader?
URL:表示一个统一资源定位符。URLClassLoader:可以加载来自 URL 指定位置的类和资源。
3. BIO(阻塞 I/O)
Socket、ServerSocket 编程模型?
ServerSocket:用于服务端,监听指定端口的连接请求。Socket:用于客户端和服务端,代表一个网络连接。
实现一个简单的客户端-服务器通信?
以下是一个完整的、可运行的简单客户端-服务器通信示例。
服务端 (Server)
importjava.io.*;importjava.net.*;publicclassSimpleServer{publicstaticvoidmain(String[] args)throwsIOException{// 创建 ServerSocket,监听 8080 端口ServerSocket serverSocket =newServerSocket(8080);System.out.println("服务器启动,等待客户端连接...");// 接受客户端连接Socket clientSocket = serverSocket.accept();System.out.println("客户端已连接: "+ clientSocket.getInetAddress());// 获取输入流,读取客户端消息BufferedReader in =newBufferedReader(newInputStreamReader(clientSocket.getInputStream()));String clientMessage = in.readLine();System.out.println("收到客户端消息: "+ clientMessage);// 获取输出流,向客户端发送响应PrintWriter out =newPrintWriter(clientSocket.getOutputStream(),true); out.println("Hello from Server! 你的消息已收到: "+ clientMessage);// 关闭资源 in.close(); out.close(); clientSocket.close(); serverSocket.close();}}客户端 (Client)
importjava.io.*;importjava.net.*;publicclassSimpleClient{publicstaticvoidmain(String[] args)throwsIOException{// 连接到本地 8080 端口的服务器Socket socket =newSocket("localhost",8080);// 获取输出流,向服务器发送消息PrintWriter out =newPrintWriter(socket.getOutputStream(),true); out.println("Hello from Client!");// 获取输入流,读取服务器响应BufferedReader in =newBufferedReader(newInputStreamReader(socket.getInputStream()));String serverResponse = in.readLine();System.out.println("收到服务器响应: "+ serverResponse);// 关闭资源 out.close(); in.close(); socket.close();}}运行说明:
- 先运行
SimpleServer,它会启动并等待连接。 - 再运行
SimpleClient,它会连接到服务器并发送消息。 - 服务器会接收消息并发送响应,客户端会打印出响应。
4. NIO(非阻塞 I/O)
Buffer、Channel、Selector 的作用?
- Buffer:数据容器,用于读写数据。
- Channel:类似于流,但可以双向读写,数据从
Buffer到Channel或反之。 - Selector:可以监控多个
Channel的事件(如连接、读就绪),实现单线程管理多个连接。
SocketChannel、ServerSocketChannel?
SocketChannel:NIO 中的客户端通道。ServerSocketChannel:NIO 中的服务端通道,用于监听连接。
SelectionKey 的四种事件(OP_READ、OP_WRITE 等)?
OP_READ:读就绪。OP_WRITE:写就绪。OP_CONNECT:连接就绪。OP_ACCEPT:接收连接就绪。
5. Netty(扩展)
Netty 是一个基于 NIO 的高性能网络应用框架,解决了 NIO 的复杂性,提供了更高级的 API。
十一、泛型
1. 基本语法
泛型类、泛型方法、泛型接口的定义?
- 泛型类:
public class Generic<T> { ... } - 泛型方法:
public <E> void printArray(E[] inputArray) { ... } - 泛型接口:
public interface Generator<T> { T method(); }
类型通配符:<?>、<? extends T>、<? super T>(PECS 原则)?
<?>:无界通配符,表示未知类型。<? extends T>:上界通配符,表示T或其子类型。Producer Extends (PECS)。<? super T>:下界通配符,表示T或其父类型。Consumer Super (PECS)。
2. 类型擦除
Java 泛型是通过类型擦除实现的?编译后泛型信息还在吗?
是的,Java 泛型是通过类型擦除实现的。在编译期间,所有泛型类型信息都会被擦除,替换为它们的边界(如 Object 或指定的上界)。因此,运行时无法获取泛型的类型信息。
什么是桥方法(Bridge Method)?为什么需要?
桥方法是编译器为了实现泛型重写而自动生成的合成方法。它用于解决类型擦除后方法签名冲突的问题,确保多态性正常工作。
3. 泛型限制
不能 new T()?为什么?
因为在运行时,泛型类型 T 已经被擦除,JVM 无法知道 T 的具体类型,因此无法实例化。
不能创建泛型数组?如何绕过?(Array.newInstance())
因为数组在运行时需要知道确切的组件类型,而泛型类型在运行时被擦除。可以通过 Array.newInstance() 方法绕过,例如 Array.newInstance(componentType, size)。
4. 常见问题
List 和 List 是否有继承关系?
没有。List<String> 不是 List<Object> 的子类型。泛型是不可变的。
原始类型(Raw Type)的风险?
使用原始类型会失去泛型的类型安全检查,可能导致 ClassCastException 在运行时抛出。
十二、反射(Reflection)
1. 基本使用
如何获取 Class 对象?(Class.forName()、.class、getClass())
Class.forName("全限定类名")类名.class对象.getClass()
反射获取构造器、方法、字段?
getConstructor()/getDeclaredConstructor()getMethod()/getDeclaredMethod()getField()/getDeclaredField()
调用私有方法或访问私有字段?(setAccessible(true))
通过 setAccessible(true) 可以绕过访问控制检查,访问私有成员。
2. 应用场景
依赖注入、工厂模式、JDBC 加载驱动、Spring AOP、动态代理等?
反射广泛应用于框架中,如 Spring 通过反射创建和管理 Bean,JDBC 通过 Class.forName() 加载驱动,动态代理在运行时生成代理类。
3. 性能与安全
反射的性能开销?如何优化?(MethodHandle、缓存 Method 对象)
反射性能较低,因为需要进行类型检查和安全检查。可以通过缓存Method、Field 对象或使用 MethodHandle 来优化。
反射破坏单例?如何防止?
反射可以通过调用私有构造器破坏单例。防止方法是在构造器中检查实例是否已存在并抛出异常。
4. java.lang.reflect 包
该包提供了 Field、Method、Constructor 等类,用于表示和操作类的成员。
十三、注解
1. 注解的概念和理解
说一下你对注解的理解?
Java 注解本质上是一个标记,可以理解成生活中的一个人的一些小装扮,比如戴什么什么帽子,戴什么眼镜。
注解可以标记在类上、方法上、属性上等,标记自身也可以设置一些值,比如帽子颜色是绿色。
有了标记之后,我们就可以在编译或者运行阶段去识别这些标记,然后搞一些事情,这就是注解的用处。
例如我们常见的 AOP,使用注解作为切点就是运行期注解的应用;比如 lombok,就是注解在编译期的运行。
注解生命周期有三大类,分别是:
● RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件
● RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了
● RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息
像常见的 @Override 就是给编译器用的,编译器编译的时候检查没问题就 over 了,class 文件里面不会有 Override 这个标记。
再比如 Spring 常见的 @Autowired,就是 RUNTIME 的,所以在运行的时候可以通过反射得到注解的信息,还能拿到标记的值 required。
十四、JDK1.8新特性
1. JDK1.8 新特性概述
JDK 1.8 都有哪些新特性?
JDK 1.8 新增了不少新的特性,如 Lambda 表达式、接口默认方法、Stream API、日期时间 API、Optional 类等。
①、Java 8 允许在接口中添加默认方法和静态方法。
public interface MyInterface {
default void myDefaultMethod() {
System.out.println(“My default method”);
}
static void myStaticMethod() {
System.out.println(“My static method”);
}
}
②、Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行。
public class LamadaTest {
public static void main(String[] args) {
new Thread(() -> System.out.println(“沉默王二”)).start();
}
}
《Effective Java》的作者 Josh Bloch 建议使用 Lambda 表达式时,最好不要超过 3 行。否则代码可读性会变得很差。
③、Stream 是对 Java 集合框架的增强,它提供了一种高效且易于使用的数据处理方式。
List list = new ArrayList<>();
list.add(“中国加油”);
list.add(“世界加油”);
list.add(“世界加油”);
long count = list.stream().distinct().count();
System.out.println(count);
④、Java 8 引入了一个全新的日期和时间 API,位于 java.time 包中。这个新的 API 纠正了旧版 java.util.Date 类中的许多缺陷。
LocalDate today = LocalDate.now();
System.out.println("Today’s Local date : " + today);
LocalTime time = LocalTime.now();
System.out.println("Local time : " + time);
LocalDateTime now = LocalDateTime.now();
System.out.println("Current DateTime : " + now);
⑤、引入 Optional 是为了减少空指针异常。
Optional optional = Optional.of(“沉默王二”);
optional.isPresent(); // true
optional.get(); // “沉默王二”
optional.orElse(“沉默王三”); // “沉默王二”
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // “沉”
2. Lambda 表达式
Lambda 表达式了解多少?
Lambda 表达式主要用于提供一种简洁的方式来表示匿名方法,使 Java 具备了函数式编程的特性。
比如我们可以使用 Lambda 表达式来简化线程的创建:
new Thread(() -> System.out.println(“Hello World”)).start();
这比以前的匿名内部类要简洁很多。
所谓的函数式编程,就是把函数作为参数传递给方法,或者作为方法的结果返回。比如说我们可以配合 Stream 流进行数据过滤:
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
其中 n -> n % 2 == 0 就是一个 Lambda 表达式。表示传入一个参数 n,返回 n % 2 == 0 的结果。
Java8 有哪些内置函数式接口?
JDK 1.8 API 包含了很多内置的函数式接口。其中就包括我们在老版本中经常见到的 Comparator 和 Runnable,Java 8 为他们添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。
除了这两个之外,还有 Callable、Predicate、Function、Supplier、Consumer 等等。
3. Optional 类
Optional 了解吗?
Optional 是用于防范 NullPointerException。
可以将 Optional 看做是包装对象(可能是 null,也有可能非 null)的容器。当我们定义了一个方法,这个方法返回的对象可能是空,也有可能非空的时候,我们就可以考虑用 Optional 来包装它,这也是在 Java 8 被推荐使用的做法。
Optional optional = Optional.of(“bam”);
optional.isPresent(); // true
optional.get(); // “bam”
optional.orElse(“fallback”); // “bam”
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // “b”
4. Stream API
Stream 流用过吗?
Stream 流,简单来说,使用 java.util.stream 对一个包含一个或多个元素的集合做各种操作。这些操作可能是中间操作亦或是终端操作。终端操作会返回一个结果,而中间操作会返回一个 Stream 流。
Stream 流一般用于集合,我们对一个集合做几个常见操作:
List stringCollection = new ArrayList<>();
stringCollection.add(“add2”);
stringCollection.add(“aaa2”);
stringCollection.add(“bbb1”);
stringCollection.add(“aaa1”);
stringCollection.add(“bbb3”);
stringCollection.add(“ccc”);
stringCollection.add(“bbb2”);
stringCollection.add(“ddd1”);
- Filter 过滤
stringCollection
.stream()
.filter((s) -> s.startsWith(“a”))
.forEach(System.out::println);
// “aaa2”, “aaa1” - Sorted 排序
stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith(“a”))
.forEach(System.out::println);
// “aaa1”, “aaa2” - Map 转换
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);
// “DDD2”, “DDD1”, “CCC”, “BBB3”, “BBB2”, “AAA2”, “AAA1” - Match 匹配
// 验证 list 中 string 是否有以 a 开头的,匹配到第一个,即返回 true
boolean anyStartsWithA = stringCollection
.stream()
.anyMatch((s) -> s.startsWith(“a”));
System.out.println(anyStartsWithA); // true
// 验证 list 中 string 是否都是以 a 开头的
boolean allStartsWithA = stringCollection
.stream()
.allMatch((s) -> s.startsWith(“a”));
System.out.println(allStartsWithA); // false
// 验证 list 中 string 是否都不是以 z 开头的,
boolean noneStartsWithZ = stringCollection
.stream()
.noneMatch((s) -> s.startsWith(“z”));
System.out.println(noneStartsWithZ); // true - Count 计数
count 是一个终端操作,它能够统计 stream 流中的元素总数,返回值是 long 类型。
// 先对 list 中字符串开头为 b 进行过滤,让后统计数量
long startsWithB = stringCollection
.stream()
.filter((s) -> s.startsWith(“b”))
.count();
System.out.println(startsWithB); // 3 - Reduce
Reduce 中文翻译为:减少、缩小。通过入参的 Function,我们能够将 list 归约成一个值。它的返回类型是 Optional 类型。
Optional reduced = stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + “#” + s2);
reduced.ifPresent(System.out::println);
// “aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2”