接上文《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 关键字:
- super 关键字也有两种意义:调用父类的方法,或是调用父类的构造器。但是,super并不表示一个指向对象的引用,它只是一个特殊的关键字,用来告诉编译器,现在要调用的是父类的方法。
- 理论上,子类一定会调用父类相应的构造函数,只是使用了 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();
- 编译器检查对象的声明类型和方法名;
- 假如我们有 Children 的实例对象 child,此时想要调用 Children 的 fun(args) 方法,那么编译器就会列举出所有名称为 fun 的方法(所有方法签名相同,参数列表不同的 fun 方法),并列举 Children 的超类 Parent 中 fun 方法;
- 编译器检查方法调用中提供的参数类型;
- 如果所有签名为 fun 的方法中,有一个参数类型和调用时提供的参数类型最匹配,那么就调用该方法。该过程成为<font color=red>重载解析</font>;
- 当程序运行并使用动态绑定调用方法时,虚拟机必须调用与 child 指向的对象的实际类型相匹配的方法版本。调用方法的时候,如果当前子类已经对父类实现了方法的重写,则调用子类重写后的方法;否则只调用父类的方法。即如果子类 Children 中如果实现了对应的 fun(args) 方法,则调用 Children 的方法,否则就在父类 Parent 中寻找;在 Parent 中找不到,则在 Parent 的父类中找,直到最顶层的父类。
JVM 调用一个类方法时(即标注 static 的静态方法),它会基于对象引用的类型来选择所调用的方法,通常在编译时 JVM 就知道了要调用什么方法,这就是静态绑定。相反,如果 JVM 调用一个实例对象方法时,它会基于对象实际的类型来选择所调用的方法,具体调用什么方法只能在运行时得知。这就是动态绑定,是多态的一种。动态绑定为解决实际的业务问题提供了很大的灵活性,是一种非常优美的机制。
参考网址:《Java静态绑定与动态绑定》
3. 类的初始化顺序
创建一个实例对象时,考虑到该对象的父子关系,JVM 按照一定的顺序进行初始化:
- 先父类静态,再子类静态
- 父类的定义初始化 + 构造函数
- 子类定义初始化 + 构造函数
以例程来说明初始化顺序:
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...");
}
}
分析该段程序,先后顺序应该如下:
- 父类 C 的静态初始化:
bb..
- 子类 D 的静态初始化:
ee..
- 父类 C 的定义初始化:
aa..
- 父类 C 的构造函数:
cc..
- 子类 D 的定义初始化:
dd...
- 子类 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
结果如图:
从编译的结果就可以看出来,编译后外部类及其内部类会生成两个独立的 .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)详解》