写点什么

白嫖到阿里 P7 整合的万字 Java 精髓文档,学完斩获了 3 个大厂 offer

发布于: 2021 年 05 月 12 日
白嫖到阿里P7整合的万字Java精髓文档,学完斩获了3个大厂offer

今日分享开始啦,请大家多多指教~


作为一名 Java 工程师,很多人很喜欢把 Java 的表象与底层结合结合在一起来理解,这样会让自己理解得更加充分。如果问你 java 的 main 方法为什么没有返回值,java 的 main 方法为什么格式固定?这些看起来很不起眼的问题,但真正问你的时候,你真的能答得上来吗?好了,我们保留一份反思,先来一起看看吧!

思考 Java 中 Main 方法执行验证

Java 的 Main 方法的运行怎么确保正常进行,要想搞清楚这个问题,首先需要搞清楚的是:Java 的 Main 方法到底是谁在调用的?

Java 中调用成员方法的方式有多种多样,在同一个类中调用自身类的成员方法的方式可以直接写方法名,当然这个是建立在没有使用 static 关键字的基础之上的,如果使用了关键字 static,那么直接写方法名是不可以的,调用目标方法的方法也需要使用 static 关键字对方法进行修饰。

在不同的类中,调用目标方法的方式就是先声明这个类的对象,然后使用这个类的对象去引用类的成员方法。可是现在呢?main 方法我们都知道,是一个类进行编译后执行的入口,入口是谁调用的?类中的其他方法调用的?其实不是,细心的同学就会发现,在 Java 中,每一个类都会有一个 main 方法,这个 main 方法的调用者其实是 JVM 也就是 Java 虚拟机,是他在调用这个 main 方法。

首先,我们从形式上来分析一下 main 方法的组成:

public static void main(String[] args){}

私有化主方法

private static void main(String[] args){}

无参数类型

public static void main(){}

这里为了方便引起大家的关注,我这里将 main 方法的首个修饰符修改成了 private,而非 public,为什么要这么做呢?

这是为了验证 public 关键字对 main 方法修饰的主要作用,不知大家有没有注意到,这里可能还看不出来会不会报错,我们索性直接一点,将代码复制粘贴到 t 桌面使用 dos 命令来执行:

我们再使用编译器进行编译测试:

当我们将 public 关键字修改为 private 后运行我们发现,运行阶段开始报错了,这里强调一下,并不是在编译阶段报错,很多人在解释报错的时候根本不懂其中的执行流程,误导读者,这里我简单做一个题外解释:

Java 程序在执行的过程中,要经历编译器编译再到解释器解释执行,所谓编译器编译在 windows 里面具体体现在 dos 命令编译 java 程序,也就是所谓的 javac,注意,这里的 Javac 是指 java 编译器,全称应该是 java compiler。

如图所示,这里的报错在编译阶段是没有问题的,问题是出在执行(运行)阶段,这里是错误,而非异常,读者需要注意!

错误是:在 xxx 包底下找不到相应的类,这是什么原因呢?

紧接着他就给出了解释,需要将 Java 的 main 方法定义成 public static void main(String[] args)这个格式,这是为什么呢?

直接给出答案:启动 Java 程序不是非要从 main 方法开始,从上图报错很清晰能看出来,如果想不通过 Main 方法开启 Java 程序,那当前的这个类需要继承了 FX Application。

如何去理解?那么我们就来一起探讨一下!

首先,分析一下今天的主题:关于 Java 的 Main 方法的一些为什么!

Java 的 Main 方法的原理与实现

Main 方法的整体结构我们再次拿出来:

public static void main(String[] args) {}

我们再来对该主方法进行结构化解析,重复的解释就不多说了!

一、首先思考:结构上 Java 的 Main 方法为什么格式固定?

(1)为什么固定有 public?

在思考这个问题之前,大家不妨手写一下 Java 的 main 方法…如下:

public static void main(String... args){ } //这里使用可变长参数代替了数组

想必上面的代码大家用脚都能打出来,但我想知道的是,有多少人真的去想过,Java 中是不是真的 Main 方法的格式就是固定的呢?Java 中的 Main 方法到底代表着什么呢?那么我们来想一下吧,这其中的为什么,首先聊到 public,这个关键字不知道大家熟不熟悉,我们在刚刚接触 Java 的时候,就应该看到过下面这个数据表:

public,访问控制修饰符,旨在让调用不受任何限制。

public 关键字修饰后的属性对所有类可见。可用于修饰对象:类、接口、变量、方法。在修饰类的时候我们还会发现,如果使用 public 修饰了类,那么文件名必须与类名的一致的,这其实也是 public 的作用的一种。

没错,public 倒是没有那么特别,如果将 Java 看作是一场有固定规则的游戏,Java 的游戏规则就是这么定的,public 他就是归 Java 所有的关键字,其他人不准用他来做变量名,但是你想用你可以向我申请,我给你这个 public 关键字供你使用,用来对 Java 这个游戏世界的变量进行修饰,至于为什么要用来修饰 main 方法,看过我过于 static 关键字的文章的同学很快就知道,这是修饰符的作用,在 Java 程序初次启动的时候,是 jvm 也就是 java 虚拟机去主动寻找当前类的 main 方法,还记得吗,使用了 static 修饰的方法直接使用类名+" . "的方式去访问,我这里也是对下面的 static 关键字的一个通性回答。

所以虚拟机也是使用种方法对 Main 方法进行调用的,不给你 public 你怎么便捷访问呢?虽然 jvm 也可以访问 private,并且访问的方式多种多样,但这并没有 public 来的便捷,执行的速率或是一个最主要的因素!

好的,到这里我们都已经清楚了,public 就是个关键字,用来修饰 Java 中的变量目的是为了进行访问控制。先易后难,我们继续往下看~

(2)为什么固定有 static?

直白了讲,首先我们要明白 static 的作用是什么,在一个类中,如果这个类的方法未被 static 关键字进行修饰,那么这个类的方法所属的级别是对象层级,当变量处于对象层级是什么概念呢?

首先,处于对象层级就表示这个变量想要得到访问需要通过这个类的实例化对象进行引用才可以得到访问,如果加上 static 关键字之后呢?加上 static 关键字之后,此时的变量所属的层级就属于类的级别,就属于类的了,跟对象就没什么关系了。

当然了,Java 没有这么绝情,对象是我派生出来的,是我产生的,你想要引用,我当然同意你可以引用。

从另一个角度来分析,我们都知道,在 Java 中静态方法内不能调用非静态方法和引用非静态的成员变量,反之则亦然,如果 main 方法不是 static 修饰的,main 可以随便调用 static 修饰变量,这会怎么样啊?

这里我重点解释一下为什么说反之亦然。

(1)Java 的程序从编写完到执行大体要经过两个步骤,一个就是编译期,一个是运行期。

编译期间,java compiler 会将 java 文件编译为字节码文件,字节码文件随后被加载入内存。

jvm 也就是 Java 虚拟机,是字节码文件执行的所在地,字节码文件在 jvm 中获得相应分配的资源,然后开始对类的成员进行初始化,被 static 变量修饰后,不需要通过创建对象来初始化,而是在类加载的时候变量就已经被加载。

我们说为什么不用等对象被创建,对象被创建的时候是不是调用默认的构造方法对对象进行初始化的?

而 java 的游戏规则是 static 变量要先加载,不等构造函数后加载,它就要先加载,没毛病啊。所以啊,java 的静态变量和静态方法在 java 的类被加载的时候就分配了内存空间,所以才有非静态的方法调用他们的时候可以直接调用,因为早就有了内存空间,就等于有了内存地址,调用不就是访问的另一种表现形式吗,其本质不就是访问吗?

你都能有自己的内存地址了,还不能访问了吗?所以啊,这才是为什么反之亦然,这才是为什么 Java 的非静态方法可以调用静态方法和静态变量的真正原因!清楚了吗?

这问题就大了,我们知道 static 修饰的变量被对象所共享,万一在生产环境中,我在这个类的某一个非静态方法中我改了个 static 变量,导致这个类所有的 static 变量的值都统一换了。

所以,为了安全考虑,封装的特性与单例设计模式的重要性就凸显了,再者说,使用了 static 修饰的方法直接使用类名+" . "的方式去访问,虚拟机也是使用种方法对 Main 方法进行调用的,这也是其中之一的原因。在总结之前,我们再看一个代码,如下所示:


以上代码的执行流程是:

(2)java 编译器 java compiler 也就是 dos 中的 javac 命令,执行编译操作后,生成了字节码文件,我们说,使用 new 关键字创建对象后,会在 Heap 堆空间中申请开辟内存空间。

正如下图所示,创建完对象后,People 类的引用被压入栈中,根据栈的数据结构定义,先进后出,先创建的引用存放于 stack 栈内存区域的底部,后创建的在上面,他们分别指向 Heap 堆空间的内存地址,这是因为他们实际存的值就是地址。

hobby 被 static 静态修饰符修饰后,变成了类级别,并且被这个类的所有对象所共享!这就是为什么创建不同的对象能够共用一个值。

Tip:我们都知道赋值语句的写法:

People man = new People();

赋值语句之前 是在创建引用,赋值语句之后,是在使用赋值符号(=)

将等号后面的值赋给等号前面的引用,而除了直接量之外,new 关键字创建的对象也称之为引用类型数据,所谓引用也就是只能引用他的地址,故此,man 存放的就是 new 出来的 People 对象的地址。这个地址也就是 Heap 堆空间中的地址。

此过程如图所示:

此代码运行的结果为:

到这里,static 的作用想必都清楚了吧,static 方法也就是静态方法,static 变量就是静态变量,我们都知道,被 static 修饰的变量叫做静态变量,在类加载的时候就已经完成了初始化,上面我们也讲了在介绍 static 关键字的时候我们讲过了,类的加载是指 java compiler 将 java 后缀的文件编译成后缀名为 class 文件后,将文件加载进入内存中,而 static 关键字修饰后的变量经过编译后加载进内存之后只有一份,这也就是为什么这个类的对象都共享这一个变量了。

(3)为什么固定有 void?

对于这个问题,可以往深了讲,也可以简单浅显地讲解,两个层面都能解释原理,从简单层面来说,Java 的 main 方法是 java 程序的入口,java 就是这么规定的。而往深里讲,至于为什么成为 Java 程序的入口,大可不必着急,因为接下来的详细剖析会带你解读一下为什么。

在解读之前呢,我想先埋一个伏笔,先简单介绍一下,main 方法作为程序的入口,Java 程序执行后从 main 方法开始执行,此时会创建相应的线程,这个线程是隐式的,并且主方法的线程也是有生命周期的,Java 类有八个生命周期,分别是编译,加载,验证,准备,解析,初始化,运行,完成。

Java 类是有八个生命周期,Java 类在编译期间后,转变为字节码文件,字节码文件后续占了七个生命周期,而字节码文件虽然都是顺序执行的,但由于 Java 字节码文件编译后不同的类之间交叉执行,交叉激活,就会导致出现越级执行。

我们的 main 方法是在哪里开始执行的呢?是在运行阶段,可能有同学测试过,在一个主方法中使用 Thread 类来实现多线程,调用 start()方法后,在执行一句其他语句,比如下代码:


执行的结果却是结束了先执行,而单开的线程依旧在跑着,这是为什么呢?提到这个单开的线程就不得不提守护线程!

Java 的程序执行入口是 main 方法,执行完毕后后如何退出呢?首先需要等待用户线程结束,当所有用户线程完全结束后,用户线程需要设置结束机制,不然守护线程也会无休止进行下去,一旦用户进程终止,此时守护线程会结束,这样 jvm 会自动结束运行并退出,这样看来 jvm 是通过 main 方法所在的非守护线程的状态(结束)来执行 jvm 的退出。

现在我们再来看,即使 main 方法有返回值,那么 jvm 是根据返回值来判断进程状态还是根据非守护线程来获得 main 方法的状态呢?就算根据 main 方法返回值来判断,那 jvm 怎么去接收呢?返回值也应有相应的准则定义才行,不然获得 main 方法的返回值有何意义可言?

什么是 Java 的守护线程?Java 中有两种类型的线程,一种叫做 User Thread(用户线程)、另一种叫做 Daemon Thread(守护线程) 。大白话讲,守护线程就是用来保护非守护线程的。

在 java 虚拟机中,如果 JVM 实例中尚存在任何一个非守护线程没有结束,则守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着 JVM 一同结束工作。守护线程是指为其他线程服务的线程,在 JVM 中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。简单总结就三点:

  1. 守护线程是为其他线程服务的线程;

  2. 所有非守护线程都执行完毕后,虚拟机退出;

  3. 守护线程不能持有需要关闭的资源(如打开文件等)。

因此,JVM 退出时,不必关心守护线程是否已结束,只要 main 方法所在的非守护线程结束了,就可以停了,上述代码中,在 main 方法中创建了非守护线程定时任务,这就代表着主方法的 main 线程的子线程未结束,主方法线程就未结束,万万不可根据线程启动后的打印语句来判断是否主方法已结束,应该看主方法衍生的所有非守护线程的状态是否结束,结束了 jvm 才会正常退出结束运行。

所以说了这么多,为什么 main 方法无返回值想必你也懂了。总结一下:

(1)main 方法是程序的主入口,Java 规定必须是 void 无返回值,格式要求。jvm 运行 java 程序时期就是按照固定的 main 方法格式去加载运行方法,能到这一步,说明前期的加载验证都已通过,所以不必纠结。

(2)main 方法带返回值是没有任何意义的,jvm 不需要接收。jvm 并不需要去接受、识别、验证 main 方法的返回值,因为程序的结束并不是通过 main 方法有无返回值决定的,而是根据非守护线程的状态来决定的。

结语

最后我们还剩最后一个问题没有仔细参透,就是 jvm 是如何调用 java 的 main 方法的,我先给大家一个简单的解释:在 java 核心编程中,JVM 会查找类中的 public static void main(String[] args),如果找不到该方法就抛出错误 NoSuchMethodError:main 程序终止,在 JavaFX 出现后,java 出现了非 main 方法也可执行的情况,贴出一句来自 javafx 文档的话:

The main() method is not required for JavaFX applications

when the JAR file for the application is created with the JavaFX

Packager tool, which embeds the JavaFX Launcher in the JAR file

翻译就是:当使用 JavaFX Packager 工具创建应用程序的 JAR 文件时,对于 JavaFX 应用程序不需要 main()方法,该工具将 JavaFX Launcher 嵌入到 JAR 文件中。

其实我们发现,我们还有一些问题尚未解决:

  • Main 方法为什么需要 String 类型数组参数,并且所有 main 方法的参数名均为 args?

  • Main 方法是否允许使用添加其他修饰符?

  • Main 方法底层是如何实现的?Main 方法在 jvm 的内存中是如何分配的?

  • Main 方法的执行所依赖的基础环境是什么?

  • Main 为什么可以成为线程树的根节点?


小结

我们在这里也只是简简单单地介绍了 java 的 main 方法,为什么格式固定以及针对固定格式的三要素进行了代码分析评定。

我们都清楚,在 Java 中,方法是用来描述一个 Java 类的行为的代名词,那么方法的定义肯定离不开类的实际行为,main 方法也是如此,在 Java 中,一个类的方法称之为这个类的成员方法,成员方法有的隶属于对象层级,有的隶属于类层级,这些都是根据修饰方法所使用的关键字来决定的。我仅将这篇文章仅作为研究 Java 的 Main 方法的初始篇章。


今日份分享已结束,请大家多多包涵和指点!

用户头像

还未添加个人签名 2021.04.20 加入

Java工具与相关资料获取等WX: pfx950924(备注来源)

评论

发布
暂无评论
白嫖到阿里P7整合的万字Java精髓文档,学完斩获了3个大厂offer