北京两学一做网站,网站目录管理模板下载,博客网站排名,网站页面建设规划文案目录
组合
继承
委托
组合和继承的结合
确保正确的清理
名称隐藏
在组合和继承之间选择
protected关键字
向上转型
final关键字
final数据
final方法
final类
初始化及类的重载 本笔记参考自#xff1a; 《On Java 中文版》 对面向对象的编程语言而言#xff0…目录
组合
继承
委托
组合和继承的结合
确保正确的清理
名称隐藏
在组合和继承之间选择
protected关键字
向上转型
final关键字
final数据
final方法
final类
初始化及类的重载 本笔记参考自 《On Java 中文版》 对面向对象的编程语言而言复用意味着可以在新类中使用其他人已经构建和调试过的类而不必从头开始编写它们。要在不污染原有代码的基础上使用新类可以通过两种方式
组合在新类中创建现有类的对象继承直接复制原有类的形式然后向其中添加代码不修改原有类。
组合 组合意味着将对象引用放入到新类中。
class WaterSource {private String s;WaterSource() {System.out.println(一个WaterSource类的构造器);s 构造器;}Overridepublic String toString() {return s;}
}public class SprinklerSystem {private String value1, value2, value3, value4;private WaterSource source new WaterSource();private int i;private float f;Overridepublic String toString() {return value1 value1 , value2 value2 , value3 value3 , value4 value4 \n i i , f f \n source source; // 为了将对象source转换为字符串会调用toString()方法}public static void main(String[] args) {SprinklerSystem sprinklers new SprinklerSystem();System.out.println(sprinklers);}
} 程序执行的结果如下 在上述定义的方法中存在一个特殊的方法toString()。每个非基本类型的对象都有一个toString()方法这个方法会在一些特殊情况下比如将对象转换为字符串的时候被调用。 Override被用在了toString()方法的修饰中这种用法有助于检测问题比如拼写错误。 一致编译器会初始化类中的基本类型字段将其全部置为0。但是对于引用而言编译器并不会简单地为其创建默认对象这会产生不必要的开销。因此引用的初始化一般有4种方式
在定义对象时进行初始化这会使得它们在调用构造器前被初始化在类的构造器中进行初始化延迟初始化即在对象被实际使用之前进行初始化减少开销实例初始化。 4中初始化方式的使用例如下
class Soap {private String s;Soap() {System.out.println(Soap()的构造器);s 构造器;}Overridepublic String toString() {return s;}
}public class Bath {private String s1 Hello, s2 World; // 在定义时进行初始化private String s3, s4;private Soap castile;private int i;private float toy;public Bath() {System.out.println(在构造器Bath()中);s3 Begin; // 在构造器中进行初始化toy 3.14f;castile new Soap();}{ // 实例初始化i 47;}Overridepublic String toString() {if (s4 null) // 延迟初始化s4 Java;return s1 s1 , s2 s2 , s3 s3 , s4 s4 \n i i , toy toy \n castile castile;}public static void main(String[] args) {Bath b new Bath();System.out.println(b);}
}程序运行的结果是 若在对象初始化之前向对象引用发送消息就会得到一个运行时异常。 继承 事实上当创建一个类时继承总是在发生若没有明确指定要继承的类那么就会隐式继承Java的标准根类Object。 继承使用的是一种特殊的语法要求在类主体的左花括号之前使用extends关键字后跟基类的名称进行声明。这会使新类自动获得基类的所有字段和方法
class Cleanser {private String s Cleanser;public void append(String a) {s a;}public void scrub() {append(scrub());}Overridepublic String toString() {return s;}public static void main(String[] args) {Cleanser x new Cleanser();x.scrub();System.out.println(x);}
}public class Detergent extends Cleanser {Overridepublic void scrub() { // 修改方法append(Detergent.scrub());super.scrub(); // 调用当前方法的基类版本}public void foam() { // 添加新的方法append(foam());}public static void main(String[] args) {Detergent x new Detergent();x.scrub();x.foam();System.out.println(x);System.out.println(\n调用基类);Cleanser.main(args);}
} 程序运行的结果如下 在上述的程序中Cleanser和Detergent都有一个main()方法。每个类都可以有一个main()方便后继的测试。但是唯一能够运行的main()是在命令行中被调用的那一个。 Cleanser中的方法都是public的因此在同一个包中的所有类都可以访问这些方法。这种做法就是考虑了继承作为一般规则将所有字段设为private将所有方法设为public或protected。 最后可以在子类中对基类中定义的方法进行修改例如上述例子中的scrub()。 使用super关键字来指代当前类继承的基类又称“超类”。因此super.scrub()调用的是基类的scrub()方法。 初始化基类 基类和子类的关系并不简单。当创建了一个子类的时候其中就会包含一个基类的子对象和直接通过积累创建的对象是一样的。但从外面看基类的子对象被包裹在了子类的对象中。为了正确初始化基类的自对象只能在子类构造器中调用基类构造器来执行初始化。Java会自动往子类构造器中插入基类构造器。
class Art {Art() {System.out.println(类Art的构造器);}
}class Drawing extends Art {Drawing() {System.out.println(Drawing的构造器);}
}public class Cartoon extends Drawing {public Cartoon() {System.out.println(Cartoon的构造器);}public static void main(String[] args) {Cartoon x new Cartoon();}
} 程序运行的结果如下 从上述程序可以总结出构造过程的顺序即从基类开始“向外”进行。因此基类会在子类构造器访问它之前被初始化。另外即使子类没有构造器编译器也会自动合成一个可以调用基类构造器的无参构造器。
---
带参数的构造器 若基类没有无参构造器或者必须调用具有参数的构造器则需要使用super关键字和其对应的参数列表显式地调用基类构造器否则会报错
class Game {Game(int i) {System.out.println(含参的Game构造器传入参数为 i);}
}class BoardGame extends Game {BoardGame(int i) {super(1);System.out.println(含参的BoardGame构造器传入参数为 i);}
}public class Chess extends BoardGame {Chess() {super(11);System.out.println(Chess的构造器);}public static void main(String[] args) {Chess x new Chess();}
} 程序运行的结果如下 注意对基类构造器的调用必须是子类构造器的第一个操作。 委托 尽管Java没有提供直接支持但是Java中确实存在除组合和继承以外的第三种关系委托。这种关系介于组合和继承之间有两者的一些特点
类似组合将成员对象放在正在构建的类中类似继承在新类中公开了成员对象的所有方法。 例如现在有一个飞船控制模块
public class SapceShipControls {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 DerivedSpaceShip extends SapceShipControls {private String name;public DerivedSpaceShip(String name) {this.name name;}Overridepublic String toString() {return name;}public static void main(String[] args) {DerivedSpaceShip protector new DerivedSpaceShip(保护器);protector.forward(100);}
} 但是这里存在着一个问题。当这种类的继承发生的时候也就意味着基类的所有方法都会因为这个子类而被暴露给了外部。因此就需要使用委托进行解决
public class SpaceShipDelegation {private String name;private SapceShipControls controls new SapceShipControls();public SpaceShipDelegation(String name) {this.name name;}// 下面开始使用委托public void up(int velocity) {controls.up(velocity);}public void down(int velocity) {controls.down(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(保护器);protector.forward(100);}
} 通过委托方法调用会被转发到隐藏的controls对象这样接口可以得到的就和使用继承得到的是相同的。当然更好的委托控制的方式是仅提供部分的方法。 尽管Java本身不支持委托但是开发工具通常支持。 组合和继承的结合 在实际的编程中会将继承和组合同时进行使用。例如
class Plate {Plate(int i) {System.out.println(Plate的构造器);}
}class DinnerPlate extends Plate {DinnerPlate(int i) {super(i); // 调用基类构造器System.out.println(DinnerPlate的构造器);}
}class Custom {Custom(int i) {System.out.println(Custom的构造器);}
}public class PlaceSetting extends Custom {private DinnerPlate pl;public PlaceSetting(int i) {super(i 1);pl new DinnerPlate(i 2);System.out.println(PlaceSetting的构造器);}public static void main(String[] args) {PlaceSetting x new PlaceSetting(3);}
} 程序执行的结果如下 虽然编译器会强制要求对基类的初始化但对于类的成员对象编译器不会进行监督。 确保正确的清理 Java没有C的析构函数或许是因为通过垃圾收集器就可以在需要时回收内存。但在类的生命周期中也可能存在一些需要清理的行为。因为不知道垃圾收集器合适被调用所以为了清理某些东西就必须在finally子句中设置清理活动。
class Shape {Shape(int i) {System.out.println(开始Shape的构造);}void dispose() {System.out.println(进行Shape的清理工作);}
}class Circle extends Shape {Circle(int i) {super(i);System.out.println(画一个圆);}Overridevoid dispose() {System.out.println(擦除圆);super.dispose();}
}class Line extends Shape {private int start, end;Line(int start, int end) {super(start);this.start start;this.end end;System.out.println(画一条线 start , end);}Overridevoid dispose() {System.out.println(擦除线 start , end);super.dispose();}
}public class CADSystem extends Shape {private Circle c;private Line[] lines new Line[3];public CADSystem(int i) {super(i 1);for (int j 0; j lines.length; j)lines[j] new Line(j, j * j);c new Circle(1);System.out.println(合并构造器);}Overridepublic void dispose() {System.out.println();System.out.println(开始总的清理工作);// 清理的顺序和初始化的顺序相反c.dispose();for (int i lines.length - 1; i 0; i--)lines[i].dispose();super.dispose();}public static void main(String[] args) {CADSystem x new CADSystem(47);try {// 代码及异常处理...} finally {x.dispose();}}
} 上述程序的运行结果是 在上述程序中每一个类都有自己的dispose()方法用来将非内存相关的事物回复到原本的状态。 上述程序中出现了这样的结构
try {// ...
} finally {// ...
}
其中try关键字后面的代码块是应该保护区域用来进行特殊处理。这种特殊处理之一就是无论try以何种形式推出保护区域后面的finally子句都会执行这种设定允许以各种非常规的方式退出try代码块。 除此之外在调用清理方法时应该注意调用顺序防止子对象依赖另一个的情况出现。类的特定清理工作顺序应该和创建顺序相反。 除了内存回收其他情况并不建议依赖垃圾收集。 名称隐藏 若Java基类的方法名称被多次重载那么子类中重新定义的该方法名称将不会隐藏任何基类版本。
class Homer {char doh(char c) {System.out.println(方法的形式是doh(char));return d;}float doh(float f) {System.out.println(方法的形式是doh(float));return 1.0f;}
}class Milhouse {
}class Bart extends Homer {void doh(Milhouse m) {System.out.println(方法的形式是doh(Milhouse));}
}public class Hide {public static void main(String[] args) {Bart b new Bart();b.doh(1);b.doh(x);b.doh(1.0f);b.doh(new Milhouse());}
} 程序执行的结果如下 上述程序中Homer类所有重载的doh()方法都可以在Bart中被使用并且Bart中也引入了一个新的重写方法。为了区分重写和重载在重写同名方法时应该使用与基类完全相同的签名和返回类型。 Override可以帮助检测分析一个方法是重载或是重写。例如如果不小心进行了重载
public class Lisa extends Homer {Overridevoid doh(Milhouse m) {System.out.println(方法的形式是doh(Milhouse));}
} 尝试编译发生报错 在组合和继承之间选择 一般可以参考下列意见进行选择
使用组合时往往希望在新类中使用现有类的功能而不是其接口。对于新类中通过组合得到的成员有时可以允许类的使用者直接访问它们可将这种成员设为public。因为成员对象的实现是隐藏的所以这种做法也是安全的。使用继承时需要通过现有类生成一个其的特殊版本。也就是说需要对通用类进行“定制”使其满足特定需求。 除此之外也可以这样说继承表示的是“is-a”的关系而组合表示的是“has-a”的关系。以汽车为例汽车是is一种交通工具因此使用“交通工具”来组合has一部“汽车”是无意义的。 继承的使用应该是谨慎的因此只有在继承能够明显发挥作用时再使用它。确定使用继承或是组合除了上述标准外还可以通过判断是否需要从新类向上转型到基类来进行。 protected关键字 protected关键字对于类的用户而言这是private的但对于继承该类的任何类或同一个包中的其他类而言这是可用的protected会提供包访问权限。 虽然字段也可以是protected的但是最好将字段设置为private保证修改的权利而将方法设置为protected以此来控制继承者的访问权限。例如
class Output {private String name;protected void set(String nm) {name nm;}Output(String name) {this.name name;}Overridepublic String toString() {return 这是Output函数即将输出 name;}
}public class Orc extends Output {private int orcNumber;public Orc(String name, int orcNumber) {super(name);this.orcNumber orcNumber;}public void change(String name, int orcNumber) {set(name); // 可以使用protected的方法this.orcNumber orcNumber;}Overridepublic String toString() {return Orc orcNumber : super.toString();}public static void main(String[] args) {Orc orc new Orc(Chicken, 12);System.out.println(orc);orc.change(Duck, 22);System.out.println(orc);}
} 程序运行的结果是 向上转型 继承除了用来为新类提供方法更重要的是表达了新类和基类之间的关系这种关系具体可以概括为新类是现有类的一种类型。例如
class Instrument {public void play() {System.out.println(调用play方法);}static void tune(Instrument i) {// ...i.play();}
}public class Wind extends Instrument {public static void main(String[] args) {Wind flute new Wind(); // Wind方法也是Instrument它们有相同的接口// 下方语句发生了向上转型Instrument.tune(flute);}
} tune()方法接受一个Instrument引用。但是在上述程序中它也接受了Wind对象这是因为Wind对象也是一个Instrument对象Wind拥有所有的Instrument接口。因此tune()对Instrument及其的任何子类起作用。这种将子类引用转换为基类引用的行为就是向上转型 也可以将子类称为基类的超集。 final关键字 final关键字一般表示“这是无法更改的”。之所以需要阻止更改可能的原因有两个设计或是效率。final关键字可以在三个地方进行使用数据、方法和类。
final数据 在此之前需要先讨论常量常量会有用有两个原因
这种数据可以是一个永远不会改变的编译时常量这种数据可以是在运行时初始化的值并且程序员不希望数据被修改。 在Java中常量必须是基本类型并且使用final关键字表示必须在初始化时为其提供一个值。 一个即是static又是final的字段只会分配一块不能改变的储存空间。 若一个非基本类型对象引用使用了final关键字那么final会使得这一引用无法改变但是对象本身是可以进行修改的Java没有提供使对象恒定不变的方法。 final字段的使用例
import java.util.*;class Value {int i; // 拥有包访问权限的字段Value(int i) {this.i i;}
}public class FinalData {private static Random rand new Random(47);private String id;public FinalData(String id) {this.id id;}// 编译时常量private final int valueOne 9;private static final int VALUE_TWO 99;// 典型的公共常量public static final int VALUE_THREE 39;// 不可作为编译时常量private final int i4 rand.nextInt(20);static final int INT_5 rand.nextInt(20);private Value v1 new Value(11);private final Value v2 new Value(11);// 数组private final int[] a { 1, 2, 3, 4, 5, 6 };Overridepublic String toString() {return id : i4 i4 , INT_5 INT_5;}public static void main(String[] args) {FinalData fd1 new FinalData(fd1);// final修饰无法改变值// fd1.valueOne;// fd1.VALUE_TWO;// 非恒定不变的对象fd1.v2.i;fd1.v1 new Value(9);for (int i 0; i fd1.a.length; i)fd1.a[i];// 对象引用无法修改// fd1.v2 new Value(0);// fd1.a new int[3];System.out.println(fd1);System.out.println();System.out.println(创建一个新的FinalData对象);FinalData fd2 new FinalData(fd2);System.out.println(fd1);System.out.println(fd2);}
} 程序执行的结果如下 在上述程序中创建第二个对象并没有改变INT_5的值因为这个数据是静态的它只会在加载时初始化一次。 按照惯例具有常量初始值的final static基本类型编译时常量全部使用大写字母命名单词之间使用下划线分隔。 空白final 空白final即没有初始值的final字段。编译器会确保这种空白final字段在使用前被初始化。这种做法可以让类中的final字段对每个对象而言都是不同的同时保持其不可改变的特性
class Poppet {private int i;Poppet(int i2) {i i2;}
}public class BlankFinal {private final int i 0; // 进行了初始化的final字段private final int j; // 空白final字段private final Poppet p; // 空白final引用// 空白final必须在构造器中进行初始化public BlankFinal() {j 1;p new Poppet(1);}public BlankFinal(int x) {j x;p new Poppet(x);}public static void main(String[] args) {new BlankFinal();new BlankFinal(3);}
} final的赋值操作只能发生在
字段定义处构造器中。
---
final参数 在参数列表中也可以创建final参数。在方法内部无法改变final参数指向的内容而只能进行参数的读取。 final方法 使用final方法有两个原因
防止继承的类通过重写改变该方法的含义为了提高效率但一般不推荐这样考虑。 类中的任何一个private方法都是隐式的final。因为private方法即不可以访问也不能被重写。但若尝试重写一个private方法会发现编译器不会进行报错
class WithFinal {private final void f() { // final使用与否没有区别System.out.println(类WithFinal的方法f());}private void g() {System.out.println(类WithFinal的方法g());}
}class HavePrivate extends WithFinal {private final void f() {System.out.println(类HavePrivate的方法f());}private void g() {System.out.println(类HavePrivate的方法g());}
}class OverridingPrivate extends HavePrivate {public final void f() {System.out.println(类OverridingPrivate的方法f());}public void g() {System.out.println(类OverridingPrivate的方法g());}
}public class FinalOverridingIllusion {public static void main(String[] args) {OverridingPrivate op new OverridingPrivate();op.f();op.g();// 可以使用向上转型HavePrivate hp op;// 但是hp的方法是不可被调用的// hp.f();// hp.g();// 基类的方法也无法使用WithFinal wf op;// wf.f();// wf.g();}
} 程序运行的结果如下 注意重写只有在方法是基类接口的一部分时才会发生。 若一个方法是private的那么它就不是基类接口的一部分。它是隐藏在类中的代码只是恰好具有相同的名称罢了。即使在子类中创建了具有相同名称的方法这个方法也与基类中被隐藏的代码无关。 使用Override可以产生有用的报错信息。 final类 将一个类定义为final时会阻止该类的所有继承。这么做往往是不希望类的设计被修改或者不允许该类存在子类。
class SmallBrain {
}final class Dinosaur {int i 7;int j 1;SmallBrain x new SmallBrain();void f() {};
}// class Further extends Dinosaur{} // 无法继承final类public class Jurassic {public static void main(String[] args) {Dinosaur n new Dinosaur(); // 可以创建final类的对象n.f();n.i;n.j;}
} 由于上述Dinosaur类的方法都是隐式的所以无法被继承也就无法进行重写了。 无论类是否被定义为final相同的规则都会适用于字段的final定义。 初始化及类的重载 在Java中每个类的编译代码都存在于自己的单独文件中只有在需要使用时才会加载。一般认为“类的代码在第一次使用时才加载”在构造类的第一个对象或访问静态成员时。因为这种更加灵活的加载方式Java变得更容易操作。在这里需要说明的是
构造器是一个静态方法当一个类的任何静态成员被访问时都会触发其的加载。静态初始化发生在初次使用时所有静态对象和静态代码块在加载时按照文本顺序进行初始化。静态成员只初始化一次。
class Insect {private int i 0;protected int j;Insect() {System.out.println(i i , j j);j 39;}private static int x1 printInit(静态成员Insect.x1初始化完毕);static int printInit(String s) {System.out.println(s);return 47;}
}public class Beetle extends Insect {private int k printInit(成员Beetle.k初始化完毕);public Beetle() {System.out.println(k k);System.out.println(j j);}private static int x2 printInit(静态成员Beetle.x2初始化完毕);public static void main(String[] args) {System.out.println(调用Beetle的构造器);Beetle b new Beetle();}
} 程序执行的结果是 从程序的输出可以看出加载器加载的规律当运行java Beetle时加载器会在Beetle.class中找到Beetle的编译代码。在编译过程中加载器发现还有一个基类就会先去加载这一基类。而若基类还有自己的基类那么第二个基类也会被加载以此类推。直到执行完根基类上述程序中是Insect的静态初始化然后继加载根基类的子类。 这种加载方式的合理性在于子类的静态初始化可能会依赖于基类成员的正确初始化。 而对象的创建规律是① 子类Beetle中的所有基本类型先被设为默认值对象置为null。② 然后基类构造器会被调用重复子类构造器相同的过程。③ 基类构造器完毕后子类的实例变量按文本顺序初始化。④ 执行子类构造器的剩余部分。