Java中一切都是对象,而对象源自于类型,也就是类。而将类做到复用是简化代码的一项重要措施。
在Java中将类复用有两种方法:
-
在一个新的类中包含已经有的类的对象。
public class Book { private String name; private StoryBook storyBook; }
如上代码,在Book类中我们又组合进了一个新的StoryBook类型的对象,这种方式就是Java中的组合,它可以达到代码复用的功能
-
继承。它按照现有的类型来创建新的类型,不必改变现有类的形式,采用现有类的形式并在其中添加新的代码。
public class StoryBook extends Book{ String name; }
如上代码,StoryBook是新的类型,但是他是从原有的Book类型中扩展而来的。
我们都知道,一个类中的字段或着参数是基本类型的时候,会自动初始化为零,但是对象类型的引用数据类型会被初始化为null,这个时候,如果你想用这个引用类型的字段调用它其中的方法,那一定会报空指针异常的错误,因为这个引用没有指向任何一个实际的对象。
组合语法
组合语法及其简单,只需将对象的引用置于新的类中即可。
初始化引用类型字段的时机
那么我们如何对引用类型的字段进行正确的初始化?下面有几个初始化的时机需要牢记。
在定义对象的地方
public class Book {
private String name;
private StoryBook storyBook = new StoryBook();
}
如上代码,在定义的时候就要使用new 关键字为其分配内存空间以对其初始化。
在类的构造器中
public class Book {
private String name;
private StoryBook storyBook = null;
Book(){
this.storyBook = new StoryBook();
}
}
如上代码,可以在类的构造器中进行初始化,保证这个类的对象在使用的时候它其中的所有字段都是可用的。
在正要使用这个类型的对象之前
这种方式称之为惰性初始化。也就是不到使用这个对象的时候不会为他分配空间,这样能做到一定程度的性能提升。
public class Book {
private String name;
@Override
public String toString() {
if (name == null){
name = "yes";
}
return "Book{" +
"name='" + name + '\'' +
'}';
}
}
如上代码,即展示了惰性初始化,在只有在使用toString方法的时候,才会对name字段进行复制,否则它的值就是null;
使用实例初始化
public class Book {
private String name;
public static void main(String[] args) {
Book book = new Book();
book.name = new String("yes");
}
}
如上代码,就是对引用字段进行实例初始化
其实对于引用类型字段的初始化没有想的那么复杂,无非也就是这四种情况。
继承语法
public class StoryBook extends Book{
}
继承是一件很简单的事儿,我们只需要加上extend关键字即可获得基类的所有字段和方法。
在继承了基类之后仍然可以在本来中定义和使用只属于本类的方法和字段。
同时当前类和基本都拥有相同的字段和方法的时候会优先使用当前类的方法和字段,只有在当前类中找不到目标字段或者方法的时候才会去基类(父类)中寻找并调用。
初始化基类
现在存在基类(父类)和导出类(子类)这两个类。当我们创建一个子类对象的时候,这个对象实际上包含了一个父类的子对象,这个子对象和直接使用父类创建的对象是一样的,不过它不是独立的,而实包含在子类对象内部。
对父类子对象的初始化是十分重要的,这种初始化的方式只有一种,就是在子类的构造器中调用父类的构造器来对父类子对象进行初始化,如果我们不手动调用这个构造器,那么Java会自动在构造器中插入对父类构造器的调用。
class Art{
Art(){
System.out.println("第一代");
}
}
class Drawing extends Art{
Drawing(){
System.out.println("第二代");
}
}
public class Cartoon extends Drawing{
Cartoon(){
System.out.println("第三代");
}
public static void main(String[] args) {
Cartoon cartoon = new Cartoon();
}
}
如上代码,其结果为:
第一代
第二代
第三代
从结果中明显可以看出,对于存在继承的类,他们的构建过程是从基类向外扩散的。所以在子类可以访问父类之前,父类(基类)就已经完成了初始化。
初始化基类——带参数的构造器
上面的情形都是没有带参数的构造器(也就是默认构造器),但是如果父类的构造器是自定义的,有参数的构造器的时候,那么我们继承了父类的子类必须要我们手动的调用父类的构造器。使用super关键字,并且调用语句必须写在子类构造器的第一行。否则编译器就会报错。
class Game{
Game(int i){
System.out.println("游戏基础");
}
}
class BoardGame extends Game{
BoardGame(int i) {
super(i);
System.out.println("游戏进阶");
}
}
public class Chess extends BoardGame{
Chess(int i) {
super(i);
System.out.println("游戏高级");
}
public static void main(String[] args) {
Chess chess = new Chess(12);
}
}
代理
上面知道了Java中类的复用可以使用类的组合和继承两种方式来实现,但是这两种方式也有一些缺点。使用继承的话那子类会拥有父类的所有方法和字段,但是有时为了安全,我们只希望有一部分方法能被子类使用,这个时候就可以使用代理来完成,代理实际上是一个中间人的角色,父类把一些方法交给代理,而在子类中使用代理去调用父类的方法,这样就能达到控制的目的。
/**
* 太空飞船
* @author xiaopu
*/
public class SpaceShip {
String name;
SpaceShip(String name) {
this.name = name;
}
public static void main(String[] args) {
SpaceShip spaceShip = new SpaceShip("一号");
}
}
/**
* 太空飞船控制模块
* @author xiaopu
*/
public class SpaceShipControls {
void up(int velocity){}
void down(int velocity){}
void left(int velocity){}
void right(int velocity){}
void forword(int velocity){}
void back(int velocity){}
void turboBoost(int velocity){}
}
/**
* 太空飞船的控制模块代理
* @author xiaopu
*/
public class SpaceShipDelegation {
private SpaceShipControls spaceShipControls = new SpaceShipControls();
public void up(int velocity) {
spaceShipControls.up(velocity);
}
public void down(int velocity) {
spaceShipControls.down(velocity);
}
public void left(int velocity) {
spaceShipControls.left(velocity);
}
public void right(int velocity) {
spaceShipControls.right(velocity);
}
public void forword(int velocity) {
spaceShipControls.forword(velocity);
}
public void back(int velocity) {
spaceShipControls.back(velocity);
}
public void turboBoost(int velocity) {
spaceShipControls.turboBoost(velocity);
}
}
上面的例子表明,使用代理的时候,类与类之间没有明确的继承关系,飞船调用控制模块的时候通过控制模块的代理来实现,而不是直接调用控制模块的方法。
继承和组合的结合使用
继承机制和组合机制的混用,是Java开发时候常用的技巧和步骤,虽然编译器会强制提醒我们要初始化父类,但是并不会提醒我们初始化父类的成员对象,因此,这个时候,我们必须时刻注意,避免空指针异常的情况。
正确清理
在Java开发中,我们基本上可以不用管垃圾回收,因为有垃圾回收机制自动帮我们完成这个工作,但是有些特殊的时候,必须要求我们手动的清理垃圾。这个时候我们一定要注意释放顺序,因为在类与类之间可能存在着关系。如果类A正用着类B,如果这个时候我们先释放了B,那么A可能会就报一个空指针异常的错误。
在清理中,最好的办法就是,只把内存管理交给垃圾回收去做。
名称屏蔽:
Java中使用了继承机制的类,子类在重载了父类的方法后,父类的方法仍然是可以使用的。
向上转型:
向上转型是Java继承机制的极为重要的一个特性,通常来讲,Java中有着极其严格的类型检查,但是在继承机制中,子类对象却可以交给父类类型的引用。
public class Person {
}
class Student extends Person{
public static void main(String[] args) {
Person student = new Student();
}
}
如上代码,student类继承自person类,在创建student类的对象时,可以复制给person类型的变量。
为什么要向上转型:
继承机制是对父类的扩展,所以子类总是比父类更加的具体和详细,而向上转型就是一个较为专用类型向较为通用类型的转换。所以总是很安全的,其次,我们在开发过程中,通用的类型更加易用,而专用的类型的使用空间往往比较狭窄。
方法丢失
注意:在向上转型的过程中容易出现的一个问题就是丢失字段和方法。如下代码所示
public class Person {
String name;
public void work() {
System.out.println("工作");
}
}
class Student extends Person {
String studentId;
public void study() {
System.out.println("学习");
}
public static void main(String[] args) {
Student student = new Student();
Person person = new Student();
student.study();
student.work();
person.work();
}
}
student类继承自person类,我们在创建对象的时候,将student类型的对象赋值给了person类型的应用,这个时候就实现了向上转型。但是此时实际上是student类型的person变量就不能调用study方法了,因为在person类型中是没有这个方法的。这就是向上转型过程中方法的丢失。
什么时候适合使用继承
当你确定无疑要使用向上转型的时候,这时你应该考虑使用继承机制,否则其他时候通常不要使用继承。