7 复用类
复用代码是java众多引人注目的功能之一。关于达到这个目的的方法有两种:一是只需在新的类中产生现有类的对象。由于新的类是现有类的对象所组成,这种方法称之为组合;第二种方法是按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。这种方式称之为继承。
组合和继承类似,都是利用现有类型生成新类型。
7.1 组合语法
假设你需要某个对象,它要具有多个string对象,几个基本类型数据,以及另一个类的对象。对于非基本类型的对象,必须将其引用置于新的类中,但可以直接定义基本类型数据。
例如:
//: reusing/SprinklerSystem.java
// Composition for code reuse.
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = "Constructed";
}
public String toString() { return s; }
}
public class SprinklerSystem {
private String valve1, valve2, valve3, valve4;
private WaterSource source = new WaterSource();
private int i;
private float f;
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 sprinklers = new SprinklerSystem();
System.out.println(sprinklers);
}
} /* Output:
WaterSource()
valve1 = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = Constructed
*///:~
备注:在对复用类进行初始化的时候再对里面引用的对象进行初始化,一开始设置为引用空指着null,减少不必要的负担。
7.2 继承语法
继承是所有面向对象不可缺少的组成部分。当创建一个类,总是在继承。显式继承或者隐式继承object。
继承语法规则:
class Subclass extends Superclass{}
public class Person {
public String name;
public int age;
public Date birthDate;
public String getInfo() {...}
}
public class Student extends Person{
public String school;
}
//Student类继承了父类Person的所有属性和方法,并增加了一个属性school。Person中的属性和方法,Student都可以利用。
关于继承语法,一般的规则是将所有的数据成员都指定为private,将所有的方法都指定为public,特殊情况下可以做出调整。
继承的作用:
继承的出现提高了代码的复用性。
继承的出现让类与类之间产生了关系,提供了多态的前提。
不要仅为了获取其他类中某个功能而去继承
关于继承的其他规则:
子类不能直接访问父类中私有的(private)的成员变量和方法
子类同样可以获取得到父类中声明的私有的属性或方法,只是由于封装的设计,使得子类不能够直接调用。
在子类中,可以使用父类中定义的方法和属性,也可以创建新的数据和方法。
Java只支持单继承,不允许多重继承
一个子类只能有一个父类
一个父类可以派生出多个子类
class SubDemo extends Demo{ } //ok
class SubDemo extends Demo1,Demo2...//error
7.2.1 初始化基类
现在涉及到了基类(父类)和导出类(子类)这两个类。对于导出类所产生的结果对象,从外部看,他就像一个于基类具有相同接口的新类,或许还有一些额外的方法和域。但继承不只是复制基类的接口。创建了一个导出类的对象,该对象包含了一个基类的子对象。这个子对象与直接用基类创建对象是一样的。区别在于后者来自外部,而基类的子对象被包装在导出类对象内部。
对于基类的初始化至关重要,仅有一种方法来保证这一点:在构造器中调用基类构造器来执行初始化,基类构造器具有执行基类初始化所需要的所有知识和能力。java会自动在导出类的构造器中插入对基类构造器的调用。例如:
//: reusing/Cartoon.java
// Constructor calls during inheritance.
import static net.mindview.util.Print.*;
class Art {
Art() { print("Art constructor"); }
}
class Drawing extends Art {
Drawing() { print("Drawing constructor"); }
}
public class Cartoon extends Drawing {
public Cartoon() { print("Cartoon constructor"); }
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
} /* Output:
Art constructor
Drawing constructor
Cartoon constructor
*///:~
带参数构造器
上述例子中各个类均含有默认的构造器,即不带参数。如果想调用带参数的基类构造器,这时候就不必须使用super关键字,可以调用基类的构造器,关于super其他用法,在章末详解。
例如:
//: reusing/Chess.java
// Inheritance, constructors and arguments.
import static net.mindview.util.Print.*;
class Game {
Game(int i) {
print("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
print("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);
print("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
} /* Output:
Game constructor
BoardGame constructor
Chess constructor
*///:~
7.3 代理
除了组合和继承,还有一种关系叫做代理。java没有对其提供直接支持,这是继承和组合的中庸之道。将一个成员的对象置于构造的类中(组合),与此同时在新类中暴露了该成员对象的所有方法(就像继承),例如,太空船需要一个控制模块:
//: reusing/SpaceShipControls.java
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() {}
} ///:~
构造太空船的一种方法就是使用继承:
//: reusing/SpaceShip.java
public class SpaceShip extends SpaceShipControls {
private String name;
public SpaceShip(String name) { this.name = name; }
public String toString() { return name; }
public static void main(String[] args) {
SpaceShip protector = new SpaceShip("NSEA Protector");
protector.forward(100);
}
} ///:~
SpaceShip包含了SpaceShipControls,但是与此同时SpaceShipControls的所有方法都在SpaceShip中暴露出来,代理解决了这个难题。
//: reusing/SpaceShipDelegation.java
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls =
new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// Delegated methods:
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 turboBoost() {
controls.turboBoost();
}
public void up(int velocity) {
controls.up(velocity);
}
public static void main(String[] args) {
SpaceShipDelegation protector =
new SpaceShipDelegation("NSEA Protector");
protector.forward(100);
}
} ///:~
使用代理可以得到更多的控制力,可以选择只提供在成员对象中某个方法的子集。
7.4 结合使用组合和继承
同时使用组合和继承是常见的事,用来构造更加复杂的类。例如:
//: reusing/PlaceSetting.java
// Combining composition & inheritance.
import static net.mindview.util.Print.*;
class Plate {
Plate(int i) {
print("Plate constructor");
}
}
class DinnerPlate extends Plate {
DinnerPlate(int i) {
super(i);
print("DinnerPlate constructor");
}
}
class Utensil {
Utensil(int i) {
print("Utensil constructor");
}
}
class Spoon extends Utensil {
Spoon(int i) {
super(i);
print("Spoon constructor");
}
}
class Fork extends Utensil {
Fork(int i) {
super(i);
print("Fork constructor");
}
}
class Knife extends Utensil {
Knife(int i) {
super(i);
print("Knife constructor");
}
}
// A cultural way of doing something:
class Custom {
Custom(int i) {
print("Custom constructor");
}
}
public class PlaceSetting extends Custom {
private Spoon sp;
private Fork frk;
private Knife kn;
private DinnerPlate pl;
public PlaceSetting(int i) {
super(i + 1);
sp = new Spoon(i + 2);
frk = new Fork(i + 3);
kn = new Knife(i + 4);
pl = new DinnerPlate(i + 5);
print("PlaceSetting constructor");
}
public static void main(String[] args) {
PlaceSetting x = new PlaceSetting(9);
}
} /* Output:
Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
PlaceSetting constructor
*///:~
7.4.1 名称屏蔽
如果java基类拥有某个已被多次重载的方法名称,在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何一个版本。因此,无论是在该层还是它的基类中对方法进行定义,重载机制都可以正常工作。
例如:
//: reusing/Hide.java
// Overloading a base-class method name in a derived
// class does not hide the base-class versions.
import static net.mindview.util.Print.*;
class Homer {
char doh(char c) {
print("doh(char)");
return 'd';
}
float doh(float f) {
print("doh(float)");
return 1.0f;
}
}
class Milhouse {}
class Bart extends Homer {
void doh(Milhouse m) {
print("doh(Milhouse)");
}
}
public class Hide {
public static void main(String[] args) {
Bart b = new Bart();
b.doh(1);
b.doh('x');
b.doh(1.0f);
b.doh(new Milhouse());
}
} /* Output:
doh(float)
doh(char)
doh(float)
doh(Milhouse)
*///:~
java SE5新增了@Override注解,并不是关键字,但是可以当做关键字使用,如果想要覆写某个方法可以使用这个注解,在你重载时而非重写的时候生成错误信息,防止你在不想重载的时候意外进行了重载。详细见下一章多态中重写部分。
7.5 组合与继承之间选择
组合和继承都允许在新的类中放置子对象,但是组合是显式地这样做,继承则是隐式地做,二者的区别在于:
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种形式。即在新类中嵌入某个对象,让它实现需要的功能。新类用户看到的是为新类定义的接口,而非所嵌入对象的接口。即需要在新类中嵌入一个现有类的private类。
在继承的时候使用某个现有类,并开发一个它的特殊版本。通常,这意味者你在使用一个通用类,并为了某种特殊需要将其特殊化。通常“is-a”的关系用继承表达,而“has-a”的关系用组合表达。
7.6 protected 关键字
实际项目中,经常想要将某些事物尽可能的隐藏起来,但仍然允许导出类的成员访问它们。关键字protected就是起的这个作用
7.7 向上转型
为新的类提供方法并不是继承技术最重要的方面,最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”来加以概括。
例如:假设有一个称为Instrument乐器的基类和一个称为Wind的导出类,由于继承可以确保基类中所有的方法在导出类同样有效。能够向基类发送的信息同样可以发送向导出类发送。这意味着我们可以准确地说Wind对象也是一种类型的Instrument。
例如:
//: reusing/Wind.java
// Inheritance & upcasting.
class Instrument {
public void play() {}
static void tune(Instrument i) {
// ...
i.play();
}
}
// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // Upcasting
}
} ///:~
此例中,tune()方法可以接受Instrument引用。上述这种将wind引用转换为Instrument引用的动作称之为向上转型。即子类(导出类)到父类(基类)的类型转换,它可以自动进行,导出类是基类的一个超集,比基类包含更多方法,并且必须至少具备基类中所含有的方法。
向上转型还可以用来作为选择组合还是继承的判断,如果必须使用向上转型,则继承是必要的。
7.8 final关键字
final通常指的是“这是无法改变的”。不想做出改变的原因可能是设计或者效率
可能使用final的有三种情况:数据,方法,类。
7.8.1 final数据
许多编程语言都有某种方法,来向编译器告知一块数据是永恒不变的。有时数据永恒不变是很有用的,比如:
- 一个永不改变的编译时常量
- 一个在运行时被初始化的值,而你不希望它被改变。
在java中这类常量必须是基本数据类型并且是以final关键字表示,对这个常量进行定义的时候,必须对其进行赋值,例如:final int valueOne=15;
一个既是static有时final的域只占据一段不能改变的存储空间。
当对对象引用而不是基本类型运用final,是,final使其引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象,然而,对象其自身是可以被修改的,java并未提供使其对象恒定不变的途径。
例如:
//: reusing/FinalData.java
// The effect of final on fields.
import java.util.*;
import static net.mindview.util.Print.*;
class Value {
int i; // Package access
public Value(int i) { this.i = i; }
}
public class FinalData {
private static Random rand = new Random(47);
private String id;
public FinalData(String id) { this.id = id; }
// Can be compile-time constants:
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
// Typical public constant:
public static final int VALUE_THREE = 39;
// Cannot be compile-time constants:
private final int i4 = rand.nextInt(20);
static final int INT_5 = rand.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private static final Value VAL_3 = new Value(33);
// Arrays:
private final int[] a = { 1, 2, 3, 4, 5, 6 };
public String toString() {
return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
//! fd1.valueOne++; // Error: can't change value
fd1.v2.i++; // Object isn't constant!
fd1.v1 = new Value(9); // OK -- not final
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Object isn't constant!
//! fd1.v2 = new Value(0); // Error: Can't
//! fd1.VAL_3 = new Value(1); // change reference
//! fd1.a = new int[3];
print(fd1);
print("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
print(fd1);
print(fd2);
}
} /* Output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*///:~
空白final
java允许生成空白final。即被声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final使用前必须被初始化。
例如:
//: reusing/BlankFinal.java
// "Blank" final fields.
class Poppet {
private int i;
Poppet(int ii) { i = ii; }
}
public class BlankFinal {
private final int i = 0; // Initialized final
private final int j; // Blank final
private final Poppet p; // Blank final reference
// Blank finals MUST be initialized in the constructor:
public BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet(1); // Initialize blank final reference
}
public BlankFinal(int x) {
j = x; // Initialize blank final
p = new Poppet(x); // Initialize blank final reference
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(47);
}
} ///:~
必须在域的定义处或者每个构造器中用表达式对final进行赋值。(这里不可以有static表示)
final参数
java允许在参数列表中以声明的方式将参数指明为final。意味着无法再方法中更改参数引用所指向的对象。即你可以读参数,但是不可以修改参数。
7.8.2 final方法
使用final方法的原因有两个:第一个就是把方法锁定,以防任何继承类修改它的含义,即无法重写。这是出于设计考虑
第二个原因是出于效率。过去如果将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。而在最近的java版本中,虚拟机可以探测到这些情况,不需要再使用final方法来进行优化。在使用java5/6时,应该让编译器和jvm去处理效率问题,只有在想要明确禁止覆盖,才将方法设置为final的。
final和private关键字
类中所有的private方法都隐式的指定是final的。无法取用private方法,也就无法覆盖它。
如果你试图覆盖一个private方法,似乎是奏效的,但是没有任何意义。
覆盖只有在某方法是某基类的接口的一部分才会出现,即必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法是private,它就不是基本类的接口的一部分。它仅是一些隐藏类中的程序代码,只不过具有相同的名称而已。
7.8.3 final类
当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许其他人这么做。即禁止继承,不希望它有子类。
7.9 static关键字
表示静态的,可以用来修饰属性,方法,代码块(初始化块),内部块。(除了构造器不行)
1. 修饰属性:(类变量)
a) 由类创建的所有对象,都共用一个属性
b) 当其中一个对象对此属性进行修改,会导致其他对象对此属性的调用VS 实例对象(非static修饰属性)
c) 类变量随着类的加载而加载,而且独一份
d) 静态的变量可以直接通过“类.类变量”的形式来调用。
2. 修饰方法(类方法)
a) 随着类的加载而加载,在内存中也是独一份
b) 可以直接通过“类.类方法”的方式调用。
c) 在静态方法内部,可以调用静态的属性或方法,而不能调用非静态的属性或方法。反之非静态的方法是可以调用静态的属性或方法,静态的生命周期更长,同时消亡被回收也要晚于非静态结构。
使用范围:
在Java类中,可用static修饰属性、方法、代码块、内部类
被修饰后的成员具备以下特点:
随着类的加载而加载
优先于对象存在
修饰的成员,被所有对象所共享
访问权限允许时,可不创建对象,直接被类调用
因为不需要实例就可以访问static方法,因此static方法内部不能有this。(也不能有super ? YES!)
重载的方法需要同时为static的或者非static的
7.10 方法重写
定义:在子类(导出类)中可以根据需要对从父类(基类)中继承来的方法进行改造,也称方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。
要求:
重写方法必须和被重写方法具有相同的方法名称、参数列表和返回值类型。
重写方法不能使用比被重写方法更严格的访问权限。
重写和被重写的方法须同时为static的,或同时为非static的
子类方法抛出的异常不能大于父类被重写方法的异常
例如:
public class Main{
void find(){
System.out.println("Main");
}
public static void main(String[]args){
Main M= new Subclass();
m.find();
}
}
class Subclass extends Main{
void find(){
System.out.println("Subclass");
}
}
//out:
Subclass
7.11 super关键字
在继承结构中,如果想要调用基类(父类)的构造器,被重写的方法,属性(域)等,可以使用关键字来实现。
super修饰方法
当导出类重写基类的方法以后,在导出类中若要显式的调用父类的被重写的方法可以使用关键字super,格式为:super.method();
super修饰属性(域)
当子父类出现同名成员的时候,可以用super进行区分,这和this有点类似。this代表本类对象应用,super代表父类的内存空间标识。super追溯不仅限于直接父类。
super修饰构造器
通过在子类中使用super(形参列表)类显示调用父类中指定的构造器。
子类中所有的构造器默认都会访问父类中空参数的构造器
当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器,且必须放在构造器的第一行,this(形参列表)和super(形参列表)只能出现一个。
如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错
实例:
public class Student extends Person {
private String school;
public Student(String name, int age, String s) {
super(name, age);
school = s;
}
public Student(String name, String s) {
super(name);
school = s;
}
public Student(String s) { // 编译出错: no super(),系统将调用父类无参数的构造方法。
school=s;
}
}
所以设计一个类的时候,尽量设计一个空参的构造器,防止编译出错。
关键字this与super的区别
No. | 区别点 | this | super |
---|---|---|---|
1 | 访问属性 | 访问本类中的属性,如果本类没有此属性则从父类中继续查找 | 访问父类中的属性 |
2 | 调用方法 | 访问本类中的方法 | 直接访问父类中的方法 |
3 | 调用构造器 | 调用本类构造器,必须放在构造器首行 | 调用父类构造器,必须放在子类构造器的首行 |
4 | 特殊 | 表示当前对象 | 无此概念 |