Java编程思想学习笔记(7)
复用类
复用代码是Java的功能之一。
Java中对代码的复用是围绕着类展开的,可以不用创建新的类,来重新用这段代码,而不用重头开始写这个功能,只要引用和调用别人写好,调试好的类就可以,一般是有两种方法,要注意,这两种方法都是不用破坏现有的代码,而是直接调用,或者用继承:
- 第一种方法,在新的类中创建现有类的对象,这种方法称为组合。这个方法只是重新调用了现有程序代码的功能。
- 第二种方法,它按照现有类的类型来创建新类。不用改变现有类的形式,采用现有类的形式并在其中添加新的代码。这种方法称为继承,而且编译器可以完成其中大部分工作。继承是面向对象程序设计的基石之一,减少了开发者的很多工作,一定程度实现了资源共享以及扩展。
这两种方法,就组合和继承而言,由于它们都是利用现有类型生成新的类型。
组合
组合技术,只需要将对象引用置于新类中即可。
例子:
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);
}
}
编译器并不是简单的为每一个引用都创建默认对象,如果想初始化这些引用。可以在代码中的下列位置进行:
- 1 定义对象的地方,这意味着它们总是能够在构造器被调用之前就被初始化。
- 2 在类的构造器中
- 3 就在正要使用这些对象之前。
- 4 使用实例初始化
例子:
public class Soap {
private String s;
Soap() {
print("Soap() Constructed");
s = "Constructed";
}
public String toString() { return s; }
}
public class Bath {
private String // 在定义对象的地方进行初始化
s1 = "Happy",
s2 = "Happy",
s3, s4;
private Soap castille;
private int i;
private float toy;
public Bath() {
print("Inside Bath()");
s3 = "Joy"; // 在类的构造器中初始化
toy = 3.14f;
castille = new Soap();
}
// 使用实例初始化
{ i = 47; }
public String toString() {
if(s4 == null) // 惰性初始化
s4 = "Joy";
return
"s1 = " + s1 + "\n" +
"s2 = " + s2 + "\n" +
"s3 = " + s3 + "\n" +
"s4 = " + s4 + "\n" +
"i = " + i + "\n" +
"toy = " + toy + "\n" +
"castille = " + castille;
}
public static void main(String[] args) {
Bath b = new Bath();
print(b);
}
}
继承
组合语法比较平实,但继承使用的是一种特殊的语法,通过关键字extends来完成。
例子:
public class Cleanser {
private String s = "Cleanser";
public void append(String a) {
s += a;
}
public void dilute() {
append(" dilute()");
}
public void apply() {
append(" apply()");
}
public void scrub() {
append(" scrub()");
}
public String toString() {
return s;
}
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.dilute(); x.apply(); x.scrub();
print(x);
}
}
public class Detergent extends Cleanser{
// 在Cleaner中有一组方法:append(),dilute(),apply(),scrub(),toString()
// 因为Detergent从Cleanser继承而来,所以它可以自动获得这些方法,
// 尽管并不能看到这些方法在Detergent中的显示定义
// 使用基类中定义的方法以及对它进行修改是可行的
public void scrub() {
append(" Detergent.scrub()");
// 不能直接调用scrub方法,因为这样做会产生递归
// Java用super关键字来表示超类的意思
super.scrub(); // 调用原来基类的方法
}
// 在继承过程中,不一定非得使用基类的方法,也可以在导出类中添加新的方法
public void foam() { append(" foam()"); }
// Test the new class:
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
print(x);
print("Testing base class:");
System.out.println("-------------------------");
Cleanser.main(args);
}
}
初始化基类
继承并不只是简单的复制基类的接口,当创建了一个导出类的对象时,该对象包含了一个基类的子对象,这个子对象与你用基类直接创建的对象是一样的,二者的区别在于后者来自于外部,而基类的子对象被包装在导出类的对象内部。
对基类子对象的正确初始化也是很重要的,通过在构造器中调用基类构造器来执行初始化来进行保证。
Java会自动在导出类的构造器汇总插入对基类构造器的调用。
例子:
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();
}
}
可以看出,构建过程是从基类“向外”扩散的,所以基类在导出类构造器可以访问之前就已经完成了初始化。
带参数构造器
对于带参数的构造器,必须使用关键字super显示的编写调用基类构造器的语句。
例子:
class Game {
Game(int i) {
print("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
// 不进行显示调用的就会报错,因为Game中没有默认构造器
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();
}
}
代理
代理,是继承和组合之间的中庸之道。因为当我们将一个成员对象放于所要构造的类中(比如组合),这个时候我们也在新类中暴露了该成员对象的所有方法(比如继承)。
代理也就是:要使用A类的方法,不改变其原有结构,在一个新的类B中创建A的对象a,并且在B中创建方法fb,方法内部是a调用A类的方法,但是使用时是B的对象调用其自身方法fb。
例子:
public class SpaceShip extends SpaceShipControls{
// 通过继承构造太空飞船
// 但此时SpaceShip中包含SpaceShipControls,SpaceShipControls的所有
// 方法都在SpaceShip中暴露了出来
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);
}
}
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;
}
// 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);
}
}
名称屏蔽
如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以工作。
例子:
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());
}
}
protected关键字
关键字protected指明“就类用户而言,这是private的”,但是对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它是可以访问的。
例子:
public class Villain {
private String name;
protected void set(String nm) { name = nm; }
public Villain(String name) { this.name = name; }
public String toString() {
return "I’m a Villain and my name is " + name;
}
}
public class Orc extends Villain{
private int orcNumber;
public Orc(String name, int orcNumber) {
super(name);
this.orcNumber = orcNumber;
}
// 可以看到,change可以访问set,这是因为它是protected
public void change(String name, int orcNumber) {
set(name); // Available because it’s protected
this.orcNumber = orcNumber;
}
public String toString() {
return "Orc " + orcNumber + ": " + super.toString();
}
public static void main(String[] args) {
Orc orc = new Orc("Limburger", 12);
print(orc);
orc.change("Bob", 19);
print(orc);
}
}
final关键字
final数据
有时数据的恒定不变是很有用的,比如:
- 1 一个永不改变的编译时常量
- 2 一个在运行时被初始化的值,而你不希望它被改变
对于编译期常量,编译器可以将该常量代入任何可能用到的计算式中,也就是可以在编译时执行计算式。在Java中,这类常量必须是基本数据类型,并且以final表示。在对这个常量进行定义时,必须对其进行赋值。
一个既是static又是final的域只占据一段不能改变的存储空间
对于基本类型,final使得数值恒定不变,而对于对象引用,final使得引用恒定不变,一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。但是,对象本身是可以修改的。
例子:
public class Value {
int i; // 包访问权限
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; }
// 可以用作编译期常量
private final int valueOne = 9;
// 根据惯例,既是static又是final的域,用大写表示
// 可以用作编译期常量
private static final int VALUE_TWO = 99;
// 更加典型的对常量的定义方式 用public来进行修饰
public static final int VALUE_THREE = 39;
// 不可以用作编译期常量
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);
}
// 不能因为某数据时final就认为是在编译时就可以知道它的值
// 在运行时使用随机生成的数值来初始化i4和INT_5就说明了这一点
// 在fd1和fd2中,i4的值是惟一的,但INT_5的值是不可以通过创建第二个对象来加以改变的,
// 这是因为它是static的,在转载时就已经初始化了。
}
空白final
Java允许生成“空白final”,所谓“空白final”是指被声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化。
一个类中的final域就可以做到根据对象而有所不同,却又保持其恒定不变的特性。
例子:
public 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进行赋值
final参数
Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法再方法中更改参数引用所指向的对象。
例子:
public class Gizmo {
public void spin() {}
}
public class FinalArguments {
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal -- g is final
}
void without(Gizmo g) {
g = new Gizmo(); // OK -- g not final
g.spin();
}
// void f(final int i) { i++; } // Can’t change
// You can only read from a final primitive:
int g(final int i) { return i + 1; }
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
}
final方法
使用final方法的目的是把方法锁定,以防任何继承类修改它的含义。
final和private
类中所有的private方法都隐式的指定为final,由于无法取用private方法,所以也无法覆盖它,
例子:
class WithFinals {
// Identical to "private" alone:
private final void f() { print("WithFinals.f()"); }
// Also automatically "final":
private void g() { print("WithFinals.g()"); }
}
class OverridingPrivate extends WithFinals {
private final void f() {
print("OverridingPrivate.f()");
}
private void g() {
print("OverridingPrivate.g()");
}
}
class OverridingPrivate2 extends OverridingPrivate {
public final void f() {
print("OverridingPrivate2.f()");
}
public void g() {
print("OverridingPrivate2.g()");
}
}
public class FinalOverridingIllusion {
public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
// You can upcast:
OverridingPrivate op = op2;
// But you can’t call the methods:
//! op.f();
//! op.g();
// Same here:
WithFinals wf = op2;
//! wf.f();
//! wf.g();
}
}
覆盖只有在某方法是基类的接口的一部分是才能实现,如果某方法是private,那么它就不是基类的接口的一部分。
final类
当将某个类的整体定义为final时,就表明你不打算继承该类,而且也不允许别人这么做。
初始化及类的加载
Java中每个类的编译代码都存在于它自己的独立的文件中,该文件只有在需要使用程序时才会被加载。一般来说,“类的代码在初次使用时才加载”,这通常指加载发生于创建类的第一个对象时,但是当访问static域或static方法时,也会发生加载。
继承与初始化
例子:
public class Insect {
private int i = 9;
protected int j;
Insect()
{
print("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 =
printInit("static Insect.x1 initialized");
static int printInit(String s)
{
print(s);
return 47;
}
}
public class Beetle extends Insect{
private int k = printInit("Beetle.k initialized");
public Beetle() {
print("k = " + k);
print("j = " + j);
}
private static int x2 =
printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
print("Beetle constructor");
Beetle b = new Beetle();
}
// 运行程序时,所发生的第一件事就是试图访问Beetle.main方法,于是加载器开始启动并找出Beetle
// 类的编译代码,在对它进行加载过程中,编译器注意到它有一个基类,于是它继续进行加载,不管你是否
// 打算产生一个该基类的对象,这都要发生。
// 根基类中的static初始化(即Insect)被执行,然后是下一个导出类,以此类推。
}