
在之前的部分,我们学习了实例字段和实例方法。每个类的实例都有自己的字段集合,这些字段被称为实例字段。你可以创建类的几个实例,并在每个实例的字段中存储不同的值。
但是,有时候我们需要创建不属于任何类实例的字段或方法。这样的成员被称为静态字段和静态方法。当值存储在静态字段中时,它不是存储在类的实例中。事实上,即使类的实例不存在,也可以将值存储在类的静态字段中。
同样,静态方法不操作属于任何类实例的字段。相反,它们只能操作静态字段。你可以将静态字段和静态方法视为属于类而不是类的实例。
当一个字段使用static关键字声明时,这个字段属于整个类本身,而不是某个具体的对象实例。无论你创建了多少个该类的对象,内存中都只会有这一个静态字段的副本。
所有该类的实例都共享同一个静态字段,这意味着如果一个实例修改了静态字段的值,其他所有实例访问这个字段时看到的也是被修改后的新值。静态字段通常用于存储与类相关的全局信息,而不是与某个特定对象相关的数据。
让我们看一个例子:
|public class Countable { private static int instanceCount = 0; /** * 构造函数增加静态字段instanceCount。 * 这跟踪创建的此类实例的数量。 */ public Countable() { instanceCount++; } /** * getInstanceCount方法返回已创建的此类实例的数量。 * @return instanceCount字段中的值。 */ public int getInstanceCount() { return instanceCount; } }
首先,注意第2行静态字段instanceCount的声明:
|private static int instanceCount = 0;
静态字段是通过在访问说明符之后、字段数据类型之前放置static关键字创建的。注意我们明确地将instanceCount字段初始化为值0。这种初始化只发生一次,无论创建了多少个类实例。
接下来,看第7到10行的构造函数。构造函数使用++操作符来增加instanceCount字段。每次创建Countable类的实例时,都会调用构造函数,instanceCount字段会增加。因此,instanceCount字段将包含已创建的Countable类实例的数量。
让我们看一个演示这个类的程序:
|public class StaticDemo { public static void main(String[] args) { int objectCount; // 创建Countable类的三个实例 Countable object1 = new Countable(); Countable object2 = new Countable(); Countable object3 = new Countable(); // 从类的静态字段获取实例数量 objectCount = object1.getInstanceCount
运行这个程序会输出:
|3个类实例被创建。
程序创建了Countable类的三个实例,由变量object1、object2和object3引用。虽然有三个类实例,但只有一个静态字段副本。这说明了所有类实例共享静态字段。
当类包含静态方法时,不需要创建类的实例就可以执行该方法。让我们看一个包含静态方法的类的例子:
|public class Metric { /** * milesToKilometers方法将英里距离转换为公里。 * @param m 英里距离 * @return 公里距离 */ public static double milesToKilometers(double m) { return m * 1.609; } /** * kilometersToMiles方法将公里距离转换为英里。 * @param k 公里距离 * @return
静态方法是通过在方法头中的访问说明符之后放置static关键字创建的。Metric类有两个静态方法:milesToKilometers和kilometersToMiles。因为它们被声明为静态的,所以它们属于类,可以在没有任何类实例存在的情况下调用。
你只需在方法调用中的点操作符之前写入类的名称。这里是一个例子:
|kilometers = Metric.milesToKilometers(10.0);
这个语句调用milesToKilometers方法,传递值10.0作为参数。注意,该方法不是从类的实例调用的,而是直接从Metric类调用的。
让我们看一个使用Metric类的程序:
|import javax.swing.JOptionPane; public class MetricDemo { public static void main(String[] args) { String input; // 保存输入 double miles; // 英里距离 double kilos; // 公里距离 // 获取英里距离 input = JOptionPane.showInputDialog("输入英里距离。"); miles = Double.parseDouble(input);
运行这个程序会显示对话框,让用户输入距离并进行转换。
静态方法有一个重要的限制:它们不能直接访问类的非静态成员变量或调用非静态方法。原因在于,静态方法属于类本身,而非静态成员属于类的具体实例。当没有创建任何对象实例时,静态方法依然可以被调用,此时并不存在任何实例成员可供访问。 具体来说:
System.out.println(this.someField);,因为this关键字在静态方法中是不可用的。static。我们之前讨论了如何将基本值以及String对象的引用作为参数传递给方法。你也可以将其他类型对象的引用作为参数传递给方法。
当你将对象作为参数传递时,你传递的是引用变量中的值。当方法接收到对象引用作为参数时,它可以修改该变量引用的对象的内容。
让我们看一个例子:
|public class PassObject { public static void main(String[] args) { // 创建一个Rectangle对象 Rectangle box = new Rectangle(12.0, 5.0); // 将对对象的引用传递给displayRectangle方法 displayRectangle(box); } /** * displayRectangle方法显示矩形的长度和宽度 * @param r 对Rectangle对象的引用 */
运行这个程序会输出:
|长度:12.0 宽度:5.0
在这个程序的main方法中,box变量是一个Rectangle引用变量。在第8行,它的值作为参数传递给displayRectangle方法。displayRectangle方法有一个参数变量r,它也是一个Rectangle引用变量,接收参数。
当方法接收到对象引用作为参数时,方法可以修改该变量引用的对象的内容。这在下例中得到了演示:
|public class PassObject2 { public static void main(String[] args) { // 创建一个Rectangle对象 Rectangle box = new Rectangle(12.0, 5.0); // 显示对象的内容 System.out.println("box对象的内容:"); System.out.println("长度:" + box.getLength() + " 宽度:"
运行这个程序会输出:
|box对象的内容: 长度:12.0 宽度:5.0 现在box对象的内容是: 长度:0.0 宽度:0.0
在Java中,方法不仅可以返回基本数据类型的值(如int、double、float等),还可以返回对象的引用。也就是说,方法的返回类型可以是一个类名,这样的方法在执行完毕后会返回对某个对象的引用。通过这种方式,方法能够将新创建的对象、已经存在的对象,或者根据某些逻辑处理后得到的对象引用返回给调用者。这使得我们可以在方法之间灵活地传递和操作对象,极大地增强了程序的结构和功能。

让我们看一个例子,其中方法返回对BankAccount对象的引用:
|import javax.swing.JOptionPane; public class ReturnObject { public static void main(String[] args) { BankAccount account; // 获取对BankAccount对象的引用 account = getAccount(); // 显示账户余额 JOptionPane.showMessageDialog(null, "账户余额为$" + account.getBalance()); System.
注意getAccount方法有一个BankAccount的返回数据类型。这表示方法返回对BankAccount对象的引用。
我们经常需要显示表示对象状态的消息。对象的状态简单地说就是存储在对象字段中的数据。例如,BankAccount类有一个字段:balance。在任何给定时刻,BankAccount对象的balance字段将保存某个值。balance字段的值表示该时刻对象的状态。
创建表示对象状态的字符串是如此常见的任务,以至于许多程序员为他们的类配备了一个返回这种字符串的方法。在Java中,将此方法命名为toString是标准做法。
让我们看一个具有toString方法的类的例子。Stock类保存有关公司股票的数据:
|public class Stock { private String symbol; // 股票交易符号 private double sharePrice; // 每股当前价格 /** * 构造函数 * @param sym 股票交易符号 * @param price 股票每股价格 */ public Stock(String sym, double price) { symbol = sym; sharePrice = price; }
当你为类编写了toString方法后,Java会在需要将对象转换为字符串时自动调用该方法。最常见的场景包括:
|Stock xyzCompany = new Stock("XYZ", 9.62); System.out.println(xyzCompany);
Java还会在你将类的对象与字符串连接时隐式调用对象的toString方法。例如,以下代码会隐式调用xyzCompany对象的toString方法:
|Stock xyzCompany = new Stock("XYZ", 9.62); System.out.println("股票数据是:\n" + xyzCompany);
在Java中,不能仅仅通过使用==操作符来判断两个对象是否拥有相同的数据。==操作符在比较对象时,实际上比较的是它们在内存中的地址(即引用),而不是对象内部存储的数据内容。因此,即使两个对象的属性值完全相同,只要它们是不同的实例,==操作符的结果也会是false。
如果我们希望判断两个对象的内容是否相同,就需要在类中专门编写一个方法来实现内容的比较。Java为此提供了equals方法。通过重写equals方法,我们可以自定义对象内容的比较逻辑,使得可以根据实际需求判断两个对象的数据是否一致。例如,在Stock类中,可以通过重写equals方法来比较两个Stock对象的symbol和sharePrice字段,从而判断它们是否代表相同的股票信息。
你不能使用==操作符来比较两个对象的内容。例如,以下代码可能看起来比较两个Stock对象的内容,但实际上没有:
|// 创建具有相同值的两个Stock对象 Stock company1 = new Stock("XYZ", 9.62); Stock company2 = new Stock("XYZ", 9.62); // 使用==操作符比较对象(这是错误的) if (company1 == company2) { System.out.println("两个对象相同。"); } else { System.out.println("对象不同。"); }
当你使用==操作符与引用变量时,操作符比较变量包含的内存地址,而不是变量引用的对象的内容。因为这段代码中的两个数组变量引用内存中的不同对象,它们将包含不同的地址。因此,布尔表达式company1 == company2的结果为false,代码报告对象不同。
让我们为Stock类编写一个equals方法:
|public boolean equals(Stock object2) { boolean status; // 确定此对象的symbol和sharePrice字段是否等于object2的symbol和sharePrice字段 if (symbol.equals(object2.symbol) && sharePrice == object2.sharePrice) { status = true; // 是的,对象相等 } else { status = false; // 不,对象不相等 } // 返回status中的值 return status; }
equals方法接受一个Stock对象作为其参数。参数变量object2将引用作为参数传递的对象。if语句执行以下比较:如果调用对象的symbol字段等于object2的symbol字段,并且调用对象的sharePrice字段等于object2的sharePrice字段,那么两个对象包含相同的值。
让我们看一个演示equals方法的程序:
|public class StockCompare { public static void main(String[] args) { // 创建具有相同值的两个Stock对象 Stock company1 = new Stock("XYZ", 9.62); Stock company2 = new Stock("XYZ", 9.62); // 使用equals方法比较对象 if (company1.equals(company2)) { System.out.
运行这个程序会输出:
|两个对象相同。
在 Java 中,不能像复制基本数据类型变量那样,直接通过赋值语句来复制一个对象。对于基本类型(如 int、double 等),使用赋值语句会将变量的值完整地复制到另一个变量中。但对于对象,赋值语句只是复制对象的引用(即内存地址),而不会创建一个全新的对象副本。来看下面的代码示例:
|Stock company1 = new Stock("XYZ", 9.62); Stock company2 = company1;
第一个语句创建一个Stock对象并将其地址赋给company1变量。第二个语句将company1赋给company2。这不会复制company1引用的对象。相反,它复制存储在company1中的地址并将该地址存储在company2中。执行此语句后,company1和company2变量都将引用同一个对象。
这种类型的赋值操作称为引用复制,因为只复制对象的地址,而不是实际对象本身。要复制对象本身,你必须创建一个新对象,然后将新对象的字段设置为与被复制对象的字段相同的值。
让我们为Stock类添加一个copy方法:
|public Stock copy() { // 创建一个新的Stock对象并用调用对象持有的相同数据初始化它 Stock copyObject = new Stock(symbol, sharePrice); // 返回对新对象的引用 return copyObject; }
copy方法创建一个新的Stock对象,并将调用对象的symbol和sharePrice字段作为参数传递给构造函数。这使新对象成为调用对象的副本。
另一种创建对象副本的方法是使用复制构造函数。复制构造函数是接受同一类对象作为参数的构造函数。它使正在创建的对象成为作为参数传递的对象的副本。
让我们为Stock类添加一个复制构造函数:
|public Stock(Stock object2) { symbol = object2.symbol; sharePrice = object2.sharePrice; }
注意构造函数接受一个Stock对象作为参数。参数变量object2将引用作为参数传递的对象。构造函数将object2的symbol和sharePrice字段中的值复制到正在创建的对象的symbol和sharePrice字段中。
聚合是指一个类的实例是另一个类中的字段。在现实生活中,对象经常由其他对象组成。例如,房子由门对象、窗对象、墙对象等组成。正是所有这些对象的组合构成了房子对象。
在设计软件时,有时从其他对象创建对象是有意义的。例如,假设你需要一个对象来表示你在大学里正在学习的课程。你决定创建一个Course类,它将保存以下信息:
除了课程名称外,该类还将保存与教师和教科书相关的项目。你可以将每个项目的字段放在Course类中。但是,一个好的设计原则是将相关项目分离到它们自己的类中。
让我们看看如何做到这一点。Instructor类可以创建来保存教师相关的数据,TextBook类可以创建来保存教科书相关的数据。这些类的实例然后可以用作Course类中的字段。
首先,Instructor类的定义如下:
|public class Instructor { private String lastName; // 姓氏 private String firstName; // 名字 private String officeNumber; // 办公室号码 /** * 此构造函数初始化姓氏、名字和办公室号码 * @param lname 教师的姓氏 * @param fname 教师的名字 * @param office 办公室号码 */ public Instructor(String lname, String fname, String office
接下来,我们定义TextBook类,定义如下:
|public class TextBook { private String title; // 书籍标题 private String author; // 作者姓氏 private String publisher; // 出版商名称 /** * 此构造函数初始化标题、作者和出版商字段 * @param textTitle 书籍标题 * @param auth 作者姓名 * @param pub 出版商名称 */ public TextBook(String textTitle, String auth, String pub
最后,我们定义Course类,定义如下:
|public class Course { private String courseName; // 课程名称 private Instructor instructor; // 教师 private TextBook textBook; // 教科书 /** * 此构造函数初始化courseName、instructor和text字段 * @param name 课程名称 * @param instructor Instructor对象 * @param text TextBook对象 */ public Course(String name, Instructor instr, TextBook text
让我们看一个演示Course类的程序:
|public class CourseDemo { public static void main(String[] args) { // 创建一个Instructor对象 Instructor myInstructor = new Instructor("张", "三", "RH3010"); // 创建一个TextBook对象 TextBook myTextBook = new TextBook("Java入门", "李明", "教育出版社");
运行这个程序会输出:
|课程名称:Java编程入门 教师信息: 姓氏:张 名字:三 办公室号码:RH3010 教科书信息: 标题:Java入门 作者:李明 出版商:教育出版社
关键字this是对象可以用来引用自己的引用变量的名称。例如,回忆我们前面介绍的Stock类。该类有一个equals方法,它将调用的Stock对象与作为参数传递的另一个Stock对象进行比较:
|public boolean equals(Stock object2) { boolean status; // 确定此对象的symbol和sharePrice字段是否等于object2的symbol和sharePrice字段 if (symbol.equals(object2.symbol) && sharePrice == object2.sharePrice) { status = true; // 是的,对象相等 } else { status = false; // 不,对象不相等 } // 返回status中的值 return status; }
当此方法执行时,this变量包含调用对象的地址。我们可以重写if语句如下,它将执行相同的操作:
|if (this.symbol.equals(object2.symbol) && this.sharePrice == object2.sharePrice)
this关键字的一个常见用途是克服参数名称对字段名称的阴影。如果方法的参数与同一类中的字段具有相同的名称,则参数名称会遮蔽字段名称。
例如,看Stock类中的构造函数:
|public Stock(String sym, double price) { symbol = sym; sharePrice = price; }
这个方法使用参数sym来接受分配给symbol字段的参数,使用参数price来接受分配给sharePrice字段的参数。有时很难(甚至耗时)想出一个与字段名称不同的好参数名称。为了避免这个问题,许多程序员给参数与它们对应的字段相同的名称,然后使用this关键字来引用字段名称。
例如,Stock类的构造函数可以写成如下:
|public Stock(String symbol, double sharePrice) { this.symbol = symbol; this.sharePrice = sharePrice; }
虽然参数名称symbol和sharePrice遮蔽了字段名称symbol和sharePrice,但this关键字克服了遮蔽。因为this是对调用对象的引用,表达式this.symbol引用调用对象的symbol字段,表达式this.sharePrice引用调用对象的sharePrice字段。
你已经知道当创建对象时会自动调用构造函数。你也知道不能像调用其他方法那样显式调用构造函数。但是,有一个例外:你可以使用this关键字从同一类中的另一个构造函数调用一个构造函数。
为了说明这一点,请看下面的例子,它有以下构造函数:
|public Stock(String sym, double price) { symbol = sym; sharePrice = price; }
这个构造函数接受分配给symbol和sharePrice字段的参数。假设我们还想要一个只接受symbol字段参数的构造函数,并将0.0分配给sharePrice字段。以下是编写构造函数的一种方法:
|public Stock(String sym) { this(sym, 0.0); }
这个构造函数简单地使用this变量调用第一个构造函数。它将sym中的值作为第一个参数传递,将0.0作为第二个参数传递。结果是symbol字段被分配sym中的值,sharePrice字段被分配0.0。
记住关于使用this调用构造函数的以下规则:
枚举数据类型由一组预定义值组成。你可以使用数据类型创建只能保存属于枚举数据类型的值的变量。
你已经学习了数据类型的概念以及它们如何与基本变量一起使用。例如,int数据类型的变量可以在特定范围内保存整数值。你不能将浮点值分配给int变量,因为只有int值可以分配给int变量。数据类型定义了该数据类型任何变量的合法值。
有时创建具有特定合法值集的自己的数据类型是有帮助的。例如,假设你想创建一个名为Day的数据类型,该数据类型中的合法值是星期几的名称(星期日、星期一等)。 当你创建Day数据类型的变量时,你只能在该变量中存储星期几的名称。任何其他值都是非法的。在Java中,这种类型被称为枚举数据类型。
你使用enum关键字创建自己的数据类型并指定属于该类型的值。以下是枚举数据类型声明的例子:
|enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }
枚举数据类型声明以enum关键字开始,后跟类型名称,后跟大括号内的标识符列表。示例声明创建一个名为Day的枚举数据类型。 大括号内列出的标识符SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY和SATURDAY被称为枚举常量。它们表示属于Day数据类型的值。
一旦你在程序中创建了枚举数据类型,你就可以声明该类型的变量。例如,以下语句将workDay声明为Day类型的变量:
|Day workDay;
因为workDay是Day变量,我们可以合法地分配给它的唯一值是枚举常量Day.SUNDAY、Day.MONDAY、Day.TUESDAY、Day.WEDNESDAY、Day.THURSDAY、Day.FRIDAY和Day.SATURDAY。如果我们尝试分配Day类型的枚举常量以外的任何值,将导致编译器错误。
当你编写枚举类型声明时,你实际上是在创建一种特殊的类。此外,你在大括号内列出的枚举常量实际上是类的对象。 在前面的例子中,Day是一个类,枚举常量Day.SUNDAY、Day.MONDAY、Day.TUESDAY、Day.WEDNESDAY、Day.THURSDAY、Day.FRIDAY和Day.SATURDAY都是Day类的实例。
枚举常量实际上是对象,它们自动配备了几个方法。其中之一是toString方法。toString方法简单地返回调用枚举常量的名称作为字符串。
枚举常量还有一个名为ordinal的方法。ordinal方法返回表示常量序数值的整数值。常量的序数值是它在枚举声明中的位置,第一个常量位于位置0。
让我们看一个完整的例子:
|public class EnumDemo { // 声明Day枚举类型 enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY } public static void main(String[] args) { // 声明一个Day变量并为其赋值 Day workDay = Day.WEDNESDAY; // 以下语句显示WEDNESDAY System.out.
运行这个程序会输出:
|WEDNESDAY Day.SUNDAY的序数值是0 Day.SATURDAY的序数值是6 FRIDAY大于MONDAY
在Java中,switch语句不仅可以用于基本数据类型(如int、char等),还可以直接用于枚举类型。这意味着你可以在switch语句中将枚举常量作为条件进行判断,从而根据不同的枚举值执行不同的代码分支。下面是一个具体的示例,演示了如何在switch语句中使用枚举类型:
|public class SportsCarDemo2 { public static void main(String[] args) { // 创建一个SportsCar对象 SportsCar yourNewCar = new SportsCar(CarType.PORSCHE, CarColor.RED, 100000); // 获取汽车品牌并对其进行switch switch (yourNewCar.getMake()) { case PORSCHE: System.out.println("你的汽车是在德国制造的。"); break;
运行这个程序会输出:
|你的汽车是在德国制造的。
在Java中,内存管理的一项重要机制是垃圾回收(Garbage Collection,简称GC)。Java虚拟机(JVM)会定期启动一个称为垃圾收集器(Garbage Collector)的后台进程,自动检测并清理程序中不再被任何变量引用的对象。这些“无主”的对象占用的内存会被回收,从而为新的对象分配空间,避免内存泄漏。
具体来说,当你在程序中通过new关键字创建对象时,这些对象会被分配在堆内存中。只要有变量(引用)指向这些对象,它们就会一直存在于内存中。当某个对象不再被任何变量引用(即没有任何方式可以再访问到它),这个对象就变成了“不可达对象”。JVM的垃圾收集器会定期扫描内存,发现这些不可达对象后,将其占用的内存空间释放出来。
与C++等需要手动释放内存的语言不同,Java程序员无需显式地销毁对象或释放内存。垃圾回收器会自动完成这些工作。这大大降低了内存泄漏和悬挂指针等问题的风险,提高了程序的健壮性和安全性。
需要注意的是,垃圾回收的具体时机和频率由JVM自行决定,程序员无法精确控制。虽然可以通过调用System.gc()方法建议JVM进行垃圾回收,但这只是一个建议,JVM是否立即执行垃圾回收并不保证。因此,良好的编程习惯是让对象在不再需要时尽早失去引用(如将引用变量赋值为null),其余的内存管理工作交给JVM自动处理。
例如,看以下代码:
|// 声明两个BankAccount引用变量 BankAccount account1, account2; // 创建一个对象并用account1引用它 account1 = new BankAccount(500.0); // 用account2引用同一个对象 account2 = account1; // 在account1中存储null,使其不再引用对象 account1 = null; // 对象仍然被account2引用 // 在account2中存储null,使其不再引用对象 account2 = null; // 现在对象不再被引用,所以可以被垃圾收集器删除
这段代码使用两个引用变量account1和account2。创建一个BankAccount对象并由account1引用。然后,account1被赋给account2,这导致account2引用与account1相同的对象。
接下来,null值被赋给account1。这从account1变量中移除对象的地址,使其不再引用对象。对象仍然可以访问,因为它被account2变量引用。下一个语句将null赋给account2。这从account2中移除对象的地址,使其不再引用对象。因为对象不再可访问,它将在下次垃圾收集器进程运行时从内存中删除。
如果类有一个名为finalize的方法,它会在类的实例被垃圾收集器销毁之前自动调用。如果你希望在对象被销毁之前执行代码,可以在类中创建finalize方法并将代码放在那里。finalize方法不接受参数,具有void返回类型。
8. 静态成员练习
创建一个类,使用静态字段来跟踪创建的实例数量,并提供一个静态方法来获取实例数量。
|public class Student { private String name; private static int count = 0; // 静态字段,跟踪实例数量 // 构造函数 public Student(String name) { this.name = name; count++; // 每次创建对象时,计数加1 } // 静态方法,获取实例数量 public static int getCount() {
9. toString方法练习
为Student类编写一个toString方法,显示学生的姓名和年龄。
|public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } // 重写toString方法 @Override public String toString() { return "Student{姓名='"
10. this关键字练习
在类的构造函数中使用this关键字来避免参数名称遮蔽字段名称。
|public class Rectangle { private double width; private double height; // 使用this关键字区分参数和字段 public Rectangle(double width, double height) { this.width = width; // this.width是字段,width是参数 this.height = height; // this.height是字段,height是参数 } // 也可以使用不同的参数名,避免使用this public void setWidth
11. equals方法练习
为Student类编写一个equals方法,比较学生的姓名和学号是否相等。
|public class Student { private String name; private String studentId; public Student(String name, String studentId) { this.name = name; this.studentId = studentId; } // 重写equals方法 @Override public boolean equals(Object obj) { // 如果是同一个对象,返回true
输出结果:
|初始实例数量: 0 创建1个对象后: 1 创建2个对象后: 2 创建3个对象后: 3
说明:
count是静态字段,属于类本身,所有实例共享count++getCount()是静态方法,可以通过类名直接调用输出结果:
|Student{姓名='张三', 年龄=20} Student{姓名='张三', 年龄=20}
说明:
@Override注解表示重写父类方法输出结果:
|宽度: 10.0 高度: 5.0 面积: 50.0
说明:
this关键字引用当前对象this.字段名访问字段this可以明确区分参数和字段this输出结果:
|student1.equals(student2): true student1.equals(student3): false student1 == student2: false
说明: