继承
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----\r\n");
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
}
}