类与对象
学习目标
- 理解类、对象、引用、字段、方法和构造器的职责。
- 掌握
this、静态成员和初始化顺序的运行含义。 - 用封装和构造器不变式设计一个状态始终有效的领域对象。
核心知识点
专业术语
- 类(class):描述对象状态和行为的类型声明;它本身不是某一个运行时对象。
- 对象(object):按类创建的运行时实例,拥有身份、状态和可调用行为。
- 引用(reference):保存对象定位信息的变量;多个引用可以指向同一个对象,也可以是
null。 - 构造器(constructor):创建对象时执行的初始化代码,没有返回类型,可重载但不能被继承。
- 不变式(invariant):对象在构造完成及每个公开操作后都必须成立的状态约束。
白话解释与边界
类像一张规则图纸,对象是按图纸建出的实例,引用只是手里的地址卡。new 会分配对象、初始化字段并执行构造器;默认构造器只在类没有显式构造器时由编译器提供。this 保存正在执行方法的实例引用,能区分同名参数与字段;静态字段属于类而非某个实例。
构造器不变式是封装的起点:如果余额不能为负,就不能先创建一个非法账户再期待调用方修复。字段应尽量私有,公开方法负责校验状态。父子类的构造调用、初始化顺序和构造阶段动态分派属于单独专题,见构造器与初始化顺序;对象何时回收由可达性和垃圾收集决定,而不是由离开一个方法的瞬间决定。
常用用法
class/new:声明类并创建对象
类是类型声明,new 创建运行时实例;引用变量保存对象定位,多个引用可能指向同一个对象。
class User {
// 作用:类是类型声明,new 创建运行时实例;引用变量保存对象定位,多个引用可能指向同一个对象。
String name;
}
User user = new User();
// 初始状态:user 当前为 new User()。
// 作用:类是类型声明,new 创建运行时实例。
user.name = "Ann";
// 关键变化:user.name = "Ann" 把新建实例的 name 字段更新为 Ann。
System.out.println(user.name);
// 输出:Annthis:区分字段与参数
this 保存正在执行方法的实例引用,常用于同名构造器参数、链式返回和显式调用实例方法。
class User {
// 作用:this 保存正在构造的 User 实例引用,可把字段 name 与同名构造器参数区分开。
// 初始状态:User 构造器收到 name="Ann"。
// 初始状态:this.name 指对象字段,右侧 name 指同名参数。
private final String name;
User(String name) {
this.name = name;
}
String name() {
return this.name;
}
}
System.out.println(new User("Ann").name());
// 输出:Annprivate 字段 + 方法:封装对象状态
字段不直接暴露,状态变化集中到方法中才能维护不变式;getter 也不应无条件返回可变内部对象。
// 语义:字段不直接暴露,状态变化集中到方法中才能维护不变式。
// 初始状态:counter 初始为 new Counter()。
class Counter {
private int value;
void increment() {
// 作用:字段不直接暴露,状态变化集中到方法中才能维护不变式;getter 也不应无条件返回可变内部对象。
value++;
}
int value() {
return value;
}
}
Counter counter = new Counter();
// 关键变化:counter 接收表达式 new Counter() 的计算结果。
// 初始状态:counter 的初始值为 new Counter()。
counter.increment();
// 关键变化:counter.increment() 将 value 从 0 增加到 1。
System.out.println(counter.value());
// 输出:1实例方法:让行为依赖对象状态
实例方法可以读取实例字段;把需要状态的行为写成静态方法会丢失对象边界。
// 语义:实例方法读取该实例字段,不能脱离实例状态改写为静态方法。
class Greeter {
private final String prefix;
Greeter(String prefix) {
this.prefix = prefix;
}
String greet(String name) {
return prefix + ", " + name;
}
}
// 输入:new Greeter("Hello") 保存前缀 Hello,greet("Java") 返回 "Hello, Java"。
System.out.println(new Greeter("Hello").greet("Java"));
// 输出:Hello, Javastatic:访问类级成员
静态字段和方法属于类而不是某一个实例;初始化、常量和工具方法的组织细节见 static、final 与代码组织。
class Sequence {
// 作用:静态字段和方法属于类而不是某一个实例;初始化、常量和工具方法的组织细节见 [static、final 与代码组织](/courses/java/03-面向对象/07-static-final与代码组织)。
private static int next = 1;
// 初始状态:next 当前为 1。
// 作用:静态字段和方法属于类而不是某一个实例。
static int nextValue() {
return next++;
}
}
int first = Sequence.nextValue();
// 返回:第一次 Sequence.nextValue() 读取 next=1,并把静态 next 递增为 2。
int second = Sequence.nextValue();
// 返回:第二次 Sequence.nextValue() 读取 next=2,并把静态 next 递增为 3。
System.out.println(first + ", " + second);
// 输出:1, 2instanceof:判断对象是否兼容目标类型
需要在运行时判断对象能否安全地视为某个目标类型时使用 instanceof,频繁判断则应考虑改用多态方法。
Object value = "Java";
// 作用:需要在运行时判断对象能否安全地视为某个目标类型时使用 instanceof,频繁判断则应考虑改用多态方法。
// 初始状态:value 当前为 "Java"。
System.out.println(value instanceof String);
// 输出:true
// 作用:需要在运行时判断对象能否安全地视为某个目标类型时使用 instanceof,频繁判断则应考虑改用多态方法。getClass:读取对象的精确运行时类
需要获得对象的精确运行时类而不是判断类型兼容性时使用 getClass,调用前必须确保引用不为 null。
Object value = "Java";
// 初始状态:value 当前为 "Java"。
System.out.println(value.getClass().getSimpleName());
// 输出:String
// 作用:需要获得对象的精确运行时类而不是判断类型兼容性时使用 getClass,调用前必须确保引用不为 null。不常用但需要知道
实例初始化块:共享实例初始化片段
初始化块会在每个构造器执行前运行,多个构造器共享逻辑时才考虑;复杂校验优先写进明确的构造器或私有工厂方法。
class Token {
private final int value;
{
value = 42;
}
int value() {
return value;
}
}
// 输入:new Token() 执行实例初始化块,把 final 字段 value 设为 42。
System.out.println(new Token().value());
// 输出:42this(...):复用当前类构造器
this(...) 必须是构造器第一条语句;构造器重载、super(...) 和初始化顺序见构造器与初始化顺序。
class Point {
private final int x;
private final int y;
Point() {
this(0, 0);
// 作用:通过 this(...) 复用当前类构造器。
}
Point(int x, int y) {
this.x = x;
this.y = y;
}
String text() {
return x + "," + y;
}
}
// 输入:new Point() 通过 this(0, 0) 委托构造,实例字段 x、y 均为 0。
System.out.println(new Point().text());
// 输出:0,0继续阅读
- 构造器与初始化顺序:构造器重载、
this(...)、super(...)和初始化阶段。 - Object 方法与对象相等:对象身份、值相等、哈希和字符串表示。
static、final与代码组织:类级成员、常量和不可变设计。
简单案例
import java.math.BigDecimal;
import java.util.Objects;
public class AccountDemo {
static final class BankAccount {
private final String id;
private BigDecimal balance;
BankAccount(String id, BigDecimal openingBalance) {
this.id = Objects.requireNonNull(id, "id");
if (openingBalance == null || openingBalance.signum() < 0) {
throw new IllegalArgumentException("opening balance must be non-negative");
}
this.balance = openingBalance;
}
void deposit(BigDecimal amount) {
requirePositive(amount);
balance = balance.add(amount);
}
boolean withdraw(BigDecimal amount) {
requirePositive(amount);
if (balance.compareTo(amount) < 0) {
return false;
}
balance = balance.subtract(amount);
return true;
}
BigDecimal balance() {
return balance;
}
private static void requirePositive(BigDecimal amount) {
if (amount == null || amount.signum() <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
}
}
public static void main(String[] args) {
BankAccount account = new BankAccount("A-1", new BigDecimal("10.00"));
// 初始状态:account 的 id 为 A-1,余额为 10.00。
account.deposit(new BigDecimal("2.50"));
// 关键变化:account.deposit(new BigDecimal("2.50")) 将余额增加到 12.50。
// 返回:withdraw(new BigDecimal("20.00")) 因余额不足返回 false,余额仍为 12.50。
System.out.println("withdraw=" + account.withdraw(new BigDecimal("20.00")));
// 输出:withdraw=false
System.out.println("balance=" + account.balance());
// 输出:balance=12.50
}
}输出为 withdraw=false 和 balance=12.50。余额字段不能从类外直接赋值,构造器和方法共同保证“不为负、金额为正”的不变式;调用者只能通过公开行为改变状态。
易混点
- 类是类型声明,对象是运行时实例,引用变量不是对象本身;复制引用不会复制对象。
static字段由类共享,实例字段各自独立;把实例状态写成静态会让不同账户相互污染。- 构造器负责建立初始不变式,不是普通方法,不能有返回类型,也不会被子类重写。
private隐藏表示细节,getter 并不自动安全;返回可变内部对象仍可能泄露不变式。
课后小问
为什么
BankAccount不直接公开balance字段? 答案:公开字段会允许任意代码写入负数或绕过金额校验,类就无法保证不变式。 解析:封装把状态修改集中到deposit、withdraw等方法,未来更换存储方式也不会破坏调用者契约。一个类没有写构造器时一定有无参构造器吗? 答案:只有在类完全没有声明任何构造器时,编译器才会生成默认无参构造器。 解析:一旦声明了带参数构造器,默认构造器就不会自动出现;需要无参创建时必须显式声明并定义它的校验策略。
本节小结
- 类描述结构与行为,对象是实例,引用保存对象定位而不是对象副本。
- 构造器、私有字段和公开方法共同维护领域不变式。
- 静态成员属于类,实例成员属于对象,初始化顺序决定构造阶段可见状态。
- 设计类时优先保持高内聚,避免在构造器中调用开放的可重写方法。
快速回顾
- 能区分类、对象、引用和字段的含义。
- 能为一个领域对象写出构造器不变式。
- 能说明
this、static和实例方法的差异。 - 能指出公开可变字段和构造阶段开放调用的风险。
阿源的知识库