7.1 类的封装
封装是面向对象编程的核心思想,将对象的属性和行为封装起来,其级体就是类本节将谭福介绍如何将类封装。
举一个简单的例子:我到一个餐馆去吃饭,点了一盘香辣肉丝,感觉很好吃,我就想知道厨师的名字,希望让厨师再为我多做点事情。
例7.1
package Leiduixiang; //包名
public class Restaurant1 { //创建类
public static void main(String[] args) { //主方法
// TODO Auto-generated method stub
String cookName="Tom Cruise"; //厨师的名字叫Tom Cruise
System.out.println("**请让厨师为我做一份香辣肉丝。***"); //让厨师为我做一份香辣肉丝
System.out.println(cookName+"切葱花"); //让厨师切葱花
System.out.println(cookName+"洗蔬菜"); //让厨师洗蔬菜
System.out.println(cookName+"开始烹饪"+"香辣肉丝"); //厨师开始烹饪香辣肉丝
System.out.println("**请问厨师叫什么名字?***"); //厨师叫什么名字?
System.out.println(cookName); //输出厨师的名字
System.out.println("**请让厨师给我切一点葱花。***"); //让厨师给我切一点葱花
System.out.println(cookName+"切葱花"); //让厨师切葱花
}
}
例7.1运行结果
所有的逻辑代码全是在main方法中实现的,代码完全暴露,我可以任意删改。如果能随意修改代码,就无法正常运作。为防止其他人修改厨师行为将厨师单独封装成一个类,将厨师的工作定义成厨师类的行为。
System.out.println("**请让厨师给我切一点葱花。***");
System.out.println(cookName+"搅鸡蛋"); //被乱改之后
System.out.println("**请让厨师为我做一份清蒸鱼。***");
System.out.println(cookName+"你是我的小呀小苹果~");//被乱改之后
如何防止其他人修改厨师的行为呢?就是将厨师打包成类。
例7.2
package Leiduixiang;
public class Restaurant2 { //创建类
public static void main(String[] args) { //主方法
// TODO Auto-generated method stub
cook1 cook=new cook1(); // 创建厨师类的对象
System.out.println("**请让厨师为我做一份香辣肉丝。***"); //输出信息
cook.cooking("香辣肉丝"); // 厨师烹饪香辣肉丝
System.out.println("**你们的厨师叫什么名字?***"); //输出**你们的厨师叫什么名字?***
System.out.println(cook.name); // 厨师回答自己的名字
System.out.println("**请让厨师给我切一点葱花。***"); //输出**请让厨师给我切一点葱花。***
cook.cutOnion(); // 厨师去切葱花
}
}
class cook1{ //创建Cook1类
String name; // 厨师的名字
public cook1() { //普通类
this.name="Tom Cruise"; // 厨师的名字叫Tom Cruise
}
void cutOnion() { //输出厨师切葱花
System.out.println(name+"切葱花"); //输出厨师切葱花
}
void washvegetables() { // 厨师洗蔬菜
System.out.println(name+"洗蔬菜"); //输出厨师洗蔬菜
}
void cooking(String dish) { // 厨师烹饪顾客点的菜
washvegetables(); //洗蔬菜
cutOnion(); //切葱花
System.out.println(name+"开始烹饪"+dish); //输出name + "开始烹饪" + dish
}
}
例7.2运行结果
将厨师单独封装成一个类,将厨师的工作定义成厨师类的行为,当我们想让厨师做菜,只能道过调用对象成员方法的方式实现,而我们却不知道这个方法到底是怎么写的,所以就无法随意修改了。餐馆没有义务告诉我们厨师的任何信息,并且厨师也不会随意受我们差遣,所以说厨师有些属性和行为是不予公开的。
例7.3
package Leiduixiang; //类包
public class zy { //创建类
public static void main(String[] args) { //主方法
cook2 cook=new cook2(); //创建类
System.out.println("**请让厨师为我做一份香辣肉丝。***"); //输出请让厨师为我做一份香辣肉丝
cook.cooking("香辣肉丝"); //输出香辣肉丝
System.out.println("**你们的厨师叫什么名字?***"); //输出你们的厨师叫什么名字?
System.out.println(cook.name); //输出厨师的名字
System.out.println("**请让厨师给我切一点葱花。***"); //输出请让厨师给我切一点葱花
cook.cutOnion(); // 厨师去切葱花
}}
class cook2{ //创建类
private string name; // 厨师的名字
public cook2() { //创建Cook2()
this.name="Tom Cruise"; // 厨师的名字叫Tom Cruise
}
private void cutOnion() { // 厨师切葱花
System.out.println(name+"切葱花"); //输出Tom Cruise切葱花
}
private void washVegetables() { // 厨师洗蔬菜
System.out.println(name+"洗蔬菜"); //Tom Cruise洗蔬菜
}
void cooking(String dish) { // 厨师烹饪顾客点的菜
washVegetables(); //洗蔬菜
cutOnion() ; //切洋葱
System.out.println(name+"开始烹饪"+dish); // 输出厨师烹饪顾客点的菜
}
}
例7.3运行结果
例7.4
package Leiduixiang; //类包
public class Restaurant4 { //创建类
private cook2 cook=new cook2(); //创建厨师类的对象
private void takeOrder(String dish) { //下单
cook.cooking(dish); //通知厨师做菜
System.out.println("你的菜好了,请慢用."); //输出你的菜好了,请慢用
}
public String saySorry() { //拒绝顾客请求
return"抱歉,餐厅不提供此项服务."; //输出抱歉,餐厅不提供此项服务
}
public static void main(String[] args) { //主方法
Restaurant4 water=new Restaurant4(); //创建新数组
System.out.println("**请让厨师为我做一份香辣肉丝。***"); //输出请让厨师为我做一份香辣肉丝
water.takeOrder("鱼香肉丝"); //服务员给顾客下单
System.out.println("**请问厨师叫什么名字?***"); //输出请问厨师叫什么名字
System.out.println(water.saySorry()); //服务员给顾客善意的答复
System.out.println("**请让厨师给我切一点葱花。***"); //输出请让厨师给我切一点葱花
System.out.println(water.saySorry()); //服务员给顾客善意的答复
}
}
例7.4运行结果
从这个例子我们就能看出,作为顾客,我始终是和服务员进行交流,再由服务员与厨师进行交流整个过程中,顾客与厨师是完全没有交集的。作为顾客,我不知道我品尝的美食是由哪位厨师用何种方法烹任出来的,这种编程模式,就是封装。
将对象的属性和行为封装起来的载体就是类,类通常对客户隐藏其实现细节,这就是封装的思想。
7.2 类的继承
继承在面向对象开发思想中是一个非常重要的概念,它使整个程序架的具有一定的单色,在程
序中复用已经定义完善的类不仅可以减少软件开发周期,还可以提高软件的可能也都于程
本节将详细讲解类的继承。
在第6章中曾简要介绍过继承,其基本思想是基于某个父类的扩展,制定出一个新的子类,子类可以继承父类原有的属性和方法,也可以增加原来父类所不具备的属性和方法,或者直接重写又类中的某些方法。例如,平行四边形是特殊的四边形,可以说平行四边形类继束了四边形类,这时平行四边形类将所有四边形具有的属性和方法都保留下来,并基于四边形类扩展了一些新的平行四边形类特有的属性和方法。
下面演示一下继承性。创建一个新类Test,同时创建另一个新类Test2继承 Test类,其中包括重写的父类成员方法(重写的概念将在下文中详细介绍)以及新增成员方法等。在图7.5中描述了类Test与Test2的结构以及两者之间的关系。
7.2.1 extends 关键字
在Java中,让一个类继承另一个类,用extends关键字,语法如下:
child extends parents
这里child这个类作为子类继承了parents 这个类,并继承parents中的属性和方法。
child extends parents
举一个简单的例子:每个人都用过计算机,最常见的计算机就是台式机。后来随着科技的发展,计算机变得越来越小,台式机改良成了可移动的笔记本电脑,笔记本电脑又改良成了更轻薄的平板电脑。我们可以把普通计算机看成一个类,那么笔记本电脑和平板电脑都是这个类衔生出的子类。
例7.5
class Computer {//父类:电脑
String screen = "液晶显示屏"; //定义初值
void startup() { //返回参数
System.out.println("电脑正在开机,请稍等…"); //输出电脑正在开机,请等待...
}
}
public class Pad extends Computer { //父类:电脑
String battery ="5000毫安电池"; //子类独有属性
public static void main(String[] args) { //主方法
Pad pc = new Pad(); //电脑类
System.out.println("computer的屏幕是: " + pc.screen); //输出computer的屏幕是
pc.startup(); //返回参数
Pad ipad = new Pad(); //平板电脑类
System.out.println("pad的屏幕是:"+ ipad.screen); //子类可以直接使用父类属性
System.out.println("pad的电池是:"+ ipad.battery); //子类独有的属性
ipad.startup(); //子类可以直接使用父类方法
}
}
例7.5运行结果
7.2.2 方法的重写
1.重写的实现
继承并不只是扩展父类的功能,还可以重写父类的成员方法。重写(还可以称为覆盖)就是在子类中将父类的成员方法的名称保留,重新编写成员方法的实现内容,更改成员方法的存储权限,或是修改成员方法的返回值类型(重写父类成员方法的返回值类型是基于J2SE 5.0版本以上编译器提供的新功能)。在继承中还有种特殊的重写方式, 子类与父类的成员方法返回值、 方法名称、参数类型及个教完全相同,唯一不同的是方法实现内容,这种特殊重写方式被称为重构。
子类重写父类的方法还可以修改方法的返回值类型,但这只是在J2SE 5.0以上的版本中支持的面功能,但这种重写方式需要遵循一个原则,即重写的返回值类型必须是父类中同一方法返回值关主的子类。
例7.6
class Computer2 { //创建类
void showPicture() { //显示图片
System.out.println("鼠标单击"); //输出鼠标单击
}
}
public class Pad2 extends Computer2{ //子类:平板电脑
void showPicture() { //显示图片
System.out.println("手指点击触摸屏"); //输出手指点击触摸屏
}
public static void main(String[] args) { //主方法
Computer2 pc = new Computer2(); //电脑类
System.out.print("pc打开图片: "); //输出PC打开图片
pc.showPicture(); //调用方法
Pad2 ipad = new Pad2(); //平板电脑类
System.out.print("ipad打开图片:"); //输出ipad打开图片
ipad.showPicture(); //重写父类方法
Computer2 computerpad = new Pad2(); //父类声明,子类实现
System.out.print("computerpad打开图片:"); //输出computerpad打开图片
computerpad.showPicture(); //调用父类方法,实现子类重写的逻辑
}
}
例7.6运行结果
从这个结果我们可以看出,虽然子类调用了父类的方法,但实现的是子类重写后的逻辑, 而不是父类原有的逻辑。如果父类声明的对象是由子类实例化的,那么这个对象所调用的方法也是被子类重写过的。
2.super 关键字
如果子类重写了父类的方法,就再也无法调用到父类的方法了吗?如果想在子类的方法中实现父类原有的方法怎么办?为了解决这种需求,Java 提供了super 关键字。
super关键字的使用方法与this关键字类似。this 关键字代表本类对象,super 关键宇代表父类对象。使用方法如下:
super.property; //调用父类的属性
super.method(); //调用父类的方法
例7.7
class Computer3{ //创建类
String sayHello() { //定义一个方法
return "欢迎使用"; //返回一个字符串
}
}
public class Pad3 extends Computer3{ //子类:平板电脑
String sayHello() { //重写方法
return super.sayHello() +"平板电脑"; //调用父类方法,在其结果后添加字符串
}
public static void main(String[] args) { //主方法
Computer3 pc = new Computer3(); //电脑类
System.out.println(pc.sayHello()); //调用父类的方法并输出
Pad3 ipad = new Pad3(); //平板电脑类
System.out.println(ipad.sayHello()); //调用子类的方法并输出
}
}
例7.7运行结果
7.2.3 所有类的父类——Object类
在开始学习使用class关键字定义类时,就应用了继承原理,因为在Java中,所有的类都直接或间接继承了java.lang.Object 类。Object 类是比较特殊的类,它是所有类的父类,是Java类层中的最高层类。当创建一一个类时,总是在继承,除非某个类已经指定要从其他类继承,否则它就是从java.lang.Object类继承而来的,可见Java中的每个类都源于java.lang.Object 类,如String、Integer 等类都是继承于Object类;除此之外自定义的类也都继承于Object类。由于所有类都是Object子类,所以在定义类时,省略了extends Object关键字,如图7.10所示便描述了这一原则。
在Object类中主要包括clone(、finalize(、equalsO、toString0等方法,其中常用的两个方法为equals()和 toString0方法。由于所有的类都是Object 类的子类,所以任何类都可以重写Object类中的方法。
Object的重要方法:
1. getClass()方法
getClass0方法是Object类定义的方法,它会返回对象执行时的Class实例,然后使用此实例调用getName0方法可以取得类的名称。
getClass() . getName();
可以将getClass0方法与toString0方法联合使用。
2. toString()方法
toString0方法的功能是将一一个对象返回为字符串形式, 它会返回一 个String 实例。在实际的应用中通常重写toStringO方法,为对象提供一个特定的输出模式。当这个类转换为字符串或与字符串连接时,将自动调用重写的toString0方法。的重要方法。
例7.8
public class li78 { //创建类
public String toString() { //重写toString()方法
return "在" + getClass().getName() + "类中重写toString()方法"; //输出类中重写toString()方法
}
public static void main(String[] args) { //主方法
System.out.println(new ObjectInstance()); //打印本类对象
}
}
例7.8运行结果
在本实例中重写父类Object的toString()方法,在子类的toString0方法中使用Object 类中曲getClass(方法获取当前运行的类名,定义- -段输出字符串,当用户打印ObjectInstance类对象时,将自动调用toString()方法。
3. equals()方法
前面章节曾讲解过equalsO方法,当时是比较“==”运算符与equalsl0方法,说明“==”比较的是两个对象的引用是否相等,而equals0方法比较的是两个对象的实际内容。
例7.9
class V { // 自定义类v
}
public class OverWriteEquals { //创建类
public static void main(String[] args) { //主方法
String s1 = "123"; // 实例化两个对象,s1内容相同
String s2 = "123"; // 实例化两个对象,s2内容相同
System.out.println(s1.equals(s2)); // 使用equals()方法调用
V v1 = new V(); // 实例化两个新V类对象
V v2 = new V(); // 实例化两个新V类对象
System.out.println(v1.equals(v2)); // 使用equals()方法比较v1与v2对象
}
}
}
例7.9运行结果
从本实例的结果中可以看出,在自定义的类中使用equals0方法进行比较时,将返回false, 因为equals0方法的默认实现是使用“==”运算符比较两个对象的引用地址,而不是比较对象的内容,所以要想真正做到比较两个对象的内容,需要在自定义类中重写equals()方法。
7.3 类的多态
多态意为一个名字可具有多种语义,在程序设计语言中,多态性是指“一一种定义,多种实现”,例如,运算符“+”作用于两个整型量时是求和,而作用于两个字符型量时则是将其连接在一起。利用多态可以使程序具有良好的扩展性,并可以对所有类对象进行通用的处理。类的多态性可以从两方面体现:一是方法的重载,二是类的上下转型,本节将分别对它们进行详细讲解。
7.3.1 方法的重载
在第6章中曾学习过构造方法,知道构造方法的名称由类名决定,所以构造方法只有一一个名称,但如果希望以不同的方式来实例化对象,就需要使用多个构造方法来完成。由于这些构造方法都需要根据类名进行命名,为了让方法名相同而形参不同的构造方法同时存在,必须用到“方法重载”。虽然方法重载起源于构造方法,但是它也可以应用到其他方法中。本节将讲述方法的重载。
东法的重获就是在网个类中允许网时存在个以 上的同名方法,只要这些方法的参数个数或类型不同即可。
例7.10
public class OverLoadTest { //创建类
public static int add(int a) { // 定义一个方法
return a; //定义a
}
public static int add(int a, int b) { // 定义与第一个方法参数个数不同的方法
return a + b; //定义a+b
}
public static double add(double a, double b) { // 定义与第一个方法相同名称、参数类型不同的方法
return a + b; //定义a+b
}
public static int add(int a, double b) { // 定义一个成员方法
return (int) (a + b); //定义a+b
}
public static int add(double a, int b) { // 这个方法与前一个方法参数次序不同
return (int) (a + b); //定义a+b
}
public static int add(int... a) { // 定义不定长参数
int s = 0; //定义s初值
for (int i = 0; i < a.length; i++) { // 根据参数个数循环操作
s += a[i]; // 将每个参数的值相加
}
return s; // 将计算结果返回
}
public static void main(String args[]) { //主方法
System.out.println("调用add(int)方法:" + add(1)); //输出结果
System.out.println("调用add(int,int)方法:" + add(1, 2)); //输出结果
System.out.println("调用add(double,double)方法:" + add(2.1, 3.3)); //输出结果
System.out.println("调用add(int a, double b)方法:" + add(1, 3.3)); //输出结果
System.out.println("调用add(double a, int b) 方法:" + add(2.1, 3)); //输出结果
System.out.println("调用add(int... a)不定长参数方法:"+ add(1, 2, 3, 4, 5, 6, 7, 8, 9)); //输出结果
System.out.println("调用add(int... a)不定长参数方法:" + add(2, 3, 4)); //输出结果
}
}
例7.10运行结果
7.3.2 向上转型
对象类型的转换在Java编程中经常遇到,主要包括向上转型与向下转型操作。本节将首先介绍向上转型。
因为平行四边形是特殊的四边形,也就是说平行四边形是四边形的一种,那么就可以将平行四边形对象看作是一一个四边形对象。例如,鸡是家禽的一种, 而家禽是动物中的一一类,那么也可以将鸡对象看作是一个动物对象。
例7.11
class Quadrangle { //创建类
public static void draw(Quadrangle q) { // 四边形类中的方法
}
}
public class Parallelogram extends Quadrangle{ //子类继承父类
public static void main(String[] args) { //主方法
Parallelogram p = new Parallelogram(); // 实例化平行四边形类对象引用
draw(p); // 调用父类方法
}
}
平行四边形类继承了四边形类,四边形类存在一一个 draw0方法,它的参数是Quadranol(四边形类)类型,而在平行四边形类的主方法中调用draw0时给予的参数类型却是Prllelogram (平行四边形类)类型的。这里一直在强调- 一个问题, 就是平行四边形也是种类型的四边形, 所以可以物平行四边形类的对象看作是个四边形类的对象, 这就相当于“Quadrangleobj = new Parallelogram0;”,就是把子类对象赋值给父类类型的变量,这种技术被称为“向上转型”。试想一.下正方形类对象可以作为draw0方法的参数,梯形类对象同样也可以作为draw0方法的参数,如果在四边形类的draw0方法中根据不同的图形对象设置不同的处理,就可以做到在父类中定义一个方法完成各个子类的功能,这样可以使同一份代码毫无差别地运用到不同类型之上,这就是多态机制的基本思想。平行四边形类继承了四边形类,平行四边形类 与四边承图都是将顶级类设置在页面的顶部,然后逐渐向下,所以将子类对形类的关系对象看作是父类对象被称为“向上转型”。由于向上转型是从一个较具体的类到较抽象的类的转换,所以它总是安全的,如可以说平行四边形是特殊的四边形,但不能说四边形是平行四边形。
7.3.3 向下转型
通过向上转型可以推理出向下转型是将较抽象类转换为较具体的类。这样的转型通常会出现问题,例如,不能说四边形是平行四边形的一种、 所有的鸟都是鸽子,因为这非常不合乎逻辑。可以说子类对象总是父类的一个实例, 但父类对象不一定是子类的实例。
例7.12
public class ParaIIelogram extends Quadrangle{ //子类继承父类
public static void main(String args[]) { //主方法
draw(new ParaIIelogram()); // 将平行四边形类对象看作是四边形对象,称为向上转型操作
Quadrangle q = new ParaIIelogram(); // 将父类对象赋予子类对象
Parallelogram p = (Parallelogram) q; //将父类对象赋予子类对象,并强制转换为子类型
}
}
例7.12运行结果
如果将父类对象直接赋予子类,会发生编译器错误,因为父类对象不一定是子类的实例。例如,一- 个四边形不一定就是指平行四边形,它也许是梯形,也许是正方形,也许是其他带有四条边的不规则图形。
越是具体的对象具有的特性越多,越抽象的对象具有的特性越少。在做向下转型操作时,将特性范围小的对象转换为特性范围大的对象肯定会出现问题,所以这时需要告知编译器这个四边形就是平行四边形。将父类对象强制转换为某个子类对象,这种方式称为显式类型转换。
7.3.4 instanceof 关键字
当在程序中执行向下转型操作时,如果父类对象不是子类对象的实例,就会发生Class,CastException异常,所以在执行向下转型之前需要养成一个 良好的习惯,就是判断父类对象是否子类对象的实例。这个判断通常使用instanceof操作符来完成。可以使用instanceof操作符判断是口一个类实现了某个接口,也可以用它来判断一个实例对象是否属于个类。
instanceof的语法格式如下:
myobject instanceof ExampleClass
myobject: 某类的对象引用。
ExampleClass: 某个类。
使用instanceof操作符的表达式返回值为布尔值。如果返回值为true, 说明myobject对象对ExampleClas的实例对象;如果返回值为false, 说明myobjet对象不是ExmpleClas的实例对象。
例7.13
class Square extends Quadrangle { //主函数
// SomeSentence
}
class Anything { //普通类
// SomeSentence
}
public class ParaIlelogram extends Quadrangle { //子类继承父类
public static void main(String args[]) { //主方法
Quadrangle q = new Quadrangle(); // 实例化父类对象
if (q instanceof ParaIlelogram) { // 判断父类对象是否为Parallelogram子类的一个实例
ParaIlelogram p = (ParaIlelogram) q; // 进行向下转型操作
}
if (q instanceof Square) { // 判断父类对象是否为Parallelogram子类的一个实例
Square s = (Square) q; // 进行向下转型操作
}
System.out.println(q instanceof Anything); // 由于q对象不为Anything类的对象,所以这条语句是错误的
}
}
在本实例中将instanceof 操作符与向下转型操作结合使用。在程序中定义了两个子类,即平行四边形类和正方形类,这两个类分别继承四边形类。在主方法中首先创建四边形类对象,然后使用instanceof操作符判断四边形类对象是否为平行四边形类的一 个实例,是否为正方形类的一 个实例,如果判断结果为true,将进行向下转型操作。
7.4 抽象类与接口
通常可以说四边形具有4条边,或者更具体点,平行四边形是具有对边平行且相等特性的特殊四边形,等腰三角形是其中两条边相等的三角形,这些描述都是合乎情理的,但对于图形对象却不能使用具体的语言进行描述,它有几条边,究竟是什么图形,没有人能说清楚,这种类在Java中被定义为抽象类。
7.4.1 抽象类与抽象方法
在解决实际问题时,一般将父类定 义为抽象类,需要使用这个父类进行继承与多态处理。回想继承和多态原理,继承树中越是在上方的类越抽象,如鸽子类继承鸟类、鸟类继承动物类等。在多态机制中,并不需要将父类初始化对象,我们需要的只是子类对象,所以在Java语言中设置抽象类不可以实例化对象,因为图形类不能抽象出任何一一种具 体图形,但它的子类却可以。
Java中定义抽象类时,需要使用abstract关键字,其语法如下:
[权限修饰符] abstract class 类名{
类体
}
使用absrnat关键字定义的类称为抽象类,而使用abtect关键字定义的方法称为抽象方法,抽象方法的定义语法如下:
[权限修饰符] absreact 方法返回值类型 方法名(参数列表);
从上面的语法可以看出,抽象方法是直接以分号结尾的,它没有方法体,抽象方法本身没有任何意义,除非它被重写, 而承载这 个抽象方法的抽象类必须被继承, 实际上,抽象类除了被继承之外没有任何意义。
继承抽象类的所有子类都需要将抽象类中的抽象方法进行覆盖,这样在多态机制中,就可以将父类修改为抽象类,将draw(方法设置为抽象方法,然后每个子类都重写这个方法来处理。
例7.14
package tset714; //类包
public abstract class Market { //创建类
public String name; //商场名称
public String goods; //商品名称
public abstract void shop(); //抽象方法,用来输出信息
}
public class TaobaoMarket extends Market{ //创建子类
@Override
public void shop() { //购物
// TODO Auto-generated method stub
System.out.println(name+"网购"+goods); //输出网购+goods
}
}
public class WallMarket extends Market{ //创建子类
@Override
public void shop() { //购物
// TODO Auto-generated method stub
System.out.println(name+"实体店购买"+goods); //输出实体店购买+goods
}
}
public class GoShopping { //创建类
public static void main(String[] args) { //主方法
Market market = new WallMarket();// 使用派生类对象创建抽象类对象
market.name = "沃尔玛"; //沃尔玛
market.goods = "七匹狼西服"; //七匹狼西服
market.shop(); //创建商店
market = new TaobaoMarket(); // 使用派生类对象创建抽象类对象
market.name = "淘宝"; //淘宝
market.goods = "韩都衣舍碎花裙"; //韩都衣舍碎花裙
market.shop(); //创建商店
}
}
例7.14运行结果
综上所述,使用抽象类和抽象方法时,需要遵循以下原则:
(1)在抽象类中,可以包含抽象方法,也可以不包含抽象方法,但是包含了抽象方法的类必须被定义为抽象类。
(2)抽象类不能直接实例化,即使抽象类中没有声明抽象方法,也不能实例化。
(3)抽象类被继承后,子类需要实现其中所有的抽象方法。
(4)如果继承抽象类的子类也被声明为抽象类,则可以不用实现父类中所有的抽象方法。
使用抽象类时,可能会出现这样的问题:程序中会有太多冗余的代码,同时这样的父类局限性很大,例如,上面的例子中,也许某个不需要shop()方法的子类也必须重写shop()方法。如果将这个shop()方法从父类中拿出,放在别的类里,又会出现新问题,就是某些类想要实现“买衣服”的场景,竟然需要继承两个父类。Java 中规定,类不能同时继承多个父类,面临这种问题时,接口的概念便出现了。
7.4.2 接口的声明及实现
接口是抽象类的延伸,可以将它看作是纯粹的抽象类,接口中的所有方法都没有方法体。对于7.4.1小节中遗留的问题,可以将draw()方法封装到一个接口中,这样可以让一个类既能继承图形类,又能实现draw()方法接口,这就是接口存在的必要性。在图7.21中描述了各个子类继承图形类后使用接口的关系。
接口使用interface 关键字进行定义,其语法如下:
[修饰符] interface 接口名[extends 父接口名列表] {[public] [static] [final] 常量;
[public] [abstract] 方法;
}
修饰符:可选,用于指定接口的访问权限,可选值为public。 如果省略则使用默认的访问权限。
接口名: 必选参数,用于指定接口的名称,接口名必须是合法的Java标识符。一般情况下,要求首字母大写。
extends 父接口名列表:可选参数,用于指定要定义的接口继承于哪个父接口。当使用extends关键字时,父接口名为必选参数。
方法:接口中的方法只有定义而没有被实现。
一个类实现一 一个接口可以使用implements 关键字,代码如下:
public class Parallelogram extends Quadrangle implements drawTest{undefined
...//
}
例7.15
interface drawTest { // 定义接口
public void draw(); // 定义方法
}
class ParallelogramgleUseInterface implements drawTest { // 定义平行四边形类,该类实现了drawTest接口
public void draw() { // 由于该类实现了接口,所以需要覆盖draw()方法
System.out.println("平行四边形.draw()"); //输出平行四边形
}
}
class SquareUseInterface implements drawTest { //定义正方形类,该类实现了drawTest接口
public void draw() { // 由于该类实现了接口,所以需要覆盖draw()方法
System.out.println("正方形.draw()"); // 输出正方形
}
}
public class QuadrangleUseInterface { //创建类
public static void main(String[] args) { //主方法
drawTest[] d = { // 接口也可以进行向上转型操作
new SquareUseInterface(), new ParallelogramgleUseInterface() }; //新建数组
for (int i = 0; i < d.length; i++) { //控制长度,累加
d[i].draw(); // 调用draw()方法
}
}
}
例7.15运行结果
在本实例中,正方形类与平行四边形类分别实现了drawTest接口,所以需要覆盖接口中的方法。在调用draw()方法时,首先将平行四边形类对象与正方形类对象向上转型为drawTest接口形式。这里也许很多读者会有疑问,接口是否可以向上转型?其实在Java 中无论是将一个类向上转型为父类对象,还是向上转型为抽象父类对象,或者向上转型为该类实现接口,都是没有问题的。然后使用d[i]数组中的每一个对象调用draw(), 由于向上转型,所以d[i]数组中的每一个对象分别代表正方形类对象与平行四边形类对象,最后结果分别调用正方形类与平行四边形类中覆盖的draw()方法。
7.4.3 多重继承
在Java中类不允许多重继承,但使用接口就可以实现多重继承,因为一个类可以同时实现多个接口,这样可以将所有需要实现的接口放置在implements关键字后并使用逗号“,”隔开,但这可能会在一个类中产生庞大的代码量,因为继承一个接口时需要实现接口中所有的方法。
例7.16
package Jiating; //类包
public interface Father { //定义一个接口
void smoking(); //抽烟的方法
void goFishing(); //钓鱼的方法
}
public interface Mother { //定义一个接口
void watchTV(); //看电视的方法
void cooking(); //做饭的方法
}
public class Son implements Father,Mother{ //继承IFather接口和IMother接口
@Override
public void watchTV() { //重写watchTV()方法
System.out.println("我喜欢看电视"); //输出我喜欢看电视
}
@Override
public void cooking() { //重写cooking()方法
System.out.println("我喜欢做饭"); //输出我喜欢做饭
}
@Override
public void smoking() { //重写smokeing()方法
System.out.println("我喜欢抽烟"); //输出我喜欢抽烟
}
@Override
public void goFishing() { //重写fishing()方法
System.out.println("我喜欢钓鱼"); //输出我喜欢钓鱼
}
public static void main(String[] args) { //主方法
System.out.println("儿子喜欢做的事有:"); //输出儿子喜欢做的事有
Mother mother =new Son(); // 通过子类创建IMather接口对象
mother.cooking(); // 使用接口对象调用子类中实现的方法
mother.watchTV(); // 使用接口对象调用子类中实现的方法
Father father = new Son(); // 通过子类创建IFather接口对象
father.smoking(); // 使用接口对象调用子类中实现的方法
father.goFishing(); // 使用接口对象调用子类中实现的方法
}
}
例7.16运行结果
7.4.4 区分抽象类与接口
抽象类和接口都包含可以由子类继承实现的成员,但抽象类是对根源的抽象,而接口是对动作的抽象。抽象类的功能要远超过接口,那为什么还要使用接口呢?这主要是由于定义抽象类的代价高(因为每个类只能继承一个类,在这个类中, 必须继承或编写出其子类的所有共性,因此,虽然接口在功能上会弱化许多,但它只是针对一个动作的描述,而且可以在一个类中同时实现多个楼口,这样会降低设计阶段的难度。
抽象类和接口的区别主要有以下几点:
(1)子类只能继承一个抽象类,但可以实现任意多个接口。
(2)一个类要实现一个接口必须实现接口中的所有方法,而抽象类不必。
(3)抽象类中的成员变量可以是各种类型,而接口中的成员变量只能是public static final的。(4)接口中只能定义抽象方法,而抽象类中可以定义非抽象方法。
(5)抽象类中可以有静态方法和静态代码块等,接口中不可以。
(6)接口不能被实例化,没有构造方法,但抽象类可以有构造方法。
7.5 访问控制
前面多次提到了public、 private、 包等关键字或者概念,这些都是用来控制类、方法或者变量的访问范围的,Java中主要通过访问控制符、类包和final关键字对类、方法或者变量的访问范围进行控制,本节将对Java中访问控制知识进行详细讲解。
7.5.1 访问控制符
前面介绍了面向对象的几个基本特性,其中包括封装性,封装实际上有两方面的含义:把该隐藏的隐藏起来、把该暴露的暴露出来,这两个方面都需要通过使用Java提供的“访问控制符”来实现,本节将对Java中的访问控制符进行详细讲解。
Java中的访问控制符主要包括public、protected、 private 和default (缺省)等4种,这些控制符控制着类和类的成员变量以及成员方法的访问权限。
使用访问控制符时,遵循的原则。
(1)大部分顶级类都使用public修饰;
(2)如果某个类主要用作其他类的父类,该类中包含的大部分方法只是希望被其子类重写,而不想被外界直接调用,则应该使用protected 修饰;
(3)类中的绝大部分属性都应该使用private修饰,除非一些static 或者类似全局变量的属性,才考虑使用public修饰;
(4)当定义的方法只是用于辅助实现该类的其他方法(即工具方法),应该使用private修饰;(5)希望允许其他类自由调用的方法应该使用public修饰。
7.5.2 JAVA 类包
在Java义好类,通Java编译器进行编译之后,都会生成一个扩展名为.class的文件,当这个程序的规模逐渐庞大时,就很容易发生类名称冲突的现象。那么JDKAPI中提供了成千上万具有各种功能的类,又是如何管理的呢?Java 中提供了一种管理类文件的机制,就是类包。
Java中每个接口或类都来自不同的类包,无论是JavaAPI中的类与接口还是自定义的类与接口都需要隶属于某一个类包,这个类包包含了一些类和接口。如果没有包的存在,管理程序中的类名称将是一件非常麻烦的事情,如果程序只由一个类定义组成,并不会给程序带来什么影响,但是随着程序代码的增多,难免会出现类同名的问题。例如,在程序中定义一个 Login 类,因业务需要,还要定义一个名称为Login的类,但是这两个类所实现的功能完全不同,于是问题就产生了,编译器不会允许存在同名的类文件。解决这类问题的办法是将这两个类放置在不同的类包中,实际上 Java中类的完整名称是包名与类名的组合。
(1)在项目的sre节点上单击鼠标右键,选择“New- Package"命令。
(2)弹出New Java Package对话框,在Name文本框中输入新建的包名,如om.migy然后单击"Finish” 按钮。
(3)在Eclipse 中创建类时,可以在新建立的包上单击鼠标右键,选择“New" 命令,这样街建的类会默认保存在该包中。另外也可以在NewJavaClass对话框中指定新建类所在的包。在Java中包名设计应与文件系统结构相对应,如一个包名为com.mingrisoft,那么该包中的类位于com文件夹下的mingrisoft子文件夹下。没有定义包的类会被归纳在预设包(默认包)中。在实际开发中,应该为所有类设置包名,这是良好的编程习惯。
在类中定义包名的语法如下:
package包名1[.包名2[.包名3...]];
在上面的语法中,包名可以设置多个,包名和包名之间使用.分割,包名的个数没有限制,其中前面的包名包含后面的包名。
在类中指定包名时需要将package放置在程序的第一行, 它必须是文件中的第一行非注释代码,当使用package关键字为类指定包名之后,包名会成为类名中字的部分,预示着这个类必须指定全名,例如,在使用位于com.mingrisoft包下的Dog.java类时,需要使用形如com.mingrisoft.Dog这样的格式。
定义完包之后,如果使用包中的类,可以使用Java中的import关键字指定。其语法如下:
import包名1[.包名2[.包名3...]].类名;
在使用import关键字时,可以指定类的完整描述,但如果为了使用包中更多的类,则可以在包名后面加.*,这表示可以在程序中使用包中的所有类。例如:
import com.lzw.*; //指定import com.lzw包中的所有类在程序中都可以使用
import com.lzw.math //指定import com.lzw包中的math类在程序中都可以使用
7.5.3 final关键字
1. final 类
定义为final的类不能被继承。
如果希望一个类不允许任何类继承,并且不允许其他人对这个类进行任何改动,可以将这个类设置为final形式。
final类的语法如下:
final class 类名{}
如果将某个类设置为fnal 形式,则类中的所有方法都被隐式地设置为final 形式,但是final类中的成员变量可以被定义为final 或非final形式。
例7.17
final class FinalClass { //final类
int a = 3; //定义初值
void doit() { //调用 doit()方法
}
public static void main(String args[]) { //主方法
FinalClass f = new FinalClass(); //新建数组
f.a++; //累加
System.out.println(f.a); //输出结果
}
}
2. final 方法
首先,读者应该了解定义为final的方法不能被重写。
将方法定义为final 类型可以防止子类修改该类的定义与实现方式,同时定义final 的方法的执行效率要高于非final方法。在修饰权限中曾经提到过private修饰符,如果一个父类的某个方法被设置为private修饰符,子类将无法访问该方法,自然无法覆盖该方法, 所以一个定义为private的方法隐式被指定为final类型,这样无需将- .个定义为private的方法再定义为final类型。例如下面的语句:
private final void test() {undefined
...//省略些程序代码
}
但是在父类中被定义为private final的方法似乎可以被子类覆盖。
例7.18
class Parents{ //创建父类
private final void doit() { //调用final类
System.out.println("父类.doit()"); //输出调用父类
}
final void doit2() { //调用doit2()方法
System.out.println("父类.doit2()"); //输出调用父类
}
public void doit3() { //调用doit3()方法
System.out.println("父类.doit3()"); //输出调用父类
}
}
class Sub extends Parents { //创建类
public final void doit() { //在子类中定义一个doit()方法
System.out.println("子类.doit()"); //输出调用子类
}
// final void doit2(){ //final 方法不能覆盖
// System.out.println("子类.doit2()"); //输出调用子类
// }
public void doit3() { //调用doit3()方法
System.out.println("子类.doit3()"); //输出调用子类
}
}
public final class FinalMethod { //创建类
public static void main(String[] args) { //主方法
Sub s=new Sub(); //实例化
s.doit(); //调用 doit()方法
Parents p=s; //执行向上转型操作
//p.doit(); //不能调用private方法
p.doit2(); //调用 doit2()方法
p.doit3(); //调用 doit3()方法
}
}
例7.18运行结果
从本实例中可以看出,final 方法不能被覆盖,例如,doit2()方法不能在子类中被重写,但是在父类中定义了一个 private final 的doit()方法,同时在子类中也定义了一个doit()方法,从表面来看,子类中的doit()方法覆盖了父类的doit()方法,但是覆盖必须满足一个对象向 上转型为它的基本类型并调用相同方法这样一个条件。 例如,在主方法中使用“Parents p=s;"语句执行向上转型操作,对象P只能调用正常覆盖的doit3()方法,却不能调用doit()方法,可见子类中的doit()方法并不是正常覆盖,而是生成一个新的方法。
3. final 变量
final关键字可用于变量声明,一旦该变量被设定,就不可以再改变该变量的值。通常,由final定义的变量为常量。例如,在类中定义PI值,可以使用如下语句:
final double PI=3.14;
当在程序中使用PI这个常量时,它的值就是3.14,如果在程序中再次对定义为final的常量赋值,编译器将不会接受。
final 关键字定义的变量必须在声明时对其进行赋值操作。final 除了可以修饰基本数据类型的常量,还可以修饰对象引用。由于数组也可以被看作一一个对象来引用, 所以final可以修饰数组。一旦-一个对象引用被修饰为final 后,它只能恒定指向一个对象, 无法将其改变以指向另一一个对象。一 个既是satic又是final的字段只占据段不能改变的存储空间。 为了深入了解final关键字。
例7.19
import static java.lang.System.out; //导入import static java.lang.System.out类
import java.util.Random; //导入import java.util.Random
class Test { //类名
int i = 0; //定义i=0
}
public final class FinalData { //创建类
static Random rand =new Random(); //创建新数组
private final int VALUE_1 = 9; //声明一个final常量
private static final int VALUE_2 = 10; //声明一个 final、static常量
private final Test test = new Test(); //声明一个 final引用
private Test test2 = new Test(); //声明一个不是 final 的引用
private final int[] a = {1,2,3,4,5,6 }; //声明一个定义为final 的数组
private final int i4 = rand.nextInt(20); //声明一个final常量
private static final int i5= rand.nextInt(20); //声明一个final常量
public String toString() { //调用toString()
return i4 +" "+i5+" "; //输出结果
}
public static void main(String[] args){ //主方法
FinalData data = new FinalData(); //创建新数组
data.test=new Test(); //输出结果
//可以对指定为final的引用中的成员变量赋值
//但不能将定义为final的引用指向其他引用
//data.VALUE_2++; //输出2++
//不能改变定义为final的常量值
data.test2=new Test(); //可以将没有定义为 final的引用指向其他
for (int i = 0; i < data.a.length; i++) { //控制长度
//a[i]=9;
//不能对定义为final的数组赋值
}
out.println(data); //输出结果
out.println("data2"); //输出data2结果
out.println(new FinalData()); //输出数组结果
out.println(data); //输出结果
}
}
例7.19运行结果
在本实例中,被定义为final的常量定义时需要使用大写字母命名,并且中间使用下划线进行连接,这是Iana中的编码规则。同时,定义为fnal的数据无论是常量、对象引用还是数组,在主的数中都不可以被改变。
我们知道一个被定义为final 的对象引用只能指向唯一一个对象, 不可以将它再指向其他对象,但是一个对象本身的值却是可以改变的,那么为了使一个常量真正做到不可更改,可以将常量声明为static final。为了验证这个理论。
例7.20
import java.util.Random; //接口
import static java.lang.System.out; // 导入System.out
public class FinalStaticData { //创建数组
private static Random rand = new Random(); //实例化一个random类对象
private final int a1 = rand.nextInt(10); //随机产生0~10之间的随机数赋予定义为final的a2
private static final int a2 = rand.nextInt(10); //随机产生0~10之间的随机数赋予定义为static final的a2
public static void main(String[] args){ //主方法
FinalStaticData fdata = new FinalStaticData(); //实例化一个对象
out.println("重新实例化对象调用a1的值:"+ fdata.a1); //调用定义为final的a1
out.println("重新实例化对象调用a2的值:"+ fdata.a2); //调用定义为static final的a2
FinalStaticData fdata2= new FinalStaticData(); //实例化另一个对象
out.println("重新实例化对象调用a1的值:"+ fdata2.a1); //调用定义为final的al
out.println("重新实例化对象调用a2的值:"+ fdata2.a2); //调用定义为final的a2
QQQ20 fdata2 =new QQQ20(); //实例化另外一个对象
out.println("重新实例化对象调用al的值:"+ fdata2.al); out.println("重新实例化对象调用a2的值:"+ fdata2.a2); //输出结果
}
}
例7.20运行结果
从本实例的运行结果中可以看出,定义为final 的常量不是恒定不变的,将随机数赋予定义为fnal的常量,可以做到每次运行程序时改变al的值。但是a2与al不同,由于它被声明为static final形式,所以在内存中为a2开辟了一个恒定不变的区域,当再次实例化一个FinalStaticData对象时,仍然指向a2这块内存区域,所以a2的值保持不变。a2是在装载时被初始化,并不是每次创建新对象时都被初始化,而al会在重新实例化对象时被更改。
技巧:
在Java中定义全局常量,通常使用public static final修饰,这样的常量只能在定义时被赋值。
可以将方法的参数定义为final类型,这预示着无法在方法中更改参数引用所指向的对象。
最后总结一下在程序中final 数据可以出现的位置。图7.27清晰地表明了在程序中哪些位置可以定义final 数据。
7.6 内部类
前面曾经学习过在一个 文件中定义两个类,但其中任何一个类都不在另一个类的内部,而如果在类中再定义一个类,则将在类中再定义的那个类称为内部类,这里可以想像一下汽车和发动机的关系,很显然,此处不能单独用属性或者方法表示一个发动机,发动机是一个类, 而发动机又在汽车之中,汽车也是一个类,正如同内部类在外部类之中,这里的发动机类就好比是一个内部类。中部类可分为成员内部类、局部内部类以及匿名类。本节将对内部类的使用进行讲解。
7.6.1 成员内部类
1、成员内部类简介
在一个类中使用内部类,可以在内部类中直接存取其所在类的私有成员变量。本节首先介的成员内部类。
成员内部类的语法如下:
public class OuterClass{ //外部类
private class InnerClass { //内 部类
//...
}
}
在内部类中可以随意使用外部类的成员方法以及成员变量,尽管这些类成员被修饰为private图7.28充分说明了内部类的使用,尽管成员变量i以及成员方法g()都在外部类中被修饰为private但在内部类中可以直接使用外部类中的类成员。
内部类的实例一定要绑定在外部类的实例上,如果从外部类中初始化一个内 部类对象,那么内部类对象就会绑定在外部类对象上。内部类初始化方式与其他类初始化方式相同,都是使用new关键字。
例7.21
public class OuterClass { //创建类
innerClass in = new innerClass(); //在外部类实例化内部类对象引用
public void ouf() { // 普通类
in.inf(); //在外部类方法中调用内部类方法
}
class innerClass{ //创建innerClass
innerClass(){ //内部类构造方法
}
public void inf() { //内部类成员方法
}
int y = 0; //定义内部类成员变量
}
public innerClass doit(){ //创建innerClassdoit()方法
//y=4;
in.y = 4; //定义y值
return new innerClass(); //新建innerClass
}
public static void main(String[] args) { //主方法
OuterClass out = new OuterClass(); //创建新数组
OuterClass.innerClass in = out.doit(); //创建新数组doit()
OuterClass.innerClass in2 = out.new innerClass(); //创建新数组innerClass()
}
}
例7.21中的外部类创建内部类实例与其他类创建对象引用时相同。内部类可以访问它的外面类成员,但内部类的成员只有在内部类的范围之内是可知的,不能被外部类使用。图7.29说明了内部类ImerClass对象与外部类OuterClass对象的关系。
在例7.21的主方法中如果不使用doit(方 法返回内部类对象引用,可以直接使用内部类实例化内部类对象,但由于是在主方法中实例化内部类对象,必须在new操作符之前提供一个外部类的引用。
例如,在主方法中实例化一个内 部类对象。
public static void main (String args[]) {undefined
OuterClass out=new ”OuterClass() ;
OuterClass. innerClass in=out.doit() ;
OuterClass. innerClass in2=out.new innerClass();//实例化内部类对象
从上面代码可以看出,在实例化内部类对象时,不能在new操作符之前使用外部失实明化内部类对象,而应该使用外部类的对象来创建其内部类的对象。
<注意:
内部类对象会依赖于外部类对象,除非已经存在一个外部类对象,否则类中不会出现内部类对象。
2.内部类向上转型为接口
如果将一个权限修饰符为private的内部类向上转型为其父类对象,或者直接向上转型为一个接口,在程序中就可以完全隐藏内部类的具体实现过程。可以在外部提供一个接口, 在接口中声明个方法。如果在实现该接口的内部类中实现该接口的方法,就可以定义多个内部类以不同的方式实现接口中的同一个方法,而在一般的类中是不能多次实现接口中同一个方法的,这种技巧经常被用在Swing编程中,可以在-一个类中做出多个不同的响应事件( Swing编程技术会在后文中详细介绍)。例7.22下 面修改例7.21,在项目中创建InterfaceInner 类,并定义接口OutInterface,使内部类InnerClass实现这个接口,最后使doit(方法返回值类型为该接口。
例7.22
interface OutInterface {//定义一个接口
public void f(); // 普通类
}
public class InterfaceInner { //创建类
public static void main(String[] args) { //主方法
OutClass2 out = new OutClass2(); //实例化一个OutClass2对象
OutInterface outinter = out.doit(); //调用doit()方法,返回一个OutInterface 接口
outinter.f(); //调用f()方法
}
}
class OutClass2 { //定义一个内部类实现OutInterface接口
private class InnerClass implements OutInterface { //定义一个内部类实现OutInterface接口
InnerClass(String s) { //返回参数
System.out.println(s); // 输出结果
}
public void f() { //实现接口中的f()方法
System.out.println("访问内部类中的f()方法"); //输出访问内部类中的f()方法
}
}
public OutInterface doit() { //定义一个方法,返回值类型OutInterface接口
return new InnerClass ("访问内部类构造方法"); //输出访问内部类构造方法
}
}
例7.22运行结果
从上述实例中可以看出,OuterClass2 类中定义了一个修饰权限为private的内部类,这个内部类实现了Outlnterface 接口,然后修改doit(方法,使该方法返回个Outlnterface 接口。由于内部类InmerClass修饰权限为private,所以除了OuterClass2 类可以访问该内部类之外,其他类都不能访问,而可以访问doit(方法。由于该方法返回一一个外部接口类型,这个接口可以作为外部使用的接口。它包含一个f()方法,在继承此接口的内部类中实现了该方法,如果某个类继承了外部类,由于内部的权限不可以向下转型为内部类InnerClass,同时也不能访问f0方法,但是却可以访问接口中的f0方法。例如,InterfaceInner 类中最后一条语句,接口引用调用f0方法,从执行结果可以看出,这条语句执行的是内部类中的f()方法,很好地对继承该类的子类隐藏了实现细节,仅为编写子类的人留下个接口和一个外部类,同时也可以调用f()方法,但是f()方法的具体实现过程却被很好地隐藏了,这就是内部类最基本的用途。
3.使用this关键字获取内部类与外部类的引用
如果在外部类中定义的成员变量与内部类的成员变量名称相同,可以使用this关键字。
例7.23在项目中创建TheSameName类,在类中定义成员变量x,再定义一个内部类Inner,在内部类中也创建x变量,并在内部类的doit()方法中分别操作两个x变量。
例7.23
public class TheSameName { //创建类
private int x; //定义private int x方法
private class Inner{ //普通方法
private int x = 9; //定义private int x=9
public void doit(int x) { //调用的是形参x
x++; //调用的是形参x
this.x++; //调用内部类的变量x
TheSameName.this.x++; //调用外部类的变量x
}
}
}
在类中,如果遇到内部类与外部类的成员变量重名的情况,可以使用this关键字进行处理。例如,在内部类中使用this.x语句可以调用内部类的成员变量x,而使用TheSameName.this.x语句可以调用外部类的成员变量x,即使用外部类名称后跟一个点操作符和this关键字便可获取外部类的一个引用。
图7.31给出了例7.23在内存中变量的布局情况。
读者应该明确一点,在内存中所有对象均被放置在堆中,方法以及方法中的形参或局部变量放置在栈中。在图7.31中,栈中的doit()方法指向内部类的对象,而内部类的对象与外部类的对象是相互依赖的,Outer.this对象指向外部类对象
综上所述,使用成员内部类时,应该遵循以下原则:
(1)可以有各种修饰符,可以用private、 public、 protected、 static、 final、 abstract等修饰;
(2)如果内部类有static限定,就是类级别的,否则为对象级别。类级别可以通过外部类直接访问,对象级别需要先生成外部的对象后才能访问;
(3)内外部类不能同名;
(4)非静态内部类中不能声明任何static成员;
(5)内部类可以互相调用。
7.6.2 局部内部类
内部类不仅可以在类中进行定义,也可以在类的局部位置定义,如在类的方法或任意的作用域中均可以定义内部类。
例7.24
interface OutInterface2 { //创建类
}
class OuterClass3 { //普通类
public OutInterface2 doit(final String x) { // doit()方法参数为final类型
class InnerClass2 implements OutInterface2 { // 在doit()方法中定义一个内部类
InnerClass2(String s) { // 在doit()方法中定义一个内部类
s = x; //定义s=x
System.out.println(s); //输出s
}
}
return new InnerClass2("doit"); // 输出结果doit()方法中定义一个内部类
}
}
从上述代码中可以看出,内部类被定义在了doit()方法内部。但是有一点值得注意,内部类InnerClass2是doit()方法的一部分, 并非OuterClass3类中的一部分, 所以在doit()方法的外部不能间该内部类,但是该内部类可以访问当前代码块的常量以及此外部类的所有成员。
有的读者会注意到例7.24中的一个修改细节,就是将doit()方法的参数设置为fnal类型。如果需要在方法体中使用局部变量,该局部变量需要被设置为final 类型,换句话说,在方法中定义的内部类只能访问方法中final类型的局部变量,这是因为在方法中定义的局部变量相当于一个常量, 它的生命周期超出方法运行的生命周期,由于该局部变量被设置为final,所以不能在内部类中改变该局部变量的值。
7.6.3 匿名内部类
下面将例7.24中定义的内部类再次进行修改,在doit()方法中将return语向和内部类定义语句合并在一起,下面通过一个实例说明。
例7.25
interface OutInterace2 { //接口
}
class OuterClass4 { //类名
public OutInterace2 doit() { // 定义doit()方法
return new OutInterace2() { // 声明匿名内部类
private int i = 0; //定义i=0
public int getValue() { //int匿名内部类
return i; //输出结果
}
};
}
}
从例7.25中可以看出,笔者将doit0方法修改得有一些莫名其妙, 但这种写法确实被Java编译认可,在dit0方法内部首先返回一个OutercC的引用,然后在retrm语向中插入一个定义内类的代码, 由于这个类没有名称,所以这里将该内部类称为匿名内部类。实质上这种内部类的作就是创建一 个实现于OutInterface2接口的匿名类的对象。
匿名类的所有实现代码都需要在大括号之间进行编写。
语法如下:
return new A() {
...//内部类体
};
其中,A指类名。
由于匿名内部类没有名称,所以匿名内部类使用默认构造方法来生成Outinterface2对象。在匿名内部类定义结束后,需要加分号标识,这个分号并不是代表定义内部类结束的标识,而是代表创建OutInterface2引用表达式的标识。
说明:
匿名内部类编译以后,会产生以“外部类名S5序号”为名称的icass文件,序号以1-n排列,分别代表1-n个匿名内部类。
使用匿名内部类时应该遵循以下原则:
(1)匿名类没有构造方法;
(2)匿名类不能定义静态的成员;
(3)匿名类不能用private pulie. protecte. stati. final. abstract 等修饰;
(4)只可以创建一个匿名类实例。
7.6.4 静态内部类
在内部类前添加修饰符static,这个内部类就变为静态内部类了。一个静态内部类中可以声明静态成员,但是在非静态内部类中不可以声明静态成员。静态内部类有一一个最大的特点,就是不能你用外部类的非静态成员,所以静态内部类在程序开发中比较少见。
可以这样认为,普通的内部类对象隐式地在外部保存了-一个引用,指向创建它的外部类对象,但如果内部类被定义为static,就会有更多的限制。静态内部类具有以下两个特点:
(1)如果创建静态内部类的对象,不需要创建其外部类的对象;
(2)不能从静态内部类的对象中访问非静态外部类的对象。
public class staticinnerclass {
int x=100;
static class inner {
void doitinner(){
//system.out.println("外部类"+x); //不能调用外部类的成员变量x
}
}
}
上面代码中,在内部类的doitInner()方法中调用成员变量x,由于Inner 被修饰为static形式,而成员变量x却是非static 类型的,所以在doitInner()方法中不能调用x变量。
进行程序测试时,如果在每一个 Java文件中都设置一个主方法, 将出现很多额外代码,而程序本身并不需要这些主方法,为了解决这个问题,可以将主方法写入静态内部类中。
例7.26
public class StaticInnerClass { //创建类
int x = 100; //赋值i=100
static class Inner{ //普通类名
void doitInner() { //doitInner() 调用方法
// System.out.println("外部类"+x); //输出外部类+x
}
public static void main(String args[]) { //主方法
System.out.println(); //换行
}
}
}
如果编译例7.26中的类,将生成一个名称为Santiniassnerclass$inner的独立类和一个Staticinnerclass类,只要使用java staticClass$inner就可以运行主方法中的内容,这样当完成测试。需要将所有.class文件打包时,只要刪除StaticClasslnner独立类即可。
7.6.5 内部类的继承
内部类和其他普通类一样可以被继承,但是继承内部类比继承普通类复杂,需要设置专门的语法来完成。
例7.27
public class QQQ27 extends ClassA.ClassB { // 继承内部类ClassB
public QQQ27(ClassA a) { //继承类中内部类
a.super(); //构造方法体中使用 a.super()
}
}
class ClassA { //类名A
class ClassB { //内部类名B
}
}
在某个类继承内部类时,必须硬性给子这个类一个带参数的构造方法,并且该构造方法的参数必须是该内部类的外部类引用,就像例子中的ClassAa,同时在构造方法体中使用a.super()语句。
7.7 小结
通过对本章的学习,读者可以了解继承与多态的机制,掌握重载、类型转换等技术,学会使用接口与抽象类,从而对继承和多态有一一个比较深入的了解。另外,本章还介绍了Java语言中的包、final关键字的用法以及内部类,尽管读者已经了解过本章所讲的部分知识点,但还是建议初学者仔细揣摩继承与多态机制,因为继承和多态本身是比较抽象的概念,深入理解需要一段时间, 使用多态机制必须扩展自己的编程视野,将编程的着眼点放在类与类之间的共同特性以及关系上,使软件开发具有更快的速度、更完善的代码组织架构,以及更好的扩展性和维护性。