5.1 面向对象和面向过程
面向过程是以过程为中心的,遇到问题时,想的是解决问题的步骤,然后用函数把步骤一一实现,最后再依次调用。面向过程更侧重于怎么做,以执行者的角度思考问题,比较适合解决小型问题或底层问题。
面向对象以对象为中心,遇到问题时,想的是需要哪些对象一起来解决问题,然后找到这些对象。并把它们组织在一起,然后取各家之所长来共同解决一个问题。面向对象更侧重于"谁来做",以指挥者的角度思考问题,比较适合解决中大型问题。
面向对象具有三大特性:
- 封装
- 继承
- 多态
1.封装性
封装是面向对象的核心思想,它有两层含义,一是指把对象的属性和行为看成是一个密不可分的整体,将这两者“封装”在一起(即封装在对象中);另外一层含义指“信息隐藏”,将不想让外界知道的信息隐藏起来。例如,驾校的学员学开车,只需要知道如何操作汽车,无需知道汽车内部是如何工作的。
2.继承性
继承性主要描述的是类与类之间的关系,通过继承,可以在无需重新编写原有类的情况下,对原有类的功能进行扩展。例如,有一个汽车类,该类描述了汽车的普通特性和功能。进一步再产生轿车类,而轿车类中不仅应该包含汽车的特性和功能,还应该增加轿车特有的功能,这时,可以让轿车类继承汽车类,在轿车类中单独添加轿车特性和方法就可以了。继承不仅增强了代码的复用性、提高开发效率,还降低了程序产生错误的可能性,为程序的维护以及扩展提供了便利。
3.多态性
多态性指的是在一个类中定义的属性和方法被其它类继承后,它们可以具有不同的数据类型或表现出不同的行为,这使得同一个属性和方法在不同的类中具有不同的语义。例如,当听到“Cut” 这个单词时,理发师的行为是剪发,演员的行为表现是停止表演,不同的对象,所表现的行为是不一样的。多态的特性使程序更抽象、便捷,有助于开发人员设计程序时分组协同开发。
5.2 类与对象
面向对象的世界观认为世界是由各种具有自己的运动规律和内部状态的对象所组成的,不同对象之间的相互作用和通信构成了完整的现实世界。使用面向对象的编程思想来分析问题需要从以下几个步骤思考。
- 根据问题需要,找到问题所针对的现实世界的实体。
- 从实体中寻找与解决问题相关的属性和功能,这些属性和功能就形成了概念世界的抽象实体。
- 把抽象的实体用计算机语言进行描述,形成计算机世界的类,即借助程序语言,把类构造成计算机能够识别和处理的数据结构。
- 类实例化成计算机世界中的对象,对象是计算机世界中解决问题的最终工具。
5.2.1 类与对象的关系
在面向对象中,为了做到让程序对事物的描述与事物在现实中的形态保持一致,面向对象思想中提出了两个概念,即类和对象。在Java程序中类和对象是最基本、最重要的单元。类表示某类群体的一些基本特征抽象,对象表示一个个具体的事物。类是抽象的,对象是具体的,类相当于创建对象的蓝图。面向对象的程序实现需要通过类创建对应的实例化对象,来对应客观世界中的实体。
例如,在现实生活中,学生就可以表示为一个类,而一个具体的学生,就可以称为对象。一个具体的学生会有自己的姓名和年龄等信息,这些信息在面向对象的概念中称为属性;学生可以看书和打篮球,而看书和打篮球这些行为在类中就称为方法。
5.2.2 类的声明
在面向对象的思想中最核心的就是对象,而创建对象的前提是需要定义一个类,类是Java中一个重要的引用数据类型,也是组成Java程序的基本要素,所有的Java程序都是基于类的。将一组具有相同属性和功能(或行为)的对象归类抽象出类的概念,那么应怎么用Java语言来定义这个类呢?定义类的简单语法格式如下:
修饰符 class 类名{
0个或多个属性定义
0个或多个构造器定义
0个或多个方法定于
......
}
- class是定义类的关键字,类名就是一个标识符。
- 类可以包含3种最常见的成员,即属性、构造器、方法,类中各个成员的排列顺序不同是没有任何影响的,但是习惯上人们会按照属性、构造器、方法的顺序去定义。
- 属性用来描述对象的数据特征,如姓名、年龄、颜色、价格等。同一个类的实体意味着有相同的数据特征,但每个对象的属性值都是独立的。
- 构造器用来创建和初始化对象。如果不定义任何构造器,那么编译器会自动生成一个默认的构造器。
- 方法用来描述对象的功能(或行为),调用对象的方法就是让对象完成一个动作或执行某个功能。
示例:
package.com.li.section02;
public class Person{
String name;
int age;
char gender;
// 吃饭方法的定于
void eat(){
if (age < 1) {
System.out.println(name + "喝奶");
} else if (age > 80) {
System.out.println(name + "吃稀饭");
} else {
System.out.println(name + "吃饭");
}
}
}
- 修饰符访问控制,针对类、成员方法和属性,Java提供了4种访问控制权限,分别是private、default、protected和public。这4种访问控制权限按级别由小到大依次排列,如下图。
4种访问控制权限,具体介绍如下:
(1)private(当前类访问级别):private属于私有访问权限,用于修饰类的属性和方法。类的成员一旦使用了private关键字修饰,则该成员只能在本类中进行访问。
(2)default:如果一个类中的属性或方法没有任何的访问权限声明,则该属性或方法就是默认的访问权限,默认的访问权限可以被本包中的其它类访问,但是不能被其他包的类访问。
(3)protected:属于受保护的访问权限。一个类中的成员使用了protected访问权限,则只能被本包及不同包的子类访问。
(4)public:public属于公共访问权限。如果一个类中的成员使用了public访问权限,则该成员可以在所有类中被访问,不管是否在同一包中
5.2.3 对象的创建
有了类就可以创建对象了。Java语言通过new关键字来创建对象的,语法格式如下所示:
new 类型();
如果没有给对象命名,那么该对象就是一个匿名对象,只能使用一次。如果想要让这个对象被使用多次,那么可以给这个对象命名,语法格式如下所示:
类名 对象名 = new 类名();
使用Person类创建对象,示例代码如下:
package com.li.section02;
public class TestOOP{
public static void main(String[] args) {
// 使用Person类创建了两个Person实例对象
Person p1 = new Person();
Person p2 = new Person();
// 操作p1对象的属性
p1.name = "xiaoming";
p1.gender = '男';
p1.age = 100;
System.out.println("p1.name = " + p1.name + ", p1.age = " + p1.age + ", p1.gender = " + p1.gender);
//操作p2对象的属性
p2.name = "xiaohong";
p2.gender = '女';
p2.age = 18;
System.out.println("p2.name = " + p2.name + ", p2.age = " + p2.age + ", p2.gender = " + p2.gender);
// 调用p1的eat()方法
p1.eat();
// 调用p2的eat()方法
p2.eat();
}
}
运行结果:
p1.name = xiaoming, p1.age = 100, p1.gender = 男
p2.name = xiaohong, p2.age = 18, p2.gender = 女
xiaoming吃稀饭
xiaohong吃饭
5.3 类的成员之成员变量
成员变量即类声明的属性。属性是一种比较传统的说法,在Oracle官网中,也被称为Field字段,用来描述事物的特征或属性。
5.3.1 成员变量的声明
成员变量是变量的一种,与之前我们学习的变量的不同之处在于,成员变量声明的位置不同,其语法格式如下:
修饰符 class 类名 {
修饰符 数据类型 成员变量名;
}
上述语法格式说明成员变量是声明在类中方法外的。
- 成员变量的数据类型可以是任何基本数据类型(如:int、boolean)或任何引用数据类型(如:类、数组等),数据类型不能省略。
- 成员变量名就是一个标识符,如name,age,成员变量名不能省略。
- 成员变量的修饰符可以有private、protected、public、static、final、transient等,修饰符也可以没有。
// 局部变量与成员变量的不同:
// 在Java中,定义在类中的变量被称为成员变量,定义在方法中的变量被称为局部变量。
// 如果在某一个方法中定义的局部变量与成员变量同名,这种情况是允许的,
// 此时,在方法中通过变量名访问到的是局部变量,而并非成员变量。
class Student {
int age = 30; // 类中定义的变量被称作成员变量
void read() {
int age = 50; // 方法内部定义的变量被称作局部变量
System.out.println("大家好,我" + age + "岁了,我在看书!");
}
}
在对某一类实体的数据特征进行抽象的过程中,会遇到有些数据特征值是对每个对象都是完全独立的,有些则是全局共享的。例如,在对所有中国人实体进行抽象时,可以发现所有中国人都有国籍、姓名、年龄、性别等数据特征,它们都需要声明对应的成员变量,但是其中的国籍值是全局共享的,都是“中国”,但是每个人的姓名、年龄、性别值都是独立的、各不相同的。Java语言中规定,如果某个数据特征值是所有实体对象共享的,那么该数据特征对应的成员变量就是static修饰,称为静态变量或类变量。反之,如果某个数据特征值对于该类的每个实体对象都是独立的,那么该数据特征对应的成员变量就不能用static修饰,称为实例变量。
Chinese类的示例代码:
package com.li.section3;
public class Chinese{ // 类名
static String country; // 成员变量之静态变量
String name; // 成员变量之实例变量
int age; // 成员变量之实例变量
}
5.3.2 成员变量的访问
成员变量的使用说明:
package com.li.section05;
public class TestChineseField{
public static void main(String[] args){
// 实例变量不能通过“类名.”进行访问,一下两行代码不注释掉会编译报错
// System.out.println("实例变量:Chinese.name = " + Chinese.name);
// System.out.println("实例变量: Chinese.age = " + Chinese.age);
// 静态变量,可以通过"类名."进行访问
System.out.println("静态变量:Chinese.country = " + Chinese.country);
Chinese c = new Chinese();
// 实例变量,需要通过"对象.",进行访问
System.out.println("实例变量:c.name = " + c.name);
System.out.println("实例变量:c.age = " + c.age);
// 静态变量,也可以通过"对象."进行访问
System.out.println("静态变量:c.country" + c.country);
}
}
成员变量的调用说明:
调用者 | 静态变量 | 实例变量 |
---|---|---|
类名 | 可以调用 | 不可以调用 |
对象 | 可以调用 | 可以调用 |
虽然静态变量可以通过"对象."进行访问,但是不建议这么操作,因为这样会给其他读者在阅读代码时造成误解。
5.3.3 成员变量默认值
成员变量会自动初始化为默认值,静态变量的类加载时初始化,实例变量在创建对象时初始化。
成员变量的初始化值
成员变量类型 | 初始值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0 |
float | 0.0f |
double | 0.0 |
char | ‘\u0000’ |
boolean | false |
引用类型 | null |
当然也可以后动赋值,示例如下:
package com.li.section3;
public class TestChinese{
public static void main(String[] args) {
Chinese.country = "中国";
Chinese c1 = new Chinese();
c1.name = "li";
c1.age = 32;
Chinese c2 = new Chinese();
c2.name = "wang";
c2.age = 18;
// 输出各成员变量的值
System.out.println("Chinese.country = " + Chinese.country);
System.out.println("c1.country = " + c1.country);
System.out.println("c1.name = " + c1.name);
System.out.println("c1.name = " + c1.name);
System.out.println("c2.country = " + c2.country);
System.out.println("c2.name = " + c2.name);
System.out.println("c2.name = " + c2.name);
//
System.out.println("------------------------------" );
// 通过c1修改country和name的值
c1.country = "中华人民共和国";
c1.name = "lily";
// 再次输出各成员变量的值
System.out.println("Chinese.country = " + Chinese.country);
System.out.println("c1.country = " + c1.country);
System.out.println("c1.name = " + c1.name);
System.out.println("c1.name = " + c1.name);
System.out.println("c2.country = " + c2.country);
System.out.println("c2.name = " + c2.name);
System.out.println("c2.name = " + c2.name);
}
}
静态变量country的值是所有对象共享的,内存中有且仅有一份,通过c1.country进行修改时,仍然会影响到Chinese.country的值和c2的值,这个几个country值本质上就是同一个。
5.3.4 对象的内存分析
JVM相当于一台虚拟的计算机,它模拟量计算机的内存,并且有自己独特的内存管理方式。在程序运行时,JVM将内存分为了5各部分:方法区、堆、虚拟机栈、本地方法栈、程序技术器。JVM运行时的内存划分说明如下表:
区域名称 | 说明 |
---|---|
程序计数器 | 模拟CPU中的一个特殊寄存器,指向每个线程下一条要执行指令的地址;具有自动增加的功能,即一条指令执行完成后,自动指向下一条指令地址 |
本地方法栈 | 是程序调用了native的方法时,是本地方法执行期间的内存区域 |
虚拟机栈 | 存储正在执行的每个Java方法的局部变量表等,方法执行后,自动释放。平时提到内存的栈结构,指的就是虚拟机栈 |
堆 | 存储实例对象信息(包括数组对象),是线程共享的区域 |
方法区 | 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 |
JVM内存示意图:
5.3.5 成员变量与局部变量的区别
静态变量、实例变量都是在类中直接声明的变量,都是成员变量,再加上在方法内声明的变量,共有两种变量,一种是在方法中声明的局部变量;另一种是在方法外声明的成员变量。成员变量和局部变量的区别如下表:
5.4 类的成员之方法
方法(Method)又称函数(Function),代表一个独立的、可复用的功能,也就是将完成某个特定功能的一系列步骤封装到一起,对外暴露一个方法签名,供使用者调用,这样可以大大提高代码的重用性、扩展性和维护性。Java语言的方法不能独立存在,所有的方法都必须定义在类中,它是对象行为(或功能)特征的抽象。
5.4.1 方法的声明
方法作为类的成员,必须定义在类中,具体语法如下:
修饰符 返回值类型 方法名(参数列表)throws 异常列表 {
方法体语句;
}
一个完整的、可以执行的方法由方法签名和方法体两部分构成。方法签名指的是"修饰符 返回值类型 方法名 (参数列表)throws 异常列表",对于调用者来说通常只需要关注方法签名即可,不需要了解方法体具体是如何实现的。通过方法签名就可以明确方法的功能是什么,是否需要参数,最后可以得到什么结果,以及调用的方式。
方法签名中体现了方法的6个要素:
- 方法名就是一个标识符,在给方法命名时不能太随意,必须遵循"见名知意"的原则,因为方法名相当于方法的灵魂。
- 一个方法的参数列表是可选的,但是无论是否有参数,方法的小括号都不能省略。参数相当于一个方法的"输入",当某个功能的完成需要使用者传入数据时,就可以声明参数列表。参数列表的声明必须说明参数的数据类型和参数名,如果有多个参数,则每个参数之间用逗号进行分割,而参数的类型可以是基本数据类型,也可以是引用数据类型。参数名就是一个标识符,在方法体{}中可以通过这个标识符使用调用者传入的参数值。
- 一个方法的返回值类型是不能缺省的。返回值相当于一个方法的"输出"。当方法被调用后需要给调用者返回结果时,就需要在方法签名中明确该结果的类型。这个结果可以是基本数据类型的值,也可以是一个对象。因此,方法的返回值类型可以是基本数据类型,也可以是引用数据类型。就算方法调用之后不需要给调用者返回结果也不能省略返回值类型,要用void这个特殊类型表示。需要注意,一个方法至多只能有一个返回值,当有多个结果需要返回时,可以考虑将返回值类型声明为数组、集合等容器类型,然后将结果放到数组或集合容器中再返回容器。
- 方法的修饰符可以有private、protected、public、static、final、abstract等,也可以没有。
- throws异常列表代表方法可能抛出的异常类型,完全是可选的。
- 方法体{}是完成方法功能的代码实现,如果没有方法体,那么这个方法要么不能执行,要么什么也不执行。注意,如果方法的返回值类型为void以外的其他类型,那么在方法体的大括号中必须要有"return 结果;"语句来结束这个方法,否则编译会报错;如果方法的返回值类型声明为void,那么在方法体的大括号中就不需要,也不能写"return 结果;"语句。如果想要结束方法的执行可以使用"return;"语句。
根据参数是否声明了参数列表,以及返回值类型是否声明为void,可以将方法分为4种形式。方法的分类表如下:
根据方法是否有static修饰,可以将方法分为静态方法和非静态的实例方法。当方法的功能和类的实例对象无关时,可以把该方法声明为static的静态方法,这样在调用该方法时可以直接使用类名进行调用,如Math.random(),由Math发起对random()的调用。若不同的实例对象调用该方法的功能有差异,则该方法就不能声明为static的静态方法,而非静态的实例方法的调用必须使用对象,如:System.out.println()。
Person类中非静态实例方法的示例代码如下:
package com.li.section04;
public class Person{
String name;
int age;
// 方法定义
public void eat(){
if (age <= 1){
System.out.println(name + "喝奶");
} else if (age > 80){
System.out.println(name + "吃稀饭");
} else {
System.out.println(name + "吃干饭");
}
}
}
5.4.2 方法的调用
方法也必须先声明后使用,而且方法体中代码不调用是不会执行的,调用一次执行一次。如何调用一个方法,需要从以下几个方面进行阐述。
首先,要看方法的修饰符,静态方法和非静态方法的调用说明如下表所示:
其次,需要看被调用的方法是否有参数。如果被调用的方法在定义时并没有声明参数列表,则在调用时也不能在小括号中传如参数;反之,如果方法在定义时声明了参数列表,则在调用时就必须在小括号中传入对应的类型和个数的参数值。
最后,在调用时还要关注方法的返回值类型。如果被调用方法的返回值类型是void,则表示方法没有结果返回,那么调用方法的语句就不能接收和处理返回值。反之如果被调用的方法的返回值类型不是void,则表示方法有结果返回,那么调用方法的语句可以接收和处理返回值。
调用Person类的示例代码:
package com.li.section4;
publib class TestPerson{
public static void main(String[] args){
Person p1 = new Person();
p1.name = "li";
p1.age = 25;
p1.eat();
Person p2 = new Person();
p2.name = "wang";
p2.age = 30;
p2.eat()
}
}
5.4.3 方法的传参机制
方法的参数对于方法功能的实现是很重要的,那么在方法调用时参数值具体是如何传递的,首先要了解形参和实参。形参就是形式参数,它是指声明方法时在方法签名的()中声明的参数列表,只有数据类型和参数名,此时并没有具体的参数值;实参是实际参数的意思,它是指在调用方法时在方法()中传入的参数,可能是一个常量值,也可能是一个变量名或表达式,此时它有具体值。
当形参是基本数据类型时,实参会把自己的值拷贝一份给形参,即此时实参仅仅给形参传递了一个副本,那么就意味着在方法执行期间,对形参的修改完全不会影响实参。
当形参为引用数据类型时,引用数据类型变量中存储的是对象的引用,即对象的首地址,那么引用数据类型的实参传递给引用数据类型的形参自然是对象的地址值副本。而一旦持有了对象的首地址,就可以访问这个对象的所有信息,因此如果通过形参变量中保存的对象地址,访问和修改了对象中的数据,那么就会影响实参对象。
综上所述,在方法调用时,实参会将自己保持的值副本传递给形参。
5.5 方法的重载
所谓方法的重载,就是 在同一个类中,拥有两个或更多个名称相同但参数列表不同的方法,参数列表不同,可以时参数类型不同,也可以是参数的个数不同。方法的重载与返回值类型无关。
5.5.1 重载方法的声明和调用
示例代码:
package com.li.section5;
public class MathTools{
public static int max(int a, int b){
System.out.println("方法: int max(int a, int b)");
return a > b ? a : b;
}
public static double max(double a, double b){
System.out.println("方法: double max(double a, double b)");
return a > b ? a : b;
}
public static int max(int a, int b, int c){
System.out.println("方法: int max(int a, int b, int c)");
return max(max(a, b), c);
}
}
测试代码:
package com.li.section5;
public class TestMathTools{
public.out.println("3,5中较大的是:" + MathTools.max(3, 5));
public.out.println("3.0,5.2中较大的是:" + MathTools.max(3.0, 5.2));
public.out.println("3,5,2中较大的是:" + MathTools.max(3, 5, 2));
}
传入两个int参数时,编译器就会自动调用int max(int a, int b); 传入两个double参数(3.0, 5.2)时,编译器就会自动调用double max(double a, double b)这个方法,传如三个参数时,编译器就会调用int max(int a, int b, int c)这个方法。也就是说编译器会根据调用方法时传入的实参的类型和个数,找到最匹配的方法。
方法的重载形式时很常见的,如System.out.println()方法就是典型的重载方法,其声明形式如下:
public void println()
public void println(boolean x)
public void println(char x)
public void println(char[ ] x)
public void println(float x)
public void println(double x)
public void println(int x)
public void println(long x)
public void println(Object x)
public void println(String x)
所以可以通过System.out.println()方法输出各种数据类型的数据。