Java基础

1、Java 的主要特性有哪些?

●面向对象:支持封装、继承和多态等面向对象编程的基本概念。

●平台无关性:编译后的字节码可以在任何支持Java的平台上运行,实现了“编写一次,到处运行”。

●自动内存管理:通过垃圾回收机制自动管理内存,减轻程序员负担。

●安全性高:提供了多种安全机制,如字节码验证、沙箱安全模型等。

●多线程支持:内置了对多线程编程的支持,便于开发高性能应用。

●丰富的API库:拥有大量的类库,涵盖了网络编程、数据库连接、图形用户界面设计等多个方面。

2、public、private、protected、default的区别

修饰符 类内部 同包 子类 任何地方
public Yes Yes Yes Yes
protected Yes Yes Yes
default(包访问权限) Yes Yes
private Yes

3、基本数据类型有哪些?每种类型的默认值是什么?

Java中的基本数据类型分为四类,共8种,它们分别用于表示整数、浮点数、字符和布尔值。这些基本数据类型都是Java语言的构建块,用于存储简单的值。

Java基本数据类型的总结表

数据类型 字节数 取值范围 默认值
byte 1 -128到127 0
short 2 -32,768到32,767 0
int 4 -231到231-1 0
long 8 -263到263-1 0L
float 4 约为1.4E-45到3.4E+38 0.0f
double 8 约为4.9E-324到1.8E+308 0.0d
char 2 0到65,535 ‘\u0000’
boolean JVM定义 true或false false

重要提示❗ ❗ ❗ 1、默认值:以上默认值是指这些基本数据类型的实例变量(成员变量)在类中未被显式初始化时的默认值。在局部变量(如方法内部的变量)中,必须显式初始化后才能使用,否则编译会报错。 2、数据类型转换:在Java中,基本数据类型可以进行自动类型转换(从低精度到高精度,如int到long),但从高精度转换到低精度时需要进行显式类型转换(强制转换),否则可能会导致数据精度的丢失。

4、解释面向对象编程的四大基本特征:封装、继承、多态和抽象。

●封装:通过隐藏数据和实现细节,保护对象的状态。

●继承:通过继承父类的属性和方法,实现代码的重用和扩展。

●多态:通过方法重写和接口实现,允许同一方法在不同对象中表现出不同的行为。

●抽象:通过抽象类和接口,提供统一的接口,隐藏实现细节,简化编程接口。

5、接口和抽象类有什么区别?

●接口:适合用来定义类的行为规范,强调“能做什么”(What to do)。它适合定义松散耦合的模块,允许多重实现。

●抽象类:适合用来提供基础的功能和默认实现,强调“是什么”(What is it)。它更适合描述具有共同特性的一组类,并且允许部分代码重用。

根据具体需求选择接口或抽象类,在设计和开发中实现代码的高内聚和低耦合。

6、重载和重写的区别是什么?

●重载:发生在同一类中,通过相同的方法名但不同的参数列表(参数的类型、数量或顺序)来实现。重载体现了编译时的多态性。

●重写:发生在子类与父类之间,通过在子类中重新定义父类方法(该方法与父类中的方法具有相同的名称、参数列表和返回类型)来实现。重写体现了运行时的多态性。

这两者提供了不同层面的多态性,使得Java程序可以更灵活地处理方法调用。

7、什么是java序列化?或者请解释Serializable接口的作用

Java序列化是一种机制,通过将Java对象的状态转换为字节流,使其可以被存储或传输,并在需要时恢复(反序列化)为原始对象。序列化通常用于保存对象的状态以便后续恢复,或在网络中传输对象。

Serializable接口的作用主要是标记一个类的对象可以被序列化。通过实现这个接口,Java的对象输出流(ObjectOutputStream)和对象输入流(ObjectInputStream)就知道如何处理该对象的序列化和反序列化。注意,未标记为序列化的类对象在尝试序列化时会抛出NotSerializableException。

此外,如果类中包含不想序列化的字段,可以使用transient关键字修饰这些字段,这样它们在序列化过程中将被忽略。

8、Java程序是如何执行的?

Java程序的执行过程是一个多步骤的过程,从编写代码到程序的最终运行,包括编译、解释、执行等多个环节。以下是Java程序的执行步骤的详细描述:

1. 编写Java源代码

首先,开发者在文本编辑器或集成开发环境(IDE)中编写Java源代码,并将其保存为.java文件。例如,下面是一个简单的Java程序:

1
2
3
4
5
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}

2. 编译Java源代码

在这一步,Java编译器(javac)将Java源代码编译成字节码(Bytecode)。字节码是一种中间表示形式,独立于特定的硬件和操作系统。编译后的字节码保存在.class文件中。

1
javac HelloWorld.java

运行上面的命令后,将生成HelloWorld.class文件,这个文件包含了字节码。

3. 加载字节码

当运行Java程序时,Java虚拟机(JVM)会启动,并由类加载器(Class Loader)将编译好的字节码加载到内存中。JVM中的类加载器系统负责动态地加载、链接和初始化类。

4. 执行字节码

字节码被加载后,JVM中的解释器(Interpreter)或即时编译器(JIT Compiler)开始执行字节码:

●解释器:将字节码逐条解释为机器指令并执行。这种方式的执行速度较慢,但可以快速启动程序。

●即时编译器(JIT Compiler):JIT编译器在运行时将热点代码(常用代码)编译成本地机器码,这样代码在后续执行时不需要再解释,执行速度更快。JIT编译器通过这种方式提高了程序的执行效率。

5. JVM执行与内存管理

JVM不仅负责字节码的执行,还管理程序运行时的内存分配。JVM将内存分为不同的区域,例如堆(Heap)、栈(Stack)、方法区(Method Area)等:

●堆(Heap):用于存储对象实例和数组,JVM的垃圾回收器(Garbage Collector)负责在堆中回收不再使用的对象。

●栈(Stack):用于存储方法调用和局部变量,每个线程有自己独立的栈空间。

●方法区(Method Area):存储类结构(如元数据、常量池、方法数据)和静态变量。

6. 程序运行

在JVM解释或编译字节码的过程中,程序开始运行。对于上面的示例程序,main方法会被JVM执行,输出“Hello, World!”。

7. 程序终止

当main方法执行完毕,并且没有其他非守护线程在运行时,JVM会终止程序的执行并释放资源。

总结

Java程序的执行过程从编写源代码开始,经过编译生成字节码,然后通过JVM加载和解释(或JIT编译)字节码,最终在目标平台上运行。JVM的跨平台性使得Java程序只需编写一次,就可以在不同的操作系统上运行,这也是Java“Write Once, Run Anywhere”理念的体现。

9、什么是 Java 内部类?它有什么作用?

Java内部类(Inner Class)是定义在另一个类内部的类。Java支持将类定义在另一个类或方法内部,这样的类被称为内部类。内部类可以帮助我们实现更加模块化和更高内聚性的代码设计,并且可以方便地访问外部类的成员变量和方法。

1. Java内部类的分类

👉成员内部类(Member Inner Class):

●定义在外部类内部,并且不使用static修饰符的类。

●它可以直接访问外部类的所有成员,包括private成员。

1
2
3
4
5
6
7
8
9
class OuterClass {
private String message = "Hello, World!";

class InnerClass {
void display() {
System.out.println(message); // 访问外部类的成员变量
}
}
}

👉静态内部类(Static Nested Class):

●使用static修饰的内部类,静态内部类不能访问外部类的实例成员(除非通过外部类的对象)。

●它可以直接创建实例,而不需要外部类的实例。

1
2
3
4
5
6
7
class OuterClass {
static class StaticInnerClass {
void display() {
System.out.println("Inside Static Inner Class");
}
}
}

👉局部内部类(Local Inner Class):

●定义在方法或代码块内部的类,它只能在该方法或代码块内部使用。

●局部内部类只能访问外部类的成员变量和方法,以及方法的final或effectively final变量。

1
2
3
4
5
6
7
8
9
10
11
class OuterClass {
void outerMethod() {
class LocalInnerClass {
void display() {
System.out.println("Inside Local Inner Class");
}
}
LocalInnerClass inner = new LocalInnerClass();
inner.display();
}
}

👉匿名内部类(Anonymous Inner Class):

●没有名字的内部类,通常用来创建继承自一个类或实现一个接口的对象实例。

●常用于需要简化代码的地方,如事件处理器或回调函数。

1
2
3
4
5
6
7
8
9
10
11
class OuterClass {
void createThread() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Inside Anonymous Inner Class");
}
});
t.start();
}
}

2. 内部类的作用

●封装性增强:

○内部类可以很好地封装不希望对外暴露的逻辑,它们的存在仅仅是为了与外部类的紧密联系。

○可以将与外部类关系紧密的类逻辑放在内部类中,从而实现更高的内聚性。

●访问外部类成员:

○内部类可以直接访问外部类的成员变量和方法,即使它们是private的。这使得在实现外部类的功能时,可以更方便地操作其数据。

●实现特定的接口:

○匿名内部类和局部内部类非常适合用于实现回调函数、事件处理器或接口的实例化,特别是在GUI编程中。

●创建更加简洁和模块化的代码:

○使用内部类可以将逻辑紧密相关的代码放在一起,而不是散布在多个类文件中,从而使代码更简洁和易于维护。

3. 内部类的使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class OuterClass {
private String message = "Hello, World!";

class InnerClass {
void display() {
System.out.println(message); // 可以访问外部类的成员
}
}

static class StaticInnerClass {
void display() {
System.out.println("Inside Static Inner Class");
}
}

void outerMethod() {
class LocalInnerClass {
void display() {
System.out.println("Inside Local Inner Class");
}
}
LocalInnerClass inner = new LocalInnerClass();
inner.display();
}

void createThread() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Inside Anonymous Inner Class");
}
});
t.start();
}
}

public class Main {
public static void main(String[] args) {
// 成员内部类
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.display();

// 静态内部类
OuterClass.StaticInnerClass staticInner = new OuterClass.StaticInnerClass();
staticInner.display();

// 局部内部类和匿名内部类
outer.outerMethod();
outer.createThread();
}
}

总结

内部类在Java中是一种强大的工具,提供了增强封装性、逻辑组织性和代码简洁性的方法。通过合理使用内部类,可以在特定场景下更好地组织代码,提高代码的可维护性和可读性。

10、GC 如何调优?

GC 调优的核心思路就是:尽可能的使对象在年轻代被回收,减少对象进入老年代。

具体调优还是得看场景根据 GC 日志具体分析,常见的需要关注的指标是 Young GC 和 Full GC 触发频率、原因、晋升的速率 、老年代内存占用量等等。比如发现频繁会产生 Full GC,分析日志之后发现没有内存泄漏,只是 Young GC 之后会有大量的对象进入老年代,然后最终触发 Full GC。所以就能得知是 Survivor 空间设置太小,导致对象过早进入老年代,因此调大 Survivor 。或者是晋升年龄设置的太小,也有可能分析日志之后发现是内存泄漏、或者有第三方类库调用了 System.gc等等。反正具体场景具体分析,核心思想就是尽量在新生代把对象给回收了。

11、JDK 动态代理与 CGLIB 区别?

JDK 动态代理是基于接口的,所以要求代理类一定是有定义接口的。

CGLIB 基于ASM字节码生成工具,它是通过继承的方式来实现代理类,所以要注意 final 方法。

12、说一下对注解的理解?

注解其实就是一个标记,可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。

有了标记之后,我们就可以在解析的时候得到这个标记,然后做一些特别的处理,这就是注解的用处。

注解生命周期有三大类,分别是:

●RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件。

●RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了。

●RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息。

13、反射用过吗?

如果你用过,那就不用我多说啥了,场景说一下,然后等着面试官继续挖。

如果没用过,那就说生产上没用过,不过私下研究过反射的原理。

反射其实就是Java提供的能在运行期可以得到对象信息的能力,包括属性、方法、注解等,也可以调用其方法。一般的编码不会用到反射,在框架上用的较多,因为很多场景需要很灵活,所以不确定目标对象的类型,届时只能通过反射动态获取对象信息。

14、双亲委派知道不?来说说看?

双亲委派模型(Parent Delegation Model) 是 Java 中类加载机制的重要设计模式。它通过分层次的类加载器结构和委派机制,确保了 Java 类的安全加载,防止核心类库被篡改或替换。

白话一点:如果一个类加载器需要加载类,那么首先它会把这个类加载请求委派给父类加载器去完成,如果父类还有父类则接着委托,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

Java 自身提供了 3 种类加载器:

●Bootstrap ClassLoader(启动类加载器):是 JVM 自带的最顶层的类加载器,用于加载核心类库(如 rt.jar),如 java.lang.*、java.util.*等核心类。它是用原生代码实现的,负责加载 JDK 中的核心类。

●Extension ClassLoader(扩展类加载器):负责加载扩展类库,通常位于 JAVA_HOME/lib/ext 目录下的类库。它由 Java 实现,通常是 sun.misc.Launcher$ExtClassLoader 的实例。

●Application ClassLoader(应用程序类加载器):也称为系统类加载器,负责加载应用程序类路径(CLASSPATH)下的类和库。它也是由 Java 实现的,通常是 sun.misc.Launcher$AppClassLoader 的实例。

15、JDK 和 JRE 的区别?

JRE(Java Runtime Environment)指的是 Java 运行环境,包含了 JVM 和 Java 类库等。

JDK(Java Development Kit)可以视为 JRE 的超集,还提供了一些工具比如各种诊断工具:jstack,jmap,jstat等。

16、Java 按值传递还是按引用传递?

Java 只有按值传递,不论是基本类型还是引用类型。

JVM 内存有划分为栈(Stack)和堆(Heap),局部变量和方法参数是在栈上分配的,基本类型和引用类型都占 4 个字节,当然 long 和 double 占 8 个字节。而对象所占的空间是在堆(Heap)中开辟的,引用类型的变量存储对象在堆中地址来访问对象,所以传递的时候可以理解为把变量存储的地址给传递过去,因此引用类型也是值传递。

17、泛型有什么用?泛型擦除是什么?

作用:

1.泛型可以把类型当作参数一样传递,使得像一些集合类可以明确存储的对象类型,不用显示地强制转化(在没泛型之前只能是Object,然后强转)。

2.并且在编译期能识别类型,类型错误则会提醒,增加程序的健壮性和可读性。

泛型擦除:指参数类型其实在编译之后就被抹去了,也就是生成的 class 文件是没有泛型信息的,所以称之为擦除。【在代码里写死的泛型类型是不会被擦除的!例:private Example<String> example;】

18、说说强、软、弱、虚引用?

Java 根据其生命周期的长短将引用类型又分为强引用、软引用、弱引用、幻象引用。

●强引用:就是我们平时 new 一个对象的引用。当 JVM 的内存空间不足时,宁愿抛出 OutOfMemoryError 使得程序异常终止,也不愿意回收具有强引用的存活着的对象。

●软引用:生命周期比强引用短,当 JVM 认为内存空间不足时,会试图回收软引用指向的对象,也就是说在 JVM 抛出 OutOfMemoryError 之前,会去清理软引用对象,适合用在内存敏感的场景。

●弱引用:比软引用还短,在 GC 的时候,不管内存空间足不足都会回收这个对象,ThreadLocal中的 key 就用到了弱引用,适合用在内存敏感的场景。

●虚引用:也称幻象引用,之所以这样叫是因为虚引用的 get 永远都是 null,称为get 了个寂寞,所以叫虚。

19、Exception 和 Error 的区别知道吗?

Exception :是程序正常运行过程中可以预料到的意外情况,应该被开发者捕获并且进行相应的处理。

Error :是指在正常情况下不太可能出现的情况,绝大部分的 Error 都会导致程序处于不正常、不可恢复的状态,也就是挂了。所以不便也不需被开发者捕获,因为这个情况下你捕获了也无济于事。

Exception和Error都是继承了Throwable类,在Java代码中只有继承了Throwable类的实例才可以被throw或者被catch。

20、深拷贝和浅拷贝?

深拷贝:完全拷贝一个对象,包括基本类型和引用类型,堆内的引用对象也会复制一份。

浅拷贝:仅拷贝基本类型和引用,堆内的引用对象和被拷贝的对象共享。

所以假如拷贝的对象成员间有一个 list,深拷贝之后堆内有 2 个 list,之间不会影响,而浅拷贝的话堆内还是只有一个 list。因此深拷贝是安全的,浅拷贝的话如果有引用对象则原先和拷贝对象修改引用对象的值会相互影响。

image.png



21、说说Java的集合类吧?

常用的集合有 List、Set、Map 等。

👉List 常见实现类有 ArrayList 和 LinkedList。

●ArrayList 基于动态数组实现,支持下标随机访问,对删除不友好。

●LinkedList 基于双向链表实现,不支持随机访问,只能顺序遍历,但是支持O(1)插入和删除元素。

👉Set 常见实现类有:HashSet、TreeSet、LinkedHashSet。

●HashSet 其实就是 HashMap 包了层马甲,支持 O(1)查询,无序。

●TreeSet 基于红黑树实现,支持范围查询,不过基于红黑树的查找时间复杂度是O(lgn),有序。

●LinkedHashSet,比 HashSet 多了个双向链表,通过链表保证有序。

👉Map 常见实现类有:HashMap、TreeMap、LinkedHashMap

●HashMap 基于哈希表实现,支持 O(1) 查询,无序。

●TreeMap 基于红黑树实现,O(lgn)查询,有序。

●LinkedHashMap 同样也是多了双向链表,支持有序,可以很好的支持 LRU(Least Recently Used 最近最少使用,是一种常见的缓存淘汰算法)的实现。

22、同步、异步、阻塞、非阻塞 IO 的区别?

这里用简单直白的语言来总结【同步、异步、阻塞、非阻塞IO】的区别:

●同步IO:发起IO操作后等待结果,完成后才继续。

●异步IO:发起IO操作后不等待,继续执行其他任务,完成后得到通知。

●阻塞IO:数据未准备好时等待,不做其他事情。

●非阻塞IO:数据未准备好时不等待,立即返回,可做其他事情。

结合这组概念,你可以有三种组合:同步阻塞(BIO)、同步非阻塞(NIO 通常配合轮询机制)、异步非阻塞(AIO 最常见的高效IO模型)。异步和非阻塞都是为了提高程序的并发性和响应性而设计的。

23、说说Java 并发工具类-CyclicBarrier?

从名字分析,这是一个可循环的屏障。

屏障的意思是:让一组线程都运行到同一个屏障点之后,线程会阻塞等待所有线程都达到这个屏障点,然后所有线程才得以继续执行。

**原理:**首先设置了达到屏障的线程数量,当线程调用 await 的时候计数器会减一,如果计数器减一不等于 0 的时候,线程会调用 condition.await 进行阻塞等待。如果计数器减一的值等于0,说明最后一个线程也到达了屏障,于是如果有 barrierCommand 就执行 barrierCommand,然后调用 condition.signalAll 唤醒之前等待的线程,并且重置计数器(便于循环使用),然后开启下一代。

image.png 

24、Synchronized 和 ReentrantLock 区别?

●Synchronized 和 ReentrantLock 都是可重入锁,ReentrantLock 需要手动解锁,而 Synchronized 不需要。

●ReentrantLock 支持设置超时时间,可以避免死锁,比较灵活,并且支持公平锁,可中断,支持条件判断。

●Synchronized 不支持超时,非公平,不可中断,不支持条件。

总的而言,一般情况下用 Synchronized 足矣,比较简单,而 ReentrantLock 比较灵活,支持的功能比较多,所以复杂的情况用 ReentrantLock 。

25、说说线程的生命周期?

线程的生命周期可以精简为五个主要状态:

1.新建(New):线程对象被创建但还未启动,start()方法尚未调用。

2.可运行(Runnable):已调用start()方法,线程可以执行,正在或等待CPU时间片。

3.阻塞(Blocked):线程试图获取同步锁未果,暂时无法继续执行,等待获取锁。

4.等待(Waiting)/计时等待(Timed Waiting):线程在等待其他线程执行特定动作(如通过wait()或join()),可能有超时也可能没有。为了简化,可以将这两种状态统称为“等待”状态。

5.终止(Terminated):线程执行完毕或者因异常退出,生命周期结束。