第7章 面向对象进阶

第7章 面向对象进阶

本章读者可以学到如下实例:

  • 实例059 经理与员工的差异
  • 实例060 重写父类中的方法
  • 实例061 计算几何图形的面积
  • 实例062 简单的汽车销售商场
  • 实例063 使用Comparable接口自定义排序
  • 实例064 策略模式的简单应用
  • 实例065 适配器模式的简单应用
  • 实例066 普通内部类的简单应用
  • 实例067 局部内部类的简单应用
  • 实例068 匿名内部类的简单应用
  • 实例069 静态内部类的简单应用
  • 实例070 实例化Class类的几种方式
  • 实例071 查看类的声明
  • 实例072 查看类的成员
  • 实例073 查看内部类信息
  • 实例074 动态设置类的私有域
  • 实例075 动态调用类中方法
  • 实例076 动态实例化类
  • 实例077 创建长度可变的数组
  • 实例078 利用反射重写toString()方法

实例059 经理与员工的差异

实例说明

对于在同一家公司工作的经理和员工而言,两者是有很多共同点的。例如每个月都要发工资,但是经理在完成目标任务后,还会获得奖金。此时,利用员工类来编写经理类就会少写很多代码,利用继承技术可以让经理类使用员王类中定义的属性和方法。本实例将通过继承演示经理与员工的差异。实例的运行效果如图7.1所示。

实现过程

Employee

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.mingrisoft;

import java.util.Date;

public class Employee {
// 员工的姓名
private String name;
// 员工的工资
private double salary;
// 员工的生日
private Date birthday;

// 获取员工的姓名
public String getName() {
return name;
}

// 设置员工的姓名
public void setName(String name) {
this.name = name;
}

// 获取员工的工资
public double getSalary() {
return salary;
}

// 设置员工的工资
public void setSalary(double salary) {
this.salary = salary;
}

// 获取员工的生日
public Date getBirthday() {
return birthday;
}

// 设置员工的生日
public void setBirthday(Date birthday) {
this.birthday = birthday;
}

}

Manager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.mingrisoft;

public class Manager extends Employee {
// 经理的奖金
private double bonus;

// 获得经理的奖金
public double getBonus() {
return bonus;
}

// 设置经理的奖金
public void setBonus(double bonus) {
this.bonus = bonus;
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.mingrisoft;

import java.util.Date;

public class Test {
public static void main(String[] args) {
// 创建Employee对象并为其赋值
Employee employee = new Employee();
employee.setName("Java");
employee.setSalary(100);
employee.setBirthday(new Date());
System.out.println("员工的姓名:" + employee.getName());
System.out.println("员工的工资:" + employee.getSalary());
System.out.println("员工的生日:" + employee.getBirthday());


// 创建Manager对象并为其赋值
Manager manager = new Manager();
manager.setName("HelloWorld");
manager.setSalary(3000);
manager.setBirthday(new Date());
manager.setBonus(2000);
// 输出经理的属性值
System.out.println("经理的姓名:" + manager.getName());
System.out.println("经理的工资:" + manager.getSalary());
System.out.println("经理的生日:" + manager.getBirthday());
System.out.println("经理的奖金:" + manager.getBonus());
}
}

运行效果

1
2
3
4
5
6
7
员工的姓名:Java
员工的工资:100.0
员工的生日:Sun Sep 20 15:49:47 CST 2020
经理的姓名:HelloWorld
经理的工资:3000.0
经理的生日:Sun Sep 20 15:49:47 CST 2020
经理的奖金:2000.0

可以看到,经理类通过继承得到了员工类的name,salary,birthday这三个属性。

指点迷津

Manager类中并未定义姓名等域,然而却可以使用,这就是继承的好处。

脚下留神

虽然使用继承能少写很多代码,但是不要滥用继承。在使用继承前,需要考虑一下两者之间是否真的是“is-a”的关系,这是继承的重要特征。本实例中,经理显然是员工,所以可以用继承。另外,子类也可以成为其他类的父类,这样就构成了一棵继承树

技术要点

在面向对象程序设计中,继承是其基本特性之一。在Java中如果想表明类A继承了类B,可以使用下面的语法定义类A:

1
2
3
public class A extends B{

}

类A称为子类、派生类或孩子类,
类B称为超类、基类或父类。
尽管类B是一个超类,但是并不意味着类B比类A有更多的功能。相反,类A比类B拥有的功能更加丰富。

指点迷津

在继承树中,从下往上越来越抽象,从上往下越来越具体

实例060 重写父类中的方法

实例说明

在继承了一个类之后,也就可以使用父类中定义的方法。然而,父类中的方法可能并不完全适用于子类。此时,如果不想定义新的方法,则可以重写父类中的方法。本实例将演示如何重写父类中的方法,实例的运行效果如图72所示。

实现过程

Employee.java

1
2
3
4
5
6
7
package com.mingrisoft;
public class Employee {
//定义测试用的方法
public String getInfo() {
return "父类:我是 牛逼上天公司 的员工!";
}
}

Manager.java

1
2
3
4
5
6
7
8
package com.mingrisoft;
public class Manager extends Employee {
@Override
//重写测试用的方法
public String getInfo() {
return "子类:我是 牛逼上天公司 的经理!";
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.mingrisoft;

public class Test {
public static void main(String[] args) {
// 创建Employee对象
Employee employee = new Employee();
// 输出Employee对象的getInfo()方法返回值
System.out.println(employee.getInfo());
// 创建Manager对象
Manager manager = new Manager();
// 输出Manager对象的getInfo()方法返回值
System.out.println(manager.getInfo());
// // 父类引用指向子类引用,多态,调用的是子类重写后的方法
// Employee employee2 = manager;
// System.out.println(employee2.getInfo());
}
}

运行效果

1
2
父类:我是 牛逼上天公司 的员工!
子类:我是 牛逼上天公司 的经理!

指点迷津

JavaSE5.0版中新增了注解功能,@Override是最常用的注解之一。**@Override注解只能应用在方法上,可以测试该方法是否重写了父类中的方法,如果没有则会在编译时报错。使用该注解可以很妤地避免重写时发生的各种问题,因此推荐读者使用**。

技术要点

本实例主要应用的是方法的重写。方法的重写(Overriding)只能发生在存在继承关系的类中
重写方法需要注意以下几点:

  • 重写方法与原来方法签名要相同,即方法名称和参数(包括顺序)要相同。
  • 重写方法的可见性不能小于原来的方法
  • 重写方法抛出异常的范围不能大于原来方法抛出异常的范围

实例061 计算几何图形的面积

实例说明

对于每个几何图形而言,都有一些共同的属性,如名字和面积等,而其计算面积的方法却各不相同。为了简化开发,本实例将定义一个超类来实现输出名字的方法,并使用抽象方法来计算面积。实例的运行效果如图7.3所示。

实现过程

1
2
3
4
5
6
7
8
9
package com.mingrisoft;
public abstract class Shape {
//获得图形的名称
public String getName() {
return this.getClass().getSimpleName();
}
//获得图形的面积
public abstract double getArea();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.mingrisoft;
public class Rectangle extends Shape {
private double length;
private double width;
//获得矩形的长和宽
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
//计算矩形的面积
public double getArea() {
return length * width;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.mingrisoft;
public class Circle extends Shape {
private double radius;
//获得圆形的半径
public Circle(double radius) {
this.radius = radius;
}
@Override
//计算圆形的面积
public double getArea() {
return Math.PI * Math.pow(radius, 2);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.mingrisoft;

public class Test {
public static void main(String[] args) {
Shape shape = null;
// 创建圆形对象并将半径设置成1
Circle circle = new Circle(1);
shape = circle;
System.out.print("图形的名称是:" + shape.getName());
System.out.println("\t图形的面积是:" + shape.getArea());
// 创建矩形对象并将长和宽设置成1
Rectangle rectangle = new Rectangle(1, 1);
shape = rectangle;
System.out.print("图形的名称是:" + shape.getName());
System.out.println("\t图形的面积是:" + shape.getArea());
}
}

运行效果

1
2
图形的名称是:Circle    图形的面积是:3.141592653589793
图形的名称是:Rectangle 图形的面积是:1.0

总结

  • 多态

指点迷津

在抽象类中,可以定义抽象方法(使用abstract修饰的方法),也可以定义普通方法。

  • 包含抽象方法的类必须是抽象类,
  • 而抽象类不是必须包含抽象方法。

对于抽象方法而言,仅定义一个声明即可,即抽象方法是没有方法体的。

技术要点

本实例应用的主要技术就是抽象类。下面对抽象类进行介绍。在设计类的过程中,通常会将一些类所具有的公共属性和方法移到超类中,这样就不必重复定义了。然而这些类的超类却经常没有实际意义。通常将它设置成抽象的,这样可以避免创建该类的对象。声明一个最简单的抽象类的代码如下:

1
public abstract class Shape{}

脚下留神

抽象类是不能直接实例化的,如果要获得该类的实例可以使用静态方法创建其实现类对象.

实例062 简单的汽车销售商场

实例说明

当顾客在商场购物时,卖家需要根据顾客的需求提取商品。对于汽车销售商场也是如此。用户需要先指定购买的车型,然后商家去提取该车型的汽车。本实例将实现一个简单的汽车销售商场,用来演示多态的用法。实例的运行效果如图74所示。

实现过程

(2)在com.mingrisoft包中新建一个抽象类,名称为Car,在该类中定义一个抽象方法getInfo关键代码如下:

1
2
3
4
5
package com.mingrisoft;
public abstract class Car {
//用来描述汽车的信息
public abstract String getInfo();
}

(3)在com.mingrisoft包中再创建一个名称为BMW的类,该类继承自Car并实现其getInfo()方法。关键代码如下

1
2
3
4
5
6
7
8
package com.mingrisoft;
public class BMW extends Car {
@Override
//用来描述汽车的信息
public String getInfo() {
return "BMW";
}
}

(4)在com.mingrisoft包中再创建一个名称为Benz的类,该类继承自Car并实现其getInfo()方法。关键代码如下:

1
2
3
4
5
6
7
8
package com.mingrisoft;
public class Benz extends Car {
@Override
//用来描述汽车的信息
public String getInfo() {
return "Benz";
}
}

(5)在com.mingrisoft包中再创建一个名称为CarFactory的类,该类定义了一个静态方法getCar,它可以根据用户指定的车型来创建对象。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.mingrisoft;
public class CarFactory {
public static Car getCar(String name) {
//如果需要BMW则创建BMW对象
if (name.equalsIgnoreCase("BMW")) {
return new BMW();
//如果需要Benz则创建Benz对象
} else if (name.equalsIgnoreCase("Benz")) {
return new Benz();
//暂时不能支持其他车型
} else {
return null;
}
}
}

(6)在com.mingrisoft包中再创建一个名称为Customer-的类,用来进行测试。在main()方法中,根据用户的需要提取了不同的汽车。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.mingrisoft;
public class Customer {
public static void main(String[] args) {
System.out.println("顾客要购买BMW:");
//用户要购买BMW
Car bmw = CarFactory.getCar("BMW");
//提取BMW
System.out.println("提取汽车:" + bmw.getInfo());
System.out.println("顾客要购买Benz:");
//用户要购买Benz
Car benz = CarFactory.getCar("Benz");
//提取Benz
System.out.println("提取汽车:" + benz.getInfo());
}
}

运行效果:

1
2
3
4
顾客要购买BMW:
提取汽车:BMW
顾客要购买Benz:
提取汽车:Benz

技术要点

本实例应用的主要技术就是面向对象程序设计中的多态。多态是面向对象程序设计的基本特性之一。
使用多态的好处就是可以屏蔽对象之间的差异,从而增强了软件的护展性和重用性
Java的多态主要是通过重写父类(或接口)中的方法来实现的

对于香蕉、桔子等水果而言,人们通常关心其能吃的特性。如果分别说香蕉能吃、桔子能吃,则当再增加新的水果种类,如菠萝时还要写个菠萝能吃,这是非常麻烦的。使用多态的话可以写成水果能吃,当需要用到具体的水果时,系统会自动帮忙替换,从而简化开发。

实例063 使用Comparable接口自定义排序

实例说明

默认情况下,保存在List集合中的数组是不进行排序的,不过可以通过使用Comparable接口自定义排序规则并自动排序。本实例将介绍如何使用Comparable接口自定义排序规则并自动排序。实例的运行效果如图75所示

实现过程

Employee.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.mingrisoft;

public class Employee implements Comparable<Employee> {

private int id;
private String name;
private int age;

public Employee(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}

// 实现定制排序
@Override
public int compareTo(Employee o) {
// // A.compareTo(B),A大于B时,返回1表示増序排序
// if (this.id > o.id) {
// return 1;
// } else if (this.id < o.id) {
// return -1;
// }
// return 0;

// A.compareTo(B),A大于B时,返回-1表示降序排序
if (this.id > o.id) {
return -1;
} else if (this.id < o.id) {
return 1;
}
return 0;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("员工的编号:" + id + ", ");
sb.append("员工的姓名:" + name + ", ");
sb.append("员工的年龄:" + age);
return sb.toString();
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.mingrisoft;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Test {
public static void main(String[] args) {
List<Employee> list = new ArrayList<Employee>();
list.add(new Employee(1, "Java", 23));
list.add(new Employee(3, "Java", 21));
list.add(new Employee(2, "Java", 22));
System.out.println("排序前:");
for (Employee employee : list) {
System.out.println(" " + employee);
}
System.out.println("排序后:");
// 执行自动排序
Collections.sort(list);
for (Employee employee : list) {
System.out.println(" " + employee);
}
}
}

运行效果

1
2
3
4
5
6
7
8
排序前:
员工的编号:1, 员工的姓名:Java, 员工的年龄:23
员工的编号:3, 员工的姓名:Java, 员工的年龄:21
员工的编号:2, 员工的姓名:Java, 员工的年龄:22
排序后:
员工的编号:3, 员工的姓名:Java, 员工的年龄:21
员工的编号:2, 员工的姓名:Java, 员工的年龄:22
员工的编号:1, 员工的姓名:Java, 员工的年龄:23

可以看到,调用了Collections.sort(list);后,list集合里的元素都按编号进行降序排序。

技术要点

本实例主要应用java.lang包中的Comparable接口来实现自定义排序规则。Comparable接口用于强行对实现它的每个类的对象进行整体排序。在实现该接口的类中,必须实现该接口中定义的compareTo()方法,用于指定排序规则。Comparable接口的定义如下:

1
2
3
pubic interface Comparable<t>{
int compareTo(T other);
}

如果一个类要实现这个接口,可以使用如下语句声明:

1
2
3
public class Employee implements Comparable<Employee>{

}

Employee中必须实现接口中定义的compareTo()方法。实现该方法后,

  • 如果将该对象保存到列表中,那么可以通过执行Collections.Sort()方法进行自动排序;
  • 如果保存到数组中,那么可以通过执行Array.sort()方法进行自动排序

指点迷津

如果不想实现在接口中定义的方法,则可以将类声明为抽象类,将接口中定义的方法声明为抽象方法。

实例064 策略模式的简单应用

实例说明

在使用图像处理软件处理图片后,需要选择种格式进行保存,然而各种格式在底层实现的算法并不相同,这刚好适合策略模式。本实例将演示如何使用策略模式与简单工厂模式组合进行实例开发。

实现过程

(2)在com.mingrisoft包中编写接口ImageSaver,在该接曰中定义save方法。关键代码如下:

1
2
3
4
5
6
package com.mingrisoft;

public interface ImageSaver {
//定义save()方法
void save();
}

(3)在com.mingrisoft包中再编写类GIFSaver,该类实现了ImageSaver接曰。在实现save方法时将图片保存为GIF格式,关键代码如下:

1
2
3
4
5
6
7
8
9
package com.mingrisoft;

public class GIFSaver implements ImageSaver {
@Override
//实现save()方法
public void save() {
System.out.println("将图片保存成GIF格式");
}
}

指点迷津:对于将图片保存成其他格式与存储为GIF格式类似,这里就不再赘述。
(4)在com.mingrisoft包中再编写类TypeChooser,该类根据用户提供的图片类型来选择合适的图片存储方式。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.mingrisoft;
// 简单工厂模式,通过传入的字符串来判断需要创建哪个类的对象
public class TypeChooser {
public static ImageSaver getSaver(String type) {
//使用if else语句来判断图片的类型
if (type.equalsIgnoreCase("GIF")) {
return new GIFSaver();
} else if (type.equalsIgnoreCase("JPEG")) {
return new JPEGSaver();
} else if (type.equalsIgnoreCase("PNG")) {
return new PNGSaver();
} else {
return null;
}
}
}

指点迷津:此处使用了简单工厂模式,根据描述图片类型的字符串创建相应的图片保存类的对象
(5)在com.mingrisoft包中再编写类User,该类模拟用户的操作,为类型选择器提供图片的类型。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.mingrisoft;
public class User {
public static void main(String[] args) {
System.out.print("用户选择了GIF\t格式:");
//获得保存图片为GIF类型的对象
ImageSaver saver = TypeChooser.getSaver("GIF");
saver.save();
//获得保存图片为JPEG类型的对象
System.out.print("用户选择了JPEG\t格式:");
saver = TypeChooser.getSaver("JPEG");
saver.save();
//获得保存图片为PNG类型的对象
System.out.print("用户选择了PNG\t格式:");
saver = TypeChooser.getSaver("PNG");
saver.save();
}
}

运行效果

1
2
3
用户选择了GIF   格式:将图片保存成GIF格式
用户选择了JPEG 格式:将图片保存成JPG格式
用户选择了PNG 格式:将图片保存成PNG格式

技术要点

本实例应用的最重要的技术就是策略模式。
对于策略模式而言,需要定义一个接口或者抽象类来表示各种策略的抽象,这样就可以使用多态来让虚拟机选择不同的实现类。然后让每种具体的策略来实现这个接口或继承抽象类,并为其中定义的方法提供具体的实现。
由于在选择适当的策略上有些不方便,需要不断地判断需要的类型,因此用简单工厂方法来实现判断过程。

实例065 适配器模式的简单应用

实例说明

对于刚从工厂生产出来的商品,有些功能并不能完全满足用户的需要。因此,用户通常会对其进行一定的改装工作。
本实例将为普通的汽车增加GPS定位功能,借此演示适配器模式的用法。实例的运行效果如图77所示。

实现过程

(2)在com.mingrisoft包中编写类Car,在该类中,首先定义两个属性,一个是name,表示汽车的名字;另一个是speed,表示汽车的速度。并为其提供getXXX()setXXX()方法,然后通过重写toString()方法来方便输出Car对象。关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.mingrisoft;
public class Car {
//表示名称
private String name;
//表示速度
private double speed;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getSpeed() {
return speed;
}

public void setSpeed(double speed) {
this.speed = speed;
}

@Override
//重写toString()方法
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("车名:" + name + ", ");
sb.append("速度:" + speed + "千米/小时");
return sb.toString();
}
}

(3)在com.mingrisoft包中再编写接口GPS,该接口中定义了geoLocation()方法,用来确定汽车的位置。关键代码如下:

1
2
3
4
5
6
7
8
package com.mingrisoft;

import java.awt.Point;

public interface GPS {
//提供定位功能
Point getLocation();
}

(4)在com.mingrisoft包中再编写类GPSCar,该类继承Car并实现GPS接口。在该类中首先实现geoLocation方法,用于实现确定汽车位置的功能,然后重写toString方法方便输出GPSCar对象。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.mingrisoft;

import java.awt.Point;

public class GPSCar extends Car implements GPS {
@Override
//利用汽车的速度来确定汽车的位置
public Point getLocation() {
Point point = new Point();
point.setLocation(super.getSpeed(), super.getSpeed());
return point;
}
@Override
//重写toString()方法
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString());
sb.append(", 坐标:(" + getLocation().x + ", " + getLocation().y + ")");
return sb.toString();
}
}

指点迷津:可以使用super关键字调用父类中定义的方法。

(5)在com.mingrisoft包中再编写类Test进行测试。在该类中,分别创建CarGPSCar对象,并对其初始化,然后输出这两个对象。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.mingrisoft;
public class Test {
public static void main(String[] args) {
System.out.println("自定义普通的汽车:");
Car car = new Car();
car.setName("Adui");
car.setSpeed(60);
System.out.println(car);
System.out.println("自定义GPS汽车:");
GPSCar gpsCar = new GPSCar();
gpsCar.setName("Audi");
gpsCar.setSpeed(60);
System.out.println(gpsCar);
}
}

运行效果:

1
2
3
4
5
自定义普通的汽车:
车名:Adui, 速度:60.0千米/小时
自定义GPS汽车:
车名:Audi, 速度:60.0千米/小时, 坐标:(60, 60)

技术要点

适配器模式可以在符合OCP原则(开闭原则)的基础上,为类增加新的功能。该模式涉及的角色主要有以下3个:

  • 目标角色:就是期待得到的接口,如本实例的GPS接口。
  • 源角色:需要被增加功能的类或接口,如本实例的Car类。
  • 适配器角色:新创建的类,在源角色的基础上实现了目标角色,如本实例的GPSCar类。

关于各个类的继承(实现)关系如图7.8所示。