在OOP语言中,复用代码一般有两种方式:一种是组合,它只需在新的类中产生现有类的对象,它复用了现有程序代码的功能;还有一种是继承,它按照现有类的类型来创建新类,而无需改变现有类的形式,它复用了现有类的形式。
一、组合语法
示例如下:
//充电器类
public class Charger {
private Wire wire = new Wire();
private Plug plug = new Plug();
@Override
public String toString() {
return "Charger = " + wire + " + " + plug;
}
public static void main(String[] args) {
Charger charger = new Charger();
System.out.println(charger);
//打印结果:Charger = Wire Initialization + Plug Initialization
}
}
//电线类
class Wire{
private String s;
Wire(){
s = "Wire Initialization";
}
@Override
public String toString() {
return s;
}
}
//插头类
class Plug{
private String s;
Plug(){
s = "Plug Initialization";
}
@Override
public String toString() {
return s;
}
}
在上例中,有一个特殊的方法:toString()方法,每一个非基本类型的对象都有一个toString()方法,当编译器需要一个String而你却只有一个对象时,该方法便会被调用。如果你不去显式的定义该方法的话,它将使用超类Object中的toString()方法,每个类都会默认继承Object类。
二、继承语法
在Java中,每个类都会隐式的继承Object类,如果要继承其他已经存在的类,则要使用extends关键字。示例如下:
//动物类
public class Animal {
private String category;
private int age;
public void run(){
System.out.println("run");
}
public void eat(){
System.out.println("eat");
}
}
class Dog extends Animal{
//调用父类的run方法
@Override
public void run() {
super.run();
}
//重写父类的eat方法
@Override
public void eat() {
System.out.println("quickly eat");
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.run();
dog.eat();
//打印结果如下:
//run
//quickly eat
}
}
1、继承结构的初始化顺序
示例如下:
//父类
public class Parent {
Parent(){
System.out.println("Parent Initialization");
}
}
//子类
class Son extends Parent{
Son(){
System.out.println("Son Initialization");
}
}
//子类的子类
class Grandson extends Son{
Grandson(){
System.out.println("Grandson Initialization");
}
public static void main(String[] args) {
Grandson grandson = new Grandson();
//打印结果如下:
//Parent Initialization
//Son Initialization
//Grandson Initialization
}
}
从上例中可以看出来,继承结构的类初始化顺序是从基类开始,然后一级级往下,一直到最后的子类。
2、用带参数的构造器进行初始化
默认情况下,编译器会调用类的默认无参构造函数进行初始化,但是,如果没有默认无参构造函数,那么就必须使用关键字super来显式地编写调用基类构造器的语句,并且配以适当的参数列表。示例如下:
//产品类
public class Product {
Product(int i){
System.out.println("Product Initialization i = " + i);
}
}
//手机类
class MobilePhone extends Product{
MobilePhone(int i){
super(i);
System.out.println("MobilePhone Initialization i = " + i);
}
}
//苹果手机
class IPhone extends MobilePhone{
IPhone(){
super(12);
System.out.println("IPhone Initialization");
}
public static void main(String[] args) {
IPhone iPhone = new IPhone();
//打印如果如下:
//Product Initialization i = 12
//MobilePhone Initialization i = 12
//IPhone Initialization
}
}
三、代理
Java并没有提供对代理的直接支持,它是继承与组合之间的中庸之道,为了不暴露被继承类的所有成员方法,我们可以使用代理的方式来解决这个问题。
1、静态代理
静态代理,主要包含两个实例,被代理类和代理类,两者都要实现公共的接口,把被代理类组合到代理类中,在被代理类的本身功能上,加上代理类的处理逻辑,达到增强功能的效果。静态代理只能事先编写好代码,经过统一编译过才能执行。示例如下:
① 建立公共的接口
package com.thinkinjava.chapter7;
/**
* author Alex
* date 2018/11/7
* description 图书馆服务的接口
*/
public interface LibraryService {
/**
* 增加图书
* @param bookName
*/
void addBook(String bookName);
/**
* 删除图书
* @param bookName
*/
void deleteBook(String bookName);
}
② 建立接口的实现类
package com.thinkinjava.chapter7;
/**
* author Alex
* date 2018/11/7
* description 实现图书馆服务的接口,被代理类
*/
public class LibraryServiceImpl implements LibraryService{
private int bookNum;
LibraryServiceImpl(int bookNum){
this.bookNum = bookNum;
System.out.println("初始化被代理类,赋予其基本功能,图书馆现存图书:" + bookNum);
}
@Override
public void addBook(String bookName) {
System.out.println("增加图书:" + bookName + ",增加之后图书存量:" + ++bookNum);
}
@Override
public void deleteBook(String bookName) {
System.out.println("删除图书:" + bookName + ",删除之后图书存量:" + --bookNum);
}
}
③ 建立接口实现类的代理类
package com.thinkinjava.chapter7;
/**
* author Alex
* date 2018/11/7
* description 实现图书馆服务接口,代理类
*/
public class LibraryServiceProxy implements LibraryService{
private LibraryServiceImpl libraryService;
LibraryServiceProxy(LibraryServiceImpl libraryService){
this.libraryService = libraryService;
System.out.println("初始化代理类,代理图书馆实现类,增强其功能或隐藏其细节");
}
@Override
public void addBook(String bookName) {
System.out.println("新增之前...");
libraryService.addBook(bookName);
System.out.println("新增之后...");
}
@Override
public void deleteBook(String bookName) {
System.out.println("删除之前...");
libraryService.deleteBook(bookName);
System.out.println("删除之后...");
}
}
④ 建立测试类
package com.thinkinjava.chapter7;
/**
* author Alex
* date 2018/11/7
* description 测试图书馆服务的类
*/
public class LibraryServiceTest {
public static void main(String[] args) {
LibraryServiceImpl libraryService = new LibraryServiceImpl(1000);
LibraryServiceProxy proxy = new LibraryServiceProxy(libraryService);
proxy.addBook("《围城》");
proxy.deleteBook("《红楼梦》");
//打印结果如下:
//初始化被代理类,赋予其基本功能,图书馆现存图书:1000
//初始化代理类,代理图书馆实现类,增强其功能或隐藏其细节
//新增之前...
//增加图书:《围城》,增加之后图书存量:1001
//新增之后...
//删除之前...
//删除图书:《红楼梦》,删除之后图书存量:1000
//删除之后...
}
}
四、结合使用组合和继承
1、确保正确的清理
如果你想要某些类清理一些东西,就必须显式的编写一个特殊的方法来做这件事情,并要确保客户端程序员知晓他们必须要调用这一方法。在编写清理方法时,必须要将这一清理动作置于finally子句之中,以预防异常的出现,放在finally子句中的代码总是要执行的。在执行类的特定清理动作时,其顺序应该同生成顺序相反。示例如下:
package com.thinkinjava.chapter7;
//形状类
public class Shape {
Shape(int i){
System.out.println("Shape Initialization");
}
void erase(){
System.out.println("Erasing Shape");
}
}
class Circle extends Shape{
Circle(int i){
super(i);
System.out.println("Circle Initialization");
}
void erase(){
System.out.println("Erasing Circle");
super.erase();
}
}
class Line extends Shape{
private int begin,end;
Line(int begin,int end){
super(begin);
System.out.println("Line Initialization " + begin + " " + end);
}
void erase(){
System.out.println("Erasing Line");
super.erase();
}
}
class CADSystem extends Shape{
private Circle circle;
private Line[] line = new Line[2];
CADSystem(int i){
super(i);
this.circle = new Circle(i);
for(int j = 0;j<line.length;j++){
line[j] = new Line(i,i*i);
}
System.out.println("CADSystem Initialization");
}
void erase(){
for(int j = 0;j<line.length;j++){
line[j].erase();
}
circle.erase();
super.erase();
System.out.println("Erasing CADSystem");
}
}
class ShapeTest{
public static void main(String[] args) {
CADSystem system = new CADSystem(50);
try {
//other code
}finally {
system.erase();
}
//打印如果如下:
//Shape Initialization
//Shape Initialization
//Circle Initialization
//Shape Initialization
//Line Initialization 50 2500
//Shape Initialization
//Line Initialization 50 2500
//CADSystem Initialization
//Erasing Line
//Erasing Shape
//Erasing Line
//Erasing Shape
//Erasing Circle
//Erasing Shape
//Erasing Shape
//Erasing CADSystem
}
}
2、名称屏蔽
如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作。
在Java5中增加了一个@Override注解,它并不是关键字,但是可以把它当作关键字使用,当你想要重写某个方法,而不是重载时,就可以添加些注解。这时,如果你不小心进行了重载,那么编译器将会报错来提醒你。示例如下:
package com.thinkinjava.chapter7;
public class House {
void doSomething(char c){
System.out.println("House doSomething(char) " + c);
}
void doSomething(float f){
System.out.println("House doSomething(float) " + f);
}
}
class BigHouse extends House{
void doSomething(int i){
System.out.println("BigHouse doSomething(int) " + i);
}
/*@Override 使用了此注解只能进行方法的重写,而不能进行重载
void doSomething(double obj){
System.out.println("BigHouse doSomething(double)" + obj);
}*/
}
class HouseTest{
public static void main(String[] args) {
BigHouse bigHouse = new BigHouse();
bigHouse.doSomething('c');
bigHouse.doSomething(1.2f);
bigHouse.doSomething(1);
//打印结果如下:
//House doSomething(char) c
//House doSomething(float) 1.2
//BigHouse doSomething(int) 1
}
}
五、在组合和继承之间的选择
组合和继承都允许在新的类中放置子对象,组合是显式地做,而继承则是隐式地做。
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口,这样就需要在新类中嵌入一个现有类的private对象。
在继承的时候,则是使用某个现有类来开发一个它的特殊版本,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。
因此,继承是一个is-a(是什么)的关系,而组合则是has-a(有什么)的关系。
六、protected关键字
使用关键字protected所修饰的成员,就用户来说,它是private的,但对于继承此类的导出类或者其他位于同一包的类来说,它是可以访问的。示例如下:
package com.thinkinjava.chapter7;
public class Protected {
private String name;
Protected(String name){
this.name = name;
System.out.println(this.name);
}
protected void setName(String name){
this.name = name;
System.out.println(this.name);
}
}
class ProtectedSon extends Protected{
ProtectedSon(String name){
super(name);
}
public void setName(String name){
//放在不同的包也可以访问
setName(name);
}
}
class ProtectedTest{
public static void main(String[] args) {
Protected pro = new Protected("Tom");
pro.setName("Alice");
//打印结果如下:
//Tom
//Alice
}
}
七、向上转型
先看如下示例:
package com.thinkinjava.chapter7;
//乐器类
public class Instrument {
public void play(Instrument instrument){
System.out.println("开始弹奏乐器");
}
}
//钢琴类
class Piano extends Instrument{
private void play(){
System.out.println("开始使用钢琴弹奏乐器");
}
public static void main(String[] args) {
Piano piano = new Piano();
Instrument instrument = new Instrument();
instrument.play(piano);//钢琴是一种乐器,所以能向乐器类发送的消息自然可以向钢琴类发送
//打印结果:开始弹奏乐器
Instrument instrument1 = new Piano();//父类的对象指向子类的实例
instrument1.play(instrument1);//调用的是父类的方法
//打印结果:开始弹奏乐器
}
}
从上例中可以看出来,由于继承可以确保基类中所有的方法在导出类中也同样有效,所以可以向基类发送的所有信息也同样可以向导出类发送。而这种将导出类转换成基类引用的动作,称之为向上转型。
由导出类转换成基类,在继承图中是向上移动的,因此一般称之为向上转型。由于向上转型是由一个较专用类型向较通用类型转换,所以总是很安全的。导出类是基类的一个超集,它可能比基类包含有更多的方法,但它必须具有基类中所含有的方法。
八、final关键字
1、final域
在Java中,final可以用来修饰常量,常量的修饰一般采用如下形式:
public static final int BOOK_NUM = 100;
定义为public则可以用于包之外;static强调每个类只有一份 ;final说明它是一个常量,不能被修改;一个既是static又是final的域占据一段不能改变的存储空间;常量名一般使用大写字母,多个单词使用下划线进行分隔;final域在定义时必须进行初始化。
对于基本数据类型,final使用数值保持恒定不变;而对于引用数据类型,final使用引用恒定不变,但对象其自身却是可以改变的,数组也一样,它是对象。示例如下:
package com.thinkinjava.chapter7;
public class FinalFiled {
private static final Student stu = new Student();
public static void main(String[] args) {
stu.setAge(10);
System.out.println(stu.getAge());
stu.setAge(20);
System.out.println(stu.getAge());
}
}
class Student{
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
还有就是,Java允许在参数列表中将参数指明为final, 这意味着你无法在方法中更改引用所指向的对象,跟上面的示例效果一样的。
2、final方法
使用final方法的原因有两个。
第一,是把方法锁定,以防任何继承类修改它的含义,这是设计时想要确保在继承中使用方法行为保持不变,并且不会被覆盖。
第二,是提高效率。在Java的早期实现中,如果将一个方法指明为final,就是同意编译器针对该方向的所有调用都转为内嵌调用。当编译器发现一个final方法调用命令时,它会根据自己的谨慎判断,跳过插入程序代码这种正常方式而执行方法调用机制(将参数压入栈,跳至方法代码处将执行,然后跳回并清理栈中的参数,处理返回值),并且以方法体中的实际代码的副本来替代方法调用,这将消除方法调用的开销。当然,如果一个方法很大,你的程序代码就会膨胀,因而可能看不到内嵌所带来的任何性能方面的提高,因为所带来的性能提高会因为花费于方法内的时间量而被缩减。在最新的Java版本中,已经不再需要使用final方法进行优化了,我们应该让编译器和JVM处理效率问题,只有在想要明确禁止覆盖时,才将方法置为final方法。
注意:final方法不可重写,但可以重载。
3、final类
如果将某个类修饰为final,那么这个类是不可以被继承的,final类的域可以自己选择为final或者不是final。然而,由于final类禁止继承,所以final类中的所有方法都隐式的指定为final的,因为无法覆盖它们。