本讲内容:继承、变量隐藏、方法重写、包、修饰符、this、super
一、继承
1、继承的概念
继承是面向对象的三大特性之一。在语义上继承的意思是照法律或遵照遗嘱接受死者的财产、头衔、地位等,在Java程序中的继承也有这个意思,不过子类继承的是父类的属性和方法。
2、继承的语法结构(子类的定义方式)
3、继承的例子:
关于继承我们第七讲举了一个白马和马的例子,我们再举一个动物父类和鸟类子类、鱼类子类的例子,这次我们用类图表示。
当我们写好了一个动物类,我们写鸟类的时候就可以继承动物类,自动获得动物类所拥有的属性和方法,提高了代码重用性。
4、Object类
Java中的所有对象(类)都是Object类的子类。我们可以用javap查看一下一个最简单的类的字节码:
public class Lesson09 {
}
5、继承的原则:
- 子类能够继承父类中被声明为public和protected的成员变量和成员方法。
- 子类能够继承在同一个包中的默认修饰符修饰的成员变量和成员方法。
- 如果子类声明了一个与父类变量同名的成员变量,则子类不能继承父类的成员变量,这种做法叫做变量的隐藏。
- 如果子类声明了一个与父类方法同名的成员方法,则子类不能继承父类的成员方法,这种做法方法的重写。
二、包 package
1、编译单元(compilation unit)
在Java里,一个编译单元就是一个用来书写Java源代码的文本文件。我们前面讲类的定义的时候只关注了类内部的东西,类外面是不是也有东西?答案是肯定的。编译单元有三个组成部分:
这三个部分都是可选的,包声明如果要有必须写在最前面,并且只能写一份。导入声明可以写多个,类声明也可以写多个。
2、包的概念(package)
类名是类之间彼此区分的标示,一个程序中类数量增多是,必然会遇到类名冲突的情形。包提供了类的组织和管理方式。包的用途有以下三种:
- 将功能相近的类放在同一个包里,方便使用和查找
- 类名相同的文件可以放在不同的包里而不会产生冲突
- 可以依据包设定访问权限
3、包的声明
4、 包的例子:
//包的声明
package android.java.basic;
//导入声明
import java.util.Date;
//类声明
class Animal{
long birthTime = new Date().getTime();
void eat(){
System.out.println("eating");
}
}
//类声明
class Fish extends Animal {
void swim(){
System.out.println("swimming");
}
}
//类声明
public class Lesson09 {
public static void main(String[] args){
//动物类
Animal a = new Animal();
a.eat();
System.out.println(a.birthTime);
//鱼类
Fish f = new Fish();
f.eat();
f.swim();
System.out.println(f.birthTime);
}
}
运行程序,查看结果:
四、访问修饰符 public protected 默认的 private
在Java中可以用访问修饰符来控制类或者类成员对程序的其它部分的可见性,从而在语言级别实现访问控制。当一个类无权访问另一个类或者类的成员时,编译器会提示你试图访问一些可能不存在的内容。
看见性 | public | protected | 默认 | private |
从同一个类 | 是 | 是 | 是 | 是 |
从同一个包中的任何类 | 是 | 是 | 是 | 否 |
从同一个包中的子类 | 是 | 是 | 是 | 否 |
从包外的子类 | 是 | 是,通过继承 | 否 | 否 |
从包外的任何非子类的类 | 是 | 否 | 否 | 否 |
- 对于类的修饰符,只能有2个选择,用public修饰或者不用(不用就是默认修饰符)。
- 如果一个类本身对另一个类不可见,则即使将其成员声明为public,也没有一个成员是可见的,只有当你确类本身对你是可见的时,查看其各个成员的访问级别才有意义。
- 对于类的成员(member, 包括属性和方法),可以用 public protected 默认的和private 4种修饰符。
- 永远不要用访问修饰符修饰局部变量,编译器会毫不留情的报错。(记住:局部变量只有一个修饰符可以用,那就是final)
除了访问修饰符外,还有非访问修饰符 static、final、abstract、transient、synchronization、native、strictfy ,我们在今后的学习中逐步掌握。
五、变量隐藏(shadowing)、方法重写(Overiding)
当子类继承父类时,子类中一不小心就会定义出父类名字相同的成员变量,对于这种现象,规则里是怎么说的,又是怎么应用的?用一句话说,就是子类成员会覆盖父类成员;对于变量就是变量隐藏,对于方法就是方法重写(方法覆盖)。
1、变量隐藏
shadow在做名词时意思是阴影,在做动词时意思是遮蔽,那么这里的意思shadowing更多的是遮蔽的意思,不过我们翻译的时候大家已经习惯说这个叫变量的隐藏。
先看一个局部变量遮蔽成员变量的例子:
public class Lesson09_1 {
int i=1;
int j=1;
int k=1;
void test(int i){
int j=2;
System.out.println("i="+i);
System.out.println("j="+j);
System.out.println("k="+k);
}
public static void main(String[] args){
Lesson09_1 lesson = new Lesson09_1();
lesson.test(2);
}
}
我们可以看到,当方法内的局部和成员变量名字相同时,在方法内,局部变量遮蔽住了成员变量,因此打印出来的是2,而不是1。
再看一个子类成员变量遮蔽父类成员变量的例子。
public class WhiteHorse extends Horse {
private static String color ="白色";
public static int leg =4;
public static void main(String[] args){
WhiteHorse xiaobai = new WhiteHorse();
System.out.println(xiaobai.color);
System.out.println(xiaobai.leg);
//类变量是遮蔽不住的
System.out.println(Horse.color);
//强制转换后我们看到父类的实体leg变量还在,只是被隐藏了
Horse xiaobai1 = (Horse)xiaobai;
System.out.println(xiaobai1.leg);
}
}
运行程序,查看结果:
2、方法重写 Override
当子类继承父类时,如果子类方法的签名和父类方法的签名相同时,子类就无法继承父类的方法,此时子类的方法就覆盖了父类的方法,我们称之为重写。重写可以定义子类某个行为的特殊性。
譬如动物会喝水,但是猫喝水和人喝水的具体行为就不同。
重写方法的规则如下:
- 参数列表必须与重写的方法的参数列表完全匹配(方法签名相同)。如果不匹配,你得到的将是方法重载。
- 返回类型必须与超类中被重写方法中原先声明的返回类型或其子类型相同。
- 访问级别的限制性可以比被重写方法弱,但是访问级别的限制性一定不能比被重写方法的更严格。
- 仅当实例方法被子类继承时,它们才能被重写。子类和超类在同一个包内时,子类可以重写未标示为private和final的任何超类方法。不同包的子类只能重写标示为public或protected的非final方法。
- 无论父类的方法是否抛出某种运行时异常,子类的重写方法都可以抛出任意类型的运行时异常。
- 重写方法一定不能抛出比被重写方法声明的检验异常更新或更广的检验异常,可以抛出更少或更有限的异常。
- 不能重写标示为final的方法。
- 不能重写标示为static的方法。
- 如果方法不能被继承,那么方法不能被重写。
我们举一个重写的例子:
Horse.java
public class Horse {
//给马写个摆Pose的方法
public void pose(){
//样子很酷
System.out.println("Cool!");
}
}
WhiteHorse.java
public class WhiteHorse extends Horse {
//白马重写了摆pose的方法
public void pose(){
//白马更酷一点
System.out.println("Cool!!!!");
}
public static void main(String[] args){
WhiteHorse xiaobai = new WhiteHorse();
xiaobai.pose();
}
}
运行程序,查看结果:
我们再把白马类中pose方法的访问修饰符改成private试试看:
六、this 和 super
1、this.成员变量
当成员变量被局部变量隐藏时想使用成员变量,可以用this关键字来访问成员变量。
public class Lesson09_1 {
int i=1;
int j=1;
int k=1;
static int l = 1;
void test(int i){
int j=2;
int l=2;
System.out.println("i="+i);
System.out.println("j="+j);
System.out.println("k="+k);
System.out.println("l="+l);
System.out.println("this.i="+this.i);
System.out.println("this.j="+this.j);
System.out.println("this.k="+this.k);
System.out.println("this.l="+this.l);
}
public static void main(String[] args){
Lesson09_1 lesson = new Lesson09_1();
lesson.test(2);
}
}
运行程序,我们可以看到使用this关键字时可以看到被隐藏的成员变量可以正常访问了。
2、this() 构造函数
在构造方法中可以使用 this() 来引用另一个构造方法。
public class Lesson {
private int minute=0;
Lesson(){
this(45);
}
Lesson(int minute){
this.minute = minute;
}
public static void main(String[] args){
Lesson lesson = new Lesson();
System.out.println(lesson.minute);
Lesson lesson2 = new Lesson(30);
System.out.println(lesson2.minute);
}
}
运行程序查看结果:
我们看到this(45),的确调用了另外一个带参数的构造方法。需要注意的是this()必须写在构造方法的第一行。
3、super.成员
当父类的成员变量被隐藏、成员方法被重写(覆盖),此时想使用父类的这些成员时就要用super关键字。我们改一下上面马和白马的例子:
Horse.java
public class Horse {
public int height =120;
//给马写个摆Pose的方法
public void pose(){
//样子很酷
System.out.println("Cool!");
}
}
WhiteHorse.java
public class WhiteHorse extends Horse {
public int height =150;
//白马重写了摆pose的方法
public void pose(){
//先摆一个马的pose
super.pose();
//白马更酷一点
System.out.println("Cool!!!!");
}
public void printHeight(){
//打印父类被隐藏的变量
System.out.println(super.height);
//打印实例变量
System.out.println(height);
}
public static void main(String[] args){
WhiteHorse xiaobai = new WhiteHorse();
xiaobai.pose();
xiaobai.printHeight();
}
}
运行程序查看结果:
我们看到在子类的方法里可以使用super来引用被隐藏的父类变量,被覆盖(重写)的父类方法。
4、super() 父类构造函数
讲super()之前,我们先看一下这个例子:
Horse.java
public class Horse {
public Horse(){
System.out.println("马类的构造函数");
}
}
WhiteHorse.java
public class WhiteHorse extends Horse {
public WhiteHorse(){
System.out.println("白马类的构造函数");
}
public static void main(String[] args){
new WhiteHorse();
}
}
运行程序查看结果:
我们看到,构造白马类之前,虚拟机先构造了它的父类马类,由此我们看到了白马类能继承马类的属性和方法的根本原因,原来每一个白马类同时也是一个马类,还是一个Object类。在创建对象时,一个对象的逐级父类自顶向下依次都创建了。
上图是一个白马对象在内存中的示意图,我们看到最外面的是WhiteHorse,内层还有一个Horse对象,更内层还有一个Object对象。
用下面的栈上的示意图可以更清晰的看到对象的创建过程。
首先调用的是main方法,main方法调用 new WhiteHorse() ,WhiteHorse()构造函数调用了一个默认的super(),super()方法就是父类的构造方法,以此类推最后调用了Object()构造方法。
5、带参数的的super()方法
在上面的例子里,我们看到编译器在你没有调用super()方法的时候,插入了一个默认的super()方法。可惜的是编译器并不会自动插入带参数的super(), 因此我们遇到这种情况就只能自己手工插入对super()的调用。
下面我们把上面的例子更改一下:
Horse.java的构造函数添加一个参数:
public class Horse {
protected int leg = 0;
public Horse(int leg){
this.leg=4;
System.out.println("马类的构造函数");
}
}
再次编译WhiteHorse.java,出错提示如下:
标准Java编译器的提示有点故作神秘,这个提示几乎什么都没说;我们换个工具,在中文Eclipse上的提示就明显多了:
我们按照它的提示更改一下WhiteHorse类:
public class WhiteHorse extends Horse {
public WhiteHorse(){
super(4);
System.out.println("白马类的构造函数");
}
public static void main(String[] args){
new WhiteHorse();
}
}
再次编译和运行程序,我们发现这次安然通过。
到这里,我们是不是可以小小总结一下,构造函数只能用new、this() 和 super() 的方式来访问,是不能像方法一样写方法名访问的。
本讲就到这里,下次再见。