尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

YCBlogs 类初始化操作深度剖析:构造器、方法重载、this 与成员初始化顺序

YCBlogs 类初始化操作深度剖析:构造器、方法重载、this 与成员初始化顺序 教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载本篇技术指南以 YCBlogs 仓库中 类初始化操作分析 为骨架系统梳理 Java 类初始化阶段的核心机制构造器如何保证对象初始化、方法重载的区分规则、this 关键字的三大作用以及静态/非静态成员与代码块、构造器的执行顺序。读完本篇你将能够准确判断new对象、访问静态字段、调用静态方法时类的初始化时机并能在面试与源码阅读中快速推演出类加载与对象创建各阶段的确切执行顺序。一、为何需要构造器初始化与创建捆绑1.1 构造器的背景从概念上讲初始化与创建是彼此独立的。在 C 等语言中可以先调用initialize()之类的方法手动完成初始化但在 Java 中初始化和创建是捆绑在一起的两者不能分离。Java 通过构造器来确保每个对象在创建之后、被使用之前一定会完成初始化这有助于减少错误并使代码更易于阅读。构造器具备两个核心特征构造器采用与类完全相同的名称因此每个方法首字母小写的命名风格在这里不适用构造器没有返回值这与返回值为void的方法有本质区别空返回值的方法虽然本身不返回什么但你可以选择让它返回别的东西而构造器别无选择不会返回任何东西。new表达式确实返回了对新建对象的引用但那不是构造器的返回值。1.2 默认构造器如果类中没有定义任何构造器编译器会自动创建一个默认构造器无参构造器如果已经定义了一个构造器无论是否有参数编译器不会自动创建默认构造器。第二个规则是一个常见陷阱一旦为类定义了带参构造器new ClassName()就会编译失败报The constructor ClassName() is undefined。仓库中 类初始化原理分析 专门演示了这一现象并给出建议自己给出无参构造方法避免依赖编译器行为。1.3 构造器用于初始化创建对象时如果其类具有构造器Java 会在用户有能力操作对象之前自动调用相应的构造器从而保证初始化的进行。不接受任何参数的构造器叫默认构造器Java 文档中通常使用术语无参构造器。假设类Tree有一个接受整型变量的构造器来表示树的高度Tree t new Tree(12);如果Tree(int)是Tree类中唯一的构造器那么编译器将不允许你以任何其他方式创建Tree对象。为什么构造器必然被调用从 JVM 层面看new指令的执行流程本身决定了这一点。仓库文档 对象创建过程分析 给出了虚拟机创建对象的五个步骤类加载检查虚拟机遇到new指令时先检查常量池中能否定位到该类的符号引用并检查该类是否已被加载、解析和初始化过如果没有必须先执行相应的类加载过程分配内存在 Java 堆中划分内存采用指针碰撞或空闲列表两种方式并配合 CAS失败重试或 TLAB 保证线程安全初始化零值将分配到的内存空间初始化为零值不包括对象头保证实例字段不赋初值也能直接使用设置对象头记录对象属于哪个类的实例、如何找到类元数据、哈希码、GC 分代年龄等信息执行init方法从 Java 程序视角看此时对象创建才真正开始——init即构造器对应的字节码方法把对象按程序员意愿初始化一个真正可用的对象才算产生。也就是说构造器是 JVM 创建对象流程中的最后一环它在分配内存、零值初始化、设置对象头之后自动执行这就是Java 会保证初始化的进行的底层原因。二、方法重载名字不变参数变化创建对象时也就给对象分配到的存储空间取了一个名字所谓方法则是给某个动作取名字。当我们有多种多样的需求、想要初始化不同形态的对象时往往需要多个构造器才能满足需求。而构造器的名字又固定为类名所以唯一能变化的就只有参数类型或参数个数。方法重载方法名不变只改变参数类型或参数个数。它不局限于构造器普通方法同样支持重载。每个重载的方法必须有一个独一无二的参数类型列表。class Rock { Rock() { // 默认构造器 } Rock(int i) { // 带参数的构造器 System.out.println(i i); } void print() { System.out.println(i null); } void print(int i) { System.out.println(i i); } } // 使用 Rock r1 new Rock(); // 调用默认构造器 Rock r2 new Rock(1); // 调用带参数的构造器 r2.print(); r2.print(1);2.1 基本类型的重载自动提升与窄化转换涉及基本类型的重载要特别注意类型的自动提升基本类型能从一个较小的类型自动提升至一个较大的类型。例如传入常数5按int处理若存在接受int参数的重载方法它就会被调用如果实际参数类型小于方法中声明的形式参数类型实际数据类型会被提升扩展转型例如byte → short → int → long → float → double扩展转型不丢失数据信息相对安全char型略有不同如果找不到恰好接受char参数的方法char会直接提升至int如果方法只接受较小的基本类型参数而传入的实际参数比较大则必须通过类型转换显式执行窄化转换否则编译器报错。窄化转换可能丢失数据信息一般不推荐。仓库文档 类初始化原理分析 提供了一个非常完整的f1()f7()重载实验覆盖七种基本类型运行结果如下实际参数命中结果常数5intf1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)charf1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)char 无匹配时提升为 intbytef1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double)shortf1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double)intf1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)longf1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double)floatf1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double)doublef1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double)而针对double常量则必须窄化转型由于没有与double直接匹配的较小类型方法需要显式(float)x、(long)x、(int)x等强制转换才能调用。2.2 为什么不能用返回值区分重载方法以返回值区分重载方法是不可行的。比如有时候只是调用f()而不需要任何返回值此时编译器无法靠返回值分辨实际调用的是哪一个方法。方法签名由方法名 参数类型列表构成重载只与参数列表相关与参数名、返回类型和修饰符无关。三、this 关键字当前对象引用的三大作用this是 Java 中尤为重要的关键字主要有三个作用表示当前对象的引用并返回当前对象在构造器中当形参与成员变量同名时用this区分——带this的是成员变量使用this(参数)代替构造器方法名在构造器中调用另一个构造器避免重复代码。3.1 表示当前对象引用doFunc()方法返回的是一个ThisTest类型的值所以使用return this返回该类型对象的引用public class ThisTest { public ThisTest doFunc() { return this; } public static void main(String[] args) { ThisTest tt new ThisTest().doFunc(); } }从编译原理看同一类型的对象调用相同的方法时为了区分不同对象编译器会把对象引用作为参数传递给方法a.fun(1)在编译器内部会被翻译成ClassName.fun(a, 1)而this就是在方法中访问该引用的途径。this既可以在方法内部作为参数传给另一个方法也可以作为返回值返回可构造链式操作。3.2 表示类的成员变量在构造函数中形参与成员变量使用同一个字符串时可以用this来区分带this的表示成员变量public class ThisTest { private String arg; ThisTest(String arg) { this.arg arg; } public static void main(String[] args) { ThisTest tt new ThisTest(嘻嘻); System.out.println(tt.arg); } }3.3 在构造器中调用另一个构造器有时为一个类写了多个构造器可能想在一个构造器中调用另一个构造器以避免重复代码可用this(参数)做到这一点public class ThisTest { ThisTest(int i, String j) { this(j); // 调用 ThisTest(String) 构造器 System.out.println(我是构造器1); } ThisTest(String arg) { System.out.println(我是构造器2); } public static void main(String[] args) { ThisTest tt new ThisTest(2, 嘻嘻); } }使用this()调用构造器时有两条硬性约束见 类初始化原理分析 的Flower案例this()只能调用一次构造器一个构造器中只能出现一次必须将this()置于构造器最开始处否则编译器报错。这是因为 Java 要求先完成对成员变量的初始化与构造链的衔接才能继续执行当前构造器剩余的逻辑。参考Flower案例的输出顺序int arg ,petalCount 24 string int arg, s hello, petalCount 24 flower.petalCount 24 flower.s hello四、成员初始化顺序决定一切4.1 初始化的顺序静态先行初始化的总体顺序是先静态对象类变量、静态代码块后非静态对象实例变量、实例代码块、构造器。仓库 static 静态深入理解 中概括为静态代码块 → 非静态代码块 → 构造方法且静态代码块不管创建多少对象都只执行一次。4.2 案例一类名.变量只加载变量所在类先看第一个实验class A { public static int value 134; static { System.out.println(A); } } class B extends A { static { System.out.println(B); } } public class Demo { public static void main(String args[]) { int s B.value; System.out.println(s); } }错误结果不成立A B 134正确结果实际输出A 134观察代码B.value中的value变量是A类的。由此可以得出结论当遇到类名.变量加载时只加载触发初始化变量所在类。从字节码层面可以进一步解释这一现象B.value在编译后实际对应的是getstatic A.value因为value定义在A中编译期就会解析为对A静态字段的访问触发的初始化对象是A而非B因此静态块只打印A。这与 类加载的详细过程 中通过子类引用父类的静态字段子类不会被初始化的规则完全一致。那么如何打印出A B 245这种父类、子类都被初始化的结果把变量定义到各自类中即可class A { public static int valueA 134; static { System.out.println(A); } } class B extends A { public static int valueB 245; static { System.out.println(B); } } public class Demo { public static void main(String args[]) { int s B.valueB; System.out.println(s); } }此时getstatic B.valueB触发B的初始化而 JVM 规定初始化一个类之前必须先初始化其父类所以输出A B 2454.3 案例二new 加载类时父类先行再看new触发类初始化的顺序class A { public static int value 134; static { System.out.println(A); } A() { System.out.println(构造A); } } class B extends A { static { System.out.println(B); } B() { System.out.println(构造B); } } public class Demo { public static void main(String args[]) { B b new B(); } }打印结果为B 构造A 构造B注意这里B是子类B的静态代码块输出而构造A、构造B是实例初始化输出。完整顺序是触发B的类初始化clinit但在此之前 JVM 必须先完成父类A的初始化——本例中A的静态块先于B的静态块执行。观察输出只有B而没有A的静态块打印是因为在4.2案例中A已经被初始化过同一个 JVM 进程内类的初始化只进行一次此处若在全新进程中运行完整输出应为A B 构造A 构造B执行B的构造器时先隐式调用父类A的构造器构造A再执行B自己的构造器体构造B。关于父类构造器的自动调用类初始化原理分析 指出子类构造方法会首先调用直接父类的无参构造方法父类构造方法再调用其直接父类的构造方法不断重复直到java.lang.Object的构造方法即子类的所有父类也会实例化。4.4 案例三代码块与构造器的执行顺序public class C { { System.out.println(代码块C); } C() { System.out.println(构造C); } void setData() { { System.out.println(方法中代码块C); } System.out.println(方法C); } } public class Demo { public static void main(String args[]) { C c new C(); c.setData(); } }打印结果代码块C 构造C 方法中代码块C 方法C由此可以得出结论在同一个类中代码块构造代码块/实例代码块比构造方法先执行方法内部的局部代码块则与方法体一起按语句顺序执行。仓库 类初始化原理分析 将对象创建过程归纳为第一次创建类或访问其静态数据/方法JVM 加载.class文件此时按定义顺序执行所有静态初始化new时在堆上分配空间分配前被置零基本类型默认值为 0引用类型默认值为null按顺序执行非静态的初始化实例变量赋值、实例代码块执行构造器。4.5 静态数据的初始化一次且共享无论创建多少个对象静态数据都只占用一份存储区域static关键字不能应用于局部变量只能作用于域类成员如果静态基本类型域未显式初始化会获得基本类型的标准初值数值类型为 0、char为\u0000、boolean为false如果是对象引用默认值为null静态初始化动作只进行一次当首次生成这个类的一个对象时或者首次访问属于这个类的静态成员时即便从未生成过那个类的对象。静态变量存储于 JVM 方法区静态区整个内存只有一份拷贝随类加载而分配随类卸载进程结束而消亡。仓库 static 静态深入理解 特别提醒static 变量在类加载时被初始化、生命周期与 ClassLoader 一致占用内存且一般不会释放Android 中不能把 Activity 作为 static 对象否则其所有组件对象都会进入全局内存且无法被回收。底层原理clinit方法。类加载的详细过程 指出所有类变量初始化语句和静态代码块在编译时都会被前端编译器收集到一个特殊方法clinit类/接口初始化方法中该方法只能在类加载过程中由 JVM 调用。编译器收集的顺序由语句在源文件中出现的顺序决定静态语句块只能访问定义在它之前的变量。此外如果超类还未初始化会优先初始化超类但clinit方法内部不会显式调用超类的clinit由 JVM 保证其先被执行JVM 必须确保一个类在多线程同时初始化时仅允许一个线程执行初始化其余线程必须等待——这正是利用静态内部类实现线程安全单例模式的理论基础如果一个类没有类变量也没有静态代码块可以没有clinit方法。4.6 构造器初始化与自动初始化可以使用构造器进行初始化但自动初始化零值初始化将会在构造器被调用之前发生无法阻止类变量非静态成员的定义顺序决定了实例初始化的顺序类变量的默认值与静态变量一致对于静态变量Java 可以把多个初始化语句组合成静态块其在类加载时执行、顺序与定义顺序一致对于非静态变量也可以把多个初始化语句组成实例代码块在实例初始化时执行。4.7 数组初始化与可变参数列表数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。声明数组得到的只是对数组的一个引用数组的创建发生在运行时刻。int[] a new int[3]; // 用 new 创建真实数据分配在堆中默认值均为零 int[] b {1, 2, 3}; // 直接初始化长度 3 Integer[] c new Integer[3]; // 对象数组保存引用初始值均为 null int len rand.nextInt(20); int[] d new int[len]; // 数组长度可以是运行时变量需要留意数组字面量的一个坑{1, 2, 3}这种形式只能在声明时使用作为方法实参时必须写成new int[]{1, 2, 3}形式详见 类初始化原理分析。可变参数列表提供了便捷的语法来创建对象并调用方法。由于所有类都直接或间接继承于Object可以创建以Object...数组为参数的方法可变参数本质上就是数组传入基本类型时不依赖自动装箱int...实际是int[]static void printArray(Object... args) { for (Object arg : args) { System.out.print(arg ); } System.out.println(); } static void f(int required, int... args) { System.out.println(Required: required); for (int i : args) { System.out.print(i ); } System.out.println(); } public static void main(String[] args) { printArray(1, 2, 3, 4, 5); f(1); f(1, 2, 3); }4.8 枚举类型的初始化特性创建枚举时编译器会自动添加一些有用的特性toString()方法显示某个enum实例的名字ordinal()方法表示某个特定enum常量的声明顺序static values()方法按照enum常量的声明顺序产生由这些常量值构成的数组。五、总结类初始化与对象创建的完整顺序清单综合关联文档与仓库内 类加载的详细过程、对象创建过程分析可得到完整结论触发类初始化的时机new、getstatic、putstatic、invokestatic四条字节码指令使用new关键字实例化对象读取或设置类的静态字段被final修饰且已在编译期放入常量池的除外调用类的静态方法使用反射调用类初始化一个类时若其父类未初始化先触发父类初始化虚拟机启动时先初始化包含main方法的主类。不会触发初始化的情形通过子类引用父类的静态字段子类不初始化通过数组定义引用类调用编译期常量。对象创建的完整流程Person p new Person()用到了Person.class先找到并加载该类到内存执行该类中的静态代码块首次对类进行初始化clinit在堆内存中开辟空间、分配内存地址建立对象的特有属性进行默认初始化零值对属性进行显式初始化执行构造代码块实例代码块初始化执行对应的构造函数init初始化将内存地址赋给栈内存中的p变量。核心结论速记当遇到类名.变量加载时只加载初始化变量所在类当遇到new加载类时先初始化父类、再初始化子类类初始化阶段实例化阶段则父类构造器先于子类构造器执行在同一个类中静态代码块最先执行且仅一次实例代码块先于构造方法执行。上述内容均可结合 YCBlogs 中 类初始化操作分析、类加载的详细过程、对象创建过程分析、类初始化原理分析 与 static 静态深入理解 等文档逐案例验证。赞分享教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载相关推荐C Insights指定初始化C20成员初始化新语法C Insights指定初始化C20成员初始化新语法 还在为复杂的C类成员初始化而头疼C20带来的指定初始化Designated Init开发工具编译器nerfstudio权重初始化策略深度剖析He与Xavier初始化的理论与实践nerfstudio权重初始化策略深度剖析He与Xavier初始化的理论与实践 引言NeRF模型训练的隐藏挑战 你是否曾在训练NeRF神经辐射场Neur计算机视觉深度学习图形学JSONExport构造器生成原理Constructor类与初始化逻辑深度解析JSONExport构造器生成原理Constructor类与初始化逻辑深度解析 JSONExport是一款功能强大的macOS桌面应用能够将JSON对象快速开发工具代码生成上一篇3步快速实现文件夹预览QuickLook.FolderViewer插件终极指南下一篇猫抓Cat-Catch现代Web资源嗅探的技术架构深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表