Java中的构造函数


定义和属性

我们已经知道构造函数被认为是一种特殊的 方法,从本质上讲,它特别适合于实例变量的初始化。构造函数的一个简单示例是以下类中包含的一个:

public class Book {
    private String title;
    public Book(String title) {
        this.title = title;
    }
    public String getTitle () {
        return title;
    }
}

请注意,虽然使用点运算符(“ .”符号)来调用方法:

String bookTitle = book.getTitle();

构造函数是通过new运算符调用的 :

Book book = new Book("Java for Aliens");

特别是,与普通方法不同,构造函数具有以下特征:

  • 与它所属的类同名;
  • 没有返回类型;
  • 每当实例化该对象所属的类的对象时,该对象便会自动(且仅)被调用;
  • 在每个班级都有。

最后一点将在下一部分中阐明。

默认构造函数

每个编译的类中始终都有一个构造函数。实际上,即使一个类没有显式声明构造函数,编译器也会在字节码内自动添加所谓的默认构造函数。该默认构造函数中只包含一个声明,我们将在稍后讨论,也没有参数。但是显式声明的无参数构造函数不是默认构造函数。例如,以下类中的构造函数不是默认构造函数,而只是无参数构造函数:

java

public class Book {
    public Book() {
        System.out.println("Parameterless constructor (non-default)");
    }
}

在这种情况下,编译器将不会添加默认构造函数,因为此类中已存在显式构造函数。请注意,编译器的决定不取决于构造函数中的参数数量,而仅取决于是否存在显式构造函数。例如,即使在本文的第一个示例中,也不会添加默认构造函数。实际上,编译器添加了默认构造函数,仅允许使用new运算符实例化该类。这样,可以在不了解构造函数概念的情况下学习面向对象的程序设计,并且可以使学习曲线在初期不那么陡峭。

继承 和构造函数

继承不适用于构造函数。即使将它们声明为public,构造函数也不会因为非常简单的原因而被继承:它们自己的名称。例如,让我们考虑JavaBook该类的以下类扩展Book:

public class JavaBook extends Book {
}

如果要继承构造函数,则JavaBook该类将继承一个名为的构造函数Book。实际上,就好像JavaBook该类是通过以下方式编写的:

java

public class JavaBook extends Book {
    public Book() {
        System.out.println("Parameterless constructor (non-default)");
    }
}

但是Book,JavaBook永远不能调用在名为的类中调用的构造函数!实际上,要从JavaBook类中实例化一个对象,必须必须调用JavaBook使用new运算符调用的构造函数。

例如,如果我们要实例化一个JavaBook对象,我们将编写:

java

JavaBook javaBook = new JavaBook();

JavaBook用new运算符调用类的构造函数。

现在假设我们继承Book了JavaBook该类中的构造函数。为了使用此构造函数,我们应该能够编写以下语句:

java

JavaBook  javaBook  =  new  Book();

但是,这意味着将创建Book类的对象,而不是类的对象,这JavaBook也会导致编译错误,因为无法将JavaBook类型引用分配给Book类型对象。实际上,遵循以下输出:

shell

error:incompatible types:Book cannot be converted to JavaBook
            JavaBook javaBook = new Book();

构造函数与范式 构造函数不是从子类继承的事实与该语言的语法一致,但同时又与面向对象编程的原理相矛盾。尤其是,似乎违反了重用和抽象的范式。实际上,当开发人员决定实施继承机制时,他不得不使用所谓的“是一种关系”来测试其有效性。问题是:“是否可以将由候选子类实例化的对象也视为候选超类的对象?” 他确实的回答是肯定的。

在这种特殊情况下,Java书籍就是一本书,因此它必须具有书籍的所有特征。特别是,构造函数也必须被重用。无法继承它,但是似乎违反了抽象。相反,正是在这种情况下Java证明了其一致性。实际上,我们可以向构造函数定义添加另一个属性:

任何构造函数(甚至是默认构造函数)都始终在其第一条语句中调用超类构造函数。 例如,让我们在Book和JavaBook类中添加一些便利的构造函数

java

public class Book {
    public Book(){
        System.out.println("Book created!");
    }
}
public class JavaBook extends Book {
    public JavaBook() {
        System.out.println("Java Book created!");
    }
}

读者了解到构造函数没有继承之后,应该JavaBook通过以下类型的语法得出结论:a的实例:

java

new JavaBook(); /* The assignment of a reference is not
   mandatory to instantiate an object */

将输出以下字符串:

shell

Java Book created!

输出将改为:

shell

Book created!
Java Book created!

实际上,JavaBook构造函数首先调用超类的构造函数,Book然后执行。使用super关键字对超类构造函数进行强制调用,该关键字将在下面介绍。

构造函数,super关键字和编译器隐藏的工作

如果this关键字表示对当前对象的隐式引用,则可以将super关键字定义为对当前对象与其超类之间的交集的隐式引用。该引用使我们可以访问超类的组件,尤其是其构造函数。

实际上,在每个构造函数中,总是通过使用super关键字的特殊语法来调用超类的构造函数,该语法用作将参数传递给该关键字的方法。同样,如果没有显式存在该指令,则将在编译时将其添加到字节码中。例如,在JavaBook该类中,构造函数将由编译器进行如下修改:

java

public class JavaBook extends Book {
    public JavaBook() {
        // super(); //instruction added if not explicitly provided.
        System.out.println("Java Book created!");
    }
}

这就是为什么Book类的构造函数调用类的(无参数的)构造函数的原因JavaBook。我们可以对super显式进行调用,但是如果不这样做,则编译器将隐式添加this语句。因此,对超类构造函数的调用是不可避免的。

请注意,Book 该类的构造函数 还将Object通过 在编译时隐式添加的 语句来调用其超类的构造函数 super。

现在假设我们要修改超类,Book如本文第一个示例所示:

java

public class Book {
    private String title;
    public Book(String title) {
        this.title = title;
    }
    public String getTitle () {
        return title;
    }
}

该类可以成功编译,但是其JavaBook子类不再:

2.png

因此,在修改超类时,我们在子类中引入了一个错误。但是,通过解释编译器的错误消息,就可以猜测出发生了什么:这一切都取决于编译器的隐藏工作。

实际上,子类构造函数通过隐式super()指令试图调用不存在的超类的构造函数:没有参数的构造函数。实际上,由于我们已经在Book类中显式添加了一个构造函数,因此编译器的默认构造函数将不再隐式添加到该类中。但是,编译器已插入不带超类参数的构造函数调用Book,作为子类构造函数的第一条语句JavaBook(请参见代码)。但是现在Book超类中的无参数构造函数(这是默认的构造函数)消失了。

为了解决此问题,最好的解决方案似乎是JavaBook按如下所示修改类:

java

public class JavaBook extends Book {
    public JavaBook(String title) {
        super(title);
    }
}

使用前面的语法,我们已显式调用了超类构造函数,该构造函数将字符串作为输入。

如果超类中有多个构造函数,则可以选择要调用的构造函数。例如,如果Book该类按如下方式定义了多个构造函数:

java

public class Book {
    public Book(String title, String author) {
        this(title); // Call to the second constructor (see next section)
        setAuthor(author);
    }
    public Book(String title) {
        this.title = title;
    } 
    // rest of code omitted
}

本JavaBook类可以称之为最适合的构造取决于具体情况:

java

public class JavaBook extends Book {
    public JavaBook(String title) {
        super(title);
    }
    public JavaBook(String title, String author) {
        super(title, author);
    }
    // rest of code omitted
}

重载,构造函数和this关键字

现在我们知道可以使用super关键字调用超类的构造函数,但实际上我们也可以使用this关键字来调用同一类的构造函数。这是构造函数重载的简单示例,该示例使用this关键字作为对同一类的构造函数方法的调用:

java

public class Customer {
    private String name;
    private String address;
    private int phoneNumber;
    public Customer() {
      //constructor explicitly inserted (this is not the default constructor)
    }
    public Customer(String name) {
        this.name = name;
    }
    public Customer(String name, String address) {
        this(name);
        this.address = address;
    }
    public Customer(String name, String address, int phoneNumber) {
        this(name, address);
        this.phoneNumber = phoneNumber;
    }
    // rest of code omitted
}

请注意,构造函数如何相互调用,避免重复已编写的代码。

这样,重用范式将受到青睐。

澄清构造函数之间如何相互调用

通过super关键字对超类构造函数的调用或通过this关键字对相同类的构造函数的调用只能用作构造函数中的第一条指令。这意味着在构造函数之间super或之间this将仅出现一条指令。如果我们显式添加该this语句,则该super语句不能放在同一构造函数中。但是,请注意,如果一个构造函数使用该this命令调用另一个构造函数,则该另一个构造函数又会通过另一个this语句来调用第三个构造函数,或者使用以下命令来调用超类构造函数:super陈述。简而言之,在任何情况下迟早都会调用超类构造函数。例如,以下类表示要在编辑器中使用的字符(字体):

java

public class Character {
    private String type;
    private int size;
    public Character (String type) {
        this(type, 12);
    }
    public Character (String type, int size) {
        //Here a super() invocation will be inserted from the compiler
        setType(type);
        setSize(size);
    }
    //Accessor and mutator methods omitted...
}

声明第一个构造函数,该构造函数使用this()语句调用第二个构造函数。但是,在第二个构造函数中,编译器将该super()语句显式添加为第一条语句。对超类构造函数(在本例中为Object类)的调用仅被推迟。

我们甚至不能使用普通方法中的super or 关键字来调用构造函数 。只有构造函数可以使用这些指令。this

替代和构造函数

我们知道没有构造函数继承,但是由于该super语句,子类的实例化将始终导致对超类构造函数的调用。无论如何,由于没有继承,所以说构造函数的覆盖或构造函数多态是没有意义的。但是,存在一种情况,该情况涉及构造函数以及关于属性初始化的执行顺序,这些情况将被考虑在内,下面将对此进行说明。

由于该场景在摘要中说明有些复杂,因此让我们进入代码示例的上下文。让我们考虑以下抽象类Tool:

java

abstract class Tool {
    public Tool () {
        doWork();
    }
    public void doWork() {
        System.out.println("Working...");
    }
}

此类声明一个构造函数,该构造函数调用doWork同一类中定义的方法。现在考虑以下Hammer类扩展Tool和重写doWork方法:

java

class Hammer extends Tool {
    String data ="nail";
    public Hammer () {
        //implicit call to super();
    }
    @Override
    public void doWork() {
        System.out.println("Hammering on "+ data);
    }
}

最后,考虑以下实例化该Hammer子类的类:

java

public class ToolsTest {
    public static void main(String[] args) {
        Tool tool = new Hammer();
    }
}

这些类已正确编译,但是通过启动ToolsTest该类,我们将在运行时获得以下输出:

shell

Hammmering on nill

实际上,该data变量是在子类中可以初始化之前使用的,因此仍具有一个null值。特别是,当我们调用Hammer该类的构造函数时,作为第一条(隐式)指令,将Tool调用超类的构造函数,而该类又直接调用该doWork方法,该方法将在data尚未初始化变量时打印消息。

在这种情况下,我们只会得到不正确的输出,但是实际上,使用带有null值的引用很容易导致NullPointerException类型异常中断运行时。例如,如果我们doWork按如下方式修改子类中的方法:

java

public void doWork() {
    if (!data.isEmpty()) {
        System.out.println("Hammering on "+ data);
    }
}

然后程序将输出以下输出:

1.png

结论 在本文中,我们已经看到了诸如构造函数之类的语言的基本参数实际上是如何隐藏甚至复杂的场景的。像往常一样,拥有扎实的理论基础将使我们能够毫无意外地管理所有用例。使用关键字thisand super,即使使用构造函数,我们也可以正确管理重用和抽象范式,尽管它们不是继承的,因此无法在子类中重写它们。我们还看到了了解编译器行为的重要性。实际上,尽管在字节码中插入隐式代码的目的是为了促进学习并减少语言的冗长性,但实际上,如果不正确地掌握,它们也可能导致代码故障。


原文链接:http://codingdict.com/