一、get 和 set 方法的基础认知
1.1 定义与作用
get 方法(访问器)和 set 方法(修改器)是面向对象编程中实现封装的关键技术。它们提供了一种标准化的方式来访问和修改类的私有成员变量。在 Java 等面向对象语言中,为了实现良好的封装性,通常会将类的成员变量声明为 private,然后通过 public 的 get 和 set 方法来操作这些变量。
这种设计模式具有以下特点:
- get 方法(获取器):用于获取私有成员变量的值,通常以"get"开头
- set 方法(设置器):用于修改私有成员变量的值,通常以"set"开头
- 方法访问权限:通常设为 public 以保证外部可访问
- 方法命名规范:遵循 JavaBean 规范,采用驼峰命名法
示例:一个完整的 User 类实现
public class User {
// 私有成员变量
private String name;
private int age;
private String email;
// get方法:获取name的值
public String getName() {
return name;
}
// set方法:设置name的值
public void setName(String name) {
this.name = name;
}
// 可以添加更多getter和setter方法
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
1.2 为什么需要 get 和 set 方法
虽然直接将成员变量声明为 public 可以简化代码编写,但这种做法会导致严重的设计问题:
-
数据验证与控制缺失:
- 无法控制变量的赋值规则
- 例如年龄不能为负数、邮箱必须符合格式等
- 示例(带验证的set方法):
public void setAge(int age) { if (age < 0) { throw new IllegalArgumentException("年龄不能为负数"); } this.age = age; }
-
内部实现变更的影响:
- 当变量的内部实现改变时(如从String改为Date)
- 会影响所有直接访问该变量的代码
- 通过方法访问可以保持接口稳定
-
封装性破坏:
- 直接暴露成员变量违反面向对象设计原则
- 使得类的内部细节对外可见
-
额外功能无法添加:
- 无法在获取或设置时执行额外操作
- 示例(记录访问日志):
public String getName() { System.out.println("访问了name属性"); return name; }
-
线程安全问题:
- 无法在方法级别添加同步控制
- 示例(线程安全的getter):
public synchronized String getName() { return name; }
通过 get 和 set 方法,开发人员可以在不暴露内部实现的前提下,安全地提供变量的访问和修改通道,同时保持对数据访问的完全控制。这种模式是现代面向对象编程的最佳实践之一。
二、get 和 set 方法的命名规范
遵循规范的命名方式是写出可维护、易读代码的基础。在 Java 中,get 和 set 方法作为访问对象属性的标准方式,有着严格的命名约定。这些约定不仅被广泛认可,也是 JavaBean 规范的重要组成部分。
2.1 基本命名规则详解
get 方法命名规则
对于非布尔类型的变量:
- 命名格式为
get
+ 首字母大写的变量名 - 例如:
getName()
、getAge()
、getSalary()
- 方法签名示例:
public String getName() { return name; }
对于布尔类型的变量:
- 命名格式通常为
is
+ 首字母大写的变量名 - 例如:
isStudent()
、isActive()
、isAvailable()
- 方法签名示例:
public boolean isStudent() { return student; }
set 方法命名规则
适用于所有变量类型:
- 命名格式为
set
+ 首字母大写的变量名 - 例如:
setName()
、setAge()
、setStudent()
- 方法签名示例:
public void setName(String name) { this.name = name; }
2.2 命名示例与应用场景
public class Person {
// 成员变量声明
private String name; // 字符串类型
private int age; // 整型
private boolean married; // 布尔类型
// 正确的get/set方法命名示例
// 非布尔类型的getter
public String getName() {
return name;
}
// 非布尔类型的setter
public void setName(String name) {
this.name = name;
}
// 非布尔类型的getter
public int getAge() {
return age;
}
// 非布尔类型的setter
public void setAge(int age) {
this.age = age;
}
// 布尔类型的getter(使用is前缀)
public boolean isMarried() {
return married;
}
// 布尔类型的setter
public void setMarried(boolean married) {
this.married = married;
}
}
实际应用中的注意事项
- 一致性:在整个项目中应保持命名风格一致
- 特殊情况处理:
- 对于缩写词(如ID),应保持首字母大写(如
getID()
而非getId()
) - 对于多个单词组成的变量名,每个单词首字母都应大写(如
getFirstName()
)
- 对于缩写词(如ID),应保持首字母大写(如
- IDE支持:现代IDE(如IntelliJ IDEA、Eclipse)都提供自动生成getter/setter的功能,可以确保遵循规范
遵循这些命名规范不仅使代码更易读,还能确保与各种框架(如Spring、Hibernate)的良好兼容性。
三、get 和 set 方法的实现细节
3.1 参数与返回值
set 方法
- 返回值:通常返回
void
,表示该方法不返回任何值 - 参数:参数类型必须与对应的成员变量类型完全一致
- 例如:对于
private String name
成员变量,set 方法应为public void setName(String name)
- 如果成员变量是自定义对象类型,如
private User user
,则 set 方法应为public void setUser(User user)
- 例如:对于
get 方法
- 参数:不接收任何参数
- 返回值:返回值类型必须与对应的成员变量类型完全一致
- 例如:对于
private int age
成员变量,get 方法应为public int getAge()
- 如果成员变量是集合类型,如
private List<String> items
,则 get 方法应为public List<String> getItems()
- 例如:对于
3.2 方法体中的常见操作
在实际开发中,get 和 set 方法的方法体往往不只是简单的赋值和返回,还可能包含以下高级用法:
3.2.1 参数验证
在 set 方法中可以加入业务逻辑验证,确保数据有效性:
public void setAge(int age) {
// 验证年龄范围
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄必须在0-150之间");
}
// 验证通过后才赋值
this.age = age;
}
其他验证示例:
- 验证字符串非空:
if (name == null || name.trim().isEmpty())
- 验证邮箱格式:使用正则表达式匹配
- 验证数值范围:
if (price < 0)
3.2.2 触发事件
当变量值改变时,可以触发相关业务逻辑:
private String address;
public void setAddress(String address) {
// 保存旧值用于比较
String oldAddress = this.address;
this.address = address;
// 只有地址真正改变时才触发通知
if (!Objects.equals(oldAddress, address)) {
notifyAddressChanged();
}
}
private void notifyAddressChanged() {
// 实际业务逻辑可能包括:
// 1. 记录变更日志
// 2. 发送MQ消息通知其他系统
// 3. 更新相关缓存
System.out.println("地址已更新为:" + this.address);
}
典型应用场景:
- 用户资料变更通知
- 订单状态变更触发业务流程
- 配置参数修改时刷新缓存
3.2.3 延迟加载
在 get 方法中实现资源的按需初始化:
private List<String> hobbies;
public List<String> getHobbies() {
// 第一次访问时才初始化
if (hobbies == null) {
hobbies = new ArrayList<>();
// 可以添加默认值
hobbies.add("阅读");
hobbies.add("运动");
}
return Collections.unmodifiableList(hobbies); // 返回不可修改的视图
}
其他延迟加载场景:
- 数据库连接的延迟建立
- 大文件的按需加载
- 复杂计算的缓存结果
注意事项:
- 需要考虑线程安全问题
- 对于频繁访问的属性可能会影响性能
- 要确保延迟初始化的对象不会导致空指针异常
四、get 和 set 方法的使用场景
4.1 JavaBean 规范详解
JavaBean 规范是 Java 平台中用于创建可重用组件的标准规范,其核心要求包括:
-
类访问权限
类必须声明为 public,确保可以被其他类自由实例化和访问。例如:public class User { // 类实现 }
-
构造方法要求
必须提供一个无参的公共构造方法,这是框架实例化对象的基础。例如:public User() {} // 默认构造方法
-
属性访问规范
- 所有成员变量应该声明为 private,实现封装
- 通过 public 的 getter 和 setter 方法访问属性
- 方法命名遵循规范:getXxx()/setXxx()(布尔类型可用isXxx()) 示例:
private String name; public String getName() { return name; } public void setName(String name) { this.name = name; }
-
框架支持优势
遵循 JavaBean 规范的类可以:- 被 Spring 等框架自动实例化和依赖注入
- 支持 Hibernate 等 ORM 框架的持久化操作
- 实现对象的深度克隆和序列化
- 方便 IDE 的代码提示和自动补全
4.2 序列化与反序列化机制
-
JSON 序列化过程
以 Jackson 为例的序列化流程:- 通过反射获取所有 public 方法
- 识别 getter 方法(is/get 开头的方法)
- 调用 getter 获取属性值
- 将返回值转换为 JSON 字段
-
特殊字段处理
- 即使字段标记为 transient,只要存在 getter 仍会被序列化
- 可使用 @JsonIgnore 注解显式排除 示例:
private transient String password; // 不会被默认序列化 @JsonIgnore public String getPassword() { // 显式排除 return password; }
-
XML 序列化差异
JAXB 等 XML 序列化工具:- 依赖 @XmlElement 等注解配置
- 默认也会通过 getter 访问属性
- 需要无参构造方法重建对象
4.3 主流框架集成原理
-
Spring 框架集成
- 依赖注入:通过 setter 方法注入依赖对象
@Autowired public void setService(UserService service) { this.service = service; }
- 配置绑定:@ConfigurationProperties 通过 setter 绑定配置
- 依赖注入:通过 setter 方法注入依赖对象
-
MyBatis 结果映射
- 结果集自动映射时调用 setter 方法
- 支持通过 setter 进行类型转换 示例映射:
<resultMap id="userMap" type="User"> <result property="name" column="user_name"/> </resultMap>
-
JPA/Hibernate 特性
- 延迟加载通过 getter 方法触发查询
- 属性变更检测基于 setter 方法调用
- 支持在 setter 中添加业务逻辑:
public void setAge(int age) { if(age < 0) throw new IllegalArgumentException(); this.age = age; }
-
模板引擎支持
Thymeleaf、JSP EL 表达式等视图技术都依赖 getter 方法访问属性:<div th:text="${user.name}">...</div>
五、使用 get 和 set 方法的注意事项
5.1 不要过度使用
在面向对象编程中,并非所有的成员变量都需要提供 get 和 set 方法,过度使用会破坏封装性:
-
只用于内部计算的临时变量,不需要对外暴露 get/set 方法
- 例如:缓存计算结果的计算器类中的临时变量
- 这些变量通常被标记为private且不提供任何访问方法
-
常量(final 修饰的变量)通常只需要 get 方法,不需要 set 方法
- 例如:数据库连接配置中的MAX_CONNECTION_SIZE
- 常量的值在初始化后就不应被修改
-
对于不希望被修改的变量,可以只提供 get 方法
- 例如:用户注册时间createTime
- 这种设计模式被称为"只读属性"
5.2 避免在 get/set 方法中执行耗时操作
get 和 set 方法应该保持简洁高效,遵循最小惊讶原则:
-
性能影响:
- 数据库查询或文件IO等操作不应放在get/set中
- 在循环中调用时,如for(int i=0; i<10000; i++) obj.getValue(),性能问题会被放大
-
可读性问题:
- 开发者通常认为get/set是简单的属性访问
- 复杂的内部逻辑会使代码难以理解和维护
-
替代方案:
- 将复杂操作拆分为独立的方法,如calculateTotal()代替getTotal()
5.3 注意封装性
良好的封装是面向对象设计的重要原则:
-
避免链式调用破坏封装:
- 不要像这样设计:public User setName(String name) {this.name=name; return this;}
- 这会导致set操作可以被无限链式调用,违反单一职责原则
-
集合类型的正确处理:
private List<String> tags = new ArrayList<>(); // 危险做法:外部可以直接修改内部集合 public List<String> getTags() { return tags; } // 安全做法1:返回防御性副本 public List<String> getTags() { return new ArrayList<>(tags); } // 安全做法2:返回不可修改视图(Java) public List<String> getTags() { return Collections.unmodifiableList(tags); } // 安全做法3:使用不可变集合(Guava) public ImmutableList<String> getTags() { return ImmutableList.copyOf(tags); }
5.4 注意线程安全
在多线程环境下,get/set方法需要特别考虑:
-
volatile关键字:
- 适用于基本类型的原子性操作
- 例如:private volatile int counter;
-
synchronized关键字:
- 方法级同步:public synchronized void setValue(int value)
- 块级同步:synchronized(this) { /* 操作 */ }
-
更高级的并发控制:
- 使用Lock接口及其实现类
- 使用原子类:AtomicInteger, AtomicReference等
- 对于集合,可以使用ConcurrentHashMap等并发容器
-
不可变对象:
- 最安全的做法是设计不可变对象
- 所有字段设为final,不提供setter方法
六、get 和 set 方法的自动化工具
手动编写 get 和 set 方法不仅繁琐,还容易出错,特别是在处理大型类时,需要为每个字段都编写对应的访问方法,既浪费时间又增加了代码维护难度。在实际开发中,我们可以利用以下工具自动生成这些方法,提高开发效率:
6.1 IDE 自带功能
主流 IDE 都提供了快速生成 getter 和 setter 的功能:
- Eclipse:在类文件上右键→Source→Generate Getters and Setters,可以选择需要生成方法的字段
- IntelliJ IDEA:使用快捷键 Alt+Insert→Getters and Setters,支持全选或部分选择字段生成
- Visual Studio Code:Java 扩展也支持类似功能,可通过命令面板调用
6.2 Lombok 框架
Lombok 是一个 Java 库,通过注解自动生成 getter 和 setter 方法,极大简化代码:
import lombok.Getter;
import lombok.Setter;
// 类级别注解
@Getter @Setter
public class User {
private String name;
private int age;
// 字段级注解
@Getter(AccessLevel.PROTECTED)
private String password;
}
使用 Lombok 需要:
- 在项目中引入依赖(Maven/Gradle)
- 在 IDE 中安装 Lombok 插件(否则会显示编译错误)
- 启用注解处理(部分 IDE 需要额外配置)
Lombok 还支持生成构造器、toString()、equals()等方法,可以显著减少样板代码。
七、常见问题与解决方案
7.1 命名错误导致框架无法识别
当 getter/setter 方法命名不规范时,许多依赖反射机制的框架(如 Spring、Hibernate、Jackson 等)将无法正确识别和操作对象的属性。
常见问题表现:
- 框架报错提示找不到属性或方法
- 序列化/反序列化时属性丢失
- 数据绑定失败
解决方法:
- 严格遵循 JavaBean 命名规范:
- getter 方法:
getXxx()
(布尔属性可以用isXxx()
) - setter 方法:
setXxx(参数类型 value)
- getter 方法:
- 使用 IDE 自动生成方法:
- 在 IntelliJ IDEA 中:右键 → Generate → Getter and Setter
- 在 Eclipse 中:Source → Generate Getters and Setters
- 使用 Lombok 注解自动生成:
@Getter @Setter private String name;
7.2 子类覆盖父类的 get/set 方法
当子类需要覆盖父类的 getter/setter 方法时,需要特别注意:
-
基本规则:
- 方法签名必须匹配(方法名和参数列表)
- 返回值类型必须兼容(相同或更具体的类型)
- 访问修饰符不能比父类更严格
-
Java 5+ 支持协变返回类型:
public class Parent { public Number getValue() { return 0; } } public class Child extends Parent { @Override public Integer getValue() { // 合法,Integer 是 Number 的子类 return 1; } }
-
注意事项:
- 覆盖 setter 方法时,参数类型必须完全一致
- 避免在覆盖的方法中改变原始语义
- 可以使用
@Override
注解确保正确覆盖
7.3 循环依赖问题
在 getter/setter 方法中调用其他对象的方法时,容易形成循环依赖,导致以下问题:
-
典型场景:
class A { private B b; public B getB() { if(b == null) { b = new B(); b.setA(this); // 循环设置 } return b; } } class B { private A a; public A getA() { if(a == null) { a = new A(); a.setB(this); // 循环设置 } return a; } }
-
可能导致的问题:
- 栈溢出(StackOverflowError)
- 无限递归调用
- 死锁(在多线程环境下)
- 内存泄漏
-
解决方案:
- 避免在 getter/setter 中进行复杂的对象初始化
- 使用延迟加载时要特别小心
- 考虑使用设计模式(如工厂模式)管理对象创建
- 对于必须的循环引用,可以:
- 使用弱引用(WeakReference)
- 在序列化时使用
@JsonIgnore
等注解避免循环 - 采用 DTO 模式断开领域对象的直接关联