第七章复用类
复用代码的方式 1组合 2继承
方式1组合
public class Box {
String boxName;
public Box(String s) {
System.out.println("Box");
boxName = s;
}
}
public class Test {
Box box;
Test (Box box){
this.box = box;
String name;
int age;
}
}
比如上面这个例子Test类中复用了Box的类
方式2继承(略)
extends关键字
创建子类对象时父类构造函数会先被执行
子类必须调用父类的构造函数,不调用则编译器提示错误
第三种关系:代理
代理可以简单理解为继承+组合 举个例子很容易明白
例如太空船内部有个控制器控制太空船的行动,利用继承实现如下
public class SpaceControls {
void up(int velocity){}
void down(int velocity){}
void left(int velocity){}
void right(int velocity){}
void forward(int velocity){}
void back(int velocity){}
void turboBoost(){}
}
public class SpaceShip extends SpaceControls {
private String name;
public SpaceShip(String name) {
this.name = name;
}
public String toString() {
return name;
}
public static void main(String [] args){
SpaceShip protector = new SpaceShip("Angel");
protector.forward(100);
}
}
可以看到SpaceShip 继承了 SpaceControls,那么SpaceShip 可以看作一个SpaceControls,SpaceShip 就具有SpaceControls的所有方法,他们之间是is-a的关系
如果改成代理的方式呢:
public class SpaceControls {
void up(int velocity){}
void down(int velocity){}
void left(int velocity){}
void right(int velocity){}
void forward(int velocity){}
void back(int velocity){}
void turboBoost(){}
}
public class SpaceShipDelegation {
private String name;
private SpaceControls controls = new SpaceControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// Delegated methods
public void up(int velocity) {
controls.up(velocity);
}
public void down(int velocity) {
controls.up(velocity);
}
public void left(int velocity) {
controls.left(velocity);
}
public void right(int velocity) {
controls.right(velocity);
}
public void forward(int velocity) {
controls.forward(velocity);
}
public void back(int velocity) {
controls.back(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public static void main(String[] args) {
SpaceShipDelegation protector = new SpaceShipDelegation("Angel");
protector.forward(100);
}
}
可以看到这种形式和组合相似,SpaceShipDelegation中包含了SpaceControls 的实例,这其实就是组合,但是与组合不同的是,代理增加一些代理方法,up down left right back forward等等,通过代理方法调用SpaceControls 实例对象的方法,这看起来就像是继承了SpaceControls一样(但实际没有extends关键字,就是实际没有使用继承,只是形似)。所以我说代理是组合+继承
组合与继承结合使用
这里不是说代理,是使用组合的同时使用extends关键字。
对象清理顺序:
先清理当前类的对象,清理顺序为创建顺序的反序,然后调用父类的清理方法。书中利用try finally语句将回收语句放在finally中确保回收语句一定会得到执行
使用@Override注解避免错误的重载
组合VS继承
组合的关系 是has-a的关系
继承的关系 是is-a的关系
具体两个类之间该用组合还是继承就看两个类之间的关系了
比如汽车和轮胎用组合关系 汽车和劳斯莱斯用继承关系
protected关键字
与private相比放开了子类的访问权限
向上转型
前提:一般情况下我们在绘制UML类图的继承关系时习惯将父类(基类)绘制于子类的上方,如上图所示
由于子类继承了父类,所以子类具有父类的所有特征,子类可以看作一种特别的父类。
在以父类对象为参数的方法中可以传入子类对象的实例,这种情况就是向上转型,简单说就是该传父类对象的时候传递了子类对象。
反之则称为向下转型。向下转型后面几章会讲到。
练习16和17
class Amphibian {
protected void swim() {
println("Amphibian swim");
}
protected void speak() {
println("Amphibian speak");
}
void eat() {
println("Amphibian eat");
}
static void grow(Amphibian a) {
println("Amphibian grow");
a.eat();
}
}
public class Frog extends Amphibian {
public static void main(String[] args) {
Frog f = new Frog();
// call base-class methods:
f.swim();
f.speak();
f.eat();
// upcast Frog to Amphibian argument:
Amphibian.grow(f);
}
}
class Amphibian {
protected void swim() {
println("Amphibian swim");
}
protected void speak() {
println("Amphibian speak");
}
void eat() {
println("Amphibian eat");
}
static void grow(Amphibian a) {
println("Amphibian grow");
a.eat();
}
}
public class Frog17 extends Amphibian {
@Override protected void swim() {
println("Frog swim");
}
@Override protected void speak() {
println("Frog speak");
}
@Override void eat() {
println("Frog eat");
}
static void grow(Amphibian a) {
println("Frog grow");
a.eat();
}
public static void main(String[] args) {
Frog17 f = new Frog17();
// call overridden base-class methods:
f.swim();
f.speak();
f.eat();
// upcast Frog17 to Amphibian argument:
f.grow(f);
// upcast Frog17 to Amphibian and call Amphibian method:
Amphibian.grow(f);
}
}
此两题所表述的应该时静态方法无法被覆盖的意思
final关键字
final数据(final修饰的变量)
final关键字与常量息息相关,在java中,final具有禁止修改的意思,用final修饰的域 方法 类分别是禁止修改的常量 禁止子类修改的方法 禁止继承的类。
class Value {
int i; // Package access
public Value(int i) {
this.i = i;
}
}
public class FinalData {
private static Random rand = new Random();
private String id;
public FinalData(String id) {
this.id = id;
}
// Can be compile-time constants:
private final int VAL_ONE = 9;
private static final int VAL_TWO = 99;
// Typical public constant:
public static final int VAL_THREE = 39;
// Cannot be compile-time constants:
private final int i4 = rand.nextInt(20);
static final int i5 = rand.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private static final Value v3 = new Value(33);
// Arrays:
private final int[] a = { 1, 2, 3, 4, 5, 6 };
public String toString() {
return id + ": " + "i4 = " + i4 + ", i5 = " + i5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
// ! fd1.VAL_ONE++; // Error: can't change value
fd1.v2.i++; // Object isn't constant!
fd1.v1 = new Value(9); // OK -- not final
for (int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Object isn't constant!
// ! fd1.v2 = new Value(0); // Error: Can't
// ! fd1.v3 = new Value(1); // change reference
// ! fd1.a = new int[3];
System.out.println(fd1);
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
System.out.println(fd1);
System.out.println(fd2);
}
}
输出:
fd1: i4 = 6, i5 = 13
Creating new FinalData
fd1: i4 = 6, i5 = 13
fd2: i4 = 4, i5 = 13
分析:
VAL_ONE VAL_TWO区别不大,因为他们被初始化为一个基本数据类型常量
VAL_THREE是一个典型的常量声明,public表示可以被其他任意类使用。static表示只有一份且初始化早于构造函数,必定只初始化一次。final表示是一个常量,不可以被修改。
i4 i5说明即使某个数据是final的,但不意味着编译时期一定可以知道它的值,他们在每次运行时的值可能不同。i4和i5还有第二个不同:初始化时机,i4每创建一个对象就会进行重新初始化,i5不管创建多少个对象,所有对象的i5的值都相等,他们在程序刚开始运行时就初始化了而且只初始化一次。所以fd1,fd2的i4输出可能不同但是i5的值却是相同的。
v1 v2 v3说明了虽然final修饰的变量是一个引用时,即变量指向的对象不可以变动,即必须一直指向该对象,但是对象本身内容却可以变化,也就是例子中“fd1.v2.i++; ”没有报错的情况。
空白final
final变量在声明时可以不被初始化 但是使用前必须初始化。
final参数
方法中不可以修改参数内容,只可以用于读取,这个特性多用于匿名内部类的参数传递(第十章)
final方法
过去final方法的作用
1.禁止覆盖
2.提高效率
但是第二点在目前虚拟机的进化下,其功效以及几乎不存在了,而且想要指望将方法设置成final来达到提升效率,收效微乎其微。因此,现在java将方法设置final其目的只有一个,禁止覆盖
final与private:
private内部隐含final的意思
final类
出于安全或者其他目的,将类写成final的,这样该类无法被继承。final修饰的类,其方法 域隐含带有final修饰,因为没有类可以继承该类。所以,在final类的变量方法上再加上final修饰符毫无意义。
使用final时需要注意:真的有必要使用么?
初始化和类的加载
类的加载发生在第一次创建一个类的对象时,但是当访问static域或者方法时也会加载对应的类。(构造器也是静态方法,尽管static关键字没有显示写出,因此可以说,类是在static成员被访问时加载)
分析以下代码初始化顺序
class Insect {
private int i = 9;
protected int j;
Insect() {
System.out.println("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 = print("static Insect.x1 initialized");
static int print(String s) {
System.out.println(s);
return 47;
}
}
public class Beetle extends Insect {
private int k = print("Beetle.k initialized");
public Beetle() {
System.out.println("k = " + k);
System.out.println("j = " + j);
}
private static int x2 = print("static Beetle.x2 initialized");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle b = new Beetle();
}
}
输出
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
1.发现Beetle的静态main方法 尝试加载Beetle类,尝试加载Beetle类时发现它继承自一个父类Insect,优先加载父类。父类的静态成员变量初始化(static Insect.x1 initialized)。子类静态成员变量初始化(static Beetle.x2 initialized)。
2.继续执行main函数(Beetle constructor)
3.尝试创建Beetle对象,此时优先调用父类构造函数。初始化父类中的成员变量,调用父类构造函数(i = 9, j = 0)
4.初始化Beetle的成员变量 (Beetle.k initialized)调用Beetle构造函数(k = 47 \n j = 39)
本章小结
继承虽然很重要,但是使用时要小心,优先使用组合和代理,最后再决定使用继承
活用向上转型,这利于多态的使用
类的大小要适中,不能太大(臃肿 难以复用,难以阅读)不能太小(不添加其他功能就无法使用)
以下这书中段话感觉很有哲理,转述不能,剪贴如下:
第八章 多态
多态实际是基于向上转型的特征来体现的。
public enum Note {
MIDDLE_C,C_SHARP,B_FLAT;
}
public class Instrument {
public void play(Note n) {
System.out.println("Instrument play");
}
}
public class Wind extends Instrument {
@Override
public void play(Note n) {
System.out.println("wind.play "+n);
}
}
public class Music {
public static void tune(Instrument i) {
i.play(Note.B_FLAT);
}
public static void main(String [] args) {
Wind flute = new Wind();
tune(flute);
}
}
以上是一个典型的向上转型的例子,tune方法接受的类型是Instrument类型,但是我们传入的却是Wind类型。然而程序还是可以运行。这是因为Wind继承自Instrument,因此wind也是一种Instrument,向上转型将wind进行了“窄化”,它只能执行Instrument具有的方法,不能执行wind具有而Instrument没有的方法。另外,执行时调用实际对象的方法,比如上面tune(flute);将flute作为Instrument对象传递给方法,但flute的实际类型为wind,因此i.play实际调用的是wind覆盖了Instrument的play方法。
8.1.1举例说明为什么需要忘记对象类型
我们为什么要大费周章地弄出看起来这么混乱地逻辑呢?
我们明明可以这样做
public static void tune(Wind i) {
i.play(Note.B_FLAT);
}
这样不是逻辑更清晰么,是wind对象就传入wind,其他类型也要有对应地方法。
比如新增两种乐器
public class Stringed extends Instrument {
@Override
public void play(Note n) {
System.out.println("Stringed.play "+n);
}
}
public class Brass extends Instrument {
@Override
public void play(Note n) {
System.out.println("Brass.play "+n);
}
}
那么我们地music对象就需要进行修改
public static void tune(Brass i) {
i.play(Note.B_FLAT);
}
public static void tune(Stringed i) {
i.play(Note.B_FLAT);
}
这违背了对扩展开放对修改关闭的原则。如果使用多态,即
public static void tune(Instrument i) {
i.play(Note.B_FLAT);
}
无论新增多少种乐器,只要这一个方法就可以,这样就做到了对扩展开放对修改关闭的原则。
因此使用多态是为了便于扩展。
8.2.1 方法绑定类型
将一个方法调用与方法主体关联起来称为绑定,(个人理解:比如class A有方法b,class C的实例调用方法b,c.b()与b(){…}关联起来的过程,说简单点就是具体调用哪个方法的决定时间)
如果程序执行前将一个方法调用与方法主体关联起来称为前期绑定,也是面向过程的默认绑定方式
如果程序执行中将一个方法调用与方法主体关联起来称为后期绑定,也叫动态绑定,运行时绑定,要在运行时知道具体应该调用的方法具体做法就是在对象中隐含类型信息,让对象知道具体调用哪个类型的方法。
java除static和final方法(private属于final方法),其他方法均是动态绑定的。给方法添加final修饰符就相当于“关闭”动态绑定。
8.2.2如何执行正确的动作
靠运行时绑定
比如上面这种继承关系
Shape s = new Circle();
就完成了向上转型,当执行s.draw()方法时,对象中包含了类型信息知道它是一个Circle对象,然后就能调用正确的方法。
扩展:工厂模式与动态绑定
例子:
基类:
public class Shape {
public void draw() {
}
public void erase() {
}
}
三个子类
public class Circle extends Shape {
public void draw() {
System.out.println("Circle.draw");
}
public void erase() {
System.out.println("Circle.erase");
}
}
public class Square extends Shape {
public void draw() {
System.out.println("Square.draw");
}
public void erase() {
System.out.println("Square.erase");
}
}
public class Triangle extends Shape {
public void draw() {
System.out.println("Triangle.draw");
}
public void erase() {
System.out.println("Triangle.erase");
}
}
工厂类
public class RandomShapeGenerator {
private Random random = new Random(47);
public Shape next() {
switch (random.nextInt(3)) {
default:
case 0:
return new Circle();
case 1:
return new Square();
case 2:
return new Triangle();
}
}
}
测试类
public class Test {
private static RandomShapeGenerator generator = new RandomShapeGenerator();
public static void main(String[] args) {
Shape[] shapes = new Shape[9];
for (int i = 0; i < shapes.length; i++) {
shapes[i] = generator.next();
}
for(Shape shape : shapes){
shape.draw();
}
}
}
输出
Triangle.draw
Triangle.draw
Square.draw
Triangle.draw
Square.draw
Triangle.draw
Square.draw
Triangle.draw
Circle.draw
上面这个例子是工厂模式的简单实现,它与动态绑定紧密结合。多态或者说动态绑定发生在这里
public Shape next() {
switch (random.nextInt(3)) {
default:
case 0:
return new Circle();
case 1:
return new Square();
case 2:
return new Triangle();
}
}
期望返回值是shape类型 实际返回值是子类类型。因此可以执行正确的方法。
拓展练习
1.在基类shape中添加方法
并在测试类调用
发现所有类型都会调用相同方法即基类方法
public void output() {
System.out.println("output");
}
for(Shape shape : shapes){
shape.draw();
shape.output();
}
2.只在Circle类中override output方法,输出结果显示Circle实例的输出与其他类型的输出不一样
3.在所有子类override output方法,输出结果显示所有实例调用的都是子类的方法。
4.在子类中添加other方法,尝试在测试类调用该方法
for(Shape shape : shapes){
shape.draw();
shape.output();
shape.other();
}
发现报错,找不到对应的方法。
总结:
向上转型时,对象调用了一个方法,如果子类覆盖了父类的该方法,会调用子类的方法。
否则调用父类方法。
无法调用子类独有的方法。(前文所述的向上转型使得类型“变窄”,调用方法不能超过父类的方法范围)
8.2.3多态与可扩展性
我们对上述shape的例子进行扩展,添加新的shape类型或者在新类型中添加新的方法,原来的方法无需变动。这就体现了多态对可扩展性做出的贡献。我们所作的代码修改不会对程序中其他不应该受到影响的部分产生破坏。即多态让程序员能够“将改变的事物与不变的事物分离开来”
练习:创建包含两个方法的基类,在基类的方法一中调用方法二。在子类中覆盖第二个方法。在测试类使用向上转型调用第一个方法。看看结果。(此练习有助于理解多态)
public class Father {
public void method1() {
System.out.println("Father method1");
method2();
}
public void method2() {
System.out.println("Father method2");
}
}
public class Child extends Father {
@Override
public void method2() {
System.out.println("Child method2");
}
}
public class Test {
public static void main(String[] args) {
Father father = new Child();
father.method1();
}
}
输出
Father method1
Child method2
8.2.4 private方法不可覆盖
父类的private方法其实是final的,子类中没有对应的方法。即使子类写了同名的方法,那也是在子类新加一个同名的方法而不是覆盖父类的方法。最简单的验证方法就是尝试在子类的该方法添加@Override注解,会发现报错。(添加@Override是一个良好的习惯),子类最好不要使用父类private方法的名字来命名方法,这可能引起误解。
注意:域的继承
例子
public class Super {
public int field = 0;
public int getField() {
return field;
}
}
public class Sub extends Super{
public int field = 1;
public int getField() {
return field;
}
public int getSuperField() {
return super.field;
}
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub();
System.out.println("sup.field"+sup.field);
System.out.println("sup.getField"+sup.getField());
Sub sub = new Sub();
System.out.println("sub.field"+sub.field);
System.out.println("sub.getField"+sub.getField());
System.out.println("sub.getSuperField"+sub.getSuperField());
}
}
输出
sup.field0
sup.getField1
sub.field1
sub.getField1
sub.getSuperField0
上述例子中,注意super类的变量是public的,子类中继承了super类的域field,因此子类中存在两个同名变量field,一个来自父类,一个来自自身(如果注释掉子类中的public int field = 1;我们会发现子类没有报错,子类所有field值的输出全变成父类的值)。子类中调用super.field可以获取父类的field值。另外方法的调用存在多态现象,但是域的调用不存在,声明时的类型决定了它访问的域。比如无论A a = new B();还是A a = new A();当调用a.field时,始终访问的是A class中的filed。
域继承与否关系其实不大,因为我们习惯这样定义:
private int field1;
public int getField1() {
return field1;
}
public void setField1(int field1) {
this.field1 = field1;
}
因此子类并不会继承field1,这样只会出现方法的多态了。
注意:静态方法没有多态
一旦方法设置为static的,那么它只属于类,一般我们直接使用类名来调用静态方法,此类方法没有多态。只与我们调用时的类名相关。
public class Super {
static void staticMethod(){
System.out.println("super staticMethod");
}
}
public class Sub extends Super{
static void staticMethod(){
System.out.println("sub staticMethod");
}
}
public class FieldAccess {
public static void main(String[] args) {
Super.staticMethod();
Sub.staticMethod();
}
}
输出
super staticMethod
sub staticMethod
8.3构造器和多态
构造器的调用顺序
分析
前面我们提到,如果类存在父类,那么创建子类时会先调用父类的构造方法。理由是子类继承了父类,那么就需要继承父类的非private属性和非private方法,如果不初始化父类,则子类无法知道需要继承哪些东西。
结合组合,继承的例子
public class Meal {
public Meal() {
System.out.println("Meal");
}
}
public class Lunch extends Meal{
public Lunch() {
System.out.println("Lunch");
}
}
public class PortableLunch extends Lunch{
public PortableLunch() {
System.out.println("PortableLunch");
}
}
public class Bread {
public Bread() {
System.out.println("Bread");
}
}
public class test {
public test() {
System.out.println("test");
}
}
public class Cheese extends test{
public Cheese() {
System.out.println("Cheese");
}
}
public class Lettuce {
public Lettuce() {
System.out.println("Lettuce");
}
}
public class Sandwich extends PortableLunch{
private Bread bread = new Bread();
private Cheese cheese = new Cheese();
private Lettuce lettuce= new Lettuce();
public Sandwich() {
System.out.println("Sandwich");
}
public static void main(String [] args){
Sandwich sandwich = new Sandwich();
}
}
再分析
类结构如下
Meal<–Lunch<–PortableLunch<–Sandwich
Bread Cheese Lettuce组合在Sandwich类中,其中Cheese比较特殊,继承了test类
调用Sandwich时发现继承了PortableLunch,尝试初始化PortableLunch,此时发现PortableLunch继承了Lunch;尝试初始化Lunch,此时发现Lunch继承了Meal;尝试初始化Meal,初始化Meal成功。继续初始化Lunch,初始化Lunch成功。继续初始化PortableLunch,初始化PortableLunch成功。
继续初始化Sandwich,此时发现该类组合了Bread Cheese Lettuce实例(在构造函数之前),需要优先初始化他们,按照声明顺序初始化Bread Cheese Lettuce。
尝试初始化Bread,初始化成功
尝试初始化Cheese ,发现继承了test,尝试初始化test,初始化test成功。继续初始化Cheese,初始化 Cheese成功。
尝试初始化Lettuce,初始化成功
初始化Sandwich,初始化成功。
所以输出如下
Meal
Lunch
PortableLunch
Bread
test
Cheese
Lettuce
Sandwich
结论
对于一个类其方法调用顺序如下
1)优先调用基类的构造方法
2)按照声明顺序调用类的成员初始化方法
3)调用本类构造方法
8.3.2继承与清理
根据继承和覆盖的原则,如果子类覆盖了父类的清理方法,那么当子类调用清理方法时,不会调用父类的清理方法,由于上面我们分析的初始化顺序,父类中可能存在未清理对象,因此需要利用super关键字调用父类的清理方法。创建顺序与销毁顺序相反,基类的创建优先于子类,因此,父类的清理应该放在子类清理之后。
引用计数
我们可以利用引用计数来计算对象创建与销毁的数量
例如
public class Shared {
private int refcount = 0;
private static long counter = 0;
private final long id = counter++;
public Shared() {
System.out.println("Creating " + this);
}
public void addRef() {
refcount++;
}
protected void dispose() {
if (--refcount == 0) {
System.out.println("Disposing " + this);
}
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "Shared " + id;
}
}
public class Composing {
private Shared shared;
private static long counter = 0;//所有Composing对象共享
private final long id = counter++;
public Composing(Shared shared) {
System.out.println("Creating" + this);
this.shared = shared;
shared.addRef();
}
protected void dispose() {
System.out.println("Disposing " + this);
shared.dispose();
}
@Override
public String toString() {
return "Composing "+id;
}
public static void main(String[] args) {
Shared shared = new Shared();
Composing [] composings = {new Composing(shared),new Composing(shared),new Composing(shared),
new Composing(shared),new Composing(shared),new Composing(shared)};
for(Composing composing : composings){
composing.dispose();
}
}
}
分析:
利用
private static long counter = 0;//所有Composing对象共享
private final long id = counter++;
为Composing生成ID
内部利用shared对象进行引用计数
此种方式相对复杂,一是设计时要记得在创建和销毁对象时分别调用Shared对象的add和dispose
二是创建对象时要确保构造方法的shared对象是同一个
三是确保客户端调用创建之后要调用Composing对象的dispose
当需要进行类共享时,此种方法失效,因为并不能知道客户端合适创建对象,也就无法确保使用的shared对象是同一个(计数时计数不统一)
8.3.3 构造器内部的多态
先看一个例子
public class Glyph {
void draw(){
System.out.println("Glyph.draw");
}
public Glyph() {
System.out.println("Glyph Constructor before.draw");
draw();
System.out.println("Glyph Constructor after.draw");
}
}
public class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
System.out.println("RoundGlyph Constructor radius="+radius);
radius = r;
System.out.println("RoundGlyph Constructor radius="+radius);
}
void draw(){
System.out.println("RoundGlyph.draw radius="+radius);
}
}
public class Test {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
分析
按照我们之前的分析,该例子的调用顺序是什么样的呢
首先从main入手RoundGlyph调用,尝试调用RoundGlyph构造方法,但是发现继承自Glyph
调用Glyph构造方法,但是构造方法中存在一个draw方法被RoundGlyph覆盖了,怎么办呢,这里仍然使用多态,调用子类的方法,但是注意此时是父类创建的过程,子类的变量尚未初始化,因此输出RoundGlyph.draw radius=0 父类构造方法调用完毕
子类变量初始化radius先初始化为1之后在构造方法中又被赋值为5
所以输出:
Glyph Constructor before.draw
RoundGlyph.draw radius=0
Glyph Constructor after.draw
RoundGlyph Constructor radius=1
RoundGlyph Constructor radius=5
无论如何,这种输出虽然可以理解,但是总存在容易误解的地方
如何规避
创建对象时尽量用尽可能简单的方法创建对象,如果可能尽量避免在构造器调用其他方法。在构造器调用的方法只能是final或者private的,此类方法子类无法覆盖,也就不会因为多态产生影响。
禁忌是在构造方法调用被子类覆盖的方法,此时由于初始化关系,极有可能出现预想不到的错误。
构造方法调用顺序再分析
1.所有事情发生前,所有对象的存储空间初始化成二进制0
2.调用基类构造器,如果父类构造方法调用了子类覆盖的方法,产生多态,调用子类的对应方法,但是由于1步骤,子类的所有变量都被初始化为default值
3.按照声明顺序调用成员初始化方法
4.对子类进行初始化(子类初始化顺序同父类)
8.4 协变返回类型
术语很高端的样子,其实就是子类覆盖父类的方法时,其返回值类型可以比父类的小(子类的返回值类型可以是父类返回值类型的子类,涉及到两组继承,听起来可能绕口)
例子:
public class Grain {
@Override
public String toString() {
return "Grain";
}
}
public class Wheat extends Grain{
@Override
public String toString() {
return "Wheat";
}
}
public class Mill {
Grain process() {
return new Grain();
}
}
public class WheatMill extends Mill{
@Override
Wheat process() {
return new Wheat();
}
}
public class Test {
public static void main(String[] args) {
Mill mill = new Mill();
Grain grain = mill.process();
System.out.println(grain);
mill = new WheatMill();
grain = mill.process();
System.out.println(grain);
}
}
输出
Grain
Wheat
这个例子还是很好懂的
8.5继承vs组合
例子略过,该例子结合了继承和组合
1.继承很笨重 组合较灵活,应该优先选择组合
2.用继承表达行为间的差异,用字段表达状态的变化(感觉是只可意会 不可言传的经典,积累一定的经验,理解这句话也是水到渠成的事)
3.继承是is-a关系,子类可以完全替代父类
个人觉得书中的总结比自己的好多了,转贴如下
向下转型
由于向上转型会丢失子类特性,所以有时我们需要使用向下转型(强制转换),但是这不是安全的,错误的转换会导致ClassCastException,我们可以利用instanceof来规避,但是这仍然会耗费一些逻辑,而且当子类型数量庞大时,逻辑会混乱和冗杂。总之要酌情使用。
不管我们是不是进行了类型判断,每次进行强制转换前,java总会在运行时进行类型判断,如果类型错误,抛出类型转换错误。