面向对象一文搞定,终生难忘(面向对象心得)
644
2022-05-29
前面我们学习了类、对象、封装、继承、多态等面向对象编程的基本概念,初步了解了面向对象程序设计理念,接下来我们继续学习面向对象编程的一些重要概念:抽象类、接口和内部类。其中,抽象类是从多个具体类中抽象出来的父类,可以作为子类的模板,实现更加丰富的类继承;接口是Java语言中实现多重继承的重要工具,Java是单继承的语言,而实际应用中某些类往往需要继承多个类的属性与行为,接口很好地解决了单继承的这一缺陷;内部类是定义在类中的类,它同样有着非常重要的作用,如更好地实现隐藏、实现多重继承、实现同一个类中两种同名方法的调用等。
7.1 抽象类
在面向对象程序设计中,常常会遇到这样的问题:对于父类中的某些方法,其不同子类中的这些方法肯定有所不同,这些方法必须在子类中重写,故而根本无需在父类中实现这些方法。面对这类问题,Java提供了抽象类,对一系列看上去不同但本质上相同的具体概念进行抽象,用来表征对问题领域进行分析、设计中得出的抽象概念,使类设计的体系结构更加清晰。
Java语言中提供了abstract关键字,表示抽象的意思。用abstract关键字修饰的类称为抽象类,抽象类有类似“模板”的作用,我们可以定义抽象类来表征某些子类或对象的广义属类的共有特征,但是抽象类不能直接使用new关键字创建对象,只能被继承,并在其子类中将其特征具体化;用abstract关键字修饰的方法称为抽象方法,它是一个不完整的方法,只有声明,没有方法体。抽象类可以包含抽象方法,也可以不包含。定义抽象类和抽象方法的具体语法如下:
abstract class 抽象类名{ // 定义抽象类,用abstract修饰
[访问修饰符]abstract 返回类型方法名称(参数); // 定义抽象方法,用abstract修饰,无方法体
}
通过abstract关键字标记,抽象类和抽象方法可以让开发人员明确该类或方法的作用,进而在继承抽象类的时候重写其抽象方法,以针对性地解决具体问题。定义抽象类或抽象方法的时候,需要注意以下几点:
抽象类和抽象方法必须使用abstract修饰符来修饰,不能使用private修饰符,因为子类需要有继承抽象类的权限。
由于抽象类是需要被继承的,所以抽象类不能用final修饰,即关键字abstract和final不能同时使用。
抽象类不能被实例化,可以有构造方法和普通方法,且构造方法不能声明为抽象的。
普通方法内有方法体,而抽象方法不能有方法体。
抽象方法不能使用static修饰符来修饰,因为static修饰的方法可以通过类名调用,而调用一个没有方法体的方法会报错。
如果一个类继承了抽象类,则该类必须实现抽象类中的全部抽象方法,除非子类也是抽象类。
接下来,我们通过具体问题来体会抽象类与抽象方法的具体应用。几何形状是大家熟悉不过的,它们形状千姿百态,有圆形、正方形、三角形等,这些几何形状都可以计算面积,但是计算方法却不同。可见,计算面积是几何形状的共有特征,而不同的几何形状又有不同的具体实现。于是,为了设计出结构清晰的几何形状面积计算类体系,我们可以先定义抽象的几何形状类Shape类,该类包含公共的颜色属性color,也包公共的面积计算方法getArea()方法,显然该方法也必须是抽象的。接着,我们进一步定义圆形类Circle类和正方形类Square类,让这两个类继承Shape类,并实现getArea()方法。
下面我们给出上述问题的具体实现,如例7-1所示。
例7-1 Demo0701.java
1 package com.aaa.p0701;
2
3 abstract class Shape { // 定义抽象的几何形状类Shape类
4 String color;
5 public Shape(String color){
6 this.color = color;
7 System.out.println("当前图形颜色是:" + color);
8 }
9 abstract double getArea(); // 求面积的抽象方法
10 }
11 class Circle extends Shape{ // 定义Shape类的子类Circle类
12 private final double PI = 3.1415926; // 声明圆周率常量
13 private double r; // 声明圆半径变量
14 public Circle(String color,double radius) {
15 super(color);
16 this.r = radius;
17 }
18 @Override
19 double getArea() { // 重写抽象类中的getArea()方法
20 return PI * r * r;
21 }
22 }
23 class Square extends Shape{ // 定义Shape类的子类Square类
24 private double e; // 声明正方形的边长
25 public Square(String color,double edge) {
26 super(color);
27 this.e = edge;
28 }
29 @Override
30 double getArea() { // 重写抽象类中的getArea()方法
31 return e * e;
32 }
33 }
34
35 public class Demo0701{
36 public static void main(String[] args) {
37 Circle circle = new Circle("红色",3);
38 System.out.println("圆的面积是:" + circle.getArea());
39 Shape square = new Square("绿色",4);
40 System.out.println("正方形的面积是:" + square.getArea());
41 }
42 }
程序运行结果如下:
当前图形颜色是:红色
圆的面积是:28.274333400000003
当前图形颜色是:绿色
正方形的面积是:16.0
示例7-1中,定义了抽象的Shape类,并在抽象类中定义了抽象的getArea()方法。在子类Circle类和Square类中分别实现了适合其自身特性的getArea()方法。
注意:抽象类不可以直接用new关键字创建对象,但并不能说明抽象类不可以创建对象。例如在例7-1中,main()方法中第39行代码处创建了抽象类Shape类的对象,但指向的是其子类的对象。
7.2 接口
Java是一门单继承的语言,一个子类只能继承一个父类。但是在编程实践中,对于某些类,只继承一个抽象类显然无法满足要求,需要实现多个抽象类的抽象方法才能解决问题,这就需要通过接口来实现。在Java中,允许通过一个类实现多个接口来实现类似于多重继承的概念。
7.2.1 接口的定义
接口可以看作是从多个相似的类中抽象出来的规范,不提供任何实现,体现了规范和实现分离的思想。例如,计算机上提供了USB插槽,只要一个硬件遵守USB规范,就能插入USB插槽,可以是鼠标、键盘、数据线等,计算机无须关心是和哪个硬件对象建立了联系。同样,软件系统的开发也需要采用规范和实现分离的思想,即采用面向接口的编程思想,从而降低软件系统模块之间的耦合度,提高系统的扩展性和可维护性。
在Java语言中,提供了interface关键字,用于声明接口,其语法格式如下:
[public]interface 接口名[extends 接口1,接口2...]{
[public][static][final]数据类型 常量名 = 值;
[public][abstract] 返回值的数据类型 方法名(参数列表);
默认方法...
}
在上述语法中,当interface关键字前加上public修饰符时,接口可以被任何类的成员访问。如果省略public,则接口只能被与它处在同一包中的成员访问。extends语句与类继承中的extends语句基本相同,不同点在于接口可以继承自多个父接口,父接口之间使用逗号分隔。
接口中定义的变量和方法都包含默认的修饰符,其中定义的变量默认声明为“public static final”,即全局静态常量,定义的方法默认声明为“public abstract”,即抽象方法。例如,定义一个Charge接口(收费接口),内有接口常量PORT_STYLE和成员方法getCharge(),代码如下:
interface Charge(){
int PORT_STYLE = 1; // 接口常量
void getCharge(); // 接口方法声明
}
7.2.2 接口实现
接口与抽象类相似,也包含抽象方法,因此不能直接实例化接口,即不能使用new创建接口的实例。但是,可以利用接口的特性来创造一个新的类,然后再用新类来创建对象,利用接口创建新类的过程称为接口的实现。实现接口的目的主要是在新类中重写抽象的方法,当然也可以使用抽象类来实现抽象方法的重写。
接口的实现需要使用implements关键字,即在声明一个类的同时用关键字implements来实现接口,实现接口的类一般称为接口的实现类,具体语法如下:
[修饰符]class 类名 implements 接口1,接口2, 接口3,...{ // 如果实现多个接口,以逗号隔开
...
}
一个类实现一个接口时,需要注意如下问题:
如果实现接口的类不是抽象类,则该类必须实现接口的所有抽象方法。
在类中实现接口的抽象方法时,必须使用与接口中完全一致的方法名,否则只是定义一个新的方法,而不是实现已有的抽象方法。
接下来,通过案例来演示接口的实现,如例7-2所示。
例7-2 Demo0702.java
1 package com.aaa.p070202;
2
3 interface PCI { // 定义PCI接口
4 String serialNumber = "9CC0AC186027";
5 void start();
6 void run();
7 void stop();
8 }
9 public class VideoCard implements PCI{ // 定义显卡类实现PCI接口
10 @Override
11 public void start() {
12 System.out.println("显卡开始启动");
13 }
14 @Override
15 public void run() {
16 System.out.println("显卡序列号是:" + serialNumber);
17 System.out.println("显卡开始工作");
18 }
19 @Override
20 public void stop() {
21 System.out.println("显卡停止工作");
22 }
23 }
24 public class Demo0702{
25 public static void main(String[] args) {
26 VideoCard videoCard=new VideoCard();
27 videoCard.start();
28 videoCard.run();
29 videoCard.stop();
30 }
31 }
程序的运行结果如下:
显卡序列号是:9CC0AC186027
显卡开始启动
显卡开始工作
显卡停止工作
从运行结果可以看到,程序中定义了一个PCI接口,其定义了一个全局常量serialNumber和3个抽象方法start()、run()、stop(),显卡类VideoCard实现了PCI接口的这3个抽象方法。在实现类的方法中调用接口的常量,在Demo0702测试类中创建了显卡类VideoCard的实例并输出运行结果。
7.2.3 接口的继承
在现实世界中,网络通信具有一定的标准,手机只有遵守相应的标准规范才可以使用相应的网络。然而,随着移动互联网技术的发展,网络通信标准从之前的2G、3G到目前的4G、5G,而且6G也已在研发之中。Java程序中的接口与网络通信标准类似,定义之后,随着技术的不断发展以及应用需求的不断增加,接口往往也需要更新迭代,主要是功能扩展,增加新的方法,以适应新的需求。但是,使用直接在原接口中增加方法的途径来扩展接口可能会带来问题:所有实现原接口的实现类都将因为原来接口的改变而不能正常工作。为了既能扩展接口,又不影响原接口的实现类,一种可行的方法是通过创建原接口的子接口来增加新的方法。
接口的继承与类的继承相似,都是使用extends关键字来实现继承,当一个接口继承父接口时,该接口会获得父接口中定义的所有抽象方法和常量。但是,接口的继承比类的继承要灵活,一个接口可以继承多个父接口,这样可以通过接口继承将多个接口合并为一个接口。接口继承的语法格式如下:
1 interface 接口名 extends 接口1,接口2,接口3,... {
2 ...
3 }
接下来,通过案例来演示接口的继承,如例7-3所示。
例7-3 Demo0703.java
1 package com.aaa.p070203;
2
3 interface I3G {
4 void onLine(); // 上网
5 void call(); // 打电话
6 void sendMsg(); // 发短信
7 }
8 interface I4G extends I3G{
9 void watchVideo(); // 看视频
10 }
11 class Nokia implements I3G{
12 @Override
13 public void call() {
14 System.out.println("打电话功能");
15 }
16 @Override
17 public void sendMsg() {
18 System.out.println("打发信息功能");
19 }
20 @Override
21 public void onLine() {
22 System.out.println("上网功能");
23 }
24 }
25 class Mi implements I4G{
26 @Override
27 public void call() {
28 System.out.println("打电话功能");
29 }
30 @Override
31 public void sendMsg() {
32 System.out.println("打发信息功能");
33 }
34 @Override
35 public void onLine() {
36 System.out.println("上网功能");
37 }
38 @Override
39 public void watchVideo() {
40 System.out.println("看视频功能");
41 }
42 }
43 public class Demo0703 {
44 public static void main(String[] args) {
45 System.out.println("Nokia手机使用第3代通信技术");
46 Nokia nokia = new Nokia();
47 nokia.call();
48 nokia.onLine();
49 nokia.sendMsg();
50 System.out.println("小米手机使用第4代通信技术");
51 Mi mi = new Mi();
52 mi.call();
53 mi.onLine();
54 mi.sendMsg();
55 mi.watchVideo();
56 }
57 }
程序的运行结果如图下:
Nokia手机使用第3代通信技术
打电话功能
上网功能
打发信息功能
小米手机使用第4代通信技术
打电话功能
上网功能
打发信息功能
看视频功能
从运行结果可以看到,I4G接口继承了I3G接口,直接继承了I3G接口中的3个抽象方法call()、onLine()、sendMsg(),并新增了一个抽象方法watchVide(),在main()方法中Nokia类实现I3G接口,从而实现父接口的3个抽象方法,而Mi类实现了I4G接口,实现了子接口的4个抽象方法。
编程技巧: 如果一个类同时继承类并继承某个接口,需要先extends父类,再implements接口,格式如下:
子类 extends 父类 implements [接口列表]{
...
}
7.2.4 利用接口实现多重继承
Java语言规定一个类只能继承一个父类,这给实际开发带来了许多困扰,因为许多类需要继承多个父类的成员才能满足需求,这种问题称为多重继承问题。然而,我们也不能将多个父类简单地合并成一个父类,因为每个父类都有自己的一套代码,合并到一起之后可能会出现同一方法的多种不同实现,由此会产生代码冲突,增加代码的不可靠性。有了接口以后多重继承问题就迎刃而解了,由于一个了可以实现多个接口,所以在程序设计的过程中我们可以把一些“特殊类”设计成接口,进而通过接口间接地解决多重继承问题。一个类实现多个接口时,在implements语句中分隔各个接口名,此时这些接口就可以被理解成特殊的类,而这种做法实际上就是使子类获得了多个父类的成员,并且由于接口成员没有实现细节,实现接口的类只能有一个具体的实现细节,从而避免了代码冲突,保证了Java代码的安全性和可靠性。
接下来,通过案例来演示利用接口实现多重继承,如例7-4所示。
例7-4 Demo0704.java
1 package com.aaa.p070204;
2
3 interface IFly { // 定义IFly接口
4 void takeoff(); // 起飞方法
5 void land(); // 落地方法
6 void fly(); // 飞行方法
7 }
8 interface ISail{ // 定义ISail接口
9 void dock(); // 停靠方法
10 void cruise(); // 航行方法
11 }
12 class Vehicle{ // 定义交通工具Vehicle类
13 private double speed;
14 void setSpeed(int sd){ // 设置速度方法
15 this.speed = sd;
16 System.out.println("设置速度为" + speed);
17 }
18 void speedUp(int num){ // 加速方法
19 this.speed += num;
20 System.out.println("加速" + num + ",速度变为" + speed);
21 }
22 void speedDown(int num){ // 减速方法
23 this.speed -= num;
24 System.out.println("减速" + num + ",速度变为" + speed);
25 }
26 }
27 class SeaPlane extends Vehicle implements IFly,ISail{ // 定义水上飞机类
28 public void takeoff() {
29 System.out.println("水上飞机开始起飞");
30 }
31 public void land() {
32 System.out.println("水上飞机开始落地");
33 }
34 public void fly() {
35 System.out.println("水上飞机可以飞行");
36 }
37 public void dock() {
38 System.out.println("水上飞机可以停靠");
39 }
40 public void cruise() {
41 System.out.println("水上飞机可以航行");
42 }
43 }
44 public class Demo0704 {
45 public static void main(String[] args) {
46 SeaPlane sp = new SeaPlane();
47 sp.takeoff();
48 sp.setSpeed(2)
49 sp.speedUp(2)
50 sp.fly();
51 sp.speedDown(2)
52 sp.land();
53 sp.cruise();
54 sp.speedDown(2)
55 sp.dock();
56 }
57 }
程序的运行结果如图下:
水上飞机开始起飞
设置速度为2
加速2,速度变为4
水上飞机可以飞行
减速2,速度变为2
水上飞机开始落地
水上飞机可以航行
减速2,速度变为0
水上飞机可以停靠
例7-4中,水上飞机类SeaPlane继承了交通工具类Vehicle,并且实现了IFly接口和ISail接口。从程序运行结果中可以看到,它时具有了交通工具的功能,还增加了飞行功能和航行功能。
7.2.5 接口默认方法
在程序开发中,如果之前创建了一个接口,并且已经被大量的类实现,当需要再添加新的方法以扩充这个接口的功能的时候,就会导致所有已经实现的子类都要重写这个方法。但是,在接口中使用默认方法就不会有这个问题,所以从 JDK8 开始新加了接口默认方法,便于接口的扩展。
接口默认方法是一个默认实现的方法,并且不强制实现类重写此方法,使用default关键字来修饰。接口新增的默认方法在实现类中可以直接使用。
接下来,通过案例来演示接口默认方法的使用,如例7-5所示。
例7-5 Demo0705.java
1 package com.aaa.p070205;
2
3 public interface ICat { // 定义ICat接口
4 void play(); // 抽象方法
5 default void run(){ // 默认方法
6 System.out.println("猫咪在跑,猫步...");
7 }
8 }
9 class BlackCat implements ICat{ // 黑猫类实现了ICat接口
10 @Override
11 public void play() { // 重写ICat接口的抽象方法
12 System.out.println("黑猫在玩耍...");
13 }
14 }
15 public class Demo0705 {
16 public static void main(String[] args) {
17 BlackCat cat = new BlackCat();
18 cat.play();
19 cat.run();
20 }
21 }
程序的运行结果如下:
黑猫在玩耍...
猫咪在跑,猫步...
例7-5中,ICat接口定义了抽象方法play()和默认方法run(), BlackCat类实现了ICat接口,并重写了抽象方法play(),通过测试类Demo0702中的main()方法创建了BlackCat类的实例,调用play()方法和run()方法后发现,ICat接口接口的默认方法run()可以被它的实现类的对象直接调用。
注意:接口允许定义多个默认方法,其子类可以实现多个接口,因此接口默认方法可能会出现同名情况,此时子类在实现或者调用默认方法时通常遵循以下原则:
(1)子类中的同名方法优先级最高。
(2)如果第一条无法进行判断,那么子接口的优先级更高;函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果接口B继承了接口A,那么接口B就比接口A更加具体。
7.2.6 接口实现多态
在之前的章节中,我们讲解了使用继承机制来实现多态。事实上,使用接口也同样可以实现多态。
接下来,通过某汽车销售店案例演示如何通过接口实现多态,如例7-6所示。
例7-6 Demo0706.java
1 package com.aaa.p070206;
2
3 interface ICar{ // 定义ICar接口
4 String showName();
5 double getPrice();
6 }
7 class Haval implements ICar{ // 定义Haval汽车类
8 @Override
9 public String showName() {
10 return "哈佛SUV";
11 }
12 @Override
13 public double getPrice() {
14 return 150000;
15 }
16 }
17 class GreatWall implements ICar{ // 定义GreatWall汽车类
18 @Override
19 public String showName() {
20 return "长城汽车";
21 }
22 @Override
23 public double getPrice() {
24 return 68000;
25 }
26 }
27 class CarShop{ // 定义汽车销售店CarShop类
28 private double money = 0; // 定义销售金额成员
29 public void sellCar(ICar car){ // 定义销售汽车方法
30 System.out.println("车型:" + car.showName() + "价格:" + car.getPrice());
31 money += car.getPrice();
32 }
33
34 public double getMoney(){ // 定义获取金额方法
35 return money;
36 }
37 }
38
39 public class Demo0706 { // 测试类
40 public static void main(String[] args) {
41 CarShop shop = new CarShop();
42 Haval haval = new Haval(); // Haval类的对象
43 shop.sellCar(haval);
44 GreatWall greatWall = new GreatWall(); // GreatWall类对象
45 shop.sellCar(greatWall);
46 System.out.println("销售总收入:" + shop.getMoney());
47 }
48 }
程序的运行结果如下:
车型:哈佛SUV价格:150000.0
车型:长城汽车价格:68000.0
销售总收入:218000.0
例7-6中,ICar接口定义了抽象方法showName()和getPrice(), Haval类和GreatWall类实现了ICar接口,汽车销售店类CarShop针对实现ICar接口的实现类进行销售并汇总金额。在测试类 Demo0705的main 方法中创建 CarShop类的实例shop、Haval类的实例haval、GreatWall类的greatWall,通过shop对象的sellCar()方法对汽车对象havel和greatWall销售,并调用getMoney()方法统计销售总收入。这样,我们便通过ICar接口实现了多态,
7.2.7 抽象类和接口的比较
抽象类与接口是Java中对于抽象类定义进行支持的两种机制,抽象类和接口都用于为对象定义共同的行为,二者比较如下:
抽象类和接口都包含抽象方法。
抽象类可以有非抽象方法,接口中如果要定义为非抽象方法,需要标注为接口默认方法。
接口中只能有常量,不能有变量;抽象类中既可以有常量,也可以有变量。
一个类可以实现多个接口,但只能继承一个抽象类。
在程序设计时,应该根据具体业务需求来确定是使用抽象类还是接口。如果子类需要从父类继承一些变量或继承一些抽象方法、非抽象方法,可以考虑使用抽象类;如果一个类不需要继承,只需要实现某些重要的抽象方法,可以考虑使用接口。
知识点拨:在面向接口编程的思想中,接口只用关心操作,但不用关心这些操作的具体实现细节,可以使开发者将主要精力用来程序设计。通过面向接口编程,可以降低类与类、类与接口、层与层之间的耦合度。当设计和实现分离的时候,面向接口编程是一种解决问题的很好方式。
7.3 内部类
大多数情况下,类被定义为一个独立的程序单元。但在某些情况下,也可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类(也称嵌套类),包含内部类的类也被称为外部类。内部类包括4种:成员内部类、局部内部类、静态内部类和匿名内部类。
一般情况下,内部类有如下几个属性:
内部类和外部类由Java编译器编译后生成的两个类是独立的。
内部类是外部类的一个成员,可以使用外部类的类变量和实例变量,也可以使用外部类的局部变量。
内部类可以被protected或private修饰。当一个类中嵌套另一个类时,访问保护并不妨碍内部类使用外部类的成员。
内部类被static修饰后,不能再使用局部范围中或其他内部类中的数据和变量。
7.3.1 成员内部类
成员内部类是最普通的内部类,它定义于另一个类的内部,与外部类的成员变量、成员方法同级。成员内部类可以访问外部类的所有成员,外部类同样可以其成员内部类的所有成员。但是,成员内部类是依附外部类而存在的,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。
在外部类外创建一个内部类对象的语法格式如下:
外部类名.内部类名 引用变量名 = new 外部类名().new 内部类名()
接下来,通过案例来演示成员内部类的使用,如例7-7所示。
例7-7 Demo0707.java
1 package com.aaa.p070301;
2
3 class Outer { // 定义外部类
4 private String name = "外部类Outer";
5 private int num = 666;
6
7 class Inner{ // 定义内部类
8 private String name = "内部类Inner"; // 定义类成员
9 public void accessOuter()
10 {
11 System.out.println("在成员内部类中访问内部类的name:" + name);
12 // Outer.this表示外部类对象
13 System.out.println("在成员内部类中访问外部类的name:" + Outer.this.name);
14 System.out.println("在成员内部类中访问外部类的num:" + num);
15 }
16 }
17 }
18
19 public class Demo0707 {
20 public static void main(String[] args) {
21 Outer.Inner inner = new Outer().new Inner();
22 inner.accessOuter();
23 }
24 }
程序的运行结果如下:
在成员内部类中访问内部类的name:内部类Inner
在成员内部类中访问外部类的name:外部类Outer
在成员内部类中访问外部类的num:666
例7-7中,外部类Outer中定义了一个成员内部类Inner,在Inner类的成员方法accessOuter ()中访问其自身的成员变量name以及其外部类Outer的成员变量name和num,由于内部类和外部类的成员变量name重名,所以不能直接访问,只能用“Other.this.name”的形式进行访问,其中“Outer.this”表示外部类对象,而num只存在于外部类,内部类可以直接访问。
注意:成员内部类不能定义静态变量、静态方法和静态内部类。
7.3.2 局部内部类
局部内部类是指在成员方法中定义的类,这种类是局部的,和局部变量类似,只能在该方法或条件的作用域内使用,超出这些作用域就无法引用。
局部内部类的优势是,对于外部类完全隐藏,即使是包含他的外部类也是不可见的,是不能直接访问的,只有在方法中才可以创建内部类的实例并访问类中的方法和属性。
局部内部类的特点如下:
局部内部类不允许使用访问权限修饰符(public、private、protected)
局部内部类对外部完全隐藏,除了创建这个类的方法可以访问它以外,其他地方均不能访问。
接下来,通过案例来演示局部内部类的使用,如例7-8所示。
例7-8 Demo0708.java
1 package com.aaa.p070302;
2
3 class Outer{ // 定义外部类
4 private static String name = "外部类Outer";
5 private int num = 666;
6 public void display(){
7 int count = 5;
8 //局部内部类即嵌套在方法里面
9 class Inner{
10 public void accessOuter(){
11 System.out.println("在局部内部类中访问外部方法的变量:" + (count));
12 System.out.println("在局部内部类中访问外部类的name:" + Outer.name);
13 System.out.println("在局部内部类中访问外部类的num:" + num);
14 }
15 }
16 //局部内部类,在方法内部调用
17 new Inner().accessOuter();
18 }
19 }
20 public class Demo0708 {
21 public static void main(String[] args) {
22 Outer outer = new Outer();
23 outer.dispaly();
24 }
25 }
程序的运行结果如下:
在局部内部类中访问外部方法的变量:5
在局部内部类中访问外部类的name:外部类Outer
在局部内部类中访问外部类的num:666
例7-8中,外部类Outer的display()方法定义了一个内部类Inner,Inner类只能在display()方法中创建其实例对象并调用自身方法accessOuter(),该方法调用了外部类Outer的成员变量name和num以及display()方法内的局部变量count。从运行结果中发现,都可以正常输出,但是如果把第11行代码中的“count”后面加上“++”之后,会编译报错,因为局部变量是随着方法的调用而调用,随着调用结束而消失,但是我们调用局部内部类时创建的对象依旧在堆内存中,并没有被回收,如果访问的局部变量不是用final修饰的,当方法调用完毕后,依然存在堆内存中的对象就会出现找不到局部变量的问题,而被final修饰的变量可以看成是一个常量,存在于常量池中,不会被立刻回收。所以,针对局部内部类来说,它可以访问方法中的局部变量但不能进行修改。
注意:JDK1.8之后,即使不加final修饰符,系统也会默认加上。
7.3.3 静态内部类
静态内部类是指用static关键字修饰的成员内部类。静态内部类可以包含静态成员和非静态成员(实例成员),根据静态成员不能访问非静态成员的规则,静态内部类不能直接访问外部类的非静态成员,只能访问外部类的静态成员(即类成员)。创建静态内部类对象的语法格式如下:
外部类名.内部类名 引用变量名 = new 外部类名.内部类名()
接下来,通过案例来演示静态内部类的使用,如例7-9所示。
例7-9 Demo0709.java
1 Package com.aaa.p070303;
2
3 class Outer{
4 private static String name = "外部类Outer"; // 定义类静态成员
5 private static int num = 666;
6 static class Inner { // 定义静态内部类
7 public static String name = "内部类Inner"; // 定义类静态成员
8 public void accessOuter() {
9 // 静态内部类成员方法中访问外部类私有成员变量
10 System.out.println("在静态内部类中访问外部类的name:"+Outer.name);
11 System.out.println("在静态内部类中访问外部类的num:" + num);
12 }
13 }
14 }
15 public class Demo0709 {
16 public static void main(String[] args) {
17 System.out.println("静态内部类:" + Outer.Inner.name);
18 Outer.Inner obj = new Outer.Inner(); // 创建静态内部类对象
19 obj. accessOuter();
20
21 }
22 }
程序的运行结果如下所示。
静态内部类:内部类Inner
在静态内部类中访问外部类的name:外部类Outer
在静态内部类中访问外部类的num:666
例7-9中,Outer外部类中定义了一个静态内部类Inner,Inner类包含了静态成员变量name和成员方法accessOuter ()。访问静态内部类的静态成员变量,可以使用“外部类名.静态内部类名.静态成员变量”的形式;访问静态内部类的实例成员,则要先创建静态内部类对象,通过“new 外部类名.静态内部类名()”的形式访问。如果将第5行代码的“static”去掉,则在第11行代码调用的时候会报错,因为num属于外部类的非静态变量,不可以被其静态内部类直接访问。
注意:静态内部类不需要依赖外部类就可以直接创建;静态内部类不可以使用任何外部类的非static成员(包括变量和方法)。
7.3.4 匿名内部类
匿名内部类是一个没有显式名字的内部类。本质上看,它会隐式地继承一个类或者实现一个接口。换句话说,匿名内部类是一个继承了某个类或者实现了某接口的子类匿名对象。创建匿名内部类的语法格式如下:
new 类名/接口名/抽象类名(){
… // 匿名内部类实现部分
}
匿名内部类具有局部内部类的所有特点,同时它还具有如下特点:
匿名内部类必须继承一个类或者实现一个接口,类名前面不能有修饰符。
匿名内部类没有类名,因此没有构造方法。
匿名内部类创建之后只能使用一次,不能重复使用。
匿名内部类是我们平时编写代码时用得比较多的内部类,在编写事件监听的代码时使用匿名内部类不但可简化程序,而且可使代码更加容易维护。
接下来,通过案例来演示匿名内部类的使用,如例7-10所示。
例7-10 Demo0710.java
1 package com.aaa.p070304;
2
3 interface Inner { // 定义接口
4 void getName(String name);
5 }
6 public class Demo0710 {
7 public static void main(String[] args) {
8 new Inner(){ // 定义匿名类,并实现Inner接口
9 public void getName(String name) { // 重写getName()方法
10 System.out.println("我是匿名类的方法,获取name为:" + name);
11 }
12 }.getName("张三");
13 }
14 }
程序的运行结果如下:
我是匿名类的方法,获取name为:张三
例7-10中,在外部类Demo0710的main方法中创建了匿名内部类“Inner的对象”,并调用该类的成员方法getName (),传入参数“张三”,在创建Inner对象的时候,并没有给对象赋予名称,即“匿名”之意。
想一想:使用匿名内部类的优点和缺点有哪些?匿名内部类的使用场景有哪些?
7.4 本章小结
抽象类的主要作用是建立对象的抽象模型,定义通用的方法,起到类似“模板”的作用。抽象类中包含一般方法和抽象方法。抽象方法是没有方法体的方法,由抽象类的子类来定义实现。抽象类不能直接产生对象。包含抽象方法的类必须是抽象类,但抽象类可以不包含抽象的方法。
接口只规定了一个类的基本形式,不涉及任何实现细节。通过接口来创建类,称为接口的实现,该类为接口的实现类。Java语言中不允许类的多重继承,但可以通过接口实现多重继承。
Java支持在一个类A中声明一个类B,这样的类B称为内部类,类A称为内部类的外部类。内部类可以分为成员内部类、局部内部类、静态内部类、匿名内部类。内部类隐藏可以不想让用户知道的操作,极高的封装性。内部类对象可以访问创建它的外部类对象的内容,为开发者在设计时提供了更多的思路和捷径。匿名内部类一般用在写事件监听的代码时候,可以简化代码并使代码容易维护。
7.5 理论习题与实践练习
1.填空题
1.1 Java中使用____________关键字,来表示抽象的意思。
1.2 Java中使用____________关键字,来实现接口的继承。
1.3 __________是定义在类中的类,主要作用是将逻辑上相关的类放在一起。
2.选择题
2.1 以下关于Java语言抽象类的说法正确的是( )
A.抽象类可以直接实例化 B.抽象类不能有子类
C.抽象类可以多继承 D.抽象类可以有非抽象方法
2.2 内部类不包括()
A.成员内部类 B.静态内部类 C.匿名内部类 D.超级内部类
2.3 下列选项中,用于定义接口的关键字是( )
A.interface B.implements C.abstract D.class
2.4 下列选项中,接口使用时不可以加的关键字是( )
A.public B.static C.private D.final
2.5 下列选项中,可以实现类的多重继承 的是( )
A.类 B.基本类型 C.抽象类 D.接口
3.思考题
3.1 请描述什么是抽象类?
3.2 请描述如何使用接口实现多态?
3.3 请描述向上转型和向下转型的区别?
3.4 什么是接口?请简述抽象类和接口的区别?
3.5 采用内部类的好处是什么?
4.编程题
4.1 陆地上最常用的交通工具是汽车,海上最常用的交通工具是轮船,空中最常用的交通工具是飞机。现在要求实现一种海陆空三栖航行的交通工具。可以通过接口实现需求,实现思路参考如下:
创建汽车接口Motor,定义一个run (方法。
创建轮船接口Steamer,定义一个sailing ( )方法。
创建飞机接口P1ane,定义一个f1y (方法。
创建类SuperVehic 1e实现上述三个接口。
创建测试类进行测试。
4.2 模拟摄影师照相的过程,要求摄影师可以通过相机拍照也可以通过其他的设备照相,譬如:手机:摄影师可以拍人像、风景等任何物体。要求该系统具备良好的可维护性可扩展性。
摄影师可以对任何物体拍照,并没有局限于一个具体类别,所以先设计一个接口。
创建一个拍照接口,实现该接口的类都可以拍照,手机、相机、iPad等等。
创建摄影师类,实现摄影方法。
创建接口的实现类。
创建测试类,运行系统。
Java
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。