02对象与类

第 4 章 对象与类
4.1 面向对象程序设计概述
这一节只讲一个核心:C 把数据放在 struct、把函数写在外面,Java 则把二者合并进类中。
先看 C 的写法:
struct Employee { char name[32]; double salary;};
void raise_salary(struct Employee *e, double pct) { e->salary *= (1 + pct / 100);}数据在 struct 里,操作数据的函数在外面,两边唯一的联系是这个函数碰巧收了个 Employee 指针。Java 把这两半塞进同一个 class:
class Employee { private String name; private double salary;
public void raiseSalary(double pct) { salary *= 1 + pct / 100; } public String getName() { return name; }}类是构造对象的模板,对象是类的实例;字段保存状态,方法描述行为;封装则通过访问权限隐藏实现细节。
-
对象:类是定义,对象是具体实例。每个对象拥有独立的实例字段。
a == b比较的是身份(是不是同一个对象),不是内容(字段值是否相同)——两个字段完全相同的对象,a == b仍然为false。变量只是保存引用,new才创建对象。 -
要能识别类:可以先把需求中的名词作为候选类或字段,把动词作为候选方法。例如“雇员有姓名和薪水,可以加薪”,可得到
Employee、name、salary和raiseSalary()。 -
类之间的关系:类与类之间只有三种关系。
- 依赖(uses-a):一个类的方法里用到了另一个类。最弱也最常见,例如
Order的某个方法里调用了Employee的方法。书上建议尽量少依赖,去耦合。 - 聚合(has-a):一个对象里装着另一个对象当字段,即「整体-部分」,例如
Order里有个Customer字段。 - 继承(is-a):一个类是另一个类的特殊化,例如
Manageris-aEmployee。这条 Ch5 会整章展开,这里只要认得。
UML 类图的画法:依赖用虚线箭头,聚合在线尾画空心菱形,继承在线尾画空心三角。
- 依赖(uses-a):一个类的方法里用到了另一个类。最弱也最常见,例如
概括来说,C 主要靠约定连接数据和函数,Java 则用类与访问控制把它们组织在一起。
4.2 使用预定义类
对象与对象变量
Java 的对象变量保存的是引用,不是对象本身:
Date deadline = new Date();它可以粗略类比为 C 中的指针和动态分配:Date d 对应指针,new Date() 对应分配并初始化对象,null 对应空指针。Java 引用不能做指针运算,赋值也只复制引用,不复制对象:
Date a = new Date();Date b = a;b.setTime(0);System.out.println(a.getTime()); // 0,指向同一对象若引用为 null,调用方法会抛出 NullPointerException;局部变量未赋值则会在编译期报错。单独写 new Date() 虽然合法,但对象无人引用,最终会被回收。
new Date() 中的 Date() 是构造器,不是普通方法。构造器名称必须与类名相同,不能写返回类型,负责初始化新对象。
LocalDate
旧的 Date 可变且接口混乱。Java 8 的 java.time 提供了更清晰的日期类型:
import java.time.LocalDate;
LocalDate today = LocalDate.now();LocalDate d = LocalDate.of(2007, 12, 3);System.out.println(d.getMonthValue()); // 1~12LocalDate 只表示年月日,不包含时刻和时区。LocalDate.of(...) 是静态工厂方法:类库在内部创建对象并返回引用,因此不需要公开构造器。类类型方法返回的是引用,但不保证一定是新对象。
访问器、更改器与不可变对象
更改对象状态的方法叫更改器(如 Date.setTime),只读的方法叫访问器(如 getTime)。LocalDate 是不可变类,plusDays 不修改原对象,而是返回新对象:
LocalDate d = LocalDate.of(2000, 1, 1);LocalDate e = d.plusDays(100);必须接住返回值;单独写 d.plusDays(100); 是合法但无效果的代码。不可变对象可以安全共享,避免通过同一引用互相修改状态。
4.3 自定义类
4.1 里那个只有骨架的 Employee,这一节补完整——也是第一次自己写类。
- Employee类:完整版长这样:
import java.time.LocalDate;
public class Employee { private String name; private double salary; private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day) { this.name = name; this.salary = salary; this.hireDay = LocalDate.of(year, month, day); }
public String getName() { return name; } public double getSalary() { return salary; } public LocalDate getHireDay() { return hireDay; }
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; }}对照 4.1 里那版 C:
| C | Java |
|---|---|
struct Employee 里放字段 | class Employee 里放字段 |
raise_salary(&e, 5) 外面的函数 | e.raiseSalary(5) 类里的方法 |
| 字段谁都能改 | 字段 private,只能走方法 |
用起来:
Employee e = new Employee("张三", 50000, 2000, 1, 1);e.raiseSalary(5); // 加薪 5%System.out.println(e.getSalary()); // 52500.0- 使用多个源文件:Java 的硬规矩是一个
.java文件里最多只能有一个public类,而且文件名必须和这个类名一模一样。所以Employee存成Employee.java,测试程序(带main的那个)另存EmployeeTest.java:
javac EmployeeTest.javajava EmployeeTest书里强调的一点:编译 EmployeeTest.java 时,javac 发现它用到了 Employee,会自动去同目录找 Employee.java 一起编译,不用手动把两个文件都列上。(它靠比对时间戳判断:Employee.java 比 Employee.class 新就重新编。)
vs C:C 靠「
#include "employee.h"抄声明 + 链接器找实现」两套机制拼装;Java 没有头文件,编译器顺着源码和类路径自己找。同一个目录(默认包)下的类互相引用不需要 import,直接写类名就行。
-
剖析Employee类:一个类就三样东西:
- 实例字段(
name/salary/hireDay)—— 数据,每个对象各存一份 - 构造器(
Employee(...))—— 造对象时初始化字段 - 方法(
raiseSalary等)—— 行为
字段全
private、方法全public,这就是 4.1 说的「封装」落到实处。字段带
static就是「类字段」(所有对象共享一份,4.4 讲),不带就是「实例字段」(每个对象独一份)。这里三个都是实例字段。 - 实例字段(
-
从构造器开始:4.2 前向引用过它,现在正式交代。四条规矩:
- 名字必须和类名一模一样
- 没有返回类型——连
void都不写 - 只能跟着
new用,不能写成e.Employee(...) - 可以重载(同名、不同参数),
new时按实参挑
两个容易踩的点:
① 不写构造器 ≠ 没有构造器。 一个构造器都没写时,编译器自动送你一个无参构造器;但只要你自己写了任意一个(哪怕是有参的),这个免费的就立刻消失——new Employee() 直接编译报错。这条 4.6.3 会再细说。
② this.name = name; 参数名和字段同名时,右边是参数、左边是字段,靠 this 区分。this 就是「正在构造的这个对象」(4.3.7 展开):
public Employee(String name, double salary, ...) { this.name = name; // 左边 this.name 是字段,右边 name 是参数 this.salary = salary; ...}参数名起成 aName 这类不重名的,this. 就能省——但 this.x = x 是 Java 惯用写法,认得它。
顺带把「实参填进哪个门」理清。 构造器只在造对象那一刻被调用一次 ,之后你反复调的是方法——填实参这件事发生两次,进的是两个不同的门:
① 造对象:new Employee("张三", 50000, 2000, 1, 1) ← 实参填给【构造器】 ↓ 对象造好了,存在变量 e 里② 用对象:e.raiseSalary(5) ← 实参填给【方法】术语上,声明里列出的变量叫形参(public Employee(String name, ...) 里的 String name),调用时传进去的值叫实参("张三")。
方法返回什么,由返回类型决定。并不一定返回对象,拿上面那四个方法验一遍:
| 方法 | 返回类型 | 实际返回什么 | 对象变了吗 |
|---|---|---|---|
getName() | String | 一个已有对象的引用(字段里那个) | 没变 |
getSalary() | double | 一个数值,压根不是对象 | 没变 |
getHireDay() | LocalDate | 一个已有对象的引用 | 没变 |
raiseSalary(5) | void | 什么都不返回 | 变了(salary 被改) |
注意 raiseSalary 恰恰是唯一让对象发生变化的那个,而它返回 void。可见「方法改了对象」和「方法返回了新对象」是两条不同的路:
e.raiseSalary(5); // 更改器:返回 void,改自己LocalDate f = d.plusDays(100); // 访问器 + 不可变:返回新对象,自己不动前者是 4.2.3 讲的更改器,后者是访问器+不可变类。两种风格在 Java 里同时存在,别指望统一成一种。
- 用var声明局部变量:Java 10 之后,局部变量可以不写类型:
var e = new Employee("张三", 50000, 2000, 1, 1);// ↑ 编译器从右边推断出 e 的类型是 Employee动机就是省掉重复——老写法里类型出现了两次:Employee e = new Employee(...);。四条限制:
- 只能用在局部变量(方法体里的变量),不能用于字段、方法参数、方法返回类型
- 必须带初始值,
var x;直接编译错误——没有右边就无从推断 - 不是动态类型,类型在编译期就定死了,只是你没写出来
- 右边类型必须明确,
var x = null;也是错的
var e = new Employee(...);e = "字符串"; // ❌ 编译错误,e 已经是 Employee 了var n = 3; // intvar s = "hello"; // Stringvar list = new ArrayList<String>(); // ArrayList<String>vs C++11 的
auto:同一个思路,都是编译期推断。差别是 C++ 的auto用得更野(函数返回类型、结构化绑定、lambda 参数都能用),Java 的var只管局部变量,保守得多;反过来 C++ 的auto会丢掉顶层const和引用,是个老坑,Java 这边没有对应问题(对象本来就都是引用语义)。
实用建议:类型一目了然时才用
var。var e = new Employee(...)挺好;var x = compute();就不好了——读者根本不知道x是什么,不如老实写Employee x = compute();。
- 使用null引用:4.2.1 讲过「变量可能不指向任何对象」,这里讲怎么应对:
String n = null;n.length(); // 运行时抛 NullPointerException三种做法:
// ① 显式判空:能用,但只保护了这一处if (n != null) { System.out.println(n.length());}
// ② 入口处立即失败public Employee(String name, ...) { this.name = Objects.requireNonNull(name, "name 不能为 null");}
// ③ 给个兜底值(Java 9+)Objects.requireNonNullElse(n, "无名")Objects 是 java.util.Objects,一个纯工具类,方法全是静态的。
②这种做法叫 fail fast:与其让这个 null 飘到很远的地方才炸、报错现场和出事现场隔了十万八千里,不如在入口就拦下来,让报错直接指向真正的源头。书里推荐的就是这种。
vs C:
char *p = NULL;strlen(p); /* 段错误,或者更糟——未定义行为,静默破坏别处内存 */同样是空指针解引用,C 不给你任何交代:运气好当场崩,运气不好带病跑完、在别的地方炸。Java 必定抛 NullPointerException,还带完整堆栈,直接告诉你哪一行。
null是 Java 被诟病最多的一处设计(Tony Hoare 自己称之为「十亿美元错误」)。现代 Java 在补这个漏:Optional(卷二讲)、Objects这套工具、以及「不要返回 null 数组/集合,返回空集合」的约定。现阶段记住两条就够——别随便返回 null、参数该查就查。
- 隐式参数与显式参数:看这一句:
e.raiseSalary(5);// │ └─ 显式参数:括号里的 5// └─ 隐式参数:点号前面的 e,也就是方法体里的 this显式参数(explicit parameter) 就是括号里列出来的那个 5,也就是方法声明里写明的参数;隐式参数(implicit parameter) 是点号前面的 e——方法所属的那个对象,在方法体里它就是 this。所以在 raiseSalary 的方法体里:
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; // 这两个 salary 其实都是 this.salary 的简写}这里要分两种情况说清楚,否则很容易以为 this 是多余的一遍:
① 没有同名变量时,salary 和 this.salary 完全等价,写不写随你。上面 raiseSalary 里参数叫 byPercent、不叫 salary,所以裸写的 salary 就是字段。
② 一旦出现同名的参数或局部变量,情况就反过来了——它们会遮蔽(shadow) 字段,裸写的那个名字优先指离得近的那个:
public class Employee { private String name; // ← 实例字段:写在类的头上,每个对象一份 private double salary;
public Employee(String name, double salary) { // ← name / salary 是参数 System.out.println(name); // 打印的是【参数】 System.out.println(this.name); // 打印的是【实例字段】(此刻还是 null)
name = name; // ❌ 参数赋给自己,实例字段纹丝不动 this.name = name; // ✅ 右边是参数,左边 this.name 才是实例字段 this.salary = salary; // 同理,带 this. 的那个才是字段 }}关键在于头两行 private String name; 和下面参数表里的 String name 是两个不同的东西——它们只是名字撞了。这就是遮蔽会发生的前提。
所以 this 不是”把字段名多写一遍”,而是字段被同名变量挡住时,唯一能指到它的路径。这也正是 this.name = name; 左边必须带 this 的原因——不带的话,左边那个 name 同样指参数,整句等于什么都没干。
注意区分「类变量」。
private String name;这种没有static的,叫实例字段(instance field)——每个对象各有一份。Java 里的「类变量」是专有词,只指带static的字段:它属于类本身,所有对象共享一份。区分方法只有一条——看有没有static,跟”写没写在类里面”完全无关。类变量 4.4 讲。
vs C 才是关键——同样的逻辑,C 里两个参数都写在括号里:
raise_salary(&e, 5); /* struct Employee *e 和 double pct 都是显式参数 */e.raiseSalary(5); /* e 被挪到了点号前面,变成隐式参数 */Java 做的事就是:把第一个参数从括号里提到了点号左边,让它「隐」起来。反过来说,e.raiseSalary(5) 用 C 的写法展开就是 raiseSalary(e, 5),只是 e 被放到了前面。
这就是「面向对象」在调用语法上的体现:数据(e)和方法(raiseSalary)绑在一起,不必每次手动把对象当第一个参数传进去。书里专门用这两个术语,是为了让你看清「对象方法其实多带了一个隐藏参数」。
this还有个用法是this(...)——在构造器第一行调用本类另一个构造器,4.6.6 讲。
-
封装的优点:字段私有 + 给方法访问,换来四样好处。
- 内部实现想怎么改就怎么改:把
name拆成firstName/lastName,只要getName()还返回拼好的全名,外面几十处调用一行都不用动。换成 C 那种公有struct成员,字段一改,所有使用方全得改。 - setter 里能加校验:公有字段拦不住
e.salary = -99999;但方法可以。 - 只给 getter、不给 setter,就是只读字段
- 可以惰性计算:getter 里现算,不用提前存
- 内部实现想怎么改就怎么改:把
public void raiseSalary(double byPercent) { if (byPercent < 0) throw new IllegalArgumentException("加薪比例不能为负"); ...}但这里有个必须知道的坑:访问器不要返回可变对象。
class Employee { private Date hireDay;
public Date getHireDay() { return hireDay; // ❌ 把内部状态的控制权交出去了 }}Employee e = ...;Date d = e.getHireDay();d.setTime(0); // 直接从外面改掉了 e 的内部状态,e 一点没察觉getHireDay 返回的是引用(引用共享,4.2.1 讲过),对方拿到就能改你的内部数据。两种修法:
// 修法一:字段直接用不可变类型private LocalDate hireDay;public LocalDate getHireDay() { return hireDay; } // LocalDate 改不了,安全
// 修法二:返回一份副本public Date getHireDay() { return (Date) hireDay.clone(); // 每次给的是拷贝,随便改}这就是 4.2.3 讲「不可变」的实战回报——不可变对象可以放心交出去。
- 基于类的访问权限:Java 的
private是类级别,不是对象级别——同一个类里的代码,可以访问该类任何对象的私有成员:
public class Employee { private String name;
public boolean hasSameName(Employee other) { return name.equals(other.name); // 直接摸 other 的私有字段,合法 }}other 到底是哪个对象?取决于调用时传进去的是谁:
Employee a = new Employee("张三");Employee b = new Employee("张三");Employee c = new Employee("李四");
a.hasSameName(b); // this = a,other = b → truea.hasSameName(c); // this = a,other = c → false这正好是 4.3.7 的那对术语:this 是隐式参数(点号左边那个),other 是显式参数(括号里那个)。两者都是参数,只是填进去的时机不同。
所以「类级权限」的意思是:同一个类的代码,能摸到任何一个 Employee 对象(不只是 this)的私有字段;换成别的类,即便拿到同一个对象,也不许碰。
vs C:C 没有「类」这层封装单位,最接近的是
static——文件作用域,同一个.c文件里的函数能访问文件里的static变量。所以 C 的封装单位是「文件」,Java 的封装单位是「类」。C++ 除了 class 级别还有friend,Java 没有friend:需要跨类互访时,要么把类放进同一个包(4.8 讲包访问),要么干脆合成一个类。
- 私有方法:只给类内部用的辅助方法设成
private:
public class Employee { private double salary;
public void raiseSalary(double byPercent) { salary += computeRaise(byPercent); }
private double computeRaise(double byPercent) { // 外部调不到 return salary * byPercent / 100; }}好处很实在:既然外面调不到,你就可以放心删它、改它、改它的参数列表,不用担心动了谁的代码。这也是把大方法拆成小步骤的常用手段——拆出来的步骤是实现细节,不该进入公开接口。
对照 C:C 里做同样的事就是把函数声明成
static(文件内可见,链接器看不到)。思路一样,只是可见范围从「文件」变成了「类」。
- final实例字段:
class Employee { private final String name; // 构造之后就不能再改 private final int id;
public Employee(String name, int id) { this.name = name; // 必须在构造器里赋值 this.id = id; }}三条要点:
① 必须在声明处或每个构造器里赋值,一次且仅一次,之后再赋值就是编译错误。所以这种字段必须有构造器来喂——正好呼应 4.3.4 说的「你写了构造器,默认那个就没了」。
② 每个对象可以有不同的 final 值。final 不是「全局唯一」,只是「这个对象一旦定下就不变」:
private final int id; // 每个 Employee 各有各的 id,各自定死public static final double PI = 3.14159; // static + final 才是全局唯一的常量前者跟着对象走,后者跟着类走(就是类变量 + 不可变)。
③ final 锁的是引用,不是对象内容。
private final StringBuilder sb = new StringBuilder();
sb.append("x"); // ✅ 合法:改的是对象内容,不是引用sb = new StringBuilder(); // ❌ 编译错误:不能让它改指向别的对象字段是引用类型时,final 只保证「永远指向同一个对象」,不保证那个对象本身不变。
vs C 的
const:C 的const能靠强制类型转换、或者memcpy之类绕过;Java 的final字段是真的改不了(除非上反射,那属于非常规手段)。
4.4 静态字段与静态方法
static 表示成员属于类,而不是某个对象。
下面是静态方法:
class Employee { private static int nextId = 1; private int id;
Employee() { id = nextId++; }}所有对象共享一份 nextId,但每个对象拥有自己的 id。静态字段可以通过类名访问,虽然也能通过对象访问,但后者容易误导读者。public / private 控制访问范围,static 控制归属,final 控制能否再次赋值;public static final 常用于定义常量。
final 字段必须在声明处或每个构造器中初始化。对于引用类型,final 只保证引用不能换指向,不能保证对象内容不变;真正的不可变性还需要对象本身不提供修改状态的操作。
静态方法没有 this,不能直接访问实例字段或调用实例方法。方法不依赖对象状态时,可以设计为静态方法:
Math.sqrt(x);LocalDate.of(2007, 12, 3);静态工厂方法还可以用不同名称表达不同创建意图,并隐藏具体实现。Java 程序通常从以下入口开始:
public static void main(String[] args)它必须是静态方法,因为启动时还没有现成对象;每个类都可以有自己的 main 用于小测试。
4.5 方法参数
Java 永远按值传递:实参的值会被复制一份交给形参。区别只在「这个值是什么」——基本类型的值就是那个数,对象的值是引用。由此得到两类结果:
- 基本类型:形参拿到的是数值副本,怎么改都影响不到外部变量。
- 对象:形参拿到的是引用副本,和实参指向同一个对象。所以改对象的内容,外部看得见:
static void tripleSalary(Employee x) { x.raiseSalary(200);}但改形参自己的指向,动的只是副本,外部不受影响:
static void swap(Employee a, Employee b) { Employee temp = a; a = b; b = temp;}所以准确说法不是“对象按引用传递”,而是“对象引用按值传递”。这与 C 的指针参数规则完全一致:指针按值传递,可以通过它改指向的对象;要改调用方的指针本身,C 也得升到二级指针。
4.6 对象构造
对象初始化顺序固定:字段先得到默认值(数字为 0、boolean 为 false、引用为 null),再按声明顺序执行字段初始化和初始化块,最后执行构造器体。局部变量没有默认值,未初始化就使用会编译失败。
重载:同一个名字、不同的参数列表——个数、类型、顺序任一不同即可,编译器按实参挑最合适的那个:
Math.abs(-3); // 匹配 Math.abs(int)Math.abs(-3.5); // 匹配 Math.abs(double)决定「是不是同一个方法」的是签名 = 方法名 + 参数列表,返回类型不在签名里。所以 void f() 和 int f() 不算重载,而是编译错误;实参挑不出唯一匹配时也会报错。构造器同样可以重载。
C 没有这个机制,只能为类似功能起不同名字:
abs/labs/fabs。
类完全没有声明构造器时,才会自动获得一个无参构造器。 字段可以直接初始化:
private String name = "";private LocalDate hireDay = LocalDate.now();构造器可以用 this(...) 委托给同类的另一个构造器——括号里的实参决定调哪一个,跟重载挑方法同一套规则。它必须位于第一行,以集中公共初始化逻辑:
public Employee(double salary) { this("Employee", salary); // 实参是 (String, double),就匹配接收这两个参数的构造器}一个构造器里只能有一个 this(...),也不能互相绕圈(A → B → A 是编译错误),所以委托链永远是一条不循环的直线,最终停在一个不调用 this(...) 的构造器上——真正完成字段初始化的就是它。
初始化块是类体里直接写的一对 { },不属于任何方法:
class Employee { private static int nextId; private int id;
{ id = nextId++; } // 实例初始化块:每 new 一个对象执行一次 static { nextId = 1; } // 静态初始化块:类第一次初始化时执行一次}实例块和字段初始化一起,在构造器体之前、按声明顺序执行(意思就是构造器不管写在哪都最后执行),可以访问实例字段。静态块和静态字段只跑一次,用来初始化静态字段——它在类加载时执行一次,此后任何static控制的字段或块都不再执行。
两者实际都很少用:能用字段初始化直接表达的,就写在字段处。
Java 没有 C 式析构函数。对象不再被引用后由 GC 回收内存,但不能依赖回收时机清理资源;
finalize已废弃。文件句柄、数据库连接等外部资源仍须显式关闭,通常使用close()或 try-with-resources。
4.7 记录
record 是 Java 16+ 的类型声明关键字——和 class 同一层(不是 private 那种修饰符),站的就是 class 的位置,专门用来定义「一组字段 + 原样读出」的不可变类型。一行就够:
record Point(int x, int y) { }相当于:
class Point { // ① 自动生成 private final 字段(不可修改!final) private final int x; private final int y;
// ② 全参构造器,接收所有参数 public Point(int x, int y) { this.x = x; this.y = y; }
// ③ 访问器方法:名字就是字段名,没有get,没有setter! public int x() { return x; } public int y() { return y; }
// ④ 自动重写 equals()、hashCode()、toString() @Override public boolean equals(Object o) { ... } @Override public int hashCode() { ... } @Override public String toString() { ... }}编译器据此生成:私有的 final 字段、一个接收全部字段的构造器、同名访问器 x() / y()(不带 get 前缀),以及 equals / hashCode / toString(Ch5 讲)。
Point p = new Point(3, 4);p.x(); // 3构造器有三种形态:
- 标准构造器:编译器自动生成,逐个字段赋值。
- 自定义构造器:自己写,但每个字段仍必须被赋值一次。
- 简洁构造器:连参数列表都不写,只写构造体,用来做校验或修正——赋值由编译器补上:
record Range(int lo, int hi) { Range { // 简洁构造器 if (lo > hi) throw new IllegalArgumentException(); }}vs C:
struct只是数据容器,比较、打印、复制全得自己写;record自带这一整套,而且不可变。
4.8 包
包是命名空间,用来避免类名冲突。它在磁盘上就是一个目录:一个包可以装很多类,一个类只能属于一个包;不写 package 语句的类属于默认包(前面那些 Employee.java / EmployeeTest.java 就是)。
包名的惯例是用自己拥有的域名倒过来写——域名是全球唯一的,倒写出来就是一个全球唯一的前缀,跟别人撞不了名(站点 hecloud.top → 包名前缀 top.hecloud)。包名还要与目录一一对应:
com.example.myapp ←→ com/example/myapp/源文件开头写 package com.example.myapp;文件就必须放在对应目录下。同一个包里的类互相引用不需要 import。
import不是 C 的#include——它不复制任何代码,只是让你少写全名:import java.util.ArrayList;之后写ArrayList,等价于写全名java.util.ArrayList。
有两处不用 import:同一个包里的类,以及 java.lang——编译器默认替你执行了 import java.lang.*;,所以 String、System、Math、Integer 这些从来不用导入。注意它是通配符形式,按下面那条规则不覆盖 java.lang.reflect 这样的子包。
- 通配符
import java.util.*;只覆盖这一个包,不递归子包——父包和子包本来就是互不包含的两个包,只是名字看起来有层级。 - 两个包有同名类时通配符失效,必须写全限定名(如java.lang.System.out.println())。
- 静态导入
import static java.lang.Math.*;让你直接写sqrt(x)和PI,省掉类名前缀——它只导入静态成员。
包访问:修饰符什么都不写时(既非 public 也非 private),成员只对同一个包里的类可见,包外看不见。
类路径(classpath):告诉 javac / java 去哪里找 .class 文件,默认是当前目录,可以用 -cp 指定:
javac -cp build/classes MyApp.javajava -cp build/classes MyApp可以有多个路径,用 : 分隔(Windows 上用 ;)。
vs C:C 没有命名空间,全局符号一律裸奔,只能靠命名约定防撞名;Java 用包做命名空间,用
import声明依赖。
4.9 JAR 文件
JAR 本质是一个 zip 包,把一棵目录树打成一个文件。打包默认递归,子目录一并装进去;目录结构必须原样保留——类是按「包名 → 路径」找的,路径乱了就找不到了。
jar cf app.jar com/example/ # c = create,f = file;装的是 com/example/ 整棵子树因此 classpath 上 jar 和目录是等价的:-cp app.jar 和 -cp build/classes 对 javac / java 来说是一回事。jar 里也不只装 .class,资源文件(配置、图片)同样可以放进去——它就是个通用容器。
包内的 META-INF/ 是存放元数据的约定目录,其中的 MANIFEST.MF 就是清单文件——JAR 里唯一必需的文件,内容是一行行 键: 值。在清单里写上入口类,这个 jar 就能直接运行:
Main-Class: com.example.MyAppjava -jar app.jar多版本 JAR:META-INF/versions/N/ 下放的是另一套 class 文件(照抄包结构),N 是 JDK 主版本号。老 JDK 用根目录里的类,新 JDK 优先用自己版本目录里的类——一个 jar 同时兼容多代 JDK。前提是清单里写了 Multi-Release: true,否则 JVM 会忽略 versions/。
vs C:JAR 类似把多个
.o打包成.a/.so再发布;但 JAR 跨平台、自带元数据,还能直接运行。
4.10 文档注释
/** ... */ 是文档注释。它与普通注释的区别是可以被 javadoc 工具提取,生成 HTML 格式的 API 文档——JDK 官方那些在线文档就是这么来的。
/** * 给雇员加薪。 * @param byPercent 加薪百分比 * @return 加薪后的薪水 */public double raiseSalary(double byPercent) { ... }常用标签:@param、@return、@throws 分别描述参数、返回值和异常;类上还可以用 @author、@since、@see。字段也能写文档注释,包级文档则写在 package-info.java 里。
javadoc com/example/myappvs C:C 的注释只给人看;Java 的文档注释是可提取的文档,能一键生成在线 API 手册。
4.11 类设计技巧
这一节的清单可以直接当自查表用:
- 字段一律私有,需要访问就提供方法。
- 每个字段都要初始化,别把默认值当成「正常状态」。
- 方法不访问对象状态,就把它做成静态的。
- 几个基本类型字段总是一起出现(
street/city/zip),把它们包成一个类。 - 不是所有字段都要配访问器和更改器,只暴露真正需要的。
- 职责过多的类要拆开,这是最常见的坏味道。
- 类名用名词、方法名用动词,名字要能体现职责。
- 优先使用不可变类——
LocalDate的取舍就是这个道理。
两句话贯穿全章:数据私有、边界收窄。对照 C 的 struct 就明白代价在哪——成员全部公开,任何一处改动都波及所有使用方。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





