简介
复用代码是Java众多引人注目的功能之一,通过复用代码,我们可以极大地降低我们的工作量,以及提升我们代码的简洁程度。这种方法的窍门在于使用类而非破坏现有程序。复用代码的方式有两者:第一种叫做组合,该方法比较直观,只是单纯调用了现有的程序代码。第二种则细致一些,也是我们经常使用到的,叫做继承。该方法按照现有类的类型创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。
组合语法
组合只需要将对象引用置于新类中即可。也就是说,如果我们创建的类中需要使用到某个其他的类型,我们可以在我们的新类中声明一个我们所需的类型的一个对象引用,通过传入的方式为其赋予实例引用
假设我们现在要买一套房子,但是当房子买到手以后我们一定需要对其进行装修,在房子里放置各种像椅子沙发这样的软装,但是这些软装很明显不是我们自己的造的,而是由不同的厂家制造,我们根据我们喜欢的样式选择我们需要的家具
所以我们现在先创建沙发(我们假设是现有的)
class Sofa{
public Sofa(){
//沙发
}
}
房子类
class BigHouse{
Sofa sofa;
public void setSofa(Sofa sofa){
this.sofa = sofa;
}
}
场景类
public static void main(String[] args) {
//我造了/买了一套房
BigHouse house = new BigHouse();
//厂家造了一张我要的沙发
Sofa sofa = new Sofa();
//我问厂家买了沙发放到了新家
house.setSofa(sofa);
}
上面关于沙发的房子的场景中,我们简单的表现了一下所谓的组合。这种组合很方便,但是在使用组合是必须注意我们声明的沙发引用在类创建时会被初始化清理(基本类型清零,引用类型变null),所以我们在使用时一定要为我们的引用字段做好初始化。如果想要初始化这些引用,可以在以下几个位置进行:
- 定义对象的地方。这意味着对象会在构造方法执行前完成初始化
- 在类的构造器中
- 在使用这些对象之前,这种方式我们称为惰性初始化。适用于一些我们创建了对象,但是对象中某些字段我们不一定使用得到,这种方式可以一定程度上降低程序的负担
- 使用实例初始化(也就是我们上面提到的案例中的方式)
继承语法
继承是所有OOP语言和Java语言不可缺少的部分,当创建一个对象的时候,总是在继承,除非你显式地指出要从其他类中继承,否则都会隐式地继承Object类。通过继承,会自动得到基类中所有非私有和final修饰地域和方法。(继承通过extends关键字实现)
下面程序中我们可以看出FatherClass中定义地所有字段和方法都在SonClass中被使用。也就是说,在SonClass中继承了FatherClass中的一切,我们可以通过这种方式去复用一个类。同时在printFatherName()中所见,使用基类的方法以及对它进行修改是可行的,但是注意,如果我们重写了父类的方法,但我们还想调用父类的同名方法时,需要使用super关键字来表示父类,否则就会无限递归调用自己的方法
class FatherClass{
public String name = "张三";
public Integer age = 33;
public void printFatherName(){
System.out.println(name);
}
}
class SonClass extends FatherClass{
@Override
public void printFatherName(){
super.printFatherName();
System.out.println("this is fatherName");
}
public void printAge(){
System.out.println(age);
}
}
初始化基类
由于涉及基类和导出来两个类,而不是只有一个类,所以我们需要考虑导出来为什么可以拥有基类的域和方法。从外部来看,它就像是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。但继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象回包含一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。我们可以这么认为,导出类包含的基类就相当于在导出类中组合了一个基类对象,只不过这个基类对象在会在导出来初始化前完成实例化,并且可以不通过引用名直接进行调用或者通过super关键字进行调用。
当然,对基类子对象的正确初始化也十分重要,只有一种方法能完成对基类的初始化:调用父类构造器来完成初始化。如果我们我显式调用父类构造器,编译器会在编译时在构造器首行插入对基类无参构造器的调用
通过下面例子我们可以看出构建过程是从基类向外扩散的,所以基类在导出类构造器可以访问它之前就已经完成了初始化
public class InheritClass2 {
public static void main(String[] args) {
new C();
}
}
class A{
public A(){
System.out.println("A~~~~~~~~");
}
}
class B extends A{
public B(){
System.out.println("B~~~~~~~~");
}
}
class C extends B{
public C(){
System.out.println("C~~~~~~~~");
}
}
下面是ABC三个类的包含关系,当我们创建C的时候,会先去创建B,而创建B的时候,又会先去创建A。
在组合与继承之间的选择
组合和继承都允许在新的类中放置子对象,前者是显式的,后者则是隐式的。
组合技术通常用于想要在新类中使用现有类的功能而非它的接口这种情形。也就说,我们的新类相当于一个适配器的角色,封装了我们所嵌套的子对象的细节,外界只能看到新类定义的接口,但使用的则是被组合的类的业务逻辑。
而继承通常用于一种is-a的结构,也就是说新类是对旧类的一种增强实现,本身依旧是旧类。举个例子,有个作者写了一本书,但是这本书现在只有内容和简单的封皮,但是这并不妨碍这是一本书的事实,现在作者想要发行这本书,因此找到了出版社,出本书觉得这样的外观无法使得这本出畅销,因此对这本事实现了增强(换了一个更漂亮的封皮)。但是这本书不管如何增强,它终究是这本书。
protected
介绍完继承后,自然也要提到protected关键字,该关键字是为继承而生的。通常被该关键字修饰的域和方法只允许被同一个包内的类或者子类访问到。其实我们protected关键字使用的还是没有public和private更加广泛。但是我们依然还是要明白它的作用
下面对前面提到的访问范围进行验证
同个类文件
package org.example.inherit2;
public class InheritClass3 {
protected String name = "张三";
}
class A extends InheritClass3{
public void f(){
System.out.println(name);
}
}
同个包以及同个包下的继承
package org.example.inherit2;
public class InheritClass3 {
protected String name = "张三";
}
class A extends InheritClass3{
public void f(){
System.out.println(name);
}
}
不同包下的使用和继承
package org.example.inherit;
import org.example.inherit2.InheritClass3;
public class InheritClass2 {
public static void main(String[] args) {
InheritClass3 inheritClass3 = new InheritClass3();
System.out.println(inheritClass3.name);//编译失败
}
}
class A extends InheritClass3{
public A(){
System.out.println(name);
}
}
向上转型
为新类提供额外的方法并不是继承技术最重要的方面,其最重要的方面是用来表现新类和基类间的关系,这种关系可以用“新类是现有类的一种类型”这一句话概括。
举个例子,假设有个乐器类的基类和一个叫做钢琴的导出类。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的所有信息同样也可以向导出类发送,如果乐器基类有一个演奏的方法,那么钢琴类同样具备。这意味着沃恩可以准确地说钢琴对象也是一种乐器
package org.example.inherit;
public class InheritClass3 {
public static void playIntrument(Intrument intrument){
intrument.play();
}
public static void main(String[] args) {
playIntrument(new Piano());
}
}
class Intrument{
public void play(){
System.out.println("音乐演奏");
}
}
class Piano extends Intrument{
@Override
public void play(){
System.out.println("钢琴演奏");
}
}
在上面的例子中,主类中的playIntrument方法接收一个Intrument引用,但是在main中我们调用该方法时赋了一个Piano的类型实例,但是我们知道,Intrument和Piano并不是同一种类型,它们只是继承关系。其实在这种调用者,Pinao被向上转型成了一个Intrument。playIntrument只知道它接收的是一个Intrument类型,所以它也只能使用Intrument类型对应的方法
fianl关键字
根据上下文环境,Java的关键字final的含义存在细微区别,但它通常指的是“这是无法改变的”。不想做出改变可能出于两种理由:设计或效率。
下面将讨论可能使用到final的三种情况:数据、方法和类
final数据
许多语言都有某种方式来向编译器告知一块数据是恒定不变的。有时数据的恒定不变是很有用的,比如:
- 一个永不改变的编译时常量
- 一个在运行是初始化的值,但我们不希望它发生变化
对于编译时常量,编译器可以将该常量带入任何可能用到它的计算式中,也就是说可以在编译时执行计算式,这减轻了一些运行时的负担。在Java中,这些常量必须是基本数据类型,并以final表示,在对这个常量进行定义时,必须对其进行赋值
一个既是static又是final的域只占据一段不能改变的存储空间
当我们使用fianl修饰引用对象时,只能使引用恒定不变,但是引用指向的实例依旧可以发生变化
空白final
Java允许生产空白fianl,,所谓的空白final是指被声明为final但又未给定初值的域。但是编译器一定会保证空白final在使用前必须被初始化(必须在域的定义处或者每个构造器中对final数据进行赋值)
class B{
final String name1 = "name1";
final String name2;
public B(){
name2 = "name2";
}
}
final参数
Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法内部更改参数引用所指向的对象
class B{
public void f(final String name){
name = "hello";//编译失败
System.out.println(name);
}
}
final方法
使用final方法的原因有两个。第一个是吧方法锁定,以防任何继承类修改其含义。第二个原因是效率,在java早期,如果将方法声明为fianl,该方法的调用会转换为内嵌调用
类中所以private方法都隐式地指定为final
final类
当将某个类的整体定义为final时,表明你不打算继承该类,而且也不允许他人这样做。
在java中String就是一个典型的fianl类