看标题相信你已经懂了,这部分讲的就是帮助你分析选择继承还是组合。OK,看下java编程思想的说法:
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。
看到这,暂且不考虑接口的知识,只考虑组合这种方式。一般来说,我们在组合的时候会把成员对象声明为private,只对外提供公共可访问的接口,而隐藏具体的实现,防止不必要的修改。但是也有一些特例,这个跟你的业务逻辑或者负责的模块有关,下面上代码,看看特例的情况:
/**
* 一个车各个部件组合的例子
* @author dell
*/
//引擎类
class Engine{
public void start(){}
public void rev(){}
public void stop(){}
}
//轮胎
class Wheel{
public void inflate(int psi){}
}
//窗户
class Window{
public void rollup(){}
public void rolldown(){}
}
//车门
class Door{
public Window window=new Window();
public void open(){}
public void close(){}
}
//车
public class Car {
public Engine engine=new Engine();
public Wheel[] wheel=new Wheel[4];
public Door left=new Door(),right=new Door();
public Car(){
for (int i = 0; i < wheel.length; i++) {
wheel[i]=new Wheel();
}
}
public static void main(String[] args) {
Car car=new Car();
car.left.window.rollup();
car.wheel[0].inflate(80);
}
}
这种方式算是一个特例了,暴露类中的成员,对外来说,更方便且直观的知道这些成员的作用。但是内部对象的一般要隐藏具体实现。
protected关键字
在实际项目中,经常会有想要将某些事物尽可能对这个世界隐藏起来,但是仍然允许子类的成员访问它们。比如:
//Android中我们要处理的Activity的生命周期
@Override
protected void onResume(){
super.onResume();
}
@Override
protected void onRestart(){
super.onRestart();
}
再比如书上这个例子,在子类内部可以调用父类的方法:
class Villain{
private String name;
protected void set(String name){this.name=name;}
public Villain(String name){this.name=name;}
public String toString(){
return "I am 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);
this.orcNumber=orcNumber;
}
@Override
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("Mi", 17);
System.out.println(orc);
}
}
虽然组合中有暴露成员对象的特例,继承中需要使用protected关键字来让子类做一些访问,但作者建议在实际项目中,尽可能的将域保持为private,来防止底层实现的外部改动||防止某个类对底层类库的强制依赖。
向上转型
“新类是现有类的一种类型”
这句话描述实际上就是”is a”关系,因为在面向对象编程中,好的程序应该符合“里氏替换原则”。上代码,再解释:
/**
*乐器
*/
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();
//向上转型 flute is a Instrument
Instrument.tune(flute);
}
}
运行结果是没有错的,编译也没有错。原因是在java中,如果符合is a关系,则会自动发生向上转型。
由于向上转型是从一个较专用类型向较通用类型转换,所以是安全的,相反向下转型是不安全的。
组合与继承的选择
一个最清晰的办法就是:
final关键字
java的关键字final指“这是无法改变的”。
一般使用final出于两种原因:
1.设计
2.效率
final可用来修饰数据、方法、类.
final数据
final修饰数据,主要是让数据达到恒定不变的效果,比如:
1.一个永不改变的的编译器常量。
2.一个在运行时被初始化的指,而你不希望它被改变。
在java中,这类不能修改的常量必须是基本数据类型,并且以final表示,在对这个常量进行定义时,必须对其进行赋值。当对象引用而不是基本类型运用final时,其含义会有一点令人迷惑。对于基本类型,final使数值恒定不变;而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而对象本身确是可以被修改的,java并未提供使任何对象恒定不变的途径(但可以自己编写类以取得使对象恒定不变的效果)。这一限制同样适用数组,它也是对象。
空白final
java允许生成“空白final”,所谓空白final是指被声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化。
因为,可以通过空白final做到根据对象的不同其final域也有所不同,却又保持其恒定不变的特性。
class Poppet{
private int i;
Poppet(int i){this.i=i;}
@Override
public String toString() {
return "Poppet i:"+i;
}
}
public class BlankFinal {
private final int i=0;//初始化final
private final int j;//空白final
private final Poppet p;
public BlankFinal(){
j=1;
p=new Poppet(1);
}
public BlankFinal(int x){
j=x;
p=new Poppet(x);
}
@Override
public String toString() {
return "相同i:"+i+" 不同域:"+p;
}
public static void main(String[] args) {
System.out.println(new BlankFinal());
System.out.println(new BlankFinal(47));
}
}
/*
output:
相同i:0 不同域:Poppet i:1
相同i:0 不同域:Poppet i:47
*/
上面这个例子已经很明晰的演示了空白final的作用,最后,特别强调:必须在域的定义处或者每个构造器中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因所在。
- final参数
java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象:
class Gizmo{
private int i=0;
public void spin(){}
public void setI(int x){
i=x;
}
}
public class FinalArguments {
/**
* final对象
*/
void with(final Gizmo g){
//可以改变对象本身
g.setI(2);
//不可能改变对象的引用
//!g=new Gizmo();
}
void without(Gizmo g){
g=new Gizmo();
g.spin();
}
/**
*final基本数据类型
*/
int g(final int i){
return i+1;
}
//void f(final int i){i++} //不能改变
public static void main(String[] args) {
FinalArguments bf=new FinalArguments();
bf.without(null);
bf.with(null);
}
}
方法f()和g()展示了当基本类型的参数被指定为final时所出现的结果,此时,参数是只读的,不能修改。
- final方法
在java SE5以后,使用final方法的只有一个 :
想把方法锁定,以防止任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被被覆盖。
final和private关键字
类中所有用private修饰的方法隐式的都是final的。因为子类无法访问,自然也就无法重写了。
被fianl修饰的方法(非private)子类可以调用,但是不能重写。如果试图去重写,编译器不会报错,因为这只是你在子类中创建了一个和父类中重名的普通方法,如果你使用@Override注解会发现,不能这样做:
class A{
private final void f(){
System.out.println("A");
}
}
public class FinalMethod extends A{
//下面的不可以通过编译
/*
@Override
protected void f() {
}*/
//下面的可以
public final void f(){
//这是一个同名方法
}
}
- final类
当你不希望一个类被继承时,可以使用final定义此类,在final类中,所有的方法都隐式的指定为final的。
final class A{
public int i=3;
public final int j=4;
final void f(){
System.out.println("A");
}
}
//! FinalMethod extends A
public class FinalMethod {
public static void main(String[] args) {
A a=new A();
a.i++;
//! a.j++;
System.out.println(a.i);
}
}
总结
继承和组合都能从现有类型中生成新类型。组合一般是将现有类型作为新类型底层实现的一部分加以复用,而继承复用的是接口。
声明:文章中引用部分均为《java编程思想》一书的内容,仅作为个人学习使用,再次感谢前辈的宝贵结晶,让我不断进步!