美文网首页
JavaSE 基础学习之三 ——Java 的继承与接口

JavaSE 基础学习之三 ——Java 的继承与接口

作者: 琦小虾 | 来源:发表于2018-05-26 07:26 被阅读0次

    接上文《JavaSE 基础学习之二 —— Java 的部分基本语法》


    三. Java 的继承与接口

    1. java 中的继承

    继承是 java 面向对象编程技术的一块基石,因为它允许创建分等级层次的类。
    继承就是<font color=red>子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法</font>,或子类从父类继承方法,使得子类具有父类相同的行为。
    ——摘自《Java 继承 | 菜鸟教程》

    继承使用的关键字是 extend,格式为:

    class 子类 extends 父类 {
    }
    

    用圆形为例,举例如下:

    public class Circle extends Shape {
        // ...
    }
    

    继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。此外,继承的代码复用体现了一种 is-a 关系:比如PC 机是计算机,工作站也是计算机。PC 机和工作站是两种不同类型的计算机,但都继承了计算机的共同特性。因此在用 Java 语言实现时,应该将 PC 机和工作站定义成两种类,均继承计算机类。

    Java 中除了构造函数之外,子类可以继承父类所有函数。
    关于子类的构造函数,其实子类是可以通过 super() 方法访问到父类的构造函数的。子类的无参构造函数,默认调用父类无参数的构造函数。如果要显式的调用构造函数,需要使用 super 关键字,而且要把 super() 放在子类构造函数的第一句,就可以在子类中调用父类的构造函数了。大致如下所示:

    public Circle extends Shape {
        public Shape() {
            // 调用父类构造函数
            super();
            //...其他初始化方法....
        }
    }
    

    注:关于 super 关键字:

    1. super 关键字也有两种意义:调用父类的方法,或是调用父类的构造器。但是,super并不表示一个指向对象的引用,它只是一个特殊的关键字,用来告诉编译器,现在要调用的是父类的方法。
    2. 理论上,子类一定会调用父类相应的构造函数,只是使用了 super 关键字是显式的调用而已,而且通常情况下 super 关键字的调用时都被省略了;

    2. 动态绑定 (Dynamic Binding)

    程序绑定指的是一个方法的调用与方法所在的类关联起来。对 Java 来说,绑定分为<font color=red>静态绑定</font>和<font color=red>动态绑定</font>(或者叫做前期绑定和后期绑定)。

    静态绑定是指在程序执行前方法已经被绑定,也就是说在编译过程中,就已经知道该方法是属于哪个类中的方法。此时由编译器或其它连接程序实现。针对 Java,可以简单理解为程序编译期的绑定。这里特别说明一点,Java 当中的方法只有 <font color=red>final, static, private 和构造方法</font>是静态绑定。(具体分析见参考网址)

    动态绑定即后期绑定,指在运行时根据具体对象的类型进行绑定。如果一种语言实现了后期绑定(如 Java, C++),同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,在运行时编译器依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的,但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

    动态绑定的典型,就是父类的引用可以引用任何子类的实例。比如有如下父类子类关系,介绍说明动态绑定的具体过程:

    Parent p = new Children();
    
    1. 编译器检查对象的声明类型和方法名;
      • 假如我们有 Children 的实例对象 child,此时想要调用 Children 的 fun(args) 方法,那么编译器就会列举出所有名称为 fun 的方法(所有方法签名相同,参数列表不同的 fun 方法),并列举 Children 的超类 Parent 中 fun 方法;
    2. 编译器检查方法调用中提供的参数类型;
      • 如果所有签名为 fun 的方法中,有一个参数类型和调用时提供的参数类型最匹配,那么就调用该方法。该过程成为<font color=red>重载解析</font>
    3. 当程序运行并使用动态绑定调用方法时,虚拟机必须调用与 child 指向的对象的实际类型相匹配的方法版本。调用方法的时候,如果当前子类已经对父类实现了方法的重写,则调用子类重写后的方法;否则只调用父类的方法。即如果子类 Children 中如果实现了对应的 fun(args) 方法,则调用 Children 的方法,否则就在父类 Parent 中寻找;在 Parent 中找不到,则在 Parent 的父类中找,直到最顶层的父类。

    JVM 调用一个类方法时(即标注 static 的静态方法),它会基于对象引用的类型来选择所调用的方法,通常在编译时 JVM 就知道了要调用什么方法,这就是静态绑定。相反,如果 JVM 调用一个实例对象方法时,它会基于对象实际的类型来选择所调用的方法,具体调用什么方法只能在运行时得知。这就是动态绑定,是多态的一种。动态绑定为解决实际的业务问题提供了很大的灵活性,是一种非常优美的机制。

    参考网址:《Java静态绑定与动态绑定》

    3. 类的初始化顺序

    创建一个实例对象时,考虑到该对象的父子关系,JVM 按照一定的顺序进行初始化:

    1. 先父类静态,再子类静态
    2. 父类的定义初始化 + 构造函数
    3. 子类定义初始化 + 构造函数

    以例程来说明初始化顺序:

    package oop4;
    
    public class Test2 {
        public static void main(String[] args) {
            D d = new D();
        }
    }
    class C{
        // C 的定义初始化
        {System.out.println("aa..");}
        // C 的静态初始化
        static{
            System.out.println("bb..");
        }
        // C 的构造函数
        C(){System.out.println("cc..");}
    }
    class D extends C{
        // D 的定义初始化
        {System.out.println("dd...");}
        // D 的静态初始化
        static{
            System.out.println("ee..");
        }
        // D 的构造函数
        D(){
            System.out.println("ff...");
        }
    }
    

    分析该段程序,先后顺序应该如下:

    1. 父类 C 的静态初始化:bb..
    2. 子类 D 的静态初始化:ee..
    3. 父类 C 的定义初始化:aa..
    4. 父类 C 的构造函数:cc..
    5. 子类 D 的定义初始化:dd...
    6. 子类 D 的构造函数:ff...

    综上所述,该段程序输出的结果:

    bb..
    ee..
    aa..
    cc..
    dd...
    ff...

    4. Java 的单继承

    Java 中的继承只能是<font color=red>单一继承</font>,即 extends 关键字只能有一个类名;但 java 的继承具有传递性。

    为什么 Java 只能单继承,而不像 C++ 一样能够多继承?从技术的角度来说,是为了降低复杂性。例如,A 类中有一个 m 方法,B 类中也有一个 m 方法。如果 C 类单独继承 A 类或者 B 类时,C 类中的 m 方法要么继承于 A 类,要么继承于 B 类。而如果多重继承的话,C 类的 m 方法有可能来自 A 类,又有可能来自 B 类,就会造成冲突。这样的继承关系,就会增加复杂性,甚至进一步影响多态的灵活性。

    此外,java.lang.Object 是一切类的父类。或者可以说,如果一个类没有父类,那么它的父类就是 java.lang.Object。Object 类型有几个方法比较实用:

    • equals 方法:用来判断两个 obj 对象的地址是否相等。
      • 由于 Object 的原始 equals 方法比较时,比较双方如果地址相同,则返回 true,否则返回 false,所以对于很多 Object 的子类并不适用,故很多 Object 的子类经常会重写 equals 方法。以后如果有调用 equals 方法的时候,需要了解该 equals 方法的具体意义;
    • toString() 方法:打印一个对象,就会打印该对象的 toString 的返回值;

    如果要判断一个实例对象 obj 是否属于某个类型 T,可以使用关键字 instanceof。对于表达式 obj instanceof T,如果实例 obj 属于 T 类型,则返回 true;否则返回 false。

    5. 抽象类

    对于普通的类,其本身就是一个完善的功能类,可以直接产生实例化对象,并且在普通类中可以包含构造方法、普通方法、static 方法、常量和变量等内容。抽象类,就是指在普通类的结构里面增加抽象方法的组成部分。

    那么什么叫抽象方法呢?抽象方法,是指没有方法体的方法,即一个方法只有声明,没有实现。同时抽象方法还必须用 abstract 关键字来声明。只要拥有一个抽象方法的类就是抽象类。

    抽象类的使用原则如下:

    • 抽象方法必须为 public 或者 protected(因为如果为 private,则不能被子类继承,子类便无法实现该方法),缺省情况下,默认为public;
    • 抽象类不能直接实例化,需要依靠子类采用向上转型的方式处理;
    • 抽象类必须有子类,使用 extends 继承,一个子类只能继承一个抽象类;
    • 对于不是抽象类的子类,必须覆写抽象类之中的全部抽象方法(如果子类没有实现父类的抽象方法,则必须将子类也定义为 abstract 类);

    对于抽象类,还有一些需要注意的地方:

    • 抽象类继承子类,其中有明确的方法覆写要求,而普通类可以有选择性的来决定是否需要覆写;
    • 抽象类实际上就比普通类多了一些抽象方法而已,其他组成部分和普通类完全一样;
    • 普通类对象可以直接实例化,但抽象类的对象必须经过向上转型之后才可以得到

    可以看出,虽然一个类的子类可以去继承任意的一个普通类,可是从开发的实际要求来讲,普通类尽量不要去继承另外一个普通类,而是去继承抽象类

    6. final 关键字

    在 Java 中,final 关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。

    用 final 关键字修饰变量

    • final 关键字来修饰类的变量,只能被赋一次值
    • final 修饰的成员变量也只能赋值一次;但在对象创建的时候,成员变量必须赋值,即在定义初始化或构造函数中对 final 修饰的成员变量进行赋值;
    • java 语言中没有常量,但可以<font color=red>通过 public static final</font> 来定义常量,且一般大写;
      • 例:public static final int CELL_WIDTH = 50;

    用 final 关键字修饰的不能被继承;例如,String, Math 类就是 Java 中典型的 final 关键字修饰的类;
    用 final 关键字修饰的方法,不能够被重写。

    需要注意的是,用 final 修饰的数组,与普通的变量理解起来难度。如下例中:

    //========================================
    final int a = 10;
    a = 20; // 错误,a 变量只能赋值一次
    //========================================
    final int[] b = {1, 2, 3, 4};
    b[0] = 10; // 正确
    //========================================
    

    int 类型的 a 由于被 final 关键字修饰,所以不能被二次赋值,这比较容易理解。但下面的例子中,看起来好像是数组的二次赋值也可以完成。其实实际上对于被 final 关键字修饰的数组而言,数组的引用地址是不能改变的。上例程中,b[0] = 10 仅改变了 b 数组 0 位置的元素内容而已,而该位置的地址引用没有发生任何改变,所以是可以完成的。

    7. 接口

    接口体现的是一种标准,外部体现为方法的声明。接口用关键字 interface 修饰。提供一个接口,是为了实现某种标准的对接过程,而实现接口,就是意味着符合这个标准。对接口的实现,需要使用 implements 关键字。实现一个接口,就要重写接口中的方法;换个角度来说,如果不实现接口,就变成了一个抽象类。

    接口里的方法,默认都是 public abstract 类型的。此外接口里也可以声明变量,变量的类型也默认为 public static final 类型。例如:

    public interface Memory {
        public void memo(); // 等价于 public abstract void memo();
        int i = 1; // 等价于 public static final int i = 1;
    }
    

    Java 中的接口与继承最大的不同是,继承是单一继承,但接口与接口之间可以多继承。此外一个类可以继承一个父类,同时实现多个接口。举一个例子,如何定义一个英雄?我们假定一个人,如果同时满足可以飞、可以打架、可以游泳,那么他就是一个英雄。同时,人又属于动物。那么我们就可以定义英雄 Hero 如下:

    public class Hero extends Animal implemets CanFly, CanFight, CanSwim {}
    

    上例中,也可以看到接口与继承的另一个区别:继承体现了 is-a 关系(单继承),接口体现了 can-do 关系(多继承)。

    接口与抽象又有一些相似的共同点:如果看到接口类型的引用,那么引用的一定是实现了该接口的类的实例;如果看到抽象类型的引用,那么引用的一定是继承了该抽象类的类的实例。

    8. 内部类

    使用内部类的原因,在于内部类提供了更好的封装,只有外部类可以访问内部类。此外内部类中的属性和方法,即使是外部类也不能直接访问,相反,内部类可以直接访问包括 private 声明的外部类的属性和方法。另外属于内部类的匿名内部类也十分利于回调函数的编写。

    内部类与外部类是一个相对独立的实体,它与外部类并不是 is-a 关系。比如我们定义了内部类外部类的 OuterClass.java 如下:

    public class OuterClass {
        private String outerName;
        private int outerAge;
        public class InnerClass{
            private String innerName;
            private int innerAge;
        }
    }
    

    在该文件的路径下输入指令:

    javac OuterClass.java
    

    结果如图:

    3-01.png

    从编译的结果就可以看出来,编译后外部类及其内部类会生成两个独立的 .class 文件:OuterClass.class 和 OuterClass$InnerClass.class。说明内部类是一个编译时的概念。

    此外,内部类可以直接访问外部类的元素,但是外部类不可以直接访问内部类的元素;而且外部类可以通过内部类引用间接访问内部类元素。

    关于内部类的创建,如果在外部类中创建内部类,那么就和普通的创建对象是一样的:

    InnerClass innerClass = new InnerClass();
    

    如果在外部类之外创建外部类中的内部类(有点拗口),就需要 outerClass.new 来创建:

    //================================================
    OuterClass outerClass = new OuterClass();
    OuterClass.InnerClass innerClass = outerClass.new InnerClass();
    //================================================
    // 或者一步到位的方法:
    OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
    //================================================
    

    Java中内部类主要分为四种:成员内部类、方法内部类、匿名内部类、静态内部类

    (1) 成员内部类

    成员内部类也是最普通的内部类,上面的 InnerClass 与 OuterClass就是属于成员内部类与其外部类。成员内部类又称为局部内部类,它是外部类的一个成员,所以他是可以无限制的访问外围类的所有成员属性和方法,尽管是 private 的,但是外部类要访问内部类的成员属性和方法,就需要通过内部类实例来访问。

    在成员内部类中要注意两点:

    • 成员内部类中不能存在任何 static 的变量和方法
    • 成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类

    (2) 静态内部类

    static 关键字可以修饰成员变量、方法、代码块,其实它还可以修饰内部类,使用 static 修饰的内部类我们称之为静态内部类。静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类却没有。没有这个引用就意味着静态内部类的两个属性:

    • 静态内部类的创建不需要依赖于外围类,可以直接创建
    • 静态内部类不可以使用任何外围类的非 static 成员变量和方法,而内部类则都可以

    静态内部类的示例如下:

    public class OuterClass {
        private static String outerName;
        public  int age;
    
        static class InnerClass1{
            // 在静态内部类中可以存在静态成员
            public static String _innerName = "static variable";
            public void display(){
                /*=========================================
                 * 静态内部类只能访问外部类的静态成员变量和方法
                 * 不能访问外部类的非静态成员变量和方法
                 ==========================================
                 */
                System.out.println("OutClass name :" + outerName);
            }
        }
        class InnerClass2{
            // 非静态内部类中不能存在静态成员
            public String _innerName = "no static variable";
            // 非静态内部类中可以调用外部类的任何成员,不管是静态的还是非静态的
            public void display() {
                System.out.println("OuterClass name:" + outerName);
                System.out.println("OuterClass age:" + age);
            }
        }
        public void display(){
            // 外部类能直接访问静态内部类静态元素
            System.out.println(InnerClass1._innerName);
            // 静态内部类可以直接创建实例不需要依赖于外部类
            new InnerClass1().display();
            // 非静态内部的创建需要依赖于外部类
            OuterClass.InnerClass2 inner2 = new OuterClass().new InnerClass2();
            // 非静态内部类的成员需要使用非静态内部类的实例访问
            System.out.println(inner2._innerName);
            inner2.display();
        }
    
        public static void main(String[] args) {
            OuterClass outer = new OuterClass();
            outer.display();
        }
    }
    

    (3) 方法内部类

    方法内部类定义在外部类的方法中,局部内部类和成员内部类基本一致,只是它们的作用域不同,方法内部类只能在该方法中被使用,出了该方法就会失效。 对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。

    (4) 匿名内部类

    匿名内部类是没有名字的局部内部类,它没有 class, interface, implements, extends 等关键字的修饰,也没有构造器,它一般隐式的继承某一个父类,或者具体实现某一个接口

    • 什么时候用
      • 已知父类,要获取其子类的实例对象;
      • 已知接口,要获取其实现了该接口的类的实例;
    • 怎么用

    对于子类继承:

    new 父类(给父类的构造函数传递参数) {  
        // 子类具体实现部分;  
    }  
    // 此处得到的是子类的实例对象
    

    对于接口实现:

    new 接口() {  
        // 实现了该接口的类的实现部分;  
    }
    // 此处得到的是接口的实现类的实例对象
    

    后面将会在很多地方看到匿名内部类的使用,比如在后面讲到的 TreeSet,JDBC 的 JdbcTemplate.query 方法中的 RowMapper 继承类实现等。此处以 TreeSet 为例,需要实现一个比较器 Comparator 的 compareTo 方法,这里就可以实现匿名内部类。代码如下:

    TreeSet<T> ts = new TreeSet<T>(new Comparator<T>() {
        public int compare(T o1, T o2) {
            // TODO Auto-generated method stub
            return o2.getName().compareTo(o1.getName());
        }
    });
    

    上面的代码中,new TreeSet< T > 后面传入的参数,是直接定义得到的一个 new Comparator< T >(){...} 。这里就体现了匿名内部类直接对接口的实现,确定了数据类型为 T 的两个对象 o1, o2 的名称按照字母顺序进行排列的规定。

    后续的 RowMapper 继承,也会用到匿名内部类。代码大致如下,到后面会详细讲解:

    @Test  
    public void testResultSet1() {  
      jdbcTemplate.update("insert into test(name) values('name5')");  
      String listSql = "select * from test";  
      List result = jdbcTemplate.query(listSql, new RowMapper<Map>() {  
          @Override  
          public Map mapRow(ResultSet rs, int rowNum) throws SQLException {  
              Map row = new HashMap();  
              row.put(rs.getInt("id"), rs.getString("name"));  
              return row;  
      }});  
      Assert.assertEquals(1, result.size());  
      jdbcTemplate.update("delete from test where name='name5'");       
    }  
    

    内部类相关内容参考地址:
    java 内部类(inner class)详解》


    接下篇《JavaSE 基础学习之四 —— 异常的处理》

    相关文章

      网友评论

          本文标题:JavaSE 基础学习之三 ——Java 的继承与接口

          本文链接:https://www.haomeiwen.com/subject/usbpjftx.html