前言
- 本文原创作者为 Blume,版权归原创作者所有。
- 本文主要内容根据 Java 官方教程中的《What Is an Object?》和《Objects》编写而成。
- 本文更新(修订)于 2021 年 3 月 26 日。
- 本文网址为
https://blog.csdn.net/weixin_48390834/article/details/115176548
- 商业性转载请联系原创作者,非商业性转载请注明出处。
概述
对象(Object),是理解面向对象(Object-oriented)技术的关键,即世间万物皆对象(Everything is an Object)。环顾四周,就会发现许多世间对象(物体)的例子,比如衣帽鞋袜、瓜果梨桃、桌椅板凳、花鸟鱼虫等等。
物体都有两个共同的特征,就是状态(State)和行为(Behavior)。例如,在铲屎官家中的喵星人和汪星人,都有自己的状态(名字和花色)和行为(卖萌和犯二)。识别物体的状态和行为,是从面向对象编程的角度开始思考的好方法。
当看到身边的某个物体时,可以问自己两个问题:
- 这个物体可能处于什么状态?
- 这个物体可能执行什么行为?
当把观察结果记录下来以后,就会发现这些物体在状态和行为上的差异和区别。例如,普通台灯可能只有两种状态(开着和关着)和两种行为(打开和关闭),而电视机可能存在多种状态(开着、关着、当前音量、当前频道等等)和多种行为(打开、关闭、调节音量、转换频道等等)。或许还可能会注意到,某些物体可以被其他物体所包含,比如柜子里放着衣帽鞋袜、篮子里装着瓜果梨桃、房子里摆着桌椅板凳、院子里养着花鸟鱼虫等等。这些现实世界的观察结果都可以转化为面向对象编程的世界。
软件意义上的对象,在概念上与世间对象(物体)相似,也是由状态和相关行为构成的。在 Java 中,对象将其状态存储在字段(Field)(某些编程语言的变量)中,并通过方法(Method)(某些编程语言的过程或函数)公开其行为。方法用来操作对象的内部状态,并作为对象到对象通信的主要机制。
隐藏对象的内部状态并要求通过对象的方法执行所有的交互,这被称为数据封装(Data Encapsulation),这是面向对象编程的基本原则。
将与单个对象有关的代码绑定到该对象中的做法,有很多好处:
- 模块化:对象的源代码可以独立于其他对象的源代码而进行编写和维护;创建后,对象可以很容易地在系统内进行传递。
- 信息隐藏:只能通过对象的方法进行交互,而其内部的实施细节可以与世隔绝。
- 代码重用:若对象已经存在(可能是由另一个软件开发人员编写的),则可以在自己的程序中使用该对象(如果对该对象中的代码信任的话)。
- 可插拔性和易调试性:如果某个特定对象出现问题,只需简单地将其从应用程序中移除,然后插入另一个对象作为其替换。这类似于解决现实世界中的机械问题,如果机器中的一颗螺栓坏了,那就更换它,而不是更换整台机器。
典型的 Java 程序会创建许多对象,这些对象通过调用方法进行交互。通过这些对象的交互,程序可以执行各种任务,例如实现 GUI、运行动画或通过网络发送和接收信息。一旦对象完成了为其创建的工作,它的资源就会被回收,以供其他对象使用。
创建对象
众所周知,类提供了对象的蓝图,也就是通过类来创建对象。
import java.sql.Date;
import java.util.Calendar;
/**
* 创建对象示例
*
* @author Blume
*/
public class CreateObject_demo {
public static void main(String[] args) {
// 通过 MyAge 类来创建一个新对象,
// 并将创建时所需的名字和生日,作为初始值传递给它。
MyAge demo = new MyAge("Java", "1995-5-23");
System.out.println(demo.toString(2020));
System.out.println("-------- -------- --------");
// new 操作符在表达式中直接使用。
String age = new MyAge().toString();
System.out.println(age);
}
}
/**
* 定义 MyAge 类。
*
* @author Blume
*/
class MyAge {
// 不建议将成员字段声明为 public 或包私有的(如下所示)。
String name = "anonymous";
final Calendar birthday = Calendar.getInstance();
public MyAge() {
}
public MyAge(String name, String birthday) {
this.name = name;
this.birthday.setTime(Date.valueOf(birthday));
}
@Override
public String toString() {
return toString(Calendar.getInstance().get(Calendar.YEAR));
}
public String toString(int year) {
return "我的名字是:".concat(name).concat("\r\n我的年龄在 ").
concat(String.valueOf(year)).concat(" 年是:").
concat(String.valueOf(Math.max((year - birthday.get(Calendar.YEAR)), 0)));
}
}
/* 输出结果:
我的名字是:Java
我的年龄在 2020 年是:25
-------- -------- --------
我的名字是:anonymous
我的年龄在 2021 年是:0
*/
如上例所示,用来创建对象的语句,通常分为三部分:
- 声明:
MyAge demo
,这是将变量名与对象类型相关联的完整变量声明。 - 实例化:
new
,这个关键字是创建对象的 Java 操作符,表示创建一个新对象。 - 初始化:
MyAge("Java", "1995-5-23")
,这是对构造方法(Constructor)的调用,用于初始化新对象。
声明一个引用对象的变量
可以只声明一个引用对象的变量:
MyAge demo;
这样的声明,就表示其值将不确定,直到一个对象被实际创建并分配给它。仅仅声明一个引用变量是不会创建对象的,所以在代码中使用它之前,必须先将一个实际创建的对象分配给它,或用 new
操作符为其新建一个对象。否则,Java 编译器将会生成一条类似于“可能尚未初始化变量 demo
”的错误消息。
实例化一个类
new
操作符通过为新对象分配内存并返回对该内存的引用来实例化一个类。
- 提示:“
实例化一个类
”和“创建一个对象
”这两句短语在含义上完全相同,也就是当需要创建一个对象
时就需要实例化一个类
,或者通过实例化一个类
来创建一个对象
,所以实例化一个类
就是创建一个对象
。
new
操作符还需要一个后缀参数来调用构造方法。构造方法的名称,就是需要实例化的类的名称。
new
操作符返回对它创建的对象的引用。该引用通常分配给适当类型的变量:
MyAge demo = new MyAge("Java", "1995-5-23");
new
操作符返回的引用可以不分配给变量而直接用在表达式中:
String age = new MyAge().toString();
初始化一个对象
在前面的示例中,MyAge 类包含两个构造方法。可以很容易地识别构造方法,因为它的声明中使用与类相同的名称,并且没有返回类型。MyAge 类中的第二个构造方法采用两个字符串参数,正如代码 (String name, String birthday)
所声明的那样。以下语句向构造方法提供所需的两个参数,为新建对象的名字和生日提供初始值:
MyAge demo = new MyAge("Java", "1995-5-23");
MyAge 类中的第一个构造方法不接受任何参数,故称此为无参构造方法(No-argument Constructor)。当调用无参构造方法创建对象时,将使用类中已给定的初始值(如果有的话):
// 不建议将成员字段声明为 public 或包私有的(如下所示)。
String name = "anonymous";
或者使用无参构造方法中已给定的初始值(如果有的话):
public MyAge() {
name = "anonymous";
}
每个类都至少有一个构造方法。若类中没有明确声明任何一个构造方法,则 Java 编译器会自动提供一个无参构造方法,称为缺省构造方法(Default Constructor)。这个缺省构造方法将调用父类的无参构造方法:
/**
* 缺省构造方法示例
*
* @author Blume
*/
public class DefaultConstructor_demo {
public static void main(String[] args) {
System.out.println(new Subclass().toString());
}
}
/**
* 定义 NoArgument 类,该类只有一个无参构造方法。
*
* @author Blume
*/
class NoArgument {
private final String name;
public NoArgument() {
name = "这是父类的无参构造方法给定的初始值。";
}
@Override
public String toString() {
return name;
}
}
/**
* 定义 NoArgument 类的子类,该子类没有构造方法。
*
* @author Blume
*/
class Subclass extends NoArgument {
@Override
public String toString() {
return super.toString();
}
}
/* 输出结果:
这是父类的无参构造方法给定的初始值。
*/
若该类的父类也没有构造方法,或该类没有父类,则调用 Object 类的无参构造方法,因为 Object 类是所有类的父类。
使用对象
一旦你创建了一个对象,你可能想要用它做点什么:
- 使用其中一个字段值;
- 更改其中一个字段值;
- 调用其中一个方法来执行操作。
引用对象的字段
对象的字段(即类中的成员字段)通过其名称进行访问。在访问时,必须使用明确无误的名称。
在方法中,可以使用一个简单名称来表示其所在类中的成员字段:
return "我的名字是:".concat(name).concat("\r\n我的年龄在 ").
concat(String.valueOf(year)).concat(" 年是:").
concat(String.valueOf(Math.max((year - birthday.get(Calendar.YEAR)), 0)));
在这种情况下,name
和 birthday
都是简单名称。
对象的类之外的代码必须使用对象引用或表达式,然后是点(.
)操作符,然后是简单的字段名称。例如,将字段的初始值放在对象的类之外(强烈建议禁止这样做):
// 强烈建议禁止使用这种方式更改对象的字段值。
MyAge myAge = new MyAge();
myAge.name = "Java";
- 提示:强烈建议禁止使用上述方式更改对象的字段值,因为这会产生面条式代码(Spaghetti Code)。如果可以在对象之外更改对象的字段值,那么这个字段将变得不确定,因为任何一段在对象之外的代码都有可能在该对象的生命期内改变其值,所以强烈建议将对象的字段声明为
private
。若该字段在对象初始化后的生命期内固定不变,则建议将其声明为final
,此时该字段是不可变的,故可以将其声明为public
或包私有的,但仍然不建议这样做,因为这违背了面向对象编程的基本原则。
可以通过 new
操作符返回的对象引用,来访问该新对象的字段(不建议这样做):
// 不建议使用这种方式访问对象的字段。
String another = new MyAge().name;
上述语句创建一个新的对象,并立即获取其默认的名字。
- 提示:上述语句执行后,程序不再具有对该对象的引用,因为程序从未将该引用存储在任何地方。由于该对象不再被引用,其资源可由 Java 虚拟机自行回收。但不建议使用上述方式访问对象的字段,因为这违背了面向对象编程的基本原则,所以强烈建议将对象的字段声明为
private
。
调用对象的方法
还可以通过对象的引用来调用对象的方法(即类中的成员方法)。使用点(.
)操作符,将方法的简单名称附在对象引用之后。此外,在圆括号内可以为方法提供所需的任何参数:
System.out.println(demo.toString(2020));
如果方法不需要任何参数,请使用空括号:
String age = new MyAge().toString();
可以通过对象方法的调用来实现对象字段的访问。如果将以下方法添加到 MyAge 类中:
public String getName() {
return name;
}
就可以使用以下语句来实现对 name 字段的访问:
String another = new MyAge().getName();
垃圾收集器
一些面向对象的语言要求跟踪所有创建的对象,并在不再需要时显式销毁它们。显式地管理内存是令人乏味的,而且容易出错。Java 平台允许创建任意数量的对象(当然,这受系统处理能力的限制),并且无需担心如何销毁它们。当 Java 运行时环境(Java Runtime Environment,JRE)确认不再使用对象时,就会删除这些对象。这个过程称为垃圾收集(Garbage Collection)。
当不再引用一个对象时,该对象就有了获得垃圾收集的资格。当一个变量超出其作用域范围时,保存在该变量中的引用通常会被丢弃。或者,可以通过将变量设置为特殊值 null
来显式删除对象引用。
- 提示:一个程序可以对同一个对象有多个引用;在对象拥有获得垃圾收集的资格之前,必须先删除对该对象的所有引用。
JRE 具有一个垃圾收集器(Garbage Collector),可定期释放不再被引用的对象所占用的内存。垃圾收集器会在它认为合适的时间里自动完成它的工作。