1 类和对象
1.1 面向对象概述
1.1.1面向对象简史
面向对象编程思想最初的起源可以追溯到1960年的Simula语言,这被认为是第一个支持面向对象编程概念的语言。Simula引入了类、对象、继承等概念,将数据和操作进行封装。Simula的创始人奥利-约翰·达尔(Ole-Johan Dahl)和克利斯登·奈加特(Kristen Nygaard)于2001年获得了图灵奖,以表彰他们对面向对象编程概念的开创性贡献。
面向对象编程具有封装、继承和多态等核心特点。封装将数据和操作封装在类中,通过类的实例化创建对象。继承允许一个类继承另一个类的属性和方法,实现代码的重用和扩展。多态性使得不同类型的对象可以以统一的方式进行操作,提高代码的灵活性和可扩展性。
随着Java和C++等编程语言的广泛应用,面向对象编程逐渐成为主流的编程范式。这些语言的成功推动了面向对象编程思想的普及和发展,为开发人员提供了更强大和灵活的工具。面向对象编程已经成为现代软件开发的重要组成部分,被广泛应用于各个领域,如Web开发、移动应用开发和人工智能等。
面向对象编程在计算机科学领域扮演着重要的角色,它的思想和概念为程序设计带来了更高效、更灵活的方式,推动了软件开发的进步和创新。
1.1.2什么是OOP
面向对象编程(Object-Oriented Programming,简称OOP)提供了一种有组织、灵活和可重用的编程方式。它的核心思想是将数据和操作数据的方法封装在一起,形成称为对象的实体。通过使用对象,我们可以更清晰地表示和操作数据,提高代码的可维护性和可扩展性。
让我们以一个校园管理系统为例,来说明面向对象编程的优势。假设我们要管理学生的信息,包括姓名、性别、年龄和分数。如果使用分离变量来表示学生信息,代码可能会变得臃肿和重复:
String name1 = "黄河大虾";
int age1 = 25;
char gender1 = '男';
double score1 = 85.5;
System.out.println("姓名: " + name1);
System.out.println("年龄:" + age1);
System.out.println("性别:" + gender1);
System.out.println("分数:" + score1);
String name2 = "万佳";
int age2 = 28;
char gender2 = '女';
double score2 = 80.5;
System.out.println("姓名: " + name2);
System.out.println("年龄:" + age2);
System.out.println("性别:" + gender2);
System.out.println("分数:" + score2);
// 更多的学生信息...
使用分离变量表示学生信息存在一些问题。首先,变量之间的关联性不明确,我们无法将它们作为一个整体来处理。其次,如果我们需要对学生信息进行多次操作,会出现重复的代码和逻辑,导致代码冗余和难以维护。
而通过使用面向对象编程,我们可以将学生信息和处理过程作为一个整体单元进行管理。一个学生对象可以包含姓名、性别、年龄和分数等属性,并且可以定义方法来操作和处理这些属性。下面是一个使用面向对象编程的学生类示例:
通过使用学生对象,我们可以更清晰地表示学生信息,并且可以对学生对象进行更灵活的操作。在我们的案例中,学生就是一个对象。一个对象是指具有特定属性和行为的实体。属性是对象的特征,例如姓名、年龄、性别和分数。行为是对象可以执行的操作,例如打印学生信息等。
面向对象编程通过封装数据和相关操作的方法,提供了一种更有组织、灵活和可重用的编程方式。对象是包含属性和方法的实体。通过使用对象封装学生信息,我们可以提高代码的可读性、可维护性和可扩展性。
1.1.3面向对象三大特点
面向对象编程具有三大特点:封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。
封装(Encapsulation):封装是面向对象编程的核心概念之一。它指的是将数据和对数据的操作封装在一个单元中,即类中。通过封装,类将数据(属性)和操作(方法)进行了隐藏和保护,只暴露必要的接口供外部访问。这样可以确保数据的安全性和一致性,并隐藏了实现的细节。封装还使得类的使用者无需关注内部实现细节,只需要调用类提供的接口即可完成操作。
继承(Inheritance):继承是面向对象编程中的一种机制,它允许一个类(子类)继承另一个类(父类)的属性和方法。通过继承,子类可以直接使用父类的属性和方法,无需重复编写相同的代码。继承实现了代码的重用,提高了代码的可维护性和可扩展性。子类可以通过继承来扩展或修改父类的行为,同时还可以添加自己的独特功能。
多态(Polymorphism):多态是面向对象编程中的另一个重要特性。它指的是同一个方法可以在不同的对象上具有不同的行为。多态通过方法的重写(覆盖)来实现。方法的重写指的是子类可以覆盖父类中的方法,从而实现不同的行为。
这三大特点共同构成了面向对象编程的基础,使得程序具有更高的可维护性、可扩展性和可重用性。它们使得代码更加模块化、清晰和灵活,能够更好地应对复杂的程序设计和开发需求。
1.2 面向对象编程
1.2.1类(Class)和对象(Object)
类是创建对象的模板或蓝图,它定义了对象的属性和行为。我们可以将类看作是一种自定义的数据类型,它封装了数据和相关操作。
对象(Object)是类的实例化结果,它是类的具体实体。对象具有类定义的属性和行为,并可以通过访问类的成员来操作和修改对象的状态。
我们用一个例子来理解为什么我们需要类,以及类的好处。假设我们要制作月饼,但是没有月饼模子,我们只能一个一个地手工制作,这样效率会很低。但是,如果我们有一个月饼模子,就可以批量快速地制作大量相同形状的月饼了。通过重复使用月饼模子,我们能够制作出许多相同类型的月饼。在这个例子中,月饼模子就相当于Java中的类,而月饼就相当于Java的对象。类就像月饼模子一样,我们可以通过它来批量创建对象。
在Java中,我们设计类的过程就是设计月饼模子的过程。类定义了对象的属性和行为,就像月饼模子定义了月饼的形状和特征。通过定义类,我们可以创建出许多相同类型的对象,而无需重复编写相同的代码。
让我们来看一个实际的例子,假设我们有一个名为"Student"的类,它代表学生。这个类定义了学生的属性,比如姓名、年龄、性别和分数,还定义了学生的行为,比如:显示学员信息。我们可以根据这个类创建出许多不同的学生对象,每个对象都具有相同的属性和行为。
在Java中,创建类的代码如下:
public class Student {
// 属性
private String name;
private int age;
private char gender;
private double score;
// 方法
public void printlnfo(){
System.out.println("姓名: " + name);
System.out.println("年龄:" + age);
System.out.println("性别:" + gender);
System.out.println("分数:" + score);
}
}
通过这一个类作为模板,我们可以创建出不同的学生对象,每个对象都有自己的姓名、年龄、性别和分数。
通过类,我们可以更好地组织和管理代码,可以实现代码复用、可维护性和可扩展性。
首先,一个类可以创建很多对象。我们可以使用同一个类来创建多个对象,每个对象都具有相同的属性和行为。这样,我们可以轻松地创建和管理多个相似的对象,避免了重复编写相同的代码。
其次,通过修改类,我们可以统一管理所有对象的属性和行为。如果我们需要修改对象的属性或者增加新的行为,只需在类的定义中进行修改,所有基于该类创建的对象都会受到影响。这种方式使得代码维护更加方便,我们只需要关注类的定义,而不需要逐个修改每个对象的属性和行为。
最后,通过在类中添加属性和方法,我们可以为所有对象增加属性和方法。如果我们希望为所有对象添加新的属性或方法,只需在类中进行定义,而不需要逐个修改每个对象。这种扩展性使得我们能够快速、灵活地对程序进行功能扩展,而不会影响到已有的代码和对象。
总而言之,类是面向对象编程中非常重要的概念,它提供了一种组织和管理代码的方式,实现了代码复用、可维护性和可扩展性。通过类,我们能够以更高效、更灵活的方式开发程序。
1.2.2 设计一个类
在面向对象编程中,设计类是一个重要的步骤。它涉及识别业务领域中的对象,将其进行分类,并抽取出共同的特征和行为。这个过程有助于组织和管理代码,使得我们可以更加灵活和高效地开发程序。
让我们以设计学生和老师类为例,来说明这个过程。
首先,我们识别了学生和老师作为业务领域中的对象。接下来,按照对象总类进行分类,比如按照学生和老师进行分类。
在分类的基础上,我们可以抽取出学生和老师的共同特征。对于学生来说,共同特征可能包括姓名、年龄、性别、分数等。对于老师来说,共同特征可能包括姓名、年龄、性别以及薪酬等。
有了分类和共同特征,我们就可以定义类了。类名可以反映对象的分类,比如"Student"表示学生类,"Teacher"表示老师类。类中的属性用于描述对象的特征,可以包括姓名、年龄、性别、分数、薪酬等。同时,类中的方法用于描述对象的行为,可以包括学习、参加考试、教学等。
在Java语言中,定义类的成员变量可以使用如下语法:
class 类名 {
成员变量类型 变量名称;
………
}
按照如上语法,我们可以设计定义学生类和老师类:
// 学生类
public class Student {
// 属性
String name;
int age;
char gender;
double score;
}
// 老师类
public class Teacher {
// 属性
String name;
int age;
char gender;
double salary;
}
通过设计"Student"和"Teacher"类,我们将学生和老师的属性和行为封装在一起,实现了代码的组织和管理。每个类代表了一个对象分类,并包含了对象的共同特性和行为。通过实例化这些类,我们可以创建多个具体的学生和老师对象,并对它们进行操作。
这个设计过程是一个思考和抽象的过程,需要我们深入理解业务领域,识别对象和分类,抽取共同特征和行为。正是因为根据对象的共同特性和行为来设计代码,我们才称这种编程方式为"面向对象编程"。这种编程范式使得我们的代码更加模块化、可维护和可扩展,提高了代码的复用性和开发效率。
1.2.3 创建对象
创建对象是设计类的重要目标之一,它允许我们通过类模板来复用并实例化对象。创建对象的过程通常被称为"实例化"。
在Java中,创建对象的语法为:
类名 对象名 = new 类名();
通过这个语法,我们可以在内存中创建一组相关数据,这组数据就是软件中的对象实例。使用类创建对象的好处在于,我们可以通过类来复用代码,并通过分配一组组数据来创建多个对象,从而减少了重复定义数据的麻烦。
以学生类为例,我们可以使用以下代码创建两个学生对象:
// 创建学生对象
Student student1 = new Student();
Student student2 = new Student();
在创建对象的过程中,使用了new运算符。new运算符用于利用类创建对象,每创建一个对象,就会在内存中分配一组数据,即对象的属性。这些属性也被称为"实例变量",因为它们属于每个对象实例的变量。
需要注意的是,Java对象的属性(数据)具有默认的"0"值:
- 整数默认为0
- 浮点数默认为0.0
- 字符默认为编号为0的字符
- 布尔类型默认为false
- 引用类型默认为null
通过创建对象,我们可以使用类中定义的属性和方法来操作和访问对象的状态和行为。每个对象都是独立的实例,它们可以具有不同的属性值,但遵循相同的类定义。
通过面向对象编程,我们可以更好地组织和管理代码,利用类和对象的概念来实现代码的复用和扩展。创建对象是实现这一目标的重要步骤之一,它允许我们通过类模板来创建具体的对象实例,并对其进行操作和处理。
1.2.4 引用类型
在Java中,除了基本数据类型(如整数、浮点数、字符、布尔值等),还存在一种特殊的数据类型称为引用类型。
引用类型是一种用于操作对象的数据类型。它不直接存储对象的值,而是存储对象的引用或内存地址。通过引用类型,我们可以操作和访问对象的属性和方法。
在Java中,类是引用类型的数据类型。当我们创建一个对象时,实际上是在内存中分配一块空间,空间中存储了对象的属性,并将该对象的引用赋值给引用类型变量。这个引用类型变量充当了访问对象的入口,可以读写这个对象空间中的属性。
让我们以操作学生对象为例,说明引用类型如何操作对象。我们可以创建学生对象并使用引用类型变量来操作对象:
// 创建学生对象
Student student = new Student();
// 设置学生的属性
student.name = "Alice";
student.age = 20;
// 获取学生对象的属性值
System.out.println(student.name);
在上述代码中,我们首先使用new关键字创建了一个Student类的对象,然后将该对象的引用(对象的内存首地址)赋值给名为student的引用类型变量。通过这个引用类型变量,我们可以通过student.name和student.age来设置学生的姓名和年龄。
引用类型提供了一种灵活且强大的方式来操作和管理对象,使我们能够更好地利用面向对象编程的优势。
1.2.5 【案例】类与对象示例
我们编写一个案例来创建学生类,并创建两个学生对象,每个学生对象都有姓名、年龄、性别和分数属性。
首先,我们定义一个学生类Student,其中包含了姓名(name)、年龄(age)、性别(gender)和分数(score)属性:
public class Student {
String name;
int age;
char gender;
double score;
}
接下来,我们可以创建两个学生对象,并为每个对象设置相应的属性:
public class Main {
public static void main(String[] args) {
// 创建第一个学生对象
Student student1 = new Student();
student1.name = "Alice";
student1.age = 20;
student1.gender = '女';
student1.score = 85.5;
// 创建第二个学生对象
Student student2 = new Student();
student2.name = "Bob";
student2.age = 19;
student2.gender = '男';
student2.score = 92.0;
// 输出学生对象的属性
System.out.println("学生1的信息:");
System.out.println("姓名:" + student1.name);
System.out.println("年龄:" + student1.age);
System.out.println("性别:" + student1.gender);
System.out.println("分数:" + student1.score);
System.out.println();
System.out.println("学生2的信息:");
System.out.println("姓名:" + student2.name);
System.out.println("年龄:" + student2.age);
System.out.println("性别:" + student2.gender);
System.out.println("分数:" + student2.score);
}
}
在上述代码中,我们首先创建了两个学生对象student1和student2,并为每个对象设置了相应的属性。然后,我们使用System.out.println语句输出了每个学生对象的属性。
通过这个案例,我们成功创建了两个学生对象,并为每个学生对象设置了姓名、年龄、性别和分数属性。每个学生对象都可以独立地存储和访问自己的属性值。这展示了如何使用类创建对象,并通过引用类型变量对对象进行操作和访问。
1.2.6 对象内存分配
在上一个学生案例中,我们创建了两个学生对象student1和student2,并为它们的属性赋予了具体的值。让我们来详细说明一下这些对象的内存存储情况。
每个学生对象在内存中都有自己的存储空间,其中包含了对象的属性值。具体存储情况如下:
student1对象:
- name属性:存储了一个字符串"Alice"。
- age属性:存储了整数值20。
- gender属性:存储了一个字符类型值'女'
- score属性:存储了浮点数值85.5。
student2对象:
- name属性:存储了一个字符串"Bob"。
- age属性:存储了整数值19。
- gender属性:存储了一个字符类型值'男'。
- score属性:存储了浮点数值92.0。
请注意,属性中的字符串值是通过引用来访问的(本次我们先不讨论String的引用)。
1.2.7 null值含义
在Java中,null是一个特殊的值,表示引用变量没有指向任何对象。当一个引用变量被赋值为null时,它不再指向任何有效的内存空间。在上述学生案例中,如果某个属性的值被赋值为null,则表示该属性当前没有指向任何有效的对象。这通常可以表示学生已经毕业或者转学了,对应的数据不再有效。
student1 = null;
在内存存储方面,引用变量被设置为一个特殊的值null,表示它没有指向任何有效的内存地址。而没有被引用的对象空间将被Java的垃圾回收器自动回收。在逻辑方面相当数据没有了,比如:表示学生毕业了或者转学了。
总结起来,每个学生对象在内存中有自己的存储空间,存储了属性值。引用类型的属性存储的是指向实际对象数据的内存地址。而null值表示引用变量当前没有指向任何有效的对象。
1.3 构造器
1.3.1 什么是构造器
构造器(Constructor)是一种特殊的方法,用于封装对象属性的初始化过程。通过构造器,我们可以在创建对象时对对象的属性进行初始化。构造器的名称与类名相同,并且没有返回类型声明。
在上述案例中,我们可以看到对象属性的初始化代码存在重复和冗长的问题。每次创建学生对象时,都需要逐个设置属性的值,这样的代码不仅不便于维护,还容易出错。
为了解决这个问题,我们可以利用构造器来封装对象属性的初始化过程。构造器可以将对象属性的初始化代码集中在一起,以便复用。
构造器的语法如下:
public class 类名 {
// 属性声明…
// 带参数的构造器
public 类名(参数列表) {
// 初始化代码
}
}
为了为Student类定义构造器,我们可以按照以下方式进行修改:
1.3.2 使用 new 调用构造器
通过定义构造器,我们可以在创建Student对象时直接调用构造器,复用构造器中封装的对象属性初始化算法。当需要初始化对象属性时,只需使用new关键字调用构造器,它会执行构造器中封装的对象属性初始化算法,从而初始化新对象的属性。例如,可以使用以下代码创建Student对象:
Student student1 = new Student("Bob", 19, '男', 92.0); // 使用带参数的构造器
Student student2 = new Student("Alice", 20, '女', 85.5); // 使用带参数的构造器
通过构造器的使用,我们可以简化对象属性的初始化过程,提高代码的可读性和可维护性,同时实现对象属性的复用。
注意构造器的语法:
- 构造器的名称与类名必须严格一致,包括大小写也必须一致
- 访问修饰符常用 public
- 构造器不能声明返回值
- 通过构造器初始化成员变量,为属性赋值
- new运算调用构造器创建对象,创建对象过程就是复用构造器初始化对象属性的过程
1.3.3 【案例】为学生类定义有参构造器
下面是一个为学生类添加有参构造器的案例:
public class Student {
// 属性声明
String name;
int age;
char gender;
double score;
// 有参构造器
public Student(String name_, int age_, char gender_, double score_) {
// 接收参数并进行初始化
name = name_;
age = age_;
gender = gender_;
score = score_;
}
// 其他方法声明
}
上述示例中,我们为学生类添加了一个有参构造器,该构造器接收姓名、年龄、性别和分数作为参数,并用参数的值初始化学生对象的属性。
例如,我们可以使用以下代码创建学生对象并使用有参构造器进行初始化:
Student student1 = new Student("Alice", 20, '女', 85.5);
Student student2 = new Student("Bob", 19, '男', 92.0);
通过调用有参构造器,我们可以直接为学生对象的属性赋予初始值,避免了逐个设置属性的麻烦。这样,我们可以更方便地创建具有不同属性值的学生对象。
1.3.4 this关键字
在构造器中,可以使用关键字"this"来引用当前对象实例,以访问该对象的属性和方法。
在Java类中,通常使用"this."来访问对象的属性和方法。默认情况下,可以省略"this.",直接使用属性或方法名进行访问。然而,当局部变量的名称与当前对象的实例变量发生冲突时,就不能再省略"this."了。
通过使用"this.",可以明确指示编译器要访问当前对象的成员,而不是局部变量。这样可以避免命名冲突,确保访问到正确的成员。
例如,在以下情况下需要使用"this."来引用当前对象的成员:
在上述示例中,为了明确指示要访问当前对象的name属性,使用了"this.name"。这样就能区分局部变量name和对象的属性name,确保正确地设置属性的值。
总结起来,"this"关键字用于在构造器中引用当前对象实例,以访问对象的属性和方法。它可以帮助解决局部变量和实例变量之间的命名冲突。
1.3.5 【案例】this示例
使用 this. 重构Student类的构造器。
案例示意代码如下:
public class Student {
// 属性声明
String name;
int age;
char gender;
double score;
// 有参构造器
public Student(String name, int age, char gender, double score) {
// 接收参数并进行初始化
this.name = name;
this.age = age;
this.gender = gender;
this.score = score;
}
// 其他方法声明
}
1.3.6 默认构造器
在上述案例中,由于Student类定义了带参数的构造器,导致无法再使用无参数的构造器"new Student()",从而产生编译错误。这是Java中的默认构造器行为。
Java设计了一条巧妙的规则,确保每个类都有构造器:如果一个类没有声明任何构造器,Java会自动为该类添加一个默认构造器。然而,如果类中已经定义了构造器,则不会再自动添加默认构造器。默认构造器是一个无参数且方法体为空的构造器。
因此,在上述案例中,由于Student类定义了带参数的构造器,所以不再拥有默认的无参数构造器。如果需要使用无参数构造器创建Student对象,可以通过显式定义一个无参数的构造器来解决:
通过显式定义一个无参数构造器,即可在需要时使用"new Student()"来创建Student对象。
public class Student {
// 属性
String name;
int age;
char gender;
double score;
// 有参数构造器
public Student(String name, int age, char gender, double score) {
this.name = name;
this.age = age;
this.gender = gender;
this.score = score;
}
// 默认构造器
public Student() {
// 空方法体
}
}
总结起来,Java设计了默认构造器的规则,保证每个类都有构造器。如果一个类没有定义任何构造器,则Java会自动添加一个无参数且方法体为空的默认构造器。但是,如果类中已经定义了构造器,则不会再自动添加默认构造器。
1.3.7【案例】默认构造器示例
分别定义类 Foo和类 Goo,并为Goo类定义带参数的构造器;测试构造器。
案例示意代码如下:
public class ConstructorDemo2 {
public static void main(String[] args) {
Foo foo = new Foo();
Goo goo = new Goo(5);
}
}
class Foo{
public Foo() {
}
}
class Goo{
public Goo(int n) {
System.out.println("Goo(int)");
}
}
1.3.8 构造器重载
构造器重载是指在一个类中同时声明多个参数不同的构造器。构造器重载的好处是可以有更多的对象创建方式,使用起来更加灵活方便。
1.3.9 this() 重用其他构造器
除了在构造器中使用this关键字引用当前对象的属性和方法外,this关键字还有第二个用法:使用this()重用其他构造器,从而简化构造器的代码。在构造器中使用this()时,必须将其写在构造器的第一行!通过重用构造器,可以大大简化代码,特别是当被重用的构造器非常复杂时,这一特性更加突出。
假设我们有以下需求:
- 定义一个矩形类Rectangle,如果不为其传入高度和宽度,则默认高度和宽度都为10
- 如果只传入一个数值,则将高度和宽度都设置为该数值
- 如果传入两个数值,则分别设置宽度和高度
为了满足这个需求,我们需要为Rectangle类定义三个构造器:一个无参构造器、一个接收一个参数的构造器和一个接收两个参数的构造器。在接收两个参数的构造器定义中,我们将width和height属性的值设置为传入的数据。而在其他两个构造器中,我们可以直接使用this()来调用接收两个参数的构造器,并分别传入相应的数据。
下面是使用this()重用构造器的示例代码:
通过使用this()重用构造器,我们可以在创建Rectangle对象时,根据传入的参数数量自动选择合适的构造器进行属性的初始化,从而简化了代码的编写。
总结起来,this关键字除了用于引用当前对象的属性和方法外,还可以使用this()重用其他构造器。通过重用构造器,我们可以简化构造器的代码,提高代码的可读性和维护性。在使用this()时,需要注意将其写在构造器的第一行。
1.3.10 【案例】构造器重载示例
定义类 Rectangle,实现上一节中的需求;并测试。
案例示意代码如下:
public class ConstructorDemo3 {
public static void main(String[] args) {
Rectangle r1 = new Rectangle();
Rectangle r2 = new Rectangle(20);
Rectangle r3 = new Rectangle(30,40);
}
}
class Rectangle{
int width;
int height;
public Rectangle() {
this(10,10);
}
public Rectangle(int width) {
this(width, width);
}
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
}
2 方法
2.1 方法的声明与调用
2.1.1 什么是方法
在软件开发过程中,经常会遇到需要重复执行的计算过程。例如,排序功能在不同的地方都可能需要用到,如果每次都重新编写排序代码,将会非常繁琐且不便于后期的修改和维护。难道就没有一种方法可以解决这个问题吗?答案是方法(Method)。
方法可以将这些计算过程封装起来,定义成可复用的代码块。当需要使用这段计算过程时,只需要调用该方法即可复用其中的代码。通过使用方法,可以大大简化编程工作,提高编程效率。
方法常用于封装特定的逻辑功能,例如执行计算或操作。方法可以被程序中的多个地方反复调用,这样可以减少代码的重复,更方便程序的维护。
使用方法的好处包括:
- 代码复用:将重复的计算过程封装成方法后,可以在需要的地方直接调用,避免了重复编写相同的代码,提高了代码的复用性
- 提高可读性:将一段具有特定功能的代码封装成方法,可以使代码更加模块化和结构化,提高了代码的可读性和理解性
- 简化修改和维护:如果某个计算过程需要修改或优化,只需要在方法的定义处进行修改,所有调用该方法的地方都会自动生效,简化了代码的修改和维护过程
通过合理地使用方法,我们可以提高代码的可维护性和可复用性,使程序更加高效和易于开发和维护。方法是面向对象编程中非常重要的概念之一,是编写高质量代码的关键。
2.1.2 声明方法
方法用于封装一个特定的功能:通常以一些列的语句组成,并完成一个动作或者功能。
声明方法时需要考虑五个要素:修饰词、返回值类型、方法名、参数列表、方法体。语法如下:
public static int sum ( int num1 , int num2 ) {
// 方法体
}
其中:
- public static为修饰词:控制方法的有效范围(后续课程详细介绍这两个关键字)
- int为返回值类型:方法的计算结果,使用 return 进行返回,如果没有返回值则使用 void
- sum为方法名:需要遵守Java命名规范,建议命名时遵循“见名知义”原则
- int num1,int num2为参数列表:方法计算过程需要的数据
- 一对大括号{}中的为方法体:具体的业务功能实现代码
方法最大的好处是可以复用,在类上声明的方法可以被全体对象复用!
2.1.3 定义参数
方法参数是指,在调用时传递给方法,需要被方法处理的数据。比如需要记录收入时,就可以将收入的金额和备注信息传递到方法中。
为方法定义参数时,需要注意:
- 方法可以有参数也可以没有参数,有参数会使得方法处理更加灵活;
- 在方法定义时,需要声明该方法所需要的参数变量
- 在方法调用时,会将实际的参数值传入方法的参数变量
如下为几个方法定义的实例:
void say() { } //无参方法
void say( string name ) { } //1个参数方法
int sum ( int num1 , int num2 ) { } //2个参数方法
2.1.4 定义返回值
方法调用结束后可以返回一个数据,称之为返回值。当然,方法调用结束后也可以不返回数据,但不管是返回数据也好不返回数据也好,java语法规定,方法在声明时必须指定返回值类型,可分如下的两种情况进行处理:
- 若方法不需要返回数据,将返回值类型声明为void
- 若方法需要返回数据,将返回值类型声明为特定数据类型
return 关键字:
- 通过 return 语句返回数值
- return语句可以结束方法且将数据返回给调用方
2.1.5 【案例】声明方法示例
建立类 Account:
- double类型属性记载账户余额,String类型属性记载备注信息
- 在构造器中初始化账户余额为 1000
- 定义方法 add,实现增加余额操作
- 定义方法 isEnough,对账户余额进行判断
案例代码示意如下:
2.1.6 方法的调用
调用方法时:
方法名(参数列表)
注意:
- 如果调用的方法没有参数,则不用传入数据
- 如果调用的方法有参数,则需要按照顺序传入对应类型的数据
- 如果方法有返回值,则定义变量接收返回值(使用同样数据类型的变量)
调用对象的方法时,需要使用如下方式:
对象.方法名(参数列表)
比如:
//定义方法:
public int sum ( int num1 , int num2 ){ }
//调用:
int result = sum(5,6);
//定义方法:
public void sayHi(String name) { }
//调用:
sayHi(“wkj”);
2.1.7 【案例】方法调用示例
接续上述案例,定义测试类,并在测试类的 main 方法中,创建对象调用对象的方法。
案例示意代码如下:
public class MethodDemo1 {
public static void main(String[] args) {
Account a1 = new Account();
a1.add(500, "劳务费");
a1.add(80, "红包");
}
}
class Account{
double balance;
String log;
public Account(){
this.balance = 1000.0;
}
public void add(double amount, String msg){
this.balance += amount;
this.log += "收入:"+amount+",当前余额:"+balance+",备注:"+msg;
}
public boolean isEnough(double amount){
return (this.balance-amount) >= 0;
}
}
方法参数是方法运算时候的依赖数据,可以将方法运算时候的可变参数通过方法参数传递到方法中。比如需要记录收入时,就可以将收入的金额和备注信息传递到方法中。
2.1.8 关于参数
方法参数是方法运算时候的依赖数据,可以将方法运算时候的可变参数通过方法参数传递到方法中,这个过程简称“传参”。
方法参数列表可以包含多个参数,而且方法参数和方法内部声明的变量一样都是局部变量,方法结束后就销毁了,可以使用 this. 访问当前对象的实例变量,如果与局部变量没有冲突,可以省略局部变量。过程如下图所示:
2.2 方法重载
2.2.1 方法的签名
方法的签名包含:方法名和参数列表。
Java语法规定,一个类中不可以有两个方法签名完全相同的方法,即:一个类中不可以有两个方法的方法名和参数列表都完全相同,但是,如果一个类的两个方法只是方法名相同而参数列表不同,是可以的。查看如下代码:
public class Cashier {
public boolean pay(double money) { … }
public boolean pay(double money) { …}
}
分析如上代码,结论会出现编译错误,因为在同一个类Cashier中的两个方法,签名相同,这在java语法中是不允许出现的。而下面的代码就是正确的:
public class Cashier {
public boolean pay(double money) { … }
public boolean pay(String cardId,
String cardPwd) { … }
}
可以看到上面的代码,在类Cashier中,虽然pay方法名相同,但是参数列表不同,这样是被允许的,可以正常编译通过。
2.2.2 什么是方法重载
为了体现设计的优雅,Java支持方法重载(Overload)。方法重载是指在同一个类中,两个方法的方法名相同,参数列表不同。
比如,之前的案例里的 Account 类,可以拥有两个 add 方法,拥有不同的参数列表:
2.2.3 方法重载及其意义
假想收款窗口的设计,可以采用两种方式:
方式A:开设三个窗口,分别用来接收现金,信用卡和支票的交付方式(分别在窗口上标明),用户根据需要选择窗口,并投入指定的物件。如下图所示:
方式B:开设一个窗口,标为“收款”,可以接收现金,信用卡和支票三种物件。该窗口可以按照输入的不同物件实施不同的操作。例如,如果输入的是现金则按现金支付,如果输入的是信用卡则按信用卡支付,以此类推,如下图所示:
通过对如上两种方式的分析,请问:哪种方式更好一些呢?常规情况下认为,相对于A的方式,B的设计可以降低用户的负担,用户去付款时不需要去找对应的窗口,只需要到收款窗口就可以了, 减少了用户使用时的错误,B的设计更加优雅一些。
按B的设计方式即为方法重载,在Java语言中,允许多个方法的名称相同,但参数列表不同,此种方式称为方法的重载(overload)。
2.2.4 编译时根据签名绑定调用方法
方法执行时,编译器在编译时会根据签名来绑定调用不同的方法。因此,可以把重载的方法看成是完全不同的方法,只不过恰好方法名相同而已。
2.2.5 【案例】方法的重载示例
对上一个案例里的 Account 类的add方法进行重载:只有金额参数;并进行测试。案例示意代码如下:
public class OverloadDemo1 {
public static void main(String[] args) {
Account a1 = new Account();
a1.add(500, "劳务费");
a1.add(200);
System.out.println(a1.log);
}
}
class Account{
double balance;
String log;
public Account(){
this.balance = 1000.0;
this.log = "";
}
public void add(double amount){
this.balance += amount;
this.log += "收入:"+amount+",当前余额:"+balance+",备注:无\n";
}
public void add(double amount, String msg){
this.balance += amount;
this.log += "收入:"+amount+",当前余额:"+balance+",备注:"+msg+"\n";
}
}
3 Java对象内存管理
3.1 对象内存管理
3.1.1 Java 内存管理规则
在JAVA中,有java程序、虚拟机、操作系统三个层次,其中java程序与虚拟机交互,而虚拟机与操作系统交互。编译好的java字节码文件运行在JVM中。
程序中无论代码还是数据,都需要存储在内存中,而java程序所需内存均由JVM进行管理分配,开发者只需关心JVM是如何管理内存的,而无需关注某种操作系统是如何管理内存的,这就保证了java程序的平台无关性。由于Java内存是自动管理的,一般情况下不用人工干预。只有了解内存管理规则才能写出高质量的代码。
JVM会将申请的内存从逻辑上划分为三个区域:堆、栈、方法区。这三个区域分别用于存储不同的数据:
1、方法区是一个静态区,Java的类(.class)以及类中方法都加载到这个区域,在使用类之前Java会将类自动加载到方法区,只加载一次。
2、栈是Java局部变量的空间,局部变量是指在方法中声明的变量,包括方法参数和this都是局部变量。在方法运行期间所有局部变量都在栈中分配,当方法结束时候方法中分配的局部变量全部销毁。
3、堆是Java对象空间,Java的全部对象都在堆中分配,按照对象的属性在堆内存中分配对象的存储空间。对象使用以后,当对象不再被引用时候,对象变成内存垃圾,Java垃圾回收器会自动回收内存垃圾。
3.1.2 创建对象的过程
短短一行的创建对象的代码,实际上发生了很多的事情,涉及到堆、栈、方法区这三大内存空间。
如下图所示:
具体流程如下:
- 把Memory.class文件和Person.class文件加载进内存的方法区
- 在栈内存中,开辟空间,存放变量p
- 在堆内存中,开辟空间,存放Person对象
- 对成员变量进行默认的初始化
- 对成员变量进行显式初始化
- 执行构造方法
- 堆内存完成
- 把堆内存的地址值赋值给变量p ,p就是一个引用变量,引用了Person对象的地址值
3.2 堆内存
3.2.1 对象存储在堆中
JVM在其内存空间开辟了一个称为“堆”的存储空间,这部分空间用于存储使用new关键字所创建的对象。请看如下代码:
Person p = new Person ();
其内存分布如下图所示:
从上图中可以看到右侧的堆内存,new Person()所创建的对象在堆中分配,同时成员变量亦在此分配,并赋初始值为零。引用类型变量p在栈内存中分配,其中保存的数据,为对象在堆内存中的地址信息,假设对象在堆内存的地址为40DF,则p中保存的即是40DF。
3.2.2 成员变量的生命周期
当声明好对象之后,对该对象(堆中的Person)的访问需要依靠引用变量(栈中的p),那么当一个对象没有任何引用时,该对象被视为废弃的对象,属于被回收的范围,同时该对象中的所有成员变量也随之被回收。
可以这样认为,成员变量的生命周期为:从对象在堆中创建开始到对象从堆中被回收结束。
请看如下的代码,演示了对象不再被引用:
Person p = new Person();
p = null ;
//不再指向刚分配的对象空间,成员变量失效
当将c赋值为null时,表示c不再指向刚刚分配的对象空间,此时成员变量失效。
3.2.3 垃圾回收机制
垃圾回收器(Garbage Collection,GC)是JVM自带的一个线程(自动运行着的程序),用于回收没有任何引用所指向的对象。
GC线程会从栈中的引用变量开始跟踪,从而判定哪些内存是正在使用的,若GC无法跟踪到某一块堆内存,那么GC就认为这块内存不再使用了,即为可回收的。但是,java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。
3.2.4 Java程序的内存泄露问题
内存泄露是指,不再被使用的内存没有被及时的回收,严重的内存泄露会因过多的内存占用而导致程序的崩溃。在程序中应该尽量避免不必要的内存浪费。
GC线程判断对象是否可以被回收的依据是该对象是否有引用来指向,因此,当确定该对象不再使用时,应该及时的将其引用设置为null,这样,该对象即不再被引用,属于可回收的范围。
3.2.5 System.gc()方法
GC的回收对程序员来说是透明的,并不一定一发现有无引用的对象就立即回收。一般情况下,当我们需要GC线程即刻回收无用对象时,可以调用System.gc()方法。此方法用于建议JVM马上调度GC线程回收资源,但具体的实现策略取决于不同的JVM系统。
3.3 栈
3.3.1 栈用于存放方法中的局部变量
JVM在其内存空间开辟一个称为”栈”的存储空间,这部分空间用于存储程序运行时在方法中声明的所有的局部变量,例如,在main方法中有如下代码:
Person p = new Person ();
int num = 5;
其内存分配如下图 所示:
说明:方法中的变量即为局部变量,是在栈内存中分配,若变量为值类型,则在栈中存储的就是该变量的值。若变量为引用类型,则在栈中存储的是堆中对象的地址。
3.3.2 局部变量的生命周期
一个运行的Java程序从开始到结束会有多次方法的调用。JVM会为每一个方法的调用在栈中分配一个对应的空间,这个空间称为该方法的栈帧。一个栈帧对应一个正在调用中的方法,栈帧中存储了该方法的参数、局部变量等数据。当某一个方法调用完成后,其对应的栈帧将被清除,局部变量即失效。
3.3.3 成员变量和局部变量
成员变量与局部变量的差别如下:
局部变量:
- 定义在方法中
- 没有默认值,必须自行设定初始值
- 方法被调用时,存在栈中,方法调用结束时局部变量从栈中清除
成员变量:
- 定义在类中,方法外
- 由系统设定默认初始值,可以不显式初始化
- 所在类被实例化后,存在堆中,对象被回收时,成员变量失效
3.4 方法区
3.4.1 方法区用于存放类的信息
方法区用于存放类的信息,Java程序运行时,首先会通过类装载器载入类文件的字节码信息,经过解析后将其装入方法区。类的各种信息(包括方法)都在方法区存储,看如下代码:
Person p = new Person ();
程序在执行这句话时,类首先被装载到JVM的方法区,其中包括类的基本信息和方法定义等,如下图所示:
通过图示可以看出,在方法区中,包含Person类的字节码文件,及类的基本信息及方法M1等。
3.4.2 方法只有一份
当类的信息被加载到方法区时,除了类的类型信息以外,同时类内的方法定义也被加载到方法区。
类在实例化对象时,多个对象会拥有各自在堆中的空间,但所有实例对象是共用在方法区中的一份方法定义的。意味着,方法只有一份。看如下代码:
Person p1 = new Person();
Person p2 = new Person();
p1.M1( );
p2.M1( );
如上的代码中,对象有两个,但是M1方法只有一份,分别针对p1指向的对象和p2指向的对象调用了两次。