title: JavaSE高级实验
toc: true
comments: true
abbrlink: 3b0f2434
date: 2021-04-08 18:24:17
tags:
categories:
这里记录的是Java核心API与高级编程实践实验
[TOC]
(十)异常
实验 79 捕获并处理异常实验
实验项目
- 捕获并处理异常实验
实验需求
- 本实验主要练习捕获并处理异常操作。声明及抛出异常会在后续实验中进行
实验内容
捕获并处理异常
通常使用 try 和 catch 语句块来捕获并处理异常,有时候还会用到 finally 来做后续的收尾处理工作。
对于上述三个关键词所构成的语句块,try 语句块是必不可少的,catch 和 finally 语句块可以根据情况选择其一或者全选。可以把可能发生错误或出现问题的代码放到 try 语句块中,将异常发生后要执行的代码放到 catch 语句块中,而 finally 语句块里面放置的代码,不管异常是否发生,它们都会被执行。
那么是否可以把所有有关的代码都放到 try 语句块中呢?答案是否定的,因为捕获异常对于系统而言,其开销非常大,所以应尽量减少该语句块中放置的语句。
下面来开始实验
在 /home/project/目录下新建 CatchException.java:
public class CatchException {
public static void main(String[] args) {
try {
// 下面定义了一个try语句块
System.out.println("I am try block.");
Class<?> tempClass = Class.forName("");
// 声明一个空的Class对象用于引发“类未发现异常”
System.out.println("Bye! Try block.");
} catch (ClassNotFoundException e) {
// 下面定义了一个catch语句块
System.out.println("I am catch block.");
e.printStackTrace();
//printStackTrace()的意义在于在命令行打印异常信息在程序中出错的位置及原因
System.out.println("Goodbye! Catch block.");
} finally {
// 下面定义了一个finally语句块
System.out.println("I am finally block.");
}
}
}
实验结果
编译运行:
结合这些输出语句在源代码中的位置,再来体会一下三个语句块的作用。
实验总结
- 遇到异常可以有两种解决方案:
- 捕获并处理异常
- 声明及抛出异常
2.try-catch-finally是异常最常见的处理方式,必须记牢。
实验 80 多重处理异常实验
实验项目
- 多重处理异常实验
实验需求
- 在一段代码中,可能会由于各种原因抛出多种不同的异常,而对于不同的异常,我们希望用不同的方式来处理它们,而不是笼统的使用同一个方式处理,在这种情况下,可以使用异常匹配,当匹配到对应的异常后,后面的异常将不再进行匹配。
实验内容
多重catch语法
多个catch的排序要符合一定的规则:先写Exception的子类,最后写Exception类。这样可以保证当每个catch都无法捕获到异常的时候,Exception父类会“兜底”异常。避免因异常类型都不匹配导致的程序中断。
当然也不能把Exception放到子类异常的前面,这样程序虽然不会中断但是不论哪种异常类型都会进入Exception对应的处理块中处理,这样也就失去了多重catch的意义。
下面开始进行实验。
在 /home/project/ 目录下新建源代码文件 MultipleCapturesDemo.java:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class MultipleCapturesDemo {
public static void main(String[] args) {
try {
new FileInputStream("");
} catch (FileNotFoundException e) {
System.out.println("IO 异常");
} catch (Exception e) {
System.out.println("发生异常");
}
}
}
实验结果
实验总结
- 捕获多个异常需要注意:“先子类、后父类,先特殊、后一般。”
2.
实验 81 声明和抛出异常实验
实验项目
- 声明和抛出异常实验
实验需求
- 本章讲解另外一种异常处理方式:声明并抛出异常。同捕获并处理异常不同,声明并抛出异常意味着在代码中并不需要对异常处理,只需要将异常抛出交由调用者处理。
知识点
throw
throws
实验内容
声明并抛出异常
throw 抛出异常
当程序运行时数据出现错误并不想当时处理或者当不希望发生的情况出现的时候,可以通过抛出异常来处理。
异常抛出语法:throw new 异常类();
throws声明异常
throws 用于声明异常,表示该方法可能会抛出的异常。如果声明的异常中包括 checked 异常(受检异常),那么调用者必须捕获处理该异常或者使用 throws 继续向上抛出。throws 位于方法体前,多个异常之间使用 , 分割。
声明异常的语法:pubic void methodName throws Exception(){}
下面开始试验。
创建并且抛出一个异常
在 /home/project/ 目录下新建 ThrowTest.java
public class ThrowTest {
public static void main(String[] args) {
Integer a = 1;
Integer b = null;
//当a或者b为null时,抛出异常
if (a == null || b == null) {
throw new NullPointerException();
} else {
System.out.println(a + b);
}
}
}
结果显示:
抛出一个系统生成异常
public class ThrowTest2 {
public static void main(String[] args) throws FileNotFoundException{
try {
new FileInputStream("");
} catch (FileNotFoundException e) {
throw e;//不处理异常,而是将异常抛出
}
}
}
编译运行:
由上面的例子可以看出,当使用try-catch捕获到异常后并没有对异常进行处理,而是将该异常抛出(throw)由其他代码处理。这里需要注意的是,当抛出系统异常时还需要声明(throws)一下,否则会无法编译。接下来看一下声明(throws)的用法。
throws 声明异常
修改 /home/project/ 下的 ThrowsTest.java:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ThrowsTest {
public static void main(String[] args) throws FileNotFoundException {
//由方法的调用者捕获异常或者继续向上抛出
throwsTest();
}
public static void throwsTest() throws FileNotFoundException {
new FileInputStream("/home/project/shiyanlou.file");
}
}
编译运行:
实验总结
- throw和throws一定要区分开,两者虽然单词类似但是完成的功能是完全不同的。throw是人为产生异常。而throws是声明代码中会出现异常。一定要注意区分!
实验 82 自定义异常实验
实验项目
- 自定义异常实验
实验需求
尽管 Java SE 的 API 已经为我们提供了数十种异常类,然而在实际的开发过程中,你仍然可能遇到未知的异常情况。此时,你就需要对异常类进行自定义。
知识点
Exception
throw
throws
实验内容
自定义异常
自定义一个异常类非常简单,只需要让它继承 Exception 或其子类就行。在自定义异常类的时候,建议同时提供无参构造方法和带字符串参数的构造方法,后者可以为你在调试时提供更加详细的信息。
百闻不如一见,下面我们尝试自定义一个算术异常类。
在 /home/project/ 目录下创建一个 MyAriException 类。
// MyAriException.java
public class MyAriException extends ArithmeticException {
//自定义异常类,该类继承自ArithmeticException
public MyAriException() {
}
//实现默认的无参构造方法
public MyAriException(String msg) {
super(msg);
}
//实现可以自定义输出信息的构造方法,将待输出信息作为参数传入即可
}
添加一个 ExceptionTest 类作为测试用,在该类的 main() 方法中,可以尝试使用 throw 抛出自定义的异常。
代码片段如下:
// ExceptionTest.java
import java.util.Arrays;
public class ExceptionTest {
public static void main(String[] args) {
int[] array = new int[5];
//声明一个长度为5的数组
Arrays.fill(array, 5);
//将数组中的所有元素赋值为5
for (int i = 4; i > -1; i--) {
//使用for循环逆序遍历整个数组,i每次递减
if (i == 0) {
// 如果i除以了0,就使用带异常信息的构造方法抛出异常
throw new MyAriException("There is an exception occured.");
}
System.out.println("array[" + i + "] / " + i + " = " + array[i] / i);
// 如果i没有除以0,就输出此结果
}
}
}
检查一下代码,编译并运行,期待中的自定义错误信息就展现在控制台中了:
实验总结
自定义异常在开发的时候并不常见,JDK自带的异常类型基本都能满足日常开发的需要,就算有自定义的需求一般情况下公司的架构师都会提前设计好。所以这个知识点大家仅作了解。
实验 83 异常综合实验
实验项目
- 异常综合实验
实验需求
本实验是异常的综合实验,包括异常的所有产生和使用方式。
-
设计一个学生类Student.java。包括学生姓名,学号,年龄三个属性。
-
编写程序完成对学生信息的录入和展示功能。
-
录入学生信息的时候需要进行年龄判断,范围在18-30之间为正常,否则为异常数据。
-
需要用到所有的异常知识。
知识点
异常所涉及的类
实验内容
知识点回顾
Error(错误):程序在执行过程中所遇到的硬件或操作系统的错误。错误对程序而言是致命的,将导致程序无法运行。常见的错误有内存溢出,jvm虚拟机自身的非正常运行等。程序不能处理错误,只能依靠外界来干预。Error是系统内部的错误,由jvm抛出,交给系统来处理。
Exception(异常):程序正常运行中可以预料的意外情况。例如:数组下标越界、空指针等。异常可以导致也可以预先检测,被捕获处理掉,使程序继续运行。
异常又可以分为编译异常和运行时异常(RuntimeException)。
-
编译异常需要在编写代码的时候提前处理,通常是由语法错和环境因素(外部资源)造成的异常,若不处理该异常则程序无法编译成功。所以编译异常又称为检查异常。
-
运行异常则是由程序逻辑错误引起的,即语义错。比如算术异常、数组下标越界等。该异常需要在编程程序时进行提前预判并进行处理,以免导致程序中断。
本章主要讲解的是Exception的处理。
实验结果
实验总结
- 异常可分为检查时异常和运行时异常,
(1)检查时异常:- ClassNotFoundException, 类没找到异常;
- FileNotFoundException, 文件找不到异常;
- IOException, IO流异常。一般在读写数据的时候会出现这种问题。
java内部数据的传输都是通过流,或者byte来进行传递的。
就行一个文本文件。你可以通过in流写入到java中,同时也可以通过out流从java(计算机内存中)返还给具体的文件。
(2)运行时异常
- NullPointerException, 空指针异常
- ArrayIndexOutOfBoundsException, 数组下标越界异常
- IndexOutOfBoundsException, 下标越界异常
- ArithmeticException, 算术异常
- InputMismatchException, 输入不匹配异常
- ClassCastException, 类型转换异常
- NumberFormatException, 数字格式异常
- 如何在实验楼eclipse输入中文:
- 只要双击桌面下的搜狗输入法图标, 就会在右下角出现一个小键盘, 在输入的时候点击这个小键盘, 就会出现ping这个字样, 然后就可以愉快地输入中文了
- 然后左右候选词翻页是按上面的-和=号的
(十一)数据结构
实验 84 二分法查找实验
实验项目
- 二分法查找实验
实验需求
- 练习二分查找
实验内容
在查找表中不断取中间元素与查找值进行比较,以二分之一的倍率进行表范围的缩小。
import java.util.Arrays;
/**
* 测试二分法查找
* 二分法适用于已经排好序的数组
* @author Administrator
*
*/
public class TestBinarySearch {
public static void main(String[] args) {
int[] arr= {30,20,50,10,80,9,7,12,100,40,8};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
System.out.println(myBinarySearch(arr,40));
}
public static int myBinarySearch(int[] arr,int value) {
int low=0;
int high=arr.length-1;
while(low<=high) {
int mid=(low+high)/2;
if(value==arr[mid]) {
return mid;
}
if(value>arr[mid]) {
low=mid+1;
}
if(value<arr[mid]) {
high=mid-1;
}
}
return -1;//没有找到返回-1
}
}
实验结果
实验总结
- 假设数据是按升序排序的,对于给定值 x,从序列的中间位置开始比较,如果当前位置值等于 x,则查找成功;若 x 小于当前位置值,则在数列的前半段中查找;若 x 大于当前位置值则在数列的后半段中继续查找,直到找到为止。
- 如果 value==arr[mid],中间值正好等于要查找的值,则返回下标,return mid。
- 如果 value<arr[mid],要找的值小于中间的值,则再往数组的小端找,high=mid-1。
- 如果 value>arr[mid],要找的值大于中间的值,则再往数组的大端找,low=mid+1。
实验 85 插入排序实验
实验项目
- 插入排序实验
实验需求
- 练习插入排序实验
实验内容
-
以数组的某一位作为分隔位,比如index=1,假设左面的都是有序的。
-
将index位的数据拿出来,放到临时变量里,这时index位置就空出来了。
-
从leftindex=index-1开始将左面的数据与当前index位的数据(即temp)进行比较,如果array[leftindex]>temp,则将array[leftindex]后移一位,即array[leftindex+1]=array[leftindex],此时leftindex就空出来了。
-
再用index-2(即leftindex=leftindex-1)位的数据和temp比,重复步骤3,直到找到<=temp的数据或者比到了最左面(说明temp最小),停止比较,将temp放在当前空的位置上。
-
index向后挪1,即index=index+1,temp=array[index],重复步骤2-4,直到index=array.length,排序结束,此时数组中的数据即为从小到大的顺序。
import java.util.Arrays;
public class Test{
public static void main(String[] args){
int[] array = {12, 73, 45, 69, 35};
int i, j, temp;
for(i = 1; i < array.length; i++){
/**
* 第一个for循环
* 把数组分成两部分, 右边分为未排序, 左边分为已排序
* 记录排序与未排序分割点temp(temp为下一个排序对象)
*/
temp = array[i];
for(j = i-1; j >= 0; j--) {
/**
* 第二个for循环
* 将排序对象temp与已排序数组比较
* 当temp 比最近左边的数大时(按从小到大顺序排列时)
* 直接结束本次循环,进行下一个数排序
* 否则比左边这个数小时将这个数后移, 腾出这个数的位置
*/
if(temp > array[j]){
break;
}else{
array[j+1] = array[j];
}
}
array[j+1] = temp;
}
System.out.println(Arrays.toString(array));
}
}
实验结果
实验总结
- 利用插入法对无序数组排序时,我们其实是将数组R划分成两个子区间R[1..i-1](已排好序的有序区)和R[i..n](当前未排序的部分,可称无序区)。插入排序的基本操作是将当前无序区的第1个记录R[i]插人到有序区R[1..i-1]中适当的位置上,使R[1..i]变为新的有序区。因为这种方法每次使有序区增加1个记录,通常称增量法。
- 假设待排序的元素个数为n,则向有序表中逐个插入记录的操作进行了n-1趟,每趟操作分为比较关键代码和移动记录,而比较的次数和移动的次数取决于待排序列关键代码的初始排列。
实验 86 图书馆书籍管理系统
实验项目
- 图书馆书籍管理系统
实验需求
- 利用抽象类、接口等实现简单的图书管理系统。
实验内容
系统分析
Operate类
package user.impl;
public interface Operate {
void arrangeBooks(String[][] book);
void printBooksList(String[][] book);
void alter_s(String[][] book);
}
Book抽象类
package user.action;
import java.util.Scanner;
public class Book {
Scanner input = new Scanner(System.in);
protected int BookId; //书的ID
protected String BookName; //书的名字
protected String Author; //书的作者
protected int BookNum; //书的数量
protected static int count;
static String[][] book1 = new String[5][4]; //存放图书信息的数组
public Book(){}
public Book(int BookId, String BookName, String Author, int BookNum){
this.BookId = BookId;
this.BookName = BookName;
this.Author = Author;
this.BookNum = BookNum;
}
public Scanner getInput(){
return input;
}
public void setInput(Scanner input){
this.input = input;
}
public int getBookId(){
return BookId;
}
public void setBookId(int BookId){
this.BookId = BookId;
}
public String getBookName(){
return BookName;
}
public void setBookName(String BookName){
this.BookName = BookName;
}
public String getAuthor(){
return Author;
}
public void setAuthor(String Author){
this.Author = Author;
}
public int getBookNum(){
return BookNum;
}
public void setBookNum(int BookNum){
this.BookNum = BookNum;
}
/**
* 增加图书以及图书信息函数
* @param book
*/
public void add(Book book){
int BookId1, BookNum1;
String BookName1, Author1;
System.out.println("~~~~~开始添加图书信息~~~~~");
System.out.println("图书ID:");
BookId1 = input.nextInt();
book.setBookId(BookId1);
System.out.println("图书名字:");
BookName1 = input.next();
book.setBookName(BookName1);
System.out.println("图书作者:");
Author1 = input.next();
book.setAuthor(Author1);
System.out.println("图书数量:");
BookNum1 = input.nextInt();
book.setBookNum(BookNum1);
System.out.println("~~~~~添加成功~~~~~");
book1[count][0] = book.BookId + "";
book1[count][1] = book.BookName;
book1[count][2] = book.Author;
book1[count][3] = book.BookNum + "";
System.out.println("图书编号:" + book1[count][0]);
System.out.println("图书名字:" + book1[count][1]);
System.out.println("图书作者:" + book1[count][2]);
System.out.println("图书数量:" + book1[count][3]);
count++;
}
/**
* 用于删除图书信息
*/
public void delete(){
System.out.println("输入您要删除的图书名字:");
String m;
m = input.next();
for(int i = 0; i < count; i++){
if(book1[i][1].equals(m)){
book1[i] = null;
book1[i] = book1[i + 1];
break;
}
}
System.out.println("~~~~~图书信息已被删除~~~~~");
count--;
}
}
Person抽象类
package user.action;
import java.util.Scanner;
abstract class Person {
protected static String name;
protected static String sex;
protected static int age;
Scanner input = new Scanner(System.in);
public static String getName(){
return name;
}
public static String getSex(){
return sex;
}
public static int getAge(){
return age;
}
public static void setName(String name){
Person.name = name;
}
public static void setSex(String sex){
Person.sex = sex;
}
public static void setAge(int age){
Person.age = age;
}
/**
* 查找图书信息
* @param book
*/
public void selectBook(String[][] book){
String m;
System.out.println("请输入您要查找的图书名字: ");
m = input.next();
if(Book.count == 0){
System.out.println("不好意思, 无此书籍!");
}else{
boolean flag = true;
while(flag){
for(int i = 0; i < book.length; i++){
if(book[i][1].equals(m)){
System.out.println("图书编号" + book[i][0]);
System.out.println("图书名字" + book[i][1]);
System.out.println("图书作者" + book[i][2]);
System.out.println("图书数量" + book[i][3]);
System.out.println("~~~~~~~图书信息查找完毕~~~~~~~" );
flag = false;
break;
}else{
System.out.println("不好意思,无此书籍!");
flag = false;
break;
}
}
}
}
}
}
AdminPerson类
package user.action;
import user.impl.Operate;
import java.util.Scanner;
public class AdminPerson extends Person implements
Operate, Comparable<Book> {
//用户姓名
private static String adminName;
//用户性别
private static String adminSex;
//用户年龄
private static int adminAge;
Scanner input = new Scanner(System.in);
Book book = new Book();
//无参构造函数
public AdminPerson(){}
//有参构造函数
public AdminPerson(String adminName, String adminSex, int adminAGe){}
/**
* 按图书编号整理书籍
* @param book1
*/
@Override
public void arrangeBooks(String[][] book1){
String[] temp;
for(int i = 0; i < Book.count - 1; i++){
for(int j = 0; j < Book.count - 1; j++){
if(book1[j][0].compareTo(book1[j + 1][0]) > 0){
temp = book1[j];
book1[j] = book1[j + 1];
book1[j + 1] = temp;
}
}
}
System.out.println("整理完毕!");
}
/**
* 打印图书列表
* @param
*/
@Override
public void printBooksList(String[][] book){
if(Book.count == 0){
System.out.println("仓库书籍为空!");
}else{
System.out.println("所有图书信息如下:");
for(int i = 0; i < Book.count; i++){
System.out.print("图书编号:" + book[i][0] + "tt");
System.out.print("图书名字:" + book[i][1] + "tt");
System.out.print("图书作者:" + book[i][2] + "tt");
System.out.print("图书数量:" + book[i][3] + "tt");
}
}
}
/**
* 修改图书
* @param book1
* @return
*/
@Override
public void alter_s(String[][] book1){
String m;
int o;
int n = -1;
System.out.println("请输入您要修改的图书名字:");
m = input.next();
for(int i = 0; i < Book.count; i++){
if(book1[i][1].equals(m)){
n = i;
break;
}
}
System.out.println("请选择您要修改的内容: 1. 图书编号 2. 图书名字 3. 图书作者 4. 图书数量");
o = input.nextInt();
System.out.println("请输入您要修改的内容:");
book1[n][o-1] = input.next() + "";
System.out.println("~~~~~图书信息已修改~~~~~~");
}
/**
* 管理员操作函数
*/
public void start(){
boolean flag = true;
int number;
while(flag){
System.out.println("----------------------");
System.out.print("请选择: 1. 增加书籍 2. 查询书籍 3. 修改书籍 4. 删除书籍 5. 打印书籍列表 6. 整理书籍 7. 退出nn");
number = input.nextInt();
switch(number){
case 1:
book.add(book);
break;
case 2:
selectBook(Book.book1);
break;
case 3:
alter_s(Book.book1);
break;
case 4:
book.delete();
break;
case 5:
printBooksList(Book.book1);
break;
case 6:
arrangeBooks(Book.book1);
break;
case 7:
System.out.println("~~~~~您已退出系统~~~~~");
flag = false;
break;
default :
System.out.println("输入错误");
break;
}
}
}
@Override
public int compareTo(Book o){
return book.getBookId()-o.getBookId();
}
}
User类
package user.action;
import java.util.Scanner;
public class User extends Person {
//用户姓名
private static String userName;
//用户性别
private static String userSex;
//用户年龄
private static int userAge;
//无参构造函数
Book book = new Book();
public User(){}
//有参构造函数
public User(String userName, String userSex, int userAge){}
//借阅书籍
public void borrowBook(String[][] book1){
System.out.println("请输入您要借阅的图书名字:");
String m;
int n = -1;
m = input.next();
for(int i = 0; i < book1.length; i++){
if(book1[i][1].equals(m)){
n = i;
break;
}
}
int p = Integer.parseInt(Book.book1[n][3]);
if(p == 0){
System.out.println("不好意思, 已经被借光了~");
}else{
Book.book1[n][3] = (p - 1) + "";
System.out.println("借阅成功!");
}
}
/**
* 归还图书
* @param book
*/
public void returnBook(String[][] book1){
int BookId1, BookNum1;
String BookName1, Author1;
System.out.println("~~~~~开始归还图书~~~~~");
System.out.println("请输入归还图书名字:");
BookName1 = input.next();
for(int i = 0; i < Book.count; i++){
if(book1[i][1].equals(BookName1)){
int p = Integer.parseInt(book1[i][3]);
book1[i][3] = (p + 1) + "";
break;
}
}
System.out.println("~~~~~归还成功~~~~~");
}
/**
* 用户操作函数
*/
public void start(){
Scanner input = new Scanner(System.in);
boolean flag = true;
int number;
while(flag){
System.out.println("--------------------");
System.out.println("请选择:1. 查询书籍 2. 借阅书籍 3.归还书籍 4. 退出nn");
number = input.nextInt();
switch(number){
case 1:
selectBook(Book.book1);
break;
case 2:
borrowBook(Book.book1);
break;
case 3:
returnBook(Book.book1);
break;
case 4:
System.out.println("~~~~~您已退出系统~~~~~");
flag = false;
break;
default:
System.out.println("输入错误");
break;
}
}
}
}
Test类
package user.action;
import java.util.Scanner;
public class Test {
public void choose(){
while(true){
System.out.println("------------图书管理系统------------n");
System.out.print("请登录: 1.普通用户 2. 管理员登录nn");
Scanner in = new Scanner(System.in);
int choose = in.nextInt();
Scanner scan = new Scanner(System.in);
User user = null;
AdminPerson adminPerson = null;
switch(choose){
case 1:
System.out.print("请输入:姓名 n");
String userName = scan.next();
System.out.print("请输入:性别 n");
String userSex = scan.next();
System.out.print("请输入:年龄 n");
int userAge = scan.nextInt();
user = new User(userName, userSex, userAge);
System.out.println("当前用户:" + userName +
" " + userSex + " " + userAge);
user.start();
break;
case 2:
System.out.print("请输入:姓名 n");
String adminName = scan.next();
System.out.print("请输入:性别 n");
String adminSex = scan.next();
System.out.print("请输入:年龄 n");
int adminAge = scan.nextInt();
System.out.println("当前管理员:" + adminName +
" " + adminSex + " " + adminAge);
adminPerson = new AdminPerson(adminName, adminSex, adminAge);
adminPerson.start();
break;
}
}
}
public static void main(String[] args){
Test test = new Test();
test.choose();
}
}
实验结果
(十二)集合和泛型
实验 87 HashSet使用实验
实验项目
- HashSet使用实验
实验需求
HashSet 由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。
知识点
集合
Set
假设现在学生们要做项目,每个项目有一个组长,由组长来组织组员,我们便来实现项目组的管理吧。
实验内容
因为项目组的组长由一个老师担任,首先在 /home/project 目录下创建一个 PD 类
// PD.java
import java.util.HashSet;
import java.util.Set;
/*
* 项目组长类
*/
public class PD {
public String id;
public String name;
//集合后面的<>代表泛型的意思
//泛型是规定了集合元素的类型
public Set<Student> students;
public PD(String id, String name){
this.id = id;
this.name = name;
this.students = new HashSet<Student>();
}
}
在 /home/project/ 创建一个学生类 Student.java:
/**
* 学生类
*/
// Student.java
public class Student {
public String id;
public String name;
public Student(String id, String name){
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id='" + id + ''' +
", name='" + name + ''' +
'}';
}
}
接下来创建一个 SetTest 类,用来管理项目成员
// SetTest.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
public class SetTest {
public List<Student> students;
public SetTest() {
students = new ArrayList<Student>();
}
/*
* 用于往students中添加学生
*/
public void testAdd() {
//创建一个学生对象,并通过调用add方法,添加到学生管理List中
Student st1 = new Student("1", "张三");
students.add(st1);
//添加到List中的类型均为Object,所以取出时还需要强转
Student st2 = new Student("2","李四");
students.add(st2);
Student[] student = {new Student("3", "王五"),new Student("4", "马六")};
students.addAll(Arrays.asList(student));
Student[] student2 = {new Student("5", "周七"),new Student("6", "赵八")};
students.addAll(Arrays.asList(student2));
}
/**
* 通过for each 方法访问集合元素
* @param args
*/
public void testForEach() {
System.out.println("有如下学生(通过for each):");
for(Object obj:students){
Student st = (Student)obj;
System.out.println("学生:" + st.id + ":" + st.name);
}
}
public static void main(String[] args){
SetTest st = new SetTest();
st.testAdd();
st.testForEach();
PD pd = new PD("1","张老师");
System.out.println("请:" + pd.name + "选择小组成员!");
//创建一个 Scanner 对象,用来接收从键盘输入的学生 ID
Scanner console = new Scanner(System.in);
for(int i = 0;i < 3; i++){
System.out.println("请输入学生 ID");
String studentID = console.next();
for(Student s:st.students){
if(s.id.equals(studentID)){
pd.students.add(s);
}
}
}
st.testForEachForSer(pd);
// 关闭 Scanner 对象
console.close();
}
//打印输出,老师所选的学生!Set里遍历元素只能用foreach 和 iterator
//不能使用 get() 方法,因为它是无序的,不能想 List 一样查询具体索引的元素
public void testForEachForSer(PD pd){
for(Student s: pd.students) {
System.out.println("选择了学生:" + s.id + ":" + s.name);
}
}
}
实验结果
实验总结
- HashSet实现了Set接口,不允许出现重复元素,不保证集合中元素的顺序,允许包含值为null的元素,但最多只能一个。
实验 88 TreeSet使用实验
实验项目
- TreeSet使用实验
实验需求
TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。
知识点
集合
Set
假设现在人类要做项目,每个人有自己的编号,我们便来实现项目组的管理吧。
实验内容
首先在 /home/project 目录下创建一个 Persons 类
package com.lanqiao.vo;
public class Persons implements Comparable{
private int caedno;
private String name;
public int getCaedno() {
return caedno;
}
public void setCaedno(int caedno) {
this.caedno = caedno;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Persons [caedno=" + caedno + ", name=" + name + "]";
}
@Override
public int compareTo(Object o) {
// TODO Auto-generated method stub
Persons p=(Persons) o;
return this.getCaedno()>p.getCaedno() ? 1:(this.getCaedno()== p.getCaedno()? 0 :-1);
}
public Persons(int caedno,String name) {
// TODO Auto-generated constructor stub
this.caedno=caedno;
this.name=name;
}
}
在 /home/project/ 创建一个学生类 TreeSetTest.java:
package com.lanqiao.test;
import java.util.Set;
import java.util.TreeSet;
import com.lanqiao.vo.Persons;
public class TreeSetTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
Set<Persons> set=new TreeSet<Persons>();
// Persons p1=new Persons();
//
// p1.setCaedno(120);
// p1.setName("abc");
//
// set.add(p1);
//
// Persons p2=new Persons();
//
// p2.setCaedno(220);
// p2.setName("abcd");
//
// set.add(p2);
//
//
//
//Persons p3=new Persons();
//
// p3.setCaedno(320);
// p3.setName("abcd");
//
// set.add(p3);
//
//Persons p4=new Persons();
//
// p4.setCaedno(420);
// p4.setName("abcd");
//
// set.add(p4);
//
//Persons p5=new Persons();
//
// p5.setCaedno(220);
// p5.setName("abcd");
// set.add(p5);
Persons p1=new Persons(123,"a");
set.add(p1);
Persons p2=new Persons(1221,"b");
set.add(p2);
for(Persons p:set ){
System.out.println(p);
}
}
}
实验结果
实验总结
- 继承于AbstractSet抽象类,实现了NavigableSet, Cloneable, java.io.Serializable接口。 TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。
实验 89 Iterator使用实验
实验项目
- Iterator使用实验
实验需求
Iterator(迭代器)是一个接口,它的作用就是遍历容器的所有元素。
知识点
集合
Iterator
实验内容
迭代器
- iterator是为了实现对Java容器(collection)进行遍历功能的一个接口。
- 在iterator实现了Iterator接口后,相当于把一个Collection容器的所有对象,做成一个线性表(List),而iterator本身是一个指针,开始时位于第一个元素之前。
首先在 /home/project 目录下创建一个 Person 类
package com.lanqiao.vo;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Person {
/*
* 泛型
*/
private String p_no;
private String name;
public String getP_no() {
return p_no;
}
public void setP_no(String p_no) {
this.p_no = p_no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "名人 [p_no=" + p_no + ", name=" + name + "]";
}
}
接下来创建一个 ListTest 类
package com.lanqiao.test;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import com.lanqiao.vo.Person;
public class ListTest {
public static void main(String[] args) {
List<Person> list=new ArrayList<Person>();
// List<Person> list1=new LinkedList<Person>();
Person p1=new Person();
p1.setP_no("01");
p1.setName("小明");
list.add(p1);
//迭代器
Iterator<Person> it=list.iterator();
while(it.hasNext()){//判断数据是否存在
System.out.println(it.next());
}
}
}
实验结果
实验总结
- 在Java的各种容器中,例如ArrayList,HashSet等,并没有直接实现Iterator这个接口。所以ArrayList,HashSet容器内是没有hasnext(),next()的方法的,而是iterator() 这个方法,返回1个实现了Iterator接口的iterator对象。
实验 90 ArrayList使用实验
实验项目
- ArrayList使用实验
实验需求
ArrayList 类实现一个可增长的动态数组,位于 java.util.ArrayList。实现了 List 接口,它可以存储不同类型的对象(包括 null 在内),而数组则只能存放特定数据类型的值。
知识点
集合
List
学校的教务系统会对学生进行统一的管理,每一个学生都会有一个学号和学生姓名,我们在维护整个系统的时候,大多数操作是对学生的添加、插入、删除、修改等操作。
实验内容
先在 /home/project/ 创建一个学生类 Student.java:
/**
* 学生类
*/
public class Student {
public String id;
public String name;
public Student(String id, String name){
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id='" + id + ''' +
", name='" + name + ''' +
'}';
}
}
再在 /home/project/ 创建一个 ListTest.java,其中包含了一个学生列表,通过操作学生列表来管理学生
import java.util.*;
public class ListTest {
//集合后面的<>代表泛型的意思
//泛型是规定了集合元素的类型
/**
* 用于存放学生的List
*/
public List<Student> students;
public ListTest() {
this.students = new ArrayList<Student>();
}
/**
* 用于往students中添加学生
*/
public void testAdd() {
// 创建一个学生对象,并通过调用add方法,添加到学生管理List中
Student st1 = new Student("1", "张三");
students.add(st1);
// 取出 List中的Student对象 索引为0 也就是第一个
Student temp = students.get(0);
System.out.println("添加了学生:" + temp.id + ":" + temp.name);
Student st2 = new Student("2", "李四");
//添加到list中,插入到索引为0的位置,也就是第一个
students.add(0, st2);
Student temp2 = students.get(0);
System.out.println("添加了学生:" + temp2.id + ":" + temp2.name);
// 对象数组的形式添加
Student[] student = {new Student("3", "王五"), new Student("4", "马六")};
// Arrays类包含用来操作数组(比如排序和搜索)的各种方法,asList() 方法用来返回一个受指定数组支持的固定大小的列表
students.addAll(Arrays.asList(student));
Student temp3 = students.get(2);
Student temp4 = students.get(3);
System.out.println("添加了学生:" + temp3.id + ":" + temp3.name);
System.out.println("添加了学生:" + temp4.id + ":" + temp4.name);
Student[] student2 = {new Student("5", "周七"), new Student("6", "赵八")};
students.addAll(2, Arrays.asList(student2));
Student temp5 = students.get(2);
Student temp6 = students.get(3);
System.out.println("添加了学生:" + temp5.id + ":" + temp5.name);
System.out.println("添加了学生:" + temp6.id + ":" + temp6.name);
}
/**
* 取得List中的元素的方法
*/
public void testGet() {
int size = students.size();
for (int i = 0; i < size; i++) {
Student st = students.get(i);
System.out.println("学生:" + st.id + ":" + st.name);
}
}
/**
* 通过迭代器来遍历
* 迭代器的工作是遍历并选择序列中的对象,Java 中 Iterator 只能单向移动
*/
public void testIterator() {
// 通过集合的iterator方法,取得迭代器实例
Iterator<Student> it = students.iterator();
System.out.println("有如下学生(通过迭代器访问):");
while (it.hasNext()) {
Student st = it.next();
System.out.println("学生" + st.id + ":" + st.name);
}
}
/**
* 通过for each 方法访问集合元素
*
*/
public void testForEach() {
System.out.println("有如下学生(通过for each):");
for (Student obj : students) {
Student st = obj;
System.out.println("学生:" + st.id + ":" + st.name);
}
//使用java8 Steam将学生排序后输出
students.stream()//创建Stream
//通过学生id排序
.sorted(Comparator.comparing(x -> x.id))
//输出
.forEach(System.out::println);
}
/**
* 修改List中的元素
*
*/
public void testModify() {
students.set(4, new Student("3", "吴酒"));
}
/**
* 删除List中的元素
*
*/
public void testRemove() {
Student st = students.get(4);
System.out.println("我是学生:" + st.id + ":" + st.name + ",我即将被删除");
students.remove(st);
System.out.println("成功删除学生!");
testForEach();
}
public static void main(String[] args) {
ListTest lt = new ListTest();
lt.testAdd();
lt.testGet();
lt.testIterator();
lt.testModify();
lt.testForEach();
lt.testRemove();
}
}
实验结果
实验总结
- 注意, 需要输入’时, 是输入”’;
- ArrayList 该类也是实现了List的接口,实现了可变大小的数组,随机访问和遍历元素时,提供更好的性能。该类也是非同步的,在多线程的情况下不要使用。ArrayList 增长当前长度的50%,插入删除效率低。
实验 91 Vector使用实验
实验项目
- Vector使用实验
实验需求
Vector可以实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件。
知识点
集合
Vector
Vector
- Vector的大小是可以增加或者减小的,以便适应创建Vector后进行添加或者删除操作。
- 在iterator实现了Iterator接口后,相当于把一个Collection容器的所有对象,做成一个线性表(List),而iterator本身是一个指针,开始时位于第一个元素之前。
实验内容
在 /home/project 目录下创建一个 VectorTest 类
package com.lanqiao.test;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Vector;
public class VectorTest {
/**
* @param args
*/
public static void main(String[] args) {
Vector<String> v = new Vector<String>();
v.addElement("abc1");
v.addElement("abc2");
v.addElement("abc3");
v.addElement("abc4");
Enumeration en = v.elements(); //枚举类型
while(en.hasMoreElements()){//枚举的迭代方法
System.out.println("nextelment:"+en.nextElement());
}
Iterator it = v.iterator();
while(it.hasNext()){
System.out.println("next:"+it.next());
}
}
}
实验结果
实验总结
- Vector主要用在事先不知道数组的大小,或者只是需要一个可以改变大小的数组的情况。扩容时直接是原来的2倍。
实验 92 LinkedList使用实验
实验项目
- LinkedList使用实验
实验需求
LinkedList 该类实现了List接口,允许有null(空)元素。主要用于创建链表数据结构,该类没有同步方法,如果多个线程同时访问一个List,则必须自己实现访问同步,解决方法就是在创建List时候构造一个同步的List。
知识点
集合
List
实验内容
LinkedList
LinkedList集合也是List的一个实现类,它的底层是一个双向链表结构,这个结构的特点是查询慢,增删快。
首先在 /home/project 目录下创建一个 Person 类
package com.lanqiao.vo;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Person {
private String p_no;
private String name;
public String getP_no() {
return p_no;
}
public void setP_no(String p_no) {
this.p_no = p_no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "名人 [p_no=" + p_no + ", name=" + name + "]";
}
}
接下来创建一个 ListTest 类
package com.lanqiao.test;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import com.lanqiao.vo.Person;
public class ListTest {
public static void main(String[] args) {
List<Person> list=new ArrayList<Person>();
// List<Person> list1=new LinkedList<Person>();
Person p1=new Person();
p1.setP_no("01");
p1.setName("小明");
list.add(p1);
//查询
// for(int i=0;i<list.size();i++){
// System.out.println(list.get(i));
// }
//增强for循环
// for(Person p:list){
// System.out.println(p);
// }
//迭代器
Iterator<Person> it=list.iterator();
while(it.hasNext()){//判断数据是否存在
System.out.println(it.next());
}
}
}
实验结果
实验总结
- LinkedList 和 ArrayList 一样,都实现了 List 接口,但其内部的数据结构有本质的不同
实验 93 Collections使用实验
实验项目
- Collections使用实验
实验需求
java.util.Collections 是一个工具类,他包含了大量对集合进行操作的静态方法。
知识点
集合
Collections
实验内容
在 /home/project 目录下创建 CollectionsDemo.java:
package com.lanqiao.vo;
import static java.lang.System.out;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CollectionsDemo{
public static void main(String[] args){
//创建一个空List
List<Integer> list =new ArrayList<Integer>();
//赋值
list.add(3);
list.add(5);
list.add(7);
list.add(9);
list.add(12);
out.println("初始顺序:");
list.forEach(v -> out.print(v + "t"));
//打乱顺序
Collections.shuffle(list);
out.println("n打乱顺序:");
list.forEach(v -> out.print(v + "t"));
//反转
Collections.reverse(list);
out.println("n反转集合:");
list.forEach(v -> out.print(v + "t"));
//第一个位和最后一位交换
Collections.swap(list,0,list.size()-1);
out.println("n交换第一位和最后一位:");
list.forEach(v -> out.print(v + "t"));
//按自然升序排序
Collections.sort(list);
out.println("nSort排序后:");
list.forEach(v -> out.print(v + "t"));
//二分查找, 必须排序后
out.println("n二分查找数值7的位置:" + Collections.binarySearch(list, 7));
//返回线程安全的list
List<Integer> synchronizedList = Collections.synchronizedList(list);
}
}
实验结果
实验总结
Collections集合接口:
- 没有约束元素是否重复。
- 定义了集合运算等基本行为。
- 是集合的根接口。
实验 94 Comparable与Comparator
实验项目
- Comparable与Comparator
实验需求
Comparable 是排序接口。若一个类实现了Comparable接口,就意味着“该类支持排序”。Comparator 是比较器接口。
知识点
集合
Comparable
Comparator
实验内容
Comparable接口
- “实现Comparable接口的类的对象”可以用作“有序映射(如TreeMap)”中的键或“有序集合(TreeSet)”中的元素,而不需要指定比较器。
- 通过“实现Comparator类来新建一个比较器”,然后通过该比较器对类进行排序。
Comparable 接口仅仅只包括一个函数,它的定义如下:
package com.lanqiao.vo;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
说明: 假设我们通过 x.compareTo(y) 来“比较x和y的大小”。若返回“负数”,意味着“x比y小”;返回“零”,意味着“x等于y”;返回“正数”,意味着“x大于y”。
Comparator 接口仅仅只包括两个个函数,它的定义如下:
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
说明: (01) 若一个类要实现Comparator接口:它一定要实现compareTo(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数。
为什么可以不实现 equals(Object obj) 函数呢? 因为任何类,默认都是已经实现了equals(Object obj)的。 Java中的一切类都是继承于java.lang.Object,在Object.java中实现了equals(Object obj)函数;所以,其它所有的类也相当于都实现了该函数。
(02) int compare(T o1, T o2) 是“比较o1和o2的大小”。返回“负数”,意味着“o1比o2小”;返回“零”,意味着“o1等于o2”;返回“正数”,意味着“o1大于o2”。
实验总结
- Comparable是排序接口;若一个类实现了Comparable接口,就意味着“该类支持排序”。 而Comparator是比较器;我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
- 我们不难发现:Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
实验 95 Arrays使用实验
实验项目
- Arrays使用实验
实验需求
java.util.Arrays 类能方便地操作数组,它提供的所有方法都是静态的。
知识点
集合
Arrays
实验内容
Arrays方法
具体说明请查看下表:
在 /home/project 目录下创建 Test.java`:
package com.lanqiao.vo;
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
int a[]={1,8,9,2};
Arrays.sort(a);
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
}
实验结果
实验总结
Arrays具有以下功能:
- 给数组赋值:通过 fill 方法。
- 对数组排序:通过 sort 方法,按升序。
- 比较数组:通过 equals 方法比较数组中元素值是否相等。
- 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。
实验 96 HashMap使用实验
实验项目
- HashMap使用实验
实验需求
HashMap是基于哈希表的 Map 接口的一个重要实现类。
知识点
集合
HashMap
实验内容
HashMap方法
具体说明请查看下表:
在 /home/project/ 目录下创建一个 Course 类:
// Course.java
public class Course {
public String id;
public String name;
public Course(String id, String name){
this.id = id;
this.name = name;
}
}
在 /home/project/ 目录下创建一个 MapTest 类:
// MapTest.java
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.Set;
public class MapTest {
/**
* 用来承装课程类型对象
*/
public Map<String, Course> courses;
/**
* 在构造器中初始化 courses 属性
* @param args
*/
public MapTest() {
this.courses = new HashMap<String, Course>();
}
/**
* 测试添加:输入课程 ID,判断是否被占用
* 若未被占用,输入课程名称,创建新课程对象
* 并且添加到 courses 中
* @param args
*/
public void testPut() {
//创建一个 Scanner 对象,用来获取输入的课程 ID 和名称
Scanner console = new Scanner(System.in);
for(int i = 0; i < 3; i++) {
System.out.println("请输入课程 ID:");
String ID = console.next();
//判断该 ID 是否被占用
Course cr = courses.get(ID);
if(cr == null){
//提示输入课程名称
System.out.println("请输入课程名称:");
String name = console.next();
//创建新的课程对象
Course newCourse = new Course(ID,name);
//通过调用 courses 的 put 方法,添加 ID-课程映射
courses.put(ID, newCourse);
System.out.println("成功添加课程:" + courses.get(ID).name);
}
else {
System.out.println("该课程 ID 已被占用");
continue;
}
}
}
/**
* 测试 Map 的 keySet 方法
* @param args
*/
public void testKeySet() {
//通过 keySet 方法,返回 Map 中的所有键的 Set 集合
Set<String> keySet = courses.keySet();
//遍历 keySet,取得每一个键,在调用 get 方法取得每个键对应的 value
for(String crID: keySet) {
Course cr = courses.get(crID);
if(cr != null){
System.out.println("课程:" + cr.name);
}
}
}
/**
* 测试删除 Map 中的映射
* @param args
*/
public void testRemove() {
//获取从键盘输入的待删除课程 ID 字符串
Scanner console = new Scanner(System.in);
while(true){
//提示输出待删除的课程 ID
System.out.println("请输入要删除的课程 ID!");
String ID = console.next();
//判断该 ID 是否对应的课程对象
Course cr = courses.get(ID);
if(cr == null) {
//提示输入的 ID 并不存在
System.out.println("该 ID 不存在!");
continue;
}
courses.remove(ID);
System.out.println("成功删除课程" + cr.name);
break;
}
}
/**
* 通过 entrySet 方法来遍历 Map
* @param args
*/
public void testEntrySet() {
//通过 entrySet 方法,返回 Map 中的所有键值对
Set<Entry<String,Course>> entrySet = courses.entrySet();
for(Entry<String,Course> entry: entrySet) {
System.out.println("取得键:" + entry.getKey());
System.out.println("对应的值为:" + entry.getValue().name);
}
}
/**
* 利用 put 方法修改Map 中的已有映射
* @param args
*/
public void testModify(){
//提示输入要修改的课程 ID
System.out.println("请输入要修改的课程 ID:");
//创建一个 Scanner 对象,去获取从键盘上输入的课程 ID 字符串
Scanner console = new Scanner(System.in);
while(true) {
//取得从键盘输入的课程 ID
String crID = console.next();
//从 courses 中查找该课程 ID 对应的对象
Course course = courses.get(crID);
if(course == null) {
System.out.println("该 ID 不存在!请重新输入!");
continue;
}
//提示当前对应的课程对象的名称
System.out.println("当前该课程 ID,所对应的课程为:" + course.name);
//提示输入新的课程名称,来修改已有的映射
System.out.println("请输入新的课程名称:");
String name = console.next();
Course newCourse = new Course(crID,name);
courses.put(crID, newCourse);
System.out.println("修改成功!");
break;
}
}
public static void main(String[] args) {
MapTest mt = new MapTest();
mt.testPut();
mt.testKeySet();
mt.testRemove();
mt.testModify();
mt.testEntrySet();
}
}
实验结果
实验总结
- HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 该类实现了Map接口,根据键的HashCode值存储数据,具有很快的访问速度,最多允许一条记录的键为null,不支持线程同步。
实验 97 HashTable遍历实验
实验项目
- HashTable遍历实验
实验需求
HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
知识点
集合
HashTable
实验内容
HashTable方法
具体说明请查看下表:
在 /home/project/ 目录下创建一个 Course 类:
// Course.java
public class Course {
public String id;
public String name;
public Course(String id, String name){
this.id = id;
this.name = name;
}
}
在 /home/project/ 目录下创建一个 MapTest 类:
// MapTest.java
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.Set;
public class MapTest {
/**
* 用来承装课程类型对象
*/
public Map<String, Course> courses;
/**
* 在构造器中初始化 courses 属性
* @param args
*/
public MapTest() {
this.courses = new Hashtable<String, Course>();
}
/**
* 测试添加:输入课程 ID,判断是否被占用
* 若未被占用,输入课程名称,创建新课程对象
* 并且添加到 courses 中
* @param args
*/
public void testPut() {
//创建一个 Scanner 对象,用来获取输入的课程 ID 和名称
Scanner console = new Scanner(System.in);
for(int i = 0; i < 3; i++) {
System.out.println("请输入课程 ID:");
String ID = console.next();
//判断该 ID 是否被占用
Course cr = courses.get(ID);
if(cr == null){
//提示输入课程名称
System.out.println("请输入课程名称:");
String name = console.next();
//创建新的课程对象
Course newCourse = new Course(ID,name);
//通过调用 courses 的 put 方法,添加 ID-课程映射
courses.put(ID, newCourse);
System.out.println("成功添加课程:" + courses.get(ID).name);
}
else {
System.out.println("该课程 ID 已被占用");
continue;
}
}
}
/**
* 测试 Map 的 keySet 方法
* @param args
*/
public void testKeySet() {
//通过 keySet 方法,返回 Map 中的所有键的 Set 集合
Set<String> keySet = courses.keySet();
//遍历 keySet,取得每一个键,在调用 get 方法取得每个键对应的 value
for(String crID: keySet) {
Course cr = courses.get(crID);
if(cr != null){
System.out.println("课程:" + cr.name);
}
}
}
/**
* 测试删除 Map 中的映射
* @param args
*/
public void testRemove() {
//获取从键盘输入的待删除课程 ID 字符串
Scanner console = new Scanner(System.in);
while(true){
//提示输出待删除的课程 ID
System.out.println("请输入要删除的课程 ID!");
String ID = console.next();
//判断该 ID 是否对应的课程对象
Course cr = courses.get(ID);
if(cr == null) {
//提示输入的 ID 并不存在
System.out.println("该 ID 不存在!");
continue;
}
courses.remove(ID);
System.out.println("成功删除课程" + cr.name);
break;
}
}
/**
* 通过 entrySet 方法来遍历 Map
* @param args
*/
public void testEntrySet() {
//通过 entrySet 方法,返回 Map 中的所有键值对
Set<Entry<String,Course>> entrySet = courses.entrySet();
for(Entry<String,Course> entry: entrySet) {
System.out.println("取得键:" + entry.getKey());
System.out.println("对应的值为:" + entry.getValue().name);
}
}
/**
* 利用 put 方法修改Map 中的已有映射
* @param args
*/
public void testModify(){
//提示输入要修改的课程 ID
System.out.println("请输入要修改的课程 ID:");
//创建一个 Scanner 对象,去获取从键盘上输入的课程 ID 字符串
Scanner console = new Scanner(System.in);
while(true) {
//取得从键盘输入的课程 ID
String crID = console.next();
//从 courses 中查找该课程 ID 对应的对象
Course course = courses.get(crID);
if(course == null) {
System.out.println("该 ID 不存在!请重新输入!");
continue;
}
//提示当前对应的课程对象的名称
System.out.println("当前该课程 ID,所对应的课程为:" + course.name);
//提示输入新的课程名称,来修改已有的映射
System.out.println("请输入新的课程名称:");
String name = console.next();
Course newCourse = new Course(crID,name);
courses.put(crID, newCourse);
System.out.println("修改成功!");
break;
}
}
public static void main(String[] args) {
MapTest mt = new MapTest();
mt.testPut();
mt.testKeySet();
mt.testRemove();
mt.testModify();
mt.testEntrySet();
}
}
实验结果
实验总结
- HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。
- HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。
实验 98 TreeMap使用实验
实验项目
- TreeMap使用实验
实验需求
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
知识点
集合
TreeMap
实验内容
TreeMap方法
TreeMap 提供了三个常用的构造函数,说明如下:
● TreeMap()
使用该构造函数,TreeMap中的key按照自然排序进行排列。
● TreeMap(Map copyFrom)
使用该构造函数,用指定的Map填充TreeMap,TreeMap中的key按照自然排序进行排列。
● TreeMap(Comparator comparator)
使用该构造函数,指定元素排序所用的比较器,key排列顺序由比较器指定。
在 /home/project/ 目录下创建一个 MapTest 类:
import java.util.*;
public class MapTest{
public static void main(String[] args){
TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(1, "a");
treeMap.put(2, "b");
treeMap.put(3, "c");
treeMap.put(4, "d"); // treeMap: {1 = a, 2 = b, 3 = c, 4 = d}
treeMap.remove(4); //treeMap: {1=a, 2=b, 3=c}
int sizeOfTreeMap = treeMap.size(); //sizeOfTreeMap: 3
treeMap.replace(2, "e"); // treeMap: {1=a, 2=e, 3=c}
Map.Entry entry = treeMap.firstEntry(); //entry: 1 -> a
Integer key = treeMap.firstKey(); //key: 1
entry = treeMap.lastEntry(); //entry: 3 ->c
key = treeMap.lastKey(); //key: 3
String value = treeMap.get(3); //value: c
SortedMap sortedMap = treeMap.headMap(2); //sortedMap: {1=a}
sortedMap = treeMap.subMap(1, 3); //sortedMap: {1=a, 2=e}
Set setOfEntry = treeMap.entrySet(); //setOfEntry: [1=a, 2=e, 3=c]
Collection<String> values = treeMap.values(); //values: [a, e, c]
treeMap.forEach((integer, s) ->
System.out.println(integer + "->" + s));
}
}
实验结果
实验总结
- TreeMap是基于红黑树的实现的排序Map,对于增删改查以及统计的时间复杂度都控制在O(logn)的级别上,相对于HashMap和LikedHashMap的统计操作的(最大的key,最小的key,大于某一个key的所有Entry等等)时间复杂度O(n)具有较高时间效率。
实验 99 自动拆箱和装箱实验
实验项目
- 自动拆箱和装箱实验
实验需求
基本数据类型及其包装类,我们都知道Java是一种面向对象的语言,但是Java中的基本数据类型是不面向对象的,这时在使用中便会存在诸多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的包装类(Wrapper Class)。
知识点
- 基本数据类型
- 包装类
实验内容
基本类型对应的包装类
在 /home/project/ 目录下创建一个 Test 类:
// Test.java
public class MapTest {
public static void main(String[] args){
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
实验结果
实验总结
- 装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
- 第二个之所以会返回false, 原因是:
- 像Byte,Integer和Long这些包装类都缓存在数值在-128-+127之间的对象, 自动装箱的时候 如果对象值在此范围之内, 则直接返回缓存的对象, 只有在缓存中没有的时候再去创建一个对象
- 当第一次比较i1和i2这两个对象的时候, 因为其值在-128~+127之间, 所以这两个对象都是直接返回的缓存对象, 使用"=="比较时结果为true. 而第二次比较i3和i4这两个对象时, 其值超出了-128~+127的范围, 需要通过new方法创建两个新的包装类对象, 所以再使用"=="比较时结果为false
实验 100 泛型使用实验
实验项目
- 泛型使用实验
实验需求
泛型即参数化类型,也就是说数据类型变成了一个可变的参数,在不使用泛型的情况下,参数的数据类型都是写死了的,使用泛型之后,可以根据程序的需要进行改变。
知识点
泛型
实验内容
定义泛型的规则:
- 只能是引用类型,不能是简单数据类型。
- 泛型参数可以有多个。
- 可以用使用 extends 语句或者 super 语句 如 表示类型的上界,
T
只能是superClass
或其子类, 表示类型的下界,K 只能是 childClass 或其父类。 - 可以是通配符类型,比如常见的 Class。单独使用 ?可以表示任意类型。也可以结合 extends 和 super 来进行限制。
在 /home/project/ 目录下新建一个类 TestDemo.java。
/*
使用T代表类型,无论何时都没有比这更具体的类型来区分它。如果有多个类型参数,我们可能使用字母表中T的临近的字母,比如S。
*/
class Test<T> {
private T ob;
/*
定义泛型成员变量,定义完类型参数后,可以在定义位置之后的方法的任意地方使用类型参数,就像使用普通的类型一样。
注意,父类定义的类型参数不能被子类继承。
*/
//构造函数
public Test(T ob) {
this.ob = ob;
}
//getter 方法
public T getOb() {
return ob;
}
//setter 方法
public void setOb(T ob) {
this.ob = ob;
}
public void showType() {
System.out.println("T的实际类型是: " + ob.getClass().getName());
}
}
public class TestDemo {
public static void main(String[] args) {
// 定义泛型类 Test 的一个Integer版本
Test<Integer> intOb = new Test<Integer>(88);
intOb.showType();
int i = intOb.getOb();
System.out.println("value= " + i);
System.out.println("----------------------------------");
// 定义泛型类Test的一个String版本
Test<String> strOb = new Test<String>("Hello Gen!");
strOb.showType();
String s = strOb.getOb();
System.out.println("value= " + s);
}
}
实验结果
实验总结
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的)。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。
实验 101 增强for和可变参数实验
实验项目
- 增强for和可变参数实验
实验需求
Java5 引入了一种主要用于数组的增强型 for 循环。可变参数:适用于参数个数不确定,类型确定的情况,java把可变参数当做数组处理。
知识点
基本数据类型
for
实验内容
Java 增强 for 循环语法格式如下:
for(声明语句 : 表达式) {
//代码句子
}
声明语句:声明新的局部变量,该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句块,其值与此时数组元素的值相等。
表达式:表达式是要访问的数组名,或者是返回值为数组的方法。
在 /home/project/ 目录下创建一个 Test 类:
// Test.java
public class Test {
public static void main(String args[]){
int [] numbers = {10, 20, 30, 40, 50};
for(int x : numbers ){
System.out.print( x );
System.out.print(",");
}
System.out.print("n");
String [] names ={"James", "Larry", "Tom", "Lacy"};
for( String name : names ) {
System.out.print( name );
System.out.print(",");
}
}
}
编译运行:
public class Varable {
public static void main(String[] args) {
System.out.println(add(2 , 3 ));
System.out.println(add( 2 , 3 , 5 ));
}
public static int add( int x, int … args) {
int sum = x;
for (int i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
}
编译运行:
实验总结
- 当可变参数个数多余一个时,必将有一个不是最后一项,所以只支持有一个可变参数。因为参数个数不定,所以当其后边还有相同类型参数时,java无法区分传入的参数属于前一个可变参数还是后边的参数,所以只能让可变参数位于最后一项。
实验 102 图书馆项目改造数据存储至集合中
实验项目
- 图书馆项目改造数据存储至集合中
实验需求
图书管理系统,是一个由人、计算机等组成的能进行管理信息的收集、传递、加工、保存、维护和使用的系统。利用信息控制企业的行为;帮助企业实现其规划目标。
知识点
集合
面向对象
实验内容
实验设计
整个集合框架就围绕一组标准接口而设计。你可以直接使用这些接口的标准实现,诸如: LinkedList, HashSet, 和 TreeSet 等,除此之外你也可以通过这些接口实现自己的集合。
首先在 /home/project 目录下创建一个 Book 类
import java.text.SimpleDateFormat;
import java.util.Date;
public class Book{
private String b_no;
private String name;
private String writer;
private double price;
private Date date;
public void setB_no(String b_no){
this.b_no = b_no;
}
public String getB_no(){
return b_no;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
public void setWriter(String writer){
this.writer = writer;
}
public String getWriter(){
return writer;
}
public void setPrice(Double price){
this.price = price;
}
public Double getPrice(){
return price;
}
public void setDate(Date date){
this.date = date;
}
public Date getDate(){
return date;
}
@Override
public String toString(){
return "Book [b_no=" + b_no + ",name=" + name + ",writer=" + writer +
",price=" + price + ",date=" + date +
new SimpleDateFormat("yyyy-MM-dd").format(date) + "]";
}
@Override
public int hashCode(){
final int prime = 31;
int result = 1;
result = prime * result + ((b_no == null) ? 0 : b_no.hashCode());
result = prime * result + ((date == null) ? 0 : date.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
long temp;
temp = Double.doubleToLongBits(price);
result = prime * result + (int)(temp ^ (temp >>> 32));
result = prime * result + ((writer == null) ? 0 : writer.hashCode());
return result;
}
@Override
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj == null){
return false;
}
if(getClass() != obj.getClass()){
return false;
}
Book other = (Book)obj;
if(b_no == null){
if(other.b_no != null){
return false;
}
}else if(!b_no.equals(other.b_no)){
return false;
}
if(date == null){
if(other.date != null){
return false;
}
}else if(!date.equals(other.date)){
return false;
}
if(name == null){
if(other.name != null){
return false;
}
}else if(!name.equals(other.name)){
return false;
}
if(writer == null){
if(other.writer != null){
return false;
}
}else if(!writer.equals(other.writer)){
return false;
}
return true;
}
}
在 /home/project/ 创建一个学生类 Test.java:
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Test{
public static void main(String[] args){
Set<Book> set = new HashSet<Book>();
Book b1 = new Book();
b1.setB_no("001");
b1.setName("格林童话");
b1.setWriter("格林");
b1.setPrice(19.8);
b1.setDate(new Date());
set.add(b1);
Book b2 = new Book();
b2.setB_no("002");
b2.setName("伊索寓言");
b2.setWriter("伊索");
b2.setPrice(29.8);
b2.setDate(new Date());
set.add(b2);
// for(Book b: set){
// System.out.println(b);
// }
Iterator<Book> it = set.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
}
实验结果
实验总结
- Java集合框架为程序员提供了预先包装的数据结构和算法来操纵他们。
集合是一个对象,可容纳其他对象的引用。 - 集合接口声明对每一种类型的集合可以执行的操作。
- 集合框架的类和接口均在java.util包中。
(十三)IO和NIO
实验 103 文件遍历实验
实验项目
- 文件遍历实验
实验需求
使用java.io中学到的知识,用递归遍历一个已知文件夹,打印出该文件夹的名字。
知识点
java.io中的File
实验内容
文件操作
完成该实验需要先学习File的相关知识。
Java 使用 File 类来直接处理文件和文件系统。Java 中的目录当成 File 对待,它具有附加的属性——一个可以被 list() 方法检测的文件名列表。
构造方法:
常用方法:
File类更多的方法可以查看官方API
完成实验前先准备好需要遍历的文件目录,在/home/目录下创建如下目录:
src
main
java
resources
A.java
B.java
C.java
test
java
D.java
E.java
下面开始进行实验。
文件遍历实验
提示:使用递归实现。
在 src目录下新建包exp01, 再在这个包里新建源代码文件 PrintDirTree.java:

实验结果
实验总结
- File类为java.io中常用的类,它既可以表示一个文件也可以表示一个文件夹。File类有很多个属性和方法来实现相应的操作。大家在学习的时候只需要记住常用的一些属性和方法即可。
实验 104 字节流文件分割实验
实验项目
- 字节流文件分割实验
实验需求
在 /home/shiyanlou/目录下新建FileCut.java,你需要实现以下需求:
- 从控制台读取一个数值 n。
- 在 /home/shiyanlou/project 目录下新建一个文本文件 cut.txt,再新建一个file.txt文件, 填入任意内容,尽量多输入一些字符。
- 将 file.txt 文件平均分割,每份文件大小为 n 字节。
- 分割后的文件分别命名为 cut1.txt、cut2.txt … cutn.txt 保存在 /home/project 目录下。
知识点
File类
字节流
实验内容
字节流
字节流主要操作 byte 类型数据,以 byte 数组为准,java 中每一种字节流的基本功能依赖于基本类 InputStream 和 Outputstream,他们是抽象类,不能直接使用。字节流能处理所有类型的数据(如图片、avi 等)。
InputStream
InputStream 是所有表示字节输入流的父类,继承它的子类要重新定义其中所定义的抽象方法。InputStream 是从装置来源地读取数据的抽象表示,例如 System 中的标准输入流 in 对象就是一个 InputStream 类型的实例。
InputStream 类方法:
在 InputStream 类中,方法 read() 提供了三种从流中读数据的方法:
- int read():从输入流中读一个字节,形成一个 0~255 之间的整数返回(是一个抽象方法)
- int read(byte b[]):从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b中。
- int read(byte b[],int off,int len):从输入流中读取长度为 len 的数据,写入数组 b 中从索引 off 开始的位置,并返回读取得字节数。
对于这三个方法,若返回 -1,表明流结束,否则,返回实际读取的字符数。
OutputStream
OutputStream 是所有表示位输出流的类之父类。子类要重新定义其中所定义的抽象方法,OutputStream 是用于将数据写入目的地的抽象表示。例如 System 中的标准输出流对象 out 其类型是 java.io.PrintStream,这个类是 OutputStream 的子类。
OutputStream 类方法:
编程实例:
下面开始进行实验。
提示:获取文件所占字节大小,根据字节平均分割文件。
在 /home/shiyanlou/project/ 目录下新建源代码文件 FileCut.java:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;
public class FileCut {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
File file = new File("/home/shiyanlou/project/file.txt");
//需要分割的文件份数
int num;
//如果不能整除,那么需要多加一个文件,用于保存剩余多数据
if(file.length() % n == 0) {
num = (int) (file.length() / n);
}else {
num = (int) (file.length() / n) + 1;
}
try {
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[(int)file.length()];
//读取文件到bytes
fileInputStream.read(bytes);
fileInputStream.close();
for(int i = 1; i <= num; i++) {
//文件名
String fileName = "/home/shiyanlou/project/cut" + i + ".txt";
FileOutputStream fileOutputStream = new FileOutputStream(fileName);
//最后一份文件需要特殊处理, 因为它多大小不是n
if(i == num) {
//(file.length()-n*(i-1))文件多总字节数, 再减去前面已经读取多字节数, 就是
//剩余多字节数
fileOutputStream.write(bytes, n * (i - 1), (int)(file.length() - n * (i - 1)));
}else {
fileOutputStream.write(bytes, n * (i - 1), n);
}
fileOutputStream.flush();
fileOutputStream.close();
}
}catch(IOException e) {
e.printStackTrace();
}
}
}
实验结果
-
输入数字:
7 -
file.txt文件中内容:
- cut1.txt文件中内容 :
实验总结
- 一般来说,很少直接实现 InputStream 或 OutputStream 上的方法,因为这些方法比较低级,通常会实现它们的子类例如FileInputStream和FileOutputStream。
实验 105 字符流拷贝文件实验
实验项目
- 字符流拷贝文件实验
实验需求
实验内容
实验结果
实验总结
1.
实验 106 控制台输入流转换实验
实验项目
- 控制台输入流转换实验
实验需求
字节流和字符流是JavaIO中最重要的两种基础流类型。这两种类型的流是可以相互转换的。本实验的目的是用户可以从控制台输入字节流,经过转换后变成字符在控制台输出。
知识点
- InputStreamReader
//缺省规范说明
InputStreamReader(InputStream in);
//指定规范 enc
InputStreamReader(InputStream in, String enc);
//缺省规范说明
OutputStreamWriter(OutputStream out);
//指定规范 enc
OutputStreamWriter(OutputStream out, String enc);
下面让我们开始实验。
实验内容
文件操作
InputStreamReader 和 OutputStreamWriter 是 java.io 包中用于处理字符流的最基本的类,用来在字节流和字符流之间作为中介:从字节输入流读入字节,并按编码规范转换为字符;往字节输出流写字符时先将字符按编码规范转换为字节。使用这两者进行字符处理时,在构造方法中应指定一定的平台规范,以便把以字节方式表示的流转换为特定平台上的字符表示。
文件遍历实验
提示:注意流的类型。
在 /home/project/ 目录下新建源代码文件 InputStreamReaderDemo.java:
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class InputStreamReaderDemo {
public static void main(String[] args) {
System.out.println("请输入数据:");
InputStream inputStream = System.in;//字节流
//字节流转字符流对象InputStreamReader
InputStreamReader in = new InputStreamReader(inputStream);
try {
int c;
while ((c = in.read()) != -1) {
System.out.print((char) c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:因为此实验是从控制台输入得到数据,所以不会出现c=-1的情况。也就是无法像读取文件一样读到文件的最后。显示的效果是程序不会自动结束,需要手动关闭。
实验结果
实验总结
- 转换流可以实现字节流和字符流的双向转换。
PS:可以思考一下如何让程序自动停下来。
实验 107 缓冲流拷贝图片实验
实验项目
- 缓冲流拷贝图片实验
实验需求
将 /home/project/ 目录下的图片cool1.png,使用字节流拷贝到同级目录下,重命名为cool2.png。
实验前先在目录下准备好图片cool1.png。
知识点
- 字节流
- 缓冲流
实验内容
缓冲流
BufferedInputStream 和 BufferedOutputStream
实现了带缓冲的字节流,下面的例子将缓冲流与文件流相接:
FileInputStream in = new FileInputStream("file.txt");
FileOutputStream out = new FileOutputStream("file2.txt");
//设置输入缓冲区大小为256字节
BufferedInputStream bin = new BufferedInputStream(in,256)
BufferedOutputStream bout = new BufferedOutputStream(out,256)
int len;
byte bArray[] = new byte[256];
len = bin.read(bArray); //len 中得到的是实际读取的长度,bArray 中得到的是数据
对于 BufferedOutputStream,只有缓冲区满时,才会将数据真正送到输出流,但可以使用 flush() 方法人为地将尚未填满的缓冲区中的数据送出。
BufferedReader 和 BufferedWrite
实现了带缓冲的字符流。其构造方法与 BufferedInputStream 和 BufferedOutPutStream 相类似。另外,除了 read() 和 write() 方法外,它还提供了整行字符处理方法:
下面来完成一个图片拷贝的例子。
提示:先思考一下改用字节流还是字符流?
在 /home/project/ 目录下新建源代码文件 BufferFileCopy.java:
import java.io.*;
public class BufferFileCopy {
public static void main(String[] args) {
BufferedInputStream bufferedInputStream = null;
BufferedOutputStream bufferedOutputStream = null;
try{
//找到目标文件
File file = new File("/home/shiyanlou/project/cool1.png");
File descFile = new File("/home/shiyanlou/project/cool2.png");
//建立数据输入输出通道
FileInputStream inputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream(descFile);
//建立缓冲输入输出通道
bufferedInputStream = new BufferedInputStream(inputStream);
bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
//边读边写
int content = 0;
while((content = bufferedInputStream.read()) != -1){//read()方法返回值是读取到的内容
bufferedOutputStream.write(content);
//这里加了flush()的话效率就会变低,不加的话后面必须加close方法,把缓存中的数据输出。
//bufferedOutputStream.flush();
}
}catch(IOException e){
e.printStackTrace();
}finally{
try {
if(bufferedOutputStream != null){
bufferedOutputStream.close();
}
if(bufferedOutputStream != null){
bufferedInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
实验结果
实验总结
- 字节流和字符流都有与之匹配的缓冲流来实现数据缓冲操作。在日常的代码开发中缓冲流是必备的IO操作条件。无论完成那种流的操作都不会只使用基础流,都会用缓冲流来提高IO操作效率。
- PS:大家可以尝试一下不使用缓冲流拷贝图片。感觉一下效率的不同。
实验 108 使用通道I/O拷贝文件实验
实验项目
- 使用通道I/O拷贝文件实验
实验需求
在 /home/shiyanlou/project/ 目录下新建source.sql文件,使用NIO实现将该文件中的内容拷贝到同级目录下的copy.txt文件中。
知识点
- NIO相关类
- 通道FileChannel
- 缓冲区ByteBuffer
实验内容
NIO
Java NIO(New IO)全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。
Java NIO 是面向缓存的、非阻塞的 IO,而标准 IO 是面向流的,阻塞的 IO。
首先理解 NIO 的三个重要概念:
-
Buffer(缓冲区)
Buffer是一个对象,它用来存放即将发送的数据和即将到来的数据。Buffer是NIO核心思想。它与普通流的区别是:普通流IO直接把数据写入或读取到Stream对象中,而NIO是先把读写数据交给Buffer,后在用流处理。Buffer实际上就是一个数组,通常是字节数组,这个数组提供了访问数据的读写等操作属性,如位置,容量,上限等概念。 Buffer针对八种基本数据类型都进行了缓冲封装,例如:ByteBuffer、CharBuffer、IntBuffer等。 -
Channel (通道)
Channel(通道),与Stream(流)的不同之处在于通道是双向的,流只能在一个方向上操作(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行。最关键的是可以和多路复用器结合起来提供状态位,多路复用器可识别Channel所处的状态。 通道分两大类:用于网络读写的SelectableChannel,和用于文件操作的FileChannel。 -
Selector(选择者)
Selector提供选择已经就绪的任务的能力。简单说,就是Selector会不断轮询注册在Selector上的通道(Channel),如果这个通道发生了读写操作,这个通道就会处于就绪状态,会被Selector察觉到,然后通过SelectionKey可以取出就绪的Channel集合,从而进行IO操作。 一个Selector可以负责成千上万的通道,没有上限。这也是JDK使用了epoll代替传统的Select实现,获得连接句柄没有限制。意味着我们只需要一个线程负责Selector的轮询,就可以接入成百上千的客户端,这是JDK NIO库的巨大进步。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIODemo {
public static void main(String[] args) throws IOException {
// 设置输入源 & 输出地 = 文件
String inFile = "/home/shiyanlou/project/source.sql";
String outFile = "/home/shiyanlou/project/copy.txt";
// 1. 获取数据源 和 目标传输地的输入输出流(此处以数据源 = 文件为例)
FileInputStream fin = new FileInputStream(inFile);
FileOutputStream fout = new FileOutputStream(outFile);
// 2. 获取数据源的输入输出通道
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();
// 3. 创建缓冲区对象
ByteBuffer buff = ByteBuffer.allocate(1024);
while (true) {
// 4. 从通道读取数据 & 写入到缓冲区。注意:若读取到该通道数据的末尾,则返回-1。
int r = fcin.read(buff);
if (r == -1) {
break;
}
// 5. 传出数据准备:调用flip()方法
buff.flip();
// 6. 从 Buffer 中读取数据 & 传出数据到通道
fcout.write(buff);
// 7. 重置缓冲区
buff.clear();
}
}
}
实验结果
实验总结
- 本实验只使用了NIO的通道和缓冲区这两个知识点实现了文件拷贝功能。NIO中选择者(Selector)没有用到,因为该知识点是在多线程的程序中使用。后续的实验会涉及到。
实验 109 IO综合实验
实验项目
- IO综合实验
实验需求
统计一个文件/home/project/content.txt中各个字母出现次数。结果在文件/home/project/result.txt中输出,格式内容如下:
A - 8
B - 6
C - 5
...
a - 1
b - 2
c - 9
知识点
javaIO涉及的知识点
实验内容
JavaIO的分类
IO和NIO的区别
提前准备文件content.txt。内容包括a-z,A-Z。
文件字符统计实验
思考:需要使用字节流还是字符流?
分析:
- 不能保存相同的主键值,可以使用HashMap:key-value来实现。
- 先获得该key的value,如果存在key的话value的值加1 。
import java.io.*;
import java.util.HashMap;
import java.util.Set;
public class CalcCharCount {
public static void main(String[] args) {
// 设置输入源 & 输出地
String inFile = "/home/shiyanlou/project/content.txt";
String outFile = "/home/shiyanlou/project/result.txt";
BufferedReader reader = null;
BufferedWriter writer = null;
try {
// 1. 获取数据源 和 目标传输地的输入输出流(此处以数据源 = 文件为例)
reader = new BufferedReader(new FileReader(inFile));
writer = new BufferedWriter(new FileWriter(outFile));
// 创建集合HashMap类存放要保存的key-value
HashMap<Character, Integer> map = new HashMap<>();
// 读取文件
int len = 0;// 每次读取的文件长度
int count = 0;
while ((len = reader.read()) != -1) {
// 每次获取到的字母
char c = (char) len;
//这里使用try catch是因为 map.get(c + ""),第一次get不到东西会出现空指针
try {
count = map.get(c);
} catch (Exception e) {// 什么都不用输出
}
// 如果有它的key值对应的value值要加1
map.put(c, count + 1);
}
// 读完后把结果输出到result.txt文件中
Set<Character> keySet = map.keySet();
for (char key : keySet) {
writer.write(key + "-" + map.get(key));
writer.newLine();
}
System.out.println("运行完成!");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
reader.close();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
content.txt文件内容:
result文件内容:
实验总结
- 考虑下使用字节流可以吗?为什么?请尝试使用字符流再次完成此实验。
(十四)反射机制
实验 110 获取Class类中的信息实验
实验项目
- 获取Class类中的信息实验
实验需求
Java 反射说的是在运行状态中,对于任何一个类,我们都能够知道这个类有哪些方法和属性。对于任何一个对象,我们都能够对它的方法和属性进行调用。我们把这种动态获取对象信息和调用对象方法的功能称之为反射机制。本实验完成获得Class类中中的信息。
知识点
- 反射的Class类
实验内容
Class类
Class 类没有公共构造方法,可以通过以下方法获取 Class 实例。
- Object 提供的 getClass() 方法。
- 类名.Class。
- Class.forName(String className) 方法,className 为类的全限定名。
- 包装类的类名.TYPE
Class 类常用方法:
更多方法请查阅官方文档。
在 /home/project/ 目录下新建 ReflectDemo.java。
该类中有两个属性和两个方法,通过反射打印该类中属性和方法的信息。
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public class ReflectDemo {
public int a;
private int b;
public void fun1() {
}
private void fun2() {
}
public static void main(String[] args) {
Class<ReflectDemo> reflectDemoClass = ReflectDemo.class;
//输出所有的属性名称
for (Field declaredField : reflectDemoClass.getDeclaredFields()) {
//可以通过Modifier将具体的权限信息输出,否则只会显示代表权限的数值
System.out.println("属性:" + declaredField.getName() + " 修饰符:" + Modifier.toString(declaredField.getModifiers()));
}
//输出所有的公有域名称
for (Field field : reflectDemoClass.getFields()) {
System.out.println("公有属性:" + field.getName() + " 修饰符:" + Modifier.toString(field.getModifiers()));
}
//输出类的所有方法名
for (Method declaredMethod : reflectDemoClass.getDeclaredMethods()) {
System.out.println("方法:" + declaredMethod.getName() + " 修饰符:" + Modifier.toString(declaredMethod.getModifiers()));
}
}
}
实验结果
实验总结
- 该练习的重点是Declare关键字的意义,有该关键字表示输出所有属性或者方法,没有该关键字表示只输出共用属性和方法。获得Class类是反射的基础,是第一步要牢牢记住。
实验 111 动态修改属性实验
实验项目
- 动态修改属性实验
实验需求
在获得Class类后,就可以对该类的属性和方法进行调用了。但是在调用之前需要先获得Constructor类来创建对象。所以思路是:获得Class类 -> 获得Constructor类 -> 调用newInstance() -> 获得属性Field -> 修改属性。
知识点
- Constructor类
- Field类
实验内容
Constructor代表类的构造方法。常用的方法为:
Field代表类的成员变量(成员变量也称为类的属性)。
注意:当需要对私有(private)属性进行操作时,需要先授予权限。
在 /home/project/ 目录下新建 Dog.java类。代码内容如下:
package org.lanqiao.reflect;
public class Dog {
public String name = "无名";
private int age = 1;
public int getName(){
return name;
}
public void eat(String food){
System.out.println(name + "在吃" + food);
}
public void sleep(){
System.out.println(name + "在睡觉");
}
}
在 /home/project/ 目录下新建ReflectDog.java类,完成修改默认属性的操作。
package org.lanqiao.reflect;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class ReflectDog {
public static void main(String[] args) {
try {
//获得class类
Class clazz = Class.forName("org.lanqiao.reflect.Dog");
//获得class类的无参构造方法
Constructor constructor = clazz.getConstructor();
//调用newInstance方法实例化对象obj
Object object = constructor.newInstance();
//输出默认值
System.out.println("name默认:" + ((Dog) object).name);
System.out.println("age默认:" + ((Dog) object).getAge());
//获得所有属性
Field fieldName = clazz.getDeclaredField("name");//public
Field fieldAge = clazz.getDeclaredField("age");//private
//修改属性
fieldName.set(object, "旺财");
fieldAge.setAccessible(true);//设置访问权限
fieldAge.set(object, 2);
//输出修改值
System.out.println("name修改:" + ((Dog) object).name);
System.out.println("age修改:" + fieldAge.get(object));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
实验结果
注意:object对象的使用。
实验总结
- 思考:如何调用带参的构造方法呢,在Dog类中添加kind属性,可以尝试一下使用带参构造方法给kind赋值。使用反射调用带参方法获得对象并操作对象。
实验 112 动态调用方法实验
实验项目
- 动态调用方法实验
实验需求
继续前面的实验,获得Class – > 获得Constructor类并调用 – > 获得方法并调用
知识点
- Class类
- Constructor类
- Method类
实验内容
Method代表类的方法。
因为重载方法的存在,在通过Class类获得Method的时候除了方法名外,往往还需要参数来定位所需要的方法。
继续使用 /home/project/ 目录下的 Dog.java类和ReflectDog.java类完成下面实验。
使用反射完成调用Dog类中的eat()方法和sleep()方法。
继续使用 /home/project/ 目录下的 Dog.java类和ReflectDog.java类完成下面实验。
使用反射完成调用Dog类中的eat()方法和sleep()方法。
实验结果
实验总结
- 注意:无参和带参方法的不同调用方式。
- 可以发现获得和调用方法(Method)和属性(Field)的方式是一样的。访问私有(private)修饰的方法时也是需要进行权限设置。
实验 113 操作动态数组实验
实验项目
- 操作动态数组实验
实验需求
Java在创建数组的时候,需要指定数组长度,且数组长度不可变。而java.lang.reflect包下提供了一个Array类,通过这些方法可以创建动态数组,对数组元素进行赋值、取值操作。
知识点
- Class类
- Array类
实验内容
Array类常见的方法:
详细操作方法参见Array API。下面开始实验。
在/home/project/ 目录下创建类 reflectArrayDemo.java,实现通过反射创建字符串类型的数组,长度由用户输入。向数组中添加数据并显示。
import java.lang.reflect.*;
import java.util.Scanner;
public class reflectArrayDemo {
public static void main(String args[]) {
try {
Scanner input = new Scanner(System.in);
Class c = Class.forName("java.lang.String");
System.out.print("请输入班级人数:");
int stuNum = input.nextInt();
//创建长度为stuNum的字符串数组
Object arr = Array.newInstance(c, stuNum);
for (int i = 0; i < Array.getLength(arr); i++) {
System.out.print("请输入学号为" + (i + 1) + "的学生姓名:");
String stuName = input.next();
//使用Array类的set方法给数组赋值
Array.set(arr, i, stuName);
}
//使用Array类获取元素的值
for (int i = 0; i < Array.getLength(arr); i++) {
System.out.println("学号为" + (i + 1) + "的学生姓名为:" + Array.get(arr, i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
实验结果
实验总结
- 反射的Array类为我们提供了一种可以动态操作数组的方式,在一些特定的场合是非常有用的。
(十五)多线程
实验 114 创建启动线程实验
实验项目
- 创建启动线程实验
实验需求
实现两个线程分别输出1000个数字的程序。使用两种方式创建并启动线程,观察线程运行的特点。
知识点
- Thread
实验内容
线程
线程又称之为轻量级进程。一个程序就是一个进程,而一个程序中的任务则被称为线程。一个程序中只有一个任务成为为单线程程序,有多个线程称之为多线程程序。
线程的创建有两种方式:
- 继承 Thread 类
- 实现 Runnable 接口
多线程的功能代码重写run()方法,不同的创建方式决定调用的方式也不相同。下面开始实验。
线程的创建和调用
继承Thread类
在 /home/project/ 目录下新建源代码文件 ThreadDemo.java:
public class ThreadDemo {
public class ThreadDemo {
public static void main(String[] args) {
//继承父类启动
MyThread1 m1 = new MyThread1();
m1.start();
//实现接口启动
Thread m2 = new Thread(new MyThread2());
m2.start();
}
}
//继承父类创建
class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("Thread1 say:" + i);
}
}
}
//实现接口创建
class MyThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("Thread2 say:" + i);
}
}
}
实验结果
实验总结
- 线程创建和启动有实现接口和继承父类两种方式,用已经学过的接口和抽象类的方法分析一下两者的关系。查看JDK中的源码学习一下。
实验 115 线程控制实验
实验项目
- 线程控制实验
实验需求
实验需求如下:设计一个计数功能小程序,每间隔2秒输出1、2、3…一直到100结束。当用户想中止这个计数功能程序时,只要在控制台输入s即可。
知识点
- Thread
- Thread方法
实验内容
线程控制是指使用Thread提供的一些方法可以对线程进行人为的干预的过程。需要注意的是这些控制并不是绝对的,有些控制只能间接的影响线程的运行。
下面开始本实验
在 /home/project/ 目录下新建一个源代码文件 EndingThread.java。
import java.util.Scanner;
public class EndingThread {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("如果想终止输出计数线程,请输入s");
CountThread t = new CountThread();
t.start();
while (true) {
String s = scanner.nextLine();
if (s.equals("s")) {
t.interrupt();//调用方法终端计数线程
break;
}
}
System.out.println("程序结束!");
}
}
//计数功能线程
class CountThread extends Thread {
private int i = 0;
public CountThread() {
super("计数线程");
}
public void run() {
try {
while (i < 100) {
System.out.println(this.getName() + "计数:" + (i + 1));
i++;
sleep(2000);
}
} catch (InterruptedException e) {
System.out.println("线程结束!");//调用interrupt方法后线程会进入异常。
} catch (Exception e) {
e.printStackTrace();
}
}
}
实验结果
实验总结
- 线程控制还有其他的方法如join()、yield()等。这里就不一一例举了。
实验 116 线程优先级实验
实验项目
- 线程优先级实验
实验需求
实验线程优先级控制。创建两个线程A、B。A线程优先级高,B线程优先级低。运行多次查看显示结果。
知识点
- Thread
- setPriority()
实验内容
Java 中的线程优先级的范围是1~10,默认的优先级是5。10极最高。
有时间片轮循机制。“高优先级线程”被分配CPU的概率高于“低优先级线程”。根据时间片轮循调度,所以能够并发执行。无论是是级别相同还是不同,线程调用都不会绝对按照优先级执行,每次执行结果都不一样,调度算法无规律可循,所以线程之间不能有先后依赖关系。
在 /home/project/ 目录下新建一个源代码文件 ThreadPriority.java。
public class ThreadPriority {
public static void main(String[] args) {
Thread A = new ThreadP("ThreadA"); // 新建A线程
Thread B = new ThreadP("ThreadB"); // 新建B线程
A.setPriority(1); // 设置A的优先级为1
B.setPriority(10); // 设置B的优先级为10
A.start(); // 启动A
B.start(); // 启动B
}
}
class ThreadP extends Thread {
public ThreadP(String name) {
super(name);
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()
+ "(" + Thread.currentThread().getPriority() + ")"
+ ", loop " + i);
}
}
}
实验结果
实验总结
- 结论:线程虽然可以通过优先级控制,但是线程调用不会绝对按照优先级执行。
实验 117 线程变量实验
实验项目
- 线程变量实验
实验需求
实验线程变量。设置设置ThreadLocal类型变量,在线程中更新该变量。创建AB两个线程,运行查看结果。
知识点
- 线程变量
- 线程共享变量
ThreadLocal,即线程变量,是一个以 ThreadLocal 对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个 ThreadLocal 对象查询到绑定在这个线程上的一个值。 可以通过 set(T) 方法来设置一个值,在当前线程下再通过 get() 方法获取到原先设置的值。
实验内容
线程变量
在 /home/project/ 目录下新建一个源代码文件 ThreadLocalDemo.java。
public class ThreadLocalDemo {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
//启动2个线程
new Thread(threadDemo).start();
new Thread(threadDemo).start();
}
}
class ThreadDemo implements Runnable {
//使用ThreadLocal提供的静态方法创建一个线程变量 并且初始化值为0
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//get方法获取线程变量值
Integer integer = threadLocal.get();
integer += 1;
//set方法设置线程变量值
threadLocal.set(integer);
System.out.println(integer);
}
}
}
实验结果
实验总结
- 通过控制台的结果可以看到,两个线程之间的变量互不干涉。
- 若是设置线程独占数据,使用线程变量(ThreadLocal)。
实验 118 线程共享数据实验
实验项目
- 线程共享数据实验
实验需求
设置局部变量i ,在线程中对i进行递增操作。创建AB两个线程循环输出数据i,查看结果。
知识点
Thread
实验内容
线程共享数据
上个实验完成的使用ThreadLocal设置线程独占变量。线程还可以共享数据,这是与进程最大的区别。下面实验一下线程共享数据操作。
修改 /home/project/ 目录下文件 ThreadLocalDemo.java。
public class ThreadLocalDemo {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
new Thread(threadDemo).start();
}
}
class ThreadDemo implements Runnable {
private Integer integer = 0;//注意这里的写法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
integer++;
System.out.println(integer);
}
}
}
实验结果
这个时候的 integer 就变成了线程共享变量。如果多运行几次,还有可能出现最后结果是 “18”或“19” 的情况,那是因为如果不做任何处理,线程共享变量都不是线程安全的,也就是说在多线程的情况下,共享变量有可能会出错。
实验总结
- 使用局部变量可以实现多个线程共享数据。但是可能会出现线程安全问题。
思考:若是采取继承的方式创建线程,那么共享数据怎么处理?使用代码尝试实现一下!
实验 119 线程同步实验
实验项目
- 线程同步实验
实验需求
数据存储为10,使用AB两个线程循环输出数据递减值,直到数据为0为止。
知识点
- synchronized
实验内容
线程同步
当多个线程操作同一个对象时,就会出现线程安全问题,被多个线程同时操作的对象数据可能会发生错误。线程同步可以保证在同一个时刻该对象只被一个线程访问。
关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,保证了线程对变量访问的可见性和排他性。它有三种使用方法:
- 对普通方式使用,将会锁住当前实例对象。
- 对静态方法使用,将会锁住当前类的 Class 对象。
- 对代码块使用,将会锁住代码块中的对象。
public class SynchronizedDemo {
private static Object lock = new Object();
public static void main(String[] args) {
//同步代码块 锁住lock
synchronized (lock) {
//doSomething
}
}
//静态同步方法 锁住当前类class对象
public synchronized static void staticMethod(){
}
//普通同步方法 锁住当前实例对象
public synchronized void memberMethod() {
}
}
本实验使用Synchronized对代码块进行编辑。
编译执行结果为:
可以看到数据是错误的。修改代码为:
public class SynchronizedDemo {
private static Integer integer = 10;
private static Object lock = new Object();//声明锁
public static void main(String[] args) {
Thread A = new SynchronizedDemo().new ThreadA("ThreadA"); // 新建A线程
Thread B = new SynchronizedDemo().new ThreadA("ThreadB"); // 新建B线程
A.start(); // 启动A
B.start(); // 启动B
}
class ThreadA extends Thread {
public ThreadA(String name) {
super(name);
}
public void run() {
while (true)
synchronized (lock) {
if (integer > 0) {
System.out.println(Thread.currentThread().getName() + " " + integer--);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.exit(0);
}
}
}
}
}
实验总结
- 使用Synchronized可以实现锁的功能,保证多个线程操作共享数据的准确性。
- java.util.concurrent
java.util.concurrent 包是 java5 开始引入的并发类库,提供了多种在并发编程中的适用工具类。包括原子操作类,线程池,阻塞队列,Fork/Join 框架,并发集合,线程同步锁等。
- Lock 与 Unlock
JUC 中的 ReentrantLock 是多线程编程中常用的加锁方式,ReentrantLock 加锁比 synchronized 加锁更加的灵活,提供了更加丰富的功能。在此撰写实验总结内容。
实验 120 死锁实验
实验项目
- 死锁实验
实验需求
创建两个线程,线程一分别加锁A和锁B,线程二分加锁B和锁A。查看运行结果。
知识点
- Thread
- synchronized
实验内容
死锁
在多线程环境下,锁的使用非常频繁,但是它会带来一下问题,比如死锁。当死锁发生时,系统将会瘫痪。比如两个线程互相等待对方释放锁。
死锁实验
在 /home/project/ 目录下新建一个源代码文件 DeadLockDemo.java。
实验结果
实验总结
- 线程 1 获取了 lockA 的锁后再去获取 lockB 的锁,而此时 lockB 已经被线程 2 获取,同时线程 2 也想获取 lockA,两个线程进这样僵持了下去,谁也不让,造成了死锁。在编程时,应该避免死锁的出现。
- 如何避免死锁:
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
实验 121 火车票销售模拟实验
实验项目
- 火车票销售模拟实验
实验需求
本实验实现火车票的售票操作。要求如下:一共有10张票由三个售票窗口出售,每个售票窗口售票的数量不确定。需要保证数据的准确性。
知识点
- Thread
- synchronized
实验内容
在 /home/project/ 目录下新建一个源代码文件 DeadLockDemo.java。
public class SellTicketSys {
private int ticketLeft = 10;
private int ticketCount = ticketLeft;
private Object lock = new Object();
public static void main(String[] args) {
SellTicketSys sellTicketSys = new SellTicketSys();
Seller s1 = sellTicketSys.new Seller();
Seller s2 = sellTicketSys.new Seller();
Seller s3 = sellTicketSys.new Seller();
new Thread(s1, "售票点一").start();
new Thread(s2, "售票点二").start();
new Thread(s3, "售票点三").start();
}
class Seller implements Runnable {
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticketLeft > 0) {
System.out.println(Thread.currentThread().getName() + "售出第" + (ticketCount + 1 - ticketLeft) + "张车票");
ticketLeft--;
try {
Thread.sleep(200);//模拟售票工作
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("车票全部售完!");
System.exit(0);
}
}
}
}
}
}
运行结果:
多次运行可以发现很多时间都是售票点一卖出了全部的票。这样的程序虽然正确但是体验很差,有可能多次运行都是售票点一卖出所有的票。这时候我们会认为程序出错了,这个问题怎么解决呢?对比一下下面的代码,观察一下两段代码的区别。
public class SellTicketSys {
private int ticketLeft = 10;
private int ticketCount = ticketLeft;
private Object lock = new Object();
public static void main(String[] args) {
SellTicketSys sellTicketSys = new SellTicketSys();
Seller s1 = sellTicketSys.new Seller();
Seller s2 = sellTicketSys.new Seller();
Seller s3 = sellTicketSys.new Seller();
new Thread(s1, "售票点一").start();
new Thread(s2, "售票点二").start();
new Thread(s3, "售票点三").start();
}
class Seller implements Runnable {
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticketLeft > 0) {
System.out.println(Thread.currentThread().getName() + "售出第" + (ticketCount + 1 - ticketLeft) + "张车票");
ticketLeft--;
} else {
System.out.println("车票全部售完!");
System.exit(0);
}
}
try {
Thread.sleep(200);//模拟售票工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
实验总结
- 思考
这时的效果就明显了。为什么会这样呢?但是这样写代码会和业务逻辑有些冲突,sleep是模拟售票过程,这个过程不应该在售票业务流之外。怎么解决这个问题呢?后续试验中的线程协作就可以解决。
- 线程同步的过程中因为有些业务需要暂停当前线程,这时所有的线程都会阻塞。不符合业务模型。可以用线程协作解决这个问题。
实验 122 生产者消费者实验
实验项目
- 生产者消费者实验
实验需求
本实验模拟快餐店生产消费热狗的工作模型。
- 定义一个集合模拟长条容器存放热狗,集合里实际存放Integer,其值代表热狗的编号(热狗编号规则举例:300002代表编号为3的厨师做的第2个热狗),这样能通过集合添加和删除,实现先进先出;
- 以热狗集合作为对象锁,所有对热狗集合的操作(在长条容器中添加、取走热狗)互斥,这样保证不会出现多个顾客同时取最后剩下的一个热狗的情况,也不会出现多个厨师同时添加热狗造成长条容器里热狗数大于10个的情况;
- 当厨师希望往长条容器中添加热狗时,如果发现长条容器中已有10个热狗,则停止做热狗,等待顾客从长条容器中取走热狗的事件发生,以唤醒厨师可以重新进行判断,是否需要做热狗;
- 当顾客希望往长条容器中取走热狗时,如果发现长条容器中已没有热狗,则停止吃热狗,等待厨师往长条容器中添加热狗的事件发生,以唤醒顾客可以重新进行判断,是否可以取走热狗吃;
知识点
- Thread
- synchronized
- wait
- notify
- notifyAll
实验内容
线程协作
Java提供了wait()、notify()、notifyAll()三个方法,解决线程之间协作的问题。这三个方法均是java.lang.Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常。
- void wait():当前线程等待,等待其他线程调用此对象的 notify() 方法或 notifyAll() 方法将其唤醒。
- void notify():唤醒在此对象锁上等待的单个线程。
- void notifyAll():唤醒在此对象锁上等待的所有线程。
下面使用线程协作的方法来实现生产者消费者模型。
在 /home/project/ 目录下新建一个源代码文件 TestProdCons.java。
import java.util.*;
public class TestProdCons {
//定义一个存放热狗的集合,里面存放的是整数,代表热狗编号
private static final List<Integer> hotDogs = new ArrayList<Integer>();
public static void main(String[] args){
for(int i = 1;i <= 3;i++){
new Producer(i).start();
}
for(int i = 1;i <= 5;i++){
new Consumer(i).start();
}
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.exit(0);
}
//生产者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥
private static class Producer extends Thread{
int i = 1;
int pid = -1;
public Producer(int id){
this.pid = id;
}
public void run(){
while(true){
try{
//模拟消耗的时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(hotDogs){
if(hotDogs.size() < 10){
//热狗编号,300002代表编号为3的生产者生产的第2个热狗
hotDogs.add(pid*10000 + i);
System.out.println("生产者" + pid + "生产热狗,编号为:" + pid*10000 + i);
i++;
//唤醒hotDogs所有调用wait()方法的线程
hotDogs.notifyAll();
}else{
try{
System.out.println("热狗数已到10个,等待消费!");
hotDogs.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//消费者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥
private static class Consumer extends Thread {
int cid = -1;
public Consumer(int id){
this.cid = id;
}
public void run(){
while(true){
try{
//模拟消耗的时间
Thread.sleep(200);
}catch(InterruptedException e) {
e.printStackTrace();
}
synchronized (hotDogs) {
if(hotDogs.size() > 0) {
System.out.println("消费者" + this.cid + "正在消费一个热狗,其编号为:" + hotDogs.remove(0));
hotDogs.notifyAll();
}else{
try{
System.out.println("已没有热狗,等待生产!");
hotDogs.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
实验结果
可以看到多个线程协作完成了生产者消费者模型。
实验总结
注意sleep()和wait()的区别:
-
sleep()是Thread类的方法,wait()是Object类的方法。
-
sleep()睡眠时保持对象锁,仍然占有该锁。
-
wait()睡眠时,释放对象锁。
-
wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
实验 123 使用newCachedThreadPool管理并发线程
实验项目
- 使用newCachedThreadPool管理并发线程
实验需求
使用newCachedThreadPool创建一个线程池,并且实现并发操作。
知识点
- newCachedThreadPool
实验内容
线程池
前面介绍了线程的使用,当需要使用线程的时候就去创建一个线程,这样实现起来非常简便,但是这样做会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。下面我们就来详细讲解一下Java的线程池。
Java中实现线程池的核心类为
ThreadPoolExecutor,一般情况并不直接使用这个类而是使用它的拓展类。
Java通过Executors提供四种线程池
-
newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
-
newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
-
newSingleThreadExecutor: 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newCachedThreadPool
当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
优点:执行很多短期异步的小程序或者负载较轻的服务器
下面使用newCachedThreadPool来实现线程池的并发管理。
在 /home/project/ 目录下新建一个源代码文件 NewCachedThreadPoolDemo.java。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NewCachedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 30; i++) {
//这里必须要设置一个index,后面不能引用i
int index = i;
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "----" + index);
}
});
}
}
}
分析:从结果中可以看到,每个线程都会被多次调用。
实验结果
实验总结
- 使用newCachedThreadPool来进行线程管理可能会造成OOM(内存溢出)。所以再使用的使用要注意控制并发最大值。
实验 124 使用newFixedThreadPool管理并发线程
实验项目
- 使用newFixedThreadPool管理并发线程
实验需求
使用newFixedThreadPool创建一个线程池,并且实现并发操作。
知识点
- newFixedThreadPool
实验内容
newFixedThreadPool
创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)。
优点:执行长期的任务,性能好很多
在 /home/project/ 目录下新建一个源代码文件 NewFixedThreadPoolDemo.java。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NewFixedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);//设置线程数量
for (int i = 0; i < 10; i++) {
int index = i;
fixedThreadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + ",执行" + index);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
实验结果
实验总结
- 可以看出是无规律的取出线程池中的线程实现操作。
- 这里只讲解两种比较典型的线程池的例子,其它的的线程池的使用都类似,区别就在于线程的创建和管理方式。就不一一例举了。
实验 125 使用volatile实现线程控制
实验项目
- 使用volatile实现线程控制
实验需求
创建多个线程,其中一个线程为控制线程,当控制线程关闭程序时,其他的线程都即刻关闭。
知识点
- volatile
实验内容
Volatile
volatile关键字保证了变量的可见性(visibility)。被volatile关键字修饰的变量如果值发生了变更,其他线程立马可见,避免出现脏读的现象。
volatile具有可见性、有序性,不具备原子性。
结论:volatile主要用来实现线程控制。
在 /home/project/ 目录下新建一个源代码文件 VolatileTest.java。
public class VolatileTest {
static class Work {
volatile boolean isShutDown = false;
void shutdown() {
isShutDown = true;
System.out.println("shutdown!");
}
void doWork() {
while (!isShutDown) {
System.out.println("doWork");
}
}
}
public static void main(String[] args) {
Work work = new Work();
new Thread(work::doWork).start();
new Thread(work::shutdown).start();
}
}
分析:
因为当doWork线程在运行的时候,会将isShutDown变量的值拷贝一份放在自己的工作内存当中。当shutdown线程更改了isShutDown变量的值之后,此时还没来得及写入主内存当中,shutdown线程转去做其他事情了。那么doWork线程由于不知道shutdown线程对isShutDown变量的更改,因此还会一直循环下去,这就会出现死循环。
而使用volatile修饰后则不会出现这个问题。volatile关键字会强制将shutdown线程修改的isShutDown变量值立即写入主存,并且控制doWork线程的缓存无效,此时doWork线程需要用到isShutDown变量的值就会去主内存取值,此时取到的就是shutdown线程修改的值。
实验结果
实验总结
- 不使用volatile修饰也可以实现线程停止控制,但是这种控制方式并不是绝对安全的。volatile可以保障线程控制的绝对安全。但是它不具备原子性,因此不能用来存放其他功能的共享数据。
实验 126 使用Callable+Future实现多线程
实验项目
- 使用Callable+Future实现多线程
实验需求
本实验的目标,编写代码实现在子线程中计算100以内数字的和,并且将计算结果返回到主线程中。
知识点
- Callable
- Future
- ExecutorService
- Executors
实验内容
Callable和Future
前面已经学习了创建线程的2种方式:
- 直接继承Thread类
- 实现Runnable接口
但这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。自从JDK1.5开始提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
Callable
Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call()。
public interface Callable<V> {
V call() throws Exception;
}
可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。那么怎么使用Callable呢?一般情况下是配合ExecutorService来使用的。常用的调用方法:
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
Future
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。Future类也位于java.util.concurrent包下。
Future提供了三种功能:
-
判断任务是否完成
-
能够中断任务
-
能够获取任务执行结果
在 /home/project/ 目录下新建一个源代码文件 CallableFutureDemo.java。
import java.util.concurrent.*;
public class CallableFutureDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> result = executor.submit(task);//通过submit调用
executor.shutdown();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("主线程在执行任务");
try {
System.out.println("task运行结果" + result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("所有任务执行完毕");
}
}
class Task implements Callable<Integer> {
//重写Call方法,可以返回指定类型的数据,这里返回的是Integer类型。
@Override
public Integer call() throws Exception {
System.out.println("子线程在进行计算");
Thread.sleep(3000);
int sum = 0;
for (int i = 0; i < 100; i++)
sum += i;
return sum;
}
}
实验结果
实验总结
- 由上面的代码可以看出,当执行子线程的方法时主线程处于阻塞状态,直到子线程运行完毕为止。
- 由此可以看出,当需要子线程返回数据到主线程中时,可以使用Callable+Future的方式来实现。
实验 127 使用Callable+FutureTask实现多线程
实验项目
- 使用Callable+FutureTask实现多线程
实验需求
本实验的目标为:编写代码实现在子线程中计算100以内数字的和,并且将计算结果返回到主线程中。
知识点
- Callable
- FutureTask
- ExecutorService
- Executors
实验内容
FutureTask
前面讲解了Future。因为Future只是一个接口无法直接用来创建对象,所以就有了FutureTask。FutureTask是Future接口的一个唯一实现类。
根据API可以看出FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable接口和Future接口。所以FutureTask既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
FutureTask提供了2个构造方法:
FutureTask提供了2个构造方法:
下面来看一下FutureTask的具体应用。
在 /home/project/ 目录下新建一个源代码文件 CallableFutureTaskDemo.java。
import java.util.concurrent.*;
public class CallableFutureTaskDemo {
public static void main(String[] args) {
//第一种方式
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
executor.submit(futureTask);
executor.shutdown();
//第二种方式,注意这两种方式是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
/*Task task = new Task();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
Thread thread = new Thread(futureTask);
thread.start();*/
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("主线程在执行任务");
try {
System.out.println("task运行结果" + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("所有任务执行完毕");
}
}
class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子线程在进行计算");
Thread.sleep(3000);
int sum = 0;
for (int i = 0; i < 100; i++)
sum += i;
return sum;
}
}
实验结果
由代码可以看出FutureTask的特点。
实验总结
- FutureTask既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
(十六)GC和JVM
实验 128 GC内存回收实验
实验项目
- GC内存回收实验
实验需求
验证使用System.gc()可以进行内存回收。
知识点
- System.gc()
- finalize()
实验内容
执行System.gc()函数的作用只是提醒或告诉虚拟机,希望进行一次垃圾回收。至于什么时候进行回收还是取决于虚拟机,而且也不能保证一定进行回收(如果-XX:+DisableExplicitGC设置成true,则不会进行回收)。
Java提供了一个finalize方法,可以用于进行资源释放。
当List=null后,不仅列表中的对象变成了垃圾,为列表分配的空间也会回收。
在 /home/project/ 目录下新建源代码文件 GCTest.java:
import java.util.ArrayList;
import java.util.List;
public class GCTest {
int i;
static int j;
public GCTest() {
}
public GCTest(int i) {
this.i = i;
}
public static void main(String[] args) throws Exception {
List<Object> list = new ArrayList<Object>();
for (int i = 0; i < 10; i++) {
list.add(new GCTest(i));
}
list = null;//清空list,list理论会被系统回收。
System.gc();//通知系统回收内存
Thread.sleep(1000);
System.out.println("******************" + j);
}
@Override
protected void finalize() throws Throwable {
j++;
System.out.println("Over" + i + "->" + this.hashCode());
super.finalize();
}
}
实验结果
实验总结
- 通过这个实验可以看出,当使用System.gc();后,系统会调用finalize()方法。也就代表着系统立即标识了list中的10个对象可以回收。而去掉System.gc();后,系统就不会调用finalize()方法了。对象也就没有立即表示会回收。
实验 129 JVM调优实验
实验项目
- JVM调优实验
实验需求
通过创建大的数据对象集合,并且对对象进行操作和内存回收,可以观察对象在内存中不同状态的改变。
知识点
- GC
- JVM
实验内容
JVM调优原理
MinorGC是针对新生代中的EC区域的。
-
如果EC过大,那么MinorGC频度减少,好处是大部分对象可能就在E0区域销毁了,但是如果新堆大那么OC就有可能过小本来应该可以放在OC上的但是由于OC空间太小,导致FullGC,现在不得不在FullGC进行回收,有可能导致FullGC过多
-
如果EC过小 MinorGC会频繁进行,但是频率太快就会导致回收不到应该回收的对象,对象被放入OC中,OC不够用,则触发FullGC进行。
结论:能马上回收的就马上回收尽量避免进入下一代,从而增大FullGC的概率。
JVM调优实验
在 /home/project/ 目录下新建源代码文件 JVMTunning.java:
import java.util.ArrayList;
public class JVMTunning {
/**
* JVM参数 -Xms130m -Xmx130m -Xmn20m -XX:PermSize=20m -XX:MaxPermSize=20m
* -XX:+UseSerialGC OC 110M NC 20M ED :S0 : S1=16:2:2
*
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws Exception {
System.out.println("Test Start-----");
Thread.sleep(10000);
System.out.println("JStat0---Initial--");
ArrayList tempObjs = new ArrayList<GCDataObject>();
// 创建100M的内存占用,NC上不够用,肯定要触发MinorGC,促使其放在OC
for (int i = 0; i < 51200; i++) {
GCDataObject gcDataObject = new GCDataObject(2);
tempObjs.add(gcDataObject);
}
System.out.println("JStat1---Create 100M Objects--");
Thread.sleep(10000);
System.out.println("Full GC will Start-----");
// 促使FullGC使得OC上空间被占用100M,剩余10M
System.gc();
tempObjs.size();
System.out.println(tempObjs.size());
tempObjs = null;
System.out.println("JStat2---FullGC后--");
Thread.sleep(10000);
// 创建大约16M的对象,NC上放不下,只能放入OC,OC再放不下只能FullGC
ArrayList tempObj1s = new ArrayList<GCDataObject>();
for (int i = 0; i < 3200; i++) {
GCDataObject gcDataObject = new GCDataObject(5);
tempObj1s.add(gcDataObject);
}
System.out.println(tempObj1s.size());
// 诱发FullGC
tempObj1s = null;
System.out.println("JStat3---Create 16M Objects--");
Runtime.getRuntime().exec("jstat -gc");
Thread.sleep(10000);
Thread.sleep(1000000000);
}
}
class GCDataObject {
RefDataObject refDataObj = null;
byte[] gcByte = null;
public GCDataObject(int i) {
gcByte = new byte[1024 * i];
refDataObj = new RefDataObject();
}
}
class RefDataObject {
ChildDataObject childDataObject = null;
public RefDataObject() {
childDataObject = new ChildDataObject();
}
}
class ChildDataObject {
Object object = null;
public ChildDataObject() {
object = new Object();
}
}
实验结果
实验总结
- 本实验验证了GC生命周期中的内存过程。用户可以根据情况根据GC的使用情况优化JVM的配置。
(十七)网络编程
实验 130 使用IP地址类获得信息
实验项目
- 使用IP地址类获得信息
实验需求
使用InetAddress类实现获得百度服务器信息的代码。
知识点
- InetAddress
实验内容
InetAddress 类
InetAddress类用于表示 IP 地址,比如在进行 Socket 编程时,就会使用到该类。
InetAddress没有公共构造方法,我们只能使用它提供的静态方法来构建一个
InetAddress 类实例。
下面开始实验。
在 /home/project 目录下新建一个 InetAddressDemo.java。
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressDemo {
public static void main(String[] args) {
try {
InetAddress shiyanlou = InetAddress.getByName("www.baidu.com");
//toString 方法将输出主机名和ip地址
System.out.println(shiyanlou.toString());
//获取ip地址
String ip = shiyanlou.toString().split("/")[1];
//根据IP地址获取主机名
InetAddress byAddress = InetAddress.getByName(ip);
System.out.println("get hostname by IP address:" + byAddress.getHostName());
System.out.println("localhost: "+InetAddress.getLocalHost());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
实验结果
实验总结
1.
实验 131 通过URL获取百度首页实验
实验项目
- 通过URL获取百度首页实验
实验需求
通过百度首页URLwww.baidu.com 获得百度首页的HTML文本代码。
知识点
- HttpURLConnection
- Java IO
实验内容
URL类
HttpURLConnection 提供了很多方法用于使用 Http,这里只演示了使用 HttpURLConnection 类的基本流程,想要了解更多方法的同学可以查询API 文档。
通过URL获取百度首页实验
在 /home/project/ 目录下新建源代码文件 HttpUrlTest.java:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpUrlTest {
public static void main(String[] args) {
try {
//设置url
URL baidu = new URL("http://www.baidu.com");
//打开连接
HttpURLConnection urlConnection = (HttpURLConnection)baidu.openConnection();
//设置请求方法
urlConnection.setRequestMethod("GET");
//设置连接超时时间
urlConnection.setConnectTimeout(1000);
//获取输入流
InputStream inputStream = urlConnection.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//打印结果
bufferedReader.lines().forEach(System.out::println);
//关闭连接
inputStream.close();
bufferedReader.close();
urlConnection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
编译运行,可以看到百度首页的HTML代码。
实验结果
实验总结
1.
实验 132 基于TCP的Socket通信实验
实验项目
- 基于TCP的Socket通信实验
实验需求
本实验开发一个简单的 Socket 应用,实现客户端发送信息给服务端,服务端再将信息发送回客户端的回显的功能。
知识点
- Socket
- ServerSocket
- TCP协议
实验内容
基于TCP协议的Socket
使用ServerSocket和Socket实现服务器端和客户端的Socket通信。
了解完socket通信步骤后可以发现本实验需要写两个类:Server和Client,并且要先运行Server再运行Client。
服务端代码
在 /home/project/ 目录下新建源代码文件 TCPServer.java:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = null;
BufferedWriter out = null;
BufferedReader in = null;
try {
// 服务端需要使用ServerSocket类
serverSocket = new ServerSocket(1080);
System.out.println("服务器已运行!");
// 阻塞 等待客户端连接
Socket client = serverSocket.accept();
out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
in = new BufferedReader(new InputStreamReader(client.getInputStream()));
String userIn;
while ((userIn = in.readLine()) != null) {
System.out.println("收到客户端消息:" + userIn);
// 发回客户端
out.write(userIn);
out.newLine();
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
out.close();
in.close();
serverSocket.close();
}
}
}
server完成后,再完成clinet类。
客户端代码
在 /home/project/ 目录下新建源代码文件 TCPClient.java:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
public class TCPClient {
public static void main(String[] args) throws Exception {
String hostname = "127.0.0.1";
// socket端口
int port = 1080;
Scanner input = new Scanner(System.in);
BufferedWriter out = null;
BufferedReader in = null;
Socket socket = null;
try {
// 建立socket连接
socket = new Socket(hostname, port);
// 获取socket输出流
out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
// 获取输入流
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String userInput;
System.out.println("请输入信息:");
// 当用户输入exit时退出
while (!"exit".equals(userInput = input.next())) {
out.write(userInput);
out.newLine();
out.flush();
System.out.println("收到服务端回应:" + in.readLine());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
out.close();
in.close();
socket.close();
}
}
}
测试运行
打开两个 terminal,一个运行服务端,一个运行客户端。
首先启动服务端,不能先启动客户端,否则报错。
服务端启动命令:
$ javac EchoServer.java
$ java EchoServer
接着切换到客户端 terminal。客户端启动命令。
$ javac EchoClient.java
$ java EchoClient
运行结果:
- 客户端
- 服务端
实验总结
可以尝试输入更多的数据,也会返回更多的值。
本实验一定要注意以下几点:
- 端口号要保持一致。
- 先启动服务器再启动客户端。
- 分别在两个terminal中操作。
实验 133 基于UDP的Socket通信实验
实验项目
- 基于UDP的Socket通信实验
实验需求
本实验开发一个简单的 Socket 应用,实现互相发送一条信息的功能。
知识点
- DatagramSocket
- DatagramPacket
- UDP协议
实验内容
基于UDP协议的Socket
使用ServerSocket和Socket实现服务器端和客户端的Socket通信。
下面开始实验。
客户端代码
在 /home/project/ 目录下新建源代码文件 UDPClient.java:
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class UDPClient {
private static final int TIMEOUT = 5000; //设置接收数据的超时时间
private static final int MAXNUM = 5; //设置重发数据的最多次数
public static void main(String args[])throws IOException{
System.out.println("请输入要发送信息:");
Scanner input = new Scanner(System.in);
String str_send = input.next();
byte[] buf = new byte[1024];
//客户端在9000端口监听接收到的数据
DatagramSocket ds = new DatagramSocket(9000);
InetAddress loc = InetAddress.getLocalHost();
//定义用来发送数据的DatagramPacket实例
DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),loc,3000);
//定义用来接收数据的DatagramPacket实例
DatagramPacket dp_receive = new DatagramPacket(buf, 1024);
//数据发向本地3000端口
ds.setSoTimeout(TIMEOUT); //设置接收数据时阻塞的最长时间
int tries = 0; //重发数据的次数
boolean receivedResponse = false; //是否接收到数据的标志位
//直到接收到数据,或者重发次数达到预定值,则退出循环
while(!receivedResponse && tries<MAXNUM){
//发送数据
ds.send(dp_send);
try{
//接收从服务端发送回来的数据
ds.receive(dp_receive);
//如果接收到的数据不是来自目标地址,则抛出异常
if(!dp_receive.getAddress().equals(loc)){
throw new IOException("信息来源错误!");
}
//如果接收到数据。则将receivedResponse标志位改为true,从而退出循环
receivedResponse = true;
}catch(InterruptedIOException e){
//如果接收数据时阻塞超时,重发并减少一次重发的次数
tries += 1;
System.out.println("超时,还剩" + (MAXNUM - tries) + "重发请求! " );
}
}
if(receivedResponse){
//如果收到数据,则打印出来
System.out.println("收到服务端回应:");
String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength(),"utf-8") ;
System.out.println(str_receive +
" from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort());
//由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数,
//所以这里要将dp_receive的内部消息长度重新置为1024
dp_receive.setLength(1024);
}else{
//如果重发MAXNUM次数据后,仍未获得服务器发送回来的数据,则打印如下信息
System.out.println("无信息返回,放弃连接。");
}
ds.close();
}
}
服务端代码
在 /home/project/ 目录下新建源代码文件 UDPServer.java:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServer {
public static void main(String[] args) throws IOException {
byte[] buf = new byte[1024];
//服务端在3000端口监听接收到的数据
DatagramSocket ds = new DatagramSocket(3000);
//接收从客户端发送过来的数据
DatagramPacket dp_receive = new DatagramPacket(buf, 1024);
System.out.println("服务器启动,等待客户端连接......");
boolean f = true;
while (f) {
//服务器端接收来自客户端的数据
ds.receive(dp_receive);
System.out.println("收到客户端信息:");
String str_receive = new String(dp_receive.getData(), 0, dp_receive.getLength(), "utf-8");
System.out.println(str_receive + " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort());
//数据发动到客户端的3000端口
DatagramPacket dp_send = new DatagramPacket(str_receive.getBytes(), str_receive.length(), dp_receive.getAddress(), 9000);
ds.send(dp_send);
//由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数,
//所以这里要将dp_receive的内部消息长度重新置为1024
dp_receive.setLength(1024);
}
ds.close();
}
}
测试运行
分别运行服务端和客户端
在客户端输入hello后得到结果:
客户端:
服务端:
思考:客户端和服务端的启动顺序是否和TCP一样呢?
实验总结
- 编写代码时采用对比的学习方法,注意TCP协议和UDP协议的不同点。
注意UDP返回的对象保留了消息的边界信息,通过该对象可以获得更多数据。
实验 134 基于多线程的TOM猫小程序
实验项目
- 基于多线程的TOM猫小程序
实验需求
本实验模拟手机小程序TOM游戏,客户端无论向服务端发送什么信息。服务端都会返回同样的信息。
实验有以下要求:
- Server 可以同时接受多个客户端的连接
- 每个线程负责一个连接
- 客户端发送消息给服务端,服务端再将客户端发送的消息发回客户端
- 使用TCP来实现
知识点
- Socket
- ServerSocket
- TCP协议
- Thread
实验内容
服务端代码
在 /home/project/ 目录下新建源代码文件 Server.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
try {
//服务端需要使用ServerSocket类
ServerSocket serverSocket = new ServerSocket(1080);
System.out.println("服务器已启动!");
//阻塞 等待客户端连接
while (true) {
Thread thread = new Thread(new ServerThread(serverSocket.accept()));
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
static class ServerThread implements Runnable {
Socket client;
public ServerThread(Socket client) {
this.client = client;
}
@Override
public void run() {
try {
PrintWriter out = new PrintWriter(client.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
String userIn;
while ((userIn = in.readLine()) != null) {
System.out.println(client.getPort() + ":" + userIn);
//发回客户端
out.println(userIn);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端代码
在 /home/project/ 目录下新建源代码文件Client.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
String hostname = "127.0.0.1";
//socket端口
int port = 1080;
Scanner userIn = new Scanner(System.in);
try {
//建立socket连接
Socket socket = new Socket(hostname, port);
//获取socket输出流
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
//获取输入流
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String userInput;
System.out.println("我说:");
//当用户输入exit时退出
while (!"exit".equals(userInput = userIn.nextLine())) {
out.println(userInput);
System.out.println("Tom说:" + in.readLine());
}
//关闭socket
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行测试
1、先启动服务端
2、启动客户端1,输入”你好!“
3、启动客户端2,输入”hello!“
4、服务端结果
由以上结果可以看出
- 客户端1和客户端2都可以和服务端连接
- 客户端1和客户端2互不影响操作
- 服务端可以分别处理客户端1和客户端2的请求(端口号可以区别客户端)
- 可以启动更多的客户端进行测试
实验总结
- 注意多线程和socket网络编程的知识点运用。
(十八)Junit测试
实验 135 使用Junit3测试计算器实验
实验项目
- 使用Junit3测试计算器实验
实验需求
已经开发了计算器类,使用JUnit 3来测试计算器类开发是否正确。
知识点
- JUnit 3
实验内容
JUnit 3
JUnit是一个Java语言的单元测试框架,逐渐成为xUnit家族中为最成功的一个。
JUnit 是JAVA语言标准测试库,多数Java的开发环境(如:Eclipse)都已经集成了JUnit作为单元测试的工具。
JUnit3的要求:
-
测试用例需扩展junit.framework.TestCast
-
测试方法命名规则:test目标方法名(),如:testAdd()
实验结果
实验总结
1.
实验 136 使用Junit4测试计算器实验
实验项目
- 使用Junit4测试计算器实验
实验需求
已经开发了计算器类,使用Junit4来测试计算器类开发是否正确。
知识点
- JUnit 4
实验内容
JUnit 4
JUnit 4是与JUnit3完全不同的API,它基于Java 5.0中的注解、静态导入等构建而成。JUnit 4更简单、更丰富、更易于使用,并引入了更为灵活的初始化和清理工作,还有限时的和参数化测试用例。
继续使用上个示例中的计算器Calculator.java类来完成本实验。
在 /home/project/ 目录下新建一个文件 CalculatorTest4.java。
实验结果
实验总结
JUnit3 和 JUnit4的区别
-
JUnit 4使用 org.junit. 包而JUnit 3使用的是 junit.Framework.; 为了向后兼容,JUnit4发行版中加入了这两种包。
-
JUnit3中,测试类需要继承junit.framework.TestCase类,而在JUniy4则不用。
-
JUnit3通过分析方法名称来识别测试方法:方法名必须以“test”为前缀,它必须返回void,而且它必须没有任何参数(例如 public void testDivide())。不遵循这个命名约定的测试方法将被JUnit框架忽略,而且不抛出任何异常(指示发生了一个错误)。在JUnit4中,测试方法不必以’test’为前缀,而是使用@Test注解。但测试方法也必须返回void并且无参。在JUnit4中,可以在运行时控制这个要求。@Test注解支持可选参数。它声明一个测试方法应该抛出一个异常。如果它不抛出或者如果它抛出一个与事先声明的不同的异常,那么该测试失败。
-
在JUnit3中,TestCase类定义了assertEquals()方法,如果要在JUnit中向后兼容,必须静态地导入Assert类。这样一来,就可以像以前一样使用assertEquals方法。
(十九)JDK新特性
实验 137 Lambda表达式实验
实验项目
- Lambda表达式实验
实验需求
通过所学Lambda表达式的相关知识点,实现如下实验。
- 建立一个数组 1, 23, 4, 4, 22, 34, 45, 11, 33;
- 使用 lambda 求出数组中的最小数;
- 将数组去重,并将去重后数组的每个元素乘以 2,再求出乘以 2 后的数组的和,比如数组 1,2,3,3,去重后为 1,2,3,乘以 2 后为 2,4,6,最后的和为 12。
知识点
- Lambda
实验内容
Lambda
一个 Lambda 表达式具有下面这样的语法特征。它由三个部分组成:第一部分为一个括号内用逗号分隔的参数列表,参数即函数式接口里面方法的参数;第二部分为一个箭头符号:->;第三部分为方法体,可以是表达式和代码块。语法如下:
parameter -> expression body
下面列举了 Lambda 表达式的几个最重要的特征:
在 /home/project/ 目录下新建一个源代码文件 LambdaTest.java
import java.util.Arrays;
public class LambdaTest {
public static void main(String[] args) {
int[] arr = {1, 23, 4, 4, 22, 34, 45, 11, 33};
System.out.println("最小数:"+Arrays.stream(arr).min());
System.out.println("数组去重乘2求和:" + Arrays.stream(arr).distinct().map((i) -> i * 2).sum());
}
}
实验结果
实验总结
- Lambda表达式是JDK8的新特性,在后学的实验中会经常出现。
实验 138 使用Stream实现文字处理
实验项目
- 使用Stream实现文字处理
实验需求
考虑以下场景,有三个字符串 ("lan qiao", "lan qiao bei","lan qiao ruan jian"),我们希望将字符串使用空格分割,提取出单个单词。
知识点
- Lambda表达式
- Stream流
实验内容
Stream 流
Stream 是 Java 8 开始的一个新的抽象层。通过使用 Stream,你能以类似于 SQL 语句的声明式方式处理数据。
例如一个典型的 SQL 语句能够自动地返回某些信息,而不用在开发者这一端做任何的计算工作。同样,通过使用 Java 的集合框架,开发者能够利用循环做重复的检查。另外一个关注点是效率,就像多核处理器能够提升效率一样,开发者也可以通过并行化编程来改进工作流程,但是这样很容易出错。
因此,Stream 的引入是为了解决上述痛点。开发者可以通行声明式数据处理,以及简单地利用多核处理体系而不用写特定的代码。
说了这么久,Stream 究竟是什么呢?Stream 代表了来自某个源的对象的序列,这些序列支持聚集操作。下面是 Stream 的一些特性:
FlatMap
FlatMap 用于将多个流合并为一个流,使用 FlatMap 时,表达式的返回值必须是 Stream 类型。而 Map 用于将一种流转化为另外一个流。
使用Stream实现文字处理
在 /home/project/ 目录下新建一个文件 FlatMapTest.java。
import java.util.Arrays;
import java.util.stream.Stream;
public class FlatMapTest {
public static void main(String[] args) {
Stream<String> stringStream1 = Stream.of("lan qiao", "lan qiao bei","lan qiao ruan jian");
Stream<String> stringStream2 = Stream.of("lan qiao", "lan qiao bei","lan qiao ruan jian");
//map将一种类型的流 转换为另外一个类型的流 这里转换成了String[]流
//这并不是我们想要的,我们想要的是Stream<String>,而不是Stream<String[]>
Stream<String[]> mapStream = stringStream1.map(v -> v.split(" "));
//Arrays.stream将数组转换成了流 这里将分割后的String[],转换成了Stream<String>,但是我们前面定义了三个字符串
//所以这里将产生三个Stream<String>,flatMap用于将三个流合并成一个流
Stream<String> flatMapStream = stringStream2.flatMap(v -> Arrays.stream(v.split(" ")));
System.out.println("mapStream打印:");
mapStream.peek(System.out::println).count();
System.out.println("flatMapStream打印:");
flatMapStream.peek(System.out::println).count();
}
}
实验结果
实验总结
- JDK新特性中的stream主要是针对集合类型的数据进行操作,可以更好的完成对复杂集合模型的操作。
(二十)设计模式
实验 139 使用装饰器设计模式绘制圆
实验项目
- 使用装饰器设计模式绘制圆
实验需求
系统中存在一个画圆的类,该类只是用来画圆,以及其他一些大小和位置等参数的控制。现在更新功能:
- 可以对圆的边进行着色
- 可以对圆填充颜色
- 可以同时对边和内部着色
知识点
- Java
- Interface
实验内容
装饰器模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
本实验要完成的设计思路如下:
创建一个 Shape 接口和实现了 Shape 接口的实体类。然后我们创建一个实现了 Shape 接口的抽象装饰类 ShapeDecorator,并把 Shape 对象作为它的实例变量。 RedShapeDecorator 是实现了 ShapeDecorator 的实体类。 DecoratorPatternDemo,我们的演示类使用 RedShapeDecorator 来装饰 Shape 对象。
在 /home/project/ 目录下新建如下源代码文件
接口类:shape
public interface Shape {
void draw();
}
画圆类:Circle
public class Circle implements Shape {
@Override
public void draw() {
System.out.print("a circle!");
}
}
抽象装饰器类:Decorator
public abstract class Decorator implements Shape {
protected Shape circle;
public Decorator(Shape shape) {
circle = shape;
}
public void draw() {
circle.draw();
}
}
为圆边着色装饰器类:CircleEdge
public class CircleEdge extends Decorator {
public CircleEdge(Shape circle) {
super(circle);
}
private void setEdgeColor() {
System.out.print(", edge with color");
}
public void draw() {
circle.draw();
setEdgeColor();
}
}
为圆填充颜色装饰器类:CircleFill
public class CircleFill extends Decorator {
public CircleFill(Shape circle) {
super(circle);
}
private void setEdgeFill() {
System.out.print(", content with color");
}
public void draw() {
circle.draw();
setEdgeFill();
}
}
测试代码
public class Demo {
public static void main(String[] args) {
Shape circle = new Circle();
circle.draw();
System.out.println("");
Decorator circleEdge = new CircleEdge(circle);
circleEdge.draw();
System.out.println("");
Decorator circleFill = new CircleFill(circle);
circleFill.draw();
System.out.println("");
Decorator circleEdgeFill = new CircleFill(circleEdge);
circleEdgeFill.draw();
}
}
实验结果
实验总结
- 装饰器模式是一种常见的设计模式。需要熟练掌握。
实验 140 使用工厂设计模式设计手机工厂实验
实验项目
- 使用工厂设计模式设计手机工厂实验
实验需求
手机生产有一定的标准,先建立一个手机工厂,用来生产小米手机和苹果手机。
知识点
- Java
实验内容
工厂设计模式
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
工厂模式又分为以下几种:
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
本实验我们采用简单工厂模式。设计思路如下:
在 /home/project/ 目录下新建如下源代码文件
手机标准规范
public interface Phone {
void make();
}
制造小米手机
public class MiPhone implements Phone {
public MiPhone() {
this.make();
}
@Override
public void make() {
System.out.println("make xiaomi phone!");
}
}
制造苹果手机
public class IPhone implements Phone {
public IPhone() {
this.make();
}
@Override
public void make() {
System.out.println("make iphone!");
}
}
手机代工厂
public class PhoneFactory {
public Phone makePhone(String phoneType) {
if(phoneType.equalsIgnoreCase("MiPhone")){
return new MiPhone();
}
else if(phoneType.equalsIgnoreCase("iPhone")) {
return new IPhone();
}
return null;
}
}
测试代码
public class Demo {
public static void main(String[] arg) {
PhoneFactory factory = new PhoneFactory();
Phone miPhone = factory.makePhone("MiPhone");
IPhone iPhone = (IPhone)factory.makePhone("iPhone");
}
}
运行并查看结果
实验结果
实验总结
- 每种工厂模式都有自己的特点。可以根据开发的需要选择适合的模式设计程序。
实验 141 使用代理设计模式渲染图片实验
实验项目
- 使用代理设计模式渲染图片实验
实验需求
现在需要访问图片。若是直接访问速度会比较慢,性能会受影响。可以使用代理模式设计访问图片功能。
知识点
- Java
- Interface
实验内容
代理模式
代理模式主要解决无法直接访问对象的问题。例如:程序需要访问的对象在远程的服务器上,或者某些对象的操作需要安全控制,或者需要进程外的访问等,在这些情况下直接访问会给使用者或者系统结构带来很多麻烦,因此可以为此对象加上一个代理对象,需要时访问代理对象即可。
本实验要完成的设计思路如下:
将创建一个 Image 接口和实现了 Image 接口的实体类。ProxyImage 是一个代理类,减少 RealImage 对象加载的内存占用。ProxyPatternDemo,我们的演示类使用 ProxyImage 来获取要加载的 Image 对象,并按照需求进行显示
在 /home/project/ 目录下新建如下源代码文件
图片规范接口
public interface Image {
void display();
}
真实图片类
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName){
this.fileName = fileName;
loadFromDisk(fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
private void loadFromDisk(String fileName){
System.out.println("Loading " + fileName);
}
}
代理图片类
public class ProxyImage implements Image{
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName){
this.fileName = fileName;
}
@Override
public void display() {
if(realImage == null){
realImage = new RealImage(fileName);
}
realImage.display();
}
}
测试代码
public class ProxyPatternDemo {
public static void main(String[] args) {
Image image = new ProxyImage("test_10mb.jpg");
// 图像将从磁盘加载
image.display();
System.out.println("");
// 图像不需要从磁盘加载
image.display();
}
}
实验结果
实验总结
- 代理模式是一种常见的设计模式。需要熟练掌握。
(二十一)GUI图形界面
实验 142 五子棋棋盘布局实验
实验项目
- 五子棋棋盘布局实验
实验需求
使用Java GUI实现五子棋页面布局,在窗口左侧设计一个15*15的页面,在窗体右侧设计三个按钮,为“开始、悔棋、认输”。布局如下:
知识点
- GUI
- JFrame
- JPanel
- Graphics
实验内容
Swing
Swing 是一个为Java设计的GUI工具包,提供许多比AWT更好的屏幕显示元素。可以使用Swing绘制图形用户界面(GUI)和元素,例如:文本框,按钮,分隔窗格和表。
容器
此次练习会用到JFrame顶层容器、JPanel容器。
JFrame是最常用的一种顶层容器,它的作用是创建一个顶层的Windows窗体。JFrame的外观就像平常windows系统下见到的窗体,有标题、边框、菜单等等。
JPanel容器可以用来存放其他容器,用来做布局。
布局
布局可以控制组件在容器的布局方式。本实验用到一下两种布局方式:
BorderLayout:边界布局,把窗体分为东西南北中五个区域,可以只使用部分区域。
FlowLayout:流布局,容器中的组件按从上到下,从左到右的方式布局。
组件
存放在容器中,具有相应功能的个体。本实验会用到JButton组件。
五子棋棋盘布局实验
思路
- 布局方式为一个顶层容器JFrame配两个普通容器JPanel形式。
- JFrame作为窗口的载体,可以绘制一个Window窗体,并且该容器应包含一些窗体的设置。
- 左侧JPanel为绘制棋盘区域,使用Graphics绘制棋盘。棋盘设计成15*15。
- 右侧JPanel为功能区域,放置三个功能按钮。
在 /home/project/ 目录下新建一个源代码文件 EndingThread.java。
import java.awt.*;
import javax.swing.*;
/**
* 绘制五子棋棋盘
*/
public class GomokuGame {
private final int x = 60;//棋盘起始位置横坐标(左上角)
private final int y = 60;//棋盘起始位置纵坐标(左上角)
private final int row = 15;//棋盘行
private final int column = 15;//棋盘列
private final int size = 30;//行列间距
public GomokuGame() {
init();//初始化窗体
}
public void init() {
//窗体设置
JFrame frame = new JFrame("五子棋");
frame.setSize(800, 650);//窗体大小
frame.setLocationRelativeTo(null);//窗体居中
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//退出按钮功能
frame.setResizable(false);//不能改变窗体大小
frame.setLayout(new BorderLayout());//窗体采用边界布局方式
//左侧棋盘绘制
GomokuPanel gomokuPanel = new GomokuPanel();
gomokuPanel.setPreferredSize(new Dimension(600, 650));
frame.add(gomokuPanel, BorderLayout.CENTER);
//右侧工具绘制
JPanel toolPanel = new JPanel();
toolPanel.setPreferredSize(new Dimension(200, 650));
toolPanel.setLayout(null);
//开始按钮
JButton startButton = new JButton("开始");
startButton.setBounds(50, 80, 80, 40);
toolPanel.add(startButton);
//悔棋按钮
JButton backButton = new JButton("悔棋");
backButton.setBounds(50, 140, 80, 40);
toolPanel.add(backButton);
//退出按钮
JButton exitButton = new JButton("认输");
exitButton.setBounds(50, 200, 80, 40);
toolPanel.add(exitButton);
frame.add(toolPanel, BorderLayout.EAST);
frame.setVisible(true);//显示窗体
}
class GomokuPanel extends JPanel {
@Override
public void paint(Graphics g) {
// 横线
for (int i = 0; i < row; i++) {
g.drawLine(x, y + size * i, x + size * (column - 1), y + size * i);
}
// 竖线
for (int j = 0; j < column; j++) {
g.drawLine(x + size * j, y, x + size * j, y + size * (row - 1));
}
}
}
public static void main(String[] args) {
GomokuGame gomoku = new GomokuGame();
}
}
实验结果
实验总结
- JPanel作为画布
- JFrame作为画板
- Graphics作为画笔
实验 143 五子棋绑定监听实验
实验项目
- 五子棋绑定监听实验
实验需求
在上一个实验的基础上,实现点击棋盘落子功能:黑子先行,交叉落子。本实验不做规则判断,只完成落子功能。
知识点
- GUI
- JFrame
- JPanel
- Graphics
- Event
- Listener
实验内容
事件处理
思路
- 分别开发左侧棋盘的监听类和右侧按钮的监听类。
- 为左侧的棋盘绑定鼠标点击事件,判断落子位置,并且完成交叉落子操作。
- 为右侧三个按钮绑定鼠标单击事件,完成对应的功能。
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import javax.swing.*;
/**
* 绘制五子棋棋盘
*/
public class GomokuGame {
private final int x = 20;//棋盘起始位置横坐标(左上角)
private final int y = 20;//棋盘起始位置纵坐标(左上角)
private final int row = 15;//棋盘行
private final int column = 15;//棋盘列
private final int size = 40;//行列间距
//悔棋坐标
int preRow = 0;
int preColumn = 0;
GomokuPanel gomokuPanel;
public int[][] isAvail = new int[15][15];//定义一个二维数组来储存棋盘的落子情况
public GomokuGame() {
init();//初始化窗体
}
public void init() {
//窗体设置
JFrame frame = new JFrame("五子棋");
frame.setSize(800, 650);//窗体大小
frame.setLocationRelativeTo(null);//窗体居中
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//退出按钮功能
frame.setResizable(false);//不能改变窗体大小
frame.setLayout(new BorderLayout());//窗体采用边界布局方式
//左侧棋盘绘制
gomokuPanel = new GomokuPanel();
gomokuPanel.setPreferredSize(new Dimension(600, 650));
frame.add(gomokuPanel, BorderLayout.CENTER);
//右侧工具绘制
JPanel toolPanel = new JPanel();
toolPanel.setPreferredSize(new Dimension(200, 650));
toolPanel.setLayout(null);
//开始按钮
JButton startButton = new JButton("开始新游戏");
startButton.setBounds(50, 80, 80, 40);
toolPanel.add(startButton);
//悔棋按钮
JButton backButton = new JButton("悔棋");
backButton.setBounds(50, 140, 80, 40);
toolPanel.add(backButton);
//退出按钮
JButton exitButton = new JButton("退出");
exitButton.setBounds(50, 200, 80, 40);
toolPanel.add(exitButton);
frame.add(toolPanel, BorderLayout.EAST);
//绑定组件对应的监听
gomokuPanel.addMouseListener(new GomokuPanelListener());
startButton.addActionListener(new ButtonListener());
backButton.addActionListener(new ButtonListener());
exitButton.addActionListener(new ButtonListener());
//显示窗体
frame.setVisible(true);
}
class GomokuPanel extends JPanel {
@Override
public void paint(Graphics g) {
// 横线
for (int i = 0; i < row; i++) {
g.drawLine(x, y + size * i, x + size * (column - 1), y + size * i);
}
// 竖线
for (int j = 0; j < column; j++) {
g.drawLine(x + size * j, y, x + size * j, y + size * (row - 1));
}
//重绘出棋子
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
if (isAvail[i][j] == 1) {
int countx = size * i + 20;
int county = size * j + 20;
g.setColor(Color.black);
g.fillOval(countx - size / 2, county - size / 2, size, size);
} else if (isAvail[i][j] == 2) {
int countx = size * i + 20;
int county = size * j + 20;
g.setColor(Color.white);
g.fillOval(countx - size / 2, county - size / 2, size, size);
}
}
}
}
}
/**
* 为棋盘添加监听功能
*/
class GomokuPanelListener extends MouseAdapter {
public int turn = 1;//判断当前轮到谁了,1表示黑方,2表示白方
public void mouseClicked(java.awt.event.MouseEvent e) {
int x = e.getX();
int y = e.getY();
//计算棋子要落在棋盘的哪个交叉点上
int countx = (x / 40) * 40 + 20;
int county = (y / 40) * 40 + 20;
Graphics g = gomokuPanel.getGraphics();
if (isAvail[(countx - 20) / 40][(county - 20) / 40] != 0) {
System.out.println("此处已经有棋子了,请下在其它地方");
} else {
//当前位置可以落子,先计算棋盘上棋子在数组中相应的位置
int row = (countx - 20) / 40;
int column = (county - 20) / 40;
//存入悔棋坐标
preRow = row;
preColumn = column;
if (turn == 1) {
//先设置颜色
g.setColor(Color.black);
//落子
g.fillOval(countx - size / 2, (county - size / 2), size, size);
//设置当前位置已经有棋子了,棋子为黑子
isAvail[row][column] = 1;
turn++;
} else {
g.setColor(Color.white);
g.fillOval(countx - size / 2, (county - size / 2), size, size);
//设置当前位置已经有棋子了,棋子为白子
isAvail[row][column] = 2;
turn--;
}
}
}
}
/**
* 为三个按钮添加监听功能
*/
class ButtonListener implements ActionListener {
//当界面发生操作时进行处理
public void actionPerformed(ActionEvent e) {
//获取当前被点击按钮的内容,判断是不是"开始新游戏"这个按钮
if (e.getActionCommand().equals("开始新游戏")) {
isAvail = new int[15][15];
init();//重新绘制棋盘
} else if (e.getActionCommand().equals("退出")) {
System.exit(0);//退出游戏
} else {
//悔棋操作
isAvail[preRow][preColumn] = 0;//清空刚落的子
init();//重新绘制棋盘
}
}
}
public static void main(String[] args) {
GomokuGame gomoku = new GomokuGame();
}
}
实验结果
实验总结
- 思考:悔棋和重新开始的时候,调用了init()方法重新绘制棋盘,是否可以优化代码实现不重新绘制棋盘也能达到相同的目的?
实验 144 五子棋游戏
实验项目
- 五子棋游戏
实验需求
前两个实验分别完成了五子棋布局和五子棋监听实验,本实验完成最后的功能-判断输赢操作。
知识点
- GUI
- JFrame
- JPanel
- Graphics
- Event
- Listener
实验内容
思路
在前两个实验完成的基础上,只需要在黑白双方每次落子后,判断该方是否获胜即可。
按四个方向判断,相邻五个棋子是相同颜色则获胜。
- 横向判断
- 纵向判断
- 从左上至右下判断
- 从右上至左下判断
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import javax.swing.*;
/**
* 绘制五子棋棋盘
*/
public class GomokuGame {
private final int x = 20;//棋盘起始位置横坐标(左上角)
private final int y = 20;//棋盘起始位置纵坐标(左上角)
private final int row = 15;//棋盘行
private final int column = 15;//棋盘列
private final int size = 40;//行列间距
//悔棋坐标
int preRow = 0;
int preColumn = 0;
GomokuPanel gomokuPanel;
public int[][] isAvail = new int[15][15];//定义一个二维数组来储存棋盘的落子情况
public GomokuGame() {
init();//初始化窗体
}
public void init() {
//窗体设置
JFrame frame = new JFrame("五子棋");
frame.setSize(800, 650);//窗体大小
frame.setLocationRelativeTo(null);//窗体居中
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//退出按钮功能
frame.setResizable(false);//不能改变窗体大小
frame.setLayout(new BorderLayout());//窗体采用边界布局方式
//左侧棋盘绘制
gomokuPanel = new GomokuPanel();
gomokuPanel.setPreferredSize(new Dimension(600, 650));
frame.add(gomokuPanel, BorderLayout.CENTER);
//右侧工具绘制
JPanel toolPanel = new JPanel();
toolPanel.setPreferredSize(new Dimension(200, 650));
toolPanel.setLayout(null);
//开始按钮
JButton startButton = new JButton("开始新游戏");
startButton.setBounds(50, 80, 80, 40);
toolPanel.add(startButton);
//悔棋按钮
JButton backButton = new JButton("悔棋");
backButton.setBounds(50, 140, 80, 40);
toolPanel.add(backButton);
//退出按钮
JButton exitButton = new JButton("退出");
exitButton.setBounds(50, 200, 80, 40);
toolPanel.add(exitButton);
frame.add(toolPanel, BorderLayout.EAST);
//绑定组件对应的监听
gomokuPanel.addMouseListener(new GomokuPanelListener());
startButton.addActionListener(new ButtonListener());
backButton.addActionListener(new ButtonListener());
exitButton.addActionListener(new ButtonListener());
//显示窗体
frame.setVisible(true);
}
class GomokuPanel extends JPanel {
@Override
public void paint(Graphics g) {
// 横线
for (int i = 0; i < row; i++) {
g.drawLine(x, y + size * i, x + size * (column - 1), y + size * i);
}
// 竖线
for (int j = 0; j < column; j++) {
g.drawLine(x + size * j, y, x + size * j, y + size * (row - 1));
}
//重绘出棋子
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
if (isAvail[i][j] == 1) {
int countx = size * i + 20;
int county = size * j + 20;
g.setColor(Color.black);
g.fillOval(countx - size / 2, county - size / 2, size, size);
} else if (isAvail[i][j] == 2) {
int countx = size * i + 20;
int county = size * j + 20;
g.setColor(Color.white);
g.fillOval(countx - size / 2, county - size / 2, size, size);
}
}
}
}
}
/**
* 为棋盘添加监听功能
*/
class GomokuPanelListener extends MouseAdapter {
public int turn = 1;//判断当前轮到谁了,1表示黑方,2表示白方
public void mouseClicked(java.awt.event.MouseEvent e) {
int x = e.getX();
int y = e.getY();
//计算棋子要落在棋盘的哪个交叉点上
int countx = (x / 40) * 40 + 20;
int county = (y / 40) * 40 + 20;
Graphics g = gomokuPanel.getGraphics();
if (isAvail[(countx - 20) / 40][(county - 20) / 40] != 0) {
System.out.println("此处已经有棋子了,请下在其它地方");
} else {
//当前位置可以落子,先计算棋盘上棋子在数组中相应的位置
int row = (countx - 20) / 40;
int column = (county - 20) / 40;
//存入悔棋坐标
preRow = row;
preColumn = column;
int var = turn;
if (turn == 1) {
//先设置颜色
g.setColor(Color.black);
//落子
g.fillOval(countx - size / 2, (county - size / 2), size, size);
//设置当前位置已经有棋子了,棋子为黑子
isAvail[row][column] = 1;
turn++;
} else {
g.setColor(Color.white);
g.fillOval(countx - size / 2, (county - size / 2), size, size);
//设置当前位置已经有棋子了,棋子为白子
isAvail[row][column] = 2;
turn--;
}
//判断输赢
new Rule().doVictory(row, column, var);
}
}
}
/**
* 为三个按钮添加监听功能
*/
class ButtonListener implements ActionListener {
//当界面发生操作时进行处理
public void actionPerformed(ActionEvent e) {
//获取当前被点击按钮的内容,判断是不是"开始新游戏"这个按钮
if (e.getActionCommand().equals("开始新游戏")) {
repaint();//重新开始
} else if (e.getActionCommand().equals("退出")) {
System.exit(0);//退出游戏
} else {
//悔棋操作
isAvail[preRow][preColumn] = 0;//清空刚落的子
init();//重新绘制棋盘
}
}
}
/**
* 重新开始
*/
private void repaint() {
isAvail = new int[15][15];
init();//重新绘制棋盘
}
/**
* 游戏规则类
*/
class Rule {
//处理输赢
public void doVictory(int x, int y, int var) {
int a = checkVictory(x, y, var);
if (a == 1) {
JOptionPane.showMessageDialog(null, "黑方获胜!", "恭喜", JOptionPane.DEFAULT_OPTION);
repaint();
}
if (a == 2) {
JOptionPane.showMessageDialog(null, "白方获胜!", "恭喜", JOptionPane.DEFAULT_OPTION);
repaint();
}
}
//判断输赢
private int checkVictory(int x, int y, int var) {
//横向判断
int trans = 0;
for (int i = x - 4; i < x + 5; i++) {
if (i < 0 || i >= GomokuGame.this.x) continue;
if (isAvail[i][y] == var) {
trans++;
} else {
trans = 0;
}
if (trans == 5) return var;
}
//纵向判断
int longitudinal = 0;
for (int i = y - 4; i < y + 5; i++) {
if (i < 0 || i >= GomokuGame.this.y) continue;
if (isAvail[x][i] == var) {
longitudinal++;
} else {
longitudinal = 0;
}
if (longitudinal == 5) return var;
}
//从左上到右下
int leftUPToDown = 0;
for (int i = x - 4, j = y + 4; i < x + 5 && j > y - 5; i++, j--) {
if (i < 0 || i >= GomokuGame.this.x || j < 0 || j >= GomokuGame.this.y) continue;
if (isAvail[i][j] == var) {
leftUPToDown++;
} else {
leftUPToDown = 0;
}
if (leftUPToDown == 5) return var;
}
//从左下到右上
int leftDownToUP = 0;
for (int i = x + 4, j = y + 4; i > x - 5 && j > y - 5; i--, j--) {
if (i < 0 || i >= GomokuGame.this.x || j < 0 || j >= GomokuGame.this.y) continue;
if (isAvail[i][j] == var) {
leftDownToUP++;
} else {
leftDownToUP = 0;
}
if (leftDownToUP == 5) return var;
}
return 0;
}
}
public static void main(String[] args) {
GomokuGame gomoku = new GomokuGame();
}
}
实验结果
实验总结
- 至此,该实验全部完成。此次实验不仅练习了Java GUI部分的知识点,同时也练习了Java其他的知识。代码还有优化的可能,请大家认真思考一下如何优化?
提示:在重新绘制棋盘的功能处考虑优化。
实验 145 基于UI界面设计的局域网聊天室
实验项目
- 基于UI界面设计的局域网聊天室
实验需求
本实验的目标是完成一个局域网聊天室,可以实现群聊的功能。主要包含以下功能:
- 多个客户端可以连接到同一个服务端
- 每个客户端发送的请求经过服务端转发到所有的客户端
- 发送的信息带时间、昵称和内容
- 新连接用户默认没有昵称,使用名字为:“用户+端口号”。例如:用户1234
- 所有连接的用户都在用户列表中显示用户名
UI界面如下
知识点
- Java基础
- 多线程
- IO
- socket编程
实验内容
Step1:消息群发
功能描述:
消息群发是指先开启一个服务端,然后可以开启并连接任意数量的客户端。当其中一个客户端发送信息时,所有客户端(包括该客户端本身)都可以接收到信息。此时模型为一对多的模型,服务端实现管理客户端连接、群发消息两大个功能。客户端实现连接、发送信息、显示信息功能。
提示:再完成该项目前,请先完成网络编程和多线程的各个实验。
服务端开发
思路:
- 服务端要服务多个客户端,所以服务端应该使用多线程技术,为每个客户端创建一个连接。
- 所有的客户端连接存入客户端集合,以便用于循环发送信息。
- 当一个客户端发送信息时,迭代客户端集合将该信息群发给集合中的每个客户端。
package com.chatroom;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;
public class TestSockServer {
// 客户端集合,用于存放连接到该服务器的客户端。
static Set<Client> clientList = new HashSet<Client>();
@SuppressWarnings("resource")
public static void main(String[] args) {
Socket socket = null;
try {
// 服务开启
ServerSocket s = new ServerSocket(8888);
System.out.println("服务器启动!");
// 循环等待客户端连接
while (true) {
socket = s.accept();// 接受连接客户端
Client c = new TestSockServer().new Client(socket);// 创建客户端对象
c.setName(String.valueOf(socket.getPort()));// 用户名默认为端口号
clientList.add(c);// 将客户端存入客户端集合
c.start();
System.out.println(socket.getPort() + "加入群聊!");
}
} catch (IOException e) {
System.out.println("server over!");
}
}
class Client extends Thread {
Socket socket = null;
public Client(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
DataOutputStream dos = null;
DataInputStream dis = null;
try {
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
// 循环等待接收信息
while (true) {
String str = null;
// 当接收到客户端信息后,执行操作。
if ((str = dis.readUTF()) != null) {
// 向所有客户端发送信息
for (Client client : clientList) {
new DataOutputStream(client.socket.getOutputStream())
.writeUTF(Thread.currentThread().getName() + ":" + str);
}
}
}
} catch (IOException e) {
// 当客户端断开后,需要将该客户端从客户端集合中移除
System.out.println(this.socket.getPort() + "退出群聊!");
clientList.remove(this);
} finally {
// 关闭连接
try {
dis.close();
dos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端开发
思路:
- 连接服务端
- 循环发送信息,需要while关键字控制,直到输入88结束。
- 显示信息时因为和发送信息并不存在依赖关系,此时为两个线程并行进行。所以需要单独创建显示线程。
package com.chatroom;
import java.net.*;
import java.util.Scanner;
import java.io.*;
public class TestSockClient {
private DataInputStream dis;
private DataOutputStream dos;
private Socket socket;
@SuppressWarnings("resource")
public void init() {
String str = null;
Scanner input = new Scanner(System.in);
try {
// 客户端连接服务器
socket = new Socket("127.0.0.1", 8888);
System.out.println("客户端已连接,请输入发送信息:");
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
// 当连接成功后开启子线程实现循环接收服务端发送的信息的功能。
// 需要另起一个线程来接收服务器信息,这里用匿名内部类。
new Thread() {
@Override
public void run() {
String s = null;
try {
while (true) {
if ((s = dis.readUTF()) != null)
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
// 在主线程中实现从控制台接收数据并且发送到服务器的功能。
do {
str = input.next();
dos.writeUTF(str);
} while (!str.equals("88"));
System.out.println("client over!");
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭连接
try {
dis.close();
dos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TestSockClient tc = new TestSockClient();
tc.init();
}
}
运行显示:
启动服务端:
启动两个客户端:
客户端一种输入“你好!”后:
客户端一(37456):
客户端二(37458):
客户端二种输入“hello!”后:
客户端一:
客户端二:
关闭客户端一后:
Step2:更换昵称
功能描述:
在上一个功能的基础上,当用户发送的信息以“#”开头时,更换该用户昵称。
思路:
-
修改用昵称的操作是在服务端完成的。所以客户端的代码不需要改变。
-
服务端需要对客户端发送的字符串进行判断,当“#”开头时修改用户昵称。
服务端代码
package com.chatroom;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;
public class TestSockServer {
// 客户端集合,用于存放连接到该服务器的客户端。
static Set<Client> clientSet = new HashSet<Client>();
@SuppressWarnings("resource")
public static void main(String[] args) {
Socket socket = null;
try {
// 服务开启
ServerSocket s = new ServerSocket(8888);
System.out.println("服务器启动!");
// 循环等待客户端连接
while (true) {
socket = s.accept();// 接受连接客户端
Client c = new TestSockServer().new Client(socket);// 创建客户端对象
c.setName(String.valueOf(socket.getPort()));// 用户名默认为端口号
clientSet.add(c);// 将客户端存入客户端集合
c.start();
System.out.println(socket.getPort() + "加入群聊!");
}
} catch (IOException e) {
System.out.println("server over!");
}
}
class Client extends Thread {
Socket socket = null;
public Client(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
DataOutputStream dos = null;
DataInputStream dis = null;
try {
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
// 循环等待接收信息
while (true) {
String str = null;
// 当接收到客户端信息后,执行操作。
if ((str = dis.readUTF()) != null) {
// 对字符串进行判断,若是以“#”开头,则修改该用户的昵称。
if (str.startsWith("#")) {
// 修改该昵称
String msg = Thread.currentThread().getName() + "修改昵称为:" + str.substring(1);
Thread.currentThread().setName(str.substring(1));// 修改该线程的name
System.out.println(msg);// 服务端显示修改信息
new DataOutputStream(socket.getOutputStream()).writeUTF(msg);// 发送修改成功信息到客户端
} else {
// 向所有客户端发送信息
for (Client client : clientSet) {
new DataOutputStream(client.socket.getOutputStream())
.writeUTF(Thread.currentThread().getName() + ":" + str);
}
}
}
}
} catch (IOException e) {
// 当客户端断开后,需要将该客户端从客户端集合中移除
System.out.println(this.socket.getPort() + "退出群聊!");
clientSet.remove(this);
} finally {
// 关闭连接
try {
dis.close();
dos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行显示
首先分别启动服务端和客户端。
客户端发送信息并且显示
服务端显示
Step3:显示用户列表
功能描述
在上一个功能的基础上,当用户发送的信息以“$”开头时,返回用户名集合的JSON字符串到客户端。
思路:
- 获得用户列表客户端和服务端都需要进行操作。
- 服务端需要对客户端发送的字符串进行判断,当“$”开头时返回用户集合。
- 客户端获得的信息是以“$”开头时,显示集合中的名称。
服务端代码
package com.chatroom;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;
public class TestSockServer {
// 客户端集合,用于存放连接到该服务器的客户端。
static Set<Client> clientSet = new HashSet<Client>();
@SuppressWarnings("resource")
public static void main(String[] args) {
Socket socket = null;
try {
// 服务开启
ServerSocket s = new ServerSocket(8888);
System.out.println("服务器启动!");
// 循环等待客户端连接
while (true) {
socket = s.accept();// 接受连接客户端
Client c = new TestSockServer().new Client(socket);// 创建客户端对象
c.setName(String.valueOf(socket.getPort()));// 用户名默认为端口号
clientSet.add(c);// 将客户端存入客户端集合
c.start();
System.out.println(socket.getPort() + "加入群聊!");
}
} catch (IOException e) {
System.out.println("server over!");
}
}
class Client extends Thread {
Socket socket = null;
public Client(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
DataOutputStream dos = null;
DataInputStream dis = null;
try {
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
// 循环等待接收信息
while (true) {
String str = null;
// 当接收到客户端信息后,执行操作。
if ((str = dis.readUTF()) != null) {
// 对字符串进行判断,若是以“#”开头,则修改该用户的昵称。若是以“$”开头,返回用户昵称列表。
if (str.startsWith("#")) {
// 修改该昵称
String msg = Thread.currentThread().getName() + "修改昵称为:" + str.substring(1);
Thread.currentThread().setName(str.substring(1));// 修改该线程的name
System.out.println(msg);// 服务端显示修改信息
new DataOutputStream(socket.getOutputStream()).writeUTF(msg);// 发送修改成功信息到客户端
} else if (str.startsWith("$")) {
// 返回用户昵称集合字符串
String msg = getClientName();
new DataOutputStream(socket.getOutputStream()).writeUTF(msg);// 发送用户昵称字符串到客户端
} else {
// 向所有客户端发送信息
for (Client client : clientSet) {
new DataOutputStream(client.socket.getOutputStream())
.writeUTF(Thread.currentThread().getName() + ":" + str);
}
}
}
}
} catch (IOException e) {
// 当客户端断开后,需要将该客户端从客户端集合中移除
System.out.println(this.socket.getPort() + "退出群聊!");
clientSet.remove(this);
} finally {
// 关闭连接
try {
dis.close();
dos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/*
* 处理客户端集合,返回用户昵称字符串,以“$”开头。
*/
private String getClientName() {
String str = "$";
for (Client client : clientSet) {
str += client.getName() + ",";
}
return str;
}
}
}
客户端代码
package com.chatroom;
import java.net.*;
import java.util.Scanner;
import java.io.*;
public class TestSockClient {
private DataInputStream dis;
private DataOutputStream dos;
private Socket socket;
@SuppressWarnings("resource")
public void init() {
String str = null;
Scanner input = new Scanner(System.in);
try {
// 客户端连接服务器
socket = new Socket("127.0.0.1", 8888);
System.out.println("客户端已连接,请输入发送信息:");
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
// 当连接成功后开启子线程实现循环接收服务端发送的信息的功能。
// 需要另起一个线程来接收服务器信息,这里用匿名内部类。
new Thread() {
@Override
public void run() {
String s = null;
try {
while (true) {
if ((s = dis.readUTF()) != null)
if (s.startsWith("$")) {
String[] names = s.substring(1).split(",");
for (String name : names) {
System.out.println(name);
}
} else {
System.out.println(s);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
// 在主线程中实现从控制台接收数据并且发送到服务器的功能。
do {
str = input.next();
dos.writeUTF(str);
} while (!str.equals("88"));
System.out.println("client over!");
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭连接
try {
dis.close();
dos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TestSockClient tc = new TestSockClient();
tc.init();
}
}
运行显示
启动服务端和三个客户端
服务端显示:
客户端一输入显示:
可以看到客户端可以正确获得信息。
Step4:GUI客户端开发:
功能描述
上一个实验已经完成了命令行下的聊天室操作,通过测试可以发现虽然功能实现了,但是没有窗口的程序使用起来特别的不方便。因此本实验将客户端通过Java的GUI升级成窗口程序。服务端不做修改。
思路:
- 首先需要绘制窗体并在窗体中添加组件,可以使用开发工具提供的GUI插件实现此步操作。
- 显示线程同样需要,窗体创建后启动线程监听服务端发送的信息。
- 发送按钮监听组件动作,发送信息输入框中的输入信息到服务端。
- 当窗体创建时需要自服务端获得用户昵称列表。
- 当用户修改昵称的时候,需要将更新的用户昵称列表更新到客户端(重点)
实验总结
1.