Bootstrap

java SE —— 类与对象(万字长文)


引文:

在数字世界的晨曦中,Java的类与对象如同星辰与尘埃,编织着代码宇宙的秩序与生命。想象一座尚未落成的城市——蓝图是它的骨骼,勾勒出楼宇的轮廓与街道的脉络;而每一栋建筑、每一盏路灯、甚至风中摇曳的梧桐,都是蓝图的具象,是图纸在现实中的呼吸。

类,正是这样一张充满魔力的蓝图。它沉默地承载着属性和方法的密码,如同乐谱上未奏的音符,等待着被赋予旋律。而对象,则是这乐谱在指挥棒下苏醒的瞬间:一个「人」类可以幻化为行走的张三、微笑的李四;一只「鸟」类可以展翅成掠过湖面的白鹭,或栖于枝头的黄鹂。

在这片由代码构筑的天地里,每一个对象都是被类点亮的星火。它们遵循着蓝图的指引,却又在运行时演绎出独一无二的生命轨迹。若你曾好奇,如何用逻辑的丝线编织出如此灵动的世界——那么,请随我推开这扇门,踏入类与对象的哲学与艺术。

在这里,理性与诗意共存,严谨与创造共舞。


目录

引文:

一、情境引入

二、类

1、什么是类?

2、类的实例化

(1)栈区和堆区

A.栈区

B.堆区

(2)实例

3、this引用

(1)定义

(2)详析

(3)特性

三、对象 

1、初始化对象

2、构造方法

(1)定义

(2)特性

(3)通过this调用其他构造方法

A、避免重复劳动

 B、强制关键逻辑:确保一致性

C、 灵活初始化:支持多种参数组合

四、封装

1、概念

2、访问限定符

五、包

1、概念

2、包声明和包导入

(1)包声明

A、格式:

 (2)包导入

A、正常导入

 B、静态导入

3、自定义包

 4、常见的包

六、static成员

1、静态成员属性

(1)特点

(2)语法结构

(3)使用

2、静态成员方法

(1)特点

(2)语法结构

(3)使用

(4)static成员变量初始化

A、就地初始化:在定义时直接给出初始值

B、静态代码块初始化

七、代码块

1、普通代码块

2、构造代码块

3、静态代码块 

(1)特点:

(2)语法

(3)静态代码块 vs 实例代码块 

 (4)多个静态代码块


一、情境引入

  相信各位应该都听说过“面向对象”和“面向过程”。这两者其实很好理解,从字面意思上来看:

  • 面向过程:关注的是怎么做,以步骤为核心,将问题分解成一系列线性执行的函数或过程。整个编程过程是依照事情发展或完成的逻辑顺序来进行的。

例如,制作一杯咖啡的过程:烧水()--->磨豆()--->冲泡()--->装杯()。

  最经典,被广大高校设置课程的C语言,便是最经典的——面向过程的编程语言。面向过程的编程语言适合简单、线性执行的任务(如数学计算、脚本工具),而像C语言这样的语言由于更加接近底层,所以也更适合需要强性能的场景(如嵌入式开发)。

  • 面向对象:关注的是谁来做,以对象为中心,将问题抽象为具有属性和行为的对象,通过对象之间的交互解决问题。 

例如:咖啡机(对象)有方法:烧水()磨豆(),冲泡(),用户只需调用"咖啡机.制作咖啡()"。

   而我今天要讲的java,正是一门面向对象的编程语言。适合复杂系统,需要模块化、易维护和扩展的场景(如大型软件、游戏开发),适合需要模拟现实世界实体交互的场景(如电商平台、图形用户界面应用)。


二、类

1、什么是类?

   众所周知,java是一门面向对象的编程语言,它的重点放在了——“对象”上面。

那什么是对象?没错,就是现实生活中的一个个实实在在存在的东西,我们这里就用手机这个实体来举例。

public class Phone(){
    //手机的属性
    private String brand;        //品牌
    private String model;         //型号
    private int battery           //电池容量
    private int storage           //存储容量
    private int used_storage      //已用存储


    //手机可以实现的功能

    public static void main(String[] args){
        
    }

    //开关机
    class power(){

    }
 
    //充电
    class charge(){

    }

    //打电话
    clss make_call(){

    }

    //安装软件    
    class install_app(){

    }
}

在这个例子里,Phone就是一个类,其中定义了手机的属性和功能。

  class就是定义类的关键字,其后紧跟的就是类的名字,{}中的就是类的主体。

  一般的,我们把在类中定义的内容叫做类的成员,其又分为两类:

  1. 成员属性(成员变量):主要是用来描述类的,比如这个例子中,成员属性就有:品牌、型号、电池容量、存储容量和已用容量,这些都是手机的固有属性,是用来描述手机的。
  2. 成员方法:主要用来说明类具有哪些功能,并在方法中实现相应功能。在这个例子中,成员方法就有:开关机方法、充电方法、打电话方法和安装软件方法,是用来实现手机的具体功能的。

说点人话吧:所谓的类,实际上就像是一张蓝图,告诉计算机这个东西有哪些属性,有哪些功能。

   不知道有没有眼尖的小伙伴发现了,我们书写java程序,用class定义一个类,而这个class之前,为什么有一个“public”呢?而Phone类中的几个成员属性,他们的数据类型前面为什么又有“private”呢?别急,在访问限定符这一节会讲的。

注意:

  1. 一般一个文件当中只定义一个类
  2. main方法所在的类一般要使用public修饰(注意:Eclipse默认会在public修饰的类中找main方法)
  3. public修饰的类必须要和文件名相同
  4. 不要轻易去修改public修饰的类的名称,如果要修改,通过开发工具修改。

2、类的实例化

  要想了解什么是实例,就必须知道数据在计算机中是如何存储的,在java这门编程语言中,大部分数据都需经过java虚拟机存储和运算,而为了数据存储的快捷和运算的高效,java虚拟机分成几个大区,用来分别存储不同的信息。为了了解类的实例化,我们只需要了解java虚拟机中的栈区堆区,它们的功能,且听我细细道来。

(1)栈区和堆区

A.栈区

定义:存放方法调用的栈帧,大致包括其下内容:

  • 局部变量(基本类型变量、对象引用)。
  • 方法参数
  • 操作数栈(用于计算中间结果)。
  • 动态链接(指向方法所属类的符号引用)。
  • 方法返回地址

  学过C语言的同学应该听说过——函数栈帧的创建和销毁,函数的栈帧就是在内存的栈区生成和销毁的,其代表着函数或参数的生命周期。java虽然与C语言有所不同,但同样有栈帧的创建与销毁,在java中栈区用于方法执行,存储轻量级数据(局部变量、方法调用链),高效但容量小。

B.堆区

定义:是用来存放对象实例和数组的。

   存放在其中的数据的生命周期通常较长,一般来说是根据对象的引用决定的。

总结一下:

  • 栈区:就像办公的桌子,其上会存放一些常用、轻量级的文件和数据。
  • 堆区:就像仓库,存放不是很重要或者很大(内存大)的文件或数据。

   栈区的数据可以随手即拿,方便快捷,而栈区中的数据则需要到仓库中去寻找。为了方便数据的存储和管理,存放在仓库(堆区)的数据,通常会将自己所在的位置(地址)记载在书桌上的文件(栈区)中。

(2)实例

   了解了堆区和栈区的作用,我们终于可以来探讨一下什么是实例了。

   前面我们说了,类就像是一张蓝图,可光有蓝图又有什么用呢,我们要依照蓝图,将实物打造出来,而依照类打造出来的,就是这个类的实例。

   我们都知道,类中可以定义成员属性和成员方法,以此来描述一个实体,它的内存大小是不固定的,有可能很小也有可能很大(类中定义的属性和方法的多少),所以每当我们想要根据蓝图(类)构造一个实例,计算机都会在堆区开辟一块空间,用来存储类的实例。

   为了方便调用实例中的属性和方法,计算机会把这个实例在堆区的地址存放到栈区,而我们又要创建一片空间来存放这个地址,以方便访问这个实例。

   如上图所示,我们通过new关键字创建一个类(Animal)的实例 ,并将这个实例在堆区中的地址存放在dog这个变量中。在Java中,我们将这种存放实例地址的变量,叫做对象的引用。

注:在java中,引用虽然与C语言中指针的使用方式不同,但是它们在底层实现上,可能都是通过存放某个对象的地址,从而避免创建多个冗余变量空间,实现快速访问与便捷操作。

注意事项:

  1. new 关键字用于创建一个对象的实例.
  2. 使用 . 来访问对象中的属性和方法.
  3. 同一个类可以创建多个实例.

3、this引用

(1)定义

   this引用指向当前对象(成员方法运行时调用该成员方法的对象),在成员方法中所有成员变量的操作,都是通过该引用去访问。

(2)详析

   

   在这串代码中,我创建了一个Data类,并创建了三个Data实例,将实例的地址赋给d1、d2、d3。

   我在主方法中通过d1、d2、d3(类的实例的引用)调用Data类中的setDay方法,并传参。

   在Data类中可见,setDay方法中,给三个变量赋值,其中两个被this修饰,还有一个未被this修饰。显而易见,未被this修饰的语句报错了,它错在哪儿呢?搞明白这个问题,你就基本明白了this的基础玩法。

   

   请看,在setDay方法中,传递的参数和创建的变量名字相同,这时编译器不知道你设置的赋值关系是怎样的,于是可能会将创建的变量的值赋给方法传递的参数,造成数据混乱。

   而加上this引用,就不同了,this是一个指向当前对象的引用,相当于是告诉编译器,这个被修饰的变量是来自于哪里。例如,当"d1.setDay(2022,9,15)"调用setDay方法时,this会告诉编译器,"this.year"是对象引用d1指向的实例中的变量,从而与方法传递的参数做出区分。

(3)特性

  1. this的类型:对应类类型引用,即哪个对象调用就是哪个对象的引用类型。
  2. this只能在"成员方法"中使用。
  3. 在"成员方法"中,this只能引用当前对象,不能再引用其他对象。

三、对象 

1、初始化对象

众所周知,在Java中定义一个局部变量时,必须要对它进行初始化,否则会编译失败。

 此时,只需要在调用变量a之前,为a赋一个初始值即可解决问题。

   在这串代码中,我们观察到,在Date类的实例未初始化时,我们调用了printDate()方法,但他并未报错,反而得出结果“0-0-0”。这是什么原因呢?

   实际上,在Java中,有一种特殊的成员方法——构造方法。正是它导致这里不会报错。

2、构造方法

(1)定义

   构造方法是一个特殊的成员方法,他的方法名必须与类名相同,在创建对象时,由编译器自动调用,并且在整个生命周期内仅调用一次

   有这段代码可知,当我们自定义了一个构造方法,那么当我们为这个类创建实例时,就要按照构造方法的传参结构,向其中传参,令实例中的成员属性被初始化。在控制台也可以观察到“Date(int,int,int)方法被调用了” 这句话被成功打印,说明了Class_object构造方法被调用。

   有人又要问了,那你倒是说啊,为什么实例中的属性不用初始化就可以调用?

   别急,这就来了。

有着串代码可知,即使构造方法无参数 ,仍然被调用。实例中的成员属性“不用初始化”的原因也在这。

在创建一个对象时,如果用户未自定义一个构造方法,编译器就会默认生成一个不带参数的构造方法,将实例中的成员属性进行初始化赋值。

几大基本数据类型的默认初始化为以下值:

数据类型默认值
byte0
chr'\u0000'
short0
int0
long0L
float0.0f
double0.0
booleanfalse
referencenull

(2)特性

  1. 名字必须与类名相同。
  2. 没有返回值类型,设置为void也不行。
  3. 创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次(相当于人的出生,每个人只能出生一次)。
  4. 构造方法可以重载(用户根据自己的需求提供不同参数的构造方法。
  5. 如果用户没有显式定义,编译器会生成一份默认的构造方法,生成的默认构造方法一定是无参的。

(3)通过this调用其他构造方法

   前面我们知道了构造方法,可以帮助我们对对象的成员属性进行初始化,就算你在声明成员变量时就地初始化,编译器仍会将初始化语句添加到构造方法中。

   而使用this调用构造方法的主要目的是为了代码重写和简化构造方法的实现。通过这种方式,可以避免在多个构造方法中重复相同的初始化代码,从而提高代码的可维护性和可读性。

   这串代码中,创建的Date实例并未传参,其中的成员属性却已进行了初始化,这是为什么?

   在Date的第一个构造方法(无参)中,我们使用了“this(1900,1,1)”这个语句。此时,它实际上是一个指向Date中的第二个构造方法(有三个参数),并将参数传递给第二个构造函数。

A、避免重复劳动

  • 问题:当一个类有多个构造方法时,不同构造方法可能有相同的初始化步骤(例如参数校验、字段赋值)。

  • 解决:将公共逻辑写在一个“主构造方法”中,其他构造方法通过 this() 调用它,避免重复编写相同代码。

 B、强制关键逻辑:确保一致性

  • 问题:如果初始化时需要执行强制步骤(例如参数合法性检查、资源分配),每个构造方法都要重复这些逻辑,容易遗漏。

  • 解决:通过 this() 强制所有构造方法最终调用同一个主构造方法,确保关键逻辑必执行。

C、 灵活初始化:支持多种参数组合

  • 问题:用户创建对象时可能需要不同参数组合(例如全参数、部分参数、无参数),逐个编写构造器会很繁琐。

  • 解决:通过 this() 链式调用,用少量构造器覆盖多种参数情况,提供灵活的初始化方式。


四、封装

1、概念

   在面向对象的编程语言中,有三大特性,即封装、继承、多态。而在类和对象阶段,我们主要研究的是封装特性

   对于继承和多态,小编也写过对应的文章,感兴趣的朋友可以移步:

 java SE -- 继承-CSDN博客

java SE -- 多态-CSDN博客

   什么是封装?简单来说,就是套壳——通过外壳,隐藏内部的细节。

   比如,一部手机,大多数人使用它,只会浮于表面,只接触它的屏幕、边框、机身、摄像头、Type--c接口或USB接口、耳机接口、扩音孔、麦克风。

   而实际上,在手机的内部,还有很多我们不能直接看见的部件,例如——天线、电池、cpu、主板等,包括触屏、摄像这些功能的实现,都是在手机内部进行的,我们难以直接观察到。

   封装,正是如此,将不想被他人访问的数据或方法用访问限定符修饰起来,将其封装在类中,通过开放方法调用权限,用特定的方法调用封装起来的数据。就像手机,开放了如充电口、摄像头使用权、查看本机信息等“接口”,用户只需要通过开放的接口调用对应方法,而不需要管方法的具体实现,大大方便用户的使用体验和便捷程度。

   再举个例子,如图所示,你想访问银行的金融系统。银行一般会允许你查看自己余额、存入金额、取出金额,在存取金额时,银行一般会对信息进行校验,只有符合一定的条件,才能通过系统更改余额信息。这样做,可以有效防止不法分子非法修改加密信息。

   请看,在这个“银行金额管理系统”中,用户的年龄、姓名以及剩余金额,属于私有数据,只用相关人员有修改权限,个人不可随意修改。

   当我们通过Data类直接调用时,编译器直接报错,显示某某具有“private”访问权限,而通过Data类中权限开放的方法,我们可以查看和更改数据。

   当用户进行非法访问,系统就会终止方法的进程,并提示错误。

   可以看到,银行金额剩余是10000元,而zhang想要取出100000元,这是典型的非法操作,于是系统弹出提示——“用户试图进行非法操作” ,并终止方法运行进程。

总结:

封装的本质:封装就是将数据和方法打包成一个整体,对外隐藏实现细节,只暴露必要的接口

就像一个保险箱,内部存放贵重物品(数据)和复杂的加密机制(实现逻辑)。外部只留一个钥匙孔(公共方法)让用户操作,用户不需要知道保险箱的内部结构,只需通过指定方式存取物品。

封装的核心作用:

  1. 保护数据安全:通过 private 字段 + 公共方法(如 setAge())控制访问,添加校验逻辑。
  2. 隐藏实现细节:即使内部逻辑变化,外部调用不受影响。
  3. 提高代码可维护性:外部代码不依赖内部结构,修改内部实现不影响其他模块。
  4. 简化复杂操作:封装成一个方法,隐藏复杂度。

封装的实现方式:

  1. 使用访问修饰符:private(私有)、protected、default(默认)、public(公共)。

  2. 提供公共方法:用public修饰方法,使其成为一个公共接口,用户通过公共方法调用或修改成员属性。

  3. 暴露行为,隐藏状态:优先通过方法描述对象的行为,而非直接暴露数据。

2、访问限定符

   相信很多朋友已经观察到了,不论是在介绍类、介绍this引用还是介绍封装时,我在创建类中的成员属性时,都是用private修饰的,而创建的方法都是用public修饰的。这些就是“访问限定符”,是用来设定成员属性和方法的访问权限的。详情请见下表:

top范围privatedefaultprotectedpublic
1同一包中同一类的类
2同一包中的不同类
3不同包的子类
4不同包的非子类

通过private限定符设定成员属性的访问权限为私有,将其封装在该类中。

通过public 限定符设定成员方法的访问权限为公有,将其视作接口,令用户可以通过开放的接口访问类的成员。


五、包

        在访问限定符的介绍中,多次提到了“包”的概念,那包究竟是什么呢?

1、概念

   在面向对象体系中,提出了一个软件包的概念。即:为了更好的管理类,把多个类收集在一起成为一组,称为软件包,有点类似于目录。

   包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式,比如:一 个包中的类不想被其他包中的类使用,可以使用访问限定符进行修饰。包还有一个重要的作用:在同一个工程中允许存在相同名称的类,只要处在 不同的包中即可。

2、包声明和包导入

(1)包声明

A、格式:

   现在我来声明一个类——“package com.zhang.www”。

   在这个声明中,包含了三层结构,范围由大到小分别是:com   zhang   www。com包涵盖了zhang包,zhang包涵盖了www包,大致结构如下:

src/
   └── com/
       └── zhang/
           └── www/
               ├── first.java
               └── second.java

 (2)包导入

A、正常导入

在Java中,我们可以直接在实例化类的时候,通过“xxx.xxx.xxx”的结构调用包中的类:

   但这样书写十分不便,每次调用Scanner都需要写一长串包导入语句。为了方便,我们可以使用“import” 关键字,在在package关键字之后,通过——“import xxx.xxx.xxx”来调用包:

 B、静态导入

核心作用:省去重复写类名的麻烦,例如当频繁调用Math.sqrt()时,可以直接写sqrt()。

语法格式

// 导入单个静态成员
import static 包名.类名.静态方法名或字段名; 

// 导入类的所有静态成员
import static 包名.类名.*; 

示例

未使用静态导入时:

public class Calculator {
    public static void main(String[] args) {
        double radius = 10.0;
        double area = Math.PI * Math.pow(radius, 2); // 需重复写 Math.
        System.out.println(area);
    }
}

使用静态导入后:

import static java.lang.Math.PI;    // 静态导入 PI
import static java.lang.Math.pow;  // 静态导入 pow 方法

public class Calculator {
    public static void main(String[] args) {
        double radius = 10.0;
        double area = PI * pow(radius, 2); // 直接使用 PI 和 pow
        System.out.println(area);
    }
}

注意:

  1. 过度使用会导致代码含义不清晰(例如大量静态导入后,可能难以区分 assertEquals 是来自 JUnit 还是自定义类)。
  2. 建议仅在频繁使用工具类的静态方法时或测试代码中简化断言时,使用静态导入。

3、自定义包

这里使用的是 intellij IDEA。

首先,在工程中选择scr,单击右键,选择新建,选择软件包。

然后为包命名,通常用公司万维网域名的倒置来命名。写好名称后,单机回车键,创建一个三层嵌套的包。

如下图所示。如果你的包没有形成这种多层嵌套关系,可能是因为默认打开了“压缩空的中间软件包”选项。

 

这时,只需要左键单击“项目”窗口右上角的三个点图标,点击外观,取消勾选“压缩空的中间软件包”选项,这样就可以了。

接着,你想要在哪个包中创建Class文件,就右键单击它,然后选择新建,选择java类。

输入除关键字外的任意类名,最好有辨识性,具有一定意义。单机回车键,创建一个Class文件。

注意:根据书写规范,类名首字母应该大写

 4、常见的包

  1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
  2. java.lang.reflect:java 反射编程包;
  3. java.net:进行网络编程开发包。
  4. java.sql:进行数据库开发的支持包。
  5. java.util:是java提供的工具程序包。(集合类等) 非常重要
  6. java.io:I/O编程开发包。

六、static成员

在面向对象编程中,static成员是属于类本身的成员,而非类的实例。它们分为静态成员变量静态成员方法。

1、静态成员属性

(1)特点

  • 属于类本身,而非类的实例。

  • 所有实例共享同一份内存,修改后全局生效。

  • 无需实例化即可直接通过类名访问。

(2)语法结构

public class Car {
    // 静态变量(类变量)
    public static int count = 0; // 直接初始化
    
    public Car() {
        count++; // 每次创建实例时计数+1
    }
}

(3)使用

System.out.println(Car.count); // 直接通过类名访问
Car car1 = new Car();
Car car2 = new Car();
System.out.println(Car.count); // 输出 2

2、静态成员方法

(1)特点

  • 属于类,可通过 类名.方法名() 直接调用。

  • 不能直接访问非静态成员(实例变量/方法),因为静态方法没有 this 上下文。

  • 常用于工具类(如 Math.sqrt())或单例模式。

(2)语法结构

public class MathUtils {
    // 静态方法
    public static int add(int a, int b) {
        return a + b;
    }
    
    // 错误示例:静态方法中访问非静态成员
    // private int x = 10;
    // public static void printX() {
    //     System.out.println(x); // 编译错误!
    // }
}

(3)使用

int sum = MathUtils.add(3, 5); // 直接调用

(4)static成员变量初始化

   静态成员变量一般不会放在构造方法中来初始化,构造方法中初始化的是与对象相关的实例属性 静态成员变量的初始化分为两种:就地初始化 和 静态代码块初始化

A、就地初始化:在定义时直接给出初始值

public class Student{
    private String name;
    private String gender;
    private int  age;
    private double score;
    private static String classRoom = "Bit306";//静态成员变量就地初始化  
    
    // ...
 }

B、静态代码块初始化

请继续向后看……


七、代码块

使用{}定义的一段代码称为代码块。根据代码块定义的位置以及关键字,又可分为以下四种:

  1. 普通代码块
  2. 构造块
  3. 静态块
  4. 同步代码块

这里主要谈一下普通代码块、构造块和静态块。

1、普通代码块

普通代码块,就是定义在方法中的代码块。

2、构造代码块

   构造块:定义在类中的代码块(不加修饰符)。也叫:实例代码块。构造代码块一般用于初始化实例成员变量

   实例代码块(即非静态初始化块)会在每次创建对象时自动执行,且执行顺序在构造方法之前。代码块中的 this 关键字隐式指向当前正在被构造的对象实例。

   实例代码块会被编译器隐式插入到每个构造方法的开头(在构造方法显式代码之前)。因此,实例代码块中的 this 本质是构造方法中隐含的当前对象引用

   编译器编译后,实际上是这样的:

public class Student {
    private String name;
    private int age;
    
    public Student() {
        // 编译器自动插入实例代码块内容
        this.name = "bit";
        this.age = 12;
        System.out.println("实例代码块执行");
        
        // 构造方法中的显式代码
        System.out.println("构造方法执行");
    }
}

3、静态代码块 

   静态代码块是用static关键字修饰的代码块,它用于在类加载时执行一次性的初始化操作。静态代码块的核心特点是与类绑定,而非对象,因此无论创建多少实例,它都只会执行一次。

(1)特点:

  • 执行时机:在类被 JVM 加载到内存时自动执行(早于对象的创建)。

  • 执行次数:整个程序生命周期内仅执行一次

  • 访问权限

    • 只能访问类的静态成员(静态变量、静态方法)。

    • 不能访问实例成员(非静态变量/方法),因为此时对象尚未创建。

  • 用途:初始化静态资源(如加载配置文件、注册驱动、预计算静态数据等)。

(2)语法

(3)静态代码块 vs 实例代码块 

特性静态代码块实例代码块
关键字static { ... }{ ... }
执行时机类加载时(程序启动或首次使用类)每次创建对象时(在构造方法前)
执行次数一次每次创建对象时执行一次
访问权限只能访问静态成员可以访问静态和非静态成员
用途初始化静态资源初始化对象实例的公共属性

 (4)多个静态代码块

  如果有多个静态代码块或静态变量,按代码中的书写顺序执行。

   编写不易,都是我学习时的感悟,也是查了些资料,可能有地方说的不是很准确,还请各位大佬指正。

;