Skip to content

【OnJava-八】复用

如何在不污染源代码的前提下使用现存代码是需要技巧的。在本章里,你将学习到两种方式来达到这个目的:

  1. 组合: 第一种方式直接了当。在新类中创建现有类的对象。这种方式叫做 “组合”(Composition),通过这种方式复用代码的功能,而非其形式。
  2. 继承:第二种方式更为微妙。创建现有类类型的新类。照字面理解:采用现有类形式,又无需在编码时改动其代码,这种方式就叫做 “继承”(Inheritance),编译器会做大部分的工作。继承是面向对象编程(OOP)的重要基础之一。更多功能相关将在多态(Polymorphism)章节中介绍。

组合与继承的语法、行为上有许多相似的地方(这其实是有道理的,毕竟都是基于现有类型构建新的类型)。在本章中,你会学到这两种代码复用的方法

组合

...

继承

java
class Game {
  Game(int i) {
    System.out.println("Game constructor");
  }
}
class BoardGame extends Game {
  BoardGame(int i) {
    super(i);
    System.out.println("BoardGame constructor");
  }
}
public class Chess extends BoardGame {
  Chess() {
    super(11);
    System.out.println("Chess constructor");
  }
  public static void main(String[] args) {
    Chess x = new Chess();
  }
}
/*
Game constructor
BoardGame constructor
Chess constructor
*/
class Game {
  Game(int i) {
    System.out.println("Game constructor");
  }
}
class BoardGame extends Game {
  BoardGame(int i) {
    super(i);
    System.out.println("BoardGame constructor");
  }
}
public class Chess extends BoardGame {
  Chess() {
    super(11);
    System.out.println("Chess constructor");
  }
  public static void main(String[] args) {
    Chess x = new Chess();
  }
}
/*
Game constructor
BoardGame constructor
Chess constructor
*/

委托

  • Java 不直接支持的第三种重用关系称为委托。这介于继承和组合之间,因为你将一个成员对象放在正在构建的类中 (比如组合),但同时又在新类中公开来自成员对象的所有方法 (比如继承)。
  • 方法被转发到底层,但是,你对委托有更多的控制,因为你可以选择只在成员对象中提供方法的子集。
java
public class SpaceShipControls {
  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 SpaceShipControls {
  private String name;
  public DerivedSpaceShip(String name) {
    this.name = name;
  }
  @Override
  public String toString() { return name; }  
}

public class SpaceShipDelegation {
  private String name;
  private SpaceShipControls controls = new SpaceShipControls();
  public SpaceShipDelegation(String name) {
    this.name = name;
  }
  public void back(int velocity) {
    controls.back(velocity);
  }
  public void down(int velocity) {
    controls.down(velocity);
  }
  public void forward(int velocity) {
    controls.forward(velocity);
  }
  public void left(int velocity) {
    controls.left(velocity);
  }
  public void right(int velocity) {
    controls.right(velocity);
  }
  public void turboBoost() {
    controls.turboBoost();
  }
  public void up(int velocity) {
    controls.up(velocity);
  } 
  
  public static void main(String[] args) {
    SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
    protector.forward(100);
  }
}
public class SpaceShipControls {
  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 SpaceShipControls {
  private String name;
  public DerivedSpaceShip(String name) {
    this.name = name;
  }
  @Override
  public String toString() { return name; }  
}

public class SpaceShipDelegation {
  private String name;
  private SpaceShipControls controls = new SpaceShipControls();
  public SpaceShipDelegation(String name) {
    this.name = name;
  }
  public void back(int velocity) {
    controls.back(velocity);
  }
  public void down(int velocity) {
    controls.down(velocity);
  }
  public void forward(int velocity) {
    controls.forward(velocity);
  }
  public void left(int velocity) {
    controls.left(velocity);
  }
  public void right(int velocity) {
    controls.right(velocity);
  }
  public void turboBoost() {
    controls.turboBoost();
  }
  public void up(int velocity) {
    controls.up(velocity);
  } 
  
  public static void main(String[] args) {
    SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
    protector.forward(100);
  }
}

final 关键字

许多编程语言都有某种方法告诉编译器有一块数据是恒定不变的。恒定是有用的,如:

  1. 一个永不改变的编译时常量。
  2. 一个在运行时初始化就不会改变的值。

对于编译时常量这种情况,编译器可以把常量带入计算中;也就是说,可以在编译时计算,减少了一些运行时的负担。在 Java 中,这类常量必须是基本类型,而且用关键字 final 修饰。你必须在定义常量的时候进行赋值。

一个被 static 和 final 同时修饰的属性只会占用一段不能改变的存储空间。

当用 final 修饰对象引用而非基本类型时,其含义会有一点令人困惑。对于基本类型,final 使 数值 恒定不变,而对于对象引用,final 使 引用 恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。但是,对象本身是可以修改的,Java 没有提供使任何对象恒定不变的方法。(你可以自己编写类达到使对象恒定不变的效果)这一限制同样适用数组,数组也是对象。

空白 final

空白 final 指的是没有初始化值的 final 属性。编译器确保空白 final 在使用前必须被初始化。这样既能使一个类的每个对象的 final 属性值不同,也能保持它的不变性。

java
class Poppet {
    private int i;
    Poppet(int ii) {
        i = ii;
    }
}
public class BlankFinal {
    private final int i = 0; 
    private final int j; 
    private final Poppet p; 
    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(47);
    }
}
class Poppet {
    private int i;
    Poppet(int ii) {
        i = ii;
    }
}
public class BlankFinal {
    private final int i = 0; 
    private final int j; 
    private final Poppet p; 
    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(47);
    }
}

你必须在定义时或在每个构造器中执行 final 变量的赋值操作。这保证了 final 属性在使用前已经被初始化过。

final 参数

在参数列表中,将参数声明为 final 意味着在方法中不能改变参数指向的对象或基本变量:

java
class Gizmo {
    public void spin() {
    }
}
public class FinalArguments {
    void with(final Gizmo g) {
    }
    void without(Gizmo g) {
        g = new Gizmo(); 
        g.spin();
    }
    int g(final int i) {
        return i + 1;
    }
    public static void main(String[] args) {
        FinalArguments bf = new FinalArguments();
        bf.without(null);
        bf.with(null);
    }
}
class Gizmo {
    public void spin() {
    }
}
public class FinalArguments {
    void with(final Gizmo g) {
    }
    void without(Gizmo g) {
        g = new Gizmo(); 
        g.spin();
    }
    int g(final int i) {
        return i + 1;
    }
    public static void main(String[] args) {
        FinalArguments bf = new FinalArguments();
        bf.without(null);
        bf.with(null);
    }
}

方法 f() 和 g() 展示了 final 基本类型参数的使用情况。你只能读取而不能修改参数。这个特性主要用于传递数据给匿名内部类。这将在” 内部类 “章节中详解。

java
/**
 * final修饰的基本数据类型的值是不能够改变的
 * @param i
 */
public static void setValue(final int i) {
	//编译通不过,基本数据类型不能够改变
	i = 10;
}

/**
 * 对应final修饰的基本数据类型方法内部是不可以变得,但是引用数据类型是引用不可以变,但是值可以变
 * @param user
 */
public static void setUser(final User user) {
	//引用的数据类型的值是可以改变的,但是指向的引用是不能够变的
	user.setPassword("sdf");
	//引用的数据类型引用是不可以变得,否则编译是不能够通过的
	user = new User();
}
> 原文链接:https://blog.csdn.net/yaomingyang/article/details/79253161
/**
 * final修饰的基本数据类型的值是不能够改变的
 * @param i
 */
public static void setValue(final int i) {
	//编译通不过,基本数据类型不能够改变
	i = 10;
}

/**
 * 对应final修饰的基本数据类型方法内部是不可以变得,但是引用数据类型是引用不可以变,但是值可以变
 * @param user
 */
public static void setUser(final User user) {
	//引用的数据类型的值是可以改变的,但是指向的引用是不能够变的
	user.setPassword("sdf");
	//引用的数据类型引用是不可以变得,否则编译是不能够通过的
	user = new User();
}
> 原文链接:https://blog.csdn.net/yaomingyang/article/details/79253161

final 方法

使用 final 方法的原因有两个

第一个原因是给方法上锁,防止子类通过覆写改变方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变

(略)
过去建议使用 final 方法的第二个原因是效率。在早期的 Java 实现中,如果将一个方法指明为 final,就是同意编译器把对该方法的调用转化为内嵌调用。当编译器遇到 final 方法的调用时,就会很小心地跳过普通的插入代码,以执行方法的调用机制(将参数压栈,跳至方法代码处执行,然后跳回并清理栈中的参数,最终处理返回值),而用方法体内实际代码的副本替代方法调用。这消除了方法调用的开销。但是如果一个方法很大代码膨胀,你也许就看不到内嵌带来的性能提升,因为内嵌调用带来的性能提高被花费在方法里的时间抵消了。

在最近的 Java 版本中,虚拟机可以探测到这些情况(尤其是 hotspot 技术),并优化去掉这些效率反而降低的内嵌调用方法。有很长一段时间,使用 final 来提高效率都被阻止。

你应该让编译器和 JVM 处理效率问题,只有在防止方法覆写时才使用 final

final 类

当说一个类是 final (final 关键字在类定义之前),就意味着它不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类。