5.6.4 调用父类构造器

子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码。类似于前面所介绍的一个构造器调用另一个重载的构造器的情况

调用 本类 的构造器

在一个构造器中调用另一个重载的构造器使用this调用来完成,

调用 父类 的构造器

在子类构造器中调用父类构造器使用super调用来完成。

this调用不super调用不能同时出现

使用super调用父类构造器也必须出现在子类构造器执行体的第一行,所以**this调用和super调用**不能同时出现。

子类构造器会默认调用父类构造器

不管是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次

子类构造器调用父类构造器的情况

子类构造器调用父类构造器分如下几种情况:

  1. 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。
  2. 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时即会调用父类构造器
  3. 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器

父类构造器最先执行完毕

不管上面哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器,依此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器

创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果某个父类通过this调用了该父类中重载的构造器,就会依次执行此父类的多个构造器。

5.6.3 super限定

如果需要在子类方法中调用父类被覆盖的实例方法,则**可使用super限定来调用父类被覆盖的实例方法**。

superJava提供的一个关键字,super用于限定该对象调用它从父类继承得到实例变量实例方法

super不能出现在staitc修饰的方法中

正如this不能出现在static修饰的方法中一样, super也不能出现在static修饰的方法中。

如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。

子类同名实例变量会覆盖父类变量

如果子类定义了和父类同名的实例变量,则会发生子类实例变量隐藏父类实例变量的情形。
在正常情况下,子类里定义的方法直接访问该实例变量默认会访问到子类中定义的实例变量,无法访问到父类中被隐藏的实例变量
在子类定义的实例方法中可以通过super来访问父类中被隐藏的实例变量。

如果子类里没有包含和父类同名的成员变量,那么在子类实例方法中访问该成员变量时,则无须显式使用super父类名作为调用者。

系统查找变量的顺序

如果在某个方法中访问名为a的变量,但没有显式指定调用者,则系统查找a的顺序为:

  1. 查找该方法中是否有名为a的局部变量。
  2. 查找当前类中是否包含名为a的成员变量。
  3. 查找a的直接父类中是否包含名为a的成员变量,
  4. 如果没有依次上溯a的所有父类,直到java.lang.Object类,如果最终不能找到名为a的成员变量,则系统出现编译错误。

被覆盖的是类变量的情况

如果被覆盖的是类变量,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量

系统依旧为被覆盖的变量分配内存空间

如果在子类里定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。注意不是完全覆盖,因此系统在创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间

引用变量类型决定调用的是哪个实例变量

如果子类和父类存在同名的实例变量,

  • 当通过子类引用变量访问这个实例变量时,访问的是子类的实例变量,
  • 当在该子类引用变量强制向上转型为父类引用变量时,访问的是被覆盖的父类的成员变量。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent {
// 父类实例变量
public String tag = "父类 实例变量 的值";
}

class Derived extends Parent {
// 子类实例变量,会覆盖父类
public String tag = "子类 实例变量 的值";
}

public class HideTest {
public static void main(String[] args) {
Derived d = new Derived();
// d现在是子类类型的引用变量,访问的是 子类中 新定义的 实例变量
System.out.println(d.tag);
// 向上转型后d变成父类类型的引用变量,访问的是 子类中 被隐藏的 父类实例变量
System.out.println(((Parent) d).tag);
}
}

运行效果:

1
2
子类 实例变量 的值
父类 实例变量 的值

5.6.2 重写父类的方法

子类扩展了父类,子类是一个特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的成员变量和方法。但有一种情况例外:子类需要重写父类的方法。

方法重写

子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法

方法重写规则

方法的重写要遵循”两同两小一大”规则,

  • “两同”即
    • 方法名相同、
    • 形参列表相同:
  • “两小”指的是
    • 子类方法返回值类型应比父类方法返回值类型更小或相等,
    • 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
  • “一大”指的是
    • 子类方法的访问权限应比父类方法的访问权限更大或相等

尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,个是实例方法

子类 对象 无法访问被覆盖父类的方法

当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,

子类 方法 可以调用被覆盖的父类方法

虽然子类对象无法调父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。

子类方法 中如何调用被覆盖的父类方法

如果需要在子类方法中调用父类中被覆盖的方法,

  • 若被覆盖的是实例方法则可以使用super作为调用者来调用父类中被覆盖的方法
  • 若被覆盖的是类方法则可以使用父类类名作为调用者来调用父类中被覆盖的方法

父类中private修饰的方法无法重写

如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。

所谓重写父类private方法只不过是子类中定义的新方法,不算重写

如果子类中定义了一个与父类private方法具有相同的方法名相同的形参列表相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。例如,下面代码是完全正确的

重载和重写的区别

方法重载和方法重写在英语中分别是overloadoverride,
其实把重载和重写放在一起比较本身没有太大的意义

  • 因为重载主要发生在同一个类的多个同名方法之间,
  • 而重写发生在子类父类的同名方法之间。

它们之间的联系很少,除二者都是发生在方法之间,并要求方法名相同之外,没有太大的相似之处。

父类方法和子类方法 可以重载

父类方法和子类方法之间也可能发生重载,因为子类会获得父类方法。

方法重写要求

  1. 方法名相同
  2. 形参列表相同:
  3. 返回值类型更小或相等
  4. 抛出的异常类更小或相等
  5. 访问权限更大或相等

方法重载的要求

方法重载的要求就是两同一不同:

  • 同一个类中
  • 方法名相同,
  • **形参列表不同**。

什么时候发生子类方法和父类方法的重载

如果子类定义了一个与父类方法有相同的方法名,但参数列表不同的方法,就会形成父类方法和子类方法的重载,

参考资料

5.2.5 方法重载

5.6 类的继承

继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Java的继承具有单继承的特点。

5.6.1 继承的特点

继承关键字

Java的继承通过extends关键字来实现

被继承的类被称为父类,有的也称其为基类、超类。实现继承的类被称为子类,。父类和子类的关系,是一种一般和特殊的关系。例如水果和苹果的关系,苹果继承了水果,苹果是水果的子类,则苹果是一种特殊的水果。

因为子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。

继承语法格式

Java里子类继承父类的语法格式如下

1
2
3
4
修饰符 class SubClass extends SuperClass
{
//类定义部分
}

从上面语法格式来看,定义子类的语法非常简单,只需在原来的类定义上增加extends SuperClass即可,即表明该子类继承了SuperClass

extends关键字也可以叫扩展

Java使用extends作为继承的关键字, extends关键字在英文中是扩展,而不是继承!这个关键字很好地体现了子类和父类的关系:子类是对父类的扩展,子类是一种特殊的父类。

Java类只能有一个直接父类,实际上,Java类可以有无限多个间接父类(或者说祖先类)。

默认父类

如果定义一个Java类时并未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类因此, java.lang.Object类是所有类的父类,要么是其直接父类,要么是其间接父类。因此所有的Java对象都可调用java.lang.Object类所定义的实例方法。

5.5.2 构造器重载

什么是构造器重载

同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。
构造器重载允许java类里包含多个初始化逻辑,从而允许使用不同的构造器来初始化Java对象。
构造器重载要求参数列表必须不同

通过this关键调用其他构造器

在构造器中可以通过this关键字可以调用另一个构造器

使用this调用构造器的条件

使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句

this如果确定调用哪个构造器

使用this调用重载的构造器时,系统会根据this后括号里的实参来调用形参列表与之对应的构造器

5.5 深入构造器

构造器用于创建实例时执行初始化。Java类必须包含个或一个以上的构造器。

什么时候系统提供默认无参构造器

如果程序员没有为Java类提供任何构造器,则系统会为这个类提供一个无参数的构造器,这个构造器的执行体为空,不做任何事情。无论如何,Java类至少包含一个构造器。

5.5.1 使用构造器执行初始化

构造器最大的用处就是在创建对象时执行初始化

默认的初始化规则

当创建一个对象时,系统为这个对象的实例变量进行默认初始化,这种默认的初始化

  • 基本类型的数值型实例变量设为0
  • 基本类型的布尔型实例变量设为false
  • 把所有引用类型的实例变量设为null

执行构造器执行体之前对象已经创建好了

当系统开始执行构造器的执行体之前,系统已经创建了一个对象,只是这个对象还不能被外部程序访问,只能在构造器中通过this来引用。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回,这个返回值通常会赋给另一个引用类型的变量,从而让外部程序可以访问该对象。

若自定义构造器则系统不再提供默认无参构造器

一旦程序员提供了自定义的构造器,系统就不再提供默认的无参构造器。此时,如果用户想要使用无参数的构造器,则要自己写一个无参的构造器。

构造器重载

如果一个类里提供了多个构造器,就形成了构造器的重载

构造器的访问修饰符

  • 因为构造器主要用于被其他方法调用,用以返回该类的实例,所以通常把构造器设置成public访问权限,从而允许系统中任何位置的类来创建该类的对象。
  • 构造器设置为protected,主要用于被其子类调用;
  • 构造器设置为private,阻止其他类调用构造器来创建该类的实例。

5.4.4 Java的常用包

Java的核心类都放在java包以及其子包下,Java扩展的许多类都放在javax包以及其子包下。这些实用类也就是前面所说的API(应用程序接口), 下面几个包是Java语言中的常用包。

描述
java.lang 这个包下包含了Java语言的核心类,如StringMathSystemThread类等,使用这个包下的类无须使用import语句导入,系统会自动导入这个包下的所有类.
java.util 这个包下包含了Java的大量 工具类/接口 和 集合框架类/接口 ,例如ArraysListSet等.
java.net 这个包下包含了一些Java网络编程相关的类/接口
java.io 这个包下包含了一些Java输入输出编程相关的类/接口
java.text 这个包下包含了一些Java格式化相关的类。
java.sql 这个包下包含了Java进行JDBC数据库编程的相关类/接口
java.awt 这个包下包含了抽象窗口工具集(Abstract Window Toolkits)的相关类接口,这些类主要用于构建图形用户界面(GUI)程序。
java.swing 这个包下包含了Swing图形用户界面编程的相关类/接口,这些类可用于构建平台无关的GUI程序。

5.4.3 package、import和import static

package

Java引入了包( package)机制,提供了类的多层命名空间,用于解决类的命名冲突、类文件管理等问题。
Java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元。如果希望把个类放在指定的包结构下,应该在Java源程序的第一个非注释行放置如下格式的代码:
package packageName;
一旦在Java源文件中使用了这个package语句,就意味着该源文件里定义的所有类都属于这个包。位于包中的每个类的完整类名都应该是包名和类名的组合,如果其他人需要使用该包下的类,也应该使用包名加类名的组合.

包名的命名规则

Java语法只要求包名是有效的标识符即可,但从可读性规范角度来看,包名应该全部是小写字母而且应该由一个或多个有意义的单词连缀而成。

一个源文件只能指定一个包

package语句必须作为源文件的第一条非注释性语句,一个源文件只能指定一个包,即只能包含一条package语句,该源文件中可以定义多个类,则这些类将全部位于该包下。

没有package语句这处于默认包

如果没有显式指定package语句,则处于默认包下。在实际企业开发中,通常不会把类定义在默认包下。

同一个包下的类可以自由访问

父包和子包在用法上则不存在任何关系,如果父包中的类需要使用子包中的类,则必须使用子包的全名,而不能省略父包部分

import关键字

Java引入了import关键字, import可以向某个Java文件中导入指定包层次下某个类或全部类,
import语句应该出现在package语句之后、类定义之前

一个Java源文件只能包含一个package语句,但可以包含多个import语句,多个import语句用于导入多个包层次下的类。

import导入单个类

使用import语句导入单个类的用法如下:
import package.ClassName

import导入一个包下的所有类

使用import语句导入指定包下全部类的用法如下:
import package.*
上面import语句中的*只能代表类,不能代表包。

import语句可以简化编程

正如上面代码中看到的,通过使用import语句可以简化编程。但import语句并不是必需的,只要坚持在类里使用其他类的全名,则可以无须使用import语句。

java默认导入java.lang包下的所有类

Java默认为所有源文件导入java.lang包下的所有类,因此前面在Java程序中使用StringSystem类时都无须使用import语句来导入这些类。但对于前面介绍数组时提到的Arrays类,其位于java.util包下,则必须使用import语句来导入该类。

import语句导入同名类的情况

在一些极端的情况下, import语句也帮不了我们,此时只能在源文件中使用类全名
例如,需要在程序中使用java.sql包下的类,也需要使用java.util包下的类,则可以使用如下两行import语句:
import java.util.*;
import java.sql*;
如果接下来在程序中需要使用Date类,则会引起编译错误。而import语句导入的java.sqljava.util包下都包含了Date类,系统不知道你要调用的是哪个Date类。在这种情况下,如果需要指定包下的Date类,则只能使用该类的全名。

静态导入

JDK1.5以后更是增加了一种静态导入的语法,它用于导入指定类的某个静态成员变量某个静态方法全部的静态成员变量全部的静态方法
静态导入使用import static语句,静态导入也有两种语法,分别用于导入指定类的单个静态成员变量、方法和全部静态成员变量、方法,

导入单个静态成员变量或静态方法

导入指定类的单个静态成员变量、方法的语法格式如下:
import static package.ClassName.fieldName;
import static package.ClassName.methodName;
上面语法导入package.ClassName类中名为fieldName的静态成员变量
或者package.ClassName类中名为methodName的静态方法。
例如,可以使用import static java.lang.System.out;语句来导入java.lang.System类的out静态成员变量。

静态成员变量和静态成员方法同名的情况

如果一个类中的静态成员变量和静态成员方法同名,则直接一个名字,即可同时导入静态成员变量和静态成员方法,如下所示:
import static package.ClassName.sameName;
虽然静态成员变量和静态成员方法同名,不过两者用法上存在区别,编译器还是可以区分同名的变量和方法的(方法调用会带括号)

1
2
3
4
5
6
7
8
9
10
package statictest;

public class Statics {
//定义一个静态成员变量.
public static String test = "静态成员 变量";
// 定义一个静态成员方法.
public static String test() {
return "静态成员 方法";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import static java.lang.System.*;
import static java.lang.Math.*;
// 由于成员变量和方法都是test,所以同时导入的静态成员变量和静态成员方法.
import static statictest.Statics.test;

public class StaticImportTest {
public static void main(String[] args) {
// out是java.lang.System类的静态成员变量,代表标准输出
// PI是java.lang.Math类的静态成员变量,表示π常量
out.println(PI);
// 直接调用Math类的sqrt静态方法
out.println(sqrt(256));
out.println(test);
//方法调用会带有括号:test(),所以知道调用的是方法而不是变量.
out.println(test());
}
}
1
2
3
4
3.141592653589793
16.0
静态成员 变量
静态成员 方法

导入类的全部静态成员变量或静态方法

导入指定类的全部静态成员变量、方法的语法格式如下:
import static package.ClassName.*;
上面语法中的星号只能代表静态成员变量或方法名。

import static语句应该写在哪里

import static语句也放在Java源文件的package语句之后、类定义之前,即放在与普通import语句相同的位置,而且import语句和import static语句之间没有任何顺序要求。

import static也就被翻译成了”静态导入”。

用一句话来归纳importimport static的作用:使用import可以省略写包名;而使用import static则可以连类名都省略。

importimport static的功能非常相似,只是它们导入的对象不一样而己。import语句和import static语句都是用于减少程序中代码编写量的。

5.4.2 使用访问控制符

Java提供了3个访问控制符:privateprotectedpublic,分别代表了3个访问控制级别,另外还有一个不加任何访问控制符的访问控制级别,也就是说Java提供了4个访问控制级别。
Java的访问控制级别由小到大的顺序为:privatedefaultprotectedpublic

4个访问控制级别中的default并没有对应的访问控制符,当不使用任何访问控制符来修饰类或类成员时,系统默认使用该访问控制级别。这4个访问控制级别的详细介绍如下。

当前类访问权限private

如果类里的一个成员(包括成员变量、方法和构造器等)使用private访问控制符来修饰,则这个成员只能在当前类的内部被访问。很显然,这个访问控制符用于修饰成员变量最合适,使用它来修饰成员变量就可以把成员变量隐藏在该类的内部。

包访问权限default

如果类里的一个成员(包括成员变量、方法和构造器等)或者一个外部类不使用任何访问控制符修饰,就称它是包访问权限, default访问控制的成员或外部类可以被相同包下的其他类访问

子类访问权限protected

如果一个成员(包括成员变量、方法和构造器等)使用protected访问控制符修饰,那么这个成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。在通常情况下,如果使用protected来修饰一个方法,通常是希望其子类来重写这个方法

公共访问权限public

这是一个最宽松的访问控制级别,如果一个成员(包括成员变量、方法和构造器等)或者一个外部类使用public访问控制符修饰,那么这个成员或外部类就可以被所有类访问,不管访问类和被访问类是否处于同一个包中,是否具有父子继承关系。

访问控制级别汇总表

这里有一张图片

访问控制级别表 从大到小

public protected default private
同一个类中 可以 可以 可以 可以
同一个包中 可以 可以 可以
子类中 可以 可以
全局范围 可以

访问控制符用于修饰类的成员

访问控制符用于修饰类的成员

访问控制符用于控制一个类的成员是否可以被其他类访问。

不要使用访问控制符修饰局部变量

对于局部变量而言,其作用域就是它所在的方法,不可能被其他类访问,因此不能使用访问控制符来修饰局部变量

外部类只能使用public或默认修饰

对于外部类而言,它也可以使用访问控制符修饰,但外部类只能有两种访问控制级别:public和默认,外部类不能使用privateprotected修饰,因为外部类没有处于任何类的内部,也就没有其所在类的内部、所在类的子类两个范围,因此**privateprotected访问控制符对外部类没有意义**

  • 使用public修饰的外部类可以被所有类使用,如声明变量、创建实例;
  • 不使用任何访问控制符修饰的外部类只能被同一个包中的其他类使用。

public修饰的类的源文件名必须与类名相同

如果一个Java源文件里定义的所有类都没有使用public修饰,则这个Java源文件的文件名可以是一切合法的文件名;但如果一个Java源文件里定义了一个public修饰的类则这个源文件的文件名必须与public修饰的类的类名相同

setter getter方法

某个类里包含了名为abc的实例变量,则其对应的settergetter方法名应为setAbc()getAbc()

setter和getter方法的命名规则

将原实例变量名的首字母大写,并在前面分别增加setget动词,就变成settergetter方法名

什么是符合JavaBean规范的类

如果一个Java类的每个实例变量都被使用private修饰,并为毎个实例变量提供了public修饰settergetr方法,那么这个类就是一个符合JavaBean规范的类。因此, JavaBean总是一个封装良好的类。

高内聚 低耦合

一个类常常就是一个小的模块,应该只让这个模块公开必须让外界知道的内容,而隐藏其他一切内容。进行程序设计时,应尽量避免一个模块直接操作和访问另一个模块的数据,模块设计追求高内聚,低耦合

什么是高内聚

所谓高内聚就是尽可能把模块的内部数据隐藏在模抉内部把模块的功能在模块内部实现,不允许外部直接干预。

什么是低耦合

所谓低耦合就是仅仅暴露少量的方法给外部使用。

关于访问控制符的使用,存在如下几条基本原则。

应该使用private修饰符的情况

  1. 类里的绝大部分成员变量都应该使用private修饰,
  2. 只有一些static修饰的、类似全局变量的成员变量,才可能考虑使用public修饰。
  3. 工具方法也应该使用private修饰,所谓工具方法就是那些只用于辅助实现该类的其他方法的方法

应该使用protected修饰符的情况

如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。

应该使用public修饰的情况

希望暴露出来给其他类自由调用的方法应该使用public修饰。

  • 类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例。
  • 外部类通常都希望被其他类自由使用,所以大部分外部类都使用public修饰。

5.4 隐藏和封装

Java程序推荐将类和对象的成员变量进行封装。

5.4.1 理解封装

封装( Encapsulation)是面向对象的三大特征之一(另外两个是继承和多态),

封装指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来操作或访问内部信息

封装的好处

对一个类或对象实现良好的封装,可以实现以下目的。

  1. 隐藏类的实现细节。
  2. 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问
  3. 可进行数据检查,从而有利于保证对象信息的完整性
  4. 便于修改,提高代码的可维护性

如何做好封装

为了实现良好的封装,需要从两个方面考虑:

  1. 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问。
  2. 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作

如何实现封装

通过使用Java提供的访问控制符来实现