0%

Java基础知识

一. Java 开发环境

  1. JVM(Java Virtual Machine)java 虚拟机

    主要负责将Java程序经过编译之后生成的和平台无关的字节码文件解释成具体的平台能够识别的机器指令。

  2. JRE(Java Runtime Environment)Java 运行环境

    JRE = JVM + Java程序执行所需要的核心类库。

  3. JDK(Java Development Kit)Java 开发工具包
    JDK = JRE + 开发工具(java、javac…)

JVM 是整个 java 实现跨平台的最核心的部分,但是只有 JVM 并不能运行程序;

如果想要运行一个 Java 程序,可以只安装 JRE,不安装 JDK;

如果想要开发一个 Java 程序 ,必须安装 JDK;

二. Java 语法规范

1. 基本概念

  • :类是一个模板,它描述一类对象的行为和状态。
  • 对象:对象是类的一个实例,有状态和行为。
  • 方法:方法就是行为,一个类可以有很多方法。逻辑运算、数据修改以及所有动作都是在方法中完成的。
  • 实例变量:每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。

2. Java 关键字

关键字 :是指在程序中,Java 已经定义好的单词,具有特殊含义。如 public、class、int 等等(关键字中所有字母都为小写)。

3. Java 标识符

标识符:程序中的我们给类、变量以及方法所起的名字。

Java中的类、变量、方法的名称不是随便都可以起的,需要满足一些规则

​ ① 标识符可以包含名称只能由字母、数字、下划线、$ 符号 。

​ ② 所有的标识符都应该名称只能由字母、下划线、$ 符号开头,不能以数字开头。

​ ③ 标识符不能是关键字。

为了使得程序的可读性更好,一般编写Java程序时,也要满足一定的规范

​ ① 类名建议采用大驼峰式(首字母大写,后面每个单词首字母大写)。

​ ② 方法名和变量名建议采用小驼峰式(首字母小写,后面每个单词首字母大写)。

​ ③ 常量名建议全部大写,单词之间用下划线分割。

​ ④ 包名、模块名、项目名建议所有字母都小写。

4. Java 编程的注意事项

  • Java 是大小写敏感的。
  • 源文件名必须和类名相同。(如果文件名和类名不相同则会导致编译错误)。
  • 所有的 Java 程序由 public static void main(String[] args) 方法开始执行。

三. Java 基础语法

1. 注释

单行注释: 以 // 开头 换行结束
多行注释: 以 /* 表示注释开始 , 以*/表示注释结束

1
2
3
4
5
6
// 这是一个单行注释
/* 这是
一个
多行
注释
*/

2. 数据类型

Java 数据类型分为两大类。

① 基本类型(4 类 8 种)

  • 整数型(byte、short、int、long)
  • 浮点型(float double)
  • 字符型(char)
  • 布尔型(boolean)
数据类型 关键字 内存占用/字节 取值范围 最小值符号 最大值符号
字节型 byte 1 -128 ~ +127 Byte.MIN_VALUE Byte.MAX_VALUE
短整型 short 2 -32768 ~ +32767 Short.MIN_VALUE Short.MAX_VALUE
整型 int 4 -2^31 ~ 2^31-1(超过20亿) Integer.MIN_VALUE Integer.MAX_VALUE
长整型 long 8 -2^63 ~ 2^63-1 Long.MIN_VALUE Long.MAX_VALUE
单精度浮点型 float 4 1.4E-45 ~ 3.4028235E38(有效位数6~7位) Float.MIN_VALUE Float.MAX_VALUE
双精度浮点型 double 8 4.9E-324~1.7977E+308(有效位数15位) Double.MIN_VALUE Double.MAX_VALUE
字符型 char 2 0 ~ 65535 Character.MIN_VALUE Character.MAX_VALUE
布尔型 Boolean 1 true、false * *

Java 没有任何无符号 (unsigned) 形式的 int、long、short 或 byte 类型,但可以通过其包装类转换得到。

基本数据类型必须进行初始化,没有默认值。

基本类型之间可以进行转换,分为自动类型转换和强制类型转换。

  • 自动类型转换(将容量小的数据类型转换为容量大的数据类型)

    注意:

    • 不能对 boolean 类型进行类型转换。
    • byte,short,char 之间不会相互转换,他们三者在计算时首先转换为int类型。
    • 当把任何基本数据类型的值和字符串 (String) 进行连接运算时 (+),基本数据类型的值将自动转化为字符串(String) 类型。
  • 强制类型转换(将容量大的数据类型转换为容量小的数据类型)

    格式:级别低的数据类型 变量名 = (级别低的数据类型) 级别高的数据类型;

    1
    如:int num = (int)10.5// 结果为 10,直接砍掉小数部分。

    注意:强制类型转换可能造成精度降低或溢出

② 引用类型

  • 类(class)
  • 接口(interface)
  • 数组(array)

3. 常量

常量是在程序运行时不能被修改的量。通常使用大写字母表示常量。

在 Java 中,利用关键字 final 指示常量,比如 final int a = 10;

  • 整数常量

    十进制: 正常的写法,如 5,-125 等。

    二进制: 在数字前面加前缀 0B 或 0b,如 0B11,0B110等。

    八进制: 在数字前面加前缀 0,如 05,01等。

    十六进制: 在数字前面加前缀 0x 或 0X,如 0XA,0x3等。

  • 浮点数常量:如 0.1,10.8等等。

  • 字符常量: 如 ‘A’,’C’ 等。

  • 字符串常量: 如 “zhang”,”A” 等。

  • 布尔常量: 只有 true 和 false。

  • 空常量: null。

4. 变量

变量是在程序运行时能被修改的量。通常变量名采用小驼峰式(首字母小写,后面每个单词首字母大写)。

变量的声明:

1
2
//规则:数据类型 变量名;
int a;

变量的初始化一般有两种方法:

1
2
3
4
5
6
7
8
9
/*  第一种变量赋值方法,在声明的时候直接赋值。
规则:数据类型 变量名 = 要赋的值; */
int a = 5

/* 第二种变量赋值方法,先声明,后赋值。
规则:数据类型 变量名;
变量名 = 要赋的值; */
int a;
a = 10;

注意:

  • 整数默认为 int 类型,如果需要的数据类型是 long,那么需要在数据后加后缀 L 或 l,如 long a = 10L;
  • 浮点数默认为 double 类型,如果需要的数据类型是 float,那么需要在数据后加后缀 F 或 f,如 float a = 0.5F;
  • 变量未赋值时不可使用;
  • 不可重复定义变量。
  • 从Java 10 开始,对于局部变量,如果可以从变量的初始值推断出它的类型,就不再需要声明类型。只需要使用关键字 var 而无需指定类型,比如:var vacationDays =12; 整型

5. 运算符

① 运算符的分类

  • 算数运算符(+、-、*、/、%、++、–)

    1
    /*注意:整数除法的结果仍然会是整数,规则是直接砍掉小数取整,而不是四舍五入。*/
  • 关系运算符(>、<、<=、>=、==、!=)

    1
    /*注意:关系运算符用于比较两个数据的大小,结果会产生一个布尔(boolean)值。如果关系为真,则结果为 true,如果关系为假,则结果为false。*/
  • 逻辑运算符(&&、||、!)

    1
    2
    3
    4
    5
    6
    /* 注意:逻辑运算符的运算结果只能是一个布尔值(true/false); && 和 || 具有短路现象,
    当&&时,如果第一个表达式已经为false,不管第二个表达式为true/false,结果一定为false ,此时第二个表达式将不会再执行。
    当||时,如果第一个表达式已经为true,不管第二个表达式为true/false,结果一定为true,此时第二个表达式将不会再执行。*/
    int a = 5;
    boolean flag = (3>5) && (++a == 6 )
    System.out.println(a);//由于短路现象,++a表达式并没有被执行,因此输出结果为5
  • 条件运算符(也称三目运算符)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /*  条件运算符语法规则: 数据类型 变量名 = 布尔表达式?结果1:结果2;
    条件运算符含义:如果布尔表达式为true,则执行变量名=结果1,否则执行变量名=结果2。 */
    int a=2,b=4;
    int num = a < b ? a : b;//因为a<b为真,故将执行结果1,即num=a,则可得结果为num=2
    /*条件运算符相比于if else语句更加简洁,且执行效率更高,但运算完必须要有一个结果。而if else运算完不需要一个结果。比如三元运算符内不能单独列出输出表达式。凡是可以使用三元运算符的地方,都可以改写为if-else;反之,不成立。*/
    if (num > 0) {
    System.out.println("num是正数");
    }else{
    System.out.println("num是负数");
    }//不能使用条件运算符代替
  • 赋值运算符(=、+=、-=、*=、/=、%=)

    1
    /* a + = b; 等价于  a = a + b ;  */
  • 位运算符(&、|、~、^、<<、>>)

② 运算符的优先级(算、关、逻、条、赋)

6. 流程控制

(1)顺序结构

1
2
3
4
5
// 顺序执行,即根据编写的顺序,从上到下运行,没有跳转。
public static void main(String[] args){
    System.out.println("Hello");
    System.out.println("World");
}

(2)选择结构

  • if 选择结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 第一种:if,如果布尔表达式为真,则执行语句体,否则不执行。
    if(布尔表达式){
    语句体;
    }
    // 第二种:if-else,如果布尔表达式为真,则执行语句体1,否则执行语句体2.
    if(布尔表达式){
    语句体1;
    }else{
    语句体2;
    }
    // 第三种:if—else if—...-else
    if(布尔表达式){
    语句体1;
    }else if {
    语句体2;
    }
    ...
    else{
    语句体n+1;
    }
  • switch 选择结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    switch(表达式){
    case 常量1:
    语句体1;
    break;
    ...
    case 常量n:
    语句体n;
    break;
    default:
    语句n+1;
    break;
    }

    case 标签可以为:

    • 类型为 char、byte、short、或 int 的常量表达式
    • 枚举常量
    • 从Java 7开始,还可以是字符串字面量。

(3)循环结构

  • for 循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /*  
    for(初始化表达式①;布尔表达式②;递进表达式③){
    循环体④;
    }
    注意:
    执行顺序:①②④③-②④③-②④③-...
    当布尔表达式为假时,循环结束,执行后续语句。
    */
    // 示例:计算1+2+..+100的值
    public static void main(String[] args) {
    int sum = 0;
    for (int i = 1; i < 101; i++) {
    sum+=i;
    }
    System.out.println("1+2+..+100的结果为:"+sum);// 1+2+..+100的结果为为:5050
    }
  • while 循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /*
    while(布尔表达式){
    循环体;
    }
    注意:当布尔表达式为假时,循环结束,执行后续语句。
    */
    // 示例:计算1+2+..+100的值
    public static void main(String[] args) {
    int sum = 0;
    int i = 1;
    while (i <= 100) {
    sum += i;
    i++;
    }
    System.out.println("1+2+..+100的结果为:" + sum);// 1+2+..+100的结果为为:5050
    }
  • do while 循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /*
    do{
    循环体;
    }while(布尔表达式);
    注意:
    do while中的循环体至少执行一次;
    当布尔表达式为假时,循环结束,执行后续语句。
    */
    // 示例:计算1+2+..+100的值
    public static void main(String[] args) {
    int sum = 0;
    int i = 1;
    do {
    sum+=i;
    i++;
    }while(i<=100);
    System.out.println("1+2+..+100的结果为:" + sum);//1+2+..+100的结果为为:5050
    }
  • break continue

    • break 和 continue 均为循环控制语句;
    • break 用于终止当前整个循环;
    • continue 用于跳出本次循环,转而去判断是否需要执行下次循环。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 示例:计算2+4+..+100的值
    public static void main(String[] args) {
    int sum = 0;
    for (int i = 0; i < 101 ; i++) {
    if( i % 2!=0 ){
    continue;
    }
    sum+=i;
    }
    System.out.println("2+4+..+100的结果为:" + sum);//2+4+..+100的结果为:2550
    }

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
/*
★一维数组:
1.动态初始化(指定长度):
格式:
数组存储的数据类型[] 数组名 = new 数组存储的数据类型[长度];
格式说明:
数组存储的数据类型:创建的数组中要存储的数据类型,可以是基本数据类型,也可以是引用数据类型。
[] : 表示定义的是数组。
数组名字:为定义的数组起个变量名,满足标识符规范,可以使用名字来操作数组,数组的命名一般使用小驼峰式。
new :关键字,创建数组使用的关键字。
[长度]:数组的长度,表示数组中可以存储多少个元素。
定义示例:
int[] array = new int[10];
2.静态初始化(指定内容):
格式:
数组存储的数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3...};//静态定义的标准格式
数组存储的数据类型[] 数组名 = {元素1,元素2,元素3...}; //静态定义的省略格式
格式说明:
{}中是数组中想要存储的元素,在定义数组的同时,对数组进行了初始化。
定义示例:
int[] array = new int[]{1,2,3,4,5};

★二维数组:(二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组)
1.动态初始化
格式:
数组存储的数据类型[][] 数组名 = new 数组存储的数据类型[行数][【列数】];
定义示例:
String[][] array = new String[3][2];//二维数组中有3个一维数组,每一个一维数组中有2个元素
String[][] arr = new String[3][];//只定义行数,然后可以自行定义每一行的列数,不必都是规则矩阵形式
arr[0] = new String[3]; arr[1] = new String[1]; arr[2] = new String[2];
2.静态初始化
格式:
数组存储的数据类型[][] 数组名 = new 数据类型{{元素1,元素2...},{元素1,元素2...},{元素1,元素2...}.....};
数组存储的数据类型[][] 数组名 = {{元素1,元素2...},{元素1,元素2...},{元素1,元素2...}.....};
定义示例:
int[] arr4[] = new int[][]{{1,2,3},{4,5,9,10},{6,7,8}};
int[] arr5[] = {{1,2,3},{4,5},{6,7,8}};
*/

数组的使用:

数组元素的引用方式:数组名[数组元素下标];

  • 数组元素下标(也称为索引)从 0 开始;长度为 n 的数组合法下标取值范围: 0~n-1;
  • 数组的长度:通过使用【数组名 .length】 可以获取数组的长度;
  • 数组名是该数组所在内存的地址值;
  • new 出来的东西在内存的堆区;
  • 数组如果没有初始化,会有默认值(数字默认全为0,boolen默认为false)
1
2
3
4
5
6
7
8
// 示例:定义一个数组,存放1,2,3,4,5,并将数组元素遍历打印。
public static void main(String[] args) {
int [] array = new int[]{1,2,3,4,5};
System.out.println("数组中的元素为:");
for (int i = 0; i < array.length; i++) {
System.out.print(" "+array[i]);// 1 2 3 4 5
}
}

四. Java 面向对象

1. 类和对象

(1)基本概念

  • :类是一个模板,它描述一类对象的行为和状态。
  • 对象:对象是类的一个实例,有状态和行为。
  • 属性 :一个类的状态信息,对应类中的成员变量。
  • 行为 :一个类能够做什么,对应类中的成员方法

(2)类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
格式:
修饰符 class 类名{
成员变量;//即类的属性
成员方法;//即类的行为
}
步骤:
1. 定义类(考虑修饰符、类名)
2. 编写类的成员变量(考虑修饰符、变量类型、变量名、初始化值)
3. 编写类的成员方法(考虑修饰符、返回值类型、方法名、形参等)
*/
// 示例:创建学生类:
public class Student {
// 成员变量
String name;// 姓名  
int age;// 年龄
// 成员方法
public void introduce() {
System.out.println("我是:"+name+",年龄:"+age);
}
}

(3)对象的创建与使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
创建对象:
类名 对象名 = new 类名();
使用对象访问类中的成员:
对象名.成员变量;
对象名.成员方法();
匿名对象:
格式:
new 类名();(不定义对象的句柄,而直接调用这个对象的方法)
使用情况:
①如果对一个对象只需要进行一次方法调用,那么就可以使用匿名对象。
②我们经常将匿名对象作为实参传递给一个方法调用。
*/
// 示例:创建一个学生对象,并访问类中成员。
public static void main(String[] args) {
Student student = new Student();
student.name = "张三";
student.age = 18;
student.introduce();// 我是:张三,年龄:18
}
}

(4)成员变量

1
2
3
4
5
6
7
8
9
/*
格式:
修饰符 数据类型 成员变量名 = 初始化值;
格式说明:
常用的权限修饰符有:public、private、缺省、protected
其他修饰符:static、final
数据类型可以是任何基本数据类型或任何引用数据类型。
成员变量名的规则同一般变量名。
*/

★ 成员变量与局部变量的区别:

在类中的位置 作用范围 初始化值 修饰符 内存位置
成员变量 类中,方法外 类中 有默认值 public、private、static、final 等 堆区
局部变量 方法中或者方法声明上 (形式参数) 方法中 没有默认值。必须先定义,赋值,最后使用 不能用权限修饰符修饰,可以用 final 修饰 栈区

(5)成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
格式:
修饰符 返回值类型 方法名 ( 参数类型 形参1, 参数类型 形参2, … ){
方法体;
return 返回值;
}
格式说明:
 修饰符:告诉编译器如何调用该方法。定义了该方法的访问类型。有public, 缺省,private, protected等
 如果没有返回值,那么返回值类型写void,并且不需要return 返回值;
如果有返回值,返回值类型就是return 返回值;这一语句的返回值的类型,可以是任意数据类型。
 方法名的命名规则同变量名;
 形参列表:可以包含零个,一个或多个参数。多个参数时,中间用逗号隔开;
 return:将方法执行后的结果带给调用者,方法执行到return语句后,整体方法运行结束。
注意:
① 返回值类型,必须要和return语句返回的类型相同,否则编译失败 。
② 不能在return语句后面写代码, return意味着方法结束,所有后面的代码永远不会执行,属于无效代码。
③ 没有返回值时,方法体中可以不必使用return语句。如果使用,仅用来结束方法,格式为:return;
④ 方法中只能调用方法或属性,不可以在方法内部定义方法。
*/

方法的的重载:

  • 概念:在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。
  • 特点:与返回值类型无关,只看参数列表,且参数列表必须不同(个数不同 / 数据类型不同 / 顺序不同)。
  • 调用:通过方法的参数列表,调用不同的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 示例:创建一个sum方法,可以计算两个整数或三个整数的和
public class Overloading {
public static void main(String[] args) {
System.out.println(sum(1, 2));//3
System.out.println(sum(1, 2, 3));//6
}
// 计算两个整数的和
public static int sum(int a, int b) {
return a + b;
}
// 方法重载,计算三个整数的和
public static int sum(int a, int b, int c) {
return a + b + c;
}
}

形参个数可变的方法:

  • 声明格式:方法名(参数的类型名 …参数名)
  • 可变参数方法的使用与方法参数部分使用数组是一致的;
  • 可变参数:方法参数部分指定类型的参数个数是可变多个:0个,1个或多个;
  • 可变个数形参的方法与同名的方法之间,彼此构成重载;
  • 方法的参数部分有可变形参,需要放在形参声明的最后;
  • 在一个方法的形参位置,最多只能声明一个可变个数形参。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 示例:定义形参可变的方法,并与定义数组形式的新参方法做对比。
public class Test {
// 定义了形参个数可变的方法
public static void test1(String... str) {
for (int i = 0; i < str.length; i++) {
System.out.print(str[i]+" ");
}
}
// 定义了数组形式的形参方法
public static void test2(String[] str) {
for (int i = 0; i < str.length; i++) {
System.out.print(str[i]+" ");
}
}
public static void main(String[] args) {
test1("张三丰", "张无忌", "张翠山");// 张三丰 张无忌 张翠山
test2(new String[]{"张三丰", "张无忌", "张翠山"});// 使用了匿名对象,结果为:张三丰 张无忌 张翠山
}
}

方法的参数传递机制

  • Java 里方法的参数传递方式只有一种:值传递。(即将实际参数值的副本传入方法内,而参数本身不受影响。)
  • 如果形参是基本数据类型,则将实参基本数据类型变量的“数据值”传递给形参;
  • 如果形参是引用数据类型,则将实参引用数据类型变量的“地址值”传递给形参;
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
int num = 5;
System.out.println("值传递前的值为:"+num);//5
change(num);
System.out.println("值传递后的值为:"+num);//5
}
// 定义一个方法,将传入的参数值修改为0
public static void change(int num){
num = 0;
System.out.println("改变值的方法被调用了!");
}
}

(6)static 关键字

static 关键字可以用来修饰:属性、方法、内部类、代码块;被修饰的成员是从属于类的,而不是单单是属于某个对象的。既然属于类,就可以不靠创建对象来调用了,而直接通过类进行调用。

  • 静态变量

    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
    /*
    概念:
    当static修饰成员变量时,该变量称为静态变量,也称为类变量。
    格式:
    static 数据类型 变量名;
    注意:
    ① 如果一个成员变量使用了static关键字,那么这个变量不再属于对象自己,而是属于所在的类。多个对象共享同一份数据。
    ② 任何对象都可以更改静态变量的值,但也可以在不创建该类的对象的情况下,通过【类.属性】的方式对静态变量进行操作。
    ③ 当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的。
    ④ 静态变量随着类的加载而加载,它的加载要早于对象的创建。由于类只会加载一次,则静态变量在内存中也只会存在一份,而且是存在方法区的静态域中。
    */
    public class Test {
    static int num = 10;
    }

    public class Main {
    public static void main(String[] args) {
    Test test1 = new Test();
    Test test2 = new Test();
    Test.num = 20;
    test1.num = 30;
    test2.num = 40;
    System.out.println(Test.num); //40
    System.out.println(test1.num); //40
    System.out.println(test2.num); //40
    }
    }

    ★ 静态变量与普通成员变量的区别

    所属 存储区域 生命周期 调用方式
    静态变量 方法区 与类的生命周期相同 类.属性 / 对象.属性
    普通成员变量 某个对象 堆区 与其所属对象的生命周期相同 所属对象.属性
  • 静态方法

    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
    /*
    概念:
    当static修饰成员方法时,该变量称为静态方法,也称为类方法。
    格式:
    修饰符 static 返回值类型 方法名 (参数列表){
    方法体;     
    }
    注意:
    ① 在静态的方法内,不能使用this关键字、super关键字。
    ② 静态方法随着类的加载而加载,可以通过【类名.静态方法】进行调用,也可以通过【对象名.静态方法】调用。
    ③ 静态方法中,只能调用静态的方法或变量。
    ④ 非静态方法中,既可以调用非静态的方法或变量,也可以调用静态的方法或变量。
    */
    public class Test {
    public static void staticMethod() {
    System.out.println("这是一个静态方法");
    }
    }

    public class Main {
    public static void main(String[] args) {
    Test test = new Test();
    Test.staticMethod(); // 这是一个静态方法
    test.staticMethod(); // 这是一个静态方法
    }
    }

2. 封装

(1)高内聚低耦合

好的程序设计应该具有的特性:

  • 高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;
  • 低耦合 :仅对外暴露少量的方法用于使用。

(2)封装的好处

  • 良好的封装能够减少耦合。
  • 隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性以及安全性。
  • 可以对成员变量进行更精确的控制。(使用者对类内部定义的属性直接操作可能会导致数据的错误、混乱或安全性问题。)

(3)访问权限修饰符

Java 权限修饰符 public、protected、default(缺省)、private 置于类的成员定义前,用来限定对象对该类成员的访问权限。

  • public : 对所有类可见。使用对象:类、接口、变量、方法
  • default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
  • private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
  • protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

以下是访问控制级别:

public > protected > (default) > private

修饰符 同一个类 同一个包 不同包的子类 同一个工程
public
protected
default(缺省)
private

注意:对于 class 的权限修饰只可以用 public 和 default(缺省)。

(4)实现封装的步骤

  • 使用 private 关键字来修饰成员变量(成员变量私有化)。
  • 对需要访问的成员变量,提供对应的一对公共的(public)setter 方法 、getter 方法实现对该成员变量的操作(提供对外接口)。
    • setter、getter 方法的名称应该为 private 修饰的成员变量的首字母大写,并在前面添加 get、set,比如一个private的成员变量名为 age,那么其 setter、getter 方法的名称为 setAge、getAge。
    • 对于 setter 来说,不能有返回值,参数类型和成员变量对应(用于给变量赋值)
    • 对于 getter 来说,不能有参数,返回值类型和成员变量对应(用于获取变量值)
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 Student {
String name;// 姓名  
private int age;// 将年龄age定义为private,只能被Student类内部访问

// 定义setAge方法,并定义为public
public void setAge(int i) {
if(i < 0 || i > 200){
age = -1;
System.out.println("年龄大小输入不符合实际!请重新设置一次");
}else{
age = i;
}
}

// 定义getAge方法
public int getAge() {
return age;
}
}
// 示例:定义一个Test类,创建Student对象,并给对象赋值为张三,18岁
public class Test {
public static void main(String[] args) {
Student student = new Student();
student.name = "张三";
// student age = 18; 此语句是错误的,因为Student类中的age修饰符为private,只能在Student类中使用。
student.setAge(18);// 通过setAge()方法对年龄赋值
System.out.println("姓名:"+ student.name );// 姓名:张三
System.out.println("年龄:" + student.getAge());// 年龄:18
}
}

(5)构造方法

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
/*
作用:
1.创建对象(当一个对象被创建时候,构造方法用来初始化该对象,给对象的成员变量赋初始值。当我们通过关键字new来创建对象时,其实就是在调用构造方法)
2.初始化对象的信息。
格式:
public 类名称(参数类型 参数名称) {
方法体;
}
注意:
①构造方法的名称必须和所在的类名称完全一样,就连大小写也要一样
②构造方法不要写返回值类型,连void都不写
③不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值
④如果没有编写任何构造方法,那么编译器将会默认提供一个构造方法,没有参数、方法体什么事情都不做。public Student() {}
也就是在Java语言中,每个类都至少有一个构造方法
⑤一旦编写了至少一个构造方法,那么编译器将不再提供空的构造方法。
⑥构造方法可以进行重载。
*/
//示例:定义一个学生类,并构造一个无参构造函数和全参构造函数
public class Student {

String name;// 姓名  
private int age;// 年龄

// 定义一个无参的构造方法
public Student() {
name = "张三";
age = 18;
}

// 定义一个全参的构造方法
public Student(String str , int i){
name = str;
age = i;
}

// 定义setter方法
public void setAge(int i) {
age = i;
}

// 定义getter方法
public int getAge() {
return age;
}
}

(6)this关键字

  • 当方法的局部变量和类的成员变量重名的时候,根据“就近原则”,会优先使用局部变量。如果需要访问本类当中的成员变量,需要使用格式:this.成员变量名
  • this 关键字指向的是当前对象的引用(this 理解为:当前对象或当前正在创建的对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用this关键字区分局部变量和成员变量
public class Student {

String name;// 姓名  
int age;// 年龄
// 定义一个全参的构造方法
public Student(String name, int age){
// name = name;错误,此时根据就近原则,两个name都是局部变量,并不能给成员变量初始化
// age = age;
this.name = name;// this.name指成员变量,name指局部变量
this.age = age;
}
}

(7)标准代码——JavaBean

JavaBean 是一种可重用的Java组件,它可以被 Applet、Servlet、JSP 等 Java 应用程序调用,也可以可视化地被Java开发工具使用。

一个标准的类(也叫做JavaBean)通常要拥有下面四个组成部分:

  • 所有的成员变量都要使用 private 关键字修饰;
  • 为每一个成员变量编写一对儿 getter/setter 方法;
  • 编写一个无参数的构造方法;
  • 编写一个全参数的构造方法。
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
// 定义一个标准的学生类
public class Student {
// 定义成员变量,都用private修饰
private String name; // 姓名
private int age; // 年龄

// 定义无参数的构造函数
public Student() {
}

//定义全参数的构造函数
public Student(String name, int age) {
this.name = name;
this.age = age;
}

//定义gettter方法
public String getName() {
return name;
}

// 定义setter方法
public void setName(String name) {
this.name = name;
}

// 定义gettter方法
public int getAge() {
return age;
}

// 定义setter方法
public void setAge(int age) {
this.age = age;
}
}

(8)代码块

在 Java 中,使用{}大括号括起来的代码被称为代码块。

  • 普通代码块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 最常见的一种代码块,有方法名称,即类中方法的方法体,可以控制变量的生命周期,提高内存利用率。
    public class Test {
    public static void Method() { // 普通代码块
    System.out.println("这是一个普通代码块");
    }
    }

    public class Main {
    public static void main(String[] args) {
    Test test = new Test();
    System.out.println(test.Method);// 这是一个普通代码块
    }
    }

  • 构造代码块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /*
    定义:
    在类中直接定义没有任何修饰符、前缀、后缀的代码块即为构造代码块,其可以给所有对象进行初始化。
    注意:
    >构造代码块内部可以输出语句;
    >每创建一个对象,就执行一次构造代码块,再执行构造方法;
    >如果一个类中定义了多个构造代码块,则按照声明的先后顺序执行,(一般也不会构造多个);
    >构造代码块内可以调用静态变量、静态方法、普通成员变量、普通成员方法。
    */
    public class Test {
    int num;
    { //构造代码块
    num = 10;
    System.out.println("这是一个构造代码块");
    }
    }

    public class Main {
    public static void main(String[] args) {
    Test test = new Test();
    System.out.println(test.num);//这是一个构造代码块\n10
    }
    }
  • 静态代码块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /*
    定义:
    在类中直接定义的只有static关键字修饰的代码块即为静态代码块,其可以对静态属性、类进行初始化,并且只执行一次。
    注意:
    >静态代码块内部可以输出语句;
    >静态代码块随着类的加载而执行,而且只执行一次,优先于main方法和构造方法的执行;
    >如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行;
    >静态代码块的执行要优先于非静态代码块的执行;
    >静态代码块内只能调用静态变量和静态方法。
    */
    public class Test {
    static int num;

    static {
    num = 10;
    System.out.println("这是一个静态代码块");
    }
    }

    public class Main {
    public static void main(String[] args) {
    System.out.println(Test.num);//这是一个静态代码块\n10
    }
    }

3. 继承

(1)继承的概念

子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。

(2)继承的优缺点

优点:代码共享,减少重复代码。

  • 减少了代码冗余,提高了代码的复用性。
  • 提高了代码的维护性,更有利于功能的扩展。
  • 让类与类之间产生了关系,是多态的前提

缺点:提高了类的耦合性,使得代码之间的联系更加紧密,那么代码独立性更差。

(3)继承的格式

1
2
3
4
5
6
7
8
// 使用extends关键字。
public class 父类名称{
代码体;
}

public class 子类名称 extends 父类名称{
代码体;
}

(4)继承的特点

  • 子类拥有父类非 private 的属性、方法。

  • 子类可以拥有自己特有的属性和方法,即子类可以对父类进行扩展。

  • 子类可以对父类的方法进行重写,即子类可以用自己的方式实现父类的方法。

  • 顶层父类是Object类。所有的类默认继承Object作为父类。

  • java支持单继承、多级继承、不同类继承同一个类,不支持多继承

(5)重写

重写(也称为覆盖):子类对父类方法的实现过程进行重新编写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class 父类名称{
// 父类被重写的方法A
修饰符 返回值类型 方法名称(参数列表){
方法体;
}
...
}

public class 子类名称 extends 父类名称{
// 子类重写父类的方法A
修饰符 返回值类型 和父类相同方法名称(和父类相同的参数列表){
新的方法体;
}
...
}

重写的好处:子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

重写的要求

  • 子类重写的方法必须和父类被重写的方法具有相同的方法名称和参数列表。

  • 父类被重写的方法的返回值类型是 void,则子类重写的方法的返回值类型只能是 void

    父类被重写的方法的返回值类型是 A 类型,则子类重写的方法的返回值类型可以是 A 类或 A 类的子类

    父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须是相同的基本数据类型(一模一样)。

  • 子类方法的权限必须【大于等于】父类方法的权限修饰符。public > protected > (default) > private

    子类不能重写父类中声明为 private 权限的方法。

  • 子类方法抛出的异常不能大于父类被重写方法的异常。

  • 在子类的重写方法前,可以加一个注解 @override,用来检测是不是有效的正确覆盖重写。

重写的特点:创建的是子类对象,则优先用子类方法。

重写与重载的区别★★★:

重载:同一个类中不同方法具有相同的名字,但是参数不一样,即参数的名称和参数的类型不一样。同类不同参。

重写:子父类的,即子类与父类具有相同的方法名字还有参数参数相同和相同的返回类型。即同名同参同类型。

参数列表 是否有继承关系
重载(overload) 必须不同 无继承关系,在同一个类中
重写(override) 必须相同 有继承关系,在不同类中

(6)super 关键字

如果子类想要调用父类中的内容,可以使用super关键字,主要有下面三种用法:

​ ① super 可用于访问父类中定义的成员变量【因为成员变量时私有的,所有子类不能直接调用父类的,需要用到super关键字】

​ ② super 可用于调用父类中定义的成员方法

​ ③ super 可用于在子类构造器中调用父类的构造器

注意:

  • 尤其当子父类出现同名成员时,可以用 super 表明调用的是父类中的成员
  • super 的追溯不仅限于直接父类
  • super 和 this 的用法相像,this 代表本类对象的引用,super 代表父类的内存空间的标识

super 关键字的使用

访问成员变量

this.成员变量 ->本类的

super.成员变量 ->父类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class A{
int num = 20
}

public class B extends A{
int num = 20;
public void methodB(){
int num = 30;
System.out.println(num);// 没有this和super,局部变量,30
System.out.println(this.num);// 有this,本类中的成员变量,20
System.out.println(super.num);// 有super,父类中的成员变量,10
}
}

public class Main {
public static void main(String[] args) {
B b = new B();
b.methodB();
}
}

访问成员方法

this.成员方法名() ->本类的

super.成员方法名() ->父类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class A {
public void method(){
System.out.println("父类的成员方法");
}
}

public class B extends A{
int num = 20;
public void method(){
System.out.println("子类的成员方法");
}
public void show(){
this.method();// 有this,调用子类中方法
super.method();// 有super,调用父类中的方法
}
}

public class Main {
public static void main(String[] args) {
B b = new B();
b.show();
}
}

访问构造方法

this.(形参列表) ->本类的

super(形参列表) ->父类的

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
/*
1. 子类构造方法当中有一个默认隐含的“super()”调用,调用父类的空参构造。所以一定是先调用的父类构造,后执行的子类构造。
2. 子类构造可以通过super关键字来调用父类重载构造,手动调用父类构造会覆盖默认的super(),
3. super的父类构造调用,必须是子类构造方法的第一个语句。不能一个子类构造调用多次super构造。
*/
public class A {
public A() {
System.out.println("父类的构造方法执行了");
}
}

public class B extends A{
public B() {
// 隐含有一个super();
System.out.println("子类的构造方法执行了");
}
}

public class Main {
public static void main(String[] args) {
B b = new B();
}
}
/*
在main方法中,new了一个子类B,那么会使用B的构造方法,而B继承了A,则会先调用父类A的构造方法,再调用子类B的构造方法
结果为:
父类的构造方法执行了
子类的构造方法执行了
*/

总结:★ this 和 super 的区别

关键字 访问成员变量 调用成员方法 调用构造方法
this 访问本类中的成员变量,如果本类没有,则从父类中继续查找 访问本类中的成员方法,如果本类没有,则从父类中继续查找 访问本类中的构造方法
super 直接访问父类中的成员变量 直接访问父类中的成员方法 访问父类中的构造方法

在主函数中,创建具有继承关系的对象时,

如果成员变量或者静态方法重名,看等号左边是谁,则优先用谁,没有则向上找。(编译看左边,运行看左边)

如果成员方法重名,看 new 的谁,则优先用谁,没有则向上找。(编译看左边,运行看右边)

4. 多态

(1)多态的概念

同一行为发生在不同的对象上会产生不同的结果。(同一行为,具有多个不同表现形式)

(2)多态的优缺点

优点:提高了代码的扩展性,前期定义的代码可以使用后期的内容,而且可以接口复用(子类共用一个父类接口)。

缺点:多态不能访问子类特有的功能( 前期定义的内容不能使用(调用)后期子类的特有内容。)

(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
/*
多态的格式:
父类类型 变量名 = new 子类对象;
变量名.方法名();
在多态的代码当中,
成员方法的访问规则是:看new的是谁,就优先用谁,没有则向上找。(编译看左边,运行看右边)
成员变量或者静态方法的访问规则是:看等号左边是谁,则优先用谁,没有则向上找。(编译看左边,运行看左边)
*/
// 创建一个父类Animal
public class Animal {
String name = "动物";
public void eat(){
System.out.println("动物在吃饭");
}
public static void sleep(){
System.out.println("动物在睡觉");
}
}

// 创建一个子类Cat
public class Cat extends Animal {
int num = 20;
static String name = "猫";
@Override
public void eat(){
System.out.println("猫吃鱼");
}
// 这个方法不是重写,因为有static关键字,如果加上 @override 会报错
// 如果子类有和父类相同的静态方法,那么父类的静态方法将会被隐藏,对于子类不可见
public static void sleep(){
System.out.println("猫在睡觉");
}

public void catchMouse(){
System.out.println("猫抓老鼠");
}
}

// 创建一个子类Dog
public class Dog extends Animal {
int num = 30;
static String name = "狗";
@Override
public void eat(){
System.out.println("狗吃骨头");
}
// 这个方法不是重写,因为有static关键字,如果加上@override会报错
public static void sleep(){
System.out.println("狗在睡觉");
}
}

// 创建主方法
public class Main {
public static void main(String[] args) {
Animal animal = new Cat();
System.out.println(animal.name);// 成员变量,编译看左,运行也看左。运行的是父类的成员变量,结果为:动物
animal.eat();// 成员方法,编译看左,运行看右。运行的是子类的成员方法,结果为:猫吃鱼
animal.sleep();// 静态方法,编译看左,运行也看左。运行的是父类的静态方法,结果为:动物在睡觉
// animal.catchMouse();错误!多态时,不能使用子类特有的属性和方法

Cat cat = new Cat();
Dog dog = new Dog();
function(cat);// 发生了多态
function(dog);// 发生了多态,实现了接口复用
}

public static void function(Animal animal){ // 接口复用,如果没有多态性,则需要重载
animal.eat();
animal.sleep();
}
}

(4)instanceof 关键字

instanceof 关键字用于来判断多态中父类引用的对象,原本是哪个子类(判断某个对象是否是某个 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
32
33
/*
格式:对象名 instanceof 数据类型
返回值为boolean。
如果对象属于该数据类型,返回true。
如果对象不属于该数据类型,返回false。
*/
public class Animal {
public void eat(){
System.out.println("动物在吃饭");
}
}

public class Cat extends Animal {
@Override
public void eat(){
System.out.println("猫吃鱼");
}
}

public class Dog extends Animal {
@Override
public void eat(){
System.out.println("狗吃骨头");
}
}

public class Main {
public static void main(String[] args) {
Animal animal = new Cat();
System.out.println(animal instanceof Cat);//由于定义的时候,animal原本是Cat类,则结果为true
System.out.println(animal instanceof Dog);//由于定义的时候,animal原本是Cat类,不是Dog类,则结果为false
}
}

(5)引用类型转换

  • 向上转型

    当父类引用指向一个子类对象时,便是向上转型。

    1
    2
    3
    4
    5
    /*	
    多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。(类似于基本类型中的自动类型转换)
    使用格式:
    父类类型  变量名 = new 子类类型();
    */
  • 向下转型

    一个已经向上转型的子类对象,将父类引用再转为子类引用,可以使用强制类型转换的格式,便是向下转型。

    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
    /*
    父类类型向子类类型向下转换的过程,这个过程是强制的。(类似于基本类型中的强制类型转换)
    使用格式:
    变量名 = (子类类型) 父类变量名;
    向下转型的目的:
    多态的一个缺点就是不能调用子类中特有的方法。而如果想要调用子类特有的方法,就需要向下转型。
    向下转型的注意事项:
    使用强转时,可能出现ClassCastException的异常,出现异常的原因是多态中父类引用的对象,原本不是此子类,却想要转型为此子类。为了避免在向下转型时出现ClassCastException的异常,在向下转型之前,先进行instanceof的判断,一旦返回true,就进行向下转型。如果返回false,不进行向下转型。
    */
    // 创建一个父类Animal
    public class Animal {
    public void eat() {
    System.out.println("动物在吃饭");
    }
    }

    // 创建一个子类Cat
    public class Cat extends Animal {

    @Override
    public void eat() {
    System.out.println("猫吃鱼");
    }

    // 猫特有的方法
    public void catchMouse() {
    System.out.println("猫抓老鼠");
    }
    }

    // 创建一个子类Dog
    public class Dog extends Animal {

    @Override
    public void eat() {
    System.out.println("狗吃骨头");
    }

    // 狗特有的方法
    public void keepHouse() {
    System.out.println("狗看家");
    }
    }

    // 创建主方法
    public class Main {
    public static void main(String[] args) {
    Animal animal = new Cat(); // 向上转型
    // Dog dog = (Dog)animal; 语句错误,本来new的是Cat,却要转换为Dog,会出现ClassCastException
    // Cat cat = (Cat)animal; //向下转型

    function(animal);
    }

    public static void function(Animal animal) { // 接口复用
    if (animal instanceof Cat) { // 如果animal对象原本属于Cat类,则进行向下转型,并调用Cat特有的方法
    Cat cat = (Cat) animal;
    cat.catchMouse();
    }
    if (animal instanceof Dog) { // 如果animal对象原本属于Dog类,则进行向下转型,并调用Dog特有的方法
    Dog dog = (Dog) animal;
    dog.keepHouse();
    }
    }
    }
    // 由于定义的animal原本属于Cat类,故最终结果为:猫抓老鼠

(6)final关键字

final 关键字代表最终、不可改变的,可以用来修饰:类、方法、变量。

  • final 修饰类

    1
    2
    3
    4
    5
    6
    7
    /*
    格式:
    final class 类名 {
    }
    注意:
    被final修饰的类,不能被继承。比如:String类、System类、StringBuffer类
    */
  • final 修饰方法

    1
    2
    3
    4
    5
    6
    7
    8
    /*
    格式:
    修饰符 final 返回值类型 方法名(参数列表){
         //方法体
    }
    注意:
    被final修饰的方法,不能被重写。比如:Object类中getClass();
    */
  • final 修饰变量

    ① 修饰局部变量——基本类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /*
    格式:
    方式一:
    final 数据类型 变量名;
    变量名 = 值;
    方式二:
    final 数据类型 变量名 = 值;
    注意:
    被final修饰的局部变量,只能被赋值一次。
    被final修饰的常量名称,一般都有书写规范,所有字母都大写。
    */
    public class Test {
        public static void main(String[] args) {
            final int NUM; //声明变量,使用final修饰        
            NUM = 10; //赋值
            //NUM = 20; // 报错,不可重新赋值
            final int NUM1 = 10;// 声明变量,直接赋值,使用final修饰
            //NUM1 = 20; // 报错,不可重新赋值
        }
    }

    ② 修饰局部变量——引用类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /*
    格式:
    final 引用数据类型 变量名 = new 引用数据类型(参数列表);
    注意:
    引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。
    */
    public class Test {
    public void method(){
    System.out.println("引用类型final关键字的测试");
    }
    }

    public class Main {
    public static void main(String[] args) {
    final Test test = new Test();
    //test =new Test();//报错,不能指向新的对象。
    test.method();//引用类型final关键字的测试
    }
    }

    ③ 修饰成员变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /*
    成员初始化方式有两种,只能二选一:
    ①显示初始化;
    public class Test {
         final int NUM = 10;
    }
    ②构造方法初始化。
    public class Test {
          final int NUM;
          public Test(int num) {
              this.NUM = num;
         }
    }
    */

5. 抽象

(1)抽象类

用 abstract 关键字来修饰一个类,这个类就叫做抽象类。

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类;

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

1
2
3
4
5
6
7
8
9
10
11
/*
格式:
修饰符 abstract class 类名 {
  代码体;
}
注意:
① 抽象类不能实例化,即不能创建对象;
② 抽象类一定要被继承,否则没有意义。且抽象类中一定有构造器,便于其子类实例化时调用。
*/
public abstract class animal{
}

(2)抽象方法

用 abstract 关键字来修饰一个方法,这个方法就叫做抽象方法。

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。

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
/*
格式:
修饰符 abstract 返回值类型 方法名 (参数列表);
注意:
① 抽象方法只有方法声明,没有方法体;
② 如果一个类中含有抽象方法,那么这个类必须定义为抽象类。反之不成立,抽象类中可以没有抽象方法的。
③ 继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。
④ 不能用abstract修饰变量、代码块、构造器;
⑤ 不能用abstract修饰私有方法、静态方法、final的方法、final的类。
*/
// 创建一个抽象的父类Animal
public abstract class Animal{
public abstract void eat(); //抽象方法
}

// 创建一个子类Dog
public class Dog extends Animal{
@Override //子类重写父类的抽象方法
public void eat(){
System.out.println("狗吃骨头");
}
}

public class Main{
public static void main(String[] args) {
//Animal animal = new Aniaml();错误,抽象类不能实例化
Dog dog = new Dog();
dog.eat();
}
}

6. 接口

Java 接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。

类的内部封装了成员变量、构造方法和成员方法,而接口的内部封装了常量和抽象方法(JDK 7及以前)、默认方法和静态方法(JDK 8)、私有方法(JDK 9)。

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
/*
格式:
public interface 接口名称 {
// 成员变量,使用 public static final 修饰,关键字可以省略,供子类调用或者子类重写。
     // 抽象方法,使用 public abstract 修饰,关键字可以省略,没有方法体。该方法供子类实现使用。
      // 默认方法,使用 public default 修饰,default不可省略,供子类调用或者子类重写。(JDK 8有的新特性)
    // 静态方法,使用 public static 修饰,static不可省略,供接口直接调用。(JDK 8有的新特性)
      // 私有方法,使用 private 修饰,供接口中的默认方法或者静态方法调用。(JDK 9有的新特性)
}

注意:
① 接口中没有构造器,意味着接口不可以实例化。
② 接口不能直接使用,必须有一个“实现类”来实现(implements)该接口。如果实现类覆盖了接口中的所抽象方法,则此实现类就可以实例化;如果实现类没覆盖接口中所有的抽象方法,则此实现类必须为一个抽象类。
③ 一个类可以实现多个接口(多实现),接口也可以继承其它接口(且可以多继承)。--->弥补了Java单继承性的局限性
④ 接口与实现类之间存在多态性。
⑤ 继承是一个"是不是"(is a)的关系,而接口实现则是 "能不能"(hava a)的关系。
⑥ 默认方法可以解决接口升级的问题,直接在接口中写默认方法,而实现类不需要重写,默认就有了此方法。
⑦ 优先级的问题:当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。
⑧ 接口中,有多个默认方法时,实现类都可继承使用。如果默认方法有重名的,必须重写一次。
⑨ 接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
接口的使用步骤(实现):
public class 实现类名称 implements 接口名称 {
// 重写接口中抽象方法【必须】
   // 重写接口中默认方法【可选】
}
*/
// 定义一个接口
public interface Fly {

public abstract void fly();

public default void haveWings() {
System.out.println("有翅膀可以飞");
}
}

// 定义一个实现类
public class Bird implements Fly {
@Override // 重写接口中的抽象方法
public void fly() {
System.out.println("小鸟正在飞");
}
}

public class Main {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly();// 小鸟正在飞
bird.haveWings(); // 有翅膀可以飞
}
}

抽象类与接口的对比

  • 相同点
    • 均不能被实例化。
    • 都可以包含抽象方法。
    • 接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
  • 不同点
    • 抽象类中有构造方法,而接口中没有。
    • 抽象类不能多继承,而接口可以。
    • 接口中的变量必须有 static、final 修饰,实际是一个常量,必须赋初值,而抽象类可以任意。

7. 内部类

如果将一个类定义在另一个类里面或者一个方法里面,这样的类就称为内部类。内部类又可以分为成员内部类、局部内部类、匿名内部类和静态内部类。

(1)成员内部类

成员内部类是最普通的内部类,它的定义为位于另一个类的内部。在描述事物时,若一个事物内部还包含其他事物,就可以使用内部类这种结构。比如,汽车类 Car 中包含发动机类 Engine ,这时,Engine 就可以使用成员内部类来描述。

内部类仍然是一个独立的类,在编译之后会内部类会被编译成独立的 .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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
格式:
class 外部类名 {
外部类属性;
外部类方法;
class 内部类名 {
内部类属性;
内部类方法;
}
}
创建内部类对象格式:
(成员内部类是依附外部类的,只有创建了外部类才能创建内部类)
方式一:
外部类名.内部类名 内部对象名 = new 外部类名().new 内部类名();
方式二:
外部类名 外部对象名 = new 外部类名();
外部类名.内部类名 内部对象名 = 外部对象名.new 内部类名();
注意:
① 和外部类不同,内部类可以被任意权限修饰符修饰;
② 内部类可以直接访问外部类的成员,包括私有成员;
③ 外部类要访问内部类的成员,必须要建立内部类的对象;
④ 当内部类属性和外部类属性重名时,内部类可以通过【外部类名.this.属性名】的方式调用外部类重名属性;
⑤ 当内部类方法和外部类方法重名时,内部类可以通过【外部类名.this.方法名】的方式调用外部类重名方法;
⑥ 内部类的内部不能有静态信息。
*/
public class Outer {
int num = 10;// 外部类属性
public void outerMethod() { // 外部类方法
System.out.println("这是一个外部类方法");
}

public class Inner {
int num = 20; // 内部类属性
void innerMethod() { // 内部类方法
System.out.println("这是一个内部类方法");
System.out.println(num); // 内部类调用内部类属性,20
System.out.println(Outer.this.num); // 内部类调用外部类属性,10
}
}
}

public class Main {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = new Outer().new Inner();
outer.outerMethod(); // 这是一个外部类方法
inner.innerMethod(); // 这是一个内部类方法 \n20 \n10
}
}

(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
/*
格式:
class 外部类名 {
外部类属性;
修饰符 返回值类型 外部类方法名(形参){
class 内部类名 {
内部类属性;
内部类方法;
}
}
}
注意:
① 局部内部类和局部变量地位类似,不能使用public,protected,缺省,private;
② 局部内部类不能使用static修饰,因此也不能包含静态成员,因为在方法结束之后,内存需要释放;
③ 局部内部类只能在定义的方法中使用,创建对象后使用,并且可以直接访问方法内的局部变量和参数,但是不能更改。
④ 当内部类属性和外部类属性重名时,内部类可以通过【外部类名.this.属性名】的方式调用外部类重名属性;
⑤ 当内部类方法和外部类方法重名时,内部类可以通过【外部类名.this.方法名】的方式调用外部类重名方法;
*/
public class Outer {
int num = 10;// 外部类属性

public void outerMethod() { // 外部类方法
int num = 20;// 方法的局部变量
class Inner {
int num = 30; // 内部类属性
void innerMethod() { // 内部类方法
System.out.println("这是一个内部类方法");
System.out.println(num); // 内部类调用内部类属性,30
System.out.println(Outer.this.num); // 内部类调用外部类属性,10
}
}
Inner inner = new Inner();
inner.innerMethod();
}
}

public class Main {
public static void main(String[] args) {
Outer outer = new Outer();
outer.outerMethod(); // 这是一个外部类方法 \n30 \n10

}
}

(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
/*
格式:
方式一:(匿名内部类,非匿名对象)
父接口名 实现名 = new 父类名或父接口名() {
重写父类或父接口的abstract方法;
自己特有的方法;
};
实现名.方法名(形参);
方式二:(匿名内部类且匿名对象)
new 父类名或父接口名() {
重写父类或父接口的abstract方法;
自己特有的方法;
}.方法名(形参);
注意:
① 匿名内部类必须继承一个父类或者实现一个父接口;
② 如果某个局部类你只需要用一次,则可以考虑使用匿名内部类;
③ 匿名内部类没有类名,因此没有构造方法。
*/
//定义一个父类接口
public interface A {
public abstract void method();
}

public class Main {
public static void main(String[] args) {
//匿名内部类,非匿名对象
A a = new A() { // 匿名内部类
@Override
public void method() {
System.out.println("实现接口中的抽象方法");
}
};
a.method(); // 实现接口中的抽象方法

// 匿名内部类且匿名对象
new A() { // 匿名内部类
@Override
public void method() {
System.out.println("实现接口中的抽象方法");
}
}.method();// 在大括号后直接调用类中方法
}
}

(4)静态内部类

静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
静态内部类创建对象格式:
外部类名.内部类名 对象名 = new 外部类名.内部类名(); //注意和成员内部类不同
注意:
① 静态内部类的创建不需要依赖外部类可以直接创建。
② 静态内部类不可以使用任何外部类的非static类(包括属性和方法),但可以存在自己的成员变量。
*/
public class Outer {
int num = 10;// 外部类属性
public void outerMethod() { // 外部类方法
System.out.println("这是一个外部类方法");
}

// 定义一个静态内部类
static class Inner {
int num = 20; // 内部类属性
void innerMethod() { // 内部类方法
System.out.println("这是一个内部类方法");
//outerMethod();// 错误,不能调用非静态方法
System.out.println(num); // 内部类调用内部类属性,20
//System.out.println(Outer.this.num);// 错误,不能调用外部非静态属性
}
}
}

五. Java常用API

API(Application Programming Interface),应用程序编程接口。它是 JDK 中提供给使用者的一些常用类的说明文档,使用者不需要访问源码或理解内部工作机制的细节,只需要会使用即可。在查看 API 文档时,使用者需要查看当前类的包路径、构造方法以及方法摘要。

引用类型的一般使用步骤:

  • 导包。import 包路径.类名称;

    • 如果需要使用的目标类,和当前类位于同一个包下,则可以省略导包语句不写。
    • 如果使用的类或接口是 java.lang 包下定义的,则可以省略 import 结构。
    • 可以使用 “xxx.*” 的方式,表示可以导入 xxx 包下的所结构。
    • 如果在源文件中,使用了不同包下的同名的类,则必须至少一个类需要以全类名的方式显示。
  • 创建。类名称 对象名 = new 类名称 ( 参数列表 );

  • 使用。对象名.成员方法名 ();

1. Object 类

  • Object 类在 java.lang 包下,导包语句可以省略不写
  • java.lang.Object 类是 Java 语言中的根类,即所有类的父类。
  • 如果一个类没有特别指定父类,那么默认则继承自 Object 类。
  • Object 类只声明了一个空参的构造器
  • Object 类中的所有方法,子类均都可以使用。(equals() 、toString() 、getClass() 、hashCode()…)
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
/*
★ equals方法
定义:
public boolean equals(Object obj) {
return (this == obj);
}
说明:
①Object类中定义的equals()和==的作用是相同的:比较两个对象的【地址值】是否相同,即两个引用是否指向同一个对象实体。
②对于自定义的类,如果希望进行对象【内容】的比较,则可以覆盖重写equals方法。
③像String、Date、File、包装类等都重写了Object类中的equals()方法。重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的"实体内容"是否相同。
*/
//equals方法的重写:
public class Person {
private String name;
private int age;

//对equals方法进行重写
@Override
public boolean equals(Object obj) {
// 如果对象地址一样,则认为相同
if (this == obj) return true;
// 如果参数为空,或者类型信息不一样,则认为不同
if (obj == null || getClass() != obj.getClass()) return false;
// 转换为当前类型
Person person = (Person) obj;
// 要求基本类型相等,并且将引用类型交给java.util.Objects类的equals静态方法取用结果
return age == person.age && Objects.equals(name, person.name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
★ toString方法
定义:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
说明:
①当我们输出一个对象的引用时,实际上就是调用当前对象的toString()。
②toString方法返回该对象的字符串表示,其实该字符串内容就是对象的类型+@+内存地址值。
③对于自定义的类,如果希望返回对象的【实体内容】,则可以覆盖重写toSting方法。
④像String、Date、File、包装类等都重写了Object类中的toString()方法。使得在调用对象的toString()时,返回"实体内容"信息
*/
// toString方法的重写:
public class Person {
private String name;
private int age;

// 对toSting方法进行重写
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}

2. Scanner类

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
/*
Scanner类的功能:可以实现键盘输入数据,到程序当中,实现人机交互。
Scanner类的使用:
①导包
import java.util.Scanner;
②创建
Scanner 对象名 = new Scanner(System.in);
③使用
● 获取键盘输入的一个int数字:int 变量名 = 对象名.nextInt();
● 获取键盘输入的一个double数字:double 变量名 = 对象名.nextDouble();
● 获取键盘输入的一个字符串:String 变量名 = 对象名.next();//结束符是回车/空格/Tab
● 获取键盘输入的一个字符串:String 变量名 = 对象名.nextLine();//读取一行,结束符只有回车
*/
//示例:从键盘输入两个int数字并求和
import java.util.Scanner;
public class Test{
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
System.out.println("请输入第一个数:");
int num1 = scanner.nextInt();
System.out.println("请输入第二个数:");
int num2 = scanner.nextInt();
System.out.println("两数之和为:"+(num1+num2));
}
}

3. Random类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
Random类的功能:用来生成随机数字。
Random类的使用:
①导包
import java.util.Random;
②创建
Random 对象名 = new Random();
③使用
● 获取一个随机的int数字(范围是int所有范围):int 变量名 = 对象名.nextInt();
● 获取一个随机的int数字(参数代表了范围,左闭右开区间):int 变量名 = 对象名.nextInt(n);
● 如果输入的为n,那么此时随机的数字范围是【0,n),又由于是整数,那么实际取值为0 1 2 ... n-1
*/
//示例:创建一个随机整数,范围为[1, 10]
import java.util.Random;
public class Test{
public static void main(String[] args){
Random random = new Random();
int num = random.nextInt(10)+1;
System.out.println("随机数为:"+num);
}
}

4. String类

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
102
103
104
105
106
/*
String类的功能:字符串。
String类的使用:
①导包
import java.lang.String;//String类在java.lang包下,导包语句可以省略不写
②创建
方式一:直接创建
String 对象名 = "Hello"; //最常用
方式二:无参或有参构造
String 对象名 = new String();// 创建了一个空白字符串,不含有任何内容。
String 对象名 = new String(String original);。
方式三:通过字符数组构造
char[] 字符数组名= {'a', 'b', 'c'};    
String 对象名 = new String(字符数组名);
方式四:通过字节数组构造
byte [] 字节数组名= { 97, 98, 99 };    
String 对象名 = new String(字节数组名);
③常用方法:
● ★boolean equals(Object obj):比较字符串的内容是否相同,如果相同,则返回true,如果不同,则返回false。
注意:如果比较双方一个常量一个变量,推荐把常量字符串写在前面,即
推荐:"abc".equals(str)
不推荐:str.equals("abc")(因为如果变量值为null,则会报NullPointerException的错误)
● ★boolean equalsIgnoreCase(String str):忽略大小写,进行内容比较。(比如验证码)
● ★int length() :返回此字符串的长度。
● String concat (String str) :将指定的字符串连接到该字符串的末尾,等价于用“+”。
● ★char charAt (int index) :返回指定索引处的char值。
● int indexOf (String str / Char c) :返回指定子字符串/字符第一次出现在该字符串内的索引。如果没有,返回-1
● int indexOf (String str,int Startindex) :返回指定子字符串/字符第一次出现在该字符串内的索引(从StartIndex处开始进行搜索)。如果没有,返回-1
● boolean isEmpty() :判断是否是空字符串
● String substring (int beginIndex) :返回一个子字符串,从beginIndex开始截取字符串到字符串结尾。
● String substring (int beginIndex, int endIndex) :返回一个子字符串,从beginIndex到endIndex截取字符串。包含beginIndex,不包含endIndex。
● int compareTo(String anotherString):比较两个字符串的大小(一个字符一个字符的比较),如果原字符串更大,返回1,相同返回0,原字符串更小返回-1
● String replace(char oldChar, char newChar):返回一个新的字符串,用newChar替换此字符串中出现的所有oldChar(比如敏感词用*替代)
● ★char[] toCharArray () :将此字符串转换为新的字符数组。
● ★byte[] getBytes () :使用平台的默认字符集将该 String编码转换为新的字节数组。
● String[] split(String regex) :将此字符串按照给定的regex(规则)拆分为字符串数组。

String类的注意事项:
①Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例实现。
②字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改,即不可以修改字符串中的单个字符。
③字符串效果上相当于是char[]字符数组,但是底层原理是byte[]字节数组。
④字符串常量池:程序当中直接写上的双引号字符串,就在字符串常量池中,而new出来的不在字符串常量池中。
对于基本类型来说,==是进行数值的比较(数据类型不一定要相同,会发生自动类型转换)。
对于引用类型来说,==是进行【地址值】的比较。
*/
// String中,==和equals方法的比较
public class Test {
public static void main(String[] args) {
String str1 = "abc";// 直接创建的字符串在字符串常量池中
String str2 = "abc";
char[] chars = {'a', 'b', 'c'};
String str3 = new String(chars);// 通过字符数组创建的字符串不在字符串常量池中。
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false,对于引用类型来说,==是进行【地址值】的比较。
System.out.println(str1.equals(str3));// true,string类中equals方法比较的是内容。
}
}
// 示例:输入一个字符串,统计其中的大写字母个数、小写字母个数、数字个数以及其他字符个数。
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
System.out.println("请输入一个字符串:");
Scanner scanner = new Scanner(System.in);
String str = scanner.nextLine();
int upper = 0;
int lower = 0;
int numbers = 0;
int others = 0;
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (chars[i] >= 'A' && chars[i] <= 'Z') {
upper++;
} else if (chars[i] >= 'a' && chars[i] <= 'z') {
lower++;
} else if (chars[i] >= '0' && chars[i] <= '9') {
numbers++;
} else {
others++;
}
}
System.out.println("大写字母有" + upper + "个");
System.out.println("小写字母有" + lower + "个");
System.out.println("数字有" + numbers + "个");
System.out.println("其他字符有" + others + "个");

}
}
// 字符串一旦创建,不可改变(优点是编译器可以让字符串共享)
public class Test {
public static void main(String[] args) {
String str = "aaa";
System.out.println(str);// aaa
str = "bbb";
System.out.println(str);// bbb,此时并不是字符串改变了,只是str不指向aaa,而指向一个新的字符串bbb了。
}
}
// 示例:输入一个字符串,如果字符串中有“你大爷的”四个汉字,则用“****”替代。
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个字符串:");// 输入:我可去你大爷的,真的醉了,你大爷的!
String string = scanner.nextLine();
String string1 = string.replace("你大爷的","****");
System.out.println(string1);// 输出:我可去****,真的醉了,****!
}
}

5. StringBuffer、StringBuilder

String 类对象一旦创建不可改变,如果想要对字符串进行修改,并且不产生新的对象,可以使用 StringBuffer 和 StringBuilder 类。

StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。

由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。

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
/*
StringBuffer类的功能:线程安全的可变字符串。
StringBuffer类的使用:
①导包
import java.lang.StringBuffer;//导包语句可以省略不写
②创建
StringBuffer 对象名 = new StringBuffer();// 构造一个其中不带字符的字符串缓冲区,其初始容量为16个字符。
StringBuffer 对象名 = new StringBuffer(String str);//指定字符串内容。
StringBuffer 对象名 = new StringBuffer(int capacity);//指定初始容量
③常用方法:
● 增 StringBuffer append(xxx): 将任意数据类型参数的字符串表示形式追加到此序列,并返回当前对象自身。
StringBuffer insert(int offset, xxx):将任意数据类型参数的字符串表示形式插入此序列中的指定位置。
● 删 StringBuffer delete(int start, int end):移除此序列指定位置的子字符串中的字符(左闭右开)。
StringBuffer deleteCharAt(int index) :移除此序列指定位置的char。
● 改 StringBuffer replace(int start, int end, String str) :使用给定字符串替换序列子字符串中的字符。
void setCharAt(int index, char ch):将给定索引处的字符设置为 ch。
● 查 char charAt(int index):返回此序列中指定索引处的char值。
● 反转 StringBuffer reverse():将此字符序列用其反转形式取代。
● 长度 int length():返回长度(字符数)。(实际值)
int capacity():返回当前容量。 (理论值)
● 遍历 String toString() :返回此序列中数据的字符串表示形式。
// for() + charAt()
StringBuffer类的注意事项:
① StringBuilder和StringBuffer非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样
② String、StringBuilder和StringBuffer底层均为字符数组
③ 执行效率:StringBuilder > StringBuffer > String
④ StringBuffe对字符串进行修改时不产生新的对象。调用方法时,本身就被修改了。
*/

6. Arrays 类

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
/*
Arrays类的功能:用来操作数组的各种方法,比如排序和搜索等。其所有方法均为静态方法,不需要创建对象,调用起来非常简单。
Arrays类的使用:
①导包
import java.util.Arrays;
②使用:
Arrays.方法名
③常用方法:
● static String toString(数组):将参数数组变成字符串(按照默认格式:[元素1, 元素2, 元素3...])
● static 新数组 copyOf(数组,长度):会将数组赋值给新数组,第二个参数是新数组的长度(直接使用==的话,其实复制的是地址而不是值),此方法还可以用于扩容。
● static void sort(数组):按照默认升序(从小到大)对数组的元素进行排序(直接对数组进行了排序,没有返回值,采用的是优化的快速排序)
● static <T> List<T> asList(T... a) 返回一个受指定数组支持的固定大小的列表,【此方法有坑,易错】,
Arrays类的注意事项:
①如果是数值,sort默认按照升序从小到大;
②如果是字符串,sort默认按照字母升序;
③如果是自定义的类型,那么这个自定义的类需要有Comparable或者Comparator接口的支持。
*/
// 示例:创建一个数组,并按照 [元素1, 元素2, 元素3...] 格式打印输出
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arrays =new int[]{1,2,3,4,5};
System.out.println(Arrays.toString(arrays));//[1, 2, 3, 4, 5]
}
}
// 示例:创建一个数组,排序后打印输出
import java.util.Arrays;
public class SortArray {
public static void main(String[] args) {
int[] arrays = new int[]{4, 2, 3, 1, 5};
Arrays.sort(arrays);
for (int i = 0; i < arrays.length; i++) {
System.out.print(arrays[i] + " ");//1 2 3 4 5
}
}
}
// 示例:asList的坑
public static void main(String[] args) {
int[] a = new int[]{1, 2, 3};
Integer[] b = new Integer[]{1, 2, 3};
List<int[]> ints = Arrays.asList(a);
List<Integer> integers = Arrays.asList(b);
System.out.println(ints.size());//1,而不是3,它把整个数组当成了一个整体
System.out.println(integers.size());//3
}

7. Math 类(包含 BigDecimal)

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
/*
Math类的功能:用于执行基本数学运算的方法,其所有方法均为静态方法,不需要创建对象,调用起来非常简单。
Math类的使用:
①导包
import java.lang.Math;(位于java.lang包下,导包可以省略不写)
②使用:
Math.方法名
③常用方法:
● static double abs(double num):获取绝对值。有多种重载。
● static double ceil(double num):向上取整。
● static double floor(double num):向下取整。
● static long round(double num):四舍五入(注意返回值为整型)。
● static double random():返回带正号的double值,该值【大于等于】0.0且【小于】1.0。
● static double pow(double x,double a):返回x^a,幂运算,返回值类型为double
● Math.PI代表近似的圆周率常量(double)。
*/
//示例:使用Math中的常用方法
public class Test {
public static void main(String[] args) {
System.out.println( Math.abs(-5.6));//5.6
System.out.println( Math.ceil(4.1));//5.0
System.out.println( Math.floor(4.9));//4.0
System.out.println( Math.round(4.6));//5
}
}

/*
如果基本的整数和浮点数精度不能够满足需求,那么可以使用java.math包中的:BigInteger和BigDecimal
BigInteger类实现任意精度的整数运算,BigDecimal实现任意精度的浮点数运算。
初始化:
①使用静态的valueOf方法,将普通的数值转化为大数
BigInteger a = BigInteger.valueOf(100)
② 使用带字符串参数的构造器得到大数
BigInteger b = new BigInteger("123456789123456789123456789")
常用方法:
● BigInteger add(BigInteger other)
● BigInteger subtract(BigInteger other)
● BigInteger multiply(BigInteger other)
● BigInteger devide(BigInteger other)
● BigInteger compareTo(BigInteger other)
*/

// 高精度
public class Test {
public static void main(String[] args) {
Double a1 = 2.0-1.1;
BigDecimal a2 = BigDecimal.valueOf(1.0).subtract(BigDecimal.valueOf(0.1));
System.out.println(a1); // 0.8999999999999999
System.out.println(a2); // 0.9
}
}

8. 包装类

为了使基本数据类型的变量具有类的特征,引入了包装类的概念。基本数据类型及其对应的包装类如下所示:

基本类型 对应的包装类(位于java.lang包中)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean
  • 基本数据类型与对应的包装类对象之间的转换【记住自动装箱与自动拆箱】
1
2
3
4
5
6
7
8
9
10
11
12
/*
★基本数据类型---->包装对象【装箱】
int i = 1;
Integer i1 = new Integer(i);//通过包装类的构造方法实现
Integer i2 = Integer.valueOf(i);//使用包装类中的valueOf方法
★包装对象---->基本数据类型【拆箱】
int num = i1.intValue();//调用包装类的.xxxValue()方法:
自动装箱与自动拆箱:从Java 1.5开始,【基本类型与包装类的装箱、拆箱动作可以自动完成(但类型必须匹配)】例如:
int i = 1
Integer i1 = i;//自动装箱。相当于Integer i = Integer.valueOf(i);
int num = i1; //自动拆箱。 相当于 int num = i1.intValue();
*/
  • 基本数据类型与字符串之间的转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
★基本数据类型---->String
方式一:调用字符串重载的valueOf()方法:
String str = String.valueOf(2.34f);
方式二:基本数据类型通过+号与””相连接
String str = 1 + “”;
★String---->基本数据类型
方式一:通过包装类的构造器实现:
int i = new Integer(“12”);//此时会有自动拆箱
方式二:通过包装类的parseXxx(String s)或者valueOf(String s)静态方法
float f = Float.parseFloat(“12.1”)//此时会有自动拆箱
float f = Float.valueOf(“12.1”)//此时会有自动拆箱
注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出`java.lang.NumberFormatException`异常。例如:
int num = Integer.parseInt("abc");
*/

包装类是不可变的,即一旦构造了包装类,就不允许更改包装在其中的值,同时包装类还是 final,因此不能派生它们的子类。

9. 日期时间类

Date类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
java.util.Date类表示特定的瞬间,精确到毫秒。
构造方法:
public Date():分配Date对象并初始化此对象,表示分配它的时间(即系统当前时间)。
public Date(long date):分配Date对象并初始化此对象,表示从基准时间(1970年1月1日00:00:00 GMT)以来的指定毫秒数。
常用方法:
★ getTime():返回自1970年1月1日00:00:00 GMT以来,此Date对象表示的毫秒数。
★ toString():把此Date对象转换为以下形式的 String: 星期 月 日 时:分:秒 日期标准 年
*/
public class Main {
public static void main(String[] args) {
Date date = new Date();
System.out.println(date); //Sat Aug 01 11:09:31 CST 2020
}
}

DateFormat类

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
/*
java.text.DateFormat是日期/时间格式化子类的抽象类,它以与语言无关的方式格式化并解析日期或时间。由于DateFormat为抽象类,不能直接使用,所以常用其子类java.text.SimpleDateFormat。
构造方法:
SimpleDateFormat() :用默认的模式和默认语言环境的日期格式符号构造SimpleDateFormat。
SimpleDateFormat(String pattern) :用给定的模式和默认语言环境的日期格式符号构造SimpleDateFormat。日期和时间格式由日期和时间模式字符串指定。常用模式字母:y年、M月、d日、H时、m分、s秒。模式举例:SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss")
常用方法:
★ String format(Date date):将Date对象按照给定模式格式化为字符串。
★ Date parse(String source):将字符串解析为Date对象。(需要异常处理,因为字符串可能没有按照指定模式创建)

*/
public class Main {
public static void main(String[] args) {
SimpleDateFormat s = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Date date = new Date();
System.out.println(date);//格式化日期之前:Sat Aug 01 11:34:53 CST 2020
System.out.println(s.format(date)); //格式化日期之后:2020年08月01日 11:34:53

try {
date = s.parse("2020年01月01日 00:00:00");//将指定模式的字符串解析为一个日期
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(date); //解析后的结果:Wed Jan 01 00:00:00 CST 2020
}
}

Calendar类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
java.util.Calendar是一个抽象类,该类将所有可能用到的时间信息封装为静态成员变量,方便获取。
获取Calendar实例的方法:
①使用Calendar.getInstance()方法
②调用它的子类GregorianCalendar的构造器。
常用方法有:
★ int get(int field):返回给定日历字段的值。
常见字段YEAR年、MONTH月、HOUR时(12小时制)、MINUTE分、SECOND秒、DAY_OF_MONTH月中的天、HOUR_OF_DAY时(24小时 制)、DAY_OF_WEEK周中的天(周几,周日为1,可以-1使用)
★ void set(int field, int value):将给定的日历字段设置为给定值。
★ abstract void add(int field, int amount):根据日历的规则,为给定的日历字段添加或减去指定的时间量。
★ Date getTime():返回一个表示此Calendar时间值(从历元到现在的毫秒偏移量)的Date对象。
注意:
① 西方星期的开始为周日,即周日为1
② 在Calendar类中,月份的表示是以0-11代表1-12月。
③ 日期是有大小关系的,时间靠后,时间越大。
*/
public class Main {
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.get(Calendar.YEAR));//2020
Date time = calendar.getTime();
System.out.println(time);//Sat Aug 01 16:42:09 CST 2020
}
}

10. java 比较器

在Java 中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题,此时不能直接通过关系运算符(>、<、<=、>=、==、!=)进行比较,java 有三种实现对象比较的方法:

1)重写 Object 类的 equals() 方法,但不能比较大小,只能比较是否相等。

2)自然排序:继承 Comparable 接口,并实现 compareTo() 方法;

3)定制排序:定义一个单独的对象比较器,继承自 Comparator 接口,实现 compare() 方法。

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
/*自然排序
Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。
注意:
① 实现Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象this大于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回负整数,如果当前对象this等于形参对象obj,则返回零。
② 实现Comparable接口的对象列表(和数组)可以通过 Collections.sort 或Arrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
③ 对于类C的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与e1.equals(e2) 具有相同的 boolean 值时,类C的自然排序才叫做与 equals 一致。建议(虽然不是必需的)最好使自然排序与equals一致。
*/
// 定义一个学生类,具有的属性为姓名,年龄,成绩,对于学生,首先按照成绩从低到高排序,如果成绩相同,再按照姓名自然排序
public class Student implements Comparable{
String name;
int age;
int score;

public Student() {
}

public Student(String name, int age, int score) {
this.name = name;
this.age = age;
this.score = score;
}

@Override
public int compareTo(Object o) {
if(o instanceof Student){
Student s = (Student) o;
if (this.score > s.score){
return 1;
}else if (this.score < s.score){
return -1;
}else{
return this.name.compareTo(s.name);
}
}else{
throw new RuntimeException("比较类型错误");
}
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}

public class Main {
public static void main(String[] args) {
Student[] student = new Student[3];
student[0]= new Student("zhangsan",12,90);
student[1]= new Student("wanger",16,80);
student[2]= new Student("lisi",14,90);
Arrays.sort(student);
for (int i = 0; i < student.length; i++) {
System.out.println(student[i]);
}
}
}
控制台信息如下:
Student{name='wanger', age=16, score=80}
Student{name='lisi', age=14, score=90}
Student{name='zhangsan', age=12, score=90}
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
/*定制排序
当元素的类型没实现java.lang.Comparable接口而又不方便修改代码,或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用Comparator的对象来排序。
注意:
① 重写compare(Object o1,Object o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2 ;如果返回0,表示相等;返回负整数,表示o1 小于o2。
② 可以将Comparator传递给sort方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。
③ 可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。
*/
// 定义一个学生类,具有的属性为姓名,年龄,成绩,对于学生,首先按照成绩从低到高排序,如果成绩相同,再按照姓名自然排序
public class Student{
String name;
int age;
int score;

public Student() {
}

public Student(String name, int age, int score) {
this.name = name;
this.age = age;
this.score = score;
}


@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}

public class Main {
public static void main(String[] args) {
Comparator comparator = new Comparator() { //匿名内部类
@Override
public int compare(Object o1, Object o2){
if(o1 instanceof Student && o2 instanceof Student){
Student s1 = (Student)o1;
Student s2 = (Student)o2;
if(s1.score > s2.score){
return 1;
}else if (s1.score <s2.score){
return -1;
}else{
return s1.name.compareTo(s2.name);
}
}else{
throw new RuntimeException("类型比较错误");
}
}
};
Student[] student = new Student[3];
student[0]= new Student("zhangsan",12,90);
student[1]= new Student("wanger",16,80);
student[2]= new Student("lisi",14,90);

Arrays.sort(student,comparator);//使用Arrays.sort进行排序
for (int i = 0; i < student.length; i++) {
System.out.println(student[i]);
}
}
}
控制台信息如下:
Student{name='wanger', age=16, score=80}
Student{name='lisi', age=14, score=90}
Student{name='zhangsan', age=12, score=90}

六. 异常

1. 异常的概念

异常是指程序在执行过程中,出现的非正常的情况,最终会导致 JVM 的非正常停止。在 Java 等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java 处理异常的方式是中断处理。

2. 异常的分类

Throwable:Throwable 类是 Java 语言中所有错误或异常的超类。类中常用方法有:

  • public void printStackTrace():打印异常的详细信息。

    包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。

  • public String getMessage() :获取发生异常的原因。

    提示给用户的时候,就提示错误原因。

Error:程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。比如栈溢出、堆溢出等错误。

Exception:程序本身可以捕获并且可以处理的异常。可以分为两大类:

  • 非受检异常(运行时异常):指 RuntimeException 类及其子类异常,编译器不会检查此类异常,并且不要求处理异常。程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。比如数组索引越界异常,空指针异常等等。
  • 受检异常(编译时异常):是 RuntimeException 以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常。

3. 常见异常

  • java.lang.ArrayIndexOutOfBoundsException 数组索引越界异常。

    1
    2
    3
    4
    5
    6
    7
    //用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
    public class Main {
    public static void main(String[] args) {
    int[] array = new int[10];
    System.out.println(array[10]);//报错
    }
    }
  • java.lang.NullPointerException 空指针异常

    1
    2
    3
    4
    5
    6
    7
    /*当应用程序试图在需要对象的地方使用null时,抛出该异常。这种情况包括:调用 null 对象的实例方法、访问或修改null对象的字段、将null作为一个数组,获得其长度、将null作为一个数组,访问或修改其时间片、将null作为Throwable值抛出。*/
    public class Main {
    public static void main(String[] args) {
    String string = null;
    System.out.println(string.equals(null));//报错
    }
    }
  • java.lang.ClassCastException 类型转换异常

    1
    2
    3
    4
    5
    6
    7
    8
    //当试图将对象强制转换为不是实例的子类时,抛出该异常。
    public class Main {
    public static void main(String[] args) {
    Object obj = new String("a"); //多态
    Math math = (Math) obj; //编译不报错,但运行会报错
    System.out.println(math);
    }
    }
  • java.lang.ArithmeticException 算术异常

    1
    2
    3
    4
    5
    6
    //当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例。
    public class Main {
    public static void main(String[] args) {
    System.out.println(1 / 0);//除0
    }
    }
  • java.lang.NumberFormatException 数字格式异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
    public class Main {
    public static void main(String[] args) {
    String string1 = "123";
    String string2 = "abc";
    Integer integer1 = Integer.valueOf(string1);
    Integer integer2 = Integer.valueOf(string2);//编译不报错
    System.out.println(integer1);
    System.out.println(integer2);//运行报错
    }
    }

4. 异常的处理

异常的处理分为两步:

第一步:抛出异常。程序在正常执行的过程中,如果出现异常,会在异常代码处生成一个对应异常类的对象,并将此对象抛出,其后的代码就不再执行。对于异常对象的产生分为两种:① 系统自动生成的异常对象 ② 手动的生成一个异常对象,并抛出(throw)

第二步:捕获异常。捕获异常分为两种:① try…catch…finally 代码块直接处理 ② 通过 throws 进行声明,让调用者去处理。

(1)try…catch…finally

通过 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
39
40
41
42
43
44
45
46
47
48
49
50
/*
格式:
try{
//可能发生异常的代码。
}catch(异常类型 e){
//对异常进行处理
}catch(异常类型 e){
...
}finally{
//一定要执行的代码
}
注意:
① 如果执行完try没有发生异常,则执行finally块和finally后面的代码(如果有的话),如果发生异常,则尝试去匹配catch块。
② 每一个catch块用于捕获并处理一个特定的异常,或者此异常类型的子类。
③ catch中的异常类型如果有子父类关系,则要求子类一定声明在父类的上面。否则,报错。
④ 在try结构中声明的变量,再出了try结构以后,就不能再被调用
⑤ finally块通常是可选的。
⑥ 无论异常是否发生,异常是否匹配被处理,finally都会执行。
⑦ finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。
*/
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入被除数:");
int a = scanner.nextInt();
System.out.println("请输入除数:");
int b = scanner.nextInt();
try {
int result = a / b;
System.out.println("两个数相除的结果为:" + result);
} catch (ArithmeticException e) {
e.printStackTrace();
} finally {
System.out.println("这是一条一定会执行的语句");
}

System.out.println("这是一条测试语句");
}
}
控制台信息如下:
请输入被除数:
10
请输入除数:
0
java.lang.ArithmeticException: / by zero
at innerclass.Main.main(Main.java:17)
这是一条一定会执行的语句
这是一条测试语句
//如果没有try...catch...finally代码块处理异常,那么测试语句则不会输出。

(2)throws

如果一个方法中的语句执行时可能生成某种异常,但是并不能确定如何处理这种异常或者由调用者处理更好,则此方法应显示地通过 throws 关键字声明抛出异常,表明该方法本身将不对这些异常进行处理,而由该方法的调用者负责处理。

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
/*
格式:
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{
代码体;
}
*/
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入被除数:");
int a = scanner.nextInt();
System.out.println("请输入除数:");
int b = scanner.nextInt();

try { //调用者自己处理异常
int result = divide(a, b);
System.out.println("两数相除的结果为:" + result);
}catch (ArithmeticException e){
e.printStackTrace();
}
System.out.println("这是一条测试语句");
}

public static int divide(int a, int b) throws ArithmeticException { //自己进行处理,而是声明交给调用者处理
return a / b;
}
}
控制台信息如下:
请输入被除数:
10
请输入除数:
0
java.lang.ArithmeticException: / by zero
at innerclass.Main.divide(Main.java:26)
at innerclass.Main.main(Main.java:17)
这是一条测试语句

(3)throw

Java 异常类对象除在程序执行过程中出现异常时由系统自动生成并抛出,也可根据需要使用人工创建并抛出 ,即通过 throw 语句手动显式的抛出一个异常。throw 语句的后面必须是一个异常对象

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
/*
使用步骤:
① 异常类名 对象名 = new 异常类名(参数); // 创建一个异常对象。封装一些提示信息(信息可自行编写)。
如:ArrayIndexOutOfBoundsException e = new ArrayIndexOutOfBoundsException(数组索引越界了!);
② throw 对象名; // 在方法内使用关键字throw,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。
如:throw e;

注意:
① 注意:上述两步可以通过使用匿名对象合二为一,格式为:throw new 异常类名(参数);
如:throw new ArrayIndexOutOfBoundsException(数组索引越界了!);
② throw 仅仅是将异常进行抛出,返回给该方法的调用者并没有进行处理,对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续将问题声明出去,使用throws声明处理。
*/
public class Main {
public static void main(String[] args) {
int[] arrays = new int[]{0, 1, 2, 3, 4};
int result = value(arrays,5);
}

public static int value(int[] arrays ,int num) {
if(num < 0 || num >= arrays.length){ //此时不能返回一个整数,可以考虑抛出异常
throw new ArrayIndexOutOfBoundsException("数组角标越界了!");
}else {
return arrays[num];
}
}
}

throw 和 throws 的区别:

throw 表示抛出一个异常类的对象,生成异常对象的过程。声明在方法体内。
throws 属于异常处理的一种方式,声明在方法的声明处。

5. 自定义异常类

在开发中根据自己业务的异常情况来自定义异常类。

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
/*
定义步骤:
① 继承于现的异常结构:RuntimeException 、Exception
③ 提供重载的构造器
*/
// 计算一个圆的面积,并自定义一个异常类 RediusException,当输入半径小于0时,抛出异常信息。
public class RediusException extends Exception {
public RediusException() {
}

public RediusException(String message) {
super(message);
}
}

public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入圆的半径:");
double redius = scanner.nextDouble();
try{
if (redius < 0) {
throw new RediusException("圆的半径不能小于0!");
} else {
System.out.println("圆的面积为:" + Math.PI * redius * redius);
}
}catch(RediusException r){
r.printStackTrace();
}
}
}
控制台信息如下:
请输入圆的半径:
-5
commonexception.RediusException: 圆的半径不能小于0
at first.Main.main(Main.java:18)

七. 多线程

1. 程序、进程与线程

程序:一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具。即一段静态的代码。

进程:一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。进程是系统进行资源分配和调度的基本单位,

线程:进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。线程是程序执行的最小单位。

2. 多线程的创建

  • 方式一:继承 Thread 类

    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
    /*
    步骤:
    ① 创建一个继承于Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
    ② 创建Thread子类的对象。
    ③ 调用子类对象的start()方法来启动该线程。
    注意:
    ① 启动一个线程,必须调用start(),不能调用run()的方式启动线程。
    ② 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出异常“IllegalThreadStateException”。
    ③ 如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().
    ★Thread类的构造方法:
    Thread() :分配一个新的线程对象。
    Thread(String name) :分配一个指定名字的新的线程对象。
    Thread(Runnable target) :分配一个带有指定目标新的线程对象。
    Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
    ★Thread类的常用方法:
    String getName() :获取当前线程名称。
    void setName(String name) :改变线程名称,使之与参数 name 相同
    void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
    void run() :此线程要执行的任务在此处定义代码。
    void join() :等待该线程终止。(在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态)
    static void yield() :暂停当前正在执行的线程对象,并执行其他线程(释放当前CPU的执行权)。
    static void sleep(long millis) :在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
    static Thread currentThread() :返回对当前正在执行的线程对象的引用。
    */
    public class MyThread1 extends Thread {// 第一步:创建一个继承于Thread类的子类,并重写该类的run()方法
    @Override
    public void run() {
    for (int i = 0; i < 10 ; i++) {
    System.out.println(Thread.currentThread().getName() + ":" + i);
    }
    }
    }

    public class Main {
    public static void main(String[] args) {
    MyThread1 m1 = new MyThread1(); // 第二步:创建Thread子类对象
    m1.start(); // 第三步:启动分线程

    for (int i = 0; i < 10 ; i++) {
    System.out.println(Thread.currentThread().getName() +":" + i);
    }
    }
    }
  • 方式二:实现 Runnable 接口

    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
    /*
    步骤:
    ① 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    ② 创建Runnable实现类的对象。
    ③ 将实现类对象作为Thread类构造方法的参数,创建Thread对象,该Thread对象才是真正的线程对象。
    ④ 调用Thread对象的start()方法来启动线程。
    注意:在开发中,相较于方式一,优先选择实现Runable接口的方式,因为可以实现“多继承”。
    */
    public class MyThread2 implements Runnable { // 第一步:定义Runnable接口的实现类,并重写该接口的run()方法
    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    System.out.println(Thread.currentThread().getName() + ":" + i);
    }
    }
    }

    public class Main {
    public static void main(String[] args) {
    MyThread2 m2 = new MyThread2();// 第二步:创建Runnable实现类的对象
    Thread thread = new Thread(m2); // 第三步:创建Thread类的对象,构造方法参数为Runnable实现类对象
    thread.start(); // 第四步:通过start()方法启动线程。

    for (int i = 0; i < 10; i++) {
    System.out.println(Thread.currentThread().getName() + ":" + i);
    }
    }
    }
  • 方式三:实现 Callable 接口

    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
    /*
    步骤:
    ① 创建Callable接口的实现类 ,并实现Call方法。
    ② 创建Callable实现类的对象。
    ③ 将Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
    ④ 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象。
    ⑤ 调用Thread对象的start()方法来启动线程。
    注意:
    ① call()方法是有返回值的,可以通过FutureTask的get()方法获取返回值。
    ② call()可以抛出异常,被外面的操作捕获,获取异常的信息
    ③ Callable是支持泛型的。
    */
    public class MyThread3 implements Callable{ // 第一步:创建Callable接口的实现类 ,并实现Call方法。
    @Override
    public Object call() throws Exception {
    for (int i = 0; i < 10; i++) {
    System.out.println(Thread.currentThread().getName() + ":" + i);
    }
    return null;// 注意有返回值。
    }
    }

    public class Main {
    public static void main(String[] args) {
    MyThread3 m3 = new MyThread3(); // 第二步:创建Callable实现类的对象。
    FutureTask f = new FutureTask(m3);// 第三步:创建FutureTask的对象,Callable实现类的对象为参数
    Thread thread = new Thread(f);// 第四步:创建Thread类的对象,FutureTask类的对象为参数
    thread.start();// 第五步:通过start()方法启动线程。

    for (int i = 0; i < 10; i++) {
    System.out.println(Thread.currentThread().getName() + ":" + i);
    }

    try {
    System.out.println(futureTask.get()); // 获取call()方法的返回值
    } catch (InterruptedException e) {
    e.printStackTrace();
    } catch (ExecutionException e) {
    e.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
    29
    30
    31
    /*
    概念:一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
    好处:
    ① 降低资源消耗。(减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务)
    ② 提高响应速度。(当任务到达时,任务可以不需要等到线程创建就能立即执行)
    ③ 提高线程的可管理性。(可以调整线程池中工作线线程的数目,防止内存消耗过多而出现故障)
    步骤:
    1. 创建线程池对象。(提供指定线程数量的线程池)
    2. 创建 Runnable 接口子类对象。
    3. 提交 Runnable 接口子类对象。
    4. 关闭线程池。
    */
    public class MyThread4 implements Runnable {
    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    System.out.println(Thread.currentThread().getName() + ":" + i);
    }
    }
    }

    public static void main(String[] args) {
    ExecutorService service = Executors.newFixedThreadPool(10);//创建线程池对象
    MyThread4 r = new MyThread4();//创建Runnable接口子类对象。
    service.submit(r);//提交Runnable接口子类对象。
    service.submit(r);
    service.shutdown();//关闭线程池。
    for (int i = 0; i < 10; i++) {
    System.out.println(Thread.currentThread().getName() + ":" + i);
    }
    }

3. 线程的生命周期

一个线程的生命周期包含 5 个状态:新建、就绪、运行、阻塞、死亡。

  • 新建(New):当线程对象对创建后(new),即进入了新建状态。

  • 就绪(Runnable):当调用线程对象的 start() 方法后,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待 CPU 调度执行,并不是说此线程立即就会执行;

  • 运行(Running):当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

  • 阻塞(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    1.等待阻塞:运行状态中的线程执行 wait() 方法,使本线程进入到等待阻塞状态;

    2.同步阻塞 – 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    3.其他阻塞 – 通过调用线程的 sleep() 或 join() 或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

  • 死亡(Dead):线程执行完了或者因异常退出了 run() 方法,该线程结束生命周期。

4. 线程的同步

当多个线程共同操作同一块共享数据时,很有可能引发线程安全问题,从而造成数据异常。(一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。)

// 经典案例:三个窗口共同卖10张票。

可能出现两个问题:

  • 三个窗口卖出同一张票(比如都卖出了第10号票)
  • 卖出不应该存在的票(比如出现了第-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
public class SellTickets implements Runnable {
int tickets = 10;

@Override
public void run() {
while (true) {
if (tickets > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} // 为了更明显的突出线程安全问题,使用sleep()让线程执行慢一些
System.out.println(Thread.currentThread().getName() + ":卖第" + tickets + "张票");
tickets--;
}
}
}
}

public class Test {
public static void main(String[] args) {
SellTickets s = new SellTickets();
Thread t1 = new Thread(s,"窗口1"); // 新建线程1
Thread t2 = new Thread(s,"窗口2"); // 新建线程2
Thread t3 = new Thread(s,"窗口3"); // 新建线程3
t1.start(); // 启动线程1
t2.start(); // 启动线程2
t3.start(); // 启动线程3
}
}
结果如下(每次结果都可能不同):
窗口3:卖第10张票
窗口2:卖第10张票
窗口1:卖第10张票
窗口3:卖第7张票
窗口2:卖第7张票
窗口1:卖第5张票
窗口3:卖第4张票
窗口2:卖第4张票
窗口1:卖第2张票
窗口2:卖第1张票
窗口3:卖第1张票
窗口1:卖第-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
    /*
    同步代码块:在方法中的某个区块中使用synchronized关键字,表示只对这个区块的资源实行互斥访问。
    格式:
    synchronized(同步锁){
    //需要同步操作的代码
    }
    注意:
    ① 任意对象都可以作为同步锁,但是多个线程要使用同一把锁【重要】。
    ② 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED),当线程执行完对应代码块/代码块发生异常后自动释放锁。
    ③ 同步代码块的锁除了自己指定外,很多时候也可以指定为this或类名.class
    ④ 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
    ⑤ 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
    ⑥ 需要同步操作的代码范围不能太大,也不能太小。
    */
    public class SellTickets implements Runnable {
    int tickets = 10;
    @Override
    public void run() {
    while (true) {
    synchronized (this) { // 同步代码块
    if (tickets > 0) {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    } // 为了更明显的突出线程安全问题,使用sleep()让线程执行慢一些
    System.out.println(Thread.currentThread().getName() +":卖第" + tickets + "张票");
    tickets--;
    }else{
    break;
    }
    }
    }
    }
    }
    public class Test {
    public static void main(String[] args) {
    SellTickets s = new SellTickets();
    Thread t1 = new Thread(s,"窗口1"); // 新建线程1
    Thread t2 = new Thread(s,"窗口2"); // 新建线程2
    Thread t3 = new Thread(s,"窗口3"); // 新建线程3
    t1.start(); // 启动线程1
    t2.start(); // 启动线程2
    t3.start(); // 启动线程3
    }
    }
    结果如下(每次结果都可能不同):
    窗口1:卖第10张票
    窗口2:卖第9张票
    窗口2:卖第8张票
    窗口2:卖第7张票
    窗口2:卖第6张票
    窗口2:卖第5张票
    窗口2:卖第4张票
    窗口2:卖第3张票
    窗口2:卖第2张票
    窗口2:卖第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
    /*
    同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
    格式:
    public synchronized void method(){
    //可能会产生线程安全问题的代码
    }
    注意:
    ① 对于非static方法,此时的同步锁就是this。
    ② 对于static方法,此时的同步锁是方法所在类的字节码对象(类名.class)。
    ③ 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
    */
    public class SellTickets implements Runnable {
    int tickets = 10;

    @Override
    public void run() {
    while (tickets > 0) {
    sell();
    }
    }

    public synchronized void sell() { // 同步方法
    if (tickets > 0) {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    } // 为了更明显的突出线程安全问题,使用sleep()让线程执行慢一些
    System.out.println(Thread.currentThread().getName() + ":卖第" + tickets + "张票");
    tickets--;
    }
    }
    }

    public class Test {
    public static void main(String[] args) {
    SellTickets s = new SellTickets();
    Thread t1 = new Thread(s,"窗口1"); // 新建线程1
    Thread t2 = new Thread(s,"窗口2"); // 新建线程2
    Thread t3 = new Thread(s,"窗口3"); // 新建线程3
    t1.start(); // 启动线程1
    t2.start(); // 启动线程2
    t3.start(); // 启动线程3
    }
    }
    结果如下(每次结果都可能不同):
    窗口1:卖第10张票
    窗口1:卖第9张票
    窗口1:卖第8张票
    窗口1:卖第7张票
    窗口1:卖第6张票
    窗口1:卖第5张票
    窗口1:卖第4张票
    窗口1:卖第3张票
    窗口1:卖第2张票
    窗口1:卖第1张票
  • 方式三:Lock 锁

    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
    /*
    Lock锁机制:通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块
    格式:
    //类中
    Lock 对象名 = new ReentrantLock();//创建对象
    //run方法中
    对象名.lock();//加同步锁
    try{
    //同步代码块
    }catch(异常类型 e){
    //异常处理
    }finally{
    对象名.unlock();//释放同步锁
    }
    注意:
    ① Lock实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作,优先使用Lock锁。
    ② java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
    ③ ReentrantLock类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
    ④ 锁定和取消锁定出现在不同作用范围中时,最好用try-【catch】-finally 加以保护,以确保在必要时释放锁。
    */
    public class SellTickets implements Runnable {
    int tickets = 10;
    Lock lock =new ReentrantLock();
    @Override
    public void run() {
    while (true) {
    lock.lock();// 加同步锁
    try {
    if (tickets > 0) {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    } // 为了更明显的突出线程安全问题,使用sleep()让线程执行慢一些
    System.out.println(Thread.currentThread().getName() +":卖第" + tickets + "张票");
    tickets--;
    }else{
    break;
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    lock.unlock();// 释放同步锁
    }
    }
    }
    }
    public class Test {
    public static void main(String[] args) {
    SellTickets s = new SellTickets();
    Thread t1 = new Thread(s,"窗口1"); // 新建线程1
    Thread t2 = new Thread(s,"窗口2"); // 新建线程2
    Thread t3 = new Thread(s,"窗口3"); // 新建线程3
    t1.start(); // 启动线程1
    t2.start(); // 启动线程2
    t3.start(); // 启动线程3
    }
    }
    结果如下(每次结果都可能不同):
    窗口1:卖第10张票
    窗口1:卖第9张票
    窗口1:卖第8张票
    窗口1:卖第7张票
    窗口1:卖第6张票
    窗口1:卖第5张票
    窗口1:卖第4张票
    窗口1:卖第3张票
    窗口1:卖第2张票
    窗口1:卖第1张票

★ Synchronized 和 Lock 的对比:

  • Lock 是显式锁(手动开启和关闭锁),synchronized 是隐式锁,出了作用域自动释放锁。
  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁
  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

5. 线程的通信

多个线程在处理同一个资源时,处理的动作(线程的任务)不相同,线程之间进行通信用来保证线程协调运行,比如控制线程执行先后顺序、获取某个线程执行的结果等

等待唤醒机制:在一个线程进行了规定操作后,就进入等待状态, 等待其他线程执行完他们的指定代码过后 再将其唤醒。

  • wait():令当前线程挂起,放弃CPU同步资源并等待(此时不会去竞争锁了,处于阻塞状态),使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。

  • notify():唤醒正在排队等待同步资源的线程,如果有多个会任意选择一个唤醒。

  • notifyAll ():唤醒正在排队等待资源的所有线程结束等待。

注意:

​ ① wait 方法与 notify 方法必须由同一个锁对象调用。只有对应的锁对象可以通过 notify 唤醒使用同一个锁对象调用 wait 方法后的线程。

​ ② wait 方法与 notify 方法是属于 Object 类的方法的。

​ ③ wait 方法与 notify 方法必须要在同步代码块或者是同步函数中使用,否则会报异常。因为:必须要通过锁对象调用这2个方法。

// 经典案例:生产者消费者问题(Producer-consumer problem)

生产者负责生产产品,而消费者负责消费产品。两个线程同时运行,要求产品的个数要大于0个,并小于10个。
如果产品个数为0,那么消费者需要等待生产者进行生产(只要生产了1个,消费者就可以继续进行消费)。
如果产品个数为10,那么生产者需要等待消费者进行消费(只要消费了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
// 产品数量
public class Amount {
int num = 0;

public synchronized void consumer() { // 进行消费
if (num <= 0) {
try {
this.wait(); // 如果数量小于等于0时,消费者就进行等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":消费第" + num +"个产品");
num --;
this.notify();
}
}

public synchronized void producer() { // 进行生产
if (num >= 10) {
try {
this.wait(); // 如果数量大于等于10时,生产者就进行等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
num ++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":生产第" + num +"个产品");
this.notify();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 生产者
public class Producer implements Runnable {
Amount amount;

public Producer(Amount amount) {
this.amount = amount;
}

@Override
public void run() {
while(true){
amount.producer();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 消费者
public class Consumer implements Runnable {
Amount amount;

public Consumer(Amount amount) {
this.amount = amount;
}

@Override
public void run() {
while(true){
Thread.currentThread().yield();
amount.consumer();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// 测试类
public class Test {
public static void main(String[] args) {
Amount amount = new Amount();
Producer p = new Producer(amount);
Consumer c = new Consumer(amount);
Thread t1 = new Thread(p,"生产者");
Thread t2 = new Thread(c,"消费者");
t1.start();
t2.start();
}
}

★sleep() 和 wait()的异同

相同点:一旦执行,都可以使得当前的线程进入阻塞状态。

不同点:

​ 1)两个方法声明的位置不同:Thread 类中声明 sleep() , Object 类中声明 wait()

​ 2)调用的要求不同:sleep() 可以在任何需要的场景下调用。 wait() 必须使用在同步代码块或同步方法中

​ 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放锁。

八. 枚举

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
/*
步骤:
class Season{
// 1.声明Season对象的属性:private final修饰
private final String seasonName;
private final String seasonDesc;

// 2.私化类的构造器,并给对象属性赋值
private Season(String seasonName,String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}

// 3.提供当前枚举类的多个对象:public static final的
public static final Season SPRING = new Season("春天","春暖花开");
public static final Season SUMMER = new Season("夏天","夏日炎炎");
public static final Season AUTUMN = new Season("秋天","秋高气爽");
public static final Season WINTER = new Season("冬天","冰天雪地");

// 4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}

public String getSeasonDesc() {
return seasonDesc;
}
// 4.其他诉求2:提供toString()
@Override
public String toString() {
return "Season{" +
"seasonName='" + seasonName + '\'' +
", seasonDesc='" + seasonDesc + '\'' +
'}';
}
}
注意:
① 枚举类对象的属性不应允许被改动, 所以应该使用 private final 修饰;
② 枚举类的使用 private final 修饰的属性应该在构造器中为其赋值;
③ 若枚举类显式的定义了带参数的构造器, 则在列出枚举值时也必须对应的传入参数;
④ 如果枚举类中只一个对象,则可以作为单例模式的实现方式;
⑤ 私有化类的构造器,保证不能在类的外部创建其对象。
*/

2. enum关键字

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
/*
步骤:
enum Season1 {
// 1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
SPRING("春天","春暖花开"),
SUMMER("夏天","夏日炎炎"),
AUTUMN("秋天","秋高气爽"),
WINTER("冬天","冰天雪地");

// 2.声明Season对象的属性:private final修饰
private final String seasonName;
private final String seasonDesc;

// 3.私化类的构造器,并给对象属性赋值

private Season1(String seasonName,String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}

// 4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}

public String getSeasonDesc() {
return seasonDesc;
}
}
注意:
① 使用 enum 定义的枚举类默认继承了 java.lang.Enum类,因此不能再继承其他类
② 枚举类的构造器只能使用 private 权限修饰符
③ 枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾)。列出的实例系统会自动添加 public static final 修饰
④ 必须在枚举类的第一行声明枚举类对象
⑤ 枚举类是线程安全的
⑥ 每个枚举成员实际上是一个枚举实例。
Enum类的主要方法:
① values() 方法:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
② valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。
③ toString():返回当前枚举类对象常量的名称
*/

九. 注解

Annotation 其实就是代码里的特殊标记,这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处理。通过使用 Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。注解类同于标签,标签为了解释事物,注解为了解释代码。(如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创造这类工具。同时他还提供了 javac 编译器钩子在编译时使用注解。)

1. 自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
注解和class、interface一样也是一种类型,通过 @interface 关键字进行定义。
格式:
public @interface 注解名{
成员变量;
}
注意:
① 自定义注解自动继承了java.lang.annotation.Annotation
② 注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。如:int id();
③ 可以在定义 Annotation 的成员变量时为其指定初始值, 指定成员变量的初始值可使用 default 关键字
④ 如果注解有成员,在使用注解时,需要指明成员的值。除非它有默认值。格式是“参数名 = 参数值”,如果只有一个参数成员,且名称为value,可以省略“value=”。
⑤ 【重要】自定义注解必须配上注解的信息处理流程(使用反射)才意义。
*/

2. Java 预置的注解

@Override::限定重写父类方法, 该注解只能用于方法
@Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的择
@SuppressWarnings: 抑制编译器警告

3. 元注解

元注解可以理解为注解的注解。有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。

@Retention 当 @Retention 应用到一个注解上的时候,它指定了所修饰的 Annotation 的生命周期,默认为CLASS。取值如下:

  • RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
  • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
  • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

对于自定义注解,如果只存着源码中或者字节码文件中就无法发挥作用,而在运行期间能获取到注解才能实现目的,所以自定义注解中肯定是使用 **@Retention(RetentionPolicy.RUNTIME)**。

@Documented 用于指定被该元 Annotation 修饰的 Annotation 类将被javadoc 工具提取成文档。默认情况下,javadoc是不包括注解的。

@Target 用于指定被修饰的 Annotation 能用于修饰哪些程序元素。取值如下:

  • ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
  • ElementType.CONSTRUCTOR 可以给构造方法进行注解
  • ElementType.FIELD 可以给属性进行注解
  • ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
  • ElementType.METHOD 可以给方法进行注解
  • ElementType.PACKAGE 可以给一个包进行注解
  • ElementType.PARAMETER 可以给一个方法内的参数进行注解
  • ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

@Inherited 如果父类被 @Inherited 注解过的注解进行了注解,那么当其子类没有被任何注解应用,这个子类就继承了超类的注解。

@Repeatable 被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。

十. 集合

1. 集合概述

集合:java 中提供的一种容器,可以用来存储多个数据,所有集合类都位于 java.util 包下。

集合和数组的区别:

  • 数组的长度不可变,而集合的长度是可变的。
  • 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
  • 数组进行插入和删除操作较为麻烦,而集合很方便。

集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection和双列集合java.util.Map

Collection 接口:单列数据,定义了存取一组对象的方法的集合,它有两个重要的子接口:

  • List:元素有序且可重复的集合,主要实现类有 ArrayListLinkedList 和 Vector 。
  • Set: 元素无序且不可重复的集合,主要实现类有 HashSet 、LinkedHashSet 和 TreeSet 。

Map 接口:双列数据,保存具有映射关系“key-value”的集合,主要实现类有 HashMap,LinkedHashMap,TreeMap。

2. Collection 接口

  • Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。

  • JDK 不提供 Collection 接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。

  • Collection 是所有单列集合的父接口,因此在 Collection 中定义了单列集合( List 和 Set )通用的一些方法,这些方法可用于操作所有的单列集合,常用方法如下:

    • boolean add(E e): 把给定的对象添加到当前集合中 。

    • boolean addAll(Collection<? extends E> c):将指定 collection 中的所有元素都添加到此 collection 中)。

    • void clear() :清空集合中所有的元素。

    • boolean remove(E e): 把给定的对象在当前集合中删除。

    • boolean removeAll(Collection<?> c) :移除此 collection 中那些也包含在指定 collection 中的所有元素(差集)。

    • boolean contains(E e): 通过元素的 equals 方法判断当前集合中是否包含给定的对象。

    • boolean isEmpty(): 判断当前集合是否为空。

    • int size(): 返回集合中有效元素的个数。

    • Object[] toArray(): 把集合中的元素,存储到数组中。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      public class Test {
      public static void main(String[] args) {
      Collection collection = new ArrayList();
      //增
      collection.add("aaa");
      collection.add("bbb");
      collection.add("ccc");
      System.out.println(collection); //[aaa, bbb, ccc]
      //删
      collection.remove("bbb");
      System.out.println(collection);//[aaa, ccc]
      //长度
      System.out.println(collection.size());//2
      //清空
      collection.clear();
      System.out.println(collection);//[]
      }
      }
  • Collection 集合的遍历

    • 方式一:Iterator 迭代器

      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
      /*
      说明:
      ① Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
      ② Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
      ③ Iterator仅用于遍历集合,本身并不提供承装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。
      ④ 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
      ⑤ 迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
      ⑥ Iterator接口的常用方法如下:
      public E next():返回迭代的下一个元素。
      public boolean hasNext():如果仍有元素可以迭代,则返回 true。
      注意:在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,则会发生异常。
      格式:
      Iterator 对象名 = 集合名.iterator();
      while(对象名.hasnext()){
      System.out.println(iterator.next());
      }
      */
      public class Test {
      public static void main(String[] args) {
      Collection collection = new ArrayList(); //使用多态方式 创建对象
      collection.add("aaa"); // 添加元素到集合
      collection.add("bbb");
      collection.add("ccc");
      collection.add("ddd");
      Iterator iterator = collection.iterator(); //创建迭代器对象
      while(iterator.hasNext()){ //判断是否有迭代元素
      System.out.println(iterator.next()); //输出迭代出的元素
      }
      }
      }
      结果为:
      aaa
      bbb
      ccc
      ddd
    • 方式二:增强 for 循环

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      /*
      说明:
      ① 增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
      ② 遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
      格式:
      for(元素的数据类型 变量名 : Collection集合or数组){
      System.out.println(变量名);
      }
      */
      public class Test {
      public static void main(String[] args) {
      Collection collection = new ArrayList(); //使用多态方式 创建对象
      collection.add("aaa"); // 添加元素到集合
      collection.add("bbb");
      collection.add("ccc");
      collection.add("ddd");
      for (Object obj : collection){
      System.out.println(obj);
      }
      }
      }

3. List 接口

(1)List 接口概述

List 接口继承自 Collection 接口,是单列集合的一个重要分支,习惯性地会将实现了 List 接口的对象称为 List 集合。List 作为 Collection集合的子接口,不但继承了 Collection 接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法。

List接口特点:

  • 元素存取有序。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
  • 带有索引,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  • 集合中可以有重复的元素,通过元素的 equals 方法,来比较是否为重复的元素。

(2)List 接口常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
增:
boolean add(E e): 向列表的尾部添加指定的元素。
void add(int index, E element): 在列表的指定位置插入指定元素
删:
boolean remove(Object obj): 从此列表中移除第一次出现的指定元素(如果存在)
E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
改:
E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
查:
E get(int index):返回集合中指定位置的元素。
长度:
int size(): 返回集合中有效元素的个数。
遍历:
① Iterator迭代器方式
② 增强for循环
③ 普通的循环

(3)List接口实现类

  • ArrayList:作为 List 接口的主要实现类;线程不安全的,效率高;底层使用 Object[] elementData 存储

    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
    /*
    ArrayList类的功能:用于实现长度可变的数组。
    ArrayList类的使用:
    ①导包
    import java.util.ArrayList;
    ②创建
    ArrayList<引用数据类型> 对象名 = new ArrayList<>();//构造一个初始容量为10的空列表。
    ArrayList<引用数据类型> 对象名 = new ArrayList<>(int initialCapacity);//构造有指定初始容量的空列表。
    ③使用
    增删改查长度遍历
    ArrayList类的注意事项:
    ①对于ArrayList来说,有一个尖括号<E>代表泛型,E取自Element(元素)的首字母。使用一种引用数据类型将E其替换即可。
    ②泛型:也就是装在集合当中的所有元素,全都是统一的什么类型。(泛型只能是引用类型,不能是基本类型。)
    ③对于ArrayList集合来说,直接打印得到的不是地址值,而是内容。如果内容是空,得到的是空的中括号:[]
    ④对于ArrayList集合来说,add添加动作一定是成功的,所以返回值可用可不用。但是对于其他集合来说,add添加动作不一定成功。
    ⑤如果希望向集合ArrayList当中存储基本类型数据,必须使用基本类型对应的“包装类”。
    ⑥ArrayList元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList是最常用的集合。
    */
    //示例:生成3个范围为[1,10]的随机数字,添加到ArrayList集合中,并遍历输出。
    import java.util.Random;
    import java.util.ArrayList;
    public class Test {
    public static void main(String[] args) {
    Random random = new Random();
    ArrayList<Integer> list = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
    int num = random.nextInt(10) + 1;
    list.add(num);
    }
    System.out.print("集合中的元素为:");
    for (int i = 0; i < list.size(); i++) {
    System.out.print(list.get(i) + " ");
    }
    }
    }
  • LinkedList:对于频繁的插入、删除操作,使用此类效率比 ArrayList 高;底层使用双向链表存储。

    1
    2
    3
    4
    5
    6
    7
    // 新增方法:
    void addFirst(E e):将指定元素插入此列表的开头。
    void addLast(E e):将指定元素添加到此列表的结尾。
    E getFirst():返回此列表的第一个元素。
    E getLast():返回此列表的最后一个元素。
    E removeFirst():移除并返回此列表的第一个元素。
    E removeLast():移除并返回此列表的最后一个元素。
  • Vector:作为 List 接口的古老实现类;线程安全的,效率低;底层使用 Object[] elementData 存储

    与 ArrayList 相似,但是 Vector 是同步的。所以说 Vector 是线程安全的动态数组。它的操作与 ArrayList 几乎一样。

★ ArrayList 和 LinkedList 的异同

二者都线程不安全,相对线程安全的 Vector,执行效率高。此外,ArrayList 是实现了基于动态数组的数据结构。LinkedList 基于链表的数据结构。对于随机访问 get 和 set,ArrayList 优于 LinkedList,因为 LinkedList 要移动针。对于新增和删除操作 add(特指插入)和 remove,LinkedList 比较占优势,因为 ArrayList 要移动数据。

4. Set 接口

(1)Set 接口概述

Set 接口和 List 接口一样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。与 List 接口不同的是,Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。

Set 接口特点:

  • 元素存取无序。不等于随机性。存储的数据在底层数组中并非照数组索引的顺序添加,而是根据数据的哈希值决定的。
  • 集合中不可以有重复的元素。保证添加的元素照 equals() 判断时,不能返回 true。

(2)Set接口实现类

  • HashSet:作为 Set 接口的主要实现类;线程不安全的;可以存储 null 值。

    • java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持。

    • HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCode与equals方法。两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。

    • 对于存放在 Set 容器中的对象,对应的类一定要重写 equals() 和 hashCode(Object obj) 方法,以实现对象相等规则。

    • 向HashSet中添加元素的过程

      1. 当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode()方法来得到该对象的hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)
      2. 如果两个元素的 hashCode 值相等,会再继续调用 equals 方法,如果 equals 方法结果为 true,添加失败;如果为 false,那么会保存该元素,若该数组的位置已经有元素了,那么会通过链表的方式继续链接。
      3. 如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功【因此当重写了 equals 后一定要重写 hashCode】。
    • 重写 hashCode() 方法的基本原则

      • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
      • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。
      • 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
    • 重写 equals() 方法的基本原则

      当一个类有自己特有的“逻辑相等”概念,当改写 equals() 的时候,总是要改写 hashCode(),根据一个类的equals 方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据 Object.hashCode() 方法,它们仅仅是两个对象。因此,违反了“相等的对象必须具有相等的散列码”。

      结论:重写 equals 方法的时候一般都需要同时复写 hashCode方法。通常参与计算 hashCode 的对象的属性也应该参与到 equals() 中进行计算。

    • HashSet集合存储数据的结构(哈希表)

      JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一 hash 值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过 key 值依次查找的效率较低。而 JDK1.8 中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

      简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。

    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
    public class Student {  //自定义学生类
    String name;
    int age;

    public Student() {
    }

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    @Override //重写equals方法
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return age == student.age &&
    Objects.equals(name, student.name);
    }

    @Override //重写hashcode方法
    public int hashCode() {
    return Objects.hash(name, age);
    }

    @Override //重写toString方法
    public String toString() {
    return "Student{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }
    }

    public class Main {
    public static void main(String[] args) {
    HashSet hashSet = new HashSet();
    hashSet.add(new Student("张三",18));
    hashSet.add(new Student("李四",20));
    hashSet.add(new Student("王二",24));
    hashSet.add(new Student("李四",20));

    for (Object obj: hashSet){ //遍历
    System.out.println(obj);
    }
    }
    }

    遍历结果为://不可重复,不按插入顺序输出
    Student{name='王二', age=24}
    Student{name='张三', age=18}
    Student{name='李四', age=20}
  • LinkedHashSet:作为 HashSet 的子类;遍历其内部数据时,可以按照添加的顺序遍历。

    • LinkedHashSet 是 HashSet 的子类。
    • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的,在遍历时会按照插入顺序输出。
    • LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
    • LinkedHashSet 不允许集合元素重复
    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
    public class Student {  //自定义学生类
    String name;
    int age;

    public Student() {
    }

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    @Override //重写equals方法
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return age == student.age &&
    Objects.equals(name, student.name);
    }

    @Override //重写hashcode方法
    public int hashCode() {
    return Objects.hash(name, age);
    }

    @Override //重写toString方法
    public String toString() {
    return "Student{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }
    }

    public class Main {
    public static void main(String[] args) {
    LinkedHashSet linkedHashSet = new LinkedHashSet();
    linkedHashSet.add(new Student("张三",18));
    linkedHashSet.add(new Student("李四",20));
    linkedHashSet.add(new Student("王二",24));
    linkedHashSet.add(new Student("李四",20));

    Iterator iterator = linkedHashSet.iterator();
    while(iterator.hasNext()){ //遍历
    System.out.println(iterator.next());
    }
    }
    }

    遍历结果为://不可重复,按插入顺序输出
    Student{name='张三', age=18}
    Student{name='李四', age=20}
    Student{name='王二', age=24}
  • TreeSet:可以照添加对象的指定属性,进行排序。

    • TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
    • TreeSet 底层使用 红黑树结构存储数据。
    • TreeSet 支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。当我们构造 TreeSet时,若使用不带参数的构造函数,则 TreeSet 的使用自然比较器;若用户需要使用自定义的比较器,则需要使用带比较器的参数。
    • 注意 TreeSet 集合不是通过 hashcode 和 equals 函数来比较元素的。它是通过 compare 或者 comparaeTo 函数来判断元素是否相等。compare 函数通过判断两个对象的 id,相同的 id 判断为重复元素,不会被加入到集合中。
    • 向 TreeSet 中添加的数据,要求是相同类的对象。
    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
    // 自然排序:TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素的大小,然后将集合元素按升序(默认情况)排列。
    public class Student implements Comparable {
    String name;
    int age;

    public Student() {
    }

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    @Override //重写toString方法
    public String toString() {
    return "Student{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }

    @Override
    public int compareTo(Object o) { //先按照学生的age进行排序,如果age相同,再按照姓名自然排序
    if(o instanceof Student){
    Student s = (Student) o;
    if (this.age > s.age){
    return 1;
    }else if (this.age < s.age){
    return -1;
    }else{
    return this.name.compareTo(s.name);
    }
    }else{
    throw new RuntimeException("比较类型错误!");
    }
    }
    }

    public class Main {
    public static void main(String[] args) {
    TreeSet treeSet = new TreeSet();
    treeSet.add(new Student("zhangsan",24));
    treeSet.add(new Student("lisi",15));
    treeSet.add(new Student("wanger",20));
    treeSet.add(new Student("lisi",15));
    treeSet.add(new Student("lisi",24));

    Iterator iterator = treeSet.iterator();
    while(iterator.hasNext()){ //遍历
    System.out.println(iterator.next());
    }
    }
    }
    结果如下:
    Student{name='lisi', age=15}
    Student{name='wanger', age=20}
    Student{name='lisi', age=24}
    Student{name='zhangsan', age=24}
    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
    // 定制排序,要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
    public class Student {
    String name;
    int age;

    public Student() {
    }

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    @Override //重写toString方法
    public String toString() {
    return "Student{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }
    }

    public class Main {
    public static void main(String[] args) {
    Comparator comparator = new Comparator() {
    @Override
    public int compare(Object o1, Object o2) { //先按学生的age排序,如果age相同,再按照姓名自然排序
    if(o1 instanceof Student && o2 instanceof Student){
    Student s1 = (Student) o1;
    Student s2 = (Student) o2;

    if (s1.age > s2.age){
    return 1;
    }else if (s1.age < s2.age){
    return -1;
    }else{
    return s1.name.compareTo(s2.name);
    }
    }else{
    throw new RuntimeException("比较类型错误!");
    }
    }
    };
    TreeSet treeSet = new TreeSet(comparator); //定制排序
    treeSet.add(new Student("zhangsan",24));
    treeSet.add(new Student("lisi",15));
    treeSet.add(new Student("wanger",20));
    treeSet.add(new Student("lisi",15));
    treeSet.add(new Student("lisi",24));

    Iterator iterator = treeSet.iterator();
    while(iterator.hasNext()){ //遍历
    System.out.println(iterator.next());
    }
    }
    }
    结果如下:
    Student{name='lisi', age=15}
    Student{name='wanger', age=20}
    Student{name='lisi', age=24}
    Student{name='zhangsan', age=24}

5. Map 接口

(1)Map 接口概述

Map 与 Collection 并列存在。用于保存具有映射关系的数据:key-value。

List 接口特点:

  • key 和 value 都可以是任何引用类型的数据
  • key 用 Set 来存放, 不允许重复,即同一个 Map 对象所对应的类,必须重写hashCode()和equals()方法
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value。

(2)Map接口常用方法

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
增:
V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
删:
V remove(Object key): 移除指定key的key-value对,并返回value(如果存在)
改:
V put(K key, V value): 将指定key-value修改当前map对象中。
查:
V get(Object key): 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回null, 并把指定的键值添加到集合中。
长度:
int size(): 返回此映射中的键-值映射关系数。 。
遍历:
Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。
/*
Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在Map中是一一对应关系,这一对对象又称做Map中的一个Entry(项)。Entry将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对(Entry)对象中获取对应的键与对应的值。
既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:
K getKey():获取Entry对象中的键。
V getValue():获取Entry对象中的值。
*/

// 遍历方式一:KeySet() + get()
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("张三",90);
map.put("李四",80);
map.put("王二",90);
Set<String> set = map.keySet(); //获取所有的键
for (String str: set){
System.out.println("key:" + str + ",value:" + map.get(str));
}
}
}
结果如下:
key:李四,value:80
key:张三,value:90
key:王二,value:90

// 遍历方式二:entrySet() + getkey() getValue()
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("张三",90);
map.put("李四",80);
map.put("王二",90);
Set<Map.Entry<String,Integer>> set = map.entrySet();
for (Map.Entry str: set){
System.out.println("key:" + str.getKey() + ",value:" + str.getValue());
}
}
}
结果如下:
key:李四,value:80
key:张三,value:90
key:王二,value:90

(3)Map接口实现类

  • HashMap:作为 Map 的主要实现类;线程不安全的,效率高;可以存储 null 的 key 和 value

    • 允许使用 null 键和 null 值,与 HashSet 一样,不保证映射的顺序。

    • 所有的 key 构成的集合是 Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()

    • 所有的 value 构成的集合是 Collection:无序的、可以重复的。所以,value 所在的类要重写:equals()。

    • 一个 key-value 构成一个 entry,所有的 ntry构成的集合是 Set:无序的、不可重复的

    • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。

    • HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true。

    • HashMap 在 jdk7 中实现原理

      • HashMap map = new HashMap(); 在实例化以后,底层创建了长度是 16 的一维数组 Entry[] table

      • map.put(key1,value1);

      • 首先:调用 key1 所在类的 hashCode() 计算 key1 哈希值,此哈希值经过某种算法计算以后,得到

        在Entry数组中的存放位置。

        • 如果此位置上的数据为空,此时的 key1-value1 添加成功。 【情况1】
        • 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较 key1和已经存在的一个或多个数据的哈希值:
        • 如果key1 的哈希值与已经存在的数据的哈希值都不相同,此时 key1-value1 添加成功。【情况2】
        • 如果key1 的哈希值和已经存在的某一个数据 (key2-value2) 的哈希值相同,继续比较:调用 key1 所在类 equals(key2) 方法:
          • 如果equals() 返回false:此时key1-value1添加成功。【情况3】
          • 如果equals() 返回 true:使用 value1 替换 value2。【情况】
      • 在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来。

    • HashMap 在 jdk8 中相较于 jdk7 在底层实现方面的不同

      • new HashMap(); 底层默认创建一个长度为16的数组
      • 首次调用put()方法时,底层创建长度为16的数组,jdk8底层的数组是:Node[],而非Entry[]
      • jdk7 底层结构:数组+链表。jdk8 中底层结构:数组+链表+红黑树
      • 形成链表时(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
      • 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
    • HashMap底层典型属性的属性的说明

      • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
      • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
      • threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
      • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
      • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
  •  `LinkedHashMap`:LinkedHashMap 是 HashMap 的子类。在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序,与 LinkedHashSet 类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致。
    
  •  `TreeMap`: 保证照添加的 key-value 对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树。
     
     - 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException。
     - 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口。
     - TreeMap判断 两个key 相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
    
  •  `Hashtable`:作为古老的实现类;线程安全的,效率低;不能存储 null 的 key 和 value,Hashtable 实现原理、功能和 HashMap 相同。
    

(4)Collections 工具类

Collections 是一个操作 Set、List 和 Map 等集合的工具类,就像 Arrays 是操作数组的工具类一样。Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法,常用方法如下:

  • static <T> boolean addAll(Collection<T> c, T... elements) :将所有指定元素添加到指定 collection 中。

  • static void shuffle(List<?> list) :对 List 集合元素进行随机排序。

  • static <T> void sort(List<T> list):根据元素的自然顺序对指定列表按升序进行排序。

  • static <T> void sort(List<T> list,Comparator<? super T> ):根据指定比较器产生的顺序对指定列表进行排序。

  • static void swap(List<?> list, int i, int j) :在指定列表的指定位置处交换元素

  • static void reverse(List<?> list) :反转指定列表中元素的顺序

  • static Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素

  • static Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素。

  • Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。ArrayList和HashMap都是线程不安全的,如果程序要求线程安全,我们可以将ArrayList、HashMap转换为线程安全的。使用synchronizedList(List list)synchronizedMap(Map map)

    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
    public class Main {
    public static void main(String[] args) {
    ArrayList arrayList = new ArrayList();
    arrayList.add(2);
    arrayList.add(3);
    arrayList.add(1);
    System.out.println(arrayList);//[2, 3, 1]

    // 反转
    Collections.reverse(arrayList);
    System.out.println(arrayList);//[1, 3, 2]

    // 排序
    Collections.sort(arrayList);
    System.out.println(arrayList);//[1, 2, 3]

    // 获取最大小值
    System.out.println(Collections.max(arrayList));//3
    System.out.println(Collections.min(arrayList));//1

    // 随机排序
    Collections.shuffle(arrayList);
    System.out.println(arrayList);//[3, 1, 2]

    // 交换
    Collections.swap(arrayList,0,1);
    System.out.println(arrayList);//[1, 3, 2]
    }
    }

十一. 泛型

1. 泛型的概念

泛型,即“参数化类型”,把类型当作是参数一样传递。就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。

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
// 定义一个集合,不使用泛型
public class Main {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();//没有使用泛型,任何数据类型都可以存储,
arrayList.add("abc");
arrayList.add(123);

for (int i = 0; i < arrayList.size(); i++) {
String str = (String) arrayList.get(i);
System.out.println(str);
}
}
}
// ① 此程序编译正常,运行时就会出现ClassCastException的异常。
// ② 获取数据元素时,需要类型强制转换。

// 定义一个集合,使用泛型
public class Main {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("abc");
// arrayList.add(123); //编译报错

for (int i = 0; i < arrayList.size(); i++) {
// String str = (String) arrayList.get(i);
String str = arrayList.get(i); // 不需要再强制类型转换
System.out.println(str);
}
}
}

3. 泛型的定义与使用

(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
/*泛型类型用于类的定义中,被称为泛型类,用户使用该类的时候,才把类型明确下来。
定义格式:
修饰符 class 类名<泛型类型变量> {
//代码体;
}
注意:
① 一些常用的泛型类型变量:
E:元素(Element),多用于java集合框架
K:关键字(Key)
N:数字(Number)
T:类型(Type)
V:值(Value)
② 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
③ 泛型类的构造器如下:public GenericClass(){}。而public GenericClass<E>(){}是错误的。
④ 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
⑤ 泛型如果不指定,将被擦除,泛型对应的类型均按照 Object 处理,但不等价于 Object。
⑥ <数据类型> 只能是引用类型
泛型类的实例化:
类名<数据类型> 对象名 = new 类名<>(参数列表);
*/
//自定义泛型类
public class Student<T> {
private T phoneNumber;

public Student() {
}

public T getPhoneNumber() {
return phoneNumber;
}

public void setPhoneNumber(T phoneNumber) {
this.phoneNumber = phoneNumber;
}
}

public class Main {
public static void main(String[] args) {
Student<String> s1 = new Student<>();
Student<Long> s2 = new Student<>();
s1.setPhoneNumber("123456789");
// s1.setPhoneNumber(123456789);// 编译报错,类型必须是String
s2.setPhoneNumber(987654321L);
String str1 = s1.getPhoneNumber();// 不再需要强制类型转换
Long str2 = s2.getPhoneNumber();// 不再需要强制类型转换
System.out.println(str1);// 123456789
System.out.println(str2);// 987654321
}
}

(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
/*泛型类型用于接口的定义中,被称为泛型接口。
定义格式:
修饰符 interface 接口名<泛型类型变量>{
//代码体;
}
实现泛型接口:
方式一:定义类时确定泛型的类型
修饰符 class 类名 implements 接口名<具体数据类型>{
//方法重写
}
方式二:定义类时不确定泛型的类型(直到创建对象时,才确定泛型的类型)
修饰符 class 类名<泛型类型变量> implements 接口名<泛型类型变量>{
//方法重写
}

*/
// 自定义泛型接口
public interface USB<T> {
public void use(T t);
}

// 实现泛型接口
public class Equipment implements USB<String> {

@Override
public void use(String name) {
System.out.println(name + "的USB正在使用");
}
}

public class Main {
public static void main(String[] args) {
Equipment equipment = new Equipment();
equipment.use("电脑");// 电脑的USB正在使用
}
}

(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
/*泛型类型用于方法的定义中,被称为泛型方法。
泛型类,是在实例化类的时候指明泛型的具体类型;
泛型方法,是在调用方法的时候指明泛型的具体类型
定义格式:
修饰符 <泛型类型变量> 返回值类型 方法名(参数) {
//方法体;
}
*/
//自定义泛型方法
public class Student {
public<T> void show(T t){ // 泛型方法
if(t instanceof String){
System.out.println("这是一个String类型");
}else if(t instanceof Integer){
System.out.println("这是一个Integer类型");
}else{
System.out.println("这既不是一个String类型,也不是一个Integer类型");
}
}
}

public class Main {
public static void main(String[] args) {
Student student = new Student();
student.show("张三");// 这是一个String类型
student.show(123);// 这是一个Integer类型
student.show(new int[2]);//这既不是一个String类型,也不是一个Integer类型
}
}

4. 泛型通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用 Object 类中的共性方法,集合中元素自身方法无法使用。

无界通配符:类型名称 <?> 对象名称 (可以接收任何类型)

上界通配符:类型名称 <? extends 类 > 对象名称 (只能接收该类型及其子类)

下界通配符: 类型名称 <? super 类 > 对象名称 (只能接收该类型及其父类型)

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
public class Main {
public static void main(String[] args) {
// 现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();

// 测试无界通配符
getElement(list1);
getElement(list2);
getElement(list3);
getElement(list4);

// 测试上界通配符
getElement1(list1);
// getElement1(list2);// 编译报错
getElement1(list3);
// getElement1(list4);// 编译报错

// 测试下界通配符
// getElement2(list1);// 编译报错
// getElement2(list2);// 编译报错
getElement2(list3);
getElement2(list4);

}
// 无界通配符:任意都可以接收
public static void getElement(Collection<?> coll){}

// 上界通配符:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){ }

// 下界通配符:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){ }
}

十二. IO 流

1. File 类的使用

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
/*
java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
构造方法:
public File(String pathname) :通过将给定的路径名字符串转换为抽象路径名来创建新的File实例。
public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的File实例。
public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的File实例。。
常用方法:
获取
public String getAbsolutePath() :返回此File的绝对路径名字符串。
public String getPath() :将此File转换为路径名字符串。
public String getName() :返回由此File表示的文件或目录的名称。
public String getParent():获取上层文件目录路径。若无,返回null。
public long length() :返回由此File表示的文件的长度。
判断
public boolean exists() :此File表示的文件或目录是否实际存在。
public boolean isDirectory() :此File表示的是否为目录。
public boolean isFile() :此File表示的是否为文件。
创建
public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
public boolean mkdir() :创建由此File表示的目录。
public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录(如果父目录不存在,一并创建)。
删除
public boolean delete() :删除由此File表示的文件或目录。
目录的遍历
public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
注意:
① 一个File对象代表硬盘中实际存在的一个文件或者目录。无论该路径下是否存在文件或者目录,都不影响File对象的创建。
② File类中涉及到关于文件或文件目录的创建、删除、文件大小等方法,并未涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成。
③ 后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的"终点"。
④ 路径的分类:
相对路径:相较于某个路径下,指明的路径。(如果用IDEA开发使用JUnit中的单元测试方法测试,相对路径即为当前Module下,如果使用main()测试,相对路径即为当前的Project下)。
绝对路径:包含盘符在内的文件或文件目录的路径
⑤ 对于delete方法,如果此File表示目录,则目录必须为空才能删除,且java的删除不走回收站。
⑥ 路径分隔符
windows和DOS系统默认使用“\”来表示
UNIX和URL使用“/”来表示
*/
public class Main {
public static void main(String[] args) {
File file = new File("D:\\IO\\Hello.txt");
System.out.println(file.getName()); // Hello.txt
System.out.println(file.getParent()); // D:\IO
System.out.println(file.getAbsolutePath()); // D:\IO\Hello.txt
System.out.println(file.length()); // 0,文件是空的,所以长度为0

System.out.println(file.exists()); // true
System.out.println(file.isDirectory());// false
System.out.println(file.isFile());// true

try {
boolean newFile = file.createNewFile();
System.out.println(newFile);// false,因为文件已经存在
} catch (IOException e) {
e.printStackTrace();
}

boolean delete = file.delete();
System.out.println(delete);// true

File file1 = new File("D:\\IO");

// 获取当前目录下的文件以及文件夹的名称(如果目录下没文件和文件夹,则会报NullPointerException异常)
String[] list = file1.list();
for (String s : list) {
System.out.println(s);
}

// 获取当前目录下的文件以及文件夹对象,只要拿到了文件对象,那么就可以获取更多信息
File[] files = file1.listFiles();
for (File f : files) {
System.out.println(f);
}
}
}

2. IO 概述

(1)基本概念

I/O 是 Input/Output 的缩写, I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。Java程序中,对于数据的输入/输出操作以 “流(stream)” ” 的方式进行。

流代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象。

(2)流的分类

  • 按数据的流向分为:输入流输出流

    • 输入流 :把数据从其他设备上读取到内存中的流。

    • 输出流 :把数据从内存 中写出到其他设备上的流。

  • 按处理数据单位不同分为:字节流字符流

    • 字节流 :以字节为单位,读写数据的流(用于操作图片、视频等非文本文件,建议不要操作文本文件,可能会乱码)。

    • 字符流 :以字符为单位,读写数据的流(只能操作文本文件,不能操作图片,视频等非文本文件。)。

  • 按功能不同分为:节点流处理流

    • 节点流:以从或向一个特定的地方(节点)读写数据。如 FileInputStream

    • 处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装。

所有的流都继承下面四个抽象基类,且子类名称是以其父类名作为子类名后缀。

输入流 输出流
字节流 InputStream OutputStream
字符流 Reader Writer

IO流体系

字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader InputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流 PrintStream PrintWriter
特殊流 DataInputStream DataOutputStream

3. 抽象基类

(1)InputStream

java.io.InputStream 类是字节输入流所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

  • void close() :关闭此输入流并释放与此流相关联的任何系统资源。(程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应显式关闭文件 IO 资源。)
  • abstract int read() :从输入流读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。
  • int read(byte[] b):从输入流中读取最多 b.length 一些字节数,并将它们存储到字节数组 b 中。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。
  • int read(byte[] b, int off, int len):将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值 -1

(2)OutputStream

java.io.OutputStream 类是字节输出流所有类的超类,将指定字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • void write(byte[] b) :将 b.length 字节从指定的字节数组写入此输出流。
  • void write(byte[] b, int off, int len) :从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流。
  • abstract void write(int b) :将指定的字节写入此输出流。

(3)Reader

java.io.Reader 类是用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

  • void close() :关闭此流并释放与此流相关联的任何系统资源。
  • int read() : 从输入流读取一个字符,自动提升为int类型。 范围在 0 到 65535 之间 ,如果已到达流的末尾,则返回 -1
  • int read(char[] cbuf) : 将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
  • int read(char[] cbuf,int off,int len) : 将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。

(4)Writer

java.io.Writer 类是用于写出字符流的所有类的超类,将指定字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • void write(int c) : 写入单个字符。
  • void write(char[] cbuf) : 写入字符数组。
  • abstract void write(char[] cbuf, int off, int len) : 写入字符数组的某一部分,off为开始索引,len为字符个数。 入len个字符部分到文件写入器, 从偏移量 off 的位置读取字符数组。
  • void write(String str) : 写入字符串。
  • void write(String str, int off, int len) : 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
  • void flush() : 刷新该流的缓冲。
  • void close() : 关闭此流,但要先刷新它。

4. 文件流

(1)FileReader类

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
/*
java.io.FileReader 类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
构造方法:
FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称(底层会调用上面一个构造方法)。
读取文件步骤:
① 建立一个流对象,将已存在的一个文件加载进流。
② 读入操作
③ 关闭资源。
注意:
① 在读取文件时,必须保证该文件已存在,否则报异常。
② 为了能够确定的关闭流,应该使用try-catch-finally处理异常。
*/
//将一个文件的内容输出到控制台上
public class Main {
public static void main(String[] args) throws Exception {
File file = new File("D:\\IO\\Hello.txt");// 文本内容为HelloWorld!

// 1.创建流对象
FileReader fileReader = new FileReader(file);

// 2.读入操作
char[] chars = new char[5];
int len;
while((len = fileReader.read(chars)) !=-1){
System.out.print(new String(chars,0,len)); // HelloWorld!
}

// 3.关闭流资源
fileReader.close();
}
}

(2)FileWriter类

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
/*
java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
构造方法:
FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。
FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。
FileWriter(String fileName,true): 创建一个新的 FileWriter,给定要读取的文件的名称。
写入文件步骤:
① 建立一个流对象,将一个文件加载进流(文件可以存在,也可以不存在)。
② 写入操作
③ 关闭资源。
注意:
① 如果不关闭流资源,数据只是保存到缓冲区,并未保存到文件。
② 因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush方法了。
flush :刷新缓冲区,流对象可以继续使用。
close :先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
③ 在写入一个文件时,如果使用构造器FileWriter(file,false) / FileWriter(file),则目录下有同名文件将被覆盖。
④ 如果使用构造器FileWriter(file,true),则目录下的同名文件不会被覆盖,在文件内容末尾追加内容
*/
// 将abcdefghijklmnopqrstuvwxyz追加到Hello.txt文件末尾,注意不是覆盖。
public class Main {
public static void main(String[] args) throws Exception {
File file = new File("D:\\IO\\Hello.txt");

// 1.创建流对象
FileWriter fileWriter = new FileWriter(file,true);

// 2.写入操作
fileWriter.write("abcdefghijklm");
fileWriter.write("nopqrstuvwxyz");// 继续写

// 3.关闭流资源
fileWriter.close();
}
}


// 文本文件的复制,将Hello.txt文件复制一份为Hello1.txt
public class Main {
public static void main(String[] args) {
File srcfile = new File("D:\\IO\\Hello.txt");
File destfile = new File("D:\\IO\\Hello1.txt");

FileReader fileReader = null;
FileWriter fileWriter = null;

try {
// 1.创建流对象
fileReader = new FileReader(srcfile);
fileWriter = new FileWriter(destfile);

// 2.读出写入操作
char[] chars = new char[5];
int len;
while ((len = fileReader.read(chars)) != -1) {
String str = new String(chars, 0, len);
fileWriter.write(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3.关闭流资源
if (fileReader != null) {
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}

if (fileWriter != null) {
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

(3)FileInputStream类

1
2
3
4
5
6
7
8
9
10
/*
java.io.FileInputStream 类是文件输入流,从文件中读取字节。
构造方法
FileInputStream(File file):通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的File对象file命名。
FileInputStream(String name):通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名name命名。
读取文件步骤
① 建立一个流对象,将已存在的一个文件加载进流。
② 读入操作
③ 关闭资源。
*/

(4)FileOutputStream类

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
/*
java.io.FileOutputStream 类是文件输出流,用于将数据写出到文件。
构造方法
public FileOutputStream(File file) :创建文件输出流以写入由指定的 File对象表示的文件。
public FileOutputStream(String name) :创建文件输出流以指定的名称写入文件。
写入文件步骤:
① 建立一个流对象,将一个文件加载进流(文件可以存在,也可以不存在)。
② 写入操作
③ 关闭资源。
注意:
① 流的关闭原则:先开后关,后开先关。
② 在写入一个文件时,如果使用构造器FileOutputStream(file),则目录下有同名文件将被覆盖。
③ 如果使用构造器FileOutputStream(file,true),则目录下的同名文件不会被覆盖,在文件内容末尾追加内容。
*/
// 非文本文件的复制。将Test.jpg图片复制一份为Test1.jpg
public class Main {
public static void main(String[] args) {
File srcfile = new File("D:\\IO\\Test.jpg");
File destfile = new File("D:\\IO\\Test1.jpg");

FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;

try {
// 1.创建流对象
fileInputStream = new FileInputStream(srcfile);
fileOutputStream = new FileOutputStream(destfile);

// 2.读出写入操作
byte[] bytes = new byte[5];
int len;
while ((len = fileInputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3.关闭流资源
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

5. 缓冲流

使用缓冲流的好处是,能够高效的读写信息,原理是在创建流对象时,会创建一个内置的默认大小(8Kb)的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

  • 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区。
  • 当使用 BufferedInputStream 读取字节文件时,BufferedInputStream 会一次性从文件中读取 8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个 8192 个字节数组。
  • 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream 才会把缓冲区中的数据一次性写到文件里。使用方法 flush() 可以强制将缓冲区的内容全部写入输出流。
  • 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流。
  • flush() 方法的使用:手动将buffer中内容写入文件。
  • 如果是带缓冲区的流对象的 close() 方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出。

(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
/*
字节缓冲流:BufferedInputStream,BufferedOutputStream
构造方法:
public BufferedInputStream(InputStream in) :创建一个新的缓冲输入流。
public BufferedOutputStream(OutputStream out):创建一个新的缓冲输出流。
*/
// 非文本文件的复制。将Test.mkv视频(1.15G)复制一份
public class Main {
public static void main(String[] args) throws IOException {
// 方式一:使用文件流进行大文件的复制
long start1 = System.currentTimeMillis();
File srcfile1 = new File("D:\\IO\\Test.mkv");
File destfile1 = new File("D:\\IO\\Test1.mkv");

// 1.造流
FileInputStream fileInputStream1 = new FileInputStream(srcfile1);
FileOutputStream fileOutputStream1 = new FileOutputStream(destfile1);

// 2.复制操作
byte[] bytes1 = new byte[1024];
int len1;
while((len1 = fileInputStream1.read(bytes1))!=-1){
fileOutputStream1.write(bytes1,0,len1);
}

// 3.关闭流
fileInputStream1.close();
fileOutputStream1.close();
long end1 = System.currentTimeMillis();
System.out.println(end1 - start1);//5404

// 方式二:使用缓冲流进行大文件的复制
long start2 = System.currentTimeMillis();
File srcfile2 = new File("D:\\IO\\Test.mkv");
File destfile2 = new File("D:\\IO\\Test2.mkv");

// 1.造流
FileInputStream fileInputStream2 = new FileInputStream(srcfile2);
FileOutputStream fileOutputStream2 = new FileOutputStream(destfile2);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream2);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream2);

// 2.复制操作
byte[] bytes2 = new byte[1024];
int len2;
while((len2 = bufferedInputStream.read(bytes2))!=-1){
bufferedOutputStream.write(bytes2,0,len2);
}

// 3.关闭流
bufferedInputStream.close();
bufferedOutputStream.close();

long end2 = System.currentTimeMillis();
System.out.println(end2 - start2);// 1390,可见缓冲流的效率更高
}
}

(2)字符缓冲流

1
2
3
4
5
6
7
8
9
/*
字符缓冲流:BufferedReader,BufferedWriter
构造方法:
public BufferedReader(Reader in) :创建一个新的缓冲输入流。
public BufferedWriter(Writer out):创建一个新的缓冲输出流。
特有方法:
★ BufferedReader:public String readLine(): 读一行文字,包含该行内容的字符串,不包含任何行终止符,如果已到达流末 尾,则返回null
★ BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号。
*/

6. 转换流

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照 A 规则存储,同样按照 A 规则解析,那么就能显示正确的文本符号。反之,按照 A 规则存储,再按照 B 规则解析,就会导致乱码现象。

编码:字符(能看懂的)–字节(看不懂的)

解码:字节(看不懂的)–>字符(能看懂的)

  • 转换流提供了在字节流和字符流之间的转换
  • Java API提供了两个转换流:
    • InputStreamReader :将InputStream 转换为Reader(字节到字符的桥梁)
    • OutputStreamWriter :将Writer 转换为OutputStream(字符到字节的桥梁)
  • 字节流中的数据都是字符时,转成字符流操作更高效。
  • 很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能。

(1)InputStreamReader 类

转换流java.io.InputStreamReader,是 Reader 的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

在 IDEA 中,使用FileReader 读取项目中的文本文件。由于 IDEA 的设置,都是默认的UTF-8编码,所以没有任何问题。但是,当读取 Windows 系统中创建的文本文件时,由于 Windows 系统的默认是 GBK 编码,就会出现乱码。可以通过转换流读取 GBK 编码文件。

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
/*
构造方法:
InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。
*/
// 读取一个GBK编码文件,输出到控制台上
public class Main {
public static void main(String[] args) throws IOException {
// 方式一:用FileReader类读一个GBK编码文件(会出现乱码问题)
// 1.创建流对象
FileReader fileReader = new FileReader(new File("D:\\IO\\IO.txt"));
// 2.读操作
char[] chars1 = new char[1024];
int len1;
while((len1 = fileReader.read(chars1)) != -1){
System.out.print(new String(chars1,0,len1));//����һ��GBK�����ļ���
}
// 3.关闭流
fileReader.close();

// 方式二:用InputStreamReader类读取一个GBK编码文件
// 1.创建流对象
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(new File("D:\\IO\\IO.txt")), "GBK");
// 2.读操作
char[] chars2 = new char[1024];
int len2;
while((len2 = inputStreamReader.read(chars2)) != -1){
System.out.print(new String(chars2,0,len2));//这是一个GBK编码文件!
}
// 3.关闭流
inputStreamReader.close();
}
}

(2)OutputStreamWriter 类

转换流java.io.OutputStreamWriter ,是 Writer 的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

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
/*
构造方法:
OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。
*/
// 将一个GBK编码文件复制一份为UTF-8编码文件
public class Main {
public static void main(String[] args) throws IOException {

// 1.创建流对象
FileInputStream fileInputStream = new FileInputStream(new File("D:\\IO\\IO.txt"));
FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\IO\\IO1.txt"));
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "GBK");
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream,"UTF-8");
// 2.读写操作
char[] chars = new char[1024];
int len;
while((len = inputStreamReader.read(chars)) != -1){
outputStreamWriter.write(chars,0,len);
}
// 3.关闭流
inputStreamReader.close();
outputStreamWriter.close();
}
}

7. 对象流(序列化流)

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。

反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化对象的数据对象的类型对象中存储的数据信息,都可以用来在内存中创建对象。

ObjectOutputStream:内存中的对象—>存储中的文件、通过网络传输出去:序列化过程

ObjectInputStream:存储中的文件、通过网络接收过来 —>内存中的对象:反序列化过程

对象的序列化机制:

对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。

(1)ObjectOutputStream 类

java.io.ObjectOutputStream 类,将 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
/*
构造方法
public ObjectOutputStream(OutputStream out) :创建一个指定OutputStream的ObjectOutputStream。
序列化操作
一个对象要想序列化,必须满足下面条件:
① 该类必须实现java.io.Serializable接口,Serializable是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException。
② 当前类提供一个全局常量:serialVersionUID
③ 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient关键字修饰。
写出对象方法
public final void writeObject (Object obj): 将指定的对象写出。
*/
// 将一个Student对象存储到Test.txt文件中
public class Student implements Serializable {
String name;
int age;
private static final long serialVersionUID = 684979447754667710L;
public Student() {
}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

public class Main {
public static void main(String[] args) throws IOException {
Student student = new Student("张三",18);
// 1.创建流对象
FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\IO\\Test.txt"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
// 2.写操作
objectOutputStream.writeObject(student);
// 3.关闭流对象
objectOutputStream.close();
}
}

(2)ObjectInputStream类

ObjectInputStream 反序列化流,将之前使用 ObjectOutputStream 序列化的原始数据恢复为对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
构造方法:
public ObjectInputStream(InputStream in) `: 创建一个指定InputStream的ObjectInputStream。
反序列化操作:
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream读取对象的方法:
public final Object readObject () : 读取一个对象。
*/
// 将Test.txt文件中的数据读出到控制台上
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 1.创建流对象
FileInputStream fileInputStream = new FileInputStream(new File("D:\\IO\\Test.txt"));
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
// 2.写操作
Object o = objectInputStream.readObject();
System.out.println(o); //Student{name='张三', age=18}
// 3.关闭流对象
objectInputStream.close();
}
}

十三. 网络编程

1. IP 地址和端口号

(1)IP 地址

IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。

IP 地址分类

  • IPv4:是一个 32 位的二进制数,通常被分为 4 个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中 a、b、c、d 都是 0~255 之间的十进制整数,那么最多可以表示 42 亿个。

  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得 IP 的分配越发紧张。

    为了扩大地址空间,拟通过 IPv6 重新定义地址空间,采用 128 位地址长度,每 16 个字节一组,分成 8 组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

特殊的 IP 地址

  • 本机IP地址:127.0.0.1localhost

(2)端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

如果说 IP 地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。 端口与 号与IP 地址的组合得出一个网络套接字:Socket。

端口号标识正在计算机上运行的进程(程序),用两个字节表示的整数,它的取值范围是 0~65535。

端口分类

  • 公认端口:0~1023。被预先定义的服务通信占用(如:HTTP 占用端口 80,FTP 占用端口 21,Telnet 占用端口23)
  • 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat 占用端口 8080,MySQL 占用端口 3306)
  • 动态/ 私有端口:49152~65535。

(3)InetAddress 类

InetAddress 类:此类的一个对象就代表着一个具体的 IP 地址,拥有两个子类:Inet4Address、Inet6Address。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// InetAddress类没有提供公共的构造器,而是提供了如下几个静态方法来获取 InetAddress 实例
public static InetAddress getLocalHost()// 返回本地主机。
public static InetAddress getByName(String host)// 在给定主机名的情况下确定主机的 IP 地址。
// InetAddress类提供了如下几个常用的方法
public String getHostAddress() :返回IP地址字符串(以文本表现形式)。
public String getHostName() :获取此IP地址的主机名
public boolean isReachable(int timeout): 测试是否可以达到该地址
// 测试
public class Main {
public static void main(String[] args) throws UnknownHostException {
InetAddress localHost = InetAddress.getLocalHost();
// 返回IP地址字符串(以文本表现形式)
System.out.println(localHost.getHostAddress());
// 获取此IP地址的主机名
System.out.println(localHost.getHostName());
}
}

2. 网络协议

(1)TCP 协议和 UDP 协议

java.net 包中提供了两种常见的网络协议的支持:

  • UDP:用户数据报协议 (User Datagram Protocol)。UDP 是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。

    由于使用 UDP 协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用 UDP 协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

    但是在使用 UDP 协议传送数据时,由于 UDP 的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用 UDP 协议。

    特点:

    • 数据被限制在 64kb 以内,超出这个范围就不能发送了。
    • 可以广播发送
    • 发送数据结束时无需释放资源,开销小,速度快
    • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
  • TCP:传输控制协议 (Transmission Control Protocol)。TCP 协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在 TCP 连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。

    特点:

    • 在连接中可进行大数据量的传输
    • 传输完毕,需释放已建立的连接,效率低
    • TCP 协议进行通信的两个应用进程:客户端、服务端。

(2)TCP 通信

TCP 通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。

两端通信时步骤:

  1. 服务端程序,需要事先启动,等待客户端的连接。
  2. 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。

在Java中,提供了两个类用于实现TCP通信程序:

  1. 客户端:java.net.Socket 类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
  2. 服务端:java.net.ServerSocket 类表示。创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接

(3)Socket 类

Socket 类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
构造方法
public Socket(String host, int port):创建套接字对象并将其连接到指定主机上的指定端口号。
public Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定IP地址的指定端口号
成员方法
public InputStream getInputStream() :返回此套接字的输入流。可以用于接收网络消息
public OutputStream getOutputStream() : 返回此套接字的输出流。可以用于发送网络消息
public void close() :关闭此套接字。一旦一个socket被关闭,它不可再使用。关闭此socket也将关闭相关的InputStream和 OutputStream。
public void shutdownOutput() : 禁用此套接字的输出流。任何先前写出的数据将被发送,随后终止输出流。
注意:
① 网络通信其实就是Socket间的通信。
② Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
*/

(4)ServerSocket类

ServerSocket类:这个类实现了服务器套接字,该对象等待通过网络的请求。

1
2
3
4
5
6
/*
构造方法:
public ServerSocket(int port) :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。
成员方法:
public Socket accept() :侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。
*/

(5)简单的TCP通信实例

客户端 Socket 的工作过程包含以下四个基本的步骤 :

  • 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
  • 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输。
  • 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程。
  • 关闭 Socket:断开客户端到服务器的连接,释放线路

服务器程序的工作过程包含以下四个基本的步骤:

  • 调用 ServerSocket(int port) : 创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。
  • 调用 accept(): 监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
  • 调用该 Socket 类对象的 getOutputStream() 和 和 getInputStream (): 获取输出流和输入流,开始网络数据的发送和接收。
  • 关闭 ServerSocket 和 Socket 对象:客户端访问结束,关闭通信套接字。

TCP 通信分析流程

  1. 【服务端】启动,创建 ServerSocket 对象,等待连接。

  2. 【客户端】启动,创建 Socket 对象,请求连接。

  3. 【服务端】接收连接,调用 accept 方法,并返回一个 Socket 对象。

  4. 【客户端】Socket 对象,获取 OutputStream,向服务端写出数据。

  5. 【服务端】Scoket 对象,获取 InputStream,读取客户端发送的数据。

  6. 【服务端】Socket对象,获取OutputStream,向客户端回写数据。

  7. 【客户端】Scoket对象,获取InputStream,解析回写数据。

  8. 【客户端】释放资源,断开连接。

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
public class Server {
public static void main(String[] args) throws IOException {
// 【服务端】启动,创建ServerSocket对象,等待连接。
ServerSocket serverSocket = new ServerSocket(8888);

// 【服务端】接收连接,调用accept方法,并返回一个Socket对象。
Socket accept = serverSocket.accept();

// 【服务端】Scoket对象,获取InputStream,读取客户端发送的数据。
InputStream inputStream = accept.getInputStream();
byte[] bytes = new byte[1024];
int len;
while ((len = inputStream.read(bytes)) != -1) {
System.out.print(new String(bytes,0,len));//这是客户端向服务器发送的内容
}

//*************************************************************
// 【服务端】Socket对象,获取OutputStream,向客户端回写数据。
OutputStream outputStream = accept.getOutputStream();
outputStream.write("这是服务器回写的内容".getBytes());

// 关闭资源
accept.close();
inputStream.close();
outputStream.close();
}
}

public class Client {
public static void main(String[] args) throws IOException {
// 【客户端】启动,创建Socket对象,请求连接。
Socket socket = new Socket("localhost", 8888);

// 【客户端】Socket对象,获取OutputStream,向服务端写出数据。
OutputStream outputStream = socket.getOutputStream();
outputStream.write("这是客户端向服务器发送的内容".getBytes());

// 关闭数据的输出[如果不关闭输出,那么客户机收不到服务器发送的消息,因为服务器仍然继续等待客户机发送内容]
socket.shutdownOutput();

//*************************************************************
// 【客户端】Scoket对象,获取InputStream,解析回写数据。
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int len;
while ((len = inputStream.read(bytes)) != -1) {
System.out.print(new String(bytes,0,len));//这是服务器回写的内容
}

// 【客户端】释放资源,断开连接
socket.close();
outputStream.close();
inputStream.close();
}
}

十四. 反射

Java 的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为 Java 语言的反射机制。反射被视为动态语言的关键。

1. Class 类的理解

类的加载过程:

程序经过 javac.exe 命令以后,会生成一个或多个字节码文件(.class结尾)。接着我们使用 java.exe 命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类,就作为 Class 的一个实例。换句话说,Class 的实例就对应着一个运行时类。加载到内存中的运行时类,会缓存一定的时间。在此时间之内,我们可以通过不同的方式来获取此运行时类。

2. 获取反射中的 Class 对象

在 Java API 中,获取 Class 类对象有三种方法:

  • 方法一:使用 Class.forName 静态方法:若已知一个类的全路径名(包名.类名),可通过 Class 类的静态方法forName() 获取,可能抛出异常。例如:Class clz = Class.forName(“java.lang.String”);

    将字节码文件加载进内存,返回Class对象。多用于配置文件,将类名定义在配置文件中。读取文件,加载类。

  • 方法二:使用 .class 方法。若已知具体的类,通过类的 class 属性获取,该方法最为安全可靠,程序性能最高。例如:Class clz = String.class;

    多用于参数的传递

  • 方法三:使用类对象的 getClass() 方法。若已知某个类的实例,调用该实例的 getClass() 方法获取 Class 对象。
    例如:String str = new String(“Hello”); Class clz = str.getClass();

    getClass() 方法在 Object 类中定义着。多用于对象的获取字节码的方式

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
public class Student {
private String name;
public int age;

public String getName() {
return name;
}

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

private void study(){
System.out.println("学生在学习");
}

public void sleep(){
System.out.println("学生在睡觉");
}


public Student() {
}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}


public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Student student = new Student("张三",18);
// 方法一:使用 Class.forName 静态方法。
Class class1 = Class.forName("reflectiontest.Student");
// 方法二:使用.class方法
Class class2 = Student.class;
// 方法三:使用类对象的 getClass()方法
Class class3 = student.getClass();
}
}

3. 创建运行时类的对象

通过反射创建类对象主要有两种方法:

  • 方法一:通过 Class 对象的 newInstance() 方法。内部调用了运行时类的空参的构造器。要求:
    ① 运行时类必须提供空参的构造器
    ② 空参的构造器的访问权限得够。通常,设置为 public。

  • 方法二:通过 Constructor 对象的 newInstance() 方法。通过 Constructor 对象创建类对象可以选择特定构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) throws Exception {
Student student = new Student("张三", 18);
Class clazz = Student.class;

// 方法一:通过 Class 对象的 newInstance() 方法。
Object o = clazz.newInstance();
System.out.println(o);// Student{name='null', age=0}

// 方法二:通过 Constructor 对象的 newInstance() 方法
Constructor declaredConstructor1 = clazz.getDeclaredConstructor();
Constructor declaredConstructor2 = clazz.getDeclaredConstructor(String.class, int.class);

Object o1 = declaredConstructor1.newInstance();
Object o2 = declaredConstructor2.newInstance("张三",18);
System.out.println(o1);// Student{name='null', age=0}
System.out.println(o2);// Student{name='张三', age=18}
}
}

4. 获取类属性、方法、构造器

(1)获取类属性

  • 获取类的全部属性

    使用 Class 对象的 public Field[] getFields() 方法可以获取 Class 类的所有 public 属性,无法获取私有属性。

    使用 Class 对象的 public Field[] getDeclaredFields() 方法则可以获取包括私有属性在内的所有属性。

  • 获取类的指定属性

    public Field getField(String name) 返回此 Class 对象表示的类或接口的指定的 public 的 Field。

    public Field getDeclaredField(String name) 返回此 Class 对象表示的类或接口的指定的 Field。

    在反射机制中,可以直接通过 Field 类操作类中的属性,通过 Field 类提供的 set() 和 get() 方法就可以完成设置和取得属性内容的操作。
    public Object get (Object obj) 取得指定对象 obj 上此 Field 的属性内容
    public void set (Object obj,Object value) 设置指定对象 obj 上此 Field 的属性内容。

    ★注意:

    对于私有属性,使用 get,set 时,先使用 Filed对象.setAccessible(true); 以保证当前属性是可访问的,如果不设置,则不能访问私有属性。

    • Method 和 Field、Constructor 对象都有 setAccessible() 方法。
    • setAccessible 启动和禁用访问安全检查的开关。
    • 参数值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
    • 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为 true。使得原本无法访问的私有成员也可以访问
    • 参数值为false则指示反射的对象应该实施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
    public class Main {
    public static void main(String[] args) throws Exception {
    Class clazz = Student.class;
    Student student = (Student) clazz.newInstance();

    // 获取类的全部公有属性
    System.out.println("获取类的全部公有属性");
    Field[] fields = clazz.getFields();
    for(Field f :fields){
    System.out.println(f);// public int reflectiontest.Student.age
    }

    // 获取类的全部属性
    System.out.println("获取类的全部属性");
    Field[] declaredFields = clazz.getDeclaredFields();
    for(Field f:declaredFields){
    System.out.println(f);// private java.lang.String reflectiontest.Student.name
    // public int reflectiontest.Student.age
    }

    // 获取类的指定公有属性
    System.out.println("获取类的指定公有属性");
    Field age = clazz.getField("age");
    System.out.println(age);// public int reflectiontest.Student.age
    age.set(student,18);
    System.out.println(age.get(student));// 18

    // 获取类的指定任意属性
    System.out.println("获取类的指定任意属性");
    Field name = clazz.getDeclaredField("name");
    System.out.println(name);// private java.lang.String reflectiontest.Student.name
    name.setAccessible(true);// 暴力反射,保证当前属性是可访问的,如果不设置,则不能访问私有属性★
    name.set(student,"张三");
    System.out.println(name.get(student));// 张三
    }
    }

(2)获取类方法

  • 获取类的全部方法

    public Method[] getDeclaredMethods() 返回此 Class 对象所表示的类或接口的全部方法。

    public Method[] getMethods() 返回此 Class 对象所表示的类或接口的 public 的方法(包含其父类和父接口的 public 方法)。

  • 获取类的指定方法

    通过反射,调用类中的方法,通过 Method 类完成。步骤:

    • 使用public Method getMethod(String name,Class…parameterTypes) 返回此 Class 对象表示的类或接口的指定的 public 的 Method。参数1 :指明获取的方法的名称 参数2:指明获取的方法的形参列表

      或使用public Method getDeclaredMethod(String name,Class…parameterTypes) 返回此 Class 对象表示的类或接口的指定的 Method。

    • 之后使用 Object invoke (Object obj, Object[] args) 进行调用,并向方法中传递要设置的 obj对象的参数信息

      说明:
      ① Object 对应原方法的返回值,若原方法无返回值,此时返回 null
      ② 若原方法若为静态方法,此时形参 Object obj 可为 null
      ③ 若原方法形参列表为空,则Object[] args为null
      若原方法声明为 private,则需要在调用此 invoke() 方法前,显式调用方法对象的 setAccessible(true)方法,才可访问。

    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
    public class Main {
    public static void main(String[] args) throws Exception {
    Class clazz = Student.class;
    Student student = (Student) clazz.newInstance();

    // 获取类的全部公有方法
    System.out.println("获取类的全部公有方法");
    Method[] methods = clazz.getMethods();
    for (Method m : methods) {
    System.out.println(m);//
    }

    // 获取类的全部方法
    System.out.println("获取类的全部方法");
    Method[] declaredMethods = clazz.getDeclaredMethods();
    for (Method m : declaredMethods) {
    System.out.println(m);
    }

    // 获取类的指定公有方法
    System.out.println("获取类的指定公有方法");
    Method sleep = clazz.getMethod("sleep");
    System.out.println(sleep);// public void reflectiontest.Student.sleep()
    sleep.invoke(student);// 学生在睡觉

    // 获取类的指定任意方法
    System.out.println("获取类的指定任意方法");
    Method study = clazz.getDeclaredMethod("study");
    System.out.println(study);// private void reflectiontest.Student.study()
    study.setAccessible(true);// 保证当前方法是可访问的,如果不设置,则不能访问私有方法★
    study.invoke(student);// 学生在学习
    }
    }

(3)获取类构造器

  • 获取类的全部构造器

    public Constructor<T>[] getConstructors() 返回此 Class 对象所表示的类的所有 public 构造方法。

    public Constructor<T>[] getDeclaredConstructors() 返回此 Class 对象表示的类声明的所有构造方法。

  • 获取类的指定构造器

    public Constructor<T> getConstructor(Class<?>... parameterTypes) 返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。参数指明构造器的参数列表

    public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 返回一个 Constructor 对象,该对象反映此 Class 对象所表示的类或接口的指定构造方法。

  • 调用构造方法:

    Constructor–>newInstance(Object… initargs)

    newInstance是 Constructor类的方法(管理构造函数的类)

    api 的解释为:newInstance(Object… initargs) ,使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。

    它的返回值是 T 类型,所以 newInstance 是创建了一个构造方法的声明类的新实例对象,并为之调用。

    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
    public class Main {
    public static void main(String[] args) throws Exception {
    Class clazz = Student.class;
    Student student = (Student) clazz.newInstance();

    // 获取类的全部公有构造器
    System.out.println("获取类的全部公有构造器");
    Constructor[] constructors = clazz.getConstructors();
    for (Constructor c : constructors) {
    System.out.println(c);//
    }

    // 获取类的全部构造器
    System.out.println("获取类的全部构造器");
    Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
    for (Constructor c : declaredConstructors) {
    System.out.println(c);
    }

    // 获取类的指定公有构造器
    System.out.println("获取类的指定公有构造器");
    Constructor constructor = clazz.getConstructor(String.class,int.class);
    System.out.println(constructor);// public reflectiontest.Student(java.lang.String,int)
    Object o = constructor.newInstance("张三",18);
    System.out.println(o);// Student{name='张三', age=18}

    // 获取类的指定任意构造器
    System.out.println("获取类的指定任意构造器");
    Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class, int.class);
    System.out.println(declaredConstructor);// public reflectiontest.Student(java.lang.String,int)
    // declaredConstructor.setAccessible(true);// 保证当前方法是可访问的,如果不设置,则不能访问私有构造器★
    Object o1 = declaredConstructor.newInstance("李四",20);
    System.out.println(o1);// Student{name='李四', age=20}
    }
    }
-------本 文 结 束 感 谢 您 的 阅 读-------