恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Java入门核心:从public class到main方法深度解析
首页
资讯中心
/
Java入门核心:从public class到main方法深度解析
Java入门核心:从public class到main方法深度解析
发布时间:2026/10/11 9:07:30
前阵子有位读者给我发来一段代码说自己的Java程序跑不起来还把报错界面截了图代码本身只有半行public class 文章标题输出{public static void main(Stri很明显代码被截断了main(Stri后面应该是String[] args)。但这半截骨架特别有意思它恰好把Java入门阶段最核心的几个关键词全带出来了——public、class、main、String。我面试过不少人能把这一行解释透彻的连三成都不到。多数人背得出“main方法是程序入口”但再追问一句“为什么必须是public static void”就卡壳了。这篇就把这个被截断的代码当作引子从类结构、main方法、String一直讲到编译运行和面试高频坑。适合刚开始学Java的朋友也适合准备Java基础面试、想系统过一遍知识点的读者。我讲得会比教科书啰嗦一些但都是实际跑代码会踩到的经验和教训。1. 类的基本结构从public class说起1.1 类名和文件名的硬性规则先看代码里这段public class 文章标题输出。Java规定一个源文件里只能有一个public类并且这个public类的名字必须跟文件名保持一致。也就是说如果你写下public class 文章标题输出那么这个源文件必须命名为文章标题输出.java一个字母都不能差。为什么会有这种看起来“无理”的要求原因在于JVMJava虚拟机加载类的时候是拿着类名去找.class文件的。你编译文章标题输出.java编译器会生成一个文章标题输出.class的字节码文件。如果类名叫Foo文件名却是Bar.java编译时会直接报“类Foo是公共的应在名为Foo.java的文件中声明”这样的错误。这里建议不要用中文类名。虽然Java语法允许中文标识符你写成文章标题输出编译没有问题但在实际项目里中文文件名、中文类名很容易在跨平台、跨编码环境时出乱子。我在Windows上就遇到过中文类名配合GBK编码编译出来的字节码在其他平台执行时直接抛NoClassDefFoundError的情况。标准做法是使用英文大驼峰命名比如ArticleTitlePrinter。另外类名首字母大写方法名首字母小写变量名首字母小写这是看得见的规范面试也常问。一个.java文件里其实可以定义多个类但最多只能有一个public类。如果其他类不带public修饰符它们就是包内可见的辅助类。这种写法在某些历史遗留代码里能看到现在基本用内部类或者独立文件替代了。1.2 public、private、protected访问修饰符怎么选代码里public是访问修饰符控制这个类能被谁使用。Java一共有四种访问级别我习惯用一张表说明修饰符同类内同包内子类不同包所有类private可以不可以不可以不可以默认什么都不写可以可以不可以不可以protected可以可以可以不可以public可以可以可以可以为什么程序的入口类必须是public因为JVM在启动时要调用这个类的main方法调用者不在同一个包内如果类不是publicJVM根本没有权限访问到这个方法所在的类。你可以把JVM想象成另一个包里的陌生人它想用你写的类你必须在门口挂上“对外开放”的牌子而public就是这个牌子。对于普通字段和方法我的建议是能private就private需要让子类访问再考虑protected对外暴露用public。很多新手图省事把所有字段都写成public这会让对象内部状态完全暴露时间一长代码全是耦合改一个字段名要牵连半个项目。1.3 package与import类的位置与相互引用写了public类之后还有个隐藏知识点是包。如果没有显式写package这个类就位于默认包default package中。默认包只适合写练习代码真实项目里必须给类分包比如com.example.blog.util。包的作用相当于给类一个完整的“门牌号”两个不同包下可以存在同名类。当一个类想使用另一个包里的public类时需要用import语句引入。比如你写的类在com.example包里你想用java.util.Date可以在文件顶部写import java.util.Date;或者直接用全限定名java.util.Date date new java.util.Date();。这一节顺带提一个面试常见的Class?泛型符号。Class?中的问号是泛型通配符表示“不知道具体类型但确实是某种类型”。比如Class? clazz obj.getClass();可以接住任意类型的Class对象如果你写ClassString clazz就只能承接String的Class对象。这个知识点在反射章节会细说先留个印象。2. main方法Java程序的真正入口2.1 逐字拆解main方法的签名看这个被截断的public static void main(Stri补全之后是public static void main(String[] args)这四个单词加一个参数每一个都不能少少一个JVM都不认。我们从左到右逐一说明。public我们前面讲过是访问权限保证JVM能够访问到这个方法。static是静态修饰符表示这个方法属于类本身不需要创建对象就能调用。JVM启动时还没有任何这个类的实例对象如果main不是staticJVM就得先new一个对象出来才能调用入口方法这会有个“鸡生蛋”的循环问题。所以java设计者直接规定main必须是static。void表示没有返回值。main方法执行完程序就结束了。如果你想在程序退出时返回一个状态码给操作系统标准做法是调用System.exit(int)而main方法本身不返回任何值。main是方法名这个不能换。JVM加载类之后只认main(String[] args)这个特定签名。如果你把main拼成mian或者参数写成String args少了方括号运行时会报错误: 在类 X 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args)String[] args是方法参数用来接收命令行传进来的字符串参数。数组的方括号可以写在变量后面也就是String args[]这种写法语法合法但可读性差实际项目里几乎没人这么写。方法参数名可以改叫args、argv、param都没问题重要的是类型必须是String[]。2.2 String[] args到底能干什么命令行参数实战这个参数不是摆设。Java程序启动时可以携带一串命令行参数它们会以字符串数组的形式传入main方法。我举个例子把标题里的类补成一个能接收文章标题并打印的程序public class ArticleTitlePrinter { public static void main(String[] args) { if (args.length 0) { System.out.println(文章标题 args[0]); } else { System.out.println(请传入文章标题例如); System.out.println(java ArticleTitlePrinter 我的标题); } } }编译运行javac ArticleTitlePrinter.java java ArticleTitlePrinter Java基础学习笔记运行结果文章标题Java基础学习笔记注意args是一个数组访问之前一定要先判断长度。直接写args[0]如果用户没传参数程序会抛出ArrayIndexOutOfBoundsException这是新手最容易踩的坑。如果要遍历所有参数可以这样for (String arg : args) { System.out.println(arg); }在IDEA里你可以在Run Configuration的Program arguments栏里写上参数相当于代替命令行传参。这在实际开发中很有用比如很多工具类都通过main参数决定运行模式。2.3 JVM如何找到main方法启动流程与“找不到main”报错JVM启动一个Java程序的流程大致是先通过类路径找到指定的类文件加载到内存校验字节码然后在这个类中查找签名固定的main方法。查找不到就直接报错。前面提到过运行时报“找不到main方法”的最常见原因有三个第一方法签名写错了。比如public static void main(String args)只写了一个String没写方括号编译能过但运行时JVM不认。第二main方法写到了其他类里而你运行java 类名时指定了没有main方法的那个类。第三main方法不是public或不是static也会报错。还有一种是IDE里常见的“编译器未包含main类型”。以Eclipse为例当你选择一个没有main方法的类直接点Run As Java ApplicationIDE会提示“编辑器未包含main类型”。这不是Java编译器的问题而是IDE检测到当前文件没有可运行入口。解决办法要么给当前类补上main方法要么运行其他包含main的类。JVM加载完类后会先执行类中的静态代码块和静态变量初始化然后才调用main方法。这一点很多人忽略实际经常出现静态初始化抛异常导致Could not initialize class xxx程序还没进main就崩了。比如下面这段写法public class SomeClass { static int value 1 / 0; public static void main(String[] args) { System.out.println(不会执行到这里); } }运行时会报ExceptionInInitializerError底层原因是静态字段初始化时除零。排查这类问题要去看静态块和静态变量的赋值逻辑别盯着main方法找半天。3. StringJava里最特殊的“非基本类型”3.1 为什么String是大写为什么不可变Java的基本类型有8个byte、short、int、long、float、double、char、boolean它们全部是小写。而String是一个类不是基本类型所以它必须以大写开头。这个区别在面试里很常考为什么Java的字符串不是基本类型因为字符串需要大量的方法支持比如截取、查找、替换把它设计成类才合理。String类还有一个关键特性不可变immutable。一旦创建完成这个字符串的内容就不可能被改变。你可能会有疑问“我明明写String s hello; s s world;s不是变了吗”这里改的不是原字符串而是创建了一个新的字符串对象hello worlds变量只是改成了指向新对象。为什么要设计成不可变最直接的原因是安全。字符串经常用来当类名、文件路径、数据库连接地址如果字符串可变恶意代码就可能篡改这些值。另一个好处是缓存同一个字符串字面量可以在字符串常量池中共享。比如String a hello; String b hello; System.out.println(a b); // 输出 true因为指向常量池同一个对象但如果用newString c new String(hello); System.out.println(a c); // 输出 falsec是堆上新建的对象内容比较要用equals不能用。我见过太多新人在这里翻车了而字符串比较恰恰是Java面试的经典送分题。3.2 String常用方法速查表String类的方法很多我不建议死记硬背但下面这些是平时写代码和面试都高频出现的整理成表格方便对照。方法作用示例length()返回字符串长度abc.length()得到3charAt(int index)返回指定位置字符abc.charAt(1)得到bsubstring(int beginIndex)从指定位置截取到末尾hello.substring(2)得到lloindexOf(String str)返回子串第一次出现的位置hello.indexOf(l)得到2contains(CharSequence s)判断是否包含子串hello.contains(ell)为truereplace(CharSequence old, CharSequence new)替换所有匹配子串a-b-c.replace(-, )得到abcsplit(String regex)按正则拆分字符串a,b,c.split(,)得到数组{a,b,c}trim()去除首尾空格 hi .trim()得到hitoLowerCase() / toUpperCase()大小写转换Ab.toLowerCase()得到abequals(Object obj)内容比较abc.equals(ABC)为falseequalsIgnoreCase(String str)忽略大小写比较abc.equalsIgnoreCase(ABC)为truestartsWith(String prefix)判断是否以指定前缀开头hello.startsWith(he)为trueisEmpty()判断是否为空字符串.isEmpty()为trueString.valueOf(Object obj)将对象转换为字符串String.valueOf(123)得到123使用split要特别小心参数是正则表达式。如果你想用小数点来拆分字符串a.b.c直接写split(.)会得到空数组因为.在正则里匹配任意字符。正确写法是split(\\.)。3.3 StringBuilder与StringBuffer字符串拼接的正确姿势如果你在循环里用拼接字符串比如String result ; for (int i 0; i 100000; i) { result i; }这个写法非常低效。每次循环都会生成一个新的String对象旧对象等着被垃圾回收。100000次循环会产生十万个中间字符串耗时和内存损耗都很夸张。正确的替代方案是StringBuilderStringBuilder sb new StringBuilder(); for (int i 0; i 100000; i) { sb.append(i); } String result sb.toString();StringBuilder内部用一个可变的字符数组append只在数组尾部追加不会不断产生新对象。实测中同样是十万次拼接String的耗时可能是StringBuilder的几十倍甚至上百倍。StringBuilder和StringBuffer功能几乎一样区别在于StringBuffer的方法加了synchronized关键字是线程安全的代价是性能略低一点点。单线程环境下直接用StringBuilder就行多线程环境下如果有多个线程同时操作同一个字符串缓冲对象才用StringBuffer。关于两个缓冲类转成String方法都是toString()对应面试题里“stringbuffer转换为string”的答案。同时注意StringBuilder和StringBuffer继承自同一个抽象父类AbstractStringBuilder所以它俩不能直接强转只能通过toString输出。4. 编译与运行实操让代码真正跑起来4.1 完整代码示例补全被截断的类我把一开始那个截断代码补全成一个可运行版本。考虑到中文类名的不便这里用一个英文类名/** 文章标题输出程序 */ public class ArticleTitlePrinter { public static void main(String[] args) { if (args.length 0) { System.out.println(用法: java ArticleTitlePrinter 文章标题); return; } StringBuilder sb new StringBuilder(文章标题); for (int i 0; i args.length; i) { if (i 0) { sb.append( ); } sb.append(args[i]); } System.out.println(sb.toString()); } }这段代码把main接收到的所有参数用空格连起来输出。比如执行java ArticleTitlePrinter Java 基础 笔记输出文章标题Java 基础 笔记编译时注意如果源码文件用了UTF-8编码在Windows命令行下可能需要指定编码javac -encoding UTF-8 ArticleTitlePrinter.java否则中文字符串可能变成乱码。这是JDK在不同平台上的老问题实际项目里通常配合Maven或Gradle构建工具它们默认会处理编码。早期我直接用记事本写Java中英文混编时整过很多次乱码后来一律用IDE的默认UTF-8再也没出过问题。4.2 常见报错与解决方法这里把新手阶段常见的报错整理成一张速查表每一条都是我现场踩过或者帮人排查过的。报错信息常见原因解决办法错误: 找不到或无法加载主类类名拼写错误、java命令运行路径不对、类不在classpath中检查类名大小写切换到class文件所在目录再运行必要时用java -cp . 类名错误: 在类X中找不到main方法, 请将main方法定义为...main方法签名不对确认是public static void main(String[] args)不要少public或static或[]编译器未包含main类型IDE中直接运行了不含main的类在包含main方法的类上运行或给当前类补一个入口Could not initialize class xxx类的静态初始化抛异常检查静态变量赋值和静态代码块中的异常public key retrieval is not allowed连接MySQL时使用了caching_sha2_password插件且未允许公钥检索在JDBC连接URL加上allowPublicKeyRetrievaltrueuseSSLfalseClassNotFoundException运行时缺少依赖jar包或类路径不对确认依赖是否在classpath重新构建项目重点说下public key retrieval is not allowed。这个问题经常出现在MySQL 8以上版本配合旧版JDBC驱动时。MySQL 8默认的认证插件是caching_sha2_password客户端连接时如果服务器不允许公钥检索就会抛出这个异常。解决办法不是关闭数据库而是在连接串里允许公钥检索jdbc:mysql://localhost:3306/test?allowPublicKeyRetrievaltrueuseSSLfalse注意useSSLfalse只是为了方便本地测试生产环境应该配置正确的SSL证书否则有安全隐患。4.3 IDE里提高效率的实操习惯如果你用的是IntelliJ IDEA或者Eclipse直接点运行按钮之前先检查右上角的运行配置。常见的问题是编辑了文件名但老的运行配置还指向不存在的类导致运行时报找不到主类。我的习惯是每次重命名类之后直接把运行配置删掉重新Run省去很多莫名其妙的报错。另外IDE通常可以自动编译但命令行下的javac编译能让你更清楚Java程序的编译过程。我建议新手至少手动跑一遍javac和java命令理解 classpath 和字节码的关系。特别是遇到NoClassDefFoundError和ClassNotFoundException时你能从命令行角度快速定位到底是类不存在还是依赖的jar没被找到。还有个小技巧写main方法时可以用IDE的psvm快捷键生成完整签名少手敲一个关键字。但面试时手写代码就没有这个快捷键了所以基础功还得扎实。5. 面试高频点从这段代码延伸出去5.1 为什么main方法必须是public static void面试时一个问题往往会连环追问。public保证JVM能访问static保证无需实例化就能调用void表示程序结束不需要返回值。如果你只背了结论追问一句“为什么不能是private static void”就露馅了。private的话JVM无法访问运行直接报错。static如果去掉JVM得先创建类的实例但创建实例之前JVM还没法执行任何方法这就是循环依赖。void如果改成intJVM也不会认因为入口签名是固定的。这里有一个更深的细节JVM规范规定main方法必须接受一个String数组。为什么不接受可变参数String... args其实可变参数在字节码层面也是数组但JVM只会查找([Ljava/lang/String;)V这个签名描述符String...编译后的方法签名是([Ljava/lang/String;)V跟数组一样。所以理论上市面上那些写public static void main(String... args)的代码也能运行但不建议这么干因为规范写法就是String[] args。5.2 class?与反射机制前面提到过Class?这里展开说一说。Java里的每个类都对应一个Class对象它描述这个类的结构信息。获取Class对象有三种方式ClassString c1 String.class; Class? c2 hello.getClass(); Class? c3 Class.forName(java.lang.String);Class?中的?是泛型通配符表示“未知类型”。为什么要用通配符因为你在写通用代码时可能并不知道运行时具体是哪个类。比如写一个工具方法public static void printClassName(Class? clazz) { System.out.println(clazz.getName()); }这里如果写成ClassString那方法只能接收String的Class失去了通用性直接写Class又会有泛型警告和不安全转换的问题。用Class?是安全和通用之间的平衡。反射正是在Class对象基础上操作类结构的。通过clazz.getDeclaredMethods()、clazz.getDeclaredFields()等可以获取类的方法和字段再通过setAccessible(true)绕开访问控制。这块内容相对进阶但面试中“手写一个简单反射获取私有字段值”是高频题。5.3 面向对象基础封装、继承、多态既然标题是public class那类与对象本身就是绕不开的话题。类是一组数据和操作数据的逻辑的模板对象是这个模板创建出来的具体实例。比如public class Article { private String title; public Article(String title) { this.title title; } public String getTitle() { return title; } }这里Article类封装了标题字段和获取标题的方法。字段私有外部只能通过getTitle()读取这就叫封装。继承是子类复用父类的结构和行为多态是父类引用指向子类对象时调用同一个方法表现出的不同行为。三者是面向对象的核心Java的public class只是承载它们的容器。很多初学者喜欢把一切字段都public觉得省事。但项目大了之后你会发现自己写着写着就无法控制状态变化。比如一个订单金额字段谁都能改数值变成了负数都没人发现。做好封装对外提供受限的方法是Java工程化最基本的一课。5.4 Java怎么保证数据一致性这个热词也挺有意思属于并发方向的经典问题。Java里保证数据一致性主要靠几个手段synchronized关键字、volatile关键字、Lock接口以及原子类。synchronized可以修饰代码块或方法保证同一时间只有一个线程进入临界区。volatile则保证变量的可见性防止线程缓存导致读到过期值。比如public class Counter { private volatile int count; public synchronized void increment() { count; } }volatile解决的是可见性问题但不能解决复合操作的原子性所以这里increment还需要synchronized保证count不会被多个线程交叉执行。更现代的方式是使用AtomicIntegerprivate AtomicInteger count new AtomicInteger(0); public void increment() { count.incrementAndGet(); }这个知识点属于并发进阶面试常问“volatile和synchronized的区别”“AtomicInteger怎么实现原子性”。我建议基础阶段先把线程创建和同步的知识摸清楚再往深了挖。6. 几条从我经验里踩出来的建议最后分享点实操体会不整那些虚的。我见过很多学习者把大量时间花在背语法上结果打开IDE连一个最简单的类怎么运行都折腾半天。Java学习最有效的方式就是“抄代码—改代码—跑代码”三连。比如这个ArticleTitlePrinter你可以试着把它改成如果命令行传入了多个参数用逗号分隔或者实现一个根据标题长度画分隔线的功能。改着改着String的常用方法和数组遍历就都熟了。还有一个非常容易犯的错误是手动设置classpath。命令行运行时最稳妥的写法是java -cp . ArticleTitlePrinter把当前目录明确加进classpath。不管你在哪个目录执行都能避免“找不到或无法加载主类”。如果依赖了第三方jar就把jar路径加到classpath里或者直接用构建工具管理。我个人带新人的时候总会让他们先回答一句话public class里的public能不能去掉去掉之后代码还能不能运行如果答不出来就说明访问控制这一块还没吃透。这个问题的答案其实很简单去掉public后类变成包可见如果main所在的类自己作为运行入口而且不需要被其他包引用照样能运行。但大多数程序都会和其他包有依赖所以入口类保持public是习惯也是规范。回到一开始那个被截断的代码你会发现真正重要的不只是把它补全而是把每个关键字背后的“为什么”讲清楚。基础这东西越是看起来简单越值得反复咀嚼。等你哪一天能随口说出“main必须是public static void因为JVM需要跨包访问、无实例调用、忽略返回值”的时候Java的大门才算真正向你敞开。