
Java教程电子书:从基础语法到面向对象编程
Java 常用于后端服务、企业应用、Android 开发和各类跨平台程序。它有明确的类型系统、成熟的标准库和丰富的开发工具,适合构建需要长期维护的应用。但“写一次,到处运行”不代表程序不受操作系统、运行时版本和部署环境影响;Java 也不会自动替开发者处理输入校验、并发安全或资源管理。
本文从可运行的小程序开始,逐步介绍 Java 基础语法和面向对象编程。示例以 JDK 17 或更高版本为参考,主要使用传统语法,便于在不同 Java 项目中阅读和运行。除非特别说明,每个代码块都是独立示例,不要把多个同名的 public class 放进同一个源文件。
一、准备 JDK 并运行第一个程序
运行 Java 程序需要 Java 运行时;编译源代码则需要 JDK(Java Development Kit)。安装 JDK 后,在终端检查工具是否可用:
java -version
javac -version
创建文件 Hello.java,文件名与公开类名 Hello 一致:
public class Hello {
public static void main(String[] args) {
System.out.println("你好,Java!");
}
}
在该文件所在目录执行:
javac Hello.java
java Hello
javac 将源代码编译为字节码,通常输出 Hello.class;java 启动 Java 虚拟机并运行类。运行时写类名,不写 .class 后缀。若终端提示找不到命令,先检查 JDK 是否安装、环境变量是否配置,以及当前目录是否正确。
从 Java 11 起,也可以直接运行单文件源代码,例如 java Hello.java。这种方式适合快速试验;正式项目通常使用构建工具管理依赖、编译选项和测试。不同 JDK 版本支持的语言特性不完全相同,项目应明确要求的 Java 版本。
二、变量、类型与表达式
Java 是静态类型语言:变量声明时要确定类型,编译器会检查许多类型错误。基本类型包括整数型 byte、short、int、long,浮点型 float、double,字符型 char 和布尔型 boolean。引用类型包括类、数组、接口等;字符串 String 就是引用类型,不是基本类型。
public class Variables {
public static void main(String[] args) {
int count = 12;
long population = 8_000_000_000L;
double price = 19.95;
boolean enabled = true;
char grade = 'A';
String name = "Java";
System.out.println(name + " 的数量:" + count);
System.out.println("启用状态:" + enabled);
}
}
整数常量默认是 int,超出 int 范围的整数字面量需要使用 L 后缀表示为 long。float 字面量通常需要加 F,例如 3.14F。整数运算可能溢出,Java 不会因为普通的整数溢出自动报错;需要时应使用更合适的类型,或调用 Math.addExact 等检查溢出的方法。对于金额等需要精确十进制计算的场景,不宜直接依赖二进制浮点数,可考虑 BigDecimal 并明确舍入规则。
局部变量必须先赋值再读取;类的字段则会获得默认值,例如数值字段默认是 0、布尔字段默认是 false、引用字段默认是 null。不要把“字段有默认值”误认为“所有变量都能不初始化”。
用 final 声明变量后,变量只能赋值一次;若它保存的是对象引用,引用不能重新指向别的对象,但对象本身是否可变取决于对象类型。
final int maxRetries = 3;
// maxRetries = 4; // 编译错误
字符串通过 + 拼接。比较字符串内容应使用 equals,而不是 ==:后者比较的是引用是否指向同一个对象。
String first = new String("Java");
String second = new String("Java");
System.out.println(first.equals(second)); // true
System.out.println(first == second); // false
如果变量可能为 null,调用方法前需要先判断,否则可能触发 NullPointerException。例如,使用 if (name != null && name.isBlank()) 时,&& 会短路求值,第二个条件只会在第一个条件为真时执行。
三、条件判断与循环
if 和 else 用于按条件分支。条件表达式必须产生布尔值,Java 不会把数字自动当成真假值。
int score = 86;
if (score >= 90) {
System.out.println("优秀");
} else if (score >= 60) {
System.out.println("通过");
} else {
System.out.println("未通过");
}
当分支是对同一个变量的有限取值进行匹配时,可以使用 switch。传统 switch 需要留意 break,否则可能继续执行后续分支:
int day = 2;
switch (day) {
case 1:
System.out.println("周一");
break;
case 2:
System.out.println("周二");
break;
default:
System.out.println("其他日期");
}
for 适合次数或迭代范围明确的场景,while 适合重复执行直到条件改变的场景。循环条件必须有机会变为假,否则程序可能陷入无限循环。
for (int i = 1; i <= 3; i++) {
System.out.println(i);
}
int remaining = 2;
while (remaining > 0) {
System.out.println("剩余:" + remaining);
remaining--;
}
四、方法:组织可复用逻辑
方法将一段操作封装起来。方法声明包括返回类型、名称和参数;返回类型为 void 表示不返回值。下面的类可以保存为 Methods.java 后运行:
public class Methods {
static int add(int left, int right) {
return left + right;
}
static String greet(String name) {
if (name == null || name.isBlank()) {
return "你好!";
}
return "你好," + name + "!";
}
public static void main(String[] args) {
System.out.println(add(2, 3));
System.out.println(greet("小林"));
}
}
static 方法属于类,可以通过类名调用;不带 static 的实例方法需要先创建对象。参数类型与调用时传入的值应匹配。Java 采用值传递:传入基本类型时复制数值;传入对象时复制的是引用值,因此方法可以通过该引用修改对象状态,但不能把调用方的变量重新指向另一个对象。
方法可以重载,即在同一个类中使用相同名称、不同参数列表的方法。仅改变返回类型不能构成重载。方法参数的数量和类型应根据业务需要设计;Java 对方法参数数量没有适用于所有程序的统一业务上限,但极长参数列表通常意味着接口难以使用,可以考虑封装为参数对象。
五、数组与集合
数组长度固定,适合元素数量已知且需要按下标访问的场景。下标从 0 开始,合法范围是 0 到 length - 1;访问越界会抛出 ArrayIndexOutOfBoundsException。
public class ArraysDemo {
public static void main(String[] args) {
int[] scores = {82, 91, 76};
int total = 0;
for (int score : scores) {
total += score;
}
double average = (double) total / scores.length;
System.out.println("平均分:" + average);
}
}
强制转换为 double 可以避免整数除法丢弃小数。若数组可能为空,不要直接用 scores[0],也不要在未检查长度时除以 scores.length。
需要动态增删元素时,常用集合。ArrayList 会管理底层数组的扩容,适合按位置访问和追加元素;它并非所有操作都线程安全,也不意味着任意插入和删除都是常数时间。
import java.util.ArrayList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("安娜");
names.add("陈明");
for (String name : names) {
System.out.println(name);
}
}
}
集合泛型中的类型参数必须是引用类型,因此不能写 List<int>,应使用包装类型 List<Integer>。自动装箱会在基本类型和包装类型之间转换,但包装对象可能为 null,拆箱时会导致空指针异常。选择 List、Set 还是 Map,应看是否需要有序元素、去重或键值查询;不要只因为某种集合常见就默认使用它。
六、类、对象与封装
类描述对象的状态和行为,对象是类的实例。构造器负责初始化对象;它的名称与类名相同,没有返回类型。封装则是通过访问控制隐藏内部状态,并提供明确的操作方式。下面的示例保存为 BankAccountDemo.java 可运行:
class BankAccount {
private final String owner;
private long balanceCents;
BankAccount(String owner, long openingBalanceCents) {
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("账户名称不能为空");
}
if (openingBalanceCents < 0) {
throw new IllegalArgumentException("初始余额不能为负数");
}
this.owner = owner;
this.balanceCents = openingBalanceCents;
}
public String getOwner() {
return owner;
}
public long getBalanceCents() {
return balanceCents;
}
public void deposit(long amountCents) {
if (amountCents <= 0) {
throw new IllegalArgumentException("存入金额必须大于零");
}
balanceCents = Math.addExact(balanceCents, amountCents);
}
}
public class BankAccountDemo {
public static void main(String[] args) {
BankAccount account = new BankAccount("林", 12000);
account.deposit(3500);
System.out.println(account.getOwner());
System.out.println(account.getBalanceCents());
}
}
此例以“分”为单位保存金额,避免用 double 直接表达货币的小数金额;实际金融业务仍需明确币种、舍入和精度规则。Math.addExact 在 long 溢出时会抛出异常,而不是静默回绕。构造器和存款方法都做了基本参数校验,但这只是领域规则示例,不足以构成账户系统的安全设计。
private 成员只能在本类内部直接访问;public 成员可从其他类访问。字段设为私有并通过方法操作,能避免外部随意改写对象状态。并不是每个字段都要机械地生成 getter 和 setter:只开放业务确实需要的操作,通常更容易维护不变量。
七、继承、多态与接口
继承用 extends 表示类之间的“是一种”关系。子类可以复用父类的可访问成员,也可以重写实例方法。多态意味着通过父类或接口类型的引用调用方法时,实际执行的是对象所属类的实现。
interface Shape {
double area();
}
class Circle implements Shape {
private final double radius;
Circle(double radius) {
if (!Double.isFinite(radius) || radius < 0) {
throw new IllegalArgumentException("半径必须是有限的非负数");
}
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private final double width;
private final double height;
Rectangle(double width, double height) {
if (!Double.isFinite(width) || !Double.isFinite(height)
|| width < 0 || height < 0) {
throw new IllegalArgumentException("边长必须是有限的非负数");
}
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public class ShapesDemo {
public static void main(String[] args) {
Shape[] shapes = {
new Circle(2),
new Rectangle(3, 4)
};
for (Shape shape : shapes) {
System.out.println(shape.area());
}
}
}
将代码保存为 ShapesDemo.java,可用 javac ShapesDemo.java 编译并用 java ShapesDemo 运行。这里调用的是 Shape 接口定义的 area,实际执行哪个实现取决于对象类型。接口适合描述一组能力或约定,便于替换实现和编写测试。Java 类只能直接继承一个父类,但可以实现多个接口。
@Override 用于标记重写方法;若签名写错,编译器会提示错误。构造器不能被子类继承,静态方法也不按实例方法的方式实现动态重写。继承会建立较紧密的结构依赖;如果只是复用功能而没有清晰的“是一种”关系,组合通常更合适。
八、异常:区分可恢复问题与程序错误
异常用于报告程序无法正常继续的情况。try/catch 可以处理预期内的问题,但捕获后应采取有意义的动作,例如提示用户、记录必要信息或向上层传递;不应使用空的 catch 把错误隐藏起来。
public class ParseDemo {
public static void main(String[] args) {
String text = "123";
try {
int value = Integer.parseInt(text);
System.out.println(value);
} catch (NumberFormatException e) {
System.out.println("输入不是有效整数");
}
}
}
Java 有检查型异常和非检查型异常。检查型异常需要在方法签名中声明或在方法内处理;非检查型异常通常是 RuntimeException 及其子类。不要为了消除编译提示而随意把所有异常包装成无信息的通用异常,也不要将参数非法、文件不存在和程序逻辑缺陷混为一谈。
使用文件、网络连接等资源时,优先使用 try-with-resources,它会在离开代码块时关闭实现了 AutoCloseable 的资源:
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadFileDemo {
public static void main(String[] args) {
Path path = Path.of("notes.txt");
try (BufferedReader reader = Files.newBufferedReader(path)) {
System.out.println(reader.readLine());
} catch (IOException e) {
System.err.println("读取文件失败:" + e.getMessage());
}
}
}
此程序会读取当前工作目录下的 notes.txt 第一行;文件不存在、权限不足或发生其他 I/O 问题时会进入异常处理。部署环境中的当前工作目录可能与源代码目录不同,应明确文件路径的来源和权限。
九、常见错误与排查顺序
- 公开类名与文件名不一致:
public class Example通常应放在Example.java中。 - 把赋值和比较混淆:
=用于赋值,比较基本类型时常用==;比较字符串内容用equals。 - 数组下标越界:先检查长度,再访问下标;数组最后一个元素的下标是
length - 1。 - 整数除法丢失小数:确保运算前至少一个操作数是浮点类型,而不是在结果产生后才转换。
- 空指针异常:检查对象是否可能为
null,并确认调用链中的每个引用都已正确初始化。 - 忘记返回或处理异常:按编译器提示核对所有代码路径,区分应处理的异常与应向上层传递的异常。
- 修改循环条件不当:确认循环变量在循环中会变化,并用边界值检查是否会多执行或少执行一次。
排查时可以先看编译器给出的文件名、行号和错误类型,再检查最近修改的代码。编译错误通常意味着语法或类型不匹配;运行时异常则要根据异常类和堆栈定位实际失败位置。不要只依据报错最后一行猜原因,堆栈中第一个指向自己代码的行通常更值得优先检查。
十、参数边界与安全用途
参数校验应放在可信边界处:例如命令行输入、HTTP 请求、文件内容和外部服务响应。不能仅因为前端已校验,就假设服务端收到的值一定合法。校验范围应根据业务规则确定,既要拒绝空值、非法格式和超范围值,也不要凭空设定与业务无关的限制。
Java 的基本类型有确定的取值范围,例如 int 是有符号 32 位整数,long 是有符号 64 位整数;但业务参数能否接受这些范围内的所有值,需要额外判断。数组长度、集合大小和字符串长度受到内存与运行环境等因素影响,不能把类型范围直接等同于可安全处理的输入规模。String.length() 返回 UTF-16 代码单元数量,不一定等于用户看到的字符数;限制用户名或文本长度时,应先定义计数口径。
输入校验也不能替代安全处理。构造 SQL 时应使用参数化查询,而不是把用户输入直接拼接进 SQL;输出到 HTML 时应进行符合输出位置要求的编码;读写文件时要控制允许访问的路径,避免不可信输入改变目标位置。加密、密码散列、授权和访问控制需要采用经过审查的标准库或成熟方案,不能把本文的简单示例当作安全实现。
Java 本身不是自动安全沙箱。普通 Java 程序通常以运行它的操作系统用户权限访问资源;应用的安全性还取决于代码、依赖、部署配置和运行权限。不要在源码或版本库中硬编码口令、令牌和私钥;也不要把 System.out.println 当作敏感信息的安全日志方案。需要处理真实凭证时,应使用适合部署环境的秘密管理机制,并限制读取权限。
结语:从小程序走向项目
掌握 Java 的基础语法后,可以继续学习泛型、集合框架、Lambda 表达式、单元测试、并发与网络编程。实际项目还需要版本控制、依赖管理和构建流程;常见构建工具包括 Maven 和 Gradle。学习时应从能够独立编译运行的例子开始,再逐步加入输入校验、异常处理和测试,而不是一次堆入大量框架代码。
一个可维护的 Java 程序,不仅要“能运行”,还应让类型、方法和类的职责清楚,明确输入边界,并在错误发生时给出可诊断的行为。把这些基本习惯带入后续项目,比记住更多语法形式更重要。




