实验三:Java 面向对象编程及多线程
一、实验内容
该实验项目内容包括:多态性与动态绑定;包的使用与访问控制;接口的实现
与运用。线程的创建和使用;掌握方法覆盖的使用。
二、实验步骤
1、总体任务
- 编写程序实现多态性与动态绑定,掌握方法覆盖的使用。
- 编写程序掌握线程的创建和使用。
- 实现并运用接口。
- 编译、调试并验证结果。
- 其他内容(根据个人情况确定)。
2、具体实验程序
(1) 设计三个类,分别是学生类 Student、本科生类 Undergraduate、研究生类 Postgraduate,其中 Student 类是一个抽象类,包含一些基本的学生信息如姓名、所学课程、课程成绩等。Undergraduate 类和 Postgraduate 类均为 Student 的子类,它们之间的主要差别在于课程成绩等级的计算方法有所不同。具体成绩等级标准如下:
本科生标准 |
研究生标准 |
80-100 优秀 |
90-100 优秀 |
70-80 良好 |
80-90 良好 |
60-70 一般 |
70-80 一般 |
50-60 及格 |
60-70 及格 |
50 以下 不及格 |
60 以下 不及格 |
其中假设某班级既有本科生也有研究生,编写程序统计出全班学生的成绩等级并显示出来。此题的关键是设计一个既能存放本科生对象又能存放研究生对象的学生数组。 |
|
- 完成上述功能的代码编写,并给出运行结果。
- 在环境中编译运行程序,确定哪个 .class 文件是可运行的。
- 模仿程序,新增一个高中生类 HighSchooler,其成绩等级计算标准为:
(2)编写应用程序,创建一个文件输出流,向文件中分别写入以下类型数据:int、double 和字符串串,然后创建一个文件输入流,将文件中写入的数据显示在屏幕上。
(3) 编写一个类 MyThread,它继承自 Thread 类:类中定义一个长整型变量delay;还定义有两个参数的构造方法,第 1 个参数 str 是 String 类型,通过 super(str)调用父类构造方法给线程命名,第 2 个参数 delay 是长整型,用来初始化类中的变量delay。MyThread 类中的 run 方法如下实现:循环 3 次,每次先在命令行输出线程第几次运行,然后休眠 delay 毫秒,循环结束后输出该线程结束的信息。编写应用程序TestThread.java,在其 main 方法中创建 MyThread 类的三个对象 t1、t2、t3,分别指定线程名字为“线程 A”、“线程 B”和“线程 C”,休眠时间为 1000 毫秒、2000 毫秒、3000 毫秒,并启动这三个线程,main 方法的最后输出当前活动线程的数目。
(4)设计和实现一个 Soundable 接口,该接口具有发声功能,同时还能够调节声音大小。Soundable接口的这些功能将会由3种声音设备来具体实现,它们分别是收音机 Radio、随身听Walkman和手机Mobilephone。最后还要设计一个应用程序类来使用这些实现了Soundable 接口的声音设备类。程序运行时,先询问用户想听哪种设备,然后程序就会按照该设备的工作方式来发出声音。
(a)环境中进行编译,编译的结果将会产生哪些个class文件,分别是哪些?为什么?
(b) 编译之后运行程序,观察所得结果。
(c) 现在假定要为程序增加一个闹钟类Clock,该类也实现 Soundable 接口, 能够发出滴答声,请将以下的Clock类加入到测试类中,修改之后,重新编译并运行,记录结果。
- 现在请模仿本实验的程序设计出一个自己的接口程序,要求先设计一个moveable 可移动接口,然后分别设计 3 个线程 , 即汽车 Car 、轮船 Ship 、飞机Aircraft 来实现该接口,在测试类中可以创建多个线程,启用线程完成各自的工作(用不同的方式创建这些线程)。
三、思考题
- 接口与抽象类的区别是什么?
- 如何创建线程?有几种方法?
第一题:设计三个类
设计三个类,分别是学生类 Student、本科生类 Undergraduate、研究生类 Postgraduate,其中 Student 类是一个抽象类,包含一些基本的学生信息如姓名、所学课程、课程成绩等。Undergraduate 类和 Postgraduate 类均为 Student 的子类,它们之间的主要差别在于课程成绩等级的计算方法有所不同。
创建抽象类Student
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| abstract class Student { protected String name; protected String course; protected double score;
public Student(String name, String course, double score) { this.name = name; this.course = course; this.score = score; } public abstract String getGrade(); public String getName() { return name; }
public String getCourse() { return course; }
public double getScore() { return score; } }
|
创建本科生类
继承Student类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Undergraduate extends Student { public Undergraduate(String name, String course, double score) { super(name, course, score); }
@Override public String getGrade() { if (score >= 80) return "优秀"; else if (score >= 70) return "良好"; else if (score >= 60) return "一般"; else if (score >= 50) return "及格"; else return "不及格"; } }
|
创建研究生类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Undergraduate extends Student { public Undergraduate(String name, String course, double score) { super(name, course, score); }
@Override public String getGrade() { if (score >= 80) return "优秀"; else if (score >= 70) return "良好"; else if (score >= 60) return "一般"; else if (score >= 50) return "及格"; else return "不及格"; } }
|
创建高中生类
1 2 3 4 5 6 7 8 9 10 11 12
| class HighSchooler extends Student { public HighSchooler(String name, String course, double score) { super(name, course, score); }
@Override public String getGrade() { if (score >= 80) return "好"; else if (score >= 60) return "中"; else return "差"; } }
|
主程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Main { public static void main(String[] args) { Student[] students = new Student[5]; students[0] = new Undergraduate("Alice", "Math", 85); students[1] = new Undergraduate("Bob", "Science", 72); students[2] = new Postgraduate("Charlie", "Math", 92); students[3] = new Postgraduate("Daisy", "Science", 65); students[4] = new HighSchooler("Eve", "English", 75);
System.out.println("全班学生成绩等级:"); for (Student student : students) { System.out.println(student.getName() + " (" + student.getCourse() + "): " + student.getGrade()); } } }
|
总代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| abstract class Student { protected String name; protected String course; protected double score; public Student(String name, String course, double score) { this.name = name; this.course = course; this.score = score; } public abstract String getGrade(); public String getName() { return name; } public String getCourse() { return course; } public double getScore() { return score; } }
class Undergraduate extends Student { public Undergraduate(String name, String course, double score) { super(name, course, score); } @Override public String getGrade() { if (score >= 80) return "优秀"; else if (score >= 70) return "良好"; else if (score >= 60) return "一般"; else if (score >= 50) return "及格"; else return "不及格"; } }
class Postgraduate extends Student { public Postgraduate(String name, String course, double score) { super(name, course, score); } @Override public String getGrade() { if (score >= 90) return "优秀"; else if (score >= 80) return "良好"; else if (score >= 70) return "一般"; else if (score >= 60) return "及格"; else return "不及格"; } }
class HighSchooler extends Student { public HighSchooler(String name, String course, double score) { super(name, course, score); } @Override public String getGrade() { if (score >= 80) return "好"; else if (score >= 60) return "中"; else return "差"; } }
public class Main { public static void main(String[] args) { Student[] students = new Student[5]; students[0] = new Undergraduate("Alice", "Math", 85); students[1] = new Undergraduate("Bob", "Science", 72); students[2] = new Postgraduate("Charlie", "Math", 92); students[3] = new Postgraduate("Daisy", "Science", 65); students[4] = new HighSchooler("Eve", "English", 75); System.out.println("全班学生成绩等级:"); for (Student student : students) { System.out.println(student.getName() + " (" + student.getCourse() + "): " + student.getGrade()); } } }
|
第二题:创建文件输出流
题目:编写应用程序,创建一个文件输出流,向文件中分别写入以下类型数据:int、double 和字符串串,然后创建一个文件输入流,将文件中写入的数据显示在屏幕上。
编写输入数据的代码
定义要写入的文件名,然后写入文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import java.io.*; public class FileReadWriteExample { public static void main(String[] args) { String fileName = "data.txt"; try (DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream(fileName))) { dataOutputStream.writeInt(42); dataOutputStream.writeDouble(3.14159); dataOutputStream.writeUTF("Hello, World!");
System.out.println("数据已成功写入文件 " + fileName); } catch (IOException e) { System.err.println("写入文件时发生错误: " + e.getMessage()); } } }
|
解释
try (DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream(fileName)))
:
- 创建一个
FileOutputStream
对象,用于向指定的文件(fileName
)写入字节。
- 将
FileOutputStream
作为参数传递给 DataOutputStream
,后者是一个数据输出流,能够写入 Java 的基本数据类型(如 int
、double
等)以及字符串。
dataOutputStream.writeInt(42)
:
- 调用
dataOutputStream
的 writeInt
方法,将整数 42
写入文件。数据以字节形式存储,因此可以被其他 Java 程序读取。
} catch (IOException e) {
:
- 如果在
try
块中发生 IOException
(例如,文件不可写,磁盘满等),程序将转入此 catch
块来处理异常。
e
是捕获的异常对象,代表发生的具体异常。
System.err.println("写入文件时发生错误: " + e.getMessage())
:
- 输出错误消息到标准错误流,包含具体的异常信息。
e.getMessage()
获取异常的详细信息,帮助开发者理解发生了什么错误。
一般的try语句
1 2 3 4 5 6 7
| try { } catch (ExceptionType e) { } finally { }
|
用一般的try实现上面的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import java.io.DataOutputStream; import java.io.FileOutputStream; import java.io.IOException;
public class FileWriteExample { public static void main(String[] args) { String fileName = "output.dat"; DataOutputStream dataOutputStream = null;
try { FileOutputStream fileOutputStream = new FileOutputStream(fileName); dataOutputStream = new DataOutputStream(fileOutputStream);
dataOutputStream.writeInt(42); dataOutputStream.writeDouble(3.14159); dataOutputStream.writeUTF("Hello, World!");
System.out.println("数据已成功写入文件 " + fileName); } catch (IOException e) { System.err.println("写入文件时发生错误: " + e.getMessage()); } finally { if (dataOutputStream != null) { try { dataOutputStream.close(); } catch (IOException e) { System.err.println("关闭 DataOutputStream 时发生错误: " + e.getMessage()); } } } } }
|
- try-catch-finally:
- 用于捕获和处理异常。
- 可以捕获在
try
块中抛出的异常,并在 catch
块中提供相应的处理逻辑。
- 在
finally
块中确保在所有操作完成后正确关闭文件流,以释放资源。
- try-with-resources:
- 主要用于自动管理资源(如文件、数据库连接等)。
- 确保在使用完资源后能够自动关闭它们,从而避免资源泄露。
编写读取数据的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import java.io.*;
public class FileReadExample { public static void main(String[] args) { String fileName = "data.txt";
try (DataInputStream dataInputStream = new DataInputStream(new FileInputStream(fileName))) { int intValue = dataInputStream.readInt(); double doubleValue = dataInputStream.readDouble(); String stringValue = dataInputStream.readUTF();
System.out.println("读取的数据:"); System.out.println("int: " + intValue); System.out.println("double: " + doubleValue); System.out.println("String: " + stringValue); } catch (IOException e) { System.err.println("读取文件时发生错误: " + e.getMessage()); } } }
|
总代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import java.io.*;
public class FileReadWriteExample { public static void main(String[] args) { String fileName = "data.txt";
try (DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream(fileName))) { dataOutputStream.writeInt(42); dataOutputStream.writeDouble(3.14159); dataOutputStream.writeUTF("Hello, World!");
System.out.println("数据已成功写入文件 " + fileName); } catch (IOException e) { System.err.println("写入文件时发生错误: " + e.getMessage()); }
try (DataInputStream dataInputStream = new DataInputStream(new FileInputStream(fileName))) { int intValue = dataInputStream.readInt(); double doubleValue = dataInputStream.readDouble(); String stringValue = dataInputStream.readUTF();
System.out.println("读取的数据:"); System.out.println("int: " + intValue); System.out.println("double: " + doubleValue); System.out.println("String: " + stringValue); } catch (IOException e) { System.err.println("读取文件时发生错误: " + e.getMessage()); } } }
|
第三题:MyThread
类
题目:
编写一个类 MyThread,它继承自 Thread 类:类中定义一个长整型变量delay;还定义有两个参数的构造方法,第 1 个参数 str 是 String 类型,通过 super(str)调用父类构造方法给线程命名,第 2 个参数 delay 是长整型,用来初始化类中的变量delay。
MyThread 类中的 run 方法如下实现:循环 3 次,每次先在命令行输出线程第几次运行,然后休眠 delay 毫秒,循环结束后输出该线程结束的信息。
编写应用程序TestThread.java,在其 main 方法中创建 MyThread 类的三个对象 t1、t2、t3,分别指定线程名字为“线程 A”、“线程 B”和“线程 C”,休眠时间为 1000 毫秒、2000 毫秒、3000 毫秒,并启动这三个线程,main 方法的最后输出当前活动线程的数目。
MyThread.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class MyThread extends Thread { private long delay;
public MyThread(String str, long delay) { super(str); this.delay = delay; }
@Override public void run() { for (int i = 1; i <= 3; i++) { System.out.println(getName() + " 第 " + i + " 次运行"); try { Thread.sleep(delay); } catch (InterruptedException e) { System.err.println(getName() + " 被中断: " + e.getMessage()); } } System.out.println(getName() + " 结束"); } }
|
TestThread.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class TestThread { public static void main(String[] args) { MyThread t1 = new MyThread("线程 A", 1000); MyThread t2 = new MyThread("线程 B", 2000); MyThread t3 = new MyThread("线程 C", 3000);
t1.start(); t2.start(); t3.start();
System.out.println("当前活动线程的数目: " + Thread.activeCount()); } }
|
第四题:Soundable 接口
题目:设计和实现一个 Soundable 接口,该接口具有发声功能,同时还能够调节声音大小。Soundable接口的这些功能将会由3种声音设备来具体实现,它们分别是收音机 Radio、随身听Walkman和手机Mobilephone。最后还要设计一个应用程序类来使用这些实现了Soundable 接口的声音设备类。程序运行时,先询问用户想听哪种设备,然后程序就会按照该设备的工作方式来发出声音。
定义 Soundable 接口
1 2 3 4
| public interface Soundable { void makeSound(); void adjustVolume(int level); }
|
Radio类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Radio implements Soundable { private int volume;
@Override public void makeSound() { System.out.println("收音机正在播放音乐..."); }
@Override public void adjustVolume(int level) { volume = level; System.out.println("收音机音量调节为: " + volume); } }
|
Walkman 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Walkman implements Soundable { private int volume;
@Override public void makeSound() { System.out.println("随身听正在播放音乐..."); }
@Override public void adjustVolume(int level) { volume = level; System.out.println("随身听音量调节为: " + volume); } }
|
Mobilephone 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Mobilephone implements Soundable { private int volume;
@Override public void makeSound() { System.out.println("手机正在播放铃声..."); }
@Override public void adjustVolume(int level) { volume = level; System.out.println("手机音量调节为: " + volume); } }
|
Clock 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Clock implements Soundable { private int volume;
@Override public void makeSound() { System.out.println("闹钟发出滴答声..."); }
@Override public void adjustVolume(int level) { volume = level; System.out.println("闹钟音量调节为: " + volume); } }
|
创建应用程序类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import java.util.Scanner;
public class SoundTest { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请选择设备 (1: 收音机, 2: 随身听, 3: 手机, 4: 闹钟): "); int choice = scanner.nextInt();
Soundable device;
switch (choice) { case 1: device = new Radio(); break; case 2: device = new Walkman(); break; case 3: device = new Mobilephone(); break; case 4: device = new Clock(); break; default: System.out.println("无效的选择。"); return; }
device.makeSound(); System.out.print("请输入音量(1-10): "); int volumeLevel = scanner.nextInt(); device.adjustVolume(volumeLevel);
scanner.close(); } }
|
问题:
(a)环境中进行编译,编译的结果将会产生哪些个class文件,分别是哪些?为什么?
(b) 编译之后运行程序,观察所得结果。
(c) 现在假定要为程序增加一个闹钟类Clock,该类也实现 Soundable 接口, 能够发出滴答声,请将以下的Clock类加入到测试类中,修改之后,重新编译并运行,记录结果。
a) 在 IntelliJ IDEA 中编译上述代码后,程序将生成以下 .class
文件:
- Soundable.class:接口
Soundable
的编译结果。
- Radio.class:类
Radio
的编译结果,它实现了 Soundable
接口。
- Walkman.class:类
Walkman
的编译结果,它实现了 Soundable
接口。
- Mobilephone.class:类
Mobilephone
的编译结果,它实现了 Soundable
接口。
- Clock.class:类
Clock
的编译结果,它实现了 Soundable
接口。
- SoundTest.class:主类
SoundTest
的编译结果,包含 main
方法并用于运行程序。
(b) 运行程序观察结果
运行程序后,控制台将提示用户选择设备,程序将根据用户的选择发出对应的声音并调整音量。以下是一个可能的运行示例:
1 2 3 4 5
| 请选择设备 (1: 收音机, 2: 随身听, 3: 手机, 4: 闹钟): 2 随身听正在播放音乐... 请输入音量(1-10): 7 随身听音量调节为: 7
|
观察结果:
- 程序根据用户输入的设备类型调用相应的
makeSound()
方法,输出不同的音效信息。
- 用户输入的音量级别被传递给
adjustVolume()
方法,并显示相应的音量调整信息。
(c) 增加 Clock
类并记录结果
假设 Clock
类的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Clock implements Soundable { private int volume;
@Override public void makeSound() { System.out.println("闹钟发出滴答声..."); }
@Override public void adjustVolume(int level) { volume = level; System.out.println("闹钟音量调节为: " + volume); } }
|
运行结果:
1 2 3 4 5
| 请选择设备 (1: 收音机, 2: 随身听, 3: 手机, 4: 闹钟): 4 闹钟发出滴答声... 请输入音量(1-10): 3 闹钟音量调节为: 3
|
附加题
题目:现在请模仿本实验的程序设计出一个自己的接口程序,要求先设计一个moveable 可移动接口,然后分别设计 3 个线程 , 即汽车 Car 、轮船 Ship 、飞机Aircraft 来实现该接口,在测试类中可以创建多个线程,启用线程完成各自的工作(用不同的方式创建这些线程。
Moveable
接口
1 2 3
| public interface Moveable { void move(); }
|
Car
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Car extends Thread implements Moveable { private String name;
public Car(String name) { this.name = name; }
@Override public void move() { System.out.println(name + " 正在公路上行驶。"); }
@Override public void run() { move(); } }
|
Ship
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Ship extends Thread implements Moveable { private String name;
public Ship(String name) { this.name = name; }
@Override public void move() { System.out.println(name + " 正在海上航行。"); }
@Override public void run() { move(); } }
|
Aircraft
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Aircraft extends Thread implements Moveable { private String name;
public Aircraft(String name) { this.name = name; }
@Override public void move() { System.out.println(name + " 正在天空中飞行。"); }
@Override public void run() { move(); } }
|
TestMoveable 测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class TestMoveable { public static void main(String[] args) { Car car1 = new Car("汽车 1"); Car car2 = new Car("汽车 2"); Ship ship1 = new Ship("轮船 1"); Aircraft aircraft1 = new Aircraft("飞机 1");
car1.start(); car2.start(); ship1.start(); aircraft1.start(); } }
|
思考题
问题 |
答案 |
接口与抽象类的区别是什么? |
接口:只声明方法签名,不提供实现;支持多继承,一个类可以实现多个接口。 抽象类:可以包含已实现的方法和属性;只支持单继承。 |
如何创建线程?有几种方法? |
方法 1:继承 Thread 类并重写 run() 方法,调用 start() 启动线程。 方法 2:实现 Runnable 接口,将实例传给 Thread 构造器。 |
(2024.10.25)