Java面向对象编程基础:理解类与对象
引言
在当今软件开发的世界里,面向对象编程(Object-Oriented Programming, OOP)已经成为构建复杂、可维护和可扩展应用程序的基石。Java,作为一门历史悠久且广泛应用的编程语言,自诞生之初就深深植根于面向对象的理念之中。掌握Java的面向对象编程基础,尤其是对“类”与“对象”这两个核心概念的深刻理解,是每一位Java开发者迈向专业之路的必经门槛。
本篇博客旨在系统、全面地阐述Java面向对象编程中关于“类”与“对象”的基础知识。我们将从最基础的概念讲起,逐步深入到语法细节、设计原则和实际应用。通过大量的代码示例,力求让抽象的概念变得具体可感,帮助读者不仅“知道”类与对象是什么,更能“理解”它们如何工作,以及为何如此设计。无论你是刚刚接触编程的新手,还是希望巩固基础的有经验开发者,相信都能从本文中获得有价值的见解。
第一章:面向对象编程概述
1.1 什么是面向对象编程?
在深入Java的具体语法之前,我们首先需要理解“面向对象”这一编程范式的哲学和核心思想。
面向对象编程(OOP) 是一种编程范式,它将现实世界中的事物抽象为程序中的“对象”,并通过对象之间的交互来解决问题。OOP的核心思想是将数据(状态)和操作这些数据的方法(行为)封装在一起,形成一个独立的单元——对象。
与之相对的是过程式编程(如C语言),它更侧重于一系列按顺序执行的函数或过程,数据通常是全局或局部变量,与操作它们的函数是分离的。
OOP的主要优势在于:
- 模块化(Modularity):程序被分解为独立的、可管理的模块(对象),每个模块负责特定的功能。
- 信息隐藏(Information Hiding):对象的内部实现细节对外部是隐藏的,只能通过定义好的接口进行访问,这提高了安全性和降低了复杂性。
- 代码重用(Code Reusability):通过继承和组合,可以复用已有的代码,减少重复开发。
- 可维护性(Maintainability):由于模块化和封装,修改一个对象的内部实现通常不会影响到其他部分的代码,使得程序更容易维护和升级。
- 可扩展性(Extensibility):通过继承和多态,可以轻松地添加新的功能而不破坏现有代码。
1.2 面向对象的四大支柱
面向对象编程建立在四个基本支柱之上,它们共同构成了OOP的完整体系:
封装(Encapsulation):
- 定义:将数据(属性)和操作数据的方法(行为)捆绑在一起,形成一个独立的单元(即对象),并尽可能隐藏对象的内部实现细节。
- 目的:控制对对象内部状态的访问,防止外部代码随意修改数据,确保数据的完整性和安全性。通常通过访问修饰符(如private, public)来实现。
- 类比:就像一个电视机,你通过遥控器(接口)来开关机、换台、调节音量,但你不需要知道内部电路是如何工作的。遥控器上的按钮就是访问电视机功能的“接口”。
继承(Inheritance):
- 定义:允许一个类(子类或派生类)基于另一个类(父类或基类)来创建,子类会自动拥有父类的属性和方法,并可以添加新的属性和方法,或修改(重写)父类的行为。
- 目的:实现代码重用,建立类之间的层次关系,表达“is-a”关系(例如,Dog is a Animal)。
- 类比:孩子继承了父母的某些特征(如眼睛颜色、身高趋势),但也可能发展出自己独特的特征。
多态(Polymorphism):
- 定义:同一个操作作用于不同的对象,可以产生不同的执行结果。多态允许使用父类类型的引用来调用子类对象的方法,具体调用哪个方法在运行时决定(动态绑定)。
- 目的:提高代码的灵活性和可扩展性,使得程序可以编写得更加通用,能够处理多种类型的对象。
- 类比:同样是“发出声音”这个操作,狗会“汪汪”叫,猫会“喵喵”叫。你只需要说“叫一声”,不同的动物会以自己的方式响应。
抽象(Abstraction):
- 定义:提取事物的关键特征和行为,忽略不必要的细节,从而简化复杂系统。在Java中,抽象通常通过抽象类(abstract class)和接口(interface)来实现。
- 目的:关注“做什么”而不是“怎么做”,降低系统复杂度,便于设计和理解。
- 类比:驾驶汽车时,你只需要知道方向盘、油门、刹车的作用,而不需要了解发动机内部的燃烧过程。
理解这四大支柱是掌握面向对象编程的关键。在后续的章节中,我们将重点探讨封装和抽象,因为它们直接与“类”和“对象”的定义和创建紧密相关。继承和多态将在后续章节中详细讨论。
第二章:类(Class)—— 对象的蓝图
如果说对象是现实世界中的具体事物,那么类(Class) 就是创造这些事物的蓝图或模板。类定义了对象应该具有哪些特征(属性)和能执行哪些操作(方法)。它本身不是具体的实体,而是一种抽象的描述。
2.1 什么是类?
在Java中,类是一个用户自定义的数据类型。它将相关的数据和操作这些数据的代码组织在一起。你可以把类想象成一个模具,而对象就是用这个模具生产出来的具体产品。
类的主要组成部分:
2.2 定义类:语法与示例
在Java中,使用class关键字来定义一个类。基本语法如下:
[访问修饰符] class 类名 {
// 字段(成员变量)
[访问修饰符] 数据类型 字段名 [= 初始值];
// 构造器
[访问修饰符] 类名([参数列表]) {
// 构造器代码,用于初始化对象
}
// 方法(成员函数)
[访问修饰符] [返回类型] 方法名([参数列表]) {
// 方法体,包含执行的代码
[return 返回值;] // 如果返回类型不是void
}
}
访问修饰符(Access Modifiers) 控制类、字段、方法等的访问权限。常见的有:
- public:对所有类可见。
- private:仅在本类内部可见。
- protected:在本类、同一包内的类以及子类中可见。
- (默认,不写):仅在本包内可见(包访问权限)。
让我们通过一个具体的例子来理解:
示例:定义一个 Person 类
// Person.java
public class Person {
// —- 字段(成员变量) —-
// 使用 private 修饰,封装数据,外部不能直接访问
private String name;
private int age;
private double height; // 身高(米)
// —- 构造器(Constructors) —-
// 无参构造器:创建对象时如果不提供参数,则调用此构造器
public Person() {
// 可以设置默认值
this.name = "未知";
this.age = 0;
this.height = 0.0;
System.out.println("创建了一个 Person 对象(无参构造器)");
}
// 有参构造器:创建对象时提供必要的初始值
public Person(String name, int age, double height) {
// 使用 this 关键字区分同名的参数和字段
this.name = name;
this.age = age;
this.height = height;
System.out.println("创建了一个 Person 对象(有参构造器): " + this.name);
}
// —- 方法(成员函数) —-
// getter 方法:用于获取私有字段的值
public String getName() {
return name;
}
// setter 方法:用于设置私有字段的值(可以包含验证逻辑)
public void setName(String name) {
if (name != null && !name.trim().isEmpty()) {
this.name = name.trim(); // 去除首尾空格
} else {
System.out.println("姓名不能为空!");
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
} else {
System.out.println("年龄必须在0到150之间!");
}
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
if (height > 0 && height < 3.0) {
this.height = height;
} else {
System.out.println("身高必须大于0且小于3米!");
}
}
// 行为方法
public void walk() {
System.out.println(name + " 正在走路。");
}
public void talk(String message) {
System.out.println(name + " 说: \\"" + message + "\\"");
}
public void eat(String food) {
System.out.println(name + " 正在吃 " + food + "。");
}
// 特殊方法:toString() – 返回对象的字符串表示,便于打印
@Override
public String toString() {
return "Person{" +
"name='" + name + '\\'' +
", age=" + age +
", height=" + height +
'}';
}
}
代码解析:
- Person() 是无参构造器。即使我们不写,Java编译器也会自动提供一个默认的无参构造器(如果类中没有其他构造器的话)。但一旦我们定义了有参构造器,无参构造器就需要显式定义,否则无法使用无参方式创建对象。
- Person(String name, int age, double height) 是有参构造器,允许在创建对象时初始化其状态。
- this 关键字在这里用来引用当前对象的实例,this.name = name 表示将参数name的值赋给当前对象的name字段。
- getter和setter方法:这是封装的典型应用。getName()返回name字段的值,setName(String name)用于设置name字段的值。在setter中加入了简单的验证逻辑(检查姓名是否为空),这体现了方法可以包含业务逻辑。
- 行为方法:walk(), talk(String message), eat(String food) 定义了Person对象可以执行的操作。
- toString() 方法:这是一个从Object类(所有类的父类)继承的方法,我们通过@Override注解表明我们正在重写它。重写toString()方法可以自定义对象的字符串表示,当打印对象时会自动调用。例如,System.out.println(person) 会输出类似 Person{name='Alice', age=25, height=1.68} 的字符串。
2.3 构造器详解
构造器是类中非常重要的组成部分,它决定了对象创建时的初始化过程。
构造器的特点:
- 名称必须与类名完全相同(包括大小写)。
- 没有返回类型(既不能写void,也不能写其他类型)。
- 在创建对象时(使用new关键字)由Java虚拟机(JVM)自动调用。
- 一个类可以有多个构造器,只要它们的参数列表不同(参数的类型、数量或顺序不同),这称为构造器重载(Constructor Overloading)。
示例:构造器重载
// 在 Person 类中添加更多构造器
public Person(String name) {
this(name, 0, 0.0); // 调用已有的有参构造器,避免代码重复
System.out.println("创建了一个 Person 对象(仅姓名构造器): " + this.name);
}
public Person(String name, int age) {
this(name, age, 0.0); // 调用已有的有参构造器
System.out.println("创建了一个 Person 对象(姓名和年龄构造器): " + this.name);
}
现在,创建Person对象时,可以根据需要选择不同的构造器:
Person person1 = new Person(); // 调用无参构造器
Person person2 = new Person("Bob"); // 调用仅姓名构造器
Person person3 = new Person("Charlie", 30); // 调用姓名和年龄构造器
Person person4 = new Person("Diana", 28, 1.75); // 调用全参构造器
this() 调用:在构造器内部,可以使用this(参数列表)来调用同一个类中的另一个构造器。这必须是构造器中的第一条语句。这有助于减少代码重复。
2.4 静态成员(Static Members)
除了实例成员(字段和方法),类还可以拥有静态成员。静态成员属于类本身,而不是类的某个特定实例。
- 静态字段(Static Fields):也称为类变量。无论创建多少个对象,静态字段在内存中只有一份拷贝,被所有实例共享。
- 静态方法(Static Methods):也称为类方法。它们不依赖于任何对象实例,可以直接通过类名调用。静态方法内部不能直接访问非静态(实例)成员,因为它们没有this引用。
示例:在 Person 类中添加静态成员
public class Person {
// … (之前的字段和方法)
// 静态字段:统计创建的 Person 对象总数
private static int totalPersonCount = 0;
// 在构造器中增加计数
public Person() {
// … (之前的初始化代码)
totalPersonCount++; // 每创建一个对象,计数加1
}
public Person(String name, int age, double height) {
// … (之前的初始化代码)
totalPersonCount++; // 每创建一个对象,计数加1
}
// 静态方法:获取总人数
public static int getTotalPersonCount() {
return totalPersonCount;
}
// 静态方法:比较两个身高(不依赖于具体对象)
public static String compareHeight(double height1, double height2) {
if (height1 > height2) {
return "第一个人更高";
} else if (height1 < height2) {
return "第二个人更高";
} else {
return "两人一样高";
}
}
// 注意:静态方法不能直接访问实例字段
// public static void printName() {
// System.out.println(name); // 编译错误!name 是实例字段
// }
}
使用静态成员:
public class StaticDemo {
public static void main(String[] args) {
System.out.println("初始总人数: " + Person.getTotalPersonCount()); // 0
Person p1 = new Person("Alice", 25, 1.68);
Person p2 = new Person("Bob", 30, 1.75);
System.out.println("创建两个对象后总人数: " + Person.getTotalPersonCount()); // 2
// 调用静态方法
String result = Person.compareHeight(1.80, 1.70);
System.out.println(result); // 第一个人更高
}
}
关键点:
- 静态成员通过类名.成员名访问(如Person.getTotalPersonCount())。
- 静态成员在类加载时初始化,早于任何对象的创建。
- 静态方法常用于工具方法(如Math.max())、工厂方法或访问类级别的状态。
第三章:对象(Object)—— 类的实例
如果说类是蓝图,那么对象(Object) 就是根据这个蓝图实际建造出来的具体实体。对象是程序运行时的基本单元,它拥有类所定义的状态(字段的值)和行为(方法的实现)。
3.1 什么是对象?
对象是类的一个实例(Instance)。当你使用new关键字创建一个类的实例时,就诞生了一个新的对象。每个对象都有自己独立的一套实例字段(非静态字段)的副本,但共享类的静态成员和方法代码。
对象的生命周期:
3.2 创建对象:new 关键字
在Java中,创建对象的标准方式是使用new关键字,后跟构造器的调用。
语法:
类名 对象引用名 = new 构造器(参数列表);
- 类名:你要创建实例的类的名称。
- 对象引用名:一个变量,用来存储新创建对象的引用(内存地址)。这个变量的类型就是类名。
- new:Java关键字,负责在堆内存(Heap Memory)中为新对象分配空间。
- 构造器(参数列表):调用类的某个构造器来初始化新对象的状态。参数列表必须与所调用的构造器的参数匹配。
示例:创建 Person 对象
public class ObjectCreationDemo {
public static void main(String[] args) {
// 1. 使用无参构造器创建对象
Person person1 = new Person();
// person1 是一个引用变量,指向堆内存中创建的 Person 对象
// 2. 使用有参构造器创建对象
Person person2 = new Person("Alice", 25, 1.68);
Person person3 = new Person("Bob", 30, 1.75);
// 3. 使用重载的构造器创建对象
Person person4 = new Person("Charlie");
Person person5 = new Person("Diana", 28);
// 打印对象(会自动调用 toString() 方法)
System.out.println(person1); // Person{name='未知', age=0, height=0.0}
System.out.println(person2); // Person{name='Alice', age=25, height=1.68}
System.out.println(person3); // Person{name='Bob', age=30, height=1.75}
System.out.println(person4); // Person{name='Charlie', age=0, height=0.0}
System.out.println(person5); // Person{name='Diana', age=28, height=0.0}
}
}
内存模型简述:
- Person person2 = new Person("Alice", 25, 1.68);
- 右边的new Person(…)在堆(Heap) 内存中分配一块空间,存放name, age, height这三个字段的实际值(“Alice”, 25, 1.68)。
- 左边的Person person2在栈(Stack) 内存中创建一个名为person2的引用变量。
- = 操作将堆中对象的内存地址(引用)赋值给person2变量。
- 因此,person2这个变量并不直接存储对象的数据,而是存储一个指向堆中对象的“指针”或“引用”。
3.3 访问对象的成员
一旦创建了对象,就可以通过对象引用来访问其公开的成员(字段和方法)。由于我们通常将字段设为private,所以主要通过getter和setter方法来访问和修改字段,以及调用对象的行为方法。
语法:
对象引用名.成员名
示例:访问 Person 对象的成员
public class ObjectAccessDemo {
public static void main(String[] args) {
// 创建对象
Person alice = new Person("Alice", 25, 1.68);
// — 访问(读取)字段值 —
// 通过 getter 方法
String name = alice.getName();
int age = alice.getAge();
double height = alice.getHeight();
System.out.println(name + " is " + age + " years old and " + height + "m tall.");
// — 修改(设置)字段值 —
// 通过 setter 方法
alice.setAge(26); // 生日过了,年龄增加
alice.setHeight(1.69); // 长高了一点点(开玩笑)
// alice.setName(""); // 会输出错误信息,因为setter中有验证
// 再次打印查看变化
System.out.println(alice); // Person{name='Alice', age=26, height=1.69}
// — 调用对象的行为方法 —
alice.walk(); // Alice 正在走路。
alice.talk("Hello, everyone!"); // Alice 说: "Hello, everyone!"
alice.eat("an apple"); // Alice 正在吃 an apple。
}
}
关键点:
- alice.getName():调用alice对象的getName()方法,返回其name字段的值。
- alice.setAge(26):调用alice对象的setAge()方法,将age字段设置为26。
- alice.walk():调用alice对象的walk()方法,执行行走的动作。
- 封装的体现:我们无法直接写alice.age = 26;(因为age是private),必须通过setAge()方法。这使得我们可以在setAge()方法中加入逻辑(如年龄范围检查),确保数据的有效性。
3.4 多个对象与独立状态
每个对象都有自己独立的实例字段副本。修改一个对象的状态不会影响其他对象。
示例:多个 Person 对象
public class MultipleObjectsDemo {
public static void main(String[] args) {
Person personA = new Person("Alice", 25, 1.68);
Person personB = new Person("Bob", 30, 1.75);
System.out.println("创建时:");
System.out.println("Person A: " + personA);
System.out.println("Person B: " + personB);
// 修改 personA 的年龄
personA.setAge(26);
// 修改 personB 的身高
personB.setHeight(1.76);
System.out.println("\\n修改后:");
System.out.println("Person A: " + personA); // age 变为 26
System.out.println("Person B: " + personB); // height 变为 1.76
// 静态成员是共享的
System.out.println("总人数: " + Person.getTotalPersonCount()); // 2
}
}
输出:
创建时:
Person A: Person{name='Alice', age=25, height=1.68}
Person B: Person{name='Bob', age=30, height=1.75}
修改后:
Person A: Person{name='Alice', age=26, height=1.68}
Person B: Person{name='Bob', age=30, height=1.76}
总人数: 2
分析:
- personA和personB是两个独立的对象。
- 修改personA的age只影响personA,personB的age仍然是30。
- 同样,修改personB的height只影响personB。
- 静态字段totalPersonCount是共享的,创建两个对象后其值为2。
3.5 对象引用与赋值
理解对象引用是掌握Java内存模型的关键。
对象引用赋值:
Person person1 = new Person("Alice", 25, 1.68);
Person person2 = person1; // 将 person1 的引用赋值给 person2
此时,person1和person2都指向同一个Person对象(在堆内存中的同一个位置)。它们是同一个对象的两个“别名”。
示例:引用赋值的影响
public class ReferenceAssignmentDemo {
public static void main(String[] args) {
Person alice1 = new Person("Alice", 25, 1.68);
Person alice2 = alice1; // alice2 指向 alice1 所指向的同一个对象
System.out.println("初始状态:");
System.out.println("alice1: " + alice1);
System.out.println("alice2: " + alice2);
// 通过 alice2 修改对象
alice2.setAge(26);
alice2.setName("Alice Smith");
System.out.println("\\n通过 alice2 修改后:");
System.out.println("alice1: " + alice1); // alice1 的状态也变了!
System.out.println("alice2: " + alice2);
// 比较引用
System.out.println("alice1 == alice2: " + (alice1 == alice2)); // true (引用相等)
}
}
输出:
初始状态:
alice1: Person{name='Alice', age=25, height=1.68}
alice2: Person{name='Alice', age=25, height=1.68}
通过 alice2 修改后:
alice1: Person{name='Alice Smith', age=26, height=1.68}
alice2: Person{name='Alice Smith', age=26, height=1.68}
alice1 == alice2: true
关键点:
- alice1 == alice2 返回true,因为它们引用的是堆内存中的同一个对象。
- 通过alice2修改对象的状态,alice1看到的也是修改后的状态,因为它们是同一个对象。
创建独立的副本:
如果你想创建一个具有相同初始状态但完全独立的对象,你需要创建一个新的实例。
Person alice1 = new Person("Alice", 25, 1.68);
Person alice3 = new Person(alice1.getName(), alice1.getAge(), alice1.getHeight());
// 或者使用复制构造器(如果类中定义了的话)
// 现在 alice1 和 alice3 是两个独立的对象
alice3.setAge(30);
System.out.println("alice1: " + alice1); // age 仍然是 25
System.out.println("alice3: " + alice3); // age 是 30
System.out.println("alice1 == alice3: " + (alice1 == alice3)); // false
第四章:深入理解封装与访问控制
封装是面向对象编程的核心原则之一,它通过隐藏对象的内部实现细节,仅暴露必要的接口来与外界交互,从而保护数据的完整性和安全性。Java通过访问修饰符(Access Modifiers) 来实现封装。
4.1 访问修饰符详解
Java提供了四种访问级别,从最宽松到最严格:
public | ✅ | ✅ | ✅ | ✅ | 无限制,任何地方都可访问。 |
protected | ✅ | ✅ | ✅ | ❌ | 同一类、同一包、子类(即使子类在不同包)可访问。 |
(默认,无关键字) | ✅ | ✅ | ❌ | ❌ | 仅在同一包内可访问(包访问权限)。 |
private | ✅ | ❌ | ❌ | ❌ | 仅在本类内部可访问。 |
示例:演示不同访问修饰符的作用
// 文件: AccessDemo.java
package com.example.access;
// 基类
class BaseClass {
public String publicField = "public";
protected String protectedField = "protected";
String defaultField = "default"; // 包访问权限
private String privateField = "private";
public void publicMethod() {
System.out.println("BaseClass.publicMethod()");
// 在类内部,可以访问所有成员
System.out.println(privateField); // OK
}
protected void protectedMethod() {
System.out.println("BaseClass.protectedMethod()");
}
void defaultMethod() { // 包访问权限
System.out.println("BaseClass.defaultMethod()");
}
private void privateMethod() {
System.out.println("BaseClass.privateMethod()");
}
// 一个方法用于展示内部访问
public void demonstrateInternalAccess() {
System.out.println("=== BaseClass 内部访问 ===");
System.out.println("publicField: " + publicField);
System.out.println("protectedField: " + protectedField);
System.out.println("defaultField: " + defaultField);
System.out.println("privateField: " + privateField);
privateMethod(); // OK
}
}
// 同一包内的另一个类
class SamePackageClass {
public void accessBase(BaseClass base) {
System.out.println("=== SamePackageClass 访问 BaseClass ===");
System.out.println("publicField: " + base.publicField); // OK
System.out.println("protectedField: " + base.protectedField); // OK
System.out.println("defaultField: " + base.defaultField); // OK
// System.out.println(base.privateField); // 编译错误!
base.publicMethod(); // OK
base.protectedMethod(); // OK
base.defaultMethod(); // OK
// base.privateMethod(); // 编译错误!
}
}
// 同一包内的子类
class SubClassInSamePackage extends BaseClass {
public void accessFromSubClass() {
System.out.println("=== SubClassInSamePackage 访问 ===");
System.out.println("publicField: " + publicField); // OK
System.out.println("protectedField: " + protectedField); // OK
System.out.println("defaultField: " + defaultField); // OK
// System.out.println(privateField); // 编译错误!
publicMethod(); // OK
protectedMethod(); // OK
defaultMethod(); // OK
// privateMethod(); // 编译错误!
}
}
// 主类用于测试
public class AccessDemo {
public static void main(String[] args) {
BaseClass base = new BaseClass();
base.demonstrateInternalAccess();
SamePackageClass samePkg = new SamePackageClass();
samePkg.accessBase(base);
SubClassInSamePackage sub = new SubClassInSamePackage();
sub.accessFromSubClass();
}
}
输出分析:
- BaseClass内部可以访问所有成员。
- SamePackageClass(同包)可以访问public, protected, 和default成员,但不能访问private成员。
- SubClassInSamePackage(同包子类)的访问权限与SamePackageClass相同。
跨包示例:
// 文件: DifferentPackageClass.java
package com.example.other;
import com.example.access.BaseClass;
// 不同包的非子类
public class DifferentPackageClass {
public void accessBase(BaseClass base) {
System.out.println("=== DifferentPackageClass 访问 BaseClass ===");
System.out.println("publicField: " + base.publicField); // OK
// System.out.println(base.protectedField); // 编译错误!
// System.out.println(base.defaultField); // 编译错误!
// System.out.println(base.privateField); // 编译错误!
base.publicMethod(); // OK
// base.protectedMethod(); // 编译错误!
// base.defaultMethod(); // 编译错误!
// base.privateMethod(); // 编译错误!
}
}
// 文件: DifferentPackageSubClass.java
package com.example.other;
import com.example.access.BaseClass;
// 不同包的子类
public class DifferentPackageSubClass extends BaseClass {
public void accessFromSubClass() {
System.out.println("=== DifferentPackageSubClass 访问 ===");
System.out.println("publicField: " + publicField); // OK
System.out.println("protectedField: " + protectedField); // OK (子类特权)
// System.out.println(defaultField); // 编译错误! (不同包)
// System.out.println(privateField); // 编译错误!
publicMethod(); // OK
protectedMethod(); // OK (子类特权)
// defaultMethod(); // 编译错误! (不同包)
// privateMethod(); // 编译错误!
}
}
关键点:
- DifferentPackageClass只能访问BaseClass的public成员。
- DifferentPackageSubClass作为子类,可以访问public和protected成员,但不能访问default(包访问权限)成员。
4.2 封装的实践:为什么使用 getter 和 setter?
直接将字段设为public虽然简单,但破坏了封装性。使用private字段配合public getter和setter方法是标准做法,原因如下:
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Age must be between 0 and 150.");
}
this.age = age;
}
private final String id; // final 表示不可变
public String getId() { return id; } // 只有 getter
// 只写字段(较少见)
private String passwordHash;
public void setPassword(String password) {
this.passwordHash = hashPassword(password);
} // 只有 setter
public ExpensiveObject getExpensiveObject() {
if (expensiveObject == null) {
expensiveObject = loadExpensiveObject(); // 只在需要时加载
}
return expensiveObject;
}
public void setStatus(String status) {
String oldStatus = this.status;
this.status = status;
fireStatusChangedEvent(oldStatus, status); // 通知状态改变
}
// private int age;
// 新实现:存储出生日期,年龄通过计算得出
private LocalDate birthDate;
public int getAge() {
return Period.between(birthDate, LocalDate.now()).getYears();
}
// 外部代码调用 getAge() 的方式完全不变!
总结:封装不是为了“隐藏”而隐藏,而是为了提供一个稳定、可控的接口,保护内部状态不被意外破坏,并为未来的修改和扩展提供灵活性。
评论前必须登录!
注册