引文:
在数字世界的晨曦中,Java的类与对象如同星辰与尘埃,编织着代码宇宙的秩序与生命。想象一座尚未落成的城市——蓝图是它的骨骼,勾勒出楼宇的轮廓与街道的脉络;而每一栋建筑、每一盏路灯、甚至风中摇曳的梧桐,都是蓝图的具象,是图纸在现实中的呼吸。
类,正是这样一张充满魔力的蓝图。它沉默地承载着属性和方法的密码,如同乐谱上未奏的音符,等待着被赋予旋律。而对象,则是这乐谱在指挥棒下苏醒的瞬间:一个「人」类可以幻化为行走的张三、微笑的李四;一只「鸟」类可以展翅成掠过湖面的白鹭,或栖于枝头的黄鹂。
在这片由代码构筑的天地里,每一个对象都是被类点亮的星火。它们遵循着蓝图的指引,却又在运行时演绎出独一无二的生命轨迹。若你曾好奇,如何用逻辑的丝线编织出如此灵动的世界——那么,请随我推开这扇门,踏入类与对象的哲学与艺术。
在这里,理性与诗意共存,严谨与创造共舞。
目录
一、情境引入
相信各位应该都听说过“面向对象”和“面向过程”。这两者其实很好理解,从字面意思上来看:
- 面向过程:关注的是怎么做,以步骤为核心,将问题分解成一系列线性执行的函数或过程。整个编程过程是依照事情发展或完成的逻辑顺序来进行的。
例如,制作一杯咖啡的过程:烧水()--->磨豆()--->冲泡()--->装杯()。
最经典,被广大高校设置课程的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就是定义类的关键字,其后紧跟的就是类的名字,{}中的就是类的主体。
一般的,我们把在类中定义的内容叫做类的成员,其又分为两类:
- 成员属性(成员变量):主要是用来描述类的,比如这个例子中,成员属性就有:品牌、型号、电池容量、存储容量和已用容量,这些都是手机的固有属性,是用来描述手机的。
- 成员方法:主要用来说明类具有哪些功能,并在方法中实现相应功能。在这个例子中,成员方法就有:开关机方法、充电方法、打电话方法和安装软件方法,是用来实现手机的具体功能的。
说点人话吧:所谓的类,实际上就像是一张蓝图,告诉计算机这个东西有哪些属性,有哪些功能。
不知道有没有眼尖的小伙伴发现了,我们书写java程序,用class定义一个类,而这个class之前,为什么有一个“public”呢?而Phone类中的几个成员属性,他们的数据类型前面为什么又有“private”呢?别急,在访问限定符这一节会讲的。
注意:
- 一般一个文件当中只定义一个类
- main方法所在的类一般要使用public修饰(注意:Eclipse默认会在public修饰的类中找main方法)
- public修饰的类必须要和文件名相同
- 不要轻易去修改public修饰的类的名称,如果要修改,通过开发工具修改。
2、类的实例化
要想了解什么是实例,就必须知道数据在计算机中是如何存储的,在java这门编程语言中,大部分数据都需经过java虚拟机存储和运算,而为了数据存储的快捷和运算的高效,java虚拟机分成几个大区,用来分别存储不同的信息。为了了解类的实例化,我们只需要了解java虚拟机中的栈区和堆区,它们的功能,且听我细细道来。
(1)栈区和堆区
A.栈区
定义:存放方法调用的栈帧,大致包括其下内容:
- 局部变量(基本类型变量、对象引用)。
- 方法参数。
- 操作数栈(用于计算中间结果)。
- 动态链接(指向方法所属类的符号引用)。
- 方法返回地址。
学过C语言的同学应该听说过——函数栈帧的创建和销毁,函数的栈帧就是在内存的栈区生成和销毁的,其代表着函数或参数的生命周期。java虽然与C语言有所不同,但同样有栈帧的创建与销毁,在java中栈区用于方法执行,存储轻量级数据(局部变量、方法调用链),高效但容量小。
B.堆区
定义:是用来存放对象实例和数组的。
存放在其中的数据的生命周期通常较长,一般来说是根据对象的引用决定的。
总结一下:
- 栈区:就像办公的桌子,其上会存放一些常用、轻量级的文件和数据。
- 堆区:就像仓库,存放不是很重要或者很大(内存大)的文件或数据。
栈区的数据可以随手即拿,方便快捷,而栈区中的数据则需要到仓库中去寻找。为了方便数据的存储和管理,存放在仓库(堆区)的数据,通常会将自己所在的位置(地址)记载在书桌上的文件(栈区)中。
(2)实例
了解了堆区和栈区的作用,我们终于可以来探讨一下什么是实例了。
前面我们说了,类就像是一张蓝图,可光有蓝图又有什么用呢,我们要依照蓝图,将实物打造出来,而依照类打造出来的,就是这个类的实例。
我们都知道,类中可以定义成员属性和成员方法,以此来描述一个实体,它的内存大小是不固定的,有可能很小也有可能很大(类中定义的属性和方法的多少),所以每当我们想要根据蓝图(类)构造一个实例,计算机都会在堆区开辟一块空间,用来存储类的实例。
为了方便调用实例中的属性和方法,计算机会把这个实例在堆区的地址存放到栈区,而我们又要创建一片空间来存放这个地址,以方便访问这个实例。
如上图所示,我们通过new关键字创建一个类(Animal)的实例 ,并将这个实例在堆区中的地址存放在dog这个变量中。在Java中,我们将这种存放实例地址的变量,叫做对象的引用。
注:在java中,引用虽然与C语言中指针的使用方式不同,但是它们在底层实现上,可能都是通过存放某个对象的地址,从而避免创建多个冗余变量空间,实现快速访问与便捷操作。
注意事项:
- new 关键字用于创建一个对象的实例.
- 使用 . 来访问对象中的属性和方法.
- 同一个类可以创建多个实例.
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)特性
- this的类型:对应类类型引用,即哪个对象调用就是哪个对象的引用类型。
- this只能在"成员方法"中使用。
- 在"成员方法"中,this只能引用当前对象,不能再引用其他对象。
三、对象
1、初始化对象
众所周知,在Java中定义一个局部变量时,必须要对它进行初始化,否则会编译失败。
此时,只需要在调用变量a之前,为a赋一个初始值即可解决问题。
在这串代码中,我们观察到,在Date类的实例未初始化时,我们调用了printDate()方法,但他并未报错,反而得出结果“0-0-0”。这是什么原因呢?
实际上,在Java中,有一种特殊的成员方法——构造方法。正是它导致这里不会报错。
2、构造方法
(1)定义
构造方法是一个特殊的成员方法,他的方法名必须与类名相同,在创建对象时,由编译器自动调用,并且在整个生命周期内仅调用一次。
有这段代码可知,当我们自定义了一个构造方法,那么当我们为这个类创建实例时,就要按照构造方法的传参结构,向其中传参,令实例中的成员属性被初始化。在控制台也可以观察到“Date(int,int,int)方法被调用了” 这句话被成功打印,说明了Class_object构造方法被调用。
有人又要问了,那你倒是说啊,为什么实例中的属性不用初始化就可以调用?
别急,这就来了。
有着串代码可知,即使构造方法无参数 ,仍然被调用。实例中的成员属性“不用初始化”的原因也在这。
在创建一个对象时,如果用户未自定义一个构造方法,编译器就会默认生成一个不带参数的构造方法,将实例中的成员属性进行初始化赋值。
几大基本数据类型的默认初始化为以下值:
数据类型 | 默认值 |
byte | 0 |
chr | '\u0000' |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
boolean | false |
reference | null |
(2)特性
- 名字必须与类名相同。
- 没有返回值类型,设置为void也不行。
- 创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次(相当于人的出生,每个人只能出生一次)。
- 构造方法可以重载(用户根据自己的需求提供不同参数的构造方法。
- 如果用户没有显式定义,编译器会生成一份默认的构造方法,生成的默认构造方法一定是无参的。
(3)通过this调用其他构造方法
前面我们知道了构造方法,可以帮助我们对对象的成员属性进行初始化,就算你在声明成员变量时就地初始化,编译器仍会将初始化语句添加到构造方法中。
而使用this调用构造方法的主要目的是为了代码重写和简化构造方法的实现。通过这种方式,可以避免在多个构造方法中重复相同的初始化代码,从而提高代码的可维护性和可读性。
这串代码中,创建的Date实例并未传参,其中的成员属性却已进行了初始化,这是为什么?
在Date的第一个构造方法(无参)中,我们使用了“this(1900,1,1)”这个语句。此时,它实际上是一个指向Date中的第二个构造方法(有三个参数),并将参数传递给第二个构造函数。
A、避免重复劳动
-
问题:当一个类有多个构造方法时,不同构造方法可能有相同的初始化步骤(例如参数校验、字段赋值)。
-
解决:将公共逻辑写在一个“主构造方法”中,其他构造方法通过
this()
调用它,避免重复编写相同代码。
B、强制关键逻辑:确保一致性
-
问题:如果初始化时需要执行强制步骤(例如参数合法性检查、资源分配),每个构造方法都要重复这些逻辑,容易遗漏。
-
解决:通过
this()
强制所有构造方法最终调用同一个主构造方法,确保关键逻辑必执行。
C、 灵活初始化:支持多种参数组合
-
问题:用户创建对象时可能需要不同参数组合(例如全参数、部分参数、无参数),逐个编写构造器会很繁琐。
-
解决:通过
this()
链式调用,用少量构造器覆盖多种参数情况,提供灵活的初始化方式。
四、封装
1、概念
在面向对象的编程语言中,有三大特性,即封装、继承、多态。而在类和对象阶段,我们主要研究的是封装特性。
对于继承和多态,小编也写过对应的文章,感兴趣的朋友可以移步:
什么是封装?简单来说,就是套壳——通过外壳,隐藏内部的细节。
比如,一部手机,大多数人使用它,只会浮于表面,只接触它的屏幕、边框、机身、摄像头、Type--c接口或USB接口、耳机接口、扩音孔、麦克风。
而实际上,在手机的内部,还有很多我们不能直接看见的部件,例如——天线、电池、cpu、主板等,包括触屏、摄像这些功能的实现,都是在手机内部进行的,我们难以直接观察到。
封装,正是如此,将不想被他人访问的数据或方法用访问限定符修饰起来,将其封装在类中,通过开放方法调用权限,用特定的方法调用封装起来的数据。就像手机,开放了如充电口、摄像头使用权、查看本机信息等“接口”,用户只需要通过开放的接口调用对应方法,而不需要管方法的具体实现,大大方便用户的使用体验和便捷程度。
再举个例子,如图所示,你想访问银行的金融系统。银行一般会允许你查看自己余额、存入金额、取出金额,在存取金额时,银行一般会对信息进行校验,只有符合一定的条件,才能通过系统更改余额信息。这样做,可以有效防止不法分子非法修改加密信息。
请看,在这个“银行金额管理系统”中,用户的年龄、姓名以及剩余金额,属于私有数据,只用相关人员有修改权限,个人不可随意修改。
当我们通过Data类直接调用时,编译器直接报错,显示某某具有“private”访问权限,而通过Data类中权限开放的方法,我们可以查看和更改数据。
当用户进行非法访问,系统就会终止方法的进程,并提示错误。
可以看到,银行金额剩余是10000元,而zhang想要取出100000元,这是典型的非法操作,于是系统弹出提示——“用户试图进行非法操作” ,并终止方法运行进程。
总结:
封装的本质:封装就是将数据和方法打包成一个整体,对外隐藏实现细节,只暴露必要的接口。
就像一个保险箱,内部存放贵重物品(数据)和复杂的加密机制(实现逻辑)。外部只留一个钥匙孔(公共方法)让用户操作,用户不需要知道保险箱的内部结构,只需通过指定方式存取物品。
封装的核心作用:
- 保护数据安全:通过
private
字段 + 公共方法(如setAge()
)控制访问,添加校验逻辑。- 隐藏实现细节:即使内部逻辑变化,外部调用不受影响。
- 提高代码可维护性:外部代码不依赖内部结构,修改内部实现不影响其他模块。
- 简化复杂操作:封装成一个方法,隐藏复杂度。
封装的实现方式:
使用访问修饰符:private(私有)、protected、default(默认)、public(公共)。
提供公共方法:用public修饰方法,使其成为一个公共接口,用户通过公共方法调用或修改成员属性。
暴露行为,隐藏状态:优先通过方法描述对象的行为,而非直接暴露数据。
2、访问限定符
相信很多朋友已经观察到了,不论是在介绍类、介绍this引用还是介绍封装时,我在创建类中的成员属性时,都是用private修饰的,而创建的方法都是用public修饰的。这些就是“访问限定符”,是用来设定成员属性和方法的访问权限的。详情请见下表:
top | 范围 | private | default | protected | public |
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);
}
}
注意:
- 过度使用会导致代码含义不清晰(例如大量静态导入后,可能难以区分
assertEquals
是来自JUnit
还是自定义类)。 - 建议仅在频繁使用工具类的静态方法时或测试代码中简化断言时,使用静态导入。
3、自定义包
这里使用的是 intellij IDEA。
首先,在工程中选择scr,单击右键,选择新建,选择软件包。
然后为包命名,通常用公司万维网域名的倒置来命名。写好名称后,单机回车键,创建一个三层嵌套的包。
如下图所示。如果你的包没有形成这种多层嵌套关系,可能是因为默认打开了“压缩空的中间软件包”选项。
这时,只需要左键单击“项目”窗口右上角的三个点图标,点击外观,取消勾选“压缩空的中间软件包”选项,这样就可以了。
接着,你想要在哪个包中创建Class文件,就右键单击它,然后选择新建,选择java类。
输入除关键字外的任意类名,最好有辨识性,具有一定意义。单机回车键,创建一个Class文件。
注意:根据书写规范,类名首字母应该大写。
4、常见的包
- java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
- java.lang.reflect:java 反射编程包;
- java.net:进行网络编程开发包。
- java.sql:进行数据库开发的支持包。
- java.util:是java提供的工具程序包。(集合类等) 非常重要
- 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、构造代码块
构造块:定义在类中的代码块(不加修饰符)。也叫:实例代码块。构造代码块一般用于初始化实例成员变量。
实例代码块(即非静态初始化块)会在每次创建对象时自动执行,且执行顺序在构造方法之前。代码块中的 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)多个静态代码块
如果有多个静态代码块或静态变量,按代码中的书写顺序执行。
编写不易,都是我学习时的感悟,也是查了些资料,可能有地方说的不是很准确,还请各位大佬指正。