ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java权限修饰符详解:从访问范围到protected陷阱一次讲透

Java权限修饰符详解:从访问范围到protected陷阱一次讲透 在Java的日常开发里权限修饰符可能是最容易被忽略的基础知识之一。很多人写了很久的业务代码对public和private用得还算熟练但一旦被问到protected和默认权限到底有什么区别同一个包里的子类访问父类protected成员行不行就开始含糊了。如果你正在准备面试或者看框架源码时经常被各种修饰符组合搞晕那这篇笔记值得仔细过一遍。我会从四个访问级别的本质出发配合可运行的代码示例把权限修饰符的边界、设计意图和常见坑一次讲透。1. 先搞懂四个级别的访问范围权限修饰符在Java里一共有四个访问范围从小到大依次是private、默认也叫包级私有、protected、public。大多数教程会直接丢给你一张访问范围表格让你背下来但如果你不了解这套设计到底在解决什么问题背完很快就忘。1.1 一张表看懂权限修饰符先给出最核心的对照表后面所有内容都会围绕它展开权限修饰符同类内部同包其他类不同包子类不同包其他类private可以不行不行不行默认不写可以可以不行不行protected可以可以可以不行public可以可以可以可以这张表本身不难难点在于理解每一行的为什么以及实际会怎么表现出来。比如protected这一行很多人以为不同包的子类可以访问父类protected成员是万能成立的但真写起代码来往往不是那么回事。这个问题我在后面第2章会专门用代码演示这里先不展开。提示默认权限修饰符就是什么都不写注意它和C#里的internal概念不完全一样。Java里默认权限的作用域是包而不是程序集所以通常叫它包级私有更准确。1.2 修饰符能用在哪些目标上权限修饰符不是对什么都能用的这一点也经常被忽略。按Java语法规定类和接口外层类top-level class只能用public或默认权限修饰不能用protected或private。如果你想定义一个只有包内可见的辅助类那就用默认权限想让外部包也能使用就用public。一个源文件里最多只能有一个public类而且这个类的名字必须和文件名一致否则编译直接报错。成员变量、方法、构造器四个权限都能用这也是平时接触最多的场景。内部类作为一个成员内部类可以被private、默认、protected、public任意一个修饰这一点和普通外层类不同。正因为内部类可以有private权限所以Java中常见的隐藏实现类手法就是靠这个实现的。局部变量不能用权限修饰符局部变量的作用域就是它所在的花括号块压根不存在跨类访问的问题。我在刚开始学的时候就想当然地给一个内部类加了private修饰结果发现完全合法后来查了规范才明白内部类本质上是外部类的成员。反过来如果给局部变量加private则会直接编译错误。这种边界一定要在脑子里面分清楚否则写代码时会出现为什么这个类可以private那个类不行的困惑。1.3 权限修饰符和继承的关系权限修饰符和继承的结合是进阶理解的关键。父类中声明为private的成员子类虽然继承了父类但并不能直接访问这些private成员准确地说这些成员对子类不可见。这不是说它们不存在而是说子类不能通过this.xxx的方式直接引用。因此父类通常会通过public或protected的方法把private成员的值暴露给子类使用。protected这个级别的设计初衷很有意思它比默认权限更开放一层专门为跨包的继承关系服务。也就是说一个类如果希望把自己的某些细节开放给未来的子类哪怕子类不在同一个包里但不想开放给其他无关的类就应该用protected。这种设计在模板方法模式中非常常见基类定义好算法骨架把需要子类实现的步骤用protected abstract方法暴露出去外部调用方只能看到public的入口方法无法直接调用那些中间步骤。2. 用代码把边界试一遍光看表格和理论容易飘真有疑惑的时候最好的办法就是动手写一段代码去验证。下面我会用一个极简的demo工程把四个级别在真实代码中的表现完整演示一遍建议你自己也照着建一遍。2.1 准备工作搭一个三层结构的demo工程我习惯把测试代码分成三个包来模拟不同场景com.demo.base放父类com.demo.child放不同包下的子类com.demo.other放不同包下的无关类父类代码很简单把四种权限的字段和方法各写一个package com.demo.base; public class Parent { private String privateField private; String defaultField default; protected String protectedField protected; public String publicField public; private void privateMethod() { System.out.println(private method); } void defaultMethod() { System.out.println(default method); } protected void protectedMethod() { System.out.println(protected method); } public void publicMethod() { System.out.println(public method); // private成员唯一能访问的地方同类内部 privateMethod(); } }然后我在不同包里分别测试结论基本和表格一致当前包里的ChildSamePackage可以访问默认、protected、public成员但碰不了private成员不同包的UnrelatedClass只能访问public成员不同包的ChildOtherPackage可以访问protected和public成员但看不到默认和private成员。代码写到这里你可能会觉得就这么点事但真正的坑还在后面。2.2 protected的经典陷阱这个坑我印象太深了几乎是面试必问的经典场景。先看代码package com.demo.child; import com.demo.base.Parent; public class ChildOtherPackage extends Parent { public void test() { // 正确通过继承来的引用访问自己的protected成员 System.out.println(protectedField); protectedMethod(); } public void testWithParentRef(Parent parent) { // 编译错误不能通过父类引用访问protected成员 // System.out.println(parent.protectedField); } public void testWithChildRef(ChildOtherPackage child) { // 正确子类引用访问另一个子类对象的protected成员 System.out.println(child.protectedField); } }同样是跨包子类为什么child.protectedField能访问而parent.protectedField就不行这是protected访问规则里最关键的一条跨包时子类只能通过自己的引用或子类类型的引用来访问protected成员不能通过父类类型的引用来访问。这背后的逻辑其实很朴素protected的开放对象是继承者你只有在以子类身份使用它时它才对你可见。一旦你把这个成员当作某个普通父类对象的属性去访问那你就站在了任意外部类的位置上当然要受到访问限制。这种设计是为了防止你对同一个父类的其他派生类中那些protected成员产生不恰当地访问。注意如果你在同一个包内绕开这个限制是允许的因为同包的默认权限就已经放开了。很多人把protected等同于包内公开包外继承可用这么说对但必须记住包外继承可用是有限制条件的。2.3 default和protected到底差在哪从表格上看默认权限能访问同类、同包protected能访问同类、同包、跨包子类。所以两者的区别只在跨包子类这一栏。换句话说如果不存在跨包继承的需求默认权限和protected在行为上几乎没有差别。但在真实项目里这个区别很重要。我见过一些团队在写基础组件时把内部方法全写成默认权限理由是同包调用足够用。但一旦这个类被其他包的子类继承那些默认权限方法就全部失效了子类想复用父类的辅助逻辑只能干瞪眼。反过来如果滥用protected又会把类的内部细节过多暴露给子类增加耦合。我的建议是默认权限用于包内部协作protected用于为未来继承预留扩展点。如果你不确定一个方法将来会不会被子类用到先写private等真有跨包继承需求时再改成protected这样改动成本最低也符合最小暴露原则。这也是很多优秀框架源码里的常见做法。3. 权限修饰符不只是语法更是设计语言很多初学者把权限修饰符当成编译器的某种检查规则觉得代码能跑就行修饰符随便加。但如果你去看过Spring、MyBatis这类框架的源码会发现权限修饰符用得极其考究。它实际上是一套设计语言是类与类之间沟通的契约边界。3.1 封装为什么成员变量默认是private面向对象三大特性之一就是封装而封装落地最直接的语法支撑就是private。为什么成员变量几乎总是private因为成员变量代表的是对象的状态状态如果直接暴露给外部别人可以随便改那这个对象就谈不上自洽了。比如年龄字段如果直接public外部可以直接赋值成-1严重破坏业务约束。把它设成private对外提供setAge()方法在方法里做校验才能保证状态永远合法。我自己写代码时有一条朴素的标准能private就private能让外部少看到一样东西就少一样东西。哪怕是同包的其他类我也不希望它们随随便便拿到我的内部状态除非这个类就是为协作而生的。这会逼着你认真思考每个成员到底承担什么职责长期坚持下来代码的可维护性会有质的提升。3.2 方法重写与权限只能放宽不能收紧这个规则在面试中经常被单独拎出来考子类重写父类方法时访问权限不能比父类更低。也就是说父类方法是public子类重写时就必须是public父类是protected子类可以是protected或public父类是默认权限子类重写时如果还在同一个包里可以是默认、protected、public但跨包后就不能再碰这个方法了。从设计角度理解这条规则其实很自然子类要能替换父类使用里氏替换原则如果父类公开了一个方法子类把它藏起来不让别人调那所有用父类类型引用指向子类对象的代码都会出问题。反过来子类把父类的protected方法升级成public是允许的因为这是更开放不会破坏原有调用。这里提醒一个IDE陷阱有时你在子类里想重写一个父类方法写完发现IDE提示reduces visibility那基本就是你把权限写小了。把子类方法的权限改成和父类一致或者更大就好。3.3 构造器私有化的三种实际用途private修饰构造器看起来很奇怪构造器私有化之后外部怎么创建对象呢但这恰恰是很多设计模式的基石。最常见的三种用法第一单例模式。饿汉式单例把构造器设为private在类内部静态创建唯一实例然后提供public静态方法返回这个实例。这样外部无论如何new不出来第二个对象。第二工具类。比如java.util.Math它的构造器是private的类里全是static方法。private构造器在这里就是一个明确的信号本类不可实例化纯粹是方法的容器。如果不这样做别人就可以new Math()既浪费又没意义。第三构建者模式中的限制入口。比如某些类希望只通过Builder来创建对象就会把构造器私有化让外部只能走Builder的build()方法。这能保证所有必填参数都在Builder里校验完成避免了创建出半成品对象。从这些例子能看到private构造器不仅仅是禁止创建对象更是在表达本类的创建方式由我自己控制这一设计意图。4. 面试与实战中常见的权限问题权限修饰符在面试题里出现的频率非常高因为它是考察Java基本功的好切口。一个简单的private方法和public方法有什么区别能延伸出封装、继承、重写、可见性、反射、代理等一大串问题。我在面试别人时经常用这类问题来判断候选人到底是背过八股文还是真写过代码。4.1 高频面试题速查下面的表格是我整理的高频考点按出现频率排序。建议你在复习时先自己心里答一遍再看参考答案面试题核心考点一句话答案权限修饰符有哪些分别的可见范围基础知识同表中所示protected和默认权限的区别继承与包默认没有跨包子类访问能力父类private方法能被重写吗重写规则不能private方法对子类不可见不存在重写子类重写父类方法时权限能更小吗重写规则不能只能放宽不能收紧接口中的方法默认是什么权限接口知识public abstract一个Java文件能有两个public类吗文件结构不能最多一个且与文件名一致单例模式中构造器为什么私有设计模式控制实例创建入口private方法能被反射调用吗反射机制能但一般不建议protected成员跨包时如何访问继承陷阱只能通过子类自身引用访问最后一道题是很多人的失分点它就是我在2.2节演示的那个场景。面试官问到这个大概率是在考察你是否真正理解protected的使用边界踩过这个坑的人和没踩过的人回答深度一眼就能分辨出来。4.2 我在真实项目里踩过的权限相关的坑第一个坑是为了测试把private改成public。早期写代码图省事某个方法本来是private为了在单元测试里直接调用就改成了public。结果这个类作为对外API被别的模块使用后那些临时开放的方法就成了事实上的公共接口后续想再收回去还得考虑兼容性非常被动。后来我养成了习惯单元测试要验证私有逻辑就通过public方法间接验证或者把复杂逻辑抽到单独的包级私有类里再测。实在不行用反射也能调private方法但那是最后的办法。第二个坑是跨包继承时把父类字段写成默认权限。当时在做缓存模块的扩展点父类定义了一个默认权限的模板方法子类在另一个包里怎么都调用不到查了半天才发现是权限问题。改成protected之后问题立刻解决。这个经历让我记住了那句话如果某个成员是给子类用的要么同包要么用protected不要指望默认权限能跨包。第三个坑更隐蔽涉及接口默认方法和权限的配合。Java 8之后接口可以有default方法但接口里的字段默认是public static final方法默认是public abstract。这意味着在接口里声明的任何成员权限都被强制提到public级别。有些同事习惯在接口里写常量但没注意常量的访问范围是全局公开的结果不小心把本应是内部配置的值暴露给了所有使用方。这类问题排查起来特别费劲因为不是编译错误而是一个设计泄漏。4.3 一些容易误用的细节权限修饰符还有一些容易被忽略的细节这里单独列一下。权限修饰符不是安全机制。Java的访问控制是编译期检查不是运行时安全屏障。通过反射你可以拿到并调用任何类的private方法。所以不要把敏感信息、安全性校验放在依赖权限修饰符的地方权限修饰符是用来规范代码结构的不是用来防攻击的。protected成员在同一个包内也可以通过任意同包类访问。这一点容易让人误以为protected只能在继承关系里用其实同包内的其他类也可以访问。所以你在一个包里写了一个protected方法本包的其他类直接调用是完全允许的不需要是父子关系。如果不想让包内其他类访问就老老实实改成private。重写时协变返回类型和权限是两码事。有些同学把子类可以返回父类方法返回类型的子类型和权限规则混在一起其实协变返回类型是返回值的事权限是访问控制的事两者互不干扰但面试时经常放在一起考要分清楚。匿名内部类和Lambda的访问规则。在Java 8之前匿名内部类访问外部局部变量要求变量是final的Java 8之后只要局部变量事实上不变effectively final就可以。这和权限修饰符没有直接关系但很多人在这个知识点上和访问控制混淆顺带提一句。经验当你拿到一份别人的代码快速判断一个类的设计意图时可以先看它的public方法有几个。如果public方法超过十几个这个类大概率在上帝类的边缘试探。真正设计良好的类对外暴露的public接口通常很少大部分细节都藏在private和包级私有方法里。回到开头那个问题权限修饰符到底难不难如果只记表格确实不难。但想真正用好它你得把它放在封装、继承、重写、设计模式这些更大的语境里去理解。我个人的体会是每写一个成员都先问自己三个问题这个成员需不需要被别人看到被谁看到如果不需要就锁死为private。等你把权限修饰符用成一种本能看懂框架源码里那些为什么这个方法是protected而不是public的设计决策也就水到渠成了。这份笔记里的代码示例和踩坑记录都是我实际验证过的你可以直接照着敲一遍。遇到边界场景不确定时打开IDE写个demo实测永远比死记硬背可靠。
返回列表