Java为什么系列--关于Java的Main方法的一些为什么

Java关于Main方法的为什么(一)

默认文件1620664404389

关于Java的Main方法的一些为什么?(一)

我们都清楚,在Java中,方法是用来描述一个Java类的行为的代名词,那么方法的定义肯定离不开类的实际行为,main方法也是如此,在Java中,一个类的方法称之为这个类的成员方法,成员方法有的隶属于对象层级,有的隶属于类层级,这些都是根据修饰方法所使用的关键字来决定的。当然这都不是今天我们想要介绍的重点,作为一名大三学生,同时也作为一名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){},请注意方法的左边的绿色运行按钮!

image-20210506165433390
私有化主方法
image-20210506165421632
无参数类型
image-20210506165449338

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

这是为了验证public关键字对main方法修饰的主要作用,不知大家有没有注意到,这里可能还看不出来会不会报错,我们索性直接一点,将代码复制粘贴到t桌面使用dos命令来执行:
image-20210506170140818
我们再使用编译器进行编译测试:
当我们将public关键字修改为private后运行我们发现,运行阶段开始报错了,这里强调一下,并不是在编译阶段报错,很多人在解释报错的时候根本不懂其中的执行流程,误导读者,这里我简单做一个题外解释:Java程序在执行的过程中,要经历编译器编译再到解释器解释执行,所谓编译器编译在windows里面具体体现在dos命令编译java程序,也就是所谓的javac,注意,这里的Javac是指java编译器,全称应该是java compiler,如图所示,这里的报错在编译阶段是没有问题的,问题是出在执行(运行)阶段,这里是错误,而非异常,读者需要注意!
image-20210506170357762
错误是:在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中的变量目的是为了进行访问控制。如果想知道public关键字在Java内部是如何实现的(目前我在牛客网,csdn,StackOverflow都没有看到有人写过),那么请你加个关注,因为后面我会把Java脱得内裤都莫得了,咳咳,不搞yellow啊,各位程序猴,先易后难,我们继续往下看~
image-20210506170807170

(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方法进行调用的,这也是其中之一的原因。在总结之前,我们再看一个代码,如下所示:

package com.sinsy.fntp;
/**
 * @author fntp
 */
public class Test {
    public static void main(String[] args) {
        People man = new People();
        man.sex = "男";
        People woman = new People();
        woman.sex="女";
        System.out.println(man.toString());
        System.out.println(woman.toString());
    }
}
 class People{
    public String sex;
    public static String hobby="干饭";

     @Override
     public String toString() {
         return "People{" +
                 "sex='" + sex + '\'' +","+
                 "hobby='" + hobby + '\''+
                 '}';
     }
 }

以上代码的执行流程是:
(1)java编译器java compiler 也就是dos中的javac命令,执行编译操作后,生成了字节码文件,我们说,使用new关键字创建对象后,会在Heap堆空间中申请开辟内存空间,正如下图所示,创建完对象后,People类的引用被压入栈中,根据栈的数据结构定义,先进后出,先创建的引用存放于stack栈内存区域的底部,后创建的在上面,他们分别指向Heap堆空间的内存地址,这是因为他们实际存的值就是地址。hobby被static静态修饰符修饰后,变成了类级别,并且被这个类的所有对象所共享!这就是为什么创建不同的对象却够共用一个值。

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

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

此过程如图所示:

Main方法中的static关键字创建对象时候的演示
此代码运行的结果为:
image-20210506222403674

到这里,static的作用想必都清楚了吧,仍有不理解的不清楚的兄弟可以看我的另一篇博客,深挖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()方法后,在执行一句其他语句,比如下代码:

package com.sinsy;

import java.util.Timer;
import java.util.TimerTask;

/**
 * @author fntp
 */
public class Test01 {
    public static void main(){
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("在执行");
            }
        },100);
    }
    public static void main(String[] args) {
        Thread thread = new Thread(() -> main());
        thread.start();
        System.out.println("结束了..");
    }
}

image-20210510233315551

执行的结果却是结束了先执行,而单开的线程依旧在跑着,这是为什么呢?

提到这个单开的线程就不得不提守护线程!什么是守护线程就看下文介绍吧!在我另一篇博客《关于Java进程的为什么》中也会详细介绍,现在我们看看了下面的介绍,知道了守护线程,我们再来分析。

Java的程序执行入口是main方法,执行完毕后后如何退出呢?首先需要等待用户线程结束,当所有用户线程完全结束后,用户线程需要设置结束机制,不然守护线程也会无休止进行下去,一旦用户进程终止,此时守护线程会结束,这样jvm会自动结束运行并退出,这样看来jvm是通过main方法所在的非守护线程的状态(结束)来执行jvm的退出,现在我们再来看,即使main方法有返回值,那么jvm是根据返回值来判断进程状态还是根据非守护线程来获得main方法的状态呢?就算根据main方法返回值来判断,那jvm怎么去接收呢?返回值也应有相应的准则定义才行,不然获得main方法的返回值有何意义可言?

什么是Java的守护线程?Java中有两种类型的线程,一种叫做User Thread(用户线程)、

另一种叫做Daemon Thread(守护线程) 。大白话讲,守护线程就是用来保护非守护

线程的。在java虚拟机中,如果JVM实例中尚存在任何一个非守护线程没有结束,则

守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同

结束工作(博客园的作者 水滴石穿007 是这么介绍的)。我翻阅了Java的官方文档,

官方文档是这么介绍的:守护线程是指为其他线程服务的线程,在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方法的,我比你们更好奇,我也已经准备好了揭开jvm的神秘面纱,在这里由于我没有完全写完的缘故,我先给大家一个简单的解释:在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文件中。

写在后面

我们在这里也只是简简单单的介绍了java的main方法为什么格式固定以及针对固定格式的三要素进行了代码分析评定,其实我们发现,如果仔细研究,我们还有一些问题尚未解决:

  • Main方法为什么需要String类型数组参数,并且所有main方法的参数名均为args?
  • Main方法是否允许使用添加其他修饰符?
  • Main方法底层是如何实现的?Main方法在jvm的内存中是如何分配的?
  • Main方法的执行所依赖的基础环境是什么?
  • Main为什么可以成为线程树的根节点?

当然,在这些问题没有完全解决之前,我是不会结束这个系列的写作,我花时间长是为了取得最正确的解释与最正确的结果,不能误导大众,因此,我仅将这篇文章仅作为研究Java的Main方法的初始篇章,后续会继续更新和更正文章内容,争取与大家一起学习交流,提升文章质量,不误导粉丝。仅秉持学习分享为主的态度与大家一同讨教Java的知其所以然!我们下一篇再见!

  • 17
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

fntp

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

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

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

打赏作者

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

抵扣说明:

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

余额充值