简介:这份《Java程序设计实验指导书》面向高校Java课程学习者与授课教师,针对上机实验环节目标不清、练习缺乏体系的问题,提供一套按课时编排的完整实验方案。资源为单个doc文档,压缩包约530KB,内容围绕68学时实验教学展开,涵盖Java基础、Java核心与企业级应用选学三大模块。文档按实验逐项组织,从运行环境安装配置、基本语法练习,到面向对象编程、包与接口及异常处理,再到数组、向量与字符串、常用系统类,并延伸至图形界面、流与文件、多线程、网络编程及JDBC、JSP与Servlet等选学内容,每个实验均给出实验目的、要求与具体内容,并附课时安排表。已有239人学习,适合课堂配套练习或课外自学对照使用,便于读者按模块循序渐进地动手编程、理解Java程序结构与设计方法。
1. 一份能直接照着上的 Java 实验课:68 学时、12 个实验的落地拆解
带过 Java 实验课的人都清楚,最耗精力的不是讲课,而是把“装环境、写第一个类、跑通编译”这条链路给几十个人同时铺平。这份《Java 程序设计实验指导书》就是干这个的:12 个实验、68 学时,从 JDK 安装配置一路排到 Socket 与 URLConnection,每个实验都按“实验目的—实验要求—实验内容”三段式写死,学生照着敲就能出结果。它适合两类人:一是要开课或带实训的教师,直接拿它当课时表和任务单;二是自学 Java 但总在环境配置和面向对象概念上卡住的初学者,把它当一条有验收点的练习路线走。下面按“资源怎么用—每个实验怎么落地—哪里会翻车”拆开讲。
2. 实验 1 到实验 3:环境、语法与面向对象的可复现路径
2.1 环境配置:把“能编译”当成第一个验收点
实验 1 的内容是 SDK 下载安装、设置环境变量、编写编译运行源程序,最后还要求装 JCreator 并浏览 SDK 自带的 Applet 示例。这里有个现实问题:指导书成文较早,Applet 在新版 JDK 里已经被移除,JCreator 也基本退出主流。我的处理方式是保留实验结构、替换工具链——用 JDK 17 或 21 加任意编辑器(VS Code、IDEA 社区版都行),Applet 部分改成用 Swing 的JFrame做一个等价的小窗口,教学目的(看到“能跑起来的图形程序”)不变。
环境变量这一步是新手翻车最集中的地方,Windows 下按下面这套配,配完立刻验证:
# 假设 JDK 解压在 D:\dev\jdk-21 # 系统变量新增 JAVA_HOME = D:\dev\jdk-21 # Path 里追加(注意是引用变量,不要写死路径) %JAVA_HOME%\bin # 验证三连,三条都出结果才算过 java -version javac -version echo %JAVA_HOME%JAVA_HOME指向 JDK 根目录而不是bin,这是最常见的错位;Path里用%JAVA_HOME%\bin而不是绝对路径,是为了以后换 JDK 版本只改一处。java -version看运行时、javac -version看编译器,两者版本号必须一致——只配了 JRE 没配 JDK 时,java能跑、javac报“不是内部或外部命令”,这个现象要能一眼认出来。Linux 或 macOS 下对应写export JAVA_HOME=...和export PATH=$JAVA_HOME/bin:$PATH,写进~/.bashrc或~/.zshrc后source一下。
第一个源程序建议就用指导书里的 HelloWorld 变体,但要求文件名与 public 类名严格一致:
// 文件名必须是 Hello.java,大小写敏感 public class Hello { public static void main(String[] args) { System.out.println("Java 环境就绪"); } }编译运行两步分开做:javac Hello.java生成Hello.class,java Hello执行(注意不带.class后缀)。分开做的好处是出错时能定位到“编译期”还是“运行期”:编译报错多半是语法或文件名问题,运行报错多半是类路径或主类名问题。这一步过了,实验 1 的验收点就算拿到。
2.2 基本语法:用“变量作用域”和“运算符优先级”做两个硬练习
实验 2 覆盖变量声明、作用域、关系与逻辑运算符、表达式与复合语句、选择语句、循环语句。这部分内容看着简单,但它是后面所有实验的地基,建议不要跳。指导书里“了解变量的使用范围”这一条特别值得做实——很多学生写循环时把变量声明在循环体内外搞混,编译报“找不到符号”却不知道原因。
给一个能直接当课堂练习的对照例子:
public class ScopeDemo { static int count = 0; // 类级变量,整个类可见 public static void main(String[] args) { for (int i = 0; i < 3; i++) { int local = i * 2; // 块级变量,只在 for 体内有效 count += local; } // System.out.println(local); // 取消注释会编译失败,local 已出作用域 System.out.println("count = " + count); // 输出 6 } }这段代码的教学价值在于那行被注释掉的语句:让学生亲手取消注释、看编译错误信息,比讲十遍“作用域”都管用。运算符部分则建议用&&与&、||与|的短路差异做对比练习,写一个带副作用的表达式(比如i++ > 0 && j++ > 0),打印i、j的最终值,短路与否一目了然。选择与循环语句用“打印九九乘法表”“判断闰年”这类小题目收尾即可,重点是把if-else if的匹配顺序和switch的break缺失讲透。
2.3 面向对象:从“创建对象”到“继承与多态”的三级台阶
实验 3 是整个指导书里分量最重的一个,8 学时,内容从创建对象、显示当前日期时间,到修饰符、参数传递、继承、多态,还带思考题。这条线必须一级一级上,跳步必翻车。
第一级是“对象是什么”。写一个Clock类,构造方法里初始化,方法里输出当前时间,主程序new出来调用:
import java.time.LocalDateTime; public class Clock { private String label; public Clock(String label) { // 构造方法,与类同名、无返回值 this.label = label; } public void show() { System.out.println(label + ": " + LocalDateTime.now()); } public static void main(String[] args) { Clock c = new Clock("系统时钟"); // 引用 c 指向堆上的对象 c.show(); } }这里要讲清new做了什么:在堆上分配对象、调用构造方法、返回引用。this.label = label里的this区分成员变量与参数,是新手第一个容易懵的点。
第二级是参数传递。Java 只有值传递,但“传引用类型的值”和“传基本类型的值”表现不同,用下面这个例子一次讲透:
public class PassDemo { static void changeInt(int x) { x = 100; } // 改的是副本 static void changeArray(int[] arr) { arr[0] = 100; } // 改的是同一块堆内存 public static void main(String[] args) { int a = 1; changeInt(a); System.out.println(a); // 仍是 1 int[] b = {1, 2, 3}; changeArray(b); System.out.println(b[0]); // 变成 100 } }结论要让学生自己跑出来:基本类型传的是值的拷贝,数组传的是引用的拷贝,但两个引用指向同一个对象,所以内容被改。这个结论直接决定后面多线程共享变量、集合传参时会不会出“玄学”问题。
第三级是继承与多态。定义一个父类Shape带area()方法,两个子类Circle、Rect各自重写,主程序用父类引用装子类对象、放进数组统一调用:
abstract class Shape { abstract double area(); } class Circle extends Shape { double r; Circle(double r) { this.r = r; } @Override double area() { return Math.PI * r * r; } } class Rect extends Shape { double w, h; Rect(double w, double h) { this.w = w; this.h = h; } @Override double area() { return w * h; } } public class PolyDemo { public static void main(String[] args) { Shape[] shapes = { new Circle(2), new Rect(3, 4) }; for (Shape s : shapes) { System.out.println(s.area()); // 运行时才决定调哪个实现 } } }@Override注解建议强制加上,它能在方法签名写错时直接编译报错,比运行时发现“怎么没走我的方法”省事得多。多态的本质是“编译看左边、运行看右边”,这句话配合上面的数组循环讲,学生基本一次能记住。
3. 实验 4 到实验 6:包、异常、集合与系统类的工程化用法
3.1 包与接口:目录结构必须和 package 声明对齐
实验 4 要求使用系统包、创建自定义包、使用接口、了解异常机制。自定义包这一步的翻车率极高,根因几乎都是“目录结构和package声明不一致”。正确做法是先建目录、再放源文件、最后编译时从包根目录的上一层执行:
# 目录结构 # src/ # com/demo/util/DateUtil.java # DateUtil.java 首行必须是 # package com.demo.util; # 在 src 的上一层执行编译 javac -d out src/com/demo/util/DateUtil.java # 运行(-d 指定的输出目录作为类路径根) java -cp out com.demo.util.DateUtil-d out让编译器按包名自动生成输出目录结构,-cp out告诉运行时去哪里找类。很多人编译过了、运行报NoClassDefFoundError,就是类路径没指对。接口部分用一个Payable接口加两个实现类做练习,重点讲“接口定义行为契约、实现类各自兑现”,为后面 JDBC 那类“面向接口编程”埋线。
异常处理要区分受检异常和运行时异常。给一个故意触发ArithmeticException和NumberFormatException的小程序,让学生分别用try-catch捕获、用finally做清理、用throws向上抛:
public class ExDemo { static int divide(String s) { try { return 10 / Integer.parseInt(s); // 可能抛两种异常 } catch (NumberFormatException e) { System.out.println("不是数字: " + s); return -1; } catch (ArithmeticException e) { System.out.println("除数为零"); return -1; } finally { System.out.println("finally 一定执行"); } } public static void main(String[] args) { divide("0"); divide("abc"); } }catch 的顺序有讲究:子类异常在前、父类在后,否则编译报“已被捕获”。finally里不要写return,会吞掉 try 块的返回值,这是血泪经验级别的坑。
3.2 数组、向量与字符串:选型比语法更重要
实验 5 讲数组、Vector、字符串。这里要补一个指导书没展开但实际必讲的点:Vector是同步的、性能一般,现代 Java 里更常用ArrayList;但既然实验指定了Vector,就借它讲清“动态数组扩容”和“同步开销”两个概念,再顺带对比ArrayList,学生以后选容器就有依据。
字符串部分重点在String不可变、StringBuilder可变这个差异。用一个循环拼接的对比实验:
public class StringDemo { public static void main(String[] args) { long t1 = System.currentTimeMillis(); String s = ""; for (int i = 0; i < 10000; i++) s += i; // 每次生成新对象 long t2 = System.currentTimeMillis(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) sb.append(i); // 原地追加 long t3 = System.currentTimeMillis(); System.out.println("String: " + (t2 - t1) + "ms"); System.out.println("StringBuilder: " + (t3 - t2) + "ms"); } }跑出来的时间差通常在一个数量级以上,这个数字比任何解释都有说服力。数组部分则把“越界异常”和“默认初始值”两个点做实:int[]默认全 0、String[]默认全null,访问arr[arr.length]必抛ArrayIndexOutOfBoundsException。
3.3 常用系统类:Math、日期与 Applet 生命周期的替代方案
实验 6 是 Math 类、日期类、Applet 生命周期。前两个直接可用,Math.random()、Math.round()、LocalDate/LocalDateTime的格式化都能当堂练。Applet 生命周期(init/start/stop/destroy)在新 JDK 里没有载体了,我的替代方案是用JFrame加窗口监听器模拟:windowOpened对应 start、windowClosing对应 stop,把“生命周期回调”这个概念保住,工具换成还活着的 Swing。
日期类练习给一个格式化输出的例子即可:
import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class DateDemo { public static void main(String[] args) { LocalDateTime now = LocalDateTime.now(); DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); System.out.println(now.format(fmt)); } }ofPattern里的模式字母大小写敏感:MM是月、mm是分、HH是 24 小时制、hh是 12 小时制,写错不报错但结果错,属于典型的“静默翻车”。
4. 实验 7 到实验 9:Swing 界面、图形多媒体与综合练习
4.1 图形用户界面:事件处理是分水岭
实验 7 从创建 GUI、事件处理机制,到独立窗口、匿名类、Swing 组件、自定义对话框与内部类,是整个指导书里代码量最大的一块。核心链路是“容器 + 组件 + 布局 + 监听器”,给一个最小可运行骨架:
import javax.swing.*; public class GuiDemo { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { // 界面构建放事件分发线程 JFrame f = new JFrame("实验 7 示例"); JButton btn = new JButton("点我"); JLabel label = new JLabel("等待点击", SwingConstants.CENTER); btn.addActionListener(e -> label.setText("已点击")); // lambda 即匿名类 f.setLayout(new java.awt.BorderLayout()); f.add(label, java.awt.BorderLayout.CENTER); f.add(btn, java.awt.BorderLayout.SOUTH); f.setSize(320, 200); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible(true); }); } }SwingUtilities.invokeLater这层包装不是可有可无的:Swing 组件不是线程安全的,所有界面操作都应在事件分发线程上做,否则偶发界面卡死或刷新异常,属于查不出原因的“黑匣子”问题。addActionListener用 lambda 写,等价于指导书里的匿名内部类,讲的时候把两种写法并排展示,学生就理解“匿名类只是接口实现的语法糖”。
4.2 图形与多媒体:Graphics 绘制与资源路径
实验 8 用Graphics画图、插入图像、播放音乐。绘制部分在JPanel里重写paintComponent:
import javax.swing.*; import java.awt.*; public class DrawDemo extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 必须先调父类,否则残影 g.setColor(Color.BLUE); g.fillRect(20, 20, 100, 60); g.drawString("Graphics 练习", 20, 110); } public static void main(String[] args) { JFrame f = new JFrame(); f.add(new DrawDemo()); f.setSize(300, 200); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible(true); } }super.paintComponent(g)漏写会导致重绘时旧图形残留,这是绘图实验里最高频的现象。图像与音频的加载要用类路径资源,路径写getClass().getResource("/res/pic.png"),/开头表示从类路径根找;直接写相对文件路径在 IDE 里能跑、打包后必挂,这个差异要提前讲。
4.3 综合练习:把四个小 Demo 串成一个可交付物
实验 9 的四个题目——幻灯机效果、滚动条改背景色、Applet 与 Application 合并、电闪雷鸣动画——本质是前面知识的组合。建议把“Applet 与 Application 合并”改成“Swing 单窗口内多面板切换”,用CardLayout实现,既保留“一个程序两种入口”的思路,又不依赖已废弃技术。幻灯机效果用Timer定时切换JLabel的图标,滚动条用JSlider的ChangeListener调背景色,电闪雷鸣用随机数控制背景色闪烁加Toolkit.getDefaultToolkit().beep()。这一章的价值在于让学生第一次独立把“界面 + 事件 + 定时 + 资源”拼起来,验收标准就定“能连续切换三张图且不卡顿”。
5. 实验 10 到实验 12:流与文件、线程、网络编程的边界
5.1 流与文件:装饰器模式与关闭顺序
实验 10 覆盖标准流、文件流、随机文件、数据流、对象流。Java IO 是装饰器模式的教科书案例,讲的时候把“节点流套缓冲流”这层关系画清楚:
import java.io.*; public class IoDemo { public static void main(String[] args) throws IOException { // 写:文件字节流 -> 缓冲 -> 数据流,逐层包装 try (DataOutputStream out = new DataOutputStream( new BufferedOutputStream(new FileOutputStream("data.bin")))) { out.writeInt(42); out.writeUTF("hello"); } // 读:包装顺序与写时一致 try (DataInputStream in = new DataInputStream( new BufferedInputStream(new FileInputStream("data.bin")))) { System.out.println(in.readInt()); System.out.println(in.readUTF()); } } }try-with-resources保证异常时也能关闭,关闭顺序是后包装的先关,所以写和读的包装层次必须对称,否则读出来的字节错位。对象流要求类实现Serializable,且建议显式声明serialVersionUID,不然类一改、旧文件就读不了,报InvalidClassException。
5.2 多线程:两种创建方式与共享变量的可见性
实验 11 要求分别用继承Thread和实现Runnable两种方式建线程。实现Runnable更灵活(还能继承别的类),实际项目里优先用它。给一个带共享计数器的例子,顺带把线程安全问题引出来:
public class ThreadDemo { static int count = 0; public static void main(String[] args) throws InterruptedException { Runnable task = () -> { for (int i = 0; i < 10000; i++) count++; // 非原子操作 }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); // 等两个线程都结束 System.out.println(count); // 大概率小于 20000 } }跑出来小于 20000 是正常现象,count++是“读—改—写”三步,线程交错就丢更新。解决方式用synchronized或AtomicInteger,这里点到为止,重点是让学生亲眼看到“多线程不加锁会错”,而不是背概念。join()的作用是主线程等待子线程结束,漏写会打印出中间值。
5.3 网络编程:Socket 与 URL 两条线的最小闭环
实验 12 从InetAddress、Socket、UDP,到 URL 与URLConnection读写。TCP 部分给一个单客户端单服务端的最小闭环,服务端先起、客户端后连:
// 服务端:接收一行并回显 import java.io.*; import java.net.*; public class EchoServer { public static void main(String[] args) throws IOException { try (ServerSocket server = new ServerSocket(9000); Socket socket = server.accept(); BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) { String line = in.readLine(); out.println("echo: " + line); } } }客户端用new Socket("127.0.0.1", 9000)连接,写一行、读一行。端口被占用报BindException,先查是不是上次的服务端没退;连接被拒报ConnectException,先查服务端是否已启动、端口是否一致。UDP 部分用DatagramSocket和DatagramPacket发一个字符串即可,重点讲“无连接、不保证到达”。URL 部分用URLConnection读一个公开页面的前若干字节,注意设置连接超时和读取超时,否则网络不通时会长时间挂起。
6. 把 12 个实验压成一张验收表:我的排错顺序与几个硬习惯
带过几轮之后,我把这份指导书的落地经验收敛成一张“先查什么”的顺序表,出问题时按行往下走,比东一榔头西一棒子快得多:
| 现象 | 优先排查 | 常见根因 |
|---|---|---|
javac不是内部命令 | JAVA_HOME与Path | 只装了 JRE 或路径指向bin的上一层写错 |
NoClassDefFoundError | 类路径与包目录 | package声明与目录结构不一致 |
| 界面重绘残影 | paintComponent | 漏调super.paintComponent(g) |
| 读文件字节错位 | 流的包装顺序 | 写读两层包装不对称 |
| 多线程计数偏小 | 共享变量原子性 | count++未加锁 |
BindException | 端口占用 | 上一个服务端进程未退出 |
几个我强制自己每次都走的习惯:第一,每个实验先跑“最小可运行版本”,再往里加功能,绝不一次性写完再调;第二,所有涉及外部资源的路径统一用类路径写法,本地能跑不算数,打包后能跑才算;第三,多线程和 IO 的示例一律加超时和try-with-resources,把“资源泄漏”这类后悔药提前吃上;第四,遇到已废弃的 API(Applet、JCreator),保留教学意图、替换实现载体,而不是硬啃过时工具。这份指导书的价值不在技术有多新,而在于它把 68 学时的练习切成了有明确验收点的小台阶——照着台阶走,环境、语法、面向对象、界面、IO、线程、网络这条主线就串起来了。希望帮到你。
本文还有配套的精品资源,点击获取