Java面向对象进阶篇(抽象类和接口)

时间:2022-06-01 21:28:20

一.抽象类

在某些情况下,父类知道其子类应该包含哪些方法,但是无法确定这些子类如何实现这些方法。这种有方法签名但是没有具体实现细节的方法就是抽象方法。有抽象方法的类只能被定义成抽象类,抽象方法和抽象类必须使用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);
}
}