构造器是一个特殊的方法,(我们在前面的方法的定义和构造器的定义中就事先了解到了),这个特殊方法用于创建类的实例。JAVA语言里构造器是创建对象的重要途径(即使使用工厂模式、反射等方式创建对象,其实质是信赖于构造器),因此,JAVA类必须包含一个或一个以上的构造器
使用构造器执行初始化
构造器是最大的用处就是在创建对象时执行初始化。前面已经介绍过了,当创建一个对象时,系统为这个对象的属性进行默认初始化,这种默认初始化把所有基本类型的属性设为0(对数值属性)或false(对布尔型属性),把所有引用类型的属性设置为null.
如果我们想改变这种默认的初始化,想让系统创建对象时就为该对象各属性显式指定初始值,就可以通过构造器来实现。
如果程序员没有为JAVA类提供任何构造器,则系统会为这个类提供一个无参数的构造器,这个构造器的执行体为空,不做任何事情。无论如何,JAVA类至少包含一个构造器。
看代码:
public class ConstructorTest
{
public String name; //请大家注意下,有些代码并没有提供良好的封装
public int count;
//提供自定义的构造器,该构造器包含两个参数,,,构造器的名称必须和类名相同。
public ConstructorTest(String name , int count)
{
//构造器里的this代表它进行初始化的对象
//下面两行代码将传入的2个参数赋给this代表对象的name和count两个Field
//我们一定要记住,this在构造器中引用的是该构造器时行初始化的对象
this.name = name;
this.count = count;
}
//主方法入口
public static void main(String[] args)
{
//使用自定义的构造器来创建对象
//系统将会对该对象执行自定义的初始化
ConstructorTest tc = new ConstructorTest("世界!",12);
//创建第二个对象
ConstructorTest tc1 = new ConstructorTest("世界您好!",1200);
//输出ConstructorTest对象的name和count两个Field
System.out.println(tc.name);
System.out.println(tc.count);
//输出第二个对象
System.out.println(tc1.name);
System.out.println(tc1.count);
}
}
代码编译结果:
通过上面的程序我们可以看出,构造器是创建JAVA对象的重要途径,通过new 关键字调用构造器时,构造器也确实返回了该类的对象,但这个对象并不是完全由构造器负责创建的。实际上,当程序员调用构造器时,系统会先为该对象分配内存空间,并为这个对象执行默认初始化,这个对象已经产生了----这些操作都在构造器的执行体之前,系统已经创建了一个对象,只是这个对象还不能被外部程序访问,只能在该构造器中通过this来引用它。
当构造器的执行体结束后,这个对象作为构造器的返回值被返回,通常还会赋给另一个引用类型的变量,从而让外部程序可以访问该对象。
一旦程序员提供了自定义的构造器,则系统不再提供默认的构造器,因此上面的ConstructorTest类不能再通过new ConstructorTest()代码来创建实例,因为该类不再包含无参数的构造器。
如果用户希望该项类保留无参数的构造器,或者希望有多个初始化过程,则可以为该类提供多个构造器。如果一个类里提供了多个构造器,就形成了构造器的重载。
因此建议为JAVA类保留无参数的默认构造器。如果为一个类编写了有参数的构造器,通常建议为该类额外编写一个无参数的构造器。
因为构造器主要用于被其他方法来调用,用以返回该项类的实例,因而通常把构造器设置成public访问权限,从而允许系统中任何位置的类来创建该类的对象。除非在一些极端的情况下,我们需要限制创建该类的对象,可以把构造器设置成其他访问权限:例如设置为protected,主要用于被其子类来调用,把其设置为private阻止其他类创建该类的实例。
构造器的重载:
同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。构造器重载允许JAVA类里包含多个初始化逻辑,从而允许使用不同的构造器来初始化JAVA对象。
构造器重载和方法重载基本相似:要求构造器的名字相同,这一点无须特别要求,因为构造器必须与类名相同,所以同一个类的所有构造器名肯定相同。为了让系统能区分不同的构造器,多个构造器的参数列表必须不同。
看代码:
public class ConstructorOverload
{
public String name;
public int count;
//提供无参数的构造器
public ConstructorOverload()
{
}
//提供带两个参数的构造器,
//对该构造器返回的对象执行初始化
public ConstructorOverload(String name , int count)
{
this.name = name;
this.count = count;
}
//主方法入口
public static void main(String[] args)
{
//通过无参数构造器创建ConstructorOverload对象
ConstructorOverload oc1 = new ConstructorOverload();
System.out.println(oc1.name + "" + oc1.count);
//通过有参数构造器创建ConstructorOverload对象
ConstructorOverload oc2 = new ConstructorOverload("世界您好!",12000);
System.out.println(oc2.name + "" + oc2.count);
}
}
上面代码编译结果:
上面的ConstructorOverload类提供了2个重载的构造器,两个构造器的名字相同,但形参列表不同。
系统通过new调用构造器时,系统将根据传入的实参列表来决定调用哪个构造器。
我们再来考虑另外一种情况:
如果系统中包含了多个构造器,其中一个构造器执行体里完全包含另一个构造器的执行体。
如下图:
从图中可以看出,构造器B里完全包含了构造器A。
对于这种完全包含的情况,如果是两个方法之间存在这种关系,则可在方法B中调用方法A。但构造器不能直接被调用,构造器必须使用new关键字来调用。但一旦使用new关键字来调用构造器,将会导致系统重新创建一个对象。为了在构造器B中调用构造器A中的初始化代码,又不会重新创建一个JAVA对象,可以使用this关键字来调用相应构造器。
看代码:
//在一个构造器里直接使用另一个构造器的初始化代码:
public class Apple
{
//定义field
public String name;
public String color;
public double weight;
//定义无参构造器
public Apple()
{
}
//两个参数的构造器
public Apple(String name , String color)
{
this.name = name;
this.color = color;
}
//三个参数的构造器
public Apple(String name , String color , double weight)
{
//通过this调用另一个重载的构造器的初始化代码
this(name , color);
/ /下面this引用该构造器正在初始化的Java对象
this.weight = weight;
}
//下面我给出主方法入口,才能看看编译结果:
public static void main(String[] args)
{
//初始化不带参数的对象,并输出
Apple a = new Apple();
System.out.println("不带参数的构造器:"+a.name+" "+a.color+" "+a.weight);
//初始化带两个参数的对象,并输出
Apple a1 = new Apple("世界","red");
System.out.println("带两个参数的构造器:"+a1.name+" "+a1.color);
//初始化带三个参数的对象,并输出
Apple a2 = new Apple("世界","red",12);
System.out.println("带三个参数的构造器,并调用其它构造器初始化:"+a2.name+" "+a2.color+" "+a2.weight);
//构造器的this指向同一个类中,不同参数列表的另外一个构造器
}
}
编译结果:
上面的Apple类里包含了三个构造器,其中第三个构造器通过this调用来调用另一个重载构造器的初始化代码。程序中this(name,color); 调用表明调用该类另一个有2个字符串参数的构造器。
请记住,使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。使用this调用重载的构造器时,系统会根据this后括号里的实参来调用形参与之对应的构造器。
我们之所以要上面的代码那样写是有一定道理的。
这是因为我们从软件工程的角度来看,这样做是不利于代码的复用性,也是相当糟糕的。在软件开发里有一个规则:不要把相同的代码段书写两次以上!因为软件是一个需要不断更新的产品,如果有一天需要更新构造器A的初始化代码,假设构造器B、构造器C ....里都包含了相同的初始化代码,则需要同时打开构造器A、B、C.....的代码进行修改;反之,如果构造器B、C....是通过this调用了构造器A的初始化代码,则只需要打开构造器A进行修改即可。因此,尽量避免相同的代码重复出现,充分利用每一段代码,既可以让程序更加简洁,也可以为以后降低软件的维护成本。
前面写到过类的封装,是面向对象编程的三大特证之一,下面再写下一个特证:继承
继承是面向对象三大特征之一,也是实现软件复用的重要手段。JAVA的继承具有单继承的特点,每个子类只有一个直接父类。(这里我们在查JDK的开发文档时,也可以发现出来)
JAVA的继承通过extends 关键字来实现,实现继承的类被称为子类,被继承的类被称为父类,有的也称其为基类、超类。父类和子类的关系,是一种一般和特殊的关系。例如水果和苹果的关系,苹果继承了水果,苹果是水果的子类,则苹果是一种特殊的水果。
因为子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。
JAVA里子类继承父类的语法格式:
修饰符 class SubClass extends SuperClass
{
//类定义部分
}
从上面语法格式来看,定义子类的语法非常简单,只需在原来的类定义上增加extends SuperClass即可,即表明该子类继承了SuperClass类。
JAVA使用extends作为继承的关键字,extend关键字在英文是扩展,而不是继承!这个关键字很好地体现了子类和父类的关系;子类是对父类的扩展,子类是一种特殊的父类。从这个意义上来看,使用继承来描述子类和父类的关系是错误的,用扩展更恰当。因此这样说法更加准确:Apple 类扩展了Fruit 类。
那么为什么国内把extends翻译为继承呢?除了与历史原因有关之外,把extends翻译为继承也是有其理由的:子类扩展了父类,将可以获得父类的全部属性和方法。但JAVA的子类不能获得父类的构造器
看代码:
//子类继承父类
public class Fruit
{
public double weight;
public void info()
{
System.out.println("我是一个水果!重"
+ weight + "g!");
}
}
//下面定义Fruit 类的子类Apple
public class Apple extends Fruit
{
public static void main(String[] args)
{
//创建Apple的对象
Apple a = new Apple();
//Apple对象本身没有weight Field
//因为Apple的父类有weight Field,也可以访问Apple对象的Field
//如果没有赋初值,就会是系统默认的0
a.weight = 56; //通过继承子类初始化父类的field
//调用Apple对象的info方法
a.info();
}
}
看下上面两段程序的编译结果:
我们可以这样去理解:JAVA类只能有一个直接父类,但实际上,JAVA类可以有无限多个间接父类
如果定义一个JAVA类时并未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类。因此,java.lang.Object 是所有类的父类,要么是其直接父类,要么是其间接父类。因此所有JAVA对象都可调用java.langObject类所定义的实例方法。
从子类角度来看,子类扩展(extends)了父类;但从父类的角度来看,父类派生(derive)出了子类。也就是说,扩展和派生所描述的是同一个动作,只是观察角度不同而且已。
重写父类的方法:
子类扩展了父类,子类是一个特殊的父类。大部分时候,子类总是以父类为基础,再额外增加新的属性和方法。但有一种情况例外:子类需要重定父类的方法。例如鸟类都包含了飞翔的方法,其中鸵鸟是一种特殊的鸟类,因此鸵鸟应该是鸟类的子类,因此它也将从鸟类获得飞翔的方法,但这个飞翔的方法明显不适合鸵鸟,为此,鸵鸟需要重写鸟类的方法。
public class Bird //定义父类,也就是鸟类
{
//Bird类的fly方法
public void fly() //没有返回值,只简单输出一句话
{
System.out.println("我在天空里自由自在地飞翔...");
}
}
定义一个ostrich类,这个类扩展了Bird,重写了Bird类的fly方法
public class Ostrich extends Bird
{
//重写Bird类的fly方法
public void fly()
{
System.out.println("我只能在地上奔跑...");
}
public void callOverridedMethod() //和调用普通方法一样
{
//在子类方法中通过super来显式调用父类被覆盖的方法。
super.fly();
}
//下面为主方法的入口
public static void main(String[] args)
{
//创建Ostrich对象
Ostrich os = new Ostrich();
//执行Ostrich对象的fly方法,将输出"我只能在地上奔跑..."
os.fly();
}
}
编译结果:
从编译的结果中,我们看到执行os.fly()时执行的不再是Bird类的fly方法,而是执行Ostrich类的fly方法。
这种子类包含与父类同名方法的现象被称为方法重写(大家还记得吗?前面的一篇文章中提到过,《方法的重载》一定要区分两者的不同),方法重写,也被称为方法覆盖(Override)可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。
方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同,形参列表相同,“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常应比父类方法声明抛出的异常类更小或相等。“一大”指的子类方法的访问权限应父类方法更大或相等,尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但还可以在子类方法中调用父类中被覆盖的方法。如需要在子类方法中调用父类中被覆盖的方法,可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖方法是类方法)作为调用者来调用父类中被覆盖方法。
如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就无法重写该方法。如果子类中定久了一个与父类private方法具有相同方法名,相同形参列表,相同返回值类型的方法,依然不是重写,只是在子类中重新定久了一个新方法。
看代码:
class BaseClass
{
//test方法是private 访问权限,子类不可访问该方法
private void test()
{
}
}
class SubClass extends BaseClass
{
//此处并不是方法重写,所以可以增加static 关键字
public static void test()
{
}
}
父类实例的super引用
如果需要在子类方法中调用父类被覆盖的实例方法,可使用super作为调用者来调用父类被覆盖的实例方法。为上面的Ostrich 类添加一个方法,在这个方法调用Bird中被覆盖的fly方法:
public void callOverrideMethod()
{
/ /在子类方法中通过super显式调用父类被覆盖的实例方法。
super.fly();
}
通过callOverridedMethod 方法的帮助,就可以让Ostrich 对象既可以调用覆盖的fly方法,也可以调用Bird类中被覆盖的fly方法(调用callOverridedMethod方法即可)。
super 是JAVA提供的一个关键字,它是直接父类对象的默认引用。例如上面Bird 类中定义的fly 方法是
一个实例方法,需要通过Bird对象来调用该方法,而callOverriedMethod 方法通过super就可以调用这个方法,可见super引用了一个Bird 对象。
这里提醒大家注意: JAVA程序创建某个类的对象时,系统会隐式创建该项类父类的对象。只要有一个子类对象存在,则一定存在一个与之对应的父类对象。在子类方法中使用super引用时,super总是指向作为该方法调用者的子类所对应的父类的对象。其实,super引用和this引用很像,而super则指向this 指向对象的父类对象。 引用图:
正如this 不能出现在static 修饰的方法中一样,super也不能出现在static的方法中。static修饰的方法是属于类的,该方法的调用者可能是一个类,而不是对象,也就不存在对应的父对象了,因而super引用也就失去了意义。
也this引用类似的是,如果在构造器中使用super引用,则super引用指向该构造器正大初始化的对象所对应的父类对象。
方法重载和方法重写在英语中分别是overload 和 override ,因为重载主要发生在同一个类的多个同名方法之间(同类同名不同参),而重写发生在子类和父类的同名方法之间(两同两小一在)。它们之间的联系很少,除了二者都是发生在方法之间,并要求方法名相同之外。
父类方法和子类方法之间也可能发生重载,因为子类会获得父类方法,如果子类定义了一个父类方法有相同方法名,但参数列表不同的方法,就会形成父类方法和子类方法的重载。
如果子类定义了和父类同名的属性,也会发生子类属性覆盖父类属性的情形。正常情况下,子类里定义的方法,子类属性直接访问该属性,都会访问到覆盖属性,无法访问父类被覆盖的属性。
但在子类定义的实例方法中可以通过super来访问父类被覆盖的属性。
看代码:
class BaseClass
{
public int a = 5;
}
public class SubClass extends BaseClass
{
public int a = 7;
public void accessOwner()
{
System.out.println("子类重写父类的Field: "+a);
}
public void accessBase()
{
//通过super来限定访问从父类继承得到的a Field
System.out.println("调用被子类覆盖的父类Field: "+super.a);
}
//下面给出主方法入口
public static void main(String[] args)
{
SubClass sc = new SubClass();
//调用方法一
sc.accessOwner();
//调用方法二
sc.accessBase();
}
}
编译结果:
上面程序的BaseClass和SubClass 中都定义了名为a的实例属性,则SubClass的a实例属性将会覆盖BaseClass的a实例属性。当系统创建了SubClass对象时,系统会对应创建一个BaseClass对象,其中SubClass对象的a属性里为7,对应BaseClass对象的a属性为5。只是5这个数值只有在SubClass类定义的实例方法中使用super(JAVA关键字)作为调用者才可以访问到。
如果被覆盖的是类属性,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类属性。
如果子类里没有包含和父类同名的属性,则子类将可以继承到父类属性
如果在子类实例方法中访问该属性时,则无须显式使用super或父类名作为调用者。。(这个方法,读者可以自己试下,我就不写出来了,只需要在父类中多定义一个Field).
由此,如果我们在某个方法中访问名为a的属性,但没有显式指定调用者,系统查找a的顺序为:
》查找该方法中是否有名为a的局部变量
》查找当前类中是否包含名为a的属性
》查找a的直接父类中是否包含名为a的属性,依次上溯a的父类,直到java.lang.Object类,如果最终不能找到名为a的属性,则系统出现编译错误。
调用父类构造器
子类不会获得父类的构造器,但有的时候子类构造器里需要调用父类的构造器的初始化代码,就如前面所写的一个构造器需要调用另一个重载的构造器一样。
在一个构造器中调用另一个重载的构造器使用this调用来实现,在子类构造器中调用父类构造器使用super调用来实现
Base 和 Sub类,其中Sub 类是Base类的子类,程序在Sub类的构造器中使用super来调用Base 构造器里的初始化代码
class Base //定义父类
{
public double size;
public String name;
public Base(double size , String name) //创建构造器用于初始化对象的Field
{
this.size = size;
this.name = name;
}
}
public class Sub extends Base
{
public String color;
public Sub(double size ,String name, String color) //定义构造器,用于初始化子类Field
{
super(size,name); //通过super调用来调用父类构造器的初始化过程
this.color = color;
}
//下面为入口的主方法
public static void main(String[] args)
{
Sub s = new Sub(5.6,"世界您好!",“红色”);
//输出Sub对象的三个Field
System.out.println(s.size +"---"+s.name +"---" + s.color);
}
}
编译结果:
从上面程序中可以看出,使用super调用和使用this调用也很像,区别在于super调用的是其父类的构造器,而this调用的是同一个类中重载的构造器。因此,使用super调用父类构造器也必须出现在子类构造器执行体的第一行,所以this 调用和super调用不会同时出现。(如果读者不理解可以看这篇文章中的前面写到的内容)。
不管我们是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类的构造器一次,子类构造器调用父类构造器分如下几种情况:
》子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。
》子类构造器执行体的第一行代码使用this调用里传入的实参列表调用本类另一个构造器。执行本类中另一个构造器时即会调用父类的构造器。
》子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
不管上面哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行,不仅如此,执行父类构造器时,系统会再次上溯执行其父类的构造器....以此类推,创建任何JAVA对象,最先执行的总是java.lang.Object 类的构造器。
继承树:
对于上图,如果创建ClassB的对象,系统将先执行java.lang.Object类的构造器,再执行ClassA的构造器,然后才是执行ClassB类的构造器,这个执行过程还是最基本的情况。如果ClassB显式调用ClassA 的构造器,而该构造器又调用了ClassA类中重载的构造器,则会看到ClassA两个构造器先后执行的情形。
构造器之间的调用关系:
看代码:
class Creature //定义第一个类
{
public Creature()
{
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature //定义第二个类
{
public Animal (String name) //一个参数的构造器
{
System.out.println("Animal带一个参数的构造器,"
+ "该动物的name为" + name);
}
public Animal(String name, int age)
{
//使用this调用同一个重载的构造器,(同类同名不同参)
this(name);
System.out.println("Animal带两个参数的构造器,"
+ "其age为" + age);
}
public class Wolf extends Animal
{
public Wolf()
{
//显式调用父类有两个参数的构造器
super("灰太狼", 3);
System.out.println("Wolf无参数的构造器");
}
public static void main(String[] args)
{
new Wolf(); //创建一个Wolf对象
}
}
}
看下编译结果:
从编译结果可以看出main方法只创建了一个Wolf对象,但系统在底层则完成了复杂的操作。
从上面运行过程来,创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果某个父类通过this调用了同类中重载的构造器,就会依次执行此父类的多个构造器。
这时我们知道,自定义的类从未有显式调用过java.lang.Object类也只有一个默认的构造器可被调用。当系统执行java.lang.Object类的默认构造器时,该构造器的执行体并未输出任何内容,所以我们感觉不到调用过java.lang.Object类的构造器。
先写到这里吧,读者可以先理解上面提到的内容,有时可能会感觉理解起来困难时,请大家再看看前面的文章,或许就会有答案了。
联系客服