Java 基础高频38问
Java基础高频38问
1. 接口和抽象类的区别
接口和抽象类均用于抽象共性,但设计定位和使用场景差异显著,核心对比如下:
| 对比维度 | 抽象类(Abstract Class) | 接口(Interface) |
|---|---|---|
| 关键字 | 用abstract修饰 | 用interface修饰 |
| 方法特性 | 可包含抽象方法(无实现)和具体方法(有实现);Java 8后支持默认方法(default)和静态方法 | Java 8前仅含抽象方法(默认public abstract);Java 8后支持默认/静态方法,Java 9支持私有方法 |
| 字段特性 | 可定义任意访问修饰符的字段(实例变量、静态变量),字段可修改 | 字段默认public static final(必须是常量),不可修改 |
| 实例化能力 | 不能直接实例化,但有构造方法(供子类调用初始化) | 不能实例化,且无构造方法 |
| 继承/实现规则 | 一个类只能继承一个抽象类(单继承) | 一个类可实现多个接口(多实现);接口可多继承其他接口 |
| 设计意图 | 表示“is-a”关系,用于同类事物的共性抽象(如Animal),提供默认行为和状态 | 表示“has-a”能力(如Runnable),用于不同类的行为统一,不关注实现细节 |
| 典型场景 | 同类事物的共性封装(如Animal包含eat()具体方法和makeSound()抽象方法) | 跨类行为约定(如Comparable定义比较能力、Serializable定义序列化能力) |
2. == 和 equals() 的区别
==是运算符,equals()是Object类方法,二者在比较逻辑和适用场景上完全不同:
| 对比维度 | == | equals() |
|---|---|---|
| 适用类型 | 基本类型(int、char等)、引用类型(对象、数组) | 仅引用类型(对象) |
| 比较逻辑 | 基本类型:比较“值”是否相等; 引用类型:比较“内存地址”是否相同(是否为同一对象) | 默认(未重写):同==,比较内存地址;重写后(如 String、Integer):比较“内容”是否相等 |
| 重写能力 | 不可重写(运算符无重写概念) | 可重写(需遵循自反性、对称性、传递性、一致性、非空性) |
| 空指针风险 | 无(如null == null返回true) | 有(若调用者为null,直接抛出NullPointerException,建议用Objects.equals()规避) |
示例代码:
// 1. 基本类型比较(==)
int a = 10;
int b = 10;
System.out.println(a == b); // true(值相等)// 2. 引用类型比较(==)
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2); // false(内存地址不同)// 3. equals()比较(重写后)
System.out.println(s1.equals(s2)); // true(内容相等,String重写了equals())// 4. 规避空指针
String s3 = null;
System.out.println(Objects.equals(s3, "abc")); // false(安全不报错)
3. 重载和重写的区别
重载(Overloading)是“同类同名不同参”,重写(Overriding)是“子类重写父类方法”,核心差异如下:
| 对比维度 | 重载(Overloading) | 重写(Overriding) |
|---|---|---|
| 发生范围 | 同一类或父子类之间(水平关系) | 子类与父类(或实现类与接口)之间(垂直关系) |
| 方法签名 | 方法名相同,参数列表(个数、类型、顺序)不同 | 方法名、参数列表、返回值类型(子类可窄化)完全一致 |
| 返回值 | 可任意修改 | 需与父类方法返回值“兼容”(Java 5+支持协变返回,如子类返回值是父类返回值的子类) |
| 访问修饰符 | 可任意修改 | 子类方法权限不能低于父类(如父类public,子类不能是protected) |
| 异常抛出 | 可任意修改(新增或删除异常) | 子类抛出的异常不能比父类更宽泛(只能是父类异常的子类或同级) |
| 静态/非静态 | 静态方法和非静态方法均可重载 | 静态方法不能重写(仅能“隐藏”);非静态方法才能重写 |
| 多态类型 | 编译时多态(编译器按参数列表匹配方法) | 运行时多态(运行时按对象实际类型调用方法) |
重载示例:
public class MathUtil {// 参数个数不同public int add(int a, int b) { return a + b; }public int add(int a, int b, int c) { return a + b + c; }// 参数类型不同public double add(double a, double b) { return a + b; }
}
重写示例:
class Animal {public void makeSound() { System.out.println("动物叫"); }
}class Dog extends Animal {@Override // 重写父类方法,实现不同逻辑public void makeSound() { System.out.println("汪汪叫"); }
}
4. 请简述Java异常体系
Java异常体系基于Throwable类,分为Error(错误) 和Exception(异常) 两大分支,核心作用是“捕获运行时错误,保证程序健壮性”:
Throwable(顶层类)
├─ Error(错误):JVM层面的严重问题,无法捕获和恢复
│ ├─ OutOfMemoryError(内存溢出错误)
│ ├─ StackOverflowError(栈溢出错误)
│ └─ NoClassDefFoundError(类未找到错误)
└─ Exception(异常):程序可捕获和处理的问题├─ 编译时异常(Checked Exception):编译期强制要求处理(try-catch或throws)│ ├─ IOException(IO流异常)│ ├─ SQLException(数据库操作异常)│ └─ ClassNotFoundException(类未找到异常)└─ 运行时异常(Unchecked Exception):编译期不强制处理,运行时触发├─ NullPointerException(空指针异常)├─ ArrayIndexOutOfBoundsException(数组下标越界异常)├─ ClassCastException(类型转换异常)└─ ArithmeticException(算术异常,如除以0)
关键说明:
Error:由JVM生成(如内存溢出),程序无法处理,通常直接崩溃;- 编译时异常:必须显式处理(否则编译报错),如读取文件时的
IOException; - 运行时异常:多由代码逻辑错误导致(如空对象调用方法),可选择处理或通过代码优化规避。
5. 运行时异常和非运行时异常(编译时异常)
运行时异常(Unchecked)和编译时异常(Checked)是Exception的两大子类,核心区别在“编译要求”和“触发原因”:
| 对比维度 | 运行时异常(Unchecked Exception) | 非运行时异常(Checked Exception,编译时异常) |
|---|---|---|
| 父类 | 继承自RuntimeException | 直接继承自Exception(非RuntimeException子类) |
| 编译要求 | 编译期不强制处理(无需try-catch或throws) | 编译期强制处理(不处理则编译报错) |
| 触发原因 | 通常由代码逻辑错误导致(如空指针、数组越界、除以0) | 由外部环境或资源问题导致(如IO流失败、数据库连接超时、类未找到) |
| 常见示例 | NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException | IOException、SQLException、ClassNotFoundException、ParseException |
| 处理建议 | 优先通过优化代码逻辑避免(如判空、校验下标、避免除以0) | 必须显式处理(捕获后修复或向上抛出,让上层调用者处理) |
6. 访问修饰符public、private、protected、默认(default)的区别
Java访问修饰符控制类、字段、方法的访问权限,从宽到严依次为public > protected > 默认 > private:
| 修饰符 | 同一类中 | 同一包中(非子类) | 不同包的子类 | 不同包的非子类 |
|---|---|---|---|---|
public | 可访问 | 可访问 | 可访问 | 可访问 |
protected | 可访问 | 可访问 | 可访问 | 不可访问 |
| 默认(无) | 可访问 | 可访问 | 不可访问 | 不可访问 |
private | 可访问 | 不可访问 | 不可访问 | 不可访问 |
关键场景:
private:封装字段,仅本类可见(如Person类的name字段,通过getter/setter访问);- 默认修饰符:包内可见,适用于包内组件协作(如工具类内部的辅助方法);
protected:同包可见 + 不同包子类可见,适用于父类给子类暴露方法(如Animal的move()方法);public:全局可见,适用于对外提供的接口(如Math类的abs()方法)。
7. 请简述Java 128陷阱
Java中Integer类为优化性能,在**-128~127**范围内创建“缓存对象”,当使用Integer.valueOf(int)或自动装箱时复用缓存,超出范围则创建新对象,这就是“128陷阱”。
核心原理:
Integer内部维护了一个IntegerCache静态内部类,缓存-128~127的Integer对象;- 自动装箱(如
Integer a = 100)本质是调用Integer.valueOf(100),优先从缓存获取对象; - 直接
new Integer(100)会跳过缓存,直接创建新对象。
示例代码:
// 1. 范围内(-128~127):复用缓存,==返回true
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(同一缓存对象)// 2. 范围外(>127):创建新对象,==返回false
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false(不同对象)// 3. 直接new:不复用缓存
Integer e = new Integer(100);
Integer f = new Integer(100);
System.out.println(e == f); // false
注意:缓存范围可通过JVM参数-XX:AutoBoxCacheMax=<size>修改(如扩大到256),类似缓存机制的类还有Byte(全缓存)、Short、Long、Character(0~127)。
8. 获取Class对象的三种方式
Class对象是反射的核心,代表类的“元信息”(字段、方法、构造器等),获取方式有三种:
方式1:Class.forName("全类名")
- 特点:通过“包名+类名”动态加载类,触发类的初始化(执行静态代码块、静态变量赋值);
- 场景:仅知道类名(如配置文件中配置的类名),如JDBC加载驱动(
Class.forName("com.mysql.cj.jdbc.Driver")); - 示例:
try {Class<?> clazz = Class.forName("com.test.Person"); // 全类名:包名+类名 } catch (ClassNotFoundException e) {e.printStackTrace(); }
方式2:类名.class
- 特点:通过类的
class属性静态获取,不触发类的初始化(仅加载类),效率高; - 场景:已知类的编译时类型,如
String.class、int.class(基本类型也有Class对象); - 示例:
Class<?> clazz1 = Person.class; Class<?> clazz2 = String.class; Class<?> clazz3 = int.class; // 基本类型的Class对象
方式3:对象.getClass()
- 特点:通过对象实例调用
getClass()获取,不触发类的初始化(对象已创建,类已加载); - 场景:已知对象实例,动态获取其所属类,如
new Person().getClass(); - 示例:
Person person = new Person(); Class<?> clazz = person.getClass();
关键结论:同一类在JVM中仅存在一个Class对象,三种方式获取的Class对象是同一实例(clazz1 == clazz2 == clazz3)。
9. 请简述static关键字
static修饰类的成员(字段、方法、代码块)或内部类,表示“属于类,不属于实例”,可直接通过类名访问,无需创建对象。
修饰字段(静态变量)
- 特点:所有对象共享同一静态变量,存储在JVM的“方法区”,类加载时初始化(仅一次);
- 场景:存储全局共享数据(如计数器、常量);
- 示例:
public class Counter {public static int count = 0; // 所有对象共享public Counter() {count++; // 每次创建对象,计数器+1} }// 测试:两个对象共享count System.out.println(Counter.count); // 0(直接通过类名访问) new Counter(); new Counter(); System.out.println(Counter.count); // 2
修饰方法(静态方法)
- 特点:属于类,无
this指针(不能访问非静态成员),可直接通过类名调用; - 场景:工具方法(如
Math.abs()、Arrays.sort()); - 注意:静态方法不能重写(仅能“隐藏”,子类静态方法与父类同名时,父类方法被隐藏)。
修饰代码块(静态代码块)
- 特点:类加载时执行(仅一次),优先级高于构造方法,用于初始化静态变量;
- 示例:
public class StaticBlock {static {System.out.println("静态代码块执行"); // 类加载时执行}public StaticBlock() {System.out.println("构造方法执行"); // 创建对象时执行} }// 测试:先执行静态代码块,再执行构造方法 new StaticBlock(); // 输出:静态代码块执行 → 构造方法执行
10. 请简述final关键字
final表示“不可变”或“不可修改”,可修饰类、方法、字段,核心作用是“限制继承、防止重写、保证变量不可变”:
修饰类
- 特点:被
final修饰的类不能被继承(无子类); - 场景:确保类的完整性,不允许被扩展(如
String、Integer类,避免破坏原有逻辑); - 示例:
final class FinalClass {} // class SubClass extends FinalClass {} // 错误:FinalClass不能被继承
修饰方法
- 特点:被
final修饰的方法不能被子类重写; - 场景:确保方法逻辑不被修改(如父类的核心业务方法);
- 示例:
class Parent {final public void coreMethod() { System.out.println("父类核心方法"); } }class Child extends Parent {// @Override // 错误:coreMethod()不能被重写// public void coreMethod() {} }
修饰字段
- 特点:必须“显式初始化”(声明时、构造方法中、静态代码块中),初始化后不可修改;
- 基本类型:值不可变;
- 引用类型:引用地址不可变(但对象内容可修改);
- 场景:存储常量(如
public static final int MAX_AGE = 120); - 示例:
public class FinalField {final int num1 = 10; // 声明时初始化final int num2; // 构造方法中初始化public FinalField(int num2) {this.num2 = num2; // 必须初始化,否则编译报错}// 引用类型:地址不可变,内容可修改final List<String> list = new ArrayList<>();public void addElement() {list.add("a"); // 允许(内容可修改)// list = new LinkedList<>(); // 错误(地址不可变)} }
11. 请简述this关键字
this代表“当前对象”,即调用当前方法或字段的对象,仅在非静态方法、构造方法中使用,核心作用:
1. 区分局部变量与成员变量(重名时)
当方法参数或局部变量与成员变量同名时,用this.成员变量表示成员变量:
public class Person {private String name;// 构造方法:参数name与成员变量重名public Person(String name) {this.name = name; // this.name:成员变量;name:参数}public void setName(String name) {this.name = name; // 同上}
}
2. 调用本类的其他构造方法(this())
在构造方法中,用this(参数)调用本类的其他构造方法,必须放在构造方法第一行:
public class Student {private String name;private int age;// 无参构造:调用有参构造初始化默认值public Student() {this("未知", 0); // 调用Student(String, int)}// 有参构造public Student(String name, int age) {this.name = name;this.age = age;}
}
3. 传递当前对象
将当前对象作为参数传递给其他方法,或作为返回值实现链式调用:
public class User {// 传递当前对象给其他方法public void sendToService() {UserService service = new UserService();service.process(this); // this:当前User对象}// 返回当前对象(链式调用)public User setName(String name) {this.name = name;return this; // 支持链式调用:new User().setName("张三").setAge(20)}
}
注意:this不能在静态方法中使用(静态方法属于类,无“当前对象”)。
12. 请简述构造器的特点
构造器(Constructor)是类的特殊方法,用于创建对象并初始化成员变量,与类名同名,无返回值,在new对象时自动调用:
-
名称与类名完全一致:如
Person类的构造器名为Person(),不能自定义其他名称(若加void则变为普通方法);public class Person {public Person() {} // 正确:构造器// public void Person() {} // 错误:普通方法,不是构造器 } -
无返回值:不能声明返回值类型(包括
void),若加返回值则变为普通方法; -
自动调用:仅在
new对象时自动调用,不能手动调用(如person.Person()报错);Person person = new Person(); // new时自动调用Person()构造器 -
默认构造器:若类中未显式定义构造器,编译器会自动生成“无参默认构造器”;若显式定义了构造器,默认构造器消失(需手动定义才能使用);
public class Car {// 未显式定义构造器:编译器生成无参默认构造器 public Car() {} }public class Car {public Car(String brand) {} // 显式定义有参构造器,默认无参构造器消失// public Car() {} // 若需无参构造器,需手动定义 } -
支持重载:一个类可有多个构造器(参数列表不同),用于不同初始化场景;
public class Phone {private String brand;private int price;public Phone() { this.brand = "未知"; } // 无参构造public Phone(String brand) { this.brand = brand; } // 单参构造public Phone(String brand, int price) { this.brand = brand; this.price = price; } // 双参构造 } -
子类构造器默认调用父类无参构造器:子类构造器第一行默认隐含
super(),调用父类无参构造器;若父类无无参构造器,子类必须显式调用父类有参构造器(super(参数)),否则编译报错;class Parent {public Parent(String name) {} // 父类显式定义有参构造器,无参构造器消失 }class Child extends Parent {public Child() {super("父类名称"); // 必须显式调用父类有参构造器,否则编译报错} }
13. 常见的集合底层实现
Java集合框架分为Collection(单元素)和Map(键值对),底层实现决定其性能特性:
Collection接口下的集合
| 集合类 | 底层实现 | 核心特点 | 适用场景 |
|---|---|---|---|
ArrayList | 动态数组(Object[]) | 查询快(O(1))、插入/删除慢(O(n))、线程不安全、初始容量10、1.5倍扩容 | 查询频繁,插入/删除少(如列表展示) |
LinkedList | 双向链表 | 查询慢(O(n))、插入/删除快(O(1))、线程不安全、实现Deque(支持队列/栈) | 插入/删除频繁(如队列、栈) |
HashSet | 基于HashMap(键存储) | 无序、不可重复、线程不安全、查询/插入/删除O(1)(无哈希冲突时) | 去重、无序存储(如唯一ID) |
TreeSet | 基于TreeMap(红黑树) | 有序(自然排序/自定义排序)、不可重复、线程不安全、查询/插入O(log n) | 有序、去重存储(如按价格排序) |
LinkedHashSet | 基于LinkedHashMap | 有序(插入顺序)、不可重复、线程不安全、效率略低于HashSet | 去重、保留插入顺序(如历史记录) |
Map接口下的集合
| 集合类 | 底层实现 | 核心特点 | 适用场景 |
|---|---|---|---|
HashMap(Java8+) | 数组(哈希桶)+ 链表/红黑树 | 无序、键不可重复、值可重复、线程不安全、桶内元素≤8为链表,≥8为红黑树 | 键值对存储,查询频繁(如缓存) |
TreeMap | 红黑树 | 有序(键排序)、键不可重复、值可重复、线程不安全、查询/插入O(log n) | 有序键的键值对(如按日期排序) |
LinkedHashMap | 哈希表+双向链表 | 有序(插入/访问顺序)、键不可重复、值可重复、线程不安全 | 保留顺序的键值对(如LRU缓存) |
Hashtable | 数组(哈希桶)+ 链表 | 无序、键不可重复、值可重复、线程安全(方法加synchronized)、效率低 | 低并发键值对(已被ConcurrentHashMap替代) |
ConcurrentHashMap(Java8+) | 数组+链表/红黑树+CAS+synchronized | 无序、键不可重复、值可重复、线程安全(锁粒度为哈希桶)、效率高 | 高并发键值对(如多线程缓存) |
14. List、Set、Map之间的区别
List、Set属于Collection(单元素),Map属于独立的Map(键值对),核心区别如下:
| 对比维度 | List | Set | Map |
|---|---|---|---|
| 核心定义 | 有序、可重复的单元素集合 | 无序(除LinkedHashSet)、不可重复的单元素集合 | 无序(除TreeMap/LinkedHashMap)、键不可重复、值可重复的键值对集合 |
| 存储结构 | 线性结构(数组/链表) | 哈希表或红黑树 | 哈希表或红黑树(键),值关联到键 |
| 核心方法 | 按索引操作(get(int)、add(int, E));支持重复添加 | 无索引操作;添加重复元素返回false(去重) | 按键操作(put(K,V)、get(K));同一键重复put覆盖值 |
是否允许null | 允许(ArrayList/LinkedList可存多个null) | HashSet允许一个null,TreeSet不允许null | HashMap允许一个null键/多个null值;Hashtable不允许null |
| 遍历方式 | 迭代器、for-each、普通for循环(按索引) | 迭代器、for-each | keySet()(遍历键)、values()(遍历值)、entrySet()(遍历键值对) |
| 常见实现类 | ArrayList、LinkedList | HashSet、TreeSet、LinkedHashSet | HashMap、TreeMap、LinkedHashMap、ConcurrentHashMap |
| 适用场景 | 按顺序存储、允许重复(如购物车、列表展示) | 去重、无序存储(如唯一ID、标签) | 键值对映射(如缓存、用户信息) |
15. 重写equals()方法为什么要重写hashCode()
核心原则(Java规范)
如果两个对象通过equals()比较返回true,则它们的hashCode()必须相等;反之,hashCode()相等的两个对象,equals()不一定返回true(哈希冲突)。
若只重写equals()不重写hashCode(),会违反该原则,导致HashMap、HashSet等哈希表集合出现逻辑错误。
问题根源
Object类的默认hashCode()返回“对象内存地址相关值”,若只重写equals(),两个equals()为true的对象(内容相同),默认hashCode()可能不同,导致哈希表集合无法识别它们是“同一对象”。
错误示例(HashSet去重失效)
class Person {private String id;public Person(String id) { this.id = id; }// 只重写equals():id相同则认为相等@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return Objects.equals(id, person.id);}// 未重写hashCode():默认返回内存地址值
}// 测试:两个id相同的Person对象
Person p1 = new Person("1001");
Person p2 = new Person("1001");System.out.println(p1.equals(p2)); // true(内容相同)
System.out.println(p1.hashCode() == p2.hashCode()); // false(默认hashCode()不同)// 存入HashSet:本应去重,却存入两个对象
HashSet<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 2(错误:应为1)
解决方法:同时重写equals()和hashCode()
重写hashCode()时,需基于equals()比较的字段(如id)计算哈希值,确保“相等对象哈希值相等”:
class Person {private String id;public Person(String id) { this.id = id; }@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return Objects.equals(id, person.id);}// 基于id计算hashCode(),确保equals()为true的对象hashCode()相等@Overridepublic int hashCode() {return Objects.hash(id); // 用Objects.hash()避免空指针}
}// 重新测试:去重成功
HashSet<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 1(正确)
总结
- 若类的对象需存入
HashMap、HashSet等哈希表集合,必须同时重写equals()和hashCode(); - 重写
hashCode()的核心:基于equals()比较的字段计算哈希值,遵循“相等对象哈希值相等”原则。
16. & 和 &&的区别
&是“按位与”运算符,也可用于“逻辑与”;&&是“短路逻辑与”运算符,仅用于逻辑判断,核心区别:
| 对比维度 | & | && |
|---|---|---|
| 运算符类型 | 按位运算符 + 逻辑运算符 | 仅逻辑运算符(短路与) |
| 适用场景 | 1. 按位运算:对整数二进制位进行“与”操作; 2. 逻辑运算:判断多个布尔表达式的“与”结果 | 仅逻辑运算:判断多个布尔表达式的“与”结果(利用短路特性优化性能) |
| 短路特性 | 无短路:无论第一个表达式是否为false,都会执行所有后续表达式 | 有短路:若第一个表达式为false,直接返回false,不执行后续表达式 |
| 返回值 | 按位运算:返回整数(二进制位“与”结果); 逻辑运算:返回布尔值 | 仅返回布尔值(true/false) |
示例1:逻辑运算(短路特性对比)
int a = 5;
// &:无短路,执行所有表达式
if (a > 10 & ++a > 5) { // a>10为false,但仍执行++a// 不进入
}
System.out.println(a); // 6(a被自增)// &&:有短路,不执行后续表达式
int b = 5;
if (b > 10 && ++b > 5) { // b>10为false,直接返回false,不执行++b// 不进入
}
System.out.println(b); // 5(b未被自增)
示例2:按位运算(仅&可用)
// 按位与:二进制位都为1时,结果为1
int x = 3; // 二进制:0011
int y = 5; // 二进制:0101
int result = x & y; // 二进制:0001 → 十进制1
System.out.println(result); // 1
17. String s = “123”;这个语句有几个对象产生?
需分两种场景,核心取决于“字符串常量池是否已有"123"”:
场景1:字符串常量池中没有"123"
- 过程:
- JVM检查字符串常量池(方法区的一部分),若没有
"123",则创建一个"123"对象存入常量池; - 变量
s引用常量池中的"123"对象,不创建新对象;
- JVM检查字符串常量池(方法区的一部分),若没有
- 结论:产生1个对象(常量池中的
"123")。
场景2:字符串常量池中已有"123"
- 过程:
- JVM检查字符串常量池,发现已有
"123"对象; - 变量
s直接引用常量池中的已有对象,不创建任何新对象;
- JVM检查字符串常量池,发现已有
- 结论:产生0个对象。
关键背景:字符串常量池
Java为优化字符串性能,设计了“字符串常量池”:
- 用
""定义字符串(如"123")时,JVM优先从常量池查找,有则复用,无则创建后存入; - 用
new String("123")时,会创建两个对象(若常量池无"123"):常量池中的"123"+ 堆内存中的String对象。
示例验证:
// 1. 第一次定义:常量池无"123",创建1个对象
String s1 = "123"; // 2. 第二次定义:常量池有"123",复用,创建0个对象
String s2 = "123";
System.out.println(s1 == s2); // true(引用同一对象)// 3. new String("123"):创建2个对象(常量池1个 + 堆1个)
String s3 = new String("123");
System.out.println(s1 == s3); // false(s1指向常量池,s3指向堆)
结论:String s = "123";产生的对象数为0个或1个,取决于字符串常量池中是否已有"123"。
18. 面向对象的三大特性是什么?请简单介绍一下!
面向对象(OOP)的三大特性是封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism),是OOP编程的核心思想:
1. 封装(Encapsulation)
定义
将类的“字段(属性)”和“方法(行为)”封装在一起,隐藏内部实现细节,仅对外暴露可控的访问接口(如getter/setter),限制外部对内部数据的直接操作。
核心目的
- 保护数据安全:防止外部随意修改内部字段,确保数据合法性(如年龄不能为负数);
- 隐藏实现细节:外部只需关注“如何使用”,无需关注“如何实现”(如调用
ArrayList.add(),无需知道底层数组扩容逻辑)。
示例
public class Person {// 私有字段:隐藏内部数据private String name;private int age;// getter:允许外部获取字段值public String getName() { return name; }// setter:控制外部修改逻辑(校验年龄合法性)public void setAge(int age) {if (age < 0 || age > 150) {throw new IllegalArgumentException("年龄必须在0~150之间");}this.age = age;}
}// 使用:通过setter控制修改,通过getter获取
Person person = new Person();
person.setAge(20); // 合法
// person.setAge(200); // 抛出异常,阻止非法修改
2. 继承(Inheritance)
定义
子类(Subclass)继承父类(Superclass)的字段和方法,同时可添加新字段/方法或重写父类方法,实现“代码复用”和“层次化设计”。
核心目的
- 代码复用:父类的通用逻辑(如
Animal的eat()方法)无需在子类中重复编写; - 层次化设计:建立类之间的“is-a”关系(如
Dogis aAnimal),使代码结构更清晰。
关键规则
- Java支持单继承(一个子类只能继承一个父类),但支持多实现(一个类可实现多个接口);
- 子类继承父类的非
private成员,private成员需通过父类的public方法访问; - 子类构造器默认调用父类的无参构造器(
super()),若父类无无参构造器,需显式调用父类有参构造器。
示例
// 父类:通用逻辑
class Animal {public void eat() { System.out.println("动物吃东西"); }
}// 子类:继承父类,复用eat(),添加新方法
class Dog extends Animal {public void bark() { System.out.println("狗汪汪叫"); } // 新增方法@Override // 重写父类方法,修改逻辑public void eat() { System.out.println("狗吃骨头"); }
}// 使用:子类可调用继承和新增的方法
Dog dog = new Dog();
dog.eat(); // 重写后的方法:狗吃骨头
dog.bark(); // 新增方法:狗汪汪叫
3. 多态(Polymorphism)
定义
同一方法(或接口)在不同对象上有不同的实现,即“一个接口,多种实现”,分为编译时多态(重载) 和运行时多态(重写)。
核心目的
- 提高代码扩展性:新增子类时,无需修改原有代码(如新增
Cat类,无需修改Animal的调用逻辑); - 降低耦合:调用者只需面向父类或接口编程,无需关注具体子类。
实现条件
- 继承:子类继承父类(或实现接口);
- 重写:子类重写父类方法(或实现接口方法);
- 向上转型:父类引用指向子类对象(如
Animal animal = new Dog())。
示例
// 父类
class Animal {public void makeSound() { System.out.println("动物叫"); }
}// 子类1
class Dog extends Animal {@Overridepublic void makeSound() { System.out.println("汪汪叫"); }
}// 子类2
class Cat extends Animal {@Overridepublic void makeSound() { System.out.println("喵喵叫"); }
}// 测试:运行时多态
public class Test {public static void main(String[] args) {Animal animal1 = new Dog(); // 向上转型Animal animal2 = new Cat(); // 向上转型animal1.makeSound(); // 运行时调用Dog的方法:汪汪叫animal2.makeSound(); // 运行时调用Cat的方法:喵喵叫}
}
19. Java语言有几种基本类型,分别是什么?
Java的基本类型(Primitive Type)共8种,分为“数值类型”和“布尔类型”,直接存储值(存储在栈内存,效率高),对应的包装类属于引用类型:
| 类型分类 | 基本类型 | 关键字 | 占用字节数 | 取值范围 | 默认值 | 包装类 |
|---|---|---|---|---|---|---|
| 整数类型 | 字节型 | byte | 1字节(8位) | −27-2^7−27 ~ 27−12^7-127−1(-128 ~ 127) | 0 | Byte |
| 短整型 | short | 2字节(16位) | −215-2^{15}−215 ~ 215−12^{15}-1215−1(-32768 ~ 32767) | 0 | Short | |
| 整型 | int | 4字节(32位) | −231-2^{31}−231 ~ 231−12^{31}-1231−1(-2147483648 ~ 2147483647) | 0 | Integer | |
| 长整型 | long | 8字节(64位) | −263-2^{63}−263 ~ 263−12^{63}-1263−1(-9223372036854775808 ~ 9223372036854775807) | 0L | Long | |
| 浮点类型 | 单精度 | float | 4字节(32位) | 约±3.4e38(精度6~7位小数) | 0.0f | Float |
| 双精度 | double | 8字节(64位) | 约±1.8e308(精度15~17位小数) | 0.0d | Double | |
| 字符类型 | 字符型 | char | 2字节(16位) | \u0000(0) ~ \uffff(65535) | \u0000 | Character |
| 布尔类型 | 布尔型 | boolean | 未明确规定(JVM中通常1字节或1位) | true/false | false | Boolean |
关键说明:
- 整数类型中,
long需加L标识(如100L),否则视为int; - 浮点类型中,
float需加f标识(如3.14f),double的d可省略(如3.14); char存储Unicode字符,如'A'(ASCII码65)、'中'(Unicode码4E2D);boolean仅表示真/假,不能用0/1代替(与C/C++不同)。
20. int[]类型是不是基本类型?
不是,int[]是“数组类型”,属于引用类型,而非基本类型。
核心原因
-
存储方式不同:
- 基本类型(如
int):直接存储值,存储在栈内存; - 数组类型(如
int[]):存储“数组对象在堆内存中的地址”(引用),数组元素(int值)存储在堆内存。
- 基本类型(如
-
继承关系:
- 所有数组类型都间接继承自
Object类(引用类型的顶层类),可调用Object的方法(如toString()、hashCode()); - 基本类型不继承任何类,也没有方法(需通过包装类调用)。
- 所有数组类型都间接继承自
-
初始化方式:
- 基本类型:直接赋值(如
int a = 10),无需new; - 数组类型:需通过
new创建(如int[] arr = new int[5]),或字面量初始化(如int[] arr = {1,2,3}),本质是创建数组对象。
- 基本类型:直接赋值(如
示例验证
// 1. 数组是引用类型:存储引用地址
int[] arr1 = new int[3];
int[] arr2 = arr1; // arr2引用arr1指向的同一数组对象arr1[0] = 10;
System.out.println(arr2[0]); // 10(修改arr1的元素,arr2也受影响)// 2. 数组继承自Object:可调用Object方法
int[] arr = {1,2,3};
System.out.println(arr.toString()); // [I@1b6d3586(默认toString()格式)
System.out.println(arr instanceof Object); // true(数组是Object的子类)// 3. 存储位置不同
int a = 5; // 栈内存存储值5
int[] arr3 = {5}; // 栈存储引用,堆存储元素5
结论:int[]是引用类型,其元素int是基本类型,但数组本身属于引用类型。
21. 什么是自动拆箱、什么是自动装箱
自动装箱(Autoboxing)和自动拆箱(Unboxing)是Java 5引入的特性,用于基本类型与包装类之间的自动转换,简化代码:
自动装箱(Autoboxing):基本类型 → 包装类
定义
将基本类型的值自动转换为对应的包装类对象(如int→Integer、double→Double)。
触发场景
- 基本类型值赋值给包装类变量;
- 基本类型值作为参数传入需要包装类的方法(如
List.add(Integer))。
底层实现
通过包装类的valueOf()方法实现(如int→Integer调用Integer.valueOf(int)),且会复用缓存对象(见“128陷阱”)。
示例
// 1. 基本类型int → 包装类Integer
Integer num1 = 10; // 等价于:Integer num1 = Integer.valueOf(10);// 2. 基本类型作为参数传入List(需包装类)
List<Integer> list = new ArrayList<>();
list.add(20); // 自动装箱:20(int)→ Integer(20)
自动拆箱(Unboxing):包装类 → 基本类型
定义
将包装类对象自动转换为对应的基本类型值(如Integer→int、Double→double)。
触发场景
- 包装类对象赋值给基本类型变量;
- 包装类对象作为参数传入需要基本类型的方法(如
Math.abs(int)); - 包装类对象参与基本类型的运算(如
+、-)。
底层实现
通过包装类的xxxValue()方法实现(如Integer→int调用intValue(),Double→double调用doubleValue())。
示例
// 1. 包装类Integer → 基本类型int
Integer num2 = Integer.valueOf(30);
int a = num2; // 等价于:int a = num2.intValue();// 2. 包装类参与基本类型运算
Integer num3 = 40;
int sum = num3 + 10; // 自动拆箱:num3→40(int),再与10相加
注意事项
-
空指针风险:若包装类对象为
null,自动拆箱会抛出NullPointerException;Integer num = null; // int a = num; // 错误:NullPointerException -
缓存机制:自动装箱时,包装类会复用缓存对象(如
Integer缓存-128~127),自动拆箱无缓存影响;Integer a = 100; Integer b = 100; System.out.println(a == b); // true(复用缓存) -
性能影响:频繁自动装箱/拆箱会产生额外对象开销(如循环中大量
Integer运算),建议优先使用基本类型。
22. 位运算有哪几种?分别是什么意思
位运算对整数的二进制位直接操作,效率远高于算术运算,Java支持7种位运算:
| 位运算符 | 名称 | 运算规则(a、b为二进制位,1真0假) | 示例(a=3(0011)、b=5(0101)) |
|---|---|---|---|
& | 按位与 | 两位都为1时,结果为1;否则为0 | a & b = 0011 & 0101 = 0001(1) |
| ` | ` | 按位或 | 两位有一个为1时,结果为1;否则为0 |
^ | 按位异或 | 两位不同时,结果为1;相同为0 | a ^ b = 0011 ^ 0101 = 0110(6) |
~ | 按位非 | 对单个位取反(1变0,0变1) | ~a = ~0011 = 1100(十进制-4) |
<< | 左移 | 整体左移n位,右侧补0,等价于×2n2^n2n | a << 2 = 0011 << 2 = 1100(12) |
>> | 右移(算术) | 整体右移n位,左侧补符号位,等价于÷2n2^n2n(向下取整) | a >> 1 = 0011 >> 1 = 0001(1) |
>>> | 无符号右移 | 整体右移n位,左侧补0,结果非负 | -3 >>> 1 = 2147483646 |
关键说明
-
按位非(~):Java中整数为32位(
int)或64位(long),按位非会对所有位(包括符号位)取反,公式:~x = -(x + 1)(如~3 = -4); -
左移(<<):左移n位时,高位超出范围会被舍弃,常用于正数快速乘2n2^n2n(如
1 << 30= 2302^{30}230); -
右移(>>)vs 无符号右移(>>>):
- 正数:两者结果相同(左侧补0);
- 负数:
>>左侧补1(保持负数),>>>左侧补0(转为正数),如-2 >> 1 = -1,-2 >>> 1 = 2147483647;
常见应用
- 判断奇偶:
x & 1 == 1(奇数),x & 1 == 0(偶数); - 交换两数:
a = a ^ b; b = a ^ b; a = a ^ b;(无需临时变量); - 快速乘除:
x << n(×2n2^n2n),x >> n(÷2n2^n2n); - 清零:
x & 0(任何数与0按位与,结果为0)。
23. String类的常用函数有哪些?列举十种
String是Java中最常用的类,底层基于char[](Java 8)或byte[](Java 9+)实现,提供丰富的字符串操作方法,以下是十种常用函数:
| 方法签名 | 功能描述 | 示例 |
|---|---|---|
int length() | 返回字符串长度(字符个数) | "abc".length() → 3 |
char charAt(int index) | 返回指定索引处的字符(索引从0开始) | "abc".charAt(1) → ‘b’ |
boolean equals(Object anObject) | 比较字符串内容是否相等(区分大小写) | "abc".equals("ABC") → false |
boolean equalsIgnoreCase(String s) | 比较字符串内容是否相等(不区分大小写) | "abc".equalsIgnoreCase("ABC") → true |
String concat(String str) | 连接指定字符串到末尾,返回新字符串 | "a".concat("b") → “ab” |
boolean contains(CharSequence s) | 判断是否包含指定字符序列 | "abcde".contains("bc") → true |
int indexOf(String str) | 返回指定字符串第一次出现的索引,未找到返回-1 | "abcabc".indexOf("ab") → 0 |
int lastIndexOf(String str) | 返回指定字符串最后一次出现的索引,未找到返回-1 | "abcabc".lastIndexOf("ab") → 3 |
String substring(int begin) | 从begin索引(含)截取到末尾,返回新字符串 | "abcde".substring(2) → “cde” |
String substring(int begin, int end) | 从begin(含)截取到end(不含),返回新字符串 | "abcde".substring(1,3) → “bc” |
String trim() | 去除两端空白字符(空格、制表符等),返回新字符串 | " abc ".trim() → “abc” |
String toUpperCase() | 转为大写,返回新字符串 | "abc".toUpperCase() → “ABC” |
String toLowerCase() | 转为小写,返回新字符串 | "ABC".toLowerCase() → “abc” |
boolean startsWith(String prefix) | 判断是否以指定前缀开头 | "abcde".startsWith("ab") → true |
boolean endsWith(String suffix) | 判断是否以指定后缀结尾 | "abcde".endsWith("de") → true |
关键说明:String是不可变的,所有修改方法(如concat()、substring())都会返回新的String对象,原对象不变。
24. String、StringBuffer、StringBuilder的区别
三者均用于处理字符串,核心区别在可变性、线程安全性、性能:
| 对比维度 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变(底层final char[]/byte[],修改时创建新对象) | 可变(底层char[],修改时直接操作数组) | 可变(同StringBuffer,底层char[]) |
| 线程安全性 | 线程安全(不可变对象天然安全) | 线程安全(方法加synchronized锁) | 线程不安全(方法无锁) |
| 性能 | 最低(频繁修改创建大量新对象,GC开销大) | 中等(锁机制导致性能损耗) | 最高(无锁,比StringBuffer高10%~50%) |
| 底层实现 | Java 8:final char[];Java 9+:final byte[](按编码优化) | char[](无final,可扩容) | 同StringBuffer,char[] |
| 扩容机制 | 无扩容(修改时创建新对象) | 默认初始容量16,扩容时按2倍+2扩容(如16→34→70),需复制数组 | 同StringBuffer |
| 适用场景 | 字符串不频繁修改(如常量定义、少量拼接) | 多线程环境下频繁修改(如日志输出、多线程缓存) | 单线程环境下频繁修改(如字符串拼接、JSON构建) |
示例对比:
// 1. String:频繁修改效率低
String s = "a";
for (int i = 0; i < 1000; i++) {s += "b"; // 每次创建新对象
}// 2. StringBuffer:多线程安全
StringBuffer sb = new StringBuffer("a");
for (int i = 0; i < 1000; i++) {sb.append("b"); // 直接操作数组
}// 3. StringBuilder:单线程效率最高
StringBuilder sb2 = new StringBuilder("a");
for (int i = 0; i < 1000; i++) {sb2.append("b"); // 无锁,效率最优
}
结论:单线程优先用StringBuilder,多线程用StringBuffer,不修改字符串用String。
25. String字符串的不可变是指哪里不可变?
String的不可变是指字符串对象的内容(字符序列)不可变,一旦创建String对象,其包含的字符序列无法被修改(增删、修改字符),任何“修改”操作都会创建新的String对象。
底层实现原因(Java 8及之前)
String类的底层存储依赖private final char value[]数组,两个关键修饰符确保不可变:
private:value数组是私有成员,外部类无法直接访问或修改;final:value数组的引用(地址)不可变,即无法指向新的数组对象。
public final class String {private final char value[]; // 私有+final,确保数组引用不可变// 构造方法:复制参数数组,避免外部修改原数组public String(char value[]) {this.value = Arrays.copyOf(value, value.length);}// 示例:"修改"方法实际返回新对象public String concat(String str) {char buf[] = new char[value.length + str.value.length];System.arraycopy(value, 0, buf, 0, value.length);System.arraycopy(str.value, 0, buf, value.length, str.value.length);return new String(buf); // 返回新对象,原value数组未修改}
}
不可变的具体表现
-
无法修改字符:没有方法能直接修改
value数组中的字符(如setCharAt()),所有“修改”方法(concat()、replace())都会创建新对象;String s = "abc"; s.concat("d"); // 返回新对象"abcd",原s仍为"abc" System.out.println(s); // "abc"(原对象未修改) -
无法改变长度:字符串长度由
value数组长度决定,value引用不可变且数组长度固定,因此字符串长度也不可变;String s = "a"; String s2 = s + "b"; // s2是新对象(长度2),s仍为"a"(长度1) -
哈希值缓存:由于String不可变,其哈希值(
hashCode())在第一次计算后会被缓存到private int hash字段,后续调用直接返回缓存值,提高效率(若可变,哈希值会变化,导致哈希表集合出错)。
26. 子类实例初始化是否会触发父类实例初始化?
会,子类实例初始化时,会先触发父类的实例初始化(执行父类的构造器),再执行子类的实例初始化,这是Java继承机制的核心规则。
核心流程(子类实例化步骤)
- 加载子类时,先加载父类(JVM类加载机制:父类优先加载);
- 执行子类构造器前,先执行父类的构造器(默认调用父类无参构造器,通过
super()实现); - 父类实例初始化完成后,再执行子类的实例初始化(子类构造器中的逻辑)。
详细步骤(以Child extends Parent为例)
class Parent {// 父类实例变量private String parentName = "父类名称";// 父类构造器(实例初始化核心逻辑)public Parent() {System.out.println("父类构造器执行");}
}class Child extends Parent {// 子类实例变量private String childName = "子类名称";// 子类构造器public Child() {// 隐含super(),调用父类无参构造器(若未显式调用)System.out.println("子类构造器执行");}
}// 测试:创建子类实例
public class Test {public static void main(String[] args) {new Child();// 输出顺序:// 父类构造器执行 → 子类构造器执行}
}
关键说明
-
super()的作用:子类构造器的第一行默认隐含super(),用于调用父类的无参构造器;若父类无无参构造器,子类必须显式调用父类的有参构造器(super(参数)),否则编译报错;class Parent {public Parent(String name) { System.out.println("父类有参构造器"); } // 无无参构造器 }class Child extends Parent {public Child() {super("父类名称"); // 必须显式调用父类有参构造器,否则编译报错System.out.println("子类构造器");} } -
实例变量初始化时机:父类的实例变量在父类构造器执行前初始化,子类的实例变量在子类构造器执行前初始化;
class Parent {private String parentVar = "父类变量初始化"; // 先执行public Parent() {System.out.println(parentVar); // 后执行:输出“父类变量初始化”} }
结论:子类实例初始化必然触发父类实例初始化,遵循“父类优先”原则,确保父类的实例变量和构造器逻辑先执行,为子类提供正确的继承基础。
27. boolean类型占多少位?为什么?
Java语言规范未明确规定boolean类型的占用位数,但在JVM中,boolean类型通常有两种存储方式:
1. 单独使用时(如boolean b = true)
- 占用1字节(8位);
- 原因:JVM的内存分配最小单位是“字节”(1字节),无法直接分配1位内存,为了简化内存管理,将
boolean按1字节存储。
2. 数组中使用时(如boolean[] arr = new boolean[10])
- 占用1位;
- 原因:JVM对
boolean数组做了优化,通过“位压缩”存储(1个字节存储8个boolean值),减少内存占用(若按1字节/个存储,1000个元素需1000字节,优化后仅需125字节)。
关键说明
- Java语言层面不允许直接操作
boolean的二进制位(无位运算支持),仅支持true/false取值; - 不同JVM实现可能存在差异,但主流JVM(如HotSpot)均遵循上述存储规则;
boolean类型的包装类Boolean,由于是对象,存储在堆内存中,占用空间远大于1字节(包括对象头、实例变量等)。
结论:boolean类型的占用位数取决于使用场景,单独使用时1字节,数组中1位,核心是JVM为平衡“内存效率”和“管理复杂度”的优化结果。
28. instanceof关键字的作用是什么?
instanceof是Java的二元运算符,用于判断一个对象是否为某个类(或接口)的实例,返回值为boolean类型(true/false),核心作用是“类型校验”,避免类型转换异常。
核心语法
对象 instanceof 类/接口
关键规则
- 若对象为
null,instanceof返回false(null不是任何类的实例); - 若对象是“类的实例”或“子类的实例”,返回
true; - 若对象是“接口的实现类的实例”,返回
true; - 基本类型不能使用
instanceof(如1 instanceof int报错),需通过包装类(如Integer)使用。
示例代码
// 1. 对象是类的实例
class Animal {}
class Dog extends Animal {}
Animal animal = new Dog();System.out.println(animal instanceof Animal); // true(animal是Animal的实例)
System.out.println(animal instanceof Dog); // true(animal是Dog的实例,Dog是Animal的子类)// 2. 对象是接口的实现类实例
interface Runnable {}
class MyRunnable implements Runnable {}
Runnable runnable = new MyRunnable();
System.out.println(runnable instanceof Runnable); // true(runnable是Runnable的实现类实例)// 3. 对象为null
Animal nullAnimal = null;
System.out.println(nullAnimal instanceof Animal); // false(null不是任何类的实例)// 4. 基本类型不能使用instanceof
// int a = 1;
// System.out.println(a instanceof int); // 错误:基本类型不能用instanceof
Integer b = 1;
System.out.println(b instanceof Integer); // true(包装类可以使用)
典型应用场景
-
避免类型转换异常:在强制类型转换前,用
instanceof校验类型,确保转换安全;Object obj = new String("abc"); if (obj instanceof String) {String str = (String) obj; // 安全转换,无异常System.out.println(str.length()); } -
多态场景下判断对象实际类型:在父类引用指向子类对象时,用
instanceof判断对象的实际类型,执行不同逻辑;public void makeSound(Animal animal) {if (animal instanceof Dog) {System.out.println("汪汪叫");} else if (animal instanceof Cat) {System.out.println("喵喵叫");} }
注意:instanceof不支持“泛型擦除后的类型校验”(如List<String> list = new ArrayList<>(); list instanceof List<String>报错),需通过其他方式(如反射)校验泛型类型。
29. 基本类型的强制类型转换是否会丢失精度?引用类型的强制类型转换需要注意什么?
一、基本类型的强制类型转换:可能丢失精度
基本类型强制转换((目标类型)变量)是否丢失精度,核心取决于源类型与目标类型的取值范围,仅当“目标类型范围 < 源类型范围”时,才可能丢失精度或产生错误值。
1. 不会丢失精度的场景(目标类型范围 ≥ 源类型范围)
当目标类型能完全覆盖源类型的取值范围时,转换后值不变,无精度丢失:
- 整数类型:小范围→大范围(
byte→short→int→long),如byte b = 10; int i = (int)b;(i=10,无丢失); - 浮点类型:
float→double(float精度67位,`double`精度1517位,可完整保留float值); - 字符类型:
char→int(char取值范围065535,完全包含在`int`的02147483647范围内),如char c = 'A'; int i = (int)c;(i=65,无丢失)。
2. 可能丢失精度的场景(目标类型范围 < 源类型范围)
当源类型值超出目标类型范围时,会发生“截断”或“溢出”,导致精度丢失或值错误:
- 整数→整数:大范围→小范围,高位被截断,如
int i = 300; byte b = (byte)i;(byte范围-128~127,300超出范围,b=-56,值错误); - 浮点→整数:小数部分被舍弃(仅保留整数部分),如
double d = 3.9; int i = (int)d;(i=3,小数部分丢失);若浮点值超出整数范围,结果为整数类型的极值,如double d = 2147483648.0; int i = (int)d;(int最大值2147483647,i=-2147483648,溢出错误); - 双精度→单精度:
double→float,若double值超出float范围(±3.4e38),结果为Infinity(无穷大),如double d = 1e40; float f = (float)d;(f=Infinity,精度丢失)。
示例代码
// 1. 整数大范围→小范围:值错误
int intNum = 200;
byte byteNum = (byte)intNum;
System.out.println(byteNum); // -56(200超出byte范围,高位截断)// 2. 浮点→整数:小数部分丢失
double doubleNum = 5.8;
int intNum2 = (int)doubleNum;
System.out.println(intNum2); // 5(小数部分0.8丢失)// 3. double→float:超出范围,结果为无穷大
double bigDouble = 1e40;
float floatNum = (float)bigDouble;
System.out.println(floatNum); // Infinity(无穷大)
二、引用类型的强制类型转换:需注意类型兼容性
引用类型强制转换是“将父类引用转为子类引用”或“接口引用转为实现类引用”,核心要求是类型兼容(即对象的实际类型是目标类型或其子类型),否则会抛出ClassCastException(类型转换异常)。
1. 核心规则
- 向上转型(自动转换,无需强制):父类引用指向子类对象(如
Animal animal = new Dog();),天然兼容,无需强制转换,可直接调用父类的方法; - 向下转型(需强制转换):子类引用指向父类引用(如
Dog dog = (Dog)animal;),仅当父类引用的“实际对象类型”是子类时才合法,否则报错; - 不兼容类型转换:无继承/实现关系的类之间转换(如
String str = (String)new Animal();),编译直接报错。
2. 注意事项
(1)先通过instanceof校验类型,避免ClassCastException
向下转型前,必须用instanceof判断父类引用的实际对象类型是否为目标子类,确保转换安全:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}public class Test {public static void main(String[] args) {Animal animal = new Dog(); // 向上转型:Animal引用指向Dog对象// 错误示例:直接向下转为Cat,实际对象是Dog,抛出ClassCastException// Cat cat = (Cat)animal; // 正确示例:先通过instanceof校验if (animal instanceof Dog) {Dog dog = (Dog)animal; // 安全转换,无异常System.out.println("转换成功:Animal→Dog");}if (animal instanceof Cat) {Cat cat = (Cat)animal; } else {System.out.println("实际对象不是Cat,无法转换");}}
}
(2)接口引用转实现类引用,需确保实际对象是实现类
接口引用指向实现类对象时,向下转为实现类需校验实际类型:
interface Runnable {}
class MyRunnable implements Runnable {}
class OtherRunnable implements Runnable {}public class Test {public static void main(String[] args) {Runnable runnable = new MyRunnable(); // 接口引用指向实现类对象if (runnable instanceof MyRunnable) {MyRunnable myRunnable = (MyRunnable)runnable; // 安全转换}if (runnable instanceof OtherRunnable) {OtherRunnable otherRunnable = (OtherRunnable)runnable;} else {System.out.println("实际对象不是OtherRunnable,无法转换");}}
}
(3)null引用转型不报错,但后续使用会抛NullPointerException
null可以强制转为任何引用类型(编译不报错),但后续调用该引用的方法时,会抛出NullPointerException:
Animal animal = null;
Dog dog = (Dog)animal; // 编译通过,运行不报错(null转型合法)
// dog.bark(); // 错误:NullPointerException(dog是null)
30. 一个汉字占多少位?一个字母占多少位?
在Java中,字符的占用位数取决于编码方式和存储场景(如char类型、文件存储、网络传输),核心区别在于“字符编码规则”:
一、Java中char类型的占用位数(内存中)
Java的char类型固定占用2字节(16位),采用Unicode编码(UTF-16小端序),无论字符是汉字、字母还是符号,单个char都占2字节:
- 字母(如
'A'、'a'):char类型存储,占2字节(16位); - 汉字(如
'中'、'国'):char类型存储,占2字节(16位); - 特殊符号(如
'!'、'@'):char类型存储,占2字节(16位)。
示例代码:
// char类型固定占2字节(16位)
char letter = 'A';
char chinese = '中';
System.out.println(Character.BYTES); // 2(char类型的字节数,固定为2)
二、文件/网络传输中的占用位数(取决于编码)
当字符通过文件存储或网络传输时,会按指定编码(如UTF-8、GBK)转换为字节流,此时汉字和字母的占用位数不同:
| 编码方式 | 单个字母(如’A’、‘a’) | 单个汉字(如’中’、‘国’) | 核心特点 |
|---|---|---|---|
UTF-8 | 1字节(8位) | 3字节(24位)(部分生僻字4字节) | 可变长度编码,兼容ASCII,节省英文存储 |
GBK | 1字节(8位) | 2字节(16位) | 中文编码,仅支持中文字符和ASCII,不兼容Unicode |
UTF-16 | 2字节(16位) | 2字节(16位)(部分生僻字4字节) | 固定长度为主,与Javachar类型编码一致 |
ASCII | 1字节(8位) | 不支持(无法存储汉字) | 仅支持英文和符号,不支持中文 |
示例验证(Java中字符串按编码转字节数组):
import java.io.UnsupportedEncodingException;public class Test {public static void main(String[] args) throws UnsupportedEncodingException {String letter = "A";String chinese = "中";// UTF-8编码:字母1字节,汉字3字节System.out.println(letter.getBytes("UTF-8").length); // 1System.out.println(chinese.getBytes("UTF-8").length); // 3// GBK编码:字母1字节,汉字2字节System.out.println(letter.getBytes("GBK").length); // 1System.out.println(chinese.getBytes("GBK").length); // 2}
}
总结
| 场景 | 单个字母(如’A’) | 单个汉字(如’中’) |
|---|---|---|
Java内存中(char) | 2字节(16位) | 2字节(16位) |
| 文件/传输(UTF-8) | 1字节(8位) | 3字节(24位) |
| 文件/传输(GBK) | 1字节(8位) | 2字节(16位) |
31. ""与null的区别是什么?
""(空字符串)和null都表示“无内容”,但本质是完全不同的概念,核心区别在“是否是对象”“是否占用内存”“使用场景”:
| 对比维度 | “”(空字符串) | null |
|---|---|---|
| 本质 | 是String类的实例对象,内部维护一个长度为0的char[](Java 8)或byte[](Java 9+) | 是“空引用”,表示没有指向任何对象,不关联任何内存空间 |
| 内存占用 | 占用内存(存储String对象的元信息和空数组) | 不占用内存(仅表示“无指向”,无实际对象) |
| 方法调用 | 可调用String类的方法(如length()、equals()),不会抛空指针异常 | 不能调用任何方法(调用会直接抛出NullPointerException) |
| 字符串比较 | 需用equals()比较(如""equals(str)),或str.isEmpty()判断 | 需用str == null判断(不能用equals(),会抛空指针) |
| 默认值 | 可作为String类型变量的默认值(如String str = "";) | 是所有引用类型的默认值(如未初始化的String str;,默认值为null) |
| 示例代码 | java String str = ""; System.out.println(str.length()); // 0(正常调用) System.out.println(str.equals("abc")); // false | java String str = null; // System.out.println(str.length()); // 错误:NullPointerException System.out.println(str == null); // true |
关键注意事项
-
判断空字符串的正确方式:
- 先判断是否为
null,再判断是否为空字符串(避免空指针):String str = ...; if (str != null && str.isEmpty()) {System.out.println("是空字符串"); } - 或使用
Objects.equals(str, "")(JDK 7+),自动处理null:if (Objects.equals(str, "")) {System.out.println("是空字符串"); }
- 先判断是否为
-
避免“将null当作空字符串”:
- 错误示例:
if (str.equals(""))(若str为null,抛空指针); - 正确示例:
if ("".equals(str))(""是对象,调用equals()不会抛空指针,若str为null,返回false)。
- 错误示例:
32. 什么是switch击穿?
switch击穿(Fall-Through)是switch语句的默认行为:当case分支执行完后,若没有break、return或throw语句终止,程序会继续执行下一个case分支的代码,无论下一个case的条件是否匹配,直到遇到终止语句或switch结束。
一、switch击穿的示例
public class Test {public static void main(String[] args) {int num = 2;switch (num) {case 1:System.out.println("执行case 1");// 无break,击穿到case 2case 2:System.out.println("执行case 2");// 无break,击穿到case 3case 3:System.out.println("执行case 3");break; // 有break,终止switchcase 4:System.out.println("执行case 4");}}
}
输出结果:
执行case 2
执行case 3
原因:num=2匹配case 2,执行完case 2的代码后,因无break,击穿到case 3,执行case 3的代码,直到遇到break终止。
二、switch击穿的“合理使用”场景
虽然击穿通常是“需要避免的问题”,但在某些场景下,可利用击穿实现“多个case执行同一逻辑”:
// 需求:输入1~5,输出“工作日”;输入6~7,输出“休息日”
public class Test {public static void main(String[] args) {int day = 3;switch (day) {case 1:case 2:case 3:case 4:case 5:System.out.println("工作日");break; // 多个case共用同一逻辑,最后加breakcase 6:case 7:System.out.println("休息日");break;}}
}
输出结果:工作日
原理:day=3匹配case 3,因case 1~4均无break,击穿到case 5,执行“工作日”逻辑后,break终止,实现“1~5共用同一代码”。
三、避免switch击穿的方法
- 每个case分支末尾加
break:确保执行完当前case后,终止switch(最常用方法); - 用
return替代break:若case分支需返回值,可在执行完逻辑后用return终止(同时终止方法); - 用
throw抛出异常:若case分支是错误场景,可抛出异常终止程序(如throw new IllegalArgumentException("无效参数")); - 使用Java 14+的
switch表达式(箭头语法):箭头语法默认不击穿,无需加break,简洁安全:// Java 14+ switch表达式(箭头语法,无击穿) int day = 3; String result = switch (day) {case 1, 2, 3, 4, 5 -> "工作日"; // 多个case用逗号分隔,箭头语法不击穿case 6, 7 -> "休息日";default -> "无效日期"; }; System.out.println(result); // 工作日
33. finally的作用,一般用于什么场景?
finally是Java异常处理(try-catch-finally)的核心关键字,用于定义“无论try块是否抛出异常、catch块是否执行,都必须执行的代码”,核心作用是“释放资源、保证清理逻辑执行”。
一、finally的核心特性
- 执行时机:
- 若
try块无异常:执行try块代码 → 执行finally块代码; - 若
try块有异常且被catch捕获:执行try块异常前代码 → 执行catch块代码 → 执行finally块代码; - 若
try块有异常且未被catch捕获:执行try块异常前代码 → 执行finally块代码 → 抛出异常给上层;
- 若
- 唯一不执行的情况:若在
try/catch块中调用System.exit(0)(强制终止JVM),finally块不会执行(JVM已退出,无法执行后续代码)。
二、finally的典型应用场景
1. 释放资源(最核心场景)
当try块中使用了“需要手动关闭的资源”(如文件流、数据库连接、网络连接)时,finally块用于确保资源被释放,避免资源泄漏:
- 文件流关闭:
import java.io.FileReader; import java.io.IOException;public class Test {public static void main(String[] args) {FileReader reader = null;try {reader = new FileReader("test.txt"); // 打开文件流// 读取文件逻辑} catch (IOException e) {e.printStackTrace();} finally {// 无论是否有异常,都关闭文件流if (reader != null) {try {reader.close(); // 关闭资源} catch (IOException e) {e.printStackTrace();}}}} } - 数据库连接关闭:
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException;public class Test {public static void main(String[] args) {Connection conn = null;try {conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456"); // 打开数据库连接// 数据库操作逻辑} catch (SQLException e) {e.printStackTrace();} finally {// 无论是否有异常,都关闭数据库连接if (conn != null) {try {conn.close(); // 关闭资源} catch (SQLException e) {e.printStackTrace();}}}} }
2. 清理临时数据/状态
当try块中修改了某些全局状态或临时数据时,finally块用于恢复状态或清理数据:
public class Test {private static boolean isRunning = false;public static void main(String[] args) {try {isRunning = true; // 修改状态为“运行中”// 执行耗时操作int result = 10 / 0; // 抛出异常} catch (ArithmeticException e) {e.printStackTrace();} finally {isRunning = false; // 无论是否有异常,恢复状态为“未运行”System.out.println("状态已恢复:isRunning=" + isRunning);}}
}
输出结果:
java.lang.ArithmeticException: / by zeroat Test.main(Test.java:9)
状态已恢复:isRunning=false
3. 确保返回值之外的逻辑执行
即使try/catch块中有return语句,finally块仍会执行(执行完finally后,再返回try/catch中的值):
public class Test {public static int getNum() {try {return 1; // 执行try的return前,先执行finally} catch (Exception e) {return 2;} finally {System.out.println("finally执行"); // 会执行}}public static void main(String[] args) {System.out.println(getNum());}
}
输出结果:
finally执行
1
三、注意事项
-
避免在finally中使用return:若
finally块有return,会覆盖try/catch块的return值,且可能掩盖异常(不推荐):public static int getNum() {try {return 1;} finally {return 2; // 覆盖try的return值} } public static void main(String[] args) {System.out.println(getNum()); // 输出2(而非1) } -
Java 7+的try-with-resources:对于实现
AutoCloseable接口的资源(如FileReader、Connection),可使用try-with-resources语法替代finally,自动关闭资源,代码更简洁:// try-with-resources:自动关闭reader,无需finally try (FileReader reader = new FileReader("test.txt")) {// 读取文件逻辑 } catch (IOException e) {e.printStackTrace(); }
34. 反射能否获取私有信息,如果能,需要注意什么?
能,Java反射可以获取类的私有信息(私有字段、私有方法、私有构造器),核心是通过Class对象的getDeclaredXXX()系列方法(如getDeclaredField()、getDeclaredMethod()),并调用setAccessible(true)打破访问权限限制。
一、反射获取私有信息的示例
1. 获取并修改私有字段
import java.lang.reflect.Field;class Person {// 私有字段private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Person{name='" + name + "', age=" + age + "}";}
}public class Test {public static void main(String[] args) throws Exception {// 1. 获取Class对象Class<?> personClass = Person.class;// 2. 创建对象Person person = (Person) personClass.getConstructor(String.class, int.class).newInstance("张三", 20);System.out.println("修改前:" + person); // Person{name='张三', age=20}// 3. 获取私有字段nameField nameField = personClass.getDeclaredField("name");// 4. 打破访问权限限制(关键步骤)nameField.setAccessible(true);// 5. 修改私有字段值nameField.set(person, "李四");// 6. 获取私有字段ageField ageField = personClass.getDeclaredField("age");ageField.setAccessible(true);// 7. 获取私有字段值int age = (int) ageField.get(person);System.out.println("当前age:" + age); // 20// 8. 修改私有字段值ageField.set(person, 25);System.out.println("修改后:" + person); // Person{name='李四', age=25}}
}
2. 获取并调用私有方法
import java.lang.reflect.Method;class Person {// 私有方法private void sayHello(String msg) {System.out.println("Hello, " + msg);}
}public class Test {public static void main(String[] args) throws Exception {Class<?> personClass = Person.class;Person person = (Person) personClass.getConstructor().newInstance();// 1. 获取私有方法sayHello(参数:方法名 + 参数类型)Method sayHelloMethod = personClass.getDeclaredMethod("sayHello", String.class);// 2. 打破访问权限限制sayHelloMethod.setAccessible(true);// 3. 调用私有方法(参数:对象实例 + 方法参数)sayHelloMethod.invoke(person, "反射!"); // 输出:Hello, 反射!}
}
3. 获取并调用私有构造器
import java.lang.reflect.Constructor;class Person {private String name;// 私有构造器private Person(String name) {this.name = name;}@Overridepublic String toString() {return "Person{name='" + name + "'}";}
}public class Test {public static void main(String[] args) throws Exception {Class<?> personClass = Person.class;// 1. 获取私有构造器(参数:构造器参数类型)Constructor<?> privateConstructor = personClass.getDeclaredConstructor(String.class);// 2. 打破访问权限限制privateConstructor.setAccessible(true);// 3. 调用私有构造器创建对象Person person = (Person) privateConstructor.newInstance("王五");System.out.println(person); // Person{name='王五'}}
}
二、需要注意的事项
- 访问权限风险:
setAccessible(true)会跳过Java的访问权限检查(private、protected等修饰符失效),可能破坏类的封装性,导致代码逻辑混乱(如随意修改私有字段,违背类的设计意图); - 性能损耗:反射操作需要动态解析类的元信息(如字段、方法签名),比直接调用(非反射)慢10~100倍,频繁使用会影响程序性能,不建议在高性能场景(如循环、高频接口)中使用;
- 安全性限制:若程序运行在有安全管理器(SecurityManager)的环境中(如Applet),
setAccessible(true)可能被安全策略禁止,抛出SecurityException; - 代码可读性差:反射代码通过字符串指定字段/方法名(如
getDeclaredField("name")),编译器无法进行语法检查,若字段名/方法名写错,仅在运行时抛出NoSuchFieldException或NoSuchMethodException,排查难度大; - 版本兼容性问题:若类的私有成员(如字段名、方法参数)在后续版本中修改,反射代码会因找不到对应成员而抛出异常,且编译器无法提前预警,兼容性差。
三、使用建议
- 仅在“必须动态访问私有成员”的场景中使用(如框架开发:Spring的依赖注入、MyBatis的ORM映射);
- 尽量避免在业务代码中使用反射访问私有成员,优先通过类提供的
public方法(如getter/setter)操作; - 若使用反射,建议对字段名/方法名进行常量定义(如
private static final String FIELD_NAME = "name"),减少拼写错误; - 敏感场景下,可通过自定义安全管理器限制反射的访问权限。
35. 什么是泛型,请写一个泛型队列或者泛型栈。
一、什么是泛型
泛型(Generic)是Java 5引入的特性,用于在编译时指定集合或类的元素类型,核心作用是“类型安全”和“代码复用”:
- 类型安全:编译时检查元素类型,避免运行时抛出
ClassCastException(如避免将Integer存入List<String>); - 代码复用:无需为不同类型(如
String、Integer)编写重复代码,一个泛型类/方法可适配多种类型。
泛型的核心概念
- 泛型类:类定义时声明类型参数(如
class List<E>,E为类型参数,代表“元素类型”); - 泛型方法:方法定义时声明类型参数(如
public <T> T getValue(T obj),T为类型参数); - 类型擦除:Java泛型是“编译时泛型”,运行时会擦除类型参数(如
List<String>在运行时变为List),仅在编译时提供类型检查。
二、泛型队列实现(基于链表)
队列(Queue)遵循“先进先出(FIFO)”原则,以下是基于链表的泛型队列实现,支持enqueue(入队)、dequeue(出队)、peek(查看队首)、isEmpty(判断空)等核心操作:
// 泛型队列:基于链表实现,支持任意类型元素
public class GenericQueue<T> {// 链表节点类(泛型)private static class Node<T> {T data; // 节点存储的数据Node<T> next; // 下一个节点的引用Node(T data) {this.data = data;this.next = null;}}private Node<T> front; // 队首(出队端)private Node<T> rear; // 队尾(入队端)private int size; // 队列元素个数// 构造器:初始化空队列public GenericQueue() {front = null;rear = null;size = 0;}// 1. 入队:将元素添加到队尾public void enqueue(T data) {Node<T> newNode = new Node<>(data);if (isEmpty()) {// 空队列时,队首和队尾都指向新节点front = newNode;rear = newNode;} else {// 非空队列时,队尾指向新节点,更新队尾rear.next = newNode;rear = newNode;}size++;}// 2. 出队:移除并返回队首元素(空队列抛出异常)public T dequeue() {if (isEmpty()) {throw new IllegalStateException("队列为空,无法出队");}// 获取队首元素T data = front.data;// 更新队首为下一个节点front = front.next;size--;// 若出队后为空,队尾也置为nullif (isEmpty()) {rear = null;}return data;}// 3. 查看队首元素(不移除,空队列抛出异常)public T peek() {if (isEmpty()) {throw new IllegalStateException("队列为空,无队首元素");}return front.data;}// 4. 判断队列是否为空public boolean isEmpty() {return size == 0;}// 5. 获取队列元素个数public int size() {return size;}// 测试泛型队列public static void main(String[] args) {// 1. 存储String类型的队列GenericQueue<String> stringQueue = new GenericQueue<>();stringQueue.enqueue("Java");stringQueue.enqueue("泛型");stringQueue.enqueue("队列");System.out.println("String队列大小:" + stringQueue.size()); // 3System.out.println("队首元素:" + stringQueue.peek()); // JavaSystem.out.println("出队元素:" + stringQueue.dequeue()); // JavaSystem.out.println("出队后大小:" + stringQueue.size()); // 2// 2. 存储Integer类型的队列GenericQueue<Integer> intQueue = new GenericQueue<>();intQueue.enqueue(10);intQueue.enqueue(20);intQueue.enqueue(30);System.out.println("\nInteger队列大小:" + intQueue.size()); // 3while (!intQueue.isEmpty()) {System.out.println("出队元素:" + intQueue.dequeue()); // 10 → 20 → 30}// 3. 空队列出队(抛出异常)// intQueue.dequeue(); // 抛出IllegalStateException: 队列为空,无法出队}
}
输出结果:
String队列大小:3
队首元素:Java
出队元素:Java
出队后大小:2Integer队列大小:3
出队元素:10
出队元素:20
出队元素:30
三、泛型栈实现(基于数组)
栈(Stack)遵循“后进先出(LIFO)”原则,以下是基于数组的泛型栈实现,支持push(入栈)、pop(出栈)、peek(查看栈顶)等操作:
// 泛型栈:基于数组实现,支持任意类型元素
public class GenericStack<T> {private T[] stack; // 存储栈元素的数组private int top; // 栈顶指针(指向栈顶元素的下一个位置)private int capacity; // 栈的初始容量// 构造器:默认初始容量为10@SuppressWarnings("unchecked") // 抑制“未检查的转换”警告public GenericStack() {capacity = 10;stack = (T[]) new Object[capacity]; // 泛型数组不能直接创建,需通过Object数组转换top = 0;}// 构造器:自定义初始容量@SuppressWarnings("unchecked")public GenericStack(int capacity) {this.capacity = capacity;stack = (T[]) new Object[capacity];top = 0;}// 1. 入栈:将元素添加到栈顶(栈满时扩容)public void push(T data) {// 栈满,扩容为原容量的2倍if (isFull()) {resize(capacity * 2);}stack[top] = data;top++;}// 2. 出栈:移除并返回栈顶元素(栈空时抛出异常)public T pop() {if (isEmpty()) {throw new IllegalStateException("栈为空,无法出栈");}top--;T data = stack[top];stack[top] = null; // 释放引用,避免内存泄漏return data;}// 3. 查看栈顶元素(不移除,栈空时抛出异常)public T peek() {if (isEmpty()) {throw new IllegalStateException("栈为空,无栈顶元素");}return stack[top - 1];}// 4. 判断栈是否为空public boolean isEmpty() {return top == 0;}// 5. 判断栈是否已满public boolean isFull() {return top == capacity;}// 6. 获取栈元素个数public int size() {return top;}// 扩容:创建新数组,复制原数组元素@SuppressWarnings("unchecked")private void resize(int newCapacity) {T[] newStack = (T[]) new Object[newCapacity];for (int i = 0; i < top; i++) {newStack[i] = stack[i];}stack = newStack;capacity = newCapacity;}// 测试泛型栈public static void main(String[] args) {// 存储Double类型的栈GenericStack<Double> doubleStack = new GenericStack<>();doubleStack.push(1.1);doubleStack.push(2.2);doubleStack.push(3.3);System.out.println("Double栈大小:" + doubleStack.size()); // 3System.out.println("栈顶元素:" + doubleStack.peek()); // 3.3System.out.println("出栈元素:" + doubleStack.pop()); // 3.3System.out.println("出栈后大小:" + doubleStack.size()); // 2// 存储Person类型的栈(自定义类)class Person {private String name;Person(String name) { this.name = name; }@Overridepublic String toString() { return "Person{" + "name='" + name + "'}"; }}GenericStack<Person> personStack = new GenericStack<>();personStack.push(new Person("张三"));personStack.push(new Person("李四"));System.out.println("\nPerson栈大小:" + personStack.size()); // 2while (!personStack.isEmpty()) {System.out.println("出栈元素:" + personStack.pop()); // Person{name='李四'} → Person{name='张三'}}}
}
输出结果:
Double栈大小:3
栈顶元素:3.3
出栈元素:3.3
出栈后大小:2Person栈大小:2
出栈元素:Person{name='李四'}
出栈元素:Person{name='张三'}
36. 基本类型的数据存在JVM的哪个区域?
基本类型的数据存储位置取决于是否为局部变量,核心分为“栈内存”和“堆内存”两类,具体如下:
一、局部变量:存储在“虚拟机栈”的栈帧中
局部变量是定义在方法、代码块(如if、for)中的基本类型变量,存储在虚拟机栈(VM Stack) 的“栈帧(Stack Frame)”中:
- 栈帧:每个方法执行时,JVM会创建一个栈帧,栈帧包含“局部变量表”“操作数栈”等,局部变量存储在“局部变量表”中;
- 生命周期:方法执行时,栈帧入栈;方法执行完毕,栈帧出栈,局部变量随栈帧销毁,内存自动释放;
- 示例:
public void test() {int a = 10; // 局部变量a,存储在test()方法的栈帧局部变量表中double b = 3.14; // 局部变量b,同样存储在局部变量表中 }
二、非局部变量(成员变量、静态变量):存储在“堆”或“方法区”
非局部变量是定义在类中的基本类型变量(成员变量、静态变量),存储位置与变量类型(实例变量/静态变量)相关:
1. 实例变量(成员变量):存储在“堆内存”
实例变量是类中无static修饰的基本类型变量,属于对象的一部分,存储在堆内存(Heap) 中:
- 存储逻辑:当创建对象(
new)时,对象的实例变量随对象一起存储在堆中; - 生命周期:对象被GC(垃圾回收)回收时,实例变量随对象销毁;
- 示例:
class Person {int age; // 实例变量age,随Person对象存储在堆中boolean isStudent; // 实例变量isStudent,同样存储在堆中 }public void test() {Person person = new Person(); // person对象存储在堆中,age和isStudent也在堆中person.age = 20; // 修改堆中age的值 }
2. 静态变量(类变量):存储在“方法区”
静态变量是类中用static修饰的基本类型变量,属于类的一部分,存储在方法区(Method Area) 中:
- 存储逻辑:类加载时,静态变量被初始化,存储在方法区的“静态变量区”(JDK 8后,方法区的静态变量移至堆中的“元空间(Metaspace)”,但概念上仍属于方法区范畴);
- 生命周期:类卸载时,静态变量随类销毁,生命周期与类一致;
- 示例:
class MathUtil {public static final int MAX_VALUE = 100; // 静态变量MAX_VALUE,存储在方法区public static int count = 0; // 静态变量count,同样存储在方法区 }
三、总结:基本类型存储位置
| 变量类型 | 定义位置 | 存储区域 | 生命周期 |
|---|---|---|---|
| 局部变量 | 方法、代码块中 | 虚拟机栈-栈帧局部变量表 | 方法执行期间,随栈帧销毁 |
| 实例变量(成员变量) | 类中,无static修饰 | 堆内存 | 随对象,对象被GC回收时销毁 |
| 静态变量(类变量) | 类中,有static修饰 | 方法区(JDK 8后为元空间) | 随类,类卸载时销毁 |
37. Throw和Throws的区别
throw和throws均用于Java异常处理,但作用和使用方式完全不同:throw用于“主动抛出异常对象”,throws用于“声明方法可能抛出的异常类型”,核心区别如下:
| 对比维度 | throw | throws |
|---|---|---|
| 关键字类型 | 语句关键字(用于执行抛出异常的动作) | 方法修饰符(用于声明异常) |
| 作用 | 主动创建并抛出异常对象(触发异常) | 声明方法可能抛出的异常类型,告知调用者“需处理这些异常” |
| 使用位置 | 方法体内部(如if判断后、逻辑错误处) | 方法签名末尾(如public void test() throws IOException) |
| 语法格式 | throw new 异常类(异常信息);(只能抛出一个异常对象) | 方法返回值 方法名(参数列表) throws 异常类1, 异常类2,...;(可声明多个异常类型) |
| 异常类型 | 可抛出编译时异常(需配合throws或try-catch)或运行时异常 | 声明的异常类型可为编译时异常或运行时异常(运行时异常声明可省略) |
| 示例代码 | java public void test() { int a = -1; if (a < 0) { // 主动抛出运行时异常 throw new IllegalArgumentException("a不能为负数"); } } | java // 声明方法可能抛出IOException(编译时异常) public void readFile() throws IOException { FileReader reader = new FileReader("test.txt"); reader.close(); } |
关键使用场景
1. throw的使用场景
- 主动触发异常:当代码逻辑不符合预期时(如参数非法、状态错误),主动抛出异常,终止当前逻辑;
- 抛出运行时异常:无需在方法上声明
throws(运行时异常是RuntimeException子类,编译器不强制声明); - 抛出编译时异常:需在方法上用
throws声明该异常,或在try-catch中捕获;// 抛出编译时异常,需配合throws public void test() throws Exception {throw new Exception("主动抛出编译时异常"); }
2. throws的使用场景
- 声明编译时异常:方法内部可能抛出编译时异常(如
IOException),但不希望在方法内部捕获,而是交给调用者处理,需用throws声明; - 声明多个异常:方法可能抛出多种异常,用逗号分隔声明;
// 声明方法可能抛出IOException和SQLException public void test() throws IOException, SQLException {// 可能抛出IOException的代码// 可能抛出SQLException的代码 } - 避免冗余try-catch:在多层方法调用中,上层方法可通过
throws将异常向上传递,由最外层统一处理,减少中间层的try-catch冗余。
注意事项
- throw不能单独使用:
throw必须配合异常对象(如throw new NullPointerException()),且抛出后,后续代码不会执行(异常会中断当前代码流); - throws不处理异常:
throws仅声明异常,不处理异常,异常最终需由调用者通过try-catch处理,或继续向上传递; - 运行时异常无需throws声明:
RuntimeException及其子类(如NullPointerException、IllegalArgumentException)可不用throws声明,编译器不强制,但建议在方法注释中说明可能抛出的运行时异常。
38. Object类的方法有哪些?分别有什么作用?
Object是Java中所有类的顶层父类(包括数组、自定义类,默认隐式继承Object),它定义了11个核心方法,这些方法被所有子类继承,是Java面向对象体系的基础。以下是所有方法的详细解析,按常用程度排序:
一、常用核心方法(7个)
1. public String toString()
- 核心作用:返回对象的“字符串表示形式”,用于快速查看对象的关键信息。
- 默认实现:返回格式为
全类名@哈希码的十六进制(如com.test.Person@1b6d3586),其中哈希码由hashCode()方法生成,无实际业务意义。 - 重写建议:几乎所有自定义类都需重写
toString(),返回对象的字段信息(如Person{name='张三', age=20}),方便日志打印、调试和业务展示。 - 示例:
class Person {private String name;private int age;@Overridepublic String toString() {return "Person{name='" + name + "', age=" + age + "}"; // 重写后返回字段信息} }// 使用:System.out.println(obj) 本质是调用 obj.toString() Person person = new Person("张三", 20); System.out.println(person); // 输出:Person{name='张三', age=20}
2. public boolean equals(Object obj)
- 核心作用:判断两个对象是否“相等”,默认比较“内存地址”,可重写为比较“对象内容”。
- 默认实现:
return (this == obj);,即同==运算符,仅当两个引用指向同一对象时返回true。 - 重写场景:当需要按“内容相等”判断对象(如
String比较字符序列、Person比较身份证号)时,必须重写equals()。 - 重写规则(Java规范):
- 自反性:
x.equals(x)必须返回true; - 对称性:若
x.equals(y)为true,则y.equals(x)也必须为true; - 传递性:若
x.equals(y)和y.equals(z)为true,则x.equals(z)也必须为true; - 一致性:多次调用
x.equals(y),结果需一致(对象未修改时); - 非空性:
x.equals(null)必须返回false。
- 自反性:
- 示例:
class Person {private String id; // 按身份证号判断相等@Overridepublic boolean equals(Object o) {if (this == o) return true; // 先判断内存地址,优化性能if (o == null || getClass() != o.getClass()) return false; // 判空+判类型Person person = (Person) o;return Objects.equals(id, person.id); // 用Objects.equals避免空指针} }
3. public int hashCode()
- 核心作用:返回对象的“哈希码”(一个int值),用于哈希表集合(如
HashMap、HashSet)的“哈希桶定位”,提高查询效率。 - 默认实现:返回对象的“内存地址相关值”(不同对象的哈希码大概率不同)。
- 关键规则(与equals()联动):
- 若
x.equals(y) == true,则x.hashCode() == y.hashCode()必须成立(否则哈希表集合无法识别相等对象,导致去重、查询错误); - 若
x.hashCode() == y.hashCode(),则x.equals(y)不一定为true(哈希冲突,不同对象可能有相同哈希码)。
- 若
- 重写要求:重写
equals()时必须重写hashCode(),且哈希码的计算逻辑需与equals()一致(如Person的hashCode()基于id计算)。 - 示例:
class Person {private String id;@Overridepublic boolean equals(Object o) { /* 实现见上文 */ }@Overridepublic int hashCode() {return Objects.hash(id); // 基于id计算哈希码,与equals()逻辑一致} }
4. protected Object clone() throws CloneNotSupportedException
- 核心作用:创建并返回当前对象的“拷贝”,实现对象的复制(默认是 浅拷贝)。
- 使用前提:
- 类必须实现
Cloneable接口(标记接口,无任何方法,仅用于告知JVM“该类支持克隆”); - 若未实现
Cloneable,调用clone()会抛出CloneNotSupportedException。
- 类必须实现
- 拷贝类型:
- 浅拷贝:仅复制对象本身和基本类型字段,引用类型字段仅复制“引用地址”(不复制引用指向的对象,即原对象和拷贝对象共享引用类型字段的对象);
- 深拷贝:需手动实现,复制对象本身、基本类型字段,以及引用类型字段指向的对象(原对象和拷贝对象完全独立)。
- 示例(浅拷贝):
// 实现Cloneable接口,支持克隆 class Person implements Cloneable {private String name; // 基本类型包装类(String不可变,浅拷贝无问题)private List<String> hobbies; // 引用类型(浅拷贝仅复制引用)@Overrideprotected Person clone() throws CloneNotSupportedException {return (Person) super.clone(); // 调用Object的clone(),默认浅拷贝} }// 使用 Person p1 = new Person("张三", Arrays.asList("篮球", "游戏")); Person p2 = p1.clone(); System.out.println(p1 == p2); // false(p2是新对象) System.out.println(p1.getHobbies() == p2.getHobbies()); // true(共享hobbies对象,浅拷贝特性)
5. public final Class<?> getClass()
- 核心作用:返回当前对象所属类的
Class对象(反射的核心入口),通过Class对象可获取类的元信息(字段、方法、构造器等)。 - 关键特性:
- 方法用
final修饰,不能被重写(确保返回的Class对象真实反映对象的类型); - 同一类在JVM中仅存在一个
Class对象,无论通过哪种方式(getClass()、类名.class、Class.forName())获取,都是同一实例。
- 方法用
- 示例(反射场景):
Person person = new Person(); Class<?> clazz = person.getClass(); // 获取Person类的Class对象// 通过Class对象获取类信息 System.out.println(clazz.getName()); // 输出全类名:com.test.Person Field[] fields = clazz.getDeclaredFields(); // 获取所有字段(包括私有)
6. public final void notify()
- 核心作用:唤醒当前对象“等待池”中的任意一个线程,使其从“等待态”进入“就绪态”,参与对象锁的竞争(获取锁后继续执行)。
- 使用场景:配合
wait()实现线程间通信(如生产者-消费者模式,生产者唤醒消费者)。 - 使用限制:
- 必须在
synchronized同步块或同步方法中调用(否则抛出IllegalMonitorStateException),因为调用者需持有当前对象的“对象锁”; - 唤醒的线程是随机的(无法指定唤醒某个线程)。
- 必须在
- 示例(线程通信):
class Monitor {private boolean flag = false;// 生产者:修改flag并唤醒消费者public synchronized void produce() {flag = true;notify(); // 唤醒等待该对象锁的线程}// 消费者:等待flag为true,被唤醒后执行public synchronized void consume() throws InterruptedException {while (!flag) {wait(); // 释放对象锁,进入等待池}flag = false;System.out.println("消费完成");} }
7. public final void wait() throws InterruptedException
- 核心作用:使当前线程从“运行态”进入“等待态”,并释放当前对象的锁,直到被
notify()/notifyAll()唤醒,或被中断。 - 使用限制:
- 必须在
synchronized同步块或同步方法中调用(需持有对象锁,否则抛出IllegalMonitorStateException); - 线程被唤醒后,需重新竞争对象锁(获取锁后才会继续执行
wait()之后的代码)。
- 必须在
- 重载方法:
wait(long timeout):等待timeout毫秒(超时后自动唤醒,无需notify());wait(long timeout, int nanos):更精确的等待(timeout毫秒 +nanos纳秒,nanos范围 0~999999)。
- 注意事项:
wait()需放在while循环中(而非if),避免“虚假唤醒”(线程被唤醒后,需重新校验条件是否满足,防止条件不满足时继续执行)。 - 示例(避免虚假唤醒):
public synchronized void consume() throws InterruptedException {// 用while循环,唤醒后重新校验条件while (!flag) {wait(); }// 业务逻辑 }
二、其他方法(4个)
8. public final void notifyAll()
- 核心作用:唤醒当前对象“等待池”中的所有线程,使其进入“就绪态”,参与对象锁的竞争。
- 与
notify()的区别:notify()唤醒任意一个线程,notifyAll()唤醒所有线程(适用于多个线程等待同一资源的场景,如多个消费者等待生产者的产品)。 - 使用限制:同
notify(),需在synchronized中调用。
9. public final void wait(long timeout) throws InterruptedException
- 核心作用:带超时的等待,线程进入等待态后,若在
timeout毫秒内未被notify()/notifyAll()唤醒,则自动唤醒,避免线程永久等待。 - 示例:
public synchronized void consume() throws InterruptedException {// 等待1秒,超时后自动唤醒wait(1000); // 业务逻辑 }
10. public final void wait(long timeout, int nanos) throws InterruptedException
- 核心作用:比
wait(long timeout)更精确的等待,支持纳秒级超时(timeout毫秒 +nanos纳秒)。 - 注意事项:
nanos取值范围为 0~999999,若超过则向上取整到毫秒(如nanos=1000000等价于timeout+1毫秒),实际开发中较少使用(精度需求高时优先用java.util.concurrent包的工具类)。
11. protected void finalize() throws Throwable
- 核心作用:对象被垃圾回收器(GC)回收前,JVM会调用该方法,用于释放对象持有的资源(如关闭文件流、数据库连接)。
- 现状与问题:
- JDK 9 后被标记为
@Deprecated(过时),不推荐使用; - 执行时机不确定(GC触发时间不确定,
finalize()可能迟迟不执行,导致资源泄漏); - 可能导致对象“复活”(在
finalize()中使对象重新被引用,导致GC无法回收)。
- JDK 9 后被标记为
- 替代方案:资源释放优先使用
try-with-resources(自动关闭实现AutoCloseable接口的资源)或finally块(确保资源被释放)。
三、Object类方法总结
| 方法签名 | 核心作用 | 关键注意点 |
|---|---|---|
toString() | 返回对象字符串表示 | 必须重写,方便调试和展示 |
equals(Object) | 判断对象是否相等 | 重写时需遵循5大规则,且必须重写 hashCode() |
hashCode() | 返回对象哈希码 | 计算逻辑需与 equals() 一致,支持哈希表集合 |
clone() | 复制对象 | 需实现 Cloneable,默认浅拷贝,深拷贝需手动实现 |
getClass() | 获取 Class 对象 | 反射入口,final 方法不可重写 |
notify() | 唤醒任意等待线程 | 需在 synchronized 中调用,随机唤醒 |
notifyAll() | 唤醒所有等待线程 | 需在 synchronized 中调用,适用于多线程等待场景 |
wait() | 线程等待并释放锁 | 需在 synchronized 中调用,放 while 循环防虚假唤醒 |
wait(long) | 带超时的线程等待 | 超时后自动唤醒,避免永久等待 |
wait(long, int) | 纳秒级超时等待 | 精度高,实际使用少 |
finalize() | 垃圾回收前释放资源 | 已过时,推荐 try-with-resources 或 finally |
Object类的方法是Java面向对象的基础,尤其是 equals()、hashCode()、toString() 和反射相关的 getClass(),在日常开发和框架底层(如集合、ORM)中被广泛使用,必须熟练掌握其原理和正确用法。
