1. 继承的意义(下)
1.1. 重写
1.1.1. 方法的重写
下面在昨天的基础之上来增加需求,在输出图形之前先打印格子坐标,即调用print()方法。想实现这个需求做法很简单,只需要父类型引用直接调用print()方法即可,因为print()方法是在父类中定义的,所以可以直接调用此方法。
但是,现在要求,不同的图形类型在打印输出之前先输出相应的语句,例如: TetrominoT对象调用print()方法后,增加输出“I am a T”,TetrominoJ对象调用print()方法后,增加输出“I am a J”。因为现在print()方法是在父类中定义的,只有一个版本,无论是T类对象还是J类对象调用,都将输出相同的数据,所以现在无法针对对象的不同而输出不同的结果。若想实现此需求,需要介绍一个概念,叫做方法的重写。
在java语言中,子类可以重写(覆盖)继承自父类的方法,即方法名和参数列表与父类的方法相同,但是方法的实现不同。
当子类重写了父类的方法后,该重写方法被调用时(无论是通过子类的引用调用还是通过父类的引用调用),运行的都是子类重写后的版本。看如下的示例:
- class Foo {
- public void f() {
- System.out.println("Foo.f()");
- }
- }
- class Goo extends Foo {
- public void f() {
- System.out.println("Goo.f()");
- }
- }
- class Test{
- public static void main(String[] args){
- Goo obj1 = new Goo();
- obj1.f();
- Foo obj2 = new Goo();
- obj2.f();
- }
- }
class Foo { public void f() { System.out.println("Foo.f()"); } } class Goo extends Foo { public void f() { System.out.println("Goo.f()"); } } class Test{ public static void main(String[] args){ Goo obj1 = new Goo(); obj1.f(); Foo obj2 = new Goo(); obj2.f(); } }
分析代码得出结论:输出结果均为“Goo.f()”,因为都是Goo的对象,所以无论是子类的引用还是父类的引用,最终运行的都是子类重写后的版本。
1.1.2. 重写中使用super关键字
在子类重写的方法中,可以通过super关键字调用父类的版本,参见如下的代码:
- class Foo {
- public void f() {
- System.out.println("Foo.f()");
- }
- }
- class Goo extends Foo {
- public void f() {
- super.f();
- System.out.println("Goo.f()");
- }
- }
- class Test{
- public static void main(String[] args){
- Foo obj2 = new Goo();
- obj2.f();
- }
- }
class Foo { public void f() { System.out.println("Foo.f()"); } } class Goo extends Foo { public void f() { super.f(); System.out.println("Goo.f()"); } } class Test{ public static void main(String[] args){ Foo obj2 = new Goo(); obj2.f(); } }
上面的代码中,super.f()即可以调用父类Foo的f()方法,此程序输出结果为:Foo.f() Goo.f()。这样的语法通常用于子类的重写方法在父类方法的基础之上进行的功能扩展。
1.1.3. 重写和重载的区别
重载与重写是完全不同的语法现象,区别如下所示:
- 重载: 是指在一个类中定义多个方法名相同但参数列表不同的方法,在编译时,根据参数的个数和类型来决定绑定哪个方法。
- 重写: 是指在子类中定义和父类完全相同的方法,在程序运行时,根据对象的类型(而不是引用类型)而调用不同的方法。
分析如下代码的输出结果:
- class Super {
- public void f() {
- System.out.println ("super.f()");
- }
- }
- class Sub extends Super {
- public void f() {
- System.out.println ("sub.f()");
- }
- }
- class Goo {
- public void g(Super obj) {
- System.out.println ("g(Super)");
- obj.f();
- }
- public void g(Sub obj) {
- System.out.println ("g(Sub) ");
- obj.f();
- }
- }
- class Test{
- public static void main(String[] args){
- Super obj = new Sub();
- Goo goo = new Goo();
- goo.g(obj);
- }
- }
class Super { public void f() { System.out.println ("super.f()"); } } class Sub extends Super { public void f() { System.out.println ("sub.f()"); } } class Goo { public void g(Super obj) { System.out.println ("g(Super)"); obj.f(); } public void g(Sub obj) { System.out.println ("g(Sub) "); obj.f(); } } class Test{ public static void main(String[] args){ Super obj = new Sub(); Goo goo = new Goo(); goo.g(obj); } }
分析如上代码,输出结果为:g(Super) sub.f()。
首先,重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法, 因为变量obj为Super类型引用, 所以,Goo的g(Super)被调用,先输出g(Super)。
重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法,因为obj实际指向的是子类Sub的对象,因此,子类重写后的f方法被调用,即sub.f()。
2. 访问控制
2.1. 包的概念
2.1.1. package语句
定义类时需要指定类的名称,但是如果仅仅将类名作为类的唯一标识,则不可避免的出现命名冲突问题,这会给组件复用以及团队间的合作造成很大的麻烦!因为原则上来说,类名是不可以重复的。
在Java语言中,命名冲突问题是用包(package)的概念来解决的,也就是说,在定义一个类时,除了定义类的名称一般还要指定一个包的名称,定义包名的语法如下所示:
- package 包名;
package 包名;
需要注意的是,在定义包时,package语句必须写在Java源文件的最开始处,即在类定义之前,如下面的语句将为Point类指定包名为“test”:
- package test;
- class Point{
- ……
- }
package test; class Point{ …… }
一旦使用package指定了包名,则类的全称应该是“包名.类名”,如上语句的Point类的全称为test.Point。
使用package即可以解决命名冲突问题,只要保证在同一个包中的类名不重复即可,而不同的包中可以定义相同的类名,例如:test1.Point和test2.Point是两个截然不同的名称,虽然类名相同,但包名不同,亦表示两个完全不同的类。
在命名包名时,包名可以有层次结构,在一个包中可以包含另外一个包,可以按照如下的方法定义package语句:
- package 包名1.包名2…包名n;
package 包名1.包名2…包名n;
在实际应用中,包的命名常常是多层次的。因为如果各个公司或开发组织的程序员都随心所欲的命名包名的话,依然不能从根本上解决命名冲突的问题,依然不利于软件的复用。因此,在指定包名时应该按照一定的规范,例如:
- org.apache.commons.lang.StringUtil
org.apache.commons.lang.StringUtil
如上类的定义可以分为4个部分,其中,StringUtil是类名,org.apache.commons.lang是多层包名,其含义如下:org.apache表示公司或组织的信息(是这个公司或组织域名的反写);commons表示项目的名称信息;lang表示模块的名称信息。
2.1.2. import语句
为了避免类的命名冲突问题,在声明类时指定了包名。这时,对该类的访问就需要使用如下所示的全称:
- org.whatisjava.core.Point p = new org.whatisjava.core.Point();
org.whatisjava.core.Point p = new org.whatisjava.core.Point();
可以看到,如上的书写方式过于繁琐,不便于书写。解决这个问题,可以通过import语句对类的全称进行声明,import语句的语法如下所示:
- import 类的全局限定名(即包名+类名);
import 类的全局限定名(即包名+类名);
如上的Point类可以使用如下的import语句进行声明:
- import org.whatisjava.core.Point;
import org.whatisjava.core.Point;
通过import语句声明了类的全称后,该源文件中就可以使用如下的方式,直接使用类名来访问了:
- package org.whatisjava.core;
- import org.whatisjava.core.Point;
- public class Main {
- public static void main(String[] args) {
- Point p = new Point(100, 200);
- }
- }
package org.whatisjava.core; import org.whatisjava.core.Point; public class Main { public static void main(String[] args) { Point p = new Point(100, 200); } }
有时,在import语句中也可以使用“*”符号,例如:
- import org.whatisjava.core.*;
import org.whatisjava.core.*;
如上的import语句意味着声明该包中所有类的全称,即,在该源文件中,使用所有包名为org.whatisjava.core的类都可以仅仅通过类名来访问。在此需要注意的是,“import 包名.*”语句并不包含该名的子包中的类(org.whatisjava中的类不包含)。
为了方便起见,在Eclipse中,可以使用“Ctrl+Shift+O”,自动完成import语句。
2.2. 访问控制修饰符
2.2.1. 封装的意义
假设有水果店卖水果,分两种方式进行管理,方式一为需要店员,由店员实现取水果、包装、找零等功能。方式二为不需要店员,由顾客自行完成取水果、包装、找零等功能。那么想一想,哪一种方式更适合管理呢?一般认为方式一更适合,因为方式二没有人来进行管理,安全性较低,除非来的都是活雷锋,完全靠自觉。而方式一的安全性更高一些,并非任何人都可以操作水果。
在软件系统中,常常通过封装来解决上面的问题。即:将容易变化的、具体的实现细节(卖水果)封装起来,外界不可访问,而对外提供可调用的、稳定的功能(店员),这样的意义在于:
- 降低代码出错的可能性,更便于维护。
- 当内部实现细节改变时,只要保证对外的功能定义不变,其他的模块不需要更改。
在软件系统中,封装常常需要依靠一些访问控制修饰符来实现。
2.2.2. public和private
private与public为最最常用的两个访问控制修饰符,其中,private修饰的成员变量和方法仅仅只能在本类中调用,而public修饰的成员变量和方法可以在任何地方调用。
private修饰的内容是对内实现的封装, 像刚刚案例中的水果, 就可以将它封装起来,因为,如果“公开”它将会增加维护的成本。public修饰的内容是对外提供的可以被调用的功能,需要相对稳定,相当于刚刚案例中的店员。
public与private关键字的用法参见如下代码:
- public class Point {
- private int x;
- private int y;
- Point(int x, int y) {…}
- public int distance(Point p) {…}
- }
- public class Test{
- Public static void main(String[] args)
- {
- Point p1 = new Point(1, 2);
- Point p2 = new Point(3, 4);
- p1.x = 100
- int d = p1.distance(p2);
- }
- }
public class Point { private int x; private int y; Point(int x, int y) {…} public int distance(Point p) {…} } public class Test{ Public static void main(String[] args) { Point p1 = new Point(1, 2); Point p2 = new Point(3, 4); p1.x = 100 int d = p1.distance(p2); } }
上面的代码中定义了Point类,类中包含两个private的成员变量,一个public的成员方法distance。在main方法中,声明Point类对象,对方法distance方法的访问执行正常,而通过p1.x = 100语句访问了Point类的x成员,此时将会出现错误。因为x定义为private的了,意味着只能在本类中访问,而现在是在另外一个类中,因此,是无法访问Point类的x成员的。
2.2.3. protected和默认访问控制
protected和默认访问控制也是两种访问修饰。其中,使用protected修饰的成员变量和方法可以被子类及同一个包中的类使用。而默认访问控制即不书写任何访问控制符,默认访问控制的成员变量和方法可以被同一个包中的类调用。
2.2.4. 访问控制符修饰类
对于类的修饰可以使用public和默认方式。 其中,public修饰的类可以被任何一个类使用,而默认访问控制的类只可以被同一个包中的类使用。
而protected和private访问修饰符是不可以修饰类的,但其可以修饰内部类(后面课程详细介绍)。
2.2.5. 访问控制符修饰成员
如上所介绍的4种访问修饰(public、private、protected、默认),都可以修饰成员,其权限如下图 – 1所示:
![](http://pdf7.tarena.com.cn/tts8_source/ttsPage/JAVA/JSD_N_V06/OOP/DAY04/SUPERDOC/01/index.files/image001.png)
图- 1
其中,public 修饰符,在任何地方都可以访问;protected可以在本类、同一包中的类、子类中访问,除此之外的其它类不可以访问;默认方式为可以本类及同一包中的类访问,除此之外其它类不可以访问;private只可以在本类中访问,其它任何类都不可以。
3. static和final
3.1. static关键字
3.1.1. static修饰成员变量
static关键字可以修饰成员变量,它所修饰的成员变量不属于对象的数据结构,而是属于类的变量,通常通过类名来引用static成员。
当创建对象后,成员变量是存储在堆中的,而static成员变量和类的信息一起存储在方法区, 而不是在堆中,
一个类的static成员变量只有“一份”(存储在方法区),无论该类创建了多少对象。看如下的示例:
- class Cat {
- private int age;
- private static int numOfCats;
- public Cat(int age) {
- this.age = age;
- System.out.println(++numOfCats);
- }
- }
class Cat { private int age; private static int numOfCats; public Cat(int age) { this.age = age; System.out.println(++numOfCats); } }
在main方法中声明两个Cat类对象:
- Cat c1 = new Cat( 2);
- Cat c2 = new Cat( 3);
Cat c1 = new Cat( 2); Cat c2 = new Cat( 3);
其内存分配如下图-2所示:
![](http://pdf7.tarena.com.cn/tts8_source/ttsPage/JAVA/JSD_N_V06/OOP/DAY04/SUPERDOC/01/index.files/image002.png)
图- 2
如上的代码中,声明两个Cat类对象后,numOfCats的值为2。当声明第一个Cat类对象后,numOfCats值增1变为1,声明第二个Cat类对象后,因为numOfCats存在方法区中并且只有一份,所以其值在刚刚的1的基础之上变为2。
3.1.2. static修饰方法
通常的方法都会涉及到对具体对象的操作,这些方法在调用时,需要隐式的传递对象的引用(this),如下代码所示,在调用distance方法时,除了传递p2参数外,还隐式的传递了p1作为参数,在方法中的this关键字即表示该参数:
- int d = p1.distance(p2);
int d = p1.distance(p2);
而static修饰的方法则不需要针对某些对象进行操作,其运行结果仅仅与输入的参数有关,调用时直接用类名引用即可,如下代码所示:
- double c = Math.sqrt(3.0 * 3.0 + 4.0 * 4.0);
double c = Math.sqrt(3.0 * 3.0 + 4.0 * 4.0);
上面的方法在调用时,没有隐式的传递对象引用,因此在static方法中是不可以使用this关键字的。另外,由于static在调用时没有具体的对象,因此在static方法中不能对非static成员(对象成员)进行访问。
static方法的作用在于提供一些“工具方法”和“工厂方法”(后面课程详细介绍)等。像如下的一些工具方法,只是完成某一功能,不需要传递this:
- Point.distance(Point p1, Point p2)
- RandomUtils.nextInt()
- StringUtils.leftPad(String str, int size, char padChar);
- Math.sqrt() Math.sin() Arrays.sort()
Point.distance(Point p1, Point p2) RandomUtils.nextInt() StringUtils.leftPad(String str, int size, char padChar); Math.sqrt() Math.sin() Arrays.sort()
3.1.3. static块
static块为属于类的代码块,在类加载期间执行的代码块,只执行一次,可以用来在软件中加载静态资源(图像、音频等等)。如下代码演示了static块的执行:
- class Foo {
- static {
- //类加载期间,只执行一次
- System.out.println(" Load Foo.class ");
- }
- public Foo() {
- System.out.println("Foo()");
- }
- }
- class Test{
- public static void main(String[] args){
- Foo foo = new Foo();
- }
- }
class Foo { static { //类加载期间,只执行一次 System.out.println(" Load Foo.class "); } public Foo() { System.out.println("Foo()"); } } class Test{ public static void main(String[] args){ Foo foo = new Foo(); } }
上面代码的输出结果为:
- Load Foo.class
- Foo();
因为,在Foo类加载时,先运行了静态块,而后执行了构造方法,即,static块是在创建对象之前执行的。
3.2. final关键字
3.2.1. final修饰变量
final关键字修饰变量,意为不可改变。final可以修饰成员变量,也可以修饰局部变量,当final修饰成员变量时,可以有两种初始化方式:
- 声明同时初始化
- 构造函数中初始化
final关键字修饰局部变量,在使用之前初始化即可。参见如下示例:
- public class Emp {
- private final int no = 100; // final成员变量声明时初始化
- public static void main(String[] args) {
- no = 99;
- }
- }
public class Emp { private final int no = 100; // final成员变量声明时初始化 public static void main(String[] args) { no = 99; } }
如上的语句,no=99会出现编译期错误,因为final的变量不可被改变。
3.2.2. final修饰方法
final关键字修饰的方法不可以被重写。使一个方法不能被重写的意义在于:防止子类在定义新方法时造成的“不经意”重写。参见下面的代码:
- public class Car {
- // 点火
- public void fire() {…}
- … … …
- }
- public class Tank extends Car {
- // 开炮
- public void fire() {…}
- … … …
- }
public class Car { // 点火 public void fire() {…} … … … } public class Tank extends Car { // 开炮 public void fire() {…} … … … }
上面的代码中,Car类有一个方法为fire()点火,当点火后即汽车启动,坦克类Tank继承自Car类,重写了fire()点火方法,而Tank类的点火即为开炮,而非坦克启动。此即Tank类误重写了Car类的fire()方法。
若想避免这种情况发生,可以将Car类的fire()方法声明为final的,那样该方法将不可以被子类重写了,如下代码所示:
- public class Car {
- // 点火
- public final void fire() {…}
- … … …
- }
public class Car { // 点火 public final void fire() {…} … … … }
3.2.3. final修饰类
final关键字修饰的类不可以被继承。使一个类不能被继承的意义在于:可以保护类不被继承修改,可以控制滥用继承对系统造成的危害。在JDK中的一些基础类库被定义为final的,例如:String、Math、Integer、Double 等等。自己定义的类也可以声明为final的,如下代码所示:
- final Foo { }
- class Goo extends Foo { }
final Foo { } class Goo extends Foo { }
上面的代码中,声明了final的Foo,而后Goo继承了Foo,此句会出现编译错误,因为final修饰的类不可以被继承。
3.2.4. static final常量
static final 修饰的成员变量称为常量,必须声明同时初始化,并且不可被改变。常量建议所有字母大写。
实际应用中应用率较广,因为static final常量是在编译期被替换的,可以节约不必要的开支,如下代码演示了static final的用法:
- class Foo {
- public static final int NUM = 100;
- }
- class Goo {
- public static void main(String[] args) {
- Sytem.out.println(Foo.NUM);
- // 代码编译时,会替换为:System.out.println(100);
- }
- }
class Foo { public static final int NUM = 100; } class Goo { public static void main(String[] args) { Sytem.out.println(Foo.NUM); // 代码编译时,会替换为:System.out.println(100); } }
说明:static final常量Foo.NUM会在编译时被替换为其常量值(100),在运行Goo类时,Foo类不需要被载入。这样减少了不必要的开支。