11102 字
56 分钟
JAVA

2026年开学第一个学期,我开始了 Java 基础的学习。
Java与Csharp的语法结构十分相似,本文中 Java 使用的 IDE 是 IntelliJ IDEA

环境与第一个程序#

创建一个项目,选择 Java 模块,不需要模板,自行选择路径与命名,不要出现中文。 项目创建完成后,在 src 目录下新建一个 软件包软件包 内创建 文件,IDEA 会默认生成如下结构:

Java
public class Main {
    public static void main(String[] args) {
        // 代码写在这里
    }
}

与 C# 类似,main 方法是程序的入口,代码从这里开始执行。 在 main 方法中输入以下代码,点击左侧绿色运行按钮或按 Shift + F10 运行:

Java
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

控制台输出:

plaintext
Hello, World!

System.out.println() 是 Java 中最基础的输出语句,作用与 C# 的 Console.WriteLine() 相同,输出内容后自动换行。 System.out.print() 则不会自动换行,可以配合 \n(换行)、\t(制表符)手动控制格式。

注释与标识符#

Java
// 单行注释

/*
   多行注释
*/

/**
 * 文档注释
 */

关键字:Java 中赋予了特定用途和含义的保留词,编辑器会高亮显示,如 publicclassint 等。

标识符(自己命名的词)需满足:

  • 不能使用除 _$ 以外的符号
  • 不能以数字开头
  • 不能使用关键字
  • 区分大小写,建议见名知意

常见命名规范:

场景规范示例
包名全小写,用 . 隔开com.example.util
类名、接口名大驼峰MyClass
变量名、方法名小驼峰myVariable
常量名全大写MAX_SIZE

数据类型#

Java 的数据类型分为基本数据类型引用数据类型

基本数据类型#

Java
// 整数类型
byte  a = 127;                    // 字节
short b = 10000;                  // 短整型
int   c = 1000000000;             // 整型
long  d = 1000000000000000000L;   // 长整型,末尾加 L

// 浮点类型
float  e = 1.111f;   // 单精度,末尾加 f
double f = 1.222;    // 双精度

// 字符类型
char g = '牢';       // 存单个字符,可通过 Unicode 编码转为 int
c = g;
System.out.println(c); // 输出对应 Unicode 数值

// 布尔类型
boolean h = true;
boolean i = 5 > 3;   // 可以直接存逻辑运算的结果
System.out.println(i); // true

引用数据类型#

Java
String j = "张三";   // 字符串
// 还有类类型、数组等

数据类型转换#

自动转换:小类型转大类型,直接赋值即可。

强制转换:大类型转小类型,需要手动指定,小数转整数只保留整数部分。

Java
int  i1 = 20;
byte i2 = 10;

i2 = (byte) i1;   // 强制转换:大转小
System.out.println(i2); // 20

变量#

变量是内存中有类型、标识名、值的一块空间。

Java
// 先声明后赋值
float chenji;
chenji = 98.5f;

// 声明同时赋值
float chenji = 98.5f;
String name = "张三";
char sex = '男';

System.out.println(chenji); // 98.5
System.out.println(name);   // 张三
System.out.println(sex);    // 男

运算符#

常用运算符一览#

Java
// 赋值运算符
int a = 10;

// 算数运算符:+ - * / % += -= ++ --
int c = a++;     // 先把 a 的值赋给 c,再自增
System.out.println(a); // 11
System.out.println(c); // 10

// 关系运算符:> < >= <= != ==(结果都是布尔值)

// 逻辑运算符:&& || !
boolean f  = 1 < 2 && 2 > 1;   // true
boolean f1 = 2 == 3 || 1 == 1; // true
boolean f2 = !(1 < 2);         // false

优先级(从高到低):! > 算数 > 比较 > && > || > 赋值

小括号优先级最高,可用于调整执行顺序。

字符串比较#

字符串不能用 == 比较内容,应使用 .equals() 方法:

Java
String s1 = "aa";
String s2 = "aa";
boolean s3 = s1.equals(s2);
System.out.println("s3:" + s3); // s3:true

键盘输入#

Java 通过 Scanner 类从键盘读取用户输入:

Java
import java.util.Scanner;

Scanner input = new Scanner(System.in);
// 在线运行时可以在输入框填写:111 张三 12.5

// 读取整数
System.out.println("请输入整数");
if (input.hasNextInt()) {
    int k = input.nextInt();
    System.out.println("整数:" + k);
} else {
    System.out.println("没有读取到整数");
}

// 读取字符串
System.out.println("请输入姓名");
if (input.hasNext()) {
    String name = input.next();
    System.out.println("姓名:" + name);
} else {
    System.out.println("没有读取到姓名");
}

// 读取浮点数(带输入校验)
System.out.println("请输入金额");
if (input.hasNextDouble()) {
    double j = input.nextDouble();
    System.out.println("金额:" + j);
} else {
    System.out.println("没有读取到金额");
}

选择结构#

if 语句#

Java
// 单分支
if (true) {
    System.out.println("成立时执行");
}

// 双分支
if (true) {
    System.out.println("成立");
} else {
    System.out.println("不成立");
}

// 多分支
int score = 90;
if (score >= 90) {
    System.out.println("优秀");
} else if (score >= 80) {
    System.out.println("良好");
} else if (score >= 70) {
    System.out.println("中等");
} else if (score >= 60) {
    System.out.println("及格");
} else {
    System.out.println("不及格");
}

if 语句可以相互嵌套:

Java
int s = 9;
int sex = 0; // 0:男,-1:女

if (s < 10) {
    if (sex == 0) {
        System.out.println("男子组");
    } else {
        System.out.println("女子组");
    }
} else {
    System.out.println("out");
}

switch 语句#

switch 的表达式需要是整型或可自动转为整型的数据类型(如 char)。

Java
int i = 1;
switch (i) {
    case 1:
        System.out.println("周一");
        break;
    case 2:
        System.out.println("周二");
        break;
    // ...
    default:
        System.out.println("无效的日期");
}

循环结构#

while 循环#

先判断条件,再执行。

Java
int a = 1;
while (a < 10) {
    System.out.println(a);
    a++;
}

do-while 循环#

先执行一次,再判断条件。

Java
int a = 1;
do {
    System.out.println(a);
    a++;
} while (a < 10);

for 循环#

Java
for (int a = 1; a < 10; a++) {
    System.out.println(a);
}

循环中可以使用 break 结束整个循环,continue 跳过当前次进入下一次。

类与对象#

这部分是面向对象的起点,Java 里几乎所有东西都围着 class 转。

什么是类和对象#

:一类事物的统称,是抽象的概念,由思维产生。如:人、学生、汽车。

对象:实际存在的某个事物,能看得见摸得着。如:张三、李四。

类是对象的抽象,对象是类的实例化。

定义一个类#

Java 使用 class 关键字定义类,类中包含属性(静态特征)和方法(动态特征):

Java
public class Dog {
    // 静态特征:属性
    String name;    // 昵称
    String strain;  // 品种
    double weight;  // 体重

    // 动态特征:方法
    public void show() {
        System.out.println("我的狗的名字是:" + name + " 品种是:" + strain + " 体重是:" + weight + "KG");
    }
}

创建对象#

Java
class Dog {
    String name;
    String strain;
    double weight;

    public void show() {
        System.out.println("我的狗的名字是:" + name + " 品种是:" + strain + " 体重是:" + weight + "KG");
    }
}

// 类名 对象名 = new 类名();
Dog dog = new Dog();

// 调用属性赋值
dog.name = "kobe";
dog.strain = "拉得不多";
dog.weight = 5.45;

// 调用方法
dog.show();

方法的定义与返回值#

text
public 返回值类型 方法名() {
    // 方法体
    return 表达式;
}
  • 返回值类型决定了 return 后返回的值的类型,二者要匹配
  • 没有返回值使用 void,不需要 return 语句
  • return 是方法体的最后一条语句,只能返回一个值
Java
public class Student {
    String name;
    double score;

    public void study() {
        System.out.println(name + "的语文成绩为" + score);
    }

    public double sum() {
        double a = 350.00;
        return a * 1.2;  // 返回计算结果
    }
}

变量的作用域#

全局变量(成员变量)#

定义在类中,Java 会自动给初始值:Stringnull,整型为 0,浮点为 0.0,布尔为 false。在整个类中都可以使用。

局部变量#

定义在方法或代码块中,Java 不会给初始值,只在定义该变量的方法或代码块中有效。

全局变量和局部变量可以重名,局部变量的调用优先级更高。

有参方法#

text
public 返回值类型 方法名(参数1类型 参数1名, 参数2类型 参数2名) {
    方法体;
}

调用时传入的是实际参数,定义时括号里的是形式参数,两者的数量、数据类型、顺序要一一对应。

Java
public class Cal {
    // 计算两个数的和,有返回值
    public int sum(int a, int b) {
        int c = a + b;
        return c;
    }

    // 计算两个数的差,无返回值
    public void gap(int a, int b) {
        int c = a - b;
        System.out.println(c);
    }
}

// 调用
Cal cal = new Cal();
int result = cal.sum(10, 20); // 传入实参
System.out.println(result);

#

包名全部小写,多级用 . 隔开,例如 team.shiro.javaoop。包可以解决文件名冲突,方便管理类和接口。

Java
// 声明包,写在第一行
package team.shiro;

// 导入包,写在 package 后
import java.util.*;

数组#

数组是什么#

用来存储一组相同数据类型的数据。

特点:能存储多个值,所有值数据类型相同,操作方便。

相关概念

  • 标识符:数组的名称
  • 数组元素:存在数组中的每个数据
  • 数组下标:数据的位置编号,从 0 开始
  • 数据类型:数组可存放的数据类型

声明与使用#

Java
// 声明方式
int[] a;
int b[];

// 声明时指定大小
int[] c = new int[5];

// 声明时直接赋值
int[] d = {1, 2, 3, 4, 5};

// 分配空间
a = new int[5];

// 通过下标存取元素
a[0] = 10;
a[0] = a[0] + 10;

数组遍历#

数组遍历通过循环 + 下标实现,数组名.length 获取数组长度:

Java
import java.util.Scanner;

Scanner input = new Scanner(System.in);
double[] arr = new double[5];
double[] defaults = {12.5, 36.0, 18.8, 7.5, 20.0};
double sum = 0.0;

for (int i = 0; i < arr.length; i++) {
    System.out.println("请输入第 " + (i + 1) + " 笔金额");
    if (input.hasNextDouble()) {
        arr[i] = input.nextDouble();
    } else {
        arr[i] = defaults[i];
    }
    sum += arr[i];
}

System.out.println("序号\t\t金额");
for (int i = 0; i < arr.length; i++) {
    System.out.println((i + 1) + "\t\t" + arr[i]);
}
System.out.println("总金额\t" + sum);

数组排序#

Java 提供了 Arrays 类,sort() 方法可对数组做升序排序:

Java
import java.util.Arrays;

int[] a = {4, 8, 2, 9, 1, 6, 7, 3};
Arrays.sort(a);
// 排序后:[1, 2, 3, 4, 6, 7, 8, 9]

最大值与最小值#

将第一个值存入新变量,用循环逐个比较赋值:

Java
int[] arr = {8, 4, 2, 1, 23, 344, 12};
int min = arr[0];

for (int i = 1; i < arr.length; i++) {
    if (min > arr[i]) {
        min = arr[i];
    }
}
System.out.println(min); // 1

二维数组与多维数组#

二维数组可以理解为“数组里再放数组”,常用于表格、矩阵、成绩单这类行列结构:

Java
int[][] scores = {
    {90, 88, 95},
    {76, 80, 72},
    {100, 98}
};

// 外层循环控制行,内层循环控制每一行中的列
for (int i = 0; i < scores.length; i++) {
    for (int j = 0; j < scores[i].length; j++) {
        System.out.print(scores[i][j] + "\t");
    }
    System.out.println();
}

需要注意:Java 的二维数组每一行长度可以不同,所以遍历列时应使用 scores[i].length,不要默认每一行都和第一行一样长。

三维数组就是在二维数组外再套一层,语法上可以继续嵌套:

Java
int[][][] data = {
    {
        {1, 2, 3},
        {4, 5, 6}
    }
};

System.out.println(data[0][1][2]); // 6

字符串操作#

String 提供了一系列实用方法,StringBuffer 是它的增强版。

Java
String str = "张三";

// length():计算字符串长度
System.out.println(str.length()); // 2

// equals():比较两个字符串的值(== 比较的是内存地址)
String s1 = new String("man");
String s2 = new String("man");
System.out.println(s1.equals(s2)); // true
System.out.println(s1 == s2);      // false

// equalsIgnoreCase():忽略大小写比较
"ABC".equalsIgnoreCase("abc"); // true

// toLowerCase() / toUpperCase():转大小写
"ABc".toLowerCase(); // "abc"
"ABc".toUpperCase(); // "ABC"

// concat():连接字符串(也可以用 +)
"kobe".concat("24"); // "kobe24"

// indexOf():某字符第一次出现的位置
// lastIndexOf():某字符最后一次出现的位置
String s = "aubdiubaiucb";
System.out.println(s.indexOf("b"));     // 1
System.out.println(s.lastIndexOf("b")); // 11

// substring(n, m):截取字符串,n 开始,m 结束(不含)
s.substring(3, 5); // "di"

// split():按指定字符拆分,返回数组
String s7 = "abc cde fgh ijk";
String[] arr = s7.split(" ");
// ["abc", "cde", "fgh", "ijk"]

// StringBuffer:String 的增强类,适合频繁修改字符串
StringBuffer sb = new StringBuffer("man");
System.out.println(sb); // man

构造方法#

是什么#

方法名和类名一致,没有返回值(也不写 void)的方法。在 new 对象时自动调用,用于初始化对象。

text
// 语法
public 类名(参数) {
    // 初始化逻辑
}

特点#

  • 方法名与类名一致
  • 没有返回值,没有 return 语句
  • 可以有参数
  • new 对象时自动调用
  • Java 会默认生成一个无参构造方法,一旦自己写了构造方法,默认的就失效了,需要手动补回无参构造
Java
public class Car {
    String color;
    double speed;
    int num;

    // 无参构造(自己写了有参构造后需要手动补上)
    public Car() {
        System.out.println("空构造");
    }

    // 有参构造
    public Car(String color) {
        this.color = color;
    }

    public Car(String color, double speed) {
        this.color = color;
        this.speed = speed;
    }

    public Car(String color, double speed, int num) {
        this.color = color;
        this.speed = speed;
        this.num = num;
    }
}

this 关键字#

  • this 在方法中表示当前正在调用该方法的对象
  • 可以在方法内部访问对象的属性
  • 可以调用当前类中的其他普通方法(直接调用或 this.方法名() 效果相同)
  • 在构造方法中可以调用其他构造方法,但必须写在第一行this(参数列表)
Java
public class Employee {
    String id;
    String name;
    int age;
    String phone;
    double sal;

    public void input() {
        System.out.println("姓名:" + this.name + " 电话:" + this.phone + " 工资:" + this.sal);
    }

    public void show() {
        input(); // 也可以写 this.input();
        System.out.println("工号:" + this.id);
    }

    public Employee() {}

    public Employee(String id) {
        this.id = id;
    }

    public Employee(String id, String name) {
        this(id);         // 调用上面的单参构造,必须第一行
        this.name = name;
    }

    public Employee(String id, String name, int age) {
        this(id, name);   // 链式调用构造方法
        this.age = age;
    }

    public Employee(String id, String name, int age, String phone, double sal) {
        this(id, name, age);
        this.phone = phone;
        this.sal = sal;
    }
}

方法重载#

在一个类中,方法名相同但参数列表不同的多个方法称为重载

特点

  • 同一个类中,多个方法名相同
  • 参数列表不同(个数或数据类型不同)
  • 与返回值类型和访问修饰符无关
  • 构造方法和普通方法都可以重载
Java
public class Cal {
    // 重载:参数类型不同
    public void sum(int a, int b) {
        System.out.println(a + b);
    }

    public double sum(double a, double b) {
        return a + b;
    }

    // 重载:参数个数不同
    public void sum(int a, int b, int c) {
        System.out.println(a + b + c);
    }
}

访问修饰符#

Java 提供四种访问权限修饰符,控制类、属性、方法的可见范围:

修饰符同一个类同一个包不同包的子类不同包的其他类
public
protected
默认(不写)
private

封装#

把类的信息隐藏在类内部,通过公共接口对外提供访问,并在方法中对数据进行合法性校验。

步骤

  1. private 私有化属性
  2. 提供 publicget / set 方法
Java
public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        } else {
            this.age = 0;
            System.out.println("请重新输入");
        }
    }
}

static 关键字#

static 可以修饰属性、方法、代码块、内部类,被修饰后属于类本身,而不是某个对象。

Java
public class Student {
    static String school; // 静态属性,所有对象共享
    int height;           // 普通属性,每个对象独有

    // 静态方法,通过类名直接调用
    public static void show() {
        // 静态方法中不能访问非静态属性或方法
        System.out.println(school);
    }

    // 静态代码块,类加载时自动执行一次
    static {
        System.out.println("Student 类加载");
    }

    // 静态内部类
    public static class Inner {
        int a;
    }
}

// 通过类名直接调用静态成员
Student.school = "北大";
Student.show();

final 关键字#

final 表示”不可改变”,可以修饰类、属性、方法:

  • 修饰类:该类不能被继承(叶子类)
  • 修饰属性:变为常量,声明时必须赋值
  • 修饰方法:该方法不能被子类重写
Java
public final class Teacher {
    final int MAX = 100;        // 常量,不可修改
    final char SEX = '男';

    public final void show() { // 不可被子类重写
        System.out.println("老师");
    }
}

继承#

基本语法#

使用 extends 关键字让子类继承父类,子类会拥有父类所有非私有的属性和方法。Java 只支持单继承(一个类只能有一个父类),所有类都直接或间接继承自 Object

Java
// 父类
public class Pet {
    String name;
    int health;

    public void eat() {
        System.out.println(name + "在吃东西");
    }
}

// 子类
public class Dog extends Pet {
    String strain; // 子类特有属性

    public void work() {
        System.out.println(name + "能看大门"); // 继承自父类的属性
    }
}

// 使用
Dog dog = new Dog();
dog.name   = "kobe";
dog.health = 60;
dog.eat();   // 调用继承来的方法
dog.work();  // 调用子类自己的方法

super 关键字#

当子类与父类存在同名属性或方法时,可以用 super 明确访问父类的成员:

Java
public class Dog extends Pet {
    String name = "man"; // 子类同名属性

    public void work() {
        System.out.println(this.name);  // 先找子类:man
        System.out.println(super.name); // 找父类:kobe
    }
}

在子类构造方法中,可用 super(参数) 调用父类构造方法,必须写在第一行

Java
public class Employee extends Person {
    double sal;

    public Employee(String name, int age, double sal) {
        super(name, age); // 调用父类构造方法,必须第一行
        this.sal = sal;
    }

    public void work() {
        super.show(); // 调用父类方法
        System.out.println("工资:" + this.sal);
    }
}

方法重写#

子类对父类方法的重新实现,让子类在继承父类结构的同时拥有自己的行为。

规则

  • 方法名、参数列表与父类一致
  • 访问修饰符不能比父类更严格(可以更宽松)
  • 方法体不同
Java
public class Person {
    public void run() {
        System.out.println("人类会跑步");
    }
}

public class Employee extends Person {
    @Override
    public void run() {       // 重写父类方法
        System.out.println("员工竞走");
    }
}

抽象类#

是什么#

abstract 修饰的类叫抽象类,用 abstract 修饰的方法叫抽象方法

特点

  • 抽象方法只有声明,没有方法体
  • 抽象方法必须定义在抽象类中
  • 抽象类中可以有普通方法
  • 抽象类不能被实例化(不能 new
  • 子类继承抽象类,必须重写所有抽象方法,否则子类也要声明为抽象类
Java
public abstract class Users {
    // 抽象方法,没有方法体
    public abstract void show();

    // 普通方法,可以有实现
    public void hello() {
        System.out.println("你好");
    }
}

// 子类必须实现所有抽象方法
public class BookUsers extends Users {
    @Override
    public void show() {
        System.out.println("书籍用户");
    }
}

// 使用
// Users u = new Users(); // 错误,抽象类不能实例化
BookUsers bu = new BookUsers();
bu.show();

多态#

是什么#

同一个方法调用,因对象类型不同而产生不同的结果,就是多态

多态建立在继承方法重写的基础上,通过父类引用指向子类对象来实现。

向上转型(子类 → 父类)#

Java
class Pet {
    public void eat() {
        System.out.println("宠物吃东西");
    }

    public void run() {
        System.out.println("宠物会跑");
    }
}

class Dog extends Pet {
    @Override
    public void eat() {
        System.out.println("狗吃东西");
    }

    public void work() {
        System.out.println("狗能看大门");
    }
}

// 父类引用 = 子类对象(自动转型)
Pet p = new Dog();
p.eat(); // 调用的是 Dog 重写后的 eat()
p.run();

// 通过父类引用调用不到子类特有的方法
// p.work(); // 错误,Pet 中没有 work()

向下转型(父类 → 子类)#

父类引用转回子类类型,需要强制转换,转换失败会抛出 ClassCastException

Java
class Pet {
}

class Dog extends Pet {
    public void work() {
        System.out.println("狗能看大门");
    }
}

Pet p = new Dog();

// 强制转换,大转小
Dog d = (Dog) p;
d.work(); // 可以调用 Dog 的特有方法

instanceof 运算符#

在强制转换前,用 instanceof 判断对象的实际类型,保证代码健壮性:

Java
class Pet {
}

class Dog extends Pet {
    public void work() {
        System.out.println("狗能看大门");
    }
}

class Cat extends Pet {
    public void play() {
        System.out.println("猫会玩耍");
    }
}

Pet p = new Dog();

if (p instanceof Dog) {
    Dog dog = (Dog) p;
    dog.work();
} else if (p instanceof Cat) {
    Cat cat = (Cat) p;
    cat.play();
}

多态的三种体现#

1. 父类引用指向子类对象

Java
class Person {
    public void show() {
        System.out.println("Person");
    }
}

class Student extends Person {
    @Override
    public void show() {
        System.out.println("Student");
    }
}

class Teacher extends Person {
    @Override
    public void show() {
        System.out.println("Teacher");
    }
}

Person p = new Student();
p.show(); // 执行 Student 的 show()

p = new Teacher();
p.show(); // 执行 Teacher 的 show()

2. 父类类型作为方法参数

形参声明为父类类型,传入子类对象,方法内部自动调用对应的重写方法:

Java
class Pet {
    public void eat() {
        System.out.println("宠物吃东西");
    }
}

class Dog extends Pet {
    @Override
    public void eat() {
        System.out.println("狗吃东西");
    }
}

class Cat extends Pet {
    @Override
    public void eat() {
        System.out.println("猫吃东西");
    }
}

public class Master {
    // 形参为父类,实参传子类对象
    public void food(Pet pet) {
        pet.eat();
    }
}

Master ma = new Master();
ma.food(new Dog()); // 匿名对象
ma.food(new Cat());

3. 父类类型作为返回值

方法声明返回父类类型,实际返回子类对象:

Java
class Pet {
    public void eat() {
        System.out.println("宠物吃东西");
    }
}

class Dog extends Pet {
    @Override
    public void eat() {
        System.out.println("狗吃东西");
    }
}

public class PetAgree {
    public Pet buy() {
        return new Dog(); // 返回子类对象
    }
}

PetAgree pa = new PetAgree();
Pet pet = pa.buy();
pet.eat(); // 调用 Dog 的 eat()

Object 数组#

Object 是所有类的父类,所以 Object[] 可以保存不同类型的引用数据,也可以保存自动装箱后的基本类型:

Java
Object[] arr = new Object[4];
arr[0] = "张三";
arr[1] = 18;       // int 自动装箱为 Integer
arr[2] = 98.5;
arr[3] = new Dog();

for (Object item : arr) {
    System.out.println(item);
}

不过 Object[] 会丢失具体类型信息,取出后如果要调用子类特有方法,通常需要配合 instanceof 判断后再强制转换。

接口#

是什么#

接口是一种“规则”或“能力”的定义,只规定应该有什么方法,不关心具体怎么实现。类通过 implements 实现接口,实现后必须重写接口中的抽象方法,否则这个类也要声明为抽象类。

Java
public interface USB {
    void work();
}

public class MouseUSB implements USB {
    @Override
    public void work() {
        System.out.println("鼠标开始工作");
    }
}

public class DiskUSB implements USB {
    @Override
    public void work() {
        System.out.println("U 盘开始工作");
    }
}

使用接口类型接收不同实现类对象,就能形成面向接口的多态:

Java
USB usb = new MouseUSB();
usb.work();

usb = new DiskUSB();
usb.work();

这里左边的 USB 决定能调用什么方法,右边的实际对象决定方法执行结果。

接口的特点#

  • 接口使用 interface 定义
  • 类使用 implements 实现接口
  • 一个类可以实现多个接口,用逗号隔开
  • 接口不能直接 new 对象
  • 接口可以继承接口,并且支持多继承
  • 接口中的抽象方法默认是 public abstract
  • 接口中的常量默认是 public static final
Java
public interface SuperA {
    void a();
}

public interface SuperB {
    void b();
}

public interface SuperC {
    void c();
}

public class ImpClass implements SuperA, SuperB, SuperC {
    @Override
    public void a() {
        System.out.println("实现 a");
    }

    @Override
    public void b() {
        System.out.println("实现 b");
    }

    @Override
    public void c() {
        System.out.println("实现 c");
    }
}

接口表示能力#

一个类实现某个接口,就代表它具备接口中规定的能力。比如业务员会讲业务,程序员会写程序,一个软件工程师可以同时具备这两种能力:

Java
public interface Person {
    void setName(String name);
    String getName();
}

public interface BizAgent extends Person {
    void speakWork();
}

public interface Programmer extends Person {
    void writeWork();
}

public class SoftEngineer implements BizAgent, Programmer {
    private String name;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void speakWork() {
        System.out.println("工程师可以讲解业务");
    }

    @Override
    public void writeWork() {
        System.out.println("工程师可以写程序");
    }
}

接口的意义不只是“多写几个方法”,更重要的是降低耦合。调用方只依赖接口,不依赖具体实现类,将来更换实现时,调用方代码通常不需要改。

接口作为方法参数#

接口常用来定义协作规则。下面的打印机不关心具体纸张和墨盒是哪一个类,只要求它们遵守 PaperInkBox 这两个接口:

Java
public interface Paper {
    String getPaperSize();
}

public interface InkBox {
    String getInkBoxType();
}

public class A4Paper implements Paper {
    @Override
    public String getPaperSize() {
        return "A4";
    }
}

public class ColorBox implements InkBox {
    @Override
    public String getInkBoxType() {
        return "彩色";
    }
}

public class Print {
    public void input(Paper paper, InkBox inkBox) {
        System.out.println("使用" + paper.getPaperSize() + "大小的纸打印" + inkBox.getInkBoxType() + "色的内容");
    }
}

Print printer = new Print();
Paper paper = new A4Paper();
InkBox inkBox = new ColorBox();
printer.input(paper, inkBox);

这样以后要新增 A3PaperBlackBox,只要实现接口即可,Print 类不需要重写。


异常处理#

什么是异常#

异常是程序运行过程中出现的不正常情况,例如输入类型不匹配、数组下标越界、空指针、整数除零等。异常如果不处理,程序可能会直接中断。

Java
Scanner input = new Scanner(System.in);
System.out.println("请输入两个整数");

int a = input.nextInt();
int b = input.nextInt();
System.out.println(a / b); // b 为 0 时会出现 ArithmeticException

注意:如果是 double 类型除以 0,Java 通常会得到 InfinityNaN,不会像整数除零那样抛出 ArithmeticException

try-catch-finally#

把可能出错的代码放进 try,异常出现后由 catch 捕获处理,finally 中的代码一般用于收尾工作,无论是否发生异常都会尝试执行。

Java
import java.util.InputMismatchException;
import java.util.Scanner;

Scanner input = new Scanner(System.in);

try {
    System.out.println("请输入第一个整数");
    int a = input.nextInt();

    System.out.println("请输入第二个整数");
    int b = input.nextInt();

    System.out.println("商:" + a / b);
} catch (InputMismatchException e) {
    System.out.println("输入的不是整数");
} catch (ArithmeticException e) {
    System.out.println("除数不能为 0");
} catch (Exception e) {
    System.out.println("出现未知错误:" + e.getMessage());
} finally {
    input.close();
    System.out.println("计算结束");
}

多个 catch 同时存在时,应把更具体的异常写在前面,把父类异常 Exception 写在后面,否则前面的父类会先把所有异常都接住。

throws 与 throw#

throws 写在方法声明后面,表示这个方法可能把异常交给调用者处理;throw 写在方法内部,用来主动抛出一个异常对象。

Java
public static int divide(int a, int b) throws ArithmeticException {
    if (b == 0) {
        throw new ArithmeticException("除数不能为 0");
    }
    return a / b;
}

public static void main(String[] args) {
    try {
        System.out.println(divide(10, 0));
    } catch (ArithmeticException e) {
        System.out.println(e.getMessage());
    }
}

初学阶段更推荐先学会在合适的位置用 try-catch 处理异常,不要随意把异常一直向外抛。

自定义异常#

自定义异常类通常继承 ExceptionRuntimeException。继承 Exception 的异常属于受检异常,调用者必须处理或继续声明抛出。

Java
public class MyException extends Exception {
    public MyException() {
    }

    public MyException(String message) {
        super(message);
    }
}

public static void checkAge(int age) throws MyException {
    if (age < 0 || age > 150) {
        throw new MyException("年龄不合法");
    }
}

自定义异常的作用是把业务规则表达清楚。相比直接抛出普通 Exception,自定义异常能让调用者更准确地知道哪里出了问题。

finally、return 与 System.exit#

正常情况下,即使 trycatch 中执行了 returnfinally 仍然会先执行,然后方法才真正返回。但如果执行了 System.exit(0) 强制退出 JVM,后续代码和 finally 都不会再执行。

Java
public static int test() {
    try {
        return 1;
    } finally {
        System.out.println("finally 仍然会执行");
    }
}

可以简单记为:System.exit() 会直接结束程序;finally 负责收尾;return 负责结束当前方法。


集合框架#

是什么#

Java 提供的一套现成的接口和类,可以直接用来存储、操作一组数据对象。与数组相比:

对比项数组集合
长度固定可变
存储内容同一类型(常见基本类型)多种类型的对象
内存连续空间不一定连续

集合分类#

Java 集合分为两大体系:

plaintext
Collection(单列集合)      Map(双列集合,键值对)
├── List(有序可重复)        ├── HashMap(哈希表)
│   ├── ArrayList            └── TreeMap(红黑树,有序)
│   └── LinkedList
└── Set(无序不重复)
    ├── HashSet
    └── TreeSet(红黑树,有序)

Collection 常用方法#

这些方法 ListSet 都能用:

方法签名说明
boolean add(Object o)向集合末尾添加一个对象
void add(int i, Object o)在指定位置 i 插入对象 o(List)
int size()获取集合中元素个数
Object get(int i)获取 i 位置的对象(List)
boolean contains(Object o)判断集合中是否存在某元素
boolean remove(Object o)删除集合中的某个对象
Object remove(int i)删除 i 位置的对象,返回被删除的

ArrayList#

底层是数组,查询快,增删慢。最常用的 List 实现类。

Java
import java.util.ArrayList;
import java.util.List;

Dog d1 = new Dog("小黑", "白色");
Dog d2 = new Dog("小白", "黑色");
Dog d3 = new Dog("小花", "黄色");

// 使用泛型指定元素类型,省去强制转换
List<Dog> list = new ArrayList<>();
list.add(d1);
list.add(d2);
list.add(1, d3); // 在索引 1 处插入,后面的元素自动后移

// 普通 for 循环
for (int i = 0; i < list.size(); i++) {
    Dog dog = list.get(i);
    System.out.println(dog.getName());
}

// 增强 for 循环(forEach)
for (Dog dog : list) {
    System.out.println(dog.getName());
}

// contains & remove
System.out.println(list.contains(d2));  // true
list.remove(d2);                        // 删除指定对象
Dog removed = list.remove(0);           // 删除索引 0,返回被删除的对象

不指定泛型时,get() 返回 Object,需要手动强转为 Dog;指定泛型后可以直接用 Dog 接收,更安全。

LinkedList#

底层是双向链表,增删快,查询慢。接口同 List,但提供了额外的头尾操作方法:

Java
import java.util.LinkedList;
import java.util.List;

List<Dog> list = new LinkedList<>();
list.add(d1);
list.add(d2);

// LinkedList 独有方法,需要声明为 LinkedList 类型才能调用
LinkedList<Dog> l = new LinkedList<>();
l.add(d1);
l.add(d2);
l.add(d3);

l.addFirst(d1);        // 插入到头部
l.addLast(d3);         // 插入到尾部
System.out.println(l.getFirst().getName()); // 获取第一个元素
System.out.println(l.getLast().getName());  // 获取最后一个元素

HashSet#

无序、不重复。Set 没有索引,不能用普通 for 循环,只能用 forEach 或迭代器遍历:

Java
import java.util.HashSet;
import java.util.Set;

Set<Dog> set = new HashSet<>();
set.add(d1);
set.add(d2);
set.add(d3);

System.out.println(set.size()); // 3

for (Dog dog : set) {
    System.out.println(dog.getName()); // 顺序不固定
}

TreeSet#

有序、不重复。底层是红黑树,元素按自然顺序(数字升序、字母字典序)自动排列,不需要手动排序:

Java
import java.util.Set;
import java.util.TreeSet;

Set<Integer> set = new TreeSet<>();
set.add(5);
set.add(1);
set.add(3);
set.add(2);
set.add(4);

for (int n : set) {
    System.out.print(n + " "); // 1 2 3 4 5(自动升序)
}

字符串同理,按字典序排列:

Java
Set<String> names = new TreeSet<>();
names.add("张三");
names.add("李四");
names.add("王五");

for (String name : names) {
    System.out.println(name); // 按字典序输出
}

与 HashSet 的区别:

对比项HashSetTreeSet
底层结构哈希表红黑树
顺序无序自动按自然顺序排序
性能增删查更快略慢,但有序

需要去重且不关心顺序用 HashSet;需要去重同时保持有序用 TreeSet

迭代器 Iterator#

Iterator 提供统一的遍历方式,适用于 ListSet,取出 Map 的键也用到它:

Java
import java.util.Iterator;

// 遍历 List
List<Dog> list = new ArrayList<>();
list.add(d1);
list.add(d2);
list.add(d3);

Iterator<Dog> it = list.iterator();
while (it.hasNext()) {       // 是否还有下一个元素
    Dog dog = it.next();     // 取出下一个元素
    System.out.println(dog.getName());
}

Map / HashMap#

Map 以键值对方式存储,键唯一,值可重复,通过键获取值:

方法签名说明
V put(K key, V value)添加键值对,键已存在则覆盖原值
V get(K key)根据键获取值,不存在返回 null
V remove(K key)删除指定键的键值对,返回被删除的值
boolean containsKey(K key)判断是否包含指定键
int size()获取键值对的数量
Set<K> keySet()获取所有键组成的 Set
Set<Map.Entry<K,V>> entrySet()获取所有键值对组成的 Set
Java
import java.util.HashMap;
import java.util.Map;

Map<Integer, String> map = new HashMap<>();
map.put(100000, "北京市");
map.put(200000, "上海市");
map.put(510000, "广州市");

System.out.println(map.get(100000));           // 北京市
System.out.println(map.containsKey(200000));   // true
System.out.println(map.size());                // 3

遍历方式一:keySet + get(先取键,再通过键取值)

Java
for (String key : map.keySet()) {
    System.out.println(key + " -> " + map.get(key));
}

遍历方式二:entrySet + Map.Entry(一次取出键值对,更推荐)

Java
for (Map.Entry<Integer, String> entry : map.entrySet()) {
    System.out.println("邮政编码:" + entry.getKey() + ", 地区:" + entry.getValue());
}

Map.Entry 代表 Map 中的一个键值对,getKey() 取键,getValue() 取值。entrySet() 方式比 keySet() 少一次 get() 查找,遍历时性能更好,写法也更直接。


JDBC#

是什么#

JDBC(Java Database Connectivity)是 Java 提供的一套操作数据库的 API,通过它可以用 Java 代码执行 SQL 语句,实现对数据库的增删改查。

使用 JDBC 前需要引入数据库驱动 jar 包(以 MySQL 为例:mysql-connector-java-xxx.jar),将 jar 包添加到项目依赖中。

操作步骤(7步)#

plaintext
1. 加载驱动
2. 创建连接
3. 编写 SQL 语句
4. 预编译 SQL
5. 执行 SQL
6. 处理结果
7. 关闭资源

增(INSERT)#

Java
import com.mysql.jdbc.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

// 1. 加载驱动
try {
    Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

// 2. 创建连接
Connection con = null;
try {
    con = (Connection) DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/数据库名", "root", "123456");
} catch (SQLException e) {
    e.printStackTrace();
}

// 3. 编写 SQL(字符串变量可以用 + 拼接,但推荐用占位符 ?)
String sql = "insert into student(id, name, age) values(1, '张三', 18)";

// 4. 预编译
PreparedStatement ps = null;
try {
    ps = con.prepareStatement(sql);
} catch (SQLException e) {
    e.printStackTrace();
}

// 5. 执行(增删改用 executeUpdate,返回受影响行数)
int rows = 0;
try {
    rows = ps.executeUpdate();
} catch (SQLException e) {
    e.printStackTrace();
}

// 6. 处理结果
if (rows > 0) {
    System.out.println("添加成功");
}

// 7. 关闭资源(先开后关)
try { ps.close(); } catch (SQLException e) { e.printStackTrace(); }
try { con.close(); } catch (SQLException e) { e.printStackTrace(); }

删(DELETE)/ 改(UPDATE)#

步骤与增数据完全相同,只是 SQL 语句不同:

Java
// 删除
String sql = "delete from student where name = '张三'";

// 修改
String sql = "update student set name = '李四' where id = 1";

查(SELECT)#

查询需要用 executeQuery() 并通过 ResultSet 遍历结果集:

Java
import java.sql.ResultSet;

// 执行查询
String sql = "select * from student";
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery();

// 遍历结果集
while (rs.next()) {
    int id     = rs.getInt("id");
    String name = rs.getString("name");
    int age    = rs.getInt("age");
    System.out.println(id + " - " + name + " - " + age);
}

// 关闭资源(rs 也要关,顺序:rs → ps → con)
rs.close();
ps.close();
con.close();

占位符 ?#

SQL 中用 ? 代替具体值,再用 setXxx() 方法赋值,可以避免 SQL 注入,也让代码更清晰:

Java
// 占位符从 1 开始计数
String sql = "insert into student values(?, ?, ?)";
PreparedStatement ps = con.prepareStatement(sql);

ps.setInt(1, 3);          // 第1个 ? 设置为整数 3
ps.setString(2, "王五");  // 第2个 ? 设置为字符串
ps.setInt(3, 19);         // 第3个 ? 设置为整数 19

ps.executeUpdate();

条件查询也可以用占位符:

Java
String sql = "select * from student where id > ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setInt(1, 5);
ResultSet rs = ps.executeQuery();

BaseDao 模式#

实际开发中,加载驱动、创建连接、关闭资源这几步在每个操作里都一样,可以提取到父类 BaseDao 中复用:

Java
import com.mysql.jdbc.Connection;
import java.sql.*;

public class BaseDao {
    protected Connection con = null;
    protected PreparedStatement ps = null;
    protected ResultSet rs = null;

    public void openCon() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        try {
            con = (Connection) DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/数据库名", "root", "123456");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void closeAll() {
        try {
            if (rs  != null) rs.close();
            if (ps  != null) ps.close();
            if (con != null) con.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

业务类继承 BaseDao,直接调用 openCon()closeAll()

Java
public class EmpCRUD extends BaseDao {

    public void addEmp() {
        try {
            openCon();
            String sql = "insert into employee(e_name, e_age, e_loc) values(?, ?, ?)";
            ps = con.prepareStatement(sql);
            ps.setString(1, "高八");
            ps.setInt(2, 21);
            ps.setString(3, "四川");
            int rows = ps.executeUpdate();
            if (rows > 0) System.out.println("添加成功");
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            closeAll(); // 无论成功与否都关闭
        }
    }

    public void findAll() {
        try {
            openCon();
            String sql = "select * from employee";
            ps = con.prepareStatement(sql);
            rs = ps.executeQuery();
            while (rs.next()) {
                System.out.println(
                    rs.getInt("e_no") + " - " +
                    rs.getString("e_name") + " - " +
                    rs.getInt("e_age") + " - " +
                    rs.getString("e_loc")
                );
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            closeAll();
        }
    }
}

查询结果的处理规则#

  • 根据主键或唯一键查询 → 结果只有一条 → 封装进一个对象返回
  • 查询结果多行 → 每行封装成一个对象 → 所有对象放入 List 返回
Java
// 按 id 查单条,返回 Employee 对象
public Employee findById(int id) {
    openCon();
    Employee emp = null;
    try {
        String sql = "select * from employee where e_no = ?";
        ps = con.prepareStatement(sql);
        ps.setInt(1, id);
        rs = ps.executeQuery();
        if (rs.next()) {
            emp = new Employee();
            emp.setE_no(rs.getInt("e_no"));
            emp.setE_name(rs.getString("e_name"));
            emp.setE_age(rs.getInt("e_age"));
            emp.setE_loc(rs.getString("e_loc"));
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        closeAll();
    }
    return emp;
}

// 查多条,返回 List
public List<Employee> findByAge(int minAge) {
    List<Employee> list = new ArrayList<>();
    try {
        openCon();
        String sql = "select * from employee where e_age > ?";
        ps = con.prepareStatement(sql);
        ps.setInt(1, minAge);
        rs = ps.executeQuery();
        while (rs.next()) {
            Employee emp = new Employee();
            emp.setE_no(rs.getInt("e_no"));
            emp.setE_name(rs.getString("e_name"));
            emp.setE_age(rs.getInt("e_age"));
            emp.setE_loc(rs.getString("e_loc"));
            list.add(emp);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        closeAll();
    }
    return list;
}

模糊查询与分页#

LIKE 用于模糊匹配,LIMIT 用于分页,两者配合占位符使用:

Java
// 查询姓"王"的员工,取前 5 条
String sql = "select * from employee where e_name like ? limit ?, ?";
ps = con.prepareStatement(sql);
ps.setString(1, "王%");   // % 表示任意字符
ps.setInt(2, 0);           // 从第 0 条开始(offset)
ps.setInt(3, 5);           // 取 5 条(count)
rs = ps.executeQuery();

LIMIT offset, countoffset 是跳过的行数,count 是取几条。第一页 offset=0,第二页 offset=5(每页5条),以此类推。


分层开发(MVC 三层架构)#

为什么要分层#

前面的 JDBC 把“连接数据库、拼 SQL、业务判断、界面输出”全堆在一个类里,类一多、表一多就很难维护。分层就是按职责把代码拆开,每一层只干自己的事。

一个标准的三层结构(再加一个实体层)如下:

包名(示例)职责
实体层 entitycom.xxx.entity和数据库表一一对应的类,一个表一个类,只放属性和 get/set
数据访问层 daocom.xxx.dao只和数据库打交道,执行 SQL,做增删改查
业务逻辑层 servicecom.xxx.service处理业务规则,调用 dao,向上返回布尔值或对象
视图层 view/testcom.xxx.test和用户交互:打印菜单、读取输入、展示结果

调用方向是单向的:view → service → dao → 数据库,下层不会反过来调用上层。daoservice 通常各自再分出一个 impl 子包,接口放外层、实现类放 impl,做到“面向接口编程”。

plaintext
com.kxg.oop
├── entity                  实体层
│   ├── Employee.java
│   └── Department.java
├── dao                     数据访问层
│   ├── EmployeeDao.java        接口
│   ├── BaseDao.java            连接工具类
│   └── impl
│       └── EmployeeDaoImpl.java 实现
├── service                 业务逻辑层
│   ├── EmployeeService.java    接口
│   └── impl
│       └── EmployeeServiceImpl.java 实现
└── test                    视图层(入口)
    └── EmployeeTest.java

实体层 entity#

一个表对应一个类,表的每个字段就是类的一个私有属性,再配上 get/set 和构造方法。

Java
public class Department {
    private int depno;      // 部门编号
    private String depname; // 部门名称
    private String deploc;  // 部门地址

    public Department() {}

    public Department(int depno, String depname, String deploc) {
        this.depno = depno;
        this.depname = depname;
        this.deploc = deploc;
    }

    // 省略 get/set...
}

外键怎么表示:员工表里的 dep_no 是指向部门表的外键。实体里不要只存一个 int,而是存一个部门对象,把外键编号放进这个对象里。这样以后想拿部门名称、地址,都能顺着这个对象取到。

Java
public class Employee {
    private int eno;
    private String ename;
    private int eage;
    private String eloc;
    // 外键字段写成主表的对象,对象里再包含外键引用的编号
    private Department department;

    // 省略构造和 get/set...
}

数据访问层 dao#

dao 层只负责数据库操作。先用接口约定“有哪些方法”,再写实现类去落实。

BaseDao:和 JDBC 章节一样,把建连接、关资源提到父类里复用。conpsrsprotected 修饰,子类(各个 DaoImpl)才能直接拿来用;连接参数也可以提成成员变量,看起来更清楚:

Java
public class BaseDao {
    // protected:本包或子类中都能直接使用
    protected Connection con = null;
    protected PreparedStatement ps = null;
    protected ResultSet rs = null;

    String driver = "com.mysql.jdbc.Driver";
    String url    = "jdbc:mysql://127.0.0.1:3306/oop230501";
    String user   = "root";
    String pwd    = "123456";

    public void openCon() {
        try {
            Class.forName(driver);
            con = DriverManager.getConnection(url, user, pwd);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void closeAll() {
        try {
            if (rs  != null) rs.close();
            if (ps  != null) ps.close();
            if (con != null) con.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Dao 接口:只声明方法,不写实现。增删改返回 int(受影响的行数),查询返回 List

Java
public interface EmployeeDao {
    int insert(Employee employee);          // 增
    int delete(Employee employee);          // 删
    int update(Employee employee);          // 改
    List<Employee> selectAll();             // 查所有
    List<Employee> selectByNo(Employee employee); // 按编号查
    List<Employee> selectByAge(int a, int b);     // 按年龄区间查
}

Dao 实现类extends BaseDao implements EmployeeDao,复用父类的连接方法,逐个实现接口里的方法。注意外键的存取——存的时候从对象里取出编号,查的时候把编号塞回对象:

Java
public class EmployeeDaoImpl extends BaseDao implements EmployeeDao {

    public int insert(Employee employee) {
        openCon();
        int i = 0;
        try {
            String sql = "insert into employee values(?,?,?,?,?)";
            ps = con.prepareStatement(sql);
            ps.setInt(1, employee.getEno());
            ps.setString(2, employee.getEname());
            ps.setInt(3, employee.getEage());
            ps.setString(4, employee.getEloc());
            // 外键:先拿到部门对象,再从对象里取出部门编号
            ps.setInt(5, employee.getDepartment().getDepno());
            i = ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            closeAll();
        }
        return i;
    }

    @Override
    public List<Employee> selectAll() {
        openCon();
        List<Employee> list = new ArrayList<>();
        try {
            String sql = "select * from employee";
            ps = con.prepareStatement(sql);
            rs = ps.executeQuery();
            while (rs.next()) {
                Employee e = new Employee();
                e.setEno(rs.getInt("e_no"));
                e.setEname(rs.getString("e_name"));
                e.setEage(rs.getInt("e_age"));
                e.setEloc(rs.getString("e_loc"));

                // 结果表里的 dep_no 是外键:
                // 先存进部门对象,再把部门对象存进员工对象
                Department d = new Department();
                d.setDepno(rs.getInt("dep_no"));
                e.setDepartment(d);

                list.add(e);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            closeAll();
        }
        return list;
    }
    // delete / update / selectByNo / selectByAge 同理
}

按年龄区间查询用的是 SQL 的 between ? and ?select * from employee where e_age between ? and ?

业务逻辑层 service#

service 同样是接口 + 实现。实现类里持有一个 dao 对象,并且私有化——上层调用下层,这个 dao 只在本类内部用,不对外暴露。

增删改的 dao 返回的是受影响行数(int),service 把它转成更好用的 boolean:大于 0 就说明操作成功。

Java
public class EmployeeServiceImpl implements EmployeeService {
    // 上层调下层,私有化,只在本类使用
    private EmployeeDao employeeDao = new EmployeeDaoImpl();

    public boolean add(Employee employee) {
        // 受影响行数 > 0 即添加成功,直接返回比较结果
        return employeeDao.insert(employee) > 0;
    }

    @Override
    public boolean delete(Employee employee) {
        return employeeDao.delete(employee) > 0;
    }

    @Override
    public List<Employee> selectAll() {
        return employeeDao.selectAll();
    }
    // update / selectByNo / selectByAge 同理
}

return employeeDao.insert(employee) > 0; 是下面这段的简写,效果完全一样:

Java
int i = employeeDao.insert(employee);
boolean flag = false;
if (i > 0) {
    flag = true;
}
return flag;

视图层 / 测试类#

视图层是程序入口,负责和用户打交道:循环打印菜单,根据用户的选择调用对应的 service 方法。这里不直接碰 dao,也不写 SQL

Java
public class DeptTest {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        DepartmentService ds = new DepartmentServiceImpl();
        int choice = -1;
        while (choice != 0) {
            System.out.println("=====部门管理系统=====");
            System.out.println("1. 查询所有部门");
            System.out.println("2. 根据编号查询部门");
            System.out.println("3. 添加部门");
            System.out.println("4. 修改部门");
            System.out.println("5. 删除部门");
            System.out.println("0. 退出");
            System.out.print("请选择:");
            choice = input.nextInt();
            switch (choice) {
                case 1:
                    List<Department> all = ds.findAll();
                    System.out.println("编号\t名称\t位置");
                    for (Department dept : all) {
                        System.out.println(dept.getDepno() + "\t" + dept.getDepname() + "\t" + dept.getDeploc());
                    }
                    break;
                case 3:
                    System.out.print("请输入部门编号:");
                    int ano = input.nextInt();
                    System.out.print("请输入部门名称:");
                    String aname = input.next();
                    System.out.print("请输入部门地址:");
                    String aloc = input.next();
                    Department adept = new Department(ano, aname, aloc);
                    System.out.println(ds.add(adept) ? "添加成功" : "添加失败");
                    break;
                // case 2/4/5 略,结构相同
                case 0:
                    System.out.println("退出系统");
                    break;
                default:
                    System.out.println("无效选项,请重新选择");
            }
        }
        input.close();
    }
}

串一遍:添加一个部门#

以“添加部门”为例,一次完整的调用会穿过三层:

  1. view:读取用户输入,封装成一个 Department 对象,调用 ds.add(adept)
  2. serviceadd() 调用 dao.insert(dept),拿到受影响行数后返回 行数 > 0
  3. daoinsert() 打开连接、执行 insert SQL、关闭资源,返回受影响行数
  4. 结果一路返回到 view,view 根据 boolean 打印“添加成功 / 失败”

分层的好处就在这里:哪天要换数据库,只动 dao;要改业务规则,只动 service;要换界面(比如做成网页),只动 view,互不影响。


日期类型的处理#

学生表里有“出生日期”这种日期字段,而 Java 里和日期相关的 Date 有两个,混用很容易踩坑。

java.util.Date 与 java.sql.Date#

对比项java.util.Datejava.sql.Date
用途普通 Java 代码里的日期时间JDBC 专用,对应数据库的 date 字段
关系父类子类(只保留年月日,丢掉时分秒)

约定:实体类里的日期属性用 java.util.Date,到了 dao 操作数据库时再转成 java.sql.Date

Java
import java.util.Date;

public class Student {
    private int stu_no;
    private String stu_name;
    private Date stu_born;   // 出生日期,用 java.util.Date
    // 省略其它字段和 get/set...
}

存:util.Date → sql.Date#

PreparedStatementsetDate() 只认 java.sql.Date,所以存之前要做一次转换:用实体里 util.Date 的 getTime() 拿到毫秒数,再用这个毫秒数 new 一个 sql.Date。

Java
import java.sql.Date;   // 注意这里 import 的是 java.sql.Date

// student.getStu_born() 是 java.util.Date
// getTime() 取出毫秒值,再构造成 java.sql.Date
ps.setDate(8, new Date(student.getStu_born().getTime()));

因为实体用 java.util.Date、这里又要 java.sql.Date,两个同名类不能同时 import。一般 import java.sql.Date,另一个用全限定名 java.util.Date 区分。

取:rs.getDate#

查询时 rs.getDate("stu_born") 返回的是 java.sql.Date。由于它本身就是 java.util.Date 的子类(向上转型),可以直接塞回实体的 util.Date 属性里:

Java
s.setStu_born(rs.getDate("stu_born")); // sql.Date 自动当作 util.Date 使用

字符串 → 日期:SimpleDateFormat#

用户从键盘输入的生日是字符串(如 2008-05-01),需要用 SimpleDateFormat 按指定格式把它解析成 Date。解析过程可能因为格式不对抛出 ParseException,要用 try-catch 包住:

Java
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

System.out.print("请输入出生日期(yyyy-MM-dd):");
String bornStr = input.next();
Date born = null;
try {
    // 把字符串按 yyyy-MM-dd 格式解析成 java.util.Date
    born = new SimpleDateFormat("yyyy-MM-dd").parse(bornStr);
} catch (ParseException e) {
    e.printStackTrace();
}
student.setStu_born(born);

格式字母:yyyy 年、MM 月、dd 日(还有 HH 时、mm 分、ss 秒)。反过来,format() 方法可以把一个 Date 转成指定格式的字符串:

Java
String text = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
// 例如:2026-06-10

一句话理清这条链路:键盘输入字符串 →(SimpleDateFormat.parse)→ util.Date 存进实体 →(getTime()sql.Date)→ 存入数据库;查询时 rs.getDate 取出 sql.Date,当作 util.Date 放回实体即可。

JAVA
https://blog.shiro.team/posts/java/
作者
白咲雫
发布于
2026-03-16
许可协议
CC BY-NC-SA 4.0
留下你的足迹
Avatar