复用

145 阅读8分钟

代码复用是面向对象编程(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构造器中的第一步操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。当基类构造器完成后,实例变量按文本顺序初始化。最终,构造器的剩余部分被执行。

总结

继承和组合都是从已有类型创建新类型。组合将已有类型作为新类型底层实现的一部分,继承复用的是接口。