Skip to content

软件工程

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 包并解压到你的工作目录。

1769471806668

下载后我们将得到一个叫做project1java的文件夹,然后我们将合作方的skeleton导入到 src -> main -> java -> edu -> tufts -> project1java中。

到这一步,环境配置和skeleton衔接等工作就完成了。正确的目录如下:

1769472234908

搭建好环境、代码骨架后,就可以进入代码实现阶段了。在开始前,建议检查一下项目设置:

  1. 按下快捷键 Ctrl + Alt + Shift + S(Mac 是 Command + ;)打开 Project Structure 窗口。
  2. 点击左侧的 Project 选项卡。
  3. 确保 SDK 栏位显示的是 21
  4. 确保 Language level 栏位也选的是 21
  5. 点击 OK

SDK和语言版本确认后,我们还要确认Maven状态。如果IntelliJ右侧没有Maven图标,可能是还没有识别出这是一个Maven项目。此时我们可以手动“点醒”它:

  1. 在左侧项目树中找到文件 pom.xml
  2. pom.xml 上点击 鼠标右键
  3. 在弹出的菜单底部,找到并点击 "Add as Maven Project"
  4. 完成后,界面右侧通常就会出现Maven 选项卡了。

配置好Maven后我们会看到很多报错信息,所以我们首先要完善graph文件夹下每一个.java文件的package声明:package edu.tufts.project1java.graph;

实现核心逻辑

我们主要有两个任务:

  1. 完成ListGraph.java,使用HashMap和LinkedList来实现邻接表逻辑。
  2. 完成EdgeGraphAdapter.java,负责把一种图的接口(Graph)转换为另一种接口(EdgeGraph)。

然后我们要写两个测试文件:

  1. GraphTest.java
  2. 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面板来完成:

  1. 点击 IntelliJ 窗口最右侧边缘的 Maven 标签。
  2. 依次展开:project1java -> Lifecycle
  3. 先双击 clean (清理之前的编译残留)。
  4. 再双击 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

.


评论 #