有关构造函数的一般信息
Конструктор
是一个类似于方法的结构,其目的是创建类的实例。设计师特点:
- 构造函数的名称必须与类的名称匹配(按照约定,首字母大写,通常是名词);
- 任何类中都有一个构造函数。即使您不编写一个,Java 编译器也会创建一个默认构造函数,该构造函数将为空,除了调用超类构造函数之外不执行任何操作。
- 构造函数类似于方法,但它不是方法,甚至不被视为类的成员。因此,它不能在子类中继承或重写;
- 构造函数不是继承的;
- 一个类中可以有多个构造函数。在这种情况下,构造函数被称为重载;
- 如果类没有定义构造函数,编译器会自动在代码中添加无参构造函数;
- 构造函数没有返回类型;它甚至不能是类型
void
;如果返回类型void
,那么它就不再是构造函数,而是方法,尽管与类名一致。
- 构造函数中允许使用运算符
return
,但只能为空,没有任何返回值;
- 构造函数允许使用访问修饰符;您可以设置修饰符之一:
public
、protected
或private
不带修饰符。
- 构造函数不能具有修饰符
abstract
、final
、native
或;static
synchronized
- 该关键字
this
引用同一类中的另一个构造函数。如果使用,对其的调用必须是构造函数的第一行;
- 该关键字
super
调用父类的构造函数。如果使用,对它的引用必须是构造函数的第一行;
- 如果构造函数没有调用
super
祖先类的构造函数(带或不带参数),编译器会自动添加代码来调用祖先类的不带参数构造函数;
默认构造函数
任何类中都有一个构造函数。即使您不编写,Java 编译器也会创建一个默认构造函数。该构造函数是空的,除了调用超类构造函数之外不执行任何操作。那些。如果你写:
public class Example {}
那么这相当于写:
public class Example
{
Example()
{
super;
}
}
在这种情况下,没有显式指定祖先类,默认情况下所有Java类都继承该类,
Object
因此调用类构造函数
Object
。如果一个类定义了带参数的构造函数,但是没有重载的不带参数的构造函数,那么调用不带参数的构造函数是错误的。然而,从 Java 1.5 版本开始,可以使用带有可变长度参数的构造函数。如果有一个构造函数具有可变长度参数,那么调用默认构造函数将不会出错。不会,因为可变长度参数可以为空。例如,下面的示例将不会编译,但如果您取消带有可变长度参数的构造函数的注释,它将成功编译并运行并导致运行一行代码
DefaultDemo dd = new DefaultDemo()
;构造函数将被调用
DefaultDemo(int ... v)
。当然,这种情况下就需要使用JSDK 1.5。文件
DefaultDemo.java
class DefaultDemo
{
DefaultDemo(String s)
{
System.out.print("DefaultDemo(String)");
}
public static void main(String args[])
{
DefaultDemo dd = new DefaultDemo();
}
}
未注释构造函数的程序输出结果:
DefaultDemo(int ...)
但是,在类根本没有定义任何构造函数的常见情况下,调用默认构造函数(不带参数)将是必要的,因为默认构造函数替换会自动发生。
对象创建和构造函数
创建对象时,按顺序执行以下操作:
- 在程序中已使用的类中搜索对象类。如果不存在,则会在程序可用的所有目录和库中搜索它。一旦在目录或库中发现类,就会创建并初始化该类的静态字段。那些。对于每个类,静态字段仅初始化一次。
- 为对象分配内存。
- 正在初始化类字段。
- 类构造函数执行。
- 形成到所创建和初始化的对象的链接。该引用是创建对象的表达式的值。
newInstance()
还可以通过调用类方法来创建对象java.lang.Class
。在这种情况下,使用不带参数列表的构造函数。
重载构造函数
同一类的构造函数可以具有相同的名称和不同的签名。此属性称为组合或重载。如果一个类有多个构造函数,则存在构造函数重载。
参数化构造函数
构造函数的签名是参数的数量和类型,以及它们的类型在构造函数参数列表中的顺序。不考虑返回类型。构造函数不返回任何参数。这个说法从某种意义上解释了Java如何区分重载的构造函数或方法。Java 不是通过返回类型来区分重载方法的,而是通过输入参数的数量、类型和类型顺序来区分。构造函数甚至不能返回类型
void
,否则它将变成常规方法,即使它与类名相似。以下示例演示了这一点。文件
VoidDemo.java
class VoidDemo
{
VoidDemo()
{
System.out.println("Constructor");
}
void VoidDemo()
{
System.out.println("Method");
}
public static void main(String s[])
{
VoidDemo m = new VoidDemo();
}
}
结果,程序将输出:
Constructor
这再次证明了构造函数是一个没有返回参数的方法。但是,可以为构造函数指定三个修饰符
public
、
private
或 之一
protected
。该示例现在如下所示: 文件
VoidDemo2.java
class VoidDemo2
{
public VoidDemo2()
{
System.out.println("Constructor");
}
private void VoidDemo2()
{
System.out.println("Method");
}
public static void main(String s[])
{
VoidDemo2 m = new VoidDemo2();
}
}
构造函数中允许写操作符
return
,但只能是空操作符,没有任何返回值。文件
ReturnDemo.java
class ReturnDemo
{
public ReturnDemo()
{
System.out.println("Constructor");
return;
}
public static void main(String s[])
{
ReturnDemo r = new ReturnDemo();
}
}
使用可变长度参数参数化的构造函数
Java SDK 1.5 引入了一个期待已久的工具——构造函数和方法的可变长度参数。以前,不同数量的文档是通过两种不方便的方式处理的。第一个设计的目的是确保参数的最大数量限制在较小的范围内,并且是提前知道的。在这种情况下,可以创建该方法的重载版本,每个版本对应传递给该方法的参数列表的每个版本。第二种方法是针对事先未知的事物和大量的参数而设计的。在本例中,参数被放置在一个数组中,并且该数组被传递给该方法。可变长度参数最常涉及变量初始化的后续操作。用默认值替换某些预期的构造函数或方法参数的缺失是很方便的。可变长度参数是一个数组,并且被视为数组。例如,
Checking
具有可变数量参数的类的构造函数将如下所示:
class Checking
{
public Checking(int ... n)
{
}
}
字符组合 ... 告诉编译器将使用可变数量的参数,并且这些参数将存储在一个数组中,该数组的引用值包含在变量 n 中。可以使用不同数量的参数调用构造函数,包括根本不使用参数。参数自动放置在数组中并通过 n 传递。如果没有参数,则数组的长度为 0。参数列表以及可变长度参数还可以包含强制参数。在这种情况下,包含可变数量参数的参数必须是参数列表中的最后一个。例如:
class Checking
{
public Checking(String s, int ... n)
{
}
}
一个非常明显的限制涉及可变长度参数的数量。参数列表中只能有一个变长参数。给定两个可变长度参数,编译器不可能确定一个参数的结束位置和另一个参数的开始位置。例如:
class Checking
{
public Checking(String s, int ... n, double ... d)
{
}
}
例如
Checking.java
,有一种设备能够识别汽车牌照并记住每辆车白天访问过的区域的方格数。根据区域地图,有必要从记录的汽车总质量中选择白天访问过两个给定方格(例如 22 和 15)的车辆。一辆汽车在白天可以访问许多广场,或者也许只能访问一个广场,这是很自然的。显然,访问的方格数量受到汽车物理速度的限制。让我们创建一个小程序,其中类构造函数将汽车号码(强制参数)和访问过的区域的方块数量(数量可以是可变的)作为参数。构造函数将检查汽车是否出现在两个方格中;如果出现,则在屏幕上显示其编号。
将参数传递给构造函数
编程语言中的参数主要有两类:
术语“按值调用”意味着构造函数接收调用模块传递给它的值。相反,按引用调用意味着构造函数从调用方接收变量的地址。Java 仅使用按值调用。按参数值和按参数链接值。Java 不使用对象的引用调用(尽管许多程序员和一些书籍的作者声称这一点)。当向Java传递对象时,参数
不是通过引用传递,而是
通过对象引用的值传递!无论哪种情况,构造函数都会接收所有参数值的副本。构造函数不能处理其输入参数:
- 构造函数不能更改主(原始)类型的输入参数的值;
- 构造函数不能更改输入参数引用;
- 构造函数无法将输入参数引用重新分配给新对象。
构造函数可以使用其输入参数执行以下操作:
下面的示例证明,在 Java 中,构造函数的输入参数是通过对象引用值传递的。这个例子也反映出构造函数不能改变输入参数的引用,但实际上改变了输入参数副本的引用。文件
Empoyee.java
class Employee
{
Employee(String x, String y)
{
String temp = x;
x = y;
y = temp;
}
public static void main(String args[])
{
String name1 = new String("Alice");
String name2 = new String("Mary");
Employee a = new Employee(name1, name2);
System.out.println("name1="+name1);
System.out.println("name2="+name2);
}
}
程序的输出是:
name1=Alice
name2=Mary
如果 Java 使用引用调用来传递对象作为参数,则构造函数将交换本例中的
name1
和
name2
。构造函数实际上不会交换存储在
name1
和变量中的对象引用
name2
。这表明构造函数参数是使用这些引用的副本进行初始化的。然后构造函数交换副本。当构造函数完成其工作时,x和y变量被销毁,原始变量
name1
继续
name2
引用先前的对象。
更改传递给构造函数的参数。
构造函数不能修改传递的基本类型的参数。但是,构造函数可以修改作为参数传递的对象的状态。例如,考虑以下程序:文件
Salary1.java
class Salary1
{
Salary1(int x)
{
x = x * 3;
System.out.println("x="+x);
}
public static void main(String args[])
{
int value = 1000;
Salary1 s1 = new Salary1(value);
System.out.println("value="+value);
}
}
程序的输出是:
x=3000
value=1000
显然,这个方法不会改变main类型参数。因此,在调用构造函数之后,变量的值
value
仍然等于
1000
。本质上发生了三件事:
- 该变量
x
使用参数值的副本value
(即数字1000
)进行初始化。
- 变量的值
x
增加了三倍 - 现在等于3000
。然而,变量的值value
仍然等于1000
。
- 构造函数终止并且
x
不再使用该变量。
在下面的示例中,由于对象引用的值作为参数传递给该方法,因此员工的工资成功增加了三倍。文件
Salary2.java
class Salary2
{
int value = 1000;
Salary2()
{
}
Salary2(Salary2 x)
{
x.value = x.value * 3;
}
public static void main(String args[])
{
Salary2 s1 = new Salary2();
Salary2 s2 = new Salary2(s1);
System.out.println("s1.value=" +s1.value);
System.out.println("s2.value="+s2.value);
}
}
程序的输出是:
s1.value=3000
s2.value=1000
对象引用的值用作参数。执行该行时
Salary2 s2 = new Salary2(s1)
;构造函数
Salary2(Salary x)
将传递对变量对象的引用的值
s1
,并且构造函数实际上将工资增加三倍
s1.value
,因为即使在构造函数内创建的副本也
(Salary x)
指向变量对象
s1
。
由原语参数化的构造函数。
如果重载构造函数的参数使用可以缩小的原语(例如
int <- double
),则可以调用具有缩小值的方法,尽管事实上没有使用此类参数重载的方法。例如:文件
Primitive.java
class Primitive
{
Primitive(double d)
{
d = d + 10;
System.out.println("d="+d);
}
public static void main(String args[])
{
int i = 20;
Primitive s1 = new Primitive(i);
}
}
程序的输出是:
d=30.0
尽管该类
Primitive
没有带有类型参数的构造函数
int
,但带有输入参数的构造函数也可以工作
double
。在调用构造函数之前,变量
i
将从 type 扩展
int
到 type
double
。相反的选项,当变量
i
的类型为
double
,并且构造函数只有一个参数时
int
,在这种情况下会导致编译错误。
构造函数调用和操作符new
构造函数总是由操作符调用
new
。当使用运算符调用构造函数时
new
,构造函数始终生成对新对象的引用。除非替换正在反序列化的对象,否则不可能强制构造函数形成对已存在对象的引用而不是对新对象的引用。并且使用 new 运算符,不可能形成对已存在对象的引用,而不是对新对象的引用。例如:文件
Salary3.java
class Salary3
{
int value = 1000;
Salary3()
{
}
Salary3(Salary3 x)
{
x.value = x.value * 3;
}
public static void main(String args[])
{
Salary3 s1 = new Salary3();
System.out.println("First object creation: "+s1.value);
Salary3 s2 = new Salary3(s1);
System.out.println("Second object creation: "+s2.value);
System.out.println("What's happend with first object?:"+s1.value);
Salary3 s3 = new Salary3(s1);
System.out.println("Third object creation: "+s3.value);
System.out.println("What's happend with first object?:"+s1.value);
}
}
程序的输出是:
First object creation: 1000
Second object creation: 1000
What's happend with first object?: 3000
Third object creation: 1000
What's happend with first object?: 9000
首先,使用线路
Salary3 s1 = new Salary3()
;创建一个新对象。接下来,如果使用行
Salary3 s2 = new Salary3(s1)
; 或字符串
Salary3 s3 = new Salary3(s1)
;可以创建到已存在对象的链接,然后
s1.value s2.value
它们
s3.value
将存储相同的值
1000
。其实在行
Salary3 s2 = new Salary3(s1)
;将创建该变量的一个新对象,并且通过将其引用值传递给构造函数参数中的对象
s2
来更改该变量的对象的状态。
s1
这可以通过输出结果来验证。当执行该行时
Salary3 s3 = new Salary3(s1)
;将创建该变量的新对象,
s3
并且该变量的对象的状态将再次更改
s1
。
构造函数和初始化块,调用构造函数时的操作顺序
创建对象和构造函数部分列出了创建对象时执行的常规操作。其中包括初始化类字段和计算类构造函数的过程,这些过程也有一个内部顺序:
- 所有数据字段都初始化为其默认值(0、false 或 null)。
- 所有字段初始值设定项和初始化块都按照类声明中列出的顺序执行。
- 如果在构造函数的第一行调用另一个构造函数,则执行被调用的构造函数。
- 构造函数的主体被执行。
构造函数与初始化相关,因为在 Java 中初始化类中的字段有以下三种方法:
- 在声明中赋值;
- 在初始化块中赋值;
- 在构造函数中设置其值。
当然,您需要组织初始化代码,使其易于理解。以下面的类为例:
class Initialization
{
int i;
short z = 10;
static int x;
static float y;
static
{
x = 2000;
y = 3.141;
}
Initialization()
{
System.out.println("i="+i);
System.out.println("z="+z);
z = 20;
System.out.println("z="+z);
}
}
在上面的示例中,变量按以下顺序初始化:静态变量首先
x
使用
y
默认值初始化。接下来,执行静态初始化块。然后将变量初始化
i
为默认值并初始化变量
z
。接下来,设计师开始工作。调用类构造函数不应依赖于声明字段的顺序。这可能会导致错误。
构造函数和继承
构造函数不被继承。例如:
public class Example
{
Example()
{
}
public void sayHi()
{
system.out.println("Hi");
}
}
public class SubClass extends Example
{
}
该类自动继承父类中定义的
SubClass
方法。
sayHi()
同时,
Example()
父类的构造函数不会被其后代继承
SubClass
。
this
构造函数中的关键字
构造函数用于
this
引用同一类中的另一个构造函数,但具有不同的参数列表。如果构造函数使用关键字
this
,则它必须位于第一行;忽略此规则将导致编译器错误。例如:文件
ThisDemo.java
public class ThisDemo
{
String name;
ThisDemo(String s)
{
name = s;
System.out.println(name);
}
ThisDemo()
{
this("John");
}
public static void main(String args[])
{
ThisDemo td1 = new ThisDemo("Mary");
ThisDemo td2 = new ThisDemo();
}
}
程序的输出是:
Mary
John
在此示例中有两个构造函数。第一个接收字符串参数。第二个不接收任何参数,它只是使用默认名称“John”调用第一个构造函数。因此,您可以使用构造函数显式地默认初始化字段值,这在程序中通常是必要的。
super
构造函数中的关键字
构造函数用于
super
调用超类构造函数。如果构造函数使用
super
,则此调用必须位于第一行,否则编译器将抛出错误。下面是一个示例: 文件
SuperClassDemo.java
public class SuperClassDemo
{
SuperClassDemo()
{
}
}
class Child extends SuperClassDemo
{
Child()
{
super();
}
}
在这个简单的示例中,除了类之外, 构造
Child()
函数还包含
super()
一个创建类实例的调用。因为它必须是子类构造函数中执行的第一条语句,所以这个顺序始终相同,并且不依赖于是否. 如果不使用,则从基类开始,将首先执行每个超类的默认(无参数)构造函数。以下程序演示了何时执行构造函数。文件
SuperClassDemo
Child
super
super()
Call.java
class A
{
A()
{
System.out.println("Inside A constructor.");
}
}
class B extends A
{
B()
{
System.out.println("Inside B constructor.");
}
}
class C extends B
{
C()
{
System.out.println("Inside C constructor.");
}
}
class Call
{
public static void main(String args[])
{
C c = new C();
}
}
该程序的输出:
Inside A constructor.
Inside B constructor.
Inside C constructor.
构造函数按类从属顺序调用。这有一定道理。由于超类不知道任何子类,因此它需要执行的任何初始化都是单独的。如果可能,它应该先于子类执行的任何初始化。这就是为什么应该首先完成它。
可定制的构造函数
运行时类型识别机制是Java语言实现多态性的强大核心原理之一。然而,在某些情况下,这种机制并不能保护开发人员免受不兼容类型转换的影响。最常见的情况是操作一组对象,这些对象的各种类型事先是未知的,并在运行时确定。由于与类型不兼容相关的错误只能出现在运行时阶段,因此很难发现和消除它们。Java 2 5.0 中引入的自定义类型将其中一些错误从运行时转移到编译时,并提供了一些缺失的类型安全性。从类型转换为具体类型时,不需要
Object
显式类型转换。应该记住,类型定制工具仅适用于对象,不适用于位于类继承树之外的原始数据类型。对于自定义类型,所有转换都是在幕后自动执行的。这使您可以防止类型不匹配并更频繁地重用代码。自定义类型可以在构造函数中使用。构造函数可以是自定义的,即使它们的类不是自定义类型。例如:
class GenConstructor
{
private double val;
<T extends Number> GenConstructor(T arg)
{
val = arg.doubleValue();
}
void printValue()
{
System.out.println("val: "+val);
}
}
class GenConstructorDemo
{
public static void main(String args[])
{
GenConstructor gc1 = new GenConstructor(100);
GenConstructor gc2 = new GenConstructor(123.5F);
gc1.printValue();
gc2.printValue();
}
}
由于构造函数
GenConstructor
指定了一个自定义类型参数,该参数必须是 class 的派生类
Number
,因此可以从任何
GO TO FULL VERSION