Thinking in Java第七章学习笔记----复用类

复用代码,即使用已经开发并调试好的类。组合和继承是两种实现方法。

组合语法:

  在新类中创建现有类的对象。该方法只是复用了现有代码的功能,而非它的形式。

  组合的例子随处可见,这里不举例说明。但书中特意强调了toString方法。

  每一个非基本类型的对象都有一个toString方法,因为每一个类都是继承Object类而来的,而Object类中包含这个方法。具体需要注意的是,当你要将一个对象和字符串连接的时候,编译器会自动调用toString方法,当Object类中的toString方法不能满足要求时,则需要重写这个方法。

继承语法:

  继承是所有OOP语言不可缺少的组成部分。当创建一个类时,总是在继承,因为若没有明确指出要从其他类中继承,就默认从Java的标准根类Object进行继承。

  为了继承,一般的规则是将所有的数据成员指定为private,所有的方法指定为public。虽然在特殊的情况下必须做出调整,但是这的确是一个很有用的规则。

  当继承类中有对基类中定义的方法修改时,欲调用基类的方法,必须加上super关键字,否则程序将产生递归。当然,继承类同样可以定义属于自己的方法。

 初始化基类:

  当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与直接创建的基类对象时一样的。二者的却别在于后者来自于外部,而前者来自于导出类对象的内部。

  1)无参构造器的初始化

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");
	}
}
/*
Output:
	Art
	Drawing
	Cartoon
*/

   2)有参构造器的初始化

    对于无参构造器,当然默认的构造器也是无参的,编译器可以轻松地调用而不需要考虑传递参数的问题。但是想调用带参数的基类构造器,就必须用super显示地编写调用基类构造器的语句,而且调用基类构造器必须是你在导出类构造器中要做的第一件事,否则编译器将报错。

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 {
    public Chess() {
        super(11);
        System.out.println("Chess Constructor");
//        super(11); // 报错
    }
    public static void main(String[] args) {
        new Chess();
    }
}
/*
output:
	Game Constructor
	BoardGame Constructor
	Chess Constructor
*/

代理:

  代理是继承与组合之间的中庸之道,但是Java并没有提供对它的直接支持。代理可控制需要哪些被代理类中的方法,而组合和继承则拥有了所有方法。

  代理的具体过程,先创建被代理类的对象引用,然后创建与被代理类中方法同名的方法,并通过这个对象引用来调用被代理类的方法,这里有点像重写(注意重写是建立在继承的基础上的)。

class SpaceShipControls {
	void up(int velocity) {}
	void down(int velocity) {}
} 
public class SpaceShipDelegation {
	private String name;
	private SpaceShipControls controls = new SpaceShipControls();//创建被代理类的对象
	public SpaceShipDelegation(String name){
		this.name = name;
	}
	public void up(int velocity){//选择需要代理的方法,注意名字需一样
		controls.up(velocity);//通过对象引用,调用被代理类的方法。实现代理
	}
	public static void main (String[] args) {
		SpaceShipDelegation protector = new SpaceShipDelegation("lalala");
		protector.up(100);
	}
}

确保正确清理:

  Java中没有析构函数的概念。虽然Java中有垃圾回收机制,但是你永远不知道它什么时候才会被调用。因此,如果想要某个类清理一些东西,就必须显示地编写一个特殊方法来实现。清理的首要任务是,将这一清理动作置于finally子句之中,以防异常的出现。finally子句表示无论发生什么事,一定要执行这个动作。清理动作的顺序和生成顺序相反,因为可能存在子对象依赖于另一个子对象的情况。

名称屏蔽:

  如果导出类中有对基类的方法进行重载,不会对名称进行屏蔽,即所有重载方法都是可用的。@override注解重写基类方法,避免方法名称写错。

向上转型:

  导出类转型为基类,在继承图上是向上的,因此得名。由于向上转型是从一个较专用类型向较通用类型转换,所以总是安全的,而且在向上转型的过程中,类接口中唯一发生的事情是丢失方法,而不是获取他们,所以在安全的考虑上是可以接受的,编译器也是允许的。

组合与继承之间选择:

  尽管面向对象的过程中,一直强调继承的概念,但并不是尽可能的使用它。相反,应当慎用这一技术。一个清晰的方法是问问自己到底是否需要从新类向基类向上转型。如果需要,那就用继承吧,如果不需要,那应当好好考虑下了。所以向上转型是判断组合和继承选择的重要依据。

final数据:

  对于基本类型,final使数值恒定不变,而用于对象引用,final引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它指向另一个对象。但是对象本身是可以修改的。既是static又是final的域将用大写表示,并使用下划线分隔各个单词。

  有一点需要注意的是,空白final是指指定了final但又没有赋初值的域。无论什么情况,编译器都要确保空白final在被使用前都必须要被初始化。一般情况下,空白final是在其他类的构造器中初始化,即某个类的final域可以根据创建不同的对象具有不同的初始值,而且还能保持其恒定不变的特性。空白final大大提高了灵活性。

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) {
		/*通过调用不同的构造器,创建不同的对象,使空白final域P有不同的初始值*/
		new BlankFinal();
		new BlankFinal(1);
	}
}

final参数:

  Java允许在参数列表中以声明的方式将参数指明为final,这意味着你不能在方法中更改参数引用所指向的对象。  

class Gizmo {
	public void spin() {
	}
}

public class FinalArguments {
	void with(final Gizmo g) {
		//g = new Gizmo();  不能更改
	}
	void without(Gizmo g) {
		g = new Gizmo();
		g.spin();
	}
	//void f(final int i){i++} 不能更改i的值
	int f(final int i){ return i + 1; } //这里并没有更改i的值
	public static void main (String[] args){
		FinalArguments bf = new FinalArguments();
		bf.with(null);
		bf.without(null);
	}
} 

final方法:

  使用final方法的原因有两个,第一个原因是效率,这是使用初衷。但是这仅仅是在代码块不是很大的情况下才能显示出其作用,后来Java找到了其他的方式进行提高效率,所以在final方法的使用上不再考虑效率问题。第二个原因是为了防止在继承中对方法进行覆盖。(注意:final方法是可以被重载的!重载和覆盖不一样!)此外,定义为private访问权限的方法,其隐式指定为final。

  特别注意,覆盖只有在某方法是基类接口中的一部分时才会出现,基类中的private方法不是接口的一部分,所以如果在基类的导出类中定义一个和基类中private方法同名的public或者protect方法,不是覆盖,而是定义了一个新的方法,切记!

final类:

  当定义某个类为final时,就表示你不想继承这个类,而且也决不允许别人继承这个类,或者说这个类完全没有被继承的必要,又或者出于安全的考虑。总之,它被限制了。由于final类无法被继承,所以类下的所有方法都是final的,无论是否指定为final。

对于使用final的忠告:

  将一个方法或者一个类指定为final,大部分可能是明智的。但是你必须注意到,这些所谓的不能重载、不能被继承,都是你自己的想象,总有你意想不到的运用它的情况。所以,使用final请慎重!

初始化及类的加载:

  类只有在创建类的第一个对象或者访问static域和static方法时才会发生加载。其实创建类的对象也是在访问static方法,因为创建时调用的构造器是隐式的static。所以,类的加载之处也是static初始化之处,而所有的static只会被初始化一次,按照定义的顺序初始化。

class Insect {  
  private int i = 9;  
  protected int j;  
  Insect() {  
    print("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();  
  }  
}  
/** 
  * static Insect.x1 initialized 
  * static Beetle.x2 initialized 
  * Beetle constructor 
  * i = 9, j = 0 
  * Beetle.k initialized 
  * k = 47 
  * j = 39 
*/  

总结:

  在开始一个设计时,一般优先选择使用组合,或者可能是代理,只有确实必要时才使用继承,因为组合更具灵活性。

  在设计一个系统时,目标应该是创建某些类,其中每个类都有具体的用途,而且既不会太大,也不会太小。太大则复杂难以复用,太小则可能不添加其他功能就无法使用。所以,太大的情况下,就适当的细分。在系统的设计阶段,必须意识到这是一种增量过程,是不断累积的过程,并不是一蹴而就的。

 

原文地址:https://www.cnblogs.com/promiseslc/p/8727301.html