在java中,问题的解决都是围绕类展开的,我们通过创建新类,也可以使用别人已经开发并调试好的类而不用再从头开始编写,这时候就称为
复用类
复用类的窍门在于使用类而不破坏现有程序代码,包括两种方法(代码重用机制):
- 在新类中使用现有类的对象,由于新类是由现有类的对象组成,所以这种方法称为
组合
- 按照现有类的类型来创建新类,采用现有类的形式并添加新代码而不改变现有类的形式,这种方式称为
继承
7.1 组合语法
组合技术十分简单,基本上我们天天都在用,比如要创建一个职员类,一个职员有姓名、年龄、月薪、部门等属性,我们在创建这个类的时候需要用相应的数据类型来表示这些属性,就拿姓名属性来说,到底用什么类型表示它呢,对,就是字符串,可是,这个字符串类型在java中应该用什么类来表示呢,巧了,java正好提供了String
这个类来表示字符串类型,所以我们就直接拿来使用而不必自己再写一个表示字符串的类了。
之后你会发现,年龄(整数)、月薪(带小数的数字)属性在java中都有现成的类可以使用。但是到了部门这个属性的时候你一脸懵逼,在java中没有表示部门这个属性的类可以使用了,怎么办,只能自己写一个了。
如果幸运的话,编写部门类的时候我们不会再碰上需要我们再写新类来表示部门类中某个属性的情况。
进展很顺利,部门类Dept
写完了,我们像使用String
类一样使用Dept
类来表示职员的部门属性类型,因为使用java类库现有的类和我们自己编写的类本质上其实没什么不同。就这样,职员类便写好了:
public class Employ {
private String name;//姓名
private int age; //年龄
private Dept dept; //部门
/** getters and setters **/
}
原则上来说,只要在一个类中使用了别的类型,都可以叫做复用了类。
7.1.1 引用的初始化
职员类写好了,假定我们使用默认的构造器创建了一个职员对象然后输出这个对象的话,会发现除了年龄为0之外,另外两个属性都是null
(前提是你重写了toString()方法在其中返回了职员对象的属性信息)。
这是因为类中基本类型的域会被初始化成其默认值,而对象的引用则会被初始化成null
,编译器为了不增加不必要的负担,不会简单地为每个引用都创建默认对象,如果想初始化这些引用,可以在以下几个位置进行:
- 在定义对象的地方,这意味着这个引用总是能够在构造器调用之前被初始化
- 在构造器内部
- 在正要使用这些对象之前,这种方式称为
惰性初始化
,这种方式对于减少内存的负担是很有用的,因为有时候一个引用被声明之后根本就没有被使用,虽然从JVM的角度来说:从未被使用的对象将被回收以释放其占据的内存,但是终究来说这个没用的对象曾经还是占用过内存(强迫症) - 使用实例初始化(比如常用的使用新建的对象去调用
set
方法初始化)
7.2 继承语法
继承在java中无处不在,到目前为止,所有java类中,除了老祖宗
Object
类以外,所有其他的类都是继承着别的类的,除非明确指出继承哪个类,否则就是隐式地继承java标准根基类Object
。语法结构:
public class Sub extends Sup { /** ... */ }
7.2.1 初始化基类
继承并不只是复用基类的接口。当创建一个子类的对象时,该对象还包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者的区别在于子类的对象来自于外部,而基类的子对象被包装在子类的对象内部。
对基类对象的正确初始化也是至关重要的,仅有一种方法保证这一点:在子类构造器中调用基类构造器来执行初始化,这个工作java帮我们自动完成了。
在继承关系中,类的初始化是有一定顺序的,这种顺序是由基类“向外”扩散,即在子类初始化之前,基类的初始化就已经完成了,即使在子类中没有创建构造器,编译器也会为我们合成一个默认的构造器,并在其中调用基类的构造器。
LKZS:
class Grandpa {
Grandpa() { System.out.println("Grandpa:我是你爷爷");}
}
class Dad extends Grandpa {
Dad() {System.out.println("Dad:我是你爹");}
}
public class Son extends Dad {
Son() {System.out.println("Son:我是尼古拉斯.赵四");}
public static void main(String[] args){
Son son = new Son();
}
}
输出:
Grandpa:我是你爷爷
Dad:我是你爹
Son:我是尼古拉斯.赵四
以上例子中均只含有默认构造器,如果没有默认构造器或者想调用带参数的构造器时,必须使用关键字super
显示调用
7.3 代理
除了组合、继承,第三种关系称为“代理”。
在继承中,如果一个类A
继承了类B
,那么A
可以调用它能够调用的所有B
中的方法,但是,如果B
中的某个方法因为调用的危险性或者A
根本就不需要,这时候又该如何呢?
打个比方:一个爸爸可以干许多事,比如打工、创业、跟老婆调情、开车等,儿子也可以打工、创业,但是跟他妈调情绝对是天理不容的,爸爸也会禁止他这么做(程序表现就是一个private方法),至于开车,也许老爸不会限制,但是老司机可以开不代表小司机也可以,如果儿子不知道自己有红绿色盲而开车出去的话,多半也是九死一生。
针对上述尴尬的情形,继承显然已经不再合适,这时候代理起到了作用:
public class Dad {
public void work(){
System.out.println("打工...");
}
public void chuangye(){
System.out.println("创业...");
}
public void flirt(){
System.out.println("调情...");
}
public void drive(){
System.out.println("开车...");
}
}
/**
* 儿子只能选择打工,或者创业
*/
public class Son {
private String name;
private Dad dad;
public Son(String name){
this.name = name;
this.dad = new Dad();
}
public void chuangye() {
dad.chuangye();
}
public void work() {
dad.work();
}
public static void main(String[] args) {
Son son = new Son("三胖");
son.chuangye();
}
}
现在,儿子只能打工或者创业,不能干别的了。
从程序中可以看出,儿子在创业或打工时,实际上调用爸爸的相应方法,所以这时候就可以把儿子看成是爸爸,美其名曰:爸爸的一个代理,爸爸限制了这个代理的功能,让他只能打工或创业(好可怜的儿子)。
由此可以引申出:代理也是可以很灵活的,可以有多个代理类,比如这个爸爸还有一个儿子,这孩子身体健康没有色盲,那么他既可以打工又可以创业,还可以…..开车。
此处有一项技能可以get√
使用JetBrains Idea IDE生成代理对象的代理方法代码
以之前的程序作为例子:在Son
类中生成代理对象Dad
的代理方法代码步骤如下
在代理类
Son
中使用IDE生成快捷键Alt
+Insert
,或者右键菜单选择Generate
在
Generate
选项中选择Delegate Methods
选择要代理的对象属性
最后一步:选择要代理的方法,点击
OK
当执行以上的第3步操作的时候,也就是选择要代理的对象属性时,你会发现,你不能多选,这说明什么呢,也许很有可能说明:在java中,一个类能不能代理多个类。结合之前的分析可以得出一个结论:java中,一个类可以拥有多个代理类,但是它本身只能代理一个类
7.4 结合使用组合和继承
7.4.1 确保正确清理
在
C++
中有一个叫析构函数的概念,它是一种在对象被销毁时可以被自动调用的函数。Java中没有析构函数的概念,因为我们不需要去销毁对象,因为我们有GC
,垃圾回收器会在必要时回收那些被我们忘掉的对象释放其内存。
如上固然是极好的,但是,你并不知道GC
何时将被调用,是否将被调用,所以有时候我们仍需要进行一些必要的清理活动
通常,当我们要显式地进行一些必要清理时,必须将清理操作放在finally
子句当中,确保清理操作肯定能得以执行
社交现在很火,出现了一些新颖好玩的东西,比如发送的消息固定时间内可以撤回
,可以添加阅后即焚
的消息等,在我认为,这些对消息有趣的处理方式,也是一种正确清理的例子吧
就拿阅后即焚
来说,发送的消息就是要被清理的对象,阅读消息是一个方法,在这个方法执行完后,也就是我们看完这条消息之后,这条消息要被清理掉,防止被他人看见泄露天机。写成代码大概就是这样的:
public class Message {
private String msg;
private boolean isRead;
public Message(String msg) {
this.msg = msg;
}
/** getters and setters **/
public static void fireAfterRead(Message message) {
try {
readMessage(message);
} finally {
destroyMessage(message);
}
}
private static void destroyMessage(Message message) {
if (!message.isRead()){
throw new IllegalStateException("消息状态异常...");
}
System.out.println("开始销毁:" + message.getMsg());
message = null;
if (message == null) {
System.out.println("销毁成功");
} else {
System.out.println("销毁失败");
}
}
private static void readMessage(Message message) {
System.out.println("阅读消息:" + message.getMsg());
message.setRead(Boolean.TRUE);
}
public static void main(String[] args) {
fireAfterRead(new Message("其实我是个演员"));
}
}
输出:
阅读消息:其实我是个演员
开始销毁:其实我是个演员
销毁成功……
如果在移动开发中,destroyMessage
方法中可能还会包含从设备屏幕抹除消息然后销毁消息对象等操作。
7.5 向上转型
基类为子类提供方法,并不是继承最重要的方面,其最终要的方面是用来表现新类和基类之间的关系,这种关系可以用“新类是基类的一种类型”这句话来概括。
/**
* 交通工具
*/
public class Transport {
public void move(){}
public static void move(Transport transport){
transport.move();
}
}
/**
* 汽车
*/
public class Car extends Transport{
public static void main(String[] args) {
Car car = new Car();
Transport.move(car);
}
}
在以上代码中,Transport
类中的move()
方法可以接受Transport
参数看起来很神奇,但是在Car.main()
方法中传递的却是一个Car
引用,只有一个参数的方法能够接受两种类型的参数看起来很奇怪,但这恰好说明了其实它们已经被当做了一种类型。代码可以对Transport
和它的所有子类起作用,汽车可以,自行车、电动车们也可以。这种将Car
引用转换为Transport
引用的动作,就称为向上转型。
至于为什么叫向上转型,通俗地说是根据类的继承关系来定的,子类转型成基类,继承图上是由下到上,所以叫向上转型。
向上转型是安全的,因为它是从一个专用类型向通用类型转换。
7.5.1 慎用继承
尽管继承的好处大大的,但是并不代表要尽可能使用它,继承的使用场合仅限于明确要使用向上转型的时候,如果必须向上转型,那么继承就是必要的,如果没必要转型,就尽可能使用组合,到底应该使用哪种,只需要自问一下:“我真的需要向上转型吗?”。
7.6 final关键字
final关键字的含义归根结底就是一句话:“这是无法改变的”。
final可用于一下三种情况:
- 数据
- 方法
- 类
7.6.1 final数据
很多时候我们需要使一些数据保持恒定不变,这类数据包括:
- 一个永不改变的编译时常量
- 一个在运行时被初始化并且在此之后不会改变的值
我们用final来修饰以上两种数据来保持它们恒定不变,其中,对于编译时常量,编译器可以在编译时将该常量值代入任何需要它的地方执行计算公式,减轻了一些运行时的负担,在java中,编译时常量必须是基本数据类型,尽管经常看到诸如pubic static final String VAR = "var"
这样的代码,但是,这个“VAR”并不是一个编译时常量,也许很多人碰到这样的面试题都会做错。
当final用于对象引用而不是基本类型时,表示的是对象的引用恒定不变,对象一旦被初始化后,虽然对象自身可以随意修改,但是这个对象的引用无法再指向另一个对象。
注意:以上关于final用于对象引用的情况同时也使用于数组
,因为数组也是对象。
下面是一个例子帮助理解记忆:
public class BeijingAccount {
/**
* 北京邮政区码,编译期常量,恒定改变
*/
public static final int POST_EREA_CODE = 1000000;
public final City city = new Bejing();
}
假定以上代码表示在北京落户的户口,北京的邮政区码是个编译期常量不会改变,北京户口的城市当然肯定是北京,city
这个引用对象可以在定义的时候初始化,也可以在构造器中初始化(空白final:很多情况下对象类型的常量都是在构造器中初始化),初始化之后,你可以随意改变对象“北京”的属性,你可以修改它的人口数,你可以重新任命一个北京市长,你甚至可以把北京改名叫西京,但是不管怎么改,city
这个引用永远都是指向它而不能再指向别的城市。
7.6.2 引申:编译时常量和运行时常量
一个例子说明问题:
class Constant {
static{
System.out.println("class has been loaded...");
}
public static final int A = 47;
public static final int B = "HelloWorld".length();
}
public class ConstantDiff {
public static void main(String[] args) {
System.out.println(Constant.A);
System.out.println(Constant.B);
}
}
输出:
47
class has been loaded...
10
结论:
A
被作为编译期全局常量,并不依赖于类,而B
作为运行期的全局常量,其值还是依赖于类的(因为第2点)。- 编译时常量在编译时就可以确定值,如上例中的
A
可以确定值,但是B
在编译器是不可能确定值的。 - 由于编译时常量不依赖于类,所以对编译时常量的访问不会引发类的初始化。
- 静态块的执行在运行时常量之前,在编译时常量之后。
因此可以说:编译时常量同时也是运行时常量。
7.6.3 final方法
出于设计的考虑:想要确保在继承中使方法行为保持不变且不会被覆盖,就必须把方法锁定,防止任何继承类修改它的含义。
在使用继承时,如果想明确禁止子类重写父类的某个方法,就将此方法设置为final的。就像老和尚不管小和尚怎么念经打坐,但是红烧肉必须按照老和尚的口味来做。
7.6.4 final和private关键字
所有类中的private
方法都隐式地指定为final
的,所以对private方法添加final修饰符没有任何意义。由于无法取用private方法,所以子类不能覆盖它,如果我们恰好在子类中声明了与父类中一个private方法同名的方法,并不代表我们重写了这个private方法,这就像我们不能根据返回值来判断重载一样,它们只代表在子类中新建了一个方法而已。
7.6.5 final类
俗话说:够改不了吃屎。狗这个特性永远都不会改变,它吃屎的同时还不允许有什么别的动物跟着它学会吃屎。java中正好有这样一种类,这种类的设计永远不需要做任何变动,同时也不允许别的类继承它,这种类就是final类。
从final类的定义可以看出:由于final类禁止继承,所以final类中所有的方法都隐式地指定为是final的,所以在final类中给方法添加final修饰词也然并卵。
7.7 初始化及类的加载
也许在某一次面试时,面试官会问你:“在java中,访问类中一个static域时,这个类被加载了吗?”,你会怎么回答。
java中的加载机制是这样的:每个类的编译代码都存在于它自己独立的文件中(class文件),该文件只在需要使用程序代码时才会被加载。一般来说:“类的代码在初次使用时才被加载”,也就是指加载发生在创建类的第一个对象的时候。
但是,当访问static对象域
时(对象域,不是编译时常量,编译时常量在编译时已经确定了值可以不加载类直接使用),也会发生加载,因为:“构造器也是static方法,尽管static关键字没有显式地写出来。因此更准确地讲,类在其任何static成员被访问时会被加载”。上面有关编译时常量和运行时常量的例子中去掉“A”和“B”前面的final修饰符运行程序输出的信息就能够证明这一点。