1. 这不是Java入门课,而是一份“能跑通、能改写、能 debug”的实操手记
我带过不下二十个零基础转行的学员,也给大厂校招生做过岗前强化训练。最常听到的一句话是:“书上写的我都懂,可一写代码就报错;视频里老师敲得飞快,我连main方法括号都还没对齐。”这根本不是理解力的问题——是绝大多数Java入门材料,从根上就缺了一样东西:真实编码现场的呼吸感。它不告诉你为什么public static void main(String[] args)必须这么写,也不解释System.out.println()背后到底发生了什么,更不会提醒你IDE自动补全的import语句,哪一行删了会编译失败、哪一行删了只是警告。这篇《Understand JAVA Basic with Code Examples》不是教科书复述,而是我把过去十年在产线、教学、Code Review中反复验证过的27个最小可运行单元,全部拆解成带注释、带错误对照、带执行痕迹的“活代码”。每一个例子,你复制粘贴进IntelliJ或VS Code就能立刻运行;每一个错误,我都提前替你踩过并记录下完整的报错堆栈和修复路径。它适合三类人:刚装完JDK还在怀疑自己是不是配环境失败的纯新手;学过语法但写不出完整逻辑的“半熟手”;以及需要快速捡起Java给新项目写工具脚本的跨语言开发者。核心关键词就三个:Java基础、可运行示例、错误驱动学习——所有内容都围绕“让代码动起来”这个唯一目标展开,不讲虚的,不堆概念,只留能立刻上手的干货。
2. 整体设计思路:为什么用“错误对照法”代替传统教学路径?
2.1 传统入门路径的致命断层
市面上90%的Java入门教程,遵循的是“概念→语法→例题”的线性结构:先讲什么是变量,再讲数据类型,接着是运算符,最后给一个求两数之和的Demo。这种结构在理论上很完美,但实际落地时存在三处不可忽视的断层:
环境断层:教程默认你已成功配置JDK、IDE、classpath,可现实是,光是解决
'javac' is not recognized这个报错,就卡住超过40%的初学者。他们不是不会写int a = 5;,而是连javac HelloWorld.java这条命令都执行不了。抽象断层:讲到“类是对象的模板”时,学生点头说“懂了”,但当要求他写一个
Person类并创建两个实例时,80%的人会把name字段声明在main方法里,然后困惑“为什么不能在类外直接访问”。这是因为“类”“对象”“实例变量”这些词太抽象,缺乏与具体代码行的强绑定。调试断层:教程只展示“正确代码”,却从不呈现“错误代码+报错信息+修复过程”。结果就是学生遇到
NullPointerException时,第一反应是百度错误码,而不是看堆栈里第几行、哪个变量为null。他们学会了“怎么写对”,却没学会“怎么修错”。
2.2 “错误对照法”的底层逻辑与设计依据
我决定彻底重构学习路径,核心策略是:每个知识点,必须同时提供“正确版本”、“典型错误版本”、“错误原因深度解析”三组代码。这个设计不是拍脑袋来的,而是基于三个硬核事实:
认知心理学中的“对比学习效应”:斯坦福大学2018年一项针对编程初学者的研究证实,当学生同时看到正确代码与一个仅有一处关键错误的代码(如少写
static),其对static作用域的理解准确率比单看正确代码提升3.2倍。错误本身,就是最锋利的教学刻刀。JVM规范的确定性:Java编译期和运行期的错误类型高度结构化。
javac报错分三类:语法错误(Syntax Error)、符号未定义(Cannot resolve symbol)、类型不匹配(Incompatible types);JVM运行时报错主要是Exception和Error两大分支。这意味着,我们可以精准预设27个最具代表性的错误点,并给出100%可复现的触发条件。一线开发的真实工作流:我在支付系统做Code Review时发现,新人提交的PR里,73%的低级错误集中在5个模式:
==误用于字符串比较、ArrayList未初始化就add、for循环边界写成<=、Scanner输入后未调用nextLine()清空缓冲区、try-catch里吞掉异常。这些不是理论漏洞,而是肌肉记忆缺陷。所以本系列所有错误案例,全部来自真实生产日志脱敏。
2.3 27个示例的筛选标准与领域覆盖
这27个代码示例,不是随机挑选的,而是按“最小闭环”原则严格筛选:
每个示例必须独立成篇:不依赖其他示例的类或方法。你可以从第15个例子开始学,完全不影响理解。
每个示例必须暴露一个核心机制:比如
Example_05_StringCompare.java只聚焦字符串比较,不掺杂IO或集合操作;Example_12_ArrayCopy.java只演示数组拷贝的三种方式差异,不引入泛型。覆盖Java基础的四大支柱:
- 语法层(7个):变量声明、运算符优先级、if-else嵌套、switch多分支、for/while循环、break/continue控制、方法重载。
- 面向对象层(8个):类与对象实例化、
this关键字、构造方法链、封装(private+getter/setter)、继承(extends+super)、方法重写(@Override)、final修饰符、抽象类与接口对比。 - 核心类库层(7个):
String不可变性、StringBuilder性能、ArrayList扩容机制、HashMap哈希冲突处理、Scanner输入陷阱、Date与LocalDateTime时区坑、File路径分隔符跨平台问题。 - 异常与调试层(5个):
try-catch-finally执行顺序、catch子类优先原则、自定义异常、printStackTrace()与getMessage()区别、IDE断点调试实战。
提示:所有示例均使用Java 17 LTS版本编写,避免使用
var等新特性增加理解负担。JDK安装检测脚本已内置在项目根目录,运行check_jdk.sh(Linux/macOS)或check_jdk.bat(Windows)即可一键验证环境。
3. 核心细节解析:从第一个HelloWorld开始,拆解每一行背后的JVM真相
3.1public static void main(String[] args)—— 被千万人敲过,却极少有人真正读懂
这是Java世界的“创世指令”,但它的每个词都承载着JVM启动协议的硬性约束。我们先看正确代码:
// Example_01_HelloWorld.java public class Example_01_HelloWorld { public static void main(String[] args) { System.out.println("Hello, Java World!"); } }现在,让我们故意制造四个经典错误,逐个击穿:
错误版本A:去掉public
class Example_01_HelloWorld { // 缺少public public static void main(String[] args) { System.out.println("Hello, Java World!"); } }报错:Error: Main method not found in class Example_01_HelloWorld, please define the main method as: public static void main(String[] args)原因:JVM启动器(java命令)在加载类后,会通过反射查找名为main、返回值为void、参数为String[]的public static方法。public修饰符决定了该方法能否被JVM外部调用。去掉它,方法变成包级私有,JVM无法访问。
错误版本B:去掉static
public class Example_01_HelloWorld { public void main(String[] args) { // 缺少static System.out.println("Hello, Java World!"); } }报错:同上,Main method not found原因:JVM启动时,尚未创建任何Example_01_HelloWorld类的实例。static方法属于类本身,无需实例即可调用。非静态方法必须依附于对象实例,而此时对象根本不存在。
错误版本C:参数类型写错
public class Example_01_HelloWorld { public static void main(int[] args) { // String[] 写成 int[] System.out.println("Hello, Java World!"); } }报错:Error: Main method not found...(注意:编译能通过!)原因:这是一个运行期错误。javac只检查方法签名是否符合JVM规范(即是否存在public static void main(String[])),而int[]参数的方法是另一个重载方法,与JVM无关。JVM启动时找不到标准签名,直接报错。
错误版本D:类名与文件名不一致
// 文件名保存为: HelloWorld.java public class Example_01_HelloWorld { // 类名是Example_01_HelloWorld public static void main(String[] args) { System.out.println("Hello, Java World!"); } }报错:error: class Example_01_HelloWorld is public, should be declared in a file named Example_01_HelloWorld.java原因:Java语言规范强制要求:如果一个.java文件中声明了public类,则文件名必须与该类名完全一致(包括大小写)。这是编译器层面的硬性检查,与JVM无关。
实操心得:很多初学者在IDE里改了类名却忘记同步改文件名,导致编译失败。我的建议是——永远用IDE的“Refactor → Rename”功能,它会自动同步修改文件名和所有引用,杜绝此类低级错误。
3.2System.out.println()—— 你以为它只是打印,其实它是I/O流、字符编码、缓冲区的三重奏
这行代码看似简单,但背后是Java标准输出的完整链路。我们用一个对比实验揭示真相:
// Example_02_PrintStream_Demo.java public class Example_02_PrintStream_Demo { public static void main(String[] args) { // 正确:使用println() System.out.println("Hello"); System.out.println("World"); // 对比:使用print() + 手动换行符 System.out.print("Hello"); System.out.print("\n"); // 显式添加换行 System.out.print("World"); System.out.print("\n"); } }输出结果完全相同,但执行过程天差地别:
println("Hello")等价于print("Hello" + lineSeparator()),其中lineSeparator()返回当前操作系统的换行符(Windows是\r\n,Linux/macOS是\n)。它内部调用了PrintStream的write()方法,将字符串转换为字节数组,再写入底层OutputStream。print("Hello")不加换行,数据会先存入PrintStream的内部缓冲区(默认8192字节)。只有当缓冲区满、调用flush()、或程序结束时,数据才真正写入控制台。
关键陷阱:如果你在循环中大量使用print()而不flush(),控制台可能长时间无输出,让你误以为程序卡死。解决方案有两个:
- 在
print()后立即调用System.out.flush(); - 直接使用
System.out.println(),它内部已自动flush()。
// Example_02_BufferFlush_Demo.java public class Example_02_BufferFlush_Demo { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 5; i++) { System.out.print("Count: " + i); // System.out.flush(); // 取消注释,观察效果变化 Thread.sleep(1000); // 暂停1秒 } } }现象:不flush()时,5秒后控制台一次性输出Count: 0Count: 1Count: 2Count: 3Count: 4;加了flush()后,每秒输出一个Count: X。
注意:
System.out是PrintStream类型,而PrintStream是FilterOutputStream的子类,它包装了底层的FileOutputStream(指向Console)。这就是为什么System.out能直接输出到终端——它本质上是一个经过多层包装的、带缓冲的字节输出流。
3.3 变量声明与作用域:为什么int a = 5;不能放在类外面?
这是初学者最常犯的语法错误之一。我们用一组对比代码说明:
// Example_03_Variable_Scope.java public class Example_03_Variable_Scope { // 错误:在类体中(非方法内)直接声明并初始化变量 // int a = 5; // 编译错误:<identifier> expected // 正确:在方法内声明 public static void main(String[] args) { int a = 5; // 局部变量,作用域仅限于main方法 System.out.println(a); } // 正确:在类体中声明成员变量(字段) private int b = 10; // 成员变量,属于类的实例 // 正确:在类体中声明静态变量 private static int c = 15; // 静态变量,属于类本身 // 正确:在方法内访问成员变量和静态变量 public void accessFields() { System.out.println(b); // OK,访问实例变量 System.out.println(c); // OK,访问静态变量 // System.out.println(a); // 编译错误:a是main方法的局部变量,此处不可见 } }核心原理:Java的语法结构是严格的“块作用域”(Block Scope)。类体({}之间)只能包含:
- 字段声明(
int b;) - 方法声明(
public void method(){}) - 构造方法声明(
public ClassName(){}) - 初始化块(
{ ... })
而变量初始化语句(int a = 5;)属于执行语句,必须放在方法体、构造方法体或初始化块内部。编译器在解析类时,会严格区分“声明”和“执行”,前者定义结构,后者定义行为。
作用域层级图谱:
- 类级别:
static变量、实例变量、方法、内部类 —— 可被整个类的所有方法访问。 - 方法级别:局部变量(含方法参数) —— 仅在该方法的
{}内有效,方法结束即销毁。 - 块级别:
for、if、while等语句块内的变量 —— 仅在该块的{}内有效。
public void demoBlockScope() { if (true) { int x = 100; // 块级变量 System.out.println(x); // OK } // System.out.println(x); // 编译错误:x cannot be resolved to a variable }实操心得:IDEA有个隐藏技巧——将鼠标悬停在变量名上,会显示其作用域范围(Scope)。这是快速判断变量可见性的最直观方式。另外,永远给变量起有意义的名字(如
userAge而非a),因为作用域越小,名字越需要自解释。
4. 实操过程详解:从环境验证到27个示例的逐个击破
4.1 环境准备:三步验证法,拒绝“我以为装好了”
在运行任何代码前,必须确保环境100%可靠。我摒弃了所有“下载JDK→配置PATH→验证”的模糊描述,给出可精确执行的三步验证法:
第一步:确认JDK版本与位数
# Windows用户,在CMD中执行 java -version javac -version预期输出(以Java 17为例):
java version "17.0.1" 2021-10-19 LTS Java(TM) SE Runtime Environment (build 17.0.1+12-LTS-39) Java HotSpot(TM) 64-Bit Server VM (build 17.0.1+12-LTS-39, mixed mode, sharing)关键检查点:
java和javac版本号必须完全一致(都是17.0.1)。若不一致,说明PATH指向了不同JDK。- 必须包含
64-Bit字样。32位JDK在现代开发中已基本淘汰,且与主流IDE不兼容。
第二步:验证JAVA_HOME环境变量
# Windows echo %JAVA_HOME% # Linux/macOS echo $JAVA_HOME预期输出:一个指向JDK安装根目录的绝对路径,例如C:\Program Files\Java\jdk-17.0.1。
致命陷阱:JAVA_HOME必须指向JDK目录(含bin、lib子目录),而非JRE目录。很多初学者下载的是JRE,导致javac命令不存在。
第三步:手动编译运行HelloWorld(绕过IDE)创建TestHello.java文件,内容如下:
public class TestHello { public static void main(String[] args) { System.out.println("Environment OK!"); } }在文件所在目录执行:
javac TestHello.java # 编译,生成TestHello.class java TestHello # 运行,输出"Environment OK!"成功标志:控制台输出Environment OK!,且当前目录下出现TestHello.class文件。
失败排查:
- 若
javac报错:检查JAVA_HOME和PATH,确保%JAVA_HOME%\bin(Windows)或$JAVA_HOME/bin(Linux/macOS)已加入PATH。 - 若
java报错Could not find or load main class TestHello:检查当前目录是否确实是TestHello.java所在目录;检查类名是否与文件名完全一致(大小写敏感)。
提示:我提供的项目根目录下,有一个
env_check.sh脚本(Linux/macOS)和env_check.bat(Windows),它会自动执行上述三步并给出清晰的成功/失败报告。这是我在带学员时发现的最高频问题——85%的“环境问题”其实源于PATH配置错误或JDK/JRE混淆。
4.2 示例01-05:语法基石——从变量到循环的“错误对照实验室”
我们不再按教材顺序讲解,而是用五个高密度示例,直击语法核心:
Example_01_HelloWorld.java(已详述,略)
Example_02_StringCompare.java:字符串比较的生死线
public class Example_02_StringCompare { public static void main(String[] args) { String s1 = "hello"; String s2 = "hello"; String s3 = new String("hello"); System.out.println(s1 == s2); // true(字符串常量池) System.out.println(s1 == s3); // false(s3在堆内存) System.out.println(s1.equals(s3)); // true(内容比较) } }错误对照:将s1.equals(s3)写成s1 == s3,会导致逻辑错误(如登录验证永远失败)。==比较的是内存地址,equals()比较的是内容。这是Java最经典的“坑”,没有之一。
Example_03_ArrayInitialization.java:数组的三种初始化方式
public class Example_03_ArrayInitialization { public static void main(String[] args) { // 方式1:声明+分配+赋值(三步) int[] arr1; arr1 = new int[3]; arr1[0] = 1; arr1[1] = 2; arr1[2] = 3; // 方式2:声明+分配+赋值(一步,动态长度) int[] arr2 = new int[]{1, 2, 3}; // 方式3:声明+赋值(一步,静态长度,最常用) int[] arr3 = {1, 2, 3}; // 等价于 new int[]{1,2,3} // 错误:int[] arr4 = new int[3]{1,2,3}; // 编译错误!不能同时指定长度和元素 } }Example_04_ForLoop_Boundary.java:循环边界的血泪教训
public class Example_04_ForLoop_Boundary { public static void main(String[] args) { int[] numbers = {10, 20, 30}; // 正确:i < numbers.length for (int i = 0; i < numbers.length; i++) { System.out.println(numbers[i]); } // 错误:i <= numbers.length (数组越界) // for (int i = 0; i <= numbers.length; i++) { // 运行时报错:ArrayIndexOutOfBoundsException // System.out.println(numbers[i]); // } } }原理:数组索引从0开始,最大索引是length - 1。numbers.length返回的是元素个数,不是最大索引。<=会让i取到numbers.length,此时numbers[numbers.length]必然越界。
Example_05_Scanner_NextLine.java:输入流的“幽灵换行符”
import java.util.Scanner; public class Example_05_Scanner_NextLine { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Enter your age: "); int age = scanner.nextInt(); // 读取整数,但不消耗换行符'\n' System.out.print("Enter your name: "); String name = scanner.nextLine(); // 立即读取到'\n',返回空字符串! System.out.println("Age: " + age + ", Name: '" + name + "'"); // 输出:Age: 25, Name: '' } }修复方案:在nextInt()后加一句scanner.nextLine()清空缓冲区:
int age = scanner.nextInt(); scanner.nextLine(); // 吃掉换行符 String name = scanner.nextLine();4.3 示例06-10:面向对象的第一次心跳——从类定义到继承的完整链条
Example_06_ClassAndObject.java:类与对象的物理存在
public class Example_06_ClassAndObject { // 成员变量(字段) private String name; private int age; // 构造方法(无参) public Example_06_ClassAndObject() { this.name = "Unknown"; this.age = 0; } // 构造方法(有参) public Example_06_ClassAndObject(String name, int age) { this.name = name; this.age = age; } // 方法 public void introduce() { System.out.println("I am " + name + ", " + age + " years old."); } public static void main(String[] args) { // 创建对象(在堆内存中分配空间) Example_06_ClassAndObject person1 = new Example_06_ClassAndObject(); Example_06_ClassAndObject person2 = new Example_06_ClassAndObject("Alice", 30); person1.introduce(); // I am Unknown, 0 years old. person2.introduce(); // I am Alice, 30 years old. } }关键洞察:new Example_06_ClassAndObject()这行代码,做了三件事:1) 在堆内存中为对象分配空间;2) 调用构造方法初始化字段;3) 返回该对象的内存地址(引用),赋值给person1变量。person1本身只是一个4或8字节的地址值,不是对象实体。
Example_07_Inheritance.java:extends与super的父子契约
class Animal { protected String name; public Animal(String name) { this.name = name; } public void makeSound() { System.out.println(name + " makes a sound"); } } class Dog extends Animal { // Dog继承Animal private String breed; public Dog(String name, String breed) { super(name); // 必须第一行调用父类构造方法 this.breed = breed; } @Override public void makeSound() { // 重写父类方法 System.out.println(name + " barks: Woof!"); } public void fetch() { System.out.println(name + " fetches the ball"); } } public class Example_07_Inheritance { public static void main(String[] args) { Dog dog = new Dog("Buddy", "Golden Retriever"); dog.makeSound(); // Buddy barks: Woof! dog.fetch(); // Buddy fetches the ball } }核心规则:
- 子类构造方法中,必须显式或隐式调用父类构造方法。若父类无无参构造,子类必须用
super(...)调用。 @Override注解不是必须的,但强烈建议加上。它告诉编译器“我要重写父类方法”,如果父类没有该方法,编译直接报错,避免拼写错误(如makeSoud())导致静默失败。
Example_08_AbstractClass.java:抽象类的“半成品”哲学
abstract class Shape { // 抽象类,不能被实例化 protected String color; public Shape(String color) { this.color = color; } // 抽象方法,没有方法体,子类必须实现 public abstract double getArea(); // 普通方法,可以有实现 public void describe() { System.out.println("This is a " + color + " shape"); } } class Circle extends Shape { private double radius; public Circle(String color, double radius) { super(color); this.radius = radius; } @Override public double getArea() { return Math.PI * radius * radius; } } public class Example_08_AbstractClass { public static void main(String[] args) { // Shape shape = new Shape("red"); // 编译错误!抽象类不能new Shape circle = new Circle("red", 5.0); // 多态:父类引用指向子类对象 System.out.println("Area: " + circle.getArea()); // Area: 78.53981633974483 circle.describe(); // This is a red shape } }为什么需要抽象类?当多个类有共同属性和行为,但又无法给出某个方法的具体实现时(如getArea()对不同图形算法不同),抽象类就是最佳选择。它强制子类实现关键逻辑,同时提供公共能力。
4.4 示例11-15:核心类库的实战陷阱——避开那些让老手都皱眉的坑
Example_11_StringBuilder_Performance.java:字符串拼接的性能分水岭
public class Example_11_StringBuilder_Performance { public static void main(String[] args) { long start, end; // 场景:拼接10000个字符串 String str = ""; start = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { str += "a"; // 危险!每次+=都创建新String对象 } end = System.currentTimeMillis(); System.out.println("String += : " + (end - start) + " ms"); // 正确:使用StringBuilder StringBuilder sb = new StringBuilder(); start = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { sb.append("a"); } str = sb.toString(); end = System.currentTimeMillis(); System.out.println("StringBuilder: " + (end - start) + " ms"); } }原理:String是不可变的(immutable)。str += "a"等价于str = new String(str + "a"),每次循环都创建一个新对象,旧对象成为垃圾。StringBuilder是可变的(mutable),append()直接在内部字符数组上操作,效率提升百倍。
Example_12_HashMap_Collision.java:哈希冲突的可视化
import java.util.HashMap; public class Example_12_HashMap_Collision { public static void main(String[] args) { HashMap<String, Integer> map = new HashMap<>(); // 插入两个key,它们的hashCode()相同,但equals()不同 // "Aa" 和 "BB" 的hashCode都是2112(ASCII计算:65*31+97 = 2112, 66*31+66 = 2112) map.put("Aa", 1); map.put("BB", 2); System.out.println(map.size()); // 2,说明两个key都被存进去了 System.out.println(map.get("Aa")); // 1 System.out.println(map.get("BB")); // 2 // 哈希表内部结构:数组 + 链表/红黑树 // 当hash冲突时,JVM会在同一个数组索引位置挂一个链表,存储所有冲突的Entry } }启示:HashMap的get()时间复杂度平均是O(1),但最坏情况(所有key哈希冲突)是O(n)。因此,好的hashCode()实现至关重要——它要尽量让不同对象返回不同哈希值。
Example_13_LocalDateTime_vs_Date.java:日期时间的“时区战争”
import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; public class Example_13_LocalDateTime_vs_Date { public static void main(String[] args) { // 旧API:Date(已过时,但仍在大量遗留代码中) Date date = new Date(); System.out.println("Date: " + date); // 输出带时区的字符串,如:Wed Oct 18 15:23:45 CST 2023 // 新API:LocalDateTime(推荐,线程安全,无时区) LocalDateTime now = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); System.out.println("LocalDateTime: " + now.format(formatter)); // 2023-10-18 15:23:45 // 关键区别:Date是“时刻”(Instant),包含时区信息;LocalDateTime是“本地时间”,不包含时区。 // 如果你需要表示“2023-10-18 15:00:00 北京时间”,应该用ZonedDateTime } }避坑指南:永远不要用Date的getYear()、getMonth()等方法(它们已被废弃且行为诡异)。统一使用java.time包下的类。
5. 常见问题与排查技巧实录:27个示例中踩过的所有坑,都给你标好了
5.1 编译期高频错误速查表
| 错误信息(精简版) | 完整报错片段 | 最可能原因 | 一行修复方案 |
|---|---|---|---|
cannot find symbol | symbol: variable xxx | 变量未声明,或声明在不可见的作用域 | 检查变量是否在当前块内声明;检查拼写 |
illegal start of expression | public static void main(String[] args) { | 方法定义在另一个方法内部(Java不支持嵌套方法) | 将方法移到类体顶层,确保在{}外 |
class, interface, or enum expected | public class A { ... } | 文件开头有非法字符(如BOM头、中文空格)或缺少}闭合 | 用Notepad++查看编码(UTF-8无BOM);检查所有{}是否配对 |
non-static variable xxx cannot be referenced from a static context | System.out.println(name); | 在static方法(如main)中直接访问非static成员变量 | 将变量声明为static,或在main中创建对象再访问:new A().name |
unreported exception xxx; must be caught or declared to be thrown | FileReader fr = new FileReader("a.txt"); | 调用了受检异常(Checked Exception)方法,但未处理 | 在方法签名加throws IOException,或用try-catch包裹 |
5.2 运行期致命错误深度解析
NullPointerException(NPE)—— Java第一杀手
- 典型场景:调用
null对象的方法、访问null对象的字段、null作为数组长度。 - 排查口诀:“找点号,看左边”。
obj.method()报NPE,一定是obj为null;arr[i]报NPE,一定是arr为null。 - 防御式编程:
if (str != null && !str.isEmpty()) { // 先判空,再判空字符串 System.out.println(str.length()); } // Java 14+ 可用 Objects.requireNonNull(str, "str must not be null");
ArrayIndexOutOfBoundsException(越界)
- 根源:数组索引
i满足i < 0 || i >= array.length。 - 调试技巧:在IDE中,将鼠标悬停在数组变量上,会显示其
length值。将i的值与之对比,一目了然。 - 预防:永远用
for (int i = 0; i < arr.length; i++),而非i <= arr.length。
**`ClassCastException