Java 是面向对象的高级编程语言,类和对象是 Java 程序的构成核心。围绕着 Java 类和 Java 对象,有三大基本特性:封装是 Java 类的编写规范、继承是类与类之间联系的一种形式、而多态为系统组件或模块之间解耦提供了解决方案。
本文主要围绕这三大特性介绍一下 Java 面向对象、组件解耦的核心思想。
1、面向对象思想
面向对象编程是当今主流的程序设计思想,已经取代了过程化程序开发技术,Java 是完全面向对象编程语言,所以必须熟悉面向对象才能够编写 Java 程序。
面向对象的程序核心是由对象组成的,每个对象包含着对用户公开的特定功能和隐藏的实现部分。程序中的很多对象来自 JDK 标准库,而更多的类需要我们程序员自定义。
从理论上讲,只要对象能够实现业务功能,其具体的实现细节不必特别关心。
面向对象有以下特点:
(1)面向对象是一种常见的思想,比较符合人们的思考习惯;
(2)面向对象可以将复杂的业务逻辑简单化,增强代码复用性;
(3)面向对象具有抽象、封装、继承、多态等特性。
面向对象的编程语言主要有:C++、Java、C#等。
2、类和对象的关系
类:
对某类事物的普遍一致性特征、功能的抽象、描述和封装,是构造对象的模版或蓝图,用 Java 编写的代码都会在某些类的内部。类之间主要有:依赖、聚合、继承等关系。
对象:
使用 new 关键字或反射技术创建的某个类的实例。同一个类的所有对象,都具有相似的数据(比如人的年龄、性别)和行为(比如人的吃饭、睡觉),但是每个对象都保存着自己独特的状态,对象状态会随着程序的运行而发生改变,需要注意状态的变化必须通过调用方法来改变,这就是封装的基本原则。
3、封装思想
核心思想就是“隐藏细节”、“数据安全”:将对象不需要让外界访问的成员变量和方法私有化,只提供符合开发者意愿的公有方法来访问这些数据和逻辑,保证了数据的安全和程序的稳定。
具体的实现方式就是:
使用 private
修饰符把成员变量设置为私有,防止外部程序直接随意调用或修改成员变量,然后对外提供 public
的 set
和 get
方法按照开发者的意愿(可以编写一些业务逻辑代码,虽然很少这样做)设置和获取成员变量的值。
也可以把只在本类内部使用的方法使用 private
,这就是封装的思想,是面向对象最基本的开发规范之一。
在此,我们有必要说一下 Java 的访问权限修饰关键字。Java 中主要有 private、protected、public 和 默认访问权限 四种:
public 修饰符,具有最大的访问权限,可以访问任何一个在 CLASSPATH 下的类、接口、异常等。
protected 修饰符,主要作用就是用来保护子类,子类可以访问这些成员变量和方法,其余类不可以。
default 修饰符,主要是本包的类可以访问。
private 修饰符,访问权限仅限于本类内部,在实际开发过程中,大多数的成员变量和方法都是使用 private 修饰的。
Java 的访问控制是停留在编译层的,只在编译时进行访问权限检查,不会在类文件中留下痕迹。
通过反射机制,还是可以访问类的私有成员的。
我们举个小例子:
1 public class MobilePhone { 2 3 // 使用 private 关键字把成员变量私有化 4 private String os; 5 private String phoneNumber; 6 private String brand; 7 private double dumpEnergy; 8 9 // 对外提供访问、设置成员变量的 public 方法 10 // 这样就可以按照我们自己的意愿来访问、设置成员变量 11 // 而且也有助于在方法内部对数据有效性进行验证 12 public void setOs(String os){ 13 this.os = os; 14 } 15 public String getOs(){ 16 return this.os; 17 } 18 public void setPhoneNumber(String phoneNumber){ 19 this.phoneNumber = phoneNumber; 20 } 21 public String getPhoneNumber(){ 22 return this.phoneNumber; 23 } 24 public void setBrand(String brand){ 25 this.brand = brand; 26 } 27 public String getBrand(){ 28 return this.brand; 29 } 30 public void setDumpEnergy(double dumpEnergy){ 31 this.dumpEnergy = dumpEnergy; 32 } 33 public double getDumpEnergy(){ 34 return this.dumpEnergy; 35 } 36 37 // 发短信的方法,不需要做修改 38 public void sendMessage(String message, String targetPhoneNumber){ 39 System.out.println("发给" + targetPhoneNumber + ", 内容是:" + message); 40 } 41 42 // 充电方法,不需要做修改 43 public double charge(){ 44 System.out.println("正在充电, 剩余电量:" + dumpEnergy * 100 + "%"); 45 return dumpEnergy; 46 } 47 48 // 对外提供的开机方法 49 public void startup(){ 50 System.out.println("正在开机......"); 51 52 // 调用私有开机方法 53 startup2(); 54 55 System.out.println("完成开机"); 56 } 57 58 // 私有的开机方法,封装开机细节 59 private void startup2(){ 60 System.out.println("启动操作系统......"); 61 System.out.println("加载开机启动项......"); 62 System.out.println("......"); 63 } 64 }
在实际的开发过程中,这样的封装方式已经成了 Java Bean 代码编写的规范。现在主流的框架在使用反射技术为对象赋值、取值时使用的都是 set 和 get 方法,而不是直接操作字段的值。
4、继承和类实例化过程
(1)在多个不同的类中抽取出共性的数据和逻辑,对这些共性的内容进行封装一个新的类即父类(也叫做超类或基类),让之前的类来继承这个类,那些共性的内容在子类中就不必重复定义,比如 BaseDAO、BaseAction 等。
* (2)Java 的继承机制是单继承,即一个类只能有一个直接父类。
* (3)如果子类和父类有同名成员变量和方法,子类可以使用 super 关键字调用父类的成员变量和方法,上述使用方式前提是成员在子类可见。
* (4)在调用子类构造方法时,会隐式的调用父类的构造方法 super()。如果父类没有无参构造方法,为了避免编译错误,需要在子类构造方法中显式的调用父类的含参构造方法。
(5)子类创建时调用父类构造方法:子类需要使用父类的成员变量和方法,所以就要调用父类构造方法来初始化,之后再进行子类成员变量和方法的初始化。因此,构造方法是无法覆盖的。
* (6)当子类需要扩展父类的某个方法时,可以覆盖父类方法,但是子类方法访问权限必须大于或等于父类权限。
(7)继承提高了程序的复用性、扩展性,也是 Java 语言多态特征的前提。
(8)在实际开发、程序设计过程中,并非先有的父类,而是先有了子类中通用的数据和逻辑,然后再抽取封装出来的父类。
我们简单了解下类的实例化过程
(1)JVM 读取指定 classpath 路径下的 class 文件,加载到内存,如果有直接父类,也会加载父类;
(2)堆内存分配空间;
(3)执行父类、子类静态代码块;
(4)对象属性进行默认初始化;
(5)调用构造方法;
(6)在构造方法中,先调用父类构造方法初始化父类数据;
(7)初始化父类数据后,显示初始化,执行子类的构造代码块;
(8)再进行子类构造方法的特定初始化;
(9)初始化完毕后,将地址赋值给引用
为了说明上面的内容,我们来编写一个简单的例子,实际意义并不大,只是为了演示类继承实例化的过程。
1 /* 2 父类 3 */ 4 class Parent { 5 6 int num = 5; 7 8 static { 9 System.out.println("父类静态代码块"); 10 System.out.println(); 11 } 12 13 { 14 System.out.println("父类构造代码块1," + num); 15 num = 1; 16 System.out.println("父类构造代码块2," + num); 17 doSomething(); 18 System.out.println(); 19 } 20 21 Parent() { 22 System.out.println("父类构造方法1," + num); 23 num = 2; 24 System.out.println("父类构造方法2," + num); 25 doSomething(); 26 System.out.println(); 27 } 28 29 void doSomething() { 30 System.out.println("父类doSomething方法1," + num); 31 num = 3; 32 System.out.println("父类doSomething方法2," + num); 33 System.out.println(); 34 } 35 } 36 37 /* 38 子类 39 */ 40 class Child extends Parent { 41 42 int num = 10; 43 44 /* 45 静态代码块,在类加载时执行 46 */ 47 static { 48 System.out.println("子类静态代码块"); 49 System.out.println(); 50 } 51 52 /* 53 构造代码块 54 */ 55 { 56 System.out.println("子类构造代码块1," + num); 57 num = 11; 58 System.out.println("子类构造代码块2," + num); 59 doSomething(); 60 System.out.println(); 61 } 62 63 Child() { 64 System.out.println("子类构造方法1," + num); 65 num = 12; 66 System.out.println("子类构造方法2," + num); 67 doSomething(); 68 System.out.println(); 69 } 70 71 void doSomething() { 72 System.out.println("子类doSomething方法1," + num); 73 num = 13; 74 System.out.println("子类doSomething方法2," + num); 75 System.out.println(); 76 } 77 78 } 79 80 public class A { 81 public static void main(String[] args) { 82 Child child = new Child(); 83 child.num = 20; 84 child.doSomething(); 85 } 86 } 87 88 输出: 89 90 父类静态代码块 91 92 子类静态代码块 93 94 父类构造代码块1,5 95 父类构造代码块2,1 96 子类doSomething方法1,0 97 子类doSomething方法2,13 98 99 100 父类构造方法1,1 101 父类构造方法2,2 102 子类doSomething方法1,13 103 子类doSomething方法2,13 104 105 106 子类构造代码块1,10 107 子类构造代码块2,11 108 子类doSomething方法1,11 109 子类doSomething方法2,13 110 111 112 子类构造方法1,13 113 子类构造方法2,12 114 子类doSomething方法1,12 115 子类doSomething方法2,13 116 117 118 子类doSomething方法1,20 119 子类doSomething方法2,13
5、多态、反射和组件解耦
多态指允许不同类的对象对同一“消息”做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。可以用于消除类型之间的耦合关系,Spring 的核心就是多态和面向接口编程。
(1)Java 中可以使用父类、接口变量引用子类、实现类对象;
(2)在这个过程中,会对子类、实现类对象做自动类型提升,其特有功能就无法访问了,如果需要使用,可以做强制类型转换。
Java 的反射技术和多态特性是框架开发、组件解耦的核心,在这方面,Spring 的 IOC 和 DI 为我们提供了一个极好的学习范例,Spring 的 IOC 使用反射技术创建、管理对象,DI 使用多态技术为组件注入依赖对象。
在没有学习 Spring 之前,简单的解决方案是使用一个 .properties 文件保存程序中使用的接口、实现类类型键值信息,然后在程序中使用一个全局 Properties 对象保存这些信息,并且使用反射技术把这些实现类初始化、提供一个静态的方法获取指定接口的实现类对象,在组件中就可以使用依赖对象的键获取需要的对象。
这样的方案带来的好处就是:当我们需要修改某个组件的实现方式时,比如把之前 JDBC 的 DAO 实现改为 Hibernate 实现,只要把这些新的实现类放到 classpath 下,把 .properties 文件对应接口的实现类类型改成新的 Hibernate 实现类,而不需要修改依赖组件的代码。
在之后的文章中,我们将写一个简单的例子进一步讲解这个初级解耦方案。