Java 基础(四)

🍵 Java是当今世界最重要、使用最广泛的计算机语言之一,全球每天有超百万的开发者在用Java进行开发。

1 异常处理

  • 异常处理
    • 异常又称例外,是一个在程序执行期间发生的事件,中断正在执行程序的正常指令流。
    • 异常产生的三种原因
      • Java内部错误发生异常,Java虚拟机产生的异常。
      • 编写的程序代码错误所产生的异常,例如:空指针异常、数组越界异常等。
      • 通过throw语句手动生成的异常,用于告知该方法的调用者一些必要信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.Scanner;

public class Test137 {
public static void main(String[] args) {
System.out.print("请输入您的选择(1~3之间的整数):");
Scanner input = new Scanner(System.in);
int num = input.nextInt();
switch (num) {
case 1:
System.out.println("One");
break;
case 2:
System.out.println("Two");
break;
case 3:
System.out.println("Three");
break;
default:
System.out.println("Error");
break;
}
}
}

1-1 异常类型

  • 异常类型
    • 在Java中,所有的异常类型都是内置类java.lang.Throwable类的子类。
    • Throwable分为Error(错误)和Exception(异常)两个子类,异常又分为运行时、非运行时异常。
    • Error定义了在通常环境下不希望被程序捕获的异常,一般指JVM错误,如堆栈溢出。
    • Exception用于应用程序可能出现的异常情况,也是用来创建自定义异常类型类的类。
      • 运行时异常是RuntimeException类及其子类异常,不检查异常,程序中可选择捕获处理。
      • 非运行时异常是RuntimeException类以外的异常,属于Exception类及其子类,必须处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过无限递归演示堆栈溢出错误
class StackOverflow {
public static void test(int i) {
if (i == 0) {
return;
} else {
test(i++);
}
}
}

public class Test138 {
public static void main(String[] args) {
// 执行StackOverflow方法,引发StackOverflowError错误
StackOverflow.test(3);
}
}

1-2 基本结构

  • 基本结构
    • 异常处理通过5个关键字实现:try、catch、throw、throws、finally。
    • try..catch用于捕获并处理异常,throws用于声明可能会出现的异常。
    • throw用于抛出异常,finally则用于在任何情况下都必须执行的代码。

(1) try…catch语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.Scanner;

public class Test139 {
/**
* printStackTrace():指出异常的类型、性质、栈层次及出现在程序中的位置
* getMessage():输出错误的性质
* toString():给出异常的类型与性质
*/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String name = "";
int age = 0;
String sex = "";
try {
System.out.print("请输入学生姓名:");
name = scanner.next();
System.out.print("请输入学生年龄:");
age = scanner.nextInt();
System.out.print("请输入学生性别:");
sex = scanner.next();
} catch (Exception e) {
// 指出异常的类型、性质、栈层次及出现在程序中的位置
e.printStackTrace();
System.out.println("输入的年龄有误!");
}
System.out.println("姓名:" + name);
System.out.println("年龄:" + age);
}
}

(2) 多重catch语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.Date;
import java.io.IOException;
import java.text.DateFormat;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.text.ParseException;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.io.FileNotFoundException;

public class Test140 {
/**
* 捕获多个异常类之间存在父子关系时,一般先捕获子类,再捕获父类
* 因此子类异常必须在父类异常的前面,否则子类将捕获不到
*/
public static void main(String[] args) {
Date date = readDate();
System.out.println("读取的日期 = " + date);
}

public static Date readDate() {
FileInputStream readfile = null;
InputStreamReader ir = null;
BufferedReader in = null;
try {
// 通过输入输出流从文件读取字符串,解析成日期
readfile = new FileInputStream("./files/readme.txt");
ir = new InputStreamReader(readfile);
in = new BufferedReader(ir);
// 读取文件中的一行数据
String str = in.readLine();
if (str == null) {
return null;
}
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date date = df.parse(str);
return date;
} catch (FileNotFoundException e) {
System.out.println("处理FileNotFoundException...");
e.printStackTrace();
} catch (IOException e) {
System.out.println("处理IOException...");
e.printStackTrace();
} catch (ParseException e) {
System.out.println("处理ParseException...");
e.printStackTrace();
}
return null;
}
}

(3) try catch finally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.Scanner;

public class Test141 {
/**
* 程序在try块里打开了一些物理资源,这些物理资源必须显式回收
* 垃圾回收机制不会回收任何物理资源,只回收堆内存中对象所占用的内存
* 为确保一定能回收try块的物理资源,异常处理机制提供了finally代码块
* 并且在Java7之后提供了自动资源管理(Automatic Resource Management)
*/
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
String[] pros = { "记事本", "计算器", "浏览器" };
// 只有try块是必需的,没有try块就不能有catch和finally
// catch和finally都可选,但至少出现其一,不能只有try块
// 可有多个catch,捕获父类异常的catch须位于捕获子类异常后
// 多个catch块必须位于try块后,finally位于所有catch块后面
// finally与try语句块匹配的语法格式会导致异常丢失,不常用
try {
// 可能发生异常的语句,循环输出pros数组中的元素
for (int i = 0; i < pros.length; i++) {
System.out.println(i + 1 + "、" + pros[i]);
}
System.out.print("是否运行程序:");
String answer = input.next();
if (answer.equals("y")) {
System.out.print("输入程序编号:");
int no = input.nextInt();
System.out.println("正打开" + pros[no - 1] + "...");
}
} catch (Exception e) {
// 处理异常语句
e.printStackTrace();
} finally {
// 清理代码块
System.out.println("欢迎下次使用...");
}
}
}

1-3 资源管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.BufferedReader;
import java.io.FileOutputStream;

public class Test142 {
/**
* Java9再次增强了Java7的try语句,具体参考官方文档
*/
public static void main(String[] args) throws IOException {
try (
// 声明、初始化两个可关闭的资源,try语句会自动关闭这两个资源
// 这里如果是在IDEA中,则路径要写全src/.../Test142.java、src/.../files/input.txt
BufferedReader br = new BufferedReader(new FileReader("Test142.java"));
PrintStream ps = new PrintStream(new FileOutputStream("./files/input.txt"))) {
// 使用两个资源
System.out.println(br.readLine());
ps.println("Java7中的自动资源管理!");
}
}
}

1-4 声明和抛出

  • 声明和抛出
    • Java中除了捕获异常和处理异常,还包括了声明异常(throws)和抛出异常(throw)。
    • throws声明异常但不一定发生这些异常,执行throw则一定抛出一个具体异常类型。
    • 可通过throws在方法上声明该方法要抛出的异常,在方法内部通过throw抛出异常。
    • throws可由系统自动将所有捕获的异常抛给上级方法,throw则需要开发自己捕获。

(1) 声明异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.IOException;
import java.io.FileInputStream;

public class Test143 {
/**
* 如果有多个异常类,之间用逗号分隔
* 使用throws声明抛出异常时有一个限制,是方法重写中的一条规则
* 子类方法声明抛出的异常类型应是父类方法声明抛出的异常类型的子类或相同
* 子类方法声明抛出的异常不允许比父类方法声明抛出的异常多
*/
public void readFile() throws IOException {
// 定义方法时声明异常,这里如果是在IDEA中,则路径要写全src/.../files/input.txt
FileInputStream file = new FileInputStream("./files/input.txt");
int f;
while ((f = file.read()) != -1) {
System.out.print((char) f);
f = file.read();
}
file.close();
}

public static void main(String[] args) {
Test143 t = new Test143();
try {
// 调用方法
t.readFile();
} catch (IOException e) {
// 捕获异常
System.out.println(e);
}
}
}

class Sub extends Test143 {
// 子类方法声明抛出了比父类方法更大的异常
// Exception是父类声明抛出异常IOException类的父类
// public void readFile() throws Exception {
// }
}

(2) 抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.Scanner;

public class Test144 {
public boolean validateUserName(String username) {
boolean con = false;
if (username.length() > 8) {
for (int i = 0; i < username.length(); i++) {
// 获取每一位字符
char ch = username.charAt(i);
if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
con = true;
} else {
con = false;
throw new IllegalArgumentException("用户名只由字母和数字组成!");
}
}
} else {
throw new IllegalArgumentException("用户名的长度必须大于八位!");
}
return con;
}

public static void main(String[] args) {
Test144 t = new Test144();
Scanner input = new Scanner(System.in);
System.out.print("输入用户名:");
String username = input.next();
try {
boolean con = t.validateUserName(username);
if (con) {
System.out.println("用户名正确!");
}
} catch (IllegalArgumentException e) {
System.out.println(e);
}
}
}

1-5 多异常捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test145 {
/**
* 使用一个catch块捕获多种类型的异常时,异常类型之间用竖线|隔开
* 异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值
*/
public static void main(String[] args) {
try {
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("输入的两个数相除的结果是:" + c);
} catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException e) {
System.out.println("程序发生了数组越界、数字格式异常、算术异常之一!");
// 捕获多异常时,异常变量有隐式的final修饰,不能对异常变量重新赋值
// e = new ArithmeticException("算术异常");
} catch (Exception e) {
System.out.println("未知异常!");
// 捕获一种类型的异常时,异常变量没有final修饰
e = new RuntimeException("未知异常!");
}
}
}

1-6 自定义异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.Scanner;
import java.util.InputMismatchException;

// 自定义异常继承自Exception类,因此自定义异常类中包含父类所有的属性和方法
class MyException extends Exception {
public MyException() {
super();
}

public MyException(String str) {
super(str);
}
}

public class Test146 {
/**
* 自定义异常类一般包含两个构造方法,一个是无参的默认构造方法
* 另一构造方法以字符串形式接收定制的异常消息,并将其传递给超类的构造方法
*/
public static void main(String[] args) {
int age;
Scanner input = new Scanner(System.in);
System.out.print("请输入您的年龄:");
try {
age = input.nextInt();
if (age < 0) {
throw new MyException("输入有误!年龄不能为负数~");
} else if (age > 120) {
throw new MyException("输入有误!年龄不能大于120~");
} else {
System.out.println("您输入的年龄为:" + age);
}
} catch (InputMismatchException e1) {
System.out.println("您输入的年龄不是数字!");
} catch (MyException e2) {
System.out.println(e2.getMessage());
}
}
}

1-7 验证用户名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.util.Scanner;

// 自定义异常类LoginException
class LoginException extends Exception {
public LoginException() {
super();
}

public LoginException(String msg) {
super(msg);
}
}

// 创建测试类
public class Test147 {
// 定义validateLogin()方法,验证用户名和密码
public boolean validateLogin(String username, String pwd) {
boolean con = false;
boolean conUname = false;
try {
if (username.length() >= 6 && username.length() <= 10) {
for (int i = 0; i < username.length(); i++) {
char ch = username.charAt(i);
if (ch >= '0' && ch <= '9') {
conUname = true;
} else {
conUname = false;
throw new LoginException("用户名不能包含非数字的字符!");
}
}
} else {
throw new LoginException("用户名长度必须在6~10位之间!");
}

// 用户名格式正确之后,判断密码长度(只能6位)
if (conUname) {
if (pwd.length() == 6) {
con = true;
} else {
con = false;
throw new LoginException("密码长度必须是6位!");
}
}
} catch (LoginException e) {
System.out.println(e.getMessage());
}
return con;
}

public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("用户名:");
String username = input.next();
System.out.print("密 码:");
String password = input.next();
Test147 lt = new Test147();
boolean con = lt.validateLogin(username, password);
if (con) {
System.out.println("用户名和密码输入成功!");
}
}
}

1-8 异常跟踪栈

  • 异常跟踪栈
    • 异常对象的printStackTrace()方法用于打印异常的跟踪栈信息。
    • 根据输出结果可找到异常的源头并跟踪到异常一路触发的过程。

(1) printStackTrace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class SelfException extends RuntimeException {
SelfException() {
}

SelfException(String msg) {
super(msg);
}
}

public class Test148 {
public static void main(String[] args) {
firstMethod();
}

public static void firstMethod() {
secondMethod();
}

public static void secondMethod() {
thirdMethod();
}

// 异常从thirdMethod开始触发,传到secondMethod,再到firstMethod
// 最后传到main方法,在main方法终止,这一过程就是Java的异常跟踪栈
public static void thirdMethod() {
throw new SelfException("自定义异常信息!");
}
}

(2) 多线程异常情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test149 implements Runnable {
public void run() {
firstMethod();
}

public void firstMethod() {
secondMethod();
}

public void secondMethod() {
int a = 5;
int b = 0;
int c = a / b;
}

// 结果可看出程序在Thread的run方法中出现了ArithmeticException异常
// 该异常的源头是Test149的secondMethod方法,位于文件的第13行
public static void main(String[] args) {
new Thread(new Test149()).start();
}
}

2 日志记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.logging.Logger;

public class Test150 {
/**
* Logger的默认级别是Info,比Info级别低的日志不显示
* Logger的默认级别定义在jre/lib/logging.properties
* 可以通过编辑配置文件来修改日志系统的各种属性
*/
private static Logger log = Logger.getLogger(Test150.class.toString());

public static void main(String[] args) {
// 最好
log.finest("finest");
// 较好
log.finer("finer");
// 良好
log.fine("fine");
// 配置
log.config("config");
// 信息
log.info("info");
// 警告
log.warning("warning");
// 严重
log.severe("server");
}
}

3 反射机制

  • 反射机制
    • 编译期是指把源码交给编译器编译成计算机可以执行的文件过程。
    • Java中就是把代码编译成class文件的过程,编译期只做翻译功能。
    • 运行期是把编译后的class文件给计算机执行,直到程序运行结束。
    • Java反射机制在运行状态中
      • 对于任意一个类,都能知道这个类的所有属性和方法(动态获取信息)。
      • 对于任意一个对象,都能调用其任意方法和属性(动态调用对象方法)。

3-1 API

  • API
    • 实现Java反射机制的类都位于java.lang.reflect包中。
    • 而java.lang.Class类是Java反射机制API中的核心类。

(1) java.lang.Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Test151 {
/**
* Class类的一个实例表示Java的一种数据类型
* 包括类、接口、枚举、注解、数组、基本数据类型和void
* Class没有公有的构造方法,实例由JVM在类加载时自动创建
* 每一种数据类型都有一个class静态变量可以获得Class实例
* 每一个对象都有getClass()方法可获得Class实例,该方法由Object类提供
*/
public static void main(String[] args) {
// 获取Class实例:通过类型class静态变量
Class clz1 = String.class;
String str = "Hi~";

// 获取Class实例:通过对象的getClass()方法
Class clz2 = str.getClass();

// 获取int类型的Class实例,int是基本数据类型
Class clz3 = int.class;

// 获取Integer类型的Class实例,Integer是类,是引用数据类型
Class clz4 = Integer.class;

// 获取类名称
System.out.println(clz2.getName());
System.out.println(clz2.isInterface());
System.out.println(clz2.isArray());
System.out.println(clz2.getSuperclass().getName());
System.out.println(clz2.isPrimitive());
System.out.println(clz4.isPrimitive());
}
}

(2) java.lang.reflect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class Test152 {
/**
* Constructor:提供类的构造方法信息
* Field:提供类或接口中成员变量信息
* Method:提供类或接口成员方法信息
* Array:提供动态创建和访问Java数组的方法
* Modifier:提供类和成员访问修饰符信息
*/
public static void main(String[] args) {
try {
// 动态加载String类的运行时对象
Class c = Class.forName("java.lang.String");

// 获取成员方法集合
Method[] methods = c.getDeclaredMethods();

// 遍历成员方法集合
for (Method method : methods) {
// 打印权限修饰符
System.out.print(Modifier.toString(method.getModifiers()));

// 打印返回值类型名称
System.out.print(" " + method.getReturnType().getName() + " ");

// 打印方法名称
System.out.println(method.getName() + "():");
}
} catch (ClassNotFoundException e) {
System.out.println("找不到指定类!");
}
}
}

3-2 访问构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import java.lang.reflect.Constructor;

class Book1 {
String name;
int id, price;

// 空的构造方法
private Book1() {
}

// 带两个参数的构造方法
protected Book1(String _name, int _id) {
this.name = _name;
this.id = _id;
}

// 带可变参数的构造方法
public Book1(String... strings) throws NumberFormatException {
if (0 < strings.length)
id = Integer.valueOf(strings[0]);
if (1 < strings.length)
price = Integer.valueOf(strings[1]);
}

// 输出图书信息
public void print() {
System.out.println("name=" + name);
System.out.println("id=" + id);
System.out.println("price=" + price);
}
}

public class Test153 {
/**
* 为能动态获取对象构造方法的信息,通过方法创建Constructor类型的对象或数组
* getConstructors()、getDeclaredConstructor(Class<?>...parameterTypes)
* getConstructor(Class<?>...parameterTypes)、getDeclaredConstructors()
* 如果是访问指定的构造方法,需要根据该构造方法的入口参数的类型来访问
* objectClass.getDeclaredConstructor(int.class,String.class);
* objectClass.getDeclaredConstructor(new Class[]{int.class,String.class});
*/
public static void main(String[] args) {
// 获取动态类Book1
Class book1 = Book1.class;

// 获取Book1类的所有构造方法
Constructor[] declaredConstructors = book1.getDeclaredConstructors();

// 遍历所有构造方法
for (int i = 0; i < declaredConstructors.length; i++) {
Constructor con = declaredConstructors[i];

// 判断构造方法的参数是否可变
System.out.println("查看是否允许带有可变数量的参数:" + con.isVarArgs());

// 获取所有参数类型
Class[] parameterTypes = con.getParameterTypes();
for (int j = 0; j < parameterTypes.length; j++) {
System.out.println(parameterTypes[j]);
}

// 获取所有可能抛出的异常类型
Class[] exceptionTypes = con.getExceptionTypes();
for (int j = 0; j < exceptionTypes.length; j++) {
System.out.print("该构造方法可能抛出的异常类型为:");
System.out.println(parameterTypes[j]);
}

// 创建一个未实例化的Book1类实例
Book1 book2 = null;
while (book2 == null) {
// 如果该成员变量的访问权限为private,则抛出异常
try {
if (i == 1) {
// 通过执行带两个参数的构造方法实例化book2
book2 = (Book1) con.newInstance("Java", 10);
} else if (i == 2) {
// 通过执行默认构造方法实例化book2
book2 = (Book1) con.newInstance();
} else {
// 通过执行可变数量参数的构造方法实例化book2
Object[] parameters = new Object[] { new String[] { "100", "200" } };
book2 = (Book1) con.newInstance(parameters);
}
} catch (Exception e) {
// 设置允许访问private成员
con.setAccessible(true);
}
}
book2.print();
System.out.println("---------------------------------------------------------\n");
}
}
}

3-3 反射执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import java.lang.reflect.Method;

class Book2 {
// static作用域方法
static void staticMethod() {
System.out.println("执行staticMethod()方法");
}

// public作用域方法
public int publicMethod(int i) {
System.out.println("执行publicMethod()方法");
return 100 + i;
}

// protected作用域方法
protected int protectedMethod(String s, int i) throws NumberFormatException {
System.out.println("执行protectedMethod()方法");
return Integer.valueOf(s) + i;
}

// private作用域方法
private String privateMethod(String... strings) {
System.out.println("执行privateMethod()方法");
StringBuffer sb = new StringBuffer();
for (int i = 0; i < sb.length(); i++) {
sb.append(strings[i]);
}
return sb.toString();
}
}

public class Test154 {
/**
* 动态获取对象方法的信息,需要通过下列方法之一创建Method类型的对象或数组
* getMethods()、getDeclaredMethods(String name,Class<?>...parameterTypes)
* getMethods(String name,Class<?> …parameterTypes)、getDeclaredMethods()
* 如果是访问指定的构造方法,需要根据该方法的入口参数类型来访问
* objectClass.getDeclaredConstructor("max",int.class,String.class);
* objectClass.getDeclaredConstructor("max",new
* Class[]{int.class,String.class});
*/
public static void main(String[] args) {
// 获取动态类Book2
Book2 book = new Book2();
Class class2 = book.getClass();

// 获取Book2类的所有方法
Method[] declaredMethods = class2.getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
Method method = declaredMethods[i];
System.out.println("获取到的方法具体名称:" + method.getName());
System.out.println("是否带可变数量的参数:" + method.isVarArgs());

System.out.print("方法的参数类型依次为:");
// 获取所有参数类型
Class[] methodType = method.getParameterTypes();
for (int j = 0; j < methodType.length; j++) {
System.out.println(methodType[j]);
}

// 获取返回值类型
System.out.println("方法的返回值类型包含:" + method.getReturnType());

System.out.print("可能抛出的异常类型有:");
// 获取所有可能抛出的异常
Class[] methodExceptions = method.getExceptionTypes();
for (int j = 0; j < methodExceptions.length; j++) {
System.out.println(methodExceptions[j]);
}
boolean isTurn = true;
while (isTurn) {
// 如果该成员变量的访问权限为private,则抛出异常
try {
isTurn = false;
// 调用没有参数的方法
if (method.getName().equals("staticMethod")) {
method.invoke(book);
}
// 调用一个参数的方法
else if (method.getName().equals("publicMethod")) {
System.out.println("publicMethod(10)的返回值为:" + method.invoke(book, 10));
}
// 调用两个参数的方法
else if (method.getName().equals("protectedMethod")) {
System.out.println("protectedMethod(\"10\",15)的返回值为:" + method.invoke(book, "10", 15));
}
// 调用可变数量参数的方法
else if (method.getName().equals("privateMethod")) {
Object[] parameters = new Object[] { new String[] { "J", "A", "V", "A" } };
System.out.println("privateMethod()的返回值为:" + method.invoke(book, parameters));
}
} catch (Exception e) {
// 设置为允许访问private方法
method.setAccessible(true);
isTurn = true;
}
}
System.out.println("---------------------------------------------------------\n");
}
}
}

3-4 访问成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import java.lang.reflect.Field;

class Book3 {
String name;
public int id;
private float price;
protected boolean isLoan;
}

public class Test155 {
/**
* 通过下列任意方法访问成员变量时将返回Field类型的对象或数组
* getFields()、getDeclaredField(String name)
* getField(String name)、getDeclaredFields()
* 上述方法返回的Field对象代表一个成员变量
* object.getDeciaredField("price");
*/
public static void main(String[] args) {
Book3 book3 = new Book3();
// 获取动态类Book3
Class class3 = book3.getClass();
// 获取Book3类的所有成员
Field[] declaredFields = class3.getDeclaredFields();
// 遍历所有的成员
for (int i = 0; i < declaredFields.length; i++) {
// 获取类中的成员变量
Field field = declaredFields[i];
System.out.println("成员名称为:" + field.getName());
Class fieldType = field.getType();
System.out.println("成员类型为:" + fieldType);
boolean isTurn = true;
while (isTurn) {
try {
// 如果该成员变量的访问权限为private,则抛出异常
isTurn = false;
System.out.println("改前成员值:" + field.get(book3));
// 判断成员类型是否为int
if (fieldType.equals(int.class)) {
System.out.println("利用setInt()方法修改成员的值");
field.setInt(book3, 100);
}
// 判断成员变量类型是否为float
else if (fieldType.equals(float.class)) {
System.out.println("利用setFloat()方法修改成员的值");
field.setFloat(book3, 29.815f);
}
// 判断成员变量是否为boolean
else if (fieldType.equals(boolean.class)) {
System.out.println("利用setBoolean()方法修改成员的值");
field.setBoolean(book3, true);
} else {
System.out.println("利用set()方法修改成员的值");
field.set(book3, "Java编程");
}
System.out.println("改后成员值:" + field.get(book3));
} catch (Exception e) {
field.setAccessible(true);
isTurn = true;
}
}
System.out.println("---------------------------------------------------------\n");
}
}
}

3-5 运用反射机制

  • 运用反射机制
    • 客户端如何调用服务器端HelloServiceImpl类中的echo()和getTime()方法?
      • 把调用的方法名、参数类型、参数值及所属类名或接口名发送给服务器端。
      • 再由服务器端调用相关对象的方法,最后,将方法的返回值发送给客户端。
    • 为便于按面向对象的方式来处理客户端与服务器端的通信,把它们发送的信息用Call类表示。
    • 一个Call对象表示客户端发起的一个远程调用,包括调用的接口名、参数类型、执行结果等。
    • 这是一个网络程序,首先需要运行服务器端SimpleServer,然后再运行客户端SimpleClient。
      • javac SimpleServer.java && java SimpleServer
      • javac SimpleClient.java && java SimpleClient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Date;

// 假定在服务器端有一个HelloService接口,具有echo()和getTime()方法
interface HelloService {
public String echo(String msg);

public Date getTime();
}

// 在服务器上创建一个HelloServiceImpl类并实现HelloService接口
// 在HelloServiceImpl类中对echo()方法和getTime()方法进行了重写
public class HelloServiceImpl implements HelloService {
@Override
public String echo(String msg) {
return "echo:" + msg;
}

@Override
public Date getTime() {
return new Date();
}
}

(1) Call类的实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import java.io.Serializable;

public class Call implements Serializable {
private static final long serialVersionUID = 6659953547331194808L;
// 表示类名或接口名
private String className;
// 表示方法名
private String methodName;
// 表示方法参数类型
private Class[] paramTypes;
// 表示方法参数值
private Object[] params;
// 表示方法的执行结果,正常执行则result为方法返回值,抛出异常则result为该异常
private Object result;

public Call() {
}

public Call(String className, String methodName, Class[] paramTypes, Object[] params) {
this.className = className;
this.methodName = methodName;
this.paramTypes = paramTypes;
this.params = params;
}

public String getClassName() {
return className;
}

public void setClassName(String className) {
this.className = className;
}

public String getMethodName() {
return methodName;
}

public void setMethodName(String methodName) {
this.methodName = methodName;
}

public Class[] getParamTypes() {
return paramTypes;
}

public void setParamTypes(Class[] paramTypes) {
this.paramTypes = paramTypes;
}

public Object[] getParams() {
return params;
}

public void setParams(Object[] params) {
this.params = params;
}

public Object getResult() {
return result;
}

public void setResult(Object result) {
this.result = result;
}

public String toString() {
return "className=" + className + "methodName=" + methodName;
}
}

(2) 客户端SimpleClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.*;
import java.net.*;

public class SimpleClient {
public void invoke() throws Exception {
Socket socket = new Socket("localhost", 8000);

OutputStream out = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);

InputStream in = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);

// SimpleClient创建一个远程调用对象Call
// 包含调用HelloService接口的echo()方法信息
Call call = new Call("ch12.HelloService", "echo", new Class[] { String.class }, new Object[] { "Java" });

// SimpleClient通过对象输出流向服务器SimpleServer发送Call对象
oos.writeObject(call);

// SimpleClient通过对象输入流接收了包含方法执行结果的Call对象
call = (Call) ois.readObject();
System.out.println(call.getResult());

ois.close();
oos.close();
socket.close();
}

public static void main(String args[]) throws Exception {
new SimpleClient().invoke();
}
}

(3) 服务端SimpleServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import java.io.*;
import java.net.*;
import java.util.*;
import java.lang.reflect.*;

public class SimpleServer {
// 存放远程对象的缓存
private Map remoteObjects = new HashMap();

// 把一个远程对象放到缓存中
public void register(String className, Object remoteObject) {
remoteObjects.put(className, remoteObject);
}

public void service() throws Exception {
ServerSocket serverSocket = new ServerSocket(8000);
System.out.println("服务器启动.");

while (true) {
Socket socket = serverSocket.accept();
InputStream in = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
OutputStream out = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);

// 接收客户发送的Call对象,SimpleServer通过对象输入流读取Call对象
Call call = (Call) ois.readObject();
System.out.println(call);

// 调用相关对象的方法
call = invoke(call);
// SimpleServer通过对象输出流向客户发送包含了执行结果的Call对象
oos.writeObject(call);

ois.close();
oos.close();
socket.close();
}
}

// 运用反射机制调用HelloServiceImpl对象的echo()方法
// 把echo()方法的执行结果保存到Call对象中
public Call invoke(Call call) {
Object result = null;
try {
String className = call.getClassName();
String methodName = call.getMethodName();
Object[] params = call.getParams();
Class classType = Class.forName(className);
Class[] paramTypes = call.getParamTypes();
Method method = classType.getMethod(methodName, paramTypes);

// 从缓存中取出相关的远程对象
Object remoteObject = remoteObjects.get(className);

if (remoteObject == null) {
throw new Exception(className + "的远程对象不存在");
} else {
result = method.invoke(remoteObject, params);
}
} catch (Exception e) {
result = e;
}
// 设置方法执行结果
call.setResult(result);
return call;
}

public static void main(String args[]) throws Exception {
SimpleServer server = new SimpleServer();

// 把事先创建的HelloServiceImpl对象加入到服务器的缓存中
server.register("ch13.HelloService", new HelloServiceImpl());
server.service();
}
}

4 Java注解

  • Java注解
    • Java 5开始增加了对元数据(即注解:代码里的特殊标记)的支持,与注释有一定区别。
    • 注解,Annotation,以@符号开头,不能改变程序的运行结果,也不会影响运行性能。
    • 有些注解可以在编译时给用户提示或警告,有些则可以在运行时读写字节码文件信息。
    • 作用:生成帮助文档、跟踪代码依赖性,实现替代配置文件功能、在编译时检查格式。

4-1 指定方法重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class P1 {
private String name = "钱三两";
private int age = 23;

@Override
// 如果toString()写错了,那么程序会发生编译错误
// @Override的作用是告诉编译器检查这个方法
// 保证父类要包含一个被该方法重写的方法,否则就会编译出错
public String toString() {
return name + ":" + age + "岁!";
}
}

public class Test156 {
/*
* @Override注解用来指定方法重写
* 只能修饰方法且只能用于方法重写,不能修饰其它元素
* 可以强制一个子类必须重写父类方法或实现接口的方法
* 不加@Override注解,即便方法名错了编译器也不会提示
*/
public static void main(String[] args) {
P1 p1 = new P1();
System.out.println(p1);
System.out.println(p1.toString());
}
}

4-2 注解类接口等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Deprecated
class P2 {
@Deprecated
protected String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Deprecated
public void setNameAndAge(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return name + ":" + age + "岁!";
}
}

public class Test157 {
/*
* @Deprecated用来注解类、接口、成员方法和成员变量等
* 表示某元素已过时,当其他程序使用已过时元素时,编译器警告
* Java 9增加了两个属性
* forRemoval:boolean类型的属性指定该API在将来是否会被删除
* since:String类型的属性指定该API从哪个版本被标记为过时
* @Deprecated的作用与文档注释中的@deprecated标记基本相同
*/
public static void main(String[] args) {
P2 p2 = new P2();
p2.setNameAndAge("钱三两", 23);
p2.name = "赵五钱";
System.out.println(p2.toString());
}
}

4-3 抑制编译器警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test158 {
public static void main(String[] args) {
// 传递可变参数,参数是泛型集合
display(10, 20, 30);

// 传递可变参数,参数是非泛型集合,会有unchecked警告
display("10", 20, 30);
}

public static <T> void display(T... array) {
for (T arg : array) {
System.out.println(arg.getClass().getName() + ":" + arg);
}
}
}

(1) @SafeVarargs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test159 {
/*
* @SafeVarargs注解不适用于非static或非final声明的方法
* 对于未声明为static或final的方法,使用@SuppressWarnings注解抑制unchecked警告
*/
public static void main(String[] args) {
// 传递可变参数,参数是泛型集合
display(10, 20, 30);

// 传递可变参数,参数是非泛型集合,会有unchecked警告
display("10", 20, 30);
}

@SafeVarargs
public static <T> void display(T... array) {
for (T arg : array) {
System.out.println(arg.getClass().getName() + ":" + arg);
}
}
}

(2) @SuppressWarnings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test160 {
/*
* 抑制单类型的警告:@SuppressWarnings("unchecked")
* 抑制多类型的警告:@SuppressWarnings("unchecked","rawtypes")
* 抑制所有类型的警告:@SuppressWarnings("unchecked")
*/
@SuppressWarnings({ "deprecation" })
public static void main(String[] args) {
P2 p3 = new P2();
p3.setNameAndAge("张小巧", 23);
p3.name = "颜四六";
System.out.println(p3);
}
}

4-4 指定函数式接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@FunctionalInterface
interface FunInterface {
static void print() {
System.out.println("Hello, World!");
}

default void show() {
System.out.println("This is test.");
}

// 只定义一个抽象方法
void test();

// 第二个抽象方法,接口不再是函数式接口
// void anotherTest();
}

public class Test161 {
/*
* 函数式接口:接口中只有一个抽象方法(可包含多个默认方法或static方法)
* @FunctionalInterface用来指定某接口必须是函数式接口
* 所以@FunInterface只能修饰接口,不能修饰其他程序元素
*/
public static void main(String[] args) {
// 使用lambda表达式简洁实现函数式接口
FunInterface fi = () -> System.out.println("Lambda implementation of test().");

// 调用抽象方法
fi.test();

// 调用默认方法
fi.show();

// 调用静态方法
FunInterface.print();

// 如果试图添加第二个抽象方法,会编译报错
}
}

4-5 Java元注解使用

  • Java元注解使用
    • Java 5定义了@Documented、@Target、@Retention、@Inherited。
    • Java 8增加了@Repeatable、@Native,可在java.lang.annotation包中找到。
    • @Native注解:表示该变量可以被本地代码引用,被代码生成工具使用,不常用。
    • @Retention注解:用于描述注解的生命周期,也就是指该注解被保留的时间长短。
      • @Retention注解的成员变量value,用来设置保留策略。
      • java.lang.annotation.RetentionPolicy枚举类型。
    • RetentionPolicy的3个枚举常量
      • SOURCE(源文件保留)、CLASS(即class保留)、RUNTIME(运行时保留)。
      • 生命周期排序:SOURCE < CLASS < RUNTIME ,前者能用后者定能用。

(1) @Target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.lang.annotation.ElementType;

// 定义注解,只能用于方法上
@Target({ ElementType.METHOD })
@interface MyTarget {
}

public class Test162 {
/*
* @Target注解用于指定一个注解的使用范围
* 注解有个成员变量value用来设置适用目标
* value是java.lang.annotation.ElementType枚举类型的数组
* ElementType常用枚举常量:CONSTRUCTOR(构造方法)、PACKAGE(包)
* TYPE(类、接口或enum声明)、LOCAL_VARIABLE(局部变量)
* METHOD(方法)、PARAMETER(类型参数)、FIELD(成员变量)
*/
@MyTarget
public void annotatedMethod() {
System.out.println("annotatedMethod被调用!");
}

public void normalMethod() {
System.out.println("normalMethod被调用!");
}

public static void main(String[] args) throws Exception {
Class<Test162> clazz = Test162.class;

// 遍历所有方法
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(MyTarget.class)) {
System.out.println(method.getName() + "上存在@MyTarget注解!");
// 调用被注解的方法
method.invoke(clazz.newInstance());
} else {
System.out.println(method.getName() + "上不存在@MyTarget注解!");
}
}
}
}

(2) @Inherited

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.lang.annotation.Target;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

// 创建一个自定义注解
@Target({ ElementType.TYPE })
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface MyInherited {
}

@MyInherited
public class Test163 {
/*
* @Inherited是一个标记注解,用来指定该注解可以被继承
* 使用@Inherited注解的Class类,表示这个注解可以被用于该Class类的子类
*/
public static void main(String[] args) {
System.out.println(Test163.class.getAnnotation(MyInherited.class));
System.out.println(TestB.class.getAnnotation(MyInherited.class));
System.out.println(TestC.class.getAnnotation(MyInherited.class));
}
}

class TestB extends Test163 {
}

class TestC extends TestB {
}

(3) @Repeatable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.lang.reflect.Method;
import java.lang.annotation.Retention;
import java.lang.annotation.Repeatable;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Roles {
Role[] value();
}

@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Roles.class)
@interface Role {
String roleName();
}

public class Test164 {
/*
* @Repeatable注解是Java 8新增加的,允许在相同的程序元素中重复注解
* 在Java 8版本之前,同一个程序元素前最多只能有一个相同类型的注解
* 如果需要在同一个元素前使用多个相同类型的注解,就必须使用注解“容器”
* 重复注解只是一种简化写法,这种简化写法实际上是一种假象
* 多个重复注解同样被作为“容器”注解的value成员数组元素进行处理
*/
@Role(roleName = "role1")
@Role(roleName = "role2")
public String doString() {
return "测试";
}

public static void main(String[] args) throws NoSuchMethodException {
// 获取Test164类的doString方法对象
Method method = Test164.class.getDeclaredMethod("doString");

// 获取所有Role注解
Role[] roles = method.getAnnotationsByType(Role.class);

System.out.println("获取的Role注解数量:" + roles.length);
for (Role role : roles) {
System.out.println("Role名称:" + role.roleName());
}
}
}

(4) @Documented

  • 文件编译:javac Test165.java
  • 生成文档:javadoc -d doc Test165.java
  • 若命令执行报错“错误: 编码GBK的不可映射字符”。
  • 文件编译:javac -encoding UTF-8 Test165.java
  • 生产文档:javadoc -encoding UTF-8 -docencoding UTF-8 -charset UTF-8 -d doc Test165.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.lang.reflect.Method;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

// 表明MyDocumented注解会被JavaDoc工具提取
@Documented
// 可用于类和方法
@Target({ ElementType.TYPE, ElementType.METHOD })
// 运行时保留,方便反射读取
@Retention(RetentionPolicy.RUNTIME)
@interface MyDocumented {
String value() default "@Documented注解";
}

public class Test165 {
/*
* @Documented是一个标记注解,没有成员变量
* 用@Documented注解修饰的注解类会被JavaDoc工具提取成文档
*/
@MyDocumented("测试自定义注解")
public String test() {
return "测试";
}

public static void main(String[] args) {
try {
// 获取Test165类的Class对象
Class<Test165> clazz = Test165.class;

// 获取test方法
Method method = clazz.getDeclaredMethod("test");

// 判断方法上是否有MyDocumented注解
if (method.isAnnotationPresent(MyDocumented.class)) {
MyDocumented annotation = method.getAnnotation(MyDocumented.class);
System.out.println("方法test在@MyDocumented注解上的值:" + annotation.value());
} else {
System.out.println("方法test在@MyDocumented注解上没有值!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

4-6 Java自定义注解

  • Java自定义注解
    • 声明自定义注解使用@interface关键字实现,定义注解与定义接口很像。
    • 默认情况下注解可在程序的任何地方使用,修饰类、接口、方法和变量等。
    • 注解前面的访问修饰符和类一样都有两种:公有访问权限、默认访问权限。
    • 一个源程序文件中可声明多个注解,但只能有一个是公有访问权限的注解。
    • 并且源程序文件命名和公有访问权限的注解名需要保持一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 标记注解:不包含任何成员变量的注解
// 标记注解:是特殊的自定义注解,是自定义注解的一个类别
public @interface MyMarker {}

// 带成员变量的自定义注解
public @interface MyTag {
String name();
}

// 元数据注解:包含成员变量的注解
// 元数据注解:一般指Java标准库提供的内置注解,通常不属于“自定义注解”的范畴
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SomeMetaAnnotation {}

(1) 成员变量定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.lang.reflect.Method;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// 保留注解信息到运行时
@Retention(RetentionPolicy.RUNTIME)

// 声明了一个MyTag1注解,定义了两个成员变量
@interface MyTag1 {
String name();

int age();
}

public class Test166 {
// 使用带成员变量的注解时,需要为其赋值
@MyTag1(name = "钱三两", age = 23)
public void info() {
}

public static void main(String[] args) {
try {
// 获取Test166类的字节码对象
Class<Test166> clazz = Test166.class;

// 获取info方法
Method method = clazz.getMethod("info");

// 判断该方法是否使用了MyTag1注解
if (method.isAnnotationPresent(MyTag1.class)) {
// 获取注解实例
MyTag1 annotation = method.getAnnotation(MyTag1.class);

// 访问注解的成员变量值
System.out.println("姓名:" + annotation.name());
System.out.println("年龄:" + annotation.age());
} else {
System.out.println("info方法没有使用MyTag1注解!");
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}

(2) 默认成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.lang.reflect.Method;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// 保留注解信息到运行时
@Retention(RetentionPolicy.RUNTIME)

// 声明了一个MyTag2注解,定义了两个指定初始值的成员变量
@interface MyTag2 {
String name() default "张三丰";

int age() default 27;
}

public class Test167 {
// MyTag2注释的成员变量有默认值,可以不为其成员变量赋值
@MyTag2
public void info() {
}

public static void main(String[] args) {
try {
// 获取Test167类的字节码对象
Class<Test167> clazz = Test167.class;

// 获取info方法
Method method = clazz.getMethod("info");

// 判断该方法是否使用了MyTag1注解
if (method.isAnnotationPresent(MyTag2.class)) {
// 获取注解实例
MyTag2 annotation = method.getAnnotation(MyTag2.class);

// 访问注解的成员变量值
System.out.println("姓名:" + annotation.name());
System.out.println("年龄:" + annotation.age());
} else {
System.out.println("info方法没有使用MyTag2注解!");
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}

5 Java I/O流

  • Java I/O流
    • 流是一组有序的数据序列,即将数据从一个地方带到另一个地方。
    • Java程序通过流来完成输入输出,所有输入输出都以流形式处理。
    • 数据流是Java进行I/O操作的对象,按不同标准可分成不同的类别。
      • 按流方向分:输入流、输出流。
      • 按数据单位:字节流、字符流。
      • 按功能分类:节点流、处理流。

5-1 系统流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.IOException;

public class Test168 {
/*
* 每个Java程序运行时都带有一个系统流,对应类java.lang.System
* System.in:标准输入流,默认设备键盘,是InputStream类的对象
* System.out:标准输出流,默认设备控制台,是PrintStream类的对象
* System.err:标准错误流,默认设备控制台,是PrintStream类的对象
*/
public static void main(String[] args) {
// 声明一个字节数组
byte[] byteData = new byte[100];
System.out.print("请输入英文:");
try {
System.in.read(byteData);
} catch (IOException e) {
e.printStackTrace();
}

System.out.print("输入内容为:");
for (int i = 0; i < byteData.length; i++) {
System.out.print((char) byteData[i]);
}
}
}

5-2 字符编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.File;
import java.io.OutputStream;
import java.io.FileOutputStream;

public class Test169 {
/*
* ISO8859-1:单字节编码
* GBK/GB2312:中文国际编码,用来表示汉字,属于双字节编码
* Unicode:编码规范,为解决全球字符通用编码而设计的
* UTF:兼容了ISO8859-1编码,同时也可用来表示所有的语言字符
*/
public static void main(String[] args) throws Exception {
System.out.println("系统默认编码:" + System.getProperty("file.encoding"));

// 这里如果是在IDEA中,则路径要写全src/.../files/Test169.txt
File f = new File("./files" + File.separator + "Test169.txt");

// 实例化输出流
OutputStream out = new FileOutputStream(f);

// 指定ISO8859-1编码
byte b[] = "通过ISO8859-1进行编码转换".getBytes("ISO8859-1");

// 保存转码之后的数据
out.write(b);

// 关闭输出流
out.close();
}
}

5-3 文件操作类

  • 文件操作类
    • Java中,File类是java.io包中唯一代表磁盘文件本身的对象。
    • File类不能访问文件内容本身,需使用输入输出流才能访问。

(1) 获取文件属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.File;
import java.util.Date;

public class Test170 {
public static void main(String[] args) {
// 指定文件所在目录,这里如果是在IDEA中,则路径要写全src/.../files/
String path = "./files/";
File f = new File(path, "Test169.txt");
System.out.println("是否可读:" + (f.canRead() ? "可读" : "不可读"));
System.out.println("是否可写:" + (f.canWrite() ? "可写" : "不可写"));
System.out.println("文件目录:" + (f.isFile() ? "文件" : "非文件"));
System.out.println("文件目录:" + (f.isDirectory() ? "目录" : "非目录"));
System.out.println("是否隐藏:" + (f.isHidden() ? "隐藏" : "非隐藏"));
System.out.println("文件长度:" + f.length() + "字节");
System.out.println("文件名称:" + f.getName());
System.out.println("文件路径:" + f.getPath());
System.out.println("绝对路径:" + f.getAbsolutePath());
System.out.println("最后修改:" + new Date(f.lastModified()));
}
}

(2) 创建删除文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.File;
import java.io.IOException;

public class Test171 {
/*
* 创建文件需要调用createNewFile()方法
* 删除文件需要调用delete()方法
* 无论创建删除都要先调用exists()方法判断文件是否存在
*/
public static void main(String[] args) throws IOException {
// 创建指向文件的File对象,IDEA中src/.../files/Test171.txt
// File f = new File("./files/Test171.txt");

// 编写路径时最好可根据所在系统自动使用符合要求的分隔符,IDEA中src/.../files
String path = "./files" + File.separator + "Test171.txt";
File f = new File(path);

if (f.exists()) {
// 存在先删除
f.delete();
}
// 再创建
f.createNewFile();
}
}

(3) 创建删除目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.File;

public class Test172 {
public static void main(String[] args) {
// 指定目录位置,IDEA中src/.../test
String path = "./test";
// 创建File对象
File f = new File(path);
if (f.exists()) {
f.delete();
}
f.mkdir();
}
}

(4) 遍历目录功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.io.File;
import java.io.FilenameFilter;

class ImageFilter implements FilenameFilter {
// 实现FilenameFilter接口
@Override
public boolean accept(File dir, String name) {
// 指定允许的文件类型
return name.endsWith(".sys") || name.endsWith(".txt");
}
}

public class Test173 {
/*
* File类的list()方法提供了遍历目录功能,有两种重载形式
* String[] list():list()方法返回的数组中仅包含文件名称
* String[] list(FilenameFilter filter)
* 同list(),不同的是返回数组仅包含符合filter的文件和目录
*/
public static void main(String[] args) {
// 创建File变量,并设定由f变量变数引用
File f = new File("C:");

// 调用不带参数的list()方法
String fileList[] = f.list();

// 调用带过滤器参数的list()方法
String fileLists[] = f.list(new ImageFilter());

System.out.printf("%-26s %-8s %-10s%n", "文件名称", "文件类型", "文件大小");
System.out.println("----------------------------------------------------------");
for (int i = 0; i < fileList.length; i++) {
File file = new File(f, fileList[i]);
String type = file.isFile() ? "文件" : "目录";
long size = file.length();
System.out.printf("%-30s %-10s %d字节%n", fileList[i], type, size);
}

System.out.println("----------------------------------------------------------");
for (int i = 0; i < fileLists.length; i++) {
File file = new File(f, fileLists[i]);
String type = file.isFile() ? "文件" : "目录";
long size = file.length();
System.out.printf("%-30s %-10s %d字节%n", fileLists[i], type, size);
}
}
}

5-4 字节流使用

  • 字节流使用
    • 字节输入流:InputStream类及其子类的对象表示字节输入流,常用子类如下。
      • FileInputStream类:从文件中读取数据。
      • ObjectInputStream类:将对象反序列化。
      • SequenceInputStream类:将多个字节输入流串联成一个字节输入流。
      • PipedInputStream类:连接到一个PipedOutputStream(管道输出流)。
      • ByteArrayInputStream类:将字节数组转换为字节输入流,从中读取字节。
    • 字节输出流:OutputStream类及其子类的对象表示字节输出流,常用子类如下。
      • FileOutputStream类:向文件中写数据。
      • ObjectOutputStream类:将对象序列化。
      • ByteArrayOutputStream类:向内存缓冲区的字节数组中写数据。
      • PipedOutputStream类:连接到一个PipedlntputStream(管道输入流)。

(1) 字节数组输入流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.ByteArrayInputStream;

public class Test174 {
/*
* ByteArrayInputStream类可从内存的字节数组中读取数据
* 构造方法重载形式:ByteArrayInputStream(byte[] buf)
* ByteArrayInputStream(byte[] buf, int offse, int length)
*/
public static void main(String[] args) {
// 创建数组
byte[] b = new byte[] { 13, -1, 25, -9, -5, 23 };
// 创建字节数组输入流
ByteArrayInputStream bais = new ByteArrayInputStream(b, 0, 6);
// 从输入流中读取下一个字节,并转换成int型数据
int i = bais.read();
while (i != -1) {
// 负数的二进制形式以补码形式存在
System.out.println("原值:" + (byte) i + "\t\t转换为int类型:" + i);
// 读取下一个
i = bais.read();
}
}
}

(2) 字节数组输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Arrays;
import java.io.ByteArrayOutputStream;

public class Test175 {
/*
* ByteArrayOutputStream类可以向内存的字节数组中写入数据
* ByteArrayOutputStream()、ByteArrayOutputStream(int size)
*/
public static void main(String[] args) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 创建数组
byte[] b = new byte[] { 13, -1, 25, -9, -5, 23 };
// 将字节数组b中的前4个字节元素写到输出流中
baos.write(b, 0, 6);
// 输出缓冲区中的字节数
System.out.println("数组共包含的字节数:" + baos.size() + "字节");
// 将输出流中的当前内容转换成字节数组
byte[] newByteArray = baos.toByteArray();
// 输出数组中的内容
System.out.println(Arrays.toString(newByteArray));
}
}

(3) 字节文件输入流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.File;
import java.io.IOException;
import java.io.FileInputStream;

public class Test176 {
/*
* FileInputStream较为常用,从文件系统的某个文件中获取输入字节
* FileInputStream(File file)、FileInputStream(String name)
* FileInputStream类重写了父类InputStream中的read()、skip()
* available()、close()方法,不支持mark()方法和reset()方法
*/
public static void main(String[] args) {
// IDEA中路径为src/.../Test175.java
File f = new File("./Test175.java");
FileInputStream fis = null;
try {
// File没有读写能力,需要有个InputStream
fis = new FileInputStream(f);
// 定义一个字节数组
byte[] bytes = new byte[1024];
// 得到实际读取到的字节数
int n = 0;
System.out.println("文件内容如下:");
// 循环读取
while ((n = fis.read(bytes)) != -1) {
// 将数组从下标0到n的内容赋值给s
String s = new String(bytes, 0, n);
System.out.println(s);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

(4) 字节文件输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.io.File;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class Test177 {
/*
* FileOutputStream类继承自OutputStream类,重写和实现父类所有方法
* 创建FileOutputStream类的对象时,指定文件不存在则创建,存在则重写
* FileOutputStream(File file)
* FileOutputStream(File file, boolean append)
* FileOutputStream(String name)
* FileOutputStream(String name, boolean append)
* 创建FileOutputStream对象时,如果将append参数设为true
* 则可在目标文件的内容末尾添加数据,此时目标文件仍可暂不存在
*/
public static void main(String[] args) {
// 声明FileInputStream对象fis
FileInputStream fis = null;
// 声明FileOutputStream对象fos
FileOutputStream fos = null;
try {
// IDEA中路径为src/.../Test176.java
File srcFile = new File("./Test176.java");
// 实例化FileInputStream对象
fis = new FileInputStream(srcFile);
// 创建目标文件对象,实际文件不存在,IDEA中src/.../files/Test176.txt
File targetFile = new File("./files/Test176.txt");
// 实例化FileOutputStream对象
fos = new FileOutputStream(targetFile);
// 每次读取1024字节
byte[] bytes = new byte[1024];
int i = fis.read(bytes);
while (i != -1) {
// 向文件./files/Test176.txt写入内容
fos.write(bytes, 0, i);
i = fis.read(bytes);
}
System.out.println("写入结束啦!");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 关闭FileInputStream、FileOutputStream对象
fis.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

5-5 字符流使用

  • 字符流使用
    • 字符输入流:Reader类是所有字符流输入类的父类,常用子类如下。
      • PipedReader类:连接到一个PipedWriter。
      • BufferedReader类:为其他字符输入流提供读缓冲区。
      • StringReader类:将字符串转换为字符输入流,从中读取字符。
      • CharArrayReader类:将字符数组转换为字符输入流,从中读取字符。
      • InputStreamReader类:将字节输入流转换为字符输入流,可指定字符编码。
    • 字符输出流:Writer类是所有字符输出流的父类,常用子类如下。
      • PipedWriter类:连接到一个PipedReader。
      • BufferedWriter类:为其他字符输出流提供写缓冲区。
      • CharArrayWriter类:向内存缓冲区的字符数组写数据。
      • StringWriter类:向内存缓冲区的字符串StringBuffer写数据。
      • OutputStreamReader类:将字节输出流转换为字符输出流,可指定字符编码。

(1) 字符文件输入流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.FileReader;
import java.io.IOException;

public class Test178 {
/*
* Java提供了用来读取字符文件的便捷类FileReader
* FileReader(File file)、FileReader(String fileName)
*/
public static void main(String[] args) {
FileReader fr = null;
try {
// 创建FileReader对象,IDEA中src/.../Test177.java
fr = new FileReader("./Test177.java");
int i = 0;
System.out.println("文件内容如下:");
while ((i = fr.read()) != -1) {
// 将读取的内容强制转换为char类型
System.out.print((char) i);
}
} catch (Exception e) {
System.out.print(e);
} finally {
try {
// 关闭对象
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

(2) 字符文件输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.Scanner;
import java.io.FileWriter;
import java.io.IOException;

public class Test179 {
/*
* Java提供了写入字符文件的便捷类FileWriter
* FileWriter(String fileName)、FileWriter(File file, boolean append)
* FileWriter(File file)、FileWriter(String fileName, boolean append)
*/
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
FileWriter fw = null;
try {
// 创建FileWriter对象,IDEA中src/.../files/Test179.txt
fw = new FileWriter("./files/Test179.txt");
for (int i = 0; i < 4; i++) {
System.out.print("请输入第" + (i + 1) + "个字符串:");
// 读取输入的名称
String name = input.next();
// 循环写入文件
fw.write(name + "\r\n");
}
System.out.println("录入完成啦!");
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
// 关闭对象
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

(3) 字符缓冲区输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.FileReader;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.FileNotFoundException;

public class Test180 {
/*
* BufferedReader类主要用于辅助其他字符输入流
* 带有缓冲区,可以先将一批数据读取到内存缓冲区
* BufferedReader(Reader in)、BufferedReader(Reader in, int size)
*/
public static void main(String[] args) {
FileReader fr = null;
BufferedReader br = null;
try {
// 创建FileReader对象,IDEA中src/.../Test179.java
fr = new FileReader("./Test179.java");
// 创建BufferedReader对象
br = new BufferedReader(fr);
System.out.println("文件内容如下:");
String strLine = "";
while ((strLine = br.readLine()) != null) {
System.out.println(strLine);
}
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e2) {
e2.printStackTrace();
} finally {
try {
// 关闭对象
fr.close();
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

(4) 字符缓冲区输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.Scanner;
import java.io.FileWriter;
import java.io.IOException;
import java.io.BufferedWriter;

public class Test181 {
/*
* BufferedWriter类主要用于辅助其他字符输出流,同样带有缓冲区
* BufferedWriter(Writer out)、BufferedWriter(Writer out, int size)
*/
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
BufferedWriter bw = null;
try {
// 创建BufferedWriter对象,IDEA中src/.../files/Test181.txt
bw = new BufferedWriter(new FileWriter("./files/Test181.txt"));
for (int i = 0; i < 4; i++) {
System.out.print("请输入第" + (i + 1) + "个字符串:");
// 读取输入的名称
String name = input.next();
// 使用BufferedWriter写入字符串和换行符
bw.write(name);
bw.newLine();
}
// 写入完成后刷新缓冲区,确保数据写入文件
bw.flush();
System.out.println("录入完成啦!");
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
if (bw != null) {
// 关闭时自动flush
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

5-6 转换流使用

  • 转换流使用
    • 正常情况下字节流可以对所有的数据进行操作。
    • 但是在处理一些文本时用到字符流会更加方便。
    • Java IO流提供了两种用于将字节流转换为字符流的转换流。
    • InputStreamReader用于将字节输入流转换为字符输入流。
    • OutputStreamWriter用于将字节输出流转换为字符输出流。

(1) 输出结果转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.io.IOException;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.FileNotFoundException;

public class Test182 {
public static void main(String[] args) {
// Test181.txt文件内容为中文内容,将其保存为UTF-8格式
// 通过字节流方式读取,输出结果时中文将乱码显示
/*
try {
// IDEA中src/.../files/Test181.txt
FileInputStream fis = new FileInputStream("./files/Test181.txt");
int b = 0;
while ((b = fis.read()) != -1) {
System.out.print((char) b);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
*/

// 通过字符串设定编码格式来显示内容
/*
try {
// IDEA中src/.../files/Test181.txt
FileInputStream fis = new FileInputStream("./files/Test181.txt");
byte b[] = new byte[1024];
int len = 0;
while ((len = fis.read(b)) != -1) {
System.out.print(new String(b, 0, len, "UTF-8"));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
*/

// 当存储的文字内容较多时,容易出现解码不正确的问题
// 并且字节长度无法根据解码内容自动设定,此时就需要转换流来完成
try {
// IDEA中src/.../files/Test181.txt
FileInputStream fis = new FileInputStream("./files/Test181.txt");
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
int b = 0;
while ((b = isr.read()) != -1) {
System.out.print((char) b);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

(2) 获取键盘输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Test183 {
public static void main(String[] args) {
try {
// 将System.in对象转换成Reader对象
InputStreamReader reader = new InputStreamReader(System.in);
// 将普通的Reader包装成BufferedReader
BufferedReader br = new BufferedReader(reader);
String line = null;

// 利用循环逐行读取
while (true) {
System.out.print("请输入内容:");
line = br.readLine();

// 遇到输入流结尾时退出循环
if (line == null) {
break;
}
if (line.equals("exit")) {
System.exit(0);
}
// 打印读取的内容
System.out.println("输入内容为:" + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

5-7 图书的存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import java.util.List;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.io.BufferedReader;

// 创建Book类,包含no、name、price三个属性
class Book {
private int no;
private String name;
private double price;

public Book(int no, String name, double price) {
this.no = no;
this.name = name;
this.price = price;
}

public String toString() {
return "图书编号:" + this.no + ",图书名称:" + this.name + ",图书单价:" + this.price + "\n";
}

// 包含write()、read()两个方法
public static void write(List books) {
FileWriter fw = null;
try {
// 创建FileWriter对象,IDEA中src/.../files/books.txt
fw = new FileWriter("./files/books.txt");
for (int i = 0; i < books.size(); i++) {
fw.write(books.get(i).toString());
}
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public static void read() {
FileReader fr = null;
BufferedReader br = null;
try {
// 创建BufferedReader对象,IDEA中src/.../files/books.txt
fr = new FileReader("./files/books.txt");
br = new BufferedReader(fr);
String str = "";
while ((str = br.readLine()) != null) {
System.out.println(str);
}
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
br.close();
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

public class Test184 {
public static void main(String[] args) {
Book b1 = new Book(1001, "康熙的红票", 25.9);
Book b2 = new Book(1002, "挪威的森林", 32.5);
List books = new ArrayList();
books.add(b1);
books.add(b2);
Book.write(books);
System.out.print("----------------------");
System.out.print("图书信息");
System.out.print("----------------------\n");
Book.read();
}
}

Java 基础(四)
https://stitch-top.github.io/2025/08/21/java/java04-java-ji-chu-si/
作者
Dr.626
发布于
2025年8月21日 23:17:30
许可协议