java基础
1. Java面试核心概念
Java三大特点
:平台无关性、面向对象、内存管理。
- 平台无关性:通过JVM(Java虚拟机)实现。源代码编译成字节码(.class文件),可在任何安装了相应JVM的操作系统上运行。
- 面向对象 :核心是"对象"。对象是类的一个运行实例,且对象实际是堆内存中的一块具体空间
- 内存管理:Java具备自动垃圾回收机制(GC),开发者无需手动释放内存,这与C/C++不同。
2. JDK、JRE、JVM关系
三者是包含与被包含的关系,从大到小依次为:JDK > JRE > JVM
- JDK (Java Development Kit):完整的Java开发工具包,包含JRE和编译器(javac)、调试器(jdb)等。
- JRE (Java Runtime Environment):Java运行环境,包含JVM和Java类库(rt.jar)。
- JVM (Java Virtual Machine):核心组件,负责执行字节码、内存管理、垃圾回收等。不同平台有不同版本的JVM。
3. 值传递与引用传递
-
核心原则:Java中只有值传递,不存在真正的引用传递。
-
基本数据类型:传递的是值的副本,适用于基本数据类型。方法内部对参数的修改不影响原始变量。
-
引用数据类型:传递的是对象引用的副本(地址值),而非对象本身。如果修改对象内部的状态,会影响原始对象;如果修改引用指向,则不影响原始对象。
4. Java数据类型
-
八种基本数据类型:byte, short, int, long, float, double, char, boolean。
-
三种引用数据类型:接口, 数组, 类。
类型转换:
- 自动类型转换:由小范围向大范围转换(如int -> long),安全。
- 强制类型转换:由大范围向小范围转换(如long -> int),可能导致数据溢出或精度丢失。
- 字符串与基本类型转换 :在字符串和数值 / 布尔等类型之间互相转换,字符串 → 基本类型 :使用对应包装类的
parseXxx()方法,基本类型 → 字符串 :方式 1:String.valueOf(基本类型值),方式 2:基本类型值 + "" - 数值与字符 / 包装类之间的转换 :
int→char:直接强转(基于 ASCII/Unicode 编码),char→int:直接赋值或强转。数值类型之间的包装类转换 :通过包装类提供的方法实现,例如:Integer→Long:Integer.intValue()后再装箱,Double→Float:Double.doubleValue()后再装箱。
5. BigDecimal与Double
- Double的局限性 :在计算机底层以二进制科学计数法存储,无法精确表示十进制小数(如0.1),会导致精度丢失和溢出问题,尤其在金融计算中风险极高。
- BigDecimal的优势 :BigDecimal是Java提供的高精度数值类型,使用字符串作为底层存储,能够精确表示任意精度的十进制小数,完全避免精度丢失问题。
6. 自动装箱与拆箱
- 概念:将基本数据类型与对应的包装类(如int与Integer)之间进行自动转换。
- 自动装箱:基本类型 -> 包装类。使用Integer.valueof()方法。
- 自动拆箱:包装类 -> 基本类型。使用intvalue()方法。
- 128陷阱:Integer.valueOf()方法会对-128到127范围内的整数进行缓存复用,new Integer()创建的对象不会复用缓存中的对象。
7.面向对象三大特征
- 封装:把属性私有化,对外提供公共方法访问。。
- 继承:子类可以继承父类的属性和方法,实现代码的复用。本质是代码的复用。java只支持单继承(防止资源浪费)
- 多态:父类引用指向子类对象,调用重写方法时,执行子类的逻辑。主要体现为方法重载、方法重写、接口实现及向上转型,向下转型。
8.多态
多态是面向对象编程的核心特性,主要体现在以下四个方面:
方法重载(编译时多态)
- 定义 :同一类中存在多个同名方法,参数列表(类型、数量、顺序)不同。
- 绑定时机 :编译器在编译阶段根据传入参数确定调用版本。
- 核心作用:实现 "同名不同参" 的方法复用,提升代码可读性。
- 示例 :
add(int a, int b)与add(double a, double b)会根据参数类型被分别调用。
方法重写(运行时多态,核心实现方式)
- 定义:子类对父类同名方法提供具体实现,方法签名(方法名、参数列表)必须与父类一致。
- 绑定时机 :JVM 在运行阶段 根据对象的实际类型确定调用哪个子类的方法版本。
- 核心作用:实现 "父类引用指向子类对象,调用子类实现",是多态最典型的表现形式。
- 示例 :父类
Animal定义sound(),子类Dog重写为bark(),Cat重写为meow(),运行时自动匹配。
接口与实现(多态的扩展体现)
- 定义 :多个类实现同一个接口,用接口类型引用指向不同实现类对象。
- 调用特点:调用接口方法时,会自动执行对应实现类的具体逻辑,调用方式保持一致。
- 核心作用:解耦 "规范" 与 "实现",便于扩展和替换不同业务实现。
- 示例 :
Dog、Cat都实现Animal接口,用Animal animal = new Dog()调用makeSound()时,执行Dog的实现。
向上转型与向下转型(多态的类型转换基础)
-
向上转型
:父类引用指向子类对象(自动完成),屏蔽子类差异,统一调用父类 / 接口方法。
- 例:
Animal a = new Dog();
- 例:
-
向下转型
:将父类引用转回子类类型(需手动强转),用于调用子类特有方法。
- 风险:需用
instanceof判断类型,否则可能抛出ClassCastException。
- 风险:需用
-
核心作用:支撑 "父类 / 接口调用、子类实现" 的多态场景,实现类型灵活切换。
重载与重写有什么区别?
- 重载(Overloading)指的是在同一个类中,可以有多个同名方法,它们具有不同的参数列表(参数类型、参数个数或参数顺序不同),编译器根据调用时的参数类型来决定调用哪个方法。
- 重写(Overriding)指的是子类可以重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过 @Override 注解来明确表示这是对父类方法的重写。
重载是指在同一个类中定义多个同名方法,而重写是指子类重新定义父类中的方法。
9.抽象类与接口
- 抽象类:用于描述类的共同性,可以包含成员变量、构造方法、具体方法和抽象方法。不能被直接实例化,只能被继承。抽象类当中可以定义抽象方法,抽象方法子类必须重写。
- 接口:用于对行为进行规范,定义了常量和抽象方法。一个类可以实现多个接口。接口当中的变量都是常量。
- 区别:抽象类描述的是"是什么",接口描述的是"做什么"。抽象类可以有方法实现,接口则不能(Java 8后接口可有default方法)。
10.final关键字
- 修饰类:该类不能被继承。
- 修饰方法:该方法不能被重写。
- 修饰变量:对于基本数据类型,值不能被改变;对于引用类型,引用地址不能改变,但可以改变引用指向对象的内容。
11.抽象类能加final修饰吗?
不能,Java 中的抽象类是用来被继承的,而 final 修饰符用于禁止类被继承或方法被重写,因此,抽象类和 final 修饰符是互斥的,不能同时使用。
12.静态变量与静态方法
- 静态变量:属于类,所有实例共享一个副本,通过类名或对象均可访问,但推荐通过类名访问。
- 静态方法:属于类,不依赖于实例,可通过类名直接调用。
- 加载时机:静态变量和静态方法在类加载时初始化,只分配一次内存。
13.static的作用
static 核心作用是将类成员与类本身关联,而非与类实例(对象)关联,让成员属于类、所有实例共享。
修饰变量(静态变量)
- 归属:属于类本身,所有对象共享同一份数据,内存中仅存一份副本。
- 访问方式 :通过
类名.变量名直接访问,无需创建对象。
修饰方法(静态方法)
- 归属 :属于类,不依赖任何实例,因此不能直接访问非静态成员(非静态成员依赖对象存在),但可访问静态成员。
- 访问方式 :通过
类名.方法名直接调用,无需创建对象。
修饰代码块(静态代码块)
- 执行时机 :在类加载时执行,且仅执行一次,执行顺序优先于构造方法和非静态代码块。
修饰内部类(静态内部类)
- 特性 :不依赖外部类实例,可独立存在;不能直接访问外部类非静态成员,需通过外部类实例访问。
14.深拷贝与浅拷贝
-
浅拷贝:复制对象本身及其内部的值字段,但对象内部的引用字段仍指向原对象,不进行复制。
-
浅拷贝的逻辑是: 只拷贝对象表层
- 对「值类型字段」:直接复制值(新对象和原对象的这个字段互不影响);
- 对「引用类型字段」:只复制 "内存地址"(新对象和原对象的这个字段指向同一个数据,改一个会影响另一个)。
-
深拷贝:复制对象本身及其内部的所有引用对象,确保新旧对象相互独立。
-
深拷贝的三种核心实现方法
- 手动递归拷贝
- 通过序列化 / 反序列化实现
- 实现Cloneable接口并重写clone()方法
15.序列化与反序列化
- 概念:将对象转换为字节流(序列化)以便在网络或文件间传输,再将字节流还原为对象(反序列化)。
- 实现:需让类实现Serializable接口,并使用ObjectOutputStream进行序列化,ObjectInputStream进行反序列化。
- 局限性:存在安全漏洞、不支持跨语言、序列化后的数据体积较大等问题。
16. 泛型
- 作用 :允许类、接口和方法在定义时使用参数化类型,提高代码的通用性和安全性,在编译期进行类型检查,避免了运行时的强制类型转换(
ClassCastException)。 - 类型擦除:Java 的泛型是"伪泛型",在编译成字节码期间,所有的泛型信息都会被擦除掉,变成 Object 或者其限定的边界类型。
16.1 对象的创建与垃圾回收(GC)机制
- 创建对象的方式 :主要包括
new关键字、反射、克隆(Cloneable接口)和反序列化。 - 对象回收的原则 :无论通过哪种方式创建在堆内存中的对象,当它不再被任何"有效引用"指向时,就会成为 GC 的回收目标。
- 如何判断对象已死?(核心考点)
- 引用计数法(已淘汰) :给对象维护一个计数器。致命缺点:无法解决两个对象互相引用(循环引用)的问题,导致这俩对象永远无法被回收(内存泄漏)。
- 可达性分析算法(正在使用) :以 GC Roots 为起点顺着引用链往下找。如果一个对象到所有 GC Roots 都没有任何路径相连(即不可达),说明它死了,可以被回收。
- 哪些对象可以作为 GC Roots?(八股文必背)
- 虚拟机栈(方法中的局部变量)中引用的对象。
- 方法区中类静态变量(
static)引用的对象。 - 方法区中常量(
final)引用的对象。 - 本地方法栈(Native 方法)中引用的对象。
- finalize() 机制的坑 :
如果对象重写了finalize(),GC 回收前理论上会调用它。注意避坑 :绝对不要依赖它去清理资源(如关闭文件、数据库连接)!由于它优先级极低、执行时机完全不确定,甚至有可能导致对象"复活",还会严重拖慢 GC 的性能。在 JDK 9 以后已被彻底标记为不再推荐使用(@Deprecated)。
17. 反射机制
-
反射的本质:在运行时动态获取类信息(如字段、方法、构造器)并操作对象的能力。
-
获取类对象的方式:主要有三种,分别对应类加载的三个阶段:通过Class.forName(全类名)、通过类名.class、通过对象.getClass()。
-
反射的特点:运行时数据访问、动态数据创建、动态方法获取。
| 方法类型 | 访问权限范围 | 是否包含继承成员 |
|---|---|---|
| getDeclaredXXX() | 所有访问权限(private/protected/default/public) | 不包含 |
| getXXX() | 仅公有(public) | 包含 |
-
暴力反射:通过设置Field对象的setAccessible(true)来访问和修改private修饰的字段,称为暴力反射。
-
反射的应用:在JDBC连接池(Class.forName加载驱动)、Spring框架(通过配置文件动态创建Bean对象)中有广泛应用。
18.代理
1. 代理模式
- 定义 :不直接访问目标对象,而是通过一个 "代理对象" 作为中间层,既可以控制对目标对象的访问(比如权限校验),又能在访问前后增加额外功能(比如日志、计时)。。
2. 静态代理
- 实现方式: 代理类与目标类实现相同的接口,代理类中需要为每一个目标类编写一个独立的代理类。
- 优点: 逻辑清晰,易于理解和实现。
- 缺点: 违反了开闭原则。当目标类增多或接口功能增多时,需要频繁地编写和修改代理类,导致代码冗余且维护困难。
3. 动态代理
-
实现方式 : 代理类不直接实现接口,而是实现一个公共的接口(如java.lang.reflect.InvocationHandler),由JDK在运行时动态生成代理对象。JDK 动态代理 (Java 原生),CGLIB 动态代理(第三方库)
-
核心思想: 代理对象的创建依赖于外部传入的目标对象,使得代理类可以灵活地代理任意一个实现了特定接口的对象。
-
实现步骤:
- 代理类通过构造函数接收目标对象。
- 通过反射技术获取目标对象所实现的接口,从而得知其核心方法。
- 重写接口中的invoke()方法,在调用目标对象方法前后插入业务增强逻辑(如权限验证)。
- 最终通过invoke方法调用目标对象的核心方法。
19.Java注解原理
- 注解的本质是继承了
@interface接口的特殊接口,其具体实现类由JVM在运行时动态生成。 - 通过反射机制可以获取注解,返回的是一个动态代理对象。
- 注解分为三种类型:源码级别(编译后不保留)、类文件级别(运行时不可见)和运行时级别(可通过反射解析)。
- 元注解
- @Target
- @Retention
- @Documented
- @Inherited
- 自定义注解若要通过反射获取,其生命周期必须声明为
@Retention(RetentionPolicy.RUNTIME)。
21.IO模型
- BIO (Blocking IO): 主线程负责监听和处理请求,采用同步阻塞模式。效率较低,但实现简单。
- NIO (Non-blocking IO): 使用Selector(选择器)管理多个Channel(通道),将任务分发给多个子线程处理,是同步非阻塞模式,效率更高。
- Netty: 基于NIO的高性能网络框架,常用于处理WebSocket等实时通信场景。
22. Object类有哪些方法?
- clone() :保护方法,实现对象的浅拷贝。只有实现了
Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。 - getClass():final方法,获得运行时类型。
- toString():返回对象的字符串表示,一般子类都会被重写。
- finalize():用于释放资源。因为无法确定该方法什么时候被调用,目前已不推荐使用。
- equals():用于比较对象的值是否相等。默认比较内存地址,一般需配合业务逻辑重写。
- hashCode() :用于哈希查找,重写了
equals方法一般都要重写hashCode方法。此方法在一些具有哈希功能的集合(如HashMap、HashSet)中用到。 - wait():使当前线程等待该对象的锁,当前线程必须是该对象的拥有者。
- notify() / notifyAll():唤醒在该对象上等待的某个(或所有)线程。
23. == 与 equals 有什么区别?
**
-
== 运算符:
- 对于基本数据类型 ,
==比较的是它们的值。 - 对于引用数据类型 ,
==比较的是它们在内存中的地址(即是否为同一个对象)。
- 对于基本数据类型 ,
-
equals() 方法:
equals是Object类提供的方法,默认实现与==相同,即也是比较对象的内存地址。- 许多类(如
String、Integer等)都重写 了equals()方法,将其改为比较对象**内部的内容(值)**是否相等,而非仅比较内存地址。
24. hashcode和equals方法有什么关系?
- 答案: 先执行hashcode (一样的话)再执行equals
在使用哈希表结构的集合(比如HashMap、HashSet)时,它们的工作原理是先通过hashCode()计算对象存放在哈希表中的位置,如果位置一样(哈希冲突),再通过equals()判断对象内容是否真的相等。- 如果你只重写了
equals()而不重写hashCode():
那么两个逻辑上"相等"的对象,它们的hashCode()(默认是根据内存地址计算的)却不一样。 - 导致的后果 :
在把对象存入HashMap或HashSet时,会被存放到两个完全不同的格子里,导致集合里存放了重复的逻辑对象,破坏了集合去重和键唯一性的原则。
- 如果你只重写了
25. Java里String的常用方法有哪些?
-
获取与查找:
length():返回字符串长度。charAt(int index):返回指定索引处的字符。indexOf(String str):返回子串第一次出现的索引位置。如未找到返回 -1。substring(int beginIndex, int endIndex):截取字符串(左闭右开区间)。
-
判断与比较:
equals(Object anObject):比较字符串内容是否相等。equalsIgnoreCase(String anotherString):忽略大小写比较字符串内容。startsWith(String prefix)/endsWith(String suffix):判断是否以指定前缀/后缀开头或结尾。isEmpty():判断字符串是否为空(长度为0)。
-
转换与截取:
replace(CharSequence target, CharSequence replacement):替换字符串中的指定字符或序列。trim():去除字符串首尾的空白字符。split(String regex):根据正则表达式拆分字符串,返回字符串数组。toLowerCase()/toUpperCase():转换为小写/大写形式。toCharArray():将字符串转换为字符数组。
26. 为什么说字符串不可变
27. String、StringBuffer、StringBuilder的区别和联系
-
String
- 特性 :不可变的字符序列(底层是由
final修饰的char[]或byte[]数组)。 - 效率 :每次修改操作都会生成一个新的
String对象,效率最低,浪费内存。 - 线程安全 :由于不可变,天然是线程安全的。
- 特性 :不可变的字符序列(底层是由
-
StringBuffer
- 特性:可变的字符序列,相对String修改时不会创建新对象。
- 效率 :相比
String效率较高,但在单线程环境下不如StringBuilder。 - 线程安全 :所有修改数据的方法(如
append)都加了synchronized同步锁,是线程安全的。
-
StringBuilder
- 特性 :可变的字符序列,与
StringBuffer的 API 完全一致。 - 效率 :没有使用同步锁,执行效率最高。
- 线程安全 :非线程安全。
- 特性 :可变的字符序列,与
-
总结与联系:
- 三者都实现了
CharSequence接口,用于处理字符串。 - 使用场景 :如果字符串内容很少变化,优先使用
String;如果在单线程 中频繁拼接修改字符串,使用StringBuilder;如果在多线程 中频繁拼接修改字符串,使用StringBuffer。
- 三者都实现了
28. 异常体系(Error 与 Exception 的区别)
- 顶级父类 :Java 所有异常和错误的超类是
Throwable。它有两个重要子类:Error和Exception。 - Error(错误) :指 JVM 系统级别的重大问题,程序无法通过代码处理恢复。比如内存溢出(
OutOfMemoryError)、栈溢出(StackOverflowError)。通俗解释:病入膏肓,只能罢工拔电源。 - Exception(异常) :指程序运行中出现的可预见、可处理的问题,程序可以通过
catch捕获并恢复。通俗解释:小感冒,吃点药(catch)还能接着干活。
29. 受查异常(Checked)与非受查异常(Unchecked)的区别
- 非受查异常(RuntimeException 及其子类) :编译器不要求 强制处理。通常是由代码逻辑错误引起的。常见的有:
NullPointerException(空指针)、IndexOutOfBoundsException(数组越界)、ClassCastException(类型转换异常)。通俗解释:代码没写好导致的低级错误,运行时才会炸。 - 受查异常(非 RuntimeException 的其他 Exception) :编译器强制要求 必须处理(要么
try-catch捕获,要么throws抛出),否则编译报错无法运行。常见的有:IOException(IO异常)、SQLException(数据库异常)、ClassNotFoundException。通俗解释:老天爷(外部环境)带来的风险(比如文件不存在),写代码时必须提前买好保险。
30. throw 和 throws 的区别是什么?
- 位置不同 :
throw用在方法内部 ;throws用在方法声明处(方法签名的末尾)。 - 作用不同 :
throw是主动 抛出一个具体的异常对象(真正制造异常的动作);throws是高呼"我这个方法可能会出这个异常",把它甩锅给调用者处理。 - 数量不同 :
throw一次只能抛出一个 异常对象;throws后面可以跟多个异常类名,用逗号隔开。
31. try-catch-finally 执行顺序陷阱(带有 return 时)
- 核心规则 :
finally块中的代码一定会执行 (除非在前面执行了System.exit(0)强行退出 JVM)。 - 带有 return 的执行顺序 :
- 执行
try或catch中的逻辑。 - 遇到
return时,会先把返回值暂存起来。 - 执行
finally块中的代码。 - 最后把刚才暂存的返回值返回给调用者。
- 执行
- 神仙打架(覆盖陷阱) :如果
finally块里面也有return语句,那么它会覆盖 掉try或catch中的return值。实际开发中极度反感在 finally 里写 return!
32. String s = new String("abc") 到底创建了几个对象?
这是考察对**字符串常量池(String Pool)**的理解:
- 情况 A:创建了 1 个对象。 如果字符串常量池中已经存在
"abc"这个字面量,那么只会在堆内存(Heap)中创建一个String对象,并且这个堆对象指向常量池中的"abc"。 - 情况 B:创建了 2 个对象(常考点)。 如果字符串常量池中还没有
"abc",那么:- 首先在字符串常量池中创建一个
"abc"对象。 - 然后在堆内存中再创建一个
String对象(其实是对常量池对象的拷贝/包装),并把堆中这个对象的地址返回给变量s。
- 首先在字符串常量池中创建一个
- 通俗解释 :
new关键字就像是个强迫症,只要有它,堆里必定会建个新房子(对象)。但里面的家具(字符串内容)要不要去厂家(常量池)现做,就看之前有没有人做过同样的款式了。