Java抽象类
参考资料
[1]. 疯狂Java讲义(第三版) 李刚
抽象类
抽象方法和抽象类
抽象类可以继承非抽象类
基本规则
// 抽象类必须使用abstract修饰符来定义,即成为抽象类
// 这个类里面可以没有抽象方法
public abstract class Shape {
// 抽象方法必须使用abstract修饰符来修饰,且不能有方法体
public abstract void draw(Canvas c);
}
// 只要使用了abstract修饰符,这个类就无法被实例化
// 基本成员与普通类一样,但主要用于被其子类实现
下面定义一个Shape抽象类,用于被其他类继承:
/**
* AbstractShape class
*
* @author shuaige
* @date 2018/01/22
*/
public abstract class AbstractShape {
/**
* 初始化块
*/
{
System.out.println("执行Shape的初始块");
}
/**
* color 颜色
*/
private String color;
/**
* 定义一个计算周长的抽象方法
*
* @return 周长
*/
public abstract double calPerimeter();
/**
* 定义一个返回形状的抽象方法
* @return 返回形状
*/
public abstract String getType();
/**
* 定义Shape的构造器,该构造器并不是用于创建Shape对象,而是被子类调用
*/
public AbstractShape(){}
/**
* 构造器
* @param color 颜色
*/
public AbstractShape(String color)
{
System.out.println("执行Shape的构造器...");
this.color = color;
}
/**
* 返回颜色
* @return 颜色
*/
public String getColor()
{
return color;
}
/**
* 设置颜色
* @param color 颜色
*/
public void setColor(String color)
{
this.color = color;
}
}
声明一个Triangle子类,用于实现上面的父类:
/**
* Triangle class
*
* @author shuaige
* @date 2018/01/22
*/
public class Triangle extends AbstractShape {
/**
* 三角形的a边
*/
private double a;
/**
* 三角形的b边
*/
private double b;
/**
* 三角形的c边
*/
private double c;
/**
* 构造器
* @param color 颜色
* @param a 三角形的a边
* @param b 三角形的b边
* @param c 三角形的c边
*/
public Triangle(String color, double a, double b, double c)
{
super(color);
this.setSides(a, b, c);
}
/**
* 设置边
* @param a 三角形的a边
* @param b 三角形的b边
* @param c 三角形的c边
*/
public void setSides(double a, double b, double c)
{
if (a >= b+c || b >= a+c || c >= a+b)
{
System.out.println("三角形的两边之和必须大于第三边");
return;
}
// 如果符合三角形的规则,则设置下面的值
this.a = a;
this.b = b;
this.c = c;
}
/**
* 重写Shape类的计算周长的抽象方法
* @return 返回周长
*/
@Override
public double calPerimeter() {
return a + b + c;
}
/**
* 重写Shape类的返回形状的抽象方法
* @return 返回形状
*/
@Override
public String getType() {
return "三角形";
}
}
声明一个Circle类,用于实现上面的父类:
/**
* Circle class
*
* @author shuaige
* @date 2018/01/22
*/
public class Circle extends AbstractShape {
/**
* 半径
*/
private double radius;
/**
* 构造器
* @param color 颜色
* @param radius 半径
*/
public Circle(String color, double radius)
{
super(color);
this.radius = radius;
}
/**
* 设置半径
* @param radius 半径
*/
public void setRadius(double radius)
{
this.radius = radius;
}
/**
* 计算周长
* @return 返回周长
*/
@Override
public double calPerimeter() {
return 2 * Math.PI * radius;
}
/**
* 返回形状
* @return 形状
*/
@Override
public String getType() {
return getColor() + "圆形";
}
/**
* 实例化过程
* @param args
*/
public static void main(String[] args)
{
// 三角形
AbstractShape shape1 = new Triangle("黑色", 3, 4, 5);
System.out.println(shape1.getType());
System.out.println(shape1.calPerimeter());
// 圆形
AbstractShape shape2 = new Circle("黄色", 3);
System.out.println(shape2.getType());
System.out.println(shape2.calPerimeter());
}
}
上面的main()方法中定义了两个Shape类型的引用变量,它们分别指向Triangle对象和Circle对象。由于在Shape类中定义了calPerimeter()方法和getType()方法,无须强制类型转换为其子类型。
利用抽象类和抽象方法的优势,可以更好的发挥多态的优势,使程序更加灵活。
abstract不能用于修饰成员变量,不能用于修饰局部变量,即没有抽象变量、抽象成员变量的说法;abstract也不能用于修饰构造器,没有抽象构造器,抽象类里定义的构造器只能是普通的构造器。
抽象类的作用
抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。
父类:
/**
* CarSpeedMeter class
*
* @author shuaige
* @date 2018/01/22
*/
public abstract class AbstractSpeedMeter {
/**
* trunRate 转速
*/
private double trunRate;
public AbstractSpeedMeter()
{
}
/**
* 把返回车轮半径的方法定义成抽象方法
*
* @return null
*/
public abstract double getRadius();
public void setTrunRate(double trunRate)
{
this.trunRate = trunRate;
}
/**
* 定义计算速度的通用算法
*
* @return 速度等于 车轮半径 * 2 * PI * 转速
*/
public double getSpeed()
{
return Math.PI * 2 * getRadius() * trunRate;
}
}
实现了父类的子类:
package com.qunar;
/**
* CarSpeedMeter class
*
* @author shuaige
* @date 2018/01/22
*/
public class CarSpeedMeter extends AbstractSpeedMeter{
@Override
public double getRadius() {
return 0.28;
}
public static void main(String[] args)
{
CarSpeedMeter csm = new CarSpeedMeter();
csm.setTrunRate(15);
System.out.println(csm.getSpeed());
}
}
抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现。
父类中可能包含需要调用其他系列方法的方法,这些被调用方法既可以由父类实现,也可以由其子类实现,父类里提供的方法只是定义了一个通用的算法,其实现也许并不完全由自身实现,而必须依赖于其子类的辅助。
Java 8 改进的接口
接口(interface),接口里不能包含普通方法,接口里的所有方法都是抽象方法,。Java 8对接口进行了改进,运行在接口中定义默认方法,默认方法可以提供方法实现。
Java 8中接口的定义
基本语法如下:
[修饰符] interface 接口名 extends 父接口1, 父接口2...
{
零到多个常量定义...
零到多个抽象方法定义...
零到多个内部类、接口、枚举定义...
零到多个默认方法或类方法定义...
}
语法说明如下:
1. 修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包权限访问控制符,即只有在相同包结构下才可以访问该接口。
2. 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
不管是否使用public static final修饰符,接口里的成员变量总是使用这三个修饰符来修饰。接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值。
下面两个变量效果一样。
int MAX_SIZE = 50;
public static final int MAX_SIZE = 50;
系统自动为普通方法增加public abstract修饰符,因此下面两个方法效果一样。
int size();
public abstract int size();
默认方法必须使用default修饰,并且无论是否指定,系统总会默认使用public修饰。
类方法必须使用static方法修饰,并且无论是否指定,系统总会默认使用public修饰。
下面定义一个接口示例:
/**
* 示范接口 Output
*
* @author shuaige
* @date 2018/01/23
*/
public interface Output {
/**
* 接口里定义的成员变量只能是常量
* 这个变量是一个类变量,因为默认修饰为public static final
*/
int MAX_CACHE_LINE = 50;
/**
* 接口里定义的普通方法只能是public的抽象方法
*/
void out();
/**
* 接口里定义的普通方法只能是public的抽象方法
* @param msg 字符串参数
*/
void getData(String msg);
/**
* 在接口中定义默认方法,需要使用default修饰
* @param msgs 字符串参数
*/
default void print(String... msgs)
{
for (String msg : msgs)
{
System.out.println(msg);
}
}
/**
* 在接口中定义默认方法,需要使用default修饰
*/
default void test()
{
System.out.println("默认的test()方法");
}
/**
* 接口中定义的类方法,需要使用static修饰
* @return
*/
static String staticTest()
{
return "接口里的类方法";
}
}
调用
// 访问接口里的常量
System.out.println(Output.MAX_CACHE_LINE);
// 下面语句将引发错误
// Cannot assign a value to final variable'MAX_CACHE_LINE'
// Output.MAX_CACHE_LINE = 100;
// 调用默认类
System.out.println(Output.staticTest());
接口的继承
接口支持多继承,子接口扩展某个父接口后,将会获得父接口里定义的所有抽象方法和变量。
interfaceA 接口
/**
* interfaceA interface
*
* @author shuaige
* @date 2018/01/23
*/
public interface InterfaceA {
/**
* PROP_A常量
*/
int PROP_A = 5;
/**
* testA方法
*/
void testA();
}
interfaceB接口
/**
* interfaceB interface
*
* @author shuaige
* @date 2018/01/23
*/
public interface InterfaceB {
/**
* PROP_B常量
*/
int PROP_B = 6;
/**
* testB方法
*/
void testB();
}
interfaceC接口
/**
* interfaceC interface
*
* @author shuaige
* @date 2018/01/23
*/
public interface InterfaceC extends InterfaceA, InterfaceB{
/**
* PROP_C常量
*/
int PROP_C = 6;
/**
* testC方法
*/
void testC();
}
现在来使用interfaceC来调用A和B的接口内的常量
System.out.println(InterfaceC.PROP_A);
System.out.println(InterfaceC.PROP_B);
System.out.println(InterfaceC.PROP_C);
使用接口
接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现。接口的用途归纳如下:
1. 定义引用型变量,也可用于进行强制类型转换。
2. 调用接口中定义的常量。
3. 被其他类实现。
类实现接口的语法格式如下:
[修饰符] class 类名 extends 父类 implements 接口1, 接口2...
{
类体部分
}
下面进行一个实例
接口Product
/**
* Product interface
*
* @author shuaige
* @date 2018/01/23
*/
public interface Product {
/**
* 返回时间
* @return 返回整数
*/
int getProduceTime();
}
当子类实现了接口以后,还可以引用变量给父类,这样就可以使多个实现的子类之间兼容。
/**
* Printer interface
* 实现了Output和Product接口
* @author shuaige
* @date 2018/01/23
*/
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);
}
}
/**
*
* @param msg 字符串参数
*/
@Override
public void getData(String msg) {
if (dataNum >= MAX_CACHE_LINE)
{
System.out.println("输出队列已满,添加失败");
}else{
// 把打印数据添加到队列里,已保存数据的数量加1
printData[dataNum++] = msg;
}
}
/**
* 返回整数
* @return
*/
@Override
public int getProduceTime() {
return 45;
}
public static void main(String[] args)
{
// 创建一个Printer对象,当成Output使用
Output o = new Printer();
o.getData("a");
o.getData("b");
o.out();
o.getData("c");
o.getData("d");
o.out();
// 调用Output接口中定义的默认方法
o.print("1", "2", "3");
o.test();
// 创建一个Printer对象,当成Product使用
Product p = new Printer();
System.out.println(p.getProduceTime());
// 所有接口类型的引用变量都可直接赋给Object类型的变量
Object obj = p;
}
}
从上面程序中可以看出,Printer类实现了Output接口和Product接口,因此Printer对象既可直接赋给Output变量,也可直接赋给Product变量。仿佛Printer类既是Object类的子类,也是Product类的子类,这就是Java提供的模拟多继承。
面向接口编程
接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好的降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。
简单工厂模式
实现类
/**
* Computer class
* 接受注入的Output类型class,然后调用它的方法
* @author shuaige
* @date 2018/01/23
*/
public class Computer {
/**
* Output接口
*/
private Output out;
/**
*
* @param out Output接口
*/
public Computer(Output out)
{
this.out = out;
}
/**
* 定义一个模拟获取字符串输入的方法
* @param msg
*/
public void keyIn(String msg)
{
out.getData(msg);
}
/**
* 定义一个模拟打印的方法
*/
public void print()
{
out.out();
}
}
工厂类
/**
* OutputFactory class
* 工厂类,获取Output类型的类
* @author shuaige
* @date 2018/01/23
*/
public class OutputFactory {
/**
* 返回一个Output接口的实现类
* @return Output接口的实现类
*/
public Output getOutput()
{
return new Printer();
}
public static void main(String[] args)
{
// 工厂类,获取Output类型的类
OutputFactory of = new OutputFactory();
// 注入到实现类,供实现类调用
Computer c = new Computer(of.getOutput());
// 调用keyIn方法
c.keyIn("a");
// 调用print方法
c.print();
}
}
类之间的关联关系图如下:
工厂类组合输出类,计算机类又依赖工厂类。
假设现在Printer
类废弃了,现在新增了一个BetterPrinter
实现类,只需要把OutputFactory
里面的getOutput()
方法即可。
/**
* OutputFactory class
* 工厂类,获取Output类型的类
* @author shuaige
* @date 2018/01/23
*/
public class OutputFactory {
/**
* 返回一个Output接口的实现类
* @return Output接口的实现类
*/
public Output getOutput()
{
return new BetterPrinter();
}
...
}
BetterPrinter
实现类也需要实现Output
接口,代码如下:
package com.qunar;
/**
* BetterPrinter class
* 实现了Output和Product接口
* @author shuaige
* @date 2018/01/23
*/
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);
}
}
/**
* 获取数据
* @param msg 字符串参数
*/
@Override
public void getData(String msg) {
if (dataNum >= MAX_CACHE_LINE * 2)
{
System.out.println("输出队列已满,添加失败");
}else {
// 把打印数据添加到队列中,已保存数据的数量加1
printData[dataNum++] = msg;
}
}
}
现在类图结构如下:
命令模式
假设某个方法需要完成某一个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定,示例代码如下:
接口Commond :
/**
* Commond interface
*
* @author shuaige
* @date 2018/01/23
*/
public interface Commond {
/**
* 接口里定义的process方法用于封装“处理行为”
* @param target
*/
void process(int[] target);
}
实现类PrintCommand:
/**
* PrintCommand class
*
* @author shuaige
* @date 2018/01/23
*/
public class PrintCommand implements Commond{
/**
* 处理数组
* @param target
*/
@Override
public void process(int[] target)
{
for (int tmp : target)
{
System.out.println("迭代输出目标数组的元素:" + tmp);
}
}
}
实现类AddCommand:
/**
* AddCommand class
*
* @author shuaige
* @date 2018/01/23
*/
public class AddCommand implements Commond{
/**
* 处理数组
* @param target
*/
@Override
public void process(int[] target)
{
// 设置初始值
int sum = 0;
for (int tmp : target)
{
// 遍历相加所有元素
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
}
}
组合类:
/**
* ProcessArray class
*
* @author shuaige
* @date 2018/01/23
*/
public class ProcessArray {
/**
* 组合Commond实现和传入的int[]参数
* @param target
* @param cmd
*/
public void process(int[] target, Commond cmd)
{
cmd.process(target);
}
}
调用示例:
/**
* CommandTest class
*
* @author shuaige
* @date 2018/01/23
*/
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());
System.out.println("---------");
// 第一次处理数组,具体行为取决于AddCommand
pa.process(target, new AddCommand());
}
}
Commond是接口,然后PrintCommand类和AddCommand类都实现了它的process方法,最后由ProcessArray类组合PrintCommand类或者AddCommand类和数组就可以实现命令模式,即传入的是哪个处理类,就执行哪个处理类的逻辑。
类图如下: