欢迎光临
我们一直在努力

【Java基础|Day06】Java面向对象之封装篇:从class定义到内存

文章目录

  • 📚 今日学习目标
  • 一、什么是面向对象?
  • 二、类和对象
    • 2.1 类和对象的区别
    • 2.2 如何定义类和对象
      • 2.2.1 定义类
      • 2.2.2 创建对象
    • 2.3 Java中的类分类
  • 三、封装
    • 3.1 什么是封装?
    • 3.2 如何确定方法的归属?
    • 3.3 访问权限控制
    • 3.4 私有化后如何赋值和取值?
  • 四、标准的Javabean类
  • 五、对象内存理解
    • 5.1 一个对象的内存图
    • 5.2 this指针的内存原理

📚 今日学习目标

  • 理解面向对象编程的核心思想
  • 掌握类和对象的区别与定义
  • 深入理解封装及其在Java中的实现
  • 熟悉访问权限修饰符及构造方法的使用
  • 理解this关键字的作用及掌握使用方法
  • 了解对象在内存中的分配过程

  • 一、什么是面向对象?

    面向对象编程(OOP) 是一种软件开发方法,它将现实世界的事物抽象成“对象”。每个对象都包含数据(属性) 和操作数据的方法(行为)。这种思想让程序更贴近人类的思维方式,从而提高代码的可读性、可维护性和可复用性。

    🌰 例如:现实中的“人”有姓名、年龄等属性,有吃饭、睡觉等行为。在程序中,我们可以用一个Person对象来表示一个人,包含相应的属性和方法。


    二、类和对象

    2.1 类和对象的区别

    • 类(Class):是一类事物的抽象描述,相当于一个模具或蓝图。它定义了该类事物应有的共同特征和行为,🌰例如“人”、“手机”。
    • 对象(Object):是类的具体实例,是真实存在的个体。例如,“人”这个类的一个对象是“张三”,“手机”类的一个对象是“华为P40”。

    *注意:必须先定义类,才能通过类创建对象。

    2.2 如何定义类和对象

    2.2.1 定义类

    使用关键字 class,类中包含成员变量(属性) 和成员方法(行为)。

    public class Person {
    // 成员变量(属性数据)
    String name;
    int age;

    // 成员方法(操作数据的方法)
    public void display(){
    System.out.println("姓名:" + name + ",年龄:" + age);
    }
    }

    2.2.2 创建对象

    格式:类名 对象名 = new 类名();

    Person p = new Person(); // 创建Person类的对象p

    通过 对象名.成员变量 访问属性,通过 对象名.方法名() 调用方法。

    p.name = "张三";
    p.age = 25;
    p.display(); // 输出:姓名:张三,年龄:25

    2.3 Java中的类分类

    • Javabean类:用来描述一类事物的类,只包含属性和方法,不包含main方法。例如上面的Person。
    • 测试类:包含main方法的类,用于创建Javabean对象并对其进行操作。

    public class Test {
    public static void main(String[] args) {
    Person p = new Person();
    p.name = "李四";
    p.age = 30;
    p.display();
    }
    }


    三、封装

    3.1 什么是封装?

    封装是面向对象的三大特性之一(另两个是继承和多态)。它包含两层含义:

  • 将对象的属性和行为包装在一起,形成一个独立的整体。(引发3.2思考)
  • 控制对内部数据和方法的访问权限,隐藏实现细节,仅暴露必要的接口。(引发3.3思考)
  • 封装的好处:

    • 保护数据:防止外部随意修改,保证数据的合法性和安全性。
    • 降低耦合:隐藏内部实现,外部只需关心接口,便于维护和升级。

    3.2 如何确定方法的归属?

    一个常见的设计问题是:某个行为应该属于哪个类? 🌰例如,“人关门”或“人画圆”这两个行为,代码应该如何设计?

    核心原则: 谁拥有数据,谁提供操作。想动谁的数据,就调用谁的方法。 再换句话说 执行这个动作,主要会改变或者涉及到谁的状态或者数据 ? 如果A要操作B的数据,A应该“请求”B去做,而不是“代替”B去做。

    🌰解释:

    • 人关门:关门的动作主要改变的是门的状态(从开到关),因此关门的方法应该属于门类,人只是调用门的close()方法。
    • 人画圆:画圆需要使用圆的半径、圆心等数据,因此绘制圆的方法应该属于圆类,人调用圆的draw()方法。

    再看一个例子:公司给员工发工资

    class Company {
    private Account account; // 公司的财务账本

    void paySalary(Employee emp) {
    this.account.deduct(emp.salary); // 公司修改自己的账本数据
    emp.receiveSalary(emp.salary); // 通知员工更新他自己的数据
    }
    }

    class Employee {
    private BankCard card; // 员工的银行卡

    void receiveSalary(double amount) {
    this.card.deposit(amount); // 员工修改自己的银行卡数据
    }
    }

    3.3 访问权限控制

    如果成员变量直接暴露给外部,任何人都可以随意修改,可能导致数据不安全或不合法。Java提供了访问权限修饰符来控制成员的可见性。

    修饰符本类内部同包其他类不同包子类任何地方
    private
    默认(无)
    protected
    public

    与C++的对比

    特性C++Java
    访问权限类型 public、protected、private public、protected、private、默认(包私有)
    默认访问权限 类中默认为private,结构体中默认为public 默认为包私有(仅同一包内可见,不同包中的类无论是否有继承关系都无法访问。)
    包/模块概念 ❌ 无包概念,可见性由头文件包含和声明位置决定 ✅ 有包机制,提供同包可见性
    private 作用 • 仅本类内部可访问 • 仅本类内部可访问
    protected 作用 • 本类内部 • 派生类内部(只能通过派生类自身实例访问继承的成员) • 本类内部 • 同一包内任何类(无论是否有继承关系) • 不同包子类内部(只能通过子类自身实例访问继承的成员)
    public 作用 任何地方均可访问 任何地方均可访问
    友元机制 ✅ 支持 friend,可声明外部函数或类为友元,使其能够访问本类的私有和保护成员 ❌ 不支持,但可通过包私有\\实现同一包内的紧密协作

    3.4 私有化后如何赋值和取值?

    方式一:get/set方法

    通过编写getXxx()和setXxx()方法来控制权限。

    *注:编写get set方法,可能会疑惑,既然提供编写了get和set,那你私有化的目的是什么?

    • 为了自主决定读写权限,你可以只提供 get 方法而不提供 set 方法,这样外部代码可以获取数据,但不能随意修改它。反之,也可以只提供 set 而不提供 get,创建一个只写的属性
    • 而且这种方法,在方法内部你可以添加校验逻辑,避免非法数据。例如,年龄不能为负数。

    public class Person {
    private String name;
    private int age;

    // setter:允许外部设置值,可以加入合法性校验
    public void setAge(int age) {
    if (age >= 0 && age <= 150) {
    this.age = age;
    } else {
    System.out.println("年龄不合法!");
    }
    }

    // getter:允许外部获取值
    public int getAge() {
    return age;
    }

    // 可以只提供get而不提供set,实现只读属性
    public String getName() {
    return name;
    }
    }

    方式二:构造方法

    构造方法是一种特殊的方法,用于在创建对象时初始化对象的状态。

    格式:权限修饰符 类名(参数列表) { 方法体 }

    特点:

  • 方法名必须与类名完全相同(包括大小写)
  • 没有返回值类型,也不写void,不能有return语句
  • 可以重载(多个构造方法,参数列表不同)
  • 创建对象的时候由虚拟机自动调用,不需要手动调用,每创建一次对象,就会调用一次构造方法。
  • public class Person {
    private String name;
    private int age;

    // 无参构造
    public Person() {
    }

    // 有参构造
    public Person(String name, int age) {
    this.name = name; // this用于区分成员变量和局部变量
    this.age = age;
    }
    }

    关于this关键字: this代表当前调用者的地址。当构造方法的参数名与成员变量名相同时,必须用this来区分。

    Java与C++构造方法对比:

    类别C++Java
    内存管理 – 手动内存管理:需要显式地分配和释放内存,生命周期精确可控。 – 追求性能与控制:提供底层操作能力,开发者需对资源管理负责。 – 自动内存管理(垃圾回收 GC):JVM自动回收不再使用的对象内存。 – 追求安全与简洁:屏蔽内存细节,减少手动错误。
    默认构造函数生成规则 – 如果没有定义任何构造函数,编译器提供一个无参默认构造。 – 如果定义了有参构造函数,则不再生成无参默认构造,但仍然提供默认的拷贝构造函数。 – 拷贝构造函数是独立生成的:只要用户没有自定义拷贝构造,编译器总会生成一个默认拷贝构造(逐成员拷贝),即使已经定义了其他构造函数。如果定义了拷贝构造函数,则不提供其他任何构造函数。 – 如果没有定义任何构造方法,编译器生成一个无参默认构造。 – 如果定义了有参构造方法,则不再生成无参默认构造。 – Java 没有“默认拷贝构造方法”的概念:不会自动生成接收同类型参数的构造方法,必须手动实现拷贝逻辑。这迫使你有意识地决定拷贝行为,而不是在不知情的情况下掉进浅拷贝陷阱。
    初始化列表 – 特有语法,在构造函数体执行前初始化成员变量、基类、常量成员和引用成员。 – 无此语法。
    成员变量直接赋值 – C++98/03:不允许非静态成员在类内直接初始化,必须在构造函数初始化列表或构造体内赋值。 – C++11 起:支持类内初始化器(in-class initializers),允许在声明时直接赋值 – 始终允许:成员变量可以在声明时直接赋值,编译器会将这些赋值处理为在构造方法执行前执行(按声明顺序)。
    友元(friend) – 支持:允许外部函数或类访问当前类的私有成员,破坏封装性以换取灵活性。 – 不支持:没有直接对应的关键字,可通过包级可见性或反射实现类似效果,但语言层面不提供。
    运算符重载 – 支持:可重新定义大多数运算符(如 +、-、[])对自定义类型的含义,但需谨慎使用以保持代码可读性。 – 不支持:除了 + 对字符串的拼接(语言内置特例),开发者不能自定义运算符行为。Java 设计者认为这会导致代码复杂且易读性差。

    四、标准的Javabean类

  • 类名见名知意,大驼峰命名
  • 成员变量使用private修饰
  • 提供至少两个构造方法
    • 无参构造方法
    • 带全部参数的构造方法
  • 成员方法
    • 提供每一个成员变量对应的getXxx()和setXxx()
    • 其他方法也需写上

  • 五、对象内存理解

    5.1 一个对象的内存图

    整个过程与数组的内存分配高度一致【详细可以参考数组学习篇】:数组也是引用类型,在堆中分配空间,默认初始化元素,变量保存地址。区别在于数组的类由JVM动态生成,没有独立的class文件加载过程,且数组只有length属性和继承来的方法,不能自定义成员方法。

    黑马程序员讲解图

    过程描述(按执行顺序)

  • main方法字节码文件入方法区 JVM启动后,首先通过类加载器将TestStudent.class和Student.class加载到方法区(或元空间)。方法区中存储了类的结构信息,包括成员变量定义、方法字节码、静态常量等。此时main方法只是作为字节码的一部分存在于方法区中,尚未执行。

  • main方法入栈并声明局部变量 JVM调用main方法,在栈内存中为main方法创建栈帧,栈帧内包含局部变量表等。此时声明局部变量s(类型为Student),默认值为null,但还未指向任何对象。

  • new创建对象 执行new Student()时,JVM完成以下操作:

    • 在方法区中查找Student类信息:如果类尚未加载,则先加载(本例已在第一步加载)。
    • 在堆内存中开辟空间:为Student对象分配一块连续的内存,用于存放实例变量(name和age)以及对象头(包含哈希码、GC信息、指向方法区类元数据的指针等)。
    • 默认初始化:将实例变量设为默认值——name = null,age = 0。
    • 显示初始化:执行代码中定义的显式赋值——name = “张三”,age = 23。注意这里的"张三"是一个字符串常量,存储在方法区的字符串常量池中,对象中的name变量保存的是该字符串的引用。
    • 构造方法初始化:调用构造方法(本例为默认无参构造,没有额外操作,但概念上这一步存在)。
    • 将堆内存地址赋值给局部变量:将新创建对象的引用(堆地址)存入栈中s变量。
  • 输出与修改成员变量

    • System.out.println(s):输出的是对象的引用地址(如Student@15db9742),本质是调用toString()。
    • 首次输出s.name和s.age时,通过s持有的地址访问堆中对象,得到显示初始化后的值“张三”和23。
    • s.name = "阿强"和s.age = 23(这里其实没变)直接修改堆中对象的成员变量。
    • 再次输出,看到修改后的值。
  • 调用成员方法 s.study()执行时,JVM通过对象头的类型指针找到方法区中Student类的信息,定位到study()方法的字节码并执行。方法执行过程中,若有局部变量,会在栈中创建新的栈帧,但本例study()内部无额外变量。

  • 5.2 this指针的内存原理

    黑马程序员讲解图 补充:成员变量和局部变量的区别

    区别成员变量局部变量
    类中位置不同 类中,方法外 方法内、方法申明上
    初始化值不同 有默认初始化值 没有,使用之前需要完成赋值
    内存位置不同 堆内存 栈内存
    生命周期不同 随着对象的创建而存在,随着对象的消失而消失 随着方法的调用而存在,随着方法的运行结束而消失
    作用域 整个类中有效 当前方法中有效

    以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。

    赞(0)
    未经允许不得转载:171主机测评 » 【Java基础|Day06】Java面向对象之封装篇:从class定义到内存
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址