引言
在Java编程中,内部类和包装类是两个看似基础却内涵丰富的核心概念。内部类提供了更优雅的封装和逻辑组织方式,而包装类则架起了基本数据类型与面向对象世界之间的桥梁。掌握它们,不仅能写出更清晰、更灵活的代码,也是应对面试和技术深造的必备技能。
本文将系统性地讲解Java内部类的四大类型(实例内部类、静态内部类、局部内部类、匿名内部类)以及八大包装类(Integer、Character、Boolean等)的全套知识点。每个知识点都配有详细的代码示例和注释,力求让你一次学懂、学透。
目录
- 7.3 内部类
- 7.3.1 实例内部类(成员内部类)
- 7.3.2 静态内部类
- 7.3.3 局部内部类
- 7.3.4 匿名内部类
- 四种内部类核心差异对比表
- 7.4 包装类
- 7.4.1 装箱与拆箱
- 7.4.2 基本类型与String互转
- 7.4.3 包装类重写的重要方法
- 7.4.4 Character类特殊格式规范
- 总结与常见面试题
- 全文总结
- 高频面试题
7.3 内部类
内部类(Inner Class)是定义在另一个类内部的类。根据定义的位置和方式,可以分为以下四种类型。
7.3.1 实例内部类(成员内部类)
语法与定义 实例内部类是最常见的内部类,它像是外部类的一个普通成员(类似于实例变量),没有static修饰。
public class OuterClass {
// 外部类成员变量
private String outerField = "外部类字段";
// 实例内部类定义
class InstanceInnerClass {
private String innerField = "内部类字段";
public void print() {
// **内部类可以直接访问外部类的私有成员**
System.out.println("访问外部类私有字段: " + outerField);
System.out.println("内部类自身字段: " + innerField);
}
}
}
核心特性与要点
OuterClass.InstanceInnerClass inner = outer.new InstanceInnerClass();
private String field = "内部类字段";
public void show() {
String field = "局部变量";
System.out.println(field); // 局部变量
System.out.println(this.field); // 内部类字段
System.out.println(OuterClass.this.field); // 外部类字段
}
}
7.3.2 静态内部类
语法与定义 使用static修饰的内部类。它属于外部类本身,而不是外部类的某个实例。
public class OuterClass {
private static String staticOuterField = "外部类静态字段";
private String instanceOuterField = "外部类实例字段";
// 静态内部类定义
static class StaticInnerClass {
private String innerField = "静态内部类字段";
public void print() {
// **只能直接访问外部类的静态成员**
System.out.println("访问外部类静态字段: " + staticOuterField);
// System.out.println(instanceOuterField); // 编译错误!不能直接访问外部类实例成员
}
}
}
核心特性与要点
7.3.3 局部内部类
语法与定义 定义在方法、构造器或代码块内部的类。其作用域仅限于所在的代码块。
public class OuterClass {
public void someMethod() {
final String localVar = "方法局部变量(必须是final或等效final)";
// 局部内部类定义
class LocalInnerClass {
private String innerField = "局部内部类字段";
public void display() {
// **可以访问外部类的成员,以及所在方法的final/等效final局部变量**
System.out.println("访问方法局部变量: " + localVar);
System.out.println("内部类字段: " + innerField);
}
}
// 只能在定义它的方法内部创建和使用
LocalInnerClass localInner = new LocalInnerClass();
localInner.display();
}
}
核心特性与要点
- 可以访问外部类的所有成员。
- 在Java 8之前,只能访问所在方法中声明为final的局部变量。Java 8及之后,可以访问**等效final(effectively final)**的局部变量(即初始化后值不再改变的变量)。
7.3.4 匿名内部类
语法与定义 没有显式类名的局部内部类。通常用于一次性实现一个接口或继承一个类,并创建其对象。
public class OuterClass {
public void testAnonymousClass() {
// 1. 基于接口创建匿名内部类
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类实现的run方法");
}
};
new Thread(runnable).start();
// 2. 基于抽象类创建匿名内部类
Animal animal = new Animal("小狗") {
@Override
public void makeSound() {
System.out.println(name + " 汪汪叫!");
}
};
animal.makeSound();
}
}
// 一个抽象类示例
abstract class Animal {
String name;
Animal(String name) { this.name = name; }
abstract void makeSound();
}
核心特性与要点
内部类小节总结
- 实例内部类:最常用,持有外部类引用,可访问其所有成员。
- 静态内部类:独立,不持有引用,只能访问外部类静态成员。
- 局部内部类:定义在方法内,作用域受限,用于方法内部复杂逻辑封装。
- 匿名内部类:无类名,用于一次性实现,Lambda表达式是其重要演进。
四种内部类核心差异对比表
为了更清晰地展示四种内部类的区别,下表从多个维度进行横向对比:
| 定义位置 | 外部类的成员位置(类内,方法外) | 外部类的成员位置,用static修饰 | 方法、构造器或代码块内部 | 方法、构造器或代码块内部,无显式类名 |
| 访问修饰符 | 可以是public、protected、private、包私有 | 可以是public、protected、private、包私有 | 不能有访问修饰符(作用域已是方法内部) | 不能有访问修饰符 |
| 是否持有外部类引用 | 是(隐式持有外部类名.this) | 否 | 是(如果外部类是实例方法) | 是(如果外部类是实例方法) |
| 访问外部类成员 | 可直接访问外部类所有成员(包括私有) | 只能直接访问外部类静态成员;访问实例成员需通过外部类对象 | 可访问外部类所有成员 | 可访问外部类所有成员 |
| 访问局部变量 | 不涉及(定义在类级别) | 不涉及(定义在类级别) | 只能访问所在作用域内final或等效final的局部变量 | 只能访问所在作用域内final或等效final的局部变量 |
| 能否有静态成员 | Java 16前:不能(除static final常量)Java 16+:可以 | 可以 | 不能 | 不能 |
| 创建方式 | 外部类对象.new 内部类名() | new 外部类名.内部类名() | 在定义它的方法/代码块内直接new | new 接口名/类名() { … } |
| 生命周期 | 依赖于外部类实例,与外部类对象共存亡 | 独立于外部类实例,类加载时初始化 | 与所在方法/代码块执行周期相关 | 通常一次性使用,与创建它的语句执行周期相关 |
| 典型应用场景 | 紧密关联外部类状态,需要访问外部类实例成员的场景 | 工具类、辅助类、与外部类相关但逻辑独立(如Map.Entry) | 方法内部需要复杂逻辑封装,且该逻辑仅在该方法内使用 | 快速实现接口/抽象类,创建监听器、线程任务、比较器等一次性对象 |
| 与Lambda关系 | 无关 | 无关 | 无关 | Java 8+中,Lambda可替代基于函数式接口的匿名内部类 |
7.4 包装类
Java为八种基本数据类型提供了对应的引用类型,称为包装类(Wrapper Class),用于在需要对象的场景中代表基本类型。
| byte | Byte | Number -> Object |
| short | Short | Number -> Object |
| int | Integer | Number -> Object |
| long | Long | Number -> Object |
| float | Float | Number -> Object |
| double | Double | Number -> Object |
| char | Character | Object |
| boolean | Boolean | Object |
7.4.1 装箱与拆箱
装箱(Boxing):将基本数据类型转换为对应的包装类对象。 拆箱(Unboxing):将包装类对象转换为对应的基本数据类型。
public class BoxingUnboxingDemo {
public static void main(String[] args) {
// 手动装箱(JDK5之前)
Integer obj1 = Integer.valueOf(100);
// 手动拆箱
int int1 = obj1.intValue();
// **自动装箱与拆箱(JDK5之后)**
Integer obj2 = 200; // 自动装箱:编译器转换为 Integer.valueOf(200)
int int2 = obj2; // 自动拆箱:编译器转换为 obj2.intValue()
// 注意:自动装箱调用的是 valueOf 方法,涉及缓存机制
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true,因为-128~127有缓存
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false,超出缓存范围,比较的是对象引用
System.out.println(c.equals(d)); // true,应使用equals比较值
}
}
7.4.2 基本类型与String互转
基本类型 -> String
String -> 基本类型
public class PrimitiveStringConversion {
public static void main(String[] args) {
// int -> String
int num = 42;
String str1 = num + ""; // 方式1
String str2 = String.valueOf(num); // 方式2(推荐)
String str3 = Integer.toString(num); // 方式3
// String -> int
String strNum = "123";
int val1 = Integer.parseInt(strNum); // 方式1(推荐)
int val2 = Integer.valueOf(strNum); // 方式2(自动拆箱)
System.out.println("字符串转int结果: " + val1);
}
}
7.4.3 包装类重写的重要方法
所有包装类都重写了Object类的以下方法,使其行为更符合值对象的预期。
equals(Object obj):比较包装对象封装的基本值是否相等。
Integer x = 1000;
Integer y = 1000;
System.out.println(x.equals(y)); // true,比较值
hashCode():返回所封装基本值的哈希码。
System.out.println(x.hashCode()); // 1000
toString():返回该基本值的字符串表示。
System.out.println(x.toString()); // "1000"
compareTo(T another)(实现Comparable接口):比较两个包装对象的值。
System.out.println(x.compareTo(999)); // 1 (大于)
System.out.println(x.compareTo(1000)); // 0 (等于)
System.out.println(x.compareTo(1001)); // -1 (小于)
7.4.4 Character类特殊格式规范
Character类除了提供char的包装功能,还包含大量静态工具方法,用于判断字符类别和进行字符转换。
常用判断方法
public class CharacterDemo {
public static void main(String[] args) {
char ch1 = 'A';
char ch2 = '4';
char ch3 = ' ';
char ch4 = 'a';
System.out.println(Character.isUpperCase(ch1)); // true,是否大写字母
System.out.println(Character.isDigit(ch2)); // true,是否数字
System.out.println(Character.isWhitespace(ch3)); // true,是否空白字符
System.out.println(Character.isLetter(ch4)); // true,是否字母
System.out.println(Character.isLetterOrDigit(ch2)); // true,是否字母或数字
}
}
常用转换方法
public class CharacterConversion {
public static void main(String[] args) {
char lower = 'h';
char upper = 'H';
System.out.println(Character.toUpperCase(lower)); // 'H'
System.out.println(Character.toLowerCase(upper)); // 'h'
// 获取字符的数字值(如'7' -> 7)
char digitChar = '7';
int digitValue = Character.getNumericValue(digitChar);
System.out.println(digitValue); // 7
}
}
包装类小节总结
- 核心作用:让基本类型能以对象形式参与操作(如存入集合)。
- 装箱拆箱:自动转换是语法糖,本质调用valueOf()和xxxValue(),注意Integer等类的缓存池(-128~127)。
- 类型转换:掌握与String互转的parseXxx()和valueOf()方法。
- 值比较:务必使用equals()方法,而非==。
- Character工具类:提供了丰富的静态方法用于字符判断和转换。
总结与常见面试题
全文总结
高频面试题
内部类有哪几种?它们之间的主要区别是什么?
答:四种。主要区别在于:1) 定义位置(类内、方法内);2) 是否持有外部类引用(实例内部类持有,静态内部类不持有);3) 能否有静态成员;4) 访问权限(对局部变量的访问限制)。
为什么局部内部类和匿名内部类访问的局部变量必须是final或等效final的?
答:因为局部变量的生命周期与方法调用一致,而内部类对象的生命周期可能更长(如被其他线程引用)。Java通过复制该变量的值到内部类中来实现访问,为保证数据一致性,要求变量值不可变。
Integer a = 100; Integer b = 100; 与 Integer c = 200; Integer d = 200; 分别比较 ab 和 cd,结果是什么?为什么?
答:a==b为true,c==d为false。因为Integer在自动装箱时,对于**-128到127**之间的值,会使用缓存池中的对象(IntegerCache)。100在缓存范围内,返回的是同一个对象引用;200超出了范围,每次装箱都创建新的Integer对象。
比较两个包装类对象,为什么推荐使用equals()而不是==?
答:==比较的是对象的内存地址。由于自动装箱的缓存机制以及可能的新对象创建,即使封装的值相同,两个包装对象也可能是不同的对象。equals()方法被重写用于比较封装的基本值,因此能得出正确的逻辑结果。
Character类有哪些常用的静态方法?
答:isDigit(), isLetter(), isUpperCase(), isLowerCase(), isWhitespace(), toUpperCase(), toLowerCase(), getNumericValue()等,用于字符类别判断和转换。




