软件工程
Java与开发基础
在学习软件工程前,我们先了解一下Java和相关开发工具的概念。
| 概念 | 版本 | 解释 |
|---|---|---|
| JDK | Java21 | JDK即Java Development Kit,是Java语言运行的环境。 Java21是主流的LTS版本,此外25、17、8、11也很常见。 |
| Maven | Apache Maven 3.9.x | Maven是Build Tool,负责构建自动化。 Maven 3.9.x系列是最主流的版本。 |
| IDE | IntelliJ IDEA | 用Terminal写java效率太低,一般用IDE来开发。IDE集成了编辑器、终端、Maven控制面板、调试器等。 IDE一般保持最新版就可以,可单独配置JDK和Maven的版本。 |
其中JDK=解释器+编译器+标准库+调试工具。
可以这么认为:
- JDK = JRE + 开发工具(如javac编译器、调试器、文档工具等)
- JRE = JVM + 运行库,如果不需要开发,只需要运行java程序,安装JRE就够了
- JVM: 最核心的Java虚拟机,负责读取编译好的字节码.class文件并运行
Code Skeleton
Code Skeleton(核心代码骨架)是一个程序的基础结构,一般包括:
- Package declarations,包声明,定义代码放在哪个文件夹下
- Classes & Interfaces,类和接口定义,定义了有哪些对象
- Empty Methods,空的方法,方法名、参数、返回值都写好了,但大括号内是空的
- Comments/TODOs
package一般用像是网址一样的方式来组织,实质上是定义了代码在磁盘上的物理存放路径。比如 edu.tufts.project1java.graph,其实质对应的路径是 .../src/main/java/edu/tufts/project1java/graph/,这种写法叫做反向域名命名法。之所以这么写,是因为全球可能会有无数个程序员开发叫做graph.java的文件,所以这样写是为了方便区分,这也是软件工程课程中主要的内容:了解最佳实践。
例如下列代码块:
import java.util.*; // Makes List available
public interface Graph {
boolean addNode(String n);
boolean addEdge(String n1, String n2);
boolean hasNode(String n);
boolean hasEdge(String n1, String n2);
boolean removeNode(String n);
boolean removeEdge(String n1, String n2);
List<String> nodes();
List<String> succ(String n);
List<String> pred(String n);
Graph union(Graph g);
Graph subGraph(Set<String> nodes);
boolean connected(String n1, String n2);
}
上述骨架基本勾勒出了Graph程序有哪些功能。
为了实现这些功能,我们需要去构建具体的程序模块,比如Edge.java, ListGraph.java等。在具体的.java文件中,我们把class写好。这种将“规范”(接口,Skeleton, Interfaces)与“实现”(具体代码, Implementation)分离的思想,在软件工程中被称为 面向接口编程 。
Testing
一般用JUnit测试。JUnit是Java中最流行的单元测试框架(Unit Testing Framework)。
Configuration
上面所有的相关内容都要在Configuration(配置文件)中配置好。一般在pom.xml中完成项目级配置,在Run/Debug Configurations中配置好java语言、断言、测试类等。
POM(Project Object Model,项目对象模型),是Maven的管理体系中的项目配置文件,每个项目都必须在根目录下拥有一个pom.xml文件,否则Maven无法识别并管理该项目。
pom.xml会管理以下内容:
- dependencies,依赖,即项目运行所需的全部外部代码库
- plugins, 插件,规定生产流程
- coordinates,定义项目身份,规定了项目的唯一标识(GroupId, ArtifactId)以及版本号(如1.0-SNAPSHOT)等。
在没有Maven和pom.xml的年代,程序员需要手动下载各种 .jar 库文件并手动配置复杂的路径。因此这个配置文件是非常重要的,只要配置好,Maven就会自动通过互联网下载所需的依赖包。
Framework
最常用的Java Framework是Spring Framework。Spring Boot是基于Spring Framework的增强型快速开发框架(包括自动配置、启动器等)。简单来说,Spring Boot可以把后端开发中麻烦的东西都自动做好,从而让程序员无需关注配Web服务器、写XML/配置文件、处理依赖版本冲突、组织项目结构等。
Spring Boot非常适合:
- Web接口(REST API)
- 后端服务
- 微服务
TDD
在软件工程中,测试驱动开发(TDD - Test-Driven Development)是一种需求导向的开发方式。换句话说,在动手写功能之前,你必须先搞清楚这个功能到底应该怎么表现。此外,TDD还可以构建安全网,让你在没写一个新的功能后就能测试,来回测程序是否被破坏。同时,先写测试也能够集思广益,让我们思考一个程序的Edge Cases有哪些。
开发实例:Graph
本部分以一个具体的项目案例来说明如何使用java进行软件开发。
环境搭建与项目初始化
首先配置java开发环境,具体包括:
- JDK,这里采用JDK21
- Maven
- IDE,这里采用IntelliJ IDEA
这里要注意,在现代 Java 开发中,仅仅有 .java 文件是不够的。你需要一个符合 Maven 标准 的项目环境来管理依赖、编译代码和运行测试。比如说,合作方(比如本项目)提供了一些零散的.java文件,仅仅有这些.java文件是不够的,我们还需要Spring Initializr来生成一个带有pom.xml的标准Maven项目。
操作步骤如下:
- 访问 Spring Initializr。
- 配置参数 : 选择 Maven、Java 21,设置 Group 为edu.tufts,Artifact 为project1java。
- 下载并保存 : 点击GENERATE,下载 zip 包并解压到你的工作目录。

下载后我们将得到一个叫做project1java的文件夹,然后我们将合作方的skeleton导入到 src -> main -> java -> edu -> tufts -> project1java中。
到这一步,环境配置和skeleton衔接等工作就完成了。正确的目录如下:

搭建好环境、代码骨架后,就可以进入代码实现阶段了。在开始前,建议检查一下项目设置:
- 按下快捷键
Ctrl + Alt + Shift + S(Mac 是Command + ;)打开 Project Structure 窗口。 - 点击左侧的 Project 选项卡。
- 确保 SDK 栏位显示的是 21 。
- 确保 Language level 栏位也选的是 21 。
- 点击 OK 。
SDK和语言版本确认后,我们还要确认Maven状态。如果IntelliJ右侧没有Maven图标,可能是还没有识别出这是一个Maven项目。此时我们可以手动“点醒”它:
- 在左侧项目树中找到文件
pom.xml。 - 在
pom.xml上点击 鼠标右键 。 - 在弹出的菜单底部,找到并点击 "Add as Maven Project" 。
- 完成后,界面右侧通常就会出现Maven 选项卡了。
配置好Maven后我们会看到很多报错信息,所以我们首先要完善graph文件夹下每一个.java文件的package声明:package edu.tufts.project1java.graph;
实现核心逻辑
我们主要有两个任务:
- 完成ListGraph.java,使用HashMap和LinkedList来实现邻接表逻辑。
- 完成EdgeGraphAdapter.java,负责把一种图的接口(Graph)转换为另一种接口(EdgeGraph)。
然后我们要写两个测试文件:
- GraphTest.java
- EdgeGraphTest.java
这里我们注意,为什么测试文件写的是GraphTest.java而不是EdgeGraphTest.java呢?这是一个非常深入的问题,涉及到面向对象设计中的一个核心原则: 面向接口编程 (Coding to an Interface) 。在软件工程中,测试文件的命名通常反映了它所验证的 功能行为 ,而不仅仅是具体的 实现类 。
由于我们编写的ListGraph实际上是Graph接口的一个具体实现,所以我们要测试的应当是Graph,因为我们希望测试的是图的行为而不是邻接表的细节。
我们先打开ListGraph.java,看到如下内容:
import java.util.*;
public class ListGraph implements Graph {
private HashMap<String, LinkedList<String>> nodes = new HashMap<>();
public boolean addNode(String n) {
throw new UnsupportedOperationException();
}
public boolean addEdge(String n1, String n2) {
throw new UnsupportedOperationException();
}
public boolean hasNode(String n) {
throw new UnsupportedOperationException();
}
public boolean hasEdge(String n1, String n2) {
throw new UnsupportedOperationException();
}
public boolean removeNode(String n) {
throw new UnsupportedOperationException();
}
public boolean removeEdge(String n1, String n2) {
throw new UnsupportedOperationException();
}
public List<String> nodes() {
throw new UnsupportedOperationException();
}
public List<String> succ(String n) {
throw new UnsupportedOperationException();
}
public List<String> pred(String n) {
throw new UnsupportedOperationException();
}
public Graph union(Graph g) {
throw new UnsupportedOperationException();
}
public Graph subGraph(Set<String> nodes) {
throw new UnsupportedOperationException();
}
public boolean connected(String n1, String n2) {
throw new UnsupportedOperationException();
}
}
我们可以看到很多 throw new UnsupportedOperationException();——这行代码在编程中被称为占位符或逻辑坑位,它的意思是“抛出一个‘不支持该操作’的异常” 。之所以写这个,是因为在项目skeleton中,虽然已经定义好了方法
实现基础功能后,我们就要为每个功能写一个test,然后进行测试。我们在test文件夹下创建对应的test文件,然后写test,比如:
@Test
@DisplayName("Add new node successfully")
void testAddNodeSuccess(){
Graph g = new ListGraph();
boolean result = g.addNode("A");
assertTrue(result);
assertTrue(g.hasNode("A"));
}
这里要注意,不同的@Test方法之间并不会互相影响,他们都在各自全新的对象实例中运行。
所有代码完成后,可以打开terminal,输入 mvn clean test;如果terminal中缺乏环境依赖,也可以用IntelliJ的Maven面板来完成:
- 点击 IntelliJ 窗口最右侧边缘的 Maven 标签。
- 依次展开:
project1java->Lifecycle。 - 先双击
clean(清理之前的编译残留)。 - 再双击
test(执行单元测试)。
如果看到BUILD SUCCESS,说明你的代码编译通过,且所有的单元测试(JUnit)全部运行成功。
其他开发相关
Docker
常用命令:
# 检查 NVIDIA 驱动
nvidia-smi
# 检查 Docker GPU 支持
docker run --rm --gpus all nvidia/cuda:11.8.0-base-ubuntu22.04 nvidia-smi
# 1. 停止并删除当前容器
docker-compose down
# 2. 删除旧镜像(强制清理)
docker rmi doc-recognition:latest
# 3. 清理 Docker 构建缓存(可选但推荐)
docker builder prune -f
# 4. 重新构建镜像(不使用缓存)
docker-compose build --no-cache
# 5. 启动新容器
docker-compose up -d
# 6. 查看日志确认启动成功
docker-compose logs -f
DEBUG:找不到cuDNN的时候的一些做法
# cd到项目所在目录
docker-compose exec recognition bash
# 1. 检查 nvidia-smi
nvidia-smi
# 2. 检查 PaddlePaddle 能否找到 CUDA
python3 -c "import paddle; print('PaddlePaddle:', paddle.__version__); print('CUDA compiled:', paddle.is_compiled_with_cuda())"
# 3. 测试创建 GPU tensor(这里会失败,因为找不到 cuDNN)
python3 << 'EOF'
import paddle
try:
paddle.set_device('gpu:0')
x = paddle.randn([2, 2])
print("✓ GPU 工作正常!")
except Exception as e:
print(f"✗ GPU 错误: {e}")
EOF
# 退出
exit
.