云计算百科
云计算领域专业知识百科平台

基础09-Java面向对象编程基础:理解类与对象

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中,类是一个用户自定义的数据类型。它将相关的数据和操作这些数据的代码组织在一起。你可以把类想象成一个模具,而对象就是用这个模具生产出来的具体产品。

    类的主要组成部分:

  • 字段(Fields):也称为属性(Properties) 或成员变量(Member Variables)。它们代表了类的状态或特征。例如,一个Person类可能有name(姓名)、age(年龄)、height(身高)等字段。
  • 方法(Methods):也称为成员函数(Member Functions)。它们定义了类的行为或能执行的操作。例如,一个Person类可能有walk()(行走)、talk()(说话)、eat()(吃饭)等方法。
  • 构造器(Constructors):一种特殊的方法,用于在创建对象时初始化对象的状态。它的名字必须与类名完全相同,且没有返回类型(连void都不能有)。
  • 代码块(Code Blocks):包括实例初始化块和静态初始化块,用于在对象创建或类加载时执行一些初始化代码。
  • 内部类(Inner Classes):定义在另一个类内部的类。
  • 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 +
    '}';
    }
    }

    代码解析:

  • public class Person:定义了一个名为Person的公共类。类名通常使用大驼峰命名法(PascalCase)。
  • 字段:name, age, height 都被声明为private,实现了封装。外部代码不能直接访问或修改它们,必须通过getter和setter方法。
  • 构造器:
    • 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关键字创建一个类的实例时,就诞生了一个新的对象。每个对象都有自己独立的一套实例字段(非静态字段)的副本,但共享类的静态成员和方法代码。

    对象的生命周期:

  • 创建(Creation):使用new关键字调用构造器。
  • 使用(Usage):通过对象引用来访问其字段(通过getter/setter)和调用其方法。
  • 销毁(Destruction):当对象不再被任何引用指向时,它成为垃圾(Garbage),最终由Java的垃圾回收器(Garbage Collector, GC)自动回收内存。
  • 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方法是标准做法,原因如下:

  • 数据验证(Data Validation):在setter方法中可以加入逻辑,确保数据的有效性。public void setAge(int age) {
    if (age < 0 || age > 150) {
    throw new IllegalArgumentException("Age must be between 0 and 150.");
    }
    this.age = age;
    }

  • 控制访问(Controlled Access):可以只提供getter(只读)或只提供setter(只写),或者对读写施加不同的条件。// 只读字段
    private final String id; // final 表示不可变
    public String getId() { return id; } // 只有 getter

    // 只写字段(较少见)
    private String passwordHash;
    public void setPassword(String password) {
    this.passwordHash = hashPassword(password);
    } // 只有 setter

  • 延迟初始化(Lazy Initialization):getter方法可以在第一次访问时才计算或加载数据。private ExpensiveObject expensiveObject;
    public ExpensiveObject getExpensiveObject() {
    if (expensiveObject == null) {
    expensiveObject = loadExpensiveObject(); // 只在需要时加载
    }
    return expensiveObject;
    }

  • 触发副作用(Side Effects):setter方法可以触发其他操作,如通知监听器、更新UI等。private String status;
    public void setStatus(String status) {
    String oldStatus = this.status;
    this.status = status;
    fireStatusChangedEvent(oldStatus, status); // 通知状态改变
    }

  • 保持向后兼容性(Backward Compatibility):即使内部实现改变了,只要getter/setter的签名不变,外部代码就不需要修改。// 旧实现:直接存储年龄
    // private int age;

    // 新实现:存储出生日期,年龄通过计算得出
    private LocalDate birthDate;
    public int getAge() {
    return Period.between(birthDate, LocalDate.now()).getYears();
    }
    // 外部代码调用 getAge() 的方式完全不变!

  • 总结:封装不是为了“隐藏”而隐藏,而是为了提供一个稳定、可控的接口,保护内部状态不被意外破坏,并为未来的修改和扩展提供灵活性。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 基础09-Java面向对象编程基础:理解类与对象
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!