公司动态

第10章 封装与访问控制

📅 2026/8/21 21:32:54
第10章 封装与访问控制
第10章 封装与访问控制面向对象三大特性封装、继承、多态。本节讲第一个——封装Encapsulation以及配套的访问控制修饰符。先说明一下本系列统一按 JDK 8、JDK 11、JDK 17 三个长期支持版本讲解。封装和访问控制是 Java 最稳定的基础语法三个版本下写法完全一致本节代码在任何版本中都能直接运行。一、什么是封装封装把对象的属性数据和操作属性的方法行为包装在一起并隐藏内部实现细节只对外暴露必要的访问入口。上一节直接给属性赋值的方式有问题StudentsnewStudent();s.age-18;// 年龄可以是负数数据不合理s.score1200;// 成绩超过满分属性直接暴露外界可以随意赋值数据安全性没有保障。封装的解决方案属性用private私有化外界不能直接访问提供公开的 getter / setter 方法在方法里做合法性校验为什么必须靠方法这层闸门属性本身没有检查能力赋值一瞬间就完成坏数据直接写入方法里却能写任意校验逻辑。而且需求一变比如年龄上限从 150 改成 60直接暴露属性要全项目搜索s.age逐处修改用 setter 只需改setAge一处。封装的本质是把数据和对数据的约束绑定在一起。二、封装的标准写法publicclassStudent{// 私有属性外界无法直接访问privateStringname;privateintage;privatedoublescore;// getter获取属性值publicStringgetName(){returnname;}publicintgetAge(){returnage;}publicdoublegetScore(){returnscore;}// setter设置属性值带校验publicvoidsetName(Stringname){this.namename;}publicvoidsetAge(intage){if(age0||age150){System.out.println(年龄不合法age);return;// 拒绝非法数据}this.ageage;}publicvoidsetScore(doublescore){if(score0||score100){System.out.println(成绩不合法score);return;}this.scorescore;}}使用StudentsnewStudent();s.setName(张三);s.setAge(18);s.setAge(-18);// 输出年龄不合法-18数据被拦截s.setScore(95);System.out.println(s.getName() 的年龄是 s.getAge());习惯约定IDEA 可以用快捷键AltInsert / CmdN自动生成 getter/setter不用手写。属性名age的 getter 叫getAge()boolean 类型的 getter 叫isXxx()。boolean 属性注意命名变体getter 要叫isVip()而不是getVip()。至于创建后不许改的数据如身份证号final属性 只提供 getter 即可实现只读讲 final 时再展开。三、访问控制修饰符Java 有 4 个访问级别修饰符本类同包子类不同包任意位置private✅❌❌❌默认不写✅✅❌❌protected✅✅✅❌public✅✅✅✅publicclassPerson{privateStringidCard;// 只有本类能访问身份证号Stringphone;// 包级私有同包可访问protectedintage;// 本类同包子类publicStringname;// 所有人都能访问}选型建议属性一律 private通过 getter/setter 暴露方法需要被外部调用的用 public内部辅助方法用 privateprotected主要用于继承场景子类想访问父类的成员时默认包级用于同包下的辅助类protected最容易踩坑注意跨包子类和跨包非子类的区别。// 文件 com/demo/zoo/Dog.java不同包的子类packagecom.demo.zoo;importcom.demo.animal.Animal;// 父类在 com.demo.animal 包publicclassDogextendsAnimal{publicvoidshow(){age;// ✅ 子类内部可访问 protected 成员System.out.println(年龄 age);}}而跨包的非子类访问age会直接编译报错——访问控制把能改数据的人限定在可控范围内❌ 写法见第八节易错点 3。四、包package包用于组织类和避免命名冲突对应文件系统的目录// 文件顶部声明包packagecom.example.student;// 引入其他包的类importjava.util.Arrays;importjava.util.Scanner;包名约定公司域名倒写如com.baidu.*同一个包内的类互相直接用不用 import不同包的类要 import 或写全限定名java.util.Arrays.sort(...)全限定名java.lang包下的类String、Math、System自动导入不用写 import包名必须与目录结构一致文件src/com/example/student/Student.java里写package com.example.student;否则编译找不到类。另外两个包有同名类如java.util.Date和java.sql.Date时 import 会冲突其中一个必须写全限定名。五、JavaBean 规范封装思想的产物就是 JavaBean——一种标准化的 Java 类类必须是 public属性全部 private提供 public 的 getter/setter提供无参构造器publicclassUser{privateLongid;privateStringusername;publicUser(){}publicLonggetId(){returnid;}publicvoidsetId(Longid){this.idid;}publicStringgetUsername(){returnusername;}publicvoidsetUsername(Stringusername){this.usernameusername;}}JavaBean 是 Java 生态的基础设施Spring、MyBatis 等框架都依赖这种规范反射 getter/setter 自动映射。为什么必须有无参构造器框架拿到类名后通常先反射创建空对象、再逐个调用 setter 填数据Class.forName(...).newInstance()一类操作没有无参构造器就做不了。getter/setter 命名是框架的约定username→getUsername()/setUsername()靠方法名推断字段所以别乱改。六、static 关键字顺带介绍static修饰的成员属于类本身不属于某个对象publicclassCounter{publicstaticintcount0;// 静态变量所有对象共享publicCounter(){count;// 每创建一个对象就 1}publicstaticvoidshow(){// 静态方法System.out.println(已创建 count 个对象);}}newCounter();newCounter();Counter.show();// 用类名直接调用已创建 2 个对象System.out.println(Counter.count);// 2静态成员用类名访问不需要 new 对象。静态方法中不能直接访问非静态成员——还不存在具体对象这也是 main 必须 static 的原因JVM 启动时还没有任何对象错误写法见第八节易错点 5。七、扩展知识1. 四个修饰符怎么选按使用场景而不是背表格修饰符访问范围典型使用场景private仅本类属性、内部辅助方法默认包级同包同包协作的内部工具类protected本类同包子类留给子类扩展的钩子成员public任意位置对外暴露的 API核心原则是最小暴露能 private 就不 public每放宽一级就多一批代码能碰你的数据。实操先全部 private确实需要再逐级放开。2. getter/setter 不只是取值赋值还可以做业务校验publicclassAccount{privateStringphone;privateStringemail;publicvoidsetPhone(Stringphone){if(phonenull||!phone.matches(1\\d{10})){// 11 位、1 开头thrownewIllegalArgumentException(手机号格式不正确phone);}this.phonephone;}publicvoidsetEmail(Stringemail){if(emailnull||!email.contains()||!email.contains(.)){thrownewIllegalArgumentException(邮箱格式不正确email);}this.emailemail;}}AccountanewAccount();a.setPhone(13812345678);// ✅ 正常// a.setPhone(12345); // ❌ 抛异常手机号格式不正确12345a.setEmail(zhangsanqq.com);System.out.println(a.getPhone() / a.getEmail());// 13812345678 / zhangsanqq.com注意用throw而不是returnreturn是静默拒绝调用方不知情抛异常强制调用方处理适合数据必须合法的核心业务。3. JavaBean 规范为什么是事实标准JavaBean 不是语法而是一套约定让工具和框架不商量也能配合比如 Jackson、Gson 靠 getter/setter 名推断 JSON 字段名{username:张三}映射到setUsername()Spring MVC 按参数名找 setter 赋值日志工具按 getter 输出对象内容。没有约定每个框架都要单独适配你的类有了它一个类写出来整个生态都能直接用。4. package 与 import 的分工package解决类属于哪个命名空间import解决我要用哪个类包 命名空间 目录结构 访问控制边界。import 有三种写法单类型导入import java.util.List;、通配符导入import java.util.*;、静态导入import static导入静态成员常用于 Math 等工具类如import static java.lang.Math.PI;后可直接写PIdoubler2.0;System.out.println(PI*r*r);// 12.566370614359172πr²System.out.println(Math.sqrt(16.0));// 4.0静态导入省掉类名前缀但用多了可读性下降少量使用即可。八、易错点1. 字段全 public封装形同虚设// ❌ 属性直接 public谁都能塞坏数据publicclassStudent{publicintage;}// ✅ private setter 校验publicclassStudent{privateintage;publicvoidsetAge(intage){if(age0||age150){thrownewIllegalArgumentException(年龄不合法age);}this.ageage;}}2. 只写 private 属性忘了配 getter/setter// ❌ 属性私有化了却没给访问入口成了死数据publicclassUser{privateStringname;}// ✅ 配套提供 getter/setterpublicclassUser{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.namename;}}3. protected 的跨包子类理解错误// ❌ 跨包的非子类里访问 protected 成员publicclassZoo{voidtest(Animala){a.age;// ❌ 编译错误Zoo 不是 Animal 的子类}}// ✅ 跨包的子类内部可以访问publicclassDogextendsAnimal{voidgrow(){age;// ✅ 可以访问}}更隐蔽的规则子类里只能通过子类类型的引用访问 protected 成员通过父类引用不行other.age编译报错。4. 包名与目录不一致// ❌ 目录是 com/example/demo/包名却写别的packagecom.other.demo;// 与目录不一致IDE 报错 / 找不到类// ✅ 包名与目录一一对应packagecom.example.demo;// 文件必须位于对应目录下5. 静态方法里访问实例成员// ❌ 静态方法直接用实例变量publicclassTest{intx10;staticvoidshow(){System.out.println(x);// ❌ 编译错误静态上下文不能引用非静态 x}}// ✅ 先 new 对象再访问publicclassTest{intx10;staticvoidshow(){TesttnewTest();System.out.println(t.x);// 10}}九、小结封装 private 属性 公开 getter/setter方法内校验数据合法性访问级别从宽到窄public → protected → 默认 → private遵循最小暴露包组织类和避免冲突包名域名倒写、与目录一致JavaBean public 类 private 属性 getter/setter 无参构造器是框架协作的基础static 成员属于类、所有对象共享静态方法里不能用实例成员属性一律 private读写走 getter/setter——这是封装的核心习惯下一节学习继承。下一篇第11章 继承