Java构造函数入门指南
Java是一种面向对象编程语言,其核心概念是对象。对象类似于现实世界的实体,通过new
关键字创建并占用内存。但这一切都发生在前端代码中——那么后端呢?对象是如何创建并用值初始化的?
这就是构造函数发挥作用的地方。构造函数是一种特殊类型的方法,没有返回类型。它们主要用于初始化对象,设置其内部状态,或为其属性分配默认值。
在本教程中,我们将深入探讨Java中的构造函数。您将学习它们的工作原理以及为什么在对象创建和Java编程中至关重要。最后,希望您能理解为什么它们是OOP的核心概念之一。
让我们开始吧……
先决条件
开始学习Java中的构造函数不需要太高级的知识。只需对Java语法、类、对象、方法、参数、实参和访问修饰符有基本了解就足够了。
我们将涵盖的内容
- Java中的构造函数是什么?
- 构造函数语法
- 构造函数的类型
- 默认构造函数
- 无参构造函数
- 参数化构造函数
- 拷贝构造函数
- 在Java中调用构造函数时幕后发生了什么
- 如何在构造函数中使用
return
关键字 - 示例代码
- 结论
- 常见问题解答
Java中的构造函数是什么?
如上所述,构造函数是特殊类型的方法:
- 没有返回类型(甚至不是
void
) - 与类同名
- 在使用
new
关键字创建对象时自动调用
构造函数的主要目的是初始化新创建的对象,设置其内部状态,或为其属性分配默认值。
构造函数也可以理解为一个特殊的代码块,在创建对象时被调用——要么自动调用,要么通过硬编码手动调用——用我们想要初始化对象的值。
如果我们接受对象使用默认值(如数字为0,对象为null
),Java会自动为我们处理。但如果我们想在创建对象时赋予特定值,就需要编写一个构造函数,将这些值作为参数接收并用它们来设置对象。
构造函数语法
|
|
示例
当未显式定义构造函数时
|
|
输出:
|
|
在上面的代码中,我们有一个Car
类,包含两个变量:
brand
,类型为String
year
,类型为int
由于类只是一个蓝图,我们需要创建一个对象来实际使用它。这是在Main
类中完成的。当我们使用new Car()
创建一个Car
对象时,Java会寻找一个构造函数。因为我们没有定义,编译器会自动提供一个默认构造函数(无参构造函数)。
这使我们能够创建对象并打印其变量而不会出现任何错误。打印的值将是默认值——String
为null
,int
为0
。
我们将在后面逐步深入探讨这是如何工作的,以便更容易理解。
当我们定义了构造函数时
|
|
输出:
|
|
这是与之前相同的代码,但有一个关键区别:这次我们显式定义了一个构造函数。因此,我们看到的输出不是默认值(String
为null
,int
为0
),而是我们提供的自定义值。
这是如何发生的?很简单——我们在创建对象时传递值作为参数:
|
|
这些值被构造函数作为参数接收,然后用于初始化对象的变量。结果,我们得到的是指定的品牌和年份,而不是默认值。
构造函数的类型
主要有四种类型的构造函数:
- 默认构造函数
- 无参构造函数
- 参数化构造函数
- 拷贝构造函数
默认构造函数
一种无参构造函数,由编译器在编译过程中添加,以便初始化对象的值。只有在您没有显式添加构造函数时,编译器才会添加它。
语法:
|
|
示例:
|
|
在编译过程中编译器添加默认构造函数后,代码变为:
|
|
输出:
|
|
无参构造函数
无参构造函数是您在代码中显式编写的一种构造函数,不包含任何参数。
现在,您可能想知道……它与默认构造函数不是一样的吗?答案是既是也不是。
默认构造函数和无参构造函数之间没有太大区别,因为两者都不接受任何参数。但有一个关键区别。
正如我们已经讨论过的,默认构造函数是一种无参构造函数,当编译器在我们的代码中找不到构造函数时自动添加。相比之下,无参构造函数是我们在代码中编写的一种构造函数。
简而言之,如果编译器在编译过程中添加构造函数,则称为默认构造函数。但如果我们自己添加构造函数,则称为无参构造函数。
默认构造函数和用户定义构造函数之间的主要区别在于它们是如何创建的以及它们做什么。
-
默认构造函数是如果我们自己不添加,编译器自动添加的。它不做太多事情——它只是调用父类(通常是
Object
类)并将所有变量设置为其默认值。例如,int
变为0
,对象变为null
。 -
用户定义构造函数是我们自己编写的构造函数。我们可以在其中添加自定义逻辑,为变量设置自定义值,并使用访问修饰符如
public
、private
或protected
。这意味着我们可以决定在创建对象时应如何设置对象。
请注意,即使我们没有在构造函数中编写super()
,Java仍然会自动添加它,除非我们使用this()
调用另一个构造函数或使用参数调用不同的super(...)
。
我们将在下一节深入理解这一点。
方面 | 默认构造函数 | 无参构造函数 |
---|---|---|
定义 | 当不存在其他构造函数时,由编译器自动提供的构造函数 | 由程序员显式编写的无参构造函数 |
定义者 | 编译器 | 程序员 |
自定义逻辑 | 不可能——只进行基本的默认初始化 | 是——可以包含任何初始化逻辑 |
可用时机 | 仅当类根本没有定义任何构造函数时 | 当由程序员显式编写时 |
目的 | 允许使用默认初始化创建对象 | 允许使用程序员定义的行为创建对象 |
语法:
|
|
示例:
让我们使用与解释默认构造函数时相同的Bike
示例。
|
|
输出:
|
|
在上面的代码中,我们在编写代码时定义了一个构造函数。这意味着它是一个无参构造函数的示例。
我们知道两种类型的构造函数都是在没有任何参数的情况下定义的,但函数体呢?我们还没有说过任何关于它的事情。让我们看看如果我们编写一个没有函数体的无参构造函数代码会发生什么:
|
|
输出:
|
|
代码仍然可以编译,因为编译器在编译过程中添加了super()
关键字,该关键字使用Object
类初始化对象。
参数化构造函数
接受参数的构造函数称为参数化构造函数,仅在我们必须用自定义值初始化对象的属性时使用。
- 参数(Parameter)指的是在构造函数或方法定义中列出的变量。
- 实参(Argument)是在调用构造函数或方法时传递的实际值。
它使我们在对象创建时能够灵活地用给定的自定义值初始化对象。
语法:
以下是一个接受一个参数的参数化构造函数的语法:
|
|
示例:
我们将再次使用Bike
示例。
|
|
输出:
|
|
在这个例子中,我们使用一个Bike
类,它有一个String
数据类型的实例变量modelName
,以及一个构造函数来设置该变量的值。
构造函数接受一个名为model
的参数并将其分配给modelName
。因此,当我们创建一个新的Bike
对象并传入字符串"Mountain Bike"
时,构造函数将该值存储在modelName
变量中。
因此,当我们打印模型名称时,我们看到的是"Mountain Bike"
而不是null
(String
数据类型的默认值),因为modelName
的值已被更新。
拷贝构造函数
拷贝构造函数用于创建一个新对象作为现有对象的副本。与C++不同,Java没有默认的拷贝构造函数。相反,我们必须通过创建一个构造函数来创建自己的拷贝构造函数,该构造函数接受同一类的对象作为参数并复制其字段。
语法:
|
|
示例:
|
|
输出:
|
|
在上面的代码中,我们创建了一个拷贝构造函数来将对象(myBike
)的值复制到一个新对象(copiedBike
)中,该对象我们在main
类中定义。
但调用新对象的方式有点不同。我们没有为构造函数传递参数,而是传递了原始对象。
为什么使用拷贝构造函数?
拷贝构造函数用于制作对象的副本,但您也可以使用clone()
方法或object.clone()
方法制作副本。那么为什么我们使用拷贝构造函数呢?
拷贝构造函数进行深拷贝,而clone
方法进行对象的浅拷贝。在使用克隆技术之前,您应该了解各种事项,例如CloneNotSupportedException
。
另一方面,拷贝构造函数清晰易懂,并且与final
字段配合良好。我们可以控制复制的方式(深拷贝与浅拷贝),尤其是在处理可变对象时。
在Java中调用构造函数时幕后发生了什么?
所以,回顾一下:当我们使用new
关键字创建对象时,构造函数会自动调用。如果我们在类中没有定义任何构造函数,Java会自动为我们定义一个构造函数。
但在编写和运行代码时,我们主要关注编辑器中可见的内容,即我们可以看到的内容。让我们更深入地探索在编译器和JVM级别上,当对象被创建和执行时幕后发生了什么。
步骤1:内存分配——当我们使用new
关键字创建对象时,Java在堆中为该对象分配内存。此内存是对象字段(也称为属性)放置的位置。
步骤2:引用创建——对此对象的引用存储在栈上,这让我们的程序可以与堆中的对象交互。
步骤3:构造函数创建——Java然后确定要调用哪个构造函数。如果我们的类中没有显式定义构造函数,编译器会自动插入一个无参构造函数。
步骤4:超类构造函数调用——在执行构造函数体之前,Java首先使用super()
关键字调用超类的构造函数。这确保了从父类继承的字段被正确初始化。如果您没有显式编写super()
,编译器会自动在代码的第一行添加它,但前提是超类有一个无参构造函数,除非我们已经通过this()
调用另一个构造函数。
但不要在同一构造函数中同时使用super()
和this()
关键字(您可以在单独的构造函数中使用它们)。
假设它没有超类——那么怎么办?
答案很简单:Java有一个内置的Object
类,默认情况下有一个无参构造函数。这就是为什么即使我们不自己编写super()
,我们的类也能顺利运行,因为Java在后台调用它。
这意味着我们创建的每个类都是Object
类的子类。
属性初始化:
此时,字段被初始化:
- 首先,使用默认值(例如,
int
为0
,对象为null
) - 然后,使用我们编写的任何显式初始化(例如,
int x = 10
),默认值将被它们替换。
构造函数执行:
最后,逻辑运行。这是类中为对象创建定义的所有或部分属性通过构造函数在对象创建时使用参数初始化的地方。
但并非每个字段都可能被初始化。未被构造函数更新的字段将保持它们已有的值(要么是默认值,要么是显式初始化的值)。
简而言之,构造函数使我们在创建时能够灵活地自定义对象,但它不会自动设置每个字段,除非我们显式编写逻辑。
查看以下代码以更好地理解:
|
|
输出:
|
|
在上面的例子中,我们有三个数据成员——a
、b
和name
。我们已经在开头声明并初始化了变量b
,并在对象创建时给a
赋值。
所以我们可以看到:
a
的值已通过构造函数在对象创建时给定的值更新,具有相同的值b
已经有一个值且未被构造函数更新,打印相同的值- 字符串
name
没有值,因此打印null
,因为它是String
数据类型的默认值。
如何在构造函数中使用return
关键字
我们知道构造函数定义时没有返回类型,但我们可以在构造函数中使用return
关键字仅用于提前退出构造函数,而不是返回值。查看下面的代码。
示例代码:
|
|
让我们尝试理解上面的代码以及return
关键字的使用。我们将从如果return
关键字不在这里会发生什么开始。代码将没有任何错误地执行并收到输出。
现在,如果我们添加return
关键字会发生什么?正如我们上面讨论的,return
关键字将告诉编译器不要越过构造函数中的这一点。
因此,我们在return
关键字之后在构造函数中编写的任何内容都不会被编译,如果该内容有任何值并且对于代码的正确执行是必要的,编译器将抛出错误。
错误清楚地说明“不可达语句”,这意味着不允许编译器越过return
关键字