- 复用代码是Java众多引人注目的功能之一。但要想成为极具革命性的语言, 仅仅能够复制代码并对之加以改变是不够的,它还必须能够做更多的事情。
- 新的类是由现有类的对象所组成,这种方法称为 组合。
- 按照现有类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。被称为 继承。
组合语法
- 组合,只需将对象引用置于新类中即可。
public class WateSource {
private String s;
WateSource(){
System.out.println("WateSource");
s="Constructed";
}
@Override
public String toString() {
return s;
}
}
//第二个类
public class SprinklerSystem {
private String v1,v2,v3,v4;
private WateSource wateSource=new WateSource();
private int i;
private float f;
@Override
public String toString() {
return "SprinklerSystem{" +
"v1='" + v1 + '\'' +
", v2='" + v2 + '\'' +
", v3='" + v3 + '\'' +
", v4='" + v4 + '\'' +
", wateSource=" + wateSource +
", i=" + i +
", f=" + f +
'}';
}
public static void main(String[] args) {
SprinklerSystem system = new SprinklerSystem();
System.out.println(system);
}
//运行结果为
WateSource
SprinklerSystem{v1='null', v2='null', v3='null', v4='null', wateSource=Constructed, i=0, f=0.0}
- 上面俩个类所定义的方法中,有一个很特殊: toString()。每一个非基本类型的对象都有一个 toString()方法,而且当编译器需要一个String 而你却只有一个对象时,该方法便会被调用。所以在 SprinklerSystem.toString()的表达式:
", wateSource=" + wateSource
- 编译器将会得到你想要将一个String对象 (", wateSource=") 同 wateSource 相加。由于只能将一个String对象和另一个String对象相加,因此编译器会告诉你: 我们将调用 toString(),把source转换成为一个 String 这样做之后,它就能将俩个String 连接到一起并将结果传递给 System,out.println() 。每当想要使所创建的类具备这样的行为时,仅需要编写一个 toString() 方法。
- 编译器并不是简单地为每一个引用都创建默认对象, 这一点很有意义的, 因为若真要那样做的话, 就会在许多情况下增加不必要的负担。如果想初始化这些引用,如下这样做:
- 在定义对象的地方。意味着它们总是能够在构造器被调用之前被初始化。
- 在类的构造器中。
- 就在正在使用这些对象之前, 这种方式被称为 惰性初始化。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担。
继承语法
- 继承是所有 OOP 语言和Java 语言不可缺少的组成部分。 当创建一个类时, 总是在继承, 因此,除非已明确指出要从其他类中继承, 否则就是隐式地从Java 的标准根类 Object 进行继承。
- 组合的语法比较平实, 但是继承使用的是一种特殊的语法。
- 继承视为对类的重用。
- Java中用 super 关键字表示超类的意思, 当前类就是从超类继承来的。
初始化基类
- 对基类子对象的正确初始化也是至关重要的, 而且也仅有一种方法来保证这一点: 在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。
class Art {
Art(){
System.out.println("Art 构造器");
}
}
class Drawing extends Art{
Drawing(){
System.out.println("Drawing 构造器");
}
}
class Cartoon extends Drawing{
Cartoon(){
System.out.println("Cartoon 构造器");
}
public static void main(String[] args) {
Cartoon cartoon = new Cartoon();
}
}
//运行结果为
Art 构造器
Drawing 构造器
Cartoon 构造器
- 你可能会发现,构建的过程是 基类 向外 扩散的, 所以基类在导出类构造器可以访问它之前,就已经完成了初始化。即使你不为Cartoon()创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类的构造器。
带参数的构造器
- 如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须使用 super 关键字。
class Game{
Game(int i){
System.out.println("game i= "+i);
}
}
class BoardGame extends Game{
BoardGame(int i) {
super(i);
System.out.println("BoardGame");
}
}
public class Chess extends BoardGame {
Chess() {
super(123);
System.out.println("Chess");
}
public static void main(String[] args) {
Chess chess = new Chess();
}
}
- 如果不在 BoardGame 调用基类构造器,编译器将 无法找到符合 Game 形式的构造器,会提醒你。
代理
- Java并没有提供对它的直接支持。这是继承与组合之间的中庸之道,
- 因为我们将一个成员对象置于所要构建的类中(就像组合), 但与此我们再新类中暴露了该成员对象的所有方法(就像继承)如下
public class SpaceShipControls {
void up(int velocity){}
void down(int velocity){}
void left(int velocity){}
void right(int velocity){}
}
//构建太空船的一种方式使用继承
public class SpaceShip extends SpaceShipControls {
private String name;
SpaceShip(String name){
this.name=name;
}
@Override
public String toString() {
return name;
}
public static void main(String[] args) {
SpaceShip spaceShip = new SpaceShip("zs");
spaceShip.left(100);
}
}
- 然而,SpaceShip 并非真正地 SpaceShipControls 类型, 即便告诉你 SpaceShip 向右运动(right()),更准确地讲, SpaceShip包含 SpaceShipControls ,与此同时,SpaceShipControls 的所有方法在 SpaceShip都暴露出来 ,代理解决了此问题。
public class SpaceShipDelegation {
private String name;
private SpaceShipControls spaceShipControls=new SpaceShipControls();
SpaceShipDelegation(String name){
this.name=name;
}
public void up(int velocity){
spaceShipControls.up(velocity);
}
public void left(int velocity){
spaceShipControls.left(velocity);
}
public void right(int velocity){
spaceShipControls.right(velocity);
}
public static void main(String[] args) {
SpaceShipDelegation spaceShipDelegation = new SpaceShipDelegation("zs");
spaceShipDelegation.right(100);
}
}
- 上面的方法是如何传递给了底层 spaceShipControls 对象,而其接口由此也就与使用继承得到的接口相同。
- 我们使用代理时,可以拥有更多控制力,因为我们可以选择只提供在成员对象成员中的方法的某个子集。
- 尽管Java语言不直接支持代理,但是很多开发工具却支持代理。如 IDEA
确保正常清理
- Java中没有 C++中析构函数的概念。 析构函数是一种在对象被销毁时可以自动调用的函数。
- 其原因是 在Java中,我们习惯忘掉而不是销毁对象,并且让垃圾回收器在必要时释放其内存。
- try 用一组大括号括起来的范围 是所谓的保护区,这意味着它需要被特殊处理。
- 其中一项是无论 try 是怎样退出的, 保护区后的 finally 子句中的代码总是要被执行的。
- 执行类的所有特定的清理动作,其顺序同生成顺序相反(通常这就要求基类元素仍旧存活)。
public class Shape {
Shape(int i){
System.out.println("Shape construtor");
}
void dispose(){
System.out.println(
"Shape,dispose"
);
}
}
public class Corcle extends Shape{
Corcle(int i) {
super(i);
System.out.println(
"Corcle constrctor"
);
}
@Override
void dispose() {
System.out.println("Circle dispose");
super.dispose();
}
public static void main(String[] args) {
Corcle corcle = new Corcle(1);
corcle.dispose();
}
}
//运行结果
Shape construtor
Corcle constrctor
Circle dispose
Shape,dispose
名称屏蔽
- 如果 Java 的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本(这一点与C++不同)。
- 无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作:
public class Homer {
char doh(char c){
System.out.println("char doh method");
return 'd';
}
float doh(float f){
System.out.println("doh float method");
return 1.0f;
}
}
class Milhouse{}
class Bart extends Homer{
void doh(Milhouse milhouse){
System.out.println("dob milhouse method");
}
public static void main(String[] args) {
Bart bart = new Bart();
bart.doh(1);
bart.doh('x');
bart.doh(2.2f);
bart.doh(new Milhouse());
}
}
//运行结果
doh float method
char doh method
doh float method
dob milhouse method
- 虽然 Bart 引入了一个新的重载方法,但是在 Bart 中 Homer的所有重载方法都是可用的。
- 使用与基类完全相同的特征签名及返回类型来覆盖具有相同名称的方法,是一件及其平常的事。
- Java SE5 新增了 @Override 注解 如果你重载而并非覆写该方法,编译器会生成一个错误。如下
@Override
void doh(Milhouse milhouse){
System.out.println("dob milhouse method");
}
//错误信息
method does not override a method from its superclass
//翻译过来是
方法不会覆盖其父类中的方法
- @override 注解可以防止你在不想重载时而意外地进行重载。
在组合与继承之间选择
- 组合和继承都允许在新的类中放置子对象, 而继承是隐式地做。
- 组合技术 通常用于想在 新类中使用现有类的功能那而并非它的接口这种形式。即,在新类中嵌入某个对象,让其实现所需要的功能, 但新类的用户看到的只是为新类所定义的接口,而并非所嵌入对象的接口。
public class Car {
//创建发动机
public Engine engine=new Engine();
//创建轮胎
public Wheel[] wheels =new Wheel[4];
//创建门
public Door left=new Door(),
right=new Door();
Car(){
//初始化轮胎
for (int i = 0; i < 4; i++) {
wheels[i]=new Wheel();
}
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rolldown();
car.wheels[0].inflate();
}
}
//发动机
class Engine{
public void start(){}
public void stop(){}
}
//窗口
class Window{
//卷起
public void rollup(){}
//滚下来
public void rolldown(){}
}
//门
class Door{
public Window window=new Window();
public void open(){}
public void close(){}
}
//轮胎
class Wheel{
public void inflate(){}
}
- 用一个 交通工具 对象来构成一部车子是毫无意义的,因为车子并不包含交通工具,它仅是一种交通工具 (is - a关系)。
- is-a 的关系是用集成来表达的,而 has-a 的关系则使用组合来表达的。
protected 关键字
- 在实际项目中,经常想要把某些事物尽可能对这个世界隐藏起来,但仍然允许导出类的成员访问它们。
- 关键字 protected 就起这个作用。 但是最好的方式还是将域保持为 private ,你应当一致保留 更改底层实现 的权利。然后通过 protected 方法来控制类的集成这的访问权限。
public class Villain {
private String name;
protected void setName(String name){
this.name=name;
}
Villain(String name){
this.name=name;
}
@Override
public String toString() {
return "Villain"+name;
}
}
//第二个测试类
public class Orc extends Villain {
private int orcNumber;
Orc(String name,int orcNumber) {
super(name);
this.orcNumber=orcNumber;
}
public void change(String name,int orcNumber){
setName(name);
this.orcNumber=orcNumber;
}
@Override
public String toString() {
return "Orc{" +
"orcNumber=" + orcNumber +
super.toString()+
'}';
}
public static void main(String[] args) {
Orc orc = new Orc("zs", 13);
System.out.println(orc);
orc.change("hello",66);
System.out.println(orc);
}
}
//运行结果为
Orc{orcNumber=13Villainzs}
Orc{orcNumber=66Villainhello}
- change方法 可以访问 setName 方法,这是因为它是 protected 的。还应注意 Orc 的toString 方法的定义方式,它根据 toString 的基类版本而定义。
向上转型
- 为新的类提供方法 并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。 可以用 新类是现有类的一种类型 加以概括。
public class Instrument {
public void play(){}
public static void tune(Instrument instrument){
instrument.play();
}
}
class Wind extends Instrument{
public static void main(String[] args) {
Wind wind = new Wind();
Instrument.tune(wind);
}
}
- 在 tune方法中,程序代码可以对 Instrument 和 它所有的导出类起作用,这种将 Wind 引用转换为 Instrument 引用的动作,我们称之为 向上转型。
- 由导出类 转型为 基类,在继承图上是向上移动的,因此一般称为向上转型。
- 由于向上转型是从一个较专用类型向通用类型转换,所以总是很安全的。也就是说, 导出类是基类的一个超集。它可能比基类含有更多方法,但它必须至少具备基类中所含有的方法。
- 在向上转型的过程中,类接口中唯一可能发生的事情是丢失方法,而不是获取他们。
在论组合与继承
- 在面向对象编程中,生成和使用程序代码最有可能采用的方法就是直接将数据和方法包装进一个类中,并使用该类对象。
- 也可以使用组合技术使用现有类来开发新的类, 而继承技术其实是不太常用的。
- 尽管在教授 OOP 的过程这亚红我们多次强调继承,但这并不意味着要尽可能使用它。 相反,应当慎用这一技术,其使用场合仅限于确信使用该技术确实有效的情况。到底是使用组合还是用继承,一个最清晰的判断方法就是问一问自己是否需要从新类向基类进行向上转型。如果必须要向上转型,则继承是必要的,如果不需要,则应该好好考虑一下自己是否需要继承。
final 关键字
- 通常指 这是无法改变的。不想做改变可能出于两个原因:
- 设计
- 效率
- final 数据 :许多编程语言都有某种方法,来向编译器告知一块数据是恒定不变的。有时数据恒定不变还是很有用的。例如
- 一个永不改变的编译时常量。
- 一个在运行时被初始化的值,而你不希望它被改变。
- 在Java中,中类常量必须是基本数据类型,并且以 关键字 final表示。在对这个常量进行定义的时候,必须对其进行赋值。
- 一个即是 static 又是 final 的域只占据一段不能改变的存储空间。
- 当对象引用而不是基本数据类型运用 final时,其含义有一点令人迷惑。 一旦引用被初始化指向了一个对象,就无法在把它改为指向另一对象。然而,对象自身确实可以被修改的,Java并未提供使任何对象恒定不变的途径。
- 空白 final :空白final是指被声明为 final但又未给定初值的域。
class Poppet{
private int i;
Poppet(int i){
this.i=i;
}
}
public class BlankFinal {
private final int i=0;
private final int j;
private final Poppet poppet;
BlankFinal(){
j=1;
poppet=new Poppet(1);
}
BlankFinal(int i){
j=i;
poppet=new Poppet(i);
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(33);
}
}
- 必须在域定义处或者每个构造器中用表达式对 final进行赋值,这正是final域在使用前总是被初始化的原因所在。
- final 参数
- Java允许在参数列表中以声明的方式将参数指明为 final。这就意味着你无法更改参数引用所指向的对象。
class Gizmo{
public void spin(){
}
}
public class FinalArgments {
void with(final Gizmo gizmo){
// !gizmo=new Gizmo(); //Illegal -- g is final
}
void without(Gizmo gizmo){
gizmo=new Gizmo();
gizmo.spin();
}
//void f(final int i){ i++; //can't change }
//you can only read from a final primtive
int g(final int i){
return i+1; //can't change
}
public static void main(String[] args) {
FinalArgments argments = new FinalArgments();
argments.with(null);
argments.without(null);
}
}
- f() 和 g() 展示了当前类型的参数被指明为 final 时所出现的结果: 你可以读取参数,但却无法修改参数。
- final 方法
- 使用 final方法原因有两个:
- 把方法锁定,以防任何继承类修改它的含义。
- 效率。在Java早期实现中,如果将一个方法指明为 final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。 当编译器发现 一个 final 方法调用命令时, 它会根据自己的谨慎判断,跳过插入程序代码这种正常方式而执行方法调用机制(将参数压入栈,跳至方法代码处并执行,然后跳回并清理栈中的参数,处理返回值),并且以方法体重的实际代码的副本来替代方法的调用。这将消除方法调用的开销。
- 当然,如一个方法很大,你的程序代码就会膨胀,因而可能看不到内嵌带来的任何性能提高,因为,所带有的性能提高会因为话费于方法内的时间量而被缩减。
- 使用Java SE5/6时,应该让编译器和JVM去处理效率问题,只有在想要明确禁止覆盖时,才将方法设置为 final的。
final 和 private 关键字
- 类中所有的 private 方法都隐式地指定为是 final的,由于无法取用 private 方法,所以也就无法覆盖它,可以对 private 方法添加 final 修饰词, 但这并不能给该方法添加任何额外的意义。
final 类
- 当将某个类的整体定义为 final 时(通过将关键字 final 置于它的定义之前),就表明了你不打算继承该类,而且也不允许别人这样做。
- 简而言之 : 出于某种考虑, 你对该类的设计永远不需要做任何变动,或者出于安全考虑,你不希望它有子类。
class SmallBrain{}
final class Dinosaur{
int i=7;
int j=1;
SmallBrain smallBrain=new SmallBrain();
void f(){}
}
//class Further extends Dinosaur{}
//error Cannot extend final class Dinosaur
public class Jurassic {
public static void main(String[] args) {
Dinosaur dinosaur = new Dinosaur();
dinosaur.f();
dinosaur.i=40;
dinosaur.j++;
}
}
- final 类禁止继承,所以 final 类中所有的方法都隐式指定为 final的,因为无法覆盖它们。
- 在 final 类中可以给方法添加 final 修饰词,但这不会添加任何意义。
- 在设计类时, 将方法指明是 final 的,应该说是明智的。你可能会觉得,没人想要覆盖你的方法。有时这是对的。
初始化及类的加载
- 在许多传统语言中,程序是作为启动过程的一部分立刻被加载的。然后是初始化,紧接着程序开始运行。
- 这些语言的初始化过程必须小心控制, 以确保定义为 static 的东西, 其初始化顺序不会造成麻烦。
- Java 采用另一个不同的加载方式。使加载时众多变得更加容易的动作之一, 因为Java中的所有事物都是对象。请记住 每个类的编译代码都存在于它自己的独立的文件中。该文件只在需要使用程序代码时才会被加载。
- 初次使用之处也是 static 初始化发生之外。所有的 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 string){
System.out.println(string);
return 22;
}
}
public class Beetle extends Insect{
private int k=print("Beetle k initialized");
Beetle(){
System.out.println("k= "+k+"----- j= "+j);
}
private static int x2=print("static Beetle x2");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle beetle = new Beetle();
}
}
//运行结果
static Insect x1 initialized
static Beetle x2
Beetle constructor
i= 9---- j= 0
Beetle k initialized
k= 22----- j= 39
- 在Beetle 上运行Java时,所发生的第一件事情就是试图访问 Beetle.main()(一个static方法),于是加载器开始启动并找出 Beetle 类的编译代码(在名为 Beetle.class的文件中)。在对它进行加载的过程中,编译器注意到它有一个基类(这是由关键字 extends 得知的),于是它继续进行加载。不管你是否打算产生一个该基类的对象,这都是要发生的(请尝试将对象创建代码注释掉,以证明这一点)。
- 如果该基类还有其自身的基类,那么第二个基类就会被加载,如此类推。
- 基类构造器和导出类的构造器一样,以相同顺序来经历相同的过程。在基类构造器完成之后,实例变量按其次序被初始化。最后,构造器的其余部分被执行。
总结
- 继承和组合都能从现有类型生成新类型。组合一般是将现有类型作为新类型底层实现的一部分来加以复用,而继承复用的是接口。
- 在使用继承时,由于导出类具有基类接口,因此它可以向上转型至基类,这对多态来讲至关重要。
- 尽管面向对象编程对继承极力强调, 但在开始一个设计时,一般应优先选择使用组合(或者可能是代理),只在确实必要时才使用继承。因为组合更具灵活性。此外,通过对成员类型使用继承技术的添加技巧, 可以在运行时改变那些成员对象的类型和行为。因此,可以在运行时改变组合而成的对象的行为。
- 当你开始设计一个系统时,应该认识到程序开发是一种增量过程,犹如人类的学习一样,这一点很重要。程序开发依赖于实验,你可以尽己所能去分析,当你开始执行一个项目时,你任然无法知道所有的答案。如果将项目视作是一种有机的,进化着的生命体去培养,二步是打算像盖摩天大楼一样快速见效,就会获得更多的成功和更迅速的回馈。
- 组合与继承正是面向对象程序设计中使得你可以执行这种实验的最基本的俩个工具。