Java面向对象进阶篇(抽象类和接口)
一.抽象类
在某些情况下,父类知道其子类应该包含哪些方法,但是无法确定这些子类如何实现这些方法。这种有方法签名但是没有具体实现细节的方法就是抽象方法。有抽象方法的类只能被定义成抽象类,抽象方法和抽象类必须使用abstract修饰。抽象类里可以没有抽象方法。
1.1 抽象类和抽象方法
抽象类和抽象方法的规则如下:
1.抽象类和抽象方法都必须使用abstract修饰符修饰,抽象方法不能有方法体
2.抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使这个抽象类里不包含抽象方法。
3.抽象类可以包含成员变量,方法(普通方法、抽象方法),构造器,初始化块,内部类(接口,枚举类)5中成分。抽象类的构造器不能用于创建实例,主要是用于被子类调用。
4.含有抽象方法的类(直接定义了一个抽象方法;继承了一个父类,没有完全实现父类包含的抽象方法;或实现一个接口,但没有完全实现接口包含的抽象方法)只能被定义成抽象类。
定义抽象方法只需在普通方法上加上abstract修饰符,并把普通方法的方法体全部去掉,并在方法后增加分号即可
定义抽象类只需在普通类上增加abstract修饰符即可。
抽象类不能用于创建实例,只能当作父类被其他子类继承。
package com.company2; abstract class Shape{ private String color; //定义一个计算周长的方法 public abstract double calPerimeter(); //定义一个返回形状的方法 public abstract String getType(); public Shape(String color) { this.color = color; } public Shape() { } public String getColor() { return color; } public void setColor(String color) { this.color = color; } } public class Triangle extends Shape{ private double a; private double b; private double c; public Triangle(String color, double a, double b, double c) { super(color); setSides(a,b,c); } public void setSides(double a, double b, double c) { if(a+b > c || a+c > b || b+c > a){ this.a = a; this.b = b; this.c = c; } System.out.println("三角形两边之和必须大于第三边"); } @Override public double calPerimeter() { return a+b+c; } @Override public String getType() { return "三角形"; } }
当使用abstract修饰类时,表明这个类只能被继承;当使用abstract修饰方法时,表明这个方法必须有子类提供实现(重写)。而final修饰的方法不能被重写,final修饰的类不能被继承。因此final
和abstract永远不能同时使用。static和abstract不能同时修饰某个方法,没有类抽象方法的说法,但可以同时修饰内部类。abstract也不能修饰变量和构造器。
利用抽象类和抽象方法的优势,可以更好地发挥多态的优势,使得程序更加灵活。
1.2 抽象类的作用
从语义的角度来看,抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象,它体现的是一种模板模式的设计。抽象类作为多个子类的通用模板,子类在抽象类的基础上进行
拓展,改造,避免了子类设计的随意性。
抽象类的普通方法可依赖与抽象方法,抽象方法则推迟到子类中提供实现
package com.company2; abstract class Speedmeter { private double turnRate;//转速 public Speedmeter() { } //把返回车轮半径的方法定义成抽象方法 public abstract double getRadius(); public void setTurnRate(double turnRate){ this.turnRate = turnRate; } public double getSpeed() { return Math.PI*2*getRadius()*turnRate; } } public class CarSpeedmeter extends Speedmeter{ @Override public double getRadius() { return 0.28; } public static void main(String[] args) { CarSpeedmeter csm = new CarSpeedmeter(); csm.setTurnRate(15); System.out.println(csm.getSpeed()); } }
模板模式在面向对象的软件中很常用,其原理简单,实现也简单。下面是使用模板模式的一些简单规则:
1.抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类实现。
2.父类中可能包含需要调用其他系列方法的方法。这些被调用方法既可以由父类实现,也可以由子类实现。父类提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,必须
依赖于其子类的帮助。
二. Java 8改进的接口(interface)
2.1 接口的概念
接口是多个相似类中抽象出来的一组公共行为规范,接口不提供任何实现,它体现的是规范和实现分离的哲学。
规范和实现分离正是接口的好处,它让软件系统各组件之间面向接口耦合,可以为软件系统提供很好的松耦合设计,从而降低个模块间的耦合,为系统提供更好的可拓展性和可维护性。
接口可以有多个直接父接口,但接口只能继承接口,不能继承类
2.2 接口的定义规则
1.由于接口定义的是一种规范,用interface跟class区分,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量),方法(只能是抽象实例方法,类方法或
默认方法), 内部类(内部接口,枚举)定义。
2.因为接口定义的是多个类共同的公共行为规范。因此接口的所有成员(包括常量,方法,内部类)都是public访问权限。只能指定public修饰符,也可以省略。
3.接口定义的成员变量只能在定义时指定默认值,默认使用public static final修饰符修饰。可以省略不写。
4.接口定义的方法只能是抽象方法,类方法和默认方法。类方法和默认方法都必须有方法实现(方法体)。
下面定义一个接口
package com.company2; public interface Output { //定义的成员变量只能是常量,默认public static final修饰 int MAX_CACHE_LINE = 50; //接口里定义的普通方法只能是public的抽象方法,没有方法体 void out(); void getData(String msg); //接口里定义的默认方法,需要使用default修饰,默认public修饰,有方法体 default void print(String... msgs) { for(String msg:msgs) { System.out.println(msg); } } default void test() { System.out.println("默认的test()方法"); } //接口里定义的类方法,需要使用static修饰,默认public修饰,有方法体 static String staticTest() { return "接口里的类方法"; } }
不同包下的另一个类访问接口里的成员变量。如下
package com.company; import com.company2.Output; public class OutputFIeldTest { public static void main(String[] args) { System.out.println(Output.MAX_CACHE_LINE); System.out.println(Output.staticTest()); } }
2.3 接口的继承
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口拓展某个父接口,将会获得父接口里定义的所有抽象方法、常量
一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以逗号隔开。
2.4 使用接口
接口的主要用途
1.定义变量,也可用于强制类型转换。接口声明引用类型变量
2.调用接口中定义的常量
3.被其他类实现
类可以使用implements关键字实现一个或多个接口,这也是Java为单继承灵活性不足所做的补充。实现接口与继承父类相似,一样可以获得所实现接口里定义的常量,方法。
一个类可以继承一个父类,并同时实现多个接口,implements部分必须在extends部分之后。
一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则该类将保留从父接口那里继承得到的抽象方法,该类也必须
定义成抽象类。
一个类实现某个接口时,该类将会获得接口中定义的常量,方法等。因此可以把实现接口理解为一种特殊的继承,相当于实现类完全继承了一个彻底抽象的类。
接口不能显式继承任何类,但所有接口类型的引用变量都可以直接赋给Object类型的引用变量。
三. 接口和抽象类
3.1 接口与抽象类的相似特征
1.接口和抽象类都不能被实例化,用于被其他类实现和继承
2.接口和抽象类都包含抽象方法,实现接口和继承抽象类的普通子类都必须实现这些抽象方法
3.2 接口与抽象类的设计目的差别
接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式);对于接口的调用者而言,接口规定了调用者
可以调用哪些服务,以及如何调用这些服务(就是如何调用方法)。当一个程序中使用接口时,接口是多个模块间的耦合标准;在多个应用程序之间使用接口时,接口是多个程序之间的通讯标准。
从某种程度上来看,接口类似于整个系统的总纲,它制定了各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一但接口发生改变,其影响是辐射性的,导致系统中的大部分都需
要重写。
抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那
些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有进一步的改善。
3.3 接口与抽象类的用法差别
1.接口里只能包含抽象方法,静态方法,默认方法,不能为普通方法提供方法实现。抽象类则可以完全包含普通方法。
2.接口里只能定义静态常量,不能定义普通成员变量。抽象类里即可以定义静态常量,也可以定义普通成员变量。
3.接口里不包含构造器。抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
4.接口里不能包含初始化块。抽象类则可以完全包含初始化块
5.一个类最多只能有一个直接父类,包括抽象类。但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。
四. 面向接口编程
很多软件架构设计理论都倡导面向接口编程来降低程序的耦合。下面介绍两种常用的场景来示范接口编程的优势
4.1 简单工厂模式
假设程序中有个Computer类需要组合一个输出设备。有两种选择,让Computer类组合一个Printer对象,或者让Computer类组合一个Output。
假设让Computer类组合一个Printer对象,假如有一天系统需要重构,需要BetterPrinter来代替Printer,这就需要打开Computer类源代码进行修改,假如系统中有100个类组合了Printer,甚至1000个、
10000个...,这是多么大的工作量啊
为了避免这个问题,工厂模式建议让Computer类组合一个Output类型的对象,将Computer类和Printer类完全分离。Computer对象实际组合的是Printer对象还是BetterPrinter对象,对Computer而言
完全透明。当Printer对象切换到BetterPrinter对象时,系统完全不受影响。
package com.company2; public interface Output { //定义的成员变量只能是常量,默认public static final修饰 int MAX_CACHE_LINE = 50; //接口里定义的普通方法只能是public的抽象方法,没有方法体 void out(); void getData(String msg); //接口里定义的默认方法,需要使用default修饰,默认public修饰,有方法体 default void print(String... msgs) { for(String msg:msgs) { System.out.println(msg); } } default void test() { System.out.println("默认的test()方法"); } //接口里定义的类方法,需要使用static修饰,默认public修饰,有方法体 static String staticTest() { return "接口里的类方法"; } }
Output接口定义了一系列打印行为
package com.company2; interface Product { int getProduceTime(); } public class Printer implements Output,Product { private String[] printData = new String[MAX_CACHE_LINE]; //用以记录当前需打印的作业数 private int dataNum = 0; @Override public void out() { //只要还有作业就继续打印 while (dataNum > 0) { System.out.println("告诉打印机正在打印" + printData[0]); //把作业队列整体前移一位,并将剩下的作业数减1 System.arraycopy(printData, 1, printData, 0, --dataNum); } } @Override public void getData(String msg) { if(dataNum >= MAX_CACHE_LINE) { System.out.println("输出队列已满,添加失败"); } else{ //把打印数据添加到队列里,已保存数据的数量加1 printData[dataNum++] = msg; } } @Override public int getProduceTime() { return 45; } }
Printer实现了Output接口
package com.company2; public class Computer { private Output out; public Computer(Output out) { this.out = out; } public void keyIn(String msg) { out.getData(msg); } public void print() { out.out(); } }
Computer类完全与Printer类分离,只是与Output接口耦合。Computer不再负责创建Outputer对象,系统提供一个Ouputer工厂来负责生产Output对象。
package com.company2; public class OutputFactory { public Output getOutput() { return new Printer(); } public static void main(String[] args) { OutputFactory of = new OutputFactory(); Computer c = new Computer(of.getOutput()); c.keyIn("轻量级Java EE企业应用实战"); c.keyIn("疯狂Java讲义"); c.print(); } }
在该OutputFactory类中包含了一个getOutput()方法,返回Output实现类的实例,该方法负责创建Output实例
package com.company2; public class BetterPrinter implements Output { private String[] printData = new String[MAX_CACHE_LINE*2]; private int dataNum = 0; @Override public void out() { while(dataNum>0) { System.out.println("告诉打印机正在打印"+printData[0]); System.arraycopy(printData,1,printData,0,--dataNum); } } @Override public void getData(String msg) { if(dataNum >= MAX_CACHE_LINE * 2) { System.out.println("输出队列已满,添加失败"); } else{ printData[dataNum++] = msg; } } }
BetterPrinter类也实现了Output接口,因此也可当成Output对象使用,只要把OutputFactory工厂类的getOutput()方法中的下划线部分改为如下代码
return new BetterPrinter();
4.2 命令模式
假设一个方法需要遍历某个数组的数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。
对于这样的一个需求,必须把处理行为作为参数传入该方法,这个“处理行为”用编程来实现就是一段代码。
可以考虑使用一个Command接口定义一个方法,用这个方法来封装“处理行为”,但这个方法没有方法体,因为现在还无法确定这个处理行为
public interface Command { void process(int[] target); }
需要创建一个数组的处理类,在这个处理类包含一个process方法,这个方法还无法确定处理数组的处理行为,所以定义该方法时使用了一个Command参数,这个Command参数负责
对数组的处理行为
public class ProcessArray { public void process(int[] target,Command cmd) { cmd.process(target); } }
通过Command接口,就实现了让ProcessArray类和具体“处理行为”的分离,程序使用Command接口代表了对数组的处理行为。Command接口也没提供真正的处理,只有等到需要
调用ProcessArray对象的process()方法时,才真正传入一个Command对象,才确定对数组的处理行为。
下面代码示范了对数组的两种处理方式
public class CommandTest { public static void main(String[] args) { ProcessArray pa = new ProcessArray(); int[] target = [3,-4,6,4]; //第一次处理数组,具体处理行为取决于PrintCommand pa.process(target,new PrintCommand()); //第二次处理数组,具体处理行为取决于AddCommand pa.process(target,new AddCommand()); } }
两次不同的处理行为通过PrintCommand类和AddCommand类提供
package com.company2; public class PrintCommand implements Command{ public void process(int[] target) { for(int tmp:target) { System.out.println("迭代输出数组的元素"+tmp); } } }
package com.company2; public class AddCommand implements Command{ public void process(int[] target) { int sum = 0; for(int tmp:target) { sum += tmp; } System.out.println("数组的元素总和为"+sum); } }