Java学习之路08---方法

架构图

方法简介

甚麽是方法

方法就是用来解决特殊需求的一个功能模块、程序语句的集合,把需要重复使用的功能包装到方法里面。例如我们最常使用的System.out.println(),就是调用System类中实例化的对象out中的方法println()

在java中方法一定存在於某个类之中,这点跟C/C++, Python有点不同

方法格式

一个方法的编写可以参考下列格式:

修饰符 返回值类型 方法名(数据类型1 参数名1, 数据类型2 参数名2){

    // 方法主体

	return 返回值;
}
  1. 修饰符
    • 告诉compiler如何调用该方法(option)
    • 例如封装性的public, private,静态方法的static
  2. 返回类型
    • 决定方法执行完成後的返回值数据类型
    • 例如基本数据类型与引用数据类型
    • void类型则无返回值
  3. 方法名
    • 方法的名称,通常有约定俗成的规范
  4. 参数列表
    • 调用该方法时需要传入的数据类型与数量
    • 例如基本数据类型与引用数据类型
    • 也可以不传入任何参数
  5. 方法体
    • 方法的程序逻辑区段
  6. 返回值
    • 方法的返回值
    • 若返回类型为void,则可以不填

举例说明,下面是一个真实的方法案例:

方法的命名

方法的命名通常与变数一样,采用小驼峰法也就是说当方法名为单一英文单字时要求小写,若是两个英文单字的组合,第一个单字需要小写,第二个英文单字开头大写

另外两种英文单字的组合通常是:

  1. 动词缩写
    • 例如get, set, update, remove
    • 目的: 为了完成指定动作
  2. 以动词 + 名词方式命名
    • 例如ensureCapacity, displayMenu, initializaTable
    • 目的: 为了完成指定动作,并指名操作目标
  3. 助动词 + 动词/名词
    • 例如shouldMigrate, canInput, mayDump
    • 目的: 通常涉及到使用时机的动作
  4. 使用be动词开头 + 名词/形容词
    • 例如isValid, isOpen, isFinish
    • 目的: 用来判断当前的行为是否合法
  5. 用来表达长度、容量、大小可以使用名词
    • 例如length, size, width
    • 目的: 用来获取、运算指定参数属性

不过说到底这些命名规则共同目标都是为了让开发者能快速了解方法的作用,太简洁不易理解;太详细容易陷入冗长,该怎麽拿捏还是要看团队的共识,如果想更了解命名规则的话可以参考这篇

方法的调用

方法的调用主要取决於是否有返回值,以及方法位置

若方法调用具有返回值,则该方法实际上被视为一个数值,当控制权藉由方法返回给调用语句时,通常会将返回值赋值给一个左值变数(也可以选择不进行赋值)

返回值类型void则代表无返回值类型,调用时直接使用,不需要进行赋值配置,因为它实际上就是一条语句

另外主方法main也是一个返回类型void的方法,本质上它与其他方法没有任何差异,只不过它是由JVM调用,代表程序的入口,main方法结束後整个程序便结束了,所以不需要回传任何数值

下面程序范例介绍方法的调用方式:

public class Main
{
    public static int testOne(int num){
        return num + 10;
    }
    
    public static void testTwo(int num){
        num++;
    }
    
	public static void main(String[] args) {
	    int sum=0;

	    sum = testOne(sum); // 有返回值
		testOne(sum); // 有返回值
	    testTwo(sum); // 无返回值
	}
}

调用的方法若位於不同的类当中,则需要先创建对象,实例化後再进行调用。相同类中的方法可以直接调用,但须要注意static方法只能调用同类中的static方法(後面章节会提到)

主方法中若想调用其他类中的方法,需要先实例化操作:

public class Main
{
    public static void read(){
        System.out.println("class Main read");
    }
    
	public static void main(String[] args) {
		Student stu = new Student();
		stu.read();
		read();
	}
}

Student类中的read方法:

public class Student{
    public void read(){
        System.out.println("class Student read");
    }
}

方法的结束
有开始必定有结束,一个方法将控制权返回给调用方通常会满足下面三个条件之一:

  1. 方法执行完所有的语句
  2. 执行return
  3. 抛出异常

scope

scope就是在程序执行区段中一个变数可以被引用的范围。一个变数的作用范围可以藉由修饰符与所处程序码区段,在编译时决定

而在方法中我们使用的变数类型为局部变数,它储存在栈空间中(stack)。一个方法内的局部变数在调用方法时生成,返回时销毁。在方法体内我们可以使用{}另辟一个程序码区块,不同区块中可以宣告同名的局部变数,因为他们的scope范围各自在退出括号後结束,因此不冲突,例如:

方法中在执行的过程中维护了一个称为局部变数表的表格,如果该变数的执行区段一结束,该变数就会从该表中删除。相反的,若一个变数还未被删除,就不允许再宣告一个同名变数

在方法test中宣告的temp还未被释放就宣告一个同名的变数,会产生重复宣告的错误,例如下面的例子:

方法多载

多载简介

多载(overloading)就是一个类当中具有多个同名的方法,但会具有以下差异:

  1. 传入的参数类型不同
  2. 参数列表数量不同
  3. 参数顺序不同

换句话说就是两个方法的方法签名(Method Signature)不同

多载方法适用於功能相同但是输入参数不同的情况。也就是说会调用哪个多载方法,需要看使用者传入的引数(Argument)决定,多载能够让开发者不用为了参数不同再去创建一个具有相似功能但名称不同的方法

例如以下程序码范例,主方法调用的addSum方法会依照上述所说的三种差异而使用不同的多载方法:

public class Main{
    public static int addSum(int num1, int num2){
		System.out.print("方法一: ");
    	return num1 + num2;
    }
    
    public static int addSum(int num1, int num2, int num3){
		System.out.print("方法二: ");
    	return num1 + num2 + num3;
    }
    					   
    public static double addSum(double num1, double num2, double num3){
		System.out.print("方法三: ");
    	return num1 + num2 + num3;
    }
    
    public static double addSum(double num1, int num2){
		System.out.print("方法四: ");
    	return num1 + num2;
    }
    
    public static double addSum(int num1, double num2){
		System.out.print("方法五: ");
    	return num1 + num2;
    }
    
	public static void main(String[] args) {
		System.out.println("sum = " + addSum(3, 1));

		/*参数数量不同*/
		System.out.println("sum = " + addSum(3.5, 1.2, 10.0));

		/*参数类型不同*/
		System.out.println("sum = " + addSum(3, 1, 10));

		/*顺序不同*/
		System.out.println("sum = " + addSum(3, 1.2));
		System.out.println("sum = " + addSum(3.5, 1));
	}
}

输出结果:

方法一: sum = 4
方法三: sum = 14.7
方法二: sum = 14
方法五: sum = 4.2
方法四: sum = 4.5

编写多载方法的注意事项

  1. 方法名一定要一样
  2. 主要不同是参数类型以及参数数量
  3. 只有参数名不同不算是多载
  4. 只有返回数据类型不同不算多载
  5. 返回值可以不同,也包括void无返回值类型

下面这段程序码会依照使用者传入引数来决定各种形状的面积,刚好就是一种无返回值的多载实现:

public class Main
{
	public void area(double radius){
	    System.out.println("圆的面积为:" + (radius * radius * Math.PI));
	}

	public double area(double radius){
	    System.out.println("圆的面积为:" + (radius * radius * Math.PI));
		return radius * radius * Math.PI
	}
    
    public void area(float length, float width){
        System.out.println("长方形面积为:" + (length * width));
    }

	public void area(double top, double bottom, double high){
        System.out.println("梯形面积为:"+(top + bottom)*high/2);
	}

	public static void main(String[] args) {
		Main m = new Main();
		double r=3.9;
		float l=4, w=7;
		double t=3.0, b=11.2, h=4.3;

		m.area(r);
		m.area(l, w);
		m.area(t, b, h);
	}
}

但是就如同我们上面提到的,假若两个多载方法如果只有返回数据类型不同,不能算是多载的一种

这时候编译会产生error: method 方法签名 is already defined in class 类,代表系统将这两个方法视为同一个方法,发生重定义错误。例如我们在上一个程序码范例中添加一个圆面积的多载方法:

public class Main
{
	public void area(double radius){
	    System.out.println("圆的面积为:" + (radius * radius * Math.PI));
	}

	/*改变返回类型*/
	public double area(double radius){
	    System.out.println("圆的面积为:" + (radius * radius * Math.PI));
		return radius * radius * Math.PI;
	}
    
    public void area(float length, float width){
        System.out.println("长方形面积为:" + (length * width));
    }

	public void area(double top, double bottom, double high){
        System.out.println("梯形面积为:"+(top + bottom)*high/2);
	}

	public static void main(String[] args) {
		Main m = new Main();
		double r=3.9;
		float l=4, w=7;
		double t=3.0, b=11.2, h=4.3;

		m.area(r);
		m.area(l, w);
		m.area(t, b, h);
	}
}

输出结果:

Main.java:15: error: method area(double) is already defined in class Main
	public double area(double radius){
	              ^
1 error

至於void与其他数据类型返回值可不可以多载使用?答案当然是可以,只不过多载的中心思想是功能类似。举上面的void返回类型方法来说,方法就是透过打印输出来实现,如果我们要多加一个float返回类型的方法(返回面积),那这个多载方法事实上已经与其他同名的方法功能上有所不同了,这与多载的核心思想背道而驰

引数无匹配状况

有时候调用方法传入的引数不一定与类中的所有多载方法匹配,一般来说编译时会发出incompatible types的错误。但如果传入引数可以类型转换成符合多载方法的参数类型,那麽编译器还是可以正常调用方法。举例来说:

public class Main
{
    public static void test(float num){
        System.out.println("float类型方法");
    }
    
    public static void test(double num){
        System.out.println("double类型方法");
    }

	public static void main(String[] args) {
	    int i=10;
	    byte b=5; 
	    
	    test(i); // 调用方法签名为test(float)的方法
	    test(b); // 调用方法签名为test(float)的方法
	}
}

输出结果:

float类型方法
float类型方法

透过上述例子,我们发现在方法调用时会依照以下几个步骤找寻目标多载方法:

  1. 会依照完整参数类型来寻找
  2. 会依照数据类型转换表方式寻找符合更高阶的参数类型
  3. 能用来完整表示传入引数的最小长度参数类型

我们再把上述程序码加上几个参数类型与多载方法:

public class Main
{
	/*新增test(int)方法*/
    public static void test(int num){
        System.out.println("int类型方法");
    }
    
    public static void test(float num){
        System.out.println("float类型方法");

    }
    
    public static void test(double num){
        System.out.println("double类型方法");
    }

	public static void main(String[] args) {
	    int i=10;
	    byte b=5;
	    long l=100l; // long类型
	    
	    test(i); // 找到符合参数列表方法
	    test(b); // 类型转换成int类型
	    test(l); // 会调用test(float)而不是test(double)
	}
}

输出结果:

int类型方法
int类型方法
float类型方法

呼叫

在java中当呼叫方法将引数传递给参数时主要有两种方式:

  1. call by value(传值呼叫)
  2. call by reference(传参考呼叫)

为了理解这两种呼叫方法,我们需要特别观察实际参数(Actual Parameter)与形式参数(Formal Parameter)之间的关系和变化,例如下方范例程序码的num1就是实际参数,而num2为形式参数

换句话说,我们要观察一个变数值会不会因为调用方法而改变

public class Main
{
    public static void test(int num2){ // 形式参数
        num2 += 10;
        System.out.println("In method num = " + num2);
    }
    
	public static void main(String[] args) {
		int num1=51; // 实际参数
        System.out.println("Before num = " + num1);
		test(num);
        System.out.println("Afer num = " + num1);
	}
}

传值呼叫

当方法调用时,不管形式参数如何变动最後都不会影响到实际参数的值,这种呼叫方式称为传值呼叫。通常使用基本数据类型当作引数调用方法,都是使用传值呼叫

其实以下程序码范例就是一个传值呼叫案例,num1的值在调用完後依然保持不变:

public class Main
{
    public static void test(int num2){
        num2 += 10;
        System.out.println("In method num = " + num2);
    }
    
	public static void main(String[] args) {
		int num1=51;
        System.out.println("Before num = " + num1);
		test(num);
        System.out.println("Afer num = " + num1);
	}
}

输出结果:

Before num = 51
In method num = 61
Afer num = 51

我们先假设num1储存在地址0x0001的位置,当num1作为引数传入给方法test时,该方法会在栈空间中(stack)创建一个帧(frame),这个帧包含了参数值、程序码区段、局部变数与返回地址等

其中形式参数num2(地址0x0020)会储存num1复制过来的变数值,也就是说形式参数与实际参数虽然储存的数值相同但它们分别位於栈空间中的不同位置。而方法一旦返回,原先创建的记忆体空间就会自动被销毁,所以到头来num2值的变化仅存在於方法的scope当中,不会对num1有任何影响

传参考呼叫

当方法调用时,形式参数经过修改最後会影响到实际参数的值,这种呼叫方式称为传参考呼叫。通常以物件、阵列、字串等引用数据类型的变数当作引数调用方法,都是使用传参考呼叫

例如以下程序码范例,阵列arr经过方法array调用後,其元素会被改变:

public class Main
{
	public void print(int []arr){
        System.out.print("arr = ");
		for(int i=0; i<arr.length; i++){
		    System.out.print(arr[i] + " ");
		}
        System.out.println();
	}
	
	public void array(int []arr, int num){
        arr[num] = 100;
        print(arr);
	}
	
	public static void main(String[] args) {
		Main ad = new Main();
		int []arr = {1,2,3,4,5,6,7,8,9};
		System.out.print("Before ");
		ad.print(arr);
		System.out.print("In method  ");
	    ad.array(arr, 0); // 将index为0的元素改为100
		System.out.print("After ");
		ad.print(arr);
	}
}

输出结果:

Before arr = 1 2 3 4 5 6 7 8 9 
In method  arr = 100 2 3 4 5 6 7 8 9 
After arr = 100 2 3 4 5 6 7 8 9 

会产生这种状况是因为传入的arr储存的其实是一个参考物件的地址。经过Main ad = new Main();语句在堆空间中(heap)生成对象,然後将arr参考到该对象

因此我们调用方法时会将arr的内容,也就是该对象的地址复制给方法参数中的参考变数arr。所以不管是主方法中的arr,抑或是调用方法中的arr,都是参考到相同一个对象。这时其中一方修改该对象的内容,不管最终这个参考变数被销毁,对象的内容都会被保留下来

传递参数

一般来说调用方法时,我们会依照方法的参数列表依序传入引数。如同之前介绍,若是存在多载,则会依据参数列表决定多载方法。方法参数一般来说需要符合方法签名定义的格式。不过也是有例外的,例如我们提过的类型转换

命令行参数

不要忘了入口main也是一个方法,它的返回数据类型为void,参数列表为一个字串阵列的方法。启动程序时JVM会自动寻找main作为程序起点,一般情况下都是传入一个空的字串物件

但为了了解main方法的实现,我们编写下面的程序来捕捉传入参数:

public static void main(String[] args) {
	for(int i=0; i<args.length; i++)
		System.out.println("args[" + i + "]= " + args[i]);
}

使用命令行方法传入参数
首先我们将刚刚编写的程序储存成.java档,然後开启任何型式的命令行介面,先打上javac Test.java将.java档编译成.class文件

接者输入java Test 参数1 参数2 参数3 ...执行.class文件,其中每个字串阵列元素以空白键相隔。输入後查看运行结果,参数确实传入给main方法

使用eclipse传入参数
但话又说回来,IDE才是最普遍的使用场景,我们这里演示使用eclipse来为main方法传入参数

首先我先随便开起一个package,在编辑页面点击右键 → Run As → Run Configurations:

选择Arguments选单并在Program arguments栏位中依序填入参数,每个字串阵列以enter键区隔,设置完成後按下右下角的Run:

输出结果与使用命令行相同:

不定长度引数

什麽是不定长度引数
不定参数引数(Variable-length Argument)是编译器的语法糖,它允许调用方每次传入不同的参数数量,其中也包括传入0个参数。不定长度引数在参数列表中使用数据类型 ... 变数名来表示,调用方法时需传入相同数据类型的引数,当然也可以传入引用数据类型数据

我们常用的println方法也是一种常见的不定长度引数的应用:

/*println就是一种不定长度引数的实现*/
System.out.println(a);
System.out.println(a + " " + b);
System.out.println(a + " " + b + " " + c);

其实将型式参数数据类型 ... 变数名展开後,它就是一个一维阵列。而我们在调用该方法时,同样也是向方法传入一个一维阵列。这也代表我们可向使用不定长度引数的方法传入具有相同数据类型的阵列

public class Main{
    public static void test (int ... num){
        for(int i=0; i<num.length; i++){
            System.out.println("num[" + i + "] = " + num[i]);
        }
    }
  
    public static void main (String[] args){
        int[] arr = {1,2,3,4,5};
        
		test(5,4,3,2,1);
		System.out.println();
        test(arr);// 传入阵列
    }
}

输出结果:

num[0] = 5
num[1] = 4
num[2] = 3
num[3] = 2
num[4] = 1

num[0] = 1
num[1] = 2
num[2] = 3
num[3] = 4
num[4] = 5

使用格式注意

  1. 当与其他参数搭配时,一定要将不定长度引数放在最後面
public static void test (float f, String[] str, int ... num) // 正确
public static void test (float f str, int ... num, String[]) // 错误
  1. 不能同时使用两个不定长度引数
public static void test (int ... num, String ... str) // 不允许同时使用两个可变长度引数
  1. 数据类型均相同
    • 方法设定的不定长度引数数据类型是甚麽,调用方法时就应该传入相同数据类型的引数
public class Main{
    public static void test (float ... num){
        for(int i=0; i<num.length; i++){
            System.out.println("num[" + i + "] = " + num[i]);
        }
    }
  
    public static void main (String[] args){      
        test(5.0f,4.0f,3.0f,2.0f,1.0f); // 均传入float,可以视为一个float类型的阵列
    }
}
  1. 阵列可做为参数传给不定长度引数,但相反不成立

多载特性
不定长度引数通常配合多载一起使用,不过通常会伴随模糊性产生。这主要是因为多载时不定长度引数会与阵列视为相同数据类型的参数,因此会报错,例如以下程序码:

public class Main{
    public static void test (int ... num){
        for(int i=0; i<num.length; i++){
            System.out.println("num[" + i + "] = " + num[i]);
        }
    }
    
    public static void test (int[] num){
        for(int i: num){
            System.out.println("num = " + i);
        }
    }
    
    public static void main (String[] args){
        test(5,4,3);
    }
}

输出结果:

Main.java:22: error: cannot declare both test(int[]) and test(int...) in Main
    public static void test (int[] num){
                       ^
1 error

访问优先级
不定长度引数是最後被访问的,若有其他方法符合调用时的引数数量与类型,则优先访问,例如以下程序码:

public class Main{
    public static void test (int ... num){
        System.out.println("Variable-length Argument");
        for(int i=0; i<num.length; i++){
            System.out.println("num[" + i + "] = " + num[i]);
        }
    }
    
    public static void test (int num1, int num2, int num3){
        System.out.println("regular method");
        System.out.println("num1 = " + num1);
        System.out.println("num2 = " + num2);
        System.out.println("num3 = " + num3);        
    }
    
    public static void main (String[] args){
        test(5,4,3);
    }
}

输出结果:

regular method
num1 = 5
num2 = 4
num3 = 3

转型问题
使用不定长度引数时,若传入引数找不到完全相同数据类型的方法,可以退而求其次,使用转型来匹配方法,不过需要满足类型转换表的转换顺序

举以下程序码为例,我们在主方法中调用方法test,传入引数类型为char,但实际类中并没有参数类型为char的方法,所以编译器会将引数经过类型转换来匹配方法

这个范例中数据类型charintdouble依序转换,因此成功调用不定参数类型为double的方法。另外,若是不定长度引数与其他参数组合一同出现时,类型转换依然成立

public class Main {
	
	public void test(double ... num){
    	System.out.println("方法1");
        for(double n:num){
            System.out.print(n + " ");
        }
        System.out.println();
    }
    
    public void test(char[] arr, double ... words){ // 不定长度引数与其他参数组合一起出现
    	System.out.println("方法2");
        for(char n:arr){
            System.out.print(n + " ");
        }
        System.out.println();
        for(double n:words){
            System.out.print(n + " ");
        }
    }
    
	public static void main(String[] args) {
		Main md = new Main();
		char[] arr = {'a', 'p', 'p', 'l', 'e'};
		
		md.test('b','a'); // 转换为double类型
		System.out.println("===============");
		md.test(arr, 'b','a'); // 转换为char[]与double类型
	}
}

输出结果:

方法1
98.0 97.0 
===============
方法2
a p p l e 
98.0 97.0 

若引数是传入阵列类型则不适用类型转换,例如方法中的不定长度引数为float类型,但阵列传入int类型,编译器会报错int[]无法转换成float[]类型,因为引用数据类型之间无法使用类型转换

例如刚刚的范例程序码,我们使用字元阵列调用test,看是否能达到不定长度引数的效果:

public class Main {
	
	public void test(double ... num){
    	System.out.println("方法1");
        for(double n:num){
            System.out.print(n + " ");
        }
        System.out.println();
    }

	public static void main(String[] args) {
		Main md = new Main();
		char[] arr = {'a', 'p', 'p', 'l', 'e'};
		
		md.test(arr);
	}
}

输出结果:

Main.java:34: error: method test in class Main cannot be applied to given types;
		md.test(arr);
		  ^
  required: double[]
  found: char[]
  reason: varargs mismatch; char[] cannot be converted to double
1 error

<<:  django新手村10-----关於注册

>>:  django新手村11-----缓存

[Day16]Basically Speaking

上一篇介绍了Prime Gap,Prime number也就是质数的意思,所以这题也是要我们找质数之...

Kotlin Android 第19天,从 0 到 ML - RecyclerView 动态列表

前言: RecyclerView 可以轻松高效地显示大量数据。 RecyclerView回收这些单独...

找LeetCode上简单的题目来撑过30天啦(DAY27)

今天上班搞一整天,只解出一个BUG,结果下班以後脑袋比较灵光? 总之今天是顺利解出来了 题号:129...

Day19 PHP的常用函数-4:文件处理函数、Json

文件处理函数 fopen(): 打开文件或者 URL fclose(): 关闭一个已打开的文件指针 ...

Day 5:认识CSS+CSS tag

在上一篇,我们学会如何用HTML写出'Hello World!',而这一篇,我将会教大家怎麽帮HTM...