注:本博客内容是本人在看《Jave编程思想》这本书时从该书上抄录下来的一些片段。这里也强烈建议各位读者去购买这本书进行阅读学习。
复用代码是Java众多引人注目的功能之一。Java复用代码的诀窍在于使用类而不破坏现有程序代码。有两种可以达到一个目的的方法。第一种方法就非常直观,只需要在新的类中产生现有类的对象。由于新的类是由现有类的对象所组成,所以这种方法称为组合。该方法只是复用了现有程序代码的功能,而非它的形式。
第二种方式则更加细致一些,它按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新的代码。这种神奇的方法称为继承,而且编辑器可以完成其中大部分工作。
就组合和继承而言,其语法和行为大多是相似的。由于它们都是利用现有的类生成新的类,所以这样做极富意义。
一、组合语法
使用组合方法,只需要将对象引用置于新的类中即可。例如:定义一个学生类(Student):
public class Student {
private String name;
private String studentNo;
private int age;
}
定义一个教师(组合类)类(Teacher):
public class Teacher {
private String teacherNo;
private String teacherName;
private Student student;
}
二、继承语法
继承是所有OOP语言不可缺少的组成部分。当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从Java标准根类Object进行继承。
组合的语法比较平实,但是继承使用的是一种特殊的语法。在继承过程中,需先要声明“新类与旧类相似”。这种声明通过在类主体的左边花括号之前,书写后面紧随基类名称的关键字extend而实现。当这么做时,会自动得到基类所有的域和方法。例如:
创建一个基类:
public class Cleanser {
private String str = "Cleanser";
public void append(String s) {
s += str;
}
public void dilute() {
append(" dilute()");
}
public void apply() {
append(" apply()");
}
public void scrub() {
append(" scrub()");
}
@Override
public String toString() {
return str;
}
}
创建一个子类:
public class Detergent extends Cleanser {
// 改变一个方法
@Override
public void scrub() {
append(" Detergent.scrub()");
// 调用基类的scrub方法
super.scrub();
}
// 新增一个方法
public void foam() {
append(" foam()");
}
public static void main(String[] args) {
Detergent detergent = new Detergent();
detergent.dilute();
detergent.apply();
detergent.scrub();
detergent.foam();
System.out.println(detergent.toString());
}
}
我们可以发现Cleanser中的所有方法都是public的,这一点非常重要。为了继承,一般的规则是将所有的数据成员都指定为private,将所有的方法指定为public(稍后将会学到,protected成员也可以借助导出类来访问)。当然,在特殊情况下,必须作出调整,但是上述方法的确是一个很有用的规则。
如上代码所示,Detergent是通过关键字extends从Cleanser导出的,所以它可以在其接口中自动获取Cleanser中的方法,尽管并不能看到这些方法在Detergent中显示定义。因此,可以讲继承视为是对类的复用。
此外,正如方法scrub()所见,使用基类中定义的方法以及对它进行修改是可行的。在此例中,你可能想要在子类中调用继承而来的方法。但是在scrub()方法中,并不能直接调用scrub(),因为这样做将会产生递归,而这并不是你所期望的。为解决此问题,Java用super关键字表示超类,当前类就是从超类中继承而来的。为此,表达式super.scrub()将调用基类中的scrub()方法。
2.1 初始化基类
由于现在涉及基类和导出类这两个类,而不是一个类,所以要试着想象导出类产生的结果对象,会有一点困惑。从外部来看,它就像是一个基类具有相同接口的新类,或许还会有一些额外的方法和域。但继承并不是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在导出类对象的内部。
当然,对于基类子对象的正确初始化也是至关重要的,而且也仅有一种方法来保证这一点:在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需的所有知识和能力。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 cartoon = new Cartoon();
}
}
输出结果如下:
Art constructor
Drawing constructor
Cartoon constructor
我们可以发现,构件过程是从基类“向外”扩展的,所以基类在导出类构造器可以访问它之前,就已经完成了初始化。即使你不为Cartoon()创建构造器,编译器也会为其合成一个默认的构造器,该构造器将调用基类的构造器。
2.2 带参数的构造器
上例中各个类都含有默认的构造器,即这些构造器都不带参数。编译器可以轻松地调用它们是因为不必考虑要传递什么样的参数问题。但是,如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须使用关键字super显示地编写调用基类构造器的语句,并且配以适当的参数列表。例如:
class Game {
Game(int i) {
System.out.println("Game constructor " + i);
}
}
class BoardGame extends Game{
public BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor " + i);
}
}
public class Chess extends BoardGame{
public Chess(int i) {
super(i);
System.out.println("Chess constructor " + i);
}
public static void main(String[] args) {
Chess chess = new Chess(100);
}
}
输出结果:
Game constructor 100
BoardGame constructor 100
Chess constructor 100
如果不在BoardGame()中调用基类构造器,编译器将“抱怨”无法找到符合Game()形式的构造器。而且,调用基类构造器必须是你在导出类构造器中要做的第一件事(如果你做错了,编译器会提醒你)。
三、代理
第三种关系称为代理。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 SpaceShip extends SpaceShipControls {
private String name;
public SpaceShip(String name) {
this.name = name;
}
public static void main(String[] args) {
SpaceShip spaceShip = new SpaceShip("JACK SpaceShip");
spaceShip.forward(100);
}
}
我们使用继承会发现SpaceShipControls中的所有方法在SpaceShip中都暴露出来了。如果我们想要控制只提供部分方法,该怎么办呢?代理解决了此难题。代码如下:
public class SpaceShipDelegation {
private SpaceShipControls controls = new SpaceShipControls();
private String name;
public SpaceShipDelegation(String name) {
this.name = name;
}
// 委派方法
public void back(int velocity) {
controls.back(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 static void main(String[] args) {
SpaceShipDelegation delegation = new SpaceShipDelegation("Delegate SpaceShip");
delegation.forward(100);
}
}
可以看到,上面的方法是如何传递给了底层的controls对象,而器接口由此也就与使用继承得到的接口相同了。但是,我们使用代理时可以拥有更多的控制力,因为我们可以选择只提供在成员对象中的方法的某个子集。
四、结合使用组合和继承
同时使用组合和继承是很常见的事。下例就展示了同时使用这两种技术,并配以必要的构造器初始化,来创建更加复制的类:
package com.jackson.reuse.mulit;
/**
* 功能:
* 描述:
*
* @Author jack
* 2019/11/27下午11:53
**/
class Plate {
Plate (int i) {
System.out.println("Plate constructor");
}
}
class DinnerPlate extends Plate {
public DinnerPlate(int i) {
super(i);
System.out.println("DinnerPlate constructor");
}
}
class Utensil {
Utensil(int i) {
System.out.println("Utensil constructor");
}
}
class Spoon extends Utensil {
public Spoon(int i) {
super(i);
System.out.println("Spoon constructor");
}
}
class Fork extends Utensil {
public Fork(int i) {
super(i);
System.out.println("Fork constructor");
}
}
class Knife extends Utensil {
public 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 spoon;
private Fork fork;
private Knife knife;
private DinnerPlate dinnerPlate;
PlaceSetting(int i) {
super(i);
spoon = new Spoon(i +1);
fork = new Fork(i + 2);
knife = new Knife(i + 3);
dinnerPlate = new DinnerPlate(i + 4);
}
public static void main(String[] args) {
PlaceSetting setting = new PlaceSetting(20);
}
}
输出结果:
Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
虽然编译器强制你去初始化一个基类,并且要求你要在构造器起始处就要这么做,但它并不监督你必须将成员对象也初始化,因此在这一点上需要时刻注意。
4.1 确保正确的清理
在Java中我们的习惯只是忘掉而不是销毁对象,并且让垃圾回收器在必要时释放器内存。通常这样做是好事,但有时类可能要在其生命周期内执行一些必要的清理活动。因为你并不知道垃圾回收器何时将会调用,或者它是否将被调用。因此,如果你想要某个类清理一些东西,就必须显示编写一个特殊的方法来做这些事,并确保客户端程序员知晓他们必须要调用这一方法。同时需要将这一清理动作置于finally子句之中,以预防异常的出现。
许多情况下,清理并不是问题,仅需让垃圾回收器完成该动作就行。但当必须亲自处理清理时,就得多做努力并多加小心。因为,一旦涉及垃圾回收,能够信赖的事就不会很多了。垃圾回收器可能永远也无法被调用,即使被调用,它也可能以任何它想要的顺序来回收对象。就好的办法就是除了内存以外,不能依赖垃圾回收器去做任何事。如果需要进行清理,最好编写你自己的清理方法,但不要使用finalize()方法。
4.2 名称屏蔽
如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本(这一点与C++不同)。因此,无论是该层或者它的基类中对方法进行定义,重载机制都可以正常工作:
package com.jackson.reuse.mulit;
class Homer {
String doSomething(String s) {
System.out.println("Homer.doSomething(String)");
return "Homer";
}
float doSomething(float f) {
System.out.println("Homer.doSomething(float)");
return 1.0f;
}
}
class MilHouse {}
class Bart extends Homer {
void doSomething(MilHouse house) {
System.out.println("MilHouse");
}
}
public class Hide {
public static void main(String[] args) {
Bart bart = new Bart();
bart.doSomething(1);
bart.doSomething("hello");
bart.doSomething(2.0F);
bart.doSomething(new MilHouse());
}
}
输出结果:
Homer.doSomething(float)
Homer.doSomething(String)
Homer.doSomething(float)
MilHouse
五、在组合和继承之间选择
组合和继承都允许在新的类中放置子对象,组合是显示地这样做,而继承则是隐式地做。其二者有何区别?以及怎样在二者之间选择?
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。为取得此效果,需在新类中嵌入一个现有类的private对象。
有时,允许类的用户直接访问新类中的组合成分是极具意义的;也就是说,将成员对象声明成public。如果成员对象自身都隐藏了具体实现,那么这种做法是安全的。当用户能够了解到你正在组装一组部件时,会使的端口更加易于理解。car对象即是一个很好的例子:
package com.jackson.reuse.mulit;
class Engine {
public void start() {
System.out.println("start engine");
}
public void restart() {
System.out.println("restart engine");
}
public void stop() {
System.out.println("shutdown engine");
}
}
class Wheel {
public void inflate(int psi) {
System.out.println("inflate wheel");
}
}
class Window {
public void rollup() {
System.out.println("rollup window");
}
public void rolldown() {
System.out.println("rolldown window");
}
}
class Door {
public Window window = new Window();
public void open() {
System.out.println("open door");
}
public void close() {
System.out.println("close door");
}
}
public class Car {
public Engine engine = new Engine();
public Wheel [] wheels = new Wheel[4];
public Door left = new Door(),
right = new Door(); // create 2 door
public Car() {
// create 4 wheel
for (int i = 0; i < 4; i++) {
wheels [i] = new Wheel();
}
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rollup();
car.wheels[0].inflate(72);
car.engine.start();
}
}
运行结果如下:
rollup window
inflate wheel
start engine
由于在这个例子中car的组合也是问题分析的一部分(而不仅仅是底层设计的一部分),所以声明成员为public将有助于客户端程序员了解怎样去使用类,而且也降低了类开发者所面临的代码复杂度。但务必要记住的是这仅仅是一个特例,一般情况下应该使域成为private。
在继承的时候,使用现有的某个类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊的需要而将其特殊化。略微思考一下就会发现,用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅仅是一种交通工具(is-a关系)。“is-a”(是一个)的关系用继承来表的的,而“has-a”(有一个)的关系则是用组合来表达的。
六、protected关键字
我们了解继承后,关键字protected最终具有了意义。在理想世界中,仅靠关键字private就已经足够了。但在实际项目中,经常会想要将某些事物尽可能对这个世界隐藏起来,但仍然允许导出类的成员访问他们。
关键字protected就是起这个作用的。它指明“就类用户而言,这是private的,但对于任何继承此类的导出类或者其他任何位于同一个包内的类来说,他却是可以访问的”。(protecfed也提供了包内访问权限)。例如:
定义一个基类:(注意包的范围)
package com.jackson.reuse.mulit;
public class Villain {
private String keyword;
public Villain(String keyword) {
this.keyword = keyword;
}
/**定义一个protected方法 */
protected void set(String keyword) {
this.keyword = keyword;
}
public void changeKeyword(String keyword) {
this.keyword = keyword;
System.out.println("changeKeyword " + this.keyword);
}
@Override
public String toString() {
return "I'm a Villain and my name is " + this.keyword;
}
}
不在一个包中,使用继承:
package com.jackson.reuse.opration;
import com.jackson.reuse.mulit.Villain;
public class Orc extends Villain {
private int orcNumber;
public Orc(String keyword, int orcNumber) {
super(keyword);
this.orcNumber =orcNumber;
}
public void change(String name, int orcNumber) {
// 可以直接使用,应为它是protected
set(name);
this.orcNumber = orcNumber;
}
@Override
public String toString() {
return "Orc " + orcNumber + " " + super.toString();
}
public static void main(String[] args) {
Orc orc = new Orc("Jack", 25);
System.out.println(orc);
orc.change("Jackson", 18);
System.out.println(orc);
}
}
输出结果:
Orc 25 I'm a Villain and my name is Jack
Orc 18 I'm a Villain and my name is Jackson
在同一个包下不同类中调用:
package com.jackson.reuse.mulit;
public class InPagVillain {
public static void main(String[] args) {
Villain villain = new Villain("in the package");
System.out.println(villain);
// 在同一包下依然可以调用protected方法---印证“protected提供了包内访问权限”
villain.set("in the same package to invoke set() method ");
System.out.println(villain);
}
}
输出结果:
I'm a Villain and my name is in the package
I'm a Villain and my name is in the same package to invoke set() method
在不同包内调用:
package com.jackson.reuse.opration;
import com.jackson.reuse.mulit.Villain;
public class NotPagVillain {
public static void main(String[] args) {
Villain villain = new Villain("not same package invoke");
System.out.println(villain);
// 不在同一个包下再次调用时,发现无法调用protected访问权限定义的方法set()。
// villain.set();
villain.changeKeyword("just can invoke changeKeyword()");
System.out.println(villain);
}
}
输出结果:
I'm a Villain and my name is not same package invoke
changeKeyword just can invoke changeKeyword()
I'm a Villain and my name is just can invoke changeKeyword()
七、向上转型
“为新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以使用“新类是现有类的一种类型”这句话加以概述。
这个描述并非是一种解释继承的华丽的方式,这直接是由语言所支撑的。例如,假设由一个称为Instrument的代表乐器的基类和一个称为Wind的导出类。由于继承可以确保基类中所有的方法在导出类中也同样有效,所以能够向基类发送的所有信息同样也可以向导出发送。如果Instrument类具有一个play()方法,那么Wind乐器也将同样具备。这意味着我们可以准确地说Wind对象也是一种类型的Instrument。如下的例子说明了编译器是怎样支持这一概念的:
class Instrument {
public void play() {
System.out.println("instrument play");
}
static void tune(Instrument instrument) {
instrument.play();
}
}
public class Wind extends Instrument{
public static void main(String[] args) {
Wind wind = new Wind();
// 向上转型
Instrument.tune(wind);
}
}
在此例中,tune()方法可以接受Instrument引用,这是一个非常有趣的事。但在Wind.main()中,传递给tune()方法的是一个Wind引用。鉴于Java对类型的检查是十分严格的,接受某种类型的方法同样接受另一种类型就会显得很奇怪,除非你认识到Wind对象同样也是Instrument对象,而且也不存在任何tune()方法可以通过Instrument来调用,同时又不存在于Wind之中。在tune()中,程序代码可以对Instrument和它的所有的导出类起作用,这种将Wind引用转为Instrument引用的动作,我们称为向上转型。
7.1 为什么称为向上转型
该术语的使用有其历史原因,并且是以传统的类继承图的绘制方法为基础的:将根置于页面的顶端,然后逐渐向下。于是,Wind的继承图就是(如下图所示):
由导出类转型成基类,在继承图上是向上移动的,因此一般称为向上转型。由于向上转型是从一个专用类型向较通用的类型转换,所以总是很安全的。也就是说,导出类是基类的一个超集。它可能比基类含有更多的方法,但它必须至少具备有基类所有的方法。在向上转型过程中,类接口中唯一可能发生的事情就是丢失方法,而不是获取它们。这就是为什么编译器在“未明确表示转型”或“未曾指定特殊标记”的情况下,仍然允许向上转型的原因。
7.2 再论组合和继承
在面向对象编程中,生成和使用程序代码最有可能采用的方法就是直接将数据和方法包装进一个类中,并使用该类的对象。也可以运用组合技术使用现有的类来开发新的类;而继承技术其实是不太常用的。因此,尽管在OOP中多次强调继承,但这并不意味着要尽可能使用它。相反,应该慎用这一技术,其使用场景仅限于你确信用该技术确实有效的情况。
到底是使用组合还是继承,一个最清晰的判断就是问一下自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的;但如果不需要,则应当好好考虑自己是否需要继承。
八、final关键字
根据上下文环境,Java的关键字final的含义存在细微的区别,但它通常指的是“这是无法改变的”。不想做改变可能出于两种理由:设计或效率。由于这两个原因相差很远,所以关键字final有可能被误用。
8.1 final数据
许多编程语言都有某种方法,来向编译器告知一块数据是恒定不变的。有时数据的恒定不变是很有用的,比如:
- 一个永不改变的编译时常量
- 一个在运行时被初始化的值,而你不希望它被改变
对于编译期常量这种情况,编译器可以将该常量值代入任何可能用到它的计算中,也就是说,可以在编译时执行计算,这减轻一些运行时的负担。在Java中,这类常量必须时基本数据类,并且以关键字final表示。在对这个常量进行定义时,必须对其进行赋值。
一个既是static又是final的域只占据一段不能改变的存储空间。
当对对象引用而不是基本数据类型运用final时,其含义会有一点令人迷惑。对于基本实际类型final使数值恒定不变;而用于对象引用,final使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象。然而,对象其自身是可以被修改的,Java并未提供使任何对象恒定不变的途径(但自己可以编写类以取得是对象恒定不变的效果)。这一限制同样适用于数组,它也是对象。
如下示例示范了final域的情况。注意,根据惯例,即使static又是final的域(即编译器常量)将要大写表示,并使用下划线分隔各个单词:
package com.jackson.reuse.mulit;
import java.util.Random;
class Value {
/**包访问权限*/
int i;
public Value(int i) {
this.i = i;
}
}
public class FinalData {
private static Random random = new Random(47);
private String id;
public FinalData(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 = random.nextInt(20);
static final int INT_5 = random.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private static final Value VALUE_3 = new Value(33);
// ARRAYS
private final int [] arr= {1, 2, 3, 4, 5, 6};
@Override
public String toString() {
return id + ": " + "i4 = "+ i4 + ", INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData finalData = new FinalData("final");
// finalData.valueOne ++; ERROR:can't change value
finalData.v2.i ++; // 对象不是恒定的,只是引用是不变的,所以可以修改对象的属性值
finalData.v1 = new Value(9); // OK,because not final
for (int i = 0; i < finalData.arr.length; i++) {
finalData.arr[i] ++;
}
// finalData.v2 = new Value(0); //ERROR:Can't
// finalData.VALUE_3 = new Value(1); // Error : Can't change reference
// finalData.arr = new int[5]; // Error:Can't change reference
System.out.println(finalData);
System.out.println("create new FinalData");
FinalData fd2 = new FinalData("fd2");
System.out.println(finalData);
System.out.println(fd2);
}
}
我们不能因为某数据是final的就确认在编译时就可以知道它的值。在运行时使用随机数生成的数值类初始化i4和INT_5的值就说明了这一点。示例也展示了将final数值定义为静态和非静态的区别。此区别只有当数值在运行时内被初始化才会显现,这是因为编译器对编译时数值一视同仁(而且它们可能因优化而消失)。当运行程序时就会看到这个区别。需要注意的是,在finaleData和fd2中的i4的值是唯一的,但是INT_5的值是不可以通过创建第二个FinalData对象而加以改变的。这是因为它是static的,在装载时已被初始化,而不是每次创建新对象时都初始化(static修饰的只初始化一次)。
空白final
Java允许生成“空白final”,所谓空白final是指被声明为final但未给定初始值的域。无论什么情况,编译器都确保空白final在使用前必须初始化。但是,空白final在关键字final的使用上提供了更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却又保证其恒定不变的特性。例如:
package com.jackson.reuse.mulit;
class Poppet {
private int i;
public Poppet(int i) {
this.i = i;
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
public class BlankFinal {
private final int i = 0; // 初始化final域
private final int j; // 空白final
private final Poppet poppet; // 空白final引用
public BlankFinal() {
this.j = 1; // 初始化空白final
this.poppet = new Poppet(1); // 初始化空白final引用
}
public BlankFinal(int x) {
this.j = x; // 初始化空白final
this.poppet = new Poppet(x); // 初始化空白final引用
}
@Override
public String toString() {
return "i = " + i + ", j = " + this.j + ", poppet.i = " + poppet.getI();
}
public static void main(String[] args) {
System.out.println(new BlankFinal());
System.out.println(new BlankFinal(47));
}
}
必须在域的定义处或者每个构造器中使用表达式对final进行赋值,这正是final域使用前总是被初始化的原因所在。
final 参数
Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象。
package com.jackson.reuse.mulit;
class Apple {
public void spin(){
System.out.println("Apple.spin()");
}
}
public class FinalArguments {
void with(final Apple apple) {
// apple = new Apple(); // Error: apple is final
}
void without(Apple apple) {
apple = new Apple(); // Ok,apple is not final
apple.spin();
}
int finalBaseType(final int i) {
// return i ++; // Error:cant't change i ++(i=i+1)对i进行了改变,所以不允许
return i + 1;
}
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.with(null);
bf.without(null);
System.out.println(bf.finalBaseType(10));
}
}
方法finalBaseType()展示了当基本数据类型的参数被指明为final时所出现的结果:你可以读参数,但却无法修改参数。这一特性主要用来向匿名内部类传递数据。
8.2 final方法
使用final方法的原因有两个。第一个原因是把方法锁定,以防止任何继承类修改它的含义。这是出于设计考虑:想要确保在继承中使用方法行为保持不变,并且不会被覆盖。
过去建议使用final方法的第二个原因是效率。在Java的早期实现中,如果将一个放啊指明为final,就是同意编译器将针对该方法的所有调用转为内嵌调用。但是在最新的Java版本中,虚拟机(特别是hotsport技术)进行大量的优化,因此不在需要使用final方法来进行优化。
final和private关键字
类中所有的private方法都隐式地指定为final的。由于无法取用private方法,所以也就无法覆盖它。可以对private放啊添加final修饰词,但这并不能给方法增加任何额外的意义。
这一问题会造成混淆。因为,如果你尝试覆盖一个private方法(隐含是final的),似乎是奏效的,而且编译器也不会给出错误信息:
package com.jackson.reuse.mulit;
class WithFinals {
private final void f() {
System.out.println("WithFinals.f(),f() is private and final");
}
private void g() {
System.out.println("WithFinals.g(),g() is private");
}
}
class OverridePrivate extends WithFinals {
private final void f() {
System.out.println("OverridePrivate.f()");
}
private void g() {
System.out.println("OverridePrivate.g()");
}
}
class OverridePrivate2 extends OverridePrivate {
public final void f() {
System.out.println("OverridePrivate2.f()");
}
public void g() {
System.out.println("OverridePrivate2.g()");
}
}
public class FinalOverridePrivate {
public static void main(String[] args) {
OverridePrivate2 op2 = new OverridePrivate2();
op2.f();
op2.g();
// 可以向上转型
OverridePrivate op =op2;
// 但是不可以调用方法
//op.f(); // can't call
//op.g(); // can't call
// 相同地
WithFinals wf = op2;
// wf.f(); // can't call
// wf.g(); // can't call
}
}
“覆盖”只有在某个方法是基类的接口的一部分时才会出现。即必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private。它就不是基类接口的一部分。它仅仅是一些隐藏于类中的代码,只不过是具有相同的名称而已。但是如果在导出类中以相同的名称生成一个public、protected或包访问权限的方法的话,该方法就不会产生在基类中出现相同的“仅具有相同名称”的情况。此时你并没有覆盖该方法,仅是产生类一个新的方法。
8.3 final类
当某个类的整体定义为final时(通过将关键字final置于它的定义之前),就明了你不打算继承该类,而且也不允许别人这样做。换句话说,出于某种考虑,你对该类的设计不需要做任何的变动,或者出于安全的考虑,你不希望它有子类。
package com.jackson.reuse.mulit;
class SmallBrain {}
final class FinalClass {
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f() {
System.out.println("FinalClass.f()");
}
@Override
public String toString() {
return "i = " + i + ", j = " + j;
}
}
// class Further extends FinalClass {} // Error: cannot extend final class 'FinalClass'
public class Jurassic {
public static void main(String[] args) {
FinalClass n = new FinalClass();
System.out.println(n);
n.f();
n.i = 40;
n.j ++;
System.out.println(n);
}
}
需要注意的是,final类的域可以根据个人的意愿选择是或不是final。不论类是否被定义为final,相同的规则都适用于定义为final的域。然而,由于final类禁止继承,所以final类中所有的方法都隐式指定为final的,因为无法覆盖它们。在final类中可以给方法添加final修饰符,但这不会添加任何意义。
九、初始化以及类的加载
在许多传统语言中,程序是作为启动过程的一部分立刻被加载的。然后是初始化,紧接着程序开始允许。这些语言的初始化过程必须小心控制,以确保定义为static的东西,其初始化顺序不会造成麻烦。例如C++中,如果某个static期望另一个static在初始化之前就能够有效地使用它,那么就会出现问题。
Java就不会出现这个问题,因为它采用了一种不同的加载方式。加载是众多变得更加容易的动作之一,因为Java中所有的事物都是对象。请记住,每个类的编译代码都存在于它自己的独立文件中。该文件只在需要使用程序代码时才会加载。一般来说,可以说:“类的代码初次使用时才加载”。这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载(构造器也是static方法,尽管static关键字并没有显示地写出来。因此更准确地讲,类是在其任何static成员访问时加载的)。
初次使用之处也是static初始化发生之所。所有的static对象和static代码段都会在加载时依程序中的顺序(即,定义类时的书写顺序)而依此初始化。当然,定义为static的东西只初始化一次。
9.1 继承与初始化
了解包括继承在内的初始化全过程,以对所发生的一切有个全局性的把握,是很有益的。例如:
package com.jackson.reuse.mulit;
class Insect {
private int i = 9;
protected int j;
public Insect() {
System.out.println("i = " + i + ", j = " + j);
this.j = 39;
}
private static int x1 = printInt("static Insect.x1 initialized");
static int printInt(String s) {
System.out.println("output + " + s);
return 47;
}
}
public class Beetle extends Insect{
private int k = printInt("Beetle.k initialized");
public Beetle() {
System.out.println("k = " + k);
System.out.println("j = " + j);
}
private static int x2 = printInt("Beetle.x2 initialized");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle beetle = new Beetle();
}
}
输出结果:
output + static Insect.x1 initialized
output + Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
output + Beetle.k initialized
k = 47
j = 39
在加载Beetle时,编译器注意到它有一个基类(这是由关键字extends得知的),于是他继续进行加载。不管你是否打算产生一个该基类对象,这都要发生。如果该基类还有其自身的基类,那么第二个基类就会被加载,如此类推。接下来,跟基类中的static初始化,然后是下一个导出类,以此类推。这种方法很重要,因为导出类的static初始化可能依赖于基类成员能否被正确初始化。