我刚学 Java 时把类当「带方法的结构体」用:字段全 public、哪里要哪里改,写了两周,然后花了一晚上修一个「年龄被改成 -5 导致统计全错」的 bug,才第一次真正理解封装是什么。本章从那次翻车讲起:类与对象、构造方法、this、封装的真实收益,再把 static 和内部类收尾。这是面向对象的第一章,只讲「怎么把数据和方法捆好」,继承与多态留给下一章。
class Person {
String name; // 字段(成员变量)
int age;
void sayHello() { // 方法
System.out.println("Hi, I'm " + name);
}
}
Person p = new Person(); // new:堆上分配,返回引用
p.name = "Tom";
p.sayHello(); // 调用,输出 Hi, I'm Tom
类是模板,对象是按模板造出的实例,new 在堆上开地、返回门牌(第 6 章讲过的引用)。方法内部的 name 默认就是「当前对象的 name」,这个隐式传递的「当前对象」,就是 this,7.4 节正式介绍。
翻车事故的完整版:age 是 public 的,散落在十几个文件里的赋值语句,有一处写了 p.age = input - userAge(本意算差值),负年龄混进统计。修复方案不是「把那处改对」(下个月还会再错),而是把修改权收归一处:
class Person {
private int age; // 外界再也摸不到
public int getAge() { return age; } // 只读门
public void setAge(int age) {
if (age < 0 || age > 150)
throw new IllegalArgumentException("非法年龄: " + age);
this.age = age;
}
}
封装的收益清单:约束集中(setter 里校验一次,全体调用者受益)、内部可换(以后 age 改成 Date 生日字段,外界接口不变)、可读性自文档(getter/setter 的存在说明「这些字段有故事」)。Java 的惯例命名是 getXxx/setXxx,IDE 一键生成,别手敲。看到这里你可能会问「getter/setter 不就是多此一举吗」,这个质疑是对的如果校验和演变都不需要;工程里它们几乎总需要,所以惯例成立。
class Person {
private String name;
private int age;
public Person(String name, int age) { // 与类同名、无返回类型
this.name = name; // this.name 是字段,age 是参数
setAge(age); // 复用 setter 的校验
}
}
构造方法在 new 时自动调用,负责让对象「出生即合法」。三个规则:不写构造时编译器送一个无参版(字段取默认值);一旦手写了有参构造,默认无参版立刻消失(new Person() 编译不过,想都要就两个都写);构造间可以互相委托(this(...) 必须是构造第一句)。「出生即合法」是面向对象工程化的重要一步:非法对象根本没机会存在,比「构造完再补校验」可靠得多。
this 指当前对象。三个用途按频率排:区分同名(this.age = age,字段 vs 参数)、传自己(other.compare(this))、构造委托(this(name, age))。它的机器级真相值得一提(计组课第 12 章的 C 结构体函数指针成员拿它对照):调用 p.sayHello() 时,编译器把 p 作为隐藏第一个参数塞进去,C++/Python 的 self 同理,只是 Java 帮你藏了。第 12 章(方法表)会看到这个「隐藏参数」的多态版本。
对象不再被任何引用指向,就成了垃圾,由 JVM 的 GC(垃圾回收)自动回收。和 C 的手动 malloc/free(指针课第 9 章)对比:
System.gc() 只是「建议」,不保证立刻执行。内存泄漏在 Java 里还有吗?有,但形态变了:不是「忘了 free」,而是该忘的忘不掉(静态集合无限囤对象、监听器没注销、长命对象攥着短命对象的引用),排查工具从 valgrind 换成堆 dump + MAT 分析。这个话题第 10 章(集合类)和第 14 章(多线程)会再回来。
class Counter {
static int count = 0; // 类级:全类一份
int id; // 实例级:每对象一份
static int next() { return ++count; } // 静态方法:类名直调
}
Counter.next(); // 不 new 也能调
static 成员放在类上而非对象上,所有实例共享。规则:静态方法不能访问实例成员(它没有 this!没有当前对象,自然摸不到实例字段),实例方法可以访问静态成员。典型用途:常量(static final)、工具方法(Math.max,第 6 章埋的伏笔)、计数器、以及单例模式(私有构造 + 静态方法返回唯一实例,全局只需要一个的东西:配置、连接池)。反模式预警:static 可变状态是并发 bug 温床(第 14 章),静态集合当缓存要管住容量。
类可以定义在另一个类的体内,按位置分三种:成员内部类(持有外部类引用,能摸外部私有成员,事件处理器常用)、静态内部类(不依赖外部实例,Builder 模式标配)、匿名内部类(当场 new 接口实现,第 11 章 GUI 的事件回调主角,Lambda 出现前 Java 回调的唯一姿势)。初学阶段记住「匿名内部类 = 一次性的接口实现」即可,第 11 章反复用到。
this.age = age 写成 age = age,编译器只当你在给参数自己赋值,字段纹丝不动,对象「没初始化」还不报错(IDEA 会给灰色提示,认真看警告)。new Person(),抄录编译错误;补无参构造后修复。Counter 里加实例字段,在静态方法里访问它,抄录编译错误并解释(没有 this,哪来的实例字段)。