【Java面向对象】5-1 类与对象
§5-1 类与对象
面向对象的重要概念和工具就是类。本节将介绍类和对象的关系、创建类和对象(构造器),以及 OOP 中的重要概念:封装。
5-1.1 初识面向对象
面向过程(procedure-oriented programming,POP)是一种以过程为中心的编程思想,主要考虑程序应该做什么以及怎么做。而面向对象(object-oriented programming, OOP)是一种涉及分类思维模式的软件开发方法,主要考虑解决问题应当需要哪些分类,并对这些分类单独思考。
面向过程适合去解决一些规模较小的简单问题,但对于大型项目和多人协作这些复杂问题的角度而言,面向对象更为合适。
对于描述复杂的事物,为了从宏观上去把握,从整体上合理分析,我们需要使用面向对象的思路来分析整个系统。因此,面向对象编程常常需要开发者有较强的抽象思维能力。但是,到了具体的微观层面,对于如何操作的问题,此时则需要面向过程的编程方法。
可见,OOP和POP是不可分割的,常常在实践中混合使用。
面向对象编程的特点:
面向对象编程的本质是:以类的形式组织代码,以对象的组织封装数据。
面向对象的主要特点就是抽象(abstract),具有三大特性:封装(encapsulation)、继承(inheritance)和多态(polymorphism)。
面向对象编程常用的工具就是类(class)。简而言之,类 = 属性 + 方法。类是对一类事物的抽象,是对象的模板,而对象是具体的事物,是类的实例。从代码运行的角度来看,是先有类再有对象,而从认识论的角度而言,是先有对象再有类。
5-1.2 类与对象的关系
前文提到,类是一种抽象的数据类型,是对具有同样特征的一类事物的抽象、整体描述和定义,不能代表某一个具体的事物。类常用于描述和定义某一类事物所具有的特征、属性和行为。例如:车辆、人类、宠物等。
而对象是类中某个具体的事物,是一类事物的一个具象化例子(具体实例)。例如,猫、狗、鹦鹉等都属于宠物,它们都是宠物这一类事物的某个具体实例。
这种思想可以通过代码实现。
5-1.3 编写类和创建对象
-- 编写类
注意:在一个 Java 项目中,应只有一个类含有 main
方法,用于调用其它类以实现某种(些)功能。
从代码角度而言,要想创建对象,首先得要有其对应的类。类是一类具有相同特征的事物的抽象,类中可以定义属性(数据)和行为(方法)。
我们以学生为例子,创建一个类:
public class Student {
//属性(数据)
String name; //null
int age; //0
boolean gender; //T: male; F: female; defaults to female
//行为(方法)
public void get() {
System.out.println("Name: " + this.name);
System.out.println("Age: " + this.age);
System.out.println("Gender: " + (this.gender ? "Male" : "Female"));
}
public void study() {
System.out.println(this.name + " is currently studying.");
}
}
注意:类是一种模板,在类中写定义属性(数据)时不能够为变量赋值。
-- 实例化:
定义好类后,类通过实例化就会返回一个自己的对象。在同一包中,使用 new
关键字实例化,我们就可以在 main
函数中调用:
public class Application {
public static void main(String[] args) {
Student studentA = new Student();
studentA.get();
System.out.println("================");
studentA.name = "Zebt";
studentA.age = 17;
studentA.get();
studentA.study();
}
}
可以看到,这里我们使用了关键字 this
,this
用于在任何实例方法中指向当前的对象
注意:若二者位于同一包中,则实例化时可以直接通过 new
关键字实现。但若在其他包中调用,由于访问权限的关系,还需要在主类前加上 import
语句。同时还应当在类中保留一部分 public
访问权限的变量用作与外部沟通的接口。这将在[封装](#5-1.6 封装)中有讲解。
至此,编写类和创建对象的工作就完成了。
5-1.4 内存分析
在创建对象的过程中,变量和对象在内存空间中的情况是如何的呢?
首先得先明确三个概念:方法区、堆、栈。
方法区:由于类是最先被加载的,方法区最先有数据,用于存储类信息、class
对象、静态变量(static
)、字符串常量等,被所有线程共享。JVM 只有一个方法区。
堆:堆用于存储创建好的对象和数组(数组也是对象)、成员变量(实例变量)。可见,凡是通过 new
运算符创建出来的对象,都存储在堆中,new
的作用就是在堆内存中开辟一块空间。方法区实际也是堆,堆被所有线程共享。一个 JVM 只有一个堆。
栈:栈用于存放该线程执行方法的信息(实际参数、局部变量等)。在栈中存储基本数据类型、对象的引用,方法调用时进栈,方法执行完后出栈。创建对象所需要的内存是由栈来分配的。栈为线程私有的,不能实现线程之间的共享。
我们以 5-1.2 中的类为例。
当运行程序时,类和 static
最先被加载。Student
类和 Application
类就会出现在方法区。
运行到 main()
方法时,由栈来运行。在 main
方法中实例化了 Student
对象,栈就会为 Student
对象在堆中分配内存空间,并保存该对象的地址,通过一个栈中的引用变量来调用该对象。以下图为例,0x0001
和 0x0002
就是这两个对象的所在堆内存地址,通过引用变量 studentA, studentB
来引用对象。
堆内存就会开辟一块内存空间存储栈中所分配的对象,并且堆内存会给每个对象分配一个内存地址。new
运算符的作用就是在堆中开辟内存空间。
Java 中的数据类型有两大类:引用类型和基本类型。对象是类的实例,属于引用类型,通过引用来操作,即栈中一个引用变量名(同指针)来操作。
对于成员变量(实例变量),无论是什么数据类型,都是存储在堆中。而局部变量的基本类型存储在栈中,为线程所私有,生命周期和作用域都很短,为提高效率,没有必要放在堆中。
5-1.5 构造器
以 5-1.2 中的类为例,创建对象时,即使没有为对象中的数据赋值(初始化),使用 get()
函数获取对象信息时,对象中的数据已经被初始化。
使用 new
实例化对象时,需要为对象分配内存空间,同时给创建好的对象进行默认的初始化。另外,其本质上是在调用构造器(或构造方法)。
特点:
- 必须与类的名字相同;
- 必须不能有返回值类型,也不能写
void
; - 当类中缺省构造器时,编译器会自动添加一个空的无参构造器;
构造器在类中十分重要,是必须要掌握的。
语法:无参构造
public 类名() {
...
}
语法:有参构造
public 类名(形参列表) {
...
}
IDEA 快捷键:按下快捷键 alt + insert
,在弹出菜单中选择 构造器(Constructor)
后,选择参数后单击 确定(OK)
生成有参构造器,选择 无选择(Select None)
生成无参构造器;
注意:
-
由于构造器是在对象实例化时必须调用的,因此其访问修饰符必须为
public
; -
若已经显式定义了有参构造,则必须显式定义无参构造(原因见 [继承 -
super
关键字](5 - 2 继承.md) ); -
除了可以在构造器中为成员变量赋初始值,也可以在构造器外,类中的成员变量直接赋值,例:
public class Student { String name = "Default Name"; int age = 0; boolean gender = false; }
值得注意的是,在上述例子中,若需要定义一个有参构造器,为避免初始值冗余,还需显式定义一个无参构造器。
-
除了无参有参构造器、构造器重载,还存在一种构造器:复制构造器。但使用较少,一般考虑使用 [
clone()
方法](5 - 2 继承.md)。
结合 5-1.3 的内存分析来看,创建对象时构造器是必须被调用的。若用户已经显式定义了一个构造器,那么实例化时构造器就会在堆中的方法区中被调用。以这个例子来看,对象就有了初始化后的值。
5-1.6 封装
程序设计追求 “高内聚,低耦合”。高内聚即类的内部数据操作细节由自己完成,不允许外部干涉(也无需使用者掌握);低耦合指只暴露少量的方法给外部使用。简而言之,就是该露的露,该藏的藏。一般来说,选择隐藏数据,暴露方法。
通常来说,应当禁止访问一个对象中的数据实际表示,而应当使用接口来访问,这称为信息隐藏。这种隐藏对象的属性和实现细节,只对外公开接口的过程称为封装。
使用关键字 private
用于限制成员的访问权限,使得成员只能够被当前类访问。
若缺省,默认为 default
,使得成员只能够在同一类中和同一包中(子类与无关类)访问。
而关键字 public
可以使得成员能够被所有类访问。
在[继承](5 - 2 继承.md)中,将会介绍 protected
访问修饰符。
不妨来重写 5-1.2 的类,以实现封装:
public class Student {
private String name;
private int age;
private boolean gender;
public Student(String name, int age, boolean gender) {
this.name = name;
this.gender = gender;
if (age < 0 || age >= 120) {
this.age = 0;
} else {
this.age = age;
}
} //构造器
public String getName() {
return this.name;
} //获取姓名
public int getAge() {
return this.age;
} //获取年龄
public boolean getGender() {
return this.gender;
} //获取性别
public void setName(String name) {
this.name = name;
} //设置姓名
public void setAge(int age) {
if (age < 0 || age >= 120) {
this.age = 0;
} else {
this.age = age;
}
} //判断数据是否合法,设置年龄
public void setGender(boolean gender) {
this.gender = gender;
} //设置性别
}
这样子,就实现了信息隐藏,也保留了一个可供外部访问的接口用于读取 / 修改信息。同时也注意到,通过封装,我们可以通过方法来判定数据是否合法,从而作进一步的操作。
set
和 get
方法建议如上构造,构造器可以通过方法重载实现多种构造方法。至此,封装完成。在 IDEA 中,可以通过按下 alt + insert
在弹出菜单中选择 Getter and Setter
从而实现快速构造 set
get
方法。
使用封装的优点:
- 提高程序的安全性,保护数据;
- 隐藏代码的实现细节;
- 统一接口;
- 提高系统可维护性;
5-1.7 总结:标准 JavaBean
总而言之,标准的 JavaBean
类(咖啡豆):
- 类名要见名知意;
- 成员变量使用
private
修饰; - 至少提供两个构造方法:
- 无参构造方法;
- 带全部参数的构造方法;
- 成员方法:
- 提供每一个成员变量的对应的
Getter & Setter
; - 若还有其他行为,也必须要写上。
- 提供每一个成员变量的对应的
在 IDEA 中,可安装 PTG 插件快速一键生成标准 JavaBean
。