跳到主要内容

面向对象程序设计(Java)

Java 网络编程入门 —— 以 LAN 传输为例

信息

这次我不打算搜集各方资料了,《任务书》里有提供相关的课程链接。因此,这篇就权当视频学习笔记。下面 po 出链接:

[韩顺平讲Java] Java网络多线程专题 - Bilibili


IO 流

怎样使用 Java 读写文件?

通过生活中的例子可以得知,聊天消息是需要存储的。由于课堂上没有介绍文件 IO(虽然网课讲了,但我也没听),为此我们需要准备一点前置知识,用于文件的读写。此外还有一些可能需要的知识,例如:Java 多线程、lambda 表达式、哈希表等等,这里就不展开了,需要自行学习。

文件流

文件在程序中也是以流的形式来操作的。

提示

想象一下,文件(File)就是一个巨大的水库,里面存着海量的水(数据)。这个水库可能非常大,比如有几百 G,但你的内存(RAM)就像一个水桶,容量是非常有限的,比如只有 8G 或 16G。

现在,你的任务是把水库里的水过滤一下。可能有两种方法:

"非流"式方法:尝试把整个水库的水一次性全部装进你的水桶里。结果很明显:如果水库比水桶大,水桶会立刻溢出(程序内存溢出,最后导致崩溃)。这根本行不通。

"流"式方法:你接上一根水管(打开一个流),然后打开水龙头。水会源源不断地从水管里流出来,你用水桶接一桶,过滤完,倒掉,再接下一桶。周而复始,直到水库的水全部被处理完。

通过水管(流),无论水库有多大,都能完成整个任务。

文件数据从磁盘、网络等地方加载到内存的流,称为输入流;相反,数据从内存加载到磁盘的流,称为输出流

创建文件、文件夹

我们通过以下方法来新建一个 File 对象。在 Java 中,文件和文件夹均属于 File 对象,也就是文件。因此,这个方法既可以用来创建文件,也可以用来创建文件夹。

new File()

创建文件

这个方法有常用的三个重载形式:

形式 1:直接传入绝对路径的字符串,例如 C:\text.txt

String pathname = "C:\\text.txt";
File file = new File(String pathname);

形式 2:传入父目录文件(对象)+ 子路径字符串

其实"父目录文件"可以看作是文件夹,与子路径拼接起来就是相对路径。

File parent = new File("C:\\"); // 调用了第一种形式 File(String pathname)
String child = "text.txt";

File file = new File(File parent, String child);

形式 3:传入父目录路径字符串 + 子路径字符串

也等同于拼接。

String parent = "C:\\";
String child = "text.txt";

File file = new File(String parent, String child);

这三种形式看起来都是直接传入绝对路径的字符串,但形式 2 更多用于根目录重复使用的场景,例如应用程序的最外层文件夹,在内部路径里面不断地读写文件夹,这样就不用频繁地将最外层文件夹的路径传入方法,或传入绝对路径。

相对地,形式 3 更多用于临时的、一次性的路径拼接。

当然,new File() 方法仅仅是在内存中创建了这个文件。要想将文件写入磁盘,我们还需要调用 File 对象的另一个方法:

String pathname = "C:\\text.txt";
File file = new File(String pathname);

file.createNewFile(); // 这个语句用于将内存中生成的文件按指定路径写入磁盘

同时,这个方法可能会抛出 IOException 异常。如果我们不 catch 它,编译器会一直报错。

String pathname = "C:\\text.txt";
File file = new File(String pathname);

try {
file.createNewFile(); // 这个语句用于将内存中生成的文件按指定路径写入磁盘
System.out.println("文件创建成功!");
} catch (IOException e) {
e.printStackTrace(); // 打印出异常的"堆栈跟踪"信息到标准错误流(打印错误信息)
}

创建文件夹

前面提到,在 Java 中,文件和文件夹均属于 File 对象,也就是文件。我们同样使用 new File() 方法来创建一个文件夹。基本操作与上面的大差不差,只是路径中缺少了确定的某个文件及其拓展名。

三种重载形式与上文一致,只是路径名改为文件夹路径。

同样地,new File() 方法仅仅是在内存中创建了这个文件。要想将文件夹写入磁盘,我们还需要调用 File 对象的另一些方法:

String pathname = "C:\\fold";
File file = new File(String pathname);

file.mkdir(); // 这个语句用于将内存中生成的文件按指定路径写入磁盘

这个方法的返回值是布尔类型。当文件夹被成功创建时,方法返回 true

如果我们需要创建多个层级的文件夹,我们需要调用另一个方法:.mkdirs()

String pathname = "C:\\fold\\inner_fold\\123";
File file = new File(String pathname);

file.mkdirs(); // 这个语句用于将内存中生成的文件按指定路径写入磁盘
注意

注意,使用 .mkdir() 创建多层级文件夹不会报错,但会创建失败!(返回 false)。

获取文件、文件夹基本信息(并非内容)

File 对象还有一些常用方法,能够获取文件的基本信息。

File file = new File("C:\\text.txt");

/**** 获取文件或文件夹对象信息 ****/
file.getName(); // 返回文件或文件夹名称的字符串。getXXX 看起来像一个访问器?
file.getAbsolutePath(); // 返回文件或文件夹绝对路径的字符串
file.getParent(); // 返回文件或文件夹父级路径的字符串
file.exists(); // 返回文件或文件夹是否存在的布尔值

/**** 获取文件对象信息 ****/
file.length(); // 返回文件大小(字节)
file.isFile(); // 返回这个对象是否为文件的布尔值

/**** 获取文件夹对象信息 ****/
file.isDircetory(); // 返回这个对象是否为文件夹的布尔值

IO 流

I/O 是 Input/Output 的缩写。I/O 技术是非常实用的技术,用于处理数据传输,如读、写文件,网络通讯等。相较于文件流,IO 流是一个更加广泛的概念。也可以说,IO 流包括文件流

IO 流分类

按数据流向不同分类:

  • 输入流
  • 输出流

按流的角色不同分类:

  • 节点流:从一个特定的数据源读写数据
  • 处理流(也称包装流)

按操作数据单位不同分类:

  • 字节流(以 8bit 为单位)
  • 字符流(以字符为单位,一个字符的大小与字符编码方式有关)
提示

对于二进制文件(声音、视频等),字节流的操作效率更高。

对于文本文件,字符流的操作效率更高。

字节流和字符流的父类不同,但它们都是抽象类

类别输入输出
字节流InputStreamOutputStream
字符流ReaderWriter

Java 的 IO 流共涉及 40 多个类,都是从如上 4 个抽象基类派生的。

它们的命名方式非常规则,都以其父类名作为子类名后缀:例如 FileInputStream、PipedInputStream、SequenceInputStream …

FileInputStream(字节流)

流要与实际的 File 对象绑定。因此有常用构造方法如下:

FileInputStream(String fileName); // 传入文件的绝对路径
FileInputStream(File file); // 传入File对象

对于 FileInputStream 对象,我们使用 .read() 方法读取数据。以下是常用的重载形式。

read()
// 从输入流中读取一个数据字节。
// 如果读取正常,返回数据的下一个字节。
// 如果返回 -1,则代表没有数据可读取(读取完毕)。

read(byte[] b)
// 从输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。
// 如果读取正常,返回实际读取的字节数。
// 如果返回 -1,则代表没有数据可读取(读取完毕)。

假设我们在 C 盘根目录有一个 text.txt,内容为 hello world

int data = 0;
FileInputStream fileInputStream = null;

try {
// 创建 FileInputStream 对象,即创建输入流
// 如果在这里创建对象 FileInputStream fileInputStream = new FileInputStream,
// 它的作用域只在 try 块中,finally 块的 fileInputStream.close() 访问不到。
fileInputStream = new FileInputStream("C:\\text.txt");

/*** 使用 read() 方法 ***/
// 前面提到,read() 方法一次最多只能读取一个字节。但一个文件通常情况下
// 不可能只有一个字节。因此,我们可以使用 while 循环来读完整个文件。
while ((data = fileInputStream.read()) != -1) {
System.out.print((char) data); // 强制类型转换,在控制台输出。
// 这里也暗示了普通的 read() 方法不适合直接读取汉字,
// 因为汉字数据强制转为 char 会显示乱码。
// 如果有汉字,可以使用字符流方法
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileInputStream.close();
// 流是一种资源。使用完成后如果不关闭,会造成计算机性能下降。
// close 也会抛出异常。
} catch (IOException e) {
e.printStackTrace(); // 如果不想写这么多 try-catch,可以用 try-with-resources
}
}

下面我们尝试用 read(byte[] b) 方法读取数据:

int data = 0;
int actually_read_lenth = 0; // 实际读取的字节数
byte[] buffer = new byte[8]; // 一次读取 8 个字节

// 使用 try-with-resources
try (FileInputStream fileInputStream = new FileInputStream("C:\\text.txt")) {
while ((actually_read_lenth = fileInputStream.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, actually_read_lenth));
// String(要读取的数组, 开始读取的位置, 结束读取的位置)
}
} catch (IOException e) {
e.printStackTrace();
} // 不需要手动关闭资源,使用 try-with 可以实现自动关闭。

FileOutputStream(字节流)

常用构造方法:

FileOutputStream(String fileName); // 传入文件的绝对路径
FileOutputStream(File file); // 传入 File 对象
FileOutputStream(File file, Boolean append); // 传入 File 对象,是否启用追加模式

对于 FileOutputStream 对象,我们使用 .write() 方法写入数据:

write(int b)
// 将指定的字节写入此文件输出流——写入单个字节

write(byte[] b)
// 将 b.length 个字节从指定的字节数组写入此文件输出流——写入指定字节数组

write(byte[] b, int off, int len)
// b 数组中第 off 个字节开始写入输出流,写入 len 个字节结束——写入指定字节数组的指定部分
try (FileOutputStream fileOutputStream = new FileOutputStream("C:\\text.txt")) {
// 如果该文件不存在,write() 方法会自动创建该文件。
fileOutputStream.write('a'); // char 会自动转换为 int
} catch (IOException e) {
e.printStackTrace();
}
File file = new File("C:\\text.txt");
try (FileOutputStream fileOutputStream = new FileOutputStream(file, true)) {
String str = " hello, world!";
fileOutputStream.write(str.getBytes()); // .getBytes 字符串转为字节数组
} catch (IOException e) {
e.printStackTrace();
}

运行以上两个方法后,最终的文件内容为 a hello world!,因为我们在第二种方法启用了追加模式。

如果没有启用追加(直接用 FileOutputStream(File file)FileOutputStream(File file, false)),那么在运行第二个方法时,前面的 a 会被覆盖。

FileReader(字符流)

常用构造方法:

FileReader(String fileName); // 传入文件的绝对路径
FileReader(File file); // 传入 File 对象

常用方法,是不是跟 FileInputStream 很像?

read()
// 从输入流中读取一个字符。
// 如果读取正常,返回数据的下一个字符。
// 如果返回 -1,则代表没有数据可读取(读取完毕)。

read(char[] c)
// 从输入流中将最多 c.length 个字符读入一个 char 数组中。
// 如果读取正常,返回实际读取的字节数。
// 如果返回 -1,则代表没有数据可读取(读取完毕)。
// 使用 read() 方法
File file = new File("C:\\text.txt");
int data = 0;
try (FileReader fileReader = new FileReader(file)) {
while ((data = fileReader.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}

// 使用 read(char[]) 方法
File file = new File("C:\\text.txt");
int actually_read_lenth = 0;
char[] buffer = new char[8];

try (FileReader fileReader = new FileReader(file)) {
while ((actually_read_lenth = fileReader.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, actually_read_lenth));
}
} catch (IOException e) {
e.printStackTrace();
}

FileWriter(字符流)

注意

FileWriter 使用后,必须要关闭 .close() 或刷新 .flush(),否则文件仅在内存中!

这是因为 Java 将真正的写入文件操作放在了 .close().flush() 内。

常用构造方法:

FileWriter(String fileName); // 传入文件的绝对路径
FileWriter(File file); // 传入 File 对象
FileWriter(File file, Boolean append); // 传入 File 对象,是否启用追加模式

常用方法,是不是跟 FileOutputStream 很像?

write(int b)
// 将指定的字符写入此文件输出流——写入单个字符

write(String s)
// 将 s.length 个字符从指定的字符串写入此文件输出流——直接写入整个字符串

write(String s, int off, int len)
// s 字符串中第 off 个字符开始写入输出流,写入 len 个字节结束——写入指定字符串的指定部分

write(char[] c)
// 将 c.length 个字符从指定的字节数组写入此文件输出流——直接写入指定字符数组

write(char[] c, int off, int len)
// c 数组中第 off 个字符开始写入输出流,写入 len 个字节结束——写入指定字符数组的指定部分
char[] char_array = {'a', 'b', 'c'};
try (FileWriter fileWriter = new FileWriter("C:\\text.txt")) {
fileWriter.write('a');
fileWriter.write("hello, world!");
fileWriter.write("hello, world!", 0, 5);
fileWriter.write(char_array);
fileWriter.write("你好啊!".toCharArray(), 0, 2);
} catch (IOException e) {
e.printStackTrace();
}

网络基础

信息是怎么通过网络传输的?

IP 地址

分配给连接到互联网的每一台设备的唯一标识符,就像邮寄地址一样,它让设备在网络中可以互相找到和通信。

端口号

一个 IP 地址下可以运行多个服务。端口号帮助操作系统识别和区分多个网络服务或应用程序,就像在同一所公安局(IP 地址)里,不同的窗口(端口)可以办理不同的服务。

TCP / UDP 协议

  • TCP:三次握手,确认建立连接后发送。适用于传输大量数据,如视频。
  • UDP:不需要确认连接是否完好,直接发送,适用于传输少量数据,如字符串。

网络编程

InetAddress

InetAddress localHost = InetAddress.getLocalHost(); // 返回本机 InetAddress 对象
System.out.println(localHost);

// 输出:ROG-DESKTOP/192.168.12.1
InetAddress host1 = InetAddress.getByName("DESKTOP-M7UD4CN"); // 根据指定主机名返回 InetAddress 对象
System.out.println("host1=" + host1);

// 输出:host1=DESKTOP-M7UD4CN/fe80::a923:1e2f:59b3:d6d%7
InetAddress host2 = InetAddress.getByName("uednd.top"); // 根据指定域名返回 InetAddress 对象
System.out.println("host2=" + host2);

// 输出:host2=uednd.top/104.21.61.64
String hostAddress = host2.getHostAddress(); // 根据指定 InetAddress 对象返回 IP 地址
System.out.println("host2 对应的 ip =" + hostAddress);

// 输出:host2 对应的 ip =104.21.61.64
String hostName = host2.getHostName(); // 根据指定 InetAddress 对象返回其主机名或 IP 地址
System.out.println("host2 对应的主机名 =" + hostName);

// 输出:host2 对应的主机名 =uednd.top

Socket(套接字)

Socket 允许程序把网络连接当成一个流,数据在两个 Socket 间通过 IO 传输。

一般主动发起通信的应用程序属客户端,等待通信请求的为服务端

Socket TCP

客户端(Client)

Socket socket = new Socket("104.21.61.64", 11451); // 连接服务端(ip, 端口)
// Socket socket = new Socket(host1, 11451); 或者可以传入 InetAddress 对象

OutputStream outputStream = socket.getOutputStream(); // socket 对象转为输出流对象
outputStream.write("hello, server".getBytes()); // 字符串转为字节数组

socket.shutdownOutput(); // 写入结束

服务端(Server)

ServerSocket serverSocket = new ServerSocket(11451); // 监听 11451 端口
Socket socket = serverSocket.accept(); // 没有客户端连接 11451 端口时程序阻塞。
// 直到有客户端连接,返回 Socket 对象

System.out.println("客户端已连接");

InputStream inputStream = socket.getInputStream();
int actually_read_lenth = 0;
byte[] buffer = new byte[1024];
while ((actually_read_lenth = inputStream.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, actually_read_lenth));
}
注意

Socket 只能输入或输出字节数据(字节流),因此当我们想使用字符流时,通常要附带使用转换流。同时需要使用 .flush.close 强制输出缓冲;需要同时使用 bufferWriter.newLinesocket.shutdownOutput 表示写入结束。

Socket UDP

由于本项目不涉及 UDP,下面就不展开了。


SQLite-JDBC 入门

为什么我推荐 SQLite

简单就是硬道理。

《程序设计实践 II》任务书中的二选一题目,都提到推荐使用 MySQL 作为数据库管理系统。但我认为,对于这类小型项目,使用 MySQL 有如大炮打蚊子、杀鸡用牛刀。对于初学者来说,我更推荐使用 SQLite 作为数据库管理系统,原因有三:

  1. 配置简单:不需要任何配置,也不需要通过网络端口连接数据库等复杂操作。
  2. 语法简单:省略了很多用不上的复杂功能,基础语法非常简单。
  3. 迁移简单:只要目标环境支持且文件完整,可以直接将数据库文件拷贝到另一台电脑上。相当于你给别人发送了一个 Word 文档,只要对方下载了软件,直接点击就能打开并查看里面的内容。而 MySQL 需要复杂的迁移操作。
提示

任务书推荐 MySQL 更多是出于未来就业技能培养的考虑。如果你想快速高效地完成这个项目,并且深刻理解"用合适的工具做合适的事",那么大胆地使用 SQLite!如果你想借此机会挑战一下自己,学习和掌握更通用的企业级数据库技术,那么按照任务书的建议去折腾一下 MySQL 也是非常有价值的。

?!!SQL!!?

说了这么多,又是 MySQL 又是 SQLite,它们究竟是什么,为什么作业需要用到它们?

观察 MySQL 和 SQLite 这两个名称,你会发现它们有一个共同点——SQL

SQL (Structured Query Language),中文全称"结构化查询语言",日常读作 /ˈsiːkwəl/。它是一种专门设计用来与数据库进行通信的语言规范。

为什么需要数据库?

在上学期的课程设计任务中,我们通过 C 语言文件 IO 简单地读写文件。这种操作虽然便利,但就像把所有资料都随意扔进单个抽屉里,当需要查找时,不仅效率低下,还容易把其它资料弄乱。

数据库,就相当于配备了管理员的大图书馆。它将资料分门别类地放好,将需要频繁查看的资料放在最显眼的位置,还有防盗措施……大大提高了资料的查找效率和安全性。

学习了 SQL, 我们就能操控数据库,完成对资料的增、删、查、改等一系列操作。

MySQL 和 SQLite

可能和你猜测的不太一样,虽然 MySQL 和 SQLite 中都有 SQL,但它们并不是语言规范,而是基于 SQL 开发出的软件产品

打个不太恰当的比喻,MySQL 和 SQLite 就像谷歌浏览器和 Edge 浏览器,虽然它们的名字不同,软件页面的设计也不同,但它们都使用 Chromium 作为内核。

同样,MySQL 和 SQLite 都遵循了 SQL 标准,所以对于这两个软件,你可以用类似的 SQL 语法操作它们,但它们在功能等方面有显著差异。MySQL 主要针对企业场景以及大型项目,它的操作更复杂,语法也在 SQL 的基础上作了很多更改。而 SQLite 正如它的名字一样:轻量化,同时针对这种小型项目也更加容易配置。

安装 SQLite

既然 SQLite 是一个软件,我们就必须先安装再使用。

下面给出 Windows 和 macOS 的安装方式:

SQLite 安装界面

平台GUI (DB Browser for SQLite)CLI
Windowschoco install sqlitebrowserchoco install sqlite
macOSbrew install --cask db-browser-for-sqlitebrew install sqlite

GUI 是图形化界面,可以方便地查看和编辑数据库文件。CLI 是命令行界面,使用终端查看和编辑数据库文件。

对于 Windows,建议同时安装 GUI 和 CLI。而对于 macOS,可以只安装 GUI,因为 macOS 中自带一个较低版本的 CLI。

安装后桌面上会有两个快捷方式,其中一个是 (SQLCipher)。它是 DB Browser for SQLite 的一部分,提供了处理加密 SQLite 数据库的附加功能。

设置为中文:选择语言为中文后点击右下角 save 保存并重启应用。

DB Browser 设置

安装驱动

我们还需要一个翻译官

现在,我们再总结一下需求:

  • 我们需要使用数据库帮助我们高效地管理数据。
  • 对于数据库文件,它是一种特殊格式的文件,我们无法像编辑 .txt 一样直接打开并修改它。我们需要一个软件来帮助我们编辑数据库文件,这个软件就是 SQLite。
  • SQL 是一种语言规范,SQLite 遵循这个语言规范。掌握了 SQL 语法后,我们就可以使用 SQL 命令操控 SQLite 软件。

但是,如何在我们的 Java 程序里使用 SQL 命令呢?

事实上,SQL 命令只能在终端使用。在没有引入翻译官前,我们无法在 Java 代码中直接写 SQL 命令,Java 编译器也看不懂这些命令,只会当成普通的字符串。

我们需要引入一个翻译官,它能接收我们写在 Java 代码里的 SQL 命令,然后去和软件沟通,最终让软件对数据库文件进行操作,完成我们发送的命令。

这个翻译官就是 JDBC 驱动程序规范

JDBC,全称 Java DataBase Connectivity(Java 数据库连接)。为了更好地理解它的功能,我们将其转换成动词的形式:"Java 连接数据库"。这样看,"翻译官"的角色就很明显了。

JDBC 跟 SQL 一样,它们仅仅是开发者参考的规范,不是可以直接使用的工具。为此,我们需要使用 SQLite-JDBC,它才是"真正的"那个翻译官。SQLite-JDBC 前面的"SQLite"表明这个驱动程序是专门针对 SQLite 软件的。

前面提到,数据管理软件有很多,虽然它们大部分都基本遵循 SQL 语法,但每个软件的内部设计仍有区别,因此我们需要针对特定软件的驱动。由于我们选择了 SQLite 管理数据库,那么我们就必须选择 SQLite-JDBC 作为驱动程序。

安装方法

我是一个新手…(最简单)

下面这个就是最新版的驱动程序。你也可以去项目的源代码仓库获取 release:https://github.com/xerial/sqlite-jdbc

下载 sqlite-jdbc-3.49.1.0.jar 并解压完成后,直接将它放在项目的 lib 文件夹内。如果没有 lib 文件夹,则新建一个。

这不是必须的操作,但我们需要培养将文件整齐归纳的习惯。

瞧不起谁?挑战自己?

使用 Maven 吧!具体操作我就不展开了。

SQLite 基本语法

正式开始操作数据库。

不知道你有没有这样的疑惑:前面我们提到"掌握了 SQL 语法后,我们就可以使用 SQL 命令操控 SQLite 软件"。为什么标题是"SQLite 基本语法",而不是"SQL 基本语法"?

这些软件仅仅是基本遵守 SQL 的规范。可能在原有的语法上作了改进,可能在原有的语法上作了删减。因此,我们不得不学习针对特定软件的语法。

但别担心,虽然它们仅仅是基本遵守 SQL 的规范,但至少还是遵守了的,语法结构仍然有一定程度相似,日后再使用其他软件如 MySQL 也不会非常困难。

在正式开始前,请确保已经安装好了所有程序。为了快速上手而不是深入学习,我将直接以代码示例的形式给出,需要自己理解语法结构。

注意:

  1. SQLite 命令【关键字】对大小写不敏感,但部分命令的大写形式和小写形式有所区别。为了便于区分命令与普通字符,对于 SQLite 的命令,最好采用区分大小写的方式(通常为大写)。
  2. 哪里要加分号,哪里要加逗号。

前提

在 SQL 的规范中,我们必须先定义表格的每一列(的结构),然后才能插入每一行(的数据)

例如,正确的表格结构:

学号姓名班级成绩
1张三100.0
2李四59.0
3王五91.0

下面这种表格定义是不合理的,SQL 也不支持这种定义方式:

学号姓名班级成绩
1张三100.0
2李四59.0
3王五91.0
信息

就是说,必须先定义列结构(列名 + 数据类型),再插入行数据。

启动 SQLite

  • 打开终端
  • cd 进入指定的目录
  • 在终端中输入并回车:sqlite3

为了进行接下来的所有操作,请保证 SQLite 处于启动状态。

创建 / 打开一个数据库

.open DatabaseName.db
  • DatabaseName:数据库的名称
  • .db:数据库文件后缀(不需要更改)

如果目录中没有这个文件,将会自动创建;如果存在这个文件,将会打开这个数据库。

为了进行接下来的所有操作,请保证数据库处于打开状态。

在数据库中创建一个表格

一个数据库是由许多互相关联的表格构成的,这里的表格不是什么高大上的概念,就是你平时填表的那种表格。

CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL,
class_name TEXT,
score REAL
);
关键字说明
CREATE TABLE新建一个表格
students以"students"命名这个表格
IF NOT EXISTS如果目录中不存在叫"students"的表格
id, name, class_name, score定义了表格第 0 行从左到右每个格的变量名
VARCHAR, TEXT, REAL变量对应的数据类型
NOT NULL这里的格子不能为空
PRIMARY KEY将这个变量作为主键,每一行唯一的标识符
AUTOINCREMENT主键修饰符,表示变量自增

:::info 为什么是第 0 行? 当我们调用基本命令(没有添加筛选条件)显示表格时,上面的变量名实际上是不会显示的。

第 0 行只是为了方便理解,与生活中的表格类似。这样定义实际上代表了每一列的变量名。 :::

主键的作用是区别不同的行。没有主键时,如果两个张三都在 1 班且成绩都是 100,就无法区分。有主键后:

idnameclass_namescore
1张三1100.0
2张三1100.0
3李四259.0

AUTOINCREMENT 表示当我们插入每一行的数据时,只需要输入类似 "王五, 3, 66" 的结构。此时 id 会自动 +1,不需要输入。

SQLite 表格演示

增加表格数据

在 SQL 的规范中,新添加的每一行数据不能指定插入的位置,只能放在最后一行。

使用 CREATE TABLE 新建的表格中没有任何数据(第 0 行实际不显示)。

添加一行

INSERT INTO students (name, class_name, score)
VALUES ('陈六', '一班', 85.5);

结果:

idnameclass_namescore
1陈六一班85.5

添加多行

INSERT INTO students (name, class_name, score) VALUES
('李四', '一班', 92.0),
('王五', '二班', 76.5),
('赵六', '二班', 95.0),
('胡七', '三班', 98.0);

结果:

idnameclass_namescore
1陈六一班85.5
2李四一班92.0
3王五二班76.5
4赵六二班95.0
5胡七三班98.0

删除表格数据

DELETE FROM table_name WHERE [condition];
关键字说明
DELETE FROM从…地方删除
table_name表格名称
WHERE哪里
condition满足什么条件,往往是一个表达式
注意

WHERE 不是必须的,但如果没有 WHERE 以及后面的 condition整个表格的数据将会被删除,只留下第 0 行!

示例 1:删除 id=3 的行

DELETE FROM students WHERE id = 3;

结果(id=3 王五被删除):

idnameclass_namescore
1陈六一班85.5
2李四一班92.0
4赵六二班95.0
5胡七三班98.0

示例 2:删除 score < 90 的行

DELETE FROM students WHERE score < 90;
idnameclass_namescore
2李四一班92.0
4赵六二班95.0
5胡七三班98.0

示例 3:使用 OR 条件删除

DELETE FROM students WHERE class_name = '一班' OR class_name = '二班';
idnameclass_namescore
5胡七三班98.0
注意

以下这条语句是错误的!

DELETE FROM students WHERE class_name = '一班' AND class_name = '二班';

不能按照自然语义理解:删除 class_name = 一班 class_name = 二班 的行而使用 AND

在进行任何筛选操作时,SQLite 往往是一行一行查找的。AND 是与逻辑。因此,对于任何一行数据,它的 class_name 列只能有一个值。它不可能同时等于 '一班' 又等于 '二班'。这个条件永远不可能为真。

这就像你让一个人去找"一个既是男人又是女人的人",逻辑上是不可能存在的。

示例 4:使用 IN 简化

DELETE FROM students WHERE class_name IN ('一班', '二班');

等价于上面的 OR 写法,更加简洁。

查找表格数据

查询所有数据

SELECT * FROM students;

SELECT 查询结果

查询指定列

SELECT name, score FROM students;

结果:

namescore
陈六85.5
李四92.0
王五76.5
赵六95.0
胡七98.0

带条件筛选

SELECT * FROM students WHERE class_name = '一班';
idnameclass_namescore
1陈六一班85.5
2李四一班92.0
SELECT * FROM students WHERE score > 90;
idnameclass_namescore
2李四一班92.0
4赵六二班95.0
5胡七三班98.0

排序

SELECT * FROM students ORDER BY score DESC;
idnameclass_namescore
5胡七三班98.0
4赵六二班95.0
2李四一班92.0
1陈六一班85.5
3王五二班76.5

限制结果数量

SELECT * FROM students ORDER BY score DESC LIMIT 2;
idnameclass_namescore
5胡七三班98.0
4赵六二班95.0

修改表格数据

修改某行某列

UPDATE students
SET score = 98.5
WHERE name = '李四';
idnameclass_namescore
1陈六一班85.5
2李四一班98.5
3王五二班76.5
4赵六二班95.0
5胡七三班98.0

批量修改

UPDATE students
SET class_name = '精英班'
WHERE class_name = '二班';
idnameclass_namescore
1陈六一班85.5
2李四一班98.5
3王五精英班76.5
4赵六精英班95.0
5胡七三班98.0

查找数据库中的表格

.table

删除数据库中的表格

DROP TABLE IF EXISTS table_name;
注意

使用此命令时要特别注意,一旦一个表被删除,表中所有信息也将永远丢失。

退出数据库并退出 SQLite

.exit

在 Java 中使用 SQLite 命令

马上就要完成了……以下是一个小小的测试用例,用于测试刚刚的驱动程序能否运行,以及如何在 Java 中使用 SQLite 命令:

/**
* 导入相关的包,以下这些基本上都是必要的。你还可以根据项目需求导入其它的包
*/
import java.sql.Connection; // 用于建立与数据库的连接
import java.sql.DriverManager; // 用于管理JDBC驱动程序
import java.sql.ResultSet; // 用于存储查询结果
import java.sql.Statement; // 用于执行SQL语句

/**
* SQLite 基础操作示例
*/
public class SQLiteSimple {
public static void main(String[] args) throws Exception {
/***************** 0. 驱动注册 *****************/
// 对于较旧版本的Java,通常需要注册驱动操作
// 我们使用的Java版本较新,Java已经帮我们实现了自动注册

/*1. 与数据库建立连接(打开SQLite,创建/打开数据库)*/
Connection conn = DriverManager.getConnection("jdbc:sqlite:simple.db");
System.out.println("数据库连接成功");

/************* 2. 创建Statement对象 *************/
Statement stmt = conn.createStatement();

/************ 3. 在数据库中创建一个表格 **********/

// executeUpdate是Statement类的一个方法,用于执行数据库的更新操作。
stmt.executeUpdate("DROP TABLE IF EXISTS students"); // 如果该表格存在,先删除它
// 可以看到,在 Java 中都是以字符串的形式使用 SQLite 语句
// 与在终端中使用 SQLite 语句不同,每条语句的末尾不需要加分号,只保留Java本身的分号

stmt.executeUpdate("CREATE TABLE IF NOT EXISTS students ("
+ "id INTEGER PRIMARY KEY,"
+ "name TEXT,"
+ "score INTEGER"
+ ")");
System.out.println("创建表格成功");

/*************** 4. 增加表格数据 ***************/
stmt.executeUpdate("INSERT INTO students VALUES (1, '张三', 95)");
stmt.executeUpdate("INSERT INTO students VALUES (2, '李四', 88)");
stmt.executeUpdate("INSERT INTO students VALUES (3, '王五', 73)");
System.out.println("插入数据成功");

/*************** 5. 查询表格数据 ***************/
System.out.println("\n所有学生信息:");

// executeQuery是Statement类的一个方法,用于执行数据库的查询操作。
ResultSet rs = stmt.executeQuery("SELECT * FROM students");

// 可以将ResultSet视为一个完整表格,但这个表格只显示第一行
while (rs.next()) { // 使用.next移动到下一行
int id = rs.getInt("id");
String name = rs.getString("name");
int score = rs.getInt("score");
System.out.println(id + "\t" + name + "\t" + score);
}

/*************** 6. 更新表格数据 ***************/
stmt.executeUpdate("UPDATE students SET score = 100 WHERE name = '张三'");
System.out.println("\n更新后的张三信息:");
rs = stmt.executeQuery("SELECT * FROM students WHERE name = '张三'");
while (rs.next()) {
System.out.println(rs.getInt("id") + "\t"
+ rs.getString("name") + "\t"
+ rs.getInt("score"));
}

/*************** 7. 删除表格数据 ***************/
stmt.executeUpdate("DELETE FROM students WHERE name = '王五'");
System.out.println("\n删除王五后的所有学生信息:");
rs = stmt.executeQuery("SELECT * FROM students");
while (rs.next()) {
System.out.println(rs.getInt("id") + "\t"
+ rs.getString("name") + "\t"
+ rs.getInt("score"));
}

/**************** 8. 关闭数据库 ****************/
rs.close();
stmt.close();
conn.close();
System.out.println("\n数据库连接已关闭");
}
}

如何运行程序?

(VSCode)直接点击右上角运行按钮肯定是不行的,不信你试一下。

我是一个新手😭…(脚本运行)

如果你使用 Windows

在项目的根目录创建一个 .bat 文件,输入以下代码:

@echo off
java -cp .;lib\sqlite-jdbc-3.49.1.0.jar SQLiteSimple
pause

通过终端运行:.\run.bat(或双击 .bat 文件)

如果你使用 macOS

在项目的根目录创建一个 .sh 文件,输入以下代码:

#!/bin/sh
java -cp .:lib/sqlite-jdbc-3.49.1.0.jar SQLiteSimple

在 shell 中执行脚本:

chmod +x run.sh
sh run.sh
瞧不起我?💢 挑战自己?

打包成 JAR 文件吧!

运行结果

运行成功后,控制台应该会输出以下内容:

数据库连接成功
创建表成功
插入数据成功

所有学生信息:
1 张三 95
2 李四 88
3 王五 73

更新后的张三信息:
1 张三 100

删除王五后的所有学生信息:
1 张三 100
2 李四 88

数据库连接已关闭

最后解答一个疑惑

如果我将整个项目拷贝到其它电脑(即使没有安装 SQLite 和驱动),这个项目也可以运行吗?

答案是:"Write Once, Run Anywhere"(一次编写,到处运行)