
目录
7.1 类的封装
7.2 类的继承
7.2.1 extends关键字
7.2.2 方法的重写
7.2.3 所有类的父类——Object类
7.3 类的多态
7.3.1 方法的重载
7.3.2 向上转型
7.3.3 向下转型
7.3.4 instanceof关键字
7.4 抽象类与接口
7.4.1 抽象类与抽象方法
7.4.2 接口的声明及实现
7.4.3 多重继承
7.4.4 区分抽象类与接口
7.5 访问控制
7.5.1 访问控制符
7.5.2 Java 类包
7.5.3 final 关键字
7.6 内 部类
7.6.1 成员内部类
7.6.2 局部内部类
7.6.3 匿名内部类
7.6.4 静态内部类
7.6.5 内部类的继承
封装、继承和多态,通过应用面向对象思想,整个程序的架构将变得非常有d性,同时可以减少代码的冗余性。
7.1 类的封装 封装是面向对象编程的核心思想,将对象的属性和行为封装起来,其载体就是类。本节将详细介绍如何将类封装。
举一个简单的例子:我到一个餐馆去吃饭,点了一盘香辣肉丝,感觉很好吃,我就想知道厨师的名字,希望让厨师再为我多做点事情。
例7.1 创建Restaurantl这个类,实现餐馆点菜的场景。
代码如图所示:
运行结果如图所示:
在这个例子里面,所有的逻辑代码全是在main方法中实现的,代码完全暴露,我可以任意删改。如果能随便修改代码,餐馆可能就无法正常运作了,比如:我可以知道厨师的任何信息;我让厨师切葱花,厨师却搅鸡蛋;我让厨师做一份清蒸鱼,厨师却给我唱首歌……
System.out.print1n("**请让厨师给我切一点葱花。***");
System.out.println(cookName +"搅鸡蛋”); //被乱改之后
System.out.print1n("**请让厨师为我做一份清蒸鱼。***");
System.out.println(cookName +”你是我的小呀小苹果~”); //被乱改之后
例7.2 将厨师封装成Cook类,实现餐馆点菜的场景。
代码如图所示:
运行结果如图所示:
将厨师单独封装成一个类,将厨师的工作定义成厨师类的行为,当我们想让厨师做菜,只能通过调用对象成员方法的方式实现,而我们却不知道这个方法到底是怎么写的,所以就无法随意修改了。餐馆没有义务告诉我们厨师的任何信息,并且厨师也不会随意受我们差遭,所以说厨师有些属性和行为是不予公开的。
例7.3 将厨师的属性和部分方法用private修饰。
代码如图所示:
运行结果如图所示:
此时再运行餐馆的主方法,就会抛出异常,如上图运行结果所示,提示Cook2的name和cutOnion()不可以直接调用。
其实按照日常生活场景来讲,顾客去餐馆吃饭,下单的是服务员,上菜的也是服务员,顾客跟本没有接触厨师的机会,所以厨师这个角色是对顾客隐藏起来的,被封装在餐馆的类当中。
例7.4 将厨师对象封装在餐馆类中,顾客无法接触到厨师的任何信息。
代码如图所示:
运行结果如图所示:
从这个例子我们就能看出,作为顾客,我始终是和服务员进行交流,再由服务员与厨师进行交流,整个过程中,顾客与厨师是完全没有交集的。作为顾客,我不知道我品尝的美食是由哪位厨师用何种方法烹饪出来的,这种编程模式,就是封装。
将对象的属性和行为封装起来的载体就是类,类通常对客户隐藏其实现细节,这就是封装的思想。
继承在面向对象开发思想中是一个非常重要的概念,它使整个程序架构具有一定的d性,在程序中复用已经定义完善的类不仅可以减少软件开发周期,还可以提高软件的可维护性和可扩展性。
在第6章中曾简要介绍过继承,其基本思想是基于某个父类的扩展,制定出一个新的子类,子类可以继承父类原有的属性和方法,也可以增加原来父类所不具备的属性和方法,或者直接重写父类中的某些方法。例如,平行四边形是特殊的四边形,可以说平行四边形类继承了四边形类,这时平行四边形类将所有四边形具有的属性和方法都保留下来,并基于四边形类扩展了一些新的平行四边形类特有的属性和方法。
创建一个新类Test,同时创建另一个新类Test2继承Test类,其中包括重写的父类成员方法以及新增成员方法等。在下图7.5中描述了类Test与Test2的结构以及两者之间的关系。
7.2.1 extends关键字一个类继承另一个类,用extends关键字,语法如下:
child extends parents
这里child这个类作为子类继承了parents这个类,并继承parents中的属性和方法。举一个简单的例子:每个人都用过计算机,最常见的计算机就是台式机。后来随着科技的发展,计算机变得越来越小,台式机改良成了可移动的笔记本电脑,笔记本电脑又改良成了更轻薄的平板电脑。我们可以把普通计算机看成一个类,那么笔记本电脑和平板电脑都是这个类衍生出的子类。
注意:
Java中的类只支持单继承,即一个子类只能继承自一个父类,类似下面的代码是错误的:child extends parentsl , parents2 { // 错误的继承语法}
例 7.5 创建Pad类,继承Computer类。
代码如图所示:
运行结果如图所示:
从上面运行结果可以看出,Pad类继承了Computer类之后,虽然没有定义任何成员方法,但仍可以调用父类的方法。这个方法就是从父类那里继承过来的。
7.2.2 方法的重写1.重写的实现
继承并不只是扩展父类的功能,还可以重写父类的成员方法。重写(还可以称为覆盖)就是在子类中将父类的成员方法的名称保留,重新编写成员方法的实现内容,更改成员方法的存储权限,或是修改成员方法的返回值类型。
在继承中还有一种特殊的重写方式,子类与父类的成员方法返回值、方法名称、参数类型及个数完全相同,唯一不同的是方法实现内容,这种特殊重写方式被称为重构。
注意:
当重写父类方法时,修改方法的修饰权限只能从小的范围到大的范围改变,例如,父类中的doSomething)方法的修饰权限为protected,继承后子类中的方法doSomething)的修饰权限只能修改为public,不能修改为private。
如图所示的重写关系就是错误的:
子类重写父类的方法还可以修改方法的返回值类型,但这只是在J2SE 5.0以上的版本中支持的新功能,但这种重写方式需要遵循一个原则,即重写的返回值类型必须是父类中同一方法返回值类型的子类。
例7.6 创建Pad2类,继承Computer2类,并重写父类的showPicture()方法。
代码如图所示:
运行结果如图所示:
从这个结果我们可以看出,虽然子类调用了父类的方法,但实现的是子类重写后的逻辑,而不是父类原有的逻辑。如果父类声明的对象是由子类实例化的,那么这个对象所调用的方法也是被子类重写过的。
注意:
在Java语言中,一个类只可以有一个父类!
2. super关键字
子类重写父类的方法。为了解决这种需求,Java提供了super关键字。
super关键字的使用方法与this关键字类似。this 关键字代表本类对象,super关键字代表父类对象,使用方法如下:
super.property; //调用父类的属性
super.method(); //调用父类的方法
例 7.7 创建Pad3类,继承Computer3类,重写父类方法,并用使用super关键字调用父类方
法。
代码如图所示:
运行结果如图所示:
7.2.3 所有类的父类——Object类注意:
如果在子类构造方法中使用类似super()的构造方法,其他初始化代码只能写在super之后,不能写在前面,否则会报错。
说明:
在继承的机制中,创建一个子类对象,将包含一个父类子对象,这个对象与父类创建的对象是一样的。两者的区别在于后者来自外部,而前者来自子类对象的内部。当实例化子类对象时,父类对象也相应被实例化,换句话说,在实例化子类对象时,Java编译器会在子类的构造方法中自动调用父类的无参构造方法,但有参构造方法并不能被自动调用,只能依赖于super关键字显式地调用父类的构造方法。
学习使用class关键字定义类时 ,就应用了继承原理 ,因为在Java中,所有的类都直接或间接继承了java.lang.Object类。Object类是比较特殊的类,它是所有类的父类,是Java类层中的最高层类 。当创建一个类时 ,总是在继承 ,除非某个类已经指定要从其他类继承 ,否则它就是从java.lang.Object 类继承而来的 ,可见Java中的每个类都源于 java.lang.Object类 ,如 String 、Integer等类都是继承于添加字群 Object类;除此之外自定义的类也都继承于Object类。
由于所有类都是Object子类,所以在定义类时,省略了extends Object关键字,如下图所示便描述了这一原则:
在Object类中主要包括clone()、finalize()、equals()、toString()等方法,其中常用的两个方法为equals()和toString()方法。由于所有的类都是Object 类的子类,所以任何类都可以重写Object类中的方法。
注意:
Object类中的getClass()、notify()、notifyAll()、wait()等方法不能被重写,因为这些方法被定义为final类型。
1. getClass()方法
getClass()方法是Object类定义的方法,它会返回对象执行时的Class实例,然后使用此实例调用getName()方法可以取得类的名称。
语法如下:
getClass().getName ();
可以将getClass)方法与toString)方法联合使用。
2. toString()方法
toString()方法的功能是将一个对象返回为字符串形式,它会返回一个String实例。在实际的应用中通常重写toString()方法,为对象提供一个特定的输出模式。当这个类转换为字符串或与字符串连接时,将自动调用重写的toString()方法。
例7.8 在项目中创建ObjectInstance类,在类中重写Object类的toString()方法,并在主方法中输出该类的实例对象。
代码如图所示:
运行结果如图所示:
上图实例中重写父类Object 的toString()方法,在子类的 toString()方法中使用 Object 类中的getClass()方法获取当前运行的类名,定义一段输出字符串,当用户打印ObjectInstance类对象时,将自动调用toString()方法。
3. equals()方法
equals方法是比较“=”运算符与equals)方法,说明“=”比较的是两个对象的引用是否相等,而equals方法比较的是两个对象的实际内容。带着这样一个理论来看下面的实例。
例 7.9 在项目中创建OverWriteEquals类,在类的主方法中定义两个字符串对象,调用equals方法判断两个字符串对象是否相等。
代码如图所示:运行结果如图所示:
上图实例的结果中可以看出,在自定义的类中使用equals()方法进行比较时,将返回 false,因为equals()方法的默认实现是使用“==”运算符比较两个对象的引用地址,而不是比较对象的内容,所以要想真正做到比较两个对象的内容,需要在自定义类中重写equals()方法。
7.3 类的多态多态意为一个名字可具有多种语义,在程序设计语言中,多态性是指“一种定义,多种实现”,例如,运算符“+”作用于两个整型量时是求和,而作用于两个字符型量时则是将其连接在一起。利用多态可以使程序具有良好的扩展性,并可以对所有类对象进行通用的处理。类的多态性可以从两方面体现:一是方法的重载,二是类的上下转型。
7.3.1 方法的重载构造方法的名称由类名决定,构造方法只有一个名称,但如果希望以不同的方式来实例化对象,就需要使用多个构造方法来完成。由于这些构造方法都需要根据类名进行命名,为了让方法名相同而形参不同的构造方法同时存在,必须用到“方法重载”。虽然方法重载起源于构造方法,但是它也可以应用到其他方法中。方法的重载就是在同一个类中允许同时存在一个以上的同名方法,只要这些方法的参数个数或类型不同即可。
例7.10 在项目中创建OverLoadTest类,在类中编写add()方法的多个重载形式,然后在主方法中分别输出这些方法的返回值。
代码如图所示:
运行结果如图所示:
代码注解:
在本实例中分别定义了6个方法,在这6个方法中,前两个方法的参数个数不同,所以构成了重载关系;前两个方法与第3个方法比较时,方法的参数类型不同,并且方法的返回值类型也不同,所以这3个方法也构成了重载关系;比较第4、第5两个方法时,发现除了参数出现的顺序不同之外,其他都相同,这样同样可以根据这个区别将两个方法构成重载关系;而最后一个使用不定长参数的方法,实质上与参数数量不同是一个概念,也构成了重载。
下图表明了所有可以构成重载的条件:
注意:
虽然在方法重载中可以使两个方法的返回类型不同,但只有返回类型不同并不足以区分两个方法的重载,还需要通过参数的个数以及参数的类型来设置。
根据上面框架图所示的构成方法重载的条件,可以总结出编译器是利用方法名、方法各参数类型、参数的个数以及参数的顺序来确定类中的方法是否唯一。方法的重载使得方法以统一的名称被管理,使程序代码有条理。根据图7.14所示的构成方法重载的条件,可以总结出编译器是利用方法名、方法各参数类型、参数的个数以及参数的顺序来确定类中的方法是否唯一。方法的重载使得方法以统一的名称被管理,使程序代码有条理。
7.3.2 向上转型因为平行四边形是特殊的四边形,也就是说平行四边形是四边形的一种,那么就可以将平行四边形对象看作是一个四边形对象。例如,鸡是家禽的一种,而家禽是动物中的一类,那么也可以将鸡对象看作是一个动物对象。
例7.11 在项目中创建Quadrangle父类,再创建Parallelogram子类,并使Parallelogram子类继承
Quadrangle父类,然后在主方法中调用父类的draw()方法。
代码如图所示:
从下图中可以看出,平行四边形类继承了四边形类,常规的继承图都是将顶级类设置在页面的顶部,然后逐渐向下,所以将子类对象看作是父类对象被称为“向上转型”。由于向上转型是从一个较具体的类到较抽象的类的转换,所以它总是安全的,如可以说平行四边形是特殊的四边形,但不能说四边形是平行四边形。
注意:
在执行向上转型 *** 作时,父类的对象无法调用子类独有的属性或者方法,例如,在上面代码的Parallelogram子类中定义一个edges变量,然后在main方法中使用Parallelogram子类创建Quadrangle父类的对象,并使用该父类对象调用子类中定义的变量。
代码修改如下图所示:
运行上面代码,出现如图所示错误提示:
7.3.3 向下转型通过向上转型可以推理出向下转型是将较抽象类转换为较具体的类。这样的转型通常会出现问题,例如,不能说四边形是平行四边形的一种、所有的鸟都是鸽子,因为这非常不合乎逻辑。可以说子类对象总是父类的一个实例,但父类对象不一定是子类的实例。
例7.12 修改例7.11,在Parallelogram子类的主方法中将父类Quadrangle的对象赋值给子类Parallelogram的对象的引用变量将使程序产生错误。
代码修改如下图所示:
运行这个程序会直接抛出异常。 运行结果如图所示:
在上面运行结果中可以看到,如果将父类对象直接赋予子类,会发生编译器错误,因为父类对象不一定是子类的实例。例如,一个四边形不一定就是指平行四边形,它也许是梯形,也许是正方形,也许是其他带有四条边的不规则图形,图7.18表明了这些图形的关系。
从图7.18中可以看出,越是具体的对象具有的特性越多,越抽象的对象具有的特性越少。在做向下转型 *** 作时,将特性范围小的对象转换为特性范围大的对象肯定会出现问题,所以这时需要告知编译器这个四边形就是平行四边形。将父类对象强制转换为某个子类对象,这种方式称为显式类型转换。
例如, 将例7.12的代码修改如下:
7.3.4 instanceof关键字当在程序中执行向下转型 *** 作时 ,如果父类对象不是子类对象的实例 ,就会发生ClassCastException异常 ,所以在执行向下转型之前需要养成一个良好的习惯,就是判断父类对象是否为子类对象的实例。这个判断通常使用instanceof *** 作符来完成。可以使用instanceof *** 作符判断是否一个类实现了某个接口,也可以用它来判断一个实例对象是否属于一个类。
instanceof的语法格式如下:
myobject instanceof ExampleClass
myobject: 某类的对象引用
ExampleClass: 某个类
使用 instanceof *** 作符的表达式返回值为布尔值。如果返回值为true,说明 myobject 对象为ExampleClass的实例对象;如果返回值为false,说明myobject对象不是ExampleClass的实例对象。
注意:
instanceof是Java语言的关键字,在Java语言中的关键字都为小写。
例7.13 在项目中创建Parallelogram类和另外3个类Quadrangle、Square、Anything。其中Parallelogram类和Square 类继承Quadrangle类,在Parallelogram类的主方法中分别创建这些类的对象,然后使用instanceof *** 作符判断它们的类型并输出结果。
代码如图所示:
实例中将instanceof *** 作符与向下转型 *** 作结合使用。在程序中定义了两个子类,即平行四边形类和正方形类,这两个类分别继承四边形类。在主方法中首先创建四边形类对象,然后使用instanceof *** 作符判断四边形类对象是否为平行四边形类的一个实例,是否为正方形类的一个实例,如果判断结果为true,将进行向下转型 *** 作。
7.4 抽象类与接口通常可以说四边形具有4条边,或者更具体一点,平行四边形是具有对边平行且相等特性的特殊四边形,等腰三角形是其中两条边相等的三角形,这些描述都是合乎情理的,但对于图形对象却不能使用具体的语言进行描述,它有几条边,究竟是什么图形,没有人能说清楚,这种类在Java中被定义为抽象类。
7.4.1 抽象类与抽象方法如鸽子类继承鸟类、鸟类继承动物类等。在多态机制中,并不需要将父类初始化对象,需要的只是子类对象,所以在Java语言中设置抽象类不可以实例化对象,因为图形类不能抽象出任何一种具体图形,但它的子类却可以。
Java中定义抽象类时,需要使用abstract关键字,其语法如下:
[权限修饰符] abstract class 类名 {
类体
}
使用abstract关键字定义的类称为抽象类,而使用abstract关键字定义的方法称为抽象方法。
抽象方法的定义语法如下:
[权限修饰符] abstract 方法返回值类型 方法名 (参数列表);
从上面的语法可以看出,抽象方法是直接以分号结尾的,它没有方法体,抽象方法本身没有任何意义,除非它被重写,而承载这个抽象方法的抽象类必须被继承,实际上,抽象类除了被继承之外没有任何意义。图7.19说明了抽象类的继承关系。
从图7.19中可以看出,继承抽象类的所有子类都需要将抽象类中的抽象方法进行覆盖,这样在多态机制中,就可以将父类修改为抽象类,将draw)方法设置为抽象方法,然后每个子类都重写这个方法来处理。
注意:
构造方法不能定义为抽象方法
例7.14 使用抽象类模拟“去商场买衣服”场景。去商场买衣服,这句话描述的是一个抽象的行为:到底去哪个商场买衣服,是实体店还是网店,买什么样的衣服,是短衫、裙子,还是其他的什么衣服?在“去商场买衣服”这句话中,并没有对“买衣服”这个抽象行为指明一个确定的信息。因此,我们可以封装一个商场的抽象类,并在其中定义个买东西的抽象方法,具体是什么商场、买什么东西,交给子类去实现即可。代码如下:
定义一个TaobaoMarket类,继承自Market抽象类,实现其中的shop抽象方法,代码如下:
定义一个WallMarket类,继承自Market抽象类,实现其中的shop抽象方法,代码如下:
定义一个GoShopping类,该类中分别使用实现的WallMarket子类和TaobaoMarket 子类创建
抽象类的对象,并分别给抽象类中的成员变量赋不同的值,使用shop方法分别输出结果,代码如下:
运行GoShopping类,运行结果如图所示:
综上所述,使用抽象类和抽象方法时,需要遵循以下原则:
(1) 在抽象类中,可以包含抽象方法,也可以不包含抽象方法,但是包含了抽象方法的类必须被定义为抽象类。
(2) 抽象类不能直接实例化,即使抽象类中没有声明抽象方法,也不能实例化。
(3) 抽象类被继承后,子类需要实现其中所有的抽象方法。
(4) 如果继承抽象类的子类也被声明为抽象类,则可以不用实现父类中所有的抽象方法。
使用抽象类时,可能会出现这样的问题:程序中会有太多冗余的代码,同时这样的父类局限性很大,例如,上面的例子中,也许某个不需要shop()方法的子类也必须重写shop()方法。如果将这个shop()方法从父类中拿出,放在别的类里,又会出现新问题,就是某些类想要实现“买衣服”的场景,竟然需要继承两个父类。Java中规定,类不能同时继承多个父类,面临这种问题时,接口的概念便出现了。
7.4.2 接口的声明及实现接口是抽象类的延伸,可以将它看作是纯粹的抽象类,接口中的所有方法都没有方法体。可以将draw)方法封装到一个接口中,这样可以让一个类既能继承图形类,又能实现draw)方法接口,这就是接口存在的必要性。在图7.21中描述了各个子类继承图形类后使用接口的关系。
接口使用interface关键字进行定义,其语法如下:
[修饰符] interface 接口名 [extends 父接口名列表] {
[public] [static] [final] 常量;
[public] [abstract] 方法;
}
修饰符:可选,用于指定接口的访问权限,可选值为public。如果省略则使用默认的访问权限。
接口名:必选参数,用于指定接口的名称,接口名必须是合法的Java标识符。一般情况下要求首字母大写。extends 父接口名列表:可选参数,用于指定要定义的接口继承于哪个父接口。当使用extends关键字时,父接口名为必选参数。
方法:接口中的方法只有定义而没有被实现。
一个类实现一个接口可以使用implements关键字,代码如下:
public class Parallelogram extends Quadrangle implements drawTest {
…//
}
说明:
在接口中定义的任何变量都自动是static和final的,因此,在接口中定义变量时,必须进行初始化,而且,实现接口的子类不能对接口中的变量重新赋值。
例7.15 在项目中创建QuadrangleUseInterface类 ,该类中 ,首先创建一个drawTest接口 ,该接口中定义一个公有的draw()方法 ;然后创建两个类 ParallelogramgleUseInterface 和SquareUseInterface,使它们分别实现drawTest接口,并分别实现接口中的draw()方法;然后在主方法中分别调用这两个子类的draw()方法。
代码如图所示:
运行结果如图所示:
在本实例中,正方形类与平行四边形类分别实现了drawTest接口,所以需要覆盖接口中的方法。在调用draw()方法时,首先将平行四边形类对象与正方形类对象向上转型为drawTest接口形式。这里也许很多读者会有疑问,接口是否可以向上转型?其实在Java中无论是将一个类向上转型为父类对象,还是向上转型为抽象父类对象,或者向上转型为该类实现接口,都是没有问题的。然后使用d[]数组中的每一个对象调用draw(),由于向上转型,所以d[i]数组中的每一个对象分别代表正方形类对象与平行四边形类对象,最后结果分别调用正方形类与平行四边形类中覆盖的draw()方法。
7.4.3 多重继承说明:
由于接口中的方法都是抽象的,因此,当子类实现接口时,必须实现接口中的所有方法。
在Java中类不允许多重继承,但使用接口就可以实现多重继承,因为一个类可以同时实现多个接口,这样可以将所有需要实现的接口放置在implements关键字后并使用逗号“,”隔开,但这可能会在一个类中产生庞大的代码量,因为继承一个接口时需要实现接口中所有的方法。
通过接口实现多重继承的语法如下:
class 类名 implements 接口 1,接口 2,…,接口n
例7.16 通过类实现多个接口模拟家庭成员的继承关系,比如,爸爸喜欢抽烟和钓鱼,妈妈喜欢看电视和做饭,儿子完全继承了爸爸妈妈的爱好。定义一个IFather接口,并在其中定义两个方法smoking 和goFishing,代码如下:
定义一个IMother接口,并在其中定义两个方法watchTV和cooking,代码如下:
创建一个名称为Me的类,继承IFather和IMother两个接口,并实现接口中定义的方法;然后在main方法中使用Me子类对象分别创建IFather和IMother两个接口的对象,并通过这两个接口对象调用相应的方法执行,代码如下:
运行结果如图所示:
注意:
使用多重继承时,可能出现变量或方法名冲突的情况,解决该问题时,如果变量冲突,则需要明确指定变量的接口,即通过“接口名.变量”实现;而如果出现方法冲突时,则只要实现一个方法即可。
7.4.4 区分抽象类与接口说明:
如果是接口继承接口,使用extends关键字,而不是implements关键字,例如:interface intf1 {
}
interface intf2 extends intf1 {}
另外,如果遇到接口继承接口,则子类在实现子接口时,需要同时实现父接口和子接口中定义的所有方法,例如:
interface Father { //交接口方法 void fatherMethod(); //定义父接口 } interface Child extends Father { //定义子接口,继承父接口 void childMethod(); //子接口方法 } public class InterfaceExtendsimplements Child { //实现子接口,但必须重写所有方 public void fatherMethod() { System.out.println("实现父接口方法"); } public void childMethod() { System.out.println("实现子接口方法"); } }
抽象类和接口都包含可以由子类继承实现的成员,但抽象类是对根源的抽象,而接口是对动作的抽象。抽象类的功能要远超过接口,那为什么还要使用接口呢?这主要是由于定义抽象类的代价高(因为每个类只能继承一个类,在这个类中,必须继承或编写出其子类的所有共性,因此,虽然接口在功能上会弱化许多,但它只是针对一个动作的描述,而且可以在一个类中同时实现多个接口,这样会降低设计阶段的难度。
抽象类和接口的区别主要有以下几点:
(1) 子类只能继承一个抽象类,但可以实现任意多个接口。
(2) 一个类要实现一个接口必须实现接口中的所有方法,而抽象类不必。
(3) 抽象类中的成员变量可以是各种类型,而接口中的成员变量只能是public static final的。
(4) 接口中只能定义抽象方法,而抽象类中可以定义非抽象方法。
(5) 抽象类中可以有静态方法和静态代码块等,接口中不可以。
(6) 接口不能被实例化,没有构造方法,但抽象类可以有构造方法。
综上所述,抽象类和接口在主要成员及继承关系上的不同如表7.1所示。
前面多次提到了public、private、包等关键字或者概念,这些都是用来控制类、方法或者变量的访问范围的,Java中主要通过访问控制符、类包和final关键字对类、方法或者变量的访问范围进行控制。
7.5.1 访问控制符面向对象的几个基本特性,其中包括封装性,封装实际上有两方面的含义:把该隐藏的隐藏起来、把该暴露的暴露出来,这两个方面都需要通过使用Java提供的“访问控制符”来实现。
Java中的访问控制符主要包括public、protected、private和default(缺省)等4种,这些控制符控制着类和类的成员变量以及成员方法的访问权限。
表7.2中描述了public、protected、private和default(缺省)这4种访问控制符的访问权限。
Java语言中,类的权限设定会约束类成员上的权限设定,例如,定义一个类AnyClass,采用默认权限,该类中定义一个public的doString方法,那么,doString方法加不加public修饰符,它的访问权限都是default(缺省)。下面两段代码是等效的:
//第一段代码
class AnyClass {
public void doString() {
System.out.println("Hello");}
}
//第二段代码
class AnyClass {
void doString() {
System.out.println("Hello");}
}
使用访问控制符时,需要遵循以下原则:
(1) 大部分顶级类都使用public修饰;
(2) 如果某个类主要用作其他类的父类,该类中包含的大部分方法只是希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰;
(3) 类中的绝大部分属性都应该使用private修饰,除非一些static或者类似全局变量的属性,才考虑使用public修饰;
(4) 当定义的方法只是用于辅助实现该类的其他方法(即工具方法),应该使用private修饰;
(5) 希望允许其他类自由调用的方法应该使用public修饰。
在Java中每定义好一个类,通过Java编译器进行编译之后,都会生成一个扩展名为.class的文件,当这个程序的规模逐渐庞大时,就很容易发生类名称冲突的现象。Java中提供了一种管理类文件的机制,就是类包。
Java中每个接口或类都来自不同的类包,无论是JavaAPI中的类与接口还是自定义的类与接口都需要隶属于某一个类包,这个类包包含了一些类和接口。如果没有包的存在,管理程序中的类名称将是一件非常麻烦的事情,如果程序只由一个类定义组成,并不会给程序带来什么影响,但是随着程序代码的增多,难免会出现类同名的问题。例如,在程序中定义一个Login类,因业务需要,还要定义一个名称为Login的类,但是这两个类所实现的功能完全不同,于是问题就产生了,编译器不会允许存在同名的类文件。解决这类问题的办法是将这两个类放置在不同的类包中,实际上,Java中类的完整名称是包名与类名的组合,如图7.23所示:
在Java中采用类包机制非常重要,类包不仅可以解决类名冲突问题,还可以在开发庞大的应用程序时,帮助开发人员管理庞大的应用程序组件,方便软件复用。下面来看一下在Java中如何创建类包(以下简称包)。
说明:
同一个包中的类相互访问时,可以不指定包名。
在Eclipse 中创建包的步骤如下:
(1) 在项目的src节点上单击鼠标右键,选择“New→Package”命令。
(2) d出New Java Package对话框,在Name文本框中输入新建的包名,如com.mingrisoft,然后单击“Finish”按钮,如图所示 :
(3) 在Eclipse中创建类时,可以在新建立的包上单击鼠标右键,选择“New”命令,这样新建的类会默认保存在该包中。另外也可以在New Java Class对话框中指定新建类所在的包。
在Java中包名设计应与文件系统结构相对应,如一个包名为com.mingrisoft,那么该包中的类位于com文件夹下的mingrisoft子文件夹下。没有定义包的类会被归纳在预设包(默认包)中。在实际开发中,应该为所有类设置包名,这是良好的编程习惯。
在类中定义包名的语法如下:
package 包名1[.包名2[.包名3...]];
在上面的语法中,包名可以设置多个,包名和包名之间使用.分割,包名的个数没有限制,其中前面的包名包含后面的包名。
在类中指定包名时需要将package放置在程序的第一行,它必须是文件中的第一行非注释代码,当使用package关键字为类指定包名之后,包名会成为类名中的一部分,预示着这个类必须指定全名。例如,在使用位于com.mingrisoft包下的Dog.java类时,需要使用形如com.mingrisof.Dog这样的格式。
注意:
Java包的命名规则是全部使用小写字母,另外,由于包名将转换为文件的名称,所以包名中不能包含特殊字符。
定义完包之后,如果要使用包中的类,可以使用Java中的import关键字指定,其语法如下:
import 包名1[.包名2[.包名3...]].类名;
在使用import关键字时,可以指定类的完整描述,但如果为了使用包中更多的类,则可以在包名后面加.,这表示可以在程序中使用包中的所有类。例如:
import com.1zw.*; //指定com.lzw包中的所有类在程序中都可以使用
import com.lzw.Math //指定com.lzw包中的Math类在程序中可以使用
7.5.3 final 关键字注意:
如果类定义中已经导入com.lzw.Math类,在类体中还想使用其他包中的Math类时,则必须使用完整的带有包格式的类名,比如,这种情况再使用java.lang包的Math类时就要使用全名格式java. lang.Math。
在程序中添加import关键字时,当使用import指定了一个包中的所有类,并不会指定这个包的子包中的类,如果用到这个包中的子类,则需要再次对子包单独引用。
1.final类
定义为fǐnal的类不能被继承。
如果希望一个类不允许任何类继承,并且不允许其他人对这个类进行任何改动,可以将这个类设置为fǐnal形式。fǐnal类的语法如下:
final class类名()
如果将某个类设置为final形式,则类中的所有方法都被隐式地设置为final形式,但是final类中的成员变量可以被定义为final或非final形式。
例7.17 在项目中创建FinalClass类,在类中定义doit()方法和变量a,实现在主方法中 *** 作变量a自增。
代码如图所示:
运行结果如图所示:
2.final 方法
定义为fǐnal的方法不能被重写。
将方法定义为fǐnal类型可以防止子类修改该类的定义与实现方式,同时定义final的方法的执行效率要高于非final方法。在修饰权限中曾经提到过private修饰符,如果一个父类的某个方法被设置为private修饰符,子类将无法访问该方法,自然无法覆盖该方法,所以一个定义为private的方法隐式被指定为fǐnal类型,这样无需将一个定义为private的方法再定义为final类型。例如下面的语句:
private final void test() {
…//省略一些程序代码
}
例7.18 在项目中创建FinalMethod类,在该类中创建Parents类和继承该类的Sub类,在主方法中分别调用这两个类中的方法,并查看final类型方法能否被覆盖。
代码如图所示:
运行结果如图所示:
从本实例中可以看出,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后,它只能恒定指向一个对象,无法将其改变以指向另一个对象。一个既是static又是final的字段只占据一段不能改变的存储空间。为了深入了解 final关键字,来看下面的实例。
例7.19 在项目的com.lzw包中创建 FinalData类,在该类中创建Test内部类,并定义各种类型的final变量。
代码如图所示:
运行结果如图所示:
在本实例中,被定义为final的常量定义时需要使用大写字母命名,并且中间使用下划线进行连接,这是Java中的编码规则。同时,定义为fǐnal的数据无论是常量、对象引用还是数组,在主函数中都不可以被改变。
一个被定义为final的对象引用只能指向唯一一个对象,不可以将它再指向其他对象,但是一个对象本身的值却是可以改变的,为了使一个常量真正做到不可更改,可以将常量声明为static final。
例7.20 在项目的com.lzw包中创建FinalStaticData类,在该类中创建Random类的对象,在主方法中分别输出类中定义的final变量al与a2。
代码如图所示:
运行结果如图所示:
从本实例的运行结果中可以看出,定义为final的常量不是恒定不变的,将随机数赋予定义为final的常量,可以做到每次运行程序时改变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 成员内部类说明:
使用内部类可以节省编译后产生的字节码,.class文件的大小,而且在实现事件监听时,采用内部类很容易实现;但是,使用内部类的最大问题会使结构不清晰,所以在程序开发时,不需要刻意使用内部类。
1. 成员内部类简介
在一个类中使用内部类,可以在内部类中直接存取其所在类的私有成员变量。本节首先介绍成员内部类。成员内部类的语法如下:
public class OuterClass { //外部类
private class InnerClass { //内部类
//…
}
}
在内部类中可以随意使用外部类的成员方法以及成员变量,尽管这些类成员被修饰为private。图7.28充分说明了内部类的使用,尽管成员变量i以及成员方法g)都在外部类中被修饰为private,但在内部类中可以直接使用外部类中的类成员。
内部类的实例一定要绑定在外部类的实例上,如果从外部类中初始化一个内部类对象,那么内部类对象就会绑定在外部类对象上。内部类初始化方式与其他类初始化方式相同,都是使用new关键字。下面来看一个实例。
例 7.21 在项目中创建OuterClass类,在类中定义innerClass内部类和doit()方法,在主方法中创建OuterClass类的实例对象和doit()方法。
代码如图所示:
例7.21中的外部类创建内部类实例与其他类创建对象引用时相同。内部类可以访问它的外部
类成员,但内部类的成员只有在内部类的范围之内是可知的,不能被外部类使用。图7.29说明了内
部类InnerClass对象与外部类OuterClass对象的关系。
从图7.29中可以看出,内部类对象与外部类对象关系非常紧密,内外可以交互使用彼此类中定义的变量。
注意:
如果在外部类和非静态方法之外实例化内部类对象,需要使用外部类。内部类的形式指定该对象的类型。
在例7.21的主方法中如果不使用doit)方法返回内部类对象引用,可以直接使用内部类实例化内部类对象,但由于是在主方法中实例化内部类对象,必须在new *** 作符之前提供一个外部类的引用。
例如,在主方法中实例化一个内部类对象。
public static void main(String args[]) {
OuterClass out=new OuterClass();
OuterClass.innerClass in=out.doit();
OuterClass.innerClass in2=out.new innerClass(); //实例化内部类对象
}
从上面代码可以看出,在实例化内部类对象时,不能在new *** 作符之前使用外部类名称实例化内部类对象,而应该使用外部类的对象来创建其内部类的对象。
内部类对象会依赖于外部类对象,除非已经存在一个外部类对象,否则类中不会出现内部类对象。
2.内部类向上转型为接口
如果将一个权限修饰符为private的内部类向上转型为其父类对象,或者直接向上转型为一个接口,在程序中就可以完全隐藏内部类的具体实现过程。可以在外部提供一个接口,在接口中声明一个方法。如果在实现该接口的内部类中实现该接口的方法,就可以定义多个内部类以不同的方式实现接口中的同一个方法,而在一般的类中是不能多次实现接口中同一个方法的,这种技巧经常被用在Swing编程中,可以在一个类中做出多个不同的响应事件。
例 7.22下面修改例7.21,在项目中创建InterfaceInner类,并定义接口OutInterface,使内部类InnerClass实现这个接口,最后使doit()方法返回值类型为该接口。
代码如图所示:
运行结果如图所示:
从上述实例中可以看出,OuterClass2类中定义了一个修饰权限为private的内部类,这个内部类实现了Outlnterface 接口,然后修改doit()方法,使该方法返回一个Outlnterface 接口。由于内部类InnerClass修饰权限为private,所以除了OuterClass2类可以访问该内部类之外,其他类都不能访问,而可以访问 doit()方法。由于该方法返回一个外部接口类型,这个接口可以作为外部使用的接口。它包含一个f()方法,在继承此接口的内部类中实现了该方法,如果某个类继承了外部类,由于内部的权限不可以向下转型为内部类InnerClass,同时也不能访问f()方法,但是却可以访问接口中的f()方法。例如,InterfaceInner类中最后一条语句,接口引用调用f()方法,从执行结果可以看出,这条语句执行的是内部类中的f()方法,很好地对继承该类的子类隐藏了实现细节,仅为编写子类的人留下一个接口和一个外部类,同时也可以调用f()方法,但是f()方法的具体实现过程却被很好地隐藏了,这就是内部类最基本的用途。
注意:
非内部类不能被声明为private或protected访问类型。
3. 使用this 关键字获取内部类与外部类的引用
如果在外部类中定义的成员变量与内部类的成员变量名称相同,可以使用this关键字。
例7.23 在项目中创建TheSameName类,在类中定义成员变量x,再定义一个内部类Inner.在内部类中也创建x变量,并在内部类的dot0方法中分别 *** 作两个x变量。
代码如图所示:
遇到内部类与外部类的成员变量重名的情况,可以使用this关键字进行处理。例如,在内部类中使用this.x语句可以调用内部类的成员变量x,而使用TheSameName.this.x语句可以调用外部类的成员变量x,即使用外部类名称后跟一个点 *** 作符和this关键字便可获取外部类的一个引用。Ce 图7.31给出了例7.23在内存中变量的布局情况。
在内存中所有对象均被放置在堆中,方法以及方法中的形参或局部变量放置在栈中。在图7.31中,栈中的doit)方法指向内部类的对象,而内部类的对象与外部类的对象是相互依赖的,Outer.this对象指向外部类对象。
综上所述,使用成员内部类时,应该遵循以下原则:
(1) 可以有各种修饰符,可以用private、public、 protected、 static、final、 abstract 等修饰;
(2) 如果内部类有static限定,就是类级别的,否则为对象级别。类级别可以通过外部类直接访问,对象级别需要先生成外部的对象后才能访问;
(3) 内外部类不能同名;
(4) 非静态内部类中不能声明任何static成员;
(5) 内部类可以互相调用。
内部类不仅可以在类中进行定义,也可以在类的局部位置定义,如在类的方法或任意的作用域中均可以定义内部类。
例 7.24 修改例7.22,将InnerClass类放在doit)方法的内部。
代码如图所示:
从上述代码中可以看出,内部类被定义在了doit()方法内部。但是有一点值得注意,内部类
InnerClass2是doit()方法的一部分,并非OuterClass3类中的一部分,所以在doit()方法的外部不能访问该内部类,但是该内部类可以访问当前代码块的常量以及此外部类的所有成员。
下面将例7.24中定义的内部类再次进行修改,在doit()方法中将returm语句和内部类定义语句合并在一起,
例7.25 在return语句中编写返回值为一个匿名内部类。
代码如图所示:
从例7.25中可以看出,笔者将doit0方法修改得有一些莫名其妙,但这种写法确实被Java编译器认可,在doit0方法内部首先返回一个OutInterface2的引用,然后在returm语句中插入一个定义内部类的代码,由于这个类没有名称,所以这里将该内部类称为匿名内部类。实质上这种内部类的作用就是创建一个实现于OutInterface2接口的匿名类的对象。
匿名类的所有实现代码都需要在大括号之间进行编写。语法如下:
return new A() {
…//内部类体
};
其中,A指类名。
由于匿名内部类没有名称,所以匿名内部类使用默认构造方法来生成Outlnterface2对象。在匿名内部类定义结束后,需要加分号标识,这个分号并不是代表定义内部类结束的标识,而是代表创建OutInterface2引用表达式的标识。
说明:
匿名内部类编译以后,会产生以“外部类名$序号”为名称的,class文件,序号以1-n排列,分别代表1-n个匿名内部类。
使用匿名内部类时应该遵循以下原则:
(1) 匿名类没有构造方法;
(2)匿名类不能定义静态的成员; 等修饰;
(3) 匿名类不能用private、public.protected、static,final,abstract
(4) 只可以创建一个匿名类实例。
7.6.4 静态内部类在内部类前添加修饰符static,这个内部类就变为静态内部类了。一个静态内部类中可以声明静态成员,但是在非静态内部类中不可以声明静态成员。静态内部类有一个最大的特点,就是不能使用外部类的非静态成员,所以静态内部类在程序开发中比较少见。
普通的内部类对象隐式地在外部保存了一个引用,指向创建它的外部类对象,但如果内部类被定义为static,就会有更多的限制。静态内部类具有以下两个特点:
(1) 如果创建静态内部类的对象,不需要创建其外部类的对象;
(2) 不能从静态内部类的对象中访问非静态外部类的对象。
例如,定义一个静态内部类StaticInnerClass,可以使用如下代码:
面代码中,在内部类的doitInner()方法中调用成员变量x,由于Inner被修饰为static形式,而成员变量x却是非static类型的,所以在doitInner)方法中不能调用x变量。
进行程序测试时,如果在每一个Java文件中都设置一个主方法,将出现很多额外代码,而程序本身并不需要这些主方法,为了解决这个问题,可以将主方法写入静态内部类中。
例7.26 在静态内部类中定义主方法。
代码如图所示:
如果编译例7.26中的类,将生成一个名称为StaticInnerClass$Inner的独立类和一个StaticInner
Class类,只要使用java StaticInnerClass$Inner,就可以运行主方法中的内容,这样当完成测试,需要将所有.class文件打包时,只要删除StaticInnerClass$Inner独立类即可。
内部类和其他普通类一样可以被继承,但是继承内部类比继承普通类复杂,需要设置专门的语法来完成。
例7.27 在项目中创建OutputlnnerClass类,使OutputlnnerClass类继承ClassA类中的内部类ClassB。
代码如图所示:
在某个类继承内部类时,必须硬性给予这个类一个带参数的构造方法,并且该构造方法的参数必须是该内部类的外部类引用,就像例子中的ClassA a,同时在构造方法体中使用asuper()语句。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)