方法是类和对象的行为特征的抽象,Java中的方法与传统的函数非常相似又有着显著的不同,在结构化语言中函数是最小单位,整个软件由一个一个函数组成;在面向对象语言中,类是最小单位,整个系统由一个一个类组成,在java中方法不能独立存在,必须属于类或者对象。
如果声明方法的时候指定了形参,那么调用方法时必须给这些形参指定参数值,调用方法时实际传给形参的参数值也被称为实参,在java中,参数的传递方式只有一种,就是值传递,就是将实际参数值的副本(复制一份)传入方法内,而参数本身不会受到任何影响
public class PrimitiveTransferTest
{
public static void swap(int a, int b)
{
// 下面三行代码实现a、b变量的值交换。
// 定义一个临时变量来保存a变量的值
var tmp = a;
// 把b的值赋给a
a = b;
// 把临时变量tmp的值赋给b
b = tmp;
System.out.println("swap方法里,a的值是"
+ a + ";b的值是" + b);
}
public static void main(String[] args)
{
var a = 6;
var b = 9;
swap(a, b);
System.out.println("交换结束后,变量a的值是"
+ a + ";变量b的值是" + b);
}
}
引用类型的参数传递,仍旧是值传递方式
class DataWrap
{
int a;
int b;
}
public class ReferenceTransferTest
{
public static void swap(DataWrap dw)
{
// 下面三行代码实现dw的a、b两个成员变量的值交换。
// 定义一个临时变量来保存dw对象的a成员变量的值
var tmp = dw.a;
// 把dw对象的b成员变量值赋给a成员变量
dw.a = dw.b;
// 把临时变量tmp的值赋给dw对象的b成员变量
dw.b = tmp;
System.out.println("swap方法里,a成员变量的值是"
+ dw.a + ";b成员变量的值是" + dw.b);
}
public static void main(String[] args)
{
var dw = new DataWrap();
dw.a = 6;
dw.b = 9;
swap(dw);
System.out.println("交换结束后,a成员变量的值是"
+ dw.a + ";b成员变量的值是" + dw.b);
}
}
为了更好的证明main()方法中的dw和s’wswap()方法中的dw是两个变量,在swap()方法的最后一行添加代码
class DataWrap
{
int a;
int b;
}
public class ReferenceTransferTest
{
public static void swap(DataWrap dw)
{
// 下面三行代码实现dw的a、b两个成员变量的值交换。
// 定义一个临时变量来保存dw对象的a成员变量的值
var tmp = dw.a;
// 把dw对象的b成员变量值赋给a成员变量
dw.a = dw.b;
// 把临时变量tmp的值赋给dw对象的b成员变量
dw.b = tmp;
System.out.println("swap方法里,a成员变量的值是"
+ dw.a + ";b成员变量的值是" + dw.b);
// 把dw直接赋为null,让它不再指向任何有效地址。
dw = null;
}
public static void main(String[] args)
{
var dw = new DataWrap();
dw.a = 6;
dw.b = 9;
swap(dw);
System.out.println("交换结束后,a成员变量的值是"
+ dw.a + ";b成员变量的值是" + dw.b);
}
}
在JDK1.5之后,java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参,只需要在定义方法时,在最后一个形参的类型后增加三个点(…),则表明该形参可以接受多个参数值,多个参数值被当成数组传入
public class Varargs
{
// 定义了形参个数可变的方法
public static void test(int a, String... books)
{
// books被当成数组处理
for (var tmp : books)
{
System.out.println(tmp);
}
// 输出整数变量a的值
System.out.println(a);
}
public static void main(String[] args)
{
// 调用test方法
test(5, "疯狂Java讲义", "轻量级Java EE企业应用实战");
}
}
// 可变个数的形参定义方法
public static void test(int a, String... books);
// 数组形参定义方法
public static void test(int a, String[] books);
// 调用包含可变个数形参的函数
test(5, "davieyang", "alexyang", "Ethanyang");
// 调用包含数组形参的函数
test(5, new String[]{"davieyang", "alexyang", "Ethanyang"})
一个方法体内调用它自身,被称为方法递归,方法递归包含了一种隐式的循环,他会重复执行某段代码,但这种重复不需要执行循环控制
// f(0)=1, f(1)=4, f(n+2)=2*f(n+1)+f(n), 其中n是大于0的整数,求f(10)
public class Recursive
{
public static int fn(int n)
{
if (n == 0)
{
return 1;
}
else if (n == 1)
{
return 4;
}
else
{
// 方法中调用它自身,就是方法递归
return 2 * fn(n - 1) + fn(n - 2);
}
}
public static void main(String[] args)
{
// 输出fn(10)的结果
System.out.println(fn(10));
}
}
// f(20)=1,f(21)=4,f(n+2)=2*f(n+1)+f(n),其中n是大于0的整数,求f(10)
public class Recursive
{
public static int fn(int n)
{
if (n == 20)
{
return 1;
}
else if (n == 21)
{
return 4;
}
else
{
// 方法中调用它自身,就是方法递归
return fn(n + 2) - 2 * fn(n + 1);
}
}
public static void main(String[] args)
{
// 输出fn(10)的结果
System.out.println(fn(10));
}
}
fn(10)=fn(12)-2fn(11), 而fn(11)=fn(13)-2fn(12)…以此类推知道fn(19)=fn(21)-2*fn(20),也就是说fn(19)我们是能得到确切结果的
例如希望遍历某个文件路径,而文件路径的深度是未知的,我们可以定义一个方法将文件路径作为参数,该方法可以遍历该路径下所有的文件和文件路径,然后再调用自身来处理下层路径
Java允许同一个类里定义多个同名方法,只要形参列表不同即可,在同一个类中包含了了两个或者两个以上的同名方法,但形参不同,则称为方法重载
方法的重载聚焦的是方法名,俩同一不同,及同一个类里相同的方法名,不同的形参列表,至于方法的其他部分并不关心
public class Overload
{
// 下面定义了两个test()方法,但方法的形参列表不同
// 系统可以区分这两个方法,这种被称为方法重载
public void test()
{
System.out.println("无参数");
}
public void test(String msg)
{
System.out.println("重载的test方法 " + msg);
}
public static void main(String[] args)
{
var ol = new Overload();
// 调用test()时没有传入参数,因此系统调用上面没有参数的test()方法。
ol.test();
// 调用test()时传入了一个字符串参数,
// 因此系统调用上面带一个字符串参数的test()方法。
ol.test("hello");
}
}
Java并不能使用方法返回值类型作为区分方法重载的依据。
public class OverloadVarargs
{
public void test(String msg)
{
System.out.println("只有一个字符串参数的test方法 ");
}
// 因为前面已经有了一个test()方法,test()方法里有一个字符串参数。
// 此处的个数可变形参里不包含一个字符串参数的形式
public void test(String... books)
{
System.out.println("****形参个数可变的test方法****");
}
public static void main(String[] args)
{
var olv = new OverloadVarargs();
// 下面两次调用将执行第二个test()方法
olv.test();
olv.test("aa", "bb");
// 下面将执行第一个test()方法
olv.test("aa");
// 下面调用将执行第二个test()方法
olv.test(new String[] {"aa"});
}
}
在Java中根据变量定义的位置不同,可以将变量分为成员变量和局部变量,而成员变量和局部变量的运行机制存在较大的差异。
成员变量:是指在类里定义的变量;局部变量:是指在方法里定义的变量;而不论是什么变量都应该遵循相同的命名规则,且具备可读性,应该是多个有意义的单词连缀而成,其中第一个单词的首字母小写,后面每个单词的首字母大写。
class Person
{
// 定义一个实例变量
public String name;
// 定义一个类变量
public static int eyeNum;
}
public class PersonTest
{
public static void main(String[] args)
{
// 第一次主动使用Person类,该类自动初始化,则eyeNum变量开始起作用,输出0
System.out.println("Person的eyeNum类变量值:"
+ Person.eyeNum);
// 创建Person对象
var p = new Person();
// 通过Person对象的引用p来访问Person对象name实例变量
// 并通过实例访问eyeNum类变量
System.out.println("p变量的name变量值是:" + p.name
+ " p对象的eyeNum变量值是:" + p.eyeNum);
// 直接为name实例变量赋值
p.name = "孙悟空";
// 通过p访问eyeNum类变量,依然是访问Person的eyeNum类变量
p.eyeNum = 2;
// 再次通过Person对象来访问name实例变量和eyeNum类变量
System.out.println("p变量的name变量值是:" + p.name
+ " p对象的eyeNum变量值是:" + p.eyeNum);
// 前面通过p修改了Person的eyeNum,此处的Person.eyeNum将输出2
System.out.println("Person的eyeNum类变量值:" + Person.eyeNum);
var p2 = new Person();
// p2访问的eyeNum类变量依然引用Person类的,因此依然输出2
System.out.println("p2对象的eyeNum类变量值:" + p2.eyeNum);
}
}
注意:Java允许通过实例来访问static修饰的成员变量,这本身就有待商榷,当我们看到这种情形的时候都可以将其替换成类本身访问static成员变量,这样程序的可读性、明确性都大大提高
public class BlockTest
{
public static void main(String[] args)
{
{
// 定义一个代码块局部变量a
int a;
// 下面代码将出现错误,因为a变量还未初始化
// System.out.println("代码块局部变量a的值:" + a);
// 为a变量赋初始值,也就是进行初始化
a = 5;
System.out.println("代码块局部变量a的值:" + a);
}
// 下面试图访问的a变量并不存在
// System.out.println(a);
}
}
public class MethodLocalVariableTest
{
public static void main(String[] args)
{
// 定义一个方法局部变量a
int a;
// 下面代码将出现错误,因为a变量还未初始化
// System.out.println("方法局部变量a的值:" + a);
// 为a变量赋初始值,也就是进行初始化
a = 5;
System.out.println("方法局部变量a的值:" + a);
}
}
当通过类或对象调用方法时,系统会在该方法栈区内为所有的形参分配内存空间,并将实参的值赋给对应的形参,由系统完成了形参的初始化
public class VariableOverrideTest
{
// 定义一个name实例变量
private String name = "李刚";
// 定义一个price类变量
private static double price = 78.0;
// 主方法,程序的入口
public static void main(String[] args)
{
// 方法里的局部变量,局部变量覆盖成员变量
var price = 65;
// 直接访问price变量,将输出price局部变量的值:65
System.out.println(price);
// 使用类名作为price变量的限定,
// 将输出price类变量的值:78.0
System.out.println(VariableOverrideTest.price);
// 运行info方法
new VariableOverrideTest().info();
}
public void info()
{
// 方法里的局部变量,局部变量覆盖成员变量
var name = "孙悟空";
// 直接访问name变量,将输出name局部变量的值:"孙悟空"
System.out.println(name);
// 使用this来作为name变量的限定,
// 将输出name实例变量的值:"李刚"
System.out.println(this.name);
}
}
为了避免异常或者变量被覆盖的情形出现,减少代码调试成本,应避免变量重名,即便java提供了重名情况下的访问方法,但仍旧没必要增加调试成本
当系统加载类或者创建类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值
// 创建第一个类
Persion p1 = new Persion();
// 创建第二个类
Persion p2 = new Persion();
如果仅从程序的运行结果来看,大部分时候都可以直接使用类变量或者实例变量来解决问题,无需使用局部变量,然而如果考虑程序运行的性能那就不能这么简单处理,当我们定义一个成员变量时,成员变量将被放置在堆内存中,成员变量的作用域将扩大到类存在范围或者对象存在的范围,而这无疑增大了变量的生存时间,导致更大的内存开销,同时扩大了变量的作用域,不利于提高程序的内聚性
public class ScopeTest1
{
// 定义一个类成员变量作为循环变量
static int i;
public static void main(String[] args)
{
for (i = 0; i < 10; i++)
{
System.out.println("Hello");
}
}
}
public class ScopeTest2
{
public static void main(String[] args)
{
// 定义一个方法局部变量作为循环变量
int i;
for (i = 0; i < 10; i++)
{
System.out.println("Hello");
}
}
}
public class ScopeTest3
{
public static void main(String[] args)
{
// 定义一个代码块局部变量作为循环变量
for (var i = 0; i < 10; i++)
{
System.out.println("Hello");
}
}
}
如果有如下几个情形,则应考虑成员变量:
即时在程序中使用局部变量,也应该尽可能的缩小局部变量的作用范围,局部变量的作用范围越小,它在内存中驻留的时间越短,程序运行性能就越好。