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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ajianyingxiaoqinghan/article/details/80458729

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


三. Java 的继承与接口

1. java 中的继承

继承是 java 面向对象编程技术的一块基石,因为它允许创建分等级层次的类。 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 ——摘自《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 来说,绑定分为静态绑定动态绑定(或者叫做前期绑定和后期绑定)。

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

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

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

Parent p = new Children();
  1. 编译器检查对象的声明类型和方法名;
    • 假如我们有 Children 的实例对象 child,此时想要调用 Children 的 fun(args) 方法,那么编译器就会列举出所有名称为 fun 的方法(所有方法签名相同,参数列表不同的 fun 方法),并列举 Children 的超类 Parent 中 fun 方法;
  2. 编译器检查方法调用中提供的参数类型;
    • 如果所有签名为 fun 的方法中,有一个参数类型和调用时提供的参数类型最匹配,那么就调用该方法。该过程成为重载解析
  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 中的继承只能是单一继承,即 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 语言中没有常量,但可以通过 public static final 来定义常量,且一般大写;
    • 例: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

结果如图:

从编译的结果就可以看出来,编译后外部类及其内部类会生成两个独立的 .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 基础学习之四 —— 异常的处理》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券