复用类(1):组合、继承

    复用代码是java众多引人注目的功能之一。但要想成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,它还必须能够做更多的事情。

    上述方法常为C这类过程型语言所使用,但收效不是很好。正如java中所有事物一样,问题解决都是围绕着类展开的。可以通过创建新类来复用代码,而不必再从头开始编写。可以使用别人已开发并调试好的类。

    此方法的窍门在于使用类而不破坏现有程序代码。有两种达到这一目的方法。第一种方法非常直观:只需在新的类中产生现有类的对象。由于新的类是由现有类的对象所组成的,所以这种方法称为组合。该方法只是复用了现有程序代码的功能,而非它的形式。

    第二种方法则更细致一些,它按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。这种神奇的方式称为继承,而且编译器可以完成其中大部分工作。继承是面向对象程序设计的基石之一。

    就组合和继承而言,其语法和行为大多是相似的。由于它们是利用现有类型生成新类型,所有这样做极富意义。接下来将会了解这两种代码重用机制。

一、组合语法

    组合,只需将对象引用置于新类中即可。例如,假设你需要某个对象,他要具有多个String对象、几个基本类型数据,以及另一个类的对象。对于非基本类型的对象,必须将其引用置于新的类中,但可以直接定义基类类型数据:

class WaterSource {
	private String s;

	WaterSource() {
		System.out.println("WaterSource()");
		s = "Construccted";
	}

	public String toString() {
		return s;
	}
}

public class SprinklerSystem {
	private String value1, value2, value3, value4;
	private WaterSource source = new WaterSource();
	private int i;
	private float f;

	public String toString() {
		return "value1 = " + value1 + " " + "value2 = " + value2 + " " + "value3 = " + value3 + " " + "value4 = "
				+ value4 + "\n" + "i = " + i + " " + " f = " + f + " source = " + source;
	}

	public static void main(String[] args) {
		SprinklerSystem sprinklers = new SprinklerSystem();
		System.out.println(sprinklers);
	}
}

    在上面两个类所定义的方法中,有一个很特殊:toString()。每一个非基本类型的对象都有一个toString()方法,而且当编译器需要一个String而你却只有一个对象时,该方法便会被调用。所以在SprinklerSystem的toString()的表达式中: " source = " + source;编译器将会得知你想要将一个String对象(“source = ”)同WaterSource相加。由于只能将一个String对象和另一个String对象相加,因此编译器会告诉你:“我将调用toString(),把source转成一个String!”这样做之后,他就能够将两个String连接到一起并将结果传递给System.out.println()。每当想要使所创建的类具备这样的行为时,仅需要编写一个toString()方法即可。

    类中域为基本类型时能够自动被初始化为零。但是对象引用会被初始化为null,而且如果你试图为它们调用任何方法,都会得到一个异常--运行时错误。很方便的是,在不抛出异常的情况下仍旧可以打印一个null引用。

    编译器并不是简单地为每一个引用都创建默认对象,这一点很有意义的,因为若真要那样做的话,就会在许多情况下增加不必要的负担。如果想初始化这些引用,可以在代码中的下列位置进行:

  1. 在定义对象的地方。这意味着它们总是能够在构造器被调用之前被初始化。
  2. 在类的构造器中。
  3. 就在正要使用这些对象之前,这种方式称为惰性初始化。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担。
  4. 使用实例初始化。

    以下是这四种方式的示例:

class Soap {
	private String s;

	Soap() {
		System.out.println("Soap()");
		s = "Constructed";
	}

	public String toString() {
		return s;
	}
}

public class Bath {
	private String // Initializing at point of definition;
	s1 = "Happy", s2 = "Happy", s3, s4;
	private Soap castille;
	private int i;
	private float toy;

	public Bath() {
		System.out.println("Inside Bath()");
		s3 = "Joy";
		toy = 3.14f;
		castille = new Soap();
	}

	// Instance initialization
	{
		i = 47;
	}

	public String toString() {
		if (s4 == null)// Delayed initialization
			s4 = "Joy";
		return "s1 = " + s1 + "\n" + "s2 = " + s2 + "\n" + "s3 = " + s3 + "\n" + "s4 = " + s4 + "\n" + "i = " + i + "\n"
				+ "toy = " + toy + "\n" + "castille = " + castille;
	}

	public static void main(String[] args) {
		Bath b = new Bath();
		System.out.println(b);
	}
}

    请注意,在Bath的构造器中,有一行语句在所有初始化产生之前就已经执行了。如果没有在定义处初始化,那么除非发生了不可避免的运行期异常,否则将不能保证信息在发送给对象引用之前已经被初始化。

    当toString()被调用时,它将填充s4的值,以确保所有的域在使用之时已被妥善初始化。

二、继承语法

    继承是所有OOP语言和Java语言不可缺少的组成部分。当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从Java的标准根类Object进行继承。

    组合的语法比较平实,但是继承使用的是一种特殊的语法。在继承过程中,需要先声明“新类与旧类相似”。这种声明是通过在类主体的左边花括号之前,书写后面紧随基类名称的关键字extends而实现的。当这么做时,会自动得到基类中所有的域和方法。例如:

class Cleanser {
	private String s = "Cleanser";

	public void append(String a) {
		s += a;
	}

	public void dilute() {
		append("dilute()");
	}

	public void apply() {
		append("apply()");
	}

	public void scrub() {
		append("scrub()");
	}

	public String toString() {
		return s;
	}

	public static void main(String[] args) {
		Cleanser x = new Cleanser();
		x.dilute();
		x.apply();
		x.scrub();
		System.out.println(x);
	}
}

public class Detergent extends Cleanser {
	// Change a method
	public void scrub() {
		append("Detergent的scrub()");
		super.scrub();
	}

	// Add methods to the interface
	public void foam() {
		append("foam()");
	}

	public static void main(String[] args) {
		Detergent x = new Detergent();
		x.dilute();
		x.apply();
		x.scrub();
		x.foam();
		System.out.println(x);
		System.out.println("Testing base Class:");
		Cleanser.main(args);
	}
}

    这个程序示范了java的许多特性。首先,在Cleanser的append()方法中,我们用“+=”操作符将几个String对象连接成s,此操作符是被java设计者重载用以处理String对象的操作符之一(另一个是“+”)。

    其次,Cleanser和Detergent均含有main()方法。可以为每个类都创建一个main()方法。这种在每个类中都设置一个main()方法的技术可使每个类的单元测试都变得简便易行。而且在完成单元测试之后,也无需删除main(),可以将其留待下次测试。

    即使是一个程序中含有多个类,也只有命令行所调用的那个类的main()方法会被调用。因此,在此例中,如果命令行是java Detergent,那么Detergent.main()会被调用。即使Cleanser不是一个public类,如果命令行是java Cleanser,那么Cleanser.main()仍然会被调用。即使一个类只具有包访问权限,其public main()仍然是可访问的。

    在此例中,可以看到Detergent.main()明确调用了Cleanser.main(),并将从命令行获取的参数传递给它。当然,也可以向其传递任意的String数组。

    Cleanser中所有的方法都必须是public的,这一点非常重要。请记住,如果没有加任何访问权限修饰词,那么成员默认的访问权限是包访问权限,它仅允许包内的成员访问。因此,在此包中,如果没有访问权限修饰词,任何人都可以使用这些方法。例如,Detergent就不成问题。但是,其他包中的某个类若要从Cleanser中继承,则只能访问public成员。所以,为了继承,一般的规则是将所有的数据成员都指定为private,将所有的方法指定为public(protected成员也可以借助导出类来访问)。当然,在特殊情况下,必须做出调整,但上述方法的确是一个很有用的规则。

    在Cleanser的接口有一组方法:append()、dilute()、apply()、scrub()和toString()。由于Detergent是由关键字extends从Cleanser导出的,所以它可以在其接口中自动获得这些方法,尽管并不能看到这些方法在Detergent中的显式定义。因此,可以将继承视作是对类的复用。

    正如我们在scrub()中所见,使用基类中定义的方法及对它进行修改是可行的。在此例中,你可能想要在新版本中调用从基类继承而来的方法。但是在surub()中,并不能直接调用scrub()中,并不能直接调用scrub(),因为这样做将会产生递归,而这并不是你所期望的。为解决此问题,java用super关键字表示超类的意思,当前类就是从超类继承来的。为此,表达式super.scrub()将调用基类版本的scrub()方法。

    在继承过程中,并不一定非得使用基类的方法。也可以在导出类中添加新方法,其添加方式与在类中添加任意方法一样,即对其加以定义即可,foam()方法即为一例。

    在Detergent.main()中会发现,对于一个Detergent对象而言,除了可以调用Detergent的方法(即foam())之外,还可以调用Cleanser中所有可用的方法。

三、初始化基类

    由于现在涉及基类和导出类这两个类,而不是只有一个类,所以要试着想象导出类所产生的结果对象,会有点困惑。从外部来看,它就像是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。但继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部。

    当然,对基类子对象的正确初始化也是至关重要的,而且也仅有一种方法来保证这一点:在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。java会自动在导出类的构造器中插入对基类构造器的调用。下例展示了上述机制在三层继承关系上是如何工作的:

class Art {
	Art() {
		System.out.println("Art constructor");
	}
}

class Drawing extends Art {
	Drawing() {
		System.out.println("Drawing constructor");
	}
}

public class Cartoon extends Drawing {
	public Cartoon() {
		System.out.println("Cartoon constructor");
	}

	public static void main(String[] args) {
		Cartoon x = new Cartoon();
	}
}

    你会发现,构建过程是从基类“向外”扩散的,所以基类在导出类构造器可以访问它之前,就已经完成了初始化。即使你不为Cartoon()创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类的构造器。

四、带参数的构造器

    上例中各个类均含有默认的构造器,即这些构造器都不带参数。编译器可以轻松地调用它们是因为不必考虑要传递什么样的参数的问题。但是,如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须用关键字super显式地编写调用基类构造器的语句,并且配以适当的参数列表:

class Game {
	Game(int i) {
		System.out.println("Game constructor");
	}
}

class BoardGame extends Game {

	BoardGame(int i) {
		super(i);
		System.out.println("BoardGame constructor");
	}

}

public class Chess extends BoardGame {

	Chess() {
		super(11);
		System.out.println("Chess constructor");
	}

	public static void main(String[] args) {
		Chess x = new Chess();
	}
}

    如果不在BoardGame()中调用基类构造器,编译器将“抱怨”无法找到符合Game()形式的构造器。而且,调用基类构造器必须是你在导出类构造器中要做的第一件事(如果你做错了,编译器会提醒你)。

 如果本文对您有很大的帮助,还请点赞关注一下。

发布了100 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104105706