[java学习笔记]继承和组合

继承

OOP的三大特性之一,也是经常使用到的一特性。可以很容易的实现类的重用;但是利弊总是相伴的。它带来的一个最大的坏处就是破坏封装。相比之下,组合也是实现类重用的重要方式,而采用组合方式来实现重用则能提供更好的封装性。

子类扩展(extends)父类时,可以从父类集成得到属性和方法。如果访问权限允许(即不是private的声明),子类可以直接访问父类的属性和方法。but,子类同样可以重写(override)父类的属性和方法;

那么问题来了,这样的话就是说儿子可以随便干掉老子辛辛苦苦奋斗得来的东西;然后自己按照自己的喜好随便折腾一番。

父类:

public class father {

    public String companyName = "father's company.";

    public father() {
        System.out.println("father's companyName:" + companyName);
    }


    public void myMoney() {
        System.out.println("老子辛辛苦苦半辈子,赚了一个亿,熊孩子不争气,就给他5000W吧。");
    }
}

子类:

public class Son extends father {

    public String companyName = "son's company.";

    public void myMoney() {
        super.myMoney();
        System.out.println("----end father's info----
");
        System.out.println("这老头死抠,我要篡改信息:my father 给我留了一个亿");
    }

    public static void main(String[] args) {
        Son son = new Son();
        son.myMoney(); // 这老头死抠,我要篡改信息:my father 给我留了一个亿
        System.out.println(son.companyName); //  son's company.
        System.out.println("都是我的了,O(∩_∩)O哈哈哈~");
    }
}

输出:

father's companyName:father's company.
老子辛辛苦苦半辈子,赚了一个亿,熊孩子不争气,就给他5000W吧。
----end father's info----

这老头死抠,我要篡改信息:my father 给我留了一个亿
son's company.
都是我的了,O(∩_∩)O哈哈哈~

Process finished with exit code 0

熊孩子不省事,瞬间就能把老子给气吐血了。

========================================

总结:

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

1、尽量隐藏父类的内部数据,尽量把父类的所有属性设置为private,不用让子类直接访问父类属性;

2、不让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰。如果父类方法必须外部类调用,必须以public修饰符。如果不想让子类随便重写方法,可以声明方法为final。如果想让子类重写但是又不希望被别的类自由访问,则使用protected 来修饰方法。(亲儿子可以随便,养子啥的,呵呵)

3、不用在父类构造器中调用被子类重写的方法;

比如father构造函数中,调用了可被子类重写的方法:

public father() {
        System.out.println("father's companyName:" + companyName);
        test();
    }

    public void test(){
        System.out.println("将被子类重写的方法。");
    }

子类:

public class Son extends father {

    private String name="Son";

    public  void test(){
        System.out.println("son.test().name.length="+name.length());
    }

    public static void main(String[] args) {
        Son son = new Son(); // java.lang.NullPointerException
    }
}

为什么会出现NullPointerException呢?

因为在实例化Son son=new Son()时,会先初始化父类 father;而因为father.test()方法又被子类重写了;所以在实例化father()无参构造函数时,里面调用的test()方法并非father.test()而是 Son.test();

此时,由于没有执行到Son类中属性的赋值阶段;所以此时的name = null,只是声明了属性变量而已,并没有开辟空间、赋值;

所以,name.length() ===> null.length(),就会报错了。

 向上转型:即由子类---->父类的转换,将会把子类中已扩展的并且父类中不存在的方法和属性都会过滤掉;

        father father = new Son();// 子类向上转型,将去除掉子类自定义扩展的一些属性、方法;
        father.test(); // son.test().name.length=3
        father.sonSelfMethod();// 提示找不到方法,编译时IDE提示cannot resolved method "sonSelfMethod"

组合:

 对于继承而言,子类可以直接获得父类的public方法,程序使用子类时,将可以直接访问子类从父类哪里继承的方法;而组合则是把其他类作为新类的的属性嵌入进来,用于辅助新类实现功能;用户看到的是新类的方法,而隐藏了嵌入类的方法。因此,嵌入类在声明时,需要指定为private的访问修复符;。这样新类与旧类之间存在了一个“has a”的关系;比如:新类person有Arm、Leg;

/**
 * Created by hager.
 * 组合,主要是 has-a的关系;而继承则是is a的关系;
 */
public class CombinePerson {
    private Arm myArm;// 使用private,隐藏嵌入类,防止使用新类的场景中乱用嵌入类。
    private Leg myLeg; // 同上

    public CombinePerson(Arm myArm, Leg myLeg) {
        this.myArm = myArm;
        this.myLeg = myLeg;
    }

    public void buildaPerson() {
        System.out.println("开始造人...");
        String arms = myArm.buildArm();
        String legs = myLeg.buildLeg();

        System.out.println(String.format("one person has %s,%s",arms,legs));
    }

}

Arm、Leg类:

public class Arm {
    public String buildArm(){
        return "两只胳膊";
    }
}

// Leg类
public class Leg {
    public String buildLeg(){
        return "两条腿";
    }
}

结果:

 public static void main(String[] args) {

        Arm myArm = new Arm();
        Leg myLeg = new Leg();

        CombinePerson person = new CombinePerson(myArm, myLeg);

        person.buildaPerson();

        System.out.println("造人完毕");

    }

// 输出内容
// 开始造人...
// one person has 两只胳膊,两条腿
// 造人完毕

// 

其实,在实际场景中,如果是组合方式,一般也是基于接口的构造注入。这样面向接口的编程,配合IOC,相对后续复杂系统实现来说更加灵活一些;

==============================

扩展:到底输出6,还是9 呢?

/**
 * Created by Administrator-xierfly on 2016/11/27.
 * 属性初始化顺序测试
 * 普通初始化块、声明实例属性指定的默认值都可以认为是对象的初始化代码,他们的执行顺序与源程序中的排列顺序相同。
 * 初始化块,只在创建java对象时隐式执行,而且是在构造器之前执行
 */
public class InitOrderTest {
    /**
     * 以下示例,是先执行int a 并分配默认值为0;然后再执行初始化块,赋值 a = 6;
     * 接着,执行a = 9 的属性赋值;所以最终输出是9;
     *
     * 如果把int a = 9 放到初始化块之前,那么输出的值将是 6 ;
     *
     * 所以,初始化块和属性执行顺序是跟声明顺序有关系的;
     *
     */
    {
        a = 6;
    }

    int a = 9;


    public static void main(String[] args) {
        System.out.println("a="+new InitOrderTest().a);//a = 9
    }
}

参考:《李刚疯狂java讲义》

原文地址:https://www.cnblogs.com/hager/p/6106350.html