复用代码是Java众多引人注目的功能之一。但想要成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,它还必须能够做更多的事情。
Java通过在创建新的类时,调用标准库或第三方包的类来实现代码的复用。具体又分为三种,分别是组合对象,继承,代理。其中,Java并没有对代理提供直接支持,而是将它作为组合和继承的中庸之道。
1. 组合语法
在创建新类时,我们用的最多的就是组合,组合就是指在新类中放置已有类的对象。已有类的对象可以作为新类的成员变量也可以是新类某个方法中的局部变量。
组合语法的简单示例:
public class SprinklerSystem {
private String valve1, valve2, valve3, valve4;
private WaterSource source = new WaterSource();
private int i;
private float f;
@Override
public String toString() {
return
"valve1 = " + valve1 + " " +
"valve2 = " + valve2 + " " +
"valve3 = " + valve3 + " " +
"valve4 = " + valve4 + "\n" +
"i = " + i + " " + "f = " + f + " " +
"source = " + source;
}
public static void main(String[] args) {
SprinklerSystem sprinklerSystem = new SprinklerSystem();
System.out.println(sprinklerSystem);
}
}
class WaterSource{
private String s;
WaterSource(){
System.out.println("WaterSource()");
s = "constructed";
}
@Override
public String toString() {
return s;
}
}
输出:
WaterSource()
valve1 = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = constructed
注意到上面的两个类中都包含一个特殊的方法toString(),它最先是出现在Object类中,在需要将对象转化为字符串时会调用它。因为Java中所有类的祖先类,默认父类都是Object类,所以上面的两个类也是有默认的toString()方法。但默认的toString()方法是打印出对象的类名称和哈希值,这不利于我们直观的观察某个对象,所以就在这两个类中重写了toString()方法。
自动调用对象的toString()方法常见场景:
- System.out.print(对象),System.out.println(对象)……
- (对象 + 对象)
2. 继承语法
通过extends关键字实现,如A extends B,A继承自B,是B的子类。我们新建的任何一个类,若没有为它指定父类,则编译器会添加extends Object让它继承自Object类即所有类的默认父类是Object类。
A extends B,A中会自动得到B的域和方法。同时,A也可以覆盖B中的方法和域。也就是说当你通过A对象调用某个方法或者访问某个域时,会先从自身的类结构中找,找不到就去父类找,若是还找不到就去父类的父类中找,直到第一次找到为止。既然需要去父类中找,那就意味着父类对象的存在,即在调用A的构造方法时需要通过(super(xxxxx))调用B的构造方法,编译器会在A的构造器中默认调用B的无参构造方法,但若是B中没有无参构造方法,需要在A的构造方法的第一句程序语句添加(super())来调用B的某个构造方法以确保B对象的存在。
父类对象是子类对象的一个特殊成员变量(用super表示)
class Pa {
int i;
public Pa(int i){
this.i = i;
}
public Pa(){
i = 10;
}
void say(){
System.out.println("父类的say方法");
}
}
public class cl1 extends Pa{
public void work(){
System.out.println(super.i);
super.say();
// say()和super.say()执行效果是一样的,因为cl1中没有覆盖Pa的say方法
}
public static void main(String[] args) {
new cl1().work();
}
}
输出:
10
父类的say方法
由于继承的存在,类和对象的初始化步骤又有了些许的变化:
- 父类的静态代码块,静态成员变量
- 子类的静态代码块,静态成员变量
- 父类的成员变量赋默认值,执行实例代码块和实例初始化,构造方法
- 子类执行3一样的步骤
3. 代理
代理:类A中某个模块的功能由类B实现,因此类A中的一个成员变量为类B对象的引用,类A提供与类B完成该模块的功能所对应的相同的接口,但底部是通过B对象实现的。即暴露A的相关接口,底层由B实现**(应用面挺广的)**。
// 飞船控制类,可以控制飞船移动翻转
public class SpaceShipControls {
void up(int velocity){}
void down(int velocity){}
void left(int velocity){}
void right(int velocity){}
void forward(int velocity){}
void back(int velocity){}
void turboBoost(){}
}
// 飞船类,飞船类的一个模块就是移动翻转
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
public void back(int velocity) {
controls.back(velocity);
}
public void down(int velocity) {
controls.down(velocity);
}
public void forward(int velocity) {
controls.forward(velocity);
}
public void left(int velocity) {
controls.left(velocity);
}
public void right(int velocity) {
controls.right(velocity);
}
public void up(int velocity) {
controls.up(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public static void main(String[] args) {
new SpaceShipControls().forward(100);
}
}
可以看到,飞船类的移动翻转的方法签名是与飞船控制类对应的方法签名是相同的,通过显示的暴露飞船类的接口真正的实现由对应的飞船控制类实现。飞船类中包含飞船控制类的对象,通过该对象完成飞船相对应的功能。这种方式在很多类库中似乎都有体现,可以理解消化一下。
4. final
- final常量:final修饰基本数据类型时,该基本数据类型的变量一经赋值就不在运行改变它的值。final修饰对象引用时,该引用一经指向某个对象,就不能在将它引用到其它对象身上,但我们可以修改它所引用的对象的属性。final常量必须在定义处或者是构造器中进行初始化。
- final方法参数,final方法参数用一句话概括就是只可读而不可转向。例如final修饰的方法参数为一个对象引用,则在执行方法的过程中我们不能够将这个对象引用指向其它的实际对象。
- final类是指不允许被继承的类,final方法是指不允许被覆盖的方法,因此访问权限为private的方法默认是final的。