Java教程电子书:从基础语法到面向对象编程

技术教程2小时前发布 jdkjadmin
0 00
Java教程电子书:从基础语法到面向对象编程

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 程序,不仅要“能运行”,还应让类型、方法和类的职责清楚,明确输入边界,并在错误发生时给出可诊断的行为。把这些基本习惯带入后续项目,比记住更多语法形式更重要。

© 版权声明

相关文章