Java编程思想 第7章 复用类
标签(空格分隔): JAVA学习
7.1 组合语法
组合(Composition)关系也表示类之间整体和部分的关系,但是在组合关系中整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也将不存在,成员对象与整体对象之间具有同生共死的关系。在UML中,组合关系用带实心菱形的直线表示。例如:人的头(Head)与嘴巴(Mouth),嘴巴是头的组成部分之一,而且如果头没了,嘴巴也就没了,因此头和嘴巴是组合关系。
在代码实现组合关系时,通常在整体类的构造方法中直接实例化成员类。
public class Head {
private Mouth mouth;
public Head() {
mouth = new Mouth(); //实例化成员类
}
……
}
public class Mouth {
……
}
7.2 继承语法
继承的关键字是”extends”,可以得到基类中所有的域和方法。
例如:
class Cleanser {
private String s = "Cleanser";
public void append(String a) {
s += a;
}
public void dilute() {
append(" dilute()");
}
public void apply() {
append(" apply()");
}
public void scrub() {
append(" scrub()");
}
public String toString() {
return s;
}
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.dilute();
x.apply();
x.scrub();
print(x);
}
}
public class Detergent extends Cleanser {
// Change a method
public void scrub() {
append(" Detergent.scrub()");
super.scrub(); // Call base-class version
}
// Add methods to the interface
public void foam() {
append(" foam()");
}
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
print(x);
print("Testing base class:");
Cleanser.main(args);
}
}
/* Output:
Cleanser dilute() apply() Detergent.scrub() scrub() foam()
Testing base class:
Cleanser dilute() apply() scrub()
*/
7.2.1 初始化基类
class Art {
Art() {
print("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
print("Drawing constructor");
}
}
public class Cartoon extends Drawing {
Cartoon() {
print("Cartoon construcotr");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
}
/* Output:
Art constructor
Drawing constructor
Cartoon construcotr
*/
- 如果你写的类中没有构造器,编译器会自动帮你创建一个默认构造器。但是,如果已经定义了一个构造器(无论是否有参数),编译器则不会帮你自动创建默认构造器。
- 基类的默认构造器总是会在导出类构造器之前就被调用。
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();
}
}
/* Output:
Game constructor
BoardGame constructor
Chess constructor
*/
如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须用关键字super显示地编写调用基类构造器的语句。
7.3 代理
将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法。
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 SpaceShipDelegation {
private String name;
// 将一个成员对象置于所要构造的类中
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShipDelegation(String name){
this.name = name;
}
// Delegated methods
// 暴露了该成员对象的所有方法
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("NSEA Protector");
protector.forward(100);
}
}
7.4 结合使用组合和继承
7.4.1 确保正确清理
7.4.2 名称屏蔽
如果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());
}
}
/* Output:
doh(float
doh(char)
doh(float
doh(Milhouse)
*/
7.5 在组合与继承之间选择
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。为取得此效果,需要在新类中嵌入一个现有类的private对象。
例如:
public class Head {
// 在新类中嵌入某个对象,让其实现所需要的功能
private Mouth mouth;
public Head() {
mouth = new Mouth(); //实例化成员类
}
……
}
public class Mouth {
……
}
在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。
7.6 protected关键字
protected关键字提供了当前类访问权限,包内访问权限,以及此类的导出类访问权限。
class Villain {
private String name;
protected void set(String nm) {
name = nm;
}
public Villain(String name) {
this.name = name;
}
public String toString() {
return "I'm a Villain and my name is " + name;
}
}
public class Orc extends Villain {
private int orcNumber;
public Orc(String name, int orcNumber) {
super(name);
this.orcNumber = orcNumber;
}
public void change(String name, int orcNumber) {
set(name); // Available because it's protected
this.orcNumber = orcNumber;
}
public String toString() {
return "Orc " + orcNumber + ": " + super.toString();
}
public static void main(String[] args) {
Orc orc = new Orc("Limburger", 12);
System.out.println(orc);
orc.change("Bob", 19);
System.out.println(orc);
}
}
/* Output:
Orc 12: I'm a Villain and my name is Limburger
Orc 19: I'm a Villain and my name is Bob
*/
可以发现,change()是可以访问set()的,这是因为set()是protected的。
7.7 向上转型
class Instrument {
public void play() {
}
static void tune(Instrument i) {
i.play();
}
}
public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // Upcasting
}
}
7.7.1 为什么称为向上转型
由导出类转型成基类,在继承图上是向上移动的,因此一般称为向上转型。
7.7.2 再论组合与继承
继承需要谨慎使用,到底是该用组合还是用继承,一个最清晰的判断办法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的。
7.8 final关键字
7.8.1 final数据
final关键字用来向编译器告知一块数据是恒定不变的,比如:
- 一个永不改变的编译时常量。
- 一个在运行时被初始化的值,而你不希望它被改变。
一个既是static又是final的域只占据一段不能改变的存储空间。
对于基本类型,final使数值恒定不变;而对于对象引用(包括数组),final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身却是可以被修改的。
class Value {
int i; // Package access
public 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;
}
// Can be compile-time constants:
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
// Typical public constant:
public static final int VALUE_THREE = 39;
// Cannot be compile-time constants:
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(22);
private static final Value VAL_3 = new Value(33);
// Arrays:
private final int[] a = {1, 2, 3, 4, 5, 6};
public String toString() {
return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
// ! fd1.valueOne++; // 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.VAL_3 = 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);
}
}
/* Output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*/
- valueOne,VAL_TWO和VAL_THREE都是带有恒定初始值(即,编译期常量)的final基本类型。
- static强调只有一份;final说明它是一个常量。
- 带有恒定初始值的final static基本类型全用大写字母命名,单词之间用下划线隔开。
- final关键字并不能认为在编译时就已经知道它的值,i4和INT_5说明了这一点。
- v2是一个引用,final意味着无法将v2再次指向另一个新的对象(fd1.v2 = new Value(0); // Error: can’t)。然而,对象其自身是可以被修改的(fd1.v2.i++;)。
空白final
Java允许生成“空白final”,所谓空白final是指被声明为final但又未给定初值的域。但是,空白final必须在域的定义处或者每个构造器中用表达式对final进行赋值。
class Poppet {
private int i;
Poppet(int ii) {
i = ii;
}
}
public class BlankFinal {
private final int i = 0; // Initialized final
private final int j; // Blank final
private final Poppet p; // Blank final reference
// Blank finals MUST be initialized in the constructor
public BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet(1); // Initialize blank final reference
}
public BlankFinal(int x) {
j = x; // Initialize blank final
p = new Poppet(x); // Initialize blank final reference
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(47);
}
}
final参数
Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数所指向的对象。
class Gizmo {
public void spin() {
}
}
public class FinalArguments {
void with(final Gizmo g) {
// g = new Gizmo(); // Illegal -- g is final
}
void without(Gizmo g) {
g = new Gizmo(); // OK -- g not final
g.spin();
}
void f(final int i) {
// i++; // Can't change
}
// You can only read from a final primitive
int g(final int i) {
return i + 1;
}
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
}
7.8.2 final方法
使用final方法的主要作用是把方法锁定,以防任何继承类修改它的含义。
class WithFinals{
// Identical to "private" alone
private final void f(){
System.out.println("WithFinals.f()");
}
private void g(){
System.out.println("WithFinals.g()");
}
}
class OverridingPrivate extends WithFinals{
private final void f(){
System.out.println("OverridingPrivate.f()");
}
private void g(){
System.out.println("OverridingPrivate.g()");
}
}
class OverridingPrivate2 extends OverridingPrivate{
public final void f(){
System.out.println("OverridingPrivate2.f()");
}
public void g(){
System.out.println("OverridingPrivate2.g()");
}
}
public class FinalOverridingIllusion {
public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
// You can upcast
OverridingPrivate op = op2;
// But you can't call the methods:
// ! op.f();
// ! op.g();
// Same here:
WithFinals wf = op2;
// ! wf.f();
// ! wf.g();
}
}
7.8.3 final类
当将某个类的整体定义为final时,就表明了该类无法被继承,进而final类中所有的方法都隐式指定是final的。
7.9 初始化及类的加载
初次使用之处也是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 = printInit("static Insect.x1 initialized");
static int printInit(String s) {
print(s);
return 47;
}
}
public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized");
public Beetle() {
print("k = " + k);
print("j = " + j);
}
private static int x2 = printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
print("Beetle constructor");
Beetle b = new Beetle();
}
}
/* Output:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*/
在Beetle上运行Java时
- 首先试图访问Beetle.main()。
- 由于它有一个基类,编译器会先加载基类(不管是否产生该基类的对象)。
Beetle b = new Beetle()
创建对象,从基类到导出类依次调用默认构造器。