代码复用是面向对象编程(OOP)最具有魅力的原因之一
组合
在新类中创建现有类的对象。这种方式叫做“组合(Composition)”;通过这种方式复用代码的功能,而非其形式。
例
public class SprinklerSystem {
class WateSource {
private String s;
WateSource(){
System.out.println(" WateSource()");
s = "Constructed";
}
@Override
public String toString() {
return s;
}
}
private String value1,value2,value3,value4;
private WateSource source = new WateSource();
private int i;
private float f;
/**
* 标注为 @Override 来告诉编译器以确保正确的覆盖。
* @return
*/
@Override
public String toString() {
return "SprinklerSystem{" +
"value1='" + value1 + ''' +
", value2='" + value2 + ''' +
", value3='" + value3 + ''' +
", value4='" + value4 + ''' +
", source=" + source +
", i=" + i +
", f=" + f +
'}';
}
public static void main(String[] args) {
SprinklerSystem sprinklerSystem = new SprinklerSystem();
System.out.println(sprinklerSystem);
}
}
继承
创建现有类类型的新类。采用现有类形式又无需在编码时改动其他代码,这种方式就叫做“继承(Inheritance)”,编译器会做大部分的工作。继承是面向对象编程(OOP)的最重要基础之一。多态(Polymorphism)也是其中之一。
例
//基类
public class Art {
Art(){
System.out.println(" Art constructor");
}
}
Drawing类集成 Art 成为其派生类:
public class Drawing extends Art {
Drawing(){
System.out.println(" Drawing constructor");
}
}
Cartoon类集成Drawing类成为其Drawing类的派生类
public class Cartoon extends Drawing{
//若删除此构造函数,也会从基类向外进行构造,因为会生成一个无参的构造函数
public Cartoon(){
System.out.println(" Cartoon constructor");
}
public static void main(String[] args) {
Cartoon cartoon = new Cartoon();
}
}
初始化结果:
Art constructor//顶层基类
Drawing constructor//第二层基类
Cartoon constructor//最外层基类
初始化顺序是从最顶层开始。
组合与继承的选择
组合与集成都允许在新类中放置子对象(组合是显式的,继承是隐式的)。
当我们在想在新类中包含一个已有类的功能时,使用组合,而非继承。也就是说在新类中嵌入一个对象(通常是私有的),我们常用的注入功能( @Autowired),在 Controller 中注入 Service,这就是组合。
当使用继承时,使用一个现有类并开发出它的新版本。这通常意味着使用一个通用类,并为了某个特殊需求将其特殊化。
protected
关键字 protected 的作用就是将自己属性尽量对外界隐藏起来,而只允许自己的派生类访问。
例
public class Orc extends Villain{
Orc(String name) {
super(name);
}
private int orcNumber;
public Orc(String name,int orcNumber){
super(name);
this.orcNumber = orcNumber;
}
//change 方法可以访问 set方法,因为 set方法是 protected。extends的存在可以使得派生类访问基类的 protected修饰的方法
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("Limburger",12);
System.out.println(orc);
orc.change("Bob",18);
System.out.println(orc);
}
}
在讨论组合和继承
在面向对象编程中,创建和使用代码最有可能得方法是将数据和方法一起打包到类中,然后使用该类的对象。也可以使用已有的类通过组合来创建新类。继承我们并不常用,不仅仅是因为 Java 只能单继承,更多的是因为自己的类是否需要向上转型为基类。如果没有必要那就不必非得使用继承。
final关键字
final 数据
在 Java 中,我们称之为final 修饰的变量为常量。
一个被 static 和 final 同时修饰的属性只会占用一段不能改变的存储空间。
当用final修饰对象引用而非基本类型时,其含义会有一点令人困惑。对于基本类型,final使数值恒定不变,而对于对象引用,final使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。但是,对象本身是可以修改的,Java 没有提供将任意对象设为常量的方法。
final 参数
在参数列表中,将参数声明为 final 意味着在方法中不能改变参数指向的对象或者基本变量
final参数
使用final方法的原因有两个。
第一个原因是给方法上锁,防止子类通过覆写改变方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变。
过去建议使用final方法的第二个原因是效率。在早期的 Java 实现中,如果将一个方法指明为final,就是同意编译器把对该方法的调用转化为内嵌调用。当编译器遇到final方法的调用时,就会很小心地跳过普通的插入代码以执行方法的调用机制(将参数压栈,跳至方法代码处执行,然后跳回并清理栈中的参数,最终处理返回值),而用方法体内实际代码的副本替代方法调用。这消除了方法调用的开销。但是如果一个方法很大代码膨胀,你也许就看不到内嵌带来的性能提升,因为内嵌调用带来的性能提高被花费在方法里的时间抵消了。在最近的 Java 版本中,虚拟机可以探测到这些情况(尤其是hotspot技术),并优化去掉这些效率反而降低的内嵌调用方法。有很长一段时间,使用final来提高效率都被阻止。你应该让编译器和 JVM 处理性能问题,只有在为了明确禁止覆写方法时才使用final。
final 和 private
类中所有的 private 方法都隐式的指定为 final。因为 private 方法,所有不能覆写它。
"覆写"只发生在方法是基类的接口时。也就是说,必须能将一个对象向上转型为基类并调用相同的方法(这一点在下一章阐明)。如果一个方法是private的,它就不是基类接口的一部分。它只是隐藏在类内部的代码,且恰好有相同的命名而已。但是如果你在派生类中以相同的命名创建了public,protected或包访问权限的方法,这些方法与基类中的方法没有联系,你没有覆写方法,只是在创建新的方法而已。由于private方法无法触及且能有效隐藏,除了把它看作类中的一部分,其他任何事物都不需要考虑到它。
final 类
当说一个类是final(final关键字在类定义之前),就意味着它不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类。
类的初始化和加载
构造器也是一个static方法尽管它的static关键字是隐式的。因此,准确地说,一个类当它任意一个static成员被访问时,就会被加载。
首次使用时就是static初始化发生时。所有的static对象和static代码块在加载时按照文本的顺序(在类中定义的顺序)依次初始化。static变量只被初始化一次。
继承和初始化
public class Beetle extends Insect {
private int k = printInit("Beetle.k.initialized");
public Beetle(){
System.out.println("k = " + k);
System.out.println("j = " + j);
}
public static int x2 = printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle beetle = new Beetle();
}
}
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){
System.out.println(s);
return 47;
}
}
输出结果
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9,j = 0
Beetle.k.initialized
k = 47
j = 39
当执行java Beetle,首先会试图访问Beetle类的 main() 方法(一个静态方法),加载器启动并找出Beetle类的编译代码(在名为Beetle.class的文件中)。在加载过程中,编译器注意到有一个基类,于是继续加载基类。不论是否创建了基类的对象,基类都会被加载。(可以尝试把创建基类对象的代码注释掉证明这点。)如果基类还存在自身的基类,那么第二个基类也将被加载,以此类推。接下来,根基类(例子中根基类是Insect)的static的初始化开始执行,接着是派生类,以此类推。这点很重要,因为派生类中static的初始化可能依赖基类成员是否被正确地初始化。至此,必要的类都加载完毕,可以创建对象了。首先,对象中的所有基本类型变量都被置为默认值,对象引用被设为null—— 这是通过将对象内存设为二进制零值一举生成的。接着会调用基类的构造器。本例中是自动调用的,但是你也可以使用super调用指定的基类构造器(在Beetle构造器中的第一步操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。当基类构造器完成后,实例变量按文本顺序初始化。最终,构造器的剩余部分被执行。
总结
继承和组合都是从已有类型创建新类型。组合将已有类型作为新类型底层实现的一部分,继承复用的是接口。