《疯狂java讲义》学习(10):继承与组合

版权声明:本文为博主原创文章,如若转载请注明出处 https://blog.csdn.net/tonydz0523/article/details/86294846

类重现

继承是实现类重用的重要手段,但继承带来了一个最大的坏处:破坏封装。相比之下,组合也是实现类重用的重要方式,而采用组合方式来实现类重用则能提供更好的封装性。

使用继承的注意点

子类使扩展父类使,可以直接访问父类的Field和方法,但是有一个严重的问题:继承严重破坏了父类的封装性。前面介绍封装时提到:每个类都应该封装它内部信息和实现细节,而只暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的Field(内部信息)和方法,从而造成子类和父类的严重耦合。
为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则:

  • 尽量隐藏父类的内部数据。尽量把父类的所有Field都设置成private访问类型,不要让子类直接访问父类的Field。
  • 不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符(该修饰符后面会有更详细的介绍)来修饰该方法;如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。
  • 尽量不要在父类构造器中调用将要被子类重写的方法。
    这里有个程序:
class Base
{
    public Base()
    {
        test();
    }
    public void test()          //①号test方法
    {
        System.out.println("将被子类重写的方法");
    }
}
public class Sub extends Base
{
    private String name;
    public void test()       //②号test方法
    {
        System.out.println("子类重写父类的方法,"
                + "其name字符串长度" + name.length());
    }
    public static void main(String[] args)
    {
        //下面代码会引发空指针异常
        Sub s=new Sub();
    }
}

当系统试图创建Sub对象时,同样会先执行其父类构造器,如果父类构造器调用了被其子类重写的方法,则变成调用被子类重写后的方法。当创建Sub对象时,会先执行Base类中的Base构造器,而Base构造器中调用了test方法——并不是调用①号test方法,而是调用②号test方法,此时Sub对象的name Field是null,因此将引发空指针异常。
如果想把某些类设置成最终类,即不能被当成父类,则可以使用final修饰这个类,例如JDK提供的java.lang.String类和java.lang.System类。除此之外,使用private修饰这个类的所有构造器,从而保证子类无法调用该类的构造器,也就无法继承该类。对于把所有的构造器都使用private修饰的父类而言,可另外提供一个静态方法,用于创建该类的实例。
那么,到底何时需要从父类派生新的子类呢?不仅需要保证子类是一种特殊的父类,而且需要具备以下两个条件之一。

  • 子类需要额外增加属性,而不仅仅是属性值的改变。例如从Person类派生出Student子类,Person类里没有提供grade(年级)属性,而Student类需要grade属性来保存Student对象就读的年级,这种父类到子类的派生,就符合Java继承的前提。
  • 子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。例如从Person类派生出Teacher类,其中Teacher类需要增加一个teaching方法,该方法用于描述Teacher对象独有的行为方式:教学。

上面详细介绍了继承关系可能存在的问题,以及如何处理这些问题。如果只是出于类复用的目的,并不一定需要使用继承,完全可以使用组合来实现

利用组合实现复用

如果需要复用一个类,除了把这个类当成基类来继承之外,还可以把该类当成另一个类的组合成分,从而允许新类直接复用该类的public方法。
组合则是把旧类对象作为新类的Field嵌入,用以实现新类的功能,用户看到的是新类的方法,而不能看到被嵌入对象的方法。因此,通常需要在新类里使用private修饰被嵌入的旧类对象。
仅从类复用的角度来看,不难发现父类的功能等同于被嵌入类,都将自身的方法提供给新类使用;子类和组合关系里的整体类,都可复用原有类的方法,用于实现自身的功能。
如下列子:

class Animal
{
    private void beat()
    {
        System.out.println("心脏跳动...");
    }
    public void breath()
    {
        beat();
        System.out.println("吸一口气,吐一口气,呼吸中...");
    }
}
//继承Animal,直接复用父类的breath方法
class Bird extends Animal
{
    public void fly()
    {
        System.out.println("我在天空自在的飞翔...");
    }
}
//继承Animal,直接复用父类的breath方法
class Wolf extends Animal
{
    public void run()
    {
        System.out.println("我在陆地上的快速奔跑...");
    }
}
public class InheritTest
{
    public static void main(String[] args)
    {
        Bird b=new Bird();
        b.breath();
        b.fly();
        Wolf w=new Wolf();
        w.breath();
        w.run();
    }
}

通过让Bird和Wolf继承Animal,从而允许Wolf和Bird获得Animal的方法,从而复用了Animal提供的breath方法。通过这种方式,相当于让Wolf类和Bird类同时拥有其父类Animal的breath方法,从而让Wolf对象和Bird对象都可直接调用Animal里定义的breath方法
如果仅仅从软件复用的角度来看,将上面三个类的定义改为如下形式也可实现相同的复用:

class Animal
{
    private void beat()
    {
        System.out.println("心脏跳动...");
    }
    public void breath()
    {
        beat();
        System.out.println("吸一口气,吐一口气,呼吸中...");
    }
}
class Bird
{
    //将原来的父类嵌入原来的子类,作为子类的一个组合成分
    private Animal a;
    public Bird(Animal a)
    {
        this.a=a;
    }
    //重新定义一个自己的breath方法
    public void breath()
    {
        //直接复用Animal提供的breath方法来实现Bird的breath方法
         a.breath();
     }
        public void fly()
        {
            System.out.println("我在天空自在的飞翔...");
        }
    }
class Wolf
{
    //将原来的父类嵌入原来的子类,作为子类的一个组合成分
    private Animal a;
    public Wolf(Animal a)
    {
        this.a=a;
    }
    //重新定义一个自己的breath方法
    public void breath()
    {
        //直接复用Animal提供的breath方法来实现Wolf的breath方法
         a.breath();
    }
    public void run()
    {
        System.out.println("我在陆地上的快速奔跑...");
    }
}
public class CompositeTest
{
    public static void main(String[] args)
    {
        //此时需要显式创建被嵌入的对象
        Animal a1=new Animal();
        Bird b=new Bird(a1);
        b.breath();
        b.fly();
        //此时需要显式创建被嵌入的对象
        Animal a2=new Animal();
        Wolf w=new Wolf(a2);
        w.breath();
        w.run();
    }
}

此时的Wolf对象和Bird对象由Animal对象组合而成,因此在上面程序中创建Wolf对象和Bird对象之前先创建Animal对象,并利用这个Animal对象来创建Wolf对象和Bird对象。运行该程序时,可以看到与前面程序相同的执行结果。
到底该用继承?还是该用组合呢?继承是对已有的类做一番改造,以此获得一个特殊的版本。简而言之,就是将一个较为抽象的类改造成能适用于某些特定需求的类。因此,对于上面的Wolf和Animal的关系,使用继承更能表达其现实意义。用一个动物来合成一匹狼毫无意义:狼并不是由动物组成的。反之,如果两个类之间有明确的整体、部分的关系,例如Person类需要复用Arm类的方法(Person对象由Arm对象组合而成),此时就应该采用组合关系来实现复用,把Arm作为Person类的嵌入Field,借助于Arm的方法来实现Person的方法,这是一个不错的选择。
总之,继承要表达的是一种“是(is-a)”的关系,而组合表达的是“有(has-a)”的关系。

Java实例练习

字符串数据对象

在日常开发中,经常需要输出对象的描述信息,此时可以通过重写toString()方法来完成。

1. 重写tiString()

新建项目xiaomi,并在其中创建一个xiaomi.java文件。在该类中定义四个成员变量分别表示小米手机的名称、型号、价格和颜色,并提供构造方法来设置这些属性,本书的重点在于重写toString()方法,这样可以在输出对象时获得有意义的信息:

package xiaomi;

import java.awt.Color;
public class xiaomi {
    private String name;                                 // 表示手机的名称
    private String model;                                // 表示手机的型号
    private int price;                                   // 表示手机的价格
    private Color color;                                 // 表示手机的颜色
    // 创建xiaomi构造器
    public xiaomi(String name, String model, int price, Color color) {
        // 初始化小米的属性
        this.name = name;
        this.model = model;
        this.price = price;
        this.color = color;
    }

    @Override
    public String toString() {                           // 重写toString()方法
        StringBuilder sb = new StringBuilder();
        sb.append("\n");
        sb.append("名称:" + name + "\n");                 // 输出手机的名称
        sb.append("型号:" + model + "\n");                // 输出手机的型号
        sb.append("价格:" + price + "\n");                // 输出手机的价格
        sb.append("颜色:" + color + "\n");                // 输出手机的颜色
        return sb.toString();
    }
}

2.测试

在项目中创建一个测试类Test,在该类的main()方法中创建三个对象,并对其进行初始化,然后输出手机对象。核心代码如下所示:

package xiaomi;

import java.awt.Color;
public class Test {
    public static void main(String[] args) {
        xiaomi xiaomi1 = new xiaomi("小米M1", "双核1.5Hz", 1299, Color.BLACK);
        // 创建小米1号
        xiaomi xiaomi2 = new xiaomi("小米M1s","双核1.7Hz", 1499, Color.BLACK);
        // 创建小米2号
        xiaomi xiaomi3 = new xiaomi("小米M2", "四核1.5Hz", 1999, Color.WHITE);
        // 创建小米3号
        System.out.println("小米1号:" + xiaomi1);                 // 输出小米1号
        System.out.println("小米2号:" + xiaomi2);                 // 输出小米2号
        System.out.println("小米3号:" + xiaomi3);                 // 输出小米3号
    }
}

重写toString类型时,为了给用户提供更多的信息,通常会包括类中成员变量和成员方法的介绍等。本实例中的类比较简单,没有包含方法。因此简单的解释了各个成员变量的含义。为了让toString()方法可以有更好的通用性,可以使用反射来获得成员字段和成员方法的信息。

手机销售流程

当顾客在商场购物时,卖家需要根据顾客的需求提取商品。对于手机销售也是如此。用户需要先指定购买的机型,然后商家去仓库提取该机型的手机。

1.

新建项目MobilePhone,并在其中创建一个名为MobilePhone的抽象类,在该类中定义一个抽象方法getInfo()。

package MobilePhone;

public abstract class MobilePhone {
    public abstract String getInfo();          // 用来描述手机的信息
}

2.

接着在项目中创建一个名为Iphone5的类,该类继承自MobilePhone并实现其getInfo()方法:

public class Iphone5 extends MobilePhone{
    @Override
    public String getInfo() {                  // 用来描述Iphone5的信息
        return "Iphone5";
    }
}

3.

在项目中创建一个名为SamsungI9300的类,该类继承自MobilePhone并实现其getInfo()方法:

public class SamsungI9300 extends MobilePhone{
    @Override
    public String getInfo() {               // 用来描述SamsungI9300的信息
        return "SamsungI9300";
    }
}

4.

然后我们创建一个名为MobileFactory的工厂类,该类定义了一个静态方法getphone(),它可以根据用户指定的机型来创建对象:

public class MobileFactory {
    public static MobilePhone getphone(String name) {
        if (name.equalsIgnoreCase("Iphone5")) {
            // 如果需要Iphone5则创建Iphone5对象
            return new Iphone5();
        } else if (name.equalsIgnoreCase("SamsungI9300")) {
            // 如果需要SamsungI9300则创建SamsungI9300对象
            return new SamsungI9300();
        } else {
            //暂时不能支持其他机型
            return null;
        }
    }
}

5.

最后再创建一个Customer测试类,在该类的主方法中,根据用户的需要提取了不同的手机。核心代码如下所示:

package MobilePhone;

public class Customer {
    public static void main(String[] args) {
        System.out.println("顾客要购买Iphone5:");
        MobilePhone Iphone5 = MobileFactory.getphone("Iphone5");  // 用户要购买Iphone5
        System.out.println("提取手机:" + Iphone5.getInfo());   // 提取Iphone5
        System.out.println("顾客要购买SamsungI9300:");
        MobilePhone SamsungI9300 = MobileFactory.getphone("SamsungI9300");  // 用户要购买SamsungI9300
        System.out.println("提取手机:" + SamsungI9300.getInfo());  // 提取SamsungI9300
    }
}

方法的重写、重载与动态连接构成多态性。Java之所以引入多态的概念,原因之一是它在类的继承问题上和C++不同,后者允许多继承,这确实给其带来非常强大的功能,但是复杂的继承关系也给C++开发者带来了更大的麻烦。为了规避风险,Java只允许单继承,这样做虽然保证了继承关系的简单明了,但是势必在功能上有很大的限制。所以,Java引入了多态性的概念以弥补这点的不足。此外,抽象类和接口也是解决单继承规定限制的重要手段。同时,多态也是面向对象编程的精髓所在。

猜你喜欢

转载自blog.csdn.net/tonydz0523/article/details/86294846