【Java杂项】getXxx() 和 getDeclaredXxx() 到底有什么区别?

@[toc](【Java杂项】getXxx() 和 getDeclaredXxx() 到底有什么区别?)

🎬 博主名称: 超级苦力怕

🔥 个人专栏: 《基本功修炼大全》

🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!


文章元信息:

  • 适合读者: 已掌握 Java 继承与反射基础,正在区分成员查找 API 的开发者
  • 前置知识: 了解 Class、Field、Method、Constructor 以及 public、private 等访问修饰符

写 Java 反射代码时,下面两组调用只差一个 Declared,结果范围却完全不同。

最小现象(Java):

Child.class.getMethod("publicMethod");
Child.class.getDeclaredMethod("childPrivateMethod");

对于字段和方法,不带 Declared 的 API 只查 public,并会沿继承体系搜索;带 Declared 的 API 只查当前类型直接声明的成员,但不限制访问级别。构造器不会继承,接口方法也有静态方法不继承等额外边界。下面按字段、方法、构造器逐层拆开。

一、先给结论:看 public、声明位置和继承范围

反射查找字段、方法和构造器时,getXxx()getDeclaredXxx() 的区别可以归纳为两个问题:

  1. 要不要限制为 public
  2. 要不要沿继承关系查找?
API 类型访问级别是否包含继承成员
getXxx() / getXxxs()只获取 public字段和方法会搜索继承成员
getDeclaredXxx() / getDeclaredXxxs()获取当前类声明的全部访问级别不搜索继承成员

核心规则(文本):

不带 Declared:只查 public;字段和方法会沿继承体系搜索。
带 Declared:只查当前类直接声明的成员,但不限制访问级别。
构造器不会继承,因此构造器 API 始终只查当前类。

图1:getXxx 与 getDeclaredXxx 的查找范围

这里的“全部访问级别”包括:

  • public
  • protected
  • 包级访问(没有显式修饰符)
  • private

获取与访问是两回事
getDeclaredXxx() 能取得非 public 成员的反射对象,不代表调用方一定能直接读取、修改或调用它。访问控制属于下一步操作,不能与“能否找到成员”混为一谈。


二、用一个继承示例观察边界

继承示例(Java):

class Parent {
    public String publicField;
    protected String protectedField;
    String packageField;
    private String privateField;

    public void publicMethod() {
    }

    protected void protectedMethod() {
    }

    void packageMethod() {
    }

    private void privateMethod() {
    }
}

class Child extends Parent {
    public String childPublicField;
    private String childPrivateField;

    public Child() {
    }

    private Child(String name) {
    }

    public void childPublicMethod() {
    }

    private void childPrivateMethod() {
    }
}

Child.class 执行反射查找时,可以先建立下面这张结果表:

成员getXxx() 能否找到getDeclaredXxx() 能否找到
子类自己声明的 public 成员
子类自己声明的非 public 成员不能
从父类继承的 public 字段或方法不能
父类声明的 protected、包级或 private 成员不能不能

最后一行尤其容易误解。

Declared 的范围(文本):

getDeclaredXxx() 的“全部”只指当前 Class 对象所表示的类直接声明的全部,
不是整条继承链上所有类的全部成员。

如果需要获取父类声明的非 public 成员,必须先拿到父类的 Class 对象,再调用父类的 getDeclaredXxx()

逐层查找父类成员(Java):

Field field = Child.class
        .getSuperclass()
        .getDeclaredField("protectedField");

框架需要扫描整条继承链时,通常会循环调用 getSuperclass(),并对每一层分别执行 getDeclaredFields()getDeclaredMethods()


三、字段:getField 与 getDeclaredField

3.1 查找单个字段

字段查找示例(Java):

Field inheritedPublic =
        Child.class.getField("publicField");

Field childPrivate =
        Child.class.getDeclaredField("childPrivateField");

两组 API 的范围如下:

API查找范围
getField(name)当前类及继承体系中可见的 public 字段
getDeclaredField(name)当前类直接声明的指定字段,不限制访问级别
getFields()当前类及继承体系中可见的全部 public 字段
getDeclaredFields()当前类直接声明的全部字段,不限制访问级别

字段查找失败示例(Java):

Child.class.getField("childPrivateField");
// 非 public,不在 getField() 的范围内

Child.class.getDeclaredField("privateField");
// privateField 由 Parent 声明,不是 Child 直接声明

查找单个字段失败时,会抛出 NoSuchFieldException

3.2 字段不会因为继承就改变声明位置

publicField 可以通过 Child.class.getField("publicField") 找到,但它仍然由 Parent 声明。

确认字段声明位置(Java):

Field field = Child.class.getField("publicField");

System.out.println(field.getDeclaringClass());
// class Parent

getDeclaringClass() 可以用来确认一个成员最初声明在哪个类中。


四、方法:getMethod 与 getDeclaredMethod

4.1 查找单个方法必须给出参数类型

方法可能发生重载,所以按名称查找时还要传入形参类型。

按名称和参数类型查找方法(Java):

Method noArgs =
        Child.class.getMethod("childPublicMethod");

Method method =
        SomeService.class.getDeclaredMethod(
                "save",
                String.class,
                int.class
        );

两组 API 的范围如下:

API查找范围
getMethod(name, parameterTypes...)当前类及继承体系中的 public 方法
getDeclaredMethod(name, parameterTypes...)当前类直接声明的指定方法,不限制访问级别
getMethods()当前类及继承体系中的全部 public 方法
getDeclaredMethods()当前类直接声明的全部方法,不限制访问级别

方法查找示例(Java):

Method inherited =
        Child.class.getMethod("publicMethod");

Method ownPrivate =
        Child.class.getDeclaredMethod("childPrivateMethod");

方法查找失败示例(Java):

Child.class.getMethod("childPrivateMethod");
// private 方法不是 public

Child.class.getDeclaredMethod("protectedMethod");
// protectedMethod 由 Parent 直接声明

查找单个方法失败时,会抛出 NoSuchMethodException

4.2 getMethods() 为什么会出现 Object 的方法

枚举 public 方法(Java):

Method[] methods = Child.class.getMethods();

结果中通常还能看到:

  • toString()
  • equals(Object)
  • hashCode()
  • getClass()
  • wait()
  • notify()
  • notifyAll()

原因不是 Child 重新声明了这些方法,而是 getMethods() 会搜索继承体系中的 public 方法,Object 正位于类继承链的顶端。

调用 method.getDeclaringClass() 可以区分方法究竟声明在 ChildParent,还是 Object

从设计意图看,getMethods() 给出的不是“当前类源码里写了哪些方法”,而是这个类型对外可使用的 public 方法集合。因此,当前类直接声明的 public 方法,以及从父类或父接口继承的 public 方法,都属于这组 API 的搜索范围。

4.3 接口方法也属于搜索范围

getMethods() 不只搜索超类,还会包含从接口继承的 public 实例方法。不过,父接口声明的静态方法不会被继承,因此不会作为子接口或实现类的 public 方法被这组 API 返回。

在接口的 Class 对象上调用 getDeclaredMethods() 时,结果只包含这个接口直接声明的方法,包括:

  • 抽象方法
  • 默认方法
  • 静态方法
  • private 方法

它不包含父接口继承的方法。

对于普通类,getDeclaredMethods() 同样只关心当前类直接声明的方法。一个类为了实现接口而写下的方法,属于这个类自己声明的方法,因此会出现在该类的 getDeclaredMethods() 结果中。

图2:接口方法的声明与继承边界


五、构造器:没有继承这个维度

构造器与字段、方法最大的区别如下。

构造器继承规则(文本):

构造器不会被继承。

所以不存在“通过子类的 Class 对象获取父类构造器”这种情况。

这是因为构造器负责初始化“当前这个类”的实例,它不是可以被子类继承、重写或隐藏的方法。子类构造器可以通过 super(...) 调用父类构造器,但这不等于继承了父类构造器。因此,反射查找构造器时只能从声明它的类上查找。

API查找范围
getConstructor(parameterTypes...)当前类声明的指定 public 构造器
getDeclaredConstructor(parameterTypes...)当前类声明的指定构造器,不限制访问级别
getConstructors()当前类声明的全部 public 构造器
getDeclaredConstructors()当前类声明的全部构造器,不限制访问级别

构造器查找示例(Java):

Constructor<Child> publicConstructor =
        Child.class.getConstructor();

Constructor<Child> privateConstructor =
        Child.class.getDeclaredConstructor(String.class);

即使 Parent 有一个 public 无参构造器,它也不会出现在 Child.class.getConstructors() 中。

查找指定构造器失败时同样会抛出 NoSuchMethodException,因为构造器和方法的精确查找都使用这个异常类型。


六、单数 API 与复数 API

除了有没有 Declared,还要注意方法名的单复数。

类型示例行为
单数 APIgetField()getMethod()getConstructor()精确查找一个成员,找不到时抛出异常
复数 APIgetFields()getMethods()getConstructors()枚举范围内的成员并返回数组,没有结果时返回空数组

方法和构造器的单数 API 必须提供准确的参数类型。

基本类型参数查找(Java):

clazz.getMethod("setAge", int.class);

下面这行不能匹配参数为基本类型 int 的方法。

包装类型不等价示例(Java):

clazz.getMethod("setAge", Integer.class);

因为 int.classInteger.class 是不同的 Class 对象,反射的精确查找不会替你做自动装箱匹配。


七、返回数组没有固定顺序

下面这些复数 API 返回的数组都不承诺固定顺序:

  • getFields() / getDeclaredFields()
  • getMethods() / getDeclaredMethods()
  • getConstructors() / getDeclaredConstructors()

因此不要依赖数组下标表达成员含义。

错误的顺序假设(Java):

Field[] fields = clazz.getDeclaredFields();
Field first = fields[0]; // 不要假设它永远是源码中的第一个字段

如果业务需要稳定顺序,应根据明确规则自行排序,例如按名称排序。

按名称稳定排序(Java):

Arrays.sort(
        fields,
        Comparator.comparing(Field::getName)
);

八、API 选择清单

可以按下面的顺序判断。

API 选择流程(文本):

只需要 public 成员吗?
├─ 是
│  ├─ 需要查找继承的字段或方法:getXxx() / getXxxs()
│  └─ 查找构造器:getConstructor(s)()
└─ 否,需要当前类声明的非 public 成员
   └─ getDeclaredXxx() / getDeclaredXxxs()

需要父类声明的非 public 成员吗?
└─ 需要:先取得父类 Class,再对父类调用 getDeclaredXxx()

常见需求可以直接对应到 API:

需求选择
获取当前类和父类的 public 方法getMethods()
获取当前类自己声明的全部方法getDeclaredMethods()
获取父类声明的 private 字段父类 ClassgetDeclaredField()
获取当前类的 public 构造器getConstructors()
获取当前类的全部构造器getDeclaredConstructors()
按名称和参数类型找一个 public 方法getMethod()
按名称和参数类型找当前类的 private 方法getDeclaredMethod()

图3:反射成员查找 API 选择流程

⚠️ 选对查找 API,不代表已经取得访问权限
getDeclaredXxx() 可以找到当前类声明的非 public 成员,但后续读取字段、调用方法或创建对象时仍会执行访问检查。“能找到”与“能访问”是两个步骤;访问控制的具体处理放在 R02 中讨论。


九、常见误区

误区正确认知
getDeclaredXxx() 能获取继承链上的全部成员它只获取当前类直接声明的成员
getXxx() 会获取当前类的全部成员它只获取 public 成员
子类可以通过反射取得父类构造器构造器不继承,只能从声明它的类上获取
找到 private 成员后就能直接操作找到成员与通过访问检查是两个步骤
getDeclaredMethods() 的结果顺序等于源码顺序反射 API 不保证返回顺序
int.classInteger.class 可以互换匹配精确查找时二者是不同的参数类型
getMethod("run") 只查当前类它还会查找继承体系中的 public 方法

十、总结

getXxx()getDeclaredXxx() 的差异,本质上是“可见性范围”和“声明范围”的差异:

规则getXxx()getDeclaredXxx()
public 成员获取获取当前类直接声明的
当前类非 public 成员不获取获取
继承的 public 字段和方法获取不获取
继承的非 public 成员不获取不获取
构造器只获取当前类的 public 构造器获取当前类声明的全部构造器

最终记忆(文本):

getXxx:public,并沿继承关系搜索字段和方法。
getDeclaredXxx:只看当前类直接声明,但不限制访问级别。
构造器不继承,所以永远只从当前类中查找。

结尾配图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超级苦力怕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值