day08_面向对象(中)

成员变量是用来存储对象的数据信息的,那么如何表示对象的行为功能呢?就要通过方法来实现

方法初识

什么是方法(method、函数)
  • 方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中 也称为函数或过程。
  • 将功能封装为方法的目的是,可以实现代码重用,简化代码
  • Java里的方法不能独立存在,所有的方法必须定义在类里。

方法的使用原则:

  • 必须先声明后使用。类,变量,方法等都要先声明后使用
  • 方法不调用不执行,调用一次执行一次,每次调用会在栈中有一个入栈动作,即给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值,当方法执行结束后,会释放该内存,称为出栈,如果方法有返回值,就会把结果返回调用处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令。

方法定义的语法:

day08_面向对象(中)格式详解:

  • 修饰符:项是可选项,不是必须的。控制方法的访问权限,有public,缺省,private, protected等。还有static、final、abstract 也可以来修饰的方法....
  • 返回值类型:此项可以是 java 语言当中任何一种数据类型,包括基本数据类型,也包括所有的引用数据类型。当然,如果一个方法执行结束之后不准备返回任何数据,则返回值类型必须写 void。返回值类型例如:byte,short,int,long,float,double,boolean,char,String,void 等。方法名:此项需要是合法的标识符,开发规范中要求方法名首字母小写,后面每个单 词首字母大写,遵循驼峰命名方式,见名知意,例如:login、getUsername、findAllUser 等。
    • 有返回值,则必须在方法声明时,指定返回值的类型。同时,方法中,需要使用return关键字来返回指定类型的变量或常量:“return 数据”。必须保证任何情况下至少有一个return执行。并且要求该返回值结果的类型与声明的返回值类型一致或兼容(可以发生自动转换)。表示方法运行的结果的数据类型,方法执行后将结果返回到调用者
    • 没有返回值,则方法声明时,使用void来表示。通常,没有返回值的方法中,就不需要 使用return.但是,如果使用的话,只能“return;”表示结束此方法的意思。
  • 形式参数列表:此项又被称为形参,其实每一个形参都是“局部变量”, 形参的个数为 0~N 个,如果是多个参数,则采用半角“,”进行分隔,形参中起决定性作用的 是参数的数据类型,参数名就是变量名,变量名是可以修改的,也就是说(int a , int b)也可以写 成(int x , int y)。方法内部需要用到其他方法中的数据,需要通过参数传递的形式将数据传递过来,可以是基本数据类型、引用数据类型、也可以没有参数,什么都不写 。
  • 方法体:由一对儿大括号括起来,在形参的后面,这个大括号当中的是实现功能的核 心代码,方法体由 java 语句构成,方法体当中的代码只能遵循自上而下的顺序依次逐行执行, 不能跳行执行,核心代码在执行过程中如果需要外部提供数据,则通过形参进行获取。
return关键字的使用:
  • 使用范围:使用在方法体中。如果返回值类型不是void,方法体中必须保证一定有return 返回值;语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。
  • 作用:① 结束方法 ② 针对于有返回值类型的方法,使用"return 数据"方法返回所要的数据。只能返回一个结果,要想带回多个结果可以返回一个容器。
  • 注意点:return关键字后面不可以声明执行语句。否则会报错:Unreachable code

成员方法分为两类:

  • 实例方法:没有static修饰的方法,必须通过实例对象来调用。
  • 静态方法:有static修饰的方法,也叫类方法,可以由类名来调用。

在其他类中调用静态方法

day08_面向对象(中)day08_面向对象(中)

在其他类中调用实例方法

day08_面向对象(中)

在本类中访问本类的成员变量和成员方法 

  • 可以直接写上变量或者方法名称来使用。唯一例外静态方法中不能直接访问本类的非静态的成员变量和成员方法,可以通过创建对象的方法间接引用。静态不能访问非静态的的
  • 本质原因是:静态先于非静态加载。

需要注意的是,方法在调用的时候,实际传给这个方法的数据被称为实际参数列表,简称 实参,java 语法中有这样的规定:实参和形参必须一一对应,所谓的一一对应就是,个数要一 样,数据类型要对应相同。例如:实参(100 , 200)对应的形参(int x , int y),如果不是一一对应 则编译器就会报错。当然也可能会存在自动类型转换,例如:实参(100 , 200)也可以传递给这 样的形参(long a , long b)

调用方法时,如果方法有返回值,可以接受或处理返回值结果,当然也可以不接收,那么此时返回值就丢失了。如果方法的返回值类型是void,不需要也不能接收和处理返回值结果。

形参:在定义方法时方法名后面括号中声明的变量称为形式参数(简称形参)即形参出现在方法定义时。

  • 位置:在方法的声明处
  • 语法:类型 标识符
  • 作用
    • 规定方法调用处传递的参数的数据类型必须匹配。当然也可能会存在自动类型转换
    • 接收方法调用处传递的参数

实参:调用方法时方法名后面括号中的使用的值/变量/表达式称为实际参数(简称实参)即实参出现在方法调用时。

  • 位置:方法调用处
  • 作用:传递实际的值

静态方法代码示例

package demo.method;

import java.util.Scanner;
/*
输入一个数字,判断是不是质数
 */
public class Demo02 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("亲输入数字");
        int number = scanner.nextInt();
        if (isZhiShu(number)){
            System.out.println("质数");
        }else {
            System.out.println("不是质数");
        }
    }

    //判断是不是质数
    private static boolean isZhiShu(int i) {
        //标记,true 为质数
        boolean flag = true;
        for (int j = 2; j < i; j++) {
            //判断是不是质数
            if (i % j == 0) {
                //不是质数
                flag = false;
                return flag;
            }
        }

        return flag;
    }


}

实例方法代码示例

package demo.method;

public class Circle {
    double radius = 2;

    //写一个方法,可以返回“圆对象”的详细信息
    String getDetailInfo() {
        return "半径:" + radius + ",面积:" + area() + ",周长:" + perimeter();
    }

    //写一个方法,可以返回“圆对象”的面积
    double area() {
        return Math.PI * radius * radius;
    }

    //写一个方法,可以返回“圆对象”的周长
    double perimeter() {
        return 2 * Math.PI * radius;
    }

}

class CircleTest {
    public static void main(String[] args) {
        //创建对象
        Circle circle = new Circle();
        //调用实例方法
        circle.area();
        circle.perimeter();
        circle.getDetailInfo();
        //访问属性
        double radius = circle.radius;
        System.out.println(radius);
    }


}

注 意:

  • 方法被调用一次,就会执行一次。方法的执行顺序和声明顺序无关,只于调用顺序有关,谁先调用,就谁先执行谁。
  • 没有具体返回值的情况,返回值类型用关键字void表示,那么方法体中可 以不必使用return语句。如果使用,仅用来结束方法。
  • 定义方法时,方法的结果有返回值,则会返回给调用者,交由调用者处理。
  • 方法中只能调用方法或属性
  • 必须先声明后使用。类,变量,方法等都要先声明后使用
  • 方法之间是平级关系,不可以在方法内部定义方法。
  • 方法中可以调用方法。被调用的方法执行完毕后,就会回到方法调用处,继续执行下面代码。
  • 形参后面使用一对儿大括号括起来的是方法体,方法体是完成功能的核 心代码,方法体中的代码有执行顺序的要求,遵循自上而下的顺序依次逐行 执行,不存在跳行执行的情况。
  • 调用时,需要传“实参”,实参的个数、类型、顺序顺序要与形参列表一一对应如果方法没有形参,就不需要也不能传实参。
  • 调用时,如果方法有返回值,可以接受或处理返回值结果,当然也可以不接收,那么此时返回值就丢失了。如果方法的返回值类型是void,不需要也不能接收和处理返回值结果。
  •  return语句后面不能跟数据或代码,因为永远执行不到。​

栈数据结构

要想理解方法执行过程中内存的分配,我们需要先学习一下栈数据结构,那么什么是数据 结构呢?其实数据结构是一门独立的学科,不仅是在 java 编程中需要使用,在其它编程语言 中也会使用,在大学的计算机课程当中,数据结构和算法通常作为必修课出现,而且是在学习 任何一门编程语言之前先进行数据结构和算法的学习。数据结构是计算机存储、组织数据的方 式。

数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选 择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术 有关。

常见的数据结构有哪些呢?例如:栈、队列、链表、数组、树、图、堆、散列表等。目前 我们先来学习一下栈(stack)数据结构,这是一种非常简单的数据结构。如下图所示:

                                                                                                                                                                                                                                                                                    

day08_面向对象(中)

栈(stack)又名堆栈,

  • 它是一种运算受限的线性表。其限制是:仅允许在表的一端进行插 入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。
  • 向一个栈插入新元素又称作 进栈、入栈或压栈(push),它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;
  • 从 一个栈删除元素又称作出栈、退栈或弹栈(pop),它是把栈顶元素删除掉,使其相邻的元素 成为新的栈顶元素。如下图所示:                                                                                                                                               

day08_面向对象(中)

通过以上的学习,我们可以得知栈数据结构存储数据有这样的特点:先进后出,或者后进 先出原则。也就是说最先进去的元素一定是最后出去,最后进去的元素一定是最先出去,因为 一端是开口的,另一端是封闭的。 为什么方法执行过程的内存要采用栈这种数据结构 呢,为什么不选择其它数据结构呢?

方法执行过程中内存的变化

以下要讲解的是程序的内存,例如:代码片段被存储在什么位置?方法调用的时候,在哪 里开辟内存空间等等。所以这一部分内容还是很高端大气上档次的。不过话又说回来,要想真 正掌握 java,内存的分析是必要的。一旦掌握内存的分配,在程序没有运行之前我们就可以很 精准的预测到程序的执行结果。 好了,接下来我们开始学习方法执行过程中内存是如何变化的?我们先来看一张图片:                                                                                                                              

day08_面向对象(中)

day08_面向对象(中)

上图是一张标准的 java 虚拟机内存结构图,目前我们只看其中的“栈”和“方法区”,其 它的后期研究,方法区中存储类的信息,或者也可以理解为代码片段,方法在执行过程中需要 的内存空间在栈中分配。java 程序开始执行的时候先通过类加载器子系统找到硬盘上的字节码 (class)文件,然后将其加载到 java 虚拟机的方法区当中,开始调用 main 方法,main 方法被调 用的瞬间,会给 main 方法在“栈”内存中分配所属的活动空间,此时发生压栈动作,main 方 法的活动空间处于栈底。

也就是说,方法只定义不去调用的话,只是把它的代码片段存储在方法区当中,java 虚拟 机是不会在栈内存当中给该方法分配活动空间的,只有在调用的瞬间,java 虚拟机才会在“栈 内存”当中给该方法分配活动空间,此时发生压栈动作,直到这个方法执行结束的时候,这个 方法在栈内存中所对应的活动空间就会释放掉,此时发生弹栈动作。由于栈的特点是先进后出, 所以最先调用的方法(最先压栈)一定是最后结束的(最后弹栈)。比如:main 方法最先被 调用,那么它一定是最后一个结束的。换句话说:main 方法结束了,程序也就结束了(目前 来说是这样)。

接下来我们来看一段代码,同时画出内存结构图,以及使用文字描述该程序的内存变化:

package demo.method2;

public class MethodTest {
    public static void main(String[] args) {
        System.out.println("main begin");
        m1();
        System.out.println("main over");
    }
    public static void m1() {
        System.out.println("m1 begin");
        m2();
        System.out.println("m1 over");
    }
    public static void m2() {
        System.out.println("m2 begin");
        System.out.println("m2 over");
    }
}
运行结果如下图所示:                                                                                                  

day08_面向对象(中)day08_面向对象(中)

通过上图的执行结果我们了解到,main 方法最先被调用,但是它是最后结束的,其中 m2方法最后被调用,但它是最先结束的。大家别忘了调用的时候分配内存是压栈,结束的时候是 释放内存弹栈哦。为什么会是上图的结果呢,我们来看看它执行的内存变化,请看下图:

day08_面向对象(中)

通过上图的分析,可以很快明白,为什么输出结果是这样的顺序,接下来我们再采用文字的方式描述它的内存变化:

  1. 类加载器将 class 文件加载到方法区。
  2. 开始调用 main方法,在栈内存中给 main方法分配空间,开始执行 main方法,输出”main begin”。
  3. 调用 m1()方法,在栈内存中给 m1()方法分配空间,m1()方法处于栈顶,具备活跃权, 输出”m1 begin”。
  4. 调用 m2()方法,在栈内存中给 m2()方法分配空间,m2()方法处于栈顶,具备活跃权, 输出”m2 begin”,继续输出”m2 over”。
  5. m2()方法执行结束,内存释放,弹栈。
  6. m1()方法这时处于栈顶,具备活跃权,输出”m1 over”。
  7. m1()方法执行结束,内存释放,弹栈。
  8. main()方法这时处于栈顶,具备活跃权,输出”main over”。
  9. main()方法执行结束,内存释放,弹栈。
  10. 栈空了,程序结束。

大家是否还记得之前的课程中曾经提到方法体当中的代码是有执行顺序的,必须遵循自上 而下的顺序依次逐行执行,当前行代码必须执行结束之后,下一行代码才能执行,不能跳行执 行,还记得吗?现在再和栈数据结构一起联系起来思考一下,为什么要采用栈数据结构呢?是 不是只有采用这种先进后出的栈数据结构才可以保证代码的执行顺序呢!此时,你是不是感觉 程序的设计者在此处设计的非常巧妙呢!

方法重载

概念:
  • 指在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可,与修饰符和返回值类型无关。
什么条件的时候构成方法重载呢?满足以下三个条件:
  • 在同一个类当中。
  • 方法名相同。
  • 参数列表不同:个数不同算不同,顺序不同算不同,类型不同也算不同

重载方法调用

  • Java 编译器能通过检 查调用的方法的参数类型和个数选择一个恰当的方法。调用方法时通过传递给它们的不同个数和类型的实参来决定具体使用哪个方法。

注意:

  • 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式
  • 重载仅针对同一个类中方法的名称与参数进行识别,与返回值无关,换句话说不能通过返回值来判定两个方法是否相互构成重载

代码示例

public class Demo1Overload {
    /*
            需求:使用方法重载的思想,设计比较两个整数是否相同的方法,兼容全整数类型(byte,short,int,long)
     */
    public static void main(String[] args) {
        short a = 10;
        short b = 20;
        System.out.println(compare(a,b));
    }

    public static boolean compare (int a, int b){
        return a == b;
    }

    public static boolean compare (byte a, byte b){
        return a == b;
    }

    public static boolean compare (short a, short b){
        return a == b;
    }

    public static boolean compare (long a, long b){
        return a == b;
    }
}

方法的参数传递机制

Java的实参值如何传入方法呢?
  • 形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参
  • 形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参

方法的形参是基本数据类型时

package demo.method01;

/*
需求: 通过方法交换俩数的位置
 */
public class ChangeValueTest1 {
    public static void main(String[] args) {
        int num1 = 66;
        int num2 = 88;
        System.out.println(num1 + ",num2:" + num2);// num1:66,num2:88
        changValue(num1, num2);
        System.out.println(num1 + ",num2:" + num2);//num1:66,num2:88
    }

    public static void changValue(int num1, int num2) {
        int temp = num1;
        num1 = num2;
        num2 = temp;
        System.out.println(num1 + ",num2:" + num2);//num1:88,num2:66
    }


}

结论:

  • 基本类型数值传递:传递的是值的副本。形参值的改变不会影响实参。相当于复制了一份数据传递进去了,怎么修改复制的数据,对原来的数据也没有影响。

方法的形参是引用数据类型时

package demo.method01;

/**
 * 需求:交换 x y的位置
 *
 * 引用数据类型传递的是地址值。
 */
public class ChangeValueTest2 {
    public static void main(String[] args) {
        //创建对象
        Point point = new Point();
        //赋值
        point.x = 66;
        point.y = 88;
        System.out.println("交换前:" + "(x:" + point.x + ",y:" + point.y + ")");// 交换前:(x:66,y:88)
        //调用方法
        changeValue(point);
        System.out.println("交换后:" + "(x:" + point.x + ",y:" + point.y + ")");//交换后:(x:88,y:66)
    }
    //交换的方法
    public static void changeValue(Point point) {
        int temp = point.x;
        point.x = point.y;
        point.y = temp;
        System.out.println("交换中:" + "(x:" + point.x + ",y:" + point.y + ")");//交换中:(x:88,y:66)
    }
}

class Point {
    public int x;

    public int y;
}

结论:

  • 方法的形参是引用数据类型时:形参地址值里面的数据的改变会影响实参,例如,修改数组元素的值,或修改对象的属性值。相当于复制了一份地址值传递进去了,并没有复制真正的数据。修改地址值中的数据,对原来的数据产生影响。例如,修改数组元素的值,或修改对象的属性值。
  • 注意:String、Integer等特殊类型容易错

可变形参

JavaSE 5.0 中提供了Varargs(variable number of arguments)机制,允许直接定义能和多个实参相匹配的形参。从而,可以用一种更简单的方式,来传递个数可变的实参。在JDK1.5之后,如果我们定义一个方法时,此时某个形参的类型可以确定,但是形参的个数不确定,那么我们可以使用可变参数。可变形参就是还有数组来存储,传递过来的值。

JDK 5.0以前:采用数组形参来定义方法,传入多个同一类型变量

public static void test(int a ,String[] books){
    //方法体
}

JDK5.0及之后:采用可变个数形参来定义方法,传入多个同一类型变量

  public static void test(int a ,String...books){
        //方法体
    }    

注意:

  • 上面2个test方法是等价的,不能出现在同一个类中。

格式:

  • 【修饰符】 返回值类型 方法名(【非可变参数部分的形参列表,】参数类型... 形参名){  }

jdk5.0之后既可以传递数组,又可以直接传递数组的元素,这样更灵活了。

注意事项:

  • 声明格式:方法名(参数的类型名 ...参数名)
  • 可变参数:方法参数部分指定类型的参数个数是可变多个:0个,1个或多个
  • 可变个数形参的方法与同名的方法之间,彼此构成重载
  • 可变参数方法的使用与方法参数部分使用数组是一致的
  • 方法的参数部分有可变形参,需要放在形参声明的最后
  • 在一个方法的形参位置,最多只能声明一个可变个数形参

代码示例:

//返回n个字符串拼接结果,如果没有传入字符串,那么返回空字符串""
public class ChangeArgs_Exer {
    public static void main(String[] args) {
        System.out.println(concat());
        System.out.println(concat("hello","world"));
    }
    public static String concat(String... args){
        String str = "";
        for (int i = 0; i < args.length; i++) {
            str += args[i];
        }
        return str;
    }
}

递归

什么是方法递归?我们先来看一段代码:

public class RecursionTest {
    public static void main(String[] args) {
        m();
    }
    public static void m(){
        System.out.println("m begin");
        m();
        System.out.println("m over");
    }
}

​以上代码的执行结果如下图所示:                                                  

day08_面向对象(中)

我们可以看到以上代码的执行过程中,一直输出“m begin”,“m over”一次也没有输出, 直到最终发生了错误:java.lang.*Error,这个错误是栈内存溢出错误,错误发生后, JVM 退出了,程序结束了。 实际上以上代码在 m()方法执行过程中又调用了 m()方法,方法自身调用自身,这就是方 法递归调用。以上程序实际上等同于以下的伪代码(说明问题,但是无法执行的代码)。                                                                                                                                                                                                                                                              

day08_面向对象(中)

通过伪代码我们可以看出,m()方法一直在被调用(方法中的代码必须遵循自上而下的顺 序依次逐行执行,不能跳行执行),对于栈内存来说一直在进行压栈操作,m()方法从未结束 过,所以没有弹栈操作,即使栈内存足够大(也是有限的内存),总有一天栈内存会不够用的, 这个时候就会出现栈内存溢出错误。通过以上研究得出递归必须要有合法的结束条件,没有结 束条件就一定会发生 *Error。我们再来看看有结束条件的递归,例如以下代码:

                                                                                                                                                                                                                                                       

day08_面向对象(中)

综上所述,递归其实就是方法在执行的过程中调用了另一个方法,而另一个方法则是自己 本身。在代码角度来看就是在 a()方法中调用 a()方法,使用递归须谨慎,因为递归在使用的时 候必须有结束条件,没有结束条件就会导致无终止的压栈,栈内存最终必然会溢出,程序因错 误的发生而终止。

大家再来思考一个问题,一个递归程序有合法有效的结束条件就一定不会发生栈内存溢出 错误吗?在实际开发中遇到这个错误应该怎么办? 一个递归程序有的时候存在合法有效的终止条件,但由于递归的太深,在还没有等到条件 成立的时候,栈内存已经发生了溢出,这种情况也是存在的,所以实际开发中我们尽可能使用 循环来代替递归算法,原则是:能不用递归尽量不用,能用循环代替的尽可能使用循环。当然, 如果在开发中遇到了由于使用递归导致栈内存溢出错误的发生,首先,我们要检查递归的终止 条件是否合法,如果是合法的还是发生栈内存溢出错误,那么我们可以尝试调整堆栈空间的大 小。

总结一下:

  • 递归:指在当前方法内调用自己的这种现象。

递归的分类:

递归分为两种,直接递归和间接递归。

  • 直接递归:称为方法自身调用自己。
  • 间接递归:可以A方法调用B方法,B方法调用C方法,C方法调用A方法。

注意事项

  • 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。
  • 在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。

代码示例

package com.atguigu.recursion;

/*
菲波那切数列
递归有两个条件:
     1.有出口
     2.不断的向出口靠近


 1 1 2 3 5 8 13 21 34

 求第n个数是几
 从第三个数开始
  n = (n-1)+(n-2);


 */
public class RecursionTest2 {

    public static void main(String[] args) {

        int feibo = feibo(6);
        System.out.println("feibo = " + feibo);
    }
    /**
     * @param n 第几个位置
     * @return 第几个位置的数
     */
    public static int feibo(int n) {
        if (n == 1 || n == 2) {
            return 1;
        }

        return feibo(n - 1) + feibo(n - 2);

    }
}
上一篇:c# 计算两个时间的时间差


下一篇:【Leetcode刷题笔记 持续更新】Day08