本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。
点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!
本文篇幅较长,建议先收藏再食用!
系列文章目录
JAVA学习 DAY2 java程序运行、注意事项、转义字符
JAVA学习 DAY5 变量&数据类型 [万字长文!一篇搞定!]
JAVA学习 DAY7 程序逻辑控制【万字长文!一篇搞定!】
JAVA学习 DAY11 类和对象_续1【万字长文!一篇搞定!】
JAVA学习 DAY12 继承和多态【万字长文!一篇搞定!】
JAVA学习 DAY13 抽象类和接口【万字长文!一篇搞定!】
深度剖析 Java 图书管理系统设计与实现:类、接口与对象的实战应用
拓展文章
Java避坑指南:千万别在构造方法中调用重写的方法!(附代码案例+执行流程全解析)
深入剖析 Java 中的深拷贝与浅拷贝:原理、实现与最佳实践
目录
前言
小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!
在 Java 面向对象编程中,类和对象是构建程序的核心基石。前文我们已经掌握了类的定义、对象的实例化、成员变量与成员方法等基础内容,接下来将从 this 引用切入,逐步深入对象的构造与初始化、封装特性、static 成员、代码块、内部类等关键知识点,结合大量实例与底层原理,带你全面攻克 Java 类和对象的核心难点,为后续继承、多态等高级特性打下坚实基础。
一、this 引用:揭开对象方法调用的神秘面纱

1.1 为什么需要 this 引用
在学习 this 引用之前,我们先看一个看似正常却隐藏隐患的日期类案例:
public class Date {
public int year;
public int month;
public int day;
// 设置日期的成员方法
public void setDay(int y, int m, int d) {
year = y;
month = m;
day = d;
}
// 打印日期的成员方法
public void printDate() {
System.out.println(year + "/" + month + "/" + day);
}
public static void main(String[] args) {
Date d1 = new Date();
Date d2 = new Date();
Date d3 = new Date();
d1.setDay(2020, 9, 15);
d2.setDay(2020, 9, 16);
d3.setDay(2020, 9, 17);
d1.printDate(); // 输出2020/9/15
d2.printDate(); // 输出2020/9/16
d3.printDate(); // 输出2020/9/17
}
}
这段代码运行结果完全符合预期,但仔细思考会发现两个关键问题:
问题 1:形参名与成员变量名冲突如果我们不小心将 setDay 方法的形参名改为与成员变量名相同:
public void setDay(int year, int month, int day) {
year = year;
month = month;
day = day;
}
此时函数体中的赋值操作就会变得模糊不清 —— 是成员变量给成员变量赋值?参数给参数赋值?还是参数给成员变量赋值?编译器无法区分,最终会导致成员变量无法被正确赋值(实际是参数给自己赋值,成员变量保持默认值)。
问题 2:方法如何识别当前操作的对象main 方法中创建了三个 Date 对象 d1、d2、d3,它们都调用了 setDay 和 printDate 方法。但这两个方法中并没有任何关于 “当前操作哪个对象” 的说明,setDay 方法是如何知道要给 d1、d2 还是 d3 设置日期?printDate 方法又是如何知道要打印哪个对象的日期数据?
这两个问题的核心答案,就是 Java 中的 this 引用。
1.2 什么是 this 引用
this 引用是 Java 编译器自动为成员方法添加的一个隐藏参数,它指向当前调用该成员方法的对象。在成员方法中所有对成员变量和成员方法的访问,本质上都是通过 this 引用来完成的,只不过这个过程对开发者是透明的。
我们可以将前面的日期类代码用 this 引用显式改写,就能清晰看到其作用:
public class Date {
public int year;
public int month;
public int day;
// 显式使用this引用
public void setDay(int year, int month, int day) {
this.year = year; // this.year表示当前对象的year成员变量
this.month = month; // this.month表示当前对象的month成员变量
this.day = day; // this.day表示当前对象的day成员变量
}
public void printDate() {
// 显式使用this引用访问成员变量
System.out.println(this.year + "/" + this.month + "/" + this.day);
}
public static void main(String[] args) {
Date d = new Date();
d.setDay(2020, 9, 15); // 调用时,编译器自动将d对象的引用作为this参数传递
d.printDate(); // 输出2020/9/15
}
}
在上述代码中,当执行d.setDay(2020, 9, 15)时,编译器会自动将 d 对象的引用传递给 setDay 方法的 this 参数,此时 this 就指向 d 对象,this.year = year本质上就是d.year = 2020。
同样,当多个对象调用同一个成员方法时,this 引用会动态指向当前调用的对象:
- d1 调用 setDay 时,this 指向 d1
- d2 调用 setDay 时,this 指向 d2
- d3 调用 setDay 时,this 指向 d3
这就完美解决了 “方法如何识别当前操作对象” 的问题;而通过this.成员变量名的方式,也明确区分了成员变量和局部变量(形参),解决了命名冲突问题。
1.3 this 引用的核心特性
this 引用的特性是理解其使用场景的关键,需要牢牢掌握:
-
this 的类型:this 的类型与当前类的类型一致,即哪个对象调用成员方法,this 就是哪个对象的引用类型。例如 Date 类的 this 引用类型就是 Date。
-
this 的使用范围:this 只能在非静态成员方法中使用,不能在静态方法(static 修饰的方法)、代码块或外部类的 main 方法中使用。因为静态方法属于类,不属于某个具体对象,调用时不会创建对象,自然也就没有 this 引用。
错误示例:
public class Date { public int year; public static void test() { System.out.println(this.year); // 编译报错:无法从静态上下文中引用非静态变量this } } -
this 的唯一性:在成员方法中,this 只能引用当前调用该方法的对象,不能引用其他对象。也就是说,一个成员方法在一次调用中,this 指向是唯一且确定的。
-
this 是隐藏参数:this 是成员方法的第一个隐藏参数,开发者无需显式声明,编译器会自动添加。在编译阶段,编译器会将成员方法的签名改写,例如 Date 类的 setDay 方法会被编译器改写为:
public void setDay(Date this, int year, int month, int day) { this.year = year; this.month = month; this.day = day; }当对象调用该方法时,编译器会自动将对象的引用作为 this 参数传递给方法。这一过程对开发者完全透明,但理解这一点能帮助我们更好地掌握 this 的本质。
-
this 的不可修改性:this 引用是一个常量引用,不能被重新赋值。也就是说,在成员方法中不能执行
this = new Date()这样的操作,编译器会直接报错。
1.4 this 引用的常见使用场景
除了前面提到的区分成员变量和局部变量、访问当前对象的成员方法外,this 引用还有两个重要的使用场景:
场景 1:调用当前类的其他构造方法
在构造方法中,可以通过this(参数列表)的方式调用当前类的其他构造方法,从而避免代码重复,简化对象初始化逻辑。
示例:
public class Date {
public int year;
public int month;
public int day;
// 无参构造方法
public Date() {
// 调用带三个参数的构造方法,设置默认日期为1900-01-01
this(1900, 1, 1);
System.out.println("无参构造方法被调用");
}
// 带三个参数的构造方法
public Date(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
System.out.println("带三个参数的构造方法被调用");
}
public static void main(String[] args) {
Date d = new Date(); // 调用无参构造方法
d.printDate(); // 输出1900/1/1
}
public void printDate() {
System.out.println(this.year + "/" + this.month + "/" + this.day);
}
}
运行结果:
带三个参数的构造方法被调用
无参构造方法被调用
1900/1/1
使用this(参数列表)调用其他构造方法时,必须遵守以下规则:
this(参数列表)必须是构造方法中的第一条语句,前面不能有任何其他代码(包括打印语句、变量赋值等)。错误示例:public Date() { System.out.println("无参构造方法"); this(1900, 1, 1); // 编译报错:this语句必须是构造方法中的第一条语句 }- 不能形成构造方法的递归调用(即 A 构造方法调用 B 构造方法,B 构造方法又调用 A 构造方法)。错误示例:
编译报错:public Date() { this(1900, 1, 1); // 调用带参构造方法 } public Date(int year, int month, int day) { this(); // 调用无参构造方法,形成递归 }递归构造器调用。
场景 2:作为方法的返回值返回当前对象
在某些场景下(例如链式调用),可以将 this 作为方法的返回值,返回当前对象的引用,从而实现连续调用多个方法的效果。
示例:
public class Student {
public String name;
public int age;
public double score;
// 设置姓名并返回当前对象
public Student setName(String name) {
this.name = name;
return this; // 返回当前对象的引用
}
// 设置年龄并返回当前对象
public Student setAge(int age) {
this.age = age;
return this; // 返回当前对象的引用
}
// 设置分数并返回当前对象
public Student setScore(double score) {
this.score = score;
return this; // 返回当前对象的引用
}
// 打印学生信息
public void printInfo() {
System.out.println("姓名:" + this.name + ",年龄:" + this.age + ",分数:" + this.score);
}
public static void main(String[] args) {
// 链式调用:连续调用多个方法
Student student = new Student()
.setName("张三")
.setAge(18)
.setScore(95.5);
student.printInfo(); // 输出:姓名:张三,年龄:18,分数:95.5
}
}
这种链式调用的方式让代码更加简洁流畅,在很多框架(如 MyBatis、Spring)的 API 设计中经常被使用。
1.5 this 引用的底层原理(补充)
为了更深入理解 this 引用,我们可以从 JVM 的角度简单分析其底层实现:
-
成员方法的调用过程:当对象调用成员方法时,JVM 会将该对象的引用(即 this)压入虚拟机栈的局部变量表中,作为方法的第一个参数。
-
成员变量的访问:在成员方法中访问成员变量时,JVM 会通过局部变量表中的 this 引用,找到对象在堆内存中的地址,进而访问该对象的成员变量。
-
字节码层面的体现:我们可以通过
javap -c 类名命令查看编译后的字节码,以 Date 类的 setDay 方法为例:
编译后的字节码(关键部分):
public void setDay(int, int, int);
Code:
0: aload_0 // 将this引用压入操作数栈
1: iload_1 // 将第一个参数(year)压入操作数栈
2: putfield #2 // 将操作数栈中的值赋给this的year成员变量(this.year = year)
5: aload_0 // 将this引用压入操作数栈
6: iload_2 // 将第二个参数(month)压入操作数栈
7: putfield #3 // 将操作数栈中的值赋给this的month成员变量(this.month = month)
10: aload_0 // 将this引用压入操作数栈
11: iload_3 // 将第三个参数(day)压入操作数栈
12: putfield #4 // 将操作数栈中的值赋给this的day成员变量(this.day = day)
15: return
从字节码可以清晰看到,aload_0指令每次都会将 this 引用压入操作数栈,然后通过putfield指令将参数值赋给 this 引用指向的成员变量,这与我们前面讲解的 this 引用的作用完全一致。
二、对象的构造及初始化:从默认值到自定义赋值
在 Java 中,对象的创建和初始化是一个严谨的过程。我们已经知道通过new关键字可以创建对象,但对象中的成员变量是如何被初始化的?为什么局部变量必须显式初始化才能使用,而成员变量可以直接使用?构造方法在其中扮演了什么角色?本节将详细解答这些问题。
2.1 对象初始化的必要性
先看一个简单的对比案例:
案例 1:局部变量未初始化
public class Test {
public static void main(String[] args) {
int a;
System.out.println(a); // 编译报错:可能尚未初始化变量a
}
}
案例 2:成员变量未显式初始化
public class Date {
public int year;
public int month;
public int day;
public static void main(String[] args) {
Date d = new Date();
d.printDate(); // 输出:0/0/0
}
public void printDate() {
System.out.println(this.year + "/" + this.month + "/" + this.day);
}
}
为什么成员变量未显式初始化也能正常使用,并且输出了 0?这是因为 Java 为对象的成员变量提供了默认初始化机制,而局部变量没有这种机制。
但默认初始化的值往往不符合我们的需求(比如日期默认是 0/0/0,这显然不是一个合法的日期),因此需要一种方式来为对象的成员变量赋予自定义的初始值 —— 这就是构造方法的核心作用。
2.2 构造方法:对象初始化的核心
2.2.1 构造方法的概念
构造方法(也称为构造器)是一种特殊的成员方法,它的名字必须与类名完全相同,没有返回值类型(甚至不能写 void),在创建对象时由编译器自动调用,并且在对象的整个生命周期内只调用一次。
构造方法的主要作用是对对象的成员变量进行初始化(注意:构造方法不负责为对象开辟内存空间,内存空间的分配是由new关键字完成的)。
示例:
public class Date {
public int year;
public int month;
public int day;
// 构造方法:名字与类名相同,无返回值类型
public Date(int year, int month, int day) {
this.year = year; // 初始化year成员变量
this.month = month; // 初始化month成员变量
this.day = day; // 初始化day成员变量
System.out.println("Date(int, int, int)构造方法被调用");
}
public static void main(String[] args) {
// 创建对象时,编译器自动调用构造方法
Date d = new Date(2023, 10, 1);
d.printDate(); // 输出:2023/10/1
}
public void printDate() {
System.out.println(this.year + "/" + this.month + "/" + this.day);
}
}
运行结果:
Date(int, int, int)构造方法被调用
2023/10/1
从运行结果可以看到,当使用new Date(2023, 10, 1)创建对象时,构造方法被自动调用,成员变量 year、month、day 被初始化为指定的值。
2.2.2 构造方法的核心特性
构造方法的特性是掌握其使用的关键,需要重点记忆:
-
名称必须与类名完全相同:这是构造方法最显著的标识,若方法名与类名不同,则该方法只是一个普通的成员方法,不会被编译器当作构造方法。
错误示例:
public class Date { public int year; // 方法名与类名不同,不是构造方法 public void date(int year) { this.year = year; } public static void main(String[] args) { // 编译报错:找不到符号(没有对应的构造方法) Date d = new Date(2023); } } -
没有返回值类型:构造方法不需要声明返回值类型,甚至不能写 void。如果写了 void,就变成了普通方法,不再是构造方法。
错误示例:
public class Date { // 写了void,不是构造方法 public void Date(int year) { this.year = year; } public static void main(String[] args) { // 编译报错:找不到符号 Date d = new Date(2023); } } -
创建对象时自动调用:构造方法不能被开发者显式调用,只能在使用
new关键字创建对象时由编译器自动调用。错误示例:
public class Date { public Date() { System.out.println("构造方法被调用"); } public static void main(String[] args) { Date d = new Date(); d.Date(); // 编译报错:找不到符号(Date()是构造方法,不能显式调用) } } -
生命周期内只调用一次:一个对象在其整个生命周期中,构造方法只会被调用一次(相当于人的出生,只能出生一次)。无论后续如何操作该对象,都不会再次调用构造方法。
示例:
public class Date { public Date() { System.out.println("构造方法被调用"); } public static void main(String[] args) { Date d = new Date(); // 第一次创建对象,调用构造方法 Date d2 = d; // 只是引用赋值,没有创建新对象,不调用构造方法 Date d3 = new Date(); // 第二次创建对象,再次调用构造方法 } }运行结果:
构造方法被调用 构造方法被调用 -
支持方法重载:构造方法可以像普通方法一样进行重载(即一个类中可以有多个构造方法,它们的参数列表不同,但方法名相同)。通过构造方法重载,我们可以为对象提供多种初始化方式。
示例:
public class Date { public int year; public int month; public int day; // 无参构造方法 public Date() { this.year = 1900; this.month = 1; this.day = 1; } // 带一个参数的构造方法(只初始化年份) public Date(int year) { this.year = year; this.month = 1; this.day = 1; } // 带两个参数的构造方法(初始化年份和月份) public Date(int year, int month) { this.year = year; this.month = month; this.day = 1; } // 带三个参数的构造方法(初始化年、月、日) public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public void printDate() { System.out.println(this.year + "/" + this.month + "/" + this.day); } public static void main(String[] args) { Date d1 = new Date(); // 调用无参构造方法 Date d2 = new Date(2023); // 调用带一个参数的构造方法 Date d3 = new Date(2023, 10); // 调用带两个参数的构造方法 Date d4 = new Date(2023, 10, 1); // 调用带三个参数的构造方法 d1.printDate(); // 输出1900/1/1 d2.printDate(); // 输出2023/1/1 d3.printDate(); // 输出2023/10/1 d4.printDate(); // 输出2023/10/1 } }构造方法重载的规则与普通方法重载一致:参数列表的参数个数、参数类型或参数顺序不同,与返回值类型和访问修饰符无关。
-
默认构造方法:如果一个类中没有显式定义任何构造方法,编译器会自动为该类生成一个默认的无参构造方法。默认构造方法的访问修饰符与类的访问修饰符一致(如果类是 public 的,默认构造方法也是 public 的;如果类没有访问修饰符,默认构造方法也没有)。
示例:
public class Date { public int year; public int month; public int day; // 没有显式定义构造方法,编译器会生成默认无参构造方法 public static void main(String[] args) { Date d = new Date(); // 调用编译器生成的默认无参构造方法 d.printDate(); // 输出0/0/0(成员变量默认初始化值) } public void printDate() { System.out.println(this.year + "/" + this.month + "/" + this.day); } }注意:一旦开发者显式定义了任何构造方法(无论是否有参数),编译器都不会再生成默认的无参构造方法。
错误示例:
public class Date { public int year; // 显式定义了带参构造方法 public Date(int year) { this.year = year; } public static void main(String[] args) { // 编译报错:无法将类Date中的构造器Date应用到给定类型;需要:int,找到:没有参数 Date d = new Date(); } }这是一个非常常见的错误,尤其是在使用框架(如 Spring)时,很多框架需要通过无参构造方法创建对象,如果显式定义了带参构造方法却忘记定义无参构造方法,会导致框架运行报错。因此,建议在定义类时,无论是否需要,都显式定义一个无参构造方法。
-
访问修饰符:构造方法可以使用 public、private、protected 或默认访问修饰符。
- public:最常用的修饰符,表示该构造方法可以被任何类访问(即可以在任何地方创建该类的对象)。
- private:表示该构造方法只能在当前类内部访问,外部类无法通过
new关键字创建该类的对象。这种用法通常用于单例模式(确保一个类只能创建一个对象)。 - protected:主要用于继承场景,子类可以访问父类的 protected 构造方法。
- 默认访问修饰符:只能在同一个包中的类访问。
示例(private 构造方法):
public class Singleton { // 私有构造方法,外部类无法访问 private Singleton() { System.out.println("单例对象被创建"); } // 提供一个公共静态方法,返回该类的唯一实例 public static Singleton getInstance() { return new Singleton(); } public static void main(String[] args) { // 编译报错:Singleton()在Singleton中是private访问控制 // Singleton s = new Singleton(); // 通过公共静态方法获取实例 Singleton s = Singleton.getInstance(); } }
2.3 对象初始化的完整过程
当我们使用new关键字创建一个对象时,JVM 会执行一系列操作来完成对象的初始化,整个过程可以分为以下 6 个步骤:
-
检测类是否已加载:JVM 首先会检查该对象对应的类是否已经被加载到方法区中。如果没有加载,则会执行类的加载过程(加载、验证、准备、解析、初始化);如果已经加载,则直接进入下一步。
-
为对象分配内存空间:类加载完成后,JVM 会在堆内存中为对象分配一块连续的内存空间,用于存储对象的成员变量(包括实例变量和继承自父类的成员变量)。
-
处理并发安全问题:在多线程环境下,可能会有多个线程同时创建同一个类的对象,导致内存分配冲突。JVM 会通过两种方式解决这个问题:
- 对分配内存空间的操作进行同步处理(使用 CAS 锁保证原子性)。
- 预先为每个线程分配一块独立的内存空间(TLAB,Thread Local Allocation Buffer),线程创建对象时先在自己的 TLAB 中分配内存,避免冲突。
-
默认初始化:JVM 会将分配的内存空间中的成员变量设置为默认值(即零值),这就是为什么成员变量未显式初始化也能正常使用的原因。不同数据类型的默认值如下表所示:
| 数据类型 | 默认值 |
|---|---|
| byte | 0 |
| short | 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0 |
| char | '\u0000'(空字符) |
| boolean | false |
| 引用类型(String、数组等) | null |
-
设置对象头信息:JVM 会为对象设置对象头(Object Header),对象头中包含以下信息:
- 类的元数据指针(指向该对象对应的类在方法区中的元数据)。
- 对象的哈希码。
- 对象的锁状态信息。
- 数组的长度(如果是数组对象)。
-
调用构造方法进行初始化:最后,JVM 会调用该类的构造方法,对对象的成员变量进行自定义初始化(将默认值覆盖为开发者指定的值)。
为了更直观地理解这个过程,我们结合一个具体的示例来分析:
public class Date {
public int year;
public int month;
public int day;
public Date(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public static void main(String[] args) {
Date d = new Date(2023, 10, 1);
}
}
对象 d 的初始化过程:
- JVM 检测到 Date 类未加载,执行 Date 类的加载过程。
- 在堆内存中为 d 对象分配内存空间,用于存储 year、month、day 三个成员变量。
- 处理并发安全问题(假设当前是单线程环境,直接跳过)。
- 对成员变量进行默认初始化:year=0,month=0,day=0。
- 设置 d 对象的头信息,包括指向 Date 类元数据的指针、哈希码等。
- 调用 Date 类的构造方法
Date(2023, 10, 1),将 year 改为 2023,month 改为 10,day 改为 1,对象初始化完成。
2.4 初始化的三种方式
除了通过构造方法进行初始化外,Java 还提供了两种其他的初始化方式:默认初始化和就地初始化。这三种方式共同构成了对象成员变量的完整初始化机制。
2.4.1 默认初始化
默认初始化是 JVM 自动完成的,无需开发者干预,前面已经详细介绍过。默认初始化的目的是保证成员变量在被显式初始化之前,已经有一个合法的初始值,避免出现未定义行为。
2.4.2 就地初始化
就地初始化是指在声明成员变量时,直接为其赋值。这种方式可以让成员变量的初始值更加直观,代码可读性更高。
示例:
public class Date {
// 就地初始化:声明时直接赋值
public int year = 1900;
public int month = 1;
public int day = 1;
// 无参构造方法
public Date() {
}
// 带三个参数的构造方法
public Date(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public static void main(String[] args) {
Date d1 = new Date(); // 调用无参构造方法
Date d2 = new Date(2023, 10, 1); // 调用带参构造方法
System.out.println(d1.year); // 输出1900(就地初始化的值)
System.out.println(d2.year); // 输出2023(构造方法赋值的值)
}
}
需要注意的是,就地初始化的本质是编译器将赋值语句自动添加到每个构造方法的开头(在this(参数列表)之后,如果有的话)。例如,上面的 Date 类编译后,无参构造方法会被改写为:
public Date() {
super(); // 调用父类的无参构造方法(默认添加)
this.year = 1900; // 就地初始化的赋值语句
this.month = 1; // 就地初始化的赋值语句
this.day = 1; // 就地初始化的赋值语句
}
带参构造方法会被改写为:
public Date(int year, int month, int day) {
super(); // 调用父类的无参构造方法(默认添加)
this.year = 1900; // 就地初始化的赋值语句
this.month = 1; // 就地初始化的赋值语句
this.day = 1; // 就地初始化的赋值语句
this.year = year; // 开发者添加的赋值语句
this.month = month; // 开发者添加的赋值语句
this.day = day; // 开发者添加的赋值语句
}
因此,构造方法中的赋值语句会覆盖就地初始化的值。
2.4.3 构造方法初始化
构造方法初始化是最灵活的一种初始化方式,可以根据不同的参数为成员变量赋予不同的值,前面已经详细介绍,这里不再赘述。
2.5 初始化顺序总结
当一个类中同时存在就地初始化、构造方法初始化时,成员变量的初始化顺序如下:
- 执行默认初始化(JVM 自动完成)。
- 执行就地初始化(编译器将赋值语句添加到构造方法开头)。
- 执行构造方法中的赋值语句(开发者自定义的初始化逻辑)。
示例:
public class Test {
public int a = 10; // 就地初始化
public Test() {
a = 20; // 构造方法初始化
}
public static void main(String[] args) {
Test t = new Test();
System.out.println(t.a); // 输出20
}
}
初始化过程分析:
- 默认初始化:a=0。
- 就地初始化:a=10。
- 构造方法初始化:a=20。
- 最终 a 的值为 20。
如果类中还有静态成员变量和静态代码块(后面会介绍),初始化顺序会更复杂,我们将在 static 成员和代码块部分详细总结。
三、封装:面向对象的核心特性之一
封装是面向对象编程的三大特性(封装、继承、多态)之一,其核心思想是 “隐藏对象的属性和实现细节,仅对外公开接口来与对象进行交互”。封装可以提高代码的安全性、可维护性和复用性,是 Java 类设计的重要原则。
3.1 封装的概念与意义
3.1.1 什么是封装
封装就像一个 “黑盒子”,它将对象的数据(成员变量)和操作数据的方法(成员方法)有机结合在一起,隐藏对象内部的实现细节,只对外提供有限的、安全的接口供其他对象访问。
举个生活中的例子:我们日常使用的手机,其内部包含了 CPU、内存、电池、主板等复杂的硬件元件,以及操作系统、应用软件等软件逻辑。但手机厂商并没有将这些内部细节暴露给用户,而是提供了屏幕、按键、摄像头、充电口等简单易用的接口。用户不需要知道手机内部的硬件如何工作、软件如何运行,只需要通过这些接口就能完成打电话、发短信、拍照等操作 —— 这就是封装的思想。
在 Java 中,封装主要通过以下两种方式实现:
- 将成员变量设置为私有(private),禁止外部类直接访问。
- 提供公共(public)的成员方法(getter 和 setter 方法),用于对私有成员变量进行访问和修改,并在方法中添加必要的逻辑控制(如数据验证)。
3.1.2 封装的意义
封装带来了以下几个重要的好处:
-
提高代码的安全性:通过将成员变量私有化,避免外部类直接修改对象的内部数据,防止数据被非法篡改。例如,一个学生的年龄不能是负数,通过 setter 方法可以添加年龄的合法性校验,确保数据的正确性。
-
提高代码的可维护性:对象的内部实现细节被隐藏,当需要修改内部逻辑时,只需要修改类内部的代码,而不需要修改外部类的调用代码,降低了代码的耦合度。例如,修改学生年龄的验证规则时,只需要修改 setAge 方法,而不需要修改所有使用该方法的外部类。
-
简化代码的使用:封装后,外部类只需要通过公共接口与对象交互,不需要关心对象内部的复杂实现,降低了使用成本。例如,使用手机时,只需要按按键即可,不需要知道按键背后的电路原理。
-
实现代码复用:将对象的属性和方法封装在类中,该类可以被多个外部类复用,提高了代码的复用性。
3.2 访问限定符:封装的实现基础
Java 提供了四种访问限定符,用于控制类、成员变量和成员方法的访问权限,它们是实现封装的基础。四种访问限定符分别是:private、default(默认,无关键字)、protected、public。
3.2.1 访问限定符的作用范围
四种访问限定符的作用范围如下表所示(“√” 表示可以访问,“×” 表示不能访问):
| 访问限定符 | 同一类中 | 同一包中的其他类 | 不同包中的子类 | 不同包中的非子类 |
|---|---|---|---|---|
| private | √ | × | × | × |
| default(无关键字) | √ | √ | × | × |
| protected | √ | √ | √ | × |
| public | √ | √ | √ | √ |
下面我们通过具体的示例来详细说明每种访问限定符的使用场景:
3.2.2 private:私有访问权限
private 修饰的成员(成员变量或成员方法)只能在当前类内部访问,其他任何类(包括同一包中的其他类、不同包中的子类)都不能访问。
示例:
// 包com.example.demo1
package com.example.demo1;
public class Student {
// 私有成员变量:只能在Student类内部访问
private String name;
private int age;
// 公共的setter方法:用于修改私有成员变量
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
// 添加数据验证逻辑:年龄不能小于0或大于150
if (age >= 0 && age <= 150) {
this.age = age;
} else {
System.out.println("年龄不合法!");
}
}
// 公共的getter方法:用于获取私有成员变量的值
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
// 私有成员方法:只能在Student类内部访问
private void study() {
System.out.println(name + "正在学习");
}
// 公共方法:可以调用私有方法
public void doStudy() {
study(); // 同一类中可以访问私有方法
}
}
// 包com.example.demo1(同一包中的其他类)
package com.example.demo1;
public class TestStudent {
public static void main(String[] args) {
Student student = new Student();
// 编译报错:name是private的,不能直接访问
// student.name = "张三";
// 编译报错:age是private的,不能直接访问
// student.age = 20;
// 编译报错:study()是private的,不能直接调用
// student.study();
// 通过公共的setter方法修改私有成员变量
student.setName("张三");
student.setAge(20); // 年龄合法,赋值成功
student.setAge(200); // 年龄不合法,输出"年龄不合法!"
// 通过公共的getter方法获取私有成员变量的值
System.out.println(student.getName()); // 输出"张三"
System.out.println(student.getAge()); // 输出20
// 通过公共方法调用私有方法
student.doStudy(); // 输出"张三正在学习"
}
}
从示例可以看到,private 修饰的成员变量 name、age 和成员方法 study () 不能被同一包中的 TestStudent 类直接访问,但可以通过公共的 setter、getter 方法和 doStudy () 方法间接访问,并且在 setter 方法中可以添加数据验证逻辑,确保数据的合法性。
3.2.3 default:默认访问权限
default 访问权限是指不使用任何访问限定符修饰的成员,其作用范围是同一包中的所有类(包括同一类和同一包中的其他类),不同包中的类(无论是否是子类)都不能访问。
示例:
// 包com.example.demo1
package com.example.demo1;
public class Student {
// default成员变量:同一包中的类可以访问,不同包中的类不能访问
String gender;
// default成员方法:同一包中的类可以访问,不同包中的类不能访问
void eat() {
System.out.println("吃饭");
}
}
// 包com.example.demo1(同一包中的其他类)
package com.example.demo1;
public class TestStudent {
public static void main(String[] args) {
Student student = new Student();
// 同一包中的类可以直接访问default成员变量
student.gender = "男";
System.out.println(student.gender); // 输出"男"
// 同一包中的类可以直接调用default成员方法
student.eat(); // 输出"吃饭"
}
}
// 包com.example.demo2(不同包中的类)
package com.example.demo2;
import com.example.demo1.Student;
public class TestStudent2 {
public static void main(String[] args) {
Student student = new Student();
// 编译报错:gender是default的,不同包中的类不能访问
// student.gender = "女";
// 编译报错:eat()是default的,不同包中的类不能调用
// student.eat();
}
}
3.2.4 protected:受保护的访问权限
protected 修饰的成员的作用范围是:同一类中、同一包中的其他类、不同包中的子类,不同包中的非子类不能访问。protected 主要用于继承场景,允许子类访问父类的成员。
示例:
// 包com.example.demo1(父类)
package com.example.demo1;
public class Student {
// protected成员变量
protected String school;
// protected成员方法
protected void goToSchool() {
System.out.println("去上学");
}
}
// 包com.example.demo1(同一包中的其他类)
package com.example.demo1;
public class TestStudent {
public static void main(String[] args) {
Student student = new Student();
// 同一包中的类可以直接访问protected成员变量
student.school = "北京大学";
System.out.println(student.school); // 输出"北京大学"
// 同一包中的类可以直接调用protected成员方法
student.goToSchool(); // 输出"去上学"
}
}
// 包com.example.demo2(不同包中的子类)
package com.example.demo2;
import com.example.demo1.Student;
// 子类继承自Student类
public class GraduateStudent extends Student {
public void test() {
// 不同包中的子类可以访问父类的protected成员变量
this.school = "清华大学";
System.out.println(this.school); // 输出"清华大学"
// 不同包中的子类可以调用父类的protected成员方法
this.goToSchool(); // 输出"去上学"
}
public static void main(String[] args) {
GraduateStudent gs = new GraduateStudent();
gs.test();
}
}
// 包com.example.demo2(不同包中的非子类)
package com.example.demo2;
import com.example.demo1.Student;
public class TestStudent2 {
public static void main(String[] args) {
Student student = new Student();
// 编译报错:school是protected的,不同包中的非子类不能访问
// student.school = "复旦大学";
// 编译报错:goToSchool()是protected的,不同包中的非子类不能调用
// student.goToSchool();
}
}
3.2.5 public:公共访问权限
public 修饰的成员可以被所有类访问,无论是否在同一个包中,是否是子类。public 是访问权限最高的限定符,通常用于修饰需要对外公开的接口(如 getter、setter 方法)。
示例:
// 包com.example.demo1
package com.example.demo1;
public class Student {
// public成员变量(不推荐直接用public修饰成员变量,这里仅作示例)
public String name;
// public成员方法
public void introduce() {
System.out.println("我是" + name);
}
}
// 包com.example.demo2(不同包中的非子类)
package com.example.demo2;
import com.example.demo1.Student;
public class TestStudent {
public static void main(String[] args) {
Student student = new Student();
// 不同包中的非子类可以直接访问public成员变量
student.name = "李四";
System.out.println(student.name); // 输出"李四"
// 不同包中的非子类可以直接调用public成员方法
student.introduce(); // 输出"我是李四"
}
}
3.2.6 访问限定符的使用建议
在实际开发中,访问限定符的使用应遵循 “最小权限原则”(即尽可能限制成员的访问权限,只开放必要的接口),具体建议如下:
-
成员变量:优先使用 private 修饰,通过 public 的 getter 和 setter 方法对外提供访问和修改接口,并在 setter 方法中添加数据验证逻辑。这样可以保证数据的安全性和一致性。
-
成员方法:
- 仅在类内部使用的方法:使用 private 修饰。
- 同一包中的多个类需要使用的方法:使用 default 修饰。
- 子类需要重写或访问的方法:使用 protected 修饰。
- 对外提供的公共接口:使用 public 修饰。
-
类:类只能使用 public 或 default 修饰(不能使用 private 或 protected)。
- 需要被其他包中的类访问的类:使用 public 修饰,且类名必须与文件名相同。
- 仅在当前包中使用的类:使用 default 修饰。
3.3 封装的实践:JavaBean
JavaBean 是一种遵循特定规范的 Java 类,它是封装思想的典型应用。JavaBean 的规范如下:
- 类必须是公共的(public 修饰)。
- 提供无参的公共构造方法。
- 成员变量必须是私有(private 修饰)。
- 为每个私有成员变量提供公共的 getter 和 setter 方法:
- getter 方法:用于获取成员变量的值,方法名以
get开头,后面跟成员变量名(首字母大写),没有参数,返回值类型与成员变量类型一致。 - setter 方法:用于修改成员变量的值,方法名以
set开头,后面跟成员变量名(首字母大写),有一个参数(参数类型与成员变量类型一致),返回值类型通常为 void(也可以返回当前对象,支持链式调用)。
- getter 方法:用于获取成员变量的值,方法名以
- 可选:实现 Serializable 接口(用于对象序列化)。
示例:一个标准的 JavaBean 类
public class User implements java.io.Serializable {
// 私有成员变量
private String username;
private String password;
private int age;
private String email;
// 无参公共构造方法
public User() {
}
// 带参公共构造方法(可选,方便初始化)
public User(String username, String password, int age, String email) {
this.username = username;
this.password = password;
this.age = age;
this.email = email;
}
// username的getter和setter方法
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
// password的getter和setter方法
public String getPassword() {
return password;
}
public void setPassword(String password) {
// 数据验证:密码长度不能小于6位
if (password != null && password.length() >= 6) {
this.password = password;
} else {
throw new IllegalArgumentException("密码长度不能小于6位");
}
}
// age的getter和setter方法
public int getAge() {
return age;
}
public void setAge(int age) {
// 数据验证:年龄必须在0-150之间
if (age >= 0 && age <= 150) {
this.age = age;
} else {
throw new IllegalArgumentException("年龄不合法");
}
}
// email的getter和setter方法
public String getEmail() {
return email;
}
public void setEmail(String email) {
// 数据验证:简单的邮箱格式校验
if (email != null && email.contains("@")) {
this.email = email;
} else {
throw new IllegalArgumentException("邮箱格式不合法");
}
}
// 重写toString方法(可选,方便打印对象信息)
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
", email='" + email + '\'' +
'}';
}
public static void main(String[] args) {
// 创建User对象
User user = new User();
// 通过setter方法设置属性(会进行数据验证)
user.setUsername("zhangsan");
user.setPassword("123456"); // 密码合法
user.setAge(25); // 年龄合法
user.setEmail("zhangsan@example.com"); // 邮箱合法
// 通过getter方法获取属性
System.out.println(user.getUsername()); // 输出"zhangsan"
System.out.println(user.getAge()); // 输出25
System.out.println(user); // 输出"User{username='zhangsan', age=25, email='zhangsan@example.com'}"
// 测试不合法的数据
try {
user.setPassword("12345"); // 密码长度小于6位,抛出异常
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出"密码长度不能小于6位"
}
try {
user.setAge(200); // 年龄不合法,抛出异常
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出"年龄不合法"
}
try {
user.setEmail("zhangsan.example.com"); // 邮箱格式不合法,抛出异常
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出"邮箱格式不合法"
}
}
}
JavaBean 的优势:
- 数据安全性:通过 private 修饰成员变量,setter 方法添加数据验证,确保数据合法。
- 代码规范性:遵循统一的规范,便于其他开发者理解和使用。
- 可复用性:JavaBean 可以被多个模块复用,提高开发效率。
- 兼容性:支持序列化,便于在网络传输或文件存储中使用。
在实际开发中,JavaBean 被广泛应用于数据封装(如实体类、DTO、VO 等),是 Java 开发中不可或缺的一部分。
3.4 封装扩展:包(Package)
包(Package)是 Java 中用于组织类和接口的一种机制,它不仅是对类的分类管理,也是封装的一种延伸。通过包,可以将不同功能的类划分到不同的命名空间中,避免类名冲突,同时控制类的访问权限。
3.4.1 包的概念与作用
包就像电脑中的文件夹,用于分类存放文件(类)。在 Java 中,包的主要作用如下:
-
避免类名冲突:在同一个包中,类名必须唯一;但在不同的包中,可以存在同名的类。例如,
com.example.demo1.Date和com.example.demo2.Date是两个不同的类,不会发生冲突。 -
组织类结构:将功能相关的类放在同一个包中,使代码结构更加清晰。例如,Java 的标准库中,
java.util包存放工具类,java.sql包存放数据库相关的类,java.net包存放网络编程相关的类。 -
控制类的访问权限:结合访问限定符,包可以控制类的访问范围。例如,默认访问权限的类只能在同一个包中访问,public 类可以在任何包中访问。
-
保护代码:将类放在包中,可以避免代码被误修改,提高代码的安全性。
3.4.2 包的命名规范
Java 包的命名遵循以下规范:
- 包名通常使用小写字母,避免使用大写字母(类名使用大驼峰,包名使用小写,便于区分)。
- 包名采用 “反向域名” 的方式命名,确保包名的唯一性。例如,公司的域名为
example.com,则对应的包名可以是com.example.demo1、com.example.demo2等。 - 包名的层次结构与文件系统的目录结构一致。例如,包名
com.example.demo1对应的目录结构是com/example/demo1。
3.4.3 导入包中的类
当需要使用其他包中的类时,有三种方式:
-
使用全类名:直接在代码中使用类的全路径名(包名 + 类名)。这种方式比较繁琐,适用于只使用一次的类。
示例:
public class Test { public static void main(String[] args) { // 使用全类名创建java.util.Date类的对象 java.util.Date date = new java.util.Date(); System.out.println(date.getTime()); // 输出当前时间戳 } } -
使用 import 语句:在代码的开头使用
import语句导入需要的类,之后就可以直接使用类名。这种方式适用于多次使用的类。示例:
// 导入java.util包中的Date类 import java.util.Date; public class Test { public static void main(String[] args) { Date date = new Date(); // 直接使用类名 System.out.println(date.getTime()); } } -
导入包中的所有类:使用
import 包名.*导入包中的所有类。这种方式适用于需要使用包中多个类的场景,但不推荐过度使用,因为可能会导致类名冲突。示例:
// 导入java.util包中的所有类 import java.util.*; public class Test { public static void main(String[] args) { Date date = new Date(); // 使用java.util.Date类 List<String> list = new ArrayList<>(); // 使用java.util.List和java.util.ArrayList类 } }
3.4.4 类名冲突问题
当导入的不同包中存在同名的类时,会发生类名冲突,编译器无法确定使用哪个类,此时需要使用全类名来指定。
示例:
// 导入java.util包中的所有类
import java.util.*;
// 导入java.sql包中的所有类
import java.sql.*;
public class Test {
public static void main(String[] args) {
// 编译报错:对Date的引用不明确,java.util.Date和java.sql.Date都匹配
// Date date = new Date();
// 使用全类名指定使用java.util.Date类
java.util.Date date1 = new java.util.Date();
// 使用全类名指定使用java.sql.Date类
java.sql.Date date2 = new java.sql.Date(System.currentTimeMillis());
}
}
3.4.5 静态导入
静态导入(import static)是 Java 5 引入的特性,用于导入包中静态成员(静态成员变量和静态成员方法),导入后可以直接使用静态成员的名称,而不需要通过类名访问。
示例:
// 静态导入java.lang.Math类中的所有静态成员
import static java.lang.Math.*;
public class Test {
public static void main(String[] args) {
double x = 3;
double y = 4;
// 直接使用Math类的静态方法sqrt和pow,不需要写Math.sqrt和Math.pow
double distance = sqrt(pow(x, 2) + pow(y, 2));
System.out.println(distance); // 输出5.0
// 直接使用Math类的静态成员变量PI
System.out.println(PI); // 输出3.141592653589793
}
}
静态导入的优势是简化代码书写,尤其是对于经常使用的静态成员。但需要注意避免过度使用,否则会降低代码的可读性,让其他开发者难以确定静态成员的来源。
3.4.6 自定义包
在实际开发中,我们通常会根据项目的功能模块自定义包。下面以 IDEA 为例,介绍自定义包的步骤:
- 打开 IDEA,在项目的
src目录上右键,选择 “New”→“Package”。 - 在弹出的对话框中输入包名(例如
com.example.demo),点击 “OK”。 - IDEA 会自动在
src目录下创建对应的目录结构(com/example/demo)。 - 在自定义包上右键,选择 “New”→“Java Class”,输入类名(例如
Student),点击 “OK”。 - 此时创建的 Student 类的开头会自动添加
package com.example.demo;语句,表示该类属于com.example.demo包。
示例:自定义包中的类
// 包声明:该类属于com.example.demo包
package com.example.demo;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void introduce() {
System.out.println("我是" + name + ",今年" + age + "岁");
}
}
// 另一个包中的类,使用com.example.demo包中的Student类
package com.example.test;
import com.example.demo.Student;
public class TestStudent {
public static void main(String[] args) {
Student student = new Student("张三", 20);
student.introduce(); // 输出"我是张三,今年20岁"
}
}
3.4.7 Java 中的常见包
Java 的标准库提供了许多常用的包,以下是一些最常用的包及其功能:
-
java.lang:Java 语言的核心包,包含了 Java 的基础类,如 String、Object、Math、Integer 等。该包从 JDK 1.1 开始自动导入,不需要显式使用 import 语句。
-
java.util:工具类包,包含了大量实用的工具类和集合类,如 ArrayList、HashMap、Date、Scanner 等。
-
java.io:输入输出包,包含了用于文件操作、流操作的类,如 File、InputStream、OutputStream、Reader、Writer 等。
-
java.net:网络编程包,包含了用于网络通信的类,如 Socket、ServerSocket、URL 等。
-
java.sql:数据库编程包,包含了用于访问数据库的类和接口,如 Connection、Statement、ResultSet 等。
-
java.awt:抽象窗口工具包,包含了用于创建图形用户界面(GUI)的类,如 Frame、Button、TextField 等。
-
javax.swing:Swing 组件包,是对 java.awt 的扩展,提供了更多美观的 GUI 组件,如 JFrame、JButton、JTextField 等。
-
java.lang.reflect:反射编程包,包含了用于动态获取类信息、调用类的方法和构造方法的类,如 Class、Method、Constructor 等。
四、static 成员:属于类的成员
在 Java 中,被static关键字修饰的成员(成员变量、成员方法、代码块)称为静态成员,也叫类成员。静态成员不属于某个具体的对象,而是属于整个类,被所有对象共享。
4.1 为什么需要 static 成员
我们先看一个场景:假设我们有一个 Student 类,多个 Student 对象代表不同的学生,这些学生都属于同一个班级(例如 “高三(1)班”)。如果将班级名称作为普通成员变量定义在 Student 类中,那么每个 Student 对象都会存储一份班级名称,这会造成内存浪费(因为所有学生的班级名称都是相同的)。
示例(不使用 static 成员):
public class Student {
public String name;
public int age;
// 班级名称:每个对象都会存储一份,造成内存浪费
public String classRoom = "高三(1)班";
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Student s1 = new Student("张三", 18);
Student s2 = new Student("李四", 19);
Student s3 = new Student("王五", 18);
// 所有对象的classRoom都是相同的
System.out.println(s1.classRoom); // 输出"高三(1)班"
System.out.println(s2.classRoom); // 输出"高三(1)班"
System.out.println(s3.classRoom); // 输出"高三(1)班"
// 修改s1的classRoom,s2和s3的classRoom不会改变(因为每个对象都有独立的classRoom)
s1.classRoom = "高三(2)班";
System.out.println(s1.classRoom); // 输出"高三(2)班"
System.out.println(s2.classRoom); // 输出"高三(1)班"
System.out.println(s3.classRoom); // 输出"高三(1)班"
}
}
从示例可以看到,不使用 static 修饰 classRoom 时,每个 Student 对象都有一份独立的 classRoom 成员变量,不仅浪费内存,而且修改一个对象的 classRoom 不会影响其他对象。这显然不符合 “所有学生共享同一个班级名称” 的需求。
此时,我们可以使用 static 修饰 classRoom,将其变为静态成员变量,让所有 Student 对象共享这份数据:
示例(使用 static 成员):
public class Student {
public String name;
public int age;
// 静态成员变量:属于类,被所有对象共享
public static String classRoom = "高三(1)班";
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Student s1 = new Student("张三", 18);
Student s2 = new Student("李四", 19);
Student s3 = new Student("王五", 18);
// 通过对象访问静态成员变量(不推荐)
System.out.println(s1.classRoom); // 输出"高三(1)班"
System.out.println(s2.classRoom); // 输出"高三(1)班"
System.out.println(s3.classRoom); // 输出"高三(1)班"
// 通过类名访问静态成员变量(推荐)
System.out.println(Student.classRoom); // 输出"高三(1)班"
// 修改静态成员变量(通过类名修改)
Student.classRoom = "高三(2)班";
// 所有对象访问到的静态成员变量都会改变(因为共享同一份数据)
System.out.println(s1.classRoom); // 输出"高三(2)班"
System.out.println(s2.classRoom); // 输出"高三(2)班"
System.out.println(s3.classRoom); // 输出"高三(2)班"
System.out.println(Student.classRoom); // 输出"高三(2)班"
}
}
通过 static 修饰 classRoom 后,classRoom 成为类的成员,被所有 Student 对象共享,只在内存中存储一份(存储在方法区),不仅节省了内存,而且修改后所有对象都能访问到最新的值,完美解决了 “共享数据” 的需求。
4.2 static 成员变量
4.2.1 静态成员变量的概念
静态成员变量(也叫类变量)是被static关键字修饰的成员变量,它属于整个类,而不属于某个具体的对象。静态成员变量在类加载时被初始化,存储在方法区中,整个程序运行期间只存在一份拷贝,被所有对象共享。
4.2.2 静态成员变量的特性
-
不属于具体对象:静态成员变量是类的属性,不是对象的属性,因此不需要创建对象就能访问。
-
访问方式:
- 可以通过类名直接访问(推荐方式):
类名.静态成员变量名。 - 可以通过对象访问(不推荐方式):
对象名.静态成员变量名。这种方式容易让人误以为静态成员变量属于对象,降低代码可读性。
- 可以通过类名直接访问(推荐方式):
-
存储位置:静态成员变量存储在方法区(Method Area)中,而实例成员变量存储在堆内存中。
-
生命周期:静态成员变量的生命周期与类的生命周期一致,即类加载时创建,类卸载时销毁(整个程序运行期间都存在);而实例成员变量的生命周期与对象的生命周期一致,对象被垃圾回收时销毁。
-
初始化时机:静态成员变量在类加载的 “准备阶段” 被初始化为默认值,在 “初始化阶段” 执行显式初始化(就地初始化或静态代码块初始化);而实例成员变量在创建对象时被初始化。
4.2.3 静态成员变量与实例成员变量的区别
静态成员变量与实例成员变量的区别如下表所示:
| 特性 | 静态成员变量(类变量) | 实例成员变量(对象变量) |
|---|---|---|
| 所属对象 | 属于类,被所有对象共享 | 属于具体的对象 |
| 访问方式 | 类名。变量名(推荐)、对象名。变量名 | 对象名。变量名 |
| 存储位置 | 方法区 | 堆内存 |
| 生命周期 | 与类的生命周期一致 | 与对象的生命周期一致 |
| 初始化时机 | 类加载时初始化 | 创建对象时初始化 |
| 依赖对象 | 不依赖对象,无需创建对象即可访问 | 依赖对象,必须创建对象才能访问 |
示例:静态成员变量与实例成员变量的访问对比
public class Test {
// 静态成员变量
public static int staticVar = 10;
// 实例成员变量
public int instanceVar = 20;
public static void main(String[] args) {
// 访问静态成员变量:无需创建对象,直接通过类名访问
System.out.println(Test.staticVar); // 输出10
// 访问实例成员变量:必须创建对象,通过对象访问
Test test = new Test();
System.out.println(test.instanceVar); // 输出20
// 错误:不能通过类名访问实例成员变量
// System.out.println(Test.instanceVar);
// 错误:不能直接访问实例成员变量(未创建对象)
// System.out.println(instanceVar);
}
}
4.3 static 成员方法
4.3.1 静态成员方法的概念
静态成员方法(也叫类方法)是被static关键字修饰的成员方法,它属于整个类,而不属于某个具体的对象。静态成员方法通常用于操作静态成员变量,或提供不依赖于对象的工具方法。
4.3.2 静态成员方法的特性
-
不属于具体对象:静态成员方法是类的方法,不是对象的方法,因此不需要创建对象就能调用。
-
访问方式:
- 可以通过类名直接调用(推荐方式):
类名.静态成员方法名(参数)。 - 可以通过对象调用(不推荐方式):
对象名.静态成员方法名(参数)。
- 可以通过类名直接调用(推荐方式):
-
不能访问实例成员:静态成员方法中不能访问实例成员变量和实例成员方法,因为实例成员属于具体的对象,而静态成员方法调用时可能没有创建对象,无法确定访问哪个对象的实例成员。
错误示例:
public class Test { public static int staticVar = 10; public int instanceVar = 20; // 静态成员方法 public static void staticMethod() { System.out.println(staticVar); // 正确:可以访问静态成员变量 // 编译报错:不能访问实例成员变量 // System.out.println(instanceVar); // 编译报错:不能调用实例成员方法 // instanceMethod(); } // 实例成员方法 public void instanceMethod() { System.out.println(instanceVar); // 正确:可以访问实例成员变量 System.out.println(staticVar); // 正确:可以访问静态成员变量 } } -
不能使用 this 和 super 关键字:this 关键字指向当前调用方法的对象,super 关键字指向父类对象,而静态成员方法属于类,调用时可能没有创建对象,因此不能使用 this 和 super 关键字。
错误示例:
public class Test { public static int staticVar = 10; public static void staticMethod() { // 编译报错:不能使用this关键字 // System.out.println(this.staticVar); } } -
可以访问静态成员:静态成员方法中可以访问静态成员变量和其他静态成员方法,因为静态成员属于类,与对象无关。
示例:
public class Test { public static int staticVar = 10; public static void staticMethod1() { System.out.println("静态方法1被调用"); } public static void staticMethod2() { // 访问静态成员变量 System.out.println(staticVar); // 输出10 // 调用静态成员方法 staticMethod1(); // 输出"静态方法1被调用" } public static void main(String[] args) { Test.staticMethod2(); } } -
不能被重写:静态成员方法属于类,而方法重写是基于对象的多态特性,因此静态成员方法不能被重写(即使子类中定义了同名的静态方法,也只是子类的静态方法,与父类的静态方法无关,不属于重写)。
4.3.3 静态成员方法的使用场景
静态成员方法通常用于以下场景
1.操作静态成员变量
静态成员变量属于类、被所有对象共享,静态成员方法是访问 / 修改它的标准方式(尤其静态变量为 private 时),避免直接暴露变量导致数据混乱。
- 示例:学生类共享教室名称,通过静态方法控制访问
public class Student {
// 私有静态成员变量(类共享)
private static String classRoom = "Bit306";
// 静态getter:获取静态变量(对外提供只读接口)
public static String getClassRoom() {
return classRoom;
}
// 静态setter:修改静态变量(可添加逻辑校验)
public static void setClassRoom(String room) {
if (room != null && !room.isEmpty()) {
classRoom = room;
}
}
public static void main(String[] args) {
// 无需创建对象,直接通过类名调用
System.out.println(Student.getClassRoom()); // 输出 Bit306
Student.setClassRoom("Bit307");
System.out.println(Student.getClassRoom()); // 输出 Bit307
}
}
2.提供工具类方法
当方法逻辑 不依赖对象的实例属性 / 方法,仅完成独立功能(如数据计算、格式转换、字符串处理)时,适合定义为静态方法,方便调用且无需创建对象。
- 常见场景:数学计算、日期格式化、字符串工具等
- 示例:自定义数学工具类
// 工具类(通常无实例属性,所有方法为静态)
public class MathUtil {
// 静态方法:计算两数之和
public static int add(int a, int b) {
return a + b;
}
// 静态方法:计算圆的面积
public static double circleArea(double radius) {
if (radius < 0) {
throw new IllegalArgumentException("半径不能为负");
}
return Math.PI * radius * radius;
}
public static void main(String[] args) {
// 直接通过类名调用,无需new MathUtil()
System.out.println(MathUtil.add(10, 20)); // 输出 30
System.out.println(MathUtil.circleArea(2)); // 输出 12.566...
}
}
- 注意:Java 标准库的
java.lang.Math(Math.sqrt()、Math.random())、java.util.Arrays(Arrays.sort())都是典型的静态工具类。
3.创建对象的静态工厂方法
替代构造方法创建对象,可自定义返回逻辑、简化对象创建流程,还能根据参数返回不同子类对象(比构造方法更灵活)。
- 示例:用户类的静态工厂方法
public class User {
private String name;
private int age;
// 私有构造方法:限制外部直接通过new创建
private User(String name, int age) {
this.name = name;
this.age = age;
}
// 静态工厂方法:对外提供对象创建接口
public static User createAdultUser(String name) {
// 固定成年人年龄为18+,简化创建
return new User(name, 18);
}
// 静态工厂方法:根据年龄动态创建
public static User createUser(String name, int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合法");
}
return new User(name, age);
}
public static void main(String[] args) {
// 通过静态方法创建对象,逻辑清晰
User adult = User.createAdultUser("张三");
User teen = User.createUser("李四", 16);
}
}
4、实现单例模式
单例模式要求一个类 只能创建一个对象,通过私有构造方法 + 静态成员变量 + 静态方法实现,确保全局唯一实例。
- 示例:饿汉式单例
public class Singleton {
// 静态成员变量:存储唯一实例(类加载时初始化)
private static final Singleton INSTANCE = new Singleton();
// 私有构造方法:禁止外部new
private Singleton() {}
// 静态方法:对外提供获取唯一实例的接口
public static Singleton getInstance() {
return INSTANCE;
}
public void doSomething() {
System.out.println("单例对象执行操作");
}
public static void main(String[] args) {
// 多次调用获取的是同一个对象
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // 输出 true(地址相同)
}
}
5.处理类级别的业务逻辑
当逻辑属于 “类本身” 而非 “类的实例” 时,用静态方法。例如:统计类的实例创建个数、初始化类的全局资源。
- 示例:统计学生实例创建数量
public class Student {
private String name;
// 静态变量:统计实例个数(类共享)
private static int count = 0;
public Student(String name) {
this.name = name;
count++; // 每次创建实例,计数+1
}
// 静态方法:获取实例总数(类级别的查询逻辑)
public static int getStudentCount() {
return count;
}
public static void main(String[] args) {
new Student("张三");
new Student("李四");
new Student("王五");
// 无需创建对象,直接查询类的统计数据
System.out.println("学生总数:" + Student.getStudentCount()); // 输出 3
}
}
静态成员方法的使用禁忌
- 不能访问非静态成员变量 / 方法:静态方法属于类,执行时可能无对象,无法获取实例资源(编译报错);
- 不能使用
this/super关键字:this指向当前对象,super指向父类对象,均依赖实例,静态方法中不存在; - 避免过度使用:若方法依赖对象的状态(实例属性),必须定义为非静态方法,否则会破坏面向对象的封装性。
总结
以上就是今天要讲的内容,本文简单记录了JAVA学习笔记,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!
1万+

被折叠的 条评论
为什么被折叠?



