编译环境

  • Windows 10 Pro
  • jdk1.8.0_91

如果没有特殊说明,下面程序都有javac、java的编译运行过程。

构造方法的特点

  • 方法名与类名相同。
  • 不用定义返回值类型。
  • 没有具体的返回值。

此次整理是按照知识点具体为例子的方式。

当没有写构造方法时,Java虚拟机默认会创建一个空构造

ClassDemo1.java

class ClassDemo1{
    public static void main(String[] args){
        //new 关键字后面跟的必须是构造方法
        Car car = new Car();
    }
}

class Car{
    String color = "black" ;
    int tires ;
    //类中没有写构造方法
    void run(){
		System.out.println(color + " , run....");
	}
}

复制代码

编译运行通过。

这是为什么呢?

原因就在,于Java虚拟机(这里是 HotSpot)在没有构造方法的情况下创建了空构造。为了说明一点,我们使用JDK(Java SE Development Kit) 提供的开发工具javap查看。

javap
用法: javap <options> <classes>
其中, 可能的选项包括:
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置
复制代码
javap -p Car.class
复制代码
Compiled from "ClassDemo1.java"
class Car {
  java.lang.String color;
  int tires;
  Car();//请注意这里,方法名与类名相同,没有返回值;这就可以说明jvm在没有构造方法的情况下,默认创建了空构造
  void run();
}
复制代码

其次,我们还需要检验,看下例。

当类有写其他构造方法时,而没有空构造时,java虚拟机不会创建空构造

这一次由于写在同一个文件里,编译通不过,所以先将Car类写出来,先编译通过。

Car.java

class Car{
    String color = "black" ;
    int tires ;
	public Car(String color){  //类中有没有空构造,但有其他构造
		this.color = color;
	}
    void run(){
		System.out.println(color + " , run....");
	}
}
复制代码

ClassDemo2.java


class ClassDemo2{
    public static void main(String[] args){
        Car car = new Car(); //调用空构造
    }
}

ClassDemo2.java 编译没有通过,输出信息


ClassDemo2.java:3: 错误: 无法将类 Car中的构造器 Car应用到给定类型;
        Car car = new Car();
                  ^
  需要: String
  找到: 没有参数
  原因: 实际参数列表和形式参数列表长度不同
1 个错误

这也验证了我们所说的当类有写其他构造方法时,而没有空构造时,java虚拟机不会创建空构造。

再来看看Car类中的变量和方法^ - ^


Compiled from "Car.java"
class Car {
  java.lang.String color;
  int tires;
  public Car(java.lang.String); //没有空构造
  void run();
}

所以,我们得显式地加上空构造。

需要说明的是构造方法的访问控制符,不仅可以是public,可以缺省,还可以是private。单例设计模式中就是private,这点会在后面讲述。

构造方法没有返回值

为了突出这点,我们写成下面这样。

ClassDemo4.java


class ClassDemo4{
	public static void main(String[] args){
		Car c1 = new Car();
		c1.run();
	}
}
class Car{
	String color = "black" ;
	int tires ;
	public Car(){
		System.out.println("new Car()");
	}

	public Car Car(String color){ //编译运行通过了,且方法名与类名相同,但这是构造方法吗?
		this.color = color ;
		Car c = new Car();
		return c;
	}

	void run(){
		System.out.println(color + " , run....");
	}
}

public Car Car(String color) 这仅仅是普通方法,仅仅是像构造方法;

构造方法用来初始化对象,new后面跟的一定是构造方法,new出来就要获得,是不需要显式地注明返回值的。

同样的,如果你像下面这样写,在构造方法中用上表示函数结束的return,编译运行可以通过,但显得多此一举。

ClassDemo5.java

class ClassDemo5{
    public static void main(String[] args){
        Car car = new Car();
    }
}

class Car{
    String color = "black" ;
    int tires ;
	public Car(){
	    System.out.println("new Car()");
		return; // ?!类似返回值void!?
	}
    void run(){
		System.out.println(color + " , run....");
	}
}

构造方***和普通方法重载吗?

重载就是,函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。

但是问题来了,下面两个方法可以构成重载吗?或者说,c3.Car("white")调用的是下面哪个?

  • A. ①
  • B. ②

ClassDemo7.java

class ClassDemo7{
	public static void main(String[] args){
		Car c1 = new Car();
		c1.run();
		Car c2 = new Car("white");
		c2.run();
		Car c3 = new Car("white");
		c3.Car("white"); // 这个方法调用的是下面哪个
	}
}
class Car{
	String color = "black" ;
	int tires ;
	public Car(){
		System.out.println("new Car()");
	}

	public Car(String color){ // ① 
		this(); //this()必须是第一行。构造方法调用构造方法,这样解决了构造方法的复用。
		this.color = color ;
		System.out.println("this car's color is "+color);
	}
	
	public Car Car(String color){ // ②
		this.color = color ;
		Car c = new Car();
		return c;
	}
	void run(){
		System.out.println(color + " , run....");
	}
}

编译运行是通过的,调用的是 ② ;

输出信息


new Car()
black , run....
new Car()
this car's color is white
white , run....
new Car()
this car's color is white
new Car()

从这个问题延伸出下面这个问题。

构造方法只能通过new来调用吗?

ClassDemo8.java


class ClassDemo8{
	public static void main(String[] args){
		Car c1 = new Car();
		c1.Car("white"); //可以调用构造方法吗?
	}
}
class Car{
	String color = "black" ;
	int tires ;
	public Car(){
		System.out.println("new Car()");
	}

	public Car(String color){
		this();
		this.color = color ;
		System.out.println("this car's color is "+color);
	}
	
	void run(){
		System.out.println(color + " , run....");
	}
}

并不能,编译就通不过。

ClassDemo8.java:4: 错误: 找不到符号
		c1.Car("white");
		  ^
  符号:   方法 Car(String)
  位置: 类型为Car的变量 c1
1 个错误

说明构造函数只能由new关键字调用。

错误拾遗

  1. 根据java规范,类名的首字母是大写的,构造方法名可以小写吗? 当然不行,java是严格区分大小写的。构造方法的特点之一就是"方法名与类名相同".
  2. 构造方法没有返回值 下面这个例子是具有迷惑性的,编程的时候需要留意。 ClassDemo3.java class ClassDemo3{ public static void main(String[] args){ Car car = new Car(); } } class Car{ String color = "black" ; int tires ; public void Car(){ System.out.println("haha"); } void run(){ System.out.println(color + " , run...."); } } 复制代码 什么输出都没有,正奇怪呢。可这是构造方法吗?! javap一看,两个方法构成了重载。 Compiled from "ClassDemo3.java" class Car { java.lang.String color; int tires; Car(); //jvm创建的构造方法 public void Car();//编写的方法 void run(); } 复制代码
  3. 构造方法中可以写构造方法吗? Congratulations!当你这样写时,虽然编译通过了,但栈溢出了,相当于递归没有终止条件(除非你加了终止条件,不过你真会倒腾)。 ClassDemo6.java class ClassDemo6{ public static void main(String[] args){ Car car = new Car(); } } class Car{ String color = "black" ; int tires ; public Car(){ new Car(); } void run(){ System.out.println(color + " , run...."); } } 复制代码 错误类型:StackOverflowError Exception in thread "main" java.lang.StackOverflowError 复制代码

构造方法的使用

前面提到多个构造方法可以重载,构造方法和普通方法间并不构成重载。

构造方法调用构造方法

那么构造方法如何调用构造方法呢?其实,就是使用使用this语句,this()、this(xxx,xxx)实现对构造方法的重用。

ClassDemo9.java

class ClassDemo9{
	public static void main(String[] args){
		Car c1 = new Car();
		c1.run();
		Car c2 = new Car("white");
		c2.run();
		Car c3 = new Car("red",4); //多个构造方法重载,重载根据形参列表区分
		c3.run();

		System.out.println(Benz.getBrand());
	}
}
class Car{
	String color = "black" ;
	int tires ;
	public Car(){
		System.out.println("new Car()");
	}

	public Car(String color){
		this();// this语句必须放在第一行
		this.color = color ;
		System.out.println("this car's color is "+color);
	}

	public Car(String color,int tires){
		this(color); // this语句必须放在第一行;体现对构造方法的重用
		this.tires = tires;
		System.out.println("this car's color is "+color+" and it has "+tires+" tires");
	}
	
	void run(){
		System.out.println(color + " , run....");
	}
}

//public -- private
class Benz extends Car{ //奔驰 继承自 车
	private static String BRAND = "BENZ" ;//商标

	public static String getBrand(){
		return BRAND ;
	}
}

输出:

new Car()
black , run....
new Car()
this car's color is white
white , run....
new Car()
this car's color is red
this car's color is red and it has 4 tires
red , run....
BENZ

this语句必须放在第一行,this()、this(color)都是this语句; 但注意区别于this关键字,this代表其所在函数所属对象的引用。

说到this就得说super,这点在后面继承中详细说明。

我们用javap看看ClassDemo9的class字节码。


javap ClassDemo9

发现static {};是静态代码块,说明java静态成员变量的声明是解释成字节码的时候是作为一个静态代码块的。


Compiled from "ClassDemo9.java"
class Benz extends Car {
  Benz();
  public static java.lang.String getBrand();
  static {}; //静态代码块
}

这里谈一谈java中的代码块,因为也与理解构造函数有关。

构造方法与构造代码块和静态代码块

java中有4种代码块:普通代码块、静态代码块、构造代码块、同步代码块。

ClassDemo10.java


class ClassDemo10{

    public static void main(String[] args){
        Car car = new Car();
    }
}

class Car{
    String color = "black" ;
    int tires ;
	Car(){ // 构造方法
		System.out.println("new Car()");
	}

	{   // 构造代码块
		System.out.println("world");
	}

	static{ // 静态代码块
		System.out.println("hello");
	}
	
    void run(){
		System.out.println(color + " , run....");
	}
}

问题来了?构造方法与构造代码块和静态代码块 的执行顺序是什么呢。

我们编译运行一下。

hello
world
new Car()

说明三者的执行顺序是① 静态代码块,② 构造代码块,③ 构造方法。

需要说明的是静态代码块在类加载完成时,构造代码块和构造方法在对象创建时完成,且构造代码块在构造方法之前完成。

静态代码块和构造代码块都可以为对象创建做好初始化准备。如创建数据库连接,定义变量并赋值等。

那么创建多个对象时,它们的执行次数是什么情况呢?

ClassDemo11.java


class ClassDemo11{

    public static void main(String[] args){
        Car car1 = new Car();
		Car car2 = new Car();
		Car car3 = new Car();
    }
}

class Car{
    String color = "black" ;
    int tires ;
	Car(){
		System.out.println("new Car()");
	}

	{
		System.out.println("构造代码块");
	}

	static{
		System.out.println("静态代码块");
	}
	
    void run(){
		System.out.println(color + " , run....");
	}
}

输出:


静态代码块
构造代码块
new Car()
构造代码块
new Car()
构造代码块
new Car()

静态代码块只在最初执行一次,构造代码块和同步代码块在对象创建时执行。

通过继承理解构造方法

在堆空间里,一个对象的创建包含了整个家族树的创建。

所有类都是Object的子类。抽象类也一定有构造方法,但抽象类不能实例化。

下面例子中Dog类继承了Animal,Jing8继承了Dog。

ClassDemo12.java


class ClassDemo12{
	//static Jing8 d = new Jing8();
	public static void main(String[] args){
		Jing8 d = new Jing8();
		//d.blood = "B"; //fina变量不能被修改
		d.run();
	}
}
class Animal{
	String category ;
	public Animal(String c){
		System.out.println("new Animal()");
	}
	public final void run(){
		System.out.println("run...");
	}
}

class Dog extends Animal{
	String color ;
	public Dog(){
		super("kkk");
		System.out.println("new Dog()");
	}
}

final class Jing8 extends Dog{ //final

	final String blood = "A";
	public Jing8(){
		System.out.println("new Jing8()");
	}
}

现在问题来了,是先输出new Jing8(),然后输出new Dog(),最后输出new Animal()呢;还是先输出new new Animal(),然后输出new Dog(),最后输出new Jing8()呢?

其实堆内存中,子类调用父类构造函数是一个入栈的过程,到Object结束,JVM的栈空间中调用方法区的方法其实都是栈操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i7IEccjo-1628264707193)(7xueea.com1.z0.glb.clouddn.com/image/png/n…)]

这么来说,就是如下输出:

new Animal()
new Dog()
new Jing8()
run...

这里面,有super(),类似this()调用本类的构造函数,super()调用父类的构造函数。而super是指向父类的引用,是父类在内存中的标识。

需要说明的是,构造函数的第一行,要么是this语句(包括this()或this(xxx)),要么是super语句(包括super()或super(xxx))。

ClassDemo13.java

class ClassDemo13{
	public static void main(String[] args){
		Son s1 = new Son();
		s1.eat();
		s1.run();
		Father father = new Father();
		s1.setAsset(1000);
		System.out.println(s1.getAsset()); // 验证子类的内存中有从父类继承来的private asset变量
		System.out.println(father.getAsset());// 子类输出的值和父类输出的值不同,
		//就验证了private asset变量的内存存储位置一定不同,
		//也就negotiation说明子类的内存中有父类private变量和方法
	}
}

class Father{
	String name ;
	private int asset=10000 ;//资产
	
	private void eat(){
		System.out.println("洗手!");
	}
	public int getAsset(){
		return this.asset ;
	}
	public void setAsset(int asset){
		this.asset = asset ;
	}
	void walk(){
		System.out.println("walk...");
	}
}
class Son extends Father{

    /*
    //这里没有写构造函数,
    //① 默认创建的空构造,
    //② 构造函数的第一行默认调用的是super();
	public Son(){
		//super();
	}
	*/
	public void run(){
		super.walk();//调用父类方法
	}
	//复写 或 覆盖
	public void walk(){
	}
	/*
	//子类默认还继承了父类的非私有变量和非私有方法。
	//但并不能“继承”私有方法和私有变量,
	//需要注意的是,继承的私有方法和私有变量仍然在子类创建的内存中!!
	public int getAsset(){
		return this.asset ;
	}
	public void setAsset(int asset){
		this.asset = asset ;
	}
	*/
	public void eat(){
		//super.eat();//尝试复用父类方法,但失败,
		//原因是父类的eat()方法是private私有,没有被继承,自然不能被引用
		System.out.println("洗澡!"); //再添加上子类特有方法
	}
}

子类默认还继承了父类的非私有变量和非私有方法。但并不能“继承”私有方法和私有变量。需要注意的是,extends"继承"的实质是将父类对象完全拷贝到子类对象的内存中,私有方法和私有变量仍然在子类创建的内存中!!

验证这个仅需看看,子类对象用从父类继承来的public int getAsset()和public void setAsset(int asset)才能调用private int asset变量,同时创建一个父类对象。比较两者asset值是否相同,即是否操作的是同一块内存空间。

让我们看看输出结果:

洗澡!
walk...
1000
10000

结果说明值正是不同的,说明子类的asset变量的内存地址一定与父类继承来的private asset变量的内存地址不同。子类"继承"了父类的私有变量和私有方法,extends的继承的实质是完全拷贝!

我们画一画堆内存图

JVM分配的堆空间默认是物理内存的1/4,栈空间默认是1M(有争议)。

从上面的继承的实质看,我们要尽量少些私有方法和私有方法,子类一定无法调用私有方法!应用中,在庞大复杂的继承系统里,这些块无用的内存空间拖慢的系统的运行效率。

内部类的构造

内部类可以定义在成员的位置上,也可以定义在方法中。

内部类访问实例变量,变量要用final修饰。


作者:掘金_陶嘉恒
链接:https://juejin.cn/post/6993344253870997535