简介:面向Java桌面应用开发初学者与图形界面编程学习者,这份电子相册源码演示了如何使用JavaFX或Swing构建照片管理工具,内容涉及文件读写、图像处理、事件监听、多线程以及数据库连接等关键技术点。压缩包共10个文件,其中核心源码文件可清晰查看程序逻辑,编译后的运行类便于直接体验,五张JPEG图片与一张BMP图片用作相册素材,另有网页预览文件与说明文档帮助了解项目结构;整个压缩包仅160KB,轻量又完整。已有495人学习查看,适合用来理解桌面程序界面布局、图片文件读取和照片信息保存的实现思路。从界面控件到事件响应,再到图片数据和数据库元数据的协作,代码层次清楚,适合逐段研读;读者还可以在此基础上继续扩展缩略图生成、图片旋转裁剪、按日期筛选等功能,作为课程设计或自学练手项目都很合适。
1. 一个 Java 电子相册源码包,到底解决了什么
照片一多,按目录翻找就成了体力活;与其用商业图库把文件传上云,不如在本地跑一个把目录扫描、缩略图、标签和幻灯片播放串起来的 Java 项目。这就是 Java 电子相册源码.zip 这类包存在的原因:它不是又一份 UI demo,而是一个解压后能看懂、能改、能继续扩展的代码骨架。
对刚把 Java 基础学完的人,它把文件流、集合、Swing/JavaFX、线程和 JDBC 全用上了;对有几年经验的人,它又能很快改成内网素材库、实验室图片管理这类小工具。很多 Java 学习路线都会把这类项目放到简历实战里,面试官也喜欢从扫描线程、数据库锁、缩略图缓存三处追问,所以源码可以不大,但边界必须清楚。
拿到这种源码包别急着双击运行。先想清楚它要解决的是“照片堆积”还是“照片丢失”:前者需要快速浏览分类,后者需要增量索引和备份,这两个方向写出来的数据模型完全不同。
2. 拆解 Java 电子相册的功能边界与数据模型
做一个电子相册不必做成在线图库。大部分源码标注的“电子相册”,落到代码上只有四件事:扫描磁盘目录、生成缩略图、按相册/标签展示、大图查看。能把四条链路做顺畅,后面要加入脸分组或云备份,都只是同一套数据模型上新增字段的事。所以第一件事不是写界面,而是把功能边界和数据边界固定下来。
2.1 先定功能边界:本地照片、索引和展示
从一张家庭照片到可浏览的相册,中间隔着三条链路:文件采集、元数据入库、界面渲染。文件采集要处理哪些目录、要不要跟随符号链接;元数据要保存拍摄时间和文件摘要,避免每次启动都全盘扫描;界面渲染要处理缩略图加载失败、大图内存溢出这两类高频问题。
常见做法是启动时选择“照片根目录”,把目录下的 jpg/png/webp 文件扫出来,写入本地索引库;之后每次启动做增量扫描,只处理新增和变化过的文件。这一层最容易写歪的地方,是在扫描线程里直接刷新界面。扫描只负责把路径和时间戳写入待处理队列,缩略图生成放到另一个线程池,界面轮询数据库拿“已生成缩略图”的记录。否则用户选了十万张照片的目录,相册窗口会卡死。
2.2 数据模型:SQLite 和 H2 怎么选
源码包要给别人直接跑,数据库不能要求额外安装,所以选嵌入式数据库。常见选择是 SQLite 和 H2,两者都能以单文件方式工作,区别主要在两点:SQLite 通过 JDBC 驱动访问,适合轻量单写场景;H2 有更完整的 SQL 支持和内置 Web Console,适合需要调试数据接口的项目。
| 判断点 | SQLite | H2 |
|---|---|---|
| 驱动体积 | 小,xerial 驱动更轻 | 更大,自带 PG/MySQL 兼容模式 |
| 并发模型 | 一个进程一个写者 | 支持 MVStore,多连接读更好 |
| 数据类型 | 弱类型,字段长度不强制 | 强类型,更适合常规 JavaBean 映射 |
| 导出迁移 | 单文件,直接复制 | 可做 SQL 脚本迁移 |
| 适合场景 | 纯本机相册、内网工具 | 需要连管理界面、多人共用 |
我一般会把缩略图路径和照片路径都存成绝对路径,因为相册工具通常跑在固定机器上,绝对路径便于直接写File操作。如果做跨平台分发,则要额外保存“相对于根目录的相对路径”,否则换一台电脑索引就断链。数据模型至少要有三张表:照片表、相册表和标签表,照片与标签用中间表关联,照片与相册也用中间表关联。
2.3 用 DDL 把相册表、标签表和缩略图表落下来
下面是这套模型里最核心的建表语句,源码包里如果有schema.sql,多数也是这个骨架。
CREATE TABLE photo ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT NOT NULL UNIQUE, file_size INTEGER NOT NULL, file_modified BIGINT NOT NULL, take_time TIMESTAMP, width INTEGER, height INTEGER, thumbnail_path TEXT, imported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE album ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE photo_album ( photo_id INTEGER NOT NULL, album_id INTEGER NOT NULL, PRIMARY KEY (photo_id, album_id) ); CREATE TABLE tag ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE ); CREATE TABLE photo_tag ( photo_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, PRIMARY KEY (photo_id, tag_id) );这段 DDL 的核心在photo_album和photo_tag两张中间表:相册和标签都与照片一对多,但不直接给照片表加album_id字段,因为一张照片可能同时出现在“旅行”“家庭”“2024年”多个集合里,中间表保留多对多关系。file_modified存文件最后修改时间的 epoch 秒,用来做增量扫描比对;take_time来自 EXIF,不一定每张图都有,所以允许空。thumbnail_path单独成字段,是为了删原图时能同步清理缩略图文件,不需要再额外查一次关联表。
建完表之后,数据访问层只需要两个核心方法:批量插入待扫描文件,按file_modified查已存在记录。很多人会在这一步引入 MyBatis 或 Spring Data,但一个本地相册用JdbcTemplate或纯 JDBC 就足够,减少依赖反而让源码包更容易跑通。建表时要注意path上的 UNIQUE 约束,它会在重复扫描时自动挡住重复照片,省掉一次应用层查询。
3. 用 Java 实现电子相册的核心链路:扫描、缩略图与相册展示
数据库落好以后,程序的核心就变成三条链路:扫文件、出缩略图、上屏展示。很多电子相册源码的 bug 都集中在扫描卡 UI、缩略图黑边、自动播放崩内存三件事上,这一章的代码会直接对应到这些问题。
3.1 目录扫描用 FileVisitor 而不是递归遍历
扫描目录时最常见的写法是递归调用listFiles(),但遇到大型目录树会产生大量临时File对象,而且不方便跳过隐藏目录。用Files.walkFileTree配合FileVisitor,可以把“进入目录、读取文件、出错恢复”三个时机分开处理。
public final class PhotoScanner implements FileVisitor<Path> { private static final Set<String> EXTS = Set.of(".jpg", ".jpeg", ".png", ".webp", ".bmp"); private final Queue<Path> pending = new ConcurrentLinkedQueue<>(); public void scan(Path root) throws IOException { Files.walkFileTree(root, this); } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { String name = file.getFileName().toString().toLowerCase(); if (attrs.isRegularFile() && EXTS.stream().anyMatch(name::endsWith)) { pending.offer(file); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { String name = dir.getFileName() == null ? "" : dir.getFileName().toString(); return name.startsWith(".") ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) { System.err.println("read fail: " + file + " -> " + exc.getMessage()); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) { return FileVisitResult.CONTINUE; } }这段代码里EXTS用小写匹配,避免系统出现.JPG和.jpg混用导致漏扫;attrs.isRegularFile()过滤掉符号链接和目录,避免把快捷方式也当成图片;visitFileFailed返回CONTINUE而不是TERMINATE,防止一张权限不足的照片让整个扫描中断。Queue<Path>用并发队列,扫描线程和缩略图线程可以安全地传递文件列表。
扫描完下一步要把路径写进photo表。表上有path UNIQUE约束,重复跑同一目录会报错。常见做法不是先查一遍再插入,而是用INSERT OR IGNORE(SQLite)或MERGE INTO(H2)把“判断是否存在”交给数据库,减少一次往返。
3.2 缩略图生成:先缩到 256px 再存盘
缩略图直接决定相册首页能不能滑得流畅。用ImageIO.read()读原图生成缩略图,在 JPEG 大图场景下会把整张图解码进内存,二百张 5000 像素宽的照片就能吃满默认堆内存。正确顺序是先读图片头部尺寸,按比例算缩放目标,再生成缩略图并缓存到磁盘。
BufferedImage thumbnail(BufferedImage source, int targetSize) { int type = source.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : source.getType(); int w, h; if (source.getWidth() >= source.getHeight()) { w = targetSize; h = (int) Math.round(targetSize * (source.getHeight() / (double) source.getWidth())); } else { h = targetSize; w = (int) Math.round(targetSize * (source.getWidth() / (double) source.getHeight())); } BufferedImage out = new BufferedImage(w, h, type); Graphics2D g = out.createGraphics(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.drawImage(source, 0, 0, w, h, null); g.dispose(); return out; }这里不建议用source.getScaledInstance(w, h, Image.SCALE_FAST),那个 API 返回的对象是异步缩放,质量不稳定,而且每次访问像素都可能重复计算。用Graphics2D.drawImage是目前 Java 自带的、可控性最高的缩放方案。如果想让相册网格整齐,常见的做法是先把图居中裁剪成正方形,再缩到 256px;如果保留宽高比,就要在JList或网格布局里预留比例修正。
生成后的缩略图建议缓存到thumb/子目录,文件名用原图path的 SHA-256 哈希值,避免路径里的特殊字符影响文件系统。下表是三种缩略图方案的区别,代码评审时可以直接引用:
| 方案 | 内存占用 | 图片质量 | 适合场景 |
|---|---|---|---|
| ImageIO 整图缩放 | 高 | 一般 | 小图集 |
| Graphics2D 直接绘制 | 中 | 好 | 常规相册 |
| ImageReader 按目标尺寸解码 | 最低 | 依赖采样参数 | 大量高分辨率图片 |
3.3 相册面板与自动播放的线程模型
Swing 相册界面通常用JList或JTable展示缩略图,数据从数据库分批读取,一次只加载 50 或 100 条,避免一次性生成上千个ImageIcon。打开相册时先查photo_album关联表,再通过缩略图路径加载缓存图。为了让磁盘 IO 不阻塞 EDT,常见做法是后台线程查数据库得到路径列表,EDT 用SwingWorker异步加载图片。
class ThumbnailLoader extends SwingWorker<Map<String, Icon>, ThumbnailItem> { private final List<String> photoPaths; @Override protected Map<String, Icon> doInBackground() { Map<String, Icon> result = new LinkedHashMap<>(); for (String p : photoPaths) { Icon icon = loadThumbFromCache(p); result.put(p, icon); publish(new ThumbnailItem(p, icon)); } return result; } @Override protected void process(List<ThumbnailItem> chunks) { for (ThumbnailItem item : chunks) { listModel.set(item.path, item.icon); } } }使用SwingWorker的publish/process机制,能避免每生成一张缩略图就SwingUtilities.invokeLater一次,把多个 UI 更新挤压到同一个 EDT 周期里。自动播放一般用javax.swing.Timer每 3 秒切一张,Timer在 EDT 上执行,切换大图要从缓存里取;缓存不命中就先用占位图占位,再异步换图。很多老源码包的自动播放越播越慢,原因是图像缓存没有上限,给LinkedHashMap加一个removeEldestEntry重载,就能限制最多缓存 100 张大图。
4. 把源码整理成规范的 Java 工程并打 zip 发布
源码包最终是给人解压后直接打开的,目录结构比实现技巧更容易决定第一印象。Maven 工程放在根目录,src/main/java按功能分包,入口类只做依赖初始化;扫描、缩略图、数据库、UI 各自独立,这样代码才能被其他人快速定位。
4.1 工程根目录拆成 src 和 tools,入口类只做一件事
photo-album/ ├── pom.xml ├── README.md ├── .gitignore ├── data/ ├── src/main/java/ │ └── com/example/photoalbum/ │ ├── Main.java │ ├── repository/ │ ├── scanner/ │ ├── thumbnail/ │ └── ui/ └── tools/ └── import-samples.sh结构里data/保存 SQLite 数据库文件,不进源码包,但要在.gitignore中保留目录名;tools/只放与开发期相关的脚本,不属于运行时逻辑。Main.java里只做数据源初始化、创建主窗口、注册关闭钩子三件事,扫描逻辑放到scanner包,缩略图逻辑放到thumbnail包。入口类写得太胖的源码包,往往也意味着类之间的依赖关系没有拆开。
4.2 用 .gitignore 和构建插件控制 zip 内容
没有.gitignore时,打 zip 很容易把.idea/、target/、*.class一起塞进去。源码包要保证“用 git 管理时不会误提交 IDE 配置”,所以.gitignore至少包含下面的内容:
target/ out/ *.class *.log .idea/ *.iml .vscode/ data/* !data/.gitkeeptarget/排除 Maven 构建目录,out/排除 IDEA 默认输出目录,最后两行保留data目录但忽略里面的数据库文件。对应的 zip 打包命令可以写成:
zip -r photo-album-src.zip photo-album/ \ -x "photo-album/target/*" \ -x "photo-album/.git/*" \ -x "photo-album/.idea/*"如果需要控制“哪些必须进包、哪些必须排除”,可以按下面这张表核对:
| 必须保留 | 必须排除 |
|---|---|
| pom.xml、src/、README.md | target/、out/、.idea/ |
| .gitignore、license 文件 | data/.db、.log、*.class |
| 配置文件模板 | 本机绝对路径的启动参数 |
4.3 利用 Maven 插件一键产出源码包
命令行 zip 适合临时分发,但源码包要配合 CI 重复构建,更可控的做法是用 Maven 的maven-source-plugin生成 sources jar,再用 assembly 插件把整个工程打成可分发包。下面这段pom.xml是源码工程最常见的基础配置:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.45.1.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.3.1</version> <executions> <execution> <id>attach-sources</id> <goals><goal>jar-no-fork</goal></goals> </execution> </executions> </plugin> </plugins> </build>maven.compiler.source指定 Java 17,project.build.sourceEncoding强制 UTF-8,否则 Windows 和 Linux 跨平台解压后注释容易乱码。sqlite-jdbc 的版本要固定到具体数字,不能写3.+这种范围,因为源码包交付后没人保证未来驱动还兼容当时的 JDK。打完包后用unzip -l photo-album-src.zip看一眼文件清单,重点确认pom.xml、src/main/java存在,target/和本地数据库文件不在清单里。
5. 从源码包到可运行程序:构建脚本、参数调优和坑
源码包真正值钱的地方在于“能改”。很多 Java 学习者拿到代码后遇到的第一个问题不是看不懂业务逻辑,而是构建不过、图片乱码、相册窗口一打开就 OOM。这一章把三个高频坑的解法直接放出来。
5.1 读大图前先读尺寸,避免整图加载撑爆堆内存
看大图时,直接用ImageIO.read(file)会把一张 3000x4000 的 JPEG 完整解码成 RGBA,大约占用 48MB。看似不多,但连续翻图时前一张没有被及时 GC,堆内存就会快速增长。要避免这个问题,先在不解码像素的情况下取尺寸:
try (ImageInputStream in = ImageIO.createImageInputStream(file)) { Iterator<ImageReader> readers = ImageIO.getImageReaders(in); int width = -1; int height = -1; while (readers.hasNext()) { ImageReader reader = readers.next(); try { reader.setInput(in, true, true); width = reader.getWidth(0); height = reader.getHeight(0); break; } finally { reader.dispose(); } } }这段代码通过reader.getWidth(0)只解析文件头,不触碰像素数据。拿到的尺寸可以做两件事:超过 2000px 的大图先按比例降到显示区域大小再渲染;同时把图片按“最近浏览窗口”缓存,比如只缓存前后各 5 张大图,超过窗口的引用立刻置空,让 GC 及时回收。
5.2 图片旋转是 EXIF 处理里最常见的“歪图”坑
手机拍的照片会写 EXIF 方向字段,相册若不处理,竖拍图可能横着显示。这个“歪”不是让用户旋转原图,而是要在读取缩略图和大图时把方向折算到坐标变换里。常见做法是先读 EXIF Orientation,再按值旋转:
BufferedImage rotateByExif(BufferedImage img, int orientation) { if (orientation <= 1) return img; int width = img.getWidth(); int height = img.getHeight(); AffineTransform at = new AffineTransform(); switch (orientation) { case 3 -> at.rotate(Math.toRadians(180), width / 2.0, height / 2.0); case 6 -> { at.translate(height, 0); at.rotate(Math.toRadians(90), 0, 0); } case 8 -> { at.translate(0, width); at.rotate(Math.toRadians(270), 0, 0); } default -> { return img; } } BufferedImage rotated = new BufferedImage(height, width, BufferedImage.TYPE_INT_ARGB); Graphics2D g = rotated.createGraphics(); g.setTransform(at); g.drawImage(img, 0, 0, null); g.dispose(); return rotated; }Orientation 为 6 和 8 时,旋转后宽高互换,画布要写成height, width,否则图像会被裁掉一半。缩略图可以在入库时就应用旋转并存入缩略图文件,大图每次显示还要再做一次方向转换,所以缩略图和大图的处理逻辑不能完全共用。
5.3 JVM 参数和日志配置
源码包内置的启动脚本里,至少要把堆内存上限写出来,不要放任 JVM 默认值。常见启动脚本是:
java -Xms256m -Xmx1024m \ -Dfile.encoding=UTF-8 \ -Duser.language=zh \ -jar photo-album.jar-Xmx1024m限制最大堆,防止相册一直翻图把内存吃满;-Dfile.encoding=UTF-8解决 Windows 默认 GBK 下读取中文路径的乱码问题;-Duser.language=zh让 Swing 组件语言环境一致,避免文件对话框和菜单语言混用。如果用户目录照片特别多,堆上限可以放宽到 2048m,但翻页时 GC 停顿会更明显。
日志建议直接走java.util.logging,不要在源码包里捆绑重量级日志框架。电子相册不是服务端,日志只有两个职责:记录扫描失败的路径,记录缩略图生成异常。把日志写到logs/目录,不要和图片文件混在一起。数据库打不开时,程序要弹出错误对话框而不是静默退出,这也是很多源码包的盲区。
| 症状 | 可能原因 | 处理 |
|---|---|---|
| 启动即 OOM | 未设置-Xmx或值太小 | 修改启动脚本堆参数 |
| 中文路径显示为问号 | 文件读取编码不是 UTF-8 | 增加-Dfile.encoding=UTF-8 |
| 相册窗口白屏 | 缩略图在 EDT 同步加载 | 改用 SwingWorker 后在process更新 |
| SQL 报 database is locked | 多线程共用同一 SQLite 连接 | 扫描线程和缩略图线程分连接 |
6. 最后一步:验证源码包的完整性与可复现构建
源码包作为 zip 交付前,我会做四步检查:解压完整性、文件编码、依赖解析、干净构建。第一步是命令级验证:
unzip -t photo-album-src.zip如果输出里出现CRC error或bad file descriptor,说明 zip 在传输或打包过程中已经损坏,绝不能发布。第二步是删除临时目录,把 zip 解压到全新路径,再运行mvn -q clean verify。clean的作用是删掉上次构建的target/,让构建过程不依赖任何历史产物。
第三步是检查 README 里写的构建命令是否能照着执行。README 不需要长篇教程,只要写清 JDK 版本、mvn package后的产物位置、第一次运行会生成什么文件。不要写“需要另外安装数据库”,因为嵌入式数据库自己带;也不要在 README 里引用开发机器上的绝对路径。
最后一步是检查 zip 根目录名。如果解压出来是photo-album-master,文件全在带随机后缀的目录里,用户很难处理。正确做法是先建一个photo-album/目录,把工程内容放进去再打包:
cd .. && zip -r photo-album-src.zip photo-album/ \ -x "photo-album/target/*" \ -x "photo-album/.git/*"打完包后还要做一次反向检查:把 zip 解压到/tmp/verify,在解压目录里直接mvn -q package。这个动作能发现两种隐蔽问题:一是打包时漏掉测试资源,二是 pom 依赖了个人私有仓库的包。若出现“无法解析依赖”的情况,要把依赖换回 Maven Central 上的公共版本,否则源码包交到别人手里无法构建。
每次修改代码后重新打 zip,都应在全新目录里跑一遍构建,并同步更新 SHA-256 校验值。等到unzip -t、mvn -q package和校验值对比这三项都通过,这份 Java 电子相册源码包才算真正达到了“可分发”的状态。
本文还有配套的精品资源,点击获取