黑马程序员-java中类的继承与组合

---------------------- ASP.Net+Unity开发.Net培训、期待与您交流! ----------------------      

      继承是实现类重用的重要手段,但继承带来了一个最大的坏处:破坏封装。相比之下,组合也是实现类重用的重要方式,而采用组合方式来实现类重用则能提供更好的封装性。下面将详细介绍继承和组合之间的联系和区别。

使用继承的确可以很方便的实现代码的复用,但与此同时也带来了一个非常严重的问题:子类破坏的父类的封装性。当访问权限足够的情况下,子类可以直接访问父类的Field,并修改父类的Field。这有违一个类被设计的初衷,我们设计一个类,其主要目的就是要封装内部的信息和实现细节,并且对外只暴露公有的方法来给外界使用。但是在继承的关系中,子类可以直接访问父类的Field和方法,从而造成了子类和父类的严重耦合。父类对于子类来说不再透明,子类可以通过重写父类的方法来恶意串改父类的方法实现。比如有一个Bird类,它对外有一个fly方法,还有一个Ostrich类,它继承了BIrd类,并重写了fly方法,则完全有可能出现这样的情况:

//定义一个Bird类
class Bird{
	public void fly(){
		System.out.println("鸟儿飞翔!");
	}
}
//定义一个Ostrich类继承自Bird
public class Ostrich extends Bird{
	
	public void fly() {
		// TODO Auto-generated method stub
		System.out.println("鸵鸟奔跑!");
	}
	public static void main(String[] args) {
		Bird b = new Ostrich();
		b.fly();
	}
}
代码输出如下:

鸵鸟奔跑!
      对于上面代码声明的Bird引用变量,因为实际引用一个Ostrich对象,所以调用b的fly方法时执行的不再是Bird类提供的fly方法,而是Ostrich类重写后的fly方法。

为了保证父类有良好的封装性,不会被子类随意的改变,设计父类通常要遵循以下规则:

1、尽量隐藏父类的内部数据。尽量把父类的所有Field都设置成private访问类型,不要让子类直接访问父类的Field。

2、不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符来修饰该方法。如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。

3、不要在父类构造器中调用将要被子类重写的方法。如下:

class B{
	public B(){
		test();
	}
	//类Btest方法
	public void test(){
		System.out.println("类B的test方法");
	}
}
public class A extends B{
	private String name;
	public A(){
		name = "aaaaaaa";
	}
	//类Atest方法
	public void test() {
		// TODO Auto-generated method stub
		System.out.println("类A重写类B的test方法" + "name字符串的长度:" + name.length());
	}
	public static void main(String[] args) {
		A a = new A();
	}
}
      此段代码运行时会产生 java.lang.NullPointerException异常,当系统试图创建类A的对象时,会先执行其父类的构造器,如果父类的构造器里面调用了被其子类重写的方法,则运行时会调用子类的方法。当创建a对象时,会先执行类B的构造器,而类B的构造器中调用了类A的test方法,而此时类A的对象a还为初始化Field,name为空,当试图打印name.length()时会引发空指针异常。

      因此,何时使用类的继承关系是一个很难把握的问题,比如说对于一个Animal类,有时可能会派生出BigAnimal和SmallAnimal两个类,如果从类的继承关系上讲,完全可以的,但如果从程序设计的角度来看,完全可以在Animal里面加入一个Size属性,用来表示Animal所生成对象的大小,完全没有必要派生出两个新类。

那么,到底何时才需要从父类派生新的子类呢?不仅需要保证子类是一种特殊的父类,而且需要具备以下两个条件之一:

1、子类需要增加额外属性,而不仅仅是属性值的改变。例如从Person类派生出Student子类,Person类没有提供grad属性,而Student需要grad属性来保存Student对象就读的年纪,这种父类到子类的派生,就符合继承的前提。

2、子类需要增加自己独有的行为方式,包括增加新的方法活重写父类的方法。例如从Person类派生出Teacher类,其中Teacher类需要增加一个teaching方法,该方法用于描述Teacher对象独有的行为方式:教学。

利用组合实现类的复用

      组合就是把旧类对象作为新类的Field嵌入,用以实现新类的功能,用户看到的是新类的方法,而不能看到被嵌入对象的方法。因此,通常需要在新类里使用private修饰被嵌入的旧类对象。假设有如下三个类,Animal类,Dog类,Bird类,它们之间有如下的嵌入关系:

class Animals {
	private void beat() {
		System.out.println("心脏跳动。。。");
	}

	public void breath() {
		beat();
		System.out.println("呼吸中。。。");
	}
}

class Dog {
	// 将Animal嵌入到类里面
	private Animals animals;

	public Dog(Animals animals) {
		this.animals = animals;
	}

	// 定义一个自己的breath()方法
	public void breath() {
		// 直接复用Animals提供的方法
		animals.breath();
	}

	public void Run() {
		System.out.println("陆地上奔跑");
	}
}

class Bird {
	// 将Animal嵌入到类里面
	private Animals animals;

	public Bird(Animals animals) {
		this.animals = animals;
	}

	// 定义一个自己的breath()方法
	public void breath() {
		// 直接复用Animal提供的方法
		animals.breath();
	}

	// 定义自己的fly方法
	public void fly() {
		System.out.println("天空里面飞翔!");
	}
}

public class Combination {
	public static void main(String[] args) {
		Dog dog = new Dog(new Animals());
		dog.breath();
		dog.Run();
		Bird bird = new Bird(new Animals());
		bird.breath();
		bird.fly();
	}
}

程序输出:

心脏跳动。。。
呼吸中。。。
陆地上奔跑
心脏跳动。。。
呼吸中。。。
天空里面飞翔!
     由此可以看出,此时的Dog对象和Bird对象有Animal对象组合而成,因此在上面的程序中创建Dog对象和Bird对象之前先创建了Animal对象,并利用这个Animal对象来创建Wolf对象和Bird对象。此时和Dog对和Bird对象继承Animal对象在运行结果上是没有区别的。

当然,使用组合和使用继承在系统开销上一样的,当创建一个子类对象时,系统不仅需要为该子类定义的Field分配内存空间,而且需要为它的父类所定义的Field分配内存空间。如果采用继承的设计方式,假设父类定义了2个Field,子类定义了3个Field,当创建子类实例时,系统需要为子类实例分配5块内存空间;如果采用组合的设计方式,先创建被嵌入实例,此时也需要分配2块内存空间,在创建整体类实例,也需要分配3块内存空间,只是需要多一个引用变量来引入被嵌入的对像。因此继承设计和组合设计在系统开销不会有本质上的区别

---------------------- ASP.Net+Unity开发.Net培训、期待与您交流! ----------------------详细请查看:www.itheima.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值