@目录
面向对象
在目前的软件开发领域有两种主流的开发方法:结构化开发方法和面向对象开发方法。早期的编程 语言如C、Basic、Pascal等都是结构化编程语言;随着软件开发技术的逐渐发展,人们发现面向对象可 以提供更好的可重用性、可扩展性和可维护性,于是催生了大量的面向对象的编程语言,如Java、 C#和 Ruby 等。
结构化开发方法概述
结构化程序设计方法主张按功能来分析系统需求,其主要原则可概括为自顶向下、逐步求精、模块化等。结构化程序设计首先采用结构化分析(Structured Analysis, SA)方法对系统进行需求分析,然后使用结构化设计(Structured Design, SD)方法对系统进行概要设计、详细设计,最后釆用结构化编 程(Structured Program, SP)方法来实现系统。使用这种SA、SD和SP的方式可以较好地保证软件系 统的开发进度和质量。
因为结构化程序设计方法主张按功能把软件系统逐步细分,因此这种方法也被称为面向功能的程序设计方法;结构化程序设计的每个功能都负责对数据进行一次处理,每个功能都接受一些数据,处理完后输出一些数据,这种处理方式也被称为面向数据流的处理方式。
结构化程序设计里最小的程序单元是函数,每个函数都负责完成一个功能,用以接收一些输入数据, 函数对这些输入数据进行处理,处理结束后输出一些数据。整个软件系统由一个个函数组成,其中作为 程序入口的函数被称为主函数,主函数依次调用其他普通函数,普通函数之间依次调用,从而完成整个软件系统的功能。
从图中可以看出,结构化设计需要采用自顶向下的设计方式,在设计阶段就需要考虑每个模块应该 分解成哪些子模块,每个子模块又分解成哪些更小的模块……依此类推,直至将模块细化成一个个函数。
每个函数都是具有输入、输出的子系统,函数的输入数据包括函数形参、全局变量和常量等,函数的输出数据包括函数返回值以及传出参数等。结构化程序设计方式有如下两个局限性。
- 设计不够直观,与人类习惯思维不一致。釆用结构化程序分析、设计时,开发者需要将客观世 界模型分解成一个个功能,每个功能用以完成一定的数据处理。
- 适应性差,可扩展性不强。由于结构化设计采用自顶向下的设计方式,所以当用户的需求发生 改变,或需要修改现有的实现方式时,都需要自顶向下地修改模块结构,这种方式的维护成本 相当高。
采用结构化方式设计的软件系统,整个软件系统就由一个个函数组成,这个软件的运行入口往往由一个“主函数”代表,而主函数负责把系统中的所有函数“串起来”。
面向对象程序设计简介
面向对象是一种更优秀的程序设计方法,它的基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。它从现实世界中客观存在的事物(即对象)出发来构造软件系统,并在系统构造中尽可能运用人类的自然思维方式,强调直接以现实世界中的事物(即对象)为中心来思考,认识问题, 并根据这些事物的本质特点,把它们抽象地表示为系统中的类,作为系统的基本构成单元(而不是用一 些与现实世界中的事物相关比较远,并且没有对应关系的过程来构造系统),这使得软件系统的组件可 以直接映像到客观世界,并保持客观世界中事物及其相互关系的本来面貌。
从图中看出,面向对象的软件系统由多个类组成,类代表了客观世界中具有某种特征的一类事物,比如汽车,汽车内部存在一些状态数据(Field),比如颜色、款式。
面向对象的语言不仅使用类来封装一类事物的内部状态数据,这种状态数据就对应于图中的Field(成员变量),而且类会提供操作这些状态数据的方法,还会为这类事物的行为特征提供相应的实现,这种实现也是方法。因此可以得到如下基本等式:
从这个等式来看,面向对象比面向过程的编程粒度要大:面向对象的程序单位是类;而面向过程的程序单位是函数(相当于方法),因此面向对象比面向过程更简单、易用。
>
假设需要组装一台电脑,如果拿到手的是主板、CPU、内存条、硬盘等这种大粒度的组件,可以很轻松地组装成一台电脑;但如果拿到手的是一些二极管、三极管、 集成电路等小粒度的组件,要想把它们组装成电脑,就不简单了。如果把数据以及操j 作数据的方法都封装成对象,这就相当于提供了大粒度的组件,因此编程更容易。
从面向对象的眼光,开发者希望从自然的认识、使用角度来定义和使用类。也就是说,开发者希望直接对客观世界进行模拟:
- 定义一个类,对应客观世界的某种事物
- 业务需要关心这个事物的哪些状态, 程序就为这些状态定义成员变量
- 业务需要关心这个事物的哪些行为,程序就为这些行为定义方法
不仅如此,面向对象程序设计与人类习惯的思维方法有较好的一致性,比如希望完成“猪八戒吃西瓜”:
在面向过程的程序世界里,一切以函数为中心,函数最大,因此这件事情会用如下语句来表达:
吃(猪八戒,西瓜);
在面向对象的程序世界里,一切以对象为中心,对象最大,因此这件事情会用如下语句来表达:
猪八戒.吃(西瓜);
对比两条语句不难发现,面向对象的语句更接近自然语言的语法:主语、谓语、宾语一目了然,十 分直观,因此程序员更易理解。
面向对象基本特征
面向对象方法具有三个基本特征: 封装 (Encapsulation)、继承(Inheritance)和多态(Polymorphism)。
封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;
继承是面向向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;
多态指的是子类对象可以直接赋给父类变量,但运行时依然表现岀子类的行为特征,这意味着 同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。
除此之外,抽象也是面向对象的重要部分,抽象就是忽略一个主题中与当前目标无关的那些方面, 以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是考虑部分问题。例如, 需要考察Person对象时,不可能在程序中把Person的所有细节都定义出来,通常只能定义Person的部 分数据、部分行为特征——而这些数据、行为特征是软件系统所关心的部分。
虽然抽象是面向对象的重要部分,但它不是面向对象的特征之一,因为所有的编程语都需要抽象.当开发者进行抽象时应该考虑哪些特征是软件系统所需要的,那么这些特征就应该使用程序记录并表现出来。因此,需要抽象哪些特征没有必然的规定,而是取决于软件系统的功能需求。
类
类( class) 是对某种事物的抽象,是构造对象的模板或蓝图。我们可以将类想象成月饼模具,将对象想象为月饼。由类构造(construct) 对象的过程称为创建类的实例 (instance )。
定义类
语法:
[修饰符]class类名{
零个到多个构造器定义..
零个到多个成员变量
零个到多个方法...
}
说明:
- 修饰符可以是public, final、abstract,或者完全省略这三个修饰符
- 类名理论上只要是一个合法的标识符即可,但从程序的可读性方面来看,Java 类名必须是由一个或多个有意义的单词连缀而成的,每个单词首字母大写,其他字母全部小写,单词与 单词之间不要使用任何分隔符
- 三种最常见的成员:构造器、成员变量和方法,三种成员都可以定义零个或多个,如果三种成员都只定义零个,就是定义了一个空类,这没有太大的实际意义。
public class Dog{
//成员变量
String breed;
private int age;
String color;
//方法
void barking(){
}
public void hungry(){
}
private void sleeping(){
}
}
类型变量
一个类可以包含以下类型变量:
- 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
- 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
- 类变量:类变量也声明在类中,方法体之外,但必须声明为static类型。
static是一个特殊的关键字,它可用于修饰方法、成员变量等成员。static修饰的成员表明它属于这个类本身,而不属于该类的单个实例,因为通常把static修饰的成员变量和方法也称为类变量、类方法。 不使用static修饰的普通方法、成员变量则属于该类的单个实例,而不属于该类。因为通常把不使用static 修饰的成员变量和方法也称为实例变量、实例方法。
由于static的英文直译就是静态的意思,因此有时也把static修饰的成员变量和方法称为静态变量和静态方法,把不使用static修饰的成员变量和方法称为非静态变量和非静态方法。静态成员不能直接访问非静态成员。提示:虽然绝大部分资料都喜欢把static称为静态,但实际上这种说法很模糊,完全无法说明static的真正作用。static的真正作用就是用于区分成员变量、方法、内部类、初始化块
构造方法
每个类都有构造方法。如果没有显式地为类定义构造方法,Java编译器将会为该类提供一个默认构造方法。
在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。
public class Puppy{
//无参构造方法
public Puppy(){
}
//有参构造方法
public Puppy(String name){
// 这个构造器有一个参数:name
}
}
对象
对象是类的一个实例。
对象的三个主要特性:
- 对象的行为(behavior)—可以对对象施加哪些操作,或可以对对象施加哪些方法
- 对象的状态(state )—当施加那些方法时,对象如何响应
- 对象标识(identity )—如何辨别具有相同行为与状态的不同对象
创建对象
对象是根据类创建的。在Java中,使用关键字new来创建一个新的对象。创建对象需要以下三步:
- 声明:声明一个对象,包括对象名称和对象类型。
- 实例化:使用关键字new来创建一个对象。
- 初始化:使用new创建对象时,会调用构造方法初始化对象
public class Puppy{
public Puppy(String name){
//这个构造器仅有一个参数:name
System.out.println("小狗的名字是 : " + name );
}
public static void main(String[] args){
// 下面的语句将创建一个Puppy对象
Puppy myPuppy = new Puppy( "tommy" );
}
}
访问实例变量和方法
public class Puppy{
int age;
String name;
public Puppy(String name){
// 这个构造器有一个参数:name
this.name=name;
System.out.println("小狗的名字是 : " + name );
}
//无参构造方法
public Puppy(){
}
public void setAge( int age ){
this.age = age;
}
public int getAge( ){
System.out.println("小狗的年龄为 : " + puppyAge );
return puppyAge;
}
public static void main(String[] args){
/* 创建对象 */
Puppy myPuppy = new Puppy( "tommy" );
/* 通过方法来设定age */
myPuppy.setAge( 2 );
/* 调用另一个方法获取age */
myPuppy.getAge( );
/*可以像下面这样访问成员变量 */
System.out.println("变量值 : " + myPuppy.puppyAge );
}
}
对象、引用和指针
创建对象语句:
Puppy p = new Puppy( "tommy" );
在这行代码中实际产生了两个东西:一个是p变量, 一个是Puppy对象。
从Puppy类定义来看,Puppy对象应包含1个实例变量,而变量是需要内存来存储的。因此,当创建Puppy对象时,必然需要有对应的内存来存储Puppy对象的实例变量。
从图中可以看出,Puppy对象由多块内存组成,不同内存块分别存储了 Puppy对象的不同成员变量。当把这个Puppy对象赋值给一个引用变量时,系统如何处理呢?难道系统会把这个Puppy对象 在内存里重新复制一份吗?显然不会,Java没有这么笨,Java让引用变量指向这个对象即可。也就是说,引用变量里存放的仅仅是一个引用,它指向实际的对象。
与前面介绍的数组类型类似,类也是一种引用数据类型,因此程序中定义的Puppy类型的变量实 际上是一个引用,它被存放在栈(stack)内存里,指向实际的Puppy对象;而真正的Puppy对象则存放在堆(heap) 内存中。图5.2显示了将Person对象赋给一个引用变量的示意图。
栈内存里的引用变量并未真正存储对象的成员变量,对象的成员变量数据实际存放在堆内存里;而引用变量只是指向该堆内存里的对象。从这个角度来看,引用变量与C语言里的指针很像,它们都是存储一个地址值,通过这个地址来引用到实际对象。实际上,Java里的引用就是C里的指针,只是Java 语言把这个指针封装起来,避免开发者进行烦琐的指针操作。
当一个对象被创建成功以后,这个对象将保存在堆内存中,Java程序不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象。也就是说,不管是数组还是对象,都只能通过引用来访问它们。
在示例中,p引用变量本身只存储了一个地址值,并未包含任何实际数据,但它指向实际的Person 对象,当访问p引用变量的成员变量和方法时,实际上是访问p所引用对象的成员变量和方法。
不管是数组还是对象,当程序访问引用变量的成员变量或方法时,实际上是访问该引 变量所引用的数组、对象的成员变量或方法。
堆内存里的对象可以有多个引用,即多个引用变量指向同一个对象,如:
//将p变量的值赋值给p2变量
Puppy p2 = p;
上面代码把p变量的值赋值给p2变量,也就是将p变量保存的地址值赋给p2变量,这样p2变量和 p变量将指向堆内存里的同一个Puppy对象。不管访问p2变量的成员变量和方法,还是访问p变量的成员变量和方法,它们实际上是访问同一个Puppy对象的成员变量和方法,将会返回相同的访问结果。
如果堆内存里的对象没有任何变量指向该对象,那么程序将无法再访问该对象,这个对象也就变成 了垃圾,Java的垃圾回收机制将回收该对象,释放该对象所占的内存区。
因此,如果希望通知垃圾回收机制回收某个对象,只需切断该对象的所有引用变量和它之间的关系 即可,也就是把这些引用变量赋值为null。
this关键字
Java提供了一个this关键字,this关键字总是指向调用该方法的对象。根据this出现位置的不同, this作为对象的默认引用有两种情形。
- 构造器中引用该构造器正在初始化的对象。
- 在方法中引用调用该方法的对象。
this关键字最大的作用就是让类中一个方法,访问该类里的另一个方法或实例变量。
public class DogTest{
//定义一个jump()方法
public void jump(){
System.out.println("正在执jump方法”);
}
//定义一个run()方法,run()方法需要借助jump()方法
public void run()(
//使用this引用调用run()方法的对象
this.jump();
System.out.println("正在执行 run 方法”);
}
}
大部分时候,普通方法访问其他方法、成员变量时无须使用this前缀,但如果方法里有个局部变量 和成员变量同名,但程序又需要在该方法里访问这个被覆盖的成员变量,则必须使用this前缀。
包
Java 允许使用包( package ) 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
标准的 Java 类库分布在多个包中,包括 java.lang、java.util 和java.net 等。标准的 Java包具有一个层次结构。如同硬盘的目录嵌套一样,也可以使用嵌套层次组织包。所有标准的
Java 包都处于java 和 javax 包层次中。
使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了 Employee类。只要将这些类放置在不同的包中, 就不会产生冲突。事实上,为了保证包名的绝对唯一性, Sun 公司建议将公司的因特网域名(这显然是独一无二的) 以逆序的形式作为包名,并且对于不同的项目使用不同的子包.
从编译器的角度来看, 嵌套的包之间没有任何关系。例如,java.util 包与java.util.jar 包毫无关系。每一个都拥有独立的类集合。
类的导入
一个类可以使用所属包中的所有类, 以及其他包中的公有类( public class。) 我们可以采用两种方式访问另一个包中的公有类。
第一种方式:在每个类名之前添加完整的包名。
例如:
java.tiie.LocalDate today = java.tine.Local Date.now();
第二种方式:
可以使用 import 语句导人一个特定的类或者整个包。import 语句应该位于源文件的顶部
(但位于 package 语句的后面)。
例如, 可以使用下面这条语句导人 java.util 包中所有的类。
import java.util .*;
然后, 就可以使用
LocalDate today = Local Date.now();
import java.time.LocalDate;
参考
【1】:《Java疯狂讲义》
【2】:《Java核心技术 卷一》
【3】:https://www.runoob.com/java/java-object-classes.html