Fun-with-Java:基于Java的趣味项目实战合集
简介:“Fun-with-Java”是一个面向实践的Java个人项目合集,旨在通过构建有趣且功能丰富的应用,帮助开发者深入掌握Java核心编程技能。项目涵盖GUI开发、网络通信、游戏设计、数据处理、数据库操作与单元测试等多个方面,充分展现Java在多领域应用中的强大能力。通过Swing、JavaFX、JDBC、IO流、多线程等技术的实际运用,学习者可在动手实践中强化语法基础、面向对象思想及事件驱动编程逻辑,适合各阶段开发者提升综合开发能力。
Java编程核心思想与实战进阶:从基础到系统级开发的完整路径
在当今这个万物互联的时代,我们每天都在和各种智能设备打交道。无论是手机上的小游戏、桌面端的小工具,还是企业级后台管理系统——它们背后都离不开一个共同的语言:Java 🎯。这门诞生于1995年的语言,不仅没有被时间淘汰,反而在云原生、微服务、大数据等领域焕发新生。
但你有没有想过,为什么有些人学了几年Java,写出来的代码依然“像一锅乱炖”?而另一些人却能轻松构建出结构清晰、扩展性强的项目?
关键不在于会不会用 for 循环或 if-else ,而在于是否真正理解了 编程的本质是思维的表达 。今天我们就来一场“去套路化”的深度之旅,带你从最基础的语法出发,一步步搭建属于自己的工程化思维体系。准备好了吗?🚀
封装不是为了藏起来,而是为了让别人更方便地使用
说到面向对象三大特性——封装、继承、多态,很多人第一反应就是背定义:“封装就是把字段设成private……”停!这就像说“开车就是踩油门”一样片面 😅。
真正的封装,是一种 责任划分的艺术 。
想象一下你在设计一个银行账户类:
public class BankAccount {
private double balance;
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
} else {
throw new IllegalArgumentException("存款金额必须大于0");
}
}
public boolean withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("取款金额必须大于0");
}
if (amount > balance) return false;
balance -= amount;
return true;
}
public double getBalance() {
return balance; // 只读,不能直接修改
}
}
你看,这里的关键不是 private 关键字本身,而是通过方法暴露行为契约。用户不需要知道余额是怎么存的(可能未来会加审计日志、风控校验),他只需要知道:“调 deposit() 就能存钱”。
这才是封装的精髓: 隐藏复杂性,暴露简单接口 。
💡 工程经验:永远不要提供
setBalance()这种“万能开关”。一旦开了后门,后续所有逻辑都可能崩塌。
继承要慎用,别让“儿子”背不动“老子”的债
再来看继承。很多初学者喜欢动不动就 extends ,结果搞出个“祖父-父亲-儿子-孙子”的四层继承链,最后谁改一行代码都要提心吊胆……
// 千万别这么干!
class Animal { ... }
class Mammal extends Animal { ... }
class Dog extends Mammal { ... }
class PoliceDog extends Dog { ... }
问题在哪? 继承代表的是‘是什么’的关系,而不是‘有什么功能’的关系 。更好的做法是组合 + 接口:
interface Barkable {
void bark();
}
interface Trainable {
void followCommand(String cmd);
}
class Dog implements Barkable, Trainable {
@Override
public void bark() { System.out.println("汪!"); }
@Override
public void followCommand(String cmd) { /* 训练逻辑 */ }
}
这样PoliceDog就可以灵活选择能力组合,而不必强制继承一堆无关属性。这也是《Effective Java》里反复强调的原则: 优先使用组合而非继承 。
多态才是Java的灵魂,它让程序有了“呼吸感”
终于轮到多态出场了。如果你只把它理解为“父类引用指向子类对象”,那还停留在表面。
真正的多态,是让你的程序具备 运行时决策的能力 。比如下面这个例子:
public abstract class Animal {
public abstract void makeSound(); // 抽象行为契约
}
class Cat extends Animal {
@Override
public void makeSound() { System.out.println("喵~"); }
}
class Duck extends Animal {
@Override
public void makeSound() { System.out.println("嘎嘎!"); }
}
// 调用处完全不知道具体类型
public static void playWithPet(Animal pet) {
pet.makeSound(); // 运行时才知道叫什么
}
这段代码的魅力在于: 新增动物不需要修改任何已有逻辑 。你要加个鹦鹉?写个 Parrot 类就行, playWithPet() 照常工作。
这正是开闭原则(Open-Closed Principle)的体现:对扩展开放,对修改关闭 ✅。
控制结构不只是语法糖,它是程序的“心跳节拍器”
我们常说控制结构是程序的骨架,但我更愿意把它比作 音乐的节奏控制器 。 if-else 像是强弱拍切换, for/while 则是持续的律动,少了哪一个都会让整首曲子走调。
游戏开发中的状态机:让逻辑不再“跳来跳去”
假设你要做一个贪吃蛇游戏,里面有这几个状态:
- 等待开始(WAITING)
- 正在玩(PLAYING)
- 暂停(PAUSED)
- 游戏结束(GAME_OVER)
不用状态机的话,你的代码可能会变成这样:
if (!gameStarted) {
showStartScreen();
} else if (gameStarted && !paused && !gameOver) {
updateSnake();
} else if (paused) {
showPauseOverlay();
} // ……还有十几个判断?
天呐,这简直是维护噩梦!😱
正确的姿势是用枚举+switch打造一个 有限状态机(FSM) :
public enum GameState {
WAITING, PLAYING, PAUSED, GAME_OVER
}
private GameState currentState = GameState.WAITING;
public void gameLoop() {
while (!exitRequested) {
switch (currentState) {
case WAITING:
renderStartScreen();
if (startKeyPressed) {
currentState = GameState.PLAYING;
resetGame();
}
break;
case PLAYING:
updateSnakePosition();
checkCollision();
renderGame();
if (pauseKeyPressed) {
currentState = GameState.PAUSED;
} else if (isGameOver()) {
currentState = GameState.GAME_OVER;
}
break;
case PAUSED:
renderPauseOverlay();
if (resumeKeyPressed) {
currentState = GameState.PLAYING;
}
break;
case GAME_OVER:
renderGameOverScreen();
if (restartKeyPressed) {
currentState = GameState.WAITING;
}
break;
}
sleepFrameDelay(); // 控制帧率
}
}
| 行号 | 代码片段 | 实际意义 |
|---|---|---|
| 1-3 | while (!exitRequested) |
主循环容器,整个游戏的生命线 |
| 4 | switch (currentState) |
根据当前状态进入不同逻辑分支 |
| 6-10 | case WAITING: |
显示启动页,等待玩家按下开始键 |
| 11-18 | case PLAYING: |
核心玩法逻辑:移动、碰撞检测、渲染 |
| 19-23 | case PAUSED: |
暂停状态下只响应恢复操作 |
| 24-28 | case GAME_OVER: |
展示结果并允许重试 |
| 30 | sleepFrameDelay() |
防止CPU空转,控制每秒刷新次数 |
这套设计最大的好处是 高内聚低耦合 。每个状态的逻辑独立封闭,调试时一眼就能定位问题。而且扩展也方便,想加个“关卡选择”状态?直接新增枚举值+处理逻辑即可。
状态流转图谱 🔄
stateDiagram-v2
[*] --> WAITING
WAITING --> PLAYING : 用户按开始键
PLAYING --> PAUSED : 按下P键
PAUSED --> PLAYING : 按下R键
PLAYING --> GAME_OVER : 蛇撞墙或自咬
GAME_OVER --> WAITING : 用户点击重试
PLAYING --> WAITING : 强制退出
这张图不只是文档,更是团队沟通的语言。新人一看就知道整个流程怎么走,哪条路通哪里。
不同实现方式对比表
| 特性 | 使用 switch 实现 | 使用策略模式+Map映射 |
|---|---|---|
| 可读性 | ⭐⭐⭐⭐☆(直观易懂) | ⭐⭐⭐☆☆(需理解函数式) |
| 扩展性 | ⭐⭐☆☆☆(要改源码) | ⭐⭐⭐⭐⭐(动态注册) |
| 性能 | ⭐⭐⭐⭐⭐(编译期优化) | ⭐⭐⭐☆☆(方法引用开销) |
| 维护成本 | ⭐⭐⭐☆☆(适合小项目) | ⭐⭐☆☆☆(适合大系统) |
| 学习门槛 | ⭐☆☆☆☆(新手友好) | ⭐⭐⭐☆☆(需要经验) |
所以结论很明确:教学项目选 switch ,工业级系统考虑策略模式。
不过等等!咱们还可以玩点更酷的——把状态行为做成可配置的:
@FunctionalInterface
interface StateBehavior {
void execute();
}
Map<GameState, StateBehavior> stateActions = new HashMap<>();
// 初始化映射关系
stateActions.put(GameState.WAITING, this::handleWaiting);
stateActions.put(GameState.PLAYING, this::handlePlaying);
stateActions.put(GameState.PAUSED, this::handlePaused);
stateActions.put(GameState.GAME_OVER, this::handleGameOver);
// 主循环简化为一行
stateActions.get(currentState).execute();
这种方式已经有点Spring Bean管理的味道了,是不是感觉离高级架构又近了一步?😉
循环不只是重复执行,它是动画和数据流的发动机 💥
再来说说循环。你以为 for 和 while 只是重复几遍代码?错!它们是驱动视觉变化和批量处理的核心引擎。
动画背后的秘密:固定时间步长主循环
几乎所有图形应用都有一个“主循环”,也叫 game loop 。它的任务是不断更新画面,形成连续动作的错觉。
long lastTime = System.nanoTime();
double nsPerTick = 1_000_000_000.0 / 60; // 目标60FPS
boolean running = true;
while (running) {
long now = System.nanoTime();
double delta = (now - lastTime) / nsPerTick;
if (delta >= 1) {
processInput(); // 获取键盘鼠标输入
updateGameLogic(); // 更新角色位置、碰撞等
renderGraphics(); // 绘制当前帧
lastTime += (long) nsPerTick;
}
try {
Thread.sleep(1); // 减少CPU占用
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
running = false;
}
}
参数说明👇:
| 参数 | 类型 | 含义 |
|---|---|---|
lastTime |
long |
上一次更新的时间戳(纳秒) |
nsPerTick |
double |
每帧应占时间(~16.67ms) |
delta |
double |
实际耗时与标准帧长的比例 |
processInput() |
方法 | 处理用户输入事件 |
updateGameLogic() |
方法 | 核心逻辑计算 |
renderGraphics() |
方法 | 图形渲染 |
重点来了!这个循环采用了 固定时间步长 的设计。哪怕渲染慢了一点,逻辑更新仍保持稳定频率。否则会出现“帧率低时角色走得慢”的诡异现象。
数据处理哪家强?四种循环横评
当你要处理十万条用户数据时,该怎么遍历?来看看性能实测对比(平均值):
| 循环类型 | 平均耗时(ms) | 内存占用 | 安全性 | 适用场景 |
|---|---|---|---|---|
增强 for ( for-each ) |
45 | 中 | ⭐⭐⭐⭐☆ | 一般遍历 |
| 普通 for(index) | 42 | 中 | ⭐⭐⭐☆☆ | 需索引参与计算 |
| Iterator 显式遍历 | 48 | 中 | ⭐⭐⭐⭐☆(支持remove) | 边遍历边删元素 |
| Stream.forEach() | 68 | 高 | ⭐⭐⭐⭐⭐(函数式) | 链式操作 |
虽然 Stream 语法最优雅,但在纯遍历场景下性能确实不如传统循环。所以在高频动画或大数据批处理中,建议优先用 for 或 while 。
主循环控制流程图 🌀
graph TD
A[开始主循环] --> B{是否退出?}
B -- 否 --> C[获取当前时间]
C --> D[计算时间差Δt]
D --> E{Δt ≥ 1帧?}
E -- 是 --> F[处理输入]
F --> G[更新逻辑]
G --> H[渲染画面]
H --> I[更新基准时间]
I --> J[短暂休眠1ms]
J --> B
E -- 否 --> J
B -- 是 --> K[退出循环]
这张图揭示了一个重要理念: 时间是程序的第一资源 。合理的调度机制能让用户体验丝滑流畅。
GUI开发:从命令行走向可视化世界的桥梁 🌉
掌握了控制结构之后,下一步就是让程序“看得见摸得着”。Swing 和 JavaFX 就是你通往图形界面的大门。
Swing:老牌战士的逆袭之路
别看Swing现在常被调侃“丑”,它可是无数企业级应用的基石。更重要的是,它教会我们一件事: 事件驱动模型才是现代UI的灵魂 。
来看一个经典案例——计算器:
public class SimpleCalculator extends JFrame {
private JTextField display;
private double result = 0;
private String lastCommand = "=";
private boolean start = true;
public SimpleCalculator() {
setTitle("Swing 计算器");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout());
// 显示屏
display = new JTextField("0");
display.setEditable(false);
display.setHorizontalAlignment(JTextField.RIGHT);
add(display, BorderLayout.NORTH);
// 按钮面板
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new GridLayout(4, 4));
String[] buttons = {
"7", "8", "9", "/",
"4", "5", "6", "*",
"1", "2", "3", "-",
"0", ".", "=", "+"
};
for (String text : buttons) {
JButton button = new JButton(text);
button.addActionListener(new ButtonListener());
buttonPanel.add(button);
}
add(buttonPanel, BorderLayout.CENTER);
pack();
setLocationRelativeTo(null);
}
private class ButtonListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
String input = e.getActionCommand();
if ("0123456789.".contains(input)) {
if (start) {
display.setText("");
start = false;
}
display.setText(display.getText() + input);
} else {
calculate(Double.parseDouble(display.getText()));
lastCommand = input;
start = true;
}
}
private void calculate(double x) {
switch (lastCommand) {
case "+" -> result += x;
case "-" -> result -= x;
case "*" -> result *= x;
case "/" -> result /= x;
case "=" -> result = x;
}
display.setText(String.valueOf(result));
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
new SimpleCalculator().setVisible(true);
});
}
}
几个关键知识点划重点 🔍:
- 组件组织 :
JFrame为主窗口,JTextField显示结果,JButton组成按键阵列。 - 布局管理 :
BorderLayout将屏幕分为上下左右中五个区域,GridLayout自动排布按钮。 - 事件绑定 :每个按钮注册
ActionListener,点击时触发actionPerformed()。 - 线程安全 :GUI操作必须在 事件调度线程(EDT) 中执行,所以要用
SwingUtilities.invokeLater()包裹。
| 组件 | 用途 | 关键方法 |
|---|---|---|
JFrame |
主窗口 | setTitle() , setSize() |
JButton |
按钮 | addActionListener() |
JTextField |
文本框 | getText() , setText() |
JTextArea |
多行文本 | setLineWrap() |
JFileChooser |
文件选择 | showOpenDialog() |
控制流全景图 🗺️
graph TD
A[启动程序] --> B[创建JFrame窗口]
B --> C[添加JTextField作为显示屏]
C --> D[创建JPanel并设置GridLayout]
D --> E[循环添加JButton至面板]
E --> F[为每个按钮绑定ActionListener]
F --> G[用户点击按钮触发actionPerformed]
G --> H{判断输入类型}
H -->|数字| I[追加到显示屏]
H -->|运算符| J[执行计算并更新结果]
J --> K[刷新显示]
这就是典型的“观察者模式”:按钮是被观察者,监听器是观察者,点击动作就是事件通知。
顺便提一句,那个记事本文件打开功能也很有意思:
JMenuItem openItem = new JMenuItem("打开");
openItem.addActionListener(e -> {
JFileChooser fc = new JFileChooser();
int result = fc.showOpenDialog(this);
if (result == JFileChooser.APPROVE_OPTION) {
try (BufferedReader br = new BufferedReader(
new FileReader(fc.getSelectedFile()))) {
StringBuilder content = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
content.append(line).append("\n");
}
textArea.setText(content.toString());
} catch (IOException ex) {
JOptionPane.showMessageDialog(this, "读取失败: " + ex.getMessage());
}
}
});
这里用了 try-with-resources 确保文件流自动关闭,异常捕获防止崩溃,再加上弹窗提示,用户体验直接拉满!
JavaFX:现代化UI的新纪元 🌟
如果说Swing是实用主义者,那JavaFX就是美学派代表。它最大的优势是支持 FXML + CSS 分离设计,真正做到“前后端分离”在桌面端的落地。
声明式UI:用XML定义界面
<!-- view.fxml -->
<VBox xmlns="http://javafx.com/javafx" spacing="10" alignment="CENTER">
<children>
<Label text="欢迎使用JavaFX!" styleClass="title"/>
<Button text="开始" onAction="#handleStart" styleClass="primary-btn"/>
<Button text="退出" onAction="#handleExit" styleClass="danger-btn"/>
</children>
</VBox>
CSS美化:媲美网页的视觉体验
/* styles.css */
.title {
-fx-font-size: 24px;
-fx-font-weight: bold;
-fx-text-fill: #2c3e50;
}
.primary-btn {
-fx-background-color: #3498db;
-fx-text-fill: white;
-fx-font-size: 16px;
-fx-padding: 10px 20px;
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0, 0, 2);
}
.danger-btn {
-fx-background-color: #e74c3c;
-fx-text-fill: white;
-fx-font-size: 16px;
-fx-padding: 10px 20px;
}
主程序加载:松耦合的典范
public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/view.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root, 300, 200);
scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());
stage.setTitle("JavaFX 样式化界面");
stage.setScene(scene);
stage.show();
}
public void handleStart(ActionEvent event) {
System.out.println("开始按钮被点击");
}
public void handleExit(ActionEvent event) {
Platform.exit();
}
public static void main(String[] args) {
launch(args);
}
}
亮点在哪?
- 视图与逻辑分离 :FXML负责结构,CSS负责样式,Java负责行为。
- 事件自动绑定 :
onAction="#handleStart"会自动查找同名方法。 - 主题可更换 :换一套CSS就能彻底改变应用外观。
这种MVVM风格特别适合团队协作——设计师改UI不影响程序员写逻辑。
实时搜索:事件监听的高级玩法
最后来个压轴技巧: 键盘监听实现即时过滤
searchField.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
String query = searchField.getText().toLowerCase();
listModel.clear();
for (String item : allItems) {
if (item.toLowerCase().contains(query)) {
listModel.addElement(item);
}
}
}
});
这里用了 KeyAdapter 简化编码,只覆盖需要的方法。每次松开按键就重新过滤列表,形成“打字即搜”的流畅体验。
⚠️ 注意:频繁操作可能导致性能问题。真实项目中建议加入防抖(debounce),比如延迟300ms再执行查询。
反射机制:让程序拥有“自我意识”的超能力 🤖
到了这里,你已经掌握了大多数开发者的技术栈。但要想成为高手,还得学会一项黑科技——反射。
插件系统:运行时动态加载类
想象一下,你的程序像个乐高积木平台,用户可以随时添加新功能模块。怎么做?靠的就是 Class.forName() :
public interface Plugin {
void execute();
}
public class GreetingPlugin implements Plugin {
@Override
public void execute() {
System.out.println("Hello from GreetingPlugin!");
}
}
public class PluginManager {
public static void loadPlugin(String className) {
try {
Class<?> clazz = Class.forName(className);
if (Plugin.class.isAssignableFrom(clazz)) {
Plugin plugin = (Plugin) clazz.getDeclaredConstructor().newInstance();
plugin.execute();
}
} ender
参数详解👇:
| 参数 | 说明 |
|---|---|
className |
完全限定类名(如 com.example.MyPlugin ) |
Class.forName() |
触发类加载机制 |
isAssignableFrom() |
检查类型兼容性 |
getDeclaredConstructor().newInstance() |
创建实例(推荐替代已废弃的 newInst ance() ) |
这个机制广泛应用于:
- IDE插件系统(如IntelliJ)
- 游戏技能模块热加载
- 自动化测试框架扫描测试类
注解驱动事件总线:告别手动注册
更进一步,我们可以用反射+注解实现自动化事件绑定:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EventHandler {
String value(); // 事件名称
}
public class EventHandlers {
@EventHandler("user.login")
public void onUserLogin(String username) {
System.out.println("User logged in: " + username);
}
}
public class EventDispatcher {
private final Map<String, List<Method>> handlerMap = new HashMap<>();
public void register(Object handler) {
Class<?> clazz = handler.getClass();
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(EventHandler.class)) {
String eventName = method.getAnnotation(EventHandler.class).value();
handlerMap.computeIfAbsent(eventName, k -> new ArrayList<>()).add(method);
method.setAccessible(true);
System.out.println("Registered: " + method.getName() + " -> " + eventName);
}
}
}
public void fireEvent(String eventName, Object... args) {
List<Method> handlers = handlerMap.get(eventName);
if (handlers != null) {
for (Method m : handlers) {
try {
m.invoke(null, args);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
执行流程👇:
graph TD
A[启动EventDispatcher] --> B[调用register(handler)]
B --> C{遍历所有方法}
C --> D[检查是否有@EventHandler]
D -- 是 --> E[获取事件名]
E --> F[存入handlerMap]
D -- 否 --> G[跳过]
F --> H[完成注册]
H --> I[fireEvent(event, args)]
I --> J{查找对应方法列表}
J --> K[逐个invoke执行]
这种方式极大提升了代码的声明性和可维护性,Spring框架里的 @EventListener 就是这么干的!
反射使用场景对比表
| 场景 | 是否推荐 | 替代方案 |
|---|---|---|
| 插件系统 | ✅ 强烈推荐 | ServiceLoader |
| ORM映射 | ✅ 广泛使用 | 编译期APT生成代码 |
| 单元测试 | ✅ 必需 | - |
| 核心算法 | ❌ 不推荐 | 直接调用 |
| 高频处理 | ❌ 避免 | 缓存Method对象 |
💡 性能优化 tip:频繁使用的
Method对象一定要缓存,否则反射查找会拖慢速度。
写在最后:编程不是写代码,而是构建思维模型 🧠
看到这里,你应该发现了一个规律: 所有高级技术的本质,都是在解决“变化”带来的复杂性 。
- 封装应对需求变更
- 多态应对类型扩展
- 状态机应对流程分支
- 反射应对运行时不确定性
真正的高手,不是记住多少API,而是建立起一套 抗变能力强的思维架构 。
下次当你面对一个新项目时,不妨问问自己:
- 哪些部分一定会变?→ 用接口隔离
- 哪些逻辑会有多种状态?→ 上状态机
- 是否需要后期扩展?→ 考虑插件化
- 用户交互是否复杂?→ 引入事件总线
记住,代码终会腐烂,唯有设计永恒 💎。
所以,别再问“什么时候能学会Java”了。从现在开始,动手做一个属于自己的“Fun-with-Java”项目吧!哪怕只是一个带按钮的窗口,也是你迈向工程师之路的第一步 ✨。
Keep coding, keep growing! 🚀
简介:“Fun-with-Java”是一个面向实践的Java个人项目合集,旨在通过构建有趣且功能丰富的应用,帮助开发者深入掌握Java核心编程技能。项目涵盖GUI开发、网络通信、游戏设计、数据处理、数据库操作与单元测试等多个方面,充分展现Java在多领域应用中的强大能力。通过Swing、JavaFX、JDBC、IO流、多线程等技术的实际运用,学习者可在动手实践中强化语法基础、面向对象思想及事件驱动编程逻辑,适合各阶段开发者提升综合开发能力。
更多推荐




所有评论(0)