4-Java 语句块、方法、递归

1. 语句块

语句块(有时叫做复合语句),是用花括号扩起的任意数量的简单 Java 语句。块确定了局部变量的作用域。**块中的程序代码,作为一个整体,是要被一起执行的。**块可以被嵌套在另一个块中,但是不能在两个嵌套的块内声明同名的变量。语句块可以使用外部的变量,而外部不能使用语句块中定义的变量,因为语句块中定义的变量作用域只限于语句块。

public static void main(String[] args) {
    int n;
    int a;
    {
        int k;
        int n; // 编译错误:不能重复定义变量 n
    } // 变量 k 的作用域到此为止
}

Java 中块的分类,目前不需要了解:

  • 局部块:在方法体中
  • 构造块:类中无 static,初始化对象信息
  • 静态块:类中加 static,加载一次,初始化类信息
  • 同步块:监视对象信息 synchronized(obj){}

2. 方法

2.1 方法的基本使用

方法就是一段用来完成特定功能的代码片段,类似于其它语言的函数。

方法用于定义该类或该类的实例的行为特征和功能实现。 方法是类和对象行为特征的抽象。方法很类似于面向过程中的函数。面向过程中,函数是最基本单位,整个程序由一个个函数调用组成。面向对象中,整个程序的基本单位是类,方法是从属于类和对象的。

  • 方法声明格式:
[修饰符1  修饰符2  …]  返回值类型  方法名(形式参数列表){
    Java语句;
    … … …
 }
  • 方法的调用方式:
对象名.方法名(实参列表)
  • 方法的详细说明:
    1. 形式参数:在方法声明时用于接收外界传入的数据。
    2. 实参:调用方法时实际传给方法的数据。
    3. 返回值:方法在执行完毕后返还给调用它的环境的数据。
    4. 返回值类型:事先约定的返回值的数据类型,如无返回值,必须显示指定为为 void。
/**
 * 测试方法的基本使用
 * @author IZJ
 *
 */
public class TestMethod {
    public static void main(String[] args) {
        // 通过对象调用普通方法
        TestMethod tm = new TestMethod();
        tm.printBasic();
        int sum = tm.add(1, 2, 3);
        System.out.println(sum);
    }

    void printBasic() {
        System.out.println("IZJ");
    }

    int add(int a, int b, int c) {
        return a + b + c; // return 两个作用:1.结束方法运行;2.返回结果
    }
}

注意事项

  1. 实参的数目、数据类型和次序必须和所调用的方法声明的形式参数列表匹配。
  2. return 语句终止方法的运行并指定要返回的数据。即两个作用:1. 结束方法运行;2. 返回结果。
  3. Java 中进行方法调用中传递参数时,遵循值传递的原则(传递的都是数据的副本)。
  4. 基本数据类型传递的是该数据值的 copy 值。
  5. 引用数据类型传递的是该对象引用的 copy 值,但指向的是同一个对象

2.2 方法重载

方法重载(overload)指同一个类中定义的多个方法之间的关系,满足下列条件的多个方法相互构成重载

  • 多个方法在同一个类中
  • 多个方法具有相同的方法名
  • 多个方法的参数不相同,类型不同或者数量不同

注意:

  • 重载的方法,实际是完全不同的方法,只是名称相同而已!
  • 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式
  • 重载仅针对同一个类中方法的名称与参数进行识别,与返回值无关,换句话说不能通过返回值来判定两个方法是否相互构成重载
  • 正确示范
public class MethodDemo {
    public static void fn(int a) {
        // 方法体
    }
    public static int fn(double a) {
        // 方法体
    }
}

public class MethodDemo {
    public static float fn(int a) {
        // 方法体
    }
    public static int fn(int a , int b) {
        // 方法体
    }
}
  • 错误示范
public class MethodDemo {
  public static void fn(int a) {
      // 方法体
    }
    public static int fn(int a) {   /* 错误原因:重载与返回值无关 */
      // 方法体
    }
}

public class MethodDemo01 {
    public static void fn(int a) {
        // 方法体
    }
} 
public class MethodDemo02 {
    public static int fn(double a) { /* 错误原因:这是两个类的两个 fn 方法 */
        // 方法体
    }
}

练习

需求:使用方法重载的思想,设计比较两个整数是否相同的方法,兼容全整数类型(byte,short,int,long)

public class MethodTest {
    public static void main(String[] args) {
        // 调用方法
        System.out.println(compare(10, 20));
        System.out.println(compare((byte) 10, (byte) 20));
        System.out.println(compare((short) 10, (short) 20));
        System.out.println(compare(10L, 20L));
    }

    // int
    public static boolean compare(int a, int b) {
        System.out.println("int");
        return a == b;
    }

    // byte
    public static boolean compare(byte a, byte b) {
        System.out.println("byte");
        return a == b;
    }

    // short
    public static boolean compare(short a, short b) {
        System.out.println("short");
        return a == b;
    }

    // long
    public static boolean compare(long a, long b) {
        System.out.println("long");
        return a == b;
    }
}

2.3 方法的参数传递

1. 方法参数传递基本类型

  • 测试代码:
public class ArgsDemo01 {
    public static void main(String[] args) {
        int number = 100;
        System.out.println("调用change方法前:" + number); // 100
        change(number);
        System.out.println("调用change方法后:" + number); // 100
    }

    public static void change(int number) {
        number = 200;
    }
}
  • 结论:

    • 基本数据类型的参数,形式参数的改变,不影响实际参数
  • 结论依据:

    • 每个方法在栈内存中,都会有独立的栈空间,方法运行结束后就会弹栈消失

      方法传参-基本数据类型

2. 方法参数传递引用类型

  • 测试代码:
public class ArgsDemo02 {
    public static void main(String[] args) {
        int[] arr = {10, 20, 30};
        System.out.println("调用change方法前:" + arr[1]);
        change(arr);
        System.out.println("调用change方法后:" + arr[1]);
    }

    public static void change(int[] arr) {
        arr[1] = 200;
    }
}
  • 结论:

    • 对于引用类型的参数,形式参数的改变,影响实际参数的值
  • 结论依据:

    • 引用数据类型的传参,传入的是地址值,内存中会造成两个引用指向同一个内存的效果,所以即使方法弹栈,堆内存中的数据也已经是改变后的结果

      方法传参-引用数据类型

3. 递归

递归是一种常见的解决问题的方法,即把问题逐渐简单化。递归的基本思想就是“自己调用自己”,一个使用递归技术的方法将会直接或者间接的调用自己。

利用递归可以用简单的程序来解决一些复杂的问题。比如:斐波那契数列的计算、汉诺塔、快排等问题。

递归结构包括两个部分:

  1. 定义递归头。解答:什么时候不调用自身方法。如果没有头,将陷入死循环,也就是递归的结束条件。
  2. 递归体。解答:什么时候需要调用自身方法。

例1:阶乘

/* 阶乘 */
static long factorial(int num) {
  if(num == 1) {
    return 1;
  }
  return num * factorial(num - 1);
}

递归的缺陷

简单的程序是递归的优点之一。但是递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。

public static void main(String[] args) {
    long d1 = System.currentTimeMillis();  
    System.out.printf("%d阶乘的结果:%s%n", 50, factorial(50));
    long d2 = System.currentTimeMillis();
    System.out.printf("递归费时:%s%n", d2-d1);  // 耗时:19ms

    long d3 = System.currentTimeMillis();  
    System.out.printf("%d阶乘的结果:%s%n", 50, factorial2(50));
    long d4 = System.currentTimeMillis();
    System.out.printf("循环费时:%s%n", d4-d3);  // 耗时:0ms
}

/* 阶乘 */
static long factorial(int num) {
    if(num == 1) {
        return 1;
    }
    return num * factorial(num - 1);
}

static long factorial2(int num) {
    long re = 1;
    for(int i = num; i >= 2; i--) {
        re *= i;
    }
    return re;
}

注意事项

任何能用递归解决的问题也能使用迭代解决。当递归方法可以更加自然地反映问题,并且易于理解和调试,并且不强调效率问题时,可以采用递归;

在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。