面向对象基础篇

Object Oriented Programming - Basic

从面向过程到面向对象的编程思维转变

引言

所谓视界不是眼球所看到的画像,是大脑所理解的画像。

我们的视野被我们的常识所保护着,人是无法脱离自己的局限而活下去的

……本来的话。

——苍崎橙子《空之境界》
cover2

从面向过程到面向对象

在前面的学习中,我们已经掌握了面向过程编程的基础知识,能够独立编写简单的程序。

接下来我们将深入学习面向对象编程(Object Oriented Programming),这是Java语言的核心特性之一。

  • 面向对象是现代高级编程语言的重要特征
  • 面向对象概念对于初学者而言是一个重要的学习节点
  • 部分学习者能够快速理解,而另一些可能需要更多时间来掌握
  • 本章内容难度有所提升,建议认真学习和实践

熟练掌握面向对象编程是成为优秀Java开发者的关键一步。

什么是类与对象?

我们在日常生活中其实经常听到"类"这个词。比如说人类、鸟类、鱼类等等。

类(Class)

对一类东西的概括,是一种抽象的概念。比如鸟类,指的是所有具备鸟的特点的动物。

对象(Object)

这类事物中的每一个具体存在的个体,也叫实例。我们每个人都是人类这个大类别下的一个具体的例子。

Person 类

姓名: ?
年龄: ?
职业: ?
说话()
工作()
实例化

张三

姓名: 张三
年龄: 25
职业: 程序员

李四

姓名: 李四
年龄: 30
职业: 设计师

类与对象的禅

师父:什么是美?
弟子:花是美的。(花这个类)
师父:花只是概念,真正的美在哪里?
弟子:在那朵正在绽放的莲花中。(具体的莲花对象)
师父:悟了。类是万物之理,对象是万物之形。

类就是抽象概念的人,而对象,就是具体的某一个人。

在Java中,我们可以定义一个类,然后进一步创建许多这个类的实例对象,让所有的功能都通过对象之间的交互来表达。

这种编程方式,我们称为面向对象编程。

面向过程编程

// 函数调用方式
eat(猪八戒, 西瓜);

将数据和操作分离,通过函数调用来实现功能

面向对象编程

// 对象方法调用方式
猪八戒.eat(西瓜);

将数据和操作封装在对象中,通过对象间交互实现功能

类的定义与对象创建

接下来我们将学习如何定义一个类。以定义人类为例,演示类的创建过程:

步骤1:创建类文件

在src目录上右键,选择创建新的Java类,命名为Person

  • 类名通常使用英文单词
  • 首字母必须大写
  • 不能包含特殊字符

步骤2:基本类结构

class Person {
    
}

这与Main类的结构相同,Main本身也是一个类。

添加类的属性 (成员变量)

既然是"人类",那肯定要有与人相关的属性,比如名字、性别、年龄等。

我们可以把这些属性直接定义为类的成员变量。

public class Person {   
    // 这里定义的人类具有三个属性:名字、年龄、性别
    String name;   // 直接在类中定义变量,表示类具有的属性
    int age;
    String sex;
}

重要概念

成员变量就是这个类拥有的属性,每个实例创建出来后,这些属性的值可以各不相同。

这些变量只有在一个具体的对象中才可以使用。

创建对象

现在人类的属性都规定好了,我们就可以尝试创建一个实例对象了。

语法格式

new 类名();

实际例子

public static void main(String[] args) {
    new Person();   // 使用new关键字来创建某个类的对象
                   // 注意new后面需要跟上 类名()
                   // 这里创建出来的,就是一个具体的人了
}

整个流程为:定义类 → 创建对象 → 使用对象

对象的使用

现在我们知道了怎么创建对象,那接下来怎么去使用这个对象呢?

我们可以用一个变量来代表这个对象:

public static void main(String[] args) {
    // 这里的a存放的是具体的某个值
    int a = 10;
    
    // 创建一个变量指代我们刚刚创建好的对象
    // 变量的类型就是对应的类名
    // 这里的p存放的是对象的引用,而不是本体
    Person p = new Person();
}

重要概念:引用类型

这种变量存的是对象的"地址"或"引用",而不是直接存储对象本身。

因此,这个变量的类型也被称为"引用类型",而不再是"基本类型"(如int, double)。

对象引用的概念

为什么对象类型的变量存放的是对象的引用?看下面的例子:

public static void main(String[] args) {
    Person p1 = new Person();
    Person p2 = p1;  // 这只是传递了对象的引用
}

赋值之后,现在p2和p1都指向同一个对象(如果你学过C语言,这就有点像指针的概念)。

基本类型内存模型

栈内存 (Stack)
int a
10
int b
10
int a = 10;
int b = a;

引用类型内存模型

栈内存 (Stack)
Person p1
@0x1001
Person p2
@0x1001
堆内存 (Heap)
@0x1001
name: null
age: 0
Person p1 = new Person();
Person p2 = p1;
1. 创建基本类型变量
2. 基本类型赋值(值拷贝)
3. 创建对象(堆内存分配)
4. 引用赋值(地址拷贝)

测试验证

public static void main(String[] args) {
    Person p1 = new Person();
    Person p2 = p1;
    System.out.println(p1 == p2);  // 使用 == 判断是否引用同一个对象
    // 结果:true
    
    Person p3 = new Person();  // 创建新对象
    System.out.println(p1 == p3);  // 不同对象的引用
    // 结果:false
}

访问对象属性

现在我们有了对象的引用之后,我们就可以进行"."操作了:

public static void main(String[] args) {
    Person p = new Person();
    p.name = "小明";   // 要访问对象的属性,使用 . 运算符
    System.out.println(p.name);   // 打印对象的名字
}

重要特性

不同对象的属性是分开独立存放的,每个对象都有一个自己的空间,修改一个对象的属性并不会影响到其他对象:

Person p1 = new Person();
Person p2 = new Person();
p1.name = "小明";   // 修改第一个对象的属性
p2.name = "大明";   // 修改第二个对象的属性
System.out.println(p1.name);  // 输出:小明

空引用(null)

关于对象类型的变量,我们也可以不对任何对象进行引用:

public static void main(String[] args) {
    Person p1 = null;  // null是一个特殊的值,表示空,不引用任何对象
}
NullPointerException

⚠️ 空指针异常

若某一变量未引用任何对象,则不应通过该变量对对象进行操作:

public static void main(String[] args) {
    Person p = null;   // 此时变量没有引用任何对象
    p.name = "小红";   // ❌ 这会导致空指针异常!
    System.out.println(p.name);
}

这是Java中出现频率最高的异常,一定要注意避免!

对象属性的默认值

对象创建成功之后,它的属性没有进行赋值,但是能否直接访问呢?

public static void main(String[] args) {
    Person p = new Person();
    System.out.println("name = " + p.name);
    System.out.println("age = " + p.age);
    System.out.println("sex = " + p.sex);
}

运行结果

name = null
age = 0
sex = null

默认值规则

  • 基本类型:默认为0(boolean默认为false)
  • 引用类型:默认为null

添加类的功能(成员方法)

光有属性还不够,对象还需要能做一些事情,就像人可以走路、跳跃、思考一样。

对象也可以做各种动作,这可以通过定义方法来实现(在C语言中叫做函数)。

什么是方法?

方法就是一组指令,用来完成特定的任务。

  • 有的任务完成后需要返回结果,比如计算两个数字的和
  • 有的任务只需要执行操作而不需要返回结果,比如把数字打印到屏幕上

方法定义语法

返回值类型 方法名称(参数列表) {
    方法体...
}

案例:自我介绍的方法

比如我们现在给人类编写一个自我介绍的行为:

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

    // 自我介绍只需要完成就行,没有返回值,所以使用void
    void hello(){
        // 完成自我介绍需要执行的所有代码就在这个花括号中编写
        // 自我介绍需要用到当前对象的名字和年龄
        // 我们直接使用成员变量即可,变量的值就是当前对象的存放值
        System.out.println("我叫 " + name + " 今年 " + age + " 岁了!");
    }
}

方法调用

public static void main(String[] args) {
    Person p = new Person();
    p.name = "小明";
    p.age = 18;
    p.hello();    // 使用 . 运算符调用方法,格式:.方法名称()
}

带参数的方法

比如现在我们要让人类学会加法运算,需要别人提供两个参与加法运算的值:

// 我们可以为方法设定参数,在调用方法时,需要外部传入参数
// 参数的定义需要在小括号内部编写,类似于变量定义
// 需要填写 类型和参数名称,多个参数用逗号隔开
int sum(int a, int b){   // 这里需要两个int类型的参数进行计算
    int c = a + b;   // 参数相当于在函数中定义了局部变量
    return c;   // return后面紧跟需要返回的结果
    // 带返回值的方法,是一定要有一个返回结果的!
}

调用带参数的方法

public static void main(String[] args) {
    Person p = new Person();
    p.name = "小明";
    p.age = 18;
    int result = p.sum(10, 20);    // 让这个对象帮我们计算10 + 20
    System.out.println(result);    // 成功得到30
}

return关键字

关于return关键字,我们需要进行进一步的介绍:

1. 方法立即结束

在使用return关键字之后,方法就会直接结束并返回结果,所以在这之后编写的任何代码,都是不可到达的。

int test() {
    return 10;
    System.out.println("这行代码永远不会执行"); // ❌ 编译错误
}

2. 分支必须都有返回值

如果程序中出现了分支语句,那么必须保证每一个分支都有返回值:

int test(int a) {
    if (a > 0) {
        return 1;
    } else {
        return -1;  // 必须保证所有分支都有返回值
    }
}

方法相互调用

当然,方法之间是可以相互调用的:

void test(){
    System.out.println("我是test");   //实际上这里也是调用另一个方法
}

void say(){
    test();   //在一个方法内调用另一个方法
}

如果我们这样写的话:

void test(){
    say();
}

void say(){
    test();
}

各位猜猜看会出现什么情况?

此时又出现了一个我们不认识的异常,实际上什么原因导致的我们自己都很清楚,方法之间一直在相互调用,没有一个出口。

构造方法

我们接着来看一种比较特殊的方法,构造方法。

我们前面创建对象,都是直接使用new关键字就能直接搞定了,但是我们发现,对象在创建之后,各种属性都是默认值,那么能否实现在对象创建时就为其指定名字、年龄、性别呢?要在对象创建时进行处理,我们可以使用构造方法(构造器)来完成。

实际上每个类都有一个默认的构造方法:

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

    public Person() {}   //之前我们没有定义这个方法,但是其实编译器会自动添加一个无参构造方法
    
}

构造方法不需要填写返回值,并且方法名称与类名相同,默认情况下每个类都会自带一个没有任何参数的无参构造方法(只是不用我们去写,编译出来就自带)当然,我们也可以手动声明,对其进行修改:

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

    Person(){    //构造方法不需要指定返回值,并且方法名称与类名相同
        name = "小明";   //构造方法会在对象创建时执行,我们可以将各种需要初始化的操作都在这里进行处理
        age = 18;
        sex = "男";
    }
}

构造方法调用

构造方法会在new的时候自动执行:

public static void main(String[] args) {
    Person p = new Person();   //这里的new Person()其实就是在调用无参构造方法
    System.out.println(p.name);
}

当然,我们也可以为构造方法设定参数(一旦自定义,将不会自动添加无参构造):

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

    Person(String name, int age, String sex){   //跟普通方法是一样的
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

注意,在我们自己定义一个构造方法之后,会覆盖掉默认的那一个无参构造方法,除非我们手动重载一个无参构造,否则要创建这个类的对象,必须调用我们自己定义的构造方法:

public static void main(String[] args) {
    Person p = new Person("小明", 18, "男");   //调用自己定义的带三个参数的构造方法
    System.out.println(p.name);
}

成员变量初始化

当然,要给成员变量设定初始值,我们不仅可以通过构造方法,也可以直接在定义时赋值:

public class Person {
    String name = "未知";   //直接赋值,那么对象构造好之后,属性默认就是这个值
    int age = 10;
    String sex = "男";
}

这里需要特别注意,成员变量的初始化,并不是在构造方法之前之后,而是在这之前就已经完成了:

Person(String name, int age, String sex){
    System.out.println(age);    //在赋值之前看看是否有初始值
    this.name = name;
    this.age = age;
    this.sex = sex;
}

代码块

我们也可以在类中添加代码块,代码块同样会在对象构造之前进行,在成员变量初始化之后执行:

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

    {
        System.out.println("我是代码块");   //代码块中的内容会在对象创建时仅执行一次
    }

    Person(String name, int age, String sex){
        System.out.println("我被构造了");
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

只不过一般情况下使用代码块的频率比较低,标准情况下还是通过构造方法进行进行对象初始化工作,所以说这里做了解就行了。

类的加载时机

JVM在需要时才会加载类(懒加载)。以下情况会触发类加载:

  • 访问类的静态变量,或为静态变量赋值
  • 创建类的实例 (new)
  • 调用类的静态方法
  • 初始化子类时(会先初始化父类)
  • (反射操作等)

有关类与对象的基本内容,我们就全部讲解完毕了。

用package组织代码

通过前面的学习,我们知道该如何创建和使用类。现在我们来学习如何组织和管理这些类。

包声明和导入

包(Package)用于组织类,类似于文件夹。它可以避免类名冲突,并更好地管理代码结构。

包的命名规则通常是反向的域名,例如 com.baidu。

package com.test; // 将当前类放入com.test包中

public class Main {
    public static void main(String[] args) {
        // ...
    }
}

要使用其他包中的类,需要使用 import 关键字导入。

import com.test.entity.Person; // 导入Person类
import com.test.entity.*;      // 导入entity包下的所有类

public class Main {
    public static void main(String[] args) {
        Person person = new Person();
    }
}

Java会默认导入 java.lang 包下的所有类,因此像 String 和 System 这样的类无需手动导入。

本章总结

核心概念

  • 类是抽象的概念,对象是具体的实例
  • 面向对象编程通过对象交互来实现功能
  • 引用类型变量存储的是对象的引用

重要语法

  • 类定义:public class ClassName { }
  • 对象创建:new ClassName()
  • 属性访问:object.property
  • 方法调用:object.method()

注意事项

  • 避免空指针异常
  • 理解值传递机制
  • 方法必须有正确的返回值
  • 对象属性有默认值

掌握了类与对象的基本概念,你就迈出了面向对象编程的第一步!

实战练习

理论学习后,通过编码实践来巩固知识至关重要。以下是一些经典的算法问题,请尝试自己实现。

冒泡排序算法

将一个乱序的整数数组按照从小到大的顺序排列。

二分搜索算法

在一个有序数组中查找指定目标值,并返回其下标。

青蛙跳台阶问题

计算一只青蛙跳上n级台阶(每次可跳1或2阶)共有多少种方案。

回文串判断

判断一个字符串是否正读和反读都一样。

汉诺塔求解

输出解决N阶汉诺塔问题的每一步移动操作。

这些练习不仅能加深对Java语法的理解,还能锻炼逻辑思维和解决问题的能力。

1 / X