复用类的关键在于使用类而不破坏现有程序代码,通过两种方式可以达到此目的:
第一种,只需在新的类中产生现有类的对象。由于新的类是由现有的类所组成,所以这种方式称为组合,该方法只是复用了现有程序代码的功能,而非它的形式;
第二种,按照现有类的类型创建新类,采用现有类的形式并在其中添加新代码。这种神奇的方式称为继承,而且编译器可以完成其中大部分工作。
1. 组合语法
class WaterSource{
private String s;
WaterSource(){
System.out.println("WaterSource");
s = "Constructed";
}
public String toString(){
return s;
}
}
public class SprinklerSystem {
private String valve1;
private String valve2;
private String valve3;
private String valve4;
private WaterSource source = new WaterSource(); // 在SprinklerSystem类中使用 WaterSource类的对象
private int i;
private float f;
@Override
public String toString() { // 每个非基本类型的对象都有一个toString()方法,当编译器需要一个String而只有1个对象时,这个方法回被调用
return "SprinklerSystem{" +
"valve1='" + valve1 + '\'' +
", valve2='" + valve2 + '\'' +
", valve3='" + valve3 + '\'' +
", valve4='" + valve4 + '\'' +
", source=" + source + // 这里需要WaterSource的对象source ,当程序执行到这里时,回调用WaterSource的toString()方法,将source转换成一个String
", i=" + i +
", f=" + f +
'}';
}
public static void main(String[] args) {
SprinklerSystem sprinklers = new SprinklerSystem();
System.out.println(sprinklers);
}
}
/** Output:
WaterSource
SprinklerSystem{valve1='null', valve2='null', valve3='null', valve4='null',source=Constructed, i=0, f=0.0}
*/
以上代码执行结果中 :valve1='null', valve2='null', valve3='null', valve4='null',
说明一个问题:类中域为基本类型时能够自动被初始化为零,但是对象引用会被初始化为null。如果想要初始化引用,可以在代码中的下列位置进行:
- 在定义对象的地方,这意味着它们总是能够在构造器被调用之前被初始化
- 在类的构造器中
- 在正要使用这些对象之前,这种方式称为惰性初始化。在生成对象不值得及不必要每次都生成对象的情况下,这种方式可以减少额外的负担
- 使用实例初始化
class Soap{
private String s;
Soap(){
System.out.println("Soap()");
s = "Constructed";
}
@Override
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(){ // 在类的构造器中初始化
System.out.println("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();
System.out.println(b);
}
}
2. 继承语法
继承是所有OOP语法和Java语言不可缺少的组成部分。当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从Java的标准根类Object进行继承。
class Cleanser{ // Cleanser类中的所有方法都必须是public的。这是因为,如果没有加任何访问权限修饰词,那么成员默认的访问权限是包访问权限,它仅允许包内的成员。因此,在此包中,如果没有访问权限修饰词,任何人都可以使用这些方法。但是其他包中的类要从Cleanser中继承,则只能访问public成员,所以,为了继承,一般的规则是将所有的数据成员都指定为private,将所有的方法指定为public
private String s = "Cleanser";
public void append(String a){
s += a; // 使用“+=” 操作符号将String链接成s,此操作符是被Java设计者重载用以处理String对象的操作符之一,另一个是“+”
}
public void dilute(){
append("dilute()");
}
public void apply(){
append(" apply()");
}
public void scrub(){
append(" scrub()");
}
@Override
public String toString() {
return s;
}
}
public class Detergent extends Cleanser{
public void scrub(){ // 覆盖基类的scrub()方法
append(" Detergent.scrub()");
super.scrub(); // 调用基类的方法 ,Java用super关键字表示超类的意思,当前类就是从超类继承的。所以super.scrub()将调用基类版本的scrub()方法
}
public void foam(){ // 增加新的方法
append(" foam()");
}
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
System.out.println(x);
System.out.println("Testing base class;");
}
}
初始化基类
当创建一个子类的对象时,该对象包含了一个父类的子对象。这个子对象与用父类直接创建的对象是一样的。而且区别在于,后者来自于外部,而父类的子对象被包装在子类对象内部
因此,对父类子对象的正确初始化也是至关重要,而且也仅有一种方法来包中这一点:在构造器中调用父类构造器来执行初始化,而父类构造器具有执行父类初始化的能力。Java会自动在子类中构造器中插入对父类构造器的调用
class Art{
Art(){
System.out.println("Art constructor");
}
}
class Drawing extends Art{
Drawing(){
System.out.println("Drawing constructor");
}
}
public class Cartoon extends Drawing{
public Cartoon(){
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
}
/** 执行结果 : 通过结果会发现,构建过程是从父类“向外”扩散的,所以父类在子类构造器可以访问它之前,就已经完成了初始化了。即使不为Cartoon()创建构造器,编译器也会为你合成一个默认的构造器,该构造器将待用父类的构造器
Art constructor
Drawing constructor
Cartoon constructor
*/
带参数的构造器
如果没有默认的父类构造器,或者想调用一个带参数的父类构造器,就必须用关键字super显式地编写调用父类构造器的语句,并且配以适当的参数列表:
class Game{
Game(int i){
System.out.println("Game constructor");
}
}
class BoardGame extends Game{
BoardGame(int i){
super(i);
System.out.println("BoardGame constructor");
}
}
public class Chess extends BoardGame{
Chess(){
super(11); // 必须要在子类中调用父类的带参数的构造器
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
}
3. 代理
除了组合和继承,还有一种可以复用类就是使用代理,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(){}
}
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 dowm(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");
}
}
4. 结合使用组合和继承
class Plate{
Plate(int i){
System.out.println("Plate constructor");
}
}
class DinnerPlate extends Plate{
DinnerPlate(int i){
super(i);
System.out.println("DinnerPlate constructor");
}
}
class Utensil{
Utensil(int i){
System.out.println("Utensil constructor");
}
}
class Spoon extends Utensil{
Spoon(int i){
super(i);
System.out.println("Spoon constructor");
}
}
class Fork extends Utensil{
Fork(int i){
super(i);
System.out.println("Fork constructor");
}
}
class Knife extends Utensil{
Knife(int i){
super(i);
System.out.println("Knife constructor");
}
}
class Custom{
Custom(int i){
System.out.println("Custom constructor");
}
}
public class PlaceSetting extends Custom{
private Spoon sp;
private Fork fork;
private Knife knife;
private DinnerPlate pl;
public PlaceSetting(int i){
super(i + 1);
sp = new Spoon(i + 2);
fork = new Fork(i + 3);
knife = new Knife(i + 4);
pl = new DinnerPlate(i + 5);
System.out.println("PlaceSetting constructor");
}
public static void main(String[] args) {
PlaceSetting x = new PlaceSetting(9);
}
}
重载机制
如果Java的父类拥有某个已被多次重载的方法名称,那么在子类中重新定义该方法名称并不会屏蔽其在父类中的任何版本。因此,无论在子类或者它的父类中对方法进行定义,重载机制都可以正常工作
class Homer{
char doh(char c){
System.out.println("doh(char)");
return 'd';
}
float doh(float f){
System.out.println("doh(float)");
return 1.0f;
}
}
class Milhouse{}
class Bart extends Homer{
void doh(Milhouse m){
System.out.println("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());
}
}
从上述例子中,可以看到,虽然Bart引入了一个新的重载方法,但是Bart中Homer的所有重载方法都是可用的。使用与基类完全相同的特征签名及返回类型来覆盖具有相同名称的方法,是一件极其平常的事。
JavaSE5新增加了@Override注解,它并不是关键字,但是可以把它当作关键字使用,当你想要覆盖某个方法时,可以选择添加这个注解,但不小心重载而并非覆盖了该方法时,编译器就会生成一条错误消息。
5. 在组合与继承之间选择
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。为取得此效果,需要在新类中嵌入一个现有类的private对象
在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。通常用“is-a”(是一个)的关系是用继承来表达的,而“has-a”(又一个)的关系则用组合来表达的
6. protected关键字
关键字protected,就类用户而言,这是private的,但对于任何继承于此类的子类或其他任何位于统一包内的类来说,它确实可以访问的。(protected也提供了包内访问权限)
但最好的方式还是将域保持为private,以此保留“修改底层实现”的权利。然后通过protected方法来控制类的继承者的访问权限
7. 向上转型
继承除了为新的类提供方法,还有重要的方面是用来表现子类和父类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。
class Instrument{
public void play(){}
static void tune(Instrument i){
i.play();
}
}
public class Wind extends Instrument{
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // flute是 Instrument子类Wind的对象,tune()方法可以接受Instrument引用,这里将Wind对象传递给tune(),由于Java对类型的检查十分严格,这里Wind对象同样也是一种Instrument对象,在tune()中,程序代码可以对Instrument和它所有的子类起作用,这种将Wind引用转换为Instrument引用的动作,称之为向上转型
}
}
flute是 Instrument子类Wind的对象,tune()方法可以接受Instrument引用,这里将Wind对象传递给tune(),由于Java对类型的检查十分严格,这里Wind对象同样也是一种Instrument对象,在tune()中,程序代码可以对Instrument和它所有的子类起作用,这种将Wind引用转换为Instrument引用的动作,称之为向上转型
为什么称为向上转型
传统的类继承图的绘制方法:将根置于页面的顶端,然后逐渐向下:
Wind.java的继承图由子类转型成父类,在继承图上是向上移动的,因此一般称为向上转型。由于向上转型是从一个较专用类型向通用类型转换,所以总是很安全的。也就是说,子类就是父类的一个超集。子类可能比父类含有更多的方法,但它必须至少具有基类中所含有的方法。
8.final关键字
final会使用到的三种情况:数据、方法和类
final数据
1)一个永不改变的编译时常量
2)一个在运行时被初始化的值,而你不希望它被改变
对于编译期常量这种情况,在Java中,这类常量必须是基本数据类型,以关键字final修饰。在对这个常量进行定义的时候,必须对其进行赋值。
一个既是static又是final的域只占据一段不能改变的存储空间,用大写表示,并使用下划线分隔各个单词
当对对象引用而不是基本类型时,对于基本类型,final使数值恒定不变;而对于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向两一个对象。但,对象自身是可以被修改的。Java并未提供使任何对象恒定不变的途径。这以限制同样适用数组,它也是对象
class Value{
int i;
public Value(int i){
this.i = i;
}
}
public class FinalDate {
private static Random rand = new Random(47);
private String id;
public FinalDate(String id){
this.id = id;
}
private final int valueOne = 9; // 编译时常量
private static final int VALUE_TWO = 99; // 公共常量
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); // final修饰对象
private static final Value VAL_3 = new Value(33); // static final修饰对象
private final int[] a = {1,2,3,4,5,6}; //final修饰数组
public String toString(){
return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalDate fd1 = new FinalDate("fd1");
// fd1.valueOne++; // 不能修改值,因为final修饰
fd1.v2.i++; // 可以修改对象自身的值
fd1.v1 = new Value(9); // 不是final指定的
for(int i = 0; i < fd1.a.length;i++){
fd1.a[i]++; // 可以修改数组内部的值
}
//fd1.v2 == new Value(0); // 不能给v2重新赋值新的对象
//fd1.VAL_3 = new Value(1); // 不能重新赋值
//fd1.a = new int[3]; // 不能重新赋值
System.out.println(fd1);
System.out.println("Creating new FinalDate");
FinalDate fd2 = new FinalDate("fd2");
System.out.println(fd1);
System.out.println(fd2);
}
}
valueOne和VAL_TWO 都是带有编译时数值的final基本类型,可以用作编译期常量
VAL_THREE 是一种更加典型的对常量进行定义的方式:定义为public,则可以被用于包之外;定义为static,则强调只有一份;定义为fianl,则说明它是一个常量。
i4和INT_5说明了不能因为某数据是final的就认为在编译时可以直到它的值。i4和INT_5也展示了final数值定义为静态和非静态的区别,在fd1和fd2中,i4的值是唯一的并且每次创建对象初始化的值是不同的,但INT_5的值是不可以通过创建第二个FinalData对象而加以改变的,此区别只有当数值在运行时内被初始化时才会显现。
v1、v2、VAL_3 说明了final引用的意义。由于v2是final的,就认为无法改变它的值。由于它是一个引用,final意味着无法将v2再次指向另一个新的对象。这对数组具有同样的意义,数组只不过是另一种引用。
空白final
Java允许生成“空白final”,所谓空白final是指被声明为final但又没有给定初值的域。必须在域的定义处或者每个构造器中用表达式对final进行赋值
class Poppet{
private int i;
Poppet(int i){
this.i = i;
}
}
public class BlankFinal {
private final int i = 0;
private final int j;
private final Poppet p;
public BlankFinal(){
j = 1;
p = new Poppet(1);
}
public BlankFinal(int x){
j = x;
p = new Poppet(x);
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(47);
}
}
final参数
Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象:
class Gizmo{
public void spin(){}
}
public class FinalArguments {
void with(final Gizmo g){
// g = new Gizmo(); // 无法修改参数引用所指向的对象
}
void without(Gizmo g){
g = new Gizmo();
g.spin();
}
void f(final int i){ // 展示了当基本类型的参数被指明为final时所出现的结果:你可以读参数,但却无法修改参数。这一特性主要用来向匿名内部类传递数据
//i++; // 不能修改
}
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方法
类中所有的private方法都隐式地指定为是final的。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但这并不能给该方法增加任何额外的意义
class WithFinals{
private final void f(){
System.out.println("WithFinals.f()");
}
private void g(){
System.out.println("WithFinals.g()");
}
}
class OverridingPrivate extends WithFinals{
private final void f(){
System.out.println("OverridingPrivate.f()");
}
private void g(){
System.out.println("OverridingPrivate.g()");
}
}
class OverridingPrivate2 extends OverridingPrivate{
public final void f(){
System.out.println("OverridingPrivate2.f()");
}
public void g(){
System.out.println("OverridingPrivate2.g()");
}
}
public class FianlOverridingIllusion {
public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
OverridingPrivate op = op2;
WithFinals wf = op2;
}
}
覆盖只有在某方法是基类的接口的一部分时才会发现。即,必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private,它就不是父类接口的一部分,仅是一些隐藏于类中的程序代码,只不过是具有相同的名称而已。但如果在子类中以相同的名称生成一个public、protected或包访问权限方法的话,该方法就不会产生在基类中出现的“仅具有相同名称”的情况, 此时,并没有覆盖该方法,仅是生成了一个新的方法
final类
当将某个类的整体定义为final时,就表明这个类不能被继承
class SmallBrain{}
final class Dinosaur{
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f(){}
}
public class Jurassic {
public static void main(String[] args) {
Dinosaur n = new Dinosaur();
n.f();
n.i = 40;
n.j++;
}
}
final域可以根据的个人的意愿选择是或不是final,不论类是否被定义为final,相同的规则都适用于定义为final域。然而,由于final类禁止继承,所以final了中所有的方法都隐式的指定为是final的,因为无法覆盖它们。在final类中可以给方法添加final修饰词,但这不会增加任何意义。
9. 初始化及类的加载
在Java中的所有事物都是对象,每个类的编译代码都存在于它自己的独立的文件中。该文件只在需要使用程序代码时才会被加载。一般来说,可以说“类的代码在初次使用时才会加载”。这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载
初次使用之处也是static初始化发生之处。所有的static对象和static代码段都会在加载时依程序中的顺序(即,定义类时的书写顺序)而依次初始化。当然,定义为static只会被初始化依次
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;
}
}
public class Bettle extends Insect{
private int k = printInit("Beetle.k initialized");
public Bettle(){
System.out.println("k = " + k);
System.out.println("j = " + j);
}
private static int x2 = printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Bettle b = new Bettle();
}
}
/**
执行结果:
static Insect.x1 initialized // 父类的静态方法被调用,在创建对象前
static Beetle.x2 initialized // 子类的静态方法被调用,在创建对象前
Beetle constructor // main()方法开始执行
i = 9, j = 0 // 父类的构造器执行,在此之前,父类的字段初始化,没有赋值的基本类型被置为默认值,对象应用被设为 null,
Beetle.k initialized // 子类的字段初始化
k = 47 // 子类的构造器执行
j = 39
*/
在Bettle上运行Java时,所发生的第一件事情就是访问Bettle.main()(一个static方法),于是加载器开始启动并找出Bettle类的编译代码(在名为Bettle.class的文件之中)。在对它进行加载的过程中,编译器注意到它有一个父类(这是由关键字extends得知的),于是它继续进行加载。不管你是否打算产生一个父类的对象,这都要发生。
如果该父类还有其自身的父类,那么第二个父类就会被加载,如此类推。接下来,根父类中的static初始化(在上述例子中Insect类)即会被执行,然后是下一个子类,以此类推。这种方式很重要,因此子类的static初始化可能会依赖于父类成员能否被正确初始化
至此为止所有的类都加载完毕,对象就可以创建了。首先,对象中所有的基本类型都会被设为默认值,对象引用被设为null--这是通过将对象内存设为二进制零制而一举生成的。然后,父类的构造器会被调用。在上述例子中,它是被自动调用的。但也可以用super来指定对父类类构造器的调用。
网友评论