前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaSE 基础学习之三 —— Java 的继承与接口

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

作者头像
剑影啸清寒
发布2019-05-26 10:07:31
4240
发布2019-05-26 10:07:31
举报
文章被收录于专栏:琦小虾的Binary琦小虾的Binary

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1434899

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


三. Java 的继承与接口

1. java 中的继承

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

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

代码语言:javascript
复制
class 子类 extends 父类 {
}

用圆形为例,举例如下:

代码语言:javascript
复制
public class Circle extends Shape {
    // ...
}

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

Java 中除了构造函数之外,子类可以继承父类所有函数。

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

代码语言:javascript
复制
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++),同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,在运行时编译器依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的,但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

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

代码语言:javascript
复制
Parent p = new Children();
  1. 编译器检查对象的声明类型和方法名;
代码语言:txt
复制
- 假如我们有 Children 的实例对象 child,此时想要调用 Children 的 fun(args) 方法,那么编译器就会列举出所有名称为 fun 的方法(所有方法签名相同,参数列表不同的 fun 方法),并列举 Children 的超类 Parent 中 fun 方法;编译器检查方法调用中提供的参数类型; 
代码语言:txt
复制
- 如果所有签名为 fun 的方法中,有一个参数类型和调用时提供的参数类型最匹配,那么就调用该方法。该过程成为**重载解析**;当程序运行并使用动态绑定调用方法时,虚拟机必须调用与 child 指向的对象的实际类型相匹配的方法版本。

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

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

3. 类的初始化顺序

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

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

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

代码语言:javascript
复制
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…undefined

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 对象的地址是否相等。
代码语言:txt
复制
- 由于 Object 的原始 equals 方法比较时,比较双方如果地址相同,则返回 true,否则返回 false,所以对于很多 Object 的子类并不适用,故很多 Object 的子类经常会重写 equals 方法。以后如果有调用 equals 方法的时候,需要了解该 equals 方法的具体意义;

如果要判断一个实例对象 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 来定义常量,且一般大写;
代码语言:txt
复制
- 例:`public static final int CELL_WIDTH = 50`;

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

用 final 关键字修饰的方法,不能够被重写。

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

代码语言:javascript
复制
//========================================
final int a = 10;
a = 20; // 错误,a 变量只能赋值一次
//========================================
final int[] b = {1, 2, 3, 4};
b[0] = 10; // 正确
//========================================

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

7. 接口

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

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

代码语言:javascript
复制
public interface Memory {
    public void memo(); // 等价于 public abstract void memo();
    int i = 1; // 等价于 public static final int i = 1;
}

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

代码语言:javascript
复制
public class Hero extends Animal implemets CanFly, CanFight, CanSwim {}

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

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

8. 内部类

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

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

代码语言:javascript
复制
public class OuterClass {
    private String outerName;
    private int outerAge;
    public class InnerClass{
        private String innerName;
        private int innerAge;
    }
}

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

代码语言:javascript
复制
javac OuterClass.java

结果如图:

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

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

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

代码语言:javascript
复制
InnerClass innerClass = new InnerClass();

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

代码语言:javascript
复制
//================================================
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 成员变量和方法,而内部类则都可以

静态内部类的示例如下:

代码语言:javascript
复制
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 等关键字的修饰,也没有构造器,它一般隐式的继承某一个父类,或者具体实现某一个接口

  • 什么时候用
代码语言:txt
复制
- 已知父类,要获取其子类的实例对象;
- 已知接口,要获取其实现了该接口的类的实例;

对于子类继承:

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

对于接口实现:

代码语言:javascript
复制
new 接口() {  
    // 实现了该接口的类的实现部分;  
}
// 此处得到的是接口的实现类的实例对象

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

代码语言:javascript
复制
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 继承,也会用到匿名内部类。代码大致如下,到后面会详细讲解:

代码语言:javascript
复制
@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 基础学习之四 —— 异常的处理》

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年05月26日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 三. Java 的继承与接口
    • 1. java 中的继承
      • 2. 动态绑定 (Dynamic Binding)
        • 3. 类的初始化顺序
          • 4. Java 的单继承
            • 5. 抽象类
              • 6. final 关键字
                • 7. 接口
                  • 8. 内部类
                    • (1) 成员内部类
                    • (2) 静态内部类
                    • (3) 方法内部类
                    • (4) 匿名内部类
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档