Java实验手册:从环境配置到网络编程的实战拆解

发布时间:2026/10/24 11:46:17
Java实验手册:从环境配置到网络编程的实战拆解

简介:这是一份Java程序设计上机实验指导书,主要面向高校计算机专业学生及Java自学者,用于解决上机实验目标不明确、练习不成体系的问题。全书将68学时实验按Java基础、核心、企业级应用选学三大模块组织,依次覆盖JDK安装配置、基本语法、面向对象、包接口与异常处理、数组向量与字符串、常用系统类等知识点,并延伸至图形用户界面、多线程、综合项目等进阶内容,层次清晰、循序渐进。资源包仅含1个doc文档,大小530KB,内容完整,便于打印或按章节查阅。指导书中每项实验均列明目的、要求与具体操作步骤,并提供可运行的程序示例和扩充练习方向,读者既能对照完成课内任务,也可据此开展课外自练,有效提升Java编程动手能力。该资源已有238人学习浏览,对刚接触Java或需要系统梳理实验流程的学习者具有较高参考价值。

1. 一本能照着敲完的 Java 实验手册:从 SDK 到网络编程

把 Java 实验课算成一门技能训练,最常见的结果不是语法难,而是卡在第一天的环境上——某次上机课,一组人对着javac 不是内部命令的报错面面相觑,最后发现谁的 PATH 都没配。真正能让新手从头走到尾的实验指导书,不是把知识点列一遍,而是把每一步操作的边界条件讲清楚:SDK 装在哪、环境变量怎么配、类和文件名为什么必须一致、继承和多态在代码里长什么样。这份指导书恰好就是这样的底稿,68 学时的安排覆盖 Java 基础、Java 核心和企业级应用选学,从环境配置一路做到网络编程,适合刚开 Java 课的学生,也适合带实验课的老师拿来直接改作业任务。接下来我会按实验顺序,把其中最容易踩坑、也最值得动手复现的部分展开。

2. 环境与语法:把 SDK 配置和第一个程序一次跑通

2.1 环境变量的作用边界:PATH 与 JAVAHOME 到底该配哪个

实验 1 的前半段都在跟 SDK 和环境变量打交道。很多同学在这里犯的第一个错误,是把所有环境变量都当成必配项,其实PATH才是决定javac、java能不能用的关键。操作系统执行命令时,会在PATH列出的目录里挨个找可执行文件,所以只要 SDK 安装目录下的bin路径在这个列表里,命令行就能直接调用编译器和虚拟机。

# 临时配置,仅对当前终端窗口生效,适合先验证 SDK 是否可用 export JAVA_HOME=/usr/lib/jvm/jdk-17 export PATH=$JAVA_HOME/bin:$PATH # 验证配置是否生效 java -version javac -version

这段命令的作用是把JAVA_HOME指向 JDK 安装根目录,再把它的bin子目录加到PATH最前面。JAVA_HOME本身不参与命令查找,但它会被很多构建工具和服务器读取,所以建议一起配。验证时java -version能输出版本号就说明虚拟机可用,javac -version则确认编译器就位,两个都成功再继续写代码。

注意:JAVA_HOME的路径里不要带bin这一层,它应该指向 JDK 根目录,而不是bin目录本身。写错层级会导致部分工具解析不到lib等子目录。

2.2 第一个源程序:类名与文件名的硬性约定

配好环境后,实验 1 要求手写、编译并运行一个最简程序。这里真正要理解的不是System.out.println这个 API,而是 Java 编译单元的命名规则:public类的类名必须与文件名完全一致,大小写都不能差。

public class HelloJava { public static void main(String[] args) { System.out.println("Hello, Java Experiment!"); } }

保存时文件名必须是HelloJava.java,然后依次执行javac HelloJava.java和java HelloJava。javac负责把源码编译成字节码,生成HelloJava.class文件;java则启动虚拟机,加载并执行这个类。如果文件名写成了hello.java,javac会直接报“类 HelloJava 是公共的,应在名为 HelloJava.java 的文件中声明”的错误,这是新手第一个高频报错。

2.3 基本语法练习:选择与循环的优先级

实验 2 覆盖变量声明、作用域、关系运算符、选择语句和循环语句。这块内容如果只是看书上的示例会觉得都会,但上机时最容易暴露问题的是变量作用域——在大括号内声明的变量,出了大括号立刻消失。我在带实验时习惯让大家把三种结构揉进一个程序里写,比如用循环生成数据、用选择语句做判断,这样能同时检验语法和对作用域的理解。

public class ControlFlowDemo { public static void main(String[] args) { int sum = 0; for (int i = 1; i <= 10; i++) { if (i % 2 == 0) { sum += i; } } System.out.println("1到10之间偶数和为: " + sum); int month = 3; switch (month) { case 1: case 3: case 5: System.out.println(month + "月有31天"); break; default: System.out.println("其他月份天数待查"); break; } } }

这段代码里i是for循环内声明的变量,循环结束后就不能再访问;sum在方法体级别声明,循环内修改它没有问题。switch里多个case合并写是省代码的常见做法,但每个分支末尾必须有break,否则会穿透到下个分支。实验时如果输出和预期不符,先检查是不是漏了break,再检查变量声明的层级。

3. 面向对象三件套:类、继承、接口和异常处理

3.1 创建对象:引用类型与堆内存的关系

实验 3 从创建Applet类开始,逐步过渡到普通类的对象创建。其实不管是不是Applet,对象创建的机制都一样:new关键字在堆上分配内存,返回的是指向这块内存的引用。很多同学把Student s = new Student()里的s当成对象本身,调试时就会发现,当方法间传递这个引用并修改属性时,原始对象也被改了——因为传的是地址,不是副本。

class Student { String name; int age; Student(String name, int age) { this.name = name; this.age = age; } void printInfo() { System.out.println("姓名: " + name + ", 年龄: " + age); } } public class ObjectDemo { public static void main(String[] args) { Student s1 = new Student("A同学", 20); Student s2 = s1; s2.age = 21; s1.printInfo(); } }

这里如果直接猜输出,很多人会认为s1.age还是 20,实际是 21,因为s2和s1指向同一个堆内存地址。实验中最直观的验证方式是在赋值前后分别打印s1和s2,能看到地址一样。这一小节要解决的认知问题就是:对象变量存的是引用,赋值操作复制的是引用地址,而不是对象内容。

3.2 继承与多态:父类引用调用哪个方法

实验 3 的后半部分练习继承性和多态性。继承讲的是子类获得父类的成员,多态讲的是同一方法在不同子类中有不同表现。多态有两个前提:必须有继承关系,子类必须重写父类方法。运行时根据实际对象类型决定调用哪个方法版本,这个机制常被叫“动态绑定”。

class Animal { void speak() { System.out.println("动物发出叫声"); } } class Dog extends Animal { void speak() { System.out.println("狗叫: 汪汪"); } } public class PolymorphismDemo { public static void main(String[] args) { Animal a = new Dog(); a.speak(); } }

声明类型是Animal,实际对象是Dog,调用speak()输出来自Dog类。实验如果把这个赋值反过来写,也就是Dog d = new Animal(),编译阶段就会报类型不兼容,因为父类对象不能安全地当子类用。做题时注意区分“编译看左、运行看右”这句话,搭配上面这个例子能记很牢。

3.3 包、接口与异常:三个概念的组合用法

实验 4 把包、接口和异常放在一起练,原因是它们在工程中经常协同出现。接口定义行为规范,类通过implements实现接口;自定义包则用来组织这些类。这里最常见的错误是接口方法漏写public修饰符,导致“试图分配更弱的访问权限”的编译错误。

interface Drawable { void draw(); } class Circle implements Drawable { public void draw() { System.out.println("画一个圆形"); } } public class InterfaceDemo { public static void main(String[] args) { Drawable d = new Circle(); d.draw(); } }

接口里的方法默认是public abstract,实现类必须用public重写,这一点漏了就翻车。自定义包的做法则是在源文件第一行写package mypackage;,编译时用javac -d . ClassName.java让编译器自动生成对应目录结构。异常处理是这个阶段的另一个重点,try块里写可能出错的代码,catch捕获具体异常类型,finally做资源清理,三个块的位置和顺序不要记错。

4. 核心 API 实战:数组、向量、字符串和常用系统类

4.1 数组与向量:定长与自动扩容的取舍

实验 5 把数组、向量和字符串放在一组,正好对应了日常开发里最常打交道的三种数据结构。数组是固定长度的,一旦创建就不能扩容,优点是访问快、语法简单;Vector是早期集合类,容量不足自动增长,适合数量不确定的场景。

数组声明初始化、循环遍历填充、计算最大最小值的代码,能在java.util.Arrays类的sort方法里得到简化。

特性数组Vector
长度固定自动扩容
支持基本类型支持仅支持对象
线程安全不涉及方法同步
遍历风格for / 增强 forfor / 迭代器

实验时建议把数组和Vector各写一遍同样的业务逻辑,感受代码差异:数组用下标访问,Vector用add()添加、get()读取。Vector是 Java 1.0 留下的老集合,实际新代码里更常见的是ArrayList,两者 API 几乎一致,但指导书既然安排Vector,重点在于理解“自动扩容”这个思想,而不是死记这个类。

4.2 字符串:不可变与拼接的性能陷阱

字符串部分看起来简单,实际暗藏一个性能问题:String对象是不可变的,对字符串做拼接时,每拼一次就会生成新的对象。循环里大量拼接会产生大量中间垃圾对象,界面卡顿、内存占用高都从这里来。实验中我会让大家做一次对比,分别用String和StringBuilder循环拼接一万次,对比耗时。

StringBuilder sb = new StringBuilder(); for (int i = 1; i <= 10000; i++) { sb.append(i).append(", "); } String result = sb.toString(); System.out.println(result.substring(0, 100));

StringBuilder是可变字符序列,append在内部缓冲上直接追加,不会创建新对象;toString()在需要最终结果时才生成字符串。日常处理字符串时还有个高频操作是格式化和切割,比如String.format("%04d", 12)输出0012,split(";")按分号切割得到数组。这些方法不需要背,但要知道去哪里查文档,java.lang.String的方法列表本身就是一份很好的参考手册。

4.3 数学与日期:系统类的使用套路

实验 6 练习Math类、日期类以及Applet生命周期。这里的重点不再是语法,而是“查文档、调方法”的套路。Math.random()返回 0.0 到 1.0 之间的随机小数,要生成指定范围整数需要自己写公式,这是实验里最常见的需求。

// 生成 [1, 100] 之间的随机整数 int randomNum = (int) (Math.random() * 100) + 1; System.out.println(randomNum); // 日期格式化输出 java.util.Date now = new java.util.Date(); java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.format(now));

Math.random()本身不含下界和步长控制,需要乘法和加法自己调整范围,这是它和Random类的主要差别:java.util.Random的nextInt(n)能直接生成 0 到 n-1 的均匀分布整数。日期方面,SimpleDateFormat的格式串里MM表示月份、mm表示分钟,大小写写错会出现个位数月份补零不准的问题。做实验时如果日期输出明显不对,先检查格式串字母。

5. Java 实验常见问题排查:五个高频坑的现场记录

5.1 javac 提示找不到命令,但 java 正常

现象:命令行里输入java -version有输出,输入javac -version却提示“不是内部或外部命令”。

原因:PATH配置指向的目录里只有java.exe,没有javac,或者 SDK 安装时只装了 JRE 没装完整 JDK。更隐蔽的情况是电脑里此前装过老版本 JDK,它们的bin目录也在PATH里,新旧版本混在一起,命令解析到旧版本的路径。

解决:先执行where java和where javac看命令实际来自哪个目录,再检查JAVA_HOME是否指向完整 JDK 安装目录。确认无误后重新打开终端窗口,因为环境变量修改后已有窗口不会自动刷新。

5.2 Applet 在浏览器里白屏或无法加载

现象:实验 3 和实验 6 里创建的Applet在浏览器中打开,页面空白,控制台报类加载失败。

原因:新版 JDK 已经移除浏览器中的Applet插件支持,老教材里“用 appletviewer 或浏览器打开 HTML”的方式只在早期版本可用。这个坑属于技术栈迭代导致的历史遗留问题。

解决:改用appletviewer工具(如果当前 JDK 还带的话),或者把Applet改造成带main方法的类,用独立窗口运行。指导书在实验 9 的综合练习里也安排了“Applet 与 Application 合并运行”的内容,本质上就是为了绕开这个限制。

5.3 图形界面按钮点了没反应

现象:写了JButton和监听器,运行窗口正常显示,但点击按钮后控制台没有任何输出,按钮像坏掉一样。

原因:事件监听器没有成功注册到组件上,或者监听器被注册后又因为组件重绘而失效。更常见的是只写了addActionListener(new ListenerClass()),但ListenerClass没有实现ActionListener接口,导致编译期就发现问题——还有一种情况是监听器对象被创建两次,实际生效的是没有注册的那个。

解决:在监听器实现类里加一行System.out.println("事件触发: " + e.getActionCommand());,先确认事件有没有进到回调方法。如果输出正常,说明事件机制没问题,问题在回调方法内部逻辑;如果没输出,检查组件和监听器是否同一个实例。

5.4 对象输出流写入报 NotSerializableException

现象:实验 10 里用ObjectOutputStream写对象,运行时报java.io.NotSerializableException,提示某个自定义类无法序列化。

原因:ObjectOutputStream只能写入实现了java.io.Serializable接口的对象,这是对象序列化的前提条件。自定义类没有实现这个接口就写入,虚拟机直接抛异常。接口本身是空接口,没有需要实现的方法。

解决:public class Student implements Serializable,顺手给类声明一个serialVersionUID,避免类结构变动后反序列化失败。注意类的成员变量里如果有其他自定义类型,那些类型也要实现Serializable,否则嵌套序列化时照样报错。

5.5 Socket 程序第二次运行报端口被占用

现象:实验 12 的服务器端程序跑过一次后,再次运行报java.net.BindException: Address already in use: JVM_Bind。

原因:上一次运行没正常关闭ServerSocket,或者程序是强制终止的,端口还没被系统释放。TCP 协议下 TIME_WAIT 状态会让端口在关闭后短暂不可用。

解决:在代码里用try-with-resources或finally块确保serverSocket.close()一定执行;重启前用netstat -ano | findstr 端口号查占用情况,找到占用进程的 PID 后结束它。实验时避免使用 0-1023 的保留端口,选 8000、8899 这些高位端口能减少冲突概率。

6. 进阶实验:Socket、URLConnection 和 Applet 的合并运行技巧

6.1 Socket 编程的最小可运行对

实验 12 的网络编程是整个指导书里衔接课堂知识与真实开发最紧密的部分。实验要求用Socket和ServerSocket建立 TCP 连接,并完成一次数据交互。我建议先写一对最小程序,服务端接受连接后读取一行输入并原样返回,客户端发送内容后打印回显,跑通之后再扩展。

// 服务端 try (ServerSocket server = new ServerSocket(8899)) { System.out.println("服务端已启动,等待连接..."); try (java.net.Socket socket = server.accept(); java.io.BufferedReader in = new java.io.BufferedReader( new java.io.InputStreamReader(socket.getInputStream()))) { System.out.println("收到客户端消息: " + in.readLine()); } }
// 客户端 try (java.net.Socket socket = new java.net.Socket("127.0.0.1", 8899); java.io.PrintWriter out = new java.io.PrintWriter(socket.getOutputStream(), true)) { out.println("Hello Server"); }

服务端的accept()是阻塞方法,没有客户端连接时会一直等在这里,所以实验时先启动服务端、再启动客户端,顺序反了会看到客户端直接报连接拒绝。try-with-resources在这里的价值是让流和套接字在代码块结束后自动关闭,不用手写finally块。

6.2 URL 与 URLConnection:读网页和写数据的差异

实验 12 后半段要求用URL和URLConnection读写网络资源。URL类只负责识别和定位资源,真正建立连接的是URLConnection。读取网页内容时用openStream()最直接,但要做带参数的 POST 请求,就必须先openConnection()再配置请求属性。

import java.net.*; import java.io.*; public class UrlReadDemo { public static void main(String[] args) throws Exception { URL url = new URL("http://127.0.0.1:8899/test.txt"); URLConnection conn = url.openConnection(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8"))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } } }

这里URLConnection比URL.openStream()多出设置超时时间和请求头的余地:setConnectTimeout(3000)控制连接超时,setRequestProperty("User-Agent", "Mozilla/5.0")可以模拟浏览器头信息。指导书里把“读写”分开安排成两小节,实际写操作是拿连接上来的输出流往服务端发数据,和 Socket 客户端很相似,核心都是把字节流打开、写内容、关闭流。

6.3 Applet 与 Application 合并:一个类两种运行方式

实验 9 的综合练习要求把Applet的界面代码改造成也可以作为独立Application运行。原理很简单:Applet本身是Panel的子类,而Application运行入口是main方法,在main里创建Frame,把Applet实例添加进去,窗口一显示,这套逻辑就同时具备浏览器嵌入和独立运行两种能力。

public class DualModeApplet extends java.applet.Applet { public void init() { add(new java.awt.Label("兼容 Applet 与 Application")); } public static void main(String[] args) { java.awt.Frame frame = new java.awt.Frame("双模式运行"); DualModeApplet applet = new DualModeApplet(); frame.add(applet); applet.init(); frame.setSize(300, 100); frame.setVisible(true); } }

把这个类像普通Application一样java DualModeApplet运行,看到的是独立窗口;如果保留.class并嵌入带<applet>标签的 HTML,在支持插件的老环境里也能运行。这个技巧在应付实验考核时特别实用,因为很多考核环境已经不提供 Applet 插件,而一改用main方法,运行门槛立刻降低。

做实验设计这几年,我养成了一个习惯:每次给学生布置实验前,自己先把指导书里的示例程序完整敲一遍,从环境配置到网络编程一路跑通,记录每一步的报错信息。很多看起来“玄学”的问题,其实都是环境残留、类名不一致、流没关闭这几类老问题。从那以后,我每带一批新项目,都强制自己先走一遍“照着示例敲、改参数、再扩展”的流程。这份实验指导书的价值也正在于此——不是让你记住 Java 的每个 API,而是让你知道一个程序从源码到运行的完整路径里,哪个环节会出现问题、该怎么排查。希望这篇拆解能帮你把每个实验都顺利跑通,课内机时不再浪费在环境报错上。

本文还有配套的精品资源,点击获取