第七章 面向对象核心技术

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关键字的用法以及内部类,尽管读者已经了解过本章所讲的部分知识点,但还是建议初学者仔细揣摩继承与多态机制,因为继承和多态本身是比较抽象的概念,深入理解需要一段时间, 使用多态机制必须扩展自己的编程视野,将编程的着眼点放在类与类之间的共同特性以及关系上,使软件开发具有更快的速度、更完善的代码组织架构,以及更好的扩展性和维护性。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值