在之前的编程学习中,我们主要使用基本数据类型和简单的方法来编写程序。虽然这种方式也能解决问题,但当程序变得复杂时,我们会发现代码变得难以管理和维护。
假设我们要描述一辆汽车,我们需要考虑很多方面:品牌、型号、颜色、发动机、轮胎、座椅等。如果我们要在程序中表示这些信息,仅仅使用简单的变量是不够的。我们需要一种更强大的方式来组织和管理这些相关的数据。
这就是面向对象编程的核心思想。在面向对象编程中,我们将现实世界中的事物抽象为程序中的对象,每个对象都有自己的属性和行为。

对象是软件中的一个组件,它在内存中占有一块空间,并且在程序运行时可以用来表示现实世界中的某个事物或概念。每个对象都包含两部分内容:
对象的本质是将数据和操作数据的方法封装在一起,使得程序结构更加清晰、易于维护。
每个对象都有两种基本能力:
类就像是对象的“蓝图”或“模板”。它描述了某种类型对象应该具有什么样的属性和行为。
想象一下建筑蓝图。蓝图本身不是房子,而是房子的详细描述。当我们使用蓝图建造实际的房子时,我们就是在创建蓝图所描述房子的一个实例。
如果我们愿意,我们可以用同一个蓝图建造几座相同的房子,每座房子都是蓝图所描述房子的一个实例。 在编程中,类就是对象的蓝图。当我们创建对象时,我们就是在创建类的一个实例。
到目前为止,我们在程序中使用的对象都是来自Java API的类创建的。例如:
让我们看一个使用这些对象的例子:
|import java.util.Scanner; import java.util.Random; import java.io.*; public class ObjectDemo { public static void main(
运行这个程序会输出:
|要生成多少个随机数?5 完成
这个程序创建了三个对象:
每个对象都有自己的方法和功能,程序通过调用这些对象的方法来完成特定的任务。
在Java中,我们有两种主要的数据类型:
基本类型变量是计算机内存中的简单存储位置。当你声明一个基本类型变量时,编译器会为该变量分配足够的内存空间。
例如:
|int wholeNumber; double realNumber;
这些声明语句会在内存中分配空间:
创建对象需要两个步骤:
例如:
|Random rand = new Random();
让我们分解这个语句:
Random rand:声明一个名为rand的变量,它可以引用Random类型的对象new Random():new操作符在内存中创建一个对象,并返回该对象的内存地址=:将new操作符返回的内存地址赋给rand变量引用变量不直接存储数据,而是存储对象的内存地址。我们可以把这种关系比作风筝和线轴:
当你想要操作对象时,你使用引用变量来访问对象。
Java API(应用程序编程接口)为开发者提供了大量已经编写好的类,这些类可以直接在你的程序中使用,极大地提高了开发效率。例如,String类用于处理字符串,Scanner类用于接收用户输入,ArrayList类用于动态数组操作,等等。
然而,在实际开发过程中,你经常会遇到一些特定的需求,这些需求并没有现成的类可以直接满足。例如,你可能需要表示一个“学生”、一个“图书”、一个“订单”或者一个“几何图形”等等。这时,Java标准库中并没有为你的具体业务场景量身定制的类。
这并不是问题,因为Java是一门面向对象的编程语言,允许你根据实际需求自定义类。你可以自己设计类的结构,定义它包含哪些属性(字段)、能做哪些操作(方法),从而创建出适合你项目的对象。例如,如果你需要表示一个矩形,可以自己编写一个Rectangle类,包含长度、宽度等字段,以及计算面积、设置尺寸等方法。
通过自定义类,你可以:
总之,当Java API中没有你需要的类时,你完全可以根据自己的需求编写类,为任何场景创建具有特定属性和功能的对象。这是Java编程的核心能力之一,也是面向对象思想的体现。
让我们通过编写一个Rectangle类来学习如何创建自己的类。这个类将表示矩形,具有以下特性:
字段:
方法:
|public class Rectangle { private double length; private double width; }
这个类定义了两个私有(private)字段:length(长度)和width(宽度)。所谓“私有字段”,是指它们只能在Rectangle类的内部被访问和修改,外部代码无法直接访问这两个变量。这种做法称为“数据封装”或“数据隐藏”,它的好处是可以防止外部代码随意更改对象的内部状态,从而保护数据的完整性和安全性。只有通过类中提供的公有(public)方法(如setter和getter方法),外部代码才能间接地读取或修改这些字段的值,这样可以在方法中加入必要的校验或逻辑,进一步增强代码的健壮性。
|public class Rectangle { private double length; private double width; /** * setLength方法在length字段中存储值 * @param len 要存储在length中的值 */ public void setLength(double len) { length = len; } /** * setWidth方法在width字段中存储值 * @param w 要存储在width中的值
|public class Rectangle { private double length; private double width; /** * setLength方法在length字段中存储值 * @param len 要存储在length中的值 */ public void setLength(double len) { length = len; } /** * setWidth方法在width字段中存储值 * @param w 要存储在width中的值
|public class Rectangle { private double length; private double width; /** * setLength方法在length字段中存储值 * @param len 要存储在length中的值 */ public void setLength(double len) { length = len; } /** * setWidth方法在width字段中存储值 * @param w 要存储在width中的值
现在让我们创建一个程序来测试Rectangle类:
|public class RectangleDemo { public static void main(String[] args) { // 创建Rectangle对象 Rectangle box = new Rectangle(); // 设置长度为10.0,宽度为20.0 box.setLength(10.0); box.setWidth(20.0); // 显示长度 System.out.println("矩形的长度是:" +
运行这个程序会输出:
|矩形的长度是:10.0 矩形的宽度是:20.0 矩形的面积是:200.0

在面向对象编程中,数据隐藏是一个重要概念。对象将其内部数据隐藏起来,不让类外部的代码直接访问。只有类的方法才能直接访问和修改对象的内部数据。
你通过将类的字段设为私有(private),将访问这些字段的方法设为公有(public)来隐藏对象的内部数据。
例如:
|public class Rectangle { private double length; private double width; public double getLength() { return length; } public void setLength(double length) { this.length = length; } }
在Rectangle类中:
在Rectangle类中,getLength和getWidth方法返回存储在字段中的值,但getArea方法返回计算结果。你可能会想为什么矩形的面积不存储在字段中,就像长度和宽度一样。
面积不存储在字段中是因为它可能会过时。当一个项目的值依赖于其他数据,而该项目在其他数据改变时没有更新,就说该项目已经过时了。
如果矩形的面积存储在字段中,那么当长度或宽度字段改变时,该字段的值就会变得不正确。
在设计类时,你应该注意不要在字段中存储可能会过时的计算数据。相反,应该提供一个返回计算结果的方法。
每当你根据某个类创建一个对象(实例)时,这个对象都会拥有属于自己的那一份字段,这些字段被称为“实例字段”。实例字段是存储在每个对象内部的数据副本。 也就是说,即使多个对象都是同一个类创建的,它们的实例字段也是彼此独立、互不影响的。每个对象都可以在自己的实例字段中保存不同的值,这样每个对象就能拥有属于自己的状态。
例如,如果你用Rectangle类创建了两个矩形对象,每个对象都有自己的length和width字段,分别记录各自的长度和宽度。
让我们看一个例子,创建多个Rectangle对象:
|import javax.swing.JOptionPane; public class RoomAreas { public static void main(String[] args) { double number; // 保存数字 double totalArea; // 总面积 String input; // 保存用户输入 // 创建三个Rectangle对象 Rectangle kitchen = new Rectangle(); Rectangle bedroom = new Rectangle(); Rectangle den =
这个程序创建了三个Rectangle对象:kitchen、bedroom和den。每个对象都有自己的length和width字段,可以存储不同的值。
在Java中,实例方法(Instance Method)是指那些必须通过类的对象(实例)来调用的方法。实例方法通常用于操作该对象的属性或实现与该对象相关的功能。每个对象都有自己独立的数据,实例方法在被调用时会自动作用于调用它的那个对象。
以Rectangle类为例,类中定义的setLength、setWidth、getLength、getWidth和getArea等方法,都是实例方法。它们只能通过Rectangle类的具体对象来调用,并且每次调用时,方法内部操作的都是当前对象(即调用者)的字段。例如,kitchen.setLength(number);会把number的值存储到kitchen对象的length字段中,而bedroom.setLength(number);则会把值存储到bedroom对象的length字段中。这样,每个对象都能独立维护自己的状态。
实例方法是与对象实例相关联的方法,必须通过对象来调用,能够访问和修改该对象的属性。
例如,当程序执行以下语句时:
|kitchen.setLength(number);
这个语句调用setLength方法,将值存储在kitchen对象的length字段中。
当程序执行以下语句时:
|bedroom.setLength(number);
这个语句也调用setLength方法,但这次是将值存储在bedroom对象的length字段中。
构造函数(Constructor)是一个特殊的方法,它在使用new关键字创建类的对象时被自动调用。构造函数的主要作用是对新创建的对象进行初始化操作,比如为实例字段(成员变量)赋初值、执行必要的准备工作等。每当你通过new创建一个对象时,都会自动执行该类的构造函数,从而确保对象的状态是有效的。
构造函数有以下几个特点:
Rectangle类的构造函数名就必须是Rectangle。void也不能写。举例说明:如果你有一个Rectangle类,那么它的构造函数应该写成如下形式:
让我们为Rectangle类添加一个构造函数:
|public class Rectangle { private double length; private double width; /** * 构造函数 * @param len 矩形的长度 * @param w 矩形的宽度 */ public Rectangle(double len, double w) { length = len; width = w; }
这个构造函数接受两个参数,并将它们分别赋给length和width字段。
现在我们可以这样创建Rectangle对象:
|Rectangle box = new Rectangle(7.0, 14.0);
这个语句创建一个Rectangle对象,并将其地址赋给box变量。构造函数会将length字段设置为7.0,将width字段设置为14.0。
如果你没有为类编写构造函数,Java会在编译类时自动提供一个。Java提供的构造函数称为默认构造函数。默认构造函数不接受参数,它将对象的所有数字字段设置为0,布尔字段设置为false。
你也可以编写自己的无参构造函数:
|public Rectangle() { length = 1.0; width = 1.0; }
如果使用这个构造函数,创建Rectangle对象时就不需要传递参数:
|Rectangle r = new Rectangle(); // 调用无参构造函数
在开发面向对象的应用程序时,很多情况下我们需要让方法能够接收对象作为参数。这样做的好处是,方法不仅可以操作基本数据类型,还能直接访问和操作对象的属性和方法,从而实现更强大和灵活的功能。
当你将一个对象作为参数传递给方法时,实际上传递的是该对象在内存中的引用(即内存地址),而不是对象本身的副本。也就是说,方法参数变量会指向同一个对象实例。这样,方法内部对该对象属性的修改,会影响到原始对象。
举个例子,假设有如下方法:
为了更好地理解如何将对象作为参数传递给方法,我们来设计一个具体的案例:一个简单的骰子游戏。在这个游戏中,我们将定义三个类:
通过这个例子,你将看到如何在方法中接收和操作对象参数,以及对象之间如何协作完成一个完整的功能。
首先,让我们创建Die类:
|import java.util.Random; /** * Die类模拟一个骰子 */ public class Die { private int sides; // 骰子的面数 private int value; // 骰子的值 /** * 构造函数执行骰子的初始投掷 * @param numSides 这个骰子的面数 */ public Die(int numSides) { sides = numSides; roll();
现在让我们创建一个程序来演示如何将Die对象作为参数传递:
|public class DieArgument { public static void main(String[] args) { final int SIX_SIDES = 6; final int TWENTY_SIDES = 20; // 创建一个6面骰子 Die sixDie = new Die(SIX_SIDES); // 创建一个20面骰子 Die twentyDie = new Die(TWENTY_SIDES);
运行这个程序会输出:
|投掷一个6面骰子。 骰子的值:3 投掷一个20面骰子。 骰子的值:19
方法重载(Overloading)是面向对象编程中非常重要且常用的特性。它允许在同一个类中定义多个同名的方法,但这些方法的参数列表(参数的个数、类型或顺序)必须不同。 这样,虽然方法名相同,但根据传入的参数不同,Java编译器会自动选择合适的方法进行调用。
方法重载的意义在于:有时候我们希望对同一类操作提供多种不同的实现方式。例如,可能需要既能传递整数参数,也能传递浮点数参数,或者有时需要传递两个参数,有时只需要一个参数。 通过方法重载,我们可以让代码更加灵活、易读,并且便于维护。
两个或多个方法在同一个类中可以有相同的名称,只要它们的参数列表不同。这也适用于构造函数。
Java使用方法签名来区分同名的方法。方法签名由方法名称和参数的数据类型组成,按它们出现的顺序排列。
Rectangle类可以有多个构造函数:
|public class Rectangle { private double length; private double width; /** * 无参构造函数 */ public Rectangle() { length = 0.0; width = 0.0; } /** * 带参数的构造函数 * @param len 长度 * @param w 宽度
现在我们可以用不同的方式创建Rectangle对象:
|Rectangle box1 = new Rectangle(); // 调用无参构造函数 Rectangle box2 = new Rectangle(5.0, 10.0); // 调用带参数的构造函数
下面我们通过一个更为详细和复杂的例子来理解方法重载:BankAccount(银行账户)类。这个类不仅展示了构造函数的重载,还演示了同名方法(如deposit和withdraw)的重载用法。 通过不同的参数类型和数量,BankAccount类可以灵活地处理多种初始化和账户操作场景。
|public class BankAccount { private double balance; // 账户余额 /** * 这个构造函数将起始余额设置为0.0 */ public BankAccount() { balance = 0.0; } /** * 这个构造函数将起始余额设置为作为参数传递的值 * @param startBalance 起始余额 */ public BankAccount(double startBalance) { balance =
这个类有三个重载的构造函数和多个重载的方法,提供了不同的方式来创建账户和执行操作。
实例字段(也称为成员变量)是在类中定义但在方法体外部声明的变量。它们属于类的每一个对象(实例),并且在整个类的范围内都可见。也就是说,类的所有实例方法都可以直接访问和修改这些实例字段,无需通过参数传递。例如,如果你在类中声明了一个private double balance;字段,那么类中的所有方法(如deposit、withdraw、getBalance等)都可以直接读取或更改balance的值。
需要注意的是,实例字段的可见性还受到访问修饰符(如private、protected、public)的影响。通常我们将实例字段声明为private,以实现封装,只允许类的内部方法访问和修改它们,而不允许外部直接访问。
实例字段在类的所有方法中都是可见的,并且可以被这些方法访问和修改,这也是对象能够维护自身状态的基础。
当在方法中声明的局部变量(包括方法的参数)与类中的实例字段(成员变量)同名时,局部变量会“遮蔽”同名的实例字段。
也就是说,在该方法体内直接使用这个名字时,实际上访问的是局部变量而不是实例字段。这种情况下,如果你想在方法内部访问被遮蔽的实例字段,必须使用this关键字来明确表示。
例如,this.value表示当前对象的实例字段value,而直接写value则指的是方法的参数或局部变量。这样可以避免命名冲突导致的混淆,并确保你访问的是正确的变量。
例如:
|public class Example { private int value; // 实例字段 public void setValue(int value) { // 参数与实例字段同名 this.value = value; // 使用this访问实例字段 } public int getValue() { return value; // 可以直接访问实例字段 } }
库(Library)是Java中用于组织和管理相关类、接口、枚举等代码结构的机制。它类似于操作系统中的文件夹,可以将功能相近或属于同一模块的类归类存放,形成层次分明的结构。 通过使用库,不仅可以避免类名冲突(因为不同库下可以有同名的类),还可以提升代码的可维护性和可读性。
在实际开发中,通常会根据项目的功能模块、业务逻辑或公司/组织的域名反写来命名库。
例如,java.util库中包含了各种实用工具类,com.example.bank库可能包含银行相关的业务类。使用库还可以方便地进行访问控制和代码管理,使大型项目的结构更加清晰有序。
当你需要使用其他包中的类时,你需要使用import语句。例如:
|import java.util.Scanner; import java.util.Random; import java.io.*;
这些import语句使Scanner、Random和文件I/O类在你的程序中可用。
8. 简单类设计练习
设计一个Student类,包含学生的姓名和年龄属性,以及相应的getter和setter方法。
|public class Student { // 私有字段 private String name; private int age; // 无参构造函数 public Student() { name = ""; age = 0; } // 带参数的构造函数 public Student(String name, int age) { this.name =
9. 构造函数练习
为Student类编写多个构造函数,包括无参构造函数、只接收姓名的构造函数和接收姓名与年龄的构造函数。
|public class Student { private String name; private int age; // 无参构造函数 public Student() { this.name = "未知"; this.age = 0; } // 只接收姓名的构造函数 public Student(String name) { this.name = name; this
10. 对象作为参数练习
编写一个方法,接受Student对象作为参数,并显示学生的信息。
|public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public
11. 方法重载练习
在Student类中添加重载的方法,比如设置成绩的方法可以接受整数或浮点数参数。
|public class Student { private String name; private double score; public Student(String name) { this.name = name; this.score = 0.0; } // 方法重载:设置整数成绩 public void setScore(int score) { this.score = score;
输出结果:
|姓名: 张三 年龄: 20 ---------------- 姓名: 李四 年龄: 22
说明:
private修饰字段实现封装getter和setter方法访问私有字段this关键字区分参数和字段输出结果:
|姓名: 未知, 年龄: 0 姓名: 王五, 年龄: 0 姓名: 赵六, 年龄: 25
说明:
输出结果:
|学生信息: 姓名: 张三 年龄: 20 ---------------- 学生信息: 姓名: 李四 年龄: 22 ----------------
说明:
输出结果:
|张三的成绩已设置为: 85 姓名: 张三, 成绩: 85.0 张三的成绩已设置为: 88.5 姓名: 张三, 成绩: 88.5 张三的平均成绩已设置为: 92.5 姓名: 张三, 成绩: 92.5
说明: