上一篇blog介绍了Java的基础语法,在第一篇中我们也提到过面向对象和面向过程的区别,这篇blog我们来重点聊一下面向对象思想,首先我们得搞明白什么是对象。
面向对象基本概念
什么是对象呢?什么又是面向对象编程,面向对象的天然优势有哪些,为什么面向对象有这些优势,面向对象又有如何设计原则。
什么是对象
什么是对象什么又是类呢?二者有什么关系呢
- 对象:是把数据和数据操作方法放在一起,作为一个相互依存的整体,对象由成员变量以及方法组成
- 类:对同类对象抽象出的共性,即类、类型、类别 ,是一种抽象概念,是一系列具备相同特征和相同行为的对象的集合
所以我们面向对象进行编程,是面向一个抽象出来的实体进行编程。
对象间关系
对象之间主要有四种关系,关联关系(一个类中的某个方法调用另一个类),继承关系(父类与子类),聚合关系(整体和部分),实现关系(接口)
- 关联关系:一般是一个类【A类】中的某个方法里的某个参数是另一个类【B类】的对象:一般是一个类中的方法里的某个参数是另一个类的对象
- 聚合关系:聚集是一种松耦合,表明部分【A类】是整体【B类】的一部分,也即【A类】不一定非属于父对象;而组合是一种紧耦合,部分【A类】是整体【B类】必不可少的一部分。在属性层面上区分,一般是一个类中的某个成员变量是另一个类的对象
- 继承关系:子类是一种父类,子类来自于父类,拥有父类的全部内容,自己还可以加以扩展,java没有多重继承
- 实现关系:接口抽象出一种行为,实现类去实现,具体怎么实现不用去管。方法层面上区分
其实这四种关系构成了我们后来对类与对象的使用模式。
什么是面向对象
了解了什么是对象后,我们再说什么是面向对象和面向过程:
- 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
- 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
总而言之就是把问题分解成对象,对象直接隔离且有自己完备的结构,可以互相通过方法沟通来达成结构清晰稳定的目标
面向对象优势
由于面向对象更容易从我们自身的角度出发去看问题,所以有天然的拟人优势,而且由于结构清晰明了,所以具备如下优势:较高的开发效率;较高的扩展性和代码可重用性;保证软件的高可维护性,这些优势又是由面向对象的特征来决定的。
面向对象特征
面向对象包含三大特征:继承、封装和多态,当然有时候抽象也可以算作一个特征。
- 继承,继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。
- 封装,封装是客观事物抽象成类,每个类对自身的数据和方法进行保护,类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,高内聚低耦合。
- 多态性,多态性是指允许不同类的对象对同一消息作出响应。比如同样的加法,把两个时间加在一起和把两个整数加在一起肯定完全不同。又比如,同样的选择编辑-粘贴操作,在字处理程序和绘图程序中有不同的效果。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。
- 抽象,抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。,在面向对象的语言中抽象是以抽象类来体现的,由于抽象类不是一个具体的对象,所以不能直接被实例化,抽象有利于项目的维护和扩展
了解了面向对象的特征后,我们来看看如何设计类来满足面向对象原则使其特征发挥足够的优势.
面向对象六大原则
面向对象设计有六大原则,单一职责原则、开放封闭原则、里式替换原则、依赖导致原则、接口隔离原则、合成复用原则
- 单一职责原则(迪米特法则)(Single-Resposibility Principle):一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
- 开放封闭原则(Open-Closed principle):软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
- 里式替换原则(Liskov-Substituion Principle):子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
- 依赖倒置原则(Dependecy-Inversion Principle):依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
- 接口隔离原则(Interface-Segregation Principle):使用多个小的专门的接口,而不要使用一个大的总接口
- 合成复用原则(Composite Reuse Principle) 是尽量使用合成/聚合的方式,有合成聚合方式就尽量使用继承。
以上就是面向对象的六大原则,按照这个原则设计类与才能让其发挥更大价值
类的结构
以上讨论了类与对象的关系,面向对象的思想,那么接下来我们需要详细了解下类到底是个什么结构?我们拿个小例子来说明下
package animals; import java.util; public class Dog { //类名用大驼峰 public String furColor; public String height; public String weight; //变量名方法名用小驼峰 public void catchMouse(mouse m){ //建立狗和老鼠之间的关联关系 this.height=7; m.scream(); } }
狗类有个抓耗子的方法,耗子类有个输出的方法
public class mouse { void scream(){ System.out.println("TML是大帅比"); } }
当狗对象调用抓耗子方法的时候,耗子调用方法输出:TML是大帅比
public class test { public static void main(String[] args) { Dog dog=new Dog(); mouse m=new mouse(); //关联关系,耗子类的对象是狗类对象调用方法的参数 dog.catchMouse(m); } }
以上我们可以看的出,类是由方法和变量构成的。
关键字说明
我们看下类中涉及到的一些关键字,包括this、import、package以及一些访问修饰符。
package和import
什么是包?包的定义如下
- 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
- 如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
- 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类
package表示当前类所属包的详细地址。package语句使用方式如下:
- package语句必须作为源文件的第一条非注释性语句
- 一个源文件只能指定一个包,只能包含一条package语句
包可以通过关键字管理,为了能够使用某一个包的成员,我们需要在 Java 程序中明确导入该包。使用 import 语句可完成此功能。在 java 源文件中 import 语句应位于 package 语句之前,所有类的定义之前,可以没有,也可以有多条,其语法格式为:import package1[.package2…].(classname|*);
如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略,导入时需要注意:
- import java.util.* ; 该语句能访问java/util目录下的所有类,不能访问java/util子目录下的所有类,记住:我的附庸的附庸不是我的附庸。
导入java.util.*不能读取其子目录的类,因为如果java.util里面有个a类,java.util.regex里面也有个a类,我们若是要调用a类的方法或属性时,应该使用哪个a类呢。
this关键字
this用该对象对自己的引用,成员变量和局部变量重名的时候可以用this来区分成员变量和局部变量,用this标识的为成员变量。
访问限制修饰符
访问级别共有四种:public(全部可见),protected(本包所有类和跨包子类可见),default(本包所有类可见),private(仅对本类可见)。
对于类、变量、方法,其实修饰符有不同的选择方式:
类的访问修饰符
- public :将一个类声明为公共类,他可以被任何对象访问,一个程序的主类必须是公共类。
- 默认的修饰符(default):只有在相同包中的对象才能使用这样的类。
为什么类只有两种修饰符呢,因为要想使用一个类,需要import导入,那么只有两种情况,public的能导入成功,default的导入不成功。
成员变量的访问修饰符
- public(公共访问修饰符)
- protected(保护访问修饰符)指定该变量可以被自己的类、同包的类、子类访问。在子类中可以(通过super或者不调用来访问)。
- default,在同一个包中的类可以访问,其他包中的类不能访问。
- private(私有访问修饰符)指定该变量只允许自己的类的方法访问,其他任何类(包括子类)中的方法均不能访问
方法的访问修饰符
- public(公共修饰符)
- protected(保护访问修饰符)指定该方法可以被它的类和子类(通过super或者不调用来访问)进行访问。
- default,在同一个包中的类可以访问,其他包中的类不能访问。
- private(私有修饰符)指定此方法只能有自己的类、同包的类、子类访问,其他的类不能访问(包括子类)
需要注意的是protected,如果在子类的方法中new一个父类出来,再用父类变量去调用方法还是会报错
变量
一个类可以包含以下类型变量:
- 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
- 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
- 类变量(静态变量):类变量也声明在类中,方法体之外,但必须声明为 static 类型。
经过static修饰的类变量,该类的所有对象共享一份,成员变量为该对象独享,局部变量作用域则为一个对象中的一个方法或语句块。
方法
什么是方法? 方法包含一个方法头和一个方法体。下面是一个方法的所有部分:
- 修饰符:这是可选的,告诉编译器如何调用该方法。定义了该方法的访问类型(访问限制和是否静态)。
- 返回值类型 :方法可能会返回值。returnValueType 是方法返回值的数据类型。有些方法执行所需的操作,但没有返回值。在这种情况下,returnValueType 是关键字void。
- 方法名:是方法的实际名称。方法名和参数列表共同构成方法签名。
- 参数列表:参数像是一个占位符。当方法被调用时,传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数。
- 方法体:方法体包含具体的语句,定义该方法的功能。
一个完整的方法结构如下:
需要注意的是一个方法的方法名和参数列表共同构成了一个方法的签名
- 方法重载需满足如下条件:在同一个类中 ;方法名相同;方法的形参列表不同,具体的不同表现为: 类型、个数、顺序的不同才可以构成重载。需要注意的是方法的重载与方法的返回值类型与访问权限或是抛出异常无关,还有一点就是java中可以有多个重载的main方法,只有
public static void main(String[] args){}
是函数入口 - 方法调用:方法在调用时,形参与局部变量同等对待,都被分配到栈上,真正的对象被new在堆里,方法在调用时,形参指向传递的实参引用指向的堆中对象,并可以进行修改,方法调用完之后,给该方法分配的局部变量,返回值以及形参等全部消失,这些栈上内容指向的堆上的对象若无引用则等待垃圾回收器回收,传递的实参则走自己所属作用域的逻辑。
方法分析时需注意以上两个先导概念,之后在进行内存分析时会用到。
成员方法
上述示例中dog类中的catchMouse方法就是一种成员方法,它只有在一个类被new出的对象中调用,所以有时候也叫动态方法:
- 成员方法只能通过对象引用进行调用
- 在成员方法里可以访问静态和非静态的变量或方法
- 成员方法只在该类实例初始化后贮存在内存中,当该类调用完毕后会被垃圾回收器收集释放
我们可能更常使用的是成员方法。
静态方法
当方法声明为静态方法时,必须使用static关键字来做修饰符,静态方法有如下的使用事项:
- 静态方法可以直接通过对象引用和类名进行调用
- 在静态方法里不能直接访问非静态成员,但是可以通过在静态方法体中new出一个对象,再用这个对象显式调用非静态成员来访
- 静态方法在初始化类时初始化,并分配内存,初始化后会一直贮存在内存中,不会被垃圾回收器回收
main方法也是静态方法,在main方法中访问类中的成员变量和方法,需要在方法体中创建类的对象
构造方法
每个类都有构造方法。如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法。在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名且没有返回值(返回值也不能为void),用来初始化对象的函数,一个类可以重载多个构造方法。
public class Puppy{ //两个重载的构造方法 String dogName; public Puppy(){ } public Puppy(String name){ dogName=name; } }
构造方法主要用于初始化对象,通过new 构造方法来创建一个新的对象。Puppy p=new Puppy(”阿拉斯加")
初始化对象的过程如下:
- 在栈里给局部变量分配内存空间
- 在堆里new出一个对象,局部变量的值通过构造函数赋给对象
- 方法执行完毕以后,为该方法分配的局部变量栈里内存消失,p引用堆里的对象
当然实际的内存应用关系可能更为复杂,这块在JVM虚拟机那一块儿再详细聊聊。
使用原则
构造方法有如下几条使用原则:
- 构造方法可以被重载,重载就是方法签名中方法明相同但是参数列表不同的意思,后边会详细提到
- 一个构造方法可以通过this关键字调用另一个构造方法,this语句必须位于构造方法的第一行
- 子类通过super关键字调用父类的一个构造方法,也必须位于构造方法第一行,所以super和this不能共存 ;由于this函数指向的构造函数默认有super()方法,所以规定this()和super()不能同时出现在一个构造函数中
- 当一个类中没有定义任何构造方法,Java将自动提供一个缺省构造方法;只有在不显示声明构造方法时,系统才提供默认无参构造方法,也就是说系统不会总是提供,要看你提供没有。
- 当子类的某个构造方法没有通过super关键字调用父类的构造方法,通过这个构造方法创建子类对象时,会自动先调用父类的缺省构造方法如果子类既没有用super调用父类的构造方法,而父类中又没有无参的构造函数,则编译出错。父类有参,必须用super调用,如果是多个有参,则子类构造方法任意选一个即可,如果只有一个那就选那个,父类如果无参,也可以用super,或者不用,则默认调用
总结而言就是:Java会默认给类添加一个缺省构造方法,构造方法可以重载多个,同类中可以相互通过显式this调,但必须放在方法体第一行;子类在使用时默认自动super父类的缺省构造方法,如果父类没有缺省构造方法换言之就是父类显示声明了构造方法,则如果声明的构造方法无参不需要显式super,如果有参必须显式super
使用时机
构造函数在定义类对象(类实例化对象)时自动执行用来初始化对象,也就是new 的时候
修饰符使用规定
构造方法不能被static、final、synchronized、abstract、native修饰,但可以被public、private(单例模式)、protected修饰 也可以不加任何修饰符表示相当于被default修饰;
- 构造方法用于创建一个新的对象,不能作为类的静态方法,所以用static修饰没有意义。
- 构造方法不能被子类继承,所以用final修饰没有意义。
- 对于synchronized关键字,构造方法每次都是new一个新对象,不存在多线程共享同一变量的问题,所以不需要同步。
- 构造函数是用来初始化对象的,abstract声明为抽象函数没有意义
- native是方法修饰符,native方法是由另外一种语言(如C/C++,汇编等)实现的本地方法,因为在外部实现了方法,所以在 java代码中,就不需要声明了
总而言之构造方法只要加访问限制符来作为修饰符即可。