5.3.4 变量的使用规则

定义一个成员变量时,成员变量将被放置到堆内存中,成员变量的作用域将扩大到类存在范围或者对象存在范围,这种范围的扩大有两个害处。

  1. 增大了变量的生存时间,这将导致更大的内存开销。
  2. 扩大了变量的作用域,这不利于提高程序的内聚性。

循环变量应该在代码块内定义

对于一个循环变量而言,只需要它在循环体内有效,因此只需要把这个变量放在循环体内(也就是在代码块内定义),从而保证这个变量的作用域仅在该代码块内。
如果有如下几种情形,则应该考虑使用成员变量。

1. 用于描述某个类或某个对象的固有信息的变量

如果需要定义的变量是用于描述某个类或某个对象的固有信息的,这种变量应该定义为成员变量。

  • 如果这种信息对这个类的所有实例完全相同,或者说它是类相关的,例如人类的眼睛数量,目前所有人的眼睛数量都是2只,这种类相关的信息,应该定义成类变量;
  • 如果这种信息是实例相关的,例如人的身高、体重等,每个人实例的身高、体重可能互不相同,这种信息是实例相关的,应该定义成实例变量。

2. 用于保存某个类或某个实例状态信息的变量

如果在某个类中需要以一个变量来保存该类或者实例运行时的状态信息,例如上面五子棋程序中的棋盘数组,它用以保存五子棋实例运行时的状态信息。这种用于保存某个类或某个实例状态信息的变量通常应该使用成员变量

3. 需要在某个类的多个方法之间进行共享的信息

如果某个信息需要在某个类的多个方法之间进行共享,则这个信息应该使用成员变量来保存。

尽量使用局部变量 尽量缩小局部变量的作用范围

即使在程序中使用局部变量,也应该尽可能地缩小局部变量的作用范围,局部变量的作用范围越小,它在内存里停留的时间就越短,程序运行性能就越好。因此,能用代码块局部变量的地方,就坚决不要使用方法局部变量

5.3.3 局部变量的初始化和内存中的运行机制

局部变量不会默认初始化

局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中

局部变量保存在方法栈内存中

与成员变量不同,局部变量不属于任何类或实例,因此局部变量总是保存在其所在方法的栈内存中

  • 如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;
  • 如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。

局部变量无须垃圾会后

栈内存中的变量无须系统垃圾回收,往往随方法或代码块的运行结束而结束。

局部变量的作用域

因此,局部变量的作用域是从初始化该变量开始,直到该方法或该代码块运行完成而结束。因为局部变量只保存基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小。

5.3 成员变量和局部变量 5.3.1 成员变量和局部变量是什么

Java语言中变量分成:成员变量局部变量两大类。
成员变量指的是在类里定义的变量;
**局部变量指的是在方法里定义的变量**。

变量命名规则

  • 从语法角度来看,只要是一个合法的标识符即可;
  • 但从程序可读性角度来看,应该是多个有意义的单词连缀而成,其中第一个单词首字母小写,后面每个单词首字母大写。

Java程序中的变量划分

这里有一张图片

成员变量分类

成员变量被分为类变量实例变量两种,定义成员变量时没有static修饰的就是实例变量,static修饰的就是类变量

成员变量的作用域

  • 类变量从该类的准备阶段起开始存在,直到系统完全销毁这个类时,类变量的作用域与这个类的生存范围相同;
  • 实例变量则从该类的实例被创建起开始存在,直到系统完全销毁这个实例时,实例变量的作用域与对应实例的生存范围相同。

如何访问成员变量

  • 只要类存在,程序就可以通过类.类变量的方式访问该类的类变量。
  • 只要实例存在,程序就可以通过实例.实例变量的方式访问该实例的实例变量。
  • 类变量也可以让该类的实例通过实例.类变量来访问。这与通过该类来修改类变量的结果完全相同。

成员变量的默认初始化

成员变量无须显式初始化,只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化,成员变量默认初始化时的赋值规则与数组动态初始化时数组元素的赋值规则完全相同。

局部变量分类

局部变量根据定义形式的不同,又可以被分为如下三种。

  1. 形参:在定义方法签名时定义的变量,形参的作用域在整个方法内有效
  2. 方法局部变量:在方法体内定义的局部变量,它的作用域是从定义该变量的地方生效,到该方法结束时失效。
  3. 代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到该代码块结束时失效。

局部变量中只有形参会默认初始化

与成员变量不同的是,局部变量除形参之外,都必须显式初始化。也就是说**,必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们**。
形参的作用域是整个方法体内有效,而且形参也无须显式初始化,形参的初始化在调用该方法时由系统完成,形参的值由方法的调用者负责指定

当通过类或对象调用某个方法时,系统会在该方法栈区内为所有的形参分配内存空间,并将实参的值赋给对应的形参,这就完成了形参的初始化。

合法的同名变量规则

在同一个类里,成员变量的作用范围是整个类内有效,一个类里不能定义两个同名的成员变量,即使一个是类变量,一个是实例变量也不行;
一个方法里不能定义两个同名的方法局部变量,方法局部变量形参也不能同名;
同一个方法中不同代码块内的代码块局部变量可以同名:如果先定义代码块局部变量,后定义方法局部变量,前面定义的代码块局部变量与后面定义的方法局部变量也可以同名。

局部变量可以和成员变量同名

同名局部变量覆盖成员变量

Java允许局部变量和成员变量同名,如果方法里的局部变量和成员变量同名,局部变量会覆盖成员变量。

通过this或类名来调用被覆盖的成员变量

如果需要在这个方法里引用被覆盖的成员变量,则可使用this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量

5.2.5 方法重载

Java允许同一个类里定义多个同名方法,只要形参列表不同就行。

方法重载定义

如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。

Java程序如何确定要调用哪个方法

Java程序中确定一个方法需要如下三个要素。

  1. 调用者,也就是方法的所属者,既可以是类,也可以是对象。
  2. 方法名,方法的标识
  3. 形参列表,当调用方法时,系统将会根据传入的实参列表匹配

方法重载的要求

方法重载的要求就是两同一不同:

  • 同一个类中
  • 方法名相同,
  • 参数列表不同。

至于方法的其他部分,如方法返回值类型修饰符等,与方法重载没有任何关系。

为什么返回值类型不能区别重载

Java调用方法时可以直接调用方法,没有变量类接收返回值,此时无法判断该调用哪个方法。

1
2
3
// 有int fun()方法和void fun()方法
int result=fun(); // 可以根据返回值判断调用的是int fun()方法
fun(); // 没有变量接收返回值,无法判断调用的是哪个fun方法

重载的方法有变参的情况

此时优先调用没有变参的方法,只有参数个数超过非变参方法的参数长度才会调用变参方法.
不推荐重载形参个数可变的方法,因为变参可以匹配一个或者多个参数,自然也就包含了非变参方法的功能了。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class OverloadVarargs {
public void test(String msg) {
System.out.println("调用 一个参数 的方法");
}

public void test(String... books) {
System.out.println("调用 可变参数 的方法");
}

public static void main(String[] args) {
OverloadVarargs olv = new OverloadVarargs();
// 0个参数,执行变参方法
olv.test();
// 2个参数,执行变参方法
olv.test("aa", "bb");
// 1个参数,执行变参方法
olv.test("aa");
// 数组参数,执行变参方法
olv.test(new String[] { "aa" });
}
}

运行结果

1
2
3
4
调用 可变参数 的方法
调用 可变参数 的方法
调用 一个参数 的方法
调用 可变参数 的方法

5.2.4 递归方法

一个方法体内调用该方法自身,被称为方法递归
方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。

递归必须在某个时刻方法的返回值是确定的,不再调用它本身,否则这种递归就变成了无穷递归,类似于死循环。因此递归一定要向已知方向递归
递归是非常有用的。例如希望遍历某个路径下的所有文件,但这个路径下文件夹的深度是未知的,那么就可以使用递归来实现这个需求。

5.2.3 形参个数可变的方法

JDK1.5之后,Java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参。如果在定义方法时,在最后一个形参的类型后增加三点(),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。

可变参数和数组参数的区别

数组形式的形参可以处于形参列表的任意位置,
**个数可变的形参只能处于形参列表的最后**。

变参总结

  1. 个数可变的形参只能处于形参列表的最后
  2. 一个方法中最多只能包含一个个数可变的形参
  3. 个数可变的形参本质就是一个数组类型的形参,因此调用包含个数可变形参的方法时,该个数可变的形参既可以传入多个参数,也可以传入一个数组
  4. 遍历个数可变的形参和遍历数组的方式一样

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Varargs {
// 定义了形参个数可变的方法
public static void test(int a, String... books) {

// 输出整数变量a的值
System.out.println(a);
System.out.println("----------------- 通过 forecah 遍历变参 --------------");
// books被当成数组处理
// foreach遍历数组
for (String tmp : books) {
System.out.println(tmp);
}
System.out.println("---------------- 通过 for 遍历变参 ----------------");
// 普通遍历数组的方式
for (int i = 0; i < books.length; i++) {
System.out.println(books[i]);
}
System.out.println("################################");
}

public static void main(String[] args) {
// 调用test方法,传入可变长度的形参
test(5, "Hello", "World");
// 给可变长度的形参传入数组
test(1, new String[] { "Hello", "World" });
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
5
----------------- 通过 forecah 遍历变参 --------------------------
Hello
World
---------------- 通过 for 遍历变参 ----------------------------
Hello
World
################################
1
----------------- 通过 forecah 遍历变参 --------------------------
Hello
World
---------------- 通过 for 遍历变参 ----------------------------
Hello
World
################################

可以看到遍历变参,就跟遍历数组一模一样

5.2.2 方法的参数传递机制

形参和实参

如果声明方法时包含了形参声明,则调用方法时必须给这些形参指定参数值,调用方法时实际传给形参的参数值也被称为实参

Java里只有值传递

Java里方法的参数传递方式只有一种:值传递

什么叫值传递

所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响
注意:传入方法的是实际参数值的复制品,不管方法中对这个复制品如何操作,实际参数值本身不会受到任何影响

Java对于引用类型的参数传递,一样采用的是值传递方式

5.2 方法详解

方法是类或对象的行为特征的抽象,方法是类或对象最重要的组成部分。但从功能上来看,方法完全类似于传统结构化程序设计里的函数。值得指出的是,Java里的方法不能独立存在,所有的方法都必须定义在类里。方法在逻辑上要么属于类,要么属于对象。

5.2.1 方法的所属性

方法和函数的不同

不论是从定义方法的语法来看,还是从方法的功能来看,都不难发现方法和函数之间的相似性。实际上,方法确实是由传统的函数发展而来的,方法与传统的函数有着显著不同:
在结构化编程语言里,函数是一等公民,整个软件由一个个的函数组成;
在面向对象编程语言里,类才是一等公民,整个系统由一个个的类组成。因此在Java语言里,方法不能独立存在,方法必须属于类或对象

方法属于类或者实例

因此,如果需要定义方法,则只能在类体内定义,不能独立定义一个方法。一旦将一个方法定义在某个类的类体内,如果这个方法使用了static修饰,则这个方法属于这个类,否则这个方法属于这个类的实例

Java语言是静态的。一个类定义完成后,只要不再重新编译这个类文件,该类和该类的对象所拥有的方法是固定的,永远都不会改变。

必须指明方法的调用者

因为Java里的方法不能独立存在,它必须属于一个类或一个对象,因此方法也不能像函数那样被独立执行,执行方法时必须使用类或对象来作为调用者,即所有方法都必须使用”类.方法”或”对象.方法”的形式来调用。

同一个类中可以隐藏调用者

同一个类的一个方法调用另外一个方法时,

  • 如果被调方法是普通方法,则默认使用this作为调用者;
  • 如果被调方法是静态方法,则默认使用类作为调用者。

也就是说,表面上看起来某些方法可以被独立执行,但实际上还是使用this或者来作为调用者。

方法的所属性

永远不要把方法当成独立存在的实体,方法只能作为类和对象的附属Java语言里方法的所属性主要体现在如下几个方面。

  1. 方法不能独立定义,方法只能在类体里定义。
  2. 方法要么属于该类本身,要么属于该类的一个对象。
  3. 永远不能独立执行方法,执行方法必须使用类或对象作为调用者

对象可以调用static方法

使用static修饰的方法属于这个类本身,使用static修饰的方法既可以使用类作为调用者来调用,也可以使用对象作为调用者来调用。但值得指出的是,因为使用static修饰的方法还是属于这个类的,因此使用该类的任何对象来调用这个方法时将会得到相同的执行结果,这是由于底层依然是使用这些实例所属的类作为调用者

类不能调用实例方法

没有static修饰的方法则属于该类的对象,不属于这个类本身。因此没有static修饰的方法只能使用对象作为调用者来调用,不能使用类作为调用者来调用。使用不同对象作为调用者来调用同一个普通方法,可能得到不同的结果。

5.1.4 对象的this引用

this关键字总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形:

  1. 在构造器中引用该构造器正在初始化的对象。
  2. 在方法中引用调用该方法的对象。

this关键字的作用

this关键字最大的作用就是让类中一个方法,访问该类里的另一个方法或实例变量。

省略this前缀

对象的一个成员直接调用该对象的另一个成员时,可以省略this前缀。所以大部分时候,一个方法访问该类中定义的其他方法、成员变量时加不加this前缀的效果是完全一样。

静态成员不能直接访问非静态成员

如果在static修饰的方法中使用this关键字,则这个关键字就无法指向合适的对象。
所以, static修饰的方法中不能使用this引用
所以static修饰的方法不能访问没有static修饰的普通成员
静态成员不能直接访问非静态成员

静态成员默认使用当前类作为调用者

如果调用static修饰的成员(包括方法、成员变量)时省略了前面的主调,那么默认使用该类作为主调;

实例成员默认使用this作为调用者

如果调用没有static修饰的成员(包括方法、成员变量)时省略了前面的主调,那么默认使用this作为主调。

在方法中使用this前缀可以调用被局部变量覆盖的成员变量
this可以作为方法的返回值