• 初识Java 8-1 接口和抽象类


    目录

    抽象类和抽象方法

    接口定义

    默认方法

    多重继承

    接口中的静态方法

    作为接口的Instrument


    本笔记参考自: 《On Java 中文版》


            接口和抽象类提供了一种更加结构化的方式分离接口和实现

    抽象类和抽象方法

            抽象类,其介于普通类和接口之间。在构建具有字段而未实现方法的类时,抽象类是重要且必要的工具。以Instrument类为例:

    1. package music;
    2. public class Instrument {
    3. public void play(Note n) {
    4. }
    5. }

            由这个类可以衍生出很多子类:WindBrass等。在这里,Instrument存在的目的就是为了为它的子类创建一个公共接口。换言之,Instrument建立了一种基本形式,用于抽象出所有子类的共同之处。在这里,Instrument可以被称为抽象基类,简称抽象类

        创建抽象类的目的,就是通过一个公共接口来操作一组的类。因此,Instrument只需要提供一个接口就可以了。

            Java提供了一种抽象方法的机制,这是一个不完整的方法:只有声明,而没有方法体(类似于C++的纯虚函数)。其声明语法如下:

    abstract void f();

            包含抽象方法的类就是抽象类。若一个类包含有一个或以上的抽象方法,则该类就必须被定义为抽象类,否则就会报错。

    1. abstract class Basic {
    2. abstract void unimplemented();
    3. }

            因为抽象类是不完整的,为了防止其被误用,当试图创建一个抽象类的对象时,就会收到报错:

            若想要继承一个抽象类,那么这个新类就必须为基类中的所有抽象方法提供方法定义。否则,认为子类也是抽象的,此时编译器会强制要求使用abstract关键字来限定这个子类:

    1. abstract class Basic2 extends Basic {
    2. int f() {
    3. return 111;
    4. }
    5. abstract void g();
    6. // 仍然没有实现unimplemented()方法
    7. }

            一个抽象类可以不包含任何的抽象方法。这种用法主要用于阻止对于该方法的任何实例化:

    1. abstract class Basic3 {
    2. int f() {
    3. return 111;
    4. }
    5. // 可以不需要抽象方法
    6. }
    7. public class AbstractWithoutAbstracts {
    8. // 实例化依旧会报错:Basic3是抽象类
    9. // Basic3 b3 = new Basic3();
    10. }

            使用一个继承了抽象类的子类,若想要实例化这个子类,就需要为抽象类的所有抽象方法提供定义:

    1. abstract class Uninstantiable {
    2. abstract void f();
    3. abstract void g();
    4. }
    5. public class Instantiable extends Uninstantiable {
    6. @Override
    7. void f() {
    8. System.out.println("f()");
    9. }
    10. @Override
    11. void g() {
    12. System.out.println("g()");
    13. }
    14. public static void main(String[] args) {
    15. Uninstantiable ui = new Instantiable();
    16. }
    17. }

            在上述程序中,即使@Override不标注,只要没有使用相同的方法名称或是方法签名,抽象机制仍然可以知道程序员有没有实现抽象方法。在这里,@Override主要用于提示,表示该方法已经被重写了。

            抽象类对于访问权限没有过多限制,其的默认访问权限就是包访问权限。但是,抽象方法是不允许private的:

    1. abstract class AbstractAccess {
    2. private void m1() {
    3. }
    4. // private abstract void m1a();
    5. protected void m2() {
    6. }
    7. protected abstract void m2a();
    8. void m3() {
    9. }
    10. abstract void m3a();
    11. public void m4() {
    12. }
    13. public abstract void m4a();
    14. }

            不允许private abstract是因为,这种抽象方法无法在子类中得到一个合法的定义

            抽象类不会要求其所有的方法都是抽象的,将需要使用的公共接口声明为abstract即可。依据这些已知的知识,以乐器(Instrument)为例:

    1. package music4;
    2. import music.Note;
    3. abstract class Instrument {
    4. private int i; // 这一变量在每个对象中都会被分配储存
    5. public abstract void play(Note n);
    6. public String what() {
    7. return "Instrument";
    8. }
    9. public abstract void adjust();
    10. }
    11. class Wind extends Instrument {
    12. @Override
    13. public void play(Note n) {
    14. System.out.println("Wind.play(): " + n);
    15. }
    16. @Override
    17. public String what() {
    18. return "Wind";
    19. }
    20. @Override
    21. public void adjust() {
    22. System.out.println("对Wind进行调整");
    23. }
    24. }
    25. class Percussion extends Instrument {
    26. @Override
    27. public void play(Note n) {
    28. System.out.println("Percussion.play(): " + n);
    29. }
    30. @Override
    31. public String what() {
    32. return "Percussion";
    33. }
    34. @Override
    35. public void adjust() {
    36. System.out.println("对Percussion进行调整");
    37. }
    38. }
    39. class Stringed extends Instrument {
    40. @Override
    41. public void play(Note n) {
    42. System.out.println("Stringed.play(): " + n);
    43. }
    44. @Override
    45. public String what() {
    46. return "Stringed";
    47. }
    48. @Override
    49. public void adjust() {
    50. System.out.println("对Stringed进行调整");
    51. }
    52. }
    53. class Brass extends Wind {
    54. @Override
    55. public void play(Note n) {
    56. System.out.println("Brass.play(): " + n);
    57. }
    58. @Override
    59. public void adjust() {
    60. System.out.println("对Brass进行调整");
    61. }
    62. }
    63. class Woodwind extends Wind {
    64. @Override
    65. public void play(Note n) {
    66. System.out.println("Woodwind.play(): " + n);
    67. }
    68. @Override
    69. public String what() {
    70. return "Woodwind";
    71. }
    72. }
    73. public class Music4 {
    74. static void tune(Instrument i) { // 新的类型也可以使用tune()方法
    75. // ...
    76. i.play(Note.MIDDLE_C);
    77. }
    78. static void tuneAll(Instrument[] e) {
    79. for (Instrument i : e)
    80. tune(i);
    81. }
    82. public static void main(String[] args) {
    83. Instrument[] orchestra = {
    84. new Wind(),
    85. new Percussion(),
    86. new Stringed(),
    87. new Brass(),
    88. new Woodwind()
    89. };
    90. tuneAll(orchestra);
    91. }
    92. }

            程序执行的结果如下:

            抽象类和抽象方法明确了类的抽象性,并且告诉用户和编译器自己的预期用途,这种工具也常被用在重构之中。

    接口定义

            接口通过interface进行定义。在Java 8之前,它只被允许使用抽象方法:

    1. public interface PureInterface {
    2. int m1();
    3. void m2();
    4. double m3();
    5. }

    在接口中定义抽象方法不需要使用abstract关键字,因为在接口中不会存在方法体。

            总结一下:在Java 8之前,interface可以创建一个完全抽象的类,不代表任何实现。接口仅负责描述,它确定方法名、方法体和返回类型,但不提供方法体。接口只提供一种形式,而使用了接口的代码会知道可以为接口调用哪些方法。

            Java 8开始,接口中允许默认方法和静态方法。此时接口的基本概念依旧成立,即接口是一个类型的概念,而非实现。

            接口与抽象类的区别:

    • 接口通常暗示“类的类型”或作为形容词使用。
    • 抽象类通常是类层次结构的一部分。

        接口的方法默认是public的,而不是包访问权限。

            接口可以包含字段,这些字段是隐式的staticfinal

            使用implement关键字,可以创建一个符合特定接口(或一组接口)的类。例如:

    1. interface Concept { // 类前没有使用public修饰,是包访问权限
    2. void idea1();
    3. void idea2();
    4. }
    5. class ImplementingAnInterface implements Concept {
    6. @Override
    7. public void idea1() {
    8. System.out.println("idea1");
    9. }
    10. @Override
    11. public void idea2() {
    12. System.out.println("idea2");
    13. }
    14. }

            注意:当实现一个接口时,来自接口的方法必须被定义为public。因为Java编译器不会允许将接口方法的访问权限设为包访问权限,这会降低继承期间方法的可访问性。

    默认方法

            Java 8之后,default关键字有了一个额外的用途:在接口中,default会允许方法创建一个方法体。实现了该接口的类,可以不定义default修饰的方法而直接使用方法。

    1. interface InterfaceWithDefault {
    2. void firstMethod();
    3. void secondMethod();
    4. default void defaultMethod() {
    5. System.out.println("这是一个由default修饰的方法");
    6. }
    7. }

            只要实现上述接口的firstMethod方法和secondMethod方法,就可以使用这个接口了:

    1. // 和InterfaceWithDefault.java处于同一文件夹中
    2. public class Implementation implements InterfaceWithDefault {
    3. @Override
    4. public void firstMethod() {
    5. System.out.println("方法一");
    6. }
    7. @Override
    8. public void secondMethod() {
    9. System.out.println("方法二");
    10. }
    11. public static void main(String[] args) {
    12. InterfaceWithDefault i = new Implementation();
    13. i.firstMethod();
    14. i.secondMethod();
    15. i.defaultMethod();
    16. }
    17. }

            程序执行的结果是:

            之所以添加默认方法,原因之一是:这允许向现有接口中添加方法,而不会破坏已经在使用该接口的所有方法(默认方法也称防御方法或虚拟扩展方法)

        JDK 9中,接口的defaultstatic方法都可以是private的。


    多重继承

            多重继承,即一个类可以从多个基类型继承特性和功能。但Java在严格意义上是一种单继承语言:Java只允许继承一个类(或抽象类)。在默认方法出现之后,Java才拥有了一些多重继承的特性。

            现在,我们可以通过把接口和默认方法结合起来,来结合多个基类型的行为

            但Java只允许结合“行为”,换句话说,接口中不允许存在字段(除非是静态字段)。字段依旧只能来自单个基类或抽象类,所以我们无法获得状态的多重继承。例如:

    1. interface One {
    2. default void first() {
    3. System.out.println("方法One.first()");
    4. }
    5. }
    6. interface Two {
    7. default void second() {
    8. System.out.println("方法Two.second()");
    9. }
    10. }
    11. interface Three {
    12. default void third() {
    13. System.out.println("方法Three.third()");
    14. }
    15. }
    16. class MI implements One, Two, Three { // 结合了多个接口
    17. }
    18. public class MultipleInheritance {
    19. public static void main(String[] args) {
    20. MI mi = new MI();
    21. mi.first();
    22. mi.second();
    23. mi.third();
    24. }
    25. }

            程序执行的结果如下:

            只要所有基类方法都有不同名称和参数列表,就可以组合多个来源。否则,编译器就会报错:

    1. interface Bob1 {
    2. default void bob() {
    3. System.out.println("Bob1::bob");
    4. }
    5. }
    6. interface Bob2 {
    7. default void bob() {
    8. System.out.println("Bob2::bob");
    9. }
    10. }
    11. // class Bob implements Bob1, Bob2 { // 不可以,会发生报错
    12. // }
    13. interface Sam1 {
    14. default void sam() {
    15. System.out.println("Sam1::sma");
    16. }
    17. }
    18. interface Sam2 {
    19. default void sam(int i) {
    20. System.out.println("Sam2::sma = " + i * 2);
    21. }
    22. }
    23. class Sam implements Sam1, Sam2 { // 可以,因为方法的参数列表不同
    24. }
    25. interface Max1 {
    26. default void max() {
    27. System.out.println("Max1::max");
    28. }
    29. }
    30. interface Max2 {
    31. default int max() {
    32. System.out.println("Max2::max");
    33. return 1;
    34. }
    35. }
    36. // class Max implements Max1, Max2 { // 不可以,参数列表不足以区分方法
    37. // }

            编译器会通过方法签名来区分不同的方法,因为方法签名具有唯一性:签名包括名称和参数类型。但是,返回类型不是方法签名的一部分

            如果发生如上注释中的冲突,就需要通过重写冲突的方法来解决问题:

    1. interface Coco1 {
    2. default void coco() {
    3. System.out.println("Coco1::coco");
    4. }
    5. }
    6. interface Coco2 {
    7. default void coco() {
    8. System.out.println("Coco2::coco");
    9. }
    10. }
    11. public class Coco implements Coco1, Coco2 {
    12. @Override
    13. public void coco() { // 重写存在冲突的方法
    14. Coco2.super.coco();
    15. }
    16. public static void main(String[] args) {
    17. new Coco().coco();
    18. }
    19. }

            上述程序最终会输出:Coco2::coco 。在Coco类中进行重写方法时,通过super关键字选择了基类Coco2进行实现。除此之外,也可以通过其他任何可行的方式进行实现。


    接口中的静态方法

            Java 8还允许接口包含静态方法。这种设计使得我们可以将逻辑上属于接口的方法赋予接口本身。通常,会将用来操作接口的方法,以及通用工具放入接口中:

    1. public interface Operation {
    2. void execute();
    3. static void runOps(Operation... ops) { // 用来操作接口
    4. for (Operation op : ops)
    5. op.execute();
    6. }
    7. static void show(String msg) { // 通用方法
    8. System.out.println(msg);
    9. }
    10. }

        其中,runOps()是一个模板方法设计模式的例子。

            借由runOps()这个方法,下面展示的是创建Operation的不同方法:

    1. import operation.Operation;
    2. class Heat implements Operation {
    3. @Override
    4. public void execute() {
    5. Operation.show("Heat");
    6. }
    7. }
    8. public class MetaWork {
    9. public static void main(String[] args) {
    10. Operation twist = new Operation() {
    11. public void execute() { // 在使用前,必须在静态上下文中对方法进行定义
    12. Operation.show("Twist");
    13. }
    14. };
    15. Operation.runOps(
    16. new Heat(), // 【1】:按常规方式创建
    17. new Operation() { // 【2】:匿名类
    18. public void execute() {
    19. Operation.show("Hammer");
    20. }
    21. },
    22. twist::execute, // 使用方法引用
    23. () -> Operation.show("Anneal")); // Lambda表达式,需要最少的代码
    24. }
    25. }

            总结上述程序,可以得出各种创建Operation的不同方式:

    1. 常规类Heat
    2. 匿名类
    3. 方法引用
    4. Lambda表达式,需要最少的代码

    作为接口的Instrument

            使用接口更新关于乐器的Instrument

            接口一经实现,这个实现就会变成一个可以用常规方式扩展的普通类。接口中,任何方法的默认权限都是public的。Instrument中的play()adjust()都使用default关键字定义。

    1. package music5;
    2. import music.Note;
    3. interface Instrument {
    4. int VALUR = 5; // 默认是static并且final的,即编译时常量
    5. default void play(Note n) { // 默认权限是public的
    6. System.out.println(this + ".play()" + n);
    7. }
    8. default void adjust() {
    9. System.out.println("调整:" + this);
    10. }
    11. }
    12. class Wind implements Instrument {
    13. @Override
    14. public String toString() {
    15. return "Wind";
    16. }
    17. }
    18. class Percussion implements Instrument {
    19. @Override
    20. public String toString() {
    21. return "Percussion";
    22. }
    23. }
    24. class Stringed implements Instrument {
    25. @Override
    26. public String toString() {
    27. return "Stringed";
    28. }
    29. }
    30. class Brass extends Wind {
    31. @Override
    32. public String toString() {
    33. return "Brass";
    34. }
    35. }
    36. class Woodwind extends Wind {
    37. @Override
    38. public String toString() {
    39. return "Woodwind";
    40. }
    41. }
    42. public class Music5 {
    43. static void tune(Instrument i) {
    44. // ...
    45. i.play(Note.MIDDLE_C);
    46. }
    47. static void tuneAll(Instrument[] e) {
    48. for (Instrument i : e)
    49. tune(i);
    50. }
    51. public static void main(String[] args) {
    52. Instrument[] orchestra = {
    53. new Wind(),
    54. new Percussion(),
    55. new Stringed(),
    56. new Brass(),
    57. new Woodwind()
    58. };
    59. tuneAll(orchestra);
    60. }
    61. }

            程序执行的结果是:

            上述程序中,使用根类Object的方法toString()替代了what()方法。

        无论是向上转型为常规类、抽象类或是接口,tune()方法的行为都是一样的。实际上tune()也无从得知Instrument到底是什么类。

  • 相关阅读:
    go语言实现简单认证样例
    Windows如何设置Tomcat开机启动?
    spring 获取ioc容器,从容器中获取bean
    如何使用Goland进行远程Go项目线上调试?
    【云原生】Kubeadmin部署Kubernetes集群
    Spring SpEL表达式语言
    CopyOnWriteArrayList源码分析
    设计模式之代理模式
    深度学习-07-反向传播的自动化
    Maven仓库地址(寻找Maven依赖)
  • 原文地址:https://blog.csdn.net/w_pab/article/details/132817744