Java—面向对象设计—类和对象

理解面向对象程序设计

面向对象程序(Object-oriented programming,OOP)设计是继面向过程又一具有里程碑意义的编程思想,是现实世界模型的自然延伸。下面从结构化程序设计说起,逐步展示面向对象程序设计。

结构化程序设计简介

早期的程序设计,大量使用共享变量(全局变量)和GOTO语句,这使得代码结构比较混乱,不容易改错和复用,后来有人证明所有的有意义的程序流程都可以使用顺序、选择和循环来实现,并由此提出结构化程序设计。其概念最早由E.W.Dijikstra在1965年提出的,是软件发展的一个重要的里程碑。它的主要观点是采用自顶向下、逐步求精及模块化的程序设计方法,使用三种基本控制结构构造程序,任何程序都可由顺序、选择、循环这三种基本控制结构来构造。

结构化程序设计主要强调的是程序的易读性。在该程序设计思想的指导下,编程基本是通过写不同目的的函数/过程来实现,故又称为“面向过程编程(ProcedureOriented Programming,POP))。面向过程开发方式是对计算机底层结构的一层抽象,它把程序的内容分为数据和操纵数据的操纵两个部分。这种编程方式的核心问题是数据结构和算法的开发和优化。
Java—面向对象设计—类和对象_第1张图片
结构化程序设计方法可以用一句话概括:

程序 = 算法 + 数据结构

这里的“算法”可以用顺序、选择、循环这三种基本控制结构来实现。

这里的“数据结构”是指数据以及其相应的存取方式。程序与算法和数据结构之间的关系如上图所示。

面向对象程序设计简介

面向对象的思想主要是基于抽象数据类型(Abstract Data Type, ADT),在结构化编程过程中,人们发现把某种数据结构和专用于操纵它的各种操作以某种模块化方式绑定到一起会非常方便,做到“特定数据对应特定处理方法”,使用这种方式进行编程时数据结构的接口是固定的。 如果对抽象数据类型进一步抽象,就会发现把这种数据类型的实例当作一个具体的东西、事物、对象,就可以引发人们对编程过程中怎样看待所处理的问题的一次大的改变。

抽象数据类型方法虽然也有一定的抽象能力,但其核心仍然是数据结构和算法。而面向对象方法直接把所有事物都当作独立的对象,处理问题过程中所思考的不再主要是怎样用数据结构来描述问题,而是直接考虑重现问题中各个对象之间的关系。可以说,面向对象革命的最重要价值就在于改变了人们看待和处理问题的方式。

例如,在现实世界中桌子代表了所有具有桌子特征的事物,人类代表了所有具有人特征的生物。这个事物的类别映射到计算机程序中,就是面向对象中“类(class)”的概念。可以将现实世界中的任何实体都看作是对象,例如在人类中有个叫张三的人,张三就是人类中的实体,对象之间通过消息相互作用,比如张三这个对象和李四这个对象通过说话的方式相互传递消息。现实世界中的对象均有属性和行为,例如张三有属性:手、脚、脸等,行为有:说话、走路、吃饭等。

类似的,映射到计算机程序上,属性则表示对象的数据,行为表示对象的方法(其作用是处理数据或同外界交互)。现实世界中的任何实体都可归属于某类事物,任何对象都是某一类事物的实例。所以在面向对象的程序设计中一个类可以实例化多个相同类型的对象。面向对象编程达到了软件工程的三个主要目标:重用性、灵活性和扩展性。

面向对象程序设计的基本特征

下面,我们简述面向对象的程序设计的3个主要特征:封装性、继承性、多态性。

封装性(encapsulation):封装是一种信息隐蔽技术,它体现于类的说明,是对象的重要特性。封装使数据和加工该数据的方法(函数)封装为一个整体,以实现独立性很强的模块,使得用户只能见到对象的外特性(对象能接受哪些消息,具有哪些处理能力),而对象的内特性(保存内部状态的私有数据和实现加工能力的算法)对用户是隐蔽的。封装的目的在于把对象的设计者和对象的使用者分开,使用者不必知晓其行为实现的细节,只须用设计者提供的消息来访问该对象。

继承性:继承性是子类共享其父类数据和方法的机制。它由类的派生功能体现。一个类直接继承其他类的全部描述,同时可修改和扩充。继承具有传递性。继承分为单继承(一个子类有一父类)和多重继承(一个类有多个父类)。类的对象是各自封闭的,如果没继承性机制,则类的对象中的数据、方法就会出现大量重复。继承不仅支持系统的可重用性,而且还促进系统的可扩充性。

多态性:对象根据所接收的消息而做出动作。同一消息被不同的对象接受时可产生完全不同的行动,这种现象称为多态性。利用多态性用户可发送一个通用的信息,而将所有的实现细节都留给接受消息的对象自行决定,如是,同一消息即可调用不同的方法。例如:同样是run方法,飞鸟调用时是飞,野兽调用时是奔跑。多态性的实现受到继承性的支持,利用类继承的层次关系,把具有通用功能的协议存放在类层次中尽可能高的地方,而将实现这一功能的不同方法置于较低层次,这样,在这些低层次上生成的对象就能给通用消息以不同的响应。在OOPL中可通过在派生类中重定义基类函数(定义为重载函数或虚函数)来实现多态性。

综上可知,在面对象方法中,对象和传递消息分别表现事物及事物间相互联系的概念。类和继承是适应人们一般思维方式的描述范式。方法是允许作用于该类对象上的各种操作。这种对象、类、消息和方法的程序设计范式的基本点在于对象的封装性和类的继承性。通过封装能将对象的定义和对象的实现分开,通过继承能体现类与类之间的关系,以及由此实现动态联编和实体的多态性,从而构成了面向对象的基本特征。

面向对象编程和面向过程编程的比较

面向对象编程和面向过程编程是现在主流的两种编程模式,它们既有区别也有联系。下面就其区别和联系进行叙述以助大家更加深入的理解面向对象编程。

1. 两种编程范式之间的区别

在面向对象编程出现以前,面向过程颇受程序人员的青睐,因为面向过程编程采用的是“自上而下,步步求精”的编程思想,人们更易于理解这种思想。将程序一步一步的分解,并按照一定的顺序执行。这就是面向过程程序设计,以过程为中心,以算法为驱动

程序 = 算法 + 数据结构

但是面向过程程序设计的不足之处在于,面向过程的程序上一步和下一步环环相扣,如果需求发生变化那么代码的改动会很大,这样很不利于软件的后期维护和扩展。例如,写一个图形界面的软件,想将它改为控制台下的,如果是使用面向过程写的,那么代码的改动将是巨大的,因为前台代码和后台联系过于紧密。这对于开发一个大型的复杂的软件来说是致命的。

而面向对象程序设计的出现就可以很好的解决了这一问题,它的设计思想是,

程序 = 对象 + 消息传递

用户首先自定义的数据结构—“类”,然后用该类型下的“对象” 组装程序。对象之间通过“消息”进行通讯。每个对象包括数据以及对数据的处理,每个对象都像是一个小型的“机器” 。面向对象设计使程序更容易扩展,也更加符合现实世界的模型。

但是 “任何事物都有两面性”,面向对象程序设计有其优点,但也带来了“副作用”——执行效率要低于面向过程程序设计。所以进行科学计算和要求高效率的程序中,面向过程设计要好于面向对象设计。而且面向对象程序的复杂度要高于面向过程的程序,如果程序比较小,面向过程要比面向对象更加清晰。

更为具体来说,为解决某个任务,面向过程程序设计首先强调的“该怎么做(Howto do?)” 这里的“How”对应的解决方案就形成一个个功能块—function(函数),而面向对象程序设计首先考虑的是 “该让谁来做(Who to do?)”,这里的 “Who”就是对象,这些对象完成某项任务的能力就构成一个个method(方法),最后一系列具备一定的方法的对象“合力”能把任务完成。例如,对于“吃东西”这个任务,面向过程强调的是“如何去吃”,“人”只是一个参数;而面向对象强调的是“人”,“吃东西”只是人内部能实现的一个方法。二者之间的对比如下图所示。
Java—面向对象设计—类和对象_第2张图片
从上面的分析可知,前面章节的所有的例子虽然用了Java中class来“包装”,但本质上都是“面向过程”的思路来解决问题,因为前面的范例中没有任何“对象”存在,只存在解决问题的“方法(method)”,脱离对象的方法其实就是面向过程程序设计中的“函数(function)”。

2. 两种编程范式之间的联系

面向对象是在面向过程的基础上发展而来的,只是添加了它独有的一些特性。面向对象程序中的对象就是由数据和方法构成,所以完整的面向对象概念应该是,

对象 = 数据 + 方法

更进一步的可以描述为,

程序 =对象 + 消息传递 = (数据 + 方法) +消息传递

面向对象的基本概念

将具有相同属性及相同行为的一组对象称为类(class)。广义地讲,具有共同性质的事物的集合就称为类。在面向对象程序设计中,类是一个独立的单位,它有一个类名,其内部包括成员变量,用于描述对象的属性;还包括类的成员方法,用于描述对象的行为。在Java程序设计中,类被认为是一种抽象的数据类型,这种数据类型不但包括数据,还包括方法,这大大地扩充了数据类型的概念。

类是一个抽象的概念,要利用类的方式来解决问题,必须用类创建一个实例化的对象,然后通过对象去访问类的成员变量,去调用类的成员方法来实现程序的功能。就如同“汽车”本身是一个抽象的概念,只有使用了一辆具体的汽车,才能感受到汽车的功能。

一个类可创建多个类对象,它们具有相同的属性模式,但可以具有不同的属性值。Java程序为每一个对象都开辟了内存空间,以便保存各自的属性值。

对象

对象(object)是类的实例化后的产物。对象的特征分为静态特征和动态特征两种。静态特征指对象的外观、性质、属性等。动态特征指对象具有的功能、行为等。客观事物是错综复杂的,但人们总是从某一目的出发,运用抽象分析的能力,从众多的特征中抽取最具代表性、最能反映对象本质的若干特征加以详细研究。

人们将对象的静态特征抽象为属性,用数据来描述,在Java语言中称之为变量,将对象的动态特征抽象为行为,用一组代码来表示,完成对数据的操作,在Java语言中称之为方法(method)。一个对象由一组属性和一系列对属性进行操作的方法构成。

在现实世界中,所有事物都可视为对象,对象是客观世界里的实体。而在Java里,“一切皆为对象”,它是一门面向对象的编程语言,面向对象(Object-Oriented)的核心就是对象。要学好Java,大家需要学会使用面向对象的思想来思考问题和解决问题。

类和对象的关系

面向对象的编程思想力图使在计算机语言中对事物的描述与现实世界中该事物的本来面目尽可能地一致,类和对象就是面向对象方法的核心概念。类是对某一类事物的描述,是抽象的、概念上的定义;对象是实际存在的该类事物的个体,因而也称作实例(instance)。下图所示是一个说明类与对象关系的示意图。
Java—面向对象设计—类和对象_第3张图片
图中座椅设计图就是“类”,由这个图纸设计出来的若干的座椅就是按照该类产生的“对象”。可见,类描述了对象的属性和对象的行为,类是对象的模板。对象是类的实例,是一个实实在在的个体,一个类可以对应多个对象。可见,如果将对象比做座椅,那么类就是座椅的设计图纸,所以面向对象程序设计的重点是类的设计,而不是对象的设计。

一个类按同种方法产生出来的多个对象,其开始的状态都是一样的,但是修改其中一个对象的时候,其他的对象是不会受到影响的,例如,修改第1把座椅(如锯短椅子腿)的属性时,其他的座椅是不会受到影响。
Java—面向对象设计—类和对象_第4张图片
再举一个例子来说明类与对象的关系。17世纪德国著名的哲学家、数学家莱布尼茨(Leibniz,1646年—1716年)曾有个著名的哲学论断:“世界上没有两片完全相同的树叶。” 这里,我们用“类”与“对象”的关系来解释:类相同——它们都叫树叶,而对象各异——树叶的各个属性值(品种、大小、颜色等)是有区别的,如上图所示。从这个案例也可以得知,类(树叶)是一个抽象的概念,它是从所有对象(各片不同的树叶)提取出来的共有特征描述。而对象(各片具体的不同树叶)则是类(树叶这个概念)的实例化。

类的声明与定义

在使用类之前,必须先声明它,然后才可以声明变量,并创建对象。类声明的语法如下。
Java—面向对象设计—类和对象_第5张图片
可以看到,声明类使用的是class关键字。声明一个类时,在class关键字后面加上类的名称,这样就创建了一个类,然后在类的里面定义成员变量和方法。

在上面的语法格式中,标识符可以是public、private、protected或者完全省略这个修饰符,类名称只要是一个合法的标识符即可,但从程序的可读性方面来看,类名称最好是由一个或多个有意义的单词连缀而成,每个单词首字母大写,单词间不要使用其他分隔符。

类的标识符可以是访问控制符。Java提供了一系列的访问控制符来设置基于类(class)、变量(variable)、方法(method)及构造方法(constructor)等不同等级的访问权限。Java的访问权限主要有4类。

⑴ 默认模式(default)。在默认模式下,不需为某个类、方法等不加任何访问修饰符。这类方式声明的方法和类,只允许在同一个包(package)内是可访问的。

⑵ private(私有)。这是Java语言中对访问权限控制最严格的修饰符。如果一个方法、变量和构造方法被声明为“私有”访问,那么它仅能在当前声明它的类内部访问。类和接口(interface)的访问方式是不能被声明为私有的。

⑶ public(公有)。这是Java语言中访问权限控制最宽松的修饰符。如果一个类、方法、构造方法和接口等被声明为“公有”访问,那么它不仅可以被跨类访问,而且允许跨包访问。如果需要访问其他包里的公有成员,则需要事先导入(import)那个包含所需公有类、变量和方法等的那个包。

⑷ protected(保护)。介于public 和 private 之间的一种访问修饰符。如果一个变量、方法和构造方法在父类中被声明为“保护”访问类型,只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。类和接口(interface)的访问方式是不能声明为保护类型的。

类的标识符除了上述的4个访问控制符,还可以是final。关键字“final”有“无法改变的”或者“终态的”含义。一个类一旦被声明为final,那这个final类不能被继承,因此final类的成员方法没有机会被覆盖,默认情况下类都是default的。在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会再被扩展,那么就设计为final类。

类的组成使用(Person.java)。
Java—面向对象设计—类和对象_第6张图片
程序首先用class声明了一个名为Person的类,在这里Person是类的名称。

第3、4行先声明了两个属性(即描述数据的变量)name和age,name为String(字符串类型)型, age为int(整型)型。

第5~8行声明了一个talk()方法——操作数据(如name和age)的方法,此方法用于向屏幕打印信息。为了更好地说明类的关系,请参看下图。
Java—面向对象设计—类和对象_第7张图片

类的定义

在声明一个类后,还需要对类进行定义。定义类的语法如下。
Java—面向对象设计—类和对象_第8张图片
对一个类定义而言,构造方法(constructor,又称构造器或构造函数)、属性和方法是三种最常见的成员,它们都可以定义零个或多个。如果三种成员都只定义零个,那实际上是定义了一个空类,那就失去了定义类的意义了。

类中各个成员之间,定义的先后顺序没有任何影响。各成员可相互调用,但值得注意的是,static修饰的成员不能访问没有static修饰的成员。

属性用于定义该类或该类的实例所包含的各种数据。方法则用于定义类中的行为特征或功能实现(即对数据的各种操作)。构造方法是一种特殊的方法,专用于构造该类的实例(如实例的初始化、分配实例内存空间等),Java语言通过new关键字来调用构造方法,从而返回该类的实例。

定义一个类后,就可以创建类的实例了,创建类实例通过new关键字完成。下面通过一个实例讲解如何定义并使用类。

类的定义使用(ColorDefine.java)。
Java—面向对象设计—类和对象_第9张图片
Java—面向对象设计—类和对象_第10张图片
在ColorDefine这个类中,在第03行定义了一个String类型的属性color,并赋初值“黑色”。在第05行~第08行,定义了一个普通的方法getMes(),其完成的功能是向屏幕输出字符串“定义类”。第10行~15行,定义了一个公有访问的静态方法——main方法。在main方法中,代码第12行中,定义了ColorDefine的对象b,第13行输出了对象b的数据成员color,第14行调用了对象的方法getMes()。

还可以看出,在类ColorDefine中,没有构造方法(即与类同名的方法)。但事实上,如果用户没有显式定义构造方法,Java编译器会提供一个默认的无参构造方法。

类的属性

类的基本组成部分包括属性和方法。

通过前面的学习,其实大家对方法这个概念并不陌生。例如,在前面内容中,基本上每个范例都使用了System.out.println()语句,那么它代表什么含义呢?事实上,System是系统类(class),out是标准输出对象(object),而println()是对象out中的一个方法(method)。这句话的完整含义就是调用系统类System中的标准输出对象out中的方法println()。

一言蔽之,方法就是解决一类问题的步骤的有序组合。由于它涉及的概念很多,我会在后期文章详细探讨这个概念。这里仅做简单的提及,让大家有个初步的认知。

下面我们先来讲解类的属性,类的属性也称为字段或成员变量,不过习惯上将它称为属性。

属性的定义

类的属性是变量。定义属性的语法如下。
在这里插入图片描述
属性语法格式的详细说明如下。

⑴ 修饰符:修饰符可省略,也可是访问控制符public、protected、private及static、final,其中三个访问控制符public、protected和private只能使用其中之一, static和final则可组合起来修饰属性。

⑵ 属性类型:属性类型可以是Java允许的任何数据类型,包括基本类型(int、float等)和引用类型(类、数组、接口等)。

⑶ 属性名:从语法角度来说,属性名则只要是一个合法的标识符即可。但如果从程序可读性角度来看,属性名应该由一个或多个有意义的单词(或能见名知意的简写)连缀而成,推荐的风格是第一个单词应以小写字母作为开头,后面的单词则用大写字母开头,其他字母全部小写,单词间不使用其他分隔符。如:StringstudentNumber;

⑷ 默认值:定义属性还可以定义一个可选的默认值。

提示
属性是一种比较符合汉语习惯的说法,在Java的官方文献中,属性被称为Field,因此有些书籍也把“属性”翻译为“字段”或“域”,它们本质上是相同的。

属性的使用

下面通过一个实例来讲解类的属性的使用,通过这个实例可以看出在Java中类属性和对象属性的不同使用方法。

类的属性组使用(usingAttribute.java)
Java—面向对象设计—类和对象_第11张图片
Java—面向对象设计—类和对象_第12张图片
Java—面向对象设计—类和对象_第13张图片
代码第03-04行,定义了两个String类型的属性a和b,由于它们是静态的,所以它们是属于类的,也就是属于所有这个类定义的对象共有的,所有对象看到的静态属性值是相同的。

代码第06-07行,定义了两个String类型的属性c和d,由于它们是非静态的,所以它们是属于这个类所定义的对象私有的,每个对象都有这个属性,且它们各自的属性值可不同。

代码第09-14行,定义了静态方法块,它没有名称。使用static关键字加以修饰并用大括号“{ }”括起来的代码块称为静态代码块,用来初始化静态成员变量。如静态变量b被初始化为"string-b"。

代码第23-28行,定义了一个构造方法usingAttribute (),在这个方法中,使用了类中的各个属性。构造方法与类同名,且无返回值(包括void),它的主要目的是创建对象。这里仅是为了演示,才使用了若干输出语句。实际使用过程中,这些输出语句不是必需的。

代码30-37行,定义了公有方法print(),用于打印所有属性值,包括静态成员值。

代码39-45行,定义了常见的主方法main(),在这个方法中,第44行使用关键字new和构造方法usingAttribute ()来创建一个匿名对象。

由输出结果可以看出,Java类属性和对象属性的初始化顺序如下。

⑴ 类属性 (静态变量) 定义时的初始化,如范例中的 static String a = “string-a”。

⑵ static 块中的初始化代码,如范例中的 static {} 中的 b = “string-b”。

⑶ 对象属性 (非静态变量) 定义时的初始化,如范例中的 String c = “stirng-c”。

⑷ 构造方法 (函数) 中的初始化代码,如范例构造方法中的 d = “string-d”。

当然这里只是为了演示Java类的属性和对象属性的初始化顺序。在实际的应用中,并不建议在类中定义属性时实施初始化,如例子中的字符串变量“a”和“c”。

注意,被static修饰的变量称为类变量(class’s variables),它们被类的实例所共享。也就是说,某一个类的实例改变了这个静态值,其他这个类的实例也会受到影响。而成员变量(member variable)则是没有被static修饰的变量,为实例所私有,也就是说,每个类的实例都有一份自己专属的成员变量,只有当前实例才可更改它们的值。

static是一个特殊的关键字,其在英文中直译就是静态的意思。它不仅用于修饰属性(变量),成员,还可用于修饰类中的方法。被static修饰的方法,同样表明它是属于这个类共有的,而不是属于该类的单个实例,通常把static修饰的方法也称为类方法。

对象的声明与使用

在上述范例中,已创建好了一个Person的类,相信类的基本形式读者应该已经很清楚了。但是在实际中单单有类是不够的,类提供的只是一个模板,必须依照它创建出对象之后才可以使用。

对象的声明

下面定义了由类产生对象的基本形式。
在这里插入图片描述
创建属于某类的对象,需要通过下面两个步骤实现。

⑴ 声明指向“由类所创建的对象”的变量。

⑵ 利用new创建新的对象,并指派给先前所创建的变量。

举例来说,如果要创建Person类的对象,可用下列语句实现。
在这里插入图片描述
当然也可以用下面的这种形式来声明变量。
在这里插入图片描述
提示
对象只有在实例化之后才能被使用,而实例化对象的关键字就是new。

对象实例化的过程如下图所示。
Java—面向对象设计—类和对象_第14张图片
从图中可以看出,当语句执行到Person p1的时候,只是在“栈内存”中声明了一个Person对象p1的引用,但是这个时候p1并没有在“堆内存”中开辟空间。对象的“引用”本质上就是一个对象在堆内存的地址,所不同的是,在Java中,用户无法向C/C++那样直接操作这个地址。

本质上,“new Person()”就是使用new关键字,来调用构造方法Person(),创建一个真实的对象,并把这个对象在“堆内存”中的占据的内存首地址赋予p1,这时p1才能称为一个实例化的对象。

这里做个对比来说明“栈内存”和“堆内存”的区别。在医院里,为了迎接一个新生命的诞生,护士会先在自己的登记本上留下一行位置,来记录婴儿床的编号,一旦婴儿诞生后,就会将其安置在育婴房内的某个婴儿床上。然后护士就在登记本上记录下婴儿床编号,这个编号不那么好记,就给这个编号取个好记的名称,例如p1,那么这个p1(本质上就为婴儿床编号)就是这个婴儿“对象”的引用,找到这个引用,就能很方便找到育婴房里的婴儿。这里,护士的登记表就好比是“栈内存”,它由护士管理,无需婴儿父母费心。而育婴房就好比是“堆内存”,它由婴儿爸妈显式申请(使用new操作)才能有床位,但一旦使用完毕,会由一个专门的护工(编译器)来清理回收这个床位——在Java中,有专门的内存垃圾回收(Garbage Collection,GC)机制来负责回收不再使用的内存。

对象的使用

如果要访问对象里的某个成员变量或方法,可以通过下面的语法来实现。
在这里插入图片描述
例如,想访问Person类中的name和age属性,可用如下方法来访问。
在这里插入图片描述
因此若想将Person类的对象p中的属性name赋值为“张三”,年龄赋值为25,则可采用下面的写法。
在这里插入图片描述
如果想调用Person中的talk()方法,可以采用下面的写法。
在这里插入图片描述
对于对象属性和方法点操作符“.”,这里建议大家直接读成 “的”,例如,p1.name = “张三”,可以读成“p1的name被赋值为张三”。再例如,“p1.talk()”可以读成“p1的talk()方法”。这样读是有原因的: 点操作符“.”对应的英文为“dot [dɔt]”,通常“t”的发音弱化而读成“[dɔ ]”(大家可以尝试用英文读一下sina.com来体会一下),而“[dɔ]”的发音很接近汉语“的”的发音[de],如下图所示。此外,“的”在含义上也有“所属”关系。因此将点操作符“.”读成 “的”,音和意皆有内涵。
Java—面向对象设计—类和对象_第15张图片
使用Person类的对象调用类中的属性与方法的过程(ObjectDemo.java)。
Java—面向对象设计—类和对象_第16张图片
Java—面向对象设计—类和对象_第17张图片
第06行声明了一个Person类的实例对象p1,并通过new操作,调用构造方法Person(),直接实例化此对象。

第07-08行,对p1对象中的属性(name和age)进行赋值。

第9行调用p1对象中talk()方法,实现在屏幕上输出信息。

代码12-20行,是Person类的定义。

对照上述程序代码与下图的内容,即可了解到Java是如何对对象成员进行访问操作的。
Java—面向对象设计—类和对象_第18张图片

匿名对象

匿名对象是指就是没有名字的对象。实际上,根据前面的分析,对于一个对象实例化的操作来讲,对象真正有用的部分是在堆内存里面,而栈内存只是保存了一个对象的引用名称(严格来讲是对象在堆内存的地址),所以所谓的匿名对象就是指,只开辟了堆内存空间,而没有栈内存指向的对象。

创建匿名对象
Java—面向对象设计—类和对象_第19张图片
Java—面向对象设计—类和对象_第20张图片
代码第11行,创建匿名对象,没有被其他对象所引用。如果第11行定义一个有名对象,如:
在这里插入图片描述
那么调用类中的方法say(),可很自然的写成:
在这里插入图片描述
但是由于“new NoNameObject()”创建的是匿名对象,所以就用“NoNameObject()”整体来作为新构造匿名对象的引用,它访问类中的方法,就如同普通对象一下,使用点操作符(.):
在这里插入图片描述
匿名对象有如下两个特点。

⑴ 匿名对象是没有被其他对象所引用,即没有栈内存指向。

⑵ 由于匿名对象没有栈内存指向,所以其只能使用一次,之后就变成无法找寻的垃圾对象,故此会被垃圾回收器收回。

对象的比较

有两种方式可用于对象间的比较。(1)利用“ = = ” 运算符;(2)利用equals()方法。 “= =” 运算符用于比较两个对象的内存地址值(引用值)是否相等,equals()方法用于比较两个对象的内容是否一致。
回到那个“婴儿床编号”和“婴儿”的比喻,“==”运算符完成的是比较两个婴儿床的编号是否相等(相等则说明是同一个婴儿床),而equals()方法完成的是婴儿床内的婴儿是否相同(相同则说明是一个婴儿)。下面的两个案例分别说明了这两种方法的使用。

“==”运算符用于比较(CompareObject1.java)。
Java—面向对象设计—类和对象_第21张图片
在这里插入图片描述
Java—面向对象设计—类和对象_第22张图片
由程序的输出结果可以发现,str1不等于str2,有些人可能会问,str1与str2的内容完全一样,为什么会不等于呢?大家可以发现在程序的第5和第6行分别用new实例化了两个String类对象,此时这两个对象在“堆内存”中处于不同的内存位置,也就是它们的内存地址是不一样的。这个时候程序中是用的“= =”比较,比较的是内存地址值(即引用值),所以输出str1!=str2。程序第7行将str2的引用值直接赋给str3,这个时候就相当于str3也指向了str2的引用,此时这两个对象指向的是同一内存地址,所以比较值的结果是str2==str3。str1、str2和str3的内存布局模拟如下图所示。
Java—面向对象设计—类和对象_第23张图片
大家可能会问,那该如何去比较里面的内容呢?这就需要采用另外一种对象比较方法——“equals()”。请看下面的程序。

equals方法用于对象内容的比较(CompareObject2.java)
Java—面向对象设计—类和对象_第24张图片

Java—面向对象设计—类和对象_第25张图片
相比于上一个范例,在第08行代码处,将比较方式从“str1 == str2”换成了“str1.equals( str2 )”, equals()方法的宿主是String类的对象str1。所有String类的对象都有equals()方法,因此第08行代码换成。
在这里插入图片描述
达到的比较效果是同原来的代码是一样的。

在这里需要大家记住,“==”是比较对象内存地址值(即所谓的引用值)的,而“equals”是比较对象内容的。

对象数组的使用

我们可以把类理解为用户自定义的数据类型,它和基本数据类型(如int、float等)具有相同的地位。在前面章节中我们已介绍过如何以数组来保存基本数据类型的变量。类似地,对象也可以用数组来存放,可通过下面两个步骤来实现。

声明以类为数据类型的数组变量,并用new分配内存空间给数组。

用new产生新的对象,并分配内存空间给它。

例如,要创建3个Person类型的数组元素,语法如下。
在这里插入图片描述
创建好数组元素之后,便可把数组元素指向由Person类所定义的对象。
Java—面向对象设计—类和对象_第26张图片
当然也可以写成如下形式。
在这里插入图片描述
当然,也可以利用for循环来完成对象数组内的初始化操作,此方式属于动态初始化。
Java—面向对象设计—类和对象_第27张图片
或者也可以采用静态方式来初始化对象数组,如下所示。
在这里插入图片描述
用静态方式初始化对象数组(ObjectArray.java)。
Java—面向对象设计—类和对象_第28张图片
Java—面向对象设计—类和对象_第29张图片
Java—面向对象设计—类和对象_第30张图片
程序第20~24行用静态声明方式声明了Person类的对象数组p,它包含了3个对象。事实上,在21-23行,每一行都是返回一个对象的引用地址,而对象数组的三个元素就是这三个对象的引用地址。

程序第25~28行用for循环输出对象数组p中的所有对象,并分别调用它们talk()方法,打印出个人信息。

序第06-10行构造方法Person()的定义,在这个代码段里,有个关键词this,容易让初学者困惑。下面给予简要介绍,有关this的详细使用说明,在后期的文章中我会再详细介绍。

当创建一个对象后,Java虚拟机(JVM)就会给这个对象分配一个自身的引用——this。由于this是和对象本身相关联的,所以this只能在类中的非静态方法中使用。静态属性及静态方法属于类,它们与具体的对象无关,所以静态属性及静态方法是没有this的。同一个类定义下的不同对象,每个对象都有自己的this,虽然都叫this,但指向的对象不同。这好比一个班里的众多同学来做自我介绍:“我叫XXX”,虽然说的都是“我”,但每个“我”指向的对象是不同的。

为什么第08-09行中有赋值运算符(=)左侧的变量使用this引用呢?这是因为构造方法Person()的参数列表有形参name和age,它们是隶属于构造方法Person()的局部变量,而Person对象中有同名的属性变量name和age(分别在第03行和04行定义),如果将构造方法Person()中的形参给给同名的对象属性赋值,第08-09行就变成如下的语句。
在这里插入图片描述
这样会让部分读者产生“误解”。为什么这两个变量会自己给自己赋值呢?其实,范例中第08-09行中的this的确并不是必需的,但是为了增强代码的可读性,赋值运算符(=)左侧的变量使用this,来表明左侧变量是指当前对象的成员变量,而非 Person方法内的同名形数。因此,“this.name =name;”这个语句就可以比较清晰的解读为,用Person方法内形参name给本对象的成员变量name赋值。代码第09和13行,也可以有类似的解读,这里就不一一赘述了。

1. 栈内存和堆内存的区别

在Java中,栈(stack)是由编译器自动分配和释放的一块内存区域,主要用于存放一些基本类型(如int、 float等)的变量、指令代码、常量及对象句柄(也就是对象的引用地址)。

栈内存的操作方式类似于数据结构中的栈(仅在表尾进行插入或删除操作的线性表)。栈的优势在于,它的存取速度比较快,仅次于寄存器,栈中的数据还可以共享。其缺点表现在,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

堆(heap)是一个程序运行动态分配的内存区域,在Java中,构建对象时所需要的内存从堆中分配。这些对象通过new指令“显式”建立,放弃分配方式类似于数据结构中的链表。堆内存在使用完毕后,是由垃圾回收(Garbage Collection,简称GC)器“隐式”回收的。在这一点上,是和C/C++是有显著不同的,在C/C++中,堆内存的分配和回收都是显式的,均由用户负责,如果用户申请了堆内存,而在使用后忘记释放,则会产生“内存溢出”问题——可用内存存在,而其他用户却无法使用。

堆的优势是在于动态地分配内存大小,可以“按需分配”,其生存期也不必事先告诉编译器,在使用完毕后,Java的垃圾收集器会自动收走这些不再使用的内存块。其缺点为,由于要在运行时才动态分配内存,相比于栈内存,它的存取速度较慢。

由于栈内存比较小,如果栈内存不慎耗尽,就会产生著名的堆栈溢出(stackoverflow)问题,这能导致整个运行中的程序崩溃(crash)。由于这个问题的普通性,全球IT界最受欢迎的技术问答网站也取名为Stack Overflow(http://stackoverflow.com/),大家如果有IT问题,中文网站如不能找到合适的中文解决方案,可以尝试在这个网站上找到高质量的英文解答。

类似的,如果堆内存使用不当也会产生问题,典型的问题就是内存碎片(fragmentation),当回收堆内存时,可能会导致一些小块的但不连续的内存存在。当用户申请一块较大的堆内存,虽然可用的小块内存总和足够大,本可以满足申请所需,但是由于它们不连续,导致申请失败。这些不可用的非连续的小块内存就是所谓的内存碎片。

2. 面向对象编程与面向过程编程的感性认知

前面我们说到,面向过程程序设计主要的弊病是上一步和下一步环环相扣,如果需求发生变化那么代码的改动会很大,这样很不利于对软件的后期维护和扩展。这仅仅是理论上的描述,很多读者对此并没有感性认识,因此无法理解深刻,下面我们用实例来说明二者的区别。

假设有两个程序员分别叫POP(面向过程编程的英语简写)和OOP(面向对象编程的英语简写),他们分别来完成用户图形界面(GUI)上的显示矩形(square)、圆形(circle),当用户点击这两个图形时,这些图形分别要旋转180。,并播放一段歌曲。下面项目经理让这两个程序员POP和OOP分别来实现这个功能。

⑴ 第一回合的代码完成情况。
Java—面向对象设计—类和对象_第31张图片
由上面的描述可知,看起来面向过程代码更加简洁。如果程序比较短,面向过程要比面向对象更加的清晰。

但是用户的需求是一直在变的,软件的升级基本上是不可避免的。如果项目经理要求增加新的要求——新的升级软件需要支持“三角形”的旋转和播放歌曲。那么程序员POP和OOP是如何完成自己的工作呢?

⑵ 第二回合的代码完成情况。
Java—面向对象设计—类和对象_第32张图片
表面上看来面向过程代码POP依然占据优势,比较简洁,但是POP代码在代码维护中,“牵一发而动全身”,过程Rotate()、PlaySong()是全局性的,在前期版本Rotate()、PlaySong()可以正确响应“矩形”和“圆形”的变化,但是在维护这两个“新版本”过程中有一点错误,都会让前期的“无辜”的“矩形”和“圆形”受到牵连——无法正确运行。

而面向对象代码OOP,虽然代码过程看起来复杂一点,但是如果前期版本的软件可以正确响应“矩形”和“圆形”的变化,那么在新版本维护过程中,增加了“三角形”,即使出现了错误(不管是逻辑上的还是语法上的),那这些错误仅仅局限性于“三角形”类——这样程序的错误就可控,很方便维护。

如果代码很短,面向对象编程的模式优势并不明显,但是如果读者把Rotate()、PlaySong()过程想象成上万行的代码,就会知道将代码错误局部化、可控化,对程序的后期维护有多重要!

落后的软件生产方式无法满足迅速增长的计算机软件需求,从而导致软件开发与维护过程中出现一系列严重问题的现象。这就是所谓的软件危机。在学习了类的三个特点——封装、继承和多态,大家会发现,相比于面向过程编程,面向对象编程还有更多的优点。

Java—重复调用的代码块—方法

JAVA—重谈方法【详细版】

你可能感兴趣的:(Java,java)