深浅模式
类
在 Java 中,类和对象是最核心的一组概念。但它本质上不只是语法,而是一种看待问题和组织代码的方式。
我们写程序,本质是在“描述世界”,而类,就是我们对某一类事物的抽象定义。可以用一个更贴近直觉的例子来理解:
- 类像是“狼的设定图”
- 对象才是“真正存在的一只只狼”
设定图里写着:有毛色、年龄、体型,也定义了会嚎叫、会奔跑。但设定图本身不会动,只有真正创建出来的狼才是活的。
类的组成
在 Java 中,一个类主要由两部分组成:
- 成员变量:描述“这只狼有什么特征”
- 成员方法:描述“这只狼能做什么”
比如:
java
class Wolf {
int age; // 年龄
String color; // 毛色
public void howl() {
System.out.println("嗷——");
}
}这里把“狼”这个概念拆成了一组 数据 + 行为。
类的定义,本质上就是在用程序的方式描述一类事物:它有什么,它能做什么。
从类到对象
有了类这个“设定图”,就可以创建具体的对象。基本用法如下:
java
// 创建对象
类名 对象名 = new 类名();
// 调用方法
对象名.方法名(参数);
// 访问属性
对象名.属性名;套用我们刚刚创建的 Wolf 类 :
java
Wolf wolf = new Wolf();这行代码做了两件事:
- 创建了一只真正的“狼对象”(数据本体)
- 定义了一个变量
wolf,用来后续操作这只狼
如果此时打印这个对象名 wolf ,就能看到其地址值 Wolf@3f99bd52
这个内容并不是对象的具体数据,而是一个地址标识。
Wolf表示类名(对象属于哪一类)@后面的内容,是对象在内存中的标识值(类似地址)
如果是带包的完整写法,这一部分还会显示全类名(包名 + 类名),例如:
java
com.example.demo.Wolf@3f99bd52用于区分不同位置、不同来源的类。
类和对象内存
创建对象之后,就可以通过变量 wolf 来访问对象中的属性和方法:
java
wolf.age = 3;
wolf.howl();程序里操作对象时,表面上是在用 wolf,实际上是通过 wolf 去找到后面 new Wolf(); 出来的对象。
从内存角度来看,可以简单理解为:
- 栈内存中存放变量
wolf - 堆内存中存放真正创建出来的对象
wolf里保存的是这个对象的位置
text
wolf(栈) → 狼对象(堆)所以,wolf.age = 3; 和 wolf.howl(); 这样的写法,本质上都是先通过 wolf 找到对象,再去访问对象中的数据或调用对象的方法。
这样设计后,变量本身只需要保存一个位置,不需要把整个对象都放进去。
对象的数据统一放在堆内存中,而变量只负责记录“找到它的方法”。
这就是类和对象在内存中的基本关系。
成员变量与局部变量
前面在 Wolf 类里写过这样的内容:
java
class Wolf {
int age;
String color;
public void howl() {
System.out.println("嗷——");
}
}这里的 age 和 color,就属于成员变量。
- 成员变量,描述的是“对象长期拥有的状态”
- 局部变量,描述的是“某段代码临时使用的数据”
| 区别 | 成员变量 | 局部变量 |
|---|---|---|
| 定义位置 | 类中,方法外 | 方法中、方法参数中 |
| 属于谁 | 属于对象 | 属于方法 |
| 默认值 | 有默认值 | 没有默认值,必须先赋值再使用 |
| 生命周期 | 随对象创建而存在,随对象消失而消失 | 随方法调用而创建,方法结束就消失 |
| 存储位置 | 一般在堆中,跟着对象一起存在 | 一般在栈中,方法运行时临时存在 |
| 作用范围 | 在整个类中都可以使用 | 只能在当前方法或代码块中使用 |
这个区别非常重要。别看它们都叫“变量”,但职责根本不是一回事。
java
class Wolf {
int age = 3; // 成员变量
String color = "灰色"; // 成员变量
public void run() {
int speed = 20; // 局部变量
System.out.println("狼正在奔跑,速度是:" + speed);
}
}成员变量就算你不手动赋值,Java 也会先给它默认值;但局部变量没有这个待遇,必须先赋值才能使用。
this 关键字
有时候我们会遇到成员变量和局部变量重名,先看这个例子:
java
class Wolf {
int age = 3;
public void setAge(int age) {
age = age;
}
}这段代码看起来像是在“把参数赋值给成员变量”,但实际上什么都没发生。
因为这里的 age:
- 方法参数里的
age是局部变量 - 方法内部写的
age,优先访问的也是局部变量
也就是说,这一行:
java
age = age;本质是“把局部变量赋值给自己”,成员变量根本没被改到。问题就出在:两个同名变量冲突了,但程序默认只认离得更近的那个(局部变量)。
这时候,this 就该登场了。
java
class Wolf {
int age = 3;
public void setAge(int age) {
this.age = age;
}
}this.age表示“当前对象的成员变量 age”- 右边的
age是方法参数(局部变量)
这样就把两者明确区分开了。可以把 this 理解成:“当前这只对象自己” 。
也就是:this ≈ 当前对象
基本用法
this 主要用来调用“本类中的成员”,包括变量和方法:
java
this.成员变量
this.成员方法()例如:
java
class Wolf {
int age = 3;
public void show() {
this.howl();
System.out.println(this.age);
}
public void howl() {
System.out.println("嗷——");
}
}核心就一个场景:当成员变量和局部变量重名时,必须用 this 区分
java
public void setAge(int age) {
this.age = age;
}不写 this,你就永远改不到成员变量。如果没有重名问题,this. 是可以不写的。
java
class Wolf {
int age = 3;
public void show() {
System.out.println(age); // 默认就是 this.age
howl(); // 默认就是 this.howl()
}
public void howl() {
System.out.println("嗷——");
}
}这里其实编译器帮你做了补全:
java
System.out.println(this.age);
this.howl();只是平时可以省略。
this 的作用就是:明确指出“当前对象的成员”,尤其是在变量重名时用来区分成员变量和局部变量。
如果再换成更直观一点的理解:
- 不写
this:优先用“眼前这个变量”(局部变量) - 写了
this:强行指向“这只对象身上的那个变量”(成员变量)
封装
封装(Encapsulation)是面向对象的第一大特性,简单来说,就是:
合理隐藏,合理暴露
- 把相关的数据和操作这些数据的方法打包在一起
- 对外隐藏实现细节,只公开必要的接口
就像手机一样,只需要知道怎么点屏幕,不需要知道内部电路怎么工作。
从过程式到面向对象的转变
为什么需要封装?让我们用一个计算薪资的例子来对比:
过程式风格
先来看一种更偏过程式的写法:
java
public class SalaryDemo {
public static void main(String[] args) {
// 数据散落各处
int baseSalary = 5000;
int bonus = 10000;
char grade = 'B';
// 独立的方法处理数据
int salary = calculateSalary(baseSalary, bonus, grade);
System.out.println(salary);
}
public static int calculateSalary(int baseSalary, int bonus, char grade) {
double rate = switch (grade) {
case 'A' -> 1.0;
case 'B' -> 0.8;
case 'C' -> 0.6;
case 'D' -> 0.4;
default -> 0;
};
return baseSalary + (int)(bonus * rate);
}
}在这种写法中,数据和逻辑是分开的。
baseSalary、bonus、grade 这些数据放在外面,计算工资的方法单独写在另一边。每次调用时,都要把这些参数一个个传进去。
代码虽然直白,但不够紧凑,维护起来容易出错。
面向对象风格
换成面向对象的写法,思路就不一样了。
- 先把员工相关的数据和行为放进同一个类里:
java
// 定义员工类
public class Employee {
// 数据(属性)和方法放在一起
int baseSalary;
int bonus;
// 计算薪资的方法直接访问类内部的数据
public int calculateSalary(char grade) {
double rate = switch (grade) {
case 'A' -> 1.0;
case 'B' -> 0.8;
case 'C' -> 0.6;
case 'D' -> 0.4;
default -> 0;
};
return baseSalary + (int)(bonus * rate);
}
}这里最关键的变化在于:原本分散在外面的数据和处理逻辑,被放进了同一个对象里。
不过,这一步还只是把“员工”这个类型定义出来,还没有真正产生一个可以使用的员工对象。
也就是说,类写完之后,事情还没结束,还得继续创建对象并使用它。
- 再使用这个员工类:
java
public static void main(String[] args) {
// 创建员工对象
Employee employee = new Employee();
// 设置属性值
employee.baseSalary = 5000;
employee.bonus = 10000;
// 调用方法计算薪资
int salary = employee.calculateSalary('A');
System.out.println(salary);
}这种写法看起来更像是在和对象打交道,而不是在手动拼装一堆参数。调用者不需要操心任何细节,只需要设置参数,然后问对象:"你自己的工资是多少?"
所以,封装不只是“把东西塞进类里”这么简单。更准确地说,它是在做两件事:
- 让数据和操作这些数据的行为尽量放在一起
- 让外部通过规定好的方式使用对象,而不是直接干预对象内部细节
说到底,封装的目的就是:
让使用者只看到"操作界面",而不用在意"内部电路"。
这不仅仅是代码风格问题,而是软件设计中组织复杂性的一种武器。
Getter 和 Setter
然已经把数据和行为放进了同一个类里,但还有一个问题:对象内部的数据依然可以被外部随意更改,甚至出现不合理的值。
比如下面这样:
java
employee.baseSalary = -5000;从语法上看是没问题,但从业务上看就很离谱了。基本工资显然不应该是负数。
光把数据放进类里还不够,如果外部依然能直接乱改,那封装就还没做完整。比较常见的做法是两步:
- 将属性设为私有(private),不让外部直接访问
- 提供公开的方法来访问和修改这些私有属性
java
public class Employee {
// 私有化属性,外部不能直接访问
private int baseSalary;
private int bonus;
// 提供设置基本工资的方法,可以添加验证逻辑
public void setBaseSalary(int baseSalary) {
// 添加验证逻辑
if (baseSalary < 0) {
System.out.println("基本工资不能为负数!");
return;
}
// 通过this关键字区分成员变量和参数
this.baseSalary = baseSalary;
}
// 提供获取基本工资的方法
public int getBaseSalary() {
return baseSalary;
}
// 同样方式处理bonus属性
// ...
}当 baseSalary 被 private 修饰后,外部就不能再直接赋值,而是只能通过 setBaseSalary() 来修改。这样一来,我们就可以在方法里加上判断逻辑,把不合理的数据拦住。
这就是 getter 和 setter 的意义,给字段的访问加上一道控制。
- getter 用来获取数据
- setter 用来修改数据
顺带一提,这里的:
java
this.baseSalary = baseSalary;左边的 this.baseSalary 是成员变量,右边的 baseSalary 是方法参数。this 表示当前对象,用来区分成员变量和同名的局部变量。
字段与属性
写到这里,有两个词很容易混:字段(Field) 和 属性(Property)
字段(Field)
先说字段。字段就是类中直接声明的成员变量,例如:
java
public class Student {
private String name; // 这是字段
private int age; // 这也是字段
public String school; // 这还是字段
}这里 name、age、school 都是字段。只要是类里定义的成员变量,都可以叫字段。
属性(Property)
站在“对外访问”的角度去看,通常把有对应 getter / setter 方法的字段,称为属性。例如:
java
public class Student {
private String name; // 字段
// getter 和 setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}这里的 name 首先是一个字段;
同时,因为它有 getName() 和 setName() 这组访问方法,所以通常也会把它看作一个属性。
所以这两个词的关系可以简单记成:
- 字段:类里定义的成员变量
- 属性:对外提供访问方式的字段
不是所有字段都会被当成属性,但属性背后一般都对应着字段。
标准 JavaBean
当一个类专门用来封装数据,并且写法存在一定规范时,这种类通常就叫 JavaBean。
它本质上还是一个普通的 Java 类,只是大家约定了一套更标准的写法。要求有如下三个:
- 字段使用 private 修饰
- 提供对应的 public getter / setter 方法
- 提供一个无参构造方法
一个标准的 JavaBean 示范:
java
public class Student {
// 1. 私有字段
private String name;
private int age;
// 2. 无参构造器
public Student() {}
// 3. 公共访问器
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;
}
}这个类只是把“私有字段 + 公共访问方法 + 无参构造器”这套写法固定下来。
所以你可以把 JavaBean 理解成一种更规范的封装写法,只是更符合约定,特别适合拿来表示一组数据。
构造方法
当我们使用new关键字创建对象时,其实是在调用 构造方法 来创建对象:
java
Wolf wolf = new Wolf(); // 调用了构造方法所谓构造方法,就是把对象出生时要执行的一段初始化逻辑。比如,一只狼刚被创建出来的时候,可以顺手给它一些初始状态:
java
public class Wolf {
int age;
String color;
public Wolf() {
age = 1;
color = "灰色";
}
}当你 new Wolf(); 时,实际上会发生两件事:
- 在堆中为对象分配空间
- 成员变量先进行默认初始化
- 执行构造方法完成进一步初始化
也就是说,这只狼从类变为对象时,就已经有了默认的年龄和毛色。
无参构造方法
如果你不写什么构造方法,类同样能够创建对象。是因为 Java 会帮你补一个默认的构造方法:
java
public class Wolf {
// Java 会自动生成如下的构造方法:
// public Wolf() { }
}这样你依然可以正常创建对象。
但要注意:
一旦你自己写了构造方法,Java 就不会再帮你补默认的了。
所以如果你写了别的构造方法,还需要保留无参构造的创建方式,就必须自己显式写出来。
有参构造方法
刚才这个写法有一个问题:
Java
public Wolf() {
age = 1;
color = "灰色";
}这个模板已经被赋予了初始值。因此当你每次 new 来创建出来的狼,数据都是一样的。
这显然不足以应对现实多变的情况,这时候就可以让构造方法接收参数:
java
public class Wolf {
int age;
String color;
public Wolf(int age, String color) {
this.age = age;
this.color = color;
}
}再创建对象时,就可以直接把数据带进去:
java
Wolf wolf = new Wolf(3, "黑色");这样,在需要创建多个对象时,比 对象.属性 的初始化方式方便很多。对象在创建的同时就完成了初始化,代码会更集中,也更不容易漏掉某个字段。
至此,再来总结构造方法的特征:
- 方法名必须和类名完全一致(包括大小写)
- 没有返回值类型(连 void 都不能写)
- 不能通过 return 返回数据
构造方法会在每次 new 对象时,都会自动执行一次。
java
public class Wolf {
public Wolf() {} // 构造方法
public void Wolf() {} // 普通方法(不推荐这样写)
}权限修饰符
在前面做封装时,我们把字段改成了 private:
java
private int baseSalary;是为了让这个数据能不能被外部直接访问。在 Java 中,这种控制访问范围的方式,就叫权限修饰符。
Java 提供了四种权限修饰符:
| 修饰符 | 说明 |
|---|---|
| private | 只能在当前类中使用 |
| 默认(不写) | 同一个包中可以使用 |
| protected | 子类中也可以使用 |
| public | 所有地方都可以使用 |
private 最严格,public 最开放,中间逐渐放开。目前只用简单了解这两条
- 字段一般用
private(不让外部乱改) - 对外提供的方法用
public
Static 关键字
前面在学习类和对象时,我们接触到的变量,大多都是“属于对象”的。
实例变量与类变量
如果一个变量没有 static,那它就是普通成员变量,也被称作 实例变量,实例变量是属于对象本身的。
java
public class Wolf {
int age;
}这里的 age 就是实例变量,属于每一只具体的狼。如果创建两个对象,它们都会有各自的 age。
像年龄、名字、毛色这些数据,本来就是每个对象各自不同的状态,自然应该各存各的。
但有些数据并不是每个对象都要单独保存一份。
比如狼的种类,如果一批对象本来就都属于同一种狼,那么“种类”这个信息对每个对象来说都是一样的。
这时候如果还放在每个对象里,就会重复。
这时候就可以用 static 修饰这个变量,让同类的对象共享:
java
public class Wolf {
static String species = "雪原狼";
}这里的 species 就不再属于某一只具体的狼,而是属于 Wolf 这个类。
不管创建多少个 Wolf 对象,这个变量都只有一份,大家共用。
这种被 static 修饰的变量,就被称为 类变量。
- 实例变量:属于对象,每创建一个对象,就会有一份自己的数据
- 类变量:属于类本身,所有对象共享同一份数据
所以,static 的作用之一,就是把变量从“属于对象”变成“属于类”。
实例方法与类方法
变量有属于对象的,也有属于类的,方法也一样。前面接触到的方法,大多都是属于对象的,调用前通常要先创建对象:
java
public class Wolf {
public void howl() {
System.out.println("嗷——");
}
}
// 通过类创建出具体的对象再调用
Wolf wolf = new Wolf();
wolf.howl();这是我们之前常用的方式,也被称之为 实例方法。
实例方法依附于对象,调用前必须先 new。它既能访问实例变量,也能访问类变量,并支持使用 this 引用当前对象。
因为 howl() 这种行为,是由某一只具体的狼发出的。它依赖对象,所以应该属于对象。
但不是所有方法都一定要依赖对象。
比如现在有一个需求:写一个“攻击力计算器”,专门根据等级计算攻击力。
先按前面学过的方式写,可能会是这样:
java
public class AttackCalculator {
public int calcAttack(int level) {
// 计算攻击力的方法
return 攻击力值;
}
}因为方法属于对象,那么首先得创建对象才能使用:
java
AttackCalculator calculator = new AttackCalculator();
int attack = calculator.calcAttack(5);
System.out.println(attack);这段代码当然能运行,但这里的 calculator 本身并没有什么对象状态。
我们创建它,不是为了保存数据,而只是为了调用一次 calcAttack() 方法。
明明只是想用一个功能,却被迫先造出一个对象,这就有点别扭了。
问题的根源在于:calcAttack() 并不依赖某个具体对象。
它只需要一个 level,就可以完成计算。
像这种“不依赖对象状态,只负责完成一段独立逻辑”的方法,就更适合写成 类方法,也就是用 static 修饰的方法。
java
public class AttackCalculator {
public static int calcAttack(int level) {
// 计算攻击力的方法
return 攻击力值;
}
}这样一来,调用时就不需要再 new 对象了:
java
int attack = AttackCalculator.calcAttack(5);
System.out.println(attack);这就是 static 修饰方法后的变化:
- 实例方法:属于对象,调用前通常要先创建对象
- 类方法:属于类本身,可以直接通过类名调用
所以,如果一个方法不依赖对象状态,只是提供一段通用规则或功能,就可以考虑写成 static。
重识 main 方法
这时候再回过头看看最初的 main 方法:
java
public static void main(String[] args) {
}之所以要写成 static,就是因为程序启动时,JVM 需要直接通过类找到入口方法并执行,让 main 方法可以脱离对象存在。
- public:
main方法要被 JVM 调用,所以访问权限必须足够大。 - void:表示这个方法没有返回值。
- main:
main不是关键字,而是一个普通的方法名。但这个名字又普通的比较特殊,因为 JVM 会把它识别为程序入口方法的名字。
正因为 main 是静态方法,所以 main 中没有 当前对象。因此,如果不手动创建对象,就只能直接调用同样不依赖对象的静态方法。
例如下面这样就可以直接调用:
java
public class Demo {
public static void main(String[] args) {
show();
}
public static void show() {
System.out.println("测试");
}
}如果 show() 不是静态的,那就必须先创建对象才能调用。
String[] args是 JVM 在启动程序时,传给 main 方法的“命令行参数”。
作为拓展,可以看一个例子。假设在命令行中这样运行程序:
java
java Demo hello world 123那么 JVM 可以理解成是在调用:
java
Demo.main(new String[]{"hello", "world", "123"});命令行中写在类名后面的内容,会被按顺序放进 args 数组中。
打印出来看看:
java
public class Demo {
public static void main(String[] args) {
System.out.println("第一个参数:" + args[0]);
}
}就会在控制台输出:
第一个参数:hello所以,String[] args 的作用就是:让程序在启动时能够接收外部输入。
不过在日常开发中,这个参数通常用得不多。因为现在大多数程序都是直接在 IDE 中运行,很多时候并不会专门通过命令行传入参数。
工具类
如果一个类里的方法大多都是这种“不依赖对象、直接通过类名调用”的方法,那这个类往往就不是拿来描述某个具体事物的,而是拿来提供一组通用功能的。
这种类,通常就会被整理成 工具类。
例如这段伪代码:
java
public class MathUtil {
public static int add(int a, int b) {
// 返回两个整数相加的结果
}
public static int max(int a, int b) {
// 返回两个整数中较大的那个
}
}使用时直接通过类名调用即可:
java
int sum = MathUtil.add(5, 3);
int bigger = MathUtil.max(10, 20);不过,为了与普通类区分,工具类都会被命名成 XxxUtil/XxxHelper 等。名字本身不是语法要求,但最好一眼就能看出“这是一个拿来直接用功能的类”。
除此之外,为了避免别人误把工具类当成普通类去创建对象,通常还会把构造方法私有化。
java
public class MathUtil {
private MathUtil() {}
public static final double PI = 3.14159;
public static int add(int a, int b) {
// 返回两个整数相加的结果
}
public static int max(int a, int b) {
// 返回两个整数中较大的那个
}
}如果有一些固定不变的数据,也常常写成 static final
至此,工具类的常见设计可以概括成:
- 大多数方法用
static修饰 - 构造方法私有化,防止外部创建对象
- 类中通常放的是通用功能,而不是对象状态
这里有一个小 tips,辨识类的修饰符和构造方法的修饰符:
java
public class MathUtil {
}此处的 public 类是允许“别人能用这个类”。
java
public class MathUtil {
private MathUtil() {}
}而类中的 private 构造方法是限制“不能 new”。

评论