Java基础
阅读提示
先掌握“概念定义 + 对比区别 + 常见坑”三种回答模板。
如果时间紧,优先看:面向对象、== 与 equals、String、接口 vs 抽象类。
Java基础
回答提示:是一种比较通俗的解释,比较好理解,如果‘参考答案’不太好记住,可以直接复习回答提示也可以。
1、面向对象和面向过程的区别是什么?
回答提示:
- 面向过程就像是做菜时按步骤来,先洗菜、切菜、炒菜,一步步完成。它关注解决问题的步骤和流程,直接用函数实现每个步骤,然后依次调用。
- 而面向对象则更像是把做菜看成一个整体,关注构成这个问题的各个“对象”,比如“锅”、“铲子”、“食材”等。每个对象有自己的属性和行为(方法),如“锅”有“加热”的方法。面向对象通过创建对象并调用它们的方法来解决问题,更强调事物的属性和行为,以及它们之间的关系。
- 总结来说,面向过程更直接,关注怎么做;面向对象更抽象,关注做什么和由谁来做。面向对象具有更高的复用性、扩展性和维护性,但可能性能稍低;面向过程则性能较高,但复用和维护可能较复杂。
参考回答:
- 面向过程和面向对象是两种不同的处理问题的角度。
- 面向过程更注重事情的每一个步骤及顺序。主要是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用
- 面向对象更注重事情有哪些参与者(对象)、及各自需要做什么。主要是把事物对象化,包括其属性和行为。通过调用不同对象之间的方法,以组合的方式解决问题。
2、面向对象的三大基本特征
回答提示:
- 封装:就是把数据和操作数据的方法“打包”在一起,形成一个独立的单元(类)。外界不需要知道内部的具体实现细节,只需要通过接口(类提供的方法)与之交互。这就像是一个黑盒子,我们只知道它的输入和输出,但不知道里面是怎么工作的。
- 继承:子类可以继承父类的属性和方法,这样就可以在不需要重新编写原有代码的基础上,扩展新的功能。比如,我们可以定义一个“动物”类,然后让“猫”和“狗”类继承它,从而复用“动物”类中的属性和方法。
- 多态:是同一个接口(方法)在不同的子类中有不同的实现。这样,我们就可以用同一个父类类型的引用来调用不同子类的方法,实现不同的功能。这增加了程序的灵活性和可扩展性。
继承,方法重写,父类引用指向子类对象
父类类型 变量名 = new 子类对象 ;
变量名.方法名();
参考回答:
- 三大基本特征:封装、继承、多态。
- 封装
- 封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,提供对应的函数给外部进行调用,内部实现对外部调用不可见,外部调用无需修改或关心内部实现。
- 继承
- 继承基类的方法,并做出自己的改变和/或扩展,子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的。
- 多态
- 基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同,父类中定义的属性和方法被子类继承后或者接口定义的方法被子类实现,通过重写使得父类和子类具有不同的实现,从而同一个属性或方法在父类及其各个子类中具有不同的含义。无法调用子类特有的功能。
3、方法重载和重写有什么区别?
参考回答(适用于 JDK 21):
- 重载(overload):方法名相同、参数列表不同;仅返回类型不同不构成重载。编译器根据参数表达式的编译期类型等规则选择签名。
- 重写(override):子类为继承的实例方法提供符合规则的实现;调用已选定签名的实例方法时,再根据对象的运行时类型选择实现。静态方法是隐藏,不是重写;字段也不参与这种动态分派。
依据:JLS 21 §8.4.8、§8.4.9。
4、==和equals区别(了解)
回答提示:
==操作符:它主要用于比较基本数据类型的值是否相等,比如两个整数是否相等。对于引用数据类型,==比较的是两个对象的内存地址是否相同,即它们是否是同一个对象的引用。简单来说,==看的是“是不是同一个东西”。equals方法:它是Object类的一个方法,用于比较两个对象的内容是否相等。默认情况下,equals方法的行为和==对于引用类型是一样的,即比较内存地址。但是,很多类(如String、Integer等)都重写了equals方法,使其能够比较对象的内容是否相等,而不是内存地址。简单来说,equals看的是“内容是否一样”。
参考回答:
- ==:对于基本数据类型,直接比较值;对于引用数据类型,比较引用(内存地址)。
- equals():首先检查两个对象是否为同一个对象的引用(这通常是通过==运算符完成的,但具体实现可能有所不同),如果不是,则根据类的具体实现来比较对象的内容。许多类(如String、Integer等)都重写了equals()方法以提供更有意义的比较逻辑。
5、equals() 既然已经能实现对比的功能了,为什么还要 hashCode() 呢?(了解)
回答提示:
- 想象一下你有一堆对象,你想要快速地找出里面是否有某个特定的对象。如果只用
equals()方法逐个比较,效率会很低,因为equals()可能会比较对象的很多内部细节。 - 而
hashCode()方法则是用来快速判断两个对象在逻辑上是否可能相等的一个“捷径”。如果两个对象的hashCode()值不同,那么根据hashCode()的设计原则,这两个对象在逻辑上一定不相等,我们甚至不需要调用equals()方法就可以知道结果。这样,我们可以先将对象存储或查找在基于哈希值的集合中(如HashSet、HashMap),大大减少了不必要的equals()调用,提高了效率。 **hashCode()**方法通过提供一个快速的初步判断,帮助我们缩小了可能相等的对象的范围,与**equals()**方法一起工作,实现了高效的对象比较和查找。
参考回答:
- 因为重写的 equals() 里一般比较的比较全面比较复杂,这样效率就比较低,而利用 hashCode() 进行对比,则只要生成一个 hash 值进行比较就可以了,效率很高,那么 hashCode() 既然效率这么高为什么还要 equal() 呢?
- 因为 hashCode() 并不是完全可靠,有时候不同的对象他们生成的 hashcode 也会一样(生成 hash 值得公式可能存在的问题),所以 hashCode() 只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出结论:
- equals() 相等的两个对象他们的 hashCode() 肯定相等,也就是用 equals() 对比是绝对可靠的。
- hashCode() 相等的两个对象他们的 equals() 不一定相等,也就是 hashCode() 不是绝对可靠的。
6、String、StringBuilder和StringBuffer的区别?
回答提示:
- String是final修饰的,不可变,每次操作都会产生新的String对象.
- StringBuffer是在原对象上操作,是线程安全的,是synchronized修饰的。
- StringBuilder是在原对象上操作,是线程不安全的。
- 性能:StringBuilder > StringBuffer > String
参考回答:
- 字符串类型
String是不可变的,即一旦创建后,其值不可被修改。相比之下,StringBuilder和StringBuffer是可变的,允许对其内容进行修改。其中,StringBuffer是线程安全的,而StringBuilder是非线程安全的。
7、String str=new String("IT枫斗者")创建了几个对象?
参考回答:
- 1个或者2个
new String("IT枫斗者");- 相信很多小伙伴们听到的答案很多,有说创建了1个对象,也有说创建了2个对象。答案对,也不对,关键是要知道问题底层的原理。
String str1 = "IT枫斗者"; // 在常量池中
String str2 = new String("IT枫斗者"); // 在堆上- 当直接赋值时,字符串“IT枫斗者”会被存储在常量池中,只有1份,此时的赋值操作等于是创建0个或1个对象。如果常量池中已经存在了“IT枫斗者”,那么不会再创建对象,直接将引用赋值给str1;如果常量池中没有“IT枫斗者”,那么创建一个对象,并将引用赋值给str1。
- 那么,通过new String(“IT枫斗者”);的形式又是如何呢?答案是1个或2个。
- 当JVM遇到上述代码时,会先检索常量池中是否存在“IT枫斗者",IT枫斗者"这个字符串,则会先在常量池中创建这个一个字符串。然后再执行new操作,会在堆内存中创建一个存储“IT枫斗者”的String对象,对象的引用赋值给str2。此过程创建了2个对象。
- 当然,如果检索常量池时发现已经存在了对应的字符串,那么只会在堆内创建一个新的String对象,此过程只创建了1个对象。
8、接口和抽象类的区别?(最少要回答出1,2,3,5,7)
参考回答:
1. 定义与目的
- 接口:是一种行为规范或约定,它定义了对象应该做什么,而不关心对象具体如何做。接口没有实例字段和构造器;方法能否包含实现取决于版本和修饰符:Java 8 起支持
default、static方法,Java 9 起还支持private方法。 - 抽象类:是一种不能被实例化的类,它提供了一种模板,用于被其他类继承并实现其中的抽象方法。抽象类可以包含方法的实现,也可以不包含(即抽象方法)。
2. 成员变量
- 接口:字段隐含为
public static final,必须初始化;引用不能重新赋值,但被引用的可变对象仍可能改变。 - 抽象类:可以包含字段、属性、方法(包括抽象方法和非抽象方法)、构造器等。
3. 方法
- 接口:未写
private、default、static的接口方法隐含为public abstract,不能有方法体;Java 8 起default实例方法和static方法有实现,Java 9 起private实例方法和private static方法也可以有实现。静态方法通过声明接口调用,私有方法不被实现类继承。 - 抽象类:方法可以是抽象的,也可以是非抽象的。抽象方法没有具体实现,需要在子类中重写;非抽象方法可以有具体实现。抽象类中的方法可以有不同的访问级别。
4. 实例化
- 接口:不能被实例化,它只能被类实现。
- 抽象类:不能被直接实例化,但可以通过其子类(如果子类不是抽象的)来实例化。
5. 继承与实现
- 接口:一个类可以实现多个接口,从而获取多个接口中的方法。这是接口实现多态性的一个重要方式。
- 抽象类:一个类只能继承一个抽象类(在大多数面向对象的编程语言中,如Java)。这种单继承的特性限制了抽象类的使用范围。
6. 扩展性
- 接口:新增抽象方法通常要求具体实现类补齐实现,不能笼统说“添加方法无需修改实现类”;新增
default方法可以提供缺省行为,但仍要检查同签名默认方法冲突和既有业务语义。 - 抽象类:扩展性相对较差,如果添加新的抽象方法,所有子类都需要实现这个方法,这可能会影响到现有的代码。
7. 设计层面
- 抽象类:是对类的抽象,是一种模板式设计。它对整个类进行抽象,包括属性和行为,子类必定是抽象类的一种类型,继承是“是不是”的关系。
- 接口:是对行为的抽象,是一种辐射式设计。它是对类局部(行为)的抽象,接口实现是“有没有”的关系,比如一个类是否能实现某个接口,取决于它是否具备该接口所定义的行为
9、浅拷贝和深拷贝?
回答提示:
- 浅拷贝仍然指向原始对象的内存地址。这意味着,如果原始对象中的子对象发生了变化,浅拷贝得到的对象也会受到影响。浅拷贝适用于对象结构较简单,不包含复杂数据结构或不需要独立修改子对象的情况。
- 深拷贝递归地复制了对象内部的所有子对象或属性,创建全新的内存空间来存储这些对象。因此,深拷贝得到的对象与原始对象是完全独立的,对其中一个对象的修改不会影响到另一个对象。深拷贝适用于需要保持数据独立性和完整性的场景,如数据备份、多线程编程等。
参考答案:
1. 定义与本质
- 浅拷贝(Shallow Copy):
- 创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行逐位复制;如果字段是引用类型的话,则复制引用但不复制引用的对象。
- 因此,原始对象及其副本引用同一个对象。
BeanUtils.copyProperties方法用于将一个bean的属性值复制到另一个bean中,前提是这两个bean之间有相同的属性名和兼容的属性类型。这种方法在处理JavaBean之间的数据转换时非常有用,尤其是在不同层(如数据访问层和服务层)之间传递数据时。 --- 浅拷贝- 深拷贝(Deep Copy):
- 创建一个新对象,然后递归地将当前对象的所有非静态字段复制到该新对象,同时复制这些非静态字段引用的所有对象。
- 这意味着副本与原始对象完全独立,修改副本不会影响原始对象,反之亦然。
你通常会使用 JSONArray.parseArray 方法来将一个 JSON 字符串解析成一个 JSONArray 对象,这个对象包含了 JSON 数组中的所有元素。 -- 将对象转为json字符串后,通过JSONArray.parseArray转为对象后,就会出现一个新的对象,并且他们的内存地址不一样,对象中的内容相同2. 内存中的表现
- 浅拷贝:新对象与原始对象在内存中部分重叠,因为它们共享引用类型的字段。
- 深拷贝:新对象与原始对象在内存中完全独立,它们之间没有共享的字段或引用。
3. 修改的影响
- 浅拷贝:修改浅拷贝对象的引用类型字段可能会影响原始对象,因为它们共享同一个引用。
- 深拷贝:修改深拷贝对象不会影响原始对象,因为它们是完全独立的。
10、Java 8 新特性?(能回答出其中四五点即可)
参考回答:
- Lambda表达式:是Java 8最重要的特性之一,允许以更简洁的方式编写匿名函数,特别是与函数式接口结合使用时,可以极大地简化代码。
例子:
List<String> list = Arrays.asList("apple", "banana", "cherry");
list.forEach(item -> System.out.println(item));- 函数式接口:
描述:函数式接口是只包含一个抽象方法声明的接口。Java 8引入了一个注解@FunctionalInterface,用来表示一个接口是函数式接口。
例子:
@FunctionalInterface
public interface GreetingService {
void sayMessage(String message);
}- 方法引用:
描述:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。方法引用和Lambda表达式配合使用,可以使语言的构造更紧凑简洁,减少冗余代码。
例子:
List<String> list = Arrays.asList("apple", "banana", "cherry");
list.forEach(System.out::println);- 默认方法:
描述:默认方法允许你在接口中有默认实现,这样即使实现的类没有实现该方法,也会有默认的行为。
例子:
public interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}- Stream API:
描述:新添加的Stream API(java.util.stream)把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
例子:
List<String> myList = Arrays.asList("apple", "banana", "cherry");
myList.stream().filter(s -> s.contains("a")).forEach(System.out::println);- Optional类:
描述:Optional类是一个表示值存在或缺失的容器;Optional 引用本身不应为 null。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
例子:
Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"- 新的日期时间API:
描述:Java 8引入了新的日期时间API,在java.time包下,改进了时间日期的处理。
例子:
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.of(12, 20);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.DECEMBER, 12, 12, 20);
dateTime.plusDays(12); // 日期加12天11、反射机制在项目中的实际应用?(了解)
参考回答:
- 自定义注解里有反射的应用
- 写上注解类,注解本身是一种特殊的接口,可以在接口里面设置属性,然后通过反射对属性进行处理就可以完成注解的功能,注解的功能是我们自己进行实现的
- 写完注解类之后,我们可以把这个注解写到其他的类或者属性上,我们通过
Class<T>clazz 来获取具体类的属性和方法,然后通过获取属性或者方法上的注解值,进行逻辑操作实现注解的功能 - 通过反射记录日志
- 比较两个VO对象的值是否发生变化,用于记录日志或审批
12、Java是值传递还是引用传递?(了解)
参考回答:
- Java是值传递。
- 当传的是基本类型时,传的是值的拷贝,对拷贝变量的修改不影响原变量;当传的是引用类型时,传的是引用地址的拷贝,但是拷贝的地址和真实地址指向的都是同一个真实数据,因此可以修改原变量中的值;当传的是String类型时,虽然拷贝的也是引用地址,指向的是同一个数据,但是String的值不能被修改,因此无法修改原变量中的值。
13、Java的内部类
回答提示(以 JDK 21 为准):
- 成员内部类:没有
static的成员类,实例关联一个外部类实例,可访问其私有成员;在外部通常用outer.new Inner()创建。 - 静态嵌套类:常称“静态内部类”,但按 JLS 分类不是内部类(inner class)。无需外部类实例,用
new Outer.Nested()创建;访问外部类实例成员需要显式对象引用。 - 局部内部类:声明在方法或代码块中,受词法作用域限制。普通局部类不能用访问修饰符或
static修饰;捕获局部变量、参数时,变量须为final或事实上不可变(effectively final,Java 8 起),并满足确定赋值规则。 - 匿名内部类:通过
new 父类或接口() { ... }声明并实例化。不能显式声明构造器,可用实例初始化块;没有源码名称不意味着只能创建一个对象。
参考回答:
- 成员内部类
- 可访问外部类的私有成员;外部类也能在访问规则允许时访问内部类私有成员,但访问实例成员需要相应实例。并非所有修饰符都适用,例如普通成员类不能用
transient修饰。 - 静态内部类
- 静态嵌套类不隐含关联外部类实例。同名成员遮蔽外部静态成员时,可用
Outer.成员名区分;外部实例成员可通过传入或创建的外部对象访问。 - 局部内部类
- 普通局部类可用
final或abstract。处于实例上下文时可访问外部实例;声明在静态上下文中的局部类、匿名类没有隐含外部实例,不能直接访问Outer.this或外部实例字段。 - 捕获的局部引用不能重新赋值,但引用指向的可变对象仍可修改;“事实上不可变”约束的是变量,不是对象内容。
- 匿名内部类
- 表达式指定一个直接父类或一个直接实现接口;实现接口时,其直接父类为
Object。常用父类/接口引用接收,但不是强制要求:Java 10 起var可保留推断出的匿名类类型,并访问其可访问的新增成员。对象的getClass()也能取得运行时类信息。
静态成员的版本边界:Java 15 及以前,内部类只能声明属于常量变量的静态成员,即以常量表达式初始化的 final 基本类型或 String 变量,不能声明静态初始化块。Java 16 起放宽限制;JDK 21 的成员内部类、局部内部类、匿名内部类均可声明静态字段、静态方法、静态成员类和静态初始化块。注意:允许类体内有静态成员,不等于允许把普通局部类或匿名类本身声明为 static。
机制上,static 成员属于类,不属于单个对象;其静态上下文没有当前实例,也不能直接使用外部实例或捕获外围局部变量。同一匿名类表达式被多次执行时,每次成功求值都会创建新对象;在同一类加载上下文中,这些对象共享该匿名类的静态字段,各自保有实例字段。两个不同位置的匿名类表达式则声明不同的类。
public class InnerDemo {
static Runnable create() {
return new Runnable() { // 同一处匿名类表达式可以反复执行
static int total; // Java 16+;Java 15 及以前不允许
int own;
@Override
public void run() {
System.out.println(++total + ":" + ++own);
}
};
}
public static void main(String[] args) {
Runnable a = create();
Runnable b = create();
System.out.println(a == b); // false:两个实例
System.out.println(a.getClass() == b.getClass()); // true:同一个类
a.run(); // 1:1
b.run(); // 2:1(顺序执行;计数示例不提供并发保证)
}
}依据:JLS 21 §8.1.3 内部类与外部实例(含 Java 16 规则变更)、§14.3 局部类、§15.9.4–15.9.5 创建实例与匿名类。
14、什么关键字阻止该变量被序列化到文件中?
- 在变量声明前加上 Transient 关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
- 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串 等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在 客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的 数据安全。
版本核对与练习
接口规则依据 JLS 21 §9.4;版本对照、冲突示例与 JDK 21 实测见 接口方法实用指南。继续到 Java 面试资料 做复习记录。
