零基础学Java|第八篇:面向对象编程的类与对象(基础)

开篇:编程世界的"世界观"

在之前的篇章里,我们写的程序主要是以"方法"为核心的。我们定义了数据,然后写一堆方法来处理这些数据。这种方式叫做面向过程编程,它关注的是"怎么做"。

而从今天开始,我们要换一种更高级的"世界观"来看待编程,这就是面向对象编程。它关注的是"谁来做"。

你可以把Java想象成一个虚拟世界。在这个世界里,最小的单元不是函数,而是"对象"。一个对象既拥有属性 (比如一个人的年龄、姓名),也拥有行为(比如一个人会走路、说话)。而我们编写程序,就是在创造和指挥这些对象,让他们相互协作来完成复杂的任务。

1. 类与对象:图纸与实物的关系

1.1 什么是类?

就是分类 ,是对具有相同特性和行为的一类事物的抽象描述,就像是一张设计图纸

比如,我们可以设计一个"人类"的图纸。这张图纸上写着:所有人都应该有名字、年龄(这是属性 ),所有人都应该会说话、会走路(这是方法)。

java 复制代码
// 这就是一个类,可以看作是一张图纸
class Person {
    // 属性(也叫成员变量):描述对象有什么
    String name;
    int age;

    // 方法:描述对象能做什么
    public void speak() {
        System.out.println(name + "说:你好呀!");
    }
}

1.2 什么是对象?

对象 就是根据图纸真正制造出来的实物。图纸只有一个,但根据这张图纸,我们可以制造出千千万万个具体的人。

java 复制代码
public class Main {
    public static void main(String[] args) {
        // 根据Person类(图纸)创建一个具体的对象(实物)
        Person person1 = new Person();
        person1.name = "张三";
        person1.age = 18;
        
        Person person2 = new Person();
        person2.name = "李四";
        person2.age = 20;
        
        person1.speak(); // 输出:张三说:你好呀!
        person2.speak(); // 输出:李四说:你好呀!
    }
}

区别与联系总结:

  • 是抽象的,是概念;对象是具体的,是实实在在的存在。
  • 是创建对象的模板;对象是类的具体实例。
  • 当你执行 new Person() 时,就是在实例化一个对象。

1.3 对象在内存中的存在形式

用上面的 person1 = new Person(); 来分析内存。

假设 Person 类中有 String name;int age;

内存分配机制:

  1. :当JVM执行 main 方法时,会在栈中创建一个 main 方法的栈帧。在 main 方法中,person1 是一个变量(引用),它存储在栈中。
  2. :当执行 new Person() 时,JVM会在堆内存 中开辟一块空间,用来真正存储这个对象的实例数据(比如 age=0name=null)。
  3. 引用指向 :栈中的 person1 变量保存的并不是堆中的对象本身,而是对象在堆中的内存地址 (比如 0x1234)。我们常说 person1 是一个引用,它指向了堆中的对象。

图解:

lua 复制代码
栈内存                         堆内存
--------                     ------------------------------
main() 方法栈帧                Person对象实例
+----------+                  +----------------------------+
| person1  | ---------------> | 对象头 (Mark Word, Class指针)|
| (引用)   |   地址 0x1234     | 实例数据:                   |
+----------+                  |   name = null (引用)       |
                              |   age = 0                  |
                              +----------------------------+

当我们执行 person1.name = "张三" 时,JVM会通过 person1 存储的地址 0x1234 找到堆中的对象,然后将 "张三" 这个字符串对象的地址赋值给 name 属性。

2. 成员方法:赋予对象行为

2.1 方法的调用机制原理(重点)

方法就是让对象干活儿的指令。方法调用的背后,是JVM一场精密的"栈帧"管理工作。

执行流程分析:

  1. 程序运行,JVM调用 main 方法,在栈中为 main 方法创建一个栈帧,压入栈底。
  2. 当在 main 方法中执行 person1.speak() 时,JVM会在栈中为 speak 方法创建一个新的栈帧,并压入栈顶。当前正在执行的就是栈顶的方法。
  3. speak 方法执行完毕后,它的栈帧会被销毁(弹出),释放局部变量。程序回到 main 方法的栈帧,继续向下执行。

示意图:

scss 复制代码
栈(先进后出)
+-------------------------+
| speak() 方法的栈帧       | <--- 正在执行
| (执行完后弹出)            |
+-------------------------+
| main() 方法的栈帧         |
| (等待中)                 |
+-------------------------+

细节总结:

  • 方法调用就是压栈 ,方法结束就是弹栈
  • 每个方法都有自己的独立空间,方法内的局部变量互不影响。
  • 方法执行完,返回给调用者结果后,自己的生命就结束了。

3. 成员方法传参机制

Java的参数传递本质上只有一种:值传递。 但这个"值"的内容,对于基本类型和引用类型来说,意义不同。

3.1 基本数据类型的传参

传递的是具体的数值。方法内对形参的修改,不影响实参。

java 复制代码
public class Test {
    public void swap(int a, int b) { // a=10, b=20
        int temp = a;
        a = b;
        b = temp;
        System.out.println("swap内:a=" + a + ", b=" + b); // a=20, b=10
    }
    
    public static void main(String[] args) {
        Test t = new Test();
        int x = 10, y = 20;
        t.swap(x, y);
        System.out.println("main内:x=" + x + ", y=" + y); // x=10, y=20
    }
}

原理解析mainxy (10和20)复制了一份,传递给了 swap 方法。swap 方法修改的是自己栈帧里的副本,和 main 里的 x, y 无关。

3.2 引用数据类型的传参(重点)

传递的是内存地址的值。方法内通过这个地址修改了堆中的对象,那么所有指向该对象的引用都能看到这个变化。

java 复制代码
class Person {
    String name;
    int age;
}

public class Test {
    // 方法接收一个 Person 对象的引用(地址)
    public void changeAge(Person p) { // p 复制了 main 中的地址,比如 0x1234
        p.age = 100; // 通过地址 0x1234 找到对象,把 age 改成 100
        // p = null; // 如果加上这一行,只是把形参 p 的指向改了,和实参无关!
    }

    public static void main(String[] args) {
        Test t = new Test();
        Person person = new Person(); // 在堆中创建对象,地址假设为 0x1234
        person.age = 18;
        t.changeAge(person);
        System.out.println(person.age); // 输出 100,因为指向的是同一个对象
    }
}

传参机制总结:

  • 基本类型:传递数据值,形参修改不影响实参。
  • 引用类型 :传递地址值,形参和实参指向堆中同一个对象,通过形参修改对象属性,实参能看到变化。
  • 特别提醒 :如果方法内让形参重新 new 了一个对象或者指向 nullp = new Person()p = null),那只是改变了形参这个"遥控器"的指向,和原来的对象无关,不影响实参

3.3 应用实例:克隆对象

利用引用类型传参,我们可以实现一个克隆对象的方法。

java 复制代码
class Person {
    String name;
    int age;

    // 编写一个方法,克隆当前对象
    public Person copyPerson() {
        // 1. 创建一个新的 Person 对象
        Person clone = new Person();
        // 2. 将当前对象(this)的属性赋值给新对象
        clone.name = this.name;
        clone.age = this.age;
        // 3. 返回新对象
        return clone;
    }
}

// 使用
Person p1 = new Person();
p1.name = "张三";
Person p2 = p1.copyPerson(); // p2 是一个全新的对象,但内容与 p1 一样
System.out.println(p2.name); // 输出:张三

此时 p1 == p2false,因为它们在堆中是两块不同的内存。

4. 方法重载

4.1 什么是重载?

在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。

java 复制代码
public class Calculator {
    // 求两个整数的和
    public int sum(int a, int b) {
        return a + b;
    }
    // 重载:求三个整数的和(参数个数不同)
    public int sum(int a, int b, int c) {
        return a + b + c;
    }
    // 重载:求两个小数的和(参数类型不同)
    public double sum(double a, double b) {
        return a + b;
    }
}

4.2 重载的好处

  • 减少方法名的数量 :不用为了类似的功能起多个名字(比如 sumInt, sumDouble, sumThreeInt)。
  • 便于调用:调用者只需要记住一个方法名,传入不同的参数即可。

4.3 重载的细节

  • 必须满足:方法名相同参数列表不同(类型、个数、顺序至少有一项不同)。
  • 返回值类型无关:仅仅是返回值不同,不足以构成重载(编译器无法区分该调用哪个)。
  • 形参名称无关sum(int a, int b)sum(int x, int y) 是一样的。

5. 递归

5.1 什么是递归?

递归就是方法自己调用自己。递归能解决很多复杂的问题,让代码变得极其简洁,比如著名的"斐波那契数列"、"汉诺塔"等。

5.2 递归的规则

一个正确的递归必须包含两部分:

  1. 递归出口 :什么时候停下来(结束条件)。没有出口就是死循环,最终导致栈溢出。
  2. 递归规则:如何把原问题分解成更小的、解决方法相同的子问题。

5.3 练习:用递归求阶乘

n! (n的阶乘) = n * (n-1) * ... * 1

java 复制代码
public class Factorial {
    public static int factorial(int n) {
        // 递归出口:1! = 1
        if (n == 1) {
            return 1;
        }
        // 递归规则:n! = n * (n-1)!
        return n * factorial(n - 1);
    }

    public static void main(String[] args) {
        System.out.println(factorial(5)); // 输出 120
    }
}

执行过程分析(递推与回溯):

  1. factorial(5) 执行,发现 n != 1,执行 5 * factorial(4),此时 factorial(5) 暂停 ,等待 factorial(4) 的结果。
  2. factorial(4) 执行,需要 4 * factorial(3)暂停,等待。
  3. factorial(3) 执行,需要 3 * factorial(2)暂停,等待。
  4. factorial(2) 执行,需要 2 * factorial(1)暂停,等待。
  5. factorial(1) 执行,碰到出口 return 1
  6. 回溯开始factorial(2) 得到结果 2 * 1 = 2,返回给上一级。
  7. factorial(3) 得到结果 3 * 2 = 6,返回。
  8. factorial(4) 得到结果 4 * 6 = 24,返回。
  9. factorial(5) 得到最终结果 5 * 24 = 120

6. 可变参数

6.1 概念与语法

当我们需要定义一个方法,但不确定调用者会传入多少个参数时,就可以使用可变参数。

语法:方法名(参数类型... 形参名)

java 复制代码
public class VarArgs {
    // 计算任意数量整数的和
    public static int sum(int... numbers) {
        int total = 0;
        // 在方法内部,numbers 被当作数组处理
        for (int n : numbers) {
            total += n;
        }
        return total;
    }

    public static void main(String[] args) {
        System.out.println(sum(1, 2));       // 输出 3
        System.out.println(sum(1, 2, 3, 4)); // 输出 10
        System.out.println(sum());            // 输出 0,可以传0个参数
    }
}

6.2 细节注意

  • 一个方法最多只能有一个可变参数
  • 可变参数必须是方法的最后一个参数 。例如:public void test(String name, double... scores) { } 是正确的。
  • 本质上,可变参数就是一个数组,所以调用时可以传数组,也可以直接罗列元素。

7. 作用域

7.1 局部变量与成员变量

  • 成员变量 :定义在类里面、方法外面的变量。作用域在整个类内部,有默认初始值(如 int 默认0,String 默认 null)。
  • 局部变量 :定义在方法里面、或者方法的参数列表中的变量。作用域只在它所在的花括号 {} 内,没有默认初始值,使用前必须赋值,否则编译报错。
java 复制代码
class ScopeTest {
    int age; // 成员变量,作用域整个类,默认值0

    public void method1() {
        int num = 10; // 局部变量,作用域仅在 method1 内
        System.out.println(age); // 可以访问成员变量
        System.out.println(num);
    }

    public void method2() {
        // System.out.println(num); // 报错!无法访问 method1 的局部变量
        for (int i = 0; i < 3; i++) { // i 的作用域仅在这个 for 循环内
            // ...
        }
        // System.out.println(i); // 报错! i 已经出了作用域
    }
}

7.2 注意事项

  • **属性(成员变量)**的生命周期长,随着对象的创建而创建,随着对象的销毁而销毁。
  • 局部变量的生命周期短,随着方法的调用而创建,随着方法结束(弹栈)而销毁。
  • 当成员变量和局部变量重名 时,在方法内部默认遵循"就近原则 ",访问的是局部变量。如果想访问成员变量,需要使用 this 关键字。

8. 构造器

8.1 什么是构造器?

构造器也叫构造方法,是创建对象时自动调用的特殊方法 ,主要作用是完成对象的初始化工作(给属性赋初值)。

8.2 构造器的特点与注意事项

  1. 方法名必须和类名完全一致
  2. 没有返回值类型 ,连 void 也不能写。
  3. 当你没有定义任何构造器时,系统会默认提供一个无参构造器 (比如 Person(){})。
  4. 一旦你显式地定义了一个有参(或无参)构造器,系统就不再提供默认的无参构造器了。
java 复制代码
class Student {
    String name;
    int age;

    // 无参构造器
    public Student() {
        System.out.println("Student对象被创建了!");
    }

    // 有参构造器(重载)
    public Student(String sName, int sAge) {
        name = sName;
        age = sAge;
    }
}

// 使用
Student s1 = new Student(); // 调用无参构造器
Student s2 = new Student("王五", 22); // 调用有参构造器

9. 深入理解 this

9.1 this 是什么?

this 关键字,代表当前对象的引用 。哪个对象调用了方法,方法里的 this 就代表谁。

this 就像是每个人说话时的"我"。张三说话时,"我"指张三;李四说话时,"我"指李四。

9.2 this 的核心用途

  1. 区分重名的成员变量和局部变量 :这是最常见的用法。

    java 复制代码
    class Teacher {
        String name;
        public Teacher(String name) {
            // 左边的 name 是成员变量,右边的 name 是形参(局部变量)
            this.name = name; // 必须用 this 来指定给成员变量赋值
        }
    }
  2. 调用本类的其他构造器 :可以在一个构造器中调用另一个构造器,避免代码重复。必须放在构造器的第一行

    java 复制代码
    class Employee {
        String name;
        int id;
        
        public Employee() {
            this("匿名", 0); // 调用下面的有参构造器
            System.out.println("无参构造器");
        }
        
        public Employee(String name, int id) {
            this.name = name;
            this.id = id;
        }
    }
  3. 作为参数传递 :可以将 this 作为参数传递给其他方法,告诉方法当前是哪个对象在调用。

10. 对象创建流程分析

通过以上学习,我们现在可以完整地梳理一下,当写下 Person p = new Person("张三", 18); 时,JVM 在背后都干了些什么:

  1. 加载类信息 :JVM 先在方法区中查找 Person 类的信息。如果没有,先加载 Person.class 文件,将类的信息(成员变量定义、方法定义等)存入方法区。
  2. 栈中声明引用 :在 main 方法的栈帧中,为 p 变量分配空间。
  3. 堆中分配内存 :执行 new,在堆中为 Person 对象分配内存空间。这个空间包含了对象头和所有实例变量(此时变量是默认值,如 name=null, age=0)。
  4. 初始化默认值:将堆中分配到的内存空间初始化为零值(确保不出现随机值)。
  5. 显式初始化 :执行在类定义中属性直接赋值的操作(如果有 int age = 10,这一步会把 age 从0改为10)。
  6. 执行构造器 :调用构造器。如果构造器第一行有 this(...),则先调用另一个构造器;否则执行构造器内的代码,将 "张三"18 赋值给属性。
  7. 返回地址 :构造器执行完毕,将堆中对象的地址返回给栈中的引用变量 p

至此,一个完整可用的对象才算是创建完成。

动手实践

  1. 类的设计与对象创建 定义一个 Dog 类,包含属性 name(字符串)、age(整数)、color(字符串),以及一个方法 show() 用于打印狗狗的信息。然后在 main 方法中创建两个不同的 Dog 对象,并调用它们的 show() 方法。
  2. 方法重载------计算器升级 在 Calculator 类中,为 add 方法实现多个重载版本,分别支持:
  • 两个 int 相加
  • 两个 double 相加
  • 三个 int 相加
  • 一个 int 和一个 double 相加(顺序可变)
  1. 递归------斐波那契数列。 斐波那契数列的定义是:第1项为1,第2项为1,从第3项起,每一项等于前两项之和。即 f(1)=1,f(2)=1,f(n)=f(n-1)+f(n-2) (n≥3)。请用递归方法编写一个函数 int fib(int n),返回第 n 项的值。并在 main 中打印前10项
  2. 可变参数------求任意数量数字的最大值 定义一个方法 max,接收可变参数的 int 数字,返回其中的最大值。如果调用时不传任何参数,则返回 Integer.MIN_VALUE。
  3. 构造器与 this 的使用 定义一个 Book 类,属性包括 title(书名)、price(价格)。要求:
  • 提供无参构造器,在其中调用有参构造器,设置默认书名为"未知",价格为0.0。
  • 提供有参构造器,使用 this 为属性赋值。
  • 提供一个 setPrice 方法,如果传入的价格小于0,则打印错误信息,否则修改价格。
  • main 中测试不同方式创建对象,并调用 setPrice 方法。

动手实践:零基础学Java | 面向对象编程的类与对象(基础)


如果在练习中遇到问题,欢迎在评论区留言。我们下期见! 🚀

相关推荐
神奇小汤圆2 小时前
架构师手记:彻底终结 Kafka 丢消息与重复消费的“核武器”
后端
明月_清风3 小时前
Python 内存手术刀:sys.getrefcount 与引用计数的生死时速
后端·python
明月_清风3 小时前
Python 消失的内存:为什么 list=[] 是新手最容易踩的“毒苹果”?
后端·python
IT_陈寒17 小时前
Python开发者必知的5大性能陷阱:90%的人都踩过的坑!
前端·人工智能·后端
流浪克拉玛依17 小时前
Go Web 服务限流器实战:从原理到压测验证 --使用 Gin 框架 + Uber Ratelimit / 官方限流器,并通过 Vegeta 进行性能剖析
后端
孟沐17 小时前
保姆级教程:手写三层架构 vs MyBatis-Plus
后端
星浩AI17 小时前
让模型自己写 Skills——从素材到自动生成工作流
人工智能·后端·agent
华仔啊20 小时前
为啥不用 MP 的 saveOrUpdateBatch?MySQL 一条 SQL 批量增改才是最优解
java·后端
武子康20 小时前
大数据-242 离线数仓 - DataX 实战:MySQL 全量/增量导入 HDFS + Hive 分区(离线数仓 ODS
大数据·后端·apache hive