java面向对象基础(上)

 面向对象基础(上)

学习目标

  • 理解方法的概念和特点
  • 掌握静态方法的声明和调用
  • 知道形参与实参的关系
  • 理解方法参数传递机制的原理
  • 理解方法调用的入栈与出栈过程
  • 了解可变参数的声明和使用
  • 掌握方法重载的概念和要求
  • 了解方法递归调用的概念
  • 理解类与对象的关系
  • 掌握用class声明类的语法格式
  • 掌握用new创建对象的语法格式
  • 掌握静态变量的声明和使用
  • 掌握实例方法的声明和调用
  • 掌握实例变量的声明和使用
  • 掌握包的作用和创建
  • 知道静态变量、实例变量、局部变量的区别

5.1 方法的基础知识(重点掌握)

5.1.1 方法的概念

方法也叫函数,是一组代码语句的封装,从而实现代码重用,从而减少冗余代码,通常它是一个独立功能的定义,方法是一个类中最基本的功能单元。

Math.random()的random()方法
Math.sqrt(x)的sqrt(x)方法
System.out.println(x)的println(x)方法
Scanner input = new Scanner(System.in);
input.nextInt()的nextInt()方法

5.1.2 方法的特点

  • (1)必须先声明后使用

    类,变量,方法等都要先声明后使用

  • (2)不调用不执行,调用一次执行一次。

5.1.3 如何声明静态方法

1、声明方法的位置

声明方法的位置==必须在类中方法外==,即不能在一个方法中直接定义另一个方法。

正确示例:

类{
    方法1(){
        
    }
    方法2(){
        
    }
}

错误示例:

类{
    方法1(){
        方法2(){  //位置错误
        
           }
    }
}

2、声明方法的语法格式

【修饰符】 返回值类型 方法名(【形参列表 】)【throws 异常列表】{ 方法体的功能代码 }

一个完整的方法 = 方法头 + 方法体。

- 方法头就是 【修饰符】 返回值类型 方法名(【形参列表 】)【throws 异常列表】,也称为方法签名,通常调用方法时只需要关注方法头就可以,从方法头可以看出这个方法的功能和调用格式。方法头可能包含5个部分,但是有些部分是可能缺省的。

- 方法体就是方法被调用后要指定的代码,也是完成方法功能的具体实现代码,对于调用者来说,不了解方法体如何实现的,并不影响方法的使用。

3、方法每一个部分的含义

  • (1)**方法名(必选)**:给方法起一个名字,见名知意,能准确代表该方法功能的名字
  • (2)【**修饰符】(可选)**:会影响方法的调用方式,以及方法的可见性范围等

    修饰符:可选的。方法的修饰符也有很多,例如:public、protected、private、static、abstract、native、final、synchronized等,后面会一一学习。其中根据是否有static,可以将方法分为静态方法和非静态方法。其中静态方法又称为类方法,非静态方法又称为实例方法。==接下来咱们先学习静态方法==。

  • (3)**【throws 异常列表】(可选)**:这个部分在异常章节再讲
  • (4)**(【参数列表】)(()必选,参数列表可选)**:表示完成方法体功能时需要“调用者”提供的数据列表

    - 无论是否有参数,()不能省略

    - 如果有参数,每一个参数都要指定数据类型和参数名,多个参数之间使用**逗号**分隔,例如:

    - 一个参数: (数据类型 参数名)

    - 二个参数: (数据类型1 参数1, 数据类型2 参数2)

    - 参数的类型可以是基本数据类型、引用数据类型

  • (5)**返回值类型(必选)**: 表示方法运行的结果的数据类型,方法执行后将结果返回到调用者

    - 基本数据类型

    - 引用数据类型

    - 无返回值类型:void

  • (6)**{方法体}(必选)**:方法体必须有{}括起来,在{}中编写完成方法功能的代码,具有方法体的方法才能被调用执行。

关于方法体中return语句的说明:

  • - return语句的作用是结束方法的执行,并将方法的结果返回去
  • - 如果返回值类型不是void,方法体中必须保证一定有 return 返回值; 语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。
  • - 如果返回值类型为void时,方法体中可以没有return语句,如果要用return语句提前结束方法的执行,那么return后面不能跟返回值,直接写return ; 就可以。
  • - return语句后面就不能再写其他代码了,否则会报错:Unreachable code
package com.atguigu.test04.method;
/**
 * 方法定义案例演示
 */
public class MethodDefineDemo {
    /**
     * 无参无返回值方法的演示
     */
    public static void sayHello(){
        System.out.println("hello");
    }
    /**
     * 有参无返回值方法的演示
     * @param length int 第一个参数,表示矩形的长
     * @param width int 第二个参数,表示矩形的宽
     * @param sign char 第三个参数,表示填充矩形图形的符号
     */
    public static void printRectangle(int length, int width, char sign){
        for (int i = 1; i <= length ; i++) {
            for(int j=1; j <= width; j++){
                System.out.print(sign);
            }
            System.out.println();
        }
    }
    /**
     * 无参有返回值方法的演示
     * @return
     */
    public static int getIntBetweenOneToHundred(){
        return (int)(Math.random()*100+1);
    }
    
    /**
     * 有参有返回值方法的演示
     * @param a int 第一个参数,要比较大小的整数之一
     * @param b int 第二个参数,要比较大小的整数之二
     * @return int 比较大小的两个整数中较大者的值
     */
    public static int max(int a, int b){
        return a > b? a : b;
    }
}

5.1.4 如何调用静态方法

1、本类中

【修饰符】 class 类名{
    【public】 static 返回值类型 静态方法1(【形参列表】){
       ....
    }
    【public】 static 返回值类型 静态方法2(【形参列表】){
        静态方法1(【实参列表】);
    }
}

2、其他类中

【修饰符】 class A类名{
    【public】 static 返回值类型 静态方法1(【形参列表】){
        B类名.静态方法2(【实参列表】);
    }
}
【修饰符】 class B类名{
    【public】 static 返回值类型 静态方法2(【形参列表】){
        ....
    }
}

例如:

package com.atguigu.test04.method;
/**
 * 方法调用案例演示
 */
public class MethodInvokeDemo {
    public static void main(String[] args) {
        System.out.println("-----------------------方法调用演示-------------------------");
        //调用MethodDefineDemo类中无参无返回值的方法sayHello
        MethodDefineDemo.sayHello();
        MethodDefineDemo.sayHello();
        MethodDefineDemo.sayHello();
        //调用一次,执行一次,不调用不执行
        System.out.println("------------------------------------------------");
        //调用MethodDefineDemo类中有参无返回值的方法printRectangle
        MethodDefineDemo.printRectangle(5,10,'@');
        System.out.println("------------------------------------------------");
        //调用MethodDefineDemo类中无参有返回值的方法getIntBetweenOneToHundred();
        MethodDefineDemo.getIntBetweenOneToHundred();//语法没问题,就是结果丢失
        int num = MethodDefineDemo.getIntBetweenOneToHundred();
        System.out.println("num = " + num);
        System.out.println(MethodDefineDemo.getIntBetweenOneToHundred());
        //上面的代码调用了getIntBetweenOneToHundred三次,这个方法执行了三次
        System.out.println("------------------------------------------------");
        //调用MethodDefineDemo类中有参有返回值的方法max
        MethodDefineDemo.max(3,6);//语法没问题,就是结果丢失
        
        int bigger = MethodDefineDemo.max(5,6);
        System.out.println("bigger = " + bigger);
        System.out.println("8,3中较大者是:" + MethodDefineDemo.max(8,9));
    }
}

5.1.5 方法调用内存分析

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

栈结构:先进后出,后进先出。

5.1.6 方法的参数传递机制

方法无论是否有参数,声明方法和调用方法是==()都不能丢失==

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

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

* 实参的作用就是给形参赋值。调用时,实参的个数、类型、顺序顺序要与形参列表一一对应。如果方法没有形参,就不需要也不能传实参。

方法的参数传递机制:实参给形参赋值,那么反过来形参会影响实参吗?

* 方法的形参是基本数据类型时,形参值的改变不会影响实参;

* 方法的形参是引用数据类型时,形参地址值的改变不会影响实参,但是形参地址值里面的数据的改变会影响实参,例如,修改数组元素的值,或修改对象的属性值。

* 注意:String、Integer等特殊类型容易错

1、形参是基本数据类型

案例:编写方法,交换两个整型变量的值

public class PrimitiveTypeParam {
    public static void swap(int a, int b){//交换两个形参的值
        int temp = a;
        a = b;
        b = temp;
    }
    public static void main(String[] args) {
        int x = 1;
        int y = 2;
        System.out.println("交换之前:x = " + x +",y = " + y);//1,2
        swap(x,y);//实参x,y是基本数据类型,给形参的是数据的“副本”,调用完之后,x与y的值不变
        System.out.println("交换之后:x = " + x +",y = " + y);//1,2
    }
}

2、形参是引用数据类型

public class ArrayTypeParam {
    public static void sort(int[] arr){//这里对arr数组进行排序,就相当于对nums数组进行排序
        for (int i = 1; i < arr.length; i++) {
            for (int j = 0; j < arr.length - i; j++) {
                if(arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }
    public static void iterate(int[] arr){//输出数组的元素,元素之间使用空格分隔,元素打印完之后换行
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i]+" ");
        }
        System.out.println();
    }
    public static void main(String[] args) {
        int[] nums = {4,3,1,6,7};
        System.out.println("排序之前:");
        iterate(nums);//实参nums把数组的首地址给形参arr,这个调用相当于输出nums数组的元素
        sort(nums);//对nums数组进行排序
        System.out.println("排序之后:");
        iterate(nums);//输出nums数组的元素
        //上面的代码,从头到尾,堆中只有一个数组,没有产生新数组,无论是排序还是遍历输出都是同一个数组
    }
}

5.1.7 返回值问题

一个方法最多只能有一个返回值。如果没有值需要返回,则用void表示。如果要返回多个值,可以用数组(或其他容器,后面再讲)将多个结果装起来,然后返回数组。

1、void类型

如果被调用方法的返回值类型是void,表示没有返回值。那么调用时不需要也不能接收和处理返回值结果,即方法调用表达式==只能==直接加;成为一个独立语句。

2、非void类型

如果被调用方法的返回值类型不是void,表示有返回值。那么调用时可以选择接收也可以选择不接收返回值结果。

  • - 被调用方法有结果返回,但是调用时没有接收和使用返回值,返回值就会丢失。即方法调用表达式直接加;成为一个独立的语句。
  • - 被调用方法有结果返回,调用时可以接收和使用返回的值。返回的结果怎么用,取决于返回值的类型是什么。

    - 方法调用表达式的结果可以作为赋值表达式的值

    - 方法调用表达式的结果可以作为计算表达式的一个操作数

    - 方法调用表达式的结果可以作为另一次方法调用的实参

    - ......

package com.atguigu.test04.method;
public class MethodReturnValue {
    public static void main(String[] args) {
        //无返回值的都只能单独加;成一个独立语句
        //调用MethodDefineDemo类中无参无返回值的方法sayHello
        MethodDefineDemo.sayHello();
        //调用MethodDefineDemo类中有参无返回值的方法printRectangle
        MethodDefineDemo.printRectangle(5,10,'@');
        //有返回值的
        //(1)方法调用表达式可以作为赋值表达式的值
        int bigger = MethodDefineDemo.max(7,3);
        System.out.println("bigger = " + bigger);
        //(2)方法调用表达式可以作为计算表达式的一个操作数
        //随机产生两个[1,10]之间的整数,并求和
        int sum = MethodDefineDemo.getIntBetweenOneToHundred() + MethodDefineDemo.getIntBetweenOneToHundred();
        System.out.println("sum = " + sum);
        //(3)方法调用表达式可以作为另一次方法调用的实参
        int x = 4;
        int y = 5;
        int z = 2;
        int biggest = MethodDefineDemo.max(MethodDefineDemo.max(x,y),z);
        System.out.println("biggest = " + biggest);
        //(4)方法调用表达式直接加;成为一个独立的语句,这种情况,返回值丢失
        MethodDefineDemo.getIntBetweenOneToHundred();
    }
}

(1)基本数据类型

返回基本数据类型的数据值;

package com.atguigu.method;
public class TestMethodReturnValue1 {
    public static void main(String[] args) {
        int sum = add(5, 3);
        System.out.println("sum = " + sum);
    }
    public static int add(int a, int b){
        return a + b;
    }
}

(2)引用数据类型

返回对象,本质上返回的是对象的首地址。

package com.atguigu.method;
public class TestMethodReturnValue2 {
    public static void main(String[] args) {
        int[] numbers = getNumbersInScope(5, 10, 100);
        for (int i = 0; i < numbers.length; i++) {
            System.out.println(numbers[i]+" ");
        }
    }
    public static int[] getNumbersInScope(int length,int start, int end){
        int[] arr = new int[length];
        for (int i = 0; i < arr.length; i++) {
            arr[i]  = (int)(Math.random()*(end-start)+start);
        }
        return arr;
    }
}

5.2 命令行参数(可选拓展)

给main方法传递的实参称为命令行参数。

public class TestCommandParam{
    //形参:String[] args
    public static void main(String[] args){
        System.out.println(args);
        System.out.println(args.length);
        
        for(int i=0; i

命令行:

java TestCommandParam
java TestCommandParam 1 2 3
java TestCommandParam hello atguigu

IDEA工具:

  • (1)配置运行参数
image-20211228101828718.png
image-20211228102022216.png
  • (2)运行程序
image-20211228102059327.png

5.3 可变参数(简单了解)

在**JDK1.5**之后,当定义一个方法时,形参的类型可以确定,但是形参的个数不确定,那么可以考虑使用可变参数。

1、可变参数的格式:

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

2、可变参数的声明:

  • (1)一个方法最多只能有一个可变参数
  • (2)如果一个方法包含可变参数,那么可变参数必须是形参列表的最后一个

3、可变参数的使用:

  • (1)在声明它的方法中,可变参数当成数组使用
  • (2)调用带可变参数的方法时:    

    - 非可变参数部分必须传入对应类型和个数的实参;

       

    - 可变参数部分按照可变参数的规则传入0~n个对应类型的实参或传入1个对应类型的数组实参;

     

4、对比可变参数与数组类型的参数:

其实”数据类型...“约等于”数据类型[]“ ,只是”数据类型[]“ 这种定义,在调用时必须传递数组,而”数据类型...“更灵活,既可以传递数组,又可以直接传递数组的元素。

案例:求n个整数的和

public class VarParamTest {
    public static void main(String[] args) {
        int[] arr = {1,2,3};
        System.out.println(add(arr));
        System.out.println(add(new int[]{1,2,3,4,5}));
        System.out.println(add(new int[]{1}));
//        System.out.println(add());//报错,必须new一个数组
        System.out.println(add(new int[0]));//数组的长度为,表示没有元素
        System.out.println(sum(1));
        System.out.println(sum(1,2,3));
        System.out.println(sum(1,2,3,4,5));
        System.out.println(sum());
        System.out.println(sum(new int[]{1,2,3,4,5}));
    }
    /*
    声明一个方法,可以求任意个整数的和
     */
    public static int add(int[] arr){
        int result = 0;
        for (int i = 0; i < arr.length; i++) {
            result += arr[i];
        }
        return result;
    }
    public static int sum(int... arr){//把arr当成数组 int[]即可
        int result = 0;
        for (int i = 0; i < arr.length; i++) {
            result += arr[i];
        }
        return result;
    }
}

5.4 方法的重载(必知必会)

**方法重载**:指在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可,与修饰符和返回值类型无关。

- 参数列表:数据类型个数不同,数据类型不同(按理来说数据类型顺序不同也可以,但是很少见,也不推荐,逻辑上容易有歧义)。

- 重载方法调用:JVM通过方法的参数列表,调用匹配的方法。  

  • 先看是否有个数、类型最匹配的
  • 没有最匹配的,再看个数和类型可以兼容的,找不到兼容的将会报错

 

案例,用重载实现:

  • (1)定义方法求两个整数的最大值
  • (2)定义方法求三个整数的最大值
  • (3)定义方法求两个小数的最大值
  • (4)定义方法求n个整数最大值
public class MathTools {
    //求两个整数的最大值
    public static int max(int a,int b){
        return a>b?a:b;
    }
    //求两个小数的最大值
    public static double max(double a, double b){
        return a>b?a:b;
    }
    //求三个整数的最大值
    public static int max(int a, int b, int c){
        return max(max(a,b),c);
    }
    //求n整数的最大值
    public static int max(int[] nums){
        int max = nums[0];//如果没有传入整数,或者传入null,这句代码会报异常
        for (int i = 1; i < nums.length; i++) {
            if(nums[i] > max){
                max = nums[i];
            }
        }
        return max;
    }
}

1、找最匹配的

package com.atguigu.test06.overload;
public class MethodOverloadMosthMatch {
    public static void main(String[] args) {
        System.out.println(MathTools.max(5,3));
        System.out.println(MathTools.max(5,3,8));
        System.out.println(MathTools.max(5.7,2.5));
        System.out.println(MathTools.max(new int[]{5,6,8,9,7,2}));
    }
}

2、找可以兼容的

public class MethodOverloadMostCompatible {
    public static void main(String[] args) {
        System.out.println(MathTools.max(5.7,9));
        System.out.println(MathTools.max(5,6,8,3));
//        System.out.println(MathTools.max(5.7,9.2,6.9)); //没有兼容的
    }
}

3、多个方法可以匹配或兼容

package com.atguigu.test06.overload;
public class MathTools {
    //求两个整数的最大值
    public static int max(int a,int b){
        return a>b?a:b;
    }
    //求两个小数的最大值
    public static double max(double a, double b){
        return a>b?a:b;
    }
    //求三个整数的最大值
    public static int max(int a, int b, int c){
        return max(max(a,b),c);
    }
    //求n整数的最大值
    public static int max(int... nums){
        int max = nums[0];//如果没有传入整数,或者传入null,这句代码会报异常
        for (int i = 1; i < nums.length; i++) {
            if(nums[i] > max){
                max = nums[i];
            }
        }
        return max;
    }
/*    //求n整数的最大值
    public static int max(int[] nums){  //编译就报错,与(int... nums)无法区分
        int max = nums[0];//如果没有传入整数,或者传入null,这句代码会报异常
        for (int i = 1, i < nums.length; i++) {
            if(nums[i] > max){
                max = nums[i];
            }
        }
        return max;
    }*/
/*    //求n整数的最大值
    public static int max(int first, int... nums){  //当前类不报错,但是调用时会引起多个方法同时匹配
        int max = first;
        for (int i = 0; i < nums.length; i++) {
            if(nums[i] > max){
                max = nums[i];
            }
        }
        return max;
    }*/

4、方法的重载和返回值类型无关

public class MathTools {
    public static int getOneToHundred(){
        return (int)(Math.random()*100);
    }
    
    public static double getOneToHundred(){
        return Math.random()*100;
    }
}
//以上方法不是重载

5.5 方法的递归调用(简单了解)

**递归调用**:方法自己调用自己的现象就称为递归。

**递归的分类:**

  • * 递归分为两种,直接递归和间接递归。
  • * 直接递归称为方法自身调用自己。
  • * 间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法。

**注意事项**:

  • * 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。
  • * 在递归中虽然有限定条件,但是递归深度不能太深,否则效率低下,或者也会发生栈内存溢出。
  •   * 能够使用循环代替的,尽量使用循环代替递归

案例:计算斐波那契数列(Fibonacci)的第n个值,斐波那契数列满足如下规律,

1,1,2,3,5,8,13,21,....

即从第三个数开始,一个数等于前两个数之和。假设f(n)代表斐波那契数列的第n个值,那么f(n)满足:

f(n) = f(n-2) + f(n-1); 

package com.atguigu.test07.recursion;
public class FibonacciTest {
    public static void main(String[] args) {
        System.out.println(f(20));//6765
        System.out.println(fValue(20));//6765
        
        System.out.println("-----------------------------");
        
        for(int i=1; i<=10; i++){
            System.out.println("斐波那契数列第" +i +"个数:" + f(i));
        }
        System.out.println("-----------------------------");
        for(int i=1; i<=10; i++){
            System.out.println("斐波那契数列第" +i +"个数:" + fValue(i));
        }
        
    }
    //使用递归的写法
   public static int f(int n){//计算斐波那契数列第n个值是多少
        if(n<1){//负数是返回特殊值1,表示不计算负数情况
            return 1;
        }
        if(n==1 || n==2){
            return 1;
        }
        return f(n-2) + f(n-1);
    }
    //不用递归
    public static int fValue(int n){//计算斐波那契数列第n个值是多少
        if(n<1){//负数是返回特殊值1,表示不计算负数情况
            return 1;
        }
        if(n==1 || n==2){
            return 1;
        }
        //从第三个数开始,  等于 前两个整数相加
        int beforeBefore = 1; //相当于n=1时的值
        int before = 1;//相当于n=2时的值
        int current = beforeBefore + before; //相当于n=3的值
        //再完后
        for(int i=4; i<=n; i++){
            beforeBefore = before;
            before = current;
            current = beforeBefore + before;
            /*
            假设i=4
                beforeBefore = before; //相当于n=2时的值
                before = current; //相当于n=3的值
                current = beforeBefore + before; //相当于n = 4的值
            假设i=5
                beforeBefore = before; //相当于n=3的值
                before = current; //相当于n = 4的值
                current = beforeBefore + before; //相当于n = 5的值
               ....
             */
        }
        return current;
    }
}

5.6 面向对象的概念

5.6.1 面向对象编程思想概述(简单了解)

1、C语言和Java语言

C语言是一种面向过程的程序设计语言,因为C语言是在面向过程思想的指引下去设计、开发计算机程序的。

Java语言是一种面向对象的程序设计语言,因为Java语言是在面向对象思想的指引下去设计、开发计算机程序的。

其中面向对象和面向过程都是一种编程思想,基于不同的思想会产生不同的程序设计方法。

  • 1. 面向过程的程序设计思想(Process-Oriented Programming),简称POP    

    - 关注的焦点是过程:过程就是操作数据的步骤。面向过程是分析出解决问题所需要的步骤,不同的步骤可以抽象为一个一个的函数。

       

    - 代码结构:以函数为组织单位。独立于函数之外的数据称为全局数据,在函数内部的称为局部数据。

     

  • 2. 面向对象的程序设计思想( Object Oriented Programming),简称OOP    

    - 关注的焦点是类和对象:面向对象思想就是在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,用类来表示。某个事物的一个具体个体称为实例或对象。面向对象是把构成问题事务分解成各个对象,关注的是解决问题需要哪些对象。

       

    - 代码结构:以类为组织单位。每种事物都具备自己的**属性**(即表示和存储数据,在类中用成员变量表示)和**行为/功能**(即操作数据,在类中用成员方法表示)。

     

2、面向过程与面向对象的区别

面向过程:

  • - 优点是性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源。而系统软件(例如各种操作系统)等一般采用面向过程开发,性能是最重要的因素。
  • - 缺点是没有面向对象易维护,易复用,易扩展。可维护性差,不易修改。

面向对象:

  • - 优点是易维护,易复用,易扩展。由于面向对象由封装,继承,多态性的特性,可以设计出耦合度低的系统,使系统更加灵活,更加易于维护。 
  • - 缺点是性能比面向过程低。

举例说明:

3 故事:非活字印刷与活字印刷

故事:略 《短歌行》曹操 对酒当歌,人生几何! 譬如朝露,去日苦多。 慨当以慷,忧思难忘。 何以解忧?唯有杜康。 青青子衿,悠悠我心。 但为君故,沉吟至今。 呦呦鹿鸣,食野之苹。 我有嘉宾,鼓瑟吹笙。 明明如月,何时可掇? 忧从中来,不可断绝。 越陌度阡,枉用相存。 契阔谈讌,心念旧恩。 月明星稀,乌鹊南飞。 绕树三匝,何枝可依? 山不厌高,海不厌深。 周公吐哺,天下归心。 如果是有了活字印刷,则只需更改几个字就可,其余工作都未白做。 一、要改,只需更改要改之字,此为可维护; 二、这些字并非用完这次就无用,完全可以在后来的印刷中重复使用,此乃可复用; 三、此诗若要加字,只需另刻字加入即可,这是可扩展; 四、字的排列其实可能是竖排可能是横排,此时只需将活字移动就可做到满足排列需求,此是灵活性好。” 在活字印刷术出现之前,上面的四种特性都无法满足,要修改,必须重刻,要加字,必须重刻,要重新排列,必须重刻,印完这本书后,此版已无任何可再利用价值。

5.6.2 类和对象的概念和关系(必知必会)

1、什么是类

**类**是一类具有相同特性的事物的抽象描述,是一组相关**属性**和**行为**的集合。

  • * **属性**:就是该事物的状态信息。
  • * **行为**:就是在你这个程序中,该状态信息要做什么操作,或者基于事物的状态能做什么。

2、什么是对象

**对象**是一类事物的一个具体个体(对象并不是找个女朋友)。即对象是类的一个**实例**,必然具备该类事物的属性和行为。

3、类与对象的关系

  • - 类是对一类事物的描述,是**抽象的**。
  • - 对象是一类事物的实例,是**具体的**。
  • - **类是对象的模板,对象是类的实体**。
1.jpg

5.6.3 如何定义类和创建对象(重点掌握)

1、类的定义格式

关键字:class(小写)

【修饰符】 class 类名{ }

类的定义格式举例:

public class Student{
    
}

2、对象的创建

关键字:new

new 类名()//也称为匿名对象 //给创建的对象命名 //或者说,把创建的对象用一个引用数据类型的变量保存起来,这样就可以反复使用这个对象了 类名 对象名 = new 类名();

那么,对象名中存储的是什么呢?答:对象地址

public class TestStudent{
    public static void main(String[] args){
        System.out.println(new Student());//Student@7852e922
        Student stu = new Student();
        System.out.println(stu);//Student@4e25154f
        
        int[] arr = new int[5];
        System.out.println(arr);//[I@70dea4e
    }
}

发现学生对象和数组对象类似,直接打印对象名和数组名都是显示“类型@对象的hashCode值",所以说类、数组都是引用数据类型,引用数据类型的变量中存储的是对象的地址,或者说指向堆中对象的首地址。

那么像“Student@4e25154f”是对象的地址吗?不是,因为Java是对程序员隐藏内存地址的,不暴露内存地址信息,所以打印对象时不直接显示内存地址,而是JVM帮你调用了对象的toString方法,将对象的基本信息转换为字符串并返回,默认toString方法返回的是“对象的运行时类型@对象的hashCode值的十六进制值”,程序员可以自己改写toString方法的代码(后面会讲如何改写)。

5.7 Package包

5.7.1 包的作用(简单了解)

  • (1)可以避免类重名:有了包之后,类的全名称就变为:包.类名
  • (2)可以控制某些类型或成员的可见范围    

    如果某个类型或者成员的权限修饰缺省的话,那么就仅限于本包使用。

     

  • (3)分类组织管理众多的类

例如:

  • * java.lang----包含一些Java语言的核心类,如String、Math、Integer、 System和Thread等,提供常用功能
  • * java.net----包含执行与网络相关的操作的类和接口。
  • * java.io ----包含能提供多种输入/输出功能的类。
  • * java.util----包含一些实用工具类,如集合框架类、日期时间、数组工具类Arrays,文本扫描仪Scanner,随机值产生工具Random。
  • * java.text----包含了一些java格式化相关的类
  • * java.sql和javax.sql----包含了java进行JDBC数据库编程的相关类/接口
  • * java.awt和java.swing----包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。

5.7.2 如何声明包(重点掌握)

关键字:package

package 包名;

注意:

  • (1)必须在源文件的代码首行
  • (2)一个源文件只能有一个声明包的package语句

包的命名规范和习惯:

  • (1)所有单词都小写,每一个单词之间使用.分割
  • (2)习惯用公司的域名倒置开头
  • (3)不能以"java."开头

例如:com.atguigu.xxx;

5.7.3 如何跨包使用类(重点掌握)

注意:只有public的类才能被跨包使用

  • (1)使用类型的全名称    

    例如:java.util.Scanner input = new java.util.Scanner(System.in);

     

  • (2)使用import 语句之后,代码中使用简名称    

    import语句告诉编译器到哪里去寻找类。

       

    import语句的语法格式:

       

    import 包.类名; import 包.*;    

       

    注意:

       

    • 使用java.lang包下的类,不需要import语句,就直接可以使用简名称
    • import语句必须在package下面,class的上面
    • 当使用两个不同包的同名类时,例如:java.util.Date和java.sql.Date。一个使用全名称,一个使用简名称

     

package com.atguigu.test02.pkg;
import com.atguigu.test01.oop.Student;
import java.util.Date;
import java.util.Scanner;
public class TestPackage {
    public static void main(String[] args) {
/*        java.util.Scanner input = new java.util.Scanner(System.in);
        com.atguigu.test01.oop.Student stu = new com.atguigu.test01.oop.Student();*/
        Scanner input = new Scanner(System.in);
        Student student = new Student();
        Date d1 = new Date();
        java.sql.Date d2 = new java.sql.Date(0);
    }
}

5.7.4 静态导入(简单了解)

如果大量使用另一个类的静态成员,可以使用静态导入,简化代码。

import static 包.类名.静态成员名; import static 包.类名.*;

演示:

package com.atguigu.keyword;
import static java.lang.Math.*;
public class TestStaticImport {
    public static void main(String[] args) {
        //使用Math类的静态成员
        System.out.println(Math.PI);
        System.out.println(Math.sqrt(9));
        System.out.println(Math.random());
        System.out.println("----------------------------");
        System.out.println(PI);
        System.out.println(sqrt(9));
        System.out.println(random());
    }
}

5.8 类的成员之一:成员变量(重点掌握)

5.8.1 成员变量的分类

Java类的成员变量分为两大类:

  • - 静态变量(加staitc修饰),又称为类变量
  • - 非静态变量(不加static修饰),又称为实例变量或者属性。

5.8.2 成员变量之实例变量

1、实例变量的声明格式

【修饰符】 class 类名{    【修饰符】 数据类型  成员变量名; }

位置要求:必须在类中,方法外

类型要求:可以是Java的任意类型,包括基本数据类型、引用数据类型(类、接口、数组等)

修饰符:暂时写public。

public class Person{
    public String name;
    public char gender;
    public int age;
}

2、实例变量的特点

  • (1)实例变量的值是属于某个对象的    

    - 在类的外面必须通过对象才能访问实例变量

       

    - 每个对象的实例变量的值是独立的

     

  • (2)实例变量有默认值
分类数据类型默认值
基本类型整数(byte,short,int,long)0
 浮点数(float,double)0.0
 字符(char)'\u0000'
 布尔(boolean)false
引用类型数组,类,接口null

3、实例变量的访问

对象.实例变量

例如:

package com.atguigu.test03.field;
public class TestPerson {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.name = "张三";
        p1.age = 23;
        p1.gender = '男';
        Person p2 = new Person();
        /*
        (1)实例变量的值是属于某个对象的
        - 必须通过对象才能访问实例变量
        - 每个对象的实例变量的值是独立的
        (2)实例变量有默认值
         */
        System.out.println("p1对象的实例变量:");
        System.out.println("p1.name = " + p1.name);
        System.out.println("p1.age = " + p1.age);
        System.out.println("p1.gender = " + p1.gender);
        System.out.println("p2对象的实例变量:");
        System.out.println("p2.name = " + p2.name);
        System.out.println("p2.age = " + p2.age);
        System.out.println("p2.gender = " + p2.gender);
    }
}

4、实例变量的内存分析

内存是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。其作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来。我们编写的程序是存放在硬盘中的,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存。Java虚拟机要运行程序,必须要对内存进行空间的分配和管理,每一片区域都有特定的处理数据方式和内存管理方式。

JVM的运行时内存区域分为:方法区、堆、虚拟机栈、本地方法栈、程序计数器几大块。
 

区域名称作用
程序计数器程序计数器是CPU中的寄存器,它包含每一个线程下一条要执行的指令的地址
本地方法栈当程序中调用了native的本地方法时,本地方法执行期间的内存区域
方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆内存存储对象(包括数组对象),new来创建的,都存储在堆内存。
虚拟机栈用于存储正在执行的每个Java方法的局部变量表等。局部变量表存放了编译期可知长度的各种基本数据类型、对象引用,方法执行完,自动释放。

Java对象保存在内存中时,由以下三部分组成:

  • - 对象头    

      - Mark Word:记录了和当前对象有关的GC、锁等信息。(Java高级再详细讲)

     

      - 指向类的指针:每一个对象需要记录它是由哪个类创建出来的,而Java对象的类数据保存在方法区,指向类的指针就是记录创建该对象的类数据在方法区的首地址。该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

     

      - 数组长度(只有数组对象才有)

     

  • - 实例数据    

      - 即实例变量的值

     

  • - 对齐填充    

      - 因为JVM要求Java对象占的内存大小应该是8bit的倍数,如果不满足该大小,则需要补齐至8bit的倍数,没有特别的功能。
     

5.8.3 成员变量之静态变量

1、静态变量声明格式

有static修饰的成员变量就是静态变量。

【修饰符】 class 类{    【其他修饰剂】 static 数据类型  静态变量名; }

public class Chinese{
    public static String country; //静态变量
    public String name; //实例变量
}

2、静态变量的特点

  • - 静态变量的默认值规则和实例变量一样。
  • - 静态变量值是所有对象共享。

3、静态变量的访问

在其他类中可以通过“类名.静态变量”直接访问,也可以通过“对象.静态变量”的方式访问(但是更推荐使用类名.静态变量的方式)。

类名.静态变量

public class TestChinese{
    public static void main(String[] args){
        Chinese.country = "中国";
        
        Chinese c1 = new Chinese();
        c1.name = "张三";
        
        Chinese c2 = new Chinese();
        
        System.out.println(Chinese.country);
        System.out.println(c1.country + "," + c1.name);
        System.out.println(c2.country + "," + c2.name);
        System.out.println("=======================");
        
        c1.country = "china";
        System.out.println(Chinese.country);
        System.out.println(c1.country + "," + c1.name);
        System.out.println(c2.country + "," + c2.name);
    }
}

4、静态变量内存分析

- 静态变量的值存储在方法区。

具体内存分析图请看PPT。

5.8.4 成员变量是引用数据类型

成员变量也是变量,数据类型可以是8种基本数据类型,也可以是引用数据类型(数组、类等)。如果是引用数据类型,请警惕空指针异常。

class Husband{
    String name;
    Wife wife;
}
class Wife{
    String name;
    Husband husband;
}
class TestMarry{
    public static void main(String[] args){
        Husband h = new Husband();
        h.name = "张三";
        Wife w = new Wife();
        w.name = "翠花";
        System.out.println("丈夫的姓名:" + h.name + ",妻子:" + h.wife);
         h.wife = w;
        System.out.println("丈夫的姓名:" + h.name + ",妻子:" + h.wife.name);//警惕空指针异常
        System.out.println("妻子的姓名:" + w.name + ",丈夫:" + w.husband);
        w.husband = h;
        System.out.println("妻子的姓名:" + w.name + ",丈夫:" + w.husband.name);//警惕空指针异常
        System.out.println("---------------------------------------");
        //离婚
        h.wife = null;
        w.husband = null;
        h.wife = new Wife();
        h.wife.name = "小何";
        h.wife.husband = h;
        System.out.println("丈夫的姓名:" + h.name + ",妻子:" + h.wife.name);
        System.out.println("妻子的姓名:" + h.wife.name + ",丈夫:" + h.wife.husband.name);
    }
}

总结:

编译时:一个引用数据类型的变量,可以.出什么,和这个变量的类型有关,这个类型中有什么成员,就可以.出什么成员。

运行时:.操作是不是会发生空指针异常,要看.前面的变量有没有“引用”一个对象,即有没有给它赋值一个对象。

5.9 类的成员之二:成员方法(重点掌握)

5.9.1 类的静态方法

静态方法不依赖于对象。

1、在静态方法中直接使用本类的静态变量

当静态变量与局部变量同名时,可以用“类名.静态变量”进行区分。

public class Demo1{
    public static int a;
    public static int b;
    
    public static void main(String[] args){
        System.out.println("a = " + a);
        
        int b = 1;
        System.out.println("b = " + b);
        System.out.println("Demo1.b = " + Demo1.b);
    }
}

2、在静态方法中直接调用本类的静态方法

public class Demo2{
    public static void say(){
        System.out.println("hello");
    }
    
    public static void main(String[] args){
        say();
    }
}

3、在其他类中调用静态方法

public class Demo3{
    public static void show(){
        System.out.println("hello");
    }
}
public class Demo3Test{ 
    public static void main(String[] args){
        Demo3.show();
    }
}

5.9.2 对象的实例方法

1、在实例方法中直接使用当前对象的实例变量

实例方法依赖于对象,因为实例方法必须由对象来调用,那么调用当前方法的对象称为当前对象,在实例方法中使用this表示。

如果当前实例方法中没有局部变量与实例变量重名,也可以省略this.,如果有局部变量与实例变量重名,就必须加this.。

package com.atguigu.exer3;
//(1)声明一个MyDate类型,有属性:年,月,日
public class MyDate {
    public int year;
    public int month;
    public int day;
    public void setValue(int year, int month,int day){
        this.year = year;
        this.month = month;
        this.day = day;
    }
    public String getInfo(){
        return year+"年" + month+"月" + day+"日";
    }
}

2、在实例方法中直接使用本类的静态变量

public class Chinese{
    public static String country; //静态变量
    public String name; //实例变量
    
    public String getInfo(){
        return "country = " + country +",name = " + name;
    }
}

静态变量,又称为类变量,是当前所有类的对象共享的。

3、在实例方法中直接调用本类的静态方法

public class Chinese{
    public static String country; //静态变量
    public String name; //实例变量
    
    public static void sayCountry(){
        System.out.println("我是一个中国人");
    }
    
    public void say(){
        sayCountry();
        System.out.println("我的名字是:" + name);
    }
}

4、在实例方法中直接调用本类的实例方法

public class Chinese{
    public static String country; //静态变量
    public String name; //实例变量
    
    public static void sayCountry(){
        System.out.println("我是一个中国人");
    }
    
    public void sayName(){
        System.out.println("我的名字是:" + name);
    } 
    
    public void say(){
        sayCountry();
        sayName();
    }
}

5、其他类中调用实例方法

当出现某个类的多个对象都要进行相同操作时,这些操作的重复性代码,就可以封装为实例方法。

在其他类中调用实例方法:

对象名.实例方法(【实参列表】)

例如:

//1、创建Scanner的对象
Scanner input = new Scanner(System.in);//System.in默认代表键盘输入
//2、提示输入xx
System.out.print("请输入一个整数:"); //对象.非静态方法(实参列表)
//3、接收输入内容
int num = input.nextInt();  //对象.非静态方法()

案例演示:

package com.atguigu.exer3;
public class MyDate {
    public int year;
    public int month;
    public int day;
    public void setValue(int year, int month,int day){
        this.year = year;
        this.month = month;
        this.day = day;
    }
    public String getInfo(){
        return year+"年" + month+"月" + day+"日";
    }
}
package com.atguigu.exer3;
public class Employee {
    public String name;
    public MyDate birthday;
    public void setBirthday(int year, int month, int day){
        birthday = new MyDate();
//        birthday.year = year;
//        birthday.month = month;
//        birthday.day = day;
        birthday.setValue(year, month, day);
    }
    public String getInfo(){
//        return "姓名:" + name +",生日:" + birthday.year+"年" + birthday.month+"月" + birthday.day+"日";
        return "姓名:" + name +",生日:" + birthday.getInfo();
    }
}
public class EmployeeTest {
    public static void main(String[] args) {
        Employee e1 = new Employee();
        e1.name = "张三";
//        e1.birthday = new MyDate();
//        e1.birthday.year = 1990;
//        e1.birthday.month = 5;
//        e1.birthday.day = 1;
        e1.setBirthday(1990,5,1);
//        System.out.println("e1的姓名:" + e1.name +",生日:" + e1.birthday.year+"年" + e1.birthday.month+"月" + e1.birthday.day+"日");
        System.out.println("e1的" + e1.getInfo());
        Employee e2 = new Employee();
        e2.name = "李四";
//        e2.birthday = new MyDate();
//        e2.birthday.year = 1995;
//        e2.birthday.month = 6;
//        e2.birthday.day = 1;
        e2.setBirthday(1996,6,1);
//        System.out.println("e2的姓名:" + e2.name +",生日:" + e2.birthday.year+"年" + e2.birthday.month+"月" + e2.birthday.day+"日");
        System.out.println("e2的" + e2.getInfo());
    }
}

6、实例方法的内存分析

实例方法是由实例对象调用的,每一个实例方法中默认有一个this变量用来记录当前对象(即调用该方法的实例对象)的首地址。

调用过程分析请看PPT。

5.9.3 成员互访原则

  静态变量实例变量静态方法实例方法
本类中在静态方法中直接使用×直接使用×
 在实例方法中直接使用直接使用直接使用直接使用
在其他类中 类名.静态变量(推荐)对象名.实例变量类名.静态方法(推荐)对象名.实例方法
  对象名.静态变量(不推荐) 对象名.静态方法(不推荐) 

5.9.4 各种变量小结

- 静态类变量(简称静态变量):存储在方法区,有默认值,所有对象共享,生命周期和类相同,还可以有权限修饰符等其他修饰符

- 非静态实例变量(简称实例变量):存储在堆中,有默认值,每一个对象独立,生命周期每一个对象也独立,还可以有权限修饰符等其他修饰符

- 局部变量:存储在栈中,没有默认值,每一次方法调用都是独立的,有作用域,不能加权限修饰符

请看PPT。

5.10 构造器(重点掌握)

我们发现我们new完对象时,所有成员变量都是默认值,如果我们需要赋别的值,需要挨个为它们再赋值,太麻烦了。我们能不能在new对象时,直接为当前对象的某个或所有成员变量直接赋值呢。

可以,Java给我们提供了构造器(Constructor)。

5.10.1 构造器的作用

new对象,并在new对象的时候为实例变量赋值。

5.10.2 声明构造器

构造器又称为构造方法,构造函数,那是因为它长的很像方法。但是和方法还是有所区别的。

1、构造器语法格式和要求

【修饰符】 class 类名{    【修饰符】 构造器名(){        // 实例初始化代码    }    【修饰符】 构造器名(参数列表){        // 实例初始化代码    } }

注意事项:

  • 1. 构造器名必须与它所在的类名必须相同。
  • 2. 它没有返回值,所以不需要返回值类型,甚至不需要void
  • 3. 如果你不提供构造器,系统会给出无参数构造器,并且该构造器的修饰符默认与类的修饰符相同
  • 4. 如果你提供了构造器,系统将不再提供无参数构造器,除非你自己定义。
  • 5. 构造器是可以重载的,既可以定义参数,也可以不定义参数。
  • 6. 构造器的修饰符只能是权限修饰符,不能被其他任何修饰
package com.atguigu.constructor;
public class Student {
    public String name;
    public int age;
    // 无参构造
    public Student() {}
    // 有参构造
    public Student(String name,int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }@Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

2、构造器的调用时机

当使用new关键字创建对象时,就会自动调用对应的构造器来完成对象的初始化工作。例如:

public class TestStudent {
    public static void main(String[] args) {
        // 调用无参构造器创建对象
        Student s1 = new Student();
        s1.name = "张三";
        s1.age = 20;
        // 调用有参构造器创建对象
        Student s2 = new Student("李四", 22);
        System.out.println(s1);
        System.out.println(s2);
    }
}

5.10.3 构造器的重载

与方法重载类似,在同一个类中可以定义多个构造器,只要它们的参数列表不同即可,目的是可以通过不同的参数组合来创建对象并进行不同的初始化操作。比如:

public class Person {
    private String name;
    private int age;
    private String address;
    // 无参构造器
    public Person() {}
    // 带有姓名和年龄参数的构造器
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 带有姓名、年龄和地址参数的构造器
    public Person(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
}

调用示例:

public class TestPerson {
    public static void main(String[] args) {
        // 使用无参构造器创建对象
        Person p1 = new Person();
        // 使用带有姓名和年龄参数的构造器创建对象
        Person p2 = new Person("王五", 30);
        // 使用带有姓名、年龄和地址参数的构造器创建对象
        Person p3 = new Person("赵六", 35, "北京");
        // 后续可以对这些对象进行相应操作
    }
}

5.10.4 构造器的执行顺序(当存在继承关系时)

如果类之间存在继承关系,那么在创建子类对象时,构造器的调用顺序是先调用父类的构造器(默认调用无参构造器,如果父类没有无参构造器,需要在子类构造器中通过super关键字显式调用父类有参构造器),再调用子类的构造器。例如:

class Animal {
    public Animal() {
        System.out.println("父类Animal的构造器被调用");
    }
}
class Dog extends Animal {
    public Dog() {
        System.out.println("子类Dog的构造器被调用");
    }
}

测试代码:

public class TestConstructorOrder {
    public static void main(String[] args) {
        Dog dog = new Dog();
    }
}

运行结果会先输出“父类Animal的构造器被调用”,再输出“子类Dog的构造器被调用”,这体现了在对象初始化时,先完成父类部分的初始化,再进行子类部分的初始化。

5.10.5 用构造器进行初始化的优势

相较于手动一个个给对象的成员变量赋值,使用构造器进行初始化有以下好处:

  • 1. 代码更加简洁和规范,集中在构造器中完成对象初始值的设置,便于代码的阅读和维护。例如,当创建一个复杂对象,有多个成员变量需要赋值时,通过构造器可以清晰地看到初始化逻辑。
  • 2. 可以保证对象在创建出来后处于一个合理的初始状态,避免因忘记给某些关键成员变量赋值而导致后续程序出现错误。比如,一个表示学生的类,通过构造器可以确保每个学生对象创建时,姓名、学号等重要信息都有初始值。
  • 3. 利用构造器的重载,可以灵活地根据不同场景创建具有不同初始值的对象,满足多样化的业务需求。

5.11 代码块(可选拓展)

5.11.1 代码块的分类

在Java中代码块主要分为以下几种类型:

  • 1. **静态代码块**:用static关键字修饰的代码块,在类加载的时候执行,且只执行一次,通常用于初始化静态变量等操作。例如:
public class StaticBlockDemo {
    static int num;
    static {
        num = 10;
        System.out.println("静态代码块执行,初始化静态变量num的值为" + num);
    }
}
  • 2. **实例代码块**:也叫非静态代码块,没有static关键字修饰,每次创建对象时都会执行,在构造器之前执行,常用于初始化实例变量等操作。例如:
public class InstanceBlockDemo {
    private int count;
    {
        count = 5;
        System.out.println("实例代码块执行,初始化实例变量count的值为" + count);
    }
    public InstanceBlockDemo() {
        System.out.println("构造器执行");
    }
}
  • 3. **局部代码块**:定义在方法内部的代码块,用于限定变量的作用域,当代码块执行结束后,其中定义的变量就会被销毁,可节省内存空间等。例如:
public class LocalBlockDemo {
    public void testMethod() {
        {
            int localVar = 20;
            System.out.println("局部代码块内的变量localVar的值为" + localVar);
        }
        // 此处如果再访问localVar会报错,因为超出了其作用域
    }
}

5.11.2 代码块的执行顺序(当存在多种代码块及继承关系时)

当一个类中既有静态代码块,又有实例代码块,还有构造器,并且类之间存在继承关系时,执行顺序如下:

  1. 1. 首先执行父类的静态代码块(只执行一次,在类第一次被加载时执行)。
  2. 2. 接着执行子类的静态代码块(同样只执行一次,在子类类加载时执行)。
  3. 3. 然后在创建子类对象时,先执行父类的实例代码块,再执行父类的构造器。
  4. 4. 最后执行子类的实例代码块,再执行子类的构造器。

以下是示例代码展示这种执行顺序:

class Parent {
    static {
        System.out.println("父类静态代码块执行");
    }
    {
        System.out.println("父类实例代码块执行");
    }
    public Parent() {
        System.out.println("父类构造器执行");
    }
}
class Child extends Parent {
    static {
        System.out.println("子类静态代码块执行");
    }
    {
        System.out.println("子类实例代码块执行");
    }
    public Child() {
        System.out.println("子类构造器执行");
    }
}

测试代码:

public class BlockOrderTest {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

运行上述代码,会按照前面所述的顺序依次输出相应的执行语句,清晰地展示了各代码块在对象创建及类加载过程中的执行顺序。

5.11.3 代码块的作用及应用场景

1. **静态代码块的作用及应用场景**:

  •   - **作用**:主要用于初始化类级别的静态资源,比如加载配置文件、初始化数据库连接池等,因为这些操作只需要在类加载时进行一次即可。它还可以用于对静态变量进行一些复杂的初始化逻辑设置,确保静态变量在被使用前已经有合适的初始值。
  •   - **应用场景**:例如在一个数据库操作类中,通过静态代码块来加载数据库驱动,代码可能如下:
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseUtil {
    static {
        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("数据库驱动加载成功");
        } catch (ClassNotFoundException e) {
            System.out.println("数据库驱动加载失败");
            e.printStackTrace();
        }
    }
    // 后续可以定义其他数据库操作相关的方法等
}

2. **实例代码块的作用及应用场景**:

  •   - **作用**:常用于初始化实例对象的一些通用属性,尤其是那些每次创建对象都需要进行相同初始化操作的情况。它可以对实例变量进行赋值等操作,确保对象创建出来后部分属性有初始值,而且可以在其中编写一些相对独立于构造器但又需要在构造器之前执行的逻辑。
  •   - **应用场景**:比如创建一个表示商品的类,商品有默认的库存数量等属性,通过实例代码块可以在每次创建商品对象时将库存数量初始化为一个固定值,示例如下:
public class Product {
    private int stock;
    {
        stock = 100;
        System.out.println("实例代码块执行,将商品库存初始化为" + stock);
    }
    public Product() {
        // 构造器中可以继续其他初始化操作或者什么都不做
    }
    // 可以定义其他与商品相关的方法等
}

3. **局部代码块的作用及应用场景**:

  •   - **作用**:通过限定变量的作用域来节省内存空间,尤其是在方法内部对于一些临时使用且占用较大内存空间的变量,使用局部代码块可以让这些变量在使用完后及时释放内存。同时也可以让代码的逻辑结构更加清晰,将一组相关的代码和变量限定在一个局部范围内,便于阅读和理解。
  •   - **应用场景**:比如在一个循环中需要创建一个临时的大容量数组来存储数据,但这个数组在循环结束后就不再需要了,就可以将数组的定义和使用放在局部代码块内,示例如下:
public class LoopDemo {
    public void loopMethod() {
        for (int i = 0; i < 10; i++) {
            {
                int[] tempArray = new int[10000];
                // 使用tempArray进行一些数据处理等操作
            }
            // 此处tempArray已经超出作用域被销毁,节省了内存空间
        }
    }
}