Java笔录


一、Java入门基础

1):编程规范

  • 首先要求程序中的各个要素都遵守命名规则,然后在编码中严格按照编码格式编写代码。命名规则包括以下几点:
    • 包的名称由一个小写字母序列组成。
    • 类的名称由大写字母开头,其他字母都由小写的单词组成。
    • 类的实例的名称由一个小写字母开头,后面的单词由大写字母开头。
    • 常量的名称都大写,并且指出完整含义。
    • 参数的名称无其他具体规定。
    • 数组的命名使用“类型[] 数组名”的形式。
  • 另外,编码格式规定如下:
    • 程序最开始编写导入包和类语句,即 import 语句。import 语句可以有多行,编写完 import 语句后空一行。
    • 定义 public 类,顶格书写。类的主体左括号“{”不换行书写,右括号“}”顶格书写。
    • 定义 public 类中的变量,缩进书写。
    • 定义方法用缩进书写,方法的左括号“{”不换行书写,右括号“}”和方法首行第一个字符对齐。方法体要再次缩进书写,最后一个变量定义和第一个方法定义之间、方法和方法之间最好空一行。

2):程序的运行过程

  • Java 程序的运行必须经过编写、编译和运行 3 个步骤:

    1. 编写:是指在 Java 开发环境中进行程序代码的输入,最终形成后缀名为 .java 的 Java 源文件。
    2. 编译:是指使用 Java 编译器对源文件进行错误排査的过程,编译后将生成后缀名为 .class 的字节码文件,不像C语言那样生成可执行文件。
    3. 运行:是指使用 Java 解释器将字节码文件翻译成机器代码,执行并显示结果。

二、Java程序设计基础

1):标识符和关键字

1:标识符

  • Java 中标识符是为方法、变量或其他用户定义项所定义的名称。标识符可以有一个或多个字符。在 Java 语言中,标识符的构成规则如下:

    • 标识符由数字(09)和字母(AZ 和 a~z)、美元符号($)、下划线(_)以及 Unicode 字符集中符号大于 0xC0 的所有符号组合构成(各符号之间没有空格)。
    • 标识符的第一个符号为字母、下划线和美元符号,后面可以是任何字母、数字、美元符号或下划线。
  • 另外,Java 区分大小写,因此 myvar 和 MyVar 是两个不同的标识符:

    标识符命名时,切记不能以数字开头,也不能使用任何 Java 关键字作为标识符,而且不能赋予标识符任何标准的方法名

  • 标识符分为两类,分别为关键字和用户自定义标识符:

    1. 关键字是有特殊含义的标识符,如 true、false 表示逻辑的真假。
    2. 用户自定义标识符是由用户按标识符构成规则生成的非保留字的标识符,如 abc 就是一个标识符。

    使用标识符时一定要注意,或者使用关键字,或者使用自定义的非关键字标识符。此外,标识符可以包含关键字,但不能与关键字重名

    标识符用来命名常量、变量、类和类的对象等

2:关键字

  • Java 语言目前定义了 51 个关键字,这些关键字不能作为变量名、类名和方法名来使用。以下对这些关键字进行了分类:
    1. 数据类型:boolean、int、long、short、byte、float、double、char、class、interface。
    2. 流程控制:if、else、do、while、for、switch、case、default、break、continue、return、try、catch、finally。
    3. 修饰符:public、protected、private、final、void、static、strict、abstract、transient、synchronized、volatile、native。
    4. 动作:package、import、throw、throws、extends、implements、this、supper、instanceof、new。
    5. 保留字:true、false、null、goto、const。

2):数据类型

1:基本数据类型

  • 定义:字节型(byte)、短整形(short)、整型(int)、长整型(long)、单精度浮点型(float)、双精度浮点型(double)、字节型(char)、布尔型(boolean)

    Java的基本数据类型
    类型名称 关键字 占用内存 取值范围
    字节型 byte 1 字节 -128~127
    短整型 short 2 字节 -32768~32767
    整型 int 4 字节 -2147483648~2147483647
    长整型 long 8 字节 -9223372036854775808L~9223372036854775807L
    单精度浮点型 float 4 字节 +/-3.4E+38F(6~7 个有效位)
    双精度浮点型 double 8 字节 +/-1.8E+308 (15 个有效位)
    字符型 char 2 字节 ISO 单一字符集
    布尔型 boolean 1 字节 true 或 false

💨:整数类型
  • 定义:字节型 短整型 整型 长整型

    Java整数类型变量说明
    名称 说明
    字节型(byte) byte 类型是最小的整数类型。当用户从网络或文件中处理数据流时,或者处理可能与 Java 的其他内置类型不直接兼容的未加工的二进制数据时,该类型非常有用。
    短整型(short) short 类型限制数据的存储为先高字节,后低字节,这样在某些机器中会出错,因此该类型很少被使用。
    整型(int) int 类型是最常使用的一种整数类型。
    长整型(long) 对于大型程序常会遇到很大的整数,当超出 int 类型所表示的范围时就要使用 long 类型。
    public class DemoTest1 {
      public static void main(String[] args) {
        // 声明一个byte类型的变量并赋予初始值为20
        byte a = 20;
        // 声明一个short类型的变量并赋予初始值为10
        short b = 10;
        // 声明一个int类型的变量并赋予初始值为30
        int c = 30;
        // 声明一个long类型的变量并赋予初始值为40
        long d = 40;
        long sum = a + b + c + d;
        System.out.println("20+10+30+40=" + sum);
      }
    }
💨:浮点类型
  • 定义:浮点类型是带有小数部分的数据类型,也叫实型。浮点型数据包括单精度浮点型(float)和双精度浮点型(double),代表有小数精度要求的数字

  • 单精度浮点型(float)和双精度浮点型(double)之间的区别主要是所占用的内存大小不同,float 类型占用 4 字节的内存空间,double 类型占用 8 字节的内存空间。双精度类型 double 比单精度类型 float 具有更高的精度和更大的表示范围。

    float price = 12.2f; // 定义float类型并赋予初值
    double price = 12.254d; // 定义double类型的变量并赋予初值
    double price = 12.254; // 定义double类型的变量并赋予初值

    一个值要能被真正看作 float,它必须以 f(或 F)后缓结束;否则,会被当作 double 值。对 double 值来说,d(或 D)后缓是可选的

    public class DemoTest2 {
      public static void main(String[] args) {
        // 定义 double 类型的变量,用于存储单程距离
        double lutu = 2348.4;
        // 定义 int 类型的变量,用于存储次数
        int num = 2;
        // 定义 float 类型的变量,用于存储总距离
        float total = (float) (lutu * 2);
        System.out.println("往返 AB 两地共需要行驶:" + total + " 米");
      }
    }
💨:布尔类型
  • 布尔类型(boolean)用于对两个数值通过逻辑运算,判断结果是“真”还是“假”。Java 中用保留字 true 和 false 来代表逻辑运算中的“真”和“假”。因此,一个 boolean 类型的变量或表达式只能是取 true 和 false 这两个值中的一个

  • 在 Java 语言中,布尔类型的值不能转换成任何数据类型,true 常量不等于 1,而 false 常量也不等于 0。这两个值只能赋给声明为 boolean 类型的变量,或者用于布尔运算表达式中。

    true false
    boolean isable;
    boolean b=false
💨:字符类型
  • Java 语言中的字符类型(char)使用两个字节的 Unicode 编码表示,它支持世界上所有语言,可以使用单引号字符或者整数对 char 型赋值。

    char letter = 'D';
    char numChar = '5';
    public class DemoTest3 {
      public static void main(String[] args) {
        // 向 char 类型的 a 变量赋值为 A,所对应的 ASCII 值为 65
        char a = 'A';
        // 向 char 类型的 b 变量赋值为 B,所对应的 ASCII 值为 66
        char b = 'B';
        System.out.println("A 的 ASCII 值与 B 的 ASCII 值相加结果为:" + (a + b));
      }
    }

2:引用数据类型

  • 引用数据类型建立在基本数据类型的基础上,包括数组、类和接口。引用数据类型是由用户自定义,用来限制其他数据的类型。另外,Java 语言中不支持 C++ 中的指针类型、结构类型、联合类型和枚举类型。

  • 引用类型还有一种特殊的 null 类型。所谓引用数据类型就是对一个对象的引用,对象包括实例和数组两种。实际上,引用类型变量就是一个指针,只是 Java 语言里不再使用指针这个说法。

  • 空类型(null type)就是 null 值的类型,这种类型没有名称。因为 null 类型没有名称,所以不可能声明一个 null 类型的变量或者转换到 null 类型。

  • 空引用(null)是 null 类型变量唯一的值。空引用(null)可以转换为任何引用类型。

  • 在实际开发中,程序员可以忽略 null 类型,假定 null 只是引用类型的一个特殊直接量。

    空引用(null)只能被转换成引用类型,不能转换成基本类型,因此不要把一个 null 值赋给基本数据类型的变量。

3):数据类型转换

1:隐式转换(自动类型转换)

  • 自动转换的规则是从低级类型数据转换成高级类型数据。转换规则如下:

    • 数值型数据的转换:byte→short→int→long→float→double。
    • 字符型转换为整型:char→int。
    public class DemoTest4 {
      public static void main(String[] args) {
        // 定义牙膏的价格
        float price1 = 10.9f;
        // 定义面巾纸的价格
        double price2 = 5.8;
        // 定义牙膏的数量
        int num1 = 2;
        // 定义面巾纸的数量
      int num2 = 4;
        // 计算总价
        double res = price1 * num1 + price2 * num2;
        // 输出总价
        System.out.println("一共付给收银员" + res + "元");
      }
    }

    char 类型比较特殊,char 自动转换成 int、long、float 和 double,但 byte 和 short 不能自动转换为 char,而且 char 也不能自动转换为 byte 或 short

2:显示转换(强制类型转换)

  • 定义:当两种数据类型不兼容,或目标类型的取值范围小于源类型时,自动转换将无法进行,这时就需要进行强制类型转换

    (type) variableName 

    type 为 variableName 要转换成的数据类型,而 variableName 是指要进行类型转换的变量名称

    int a = 3;
    double b = 5.0;
    a = (int)b;
    public class DemoTest5 {
      public static void main(String[] args) {
        float price1 = 10.9f;
        double price2 = 5.8;
        int num1 = 2;
        int num2 = 4;
        int res2 = (int) (price1 * num1 + price2 * num2);
        System.out.println("一共付给收银员" + res2 + "元");
      }
    }

4):算术运算符

1:一元运算符

  • 定义:算术一元运算符(-、++、–)

    一元算术运算
    运 算 符 名 称 说 明 例 子
    - 取反符号 取反运算 b=-a
    ++ 自加一 先取值再加一,或先加一再取值 a++ 或 ++a
    自减一 先取值再减一,或先减一再取值 a– 或 –a
    public class DemoTest6 {
      public static void main(String[] args) {
        int a = 12;
        System.out.println(-a);
        int b = a++;
        System.out.println(b);
        System.out.println(a);
        b = ++a;
        System.out.println(b);
        System.out.println(a);
      }
    }

2:二元运算符

  • 定义:加(+)、减(-)、乘()和除(\)等,取模运算(%)。加(+)、减(-)、乘()、除(\)

    二元运算符
    运 算 符 名 称 说 明 例 子
    + 求 a 加 b 的和,还可用于 String 类型,进行字符串连接操作 a + b
    - 求 a 减 b 的差 a - b
    * 求 a 乘以 b 的积 a * b
    / 求 a 除以 b 的商 a / b
    % 取余 求 a 除以 b 的余数 a % b
  • 算术运算符都是双目运算符,即连接两个操作数的运算符。优先级上,*、/、% 具有相同运算级别,并高于 +、-(+、- 具有相同级别)

    public class DemoTest7 {
      public static void main(String[] args) {
        int a = 4; 
        int b = 2;
        int c = 3;
        int d = a * (b + c) % c;
        System.out.println(d);
      }
    }
    public class DemoTest8 {
      public static void main(String[] args) {
        // 保存取余后浮点类型的结果
        float f1 = 9 % 4;
        // 双精度加法
        double da = 9 + 4.5;
        // 双精度减法
        double db = 9 - 3.0;
        // 双精度乘法
        double dc = 9 * 2.5;
        // 双精度除法
        double dd = 9 / 3.0;
        // 双精度取余
        double de = 9 % 4;
        // 整数的加、减、乘、除和取余
        System.out.println("整数的算术运算");
        System.out.printf("9+4=%d \n", 9 + 4);
        System.out.printf("9-4=%d \n", 9 - 4);
        System.out.printf("9*4=%d \n", 9 * 4);
        System.out.printf("9/4=%d \n", 9 / 4);
        System.out.printf("9%%4=%d \n", 9 % 4);
        // 浮点数的加、减、乘、除和取余
        System.out.println("\n浮点数的算术运算");
        System.out.printf("9+4.5f=%f \n", 9 + 4.5f);
        System.out.printf("9-3.0f=%f \n", 9 - 3.0f);
        System.out.printf("9*2.5f=%f \n", 9 * 2.5f);
        System.out.printf("9/3.0f=%f \n", 9 / 3.0f);
        System.out.printf("9%%4=%f \n", f1);
        // 双精度数的加、减、乘、除和取余
        System.out.println("\n双精度数的算术运算");
        System.out.printf("9+4.5=%4.16f \n", da);
        System.out.printf("9-3.0=%4.16f \n", db);
        System.out.printf("9*2.5=%4.16f \n", dc);
        System.out.printf("9/3.0=%4.16f \n", dd);
        System.out.printf("9%%4=%4.16f \n", de);
        // 对字符的加法和减法
        System.out.println("\n字符的算术运算");
        System.out.printf("'A'+32=%d \n", 'A' + 32);
        System.out.printf("'A'+32=%c \n", 'A' + 32);
        System.out.printf("'a'-'B'=%d \n", 'a' - 'B');
      }
    }

3:算术赋值运算符

  • 定义:算术赋值运算符只是一种简写,一般用于变量自身的变化

    算术赋值运算
    运 算 符 名 称 例 子
    += 加赋值 a += b、a += b+3
    -= 减赋值 a -= b
    *= 乘赋值 a *= b
    /= 除赋值 a /= b
    %= 取余赋值 a %= b
    public class DemoTest9 {
      public static void main(String[] args) {
        int a = 1;
        int b = 2;
        /* 相当于 a = a + b  System.out.println(a); */
        a += b;
        // 相当于 a = a + b + 3
        a += (b + 3);
        System.out.println(a);
        // 相当于 a = a - b
        a -= b;
        System.out.println(a);
        // 相当于 a=a*b
        a *= b;
        System.out.println(a);
        // 相当于 a=a/b
        a /= b;
        System.out.println(a);
        // 相当于 a=a%b
        a %= b;
        System.out.println(a);
      }
    }

5):赋值运算符

  • 定义:赋值运算符是指为变量或常量指定数值的符号。赋值运算符的符号为“=”,它是双目运算符,左边的操作数必须是变量,不能是常量或表达式。

    变量名称=表达式内容
  • “变量名称”和“表达式”内容的类型必须匹配,如果类型不匹配则需要自动转化为对应的类型。不要将赋值运算符与相等运算符“==”混淆。

    赋值运算符和算数运算符组成的复合赋值运算的含义及其使用实例
    运算符 含义 实例 结果
    += 将该运算符左边的数值加上右边的数值, 其结果赋值给左边变量本身 int a=5; a+=2; a=7
    -= 将该运算符左边的数值减去右边的数值, 其结果赋值给左边变量本身 int a=5; a-=2; a=3
    *= 将该运算符左边的数值乘以右边的数值, 其结果赋值给左边变量本身 int a=5; a*=2; a=10
    /= 将该运算符左边的数值整除右边的数值, 其结果赋值给左边变量本身 int a=5; a/=2; a=2
    %= 将该运算符左边的数值除以右边的数值后取余,其结果赋值给左边变量本身 int a=5; a%=2; a=1
    public class DemoTest10 {
      public static void main(String[] args) {
        // 定义3个整型的变量
        int x, y, z;
        // 为变量赋初值为5
        x = y = z = 5;
        // 等价于x=x+10,结果x=15
        x += 10;
        // 等价于y=y-3,结果y=2
        y -= 3;
        // 等价于z=z*5,结果z=25
        z *= 5;
        // 等价于x=x/4,结果x=3
        x /= 4;
        // 等价于z=z%x,结果z=1
        z %= x;
      }
    }

6):逻辑运算符

  • 定义:逻辑运算符是对布尔型变量进行运算,其结果也是布尔型

    逻辑运算符的用法、含义及实例
    运算符 用法 含义 说明 实例 结果
    && a&&b 短路与 ab 全为 true 时,计算结果为 true,否则为 false。 2>1&&3<4 true
    \ a\ b 短路或 ab 全为 false 时,计算结果为 false,否则为 true。
    ! !a 逻辑非 a 为 true 时,值为 false,a 为 false 时,值为 true !(2>4) true
    \ a\ b 逻辑或 ab 全为 false 时,计算结果为 false,否则为 true
    & a&b 逻辑与 ab 全为 true 时,计算结果为 true,否则为 false 1<2&3<5 true
  • && 与 & 区别:如果 a 为 false,则不计算 b(因为不论 b 为何值,结果都为 false)

  • || 与 | 区别:如果 a 为 true,则不计算 b(因为不论 b 为何值,结果都为 true)

    短路与(&&)和短路或(||)能够采用最优化的计算方式,从而提高效率。在实际编程时,应该优先考虑使用短路与和短路或。

    用逻辑运算符进行逻辑运算
    a b a&&b a|b !a
    true true true true false
    false true false true true
    true false false true false
    false false false false true

    逻辑运算符的优先级为:!运算级别最高,&& 运算高于 || 运算。!运算符的优先级高于算术运算符,而 && 和 || 运算则低于关系运算符。结合方向是:逻辑非(单目运算符)具有右结合性,逻辑与和逻辑或(双目运算符)具有左结合性。

7):关系运算符

  • 定义:关系运算符是二元运算符,运算结果是 boolean 型。当运算符对应的关系成立时,运算结果是 true,否则是 false。

    比较运算符的含义及其实例应用
    运算符 含义 说明 实例 结果
    > 大于运算符 只支持左右两边操作数是数值类型。如果前面变量的值大于后面变量的值, 则返回 true。 2>3 false
    >= 大于或等于运算符 只支持左右两边操作数是数值类型。如果前面变量的值大于等于后面变量的值, 则返回 true。 4>=2 true
    < 小于运算符 只支持左右两边操作数是数值类型。如果前面变量的值小于后面变量的值,则返回 true。 2<3 true
    <= 小于或等于运算符 只支持左右两边操作数是数值类型。如果前面变量的值小于等于后面变量的值, 则返回 true。 4<=2 false
    == 相等运算符 如果进行比较的两个操作数都是数值类型,无论它们的数据类型是否相同,只要它们的值相等,也都将返回 true。
    如果两个操作数都是引用类型,只有当两个引用变量的类型具有父子关系时才可以比较,只要两个引用指向的不是同一个对象就会返回 true。
    Java 也支持两个 boolean 类型的值进行比较。
    4==4
    97==’a’
    5.0==5
    true==false
    true
    true
    true
    false
    != 不相等运算符 如果进行比较的两个操作数都是数值类型,无论它们的数据类型是否相同,只要它们的值不相等,也都将返回 true。
    如果两个操作数都是引用类型,只有当两个引用变量的类型具有父子关系时才可以比较,只要两个引用指向的不是同一个对象就会返回 true。
    4!=2 true
  • 注意点如下所示:

    1. 基本类型的变量、值不能和引用类型的变量、值使用 == 进行比较;boolean 类型的变量、值不能与其他任意类型的变量、值使用 == 进行比较;如果两个引用类型之间没有父子继承关系,那么它们的变量也不能使用 == 进行比较。
    2. == 和 != 可以应用于基本数据类型和引用类型。当用于引用类型比较时,比较的是两个引用是否指向同一个对象,但当时实际开发过程多数情况下,只是比较对象的内容是否相当,不需要比较是否为同一个对象。
  • 关系运算符的优先级为:>、<、>=、<= 具有相同的优先级,并且高于具有相同优先级的 !=、==。关系运算符的优先级高于赋值运算符而低于算术运算符,结合方向是自左向右

  • 关系表达式通常用于 Java 程序的逻辑判断语句的条件表达式中。使用关系表达式要注意以下几点:

    • 运算符 >=、==、!=、<= 是两个字符构成的一个运算符,用空格从中分开写就会产生语法错误。例如 x> =y; 是错误的,但是可以写成x >= y; 在运算符的两侧增加空格会提高可读性。同样将运算符写反,例如 =>、=<、=! 等形式会产生语法错误。
    • 由于计算机内存放的实数与实际的实数存在着一定的误差,如果对浮点数进行 ==(相等)或 !=(不相等)的比较,容易产生错误结果,应该尽量避免。
    • 不要将“==”写成“=”。
  • 简单实例

    a > b  // 比较变量a的值是否大于变量b的值
    x+y> = z  // 比较变量x与变量y的和是否大于或等于变量z的值
    width * width+size != area  // 比较变量width的平方加上变量size的值是否与变量area的值不相等
    name == "zhht"  // 比较变量name的值是否等于字符串nzht
    pass != "123456"  // 比较变量pass的值是否不等于字符串“123456”
  • 例子 程序使用户可以从键盘输入两个数,并判断这两个数之间的大小

    public class DemoTest11 {
      public static void main(String[] args) {
        int number1, number2; // 定义变量,保存输入的两个数
        System.out.print("请输入第一个整数(number1):");
        Scanner input = new Scanner(System.in);
        // 输入第一个数
        number1 = input.nextInt();
        System.out.print("请输入第二个整数(number2):");
        input = new Scanner(System.in);
        // 输入第二个数
        number2 = input.nextInt();
        System.out.printf("number1=%d,number2=%d\n", number1, number2);
        // 判断用户输入的两个数是否相等
        if (number1 == number2) {
          System.out.println("number1 和 number2 相等。");
        }
        // 判断用户输入的两个数据是否相等
        if (number1 != number2) {
          System.out.println("number1 和 number2 不相等。");
          // 判断用户输入的数1是否大于数2
          if (number1 > number2) {
            System.out.println("number1 大于 number2。");
          }
          // 判断用户输入的数1是否小于数2
          if (number1 < number2) {
            System.out.println("number1 小于 number2。");
          }
        }
      }
    }

8):Java自增和自减运算符

  • 定义:++ 或 – 是单目运算符,放在操作数的前面或后面都是允许的。++ 与 – 的作用是使变量的值增 1 或减 1。操作数必须是一个整型或浮点型变量

    自增、自减运算的含义及其使用实例
    运算符 含义 实例 结果
    i++ 将 i 的值先使用再加 1 赋值给 i 变量本身 int i=1;
    int j=i++;
    i=2
    j=1
    ++i 将 i 的值先加 1 赋值给变量 i 本身后再使用 int i=1;
    int j=++i;
    i=2
    j=2
    i– 将 i 的值先使用再减 1 赋值给变量 i 本身 int i=1;
    int j=i–;
    i=0
    j=1
    –i 将 i 的值先减 1 后赋值给变量 i 本身再使用 int i=1;
    int j=–i;
    i=0
    j=0
  • 在使用自增/自减运算时应注意下面几个问题。

    • 自增/自减只能作用于变量,不允许对常量、表达式或其他类型的变量进行操作。常见的错误是试图将自增或自减运算符用于非简单变量表达式中。
    • 自增/自减运算可以用于整数类型 byte、short、int、long,浮点类型 float、double,以及字符串类型 char。
    • 在 Java 1.5 以上版本中,自增/自减运算可以用于基本类型对应的包装器类 Byte、Short、Integer、Long、Float、Double 和 Character。
    • 自增/自减运算结果的类型与被运算的变量类型相同。
  • 例子 (编写一个程序,使用不同类型的数据结合自增和自减运算符进行运算,并输出变量的值)

    public class DemoTest13 {
      public static void main(String[] args) {
        // 声明用于自增和自减的整型变量
        int x = 5, y;
        // 声明用于自增和自减的字符型变量
        char cx = 'B', cy;
        // 声明用于自增和自减的浮点型变量
        float fx = 5.5f, fy;
        System.out.println("---------对整数的自增和自减---------");
        y = x++;
        System.out.printf("y=x++ 的结果为:%d ,%d \n", x, y);
        y = x--;
        System.out.printf("y=x-- 的结果为:%d ,%d \n", x, y);
        y = ++x;
        System.out.printf("y=++x 的结果为:%d ,%d \n", x, y);
        y = --x;
        System.out.printf("y=--x 的结果为:%d ,%d \n", x, y);
        System.out.println("\n---------对浮点的自增和自减---------");
        fy = fx++;
        System.out.printf("fy=fx++ 的结果为:%f ,%f \n", fx, fy);
        fy = fx--;
        System.out.printf("fy=fx-- 的结果为:%f ,%f \n", fx, fy);
        fy = ++fx;
        System.out.printf("fy=++fx 的结果为:%f ,%f \n", fx, fy);
        fy = --fx;
        System.out.printf("fy=--fx 的结果为:%f ,%f \n", fx, fy);
        System.out.println("\n---------对字符的自增和自减---------");
        cy = cx++;
        System.out.printf("cy=cx++ 的结果为:%c ,%c \n", cx, cy);
        cy = cx--;
        System.out.printf("cy=cx-- 的结果为:%c ,%c \n", cx, cy);
        cy = ++cx;
        System.out.printf("cy=++cx 的结果为:%c ,%c \n", cx, cy);
        cy = --cx;
        System.out.printf("cy=--cx 的结果为:%c ,%c \n", cx, cy);
      }
    }

9):Java位运算符

1:位逻辑运算符

  • 定义:位逻辑运算符包含 4 个:&(与)、|(或)、~(非)和 ^(异或)。除了 ~(即位取反)为单目运算符外,其余都为双目运算符

    位逻辑运算符
    运算符 含义 实例 结果
    & 按位进行与运算(AND) 4 & 5 4
    | 按位进行或运算(OR) 4 | 5 5
    ^ 按位进行异或运算(XOR) 4 ^ 5 1
    ~ 按位进行取反运算(NOT) ~ 4 -5
💨:位与运算符
  • 位与运算符为&,其运算规则是:参与运算的数字,低位对齐,高位不足的补零,如果对应的二进制位同时为 1,那么计算结果才为 1,否则为 0。因此,任何数与 0 进行按位与运算,其结果都为 0。

    100&0

    int x = 5,y = 12; // 创建整型变量保存两个数
    int z = x&y; // 对这两个数进行位与运算,结果保存到

💨:位或运算符
  • 定义:位或运算符为|,其运算规则是:参与运算的数字,低位对齐,高位不足的补零。如果对应的二进制位只要有一个为 1,那么结果就为 1;如果对应的二进制位都为 0,结果才为 0。

💨:位异或运算符
  • 定义:位异或运算符为^,其运算规则是:参与运算的数字,低位对齐,高位不足的补零,如果对应的二进制位相同(同时为 0 或同时为 1)时,结果为 0;如果对应的二进制位不相同,结果则为 1。

💨:位取反运算符
  • 定义:位取反运算符为~,其运算规则是:只对一个操作数进行运算,将操作数二进制中的 1 改为 0,0 改为 1。

    public class DemoTest13 {
      public static void main(String[] args) {
        int i = 10;
        System.out.printf("%d \n", ~i);
        System.out.printf("%x \n", ~i);
      }
    }

    位运算符的操作数只能是整型或者字符型数据以及它们的变体,不用于 float、double 或者 long 等复杂的数据类型

2:位移运算符

  • 定义:位移运算符用来将操作数向某个方向(向左或者右)移动指定的二进制位数。表 2 列出了 Java 语言中的两个位移运算符,它们都属于双目运算符。

    位移运算符
    运算符 含义 实例 结果
    » 右移位运算符 8»1 4
    « 左移位运算符 9«2 36
💨:左位移运算符
  • 定义:左移位运算符为«,其运算规则是:按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。

    • 原来数的所有二进制位都向左移动 1 位。原来位于左边的最高位 0 被移出舍弃,再向尾部追加 0 补位。最终到的结果是 22,相当于原来数的 2 倍
💨:右位移运算符
  • 定义:右位移运算符为»,其运算规则是:按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补零。

    • 原来数的所有二进制位都向右移动 1 位。原来位于右边的最低位 1 被移出舍弃,再向最高位追加 0 补位。最终到的结果是 5,相当于原数整除 2 的结果

3:复合位赋值运算符

  • 定义:复合位赋值运算符由赋值运算符与位逻辑运算符和位移运算符组合而成

    复合位赋值运算符
    运算符 含义 实例 结果
    &= 按位与赋值 num1 &= num2 等价于 num 1=num 1 & num2
    |= 按位或赋值 num1 |= num2 等价于 num 1=num 1 | num2
    ^= 按位异或赋值 num1 ^= num2 等价于 num 1=num 1 ^ num2
    -= 按位取反赋值 num1 -= num2 等价于 num 1=num 1 - num2
    «= 按位左移赋值 num1 «= num2 等价于 num 1=num 1 « num2
    »= 按位右移赋值 num1 »= num2 等价于 num 1=num 1 » num2
    public class DemoTest14 {
      public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = 3;
        int z = b & c;
        System.out.println(z);
        a &= 4;
        a |= 4;
        a ^= c;
        a -= 6;
        b >>= 1;
        c <<= 1;
        System.out.println("a = " + a);
        System.out.println("b = " + b);
        System.out.println("c = " + c);
      }
    }

10):Java三目运算符

  • 定义:Java 提供了一个特别的三元运算符(也叫三目运算符)经常用于取代某个类型的 if-then-else 语句。条件运算符的符号表示为“?:”,使用该运算符时需要有三个操作数,因此称其为三目运算符。

    result = <expression> ? <statement1> : <statement3>;

    其中,expression 是一个布尔表达式。当 expression 为真时,执行 statement1, 否则就执行 statement3。此三元运算符要求返回一个结果,因此要实现简单的二分支程序,即可使用该条件运算符。

    public class DemoTest15 {
      public static void main(String[] args) {
        int x, y, z;
        x = 6;
        y = 2;
        z = (x > y) ? (x -= y) : (x += y);
        System.out.println(z);
        System.out.print("请输入一个数:");
        Scanner input = new Scanner(System.in);
        // 由用户输入x的值
        x = input.nextInt();
        // 判断x的值是否大于5,如果是y=x,否则y=-x
        y = x > 5 ? x : -x;
        // 判断y的值是否大于x,如果是z=y,否则z=5
        z = y > x ? y : 5;
        System.out.printf("x=%d \n", x);
        System.out.printf("y=%d \n", y);
        System.out.printf("z=%d \n", z);
      }
    }

11):Java运算符优先级

  • 定义:一般而言,单目运算符优先级较高,赋值运算符优先级较低。算术运算符优先级较高,关系和逻辑运算符优先级较低。多数运算符具有左结合性,单目运算符、三目运算符、赋值运算符具有右结合性

    运算符的优先级
    优先级 运算符 结合性
    1 ()、[]、{} 从左向右
    2 !、+、-、~、++、– 从右向左
    3 *、/、% 从左向右
    4 +、- 从左向右
    5 «、»、>>> 从左向右
    6 <、<=、>、>=、instanceof 从左向右
    7 ==、!= 从左向右
    8 & 从左向右
    9 ^ 从左向右
    10 | 从左向右
    11 && 从左向右
    12 | 从左向右
    13 ?: 从右向左
    14 =、+=、-=、*=、/=、&=、|=、^=、~=、«=、»=、>>>= 从右向左
  • 使用优先级为 1 的小括号可以改变其他运算符的优先级

    (x-y)*z/5
    --y || ++x && ++z;
  • 这个表达式中包含了算术运算符和逻辑运算符。根据表 1 中列出的优先级,可以确定它的执行顺序如下:

    1. 先计算 y 的自减运算符,即 –y。
    2. 再计算 x 的自增运算符,即 ++x。
    3. 接着计算 z 的自增运算符,即 ++z。
    4. 由于逻辑与比逻辑或的优先级高,这里将 ② 和 ③ 的结果进行逻辑与运算,即 ++x && ++z。
    5. 最后将 ④ 的结果与 ① 进行逻辑或运算,即 –y||++x&&++z
    public class DemoTest16 {
      public static void main(String[] args) {
        int a = 5;
        int b = 4;
        int c = a++ - --b * ++a / b-- >> 2 % a--;
        int s = a++;
        int z = b--;
        System.out.println(z);
        System.out.println(s);
        System.out.println(c);
      }
    }

12):Java直接量

1:直接量的类型

💨:int 类型的直接量
  • 在程序中直接给出的整型数值,可分为二进制、十进制、八进制和十六进制 4 种,其中二进制需要以 0B 或 0b 开头,八进制需要以 0 开头,十六进制需要以 0x 或 0X 开头。例如 123、012(对应十进制的 10)、0x12(对应十进制的 18)等。
💨:long 类型的直接量
  • 在整型数值后添加 l 或 L 后就变成了 long 类型的直接量。例如 3L、0x12L(对应十进制的 18L)。
💨:float 类型的直接量
  • 在一个浮点数后添加 f 或 F 就变成了 float 类型的直接量,这个浮点数可以是标准小数形式,也可以是科学计数法形式。例如 5.34F、3.14E5f。
💨:double 类型的直接量
  • 直接给出一个标准小数形式或者科学计数法形式的浮点数就是 double 类型的直接量。例如 5.34、3.14E5。
💨:boolean 类型的直接量
  • 这个类型的直接量只有 true 和 false。
💨:char 类型的直接量
  • char 类型的直接量有三种形式,分别是用单引号括起来的字符、转义字符和 Unicode 值表示的字符。例如‘a’,‘\n’和‘\u0061’。
💨:String 类型的直接量
  • 一个用双引号括起来的字符序列就是 String 类型的直接量。
💨:null 类型的直接量
  • 这个类型的直接量只有一个值,即 null。

在上面的 8 种类型的直接量中,null 类型是一种特殊类型,它只有一个值:null。而且这个直接量可以赋给任何引用类型的变量,用以表示这个引用类型变量中保存的地址为空,即还未指向任何有效对象

2:直接量的赋值

  • 通常总是把一个直接量赋值给对应类型的变量,如👇

    int a = 5;
    char c = 'a';
    boolean b = true;
    float f = 5.12f;
    double d = 4.12;
    String name = "C语言中文网";
    String url = "http://c.biancheng.net";

    由于 String 类是一个典型的不可变类,因此 String 对象创建出来的就不可能改变,因此无需担心共享 String 对象会导致混乱。

    常量池(constant pool)指的是在编译期被确定,并被保存在已编译的 .class 文件中的一些数据,它包括关于类、方法、接口中的常量,也包括字符串直接量

    String s0 = "hello";
    String s1 = "hello";
    String s2 = "he" + "llo";
    System.out.println(s0 == s1);
    System.out.println(s0 == s2);

三、Java流程控制语句

1):Java语句

1:语句的编写方式

  • 定义:在 Java 中,语句是最小的组成单位,每个语句必须使用分号作为结束符

    int a=0,b,c;b=a+10;b++;c=a*b;System.out.println(c);
  • 为了使程序语句排列得更加美观、容易阅读和排除错误,一般使用如下规则格式化源代码。

    • 在一行内只写一个语句,并采用空格、空行来保证语句容易阅读。
    • 在每个复合语句内使用 Tab 键向右缩进。
    • 大括号总是放在单独的一行,便于检查是否匹配。

2:空语句

  • 定义:在程序中,空语句主要用来作为空循环体。

    ;(空语句主要作用作为空循环体)

3:表达式语句

  • 定义:Java 中将赋值作为一个运算符,因此只有赋值表达式

    pi = 3.1415926;
    output(pi); // 将pi的值传递到output()函数中作为参数
    sum = (a+b)/2;
    printf("%f",sum); // 将sum的值传递到printf()函数输出
    temp = x*y*z-y+(20-x); // 将表达式的值保存到temp变量中

4:复合语句

  • 定义:复合语句又称为语句块,是很多个语句的组合,从而可以将多个语句看作单个语句

    statement-list // 语句列表
  • 它的执行规则如下:

    • 如果语句块是空的,控制转到语句块的结束点。
    • 如果语句块不是空的,控制转到语句列表。当控制到达语句列表的结束点时,控制转到语句的结束点。
    width = 10; // 为width变量赋值
    height = 90; // 为height变量赋值
    area = width * height; // 计算width变量和height变量的乘积

2):if else分支结构

1:if结构

  • if 语句是使用最多的条件分支结构,它属于选择语句,也可以称为条件语句。

    if (条件表达式) {
        语句块;
    }

    其中“条件表达式”和“语句块”是比较重要的两个地方。

    1. 条件表达式:条件表达式可以是任意一种逻辑表达式,最后返回的结果必须是一个布尔值。取值可以是一个单纯的布尔变量或常量,也可以是使用关系或布尔运算符的表达式。如果条件为真,那么执行语句块;如果条件为假,则语句块将被绕过而不被执行。

    2. 语句块:该语句块可以是一条语句也可以是多条语句。如果仅有一条语句,可省略条件语句中的大括号 {}。当从编程规范角度不要省略大括号,省略大括号会使程序的可读性变差。

    3. 例1 (用户输入数字比较)

      public class DemoTest17 {
        public static void main(String[] args) {
          System.out.println("请输入一个数字");
          Scanner input = new Scanner(System.in);
          int num = input.nextInt();
          if (num > 100) {
            System.out.println("输入的数字大于100");
          }
          if (num == 100) {
            System.out.println("输入的数字等于100");
          }
          if (num < 100) {
            System.out.println("输入的数字小于100");
          }
        }
      }
    4. 例2 (比较数字)

      public class DemoTest18 {
        public static void main(String[] args) {
          int num1 = 50;
          int num2 = 43;
          if (num1 > num2) {
            System.out.println("num1大于num2");
          }
          if (num1 == num2) {
            System.out.println("num2等于num2");
          }
          if (num1 < num2) {
            System.out.println("num1小于num2");
          }
        }
      }
    5. 例3 (验证用户名和密码)

      public class DemoTest19 {
        public static void main(String[] args) {
          // 用户名
          String username = "admin";
          // 密码
          String userpass = "123456";
          // 验证码
          String code = "0000";
          if (username != "admin" && userpass != "123456" && code != "0000") {
            System.out.println("登录失败!");
            System.out.println("请检查输入的用户名、密码和验证码是否正确!");
          }
        }
      }

2:if-else结构

  • 定义:if 语句是使用最多的条件分支结构,它属于选择语句,也可以称为条件语句。

    if (表达式) {
        语句块1;
    } else {
        语句块2;
    }

    public class DemoTest20 {
      public static void main(String[] args) {
        int num1 = 50;
        int num2 = 34;
        // 如果num1等于num2
        if (num1 == num2) {
          System.out.println("num1等于num2");
        }
        // 如果num1大于num2
        if (num1 > num2) {
          System.out.println("num1大于num2");
        } else {
          // 否则就是num1小于num2
          System.out.println("num1小于num2");
        }
      }
    }

    双条件语句减少了代码的编写量,同时增强了程序的可读性

3:多条件 if-else-if 语句

  • 定义:if 语句的主要功能是给程序提供一个分支。

    if(表达式1) {
        语句块1;
    } else if(表达式2) {
        语句块2;
    ...
    } else if(表达式n) {
        语句块n;
    } else {
        语句块n+1;
    }

    可以看出,else-if 结构实际上是 if-else 结构的多层嵌套。明显的特点就是在多个分支中只执行一个语句组,而其他分支都不执行,所以这种结构可以用于有多种判断结果的分支中。

    public class DemoTest21 {
      public static void main(String[] args) {
        int num1 = 50;
        int num2 = 34;
        // 如果num1等于num2
        if (num1 == num2) {
          System.out.println("num1等于num2");
          // 如果num1大于num2
        } else if (num1 > num2) {
          System.out.println("num1大于num2");
        } else { // 否则就是小于
          System.out.println("num1小于num2");
      }
      }
    }
    public class DemoTest22 {
      public static void main(String[] args) {
        System.out.println("请输入考试成绩:");
        Scanner input = new Scanner(System.in);
        // 接收键盘输入数据
        int score = input.nextInt();
        // 考试成绩>=90
        if (score >= 90) {
          System.out.println("优秀");
          // 90>考试成绩>=80
        } else if (score >= 80) {
          System.out.println("良好");
          // 80>考试成绩>=60
        } else if (score >= 60) {
          System.out.println("中等");
        } else { // 考试成绩<60
          System.out.println("差");
        }
      }
    }

3:嵌套 if 的使用

  • 定义:if 语句的用法非常灵活,不仅可以单独使用,还可以在 if 语句里嵌套另一个 if 语句

    if(表达式1) {
        if(表达式2) {
            语句块1;
        } else {
            语句块2;
        }
    } else {
        if(表达式3) {
            语句块3;
        } else if(表达式4) {
            语句块4;
        } else {
            if(表达式n) {
                语句块n;
            } else {
                语句块n+1;
            }
        }
    }

    public class DemoTest23 {
      public static void main(String[] args) {
        String today = "周末";
        String weather = "晴朗";
        if (today.equals("周末")) {
          if (weather.equals("晴朗")) {
            System.out.println("去室外游乐场游玩");
          } else {
            System.out.println("去室内游乐场游玩");
          }
        } else {
          System.out.println("去上班");
        }
      }
    }

3):switch case语句

  • 定义:switch 语句提供了 if 语句的一个变通形式,可以从多个语句块中选择其中的一个执行。

1:switch 语句格式

  • 定义:switch 语句是 Java 的多路分支语句

    switch(表达式) {
        case1:
            语句块1;
            break;
        case2:
            语句块2;
            break;case 值n:
            语句块n;
            break;
        default:
            语句块n+1;
        break;
    }

    其中,switch、case、default、break 都是 Java 的关键字。

👀:switch
  • 表示“开关”,这个开关就是 switch 关键字后面小括号里的值,小括号里要放一个整型变量或字符型变量。表达式必须为 byte,short,int,char类型。
  • Java7 增强了 switch 语句的功能,允许 switch 语句的控制表达式是 java.lang.String 类型的变量或表达式。只能是 java.lang.String 类型,不能是 StringBuffer 或 StringBuilder 这两种字符串的类型。
👀:case
  • 表示“情况,情形”,case 标签可以是:

    • 类型为 char、byte、 short 或 int 的常量表达式。
    • 枚举常量。
    • 从 Java SE 7 开始, case 标签还可以是字符串字面量。
    String input = ...;
    switch (input.toLowerCase()) {  // toLowerCase用于将大写字符转换为小写
        case "yes":
            ...
            break;
    }

    当在 switch 语句中使用枚举常量时,不必在每个标签中指明枚举名,可以由 switch 的表达式值确定。例如:

    Size sz = ...;
    switch (sz) {
        case SMALL: // no need to use Size.SMALL
            ...
            break;
        ...
    }

👀:default

  • 表示“默认”,即其他情况都不满足。default 后要紧跟冒号,default 块和 case 块的先后顺序可以变动,不会影响程序执行结果。通常,default 块放在末尾,也可以省略不写。

👀:break

  • 表示“停止”,即跳出当前结构。

    • 如果在 case 分支语句的末尾没有 break 语句,有可能触发多个 case 分支。那么就会接着执行下一个 case 分支语句。这种情况相当危险,常常会引发错误。为此,我们在程序中从不使用 switch 语句

    • 如果你喜欢 switch 语句,编译代码时可以考虑加上 -Xlint:fallthrough 选项,如下所示:

      javac -Xlint:fallthrough Test.java
  • 这样一来,如果某个分支最后缺少一个 break 语句,编译器就会给出一个警告消息

  1. 例1 (抽奖)

    public class DemoTest24 {
      public static void main(String[] args) {
        System.out.println("请输入座位号:");
        Scanner input = new Scanner(System.in);
        int num = input.nextInt();
        switch (num) {
          case 1:
            System.out.println("恭喜您,获得了一等奖");
            break;
          case 8:
            System.out.println("获得二等奖");
            break;
          case 5:
            System.out.println("获得三等奖");
            break;
          default:
            System.out.println("谢谢参与");
            break;
        }
      }
    }
  2. 例2 (判断当前的星期)

    public class Week {
      public static void main(String[] args) {
        String weekDate = "";
        // 获取当前时间
        Calendar calendar = Calendar.getInstance();
        // 获取星期的第几日
        int week = calendar.get(Calendar.DAY_OF_WEEK) - 1;
        switch (week) {
          case 0:
            weekDate = "星期日";
            break;
          case 1:
            weekDate = "星期一";
            break;
          case 2:
            weekDate = "星期二";
            break;
          case 3:
            weekDate = "星期三";
            break;
          case 4:
            weekDate = "星期四";
            break;
          case 5:
            weekDate = "星期五";
            break;
          case 6:
            weekDate = "星期六";
            break;
          default:
            break;
        }
        System.out.println("今天是" + weekDate);
      }
    }

2:嵌套switch语句

  • 定义:可以将一个 switch 语句作为一个外部 switch 语句的语句序列的一部分,这称为嵌套 switch 语句

    public class DemoTest {
      public static void main(String[] args) {
        switch (count) {
          case 1:
            switch (target) {
              case 0:
                System.out.println("target is zero");
                break;
              case 1:
                System.out.println("target is one");
                break;
            }
            break;
          case 2: // ...
        }
      }
    }
    • switch 语句不同于 if 语句的是 switch 语句仅能测试相等的情况,而 if 语句可计算任何类型的布尔表达式。也就是 switch 语句只能寻找 case 常量间某个值与表达式的值相匹配。
    • 在同一个 switch 语句中没有两个相同的 case 常量。当然,外部 switch 语句中的 case 常量可以和内部 switch 语句中的 case 常量相同。
    • switch 语句通常比一系列嵌套 if 语句更有效。

3:if语句和 switch 语句的区别

👀:从使用效率上区分
  • 从使用效率上区分,在对同一个变量的不同值作条件判断时,既可以使用 switch 语句,也可以使用 if 语句。使用 switch 语句的效率更高一些,尤其是判断的分支越多,越明显。
👀:从实用性上区分
  • 从语句的实用性角度区分,switch 语句不如 if 条件语句,if 语句是应用最广泛和最实用的语句。
👀:何时使用 if 语句和 switch 语句
  • 在程序开发的过程中,何时使用 if 语句和 switch 语句,需要根据实际情况而定,应尽量做到物尽其用。不能因为 switch 语句的效率高就一直使用,也不能因为 if 语句常用就不用 switch 语句。需要根据实际情况,具体问题具体分析,使用最适合的条件语句。

    一般情况下,对于判断条件较少的,可以使用 if 条件语句,但是在实现一些多条件的判断中,最好使用 switch 语句。

4):实现淡旺季飞机票打折

1:if语句

public class Discounts {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入出行的月份:");
    int month = sc.nextInt();
    System.out.println("选择头等舱还是经济舱?数字1位头等舱,数字2位经济舱");
    int kind = sc.nextInt();
    double result = 60000;
    if (month <= 11 && month >= 4) {
      if (kind == 1) {
        result = result * 0.9;
      } else if (kind == 2) {
        result = result * 0.8;
      } else {
        System.out.println("选择种类有误,请重新输入!");
      }
    } else if ((month >= 1 && month <= 3) || month == 12) {
      if (kind == 1) {
        result = result * 0.5;
      } else if (kind == 2) {
        result = result * 0.4;
      } else {
        System.out.println("选择种类有误,请重新输入!");
      }
    } else {
      System.out.println("日期选择有误,请重新输入!");
    }
    System.out.println("您选择的机票价格为:" + result);
  }
}

2:switch语句

public class Discounts {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入出行的月份:");
    int month = sc.nextInt();
    System.out.println("选择头等舱还是经济舱?数字1位头等舱,数字2位经济舱");
    int kind = sc.nextInt();
    double result = 60000;
    switch (month) {
        // 旺季的票价计算
      case 4:
      case 5:
      case 6:
      case 7:
      case 8:
      case 9:
      case 10:
      case 11:
        switch (kind) {
            // 旺季头等舱
          case 1:
            result = result * 0.9;
            break;
          case 2:
            result = result * 0.8;
            break;
          default:
            System.out.println("选择种类有误,请重新输入!");
            break;
        }
        break;
      case 1:
      case 2:
      case 3:
      case 12:
        switch (kind) {
            // 旺季头等舱
          case 1:
            result = result * 0.5;
            break;
          case 2:
            result = result * 0.4;
            break;
          default:
            System.out.println("选择种类有误,请重新输入!");
            break;
        }
        break;
      default:
        System.out.println("日期选择有误,请重新输入!");
        break;
    }
    System.out.println("您选择的机票价格为:" + result);
  }
}

5):根据出生日期计算星座

1:if语句

public class Constellation {
  public static void main(String[] args) {
    System.out.println("请输入您的出生年月(如 0123 表示 1 月 23 日):");
    Scanner sc = new Scanner(System.in);
    int monthday = sc.nextInt();
    int month = monthday / 100;
    int day = monthday % 100;
    String xingzuo = "";
    if (month == 1) {
      if (day < 21) {
        xingzuo = "摩羯座";
      } else {
        xingzuo = "水瓶座";
      }
    } else if (month == 2) {
      if (day < 20) {
        xingzuo = "水瓶座";
      } else {
        xingzuo = "双鱼座";
      }
    } else if (month == 3) {
      if (day < 21) {
        xingzuo = "双鱼座";
      } else {
        xingzuo = "白羊座";
      }
    } else if (month == 4) {
      if (day < 21) {
        xingzuo = "白羊座";
      } else {
        xingzuo = "金牛座";
      }
    } else if (month == 5) {
      if (day < 22) {
        xingzuo = "金牛座";
      } else {
        xingzuo = "双子座";
      }
    } else if (month == 6) {
      if (day < 22) {
        xingzuo = "双子座";
      } else {
        xingzuo = "巨蟹座";
      }
    } else if (month == 7) {
      if (day < 23) {
        xingzuo = "巨蟹座";
      } else {
        xingzuo = "狮子座";
      }
    } else if (month == 8) {
      if (day < 24) {
        xingzuo = "狮子座";
      } else {
        xingzuo = "处女座";
      }
    } else if (month == 9) {
      if (day < 24) {
        xingzuo = "处女座";
      } else {
        xingzuo = "天秤座";
      }
    } else if (month == 10) {
      if (day < 24) {
        xingzuo = "天秤座";
      } else {
        xingzuo = "天蝎座";
      }
    } else if (month == 11) {
      if (day < 23) {
        xingzuo = "天蝎座";
      } else {
        xingzuo = "射手座";
      }
    } else if (month == 12) {
      if (day < 22) {
        xingzuo = "射手座";
      } else {
        xingzuo = "摩羯座";
      }
    }
    System.out.println("您的星座是:" + xingzuo);
  }
}

2:switch语句

public class Constellation {
  public static void main(String[] args) {
    System.out.println("请输入您的出生年月(如0123表示1月23日):");
    Scanner sc = new Scanner(System.in);
    int monthday = sc.nextInt();
    System.out.println(monthday);
    int month = monthday / 100;
    System.out.println(month);
    int day = monthday % 100;
    System.out.println(day);
    String Constellation = "";
    switch (month) {
      case 1:
        Constellation = day > 21 ? "摩羯座" : "水瓶座";
        break;
      case 2:
        Constellation = day < 20 ? "水瓶座" : "双鱼座";
        break;
      case 3:
        Constellation = day < 21 ? "双鱼座" : "白羊座";
        break;
      case 4:
        Constellation = day < 21 ? "白羊座" : "金牛座";
        break;
      case 5:
        Constellation = day < 22 ? "金牛座" : "双子座";
        break;
      case 6:
        Constellation = day < 22 ? "双子座" : "巨蟹座";
        break;
      case 7:
        Constellation = day < 23 ? "巨蟹座" : "狮子座";
        break;
      case 8:
        Constellation = day < 24 ? "狮子座" : "处女座";
        break;
      case 9:
        Constellation = day < 24 ? "处女座" : "天秤座";
        break;
      case 10:
        Constellation = day < 24 ? "天秤座" : "天蝎座";
        break;
      case 11:
        Constellation = day < 23 ? "天蝎座" : "射手座";
        break;
      case 12:
        Constellation = day < 22 ? "射手座" : "摩羯座";
        break;
    }
    System.out.println("您的星座是:" + Constellation);
  }
}

6):while

  • 循环语句可以在满足循环条件的情况下,反复执行某一段代码,这段被重复执行的代码被称为循环体。当反复执行这个循环体时,需要在合适的时候把循环条件改为假,从而结束循环,否则循环将一直执行下去,形成死循环。
  • 循环语句可能包含如下 4 个部分。
    • 初始化语句(init statement): 一条或多条语句,这些语句用于完成一些初始化工作,初始化语句在循环开始之前执行。
    • 循环条件(test_expression):这是一个 boolean 表达式,这个表达式能决定是否执行循环体。
    • 循环体(body_statement):这个部分是循环的主体,如果循环条件允许,这个代码块将被重复执行。如果这个代码块只有一行语句,则这个代码块的花括号是可以省略的。
    • 迭代语句(iteration_statement):这个部分在一次循环体执行结束后,对循环条件求值之前执行,通常用于控制循环条件中的变量,使得循环在合适的时候结束。

1:while 语句

  • 定义:while 语句是 Java 最基本的循环语句,是一种先判断的循环结构,可以在一定条件下重复执行一段代码。该语句需要判断一个测试条件,如果该条件为真,则执行循环语句(循环语句可以是一条或多条),否则跳出循环。

    while(条件表达式) {
        语句块;
    }
    • 其中语句块中的代码可以是一条或者多条语句,而条件表达式是一个有效的 boolean 表达式,它决定了是否执行循环体。当条件表达式的值为 true 时,就执行大括号中的语句块。执行完毕,再次检查表达式是否为 true,如果还为 true,则再次执行大括号中的代码,否则就跳出循环,执行 while 循环之后的代码。

    public class DemoTest25 {
      public static void main(String[] args) {
        int i = 1;
        int n = 1;
        while (i <= 10) {
          n = n * i;
          i++;
        }
        System.out.println("10的阶乘结果为:" + n);
      }
    }

2:do-while 语句

  • 定义:do-while循环。do-while 循环语句也是 Java 中运用广泛的循环语句,它由循环条件和循环体组成,但它与 while 语句略有不同。do-while 循环语句的特点是先执行循环体,然后判断循环条件是否成立

    do {
        语句块;
    }while(条件表达式);

    以上语句的执行过程是,首先执行一次循环操作,然后再判断 while 后面的条件表达式是否为 true,如果循环条件满足,循环继续执行,否则退出循环。while 语句后必须以分号表示循环结束,

    public class DemoTest26 {
      public static void main(String[] args) {
        int number = 1;
        int result = 1;
        do {
          result *= number;
          number++;
        } while (number <= 10);
        System.out.print("10阶乘结果是:" + result);
      }
    }
    public class DemoTest27 {
      public static void main(String[] args) {
        int bookIndex = 1;
        do {
          System.out.print(bookIndex + "\t");
          if (bookIndex % 10 == 0) {
            System.out.println();
          }
          bookIndex++;
        } while (bookIndex < 51);
      }
    }

3:while和do while的比较

  • while 循环和 do-while 循环的相同处是:都是循环结构,使用 while(循环条件) 表示循环条件,使用大括号将循环操作括起来。
  • while 循环和 do-while 循环的不同处如下:
    • 语法不同:与 while 循环相比,do-while 循环将 while 关键字和循环条件放在后面,而且前面多了 do 关键字,后面多了一个分号。
    • 执行次序不同:while 循环先判断,再执行。do-while 循环先执行,再判断。
    • 一开始循环条件就不满足的情况下,while 循环一次都不会执行,do-while 循环则不管什么情况下都至少执行一次。

7):for循环

  • 定义:for 语句是应用最广泛、功能最强的一种循环语句。大部分情况下,for 循环可以代替 while 循环、do while 循环。

  • for 语句是一种在程序执行前就要先判断条件表达式是否为真的循环语句。假如条件表达式的结果为假,那么它的循环语句根本不会执行。for 语句通常使用在知道循环次数的循环中

    for(条件表达式1;条件表达式2;条件表达式3) {
        语句块;
    }
    for 循环中 3 个表达式的含义
    表达式 形式 功能 举例
    条件表达式 1 赋值语句 循环结构的初始部分,为循环变量赋初值 int i=1
    条件表达式 2 条件语句 循环结构的循环条件 i>40
    条件表达式 3 迭代语句,通常使用
    ++ 或 – 运算符
    循环结构的迭代部分,通常用来修改循环
    变量的值
    i++
    • for 关键字后面括号中的 3 个条件表达式必须用“;”隔开。for 循环中的这 3 部分以及大括号中使循环体必需的 4 个组成部分完美地结合在一起,简单明了

      public class DemoTest28 {
        public static void main(String[] args) {
          int result = 1;
          for (int number = 1; number <= 5; number++) {
            result *= number;
          }
          // 输出"5的阶乘结果是:120"
          System.out.print("5 的阶乘结果是:" + result);
        }
      }

    for 语句中初始化、循环以及迭代部分都可以为空语句(但分号不能省略),三者均为空的时候,相当于一个无限循环

1:条件表达式1为空

  • 定义:for 语句中条件表达式 1 的作用可以在程序的其他位置给出,所以当条件表达式 1 为空时,for 语句后面括号内其他条件表达式执行的顺序不变。

    public class DemoTest29 {
      public static void main(String[] args) {
        int result = 0;
        // 相当于for语句的第1个表达式
        int number = 1;
        for (; number < 101; number++) {
          // 如果不能整除2,说明是奇数,则进行累加
          if (number % 2 != 0) {
            result += number;
          }
        }
        System.out.print("100 以内所有奇数和为:" + result);
      }
    }

2:条件表达式2为空

  • 定义:当 for 语句中条件表达式 2 为空时,将没有循环的终止条件。此时 for 语句会认为条件表达式 2 的值总是为真,循环无限制执行下去。因此,为了使循环达到某种条件时退出,需要在语句块中进行逻辑判断,并使用 break 语句来跳出循环,否则将产生死循环。

    public class DemoTest30 {
      public static void main(String[] args) {
        int result = 0;
        for (int number = 1; ; number++) {
          // 相当于for语句的表达式2,满足时就退出for循环
          if (number > 100) {
            break;
          }
          // 如果不能整除2,说明是奇数,则进行累加
          if (number % 2 != 0) {
            result += number;
          }
        }
        System.out.print("100 以内所有奇数和为:" + result);
      }
    }

3:条件表达式3为空

  • 定义:当 for 语言中条件表达式 3 为空时,也就没有设置控制变量的表达式,即每次循环之后无法改变变量的值,此时也无法保证循环正常结束。

    public class DemoTest31 {
      public static void main(String[] args) {
        int result = 0;
        for (int number = 1; number < 101; ) {
          // 如果不能整除2,说明是奇数,则进行累加
          if (number % 2 != 0) {
            result += number;
          }
          // 相当于for语句的条件表达式3,每次递增1
          number++;
        }
        System.out.print("100 以内所有奇数和为:" + result);
      }
    }
    
  • 如果没有循环体语句,number 变量的值为 1,永远小于 101,因此将无法结束循环,形成无限循环。在上面代码中将 number 的递增语句放在 for 循环体内,效果与完整 for 语句功能相同

4:三个条件都为空

  • 定义:在 for 循环语句中,无论缺少哪部分条件表达式,都可以在程序的其他位置补充,从而保持 for 循环语句的完整性,使循环正常进行。当 for 语句中循环体全为空时,即没有循环初值,不判断循环条件,循环变量不增值,此时无条件执行循环体,形成无限循环或者死循环。对于这种情况,读者在使用时应该尽量避免。

    public class DemoTest32 {
      public static void main(String[] args) {
        int result = 0;
        // 相当于for语句的条件表达式1
        int number = 1;
        for (; ; ) {
          // 相当于for语句的条件表达式2
          if (number > 100) {
            break;
          }
          // 如果不能整除2,说明是奇数,则进行累加
          if (number % 2 != 0) {
            result += number;
        }
          // 相当于for语句的条件表达式3
          number++;
        }
        System.out.print("100 以内所有奇数和为: " + result);
      }
    }
  • 例 (统计销量)

    public class DemoTest33 {
      public static void main(String[] args) {
        int sum = 0;
        int num = 0;
        Scanner sc = new Scanner(System.in);
        for (int i = 1; i <= 6; i++) {
          System.out.println("请输入第" + i + " 个月的销售数量:");
          num = sc.nextInt();
          sum += num;
        }
        System.out.println("上半年的销售总量为:" + sum);
      }
    }

5:for、do-while和while的区别

  • 如👇表

    for、do-while和while的区别
    名称 概念 适用场景 特点
    for 根据循环次数限制做多少次重复操作 适合循环次数是已知的操作 - 初始化的条件可以使用局部变量和外部变量
    - 使用局部变量时,控制执行在 for 结束后会自动释放,提高内存使用效率。
    - 且变量在 for 循环结束后,不能被访问。
    - 先判断,再执行
    while 当满足什么条件的时候,才做某种操作 适合循环次数是未知的操作 - 初始化的条件只能使用外部变量,且变量在 while 循环结束后可以访问
    - 先判断,再执行
    do-while 先执行一次,在判断是否满足条件 适合至少执行一次的循环操作 - 在先需要执行一次的情况下,代码更加简洁。
    - 先执行一次,再判断
  • 例2 分别用 for、do-while 和 while 求出 1-10 的和

1:使用for循环
public class Sum1 {
  public static void main(String[] args) {
    int sum = 0;
    for (int i = 0; i <= 10; i++) {
      sum = sum + i;
    }
    System.out.println(sum);
  }
}
2:使用do-while循环
public class Sum2 {
  public static void main(String[] args) {
    int sum = 0;
    int i = 1;
    do {
      sum = sum + i;
      i++;
    } while (i < 11);
    System.out.println(sum);
  }
}
3:使用while循环
public class Sum3 {
  public static void main(String[] args) {
    int sum = 0;
    int i = 1;
    while (i < 11) {
      sum = sum + i;
      i++;
    }
    System.out.println(sum);
  }
}

8):for循环嵌套

  • 定义:和其他编程语言一样,Java 允许循环嵌套。如果把一个循环放在另一个循环体内,那么就可以形成嵌套循环。

  • 嵌套循环既可以是 for循环嵌套 while 循环,也可以是 while 循环嵌套 do-while 循环 …… 即各种类型的循环都可以作为外层循环,也可以作为内层循环。

  • 当程序遇到嵌套循环时,如果外层循环的循环条件允许,则开始执行外层循环的循环体,而内层循环将被外层循环的循环体来执行——只是内层循环需要反复执行自己的循环体而已。

  • 当内层循环执行结束,且外层循环的循环体执行结束时,则再次计算外层循环的循环条件,决定是否再次开始执行外层循环的循环体。

  • 根据上面分析,假设外层循环的循环次数为 n 次,内层循环的循环次数为 m 次,那么内层循环的循环体实际上需要执行 n×m 次

    public class DemoTest34 {
      public static void main(String[] args) {
       System.out.println("乘法口诀表:");
       for (int i = 1; i <= 9; i++) {
         for (int j = 1; j <= i; j++) {
           System.out.print(j + "*" + i + "=" + j * i + "\t");
         }
         System.out.println();
        }
      }
    }

9):foreach语句的用法

  • 定义:foreach 循环语句是 Java 1.5 的新特征之一,在遍历数组、集合方面,foreach 为开发者提供了极大的方便。foreach 循环语句是 for 语句的特殊简化版本,主要用于执行遍历功能的循环。

    for(类型 变量名:集合) {
        语句块;
    }

    其中,“类型”为集合元素的类型,“变量名”表示集合中的每一个元素,“集合”是被遍历的集合对象或数组。每执行一次循环语句,循环变量就读取集合中的一个元素

1:例1

// 声明并初始化数组
int[] numbers = { 43, 32, 53, 54, 75, 7, 10 };
System.out.println("----for----");
// for语句
for (int i = 0; i < numbers.length; i++) {
    System.out.println("Count is:" + numbers[i]);
}
  • 声明并初始化了 7 个元素数组集合,目前大家只需要知道当初始化数组时,要把相同类型的元素放到 {…} 中并且用逗号分隔(,)即可
// 声明并初始化int数组
int[] numbers = { 43, 32, 53, 54, 75, 7, 10 };
System.out.println("----for each----");
// for-each语句
for (int item : numbers) {
    System.out.println("Count is:" + item);
}
  • item 不是循环变量,它保存了集合中的元素,for-each 语句将集合中的元素一一取出来,并保存到 item 中,这个过程中不需要使用循环变量,通过数组下标访问数组中的元素。可见 for-each 语句在遍历集合的时候要简单方便得多

2:例2

String[] urls = { "http://c.biancheng.net/java", "http://c.biancheng.net/c", "http://c.biancheng.net/golang/" };
// 使用foreach循环来遍历数组元素
// 其中book将会自动迭代每个数组元素
for (String url : urls) {
    System.out.println(url);
}
  • 使用 foreach 循环遍历数组元素时无须获得数组长度,也无须根据索引来访问数组元素
String[] urls = { "http://c.biancheng.net/java", "http://c.biancheng.net/c", "http://c.biancheng.net/golang/" };
// 使用foreach循环来遍历数组元素,其中 book 将会自动迭代每个数组元素
for (String url : urls) {
    url = "https://c.biancheng.net";
    System.out.println(url);
}
System.out.println(urls[0]);
  • 当使用 foreach 来迭代访问数组元素时,foreach 中的循环变量相当于一个临时变量,系统会把数组元素依次赋给这个临时变量,而这个临时变量并不是数组元素,它只是保存了数组元素的值。因此,如果希望改变数组元素的值,则不能使用这种 foreach 循环

3:例3

String[] languages={"Java","ASP.NET","Python","C#","PHP"};
System.out.println("现在流行的编程语言有:");
// 使用 foreach 循环语句遍历数组
for(String lang:languages) {
    System.out.println(lang);
}

10):return语句

  • 定义:return 关键字并不是专门用于结束循环的,return 语句用于终止函数的执行或退出类的方法,并把控制权返回该方法的调用者。如果这个方法带有返回类型,return 语句就必须返回这个类型的值;如果这个方法没有返回值,可以使用没有表达式的 return 语句。

    return 与方法相同类型的变量;
    public class Return {
      public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入操作数1:");
        double num1 = sc.nextDouble();
        System.out.println("请输入操作数2:");
        double num2 = sc.nextDouble();
        double d = sum(num1, num2);
        System.out.println(num1 + "+" + num2 + "=" + d);
      }
    
      /**
       * @Title: sum @Description: TODO(返回两个数之和)
       *
       * @param @param i
       * @param @param j
       * @param @return 参数
       * @return double 返回类型
       * @throws
       */
      public static double sum(double i, double j) {
        double sum = i + j;
        return sum;
      }
    }

11):break语句

  • 定义:某些时候需要在某种条件出现时强行终止循环,而不是等到循环条件为 false 时才退出循环。此时,可以使用 break 来完成这个功能。
  • break 用于完全结束一个循环,跳出循环体。不管是哪种循环,一旦在循环体中遇到 break,系统将完全结束该循环,开始执行循环之后的代码。
  • 在 Java 中,break 语句有 3 种作用,分别是:在 switch 语句中终止一个语句序列、使用 break 语句直接强行退出循环和使用 break 语句实现 goto 的功能

1:使用 break 语句直接强行退出循环(break 不带标签)

👀:例1
  • 直接退出

    public class DemoTest35 {
      public static void main(String[] args) {
        // 定义变量存储小明的回答
        Scanner input = new Scanner(System.in);
        // 一圈100米,1000米为10圈,即为循环的次数
        String answer = "";
        for (int i = 0; i < 10; i++) {
          System.out.println("跑的是第" + (i + 1) + "圈");
          // 获取小明的回答
          System.out.println("还能坚持吗?");
          // 判断小明的回答是否为y?如果不是,则放弃,跳出循环
          answer = input.next();
          if (!answer.equals("y")) {
            System.out.println("放弃");
            break;
          }
          // 循环之后的代码
          System.out.println("加油!继续!");
        }
      }
    }
  • 终止内部循环

    public class DemoTest36 {
      public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
          System.out.print("第" + (i + 1) + "次循环:");
          // 内循环,设计为循环10次
          for (int j = 0; j < 10; j++) {
            // 判断j是否等于3,如果是,则终止循环
            if (j == 2) {
              break;
            }
            System.out.print("内循环的第" + (j + 1) + "次循环\t");
          }
          System.out.println();
        }
      }
    }

    一个循环中可以有一个以上的 break 语句,但是过多的 break 语句会破坏代码结构。switch 循环语句中的 break 仅影响 switch 语句,不会影响循环

👀:例2
  • 输出成绩之和

    public class DemoTest37 {
      public static void main(String[] args) {
        // 每门课的成绩
        int score;
        // 成绩之和
        int sum = 0;
        // 记录录入的成绩是否合法
        boolean con = true;
        Scanner input = new Scanner(System.in);
        System.out.println("请输入学生的姓名:");
        // 获取用户输入的姓名
        String name = input.next();
        for (int i = 1; i <= 6; i++) {
          System.out.println("请输入第" + i + "门课程的成绩:");
          // 获取用户输入的成绩
          score = input.nextInt();
          // 判断用户输入的成绩是否为负数,如果为负数,终止循环
          if (score < 0) {
            con = false;
            break;
          }
          // 累加求和
          sum = sum + score;
        }
        if (con) {
          System.out.println(name + "的总成绩为:" + sum);
        } else {
          System.out.println("抱歉,分数录入错误,请重新录入!");
        }
      }
    }
  • 在该程序中,当录入第 3 门课的成绩时,录入的成绩为负数,判断条件“score<0”为 true,执行“con=false”,用 con 来标记录入是否有误。接着执行 break 语句,执行完之后程序并没有继续执行条件语句后面的语句,而是直接退出 for 循环。之后执行下面的条件判断语句,判断 boolean 变量的 con 是否为 true,如果为 true,则打印总成绩;否则打印“抱歉,分数录入错误,请重新录入!

2:使用 break 语句实现 goto 的功能(break 带标签)

  • 定义:有时候,在嵌套很深的循环语句中会发生一些不可预料的事情。此时可能更加希望跳到嵌套的所有循环语句之外。通过添加一些额外的条件判断实现各层循环的检测很不方便。

  • break 语句可以实现 goto 的功能,并且 Java 定义了 break 语句的一种扩展形式来处理退出嵌套很深的循环这个问题。

  • 通过使用扩展的 break 语句,可以终止执行一个或者几个任意代码块,这些代码块不必是一个循环或一个 switch 语句的一部分。同时这种扩展的 break 语句带有标签,可以明确指定从何处重新开始执行。

    break 除了具有 goto 退出深层循环嵌套作用外,还保留了一些程序结构化的特性。

    break label;
    • label 是标识代码块的标签。当执行这种形式的 break 语句时,控制权被传递出指定的代码块。被加标签的代码块必须包围 break 语句,但是它不需要直接包围 break 的块。也就是说,可以使用一个加标签的 break 语句来退出一系列的嵌套块,但是不能使用 break 语句将控制权传递到不包含 break 语句的代码块
  • 用标签(label)可以指定一个代码块,标签可以是任何合法有效的 Java 标识符,后跟一个冒号。加上标签的代码块可以作为 break 语句的对象,使程序在加标签的块的结尾继续执行。

    public class BreakGoto {
      public static void main(String[] args) {
        label:
        for (int i = 0; i < 10; i++) {
          for (int j = 0; j < 8; j++) {
            System.out.println(j);
            if (j % 2 != 0) {
              break label;
            }
          }
        }
      }
    }
  • 这里的 label 是标签的名称,可以为 Java 语言中任意合法的标识符。标签语句必须和循环匹配使用,使用时书写在对应的循环语句的上面,标签语句以冒号结束。如果需要中断标签语句对应的循环,可以采用 break 后面跟标签名的方式。

12):continue语句

  • 定义:continue 语句是跳过循环体中剩余的语句而强制执行下一次循环,其作用为结束本次循环,即跳过循环体中下面尚未执行的语句,接着进行下一次是否执行循环的判定。

    注意:continue 语句只能用在 while 语句、for 语句或者 foreach 语句的循环体之中,在这之外的任何地方使用它都会引起语法错误。

    continue //不带标签
    continue label //带标签,label是标签名
    int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    for (int i = 0; i < numbers.length; i++) {
        if (i == 3) {
            continue;
        }
        System.out.println("Count is: " + i);
    }
  • 带标签的 continue 语句示例

    public class DemoTest38 {
      public static void main(String[] args) {
        label1:
        for (int x = 0; x < 5; x++) {
          for (int y = 5; y > 0; y--) {
            if (y == x) {
              continue label1;
            }
            System.out.println(x + "," + y);
          }
        }
        System.out.println("Game Over!");
      }
    }
  • 默认情况下,continue 只会跳出最近的内循环(代码第 3 行的 for 循环),如果要跳出代码第 2 行的外循环,可以为外循环添加一个标签 label1,然后在第 5 行的 continue 语句后面指定这个标签 label1,这样当条件满足执行 continue 语句时,程序就会跳转出外循环。

13):判断闰年平年并输出某月的天数

public class Day {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入年份:(注:必须大于1990年)");
    int year = sc.nextInt();
    System.out.println("请输入月份:");
    int month = sc.nextInt();
    boolean isRen;
    if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
      System.out.println(year + "闰年");
      isRen = true;
    } else {
      System.out.println(year + "平年");
      isRen = false;
    }
    int day = 0;
    switch (month) {
      case 1:
      case 3:
      case 5:
      case 7:
      case 8:
      case 10:
      case 12:
        day = 31;
        break;
      case 4:
      case 6:
      case 9:
      case 11:
        day = 30;
        break;
      default:
        if (isRen) {
          day = 29;
        } else {
          day = 28;
        }
        break;
    }
    System.out.println(year + "年" + month + "月共有" + day + "天");
  }
}

14):输出杨辉三角形

public class Triangle {
  public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    System.out.print("打印杨辉三角形的行数:");
    int row = scan.nextInt();
    calculate(row);
  }

  public static void calculate(int row) {
    for (int i = 1; i <= row; i++) {
      for (int j = 1; j <= row - i; j++) {
        System.out.print(" ");
      }
      for (int j = 1; j <= i; j++) {
        System.out.print(num(i, j) + " ");
      }
      System.out.println();
    }
  }

  public static int num(int x, int y) {
    if (y == 1 || y == x) {
      return 1;
    }
    int c = num(x - 1, y - 1) + num(x - 1, y);
    return c;
  }
}

15):选择结构和循环结构的总结

  • 定义:不论哪一种编程语言,都会提供两种基本的流程控制结构:分支结构和循环结构。分支结构用于实现根据条件来选择性地执行某段代码,循环结构则用于实现根据循环条件重复执行某段代码。

    Java 同样提供了这两种流程控制结构的语法,Java 提供了 if 和 switch 两种分支语句,并提供了 while、do while 和 for 三种循环语句。一般写循环语句时,分以下三步走:

    1. 定义初始值
    2. 设置判断条件
    3. 初始值变化

    除此之外,JDK5 还提供了一种新的循环:foreach 循环,能以更简单的方式来遍历集合、数组的元素

  • Java 还提供了 break、continue 和 return 来控制程序的循环结构,作用如下:

    • break:表示跳出当前层循环
    • continue:表示跳出本次循环,进入下一次循环
    • return:跳出当前方法的循环

四:Java字符串处理

1):定义字符串(2种方式)

定义:Java 没有内置的字符串类型,而是在标准 Java 类库中提供了一个 String 类来创建和操作字符串。

字符串对象一旦被创建,其值是不能改变的,但可以使用其他变量重新赋值的方式进行更改。

1:直接定义字符串

  • 定义:直接定义字符串是指使用双引号表示字符串中的内容

    String str;
    str = "Hello Java";

    字符串变量必须经过初始化才能使用

2:使用 String 类定义

  • 定义:String 类的构造方法有多种重载形式

    具有和类名相同的名称,而且没有返回类型的方法称为构造方法。重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。教程后面会详细讲解,这里了解一下就可以

1:String()
  • 定义:初始化一个新创建的 String 对象,表示一个空字符序列。
2:String(String original)
  • 定义:初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列。换句话说,新创建的字符串是该参数字符串的副本

    String str1 = new String("Hello Java");
    String str2 = new String(str1);
    • 这里 str1 和 str2 的值是相等的
3:String(char[ ]value)
  • 定义:分配一个新的字符串,将参数中的字符数组元素全部变为字符串。该字符数组的内容已被复制,后续对字符数组的修改不会影响新创建的字符串

    char a[] = {'H','e','l','l','0'};
    String sChar = new String(a);
    a[1] = 's';
    • 即使在创建字符串之后,对 a 数组中的第 2 个元素进行了修改,但未影响 sChar 的值
4:String(char[ ]value,int offset,int count)
  • 定义:分配一个新的 String,它包含来自该字符数组参数一个子数组的字符。offset 参数是子数组第一个字符的索引,count 参数指定子数组的长度。该子数组的内容已被赋值,后续对字符数组的修改不会影响新创建的字符串

    char a[]={'H','e','l','l','o'};
    String sChar=new String(a,1,4);
    a[1]='s';
    • 上述 sChar 变量的值是字符串“ello”。该构造方法使用字符数组中的部分连续元素来创建字符串对象。offset 参数指定起始索引值,count 指定截取元素的个数。创建字符串对象后,即使在后面修改了 a 数组中第 2 个元素的值,对 sChar 的值也没有任何影响

2):String字符串和整型int的相互转换

1:String转换为int

  • String 字符串转整型 int 有以下两种方式:

    • Integer.parseInt(str)
    • Integer.valueOf(str).intValue()

    Integer 是一个类,是 int 基本数据类型的封装类

    public class DemoTest39 {
      public static void main(String[] args) {
        String str = "123";
        int n = 0;
        // 第一种
        n = Integer.parseInt(str);
        System.out.println("Integer.parseInt(str):" + n);
        // 第二种
        n = 0;
        n = Integer.valueOf(str).intValue();
        System.out.println("Integer.parseInt(str):" + n);
      }
    }

2:int转换为String

  • 整型 int 转 String 字符串类型有以下 3 种方法:

    • String s = String.valueOf(i);
    • String s = Integer.toString(i);
    • String s = “” + i;
    public class DemoTest40 {
      public static void main(String[] args) {
        int num = 0;
        // 第一种
        num = 10;
        String str = String.valueOf(num);
        System.out.println("str:" + str);
        // 第二种
        num = 10;
        String str2 = Integer.toString(num);
        System.out.println("str2:" + str2);
        // 第三种
        String str3 = num + "";
        System.out.println("str3:" + str3);
      }
    }

3:valueOf() 、parse()和toString()

💨:valueof()
  • 定义:valueOf() 方法将数据的内部格式转换为可读的形式。它是一种静态方法,对于所有 Java 内置的类型,在字符串内被重载,以便每一种类型都能被转换成字符串。valueOf() 方法还被类型 Object 重载,所以创建的任何形式类的对象也可被用作一个参数。这里是它的几种形式:

    static String valueOf(double num)
    static String valueOf(long num)
    static String valueOf(Object ob)
    static String valueOf(char chars[])
    • 调用 valueOf() 方法可以得到其他类型数据的字符串形式——例如在进行连接操作时。对各种数据类型,可以直接调用这种方法得到合理的字符串形式。所有的简单类型数据转换成相应于它们的普通字符串形式。任何传递给 valueOf() 方法的对象都将返回对象的 toString() 方法调用的结果。事实上,也可以通过直接调用 toString() 方法而得到相同的结果
  • valueOf() 方法返回一个相当晦涩的字符串,这说明它是一个某种类型的数组。然而对于字符数组,它创建一个包含了字符数组中的字符的字符串对象。valueOf() 方法有一种特定形式允许指定字符数组的一个子集

    static String valueOf(char chars[ ], int startIndex, int numChars)
    • 这里 chars 是存放字符的数组,startIndex 是字符数组中期望得到的子字符串的首字符下标,numChars 指定子字符串的长度
💨:parse()
  • 定义:parseXxx(String) 这种形式,是指把字符串转换为数值型,其中 Xxx 对应不同的数据类型,然后转换为 Xxx 指定的类型,如 int 型和 float 型。
💨:toString()
  • 定义:toString() 可以把一个引用类型转换为 String 字符串类型,是 sun 公司开发 Java 的时候为了方便所有类的字符串操作而特意加入的一个方法。

3):字符串拼接(连接)

  • 定义:String 字符串虽然是不可变字符串,但也可以进行拼接只是会产生一个新的对象。String 字符串拼接可以使用“+”运算符或 String 的 concat(String str) 方法。“+”运算符优势是可以连接任何类型数据拼接成为字符串,而 concat 方法只能拼接 String 类型字符串。

1:使用连接运算符 ”+“

  • 定义:与绝大多数的程序设计语言一样,Java 语言允许使用“+”号连接(拼接)两个字符串。“+”运算符是最简单、最快捷,也是使用最多的字符串连接方式。在使用“+”运算符连接字符串和 int 型(或 double 型)数据时,“+”将 int(或 double)型数据自动转换成 String 类型

    public class DemoTest41 {
      public static void main(String[] args) {
        int[] no = new int[] {501, 502, 503, 504, 505};
        String[] names = new String[] {"1", "2", "3", "4", "5"};
        String[] classes = new String[] {"数学", "语文", "数学", "语文", "英语"};
        System.out.println("本次考试结果如下:");
        for (int i = 0; i < no.length; i++) {
          System.out.println(
              "学号:" + no[i] + "|姓名:" + names[i] + "|课程:" + classes[i] + "|班级:" + "初二(三)班");
        }
      }
    }

2:使用concat()方法

  • 定义:concat() 方法实现了将一个字符串连接到另一个字符串的后面

    字符串 1.concat(字符串 2);
    public class DemoTest42 {
      public static void main(String[] args) {
        String info = "三国演义、";
        info = info.concat("西游记、");
        info = info.concat("水浒传、");
        info = info.concat("红楼梦");
        System.out.println(info);
        String cn = "中国";
        System.out.println(cn.concat("北京").concat("海淀区").concat("人民公园"));
      }
    }

3:连接其他类型数据

  • 定义:字符串也可同其他基本数据类型进行连接

    public class DemoTest43 {
      public static void main(String[] args) {
        // 字符串
        String book = "三国演义";
        // 整型变量
        int price = 59;
        // 浮点型变量
        float readtime = 2.5f;
        System.out.println("我买了一本图书,名字是:" + book + "\n价格是:" + price + "\n我每天阅读" + readtime + "小时");
      }
    }

    只要“+”运算符的一个操作数是字符串,编译器就会将另一个操作数转换成字符串形式,所以应该谨慎地将其他数据类型与字符串相连,以免出现意想不到的结果

4):获取字符串的长度(length())

  • 定义:要获取字符串的长度,可以使用 String 类的 length() 方法

    字符串名.length();
    public class DemoTest44 {
      public static void main(String[] args) {
        String sys = "学生信息管理";
        // 输出系统名称
        System.out.println("欢迎进入《" + sys + "》系统");
        System.out.println("请设置一个管理员密码:");
        Scanner input = new Scanner(System.in);
        // 获取用户输入的密码
        String pass = input.next();
        // 获取密码的长度
        int length = pass.length();
        if (length > 6 && length < 12) {
          System.out.println("密码长度符合规定。");
          System.out.println("已生效,请牢记密码:" + pass);
        } else if (length >= 12) {
          System.out.println("密码过长。");
        } else {
          System.out.println("密码过短。");
        }
      }
    }

5):字符串大小写转换

  • 定义:String 类的 toLowerCase() 方法可以将字符串中的所有字符全部转换成小写,而非字母的字符不受影响

    字符串名.toLowerCase()    // 将字符串中的字母全部转换为小写,非字母不受影响
  • 定义:toUpperCase() 则将字符串中的所有字符全部转换成大写,而非字母的字符不受影响

    字符串名.toUpperCase()    // 将字符串中的字母全部转换为大写,非字母不受影响
    String str="abcdef 我 ghijklmn";
    System.out.println(str.toLowerCase());    // 输出:abcdef 我 ghijklmn
    System.out.println(str.toUpperCase());    // 输出:ABCDEF 我 GHIJKLMN
    public class DemoTest45 {
      public static void main(String[] args) {
        String en = "The Day You Went Away";
        System.out.println("原始字符串:" + en);
        System.out.println("使用 toLowerCase() 方法之后为:" + en.toLowerCase());
        System.out.println("使用 toUpperCase() 方法之后为:" + en.toUpperCase());
    
        // 定义原始字符串
        en = "sun sun 是太阳,圆圆挂在 SKY 上";
        System.out.println("原始字符串:" + en);
        System.out.println("使用 toLowerCase() 方法之后为:" + en.toLowerCase());
        System.out.println("使用 toUpperCase() 方法之后为:" + en.toUpperCase());
    
        en = "select id,name,sex,address,birthday from students";
        // 定义原始字符串
        System.out.println("原始字符串:" + en);
        System.out.println("使用 toLowerCase() 方法之后为:" + en.toLowerCase());
        System.out.println("使用 toUpperCase() 方法之后为:" + en.toUpperCase());
      }
    }

6):去除字符串中的空格

  • 定义:去除首尾空格

    字符串名.trim()
    String str = " hello ";
    System.out.println(str.length());    // 输出 7
    System.out.println(str.trim().length());    // 输出 5

    trim() 只能去掉字符串中前后的半角空格(英文空格),而无法去掉全角空格(中文空格)。可用以下代码将全角空格替换为半角空格再进行操作,其中替换是 String 类的 replace() 方法

    // 将中文空格替换为英文空格
    str = str.replace((char) 12288, ' ');   
    str = str.trim();

7):截取(提取)子字符串

  • 方法:在 String 中提供了两个截取字符串的方法,一个是从指定位置截取到字符串结尾,另一个是截取指定范围的内容

1:substring(int beginIndex)

  • 此方式用于提取从索引位置开始至结尾处的字符串部分。调用时,括号中是需要提取字符串的开始位置,方法的返回值是提取的字符串

    String str = "我爱 Java 编程";
    String result = str.substring(3);
    System.out.println(result);    // 输出:Java 编程

2:substring(int beginIndex,int endInDex)

  • 此方法中的 beginIndex 表示截取的起始索引,截取的字符串中包括起始索引对应的字符;endIndex 表示结束索引,截取的字符串中不包括结束索引对应的字符,如果不指定 endIndex,则表示截取到目标字符串末尾。该方法用于提取位置 beginIndex 和位置 endIndex 位置之间的字符串部分

    substring() 方法是按字符截取,而不是按字节截取

    public class DemoTest46 {
      public static void main(String[] args) {
        // 原始字符串
        String day = "Today is Monday";
        System.out.println("substring(0)结果:" + day.substring(0));
        System.out.println("substring(2)结果:" + day.substring(2));
        System.out.println("substring(10)结果:" + day.substring(10));
        System.out.println("substring(2,10)结果:" + day.substring(2, 10));
        System.out.println("substring(0,5)结果:" + day.substring(0, 5));
      }
    }

8):分割字符串(spilt())

  • String 类的 split() 方法可以按指定的分割符对目标字符串进行分割,分割后的内容存放在字符串数组中

    str.split(String sign)
    str.split(String sign,int limit)
  • 其中它们的含义如下:

    • str 为需要分割的目标字符串。
    • sign 为指定的分割符,可以是任意字符串。
    • limit 表示分割后生成的字符串的限制个数,如果不指定,则表示不限制,直到将整个目标字符串完全分割为止
  • 使用分隔符注意如下:

    1. “.”和“|”都是转义字符,必须得加“\”
      • 如果用“.”作为分隔的话,必须写成String.split("\\."),这样才能正确的分隔开,不能用String.split(".")
      • 如果用“|”作为分隔的话,必须写成String.split("\\|"),这样才能正确的分隔开,不能用String.split("|")
    2. 如果在一个字符串中有多个分隔符,可以用“|”作为连字符,比如:“acount=? and uu =? or n=?”,把三个都分隔出来,可以用String.split("and|or")
    public class DemoTest47 {
      public static void main(String[] args) {
        String Colors = "Red,Black,White,Yellow,Blue";
        // 不限制元素个数
        String[] arr1 = Colors.split(",");
        // 限制元素个数为3
        String[] arr2 = Colors.split(",", 2);
        System.out.println("所有颜色为:");
        for (int i = 0; i < arr1.length; i++) {
          System.out.println(arr1[i]);
        }
        System.out.println("前三个颜色为:");
        for (int j = 0; j < arr2.length; j++) {
          System.out.println(arr2[j]);
        }
      }
    }

9):截取新闻标题

  • 超出部分显示省略

    public class NewSub {
      public static void main(String[] args) {
        // 定义存储新闻标题的数组
        String[] news =
            new String[] {
              "如何快速掌握Java", "听老王剖析Java中的运算符", "学习Java的十大忠告", "你所不知道的java网络编程技巧大全", "Java面试题大全"
            };
        String title = "************* 新闻列表 *************";
        System.out.println(title.substring(10, 30));
        System.out.println("----------------------------------------------");
        /*
         * 循环遍历数组截取数组元素中的前10个字符作为列表展示
         */
        for (int i = 0; i < news.length; i++) {
          // 判断数组元素的长度是否大于10,如果大于,则截取,否则全部显示
          if (news[i].length() > 10) {
            System.out.println(news[i].substring(0, 10) + "…");
          } else {
            System.out.println(news[i]);
          }
        }
      }
    }

10):字符串的替换

1:replace()方法

  • 定义:replace() 方法用于将目标字符串中的指定字符(串)替换成新的字符(串)

    字符串.replace(String oldChar, String newChar)

    其中,oldChar 表示被替换的字符串;newChar 表示用于替换的字符串。replace() 方法会将字符串中所有 oldChar 替换成 newChar。

    public class ReplaceTest {
      public static void main(String[] args) {
        String words = "hello java,hello php";
        System.out.println("原始字符串是'" + words + "'");
        System.out.println("replace(\"l\",\"D\")结果:" + words.replace("l", "D"));
        System.out.println("replace(\"hello\",\"你好\")结果:" + words.replace("hello", "你好 "));
        words = "hr's dog";
        System.out.println("原始字符串是'" + words + "'");
        System.out.println("replace(\"r's\",\"is\")结果:" + words.replace("r's", "is"));
      }
    }

2:replaceFirst()方法

  • 定义:replaceFirst() 方法用于将目标字符串中匹配某正则表达式的第一个子字符串替换成新的字符串

    字符串.replaceFirst(String regex, String replacement)

    其中,regex 表示正则表达式;replacement 表示用于替换的字符串

    String words = "hello java,hello php";
    String newStr = words.replaceFirst("hello","你好 ");
    System.out.println(newStr);    // 输出:你好 java,hello php

3:replaceAll()方法

  • 定义:replaceAll() 方法用于将目标字符串中匹配某正则表达式的所有子字符串替换成新的字符串

    字符串.replaceAll(String regex, String replacement)

    其中,regex 表示正则表达式,replacement 表示用于替换的字符串

    String words = "hello java,hello php";
    String newStr = words.replaceAll("hello","你好 ");
    System.out.println(newStr);    // 输出:你好 java,你好 php

11):字符串替换实例

public class DemoTest48 {
  public static void main(String[] args) {
    // 定义原始字符串
    String intro = "今天时星其天,外面时下雨天。妈米去买菜了,漏网在家写作业。" + "语文作业时”其”写 5 行,数学使第 10 页。";
    // 将文本中的所有"时"和"使"都替换为"是"
    String newStrFirst = intro.replaceAll("[时使]", "是");
    // 将文本中的所有"妈米"改为"妈妈"
    String newStrSecond = newStrFirst.replaceAll("妈米", "妈妈");
    // 将文本中的所有"漏网"改为"留我"
    String newStrThird = newStrSecond.replaceAll("漏网", "留我");
    // 将文本中第一次出现的"其"改为"期"
    String newStrFourth = newStrThird.replaceFirst("[其]", "期");
    // 输出最终字符串
    System.out.println(newStrFourth);
  }
}

12):字符串比较(3种方法)

  • 定义:字符串比较是常见的操作,包括比较相等、比较大小、比较前缀和后缀串等。在 Java 中,比较字符串的常用方法有 3 个:equals() 方法、equalsIgnoreCase() 方法、 compareTo() 方法

1:equals()方法

  • 定义:equals() 方法将逐个地比较两个字符串的每个字符是否相同。如果两个字符串具有相同的字符和长度,它返回 true,否则返回 false。对于字符的大小写,也在检查的范围之内

    str1.equals(str2);
    String str1 = "abc";
    String str2 = new String("abc");
    String str3 = "ABC";
    System.out.println(str1.equals(str2)); // 输出 true
    System.out.println(str1.equals(str3)); // 输出 false
    public class EqualsTest{
      public static void main(String[] args) {
        String sys = "学生信息管理";
        System.out.println("欢迎进入《" + sys + "》系统");
        System.out.println("请设置一个管理员密码:");
        Scanner input = new Scanner(System.in);
        String pass = input.next(); // 设置密码
        System.out.println("重复管理员密码:");
        input = new Scanner(System.in);
        String pass1 = input.next(); // 确认密码
        if (pass.equals(pass1)) { // 比较两个密码
            System.out.println("已生效,请牢记密码:" + pass);
        } else {
            System.out.println("两次密码不一致,请重新设置。");
        }
      }
    }

2:equalsIgnoreCase()方法

  • 定义:equalsIgnoreCase() 方法的作用和语法与 equals() 方法完全相同,唯一不同的是 equalsIgnoreCase() 比较时不区分大小写

    String str1 = "abc";
    String str2 = "ABC";
    System.out.println(str1.equalsIgnoreCase(str2));    // 输出 true
    public class EqualsTest {
      public static void main(String[] args) {
        String sys = "学生信息管理";
        System.out.println("欢迎进入《" + sys + "》系统");
        System.out.println("请输入管理员名称:");
        Scanner input = new Scanner(System.in);
        // 获取用户输入的名称
        String name = input.next();
        System.out.println("请输入管理员密码:");
        input = new Scanner(System.in);
        // 获取用户输入的密码
        String pass = input.next();
        // 比较用户名与密码,注意此处忽略大小写
        // 验证
        if (name.equalsIgnoreCase("admin") && pass.equalsIgnoreCase("somboy")) {
          System.out.println("登录成功。");
        } else {
          System.out.println("登录失败。");
        }
      }
    }

3:equals()与==的比较

  • 定义:理解 equals() 方法和==运算符执行的是两个不同的操作是重要的。如同刚才解释的那样,equals() 方法比较字符串对象中的字符。而==运算符比较两个对象引用看它们是否引用相同的实例

    String s1 = "Hello";
    String s2 = new String(s1);
    System.out.println(s1.equals(s2)); // 输出true
    System.out.println(s1 == s2); // 输出false

    变量 s1 指向由“Hello”创建的字符串实例。s2 所指的的对象是以 s1 作为初始化而创建的。因此这两个字符串对象的内容是一样的。但它们是不同的对象,这就意味着 s1 和 s2 没有指向同一的对象,因此它们是不==<[对象引用详解](浅谈Java中的对象和引用 - Matrix海子 - 博客园)>

4:compareTo()方法

  • 定义:通常,仅仅知道两个字符串是否相同是不够的。对于排序应用来说,必须知道一个字符串是大于、等于还是小于另一个。一个字符串小于另一个指的是它在字典中先出现。而一个字符串大于另一个指的是它在字典中后出现。字符串(String)的 compareTo() 方法实现了这种功能compareTo() 方法用于按字典顺序比较两个字符串的大小,该比较是基于字符串各个字符的 Unicode 值

    str.compareTo(String otherstr);

    它会按字典顺序将 str 表示的字符序列与 otherstr 参数表示的字符序列进行比较。如果按字典顺序 str 位于 otherster 参数之前,比较结果为一个负整数;如果 str 位于 otherstr 之后,比较结果为一个正整数;如果两个字符串相等,则结果为 0

    如果两个字符串调用 equals() 方法返回 true,那么调用 compareTo() 方法会返回 0

    public class DemoTest49 {
      public static void main(String[] args) {
        String str = "A";
        String str1 = "a";
        System.out.println("str=" + str);
        System.out.println("str1=" + str1);
        System.out.println("str.compareTo(str1)的结果是:" + str.compareTo(str1));
        System.out.println("str1.compareTo(str)的结果是:" + str1.compareTo(str));
        System.out.println("str1.compareTo('a')的结果是:" + str1.compareTo("a"));
      }
    }

13):容易混淆的空字符串和null

1:空字符串

  • 空字符串表示:一个字符串对象引用的值为空,String a =””  声明了一个字符串变量a,变量a的值为空。而a指向了空字符串的内存空间。

2:null

  • null表示:一个字符串对象的引用为空,String a=null;  声明了一个字符串变量a,变量a的引用为空。所以a没有指向任何的内存空间,在堆中也没有开辟任何的空间

    public class DemoTest50 {
      public static void main(String[] args) {
        String str = "";
        // 创建一个字符串对象的默认值为""
        String str1 = new String();
        String str2 = null;
        // str和str1被实例化,而str2没有实例化,但str和str1所指的地址不同,但值一样,都为空。
        // 内存地址的比较,返回false
        System.out.println(str == str1);
        // 值的比较,返回true
        System.out.println(str.equals(str1));
        // 内存地址的比较,返回false
        System.out.println(str1 == str2);
        // 值的比较,返回false
        System.out.println(str1.equals(str2));
        // 内存地址的比较,返回false
        System.out.println(str == str2);
        // 值的比较,返回false
        System.out.println(str.equals(str2));
    
        // 空字符串的判断方法:
        if (str.equals("")) {
          System.out.println("str是一个空字符串");
        }
        if (str == "") {
          System.out.println("str是一个空字符串");
        }
        if (str.length() == 0) {
          System.out.println("str是一个空字符串");
        }
        if (str.isEmpty()) {
          System.out.println("str是一个空字符串");
        }
        if (str1.equals("")) {
          System.out.println("1、str1是一个空字符串");
        }
        if (str1 == "") {
          System.out.println("2、str1是一个空字符串");
        }
        // 内存地址的比较,不相等,所有没有输出
        if (str1.length() == 0) {
          System.out.println("3、str1是一个空字符串");
        }
        if (str1.isEmpty()) {
          System.out.println("4、str1是一个空字符串");
        }
        // str是一个空字符串
        // str是一个空字符串
        // str是一个空字符串
        // str是一个空字符串
        // 1、str1是一个空字符串
        // 3、str1是一个空字符串
        // 4、str1是一个空字符串
        // null字符串的判断方法
        if (str2 == null) {
          System.out.println("str2是null字符串");
        }
        // 解释:为什么null判断不用equals方法
        // 由于 null 不是对象,"" 是对象,所以比较的方式是==
        // str2.equals(Object o),后面应该是对象
        /*
         * if(str2.equals(null)){
         * System.out.println("str2是null字符串");
         * }
         * //这里会报Exception in thread "main" java.lang.NullPointerException
         *
         */
        if (str1 == null) {
          System.out.println("str2是null字符串");
        }
        if (str == null) {
          System.out.println("str2是null字符串");
        }
      }
    }

14):字符串查找(3种方法)

  • 定义:字符串查找分为两种形式:一种是在字符串中获取匹配字符(串)的索引值,另一种是在字符串中获取指定索引位置的字符。

1:根据字符查找

定义:String 类的 indexOf() 方法和 lastlndexOf() 方法用于在字符串中获取匹配字符(串)的索引值。

💨:indexOf()
  • 定义:indexOf() 方法用于返回字符(串)在指定字符串中首次出现的索引位置,如果能找到,则返回索引值,否则返回 -1

    str.indexOf(value)
    str.indexOf(value,int fromIndex)
    • str 表示指定字符串;value 表示待查找的字符(串);fromIndex 表示查找时的起始索引,如果不指定 fromIndex,则默认从指定字符串中的开始位置(即 fromIndex 默认为 0)开始查找
    String s = "Hello Java";
    int size = s.indexOf('v');    // size的结果为8

    public class IndexOfTest {
      public static void main(String[] args) {
        String words = "today,monday,sunday";
        System.out.println("原始字符串是'" + words + "'");
        System.out.println("indexOf(\"day\")结果:" + words.indexOf("day"));
        System.out.println("indexOf(\"day\",5)结果:" + words.indexOf("day", 3));
        System.out.println("indexOf(\"o\")结果:" + words.indexOf("o"));
        System.out.println("indexOf(\"o\",6)结果:" + words.indexOf("o", 2));
      }
    }
💨:lastIndexOf()
  • 定义:lastIndexOf() 方法用于返回字符(串)在指定字符串中最后一次出现的索引位置,如果能找到则返回索引值,否则返回 -1。

    str.lastIndexOf(value)
    str.lastlndexOf(value, int fromIndex)

    lastIndexOf() 方法的查找策略是从右往左查找,如果不指定起始索引,则默认从字符串的末尾开始查找

    public class LastIndexOfTest {
      public static void main(String[] args) {
        String words = "today,monday,Sunday";
        System.out.println("原始字符串是'" + words + "'");
        System.out.println("lastIndexOf(\"day\")结果:" + words.lastIndexOf("day"));
        System.out.println("lastIndexOf(\"day\",5)结果:" + words.lastIndexOf("day", 5));
        System.out.println("lastIndexOf(\"o\")结果:" + words.lastIndexOf("o"));
        System.out.println("lastlndexOf(\"o\",6)结果:" + words.lastIndexOf("o", 6));
      }
    }

2:根据索引查找

  • 定义:String 类的 charAt() 方法可以在字符串内根据指定的索引查找字符

    字符串名.charAt(索引值)

    提示:字符串本质上是字符数组,因此它也有索引,索引从零开始

    String words = "today,monday,sunday";
    System.out.println(words.charAt(0));    // 结果:t
    System.out.println(words.charAt(1));    // 结果:o
    System.out.println(words.charAt(8));    // 结果:n

15):检验文件名和邮箱地址

public class ValeEmail {
  public static void main(String[] args) {
    boolean filecon = false; // 判断文件名是否合法
    boolean emailcon = false; // 判断邮箱地址是否合法
    System.out.println("****欢迎进入作业提交系统****");
    Scanner input = new Scanner(System.in);
    System.out.println("请输入你要提交的文件名称:");
    String name = input.next();
    System.out.println("请输入要提交的邮箱地址:");
    String email = input.next();
    // 检验文件名称是否合法
    int index = name.lastIndexOf("."); // 获取"n"所在的位置
    // 判断合法
    if (index != -1
        && name.charAt(index + 1) == 'j'
        && name.charAt(index + 2) == 'a'
        && name.charAt(index + 3) == 'v'
        && name.charAt(index + 4) == 'a') {
      filecon = true;
    } else {
      System.out.println("输入的文件名无效!");
    }
    // 检查邮箱地址是否合法
    if (email.indexOf('@') != 1 && email.indexOf('.') > email.indexOf('@')) {
      emailcon = true;
    } else {
      System.out.println("输入的邮箱地址无效!");
    }
    // 输出校验的结果
    if (filecon && emailcon) {
      System.out.println("作业提交成功!");
    } else {
      System.out.println("作业提交失败!");
    }
  }
}

16):字符串实现简单加密解密

public class EnDeTest {
  public static void main(String[] args) {
    Scanner input = new Scanner(System.in);
    System.out.println("请输入一个英文字符串或揭秘字符串");
    // 获取用户输入
    String password = input.nextLine();
    // 讲获取的字符串转成字符数组
    char[] c = password.toCharArray();
    // 使用for循环给字符数组加密
    for (int i = 0; i < c.length; i++) {
      c[i] = (char) (c[i] ^ 20000);
    }
    // 输出加密或者解密结果
    System.out.println("加密或者解密之后的结果如下:");
    System.out.println(new String(c));
  }
}

17):StringBuffer类

  • 定义:除了通过 String 类创建和处理字符串之外,还可以使用 StringBuffer 类来处理字符串。StringBuffer 类可以比 String 类更高效地处理字符串。

  • 因为 StringBuffer 类是可变字符串类,创建 StringBuffer 类的对象后可以随意修改字符串的内容。每个StringBuffer 类的对象都能够存储指定容量的字符串,如果字符串的长度超过了 StringBuffer 类对象的容量,则该对象的容量会自动扩大

1:创建StringBuffer类

  • StringBuffer 类提供了 3 个构造方法来创建一个字符串,如下所示:

    • StringBuffer() 构造一个空的字符串缓冲区,并且初始化为 16 个字符的容量。

    • StringBuffer(int length) 创建一个空的字符串缓冲区,并且初始化为指定长度 length 的容量。

    • StringBuffer(String str) 创建一个字符串缓冲区,并将其内容初始化为指定的字符串内容 str,字符串缓冲区的初始容量为 16 加上字符串 str 的长度。

    // 定义一个空的字符串缓冲区,含有16个字符的容量
    StringBuffer str1 = new StringBuffer();
    // 定义一个含有10个字符容量的字符串缓冲区
    StringBuffer str2 = new StringBuffer(10);
    // 定义一个含有(16+4)的字符串缓冲区,"青春无悔"为4个字符
    StringBuffer str3 = new StringBuffer("青春无悔");
    /*
    *输出字符串的容量大小
    *capacity()方法返回字符串的容量大小
    */
    System.out.println(str1.capacity());    // 输出 16
    System.out.println(str2.capacity());    // 输出 10
    System.out.println(str3.capacity());    // 输出 20

2:追加字符串

  • 定义:StringBuffer 类的 append() 方法用于向原有 StringBuffer 对象中追加字符串

    StringBuffer 对象.append(String str)

    追加内容到当前 StringBuffer 对象的末尾

    StringBuffer buffer = new StringBuffer("hello,");    // 创建一个 StringBuffer 对象
    String str = "World!";
    buffer.append(str);    // 向 StringBuffer 对象追加 str 字符串
    System.out.println(buffer.substring(0));    // 输出:Hello,World! 
    public class AppendTest {
      public static void main(String[] args) {
        StringBuffer sys = new StringBuffer("校内课程管理");
        System.out.println("欢迎进入《" + sys + "》系统");
        // 声明课程
        StringBuffer courseName = new StringBuffer();
        System.out.println("请录入本期的五门必修课程:");
        Scanner input = new Scanner(System.in);
        String name = "";
        for (int i = 0; i < 5; i++) {
          name = input.next();
          courseName.append(name + "\t");
          if (i == 4) {
            System.out.println("录入完毕!");
          }
        }
        System.out.println("本学期的必修课程有:\n" + courseName);
      }
    }

3:替换字符串

  • 定义:StringBuffer 类的 setCharAt() 方法用于在字符串的指定索引位置替换一个字符

    StringBuffer 对象.setCharAt(int index, char ch);

    该方法的作用是修改对象中索引值为 index 位置的字符为新的字符 ch

    StringBuffer sb = new StringBuffer("hello");
    sb.setCharAt(1,'E');
    System.out.println(sb);    // 输出:hEllo
    sb.setCharAt(0,'H');
    System.out.println(sb);    // 输出:HEllo
    sb.setCharAt(2,'p');
    System.out.println(sb);    // 输出:HEplo

4:反转字符串

  • 定义:StringBuffer 类中的 reverse() 方法用于将字符串序列用其反转的形式取代

    StringBuffer 对象.reverse();
    StringBuffer sb = new StringBuffer("java");
    sb.reverse();
    System.out.println(sb);    // 输出:avaj

5:删除字符串

  • 定义:StringBuffer 类提供了 deleteCharAt() 和 delete() 两个删除字符串的方法
💨:deleteCharAt()
  • 定义:deleteCharAt() 方法用于移除序列中指定位置的字符

    StringBuffer 对象.deleteCharAt(int index);
    StringBuffer sb = new StringBuffer("She");
    sb.deleteCharAt(2);
    System.out.println(sb);    // 输出:Sh
💨:delete()
  • 定义:delete() 方法用于移除序列中子字符串的字符

    StringBuffer 对象.delete(int start,int end);

    其中,start 表示要删除字符的起始索引值(包括索引值所对应的字符),end 表示要删除字符串的结束索引值(不包括索引值所对应的字符)。该方法的作用是删除指定区域以内的所有字符

    StringBuffer sb = new StringBuffer("hello jack");
    sb.delete(2,5);
    System.out.println(sb);    // 输出:he jack
    sb.delete(2,5);
    System.out.println(sb);    // 输出:heck

18):StringBuffer替换特殊字符

public class SpecialTest {
  public static void main(String[] args) {
    System.out.println("请输入你要提交的Java文件名称:");
    Scanner input = new Scanner(System.in);
    // 获取用户输入的 java 文件名称
    String fileName = input.next();
    // 定义StringBuffer对象,字符串内容为用户输入的java文件名称
    StringBuffer file = new StringBuffer(fileName);
    // 获取英文状态下的"."是否存在
    int index = file.lastIndexOf(".");
    // 判断中文状态下的"。"和"."是否存在
    int errIndex1 = file.lastIndexOf("。");
    int errIndex2 = file.lastIndexOf(".");
    if (index != -1 && file.substring(index + 1, file.length()).equals("java")) {
      System.out.println("作业提交成功!");
    } else if (errIndex1 != -1 && file.substring(errIndex1 + 1, file.length()).equals("java")) {
      // 将中文状态下的"。"改为英文状态下的"."
      file.setCharAt(errIndex1, '.');
      System.out.println("你的书写有误,已改正为:" + file + "\r\n提交成功!");
    } else if (errIndex2 != -1 && file.substring(errIndex2 + 1, file.length()).equals("java")) {
      // 将全角状态下的"."改为英文状态下的"."
      file.setCharAt(errIndex2, '.');
      System.out.println("你的书写有误,已改正为:" + file + "\r\n提交成功!");
    } else {
      System.out.println("你提供的java文件名称有误,请核实!");
    }
  }
}

19):String、StringBuffer和StringBuilder类的区别

  • 在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。String 类是不可变类,即一旦一个 String 对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁

  • Java 提供了两个可变字符串类 StringBuffer 和 StringBuilder,中文翻译为“字符串缓冲区”。

    • StringBuilder 类是 JDK 1.5 新增的类,它也代表可变字符串对象。实际上,StringBuilder 和 StringBuffer 功能基本相似,方法也差不多
    • StringBuffer 是线程安全的,而 StringBuilder 则没有实现线程安全功能,所以性能略高。因此在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用 StringBuilder 类
  • StringBuffer、StringBuilder、String 中都实现了 CharSequence 接口。CharSequence 是一个定义字符串操作的接口,它只包括 length()、charAt(int index)、subSequence(int start, int end) 这几个 API

    可见,String 直接实现了 CharSequence 接口,StringBuilder 和 StringBuffer 都是可变的字符序列,它们都继承于 AbstractStringBuilder,实现了 CharSequence 接口

👍:总结

  • String 是 Java 中基础且重要的类,被声明为 final class,是不可变字符串。因为它的不可变性,所以拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。

  • StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类。它提供了 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列。

  • 在很多情况下我们的字符串拼接操作不需要线程安全,所以 StringBuilder 登场了。StringBuilder 是 JDK1.5 发布的,它和 StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。

👌:线程安全:
  • StringBuffer:线程安全
  • StringBuilder:线程不安全
👌:速度:
  • 一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的
👌:使用环境:
  • 操作少量的数据使用 String。
  • 单线程操作大量数据使用 StringBuilder
  • 多线程操作大量数据使用 StringBuffer

20):正则表达式

  • 正则表达式(Regular Expression)又称正规表示法、常规表示法,在代码中常简写为 regex、regexp 或 RE,它是计算机科学的一个概念
  • String 类里也提供了如下几个特殊的方法。

    • boolean matches(String regex):判断该字符串是否匹配指定的正则表达式。
    • String replaceAll(String regex, String replacement):将该字符串中所有匹配 regex 的子串替换成 replacement。
    • String replaceFirst(String regex, String replacement):将该字符串中第一个匹配 regex 的子串替换成 replacement。
    • String[] split(String regex):以 regex 作为分隔符,把该字符串分割成多个子串。

1:正则表达式支持字符

  • 定义:创建正则表达式就是创建一个特殊的字符串

    正则表达式所支持的合法字符
    字符 解释
    X 字符x(x 可代表任何合法的字符)
    \0mnn 八进制数 0mnn 所表示的字符
    \xhh 十六进制值 0xhh 所表示的字符
    \uhhhh 十六进制值 0xhhhh 所表示的 Unicode 字符
    \t 制表符(“\u0009”)
    \n 新行(换行)符(‘\u000A’)
    \r 回车符(‘\u000D’)
    \f 换页符(‘\u000C’)
    \a 报警(bell)符(‘\u0007’)
    \e Escape 符(‘\u001B’)
    \cx x 对应的的控制符。例如,\cM匹配 Ctrl-M。x 值必须为 AZ 或 az 之一。
  • 除此之外,包含一些特殊字符,例如反斜杠\。如果需要匹配这些特殊字符,就必须首先将这些字符转义,也就是在前面添加一个反斜线\

    正则表达式中的特殊字符
    特殊字符 说明
    $ 匹配一行的结尾。要匹配 $ 字符本身,请使用\$
    ^ 匹配一行的开头。要匹配 ^ 字符本身,请使用\^
    () 标记子表达式的开始和结束位置。要匹配这些字符,请使用\(\)
    [] 用于确定中括号表达式的开始和结束位置。要匹配这些字符,请使用\[\]
    {} 用于标记前面子表达式的出现频度。要匹配这些字符,请使用\{\}
    * 指定前面子表达式可以出现零次或多次。要匹配 * 字符本身,请使用\*
    + 指定前面子表达式可以出现一次或多次。要匹配 + 字符本身,请使用\+
    ? 指定前面子表达式可以出现零次或一次。要匹配 ?字符本身,请使用\?
    . 匹配除换行符\n之外的任何单字符。要匹配.字符本身,请使用\.
    \ 用于转义下一个字符,或指定八进制、十六进制字符。如果需匹配\字符,请用\\
    | 指定两项之间任选一项。如果要匹配字符本身,请使用|
    "\u0041\\\\" // 匹配 A\
    "\u0061\t"  // 匹配a<制表符>
    "\\?\\["    // 匹配?[

    注:由于 Java 字符串中反斜杠本身需要转义,因此两个反斜杠(\)实际上相当于一个(前一个用于转义)

  • 正则表达式中的“通配符”远远超出了普通通配符的功能,它被称为预定义字符

    预定义字符
    预定义字符 说明
    . 可以匹配任何字符
    \d 匹配 0~9 的所有数字
    \D 匹配非数字
    \s 匹配所有的空白字符,包括空格、制表符、回车符、换页符、换行符等
    \S 匹配所有的非空白字符
    \w 匹配所有的单词字符,包括 0~9 所有数字、26 个英文字母和下画线_
    \W 匹配所有的非单词字符
    • d 是 digit 的意思,代表数字。
    • s 是 space 的意思,代表空白。
    • w 是 word 的意思,代表单词。
    • d、s、w 的大写形式恰好匹配与之相反的字符。
    c\\wt    // 可以匹配cat、cbt、cct、cOt、c9t等一批字符串
    \\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d    // 匹配如 000-000-0000 形式的电话号码
  • 若想匹配a~f的字母或中文字符外,则需要方括号表达式

    方括号表达式
    方括号表达式 说明
    表示枚举 例如[abc]表示 a、b、c 其中任意一个字符;[gz]表示 g、z 其中任意一个字符
    表示范围:- 例如[a-f]表示 af 范围内的任意字符;[\\u0041-\\u0056]表示十六进制字符 \u0041 到 \u0056 范围的字符。范围可以和枚举结合使用,如[a-cx-z],表示 ac、x~z 范围内的任意字符
    表示求否:^ 例如[^abc]表示非 a、b、c 的任意字符;[^a-f]表示不是 a~f 范围内的任意字符
    表示“与”运算:&& 例如 [a-z&&[def]]是 az 和 [def] 的交集,表示 d、e f[a-z&&^bc]]是 az 范围内的所有字符,除 b 和 c 之外 [ad-z] [a-z&&[m-p]]是 az 范围内的所有字符,除 mp 范围之外的字符
    表示“并”运算 并运算与前面的枚举类似。例如[a-d[m-p]]表示 [a-dm-p]
    • 比预定义字符灵活,几乎可以匹配任何字符。例需要匹配中文字符,表达式为: [\\u0041-\\u0056]形式。
    边界匹配符
    边界匹配符 说明
    ^ 行的开头
    $ 行的结尾
    \b 单词的边界
    \B 非单词的边界
    \A 输入的开头
    \G 前一个匹配的结尾
    \Z 输入的结尾,仅用于最后的结束符
    \z 输入的结尾
  • 前面例子中需要建立一个匹配 000-000-0000 形式的电话号码时,使用了 \d\d\d-\d\d\d-\d\d\d\d 正则表达式,这看起来比较烦琐。实际上,正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式。

    • Greedy(贪婪模式):数量表示符默认采用贪婪模式,除非另有表示。贪婪模式的表达式会一直匹配下去,直到无法匹配为止。如果你发现表达式匹配的结果与预期的不符,很有可能是因为你以为表达式只会匹配前面几个字符,而实际上它是贪婪模式,所以会一直匹配下去。
    • Reluctant(勉强模式):用问号后缀(?)表示,它只会匹配最少的字符。也称为最小匹配模式。
    • Possessive(占有模式):用加号后缀(+)表示,目前只有 Java 支持占有模式,通常比较少用。
    三种模式的数量表示符
    贪婪模式 勉强模式 占用模式 说明
    X? X?? X?+ X表达式出现零次或一次
    X* X*? X*+ X表达式出现零次或多次
    X+ X+? X++ X表达式出现一次或多次
    X{n} X{n}? X{n}+ X表达式出现 n 次
    X{n,} X{n,}? X{n,}+ X表达式最少出现 n 次
    X{n,m} X{n,m}? X{n,m}+ X表达式最少出现 n 次,最多出现 m 次
  • 关于贪婪模式和勉强模式的对比,代码如👇

    String str = "hello,java!";
    // 贪婪模式的正则表达式
    System.out.println(str.replaceFirst("\\w*" , "■"));    //输出■,java!
    // 勉强模式的正则表达式
    System.out.println(str.replaceFirst("\\w*?" , "■""));    //输出■hello, java!
    • 当从“hello java!”字符串中查找匹配\\w*子串时,因为\w*使用了贪婪模式,数量表示符*会一直匹配下去,所以该字符串前面的所有单词字符都被它匹配到,直到遇到空格,所以替换后的效果是“■,Java!”;如果使用勉强模式,数量表示符*会尽量匹配最少字符,即匹配 0 个字符,所以替换后的结果是“■hello,java!”

21):Pattern类和Matcher类的使用

  • java.util.regex 是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包。它包括两个类:Pattern 和 Matcher。

  • Pattern 对象是正则表达式编译后在内存中的表示形式,因此,正则表达式字符串必须先被编译为 Pattern 对象,然后再利用该 Pattern 对象创建对应的 Matcher 对象。执行匹配所涉及的状态保留在 Matcher 对象中,多个 Matcher 对象可共享同一个 Pattern 对象

    // 将一个字符串编译成 Pattern 对象
    Pattern p = Pattern.compile("a*b");
    // 使用 Pattern 对象创建 Matcher 对象
    Matcher m = p.matcher("aaaaab");
    boolean b = m.matches(); // 返回 true
  • 上面定义的 Pattern 对象可以多次重复使用。如果某个正则表达式仅需一次使用,则可直接使用 Pattern 类的静态 matches() 方法,此方法自动把指定字符串编译成匿名的 Pattern 对象,并执行匹配

    boolean b = Pattern.matches ("a*b","aaaaab");    // 返回 true
  • 但采用这种语句每次都需要重新编译新的 Pattern 对象,不能重复利用已编译的 Pattern 对象,所以效率不高。Pattern 是不可变类,可供多个并发线程安全使用

    Matcher 类的几个常用方法
    名称 说明
    find() 返回目标字符串中是否包含与 Pattern 匹配的子串
    group() 返回上一次与 Pattern 匹配的子串
    start() 返回上一次与 Pattern 匹配的子串在目标字符串中的开始位置
    end() 返回上一次与 Pattern 匹配的子串在目标字符串中的结束位置加 1
    lookingAt() 返回目标字符串前面部分与 Pattern 是否匹配
    matches() 返回整个目标字符串与 Pattern 是否匹配
    reset() 将现有的 Matcher 对象应用于一个新的字符序列。
    • 在 Pattern、Matcher 类的介绍中经常会看到一个 CharSequence 接口,该接口代表一个字符序列,其中 CharBuffer、String、StringBuffer、StringBuilder 都是它的实现类。简单地说,CharSequence 代表一个各种表示形式的字符串
    public class findTest {
      public static void main(String[] args) {
        String str = "java教程,13600006666,出售电话号码13611125555";
        // 创建一个Pattern对象,并用它建立一个Matcher对象
        // 该正则表达式只抓取13X和15X段的手机号
        // 实际要抓取哪些电话号码,只要修改正则表达式即可
        Matcher m = Pattern.compile("((13\\d)|(15\\d))\\d{2}").matcher(str);
        // 将所有符合正则表达式的子串(电话号码)全部输出
        while (m.find()) {
          System.out.println(m.group());
        }
      }
    }
    • find() 方法依次查找字符串中与 Pattern 匹配的子串,一旦找到对应的子串,下次调用 find() 方法时将接着向下查找

    • find() 方法还可以传入一个 int 类型的参数,带 int 参数的 find() 方法将从该 int 索引处向下搜索。start() 和 end() 方法主要用于确定子串在目标字符串中的位置

    public class lookingAtTest {
      public static void main(String[] args) {
        // 创建一个Pattern对象,并用它建立一个Matcher对象
        String regStr = "Java is very easy!";
        System.out.println("目标字符串是:" + regStr);
        Matcher m = Pattern.compile("\\w+").matcher(regStr);
        while (m.find()) {
          System.out.println(m.group() + "子串的起始位置:" + m.start() + ",其结束位置:" + m.end());
        }
      }
    }
    • matches() 和 lookingAt() 方法有点相似,只是 matches() 方法要求整个字符串和 Pattern 完全匹配时才返回 true,而 lookingAt() 只要字符串以 Pattern 开头就会返回 true。reset() 方法可将现有的 Matcher 对象应用于新的字符序列
    public class PatternTest1 {
      public static void main(String[] args) {
        String[] mails = {
          "kongyeeku@163.com", "kongyeeku@gmail.com", "ligang@crazyit.org", "wawa@abc.xx"
        };
        String mailRegEx = "\\w{3,20}@\\w+\\.(com|org|cn|net|gov)";
        Pattern mailPattern = Pattern.compile(mailRegEx);
        Matcher matcher = null;
        for (String mail : mails) {
          if (matcher == null) {
            matcher = mailPattern.matcher(mail);
          } else {
            matcher.reset(mail);
          }
          String result = mail + (matcher.matches() ? "是" : "不是") + "一个有效的邮件地址!";
          System.out.println(result);
        }
      }
    }
    • 上面程序创建了一个邮件地址的 Pattern,接着用这个 Pattern 与多个邮件地址进行匹配。当程序中的 Matcher 为 null 时,程序调用 matcher() 方法来创建一个 Matcher 对象,一旦 Matcher 对象被创建,程序就调用 Matcher 的 reset() 方法将该 Matcher 应用于新的字符序列

    • Matcher 的 matches()、lookingAt() 和 String 类的 equals() 有点相似。区别是 String 类的 equals() 都是与字符串进行比较,而 Matcher 的 matches() 和 lookingAt() 则是与正则表达式进行匹配

  • String 类里也提供了 matches() 方法,该方法返回该字符串是否匹配指定的正则表达式

    "kongyeeku@163.com".matches("\\w{3,20}@\\w+\\.(com|org|cn|net|gov)"); // 返回 true
  • 还可以利用正则表达式对目标字符串进行分割、查找、替换等操作

    public class ReplaceTest {
      public static void main(String[] args) {
        String[] msgs = {
          "Java has regular expressions in 1.4",
          "regular expressions now expressing in Java",
          "Java represses oracular expressions"
        };
        Pattern p = Pattern.compile("re\\w*");
        Matcher matcher = null;
        for (int i = 0; i < msgs.length; i++) {
          if (matcher == null) {
            matcher = p.matcher(msgs[i]);
          } else {
            matcher.reset(msgs[i]);
          }
          System.out.println(matcher.replaceAll("哈哈:"));
        }
      }
    }
  • String 类中也提供了 replaceAll()、replaceFirst()、split() 等方法

    public class StringReg {
      public static void main(String[] args) {
        String[] msgs = {
          "Java has regular expressions in 1.4",
          "regular expressions now expressing in Java",
          "Java represses oracular expressions"
        };
        for (String msg : msgs) {
          System.out.println(Arrays.toString(msg.split(" ")));
          System.out.println(msg.replaceFirst("re\\w*", "哈哈:)"));
        }
      }
    }

22):正则表达式验证电话号码

public class RegxPhone {
  public static void main(String[] args) {
    String regex = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|13[0-9]\\d{8}|15[1089]\\d{8}";
    String answer = "Y";
    do {
      System.out.print("请留下您的电话号码:");
      Scanner scan = new Scanner(System.in);
      // 接收用户在控制台输入的电话号码
      String phone = scan.next();
      // 编译正则表达式
      Pattern pattern = Pattern.compile(regex);
      // 创建给定输入模式的匹配器
      Matcher matcher = pattern.matcher(phone);
      boolean bool = ((Matcher) matcher).matches();
      // 如果验证通过
      if (bool) {
        System.out.println("输入的电话号码格式正确。");
      } else {
        System.out.println("输入的电话号码无效,格式不正确。");
      }
      System.out.print("是否继续输入?(Y/N 或者 y/n)");
      answer = scan.next();
    } while (answer.equalsIgnoreCase("Y"));
    System.out.println("注册结束。");
  }
}
  • 电话号码包括固定电话和手机号码。

  • 其中固定电话是由区号和号码组成,区号是以 0 开头的,后面是 2~3 位数,因此在匹配区号的时候可以使用正则表达式0\d{2,3}。

  • 固定电话号码由 7~8 位数字组成,因此可以使用表达式\d{7,8}来进行匹配。

  • 固定电话的组合方式可能是“区号-号码”或者是“区号号码”,因此匹配固定电话号码时,可以使用“0\d{2,3}[-]?\d{7,8}|0\d{2,3}\s?\d{7,8}”表达式

  • 手机号码是 11 位数,并且以数字 1 开头。考虑到手机号码的特殊性,这里使用“13[0-9]\d{8}|15[1089]\d{8}”表达式进行匹配。该正则表达式验证以 13 或 15 开头的手机号码; 以 15 开头的电话号码,第 3 位数字只能是 1、0、8、9 中的一个

23):正则表达式验证Ip地址

public class RegxAddress {
  public static void main(String[] args) {
    String pattern =
        "((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(\\.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}";
    String answer = "Y";
    do {
      System.out.print("请输入您的ip地址:");
      Scanner input = new Scanner(System.in);
      // 接收用户在控制台输入的ip地址
      String str = input.next();
      // 编译正则表达式
      Pattern r = Pattern.compile(pattern);
      // 创建给定输入模式的匹配器
      Matcher m = r.matcher(str);
      boolean bool = m.matches();
      // 如果验证通过
      if (bool) {
        System.out.println("输入的ip地址合法。");
      } else {
        System.out.println("输入的ip地址不合法。");
      }
      System.out.print("是否继续输入?(Y/N 或者 y/n)");
      answer = input.next();
    } while (answer.equalsIgnoreCase("Y"));
    System.out.println("注册结束。");
  }
}

五:Java数字和日期处理

1):Math类的常用方法

  • 定义:Math 类封装了常用的数学运算,提供了基本的数学操作,如指数、对数、平方根和三角函数等。Math 类位于 java.lang 包,它的构造方法是 private 的,因此无法创建 Math 类的对象,并且 Math 类中的所有方法都是类方法,可以直接通过类名来调用它们

1:静态常量

  • 定义:math类中包含E和PI两个静态常量,对应的值e(自然数)和π(圆周率)

    System.out.println("E的常量:"+Math.E);
    System.out.println("PI的常量:"+Math.PI);

2:求最大值、最小值和绝对值

  • Math 类提供的方法可以很容易实现(最大值、最小值、绝对值)

    求最大值、最小值和绝对值的方法
    方法 说明
    static int abs(int a) 返回 a 的绝对值
    static long abs(long a) 返回 a 的绝对值
    static float abs(float a) 返回 a 的绝对值
    static double abs(double a) 返回 a 的绝对值
    static int max(int x,int y) 返回 x 和 y 中的最大值
    static double max(double x,double y) 返回 x 和 y 中的最大值
    static long max(long x,long y) 返回 x 和 y 中的最大值
    static float max(float x,float y) 返回 x 和 y 中的最大值
    static int min(int x,int y) 返回 x 和 y 中的最小值
    static long min(long x,long y) 返回 x 和 y 中的最小值
    static double min(double x,double y) 返回 x 和 y 中的最小值
    static float min(float x,float y) 返回 x 和 y 中的最小值
    public class MathTest {
      public static void main(String[] args) {
        System.out.println("10 和 20 的较大值:" + Math.max(10, 20));
        System.out.println("15.6 和 15 的较小值:" + Math.min(15.6, 15));
        System.out.println("-12 的绝对值:" + Math.abs(-12));
      }
    }

3:求整运算

  • Math求整

    取整方法及其说明
    方法 说明
    static double ceil(double a) 返回大于或等于 a 的最小整数
    static double floor(double a) 返回小于或等于 a 的最大整数
    static double rint(double a) 返回最接近 a 的整数值,如果有两个同样接近的整数,则结果取偶数
    static int round(float a) 将参数加上 1/2 后返回与参数最近的整数
    static long round(double a) 将参数加上 1/2 后返回与参数最近的整数,然后强制转换为长整型
    public class IntegralOperations {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("请输入一个数字:");
        double num = input.nextDouble();
        System.out.println("大于或等于 " + num + " 的最小整数:" + Math.ceil(num));
        System.out.println("小于或等于 " + num + " 的最大整数:" + Math.floor(num));
        System.out.println("将 " + num + " 加上 0.5 之后最接近的整数:" + Math.round(num));
        System.out.println("最接近 " + num + " 的整数:" + Math.rint(num));
      }
    }

4:三角函数运算

  • Math三角函数

    三角函数方法及其说明
    方法 说明
    static double sin(double a) 返回角的三角正弦值,参数以孤度为单位
    static double cos(double a) 返回角的三角余弦值,参数以孤度为单位
    static double asin(double a) 返回一个值的反正弦值,参数域在 [-1,1],值域在 [-PI/2,PI/2]
    static double acos(double a) 返回一个值的反余弦值,参数域在 [-1,1],值域在 [0.0,PI]
    static double tan(double a) 返回角的三角正切值,参数以弧度为单位
    static double atan(double a) 返回一个值的反正切值,值域在 [-PI/2,PI/2]
    static double toDegrees(double angrad) 将用孤度表示的角转换为近似相等的用角度表示的角
    staticdouble toRadians(double angdeg) 将用角度表示的角转换为近似相等的用弧度表示的角
    public class TigerTest{
      public static void main(String[] args) {
        System.out.println("90 度的正弦值:" + Math.sin(Math.PI / 2));
        System.out.println("0 度的余弦值:" + Math.cos(0));
        System.out.println("1 的反正切值:" + Math.atan(1));
        System.out.println("120 度的弧度值:" + Math.toRadians(120.0));
      }
    }

5:指数运算

  • Math指数

    指数运算方法及其说明
    方法 说明
    static double exp(double a) 返回 e 的 a 次幂
    static double pow(double a,double b) 返回以 a 为底数,以 b 为指数的幂值
    static double sqrt(double a) 返回 a 的平方根
    static double cbrt(double a) 返回 a 的立方根
    static double log(double a) 返回 a 的自然对数,即 lna 的值
    static double log10(double a) 返回以 10 为底 a 的对数
    public class expTest{
      public static void main(String[] args) {
        System.out.println("4 的立方值:" + Math.pow(4, 3));
        System.out.println("16 的平方根:" + Math.sqrt(16));
        System.out.println("10 为底 2 的对数:" + Math.log10(2));
      }
    }

2):生成随机数

  • 定义:Random 类提供了丰富的随机数生成方法,可以产生 boolean、int、long、float、byte 数组以及 double 类型的随机数,这是它与 random() 方法最大的不同之处。random() 方法只能产生 double 类型的 0~1 的随机数

  • Random 类位于 java.util 包中,该类常用的有如下两个构造方法

    • Random():该构造方法使用一个和当前系统时间对应的数字作为种子数,然后使用这个种子数构造 Random 对象。
    • Random(long seed):使用单个 long 类型的参数创建一个新的随机数生成器

    Random 类提供的所有方法生成的随机数字都是均匀分布的,也就是说区间内部的数字生成的概率是均等的

    Random 类的常用方法
    方法 说明
    boolean nextBoolean() 生成一个随机的 boolean 值,生成 true 和 false 的值概率相等
    double nextDouble() 生成一个随机的 double 值,数值介于 [0,1.0),含 0 而不包含 1.0
    int nextlnt() 生成一个随机的 int 值,该值介于 int 的区间,也就是 -231~231-1。如果
    需要生成指定区间的 int 值,则需要进行一定的数学变换
    int nextlnt(int n) 生成一个随机的 int 值,该值介于 [0,n),包含 0 而不包含 n。如果想生成
    指定区间的 int 值,也需要进行一定的数学变换
    void setSeed(long seed) 重新设置 Random 对象中的种子数。设置完种子数以后的 Random 对象
    和相同种子数使用 new 关键字创建出的 Random 对象相同
    long nextLong() 返回一个随机长整型数字
    boolean nextBoolean() 返回一个随机布尔型值
    float nextFloat() 返回一个随机浮点型数字
    double nextDouble() 返回一个随机双精度值
    public class RandomTest {
      public static void main(String[] args) {
        Random r = new Random();
        // 生成[0,1,1]区间的小数
        double d1 = r.nextDouble();
        // 生成[0,7,0]区间的小数
        double d2 = r.nextDouble() * 7;
        // 生成[0,10]区间的整数
        int i1 = r.nextInt(10);
        // 生成[-3,15]区间的整数
        int i2 = r.nextInt(18) - 3;
        // 生成一个随机长整数值
        long l1 = r.nextLong();
        // 生成一个随机布尔型值
        boolean b1 = r.nextBoolean();
        // 生成一个随机浮点型值
        float f1 = r.nextFloat();
        System.out.println("生成的[0,1.0]区间的小数是:" + d1);
        System.out.println("生成的[0,7.0]区间的小数是:" + d2);
        System.out.println("生成的[0,10]区间的整数是:" + i1);
        System.out.println("生成的[-3,15]区间的整数是:" + i2);
        System.out.println("生成一个随机长整型值:" + l1);
        System.out.println("生成一个随机布尔型值:" + b1);
        System.out.println("生成一个随机浮点型值:" + f1);
        System.out.print("下期七星彩开奖号码预测:");
        for (int i = 0; i < 8; i++) {
          int num = r.nextInt(9);
          System.out.println(num);
        }
      }
    }

    Math 类的 random() 方法没有参数,它默认会返回大于等于 0.0、小于 1.0 的 double 类型随机数

    public class Random1Test {
      public static void main(String[] args) {
        int min = 2;
        int max = 102;
        int s = (int) min + (int) (Math.random() * (max - min));
        System.out.println(s);
        if (s % 2 == 0) {
          // 如果是偶数就输出
          System.out.println("随机偶数是:" + s);
        } else {
          // 如果是奇数就加1后输出
          System.out.println("随机奇数是:" + (s + 1));
        }
      }
    }

3):数字格式化

  • 定义:数字的格式在解决实际问题时使用非常普遍,这时可以使用 DedmalFormat 类对结果进行格式化处理。例如,将小数位统一成 2 位,不足 2 位的以 0 补齐。

    • DecimalFormat 是 NumberFormat 的一个子类,用于格式化十进制数字。DecimalFormat 类包含一个模式和一组符号
    DecimalFormat 支持的特殊字符
    符号 说明
    0 显示数字,如果位数不够则补 0
    # 显示数字,如果位数不够不发生变化
    . 小数分隔符
    - 减号
    , 组分隔符
    E 分隔科学记数法中的尾数和小数
    % 前缀或后缀,乘以 100 后作为百分比显示
    ? 乘以 1000 后作为千进制货币符显示。用货币符号代替。如果双写,用国际货币符号代替;
    如果出现在一个模式中,用货币十进制分隔符代替十进制分隔符
    public class DigitalFormatting {
      public static void main(String[] args) {
        DecimalFormat df1 = new DecimalFormat("0.0");
        DecimalFormat df2 = new DecimalFormat("#.#");
        DecimalFormat df3 = new DecimalFormat("000.000");
        DecimalFormat df4 = new DecimalFormat("###.###");
        Scanner scan = new Scanner(System.in);
        System.out.println("请输入一个float类型的数字:");
        float f = scan.nextFloat();
        // 对输入的数字应用格式,并输出结果
        System.out.println("0.0 格式:" + df1.format(f));
        System.out.println("#.# 格式:" + df2.format(f));
        System.out.println("000.000 格式:" + df3.format(f));
        System.out.println("###.### 格式:" + df4.format(f));
      }
    }

4):大数字运算

  • 定义:java.math.BigInteger 类和 java.math.BigDecimal 类。这两个类用于高精度计算,其中 BigInteger 类是针对整型大数字的处理类,而 BigDecimal 类是针对大小数的处理类

1:BigInteger类

  • 定义:BigInteger 类型的数字范围较 Integer 类型的数字范围要大得多。BigInteger 支持任意精度的整数,也就是说在运算中 BigInteger 类型可以准确地表示任何大小的整数值。除了基本的加、减、乘、除操作之外,BigInteger 类还封装了很多操作,像求绝对值、相反数、最大公约数以及判断是否为质数等。

  • 要使用 BigInteger 类,首先要创建一个 BigInteger 对象。BigInteger 类提供了很多种构造方法,其中最直接的一种是参数以字符串形式代表要处理的数字

    BigInteger(String val)

    val 是数字十进制的字符串

    BigInteger bi = new BigInteger("5")

    这里数字 5 的双引号是必需的,因为 BigInteger 类构造方法要求参数是字符串类型

    BigInteger类的常用运算方法
    方法名称 说明
    add(BigInteger val) 做加法运算
    subtract(BigInteger val) 做减法运算
    multiply(BigInteger val) 做乘法运算
    divide(BigInteger val) 做除法运算
    remainder(BigInteger val) 做取余数运算
    divideAndRemainder(BigInteger val) 做除法运算,返回数组的第一个值为商,第二个值为余数
    pow(int exponent) 做参数的 exponent 次方运算
    negate() 取相反数
    shiftLeft(int n) 将数字左移 n 位,如果 n 为负数,则做右移操作
    shiftRight(int n) 将数字右移 n 位,如果 n 为负数,则做左移操作
    and(BigInteger val) 做与运算
    or(BigInteger val) 做或运算
    compareTo(BigInteger val) 做数字的比较运算
    equals(Object obj) 当参数 obj 是 Biglnteger 类型的数字并且数值相等时返回 true, 其他返回 false
    min(BigInteger val) 返回较小的数值
    max(BigInteger val) 返回较大的数值
    public class BigIntegerTest {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("请输入一个整数:");
        int num = input.nextInt();
        // 使用输入的数字创建创建一个BigInteger对象
        BigInteger bi = new BigInteger(num + "");
        // 计算大数字加上99的结果
        System.out.println("加法操作结果:" + bi.add(new BigInteger("99")));
        // 计算大数字减去25的结果
        System.out.println("减法操作结果:" + bi.subtract(new BigInteger("25")));
        // 计算大数字乘以3的结果
        System.out.println("乘法橾作结果:" + bi.multiply(new BigInteger("3")));
        // 计算大数字除以2的结果
        System.out.println("除法操作结果:" + bi.divide(new BigInteger("2")));
        // 计算大数字除以3的商
        System.out.println("取商操作结果:" + bi.divideAndRemainder(new BigInteger("3"))[0]);
        // 计算大数字除以3的余数
        System.out.println("取余操作结果:" + bi.divideAndRemainder(new BigInteger("3"))[1]);
        // 计算大数字的2次方
        System.out.println("取 2 次方操作结果:" + bi.pow(2));
        // 计算大数字的相反数
        System.out.println("取相反数操作结果:" + bi.negate());
      }
    }

2:BigDecimal类

  • 定义:BigInteger 和 BigDecimal 都能实现大数字的运算,不同的是 BigDecimal 加入了小数的概念。一般的 float 和 double 类型数据只能用来做科学计算或工程计算,但由于在商业计算中要求数字精度比较高,所以要用到 BigDecimal 类。BigDecimal 类支持任何精度的浮点数,可以用来精确计算货币值

  • BigDecimal 常用的构造方法如下:

    • BigDecimal(double val):实例化时将双精度型转换为 BigDecimal 类型。
    • BigDecimal(String val):实例化时将字符串形式转换为 BigDecimal 类型
  • BigDecimal 类的方法可以用来做超大浮点数的运算,像加、减、乘和除等。在所有运算中,除法运算是最复杂的,因为在除不尽的情况下,末位小数的处理方式是需要考虑的

    BigDecimal add(BigDecimal augend)    // 加法操作
    BigDecimal subtract(BigDecimal subtrahend)    // 减法操作
    BigDecimal multiply(BigDecimal multiplieand)    // 乘法操作
    BigDecimal divide(BigDecimal divisor,int scale,int roundingMode )    // 除法操作
    • divide() 方法的 3 个参数分别表示除数、商的小数点后的位数和近似值处理模式
    roundingMode参数支持的处理模式
    模式名称 说明
    BigDecimal.ROUND_UP 商的最后一位如果大于 0,则向前进位,正负数都如此
    BigDecimal.ROUND_DOWN 商的最后一位无论是什么数字都省略
    BigDecimal.ROUND_CEILING 商如果是正数,按照 ROUND_UP 模式处理;如果是负数,按照 ROUND_DOWN
    模式处理
    BigDecimal.ROUND_FLOOR 与 ROUND_CELING 模式相反,商如果是正数,按照 ROUND_DOWN 模式处理;
    如果是负数,按照 ROUND_UP 模式处理
    BigDecimal.ROUND_HALF_ DOWN 对商进行五舍六入操作。如果商最后一位小于等于 5,则做舍弃操作,否则对最后
    一位进行进位操作
    BigDecimal.ROUND_HALF_UP 对商进行四舍五入操作。如果商最后一位小于 5,则做舍弃操作,否则对最后一位
    进行进位操作
    BigDecimal.ROUND_HALF_EVEN 如果商的倒数第二位是奇数,则按照 ROUND_HALF_UP 处理;如果是偶数,则按
    照 ROUND_HALF_DOWN 处理
    public class BigDecimalTest {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("请输入一个数字:");
        // 保存用户输入的数字
        double num = input.nextDouble();
        // 使用输入的数字创建BigDecimal对象
        BigDecimal bd = new BigDecimal(num);
        // 计算大数字加上99.154的结果
        System.out.println("加法操作结果:" + bd.add(new BigDecimal(99.154)));
        // 计算大数字减去-25.157904的结果
        System.out.println("减法操作结果:" + bd.subtract(new BigDecimal(-25.157904)));
        // 计算大数字乘以3.5的结果
        System.out.println("乘法操作结果:" + bd.multiply(new BigDecimal(3.5)));
        // 计算大数字除以3.14的结果,并保留小数后2位
        System.out.println(
            "除法操作结果(保留 2 位小数):" + bd.divide(new BigDecimal(3.14), 2, BigDecimal.ROUND_CEILING));
        // 计算大数字除以3.14的结果,并保留小数后5位
        System.out.println(
            "除法操作结果(保留 5 位小数):" + bd.divide(new BigDecimal(3.14), 5, BigDecimal.ROUND_CEILING));
      }
    }

5):时间日期的处理

  • 定义: java.util.Date 类和 java.util.Calendar 类完成。其中,Date 类主要封装了系统的日期和时间的信息,Calendar 类则会根据系统的日历来解释 Date 对象

1:Date类

  • 定义:Date 类表示系统特定的时间戳,可以精确到毫秒。Date 对象表示时间的默认顺序是星期、月、日、小时、分、秒、年。
💨:构造方法
  • Date 类有如下两个构造方法。

    • Date():此种形式表示分配 Date 对象并初始化此对象,以表示分配它的时间(精确到毫秒),使用该构造方法创建的对象可以获取本地的当前时间。
    • Date(long date):此种形式表示从 GMT 时间(格林尼治时间)1970 年 1 月 1 日 0 时 0 分 0 秒开始经过参数 date 指定的毫秒数。
    public class DateTest {
      public static void main(String[] args) {
        // 调用无参数构造函数
        Date date1 = new Date();
        // 输出:Wed May 18 21:24:40 CST 2016
        System.out.println(date1.toString());
        // 调用含有一个long类型参数的构造函数
        Date date2 = new Date(60000);
        // 输出:Thu Jan 0108:01:00 CST 1970
        System.out.println(date2);
      }
    }
💨:常用方法
  • 定义:Date 类提供了许多与日期和事件相关的方法

    Date类中的常用方法
    方法 描述
    boolean after(Date when) 判断此日期是否在指定日期之后
    boolean before(Date when) 判断此日期是否在指定日期之前
    int compareTo(Date anotherDate) 比较两个日期的顺序
    boolean equals(Object obj) 比较两个日期的相等性
    long getTime() 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来,此 Date 对象表示的毫秒数
    String toString() 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy。
    其中 dow 是一周中的某一天(Sun、Mon、Tue、Wed、Thu、Fri 及 Sat)
    public class OldDateTest {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("请输入要做的事情:");
        String title = input.next();
        Date date1 = new Date(); // 获取当前日期
        System.out.println("[" + title + "]这件事情发生时间为:" + date1);
        try {
          Thread.sleep(60000); // 暂停一分钟
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        Date date2 = new Date();
        System.out.println("现在时间为:" + date2);
        if (date2.before(date1)) {
          System.out.println(
              "你还有" + (date2.getTime() - date1.getTime()) / 1000 + "秒需要去完成【" + title + "】这件事!");
        } else {
          System.out.println(
              "【" + title + "】事情已经过去了" + (date2.getTime() - date1.getTime()) / 1000 + "秒");
        }
      }
    }

2:Calendar类

  • 定义:Calendar 类是一个抽象类,它为特定瞬间与 YEAR、MONTH、DAY_OF—MONTH、HOUR 等日历字段之间的转换提供了一些方法,并为操作日历字段(如获得下星期的日期) 提供了一些方法

  • 创建 Calendar 对象不能使用 new 关键字,因为 Calendar 类是一个抽象类,但是它提供了一个 getInstance() 方法来获得 Calendar类的对象。getInstance() 方法返回一个 Calendar 对象,其日历字段已由当前日期和时间初始化。

    Calendar c = Calendar.getInstance();
    Calendar类的常用方法
    方法 描述
    void add(int field, int amount) 根据日历的规则,为给定的日历字段 field 添加或减去指定的时间量 amount
    boolean after(Object when) 判断此 Calendar 表示的时间是否在指定时间 when 之后,并返回判断结果
    boolean before(Object when) 判断此 Calendar 表示的时间是否在指定时间 when 之前,并返回判断结果
    void clear() 清空 Calendar 中的日期时间值
    int compareTo(Calendar anotherCalendar) 比较两个 Calendar 对象表示的时间值(从格林威治时间 1970 年 01 月 01 日
    00 时 00 分 00 秒至现在的毫秒偏移量),大则返回 1,小则返回 -1,相等返回 0
    int get(int field) 返回指定日历字段的值
    int getActualMaximum(int field) 返回指定日历字段可能拥有的最大值
    int getActualMinimum(int field) 返回指定日历字段可能拥有的最小值
    int getFirstDayOfWeek() 获取一星期的第一天。根据不同的国家地区,返回不同的值
    static Calendar getInstance() 使用默认时区和语言坏境获得一个日历
    static Calendar getInstance(TimeZone zone) 使用指定时区和默认语言环境获得一个日历
    static Calendar getInstance(TimeZone zone,
    Locale aLocale)
    使用指定时区和语言环境获得一个日历
    Date getTime() 返回一个表示此 Calendar 时间值(从格林威治时间 1970 年 01 月 01 日 00 时
    00 分 00 秒至现在的毫秒偏移量)的 Date 对象
    long getTimeInMillis() 返回此 Calendar 的时间值,以毫秒为单位
    void set(int field, int value) 为指定的日历字段设置给定值
    void set(int year, int month, int date) 设置日历字段 YEAR、MONTH 和 DAY_OF_MONTH 的值
    void set(int year, int month, int date, int hourOfDay,
    int minute, int second)
    设置字段 YEAR、MONTH、DAY_OF_MONTH、HOUR、 MINUTE 和 SECOND 的值
    void setFirstDayOfWeek(int value) 设置一星期的第一天是哪一天
    void setTimeInMillis(long millis) 用给定的 long 值设置此 Calendar 的当前时间值
    • Calendar 对象可以调用 set() 方法将日历翻到任何一个时间,当参数 year 取负数时表示公元前。Calendar 对象调用 get() 方法可以获取有关年、月、日等时间信息,参数 field 的有效值由 Calendar 静态常量指定
  • Calendar 类中定义了许多常量,分别表示不同的意义。

    • Calendar.YEAR:年份。
    • Calendar.MONTH:月份。
    • Calendar.DATE:日期。
    • Calendar.DAY_OF_MONTH:日期,和上面的字段意义完全相同。
    • Calendar.HOUR:12小时制的小时。
    • Calendar.HOUR_OF_DAY:24 小时制的小时。
    • Calendar.MINUTE:分钟。
    • Calendar.SECOND:秒。
    • Calendar.DAY_OF_WEEK:星期几。
    获取当前月份
    int month=Calendar.getInstance().get(Calendar.MONTH)
    整型变量 month 的值是 0,表示当前日历是在 1 月份;如果值是 11,则表示当前日历在 12 月份
    public class CalendarTest {
      public static void main(String[] args) {
        // 获取当前月份
        int month = Calendar.getInstance().get(Calendar.MONTH);
        System.out.println(month);
        // 如果不设置时间,则默认为当前时间
        Calendar calendar = Calendar.getInstance();
        // 将系统当前时间赋值给Calendar对象
        calendar.setTime(new Date());
        // 获取当前时间
        System.out.println("现在时刻:" + calendar.getTime());
        // 获取当前年份
        int year = calendar.get(Calendar.YEAR);
        System.out.println("现在是:" + year + "年");
        // 获取当前月份(月份从 0 开始,所以加 1)
        int mth = calendar.get(Calendar.MONTH) + 1;
        System.out.print(mth + "月");
        // 获取日
        int day = calendar.get(Calendar.DATE);
        System.out.print(day + "日");
        // 获取今天星期几(以星期日为第一天)
        int week = calendar.get(Calendar.DAY_OF_WEEK) - 1;
        System.out.print("星期" + week + " ");
        // 获取当前小时数(24 小时制)
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        System.out.print(hour + "时");
        // 获取当前分钟
        int minute = calendar.get(Calendar.MINUTE);
        System.out.print(minute + "分");
        // 获取当前秒数
        int second = calendar.get(Calendar.SECOND);
        System.out.print(second + "秒");
        // 获取毫秒数
        int millisecond = calendar.get(Calendar.MILLISECOND);
        System.out.print(millisecond + "毫秒");
        // 获取今天是本月第几天
        int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
        System.out.println("今天是本月的第 " + dayOfMonth + " 天");
        // 获取今天是本月第几周
        int dayOfWeekInMonth = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
        System.out.println("今天是本月第 " + dayOfWeekInMonth + " 周");
        // 获取今天是今年第几天
        int many = calendar.get(Calendar.DAY_OF_YEAR);
        System.out.println("今天是今年第 " + many + " 天");
        Calendar c = Calendar.getInstance();
        // 设置年月日,时分秒将默认采用当前值
        c.set(2012, 8, 8);
        // 输出时间
        System.out.println("设置日期为 2012-8-8 后的时间:" + c.getTime());
      }
    }
  • Calendar 类来实现日历的打印功能

    public class CalendarDemo {
      public static void main(String[] args) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2022, 1, 1); // 实际的calendar对象所表示的日期为2022年2月1日
        // 判断2022年2月1日为一周中的第几天
        int index = calendar.get(Calendar.DAY_OF_WEEK) - 1;
        // System.out.println(index);
        char[] title = {'日', '一', '二', '三', '四', '五', '六'}; // 存放曰历的头部
        int daysArray[][] = new int[6][7]; // 存放日历的数据 多维数组
        int daysInMonth = 28; // 该月的天数
        int day = 1; // 自动增长
        for (int i = index; i < 7; i++) {
          // 填充第一周的日期数据,即日历中的第一行
          daysArray[0][i] = day++;
          // System.out.println(daysArray[0][i]);
        }
        for (int i = 1; i < 6; i++) {
          // 填充其他周的日历数据,控制行
          for (int j = 0; j < 7; j++) {
            // 如果当前day表示的是本月最后一天,则停止向数组中继续赋值
            if (day > daysInMonth) {
              i = 6;
              break;
            }
            daysArray[i][j] = day++;
          }
        }
        System.out.println("------------------2022 年 2 月--------------------\n");
        for (int i = 0; i < title.length; i++) {
          System.out.print(title[i] + "\t");
        }
        System.out.print("\n");
        // 输出二元数组daysArray中的元素
        for (int i = 0; i < 6; i++) {
          for (int j = 0; j < 7; j++) {
            if (daysArray[i][j] == 0) {
              if (i != 0) {
                // 如果到月末,则完成显示日历的任务,停止该方法的执行
                return;
              }
              System.out.print("\t");
              continue;
            }
            System.out.print(daysArray[i][j] + "\t");
          }
          System.out.print("\n");
        }
      }
    }

6):日期格式化

  • 定义:格式化日期表示将日期/时间格式转换为预先定义的日期/时间格式。例如将日期“Fri May 18 15:46:24 CST2016” 格式转换为 “2016-5-18 15:46:24 星期五”的格式

1:DateFormat类

  • 定义:DateFormat 是日期/时间格式化子类的抽象类,它以与语言无关的方式格式化并解析日期或时间。日期/时间格式化子类(如 SimpleDateFormat)允许进行格式化(也就是日期→文本)、解析(文本→日期)和标准化日期

  • 在创建 DateFormat 对象时不能使用 new 关键字,而应该使用 DateFormat 类中的静态方法 getDateInstance()

    DateFormat df = DateFormat.getDatelnstance();
    DateFormat类的常用方法
    方法 描述
    String format(Date date) 将 Date 格式化日期/时间字符串
    Calendar getCalendar() 获取与此日期/时间格式相关联的日历
    static DateFormat getDateInstance() 获取具有默认格式化风格和默认语言环境的日期格式
    static DateFormat getDateInstance(int style) 获取具有指定格式化风格和默认语言环境的日期格式
    static DateFormat getDateInstance(int style,
    Locale locale)
    获取具有指定格式化风格和指定语言环境的日期格式
    static DateFormat getDateTimeInstance() 获取具有默认格式化风格和默认语言环境的日期/时间
    格式
    static DateFormat getDateTimeInstance(int
    dateStyle,int timeStyle)
    获取具有指定日期/时间格式化风格和默认语言环境的
    日期/时间格式
    static DateFormat getDateTimeInstance(int
    dateStyle,int timeStyle,Locale locale)
    获取具有指定日期/时间格式化风格和指定语言环境的
    日期/时间格式
    static DateFormat getTimeInstance() 获取具有默认格式化风格和默认语言环境的时间格式
    static DateFormat getTimeInstance(int style) 获取具有指定格式化风格和默认语言环境的时间格式
    static DateFormat getTimeInstance(int style,
    Locale locale)
    获取具有指定格式化风格和指定语言环境的时间格式
    void setCalendar(Calendar newCalendar) 为此格式设置日历
    Date parse(String source) 将给定的字符串解析成日期/时间

    格式化样式主要通过 DateFormat 常量设置。将不同的常量传入到表 1 所示的方法中,以控制结果的长度。DateFormat 类的常量如下。

    • SHORT:完全为数字,如 12.5.10 或 5:30pm。

    • MEDIUM:较长,如 May 10,2016。

    • LONG:更长,如 May 12,2016 或 11:15:32am。

    • FULL:是完全指定,如 Tuesday、May 10、2012 AD 或 11:l5:42am CST。

    public class DateFormatTest {
      public static void main(String[] args) {
        // 获取不同格式化风格和中国环境的日期
        DateFormat df1 = DateFormat.getDateInstance(DateFormat.SHORT, Locale.CHINA);
        DateFormat df2 = DateFormat.getDateInstance(DateFormat.FULL, Locale.CHINA);
        DateFormat df3 = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.CHINA);
        DateFormat df4 = DateFormat.getDateInstance(DateFormat.LONG, Locale.CHINA);
        // 获取不同格式化风格和中国环境的时间
        DateFormat df5 = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.CHINA);
        DateFormat df6 = DateFormat.getTimeInstance(DateFormat.FULL, Locale.CHINA);
        DateFormat df7 = DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.CHINA);
        DateFormat df8 = DateFormat.getTimeInstance(DateFormat.LONG, Locale.CHINA);
        // 将不同格式化风格的日期格式化为日期字符串
        String date1 = df1.format(new Date());
        String date2 = df2.format(new Date());
        String date3 = df3.format(new Date());
        String date4 = df4.format(new Date());
        // 将不同格式化风格的时间格式化为时间字符串
        String time1 = df5.format(new Date());
        String time2 = df6.format(new Date());
        String time3 = df7.format(new Date());
        String time4 = df8.format(new Date());
        // 输出日期
        System.out.println("SHORT:" + date1 + " " + time1);
        System.out.println("FULL:" + date2 + " " + time2);
        System.out.println("MEDIUM:" + date3 + " " + time3);
        System.out.println("LONG:" + date4 + " " + time4);
      }
    }

2:SimpleDateFormat类

  • 定义:SimpleDateFormat 是一个以与语言环境有关的方式来格式化和解析日期的具体类,它允许进行格式化(日期→文本)、解析(文本→日期)和规范化。SimpleDateFormat 使得可以选择任何用户定义的日期/时间格式的模式。

    SimpleDateFormat 类主要有如下 3 种构造方法。

    • SimpleDateFormat():用默认的格式和默认的语言环境构造 SimpleDateFormat。

    • SimpleDateFormat(String pattern):用指定的格式和默认的语言环境构造 SimpleDateF ormat。

    • SimpleDateFormat(String pattern,Locale locale):用指定的格式和指定的语言环境构造 SimpleDateF ormat。

    日期/时间格式中的字母及其含义与示例
    字母 含义 示例
    y 年份。一般用 yy 表示两位年份,yyyy 表示 4 位年份 使用 yy 表示的年扮,如 11;
    使用 yyyy 表示的年份,如 2011
    M 月份。一般用 MM 表示月份,如果使用 MMM,则会
    根据语言环境显示不同语言的月份
    使用 MM 表示的月份,如 05;
    使用 MMM 表示月份,在 Locale.CHINA
    语言环境下,如“十月”;在 Locale.US
    语言环境下,如 Oct
    d 月份中的天数。一般用 dd 表示天数 使用 dd 表示的天数,如 10
    D 年份中的天数。表示当天是当年的第几天, 用 D 表示 使用 D 表示的年份中的天数,如 295
    E 星期几。用 E 表示,会根据语言环境的不同, 显示不
    同语言的星期几
    使用 E 表示星期几,在 Locale.CHINA 语
    言环境下,如“星期四”;在 Locale.US 语
    言环境下,如 Thu
    H 一天中的小时数(0~23)。一般用 HH 表示小时数 使用 HH 表示的小时数,如 18
    h 一天中的小时数(1~12)。一般使用 hh 表示小时数 使用 hh 表示的小时数,如 10 (注意 10 有
    可能是 10 点,也可能是 22 点)
    m 分钟数。一般使用 mm 表示分钟数 使用 mm 表示的分钟数,如 29
    s 秒数。一般使用 ss 表示秒数 使用 ss 表示的秒数,如 38
    S 毫秒数。一般使用 SSS 表示毫秒数 使用 SSS 表示的毫秒数,如 156
    public class SimpleTest {
      public static void main(String[] args) {
        // 创建一个Date对象,获取当前时间
        Date now = new Date();
        // 指定格式化
        SimpleDateFormat f = new SimpleDateFormat("今天是:" + "yyyy年MM月dd日E HH点mm分ss秒");
        System.out.println(f.format(now));
      }
    }

7):计算课程结束的日期

public class EndDate {
  /** 定义一周所要上课的天数。定义为周一、周二、周五共3天上课 */
  public static final int HAVEClass_OF_WEEK = 3;
  // 定义需要上的课次
  public static final int COURSENUM = 9;

  public static void main(String[] args) {
    // 定义上完全部课程需要多少周,取整数
    int weekCount = -1;
    // 取整后多余的天数
    int surplusDay = -1;
    // 获取上了多少次课
    int count = 0;
    // 取整后所剩的课程数
    int surplusClass = -1;
    // 上课所花的时间总数
    int total = 0;
    Calendar calendar = Calendar.getInstance();
    // 获取今天是周几
    int weekdays = (calendar.get(Calendar.DAY_OF_WEEK) - 1);

    /*
     * 定义一周中所要上课的天数,若上课一天则count+1 周一、周二、周五上课 今天是周三,这个星期只有周五上课
     */
    switch (weekdays) {
      case 0:
        // 上课
      case 1:
        count++;
        // 上课
      case 2:
        count++;
      case 3:
      case 4:
        // 上课
      case 5:
        count++;
      case 6:
      default:
        break;
    }
    /*
     * 计算今天是周几,如果不是周末,则计算到周六还有几天 (因为周日是一周的开始)
     */
    if (0 != weekdays) {
      surplusDay = 6 - weekdays;
    }
    // 获取上课所用多少周,取整
    weekCount = (COURSENUM - count) / HAVEClass_OF_WEEK;
    // 获取取整后剩下的课程数
    surplusClass = (COURSENUM - count) - (weekCount * HAVEClass_OF_WEEK);
    /*
     * 计算取整周后剩下的次数(从每周日开始算起) 若不剩课程,则日期-1(因为取整后的最后一天为周六,不上课) 则计算周五的日期
     * 若还剩1节课,肯定是周一上课,所以天数+2天 若还剩2节课,则周一、周二上课,所以天数+3天 若还剩3节课,则周一、周二、周五上课,所以天数+6天
     */
    switch (surplusClass) {
      case 0:
        surplusDay = surplusDay - 1;
        break;
      case 1:
        surplusDay += 2;
        break;
      case 2:
        surplusDay += 3;
        break;
      case 5:
        surplusDay += 6;
        break;
      default:
        break;
    }
    // 要上课所用的总天数
    total = surplusDay + (weekCount * 7);
    // 设置课程结束当天的日期
    calendar.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH) + total);
    String strWeek = "";
    int week = (calendar.get(Calendar.DAY_OF_WEEK) - 1);
    switch (week) {
      case 1:
        strWeek = "一";
        break;
      case 2:
        strWeek = "二";
        break;
      case 3:
        strWeek = "三";
        break;
      case 4:
        strWeek = "四";
        break;
      case 5:
        strWeek = "五";
        break;
      case 6:
        strWeek = "六";
        break;
      case 7:
        strWeek = "日";
        break;
      default:
        break;
    }
    // 如果取整后剩下的课程数为零,直接取周五的日期
    System.out.println(
        "距离本学期结束还有 "
            + total
            + " 天,日期为:"
            + calendar.get(Calendar.YEAR)
            + "-"
            + (calendar.get(Calendar.MONTH) + 1)
            + "-"
            + calendar.get(Calendar.DATE)
            + ",星期"
            + strWeek);
  }
}

8):日期查询

  • 定义:从功能上来看,本实例至少需要定义 3 个方法,分别完成:获取指定日期所在周的周一日期、获取两个指定日期的相差天数以及获取指定日期为所在周的星期几的日期这 3 个功能,同时需要定义 Date 类型的日期与 String 类型之间的转换方法
  1. 新建Exercise类,在该类中导入需要的包

    import java.text.Parse Exception;
    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.Date;
    public class DateSearch {
      // 在该类中有5个方法,...
    }
  2. 在 Exercise 类中定义获取指定日期所在周的周一日期的方法——getMonday(),该方法是一个静态方法,返回值为 String 类型,并且需要传递一个 Date 类型的参数,然后将该日期参数作Calendar 对象的日期,并调用 Calendar 类的 set(int field,int value)方法改变 Calendar 对象中的日期,再将使用 SimpleDateFormat 类格式化后的日期返回

    /** 获取指定日期所在周的周一日期 */
    public static String getMonday(Date date) {
      Calendar c = Calendar.getInstance();
      c.setTime(date);
      c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
      return new SimpleDateFormat("yyyy-MM-dd").format(c.getTime());
    }
  3. 继续在Exercise类定义获取两个日期间相差天数的方法—getTwoDay(),在该方法的主体中,首先需要创建一个 SimpleDateFormat 类对象,并指定日期的格式,然后需要调用SimpleDateFormat 对象的 parse() 方法,将传递过来的两个 String 类型的日期转换为 Date 类型,并将这两个 Date 类型的日期进行运算,得出相差的天数,返回一个 int 类型的变量

    /** 获取两个日期间相差的天数 */
    public static int getTwoDay(String sj1, String sj2) {
      SimpleDateFormat myFormatter = new SimpleDateFormat("yyyy-MM-dd");
      int day = 0;
      try {
        Date date = myFormatter.parse(sj1);
        Date mydate = myFormatter.parse(sj2);
        // 计算两个日期间相差的天数
        day = (int) ((date.getTime() - mydate.getTime()) / (24 * 60 * 60 * 1000));
      } catch (ParseException e) {
        return 0;
      }
      return day;
    }
  4. 在 Exercise 类中继续创建两个静态方法,分别完成 String 类型的日期与 Date 类型日期的互换功能

    /** 将String类型的日期转换为Date类型 */
    public static Date strToDate(String strToDate) {
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
      Date date = null;
      try {
        date = sdf.parse(strToDate);
      } catch (ParseException e) {
        e.printStackTrace();
      }
      return date;
    }
    /** 将Date类型的日期转换为String类型 */
    public static String dateToStr(Date date) {
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
      return sdf.format(date);
    }
  5. 在 Exercise 类中定义获取指定日期所在周的指定星期的日期方法——getWeek(),该方法需要传递两个 String 类型的参数,其中第一个参数表示指定的日期,第二个参数表示星期几。在该方法的主体中,首先调用了 strToDate() 方法,将 String 类型的日期转换为 Date 类型的日期,并将该日期作为 Calendar 类对象日期,然后判断传递过来的第二个参数的值,使用 Calendar 类中的 set(int field,int value) 方法改变日期,从而将格式化后的日期返回

    // 获取一个日期所在周的星期几的日期,如要找出2011年5月1日所在周的星期一是几号
    // @param sdate所确定的日期形式2011-05-01
    // @param num要确定的是周几(1表示周一,2表示周二)
    // return
    public static String getWeek(String sdate, String num) {
      // 再转换为时间
      Date dd = strToDate(sdate);
      Calendar c = Calendar.getInstance();
      c.setTime(dd);
      int weekNo = Integer.parseInt(num);
      switch (weekNo) {
        case 1:
          c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
          break;
        case 2:
          c.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY);
          break;
        case 3:
          c.set(Calendar.DAY_OF_WEEK, Calendar.WEEK_OF_MONTH);
          break;
        case 4:
          c.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY);
          break;
        case 5:
          c.set(Calendar.DAY_OF_WEEK, Calendar.FEBRUARY);
          break;
        case 6:
          c.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
          break;
        case 7:
          c.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
          break;
      }
      return new SimpleDateFormat("yyyy-MM-dd").format((c.getTime()));
    }
  6. 测试

    public class DateSearch {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        Date date = new Date();
        String monday = Exercise.getMonday(date);
        System.out.print("这周一的日期为【" + monday + "】,");
        String nowStr = Exercise.DateToStr(date); // 将 Date 类型的日期转换为 String 类型
        int num = Exercise.getTwoDay(nowStr, monday);
        System.out.print("与今天相差【" + num + "】天");
        System.out.println("\n请输入您要查询本周星期几的日期(1表示星期一,2表示星期二…):");
        String weekNo = input.next(); // 获取用户输入的星期
        int no = Integer.parseInt(weekNo);
        // 将用户输入的星期转换为int类型,以便使用switch语句
        String strWeekNo = "";
        switch (no) {
          case 1:
            strWeekNo = "星期一";
            break;
          case 2:
            strWeekNo = "星期二";
            break;
          case 3:
            strWeekNo = "星期三";
            break;
          case 4:
            strWeekNo = "星期四";
            break;
          case 5:
            strWeekNo = "星期五";
            break;
          case 6:
            strWeekNo = "星期六";
            break;
          case 7:
            strWeekNo = "星期日";
            break;
        }
        System.out.println("本周【" + strWeekNo + "】的日期为:" + Exercise.getWeek(nowStr, weekNo));
      }
    }

六:Java内置包装

1):包装类、装箱和拆箱

  • 定义:Java 的设计中提倡一种思想,即一切皆对象。但是从数据类型的划分中,我们知道 Java 中的数据类型分为基本数据类型和引用数据类型,但是基本数据类型怎么能够称为对象呢?于是 Java 为每种基本数据类型分别设计了对应的类,称之为包装类(Wrapper Classes),也有地方称为外覆类或数据类型类

    基本数据类型及对应的包装类
    序号 基本数据类型 包装类
    1 byte Byte
    2 short Short
    3 int Integer
    4 long Long
    5 char Character
    6 float Float
    7 double Double
    8 boolean Boolean

1:装箱和拆箱

  • 定义:基本数据类型转换为包装类的过程称为装箱,例如把 int 包装成 Integer 类的对象;包装类变为基本数据类型的过程称为拆箱,例如把 Integer 类的对象重新简化为 int

    public class Simplification {
      public static void main(String[] args) {
        int m = 500;
        // todo 自动装箱
        Integer obj = m; 
        // todo 自动拆箱
        int n = obj; 
        System.out.println("n=" + n);
        Integer obj1 = 500;
        System.out.println("obj等价于obj1返回结果为" + obj.equals(obj1));
      }
    }

2:包装类的应用

💨:实现int和integer的相互转换
public class Simplification1 {
  public static void main(String[] args) {
    int m = 500;
    // 手动装箱
    Integer obj = new Integer(m);
    // 手动拆箱
    int n = obj.intValue();
    System.out.println("n = " + n);
    Integer obj1 = new Integer(500);
    System.out.println("obj等价于obj1的返回结果为" + obj.equals(obj1));
  }
}
💨:将字符串转换为数值类型
  • Integer 类(String 转 int 型)

    int parseInt(String s);
    s 为要转换的字符串。
  • Float 类(String 转 float 型)

    float parseFloat(String s)

    使用以上两种方法时,字符串中的数据必须由数字组成,否则转换时会出现程序错误

    public class Simplification2 {
      public static void main(String[] args) {
        String str1 = "30";
        String str2 = "30.3";
        // 将字符串变为int型
        int x = Integer.parseInt(str1);
        // 将字符串变为float型
        float f = Float.parseFloat(str2);
        System.out.println("x = " + x + ";f = " + f);
      }
    }
💨:将整数转换为字符串
  • 定义:Integer类有一个静态的toString()方法,可以将整数转换为字符串

    public class Simplification3 {
      public static void main(String[] args) {
        int m = 500;
        String s = Integer.toString(m);
        System.out.println("s=" + s);
      }
    }

2):Object类

  • 定义:Object 是 Java 类库中的一个特殊类,也是所有类的父类。也就是说,Java 允许把任何类型的对象赋给 Object 类型的变量。当一个类被定义后,如果没有指定继承的父类,那么默认父类就是 Object 类。

    public class MyClass{} 等价于 public class MyClass extends Object {}
    • 由于 Java 所有的类都是 Object 类的子类,所以任何 Java 对象都可以调用 Object 类的方法
    Object 类的常用方法
    方法 说明
    Object clone() 创建与该对象的类相同的新对象
    boolean equals(Object) 比较两对象是否相等
    void finalize() 当垃圾回收器确定不存在对该对象的更多引用时,对象垃圾回收器调用该方法
    Class getClass() 返回一个对象运行时的实例类
    int hashCode() 返回该对象的散列码值
    void notify() 激活等待在该对象的监视器上的一个线程
    void notifyAll() 激活等待在该对象的监视器上的全部线程
    String toString() 返回该对象的字符串表示
    void wait() 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待

    其中,toString()、equals() 方法和 getClass() 方法在 Java 程序中比较常用

1:toString()

  • toString() 方法返回该对象的字符串,当程序输出一个对象或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的 toString() 方法返回该对象的字符串表示。

    Object 类的 toString() 方法返回“运行时类名@十六进制哈希码”格式的字符串,但很多类都重写了 Object 类的 toString() 方法,用于返回可以表述该对象信息的字符串

    哈希码(hashCode),每个 Java 对象都有哈希码属性,哈希码可以用来标识对象,提高对象在集合操作中的执行效率。

    // 定义Demo类,实际上继承Object类
    class Demo {}
    /** @author Admin */
    public class ObjectDemo01 {
      public static void main(String[] args) {
        Demo d = new Demo();
        System.out.println("不加toString()输出:" + d);
        System.out.println("加上toString()输出:" + d.toString());
      }
    }
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String toString() {
        return "姓名:" + this.name + ":年龄" + this.age;
    }
    
    public static void main(String[] args) {
        Person person = new Person("C语言中文网", 30);
        System.out.println("对象信息:" + person);
    }

    程序中的 Person 类中重写了 Object 类中的 toString() 方法,这样直接输出对象时调用的是被子类重写过的 toString() 方法

2:equals()

  • 定义:分别是==运算符和 equals() 方法,==运算符是比较两个引用变量是否指向同一个实例,equals() 方法是比较两个对象的内容是否相等,通常字符串的比较只是关心内容是否相等

    boolean result = obj.equals(Object o);
    其中,obj 表示要进行比较的一个对象,o 表示另一个对象。
例1 (验证用户名和密码)
public class ValiTest {
  public static boolean validateLogin(String uname, String upwd) {
    boolean con = false;
    if (uname.equals("admin") && upwd.equals("admin")) {
      con = true;
    } else {
      con = false;
    }
    return con;
  }

  public static void main(String[] args) {
    Scanner input = new Scanner(System.in);
    System.out.println("--欢迎使用大数据平台--");
    System.out.println("用户名:");
    String uname = input.next();
    System.out.println("密码:");
    String pwd = input.next();
    boolean con = validateLogin(uname, pwd);
    if (con) {
      System.out.println("登录成功!");
    } else {
      System.out.println("用户名或密码有误!");
    }
  }
}

3:getClass()

  • 定义:getClass() 方法返回对象所属的类,是一个 Class 对象。通过 Class 对象可以获取该类的各种信息,包括类名、父类以及它所实现接口的名字等。
例2 (用string类型调用getclass()方法)
public class getclassTest {
  public static void printClassInfo(Object obj) {
    // 获取类名
    System.out.println("类名:" + obj.getClass().getName());
    // 获取父类名
    System.out.println("父类:" + obj.getClass().getSuperclass().getName());
    System.out.println("实现的接口有:");
    // 获取实现的接口并输出
    for (int i = 0; i < obj.getClass().getInterfaces().length; i++) {
      System.out.println(obj.getClass().getInterfaces()[i]);
    }
  }

  public static void main(String[] args) {
    String strObj = new String();
    printClassInfo(strObj);
  }
}

4:接收任意引用类型的对象

  • 定义:既然 Object 类是所有对象的父类,则所有的对象都可以向 Object 进行转换,在这其中也包含了数组和接口类型,即一切的引用数据类型都可以使用 Object 进行接收

    interface A {
        public String getInfo();
    }
    
    class B implements A {
        public String getInfo() {
            return "Hello World!!";
        }
    }
    
    /** @author Admin */
    public class AnyType {
      public static void main(String[] args) {
        // 接口实例化
        A a = new B();
        // 对象向上转型
        Object obj = a;
        // 对象向下转型
        A x = (A) obj;
        System.out.println(x.getInfo());
      }
    }
    • 虽然接口不能继承一个类,但是依然是 Object 类的子类,因为接口本身是引用数据类型,所以可以进行向上转型操作
  • 同理,也可以使用 Object 接收一个数组,因为数组本身也是引用数据类型

    public class ObjectTest {
      public static void main(String[] args) {
        int temp[] = {1, 3, 5, 7, 9};
        // 使用object接受数组
        Object obj = temp;
        // 传递数组引用
        print(obj);
      }
    
      public static void print(Object O) {
        // 判断对象的类型
        if (O instanceof int[]) {
          // 向下转型
          int x[] = (int[]) O;
          // 循环输出
          for (int i = 0; i < x.length; i++) {
            System.out.println(x[i] + "\t");
          }
        }
      }
    }

3):Integer类

  • 定义:Integer 类在对象中包装了一个基本类型 int 的值。Integer 类对象包含一个 int 类型的字段。此外,该类提供了多个方法,能在 int 类型和 String 类型之间互相转换,还提供了处理 int 类型时非常有用的其他一些常量和方法

1:Integer类的构造方法

  • Integer 类中的构造方法有以下两个:

    • Integer(int value):构造一个新分配的 Integer 对象,它表示指定的 int 值。

    • Integer(String s):构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值。

    Integer integer1 = new Integer(100);    // 以 int 型变量作为参数创建 Integer 对象
    Integer integer2 = new Integer("100");    // 以 String 型变量作为参数创建 Integer 对象

2:Integer类的常用方法

  • Integer 类内部包含一些和 int 类型操作有关的方法

    Integer类中的常用方法
    方法 返回值 功能
    byteValue() byte 以 byte 类型返回该 Integer 的值
    shortValue() short 以 short 类型返回该 Integer 的值
    intValue() int 以 int 类型返回该 Integer 的值
    toString() String 返回一个表示该 Integer 值的 String 对象
    equals(Object obj) boolean 比较此对象与指定对象是否相等
    compareTo(Integer
    anotherlnteger)
    int 在数字上比较两个 Integer 对象,如相等,则返回 0;
    如调用对象的数值小于 anotherlnteger 的数值,则返回负值;
    如调用对象的数值大于 anotherlnteger 的数值,则返回正值
    valueOf(String s) Integer 返回保存指定的 String 值的 Integer 对象
    parseInt(String s) int 将数字字符串转换为 int 数值
    • 在实际的编程过程中,经常将字符串转换为 int 类型的数值,或者将 int 类型的数值转换为对应的字符串
    String str = "456";
    int num = Integer.parseInt(str);    // 将字符串转换为int类型的数值
    int i = 789;
    String s = Integer.toString(i);    // 将int类型的数值转换为字符串 
    注意:在实现将字符串转换为 int 类型数值的过程中,如果字符串中包含非数值类型的字符,则程序执行将出现异常。
    public class IntegerTest {
      public static void main(String[] args) {
        int num = 40;
        // 将数字转换成字符串
        String str = Integer.toString(num);
        // 将数字转换成二进制
        String str1 = Integer.toBinaryString(num);
        // 将数字转换成八进制
        String str2 = Integer.toHexString(num);
        // 将数字转换成十六进制
        String str3 = Integer.toOctalString(num);
        System.out.println(str + "的二进制数是:" + str1);
        System.out.println(str + "的八进制数是:" + str3);
        System.out.println(str + "的十进制数是:" + str);
        System.out.println(str + "的十六进制数是:" + str2);
      }
    }

3:Integer类的常量

  • Integer 类包含以下 4 个常量。

    • MAX_VALUE:值为 231-1 的常量,它表示 int 类型能够表示的最大值。

    • MIN_VALUE:值为 -231 的常量,它表示 int 类型能够表示的最小值。

    • SIZE:用来以二进制补码形式表示 int 值的比特位数。

    • TYPE:表示基本类型 int 的 Class 实例。

    int max_value = Integer.MAX_VALUE;    // 获取 int 类型可取的最大值
    int min_value = Integer.MIN_VALUE;    // 获取 int 类型可取的最小值
    int size = Integer.SIZE;    // 获取 int 类型的二进制位
    Class c = Integer.TYPE;    // 获取基本类型 int 的 Class 实例

4):Float类

  • 定义:Float 类在对象中包装了一个基本类型 float 的值。Float 类对象包含一个 float 类型的字段。此外,该类提供了多个方法,能在 float 类型与 String 类型之间互相转换,同时还提供了处理 float 类型时比较常用的常量和方法。

1:Float类的构造方法

  • Float 类中的构造方法有以下 3 个。

    • Float(double value):构造一个新分配的 Float 对象,它表示转换为 float 类型的参数。

    • Float(float value):构造一个新分配的 Float 对象,它表示基本的 float 参数。

    • Float(String s):构造一个新分配的 Float 对象,它表示 String 参数所指示的 float 值。

    Float float1 = new Float(3.14145);    // 以 double 类型的变量作为参数创建 Float 对象
    Float float2 = new Float(6.5);    // 以 float 类型的变量作为参数创建 Float 对象
    Float float3 = new Float("3.1415");    // 以 String 类型的变量作为参数创建 Float 对象
    Float类中的常用方法
    方法 返回值 功能
    byteValue() byte 以 byte 类型返回该 Float 的值
    doubleValue() double 以 double 类型返回该 Float 的值
    floatValue() float 以 float 类型返回该 Float 的值
    intValue() int 以 int 类型返回该 Float 的值(强制转换为 int 类型)
    longValue() long 以 long 类型返回该 Float 的值(强制转换为 long 类型)
    shortValue() short 以 short 类型返回该 Float 的值(强制转换为 short 类型)
    isNaN() boolean 如果此 Float 值是一个非数字值,则返回 true,否则返回 false
    isNaN(float v) boolean 如果指定的参数是一个非数字值,则返回 true,否则返回 false
    toString() String 返回一个表示该 Float 值的 String 对象
    valueOf(String s) Float 返回保存指定的 String 值的 Float 对象
    parseFloat(String s) float 将数字字符串转换为 float 数值
    String str = "456.7";
    float num = Float.parseFloat(str);    // 将字符串转换为 float 类型的数值
    float f = 123.4f;
    String s = Float.toString(f);    // 将 float 类型的数值转换为字符串

    在实现将字符串转换为 float 类型数值的过程中,如果字符串中包含非数值类型的字符,则程序执行将出现异常

2:Float类的常用常量

  • 在 Float 类中包含了很多常量,其中较为常用的常量如下。

    • MAX_VALUE:值为 1.4E38 的常量,它表示 float 类型能够表示的最大值。

    • MIN_VALUE:值为 3.4E-45 的常量,它表示 float 类型能够表示的最小值。

    • MAX_EXPONENT:有限 float 变量可能具有的最大指数。

    • MIN_EXPONENT:标准化 float 变量可能具有的最小指数。

    • MIN_NORMAL:保存 float 类型数值的最小标准值的常量,即 2-126。

    • NaN:保存 float 类型的非数字值的常量。

    • SIZE:用来以二进制补码形式表示 float 值的比特位数。

    • TYPE:表示基本类型 float 的 Class 实例。

    float max_value = Float.MAX_VALUE;    // 获取 float 类型可取的最大值
    float min_value = Float.MIN_VALUE;    // 获取 float 类型可取的最小值
    float min_normal = Float.MIN_NORMAL;    // 获取 float 类型可取的最小标准值
    float size = Float.SIZE;    // 获取 float 类型的二进制位

5):Double类

  • 定义:Double 类在对象中包装了一个基本类型 double 的值。Double 类对象包含一个 double 类型的字段。此外,该类还提供了多个方法,可以将 double 类型与 String 类型相互转换,同时 还提供了处理 double 类型时比较常用的常量和方法。

1:Double类的构造方法

  • Double 类中的构造方法有如下两个:

    • Double(double value):构造一个新分配的 Double 对象,它表示转换为 double 类型的参数
    • Double(String s):构造一个新分配的 Double 对象,它表示 String 参数所指示的 double 值
    Double double1 = new Double(5.456);    // 以 double 类型的变量作为参数创建 Double 对象
    Double double2 = new Double("5.456");       // 以 String 类型的变量作为参数创建 Double 对象

2:Double类的构造方法

Double类中的常用方法
方法 返回值 功能
byteValue() byte 以 byte 类型返回该 Double 的值
doubleValue() double 以 double 类型返回该 Double 的值
fioatValue() float 以 float 类型返回该 Double 的值
intValue() int 以 int 类型返回该 Double 的值(强制转换为 int 类型)
longValue() long 以 long 类型返回该 Double 的值(强制转换为 long 类型)
shortValue() short 以 short 类型返回该 Double 的值(强制转换为 short 类型)
isNaN() boolean 如果此 Double 值是一个非数字值,则返回 true,否则返回 false
isNaN(double v) boolean 如果指定的参数是一个非数字值,则返回 true,否则返回 false
toString() String 返回一个表示该 Double 值的 String 对象
valueOf(String s) Double 返回保存指定的 String 值的 Double 对象
parseDouble(String s) double 将数字字符串转换为 Double 数值
String str = "56.7809";
double num = Double.parseDouble(str);    // 将字符串转换为 double 类型的数值
double d = 56.7809;
String s = Double.toString(d);    // 将double类型的数值转换为字符串

3:Double类的常用常量

  • 在 Double 类中包含了很多常量,其中较为常用的常量如下。
    • MAX_VALUE:值为 1.8E308 的常量,它表示 double 类型的最大正有限值的常量。
    • MIN_VALUE:值为 4.9E-324 的常量,它表示 double 类型数据能够保持的最小正非零值的常量。
    • NaN:保存 double 类型的非数字值的常量。
    • NEGATIVE_INFINITY:保持 double 类型的负无穷大的常量。
    • POSITIVE_INFINITY:保持 double 类型的正无穷大的常量。
    • SIZE:用秦以二进制补码形式表示 double 值的比特位数。
    • TYPE:表示基本类型 double 的 Class 实例。

6):Number类

  • 定义:Number 是一个抽象类,也是一个超类(即父类)。Number 类属于 java.lang 包,所有的包装类(如 Double、Float、Byte、Short、Integer 以及 Long)都是抽象类 Number 的子类

    Number类的方法
    方法 说明
    byte byteValue(); 返回 byte 类型的值
    double doubleValue(); 返回 double 类型的值
    float floatValue(); 返回 float 类型的值
    int intValue(); 返回 int 类型的值
    long longValue(); 返回 long 类型的值
    short shortValue(); 返回 short 类型的值
    • 抽象类不能直接实例化,而是必须实例化其具体的子类
    Number num = new Double(12.5);
    System.out.println("返回 double 类型的值:" + num.doubleValue());
    System.out.println("返回 int 类型的值:" + num.intValue());
    System.out.println("返回 float 类型的值:" + num.floatValue());

7):Character类

  • 定义:Character 类是字符数据类型 char 的包装类。Character 类的对象包含类型为 char 的单个字段,这样能把基本数据类型当对象来处理

    Character类的常用方法
    方法 描述
    void Character(char value) 构造一个新分配的 Character 对象,用以表示指定的 char 值
    char charValue() 返回此 Character 对象的值,此对象表示基本 char 值
    int compareTo(Character anotherCharacter) 根据数字比较两个 Character 对象
    boolean equals(Character anotherCharacter) 将此对象与指定对象比较,当且仅当参数不是 null,而 是一个与此对象
    包含相同 char 值的 Character 对象时, 结果才是 true
    boolean isDigit(char ch) 确定指定字符是否为数字,如果通过 Character. getType(ch) 提供的字
    符的常规类别类型为 DECIMAL_DIGIT_NUMBER,则字符为数字
    boolean isLetter(int codePoint) 确定指定字符(Unicode 代码点)是否为字母
    boolean isLetterOrDigit(int codePoint) 确定指定字符(Unicode 代码点)是否为字母或数字
    boolean isLowerCase(char ch) 确定指定字符是否为小写字母
    boolean isUpperCase(char ch) 确定指定字符是否为大写字母
    char toLowerCase(char ch) 使用来自 UnicodeData 文件的大小写映射信息将字符参数转换为小写
    char toUpperCase(char ch) 使用来自 UnicodeData 文件的大小写映射信息将字符参数转换为大写
    Character character = new Character('S');
  • CompareTo() 方法将这个字符与其他字符比较,并且返回一个整型数组,这个值是两个字符比较后的标准代码差值。当且仅当两个字符相同时,equals() 方法的返回值才为 true

    Character character = new Character('A');
    int result1 = character.compareTo(new Character('V'));
    System.out.println(result1);    // 输出:0
    int result2 = character.compareTo(new Character('B'));
    System.out.println(resuit2);    // 输出:-1
    int result3 = character.compareTo(new Character('1'));
    System.out.println(result3);    // 输出:-2

例1 (注册会员验证用户名、密码、年龄)

public class Registration {
  public static boolean validateUser(String uname, String upwd, String age) {
    // 用户名是否符合要求
    boolean conUname = false;
    // 密码是否符合要求
    boolean conPwd = false;
    // 年龄是否符合要求
    boolean conAge = false;
    boolean con = false;
    if (uname.length() > 0) {
      for (int i = 0; i < uname.length(); i++) {
        // 验证用户名是否为全部字母,不能含有空格
        if (Character.isLetter(uname.charAt(i))) {
          conUname = true;
        } else {
          conAge = false;
          System.out.println("用户名只能由字母组成,且不能含有空格!");
          break;
        }
      }
    } else {
      System.out.println("用户名不能为空!");
    }
    if (upwd.length() > 0) {
      for (int i = 0; i < upwd.length(); i++) {
        // 验证密码是否由数字和字母组成,不能含有空格
        if (Character.isLetterOrDigit(upwd.charAt(i))) {
          conPwd = true;
        } else {
          conPwd = false;
          System.out.println("密码只能由数字或字母组成!");
          break;
        }
      }
    } else {
      System.out.println("密码不能为空!");
    }
    if (age.length() > 0) {
      for (int i = 0; i < age.length(); i++) {
        // 验证年龄是否由数字组成
        if (Character.isDigit(age.charAt(i))) {
          conAge = true;
        } else {
          conPwd = false;
          System.out.println("年龄输入有误!");
          break;
        }
      }
    } else {
      System.out.println("年龄必须输入!");
    }
    if (conUname && conPwd && conAge) {
      con = true;
    } else {
      con = false;
    }
    return con;
  }

  public static void main(String[] args) {
    Scanner input = new Scanner(System.in);
    System.out.println("----用户注册----");
    System.out.println("用户名:");
    String username = input.next();
    System.out.println("密码:");
    String pwd = input.next();
    System.out.println("年龄:");
    String age = input.next();
    boolean con = Register.validateUser(username, pwd, age);
    if (con) {
      System.out.println("注册成功!");
    } else {
      System.out.println("注册失败!");
    }
  }
}

8):Boolean类

  • 定义:Boolean 类将基本类型为 boolean 的值包装在一个对象中。一个 Boolean 类的对象只包含一个类型为 boolean 的字段。此外,此类还为 boolean 和 String 的相互转换提供了很多方法,并提供了处理 boolean 时非常有用的其他一些常用方法。

1:Boolean类的构造方法

  • Boolean 类有以下两种构造形式:

    Boolean(boolean boolValue);
    Boolean(String boolString); 
    • 其中 boolValue 必须是 true 或 false(不区分大小写),boolString 包含字符串 true(不区分大小写),那么新的 Boolean 对象将包含 true;否则将包含 false

2:Boolean类的常用方法

Boolean 类中的常用方法
方法 返回值 功能
booleanValue() boolean 将 Boolean 对象的值以对应的 boolean 值返回
equals(Object obj) boolean 判断调用该方法的对象与 obj 是否相等。当且仅当参数不是 null,且与调用该
方法的对象一样都表示同一个 boolean 值的 Boolean 对象时,才返回 true
parseBoolean(String s) boolean 将字符串参数解析为 boolean 值
toString() string 返回表示该 boolean 值的 String 对象
valueOf(String s) boolean 返回一个用指定的字符串表示的 boolean 值
public class BooleanTest {
  public static void main(String[] args) {
    boolean b1 = new Boolean(true);
    boolean b2 = new Boolean("ok");
    boolean b3 = new Boolean("true");
    System.out.println("b1 转换为 boolean 值是:" + b1);
    System.out.println("b2 转换为 boolean 值是:" + b2);
    System.out.println("b3 转换为 boolean 值是:" + b3);
  }
}

3:Boolean类的常用常量

  • 在 Boolean 类中包含了很多的常量,其中较为常用的常量如下:
    • TRUE:对应基值 true 的 Boolean 对象。
    • FALSE:对应基值 false 的 Boolean 对象。
    • TYPE:表示基本类型 boolean 的 Class 对象。

9):Byte类

  • 定义:Byte 类将基本类型为 byte 的值包装在一个对象中。一个 Byte 类的对象只包含一个类型为 byte 的字段。此外,该类还为 byte 和 String 的相互转换提供了方法,并提供了一些处理 byte 时非常有用的常量和方法。

1:Byte类的构造方法

  • Byte 类提供了两个构造方法来创建 Byte 对象:
🛩:Byte(byte value)
  • 定义:通过这种方法创建的 Byte 对象,可以表示指定的 byte 值。例如,下面的示例将 5 作为 byte 类型变量,然后再创建 Byte 对象。

    byte my_byte = 5;
    Byte b = new Byte(my_byte);
🛩:Byte(String s)
  • 定义:通过这个方法创建的 Byte 对象,可表示 String 参数所指定的 byte 值。例如,下面的示例将 5 作为 String 类型变量,然后再创建 Byte 对象。

    String my_byte = "5";
    Byte b = new Byte(my_byte);

    必须使用数值型的 String 变量作为参数才能创建成功,否则会抛出 NumberFormatException 异常

2:Byte类的常用方法

Byte 类中的常用方法
方法 返回值 功能
byteValue() byte 以一个 byte 值返回 Byte 对象
compareTo(Byte bytel) int 在数字上比较两个 Byte 对象
doubleValue() double 以一个 double 值返回此 Byte 的值
intValue() int 以一个 int 值返回此 Byte 的值
parseByte(String s) byte 将 String 型参数解析成等价的 byte 形式
toStringO String 返回表示此 byte 值的 String 对象
valueOf(String s) Byte 返回一个保持指定 String 所给出的值的 Byte 对象
equals(Object obj) boolean 将此对象与指定对象比较,如果调用该方法的对象与 obj 相等 则返回 true,否则返回 false

3:Byte类的常用方法

  • 在 Byte 类中包含了很多的常量,其中较为常用的常量如下:
    • MIN_VALUE:byte 类可取的最小值。
    • MAX_VALUE:byte 类可取的最大值。
    • SIZE:用于以二进制补码形式表示的 byte 值的位数。
    • TYPE:表示基本类 byte 的 Class 实例。

10):System类

1:System类的成员变量

  • 定义:System 类有 3 个静态成员变量,分别是 PrintStream out、InputStream in 和 PrintStream err。
🛩:PrintStream out
  • 定义:标准输出流。此流已打开并准备接收输出数据。通常,此流对应于显示器输出或者由主机环境或用户指定的另一个输出目标。

    System.out.println(data); 
    其中,println 方法是属于流类 PrintStream 的方法,而不是 System 中的方法。
🛩:InputStream in
  • 定义:标准输入流。此流已打开并准备提供输入数据。通常,此流对应于键盘输入或者由主机环境或用户指定的另一个输入源。
🛩:PrintStream err
  • 定义:标准的错误输出流。其语法与 System.out 类似,不需要提供参数就可输出错误信息。也可以用来输出用户指定的其他信息,包括变量的值。

    public class PrintStreamTest {
      public static void main(String[] args) {
        System.out.println("请输入字符,按回车键结束输入:");
        int c;
        try {
          // 读取输入的字符串
          c = System.in.read();
          // 判断输入的字符是不是回车
          while (c != '\r') {
            // 输出字符串
            System.out.print((char) c);
            c = System.in.read();
          }
        } catch (IOException e) {
          System.out.println(e.toString());
        } finally {
        System.out.println();
        }
    }
    }

    😉如果要输出汉字

    InputStreamReader in = new InputStreamReader(System.in, "GB2312");
    c = in.read();
    while(c != '\r') {
        System.out.print((char) c);
        c = in.read();
    }

2:System类的成员方法

  • 定义:System 类中提供了一些系统级的操作方法,常用的方法有 arraycopy()、currentTimeMillis()、exit()、gc() 和 getProperty()。
🛩:arraycopy()
  • 定义:该方法的作用是数组复制,即从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。

    public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length) 
  • 其中,src 表示源数组,srcPos 表示从源数组中复制的起始位置,dest 表示目标数组,destPos 表示要复制到的目标数组的起始位置,length 表示复制的个数

    public class arraycopyTest {
      public static void main(String[] args) {
        char[] srcArray = {'A', 'B', 'C', 'D'};
        char[] destArray = {'E', 'F', 'G', 'H'};
        System.arraycopy(srcArray, 1, destArray, 1, 2);
        System.out.println("源数组:");
        for (char c : srcArray) {
          System.out.println(c);
        }
        System.out.println("目标数组:");
        for (char c : destArray) {
          System.out.println(c);
        }
      }
    }
🛩:currentTimeMillis()
  • 定义:该方法的作用是返回当前的计算机时间,时间的格式为当前计算机时间与 GMT 时间(格林尼治时间)1970 年 1 月 1 日 0 时 0 分 0 秒所差的毫秒数。一般用它来测试程序的执行时间

    long m = System.currentTimeMillis();
    public class System_currentTimeMillis {
      public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
          int temp = 0;
        }
        long end = System.currentTimeMillis();
        long time = end - start;
        System.out.println("程序执行时间" + time + "秒");
      }
    }
🛩:exit()
  • 定义:该方法的作用是终止当前正在运行的 Java 虚拟机

    public static void exit(int status)

    status 的值为 0 时表示正常退出,非零时表示异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。

🛩:gc()
  • 定义:该方法的作用是请求系统进行垃圾回收,完成内存中的垃圾清除。至于系统是否立刻回收,取决于系统中垃圾回收算法的实现以及系统执行时的情况。

    public static void gc()
🛩:getProperty()
  • 定义:获得系统中属性名为 key 的属性对应的值

    public static String getProperty(String key)
    系统常见属性
    属性名 属性说明
    java.version Java 运行时环境版本
    java.home Java 安装目录
    os.name 操作系统的名称
    os.version 操作系统的版本
    user.name 用户的账户名称
    user.home 用户的主目录
    user.dir 用户的当前工作目录
    public class getPropertyTest {
      public static void main(String[] args) {
        String jversion = System.getProperty("java.version");
        String oName = System.getProperty("os.name");
        String user = System.getProperty("user.name");
        System.out.println("Java运行时的环境版本:" + jversion);
        System.out.println("当前的操作系统时:" + oName);
        System.out.println("当前用户是:" + user);
      }
    }

七:Java数组

1):数组简介

  • 定义:数组(array)是一种最简单的复合数据类型,它是有序数据的集合,数组中的每个元素具有相同的数据类型,可以用一个统一的数组名和不同的下标来确定数组中唯一的元素。根据数组的维度,可以将其分为一维数组、二维数组和多维数组等

  • 数组具有以下特点:

    • 数组可以是一维数组、二维数组或多维数组。
    • 数值数组元素的默认值为 0,而引用元素的默认值为 null。
    • 数组的索引从 0 开始,如果数组有 n 个元素,那么数组的索引是从 0 到(n-1)。
    • 数组元素可以是任何类型,包括数组类型。
    • 数组类型是从抽象基类 Array 派生的引用类型。

2):一维数组

  • 定义:当数组中每个元素都只带有一个下标时,这种数组就是“一维数组”。一维数组(one-dimensional array)实质上是一组相同类型数据的线性集合,是数组中最简单的一种数组。

    数组是引用数据类型,引用数据类型在使用之前一定要做两件事情:声明和初始化

1:创建一维数组

  • 使用一个数组,必须声明一个引用该数组的变量,并指明整个变量可以引用的数组类型

    type[] arrayName;    // 数据类型[] 数组名; 
    type arrayName[];    // 数据类型 数组名[];
    int[] score;    // 存储学生的成绩,类型为整型
    double[] price;    // 存储商品的价格,类型为浮点型
    String[] name;    // 存储商品名称,类型为字符串型

2:分配空间

  • 定义:声明了数组,只是得到了一个存放数组的变量,并没有为数组元素分配内存空间,不能使用 在 Java 中可以使用 new 关键字来给数组分配空间

    arrayName = new type[size];    // 数组名 = new 数据类型[数组长度];
    score = new int[10];
    price = new double[30];
    name = new String[20];

    声明数组时就给它分配空间

    type[] arrayName = new type[size];    // 数据类型[] 数组名 = new 数据类型[数组长度];
    int[] arr = new int[5];

    方括号“[]”中的值为数组的下标。数组通过下标来区分数组中不同的元素,并且下标是从 0 开始的。因此这里包含 5 个元素的 arr 数组最大下标为 4

    一旦声明了数组的大小,就不能再修改。这里的数组长度也是必需的,不能少

  • 尽管数组可以存储一组基本数据类型的元素,但是数组整体属于引用数据类型。当声明一个数组变量时,其实是创建了一个类型为“数据类型[]”(如 int[]、double[]、String[])的数组对象

    数组的方法和属性
    方法 名称 返回值
    clone Object
    equals(Object obj) Class
    hashCode() int
    notify() void
    notify All() void
    toString() String
    wait() void
    wait(long timeout) void
    wait(long timeout,int nanos) void
    属性 length int

3:初始化一维数组

  • 定义:数组在初始化数组的同时,可以指定数组的大小,也可以分别初始化数组中的每一个元素。
👌:使用new指定数组大小后进行初始化
  • 使用 new 关键字创建数组,在创建时指定数组的大小

    type[] arrayName = new int[size];
    创建数组之后,元素的值并不确定,需要为每一个数组的元素进行赋值,其下标从 0 开始
    int[] number = new int[5];
    number[0] = 1;
    number[1] = 2;
    number[2] = 3;
    number[3] = 5;
    number[4] = 8;
  • 如果程序员只指定了数组的长度,那么系统将负责为这些数组元素分配初始值。指定初始值时,系统按如下规则分配初始值:

    • 数组元素的类型是基本类型中的整数类型(byte、short、int 和 long),则数组元素的值是 0。
    • 数组元素的类型是基本类型中的浮点类型(float、double),则数组元素的值是 0.0。
    • 数组元素的类型是基本类型中的字符类型(char),则数组元素的值是‘\u0000’。
    • 数组元素的类型是基本类型中的布尔类型(boolean),则数组元素的值是 false。
    • 数组元素的类型是引用类型(类、接口和数组),则数组元素的值是 null。
👌:使用new指定数组元素的值
  • 使用上述方式初始化数组时,只有在为元素赋值时才确定值。可以不使用上述方式,而是在初始化时就已经确定值

    type[] arrayName = new type[]{1,2,3,4,• • •,值 n};
    int[] number = new int[]{1, 2, 3, 5, 8};
    注意:不要在进行数组初始化时,既指定数组的长度,也为每个数组元素分配初始值,这样会造成代码错误。
👌:直接指定数组元素的值
type[] arrayName = {1,2,3,...,值 n};
int[] number = {1,2,3,5,8};

4:获取单个元素

  • 定义:获取单个元素是指获取数组中的一个元素,如第一个元素或最后一个元素。获取单个元素的方法非常简单,指定元素所在数组的下标即可。

    arrayName[index];

    其中,arrayName 表示数组变量,index 表示下标,下标为 0 表示获取第一个元素,下标为 array.length-1 表示获取最后一个元素。当指定的下标值超出数组的总长度时,会拋出 ArraylndexOutOfBoundsException 异常。

    int[] number = {1,2,3,5,8};
    System.out.println("获取第一个元素:"+number[0]);
    System.out.println("获取最后一个元素:"+number[number.length-1]);
    System.out.println("获取第6个元素:"+number[5]);
    public class ArrayTest {
      public static void main(String[] args) {
        // 声明数组并分配空间
        int[] prices = new int[5];
        // 接收用户从控制输入的数据
        Scanner input = new Scanner(System.in);
        for (int i = 0; i < prices.length; i++) {
          System.out.println("请输入第" + (i + 1) + "件商品的价格:");
          // 接收用户从控制台输入的数据
          prices[i] = input.nextInt();
        }
        System.out.println("第三件商品的价格为:" + prices[2]);
      }
    }

5:获取全部元素

  • 定义:获取全部元素——使用循环语句。

    public class ArraysTest {
      public static void main(String[] args) {
        int[] number = {1, 2, 3, 6, 7};
        for (int i = 0; i < number.length; i++) {
          System.out.println("第" + (i + 1) + "个元素的值是:" + number[i]);
        }
        for (int val : number) {
          System.out.print("元素的值依次是:" + val + "\t");
        }
      }
    }

3):二维数组

1:创建二维数组

  • 定义:二维数组被看作数组的数组,即二维数组为一个特殊的一维数组,其每个元素又是一个一维数组。Java 并不直接支持二维数组,但是允许定义数组元素是一维数组的一维数组,

    type arrayName[][];    // 数据类型 数组名[][];
    type 表示二维数组的类型,arrayName 表示数组名称,第一个中括号表示行,第二个中括号表示列。
    int[][] age;
    char[][] sex;

2:初始化二维数组

  • 定义:二维数组可以初始化,和一维数组一样,可以通过 3 种方式来指定元素的初始值。

    type[][] arrayName = new type[][]{1,2,3,,值 n};    // 在定义时初始化
    type[][] arrayName = new type[size1][size2];    // 给定空间,在赋值
    type[][] arrayName = new type[size][];    // 数组第二维长度为空,可变化
👌:第一种方式声明 int 类型的二维数组,然后初始化该二维数组
int[][] temp = new int[][]{{1,2},{3,4}};

👌:第二种方式声明 int 类型的二维数组,然后初始化该二维数组
int[][] temp = new int[2][2];
👌:第三种方式声明 int 类型的二维数组,然后初始化该二维数组
int[][] temp = new int[2][];

3:获取单个元素

  • 定义:当需要获取二维数组中元素的值时,也可以使用下标来表示

    arrayName[i-1][j-1];
    • 其中,arrayName 表示数组名称,i 表示数组的行数,j 表示数组的列数。例如,要获取第二行第二列元素的值,应该使用 temp[1][1]来表示。这是由于数组的下标起始值为 0,因此行和列的下标需要减 1。
    public class GetArrayNumTest {
      public static void main(String[] args) {
        double[][] class_score = {
          {10, 20, 99, 99}, {100, 12, 13}, {10, 34, 5, 6}, {232, 45, 67, 8, 2.2}
        };
        System.out.println("第二行第二列元素的值:" + class_score[1][1]);
        System.out.println("第四行第一列元素的值:" + class_score[3][0]);
      }
    }

4:获取全部元素

  • 定义:在二维数组中,直接使用 length 属性获取的是数组的行数,在指定的索引后加上 length(如 array[0].length)表示的是该行拥有多少个元素,即列数。 简单方法,则使用嵌套 for 循环(2 层 for 循环)。

      public class AllArrayTest {
        public static void main(String[] args) {
          double[][] class_score = {{100, 99, 99}, {100, 98, 97}, {100, 100, 99.5}, {99.5, 99, 98.5}};
          for (int i = 0; i < class_score.length; i++) { // 遍历行
            for (int j = 0; j < class_score[i].length; j++) {
              System.out.println("class_score[" + i + "][" + j + "]=" + class_score[i][j]);
            }
          }
        }
    }
    public class DemoTest {
      /** 随机5行5列二点矩阵 */
      public static void main(String[] args) {
        // 创建一个二维矩阵
        int[][] matrix = new int[5][5];
        for (int i = 0; i < matrix.length; i++) {
          for (int j = 0; j < matrix[i].length; j++) {
            matrix[i][j] = (int) (Math.random() * 10);
          }
        }
        System.out.println("输出矩阵");
        for (int i = 0; i < matrix.length; i++) {
          for (int j = 0; j < matrix[i].length; j++) {
            System.out.print(matrix[i][j] + "");
          }
          System.out.println();
      }
      }
    }
    for each 循环语句
    for (double[] row : a) {
        for (double value : row) {
          ......
        }
    }
    public class DemoTest {
      /** foreach输出二维矩阵 */
      public static void main(String[] args) {
        double[][] class_score = {{100, 99, 99}, {100, 98, 97}, {100, 100, 99.5}, {99.5, 99, 98.5}};
        for (double[] row : class_score) {
          for (double value : row) {
            System.out.println(value);
          }
        }
      }
    }
  • 快速输出二维数组

    System.out.println(Arrays.deepToString(arrayName));

5:获取整行元素

  • 定义:获取指定行的元素时,需要将行数固定,然后只遍历该行中的全部列即可。

    public class GetLineTest {
      public static void main(String[] args) {
        double[][] class_score = {{100, 99, 99}, {100, 98, 97}, {100, 100, 99.5}, {99.5, 99, 98.5}};
        Scanner scan = new Scanner(System.in);
        System.out.println("当前数组只有" + class_score.length + "行,您想查看第几行的元素?请输入:");
        int number = scan.nextInt();
        for (int j = 0; j < class_score[number - 1].length; j++) {
          System.out.println("第" + number + "行的第[" + j + "]个元素的值是:" + class_score[number - 1][j]);
        }
      }
    }

6:获取整列元素

  • 定义:获取指定列的元素与获取指定行的元素相似,保持列不变,遍历每一行的该列即可。

    public class GetcolumnTest {
      public static void main(String[] args) {
        double[][] class_score = {{100, 99, 99}, {100, 98, 97}, {100, 100, 99.5}, {99.5, 99, 98.5}};
        Scanner scan = new Scanner(System.in);
        System.out.println("您要获取哪一列的值?请输入:");
        int number = scan.nextInt();
        for (int i = 0; i < class_score.length; i++) {
          System.out.println("第 " + (i + 1) + " 行的第[" + number + "]个元素的值是" + class_score[i][number]);
        }
      }
    }

4):多维数组

  • 定义:三维数组、四维数组和五维数组等,它们都属于多维数组

    public class arraysTest {
      public static void main(String[] args) {
        String[][][] namelist = {
          {{"张阳", "李风", "陈飞"}, {"乐乐", "飞飞", "小曼"}},
          {{"Jack", "Kimi"}, {"Lucy", "Lily", "Rose"}},
          {{"徐璐璐", "陈海"}, {"李丽丽", "陈海清"}}
        };
        for (int i = 0; i < namelist.length; i++) {
          for (int j = 0; j < namelist[i].length; j++) {
            for (int k = 0; k < namelist[i][j].length; k++) {
              System.out.println("namelist[" + i + "][" + j + "][" + k + "]=" + namelist[i][j][k]);
            }
          }
        }
      }
    }

5):不规则数组

  • 定义:规则的 4×3 二维数组有 12 个元素,而不规则数组就不一定了。

    int intArray[][] = {{1,2}, {11}, {21,22,23}, {31,32,33}};
    • 高维数组(二维以及二维以上的数组称为高维数组)是 4 个元素,但是低维数组元素个数不同,如图 1 所示,其中第 1 个数组有两个元素,第 2 个数组有 1 个元素,第 3 个数组有 3 个元素,第 4 个数组有 3 个元素。这就是不规则数组

    • 动态初始化不规则数组比较麻烦,不能使用 new int[4][3] 语句,而是先初始化高维数组,然后再分别逐个初始化低维数组

      int intArray[][] = new int[4][]; //先初始化高维数组为4
      // 逐一初始化低维数组
      intArray[0] = new int[2];
      intArray[1] = new int[1];
      intArray[2] = new int[3];
      intArray[3] = new int[3];

      public class DemoTest {
        public static void main(String[] args) {
          // 先初始化高维数组为4
          int intArray[][] = new int[4][];
          // 逐一初始化低维数组
          intArray[0] = new int[2];
          intArray[1] = new int[1];
          intArray[2] = new int[3];
          intArray[3] = new int[3];
          // for循环遍历
          for (int i = 0; i < intArray.length; i++) {
            for (int j = 0; j < intArray[i].length; j++) {
              intArray[i][j] = i + j;
            }
          }
          // for-each循环遍历
          for (int[] row : intArray) {
            for (int column : row) {
              System.out.print(column);
              // 在元素之间添加制表符,
              System.out.print('\t');
            }
            // 一行元素打印完成后换行
            System.out.println();
          }
          // 发生运行期错误
        System.out.println(intArray[0][2]);
        }
      }
      • 第 18 行和第 19 行采用 for-each 循环遍历不规则数组,其中代码第 18 行 for-each 循环取出的数据是 int 数组,所以 row 类型是 int[]。代码第 19 行 for-each 循环取出的数据是 int 数据,所以 column 的类型 int。另外,注意代码第 27 行试图访问 intArray[0][2]元素,由于 [0][2] 不存在所以会发生下标越界异常。

6):Arrays工具类

  • 定义:Arrays 类是一个工具类,其中包含了数组操作的很多方法。这个 Arrays 类里均为 static 修饰的方法(static 修饰的方法可以直接通过类名调用),可以直接通过 Arrays.xxx(xxx) 的形式调用方法。

💨:Arrays 类 static 修饰的方法

1:int binarySearch(type[] a,type key)
  • 定义:使用二分法查询 key 元素值在 a 数组中出现的索引,如果 a 数组不包含 key 元素值,则返回负数。调用该方法时要求数组中元素己经按升序排列,这样才能得到正确结果。
2:int binarySearch(type[] a,int formIndex,int toIndex,type key)
  • 定义:这个方法与前一个方法类似,但它只搜索 a 数组中 fromIndex 到 toIndex 索引的元素。调用该方法时要求数组中元素己经按升序排列,这样才能得到正确结果。
3:type copyOf(type[] original,int length)
  • 定义:这个方法将会把 original 数组复制成一个新数组,其中 length 是新数组的长度。如果 length 小于 original 数组的长度,则新数组就是原数组的前面 length 个元素,如果 length 大于 original 数组的长度,则新数组的前面元索就是原数组的所有元素,后面补充 0(数值类型)、false(布尔类型)或者 null(引用类型)。
4:type copyOfRange(type[] original, int from, int to)
  • 定义:这个方法与前面方法相似,但这个方法只复制 original 数组的 from 索引到 to 索引的元素。
5:boolean equals(type[] a, type[] a2)
  • 定义:如果 a 数组和 a2 数组的长度相等,而且 a 数组和 a2 数组的数组元素也一一相同,该方法将返回 true。
6:void fill(type[] a, type val)
  • 定义:该方法将会把 a 数组的所有元素都赋值为 val。
7:void fill(type[] a, int fromIndex, int toIndex, type val)
  • 定义:该方法与前一个方法的作用相同,区别只是该方法仅仅将 a 数组的 fromIndex 到 toIndex 索引的数组元素赋值为 val。
8:void sort(type[] a)
  • 定义:该方法对 a 数组的数组元素进行排序。
9:void sort(type[] a, int fromIndex, int toIndex)
  • 定义:该方法与前一个方法相似,区别是该方法仅仅对 fromIndex 到 toIndex 索引的元素进行排序。
10:String toString(type[] a)
  • 定义:该方法将一个数组转换成一个字符串。该方法按顺序把多个数组元素连缀在一起,多个数组元素使用英文逗号,和空格隔开。
public class ArraysTest {
  public static void main(String[] args) {
    // 定义一个a数组
    int[] a = new int[] {3, 4, 5, 6};
    // 定义一个a2数组
    int[] a2 = new int[] {3, 4, 5, 6};
    // a数组和a2数组的长度相等,毎个元素依次相等,将输出true
    System.out.println("a数组和a2数组是否相等:" + Arrays.equals(a, a2));
    // 通过复制a数组,生成一个新的b数组
    int[] b = Arrays.copyOf(a, 6);
    System.out.println("a数组和b数组是否相等:" + Arrays.equals(a, b));
    // 输出b数组的元素,将输出[3, 4, 5, 6, 0, 0]
    System.out.println("b 数组的元素为:" + Arrays.toString(b));
    // 将b数组的第3个元素(包括)到第5个元素(不包括)賦值为1
    Arrays.fill(b, 2, 4, 1);
    // 输出b数组的元素,将输出[3, 4, 1, 1, 0, 0]
    System.out.println("b 数组的元素为:" + Arrays.toString(b));
    // 对b数组进行排序
    Arrays.sort(b);
    // 输出b数组的元素.将输出[0,0,1,1,3,4]
    System.out.println("b数组的元素为:" + Arrays.toString(b));
  }
}

💨:Java 8 为 Arrays 类增加的工具方法

1:oid parallelPrefix(xxx[] array, XxxBinaryOperator op)
  • 定义:该方法使用 op 参数指定的计算公式计算得到的结果作为新的元素。op 计算公式包括 left、right 两个形参,其中 left 代表数组中前一个索引处的元素,right 代表数组中当前索引处的元素,当计算第一个新数组元素时,left 的值默认为 1。
2:void parallelPrefix(xxx[] array, int fromIndex, int toIndex, XxxBinaryOperator op)
  • 定义:该方法与上一个方法相似,区别是该方法仅重新计算 fromIndex 到 toIndex 索引的元素。
3)void setAll(xxx[] array, IntToXxxFunction generator)
  • 定义:该方法使用指定的生成器(generator)为所有数组元素设置值,该生成器控制数组元素的值的生成算法。
4)void parallelSetAll(xxx[] array, IntToXxxFunction generator)
  • 定义:该方法的功能与上一个方法相同,只是该方法增加了并行能力,可以利用多 CPU 并行来提高性能。
5)void parallelSort(xxx[] a)
  • 定义:该方法的功能与 Arrays 类以前就有的 sort() 方法相似,只是该方法增加了并行能力,可以利用多 CPU 并行来提高性能。
6)void parallelSort(xxx[] a,int fromIndex, int toIndex)
  • 定义:该方法与上一个方法相似,区別是该方法仅对 fromIndex 到 toIndex 索引的元素进行排序。
7)Spliterator.OfXxx spliterator(xxx[] array)
  • 定义:将该数组的所有元素转换成对应的 Spliterator 对象。
8)Spliterator.OfXxx spliterator(xxx[] array, int startInclusive, int endExclusive)
  • 定义:该方法与上一个方法相似,区别是该方法仅转换 startInclusive 到 endExclusive 索引的元素。
9)XxxStream stream(xxx[] array)
  • 定义:该方法将数组转换为 Stream,Stream 是 Java 8 新增的流式编程的 API。
10)XxxStream stream(xxx[] array, int startInclusive, int endExclusive)
  • 定义:该方法与上一个方法相似,区别是该方法仅将 fromIndex 到 toIndex 索引的元索转换为 Stream。
public class ArraysTest1 {
  public static void main(String[] args) {
    int[] arr1 = new int[] {3, 4, 25, 16, 30, 18};
    // 对数组arr1进行并发排序
    Arrays.parallelSort(arr1);
    System.out.println(Arrays.toString(arr1));
    int[] arr2 = new int[] {13, -4, 25, 16, 30, 18};
    Arrays.parallelPrefix(
        arr2,
        new IntBinaryOperator() {
          // left 代表数组中前一个索引处的元素,计算第一个元素时,left为1
          // right代表数组中当前索引处的元素
          @Override
          public int applyAsInt(int left, int right) {
            return left * right;
          }
        });
    System.out.println(Arrays.toString(arr2));
    int[] arr3 = new int[5];
    Arrays.parallelSetAll(
        arr3,
        new IntUnaryOperator() {
          // operand代表正在计算的元素索引
          @Override
          public int applyAsInt(int operand) {
            return operand * 5;
          }
        });
    System.out.println(Arrays.toString(arr3));
  }
}

7):数组和字符串相互转换

1:字符串转换为数组

💨:toCharArray() 方法将字符串转换为字符数组
String str = "123abc";
char[] arr = str.toCharArray();    // char数组
for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);    // 输出1 2 3 a b c
}
💨:split() 分割字符串,返回的是一个数组
String str = "123abc";
String[] arr = str.split("");
for (int i = 0; i < arr.length; i++) { // String数组
    System.out.print(arr[i]); // 输出 1 2 3 a b c
}
  • 使用 split() 方法注意如下:
    • 如果用“.”或“|”作为分隔的话,必须是如下写法,String.split("\.") 或 String.split("\|"),这样才能正确的分隔开,不能用 String.split(".") 或 String.split("|")
    • 如果在一个字符串中有多个分隔符,可以用“|”作为连字符,如“acount=? and uu =? or n=?”,把三个都分隔出来,可以用 String.split(“and|or”);
    • 如果想在串中使用“”字符,则也需要转义。首先要表达“aaaa\bbbb”这个串就应该用”aaaa\bbbb”,这样分隔才能得到正确结果。
💨:如果要返回 byte 数组就直接使用 getBytes 方法就可以了
String str = "123abc" ;
byte [] arr = str.getBytes();
💨:使用 StringUtils 中的 join 函数,使用前需先引入 common-lang3 的 jar 包
public class StringUtilsTest {
  public static void main(String[] args) {
    //    StringUtils.join(null) = null;
    //    StringUtils.join([]) = "";
    //    StringUtils.join([null])= "";
    //    StringUtils.join(["a", "b", "c"]) = "abc";
    //    StringUtils.join([null, "", "a"]) = "a";
    String[] str = {"1", "2", "a", "b"};
    // 将数组变成字符串
    StringUtils.join(str);
    String Arr = StringUtils.join(str);
    System.out.println(Arr);
    // 将数组用逗号分隔变成字符串
    StringUtils.join(str, ",");
    String Arr1 = StringUtils.join(str, ",");
    System.out.println(Arr1);
  }
}

2:数组转换为字符串

💨:char 字符数组转化为字符串,使用 String.copyValueOf(charArray) 函数实现
char[] arr = { 'a', 'b', 'c' };
String string = String.copyValueOf(arr);
System.out.println(string); // 输出abc
💨:String 字符串数组转化为字符串
String[] arr = { "123", "abc" };
StringBuffer sb = new StringBuffer();
for (int i = 0; i < arr.length; i++) {
    sb.append(arr[i]); // String并不拥有append方法,所以借助 StringBuffer
}
String sb1 = sb.toString();
System.out.println(sb1); // 输出123abc

8):比较两个数组是否相等

  • 定义:数组相等的条件不仅要求数组元素的个数必须相等,而且要求对应位置的元素也相等。Arrays 类提供了 equals() 方法比较整个数组

    Arrays.equals(arrayA, arrayB);
    其中,arrayA 是用于比较的第一个数组,arrayB 是用于比较的第二个数组。
    public class CompareTest {
      public static void main(String[] args) {
        double[] score1 = {99, 100, 98.5, 96.5, 72};
        double[] score2 = new double[5];
        score2[0] = 99;
        score2[1] = 100;
        score2[2] = 98.5;
        score2[3] = 96.5;
        score2[4] = 72;
        double[] score3 = {99, 96.5, 98.5, 100, 72};
        if (Arrays.equals(score1, score2)) {
          System.out.println("score1 数组和 score2 数组相等");
        } else {
          System.out.println("score1 数组和 score2 数组不等");
        }
        if (Arrays.equals(score1, score3)) {
          System.out.println("score1 数组和 score3 数组相等");
        } else {
          System.out.println("score1 数组和 score3 数组不等");
        }
      }
    }

9):数组填充

  • 定义:Arrays 类提供了一个 fill() 方法,可以在指定位置进行数值填充。fill() 方法虽然可以填充数组,但是它的功能有限制,只能使用同一个数值进行填充

    Arrays.fill(array,value);
    其中,array 表示数组,value 表示填充的值。
    public class FillTest {
      public static void main(String[] args) {
        int[] number = new int[5];
        System.out.println("number —共有 " + number.length + " 个元素,它们分别是:");
        for (int i = 0; i < number.length; i++) {
          Arrays.fill(number, i);
          System.out.println("number[" + i + "]=" + i);
        }
      }
    }

10):数组查找指定元素

  • 定义:查找数组是指从数组中查询指定位置的元素,或者查询某元素在指定数组中的位置。使用 Arrays 类的 binarySearch() 方法可以实现数组的查找,该方法可使用二分搜索法来搜索指定数组,以获得指定对象,该方法返回要搜索元素的索引值

    binarySearch() 方法有多种重载形式来满足不同类型数组的查找需要,常用的重载形式有两种

1:数组范围内查找元素

binarySearch(Object[] a,Object key);
  • a 表示要搜索的数组,key 表示要搜索的值。如果 key 包含在数组中,则返回搜索值的索引;否则返回 -1 或“-插入点”。插入点指搜索键将要插入数组的位置,即第一个大于此键的元素索引

    在进行数组查询之前,必须对数组进行排序(可以使用 sort() 方法)。如果没有对数组进行排序,则结果是不确定的。如果数组包含多个带有指定值的元素,则无法确认找到的是哪一个

    public class binarySearchEmTest {
      public static void main(String[] args) {
        double[] score = {99.5, 100, 98, 97.5, 100, 95, 85.5, 100};
        Arrays.sort(score);
        int index1 = Arrays.binarySearch(score, 100);
        int index2 = Arrays.binarySearch(score, 60);
        System.out.println("查找到 100 的位置是:" + index1);
        System.out.println("查找到 60 的位置是:" + index2);
      }
    }

2:指定的范围内查找某一元素

binarySearch(Object[] a,int fromIndex,int toIndex,Object key);
  • a 表示要进行查找的数组,fromIndex 指定范围的开始处索引(包含开始处),toIndex 指定范围的结束处索引(不包含结束处),key 表示要搜索的元素。

    public class binarySearchTest {
      public static void main(String[] args) {
        double[] score = {99.5, 100, 98, 97.5, 100, 95, 85.5, 100};
        Arrays.sort(score);
        for (int i = 0; i < score.length; i++) {
          System.out.println(score[i]);
        }
        int index1 = Arrays.binarySearch(score, 6, 7, 100);
        int index2 = Arrays.binarySearch(score, 2, 6, 60);
        System.out.println("查找到 100 的位置是:" + index1);
        System.out.println("查找到 60 的位置是:" + index2);
      }
    }

11):复制数组

1:copyOf()、copyOfRange()

  • 定义:Arrays 类的 copyOf() 方法与 copyOfRange() 方法都可实现对数组的复制。copyOf() 方法是复制数组至指定长度,copyOfRange() 方法则将指定数组的指定长度复制到一个新数组中。
💨:copyOf()
Arrays.copyOf(dataType[] srcArray,int length);
  • srcArray 表示要进行复制的数组,length 表示复制后的新数组的长度。

  • 使用这种方法复制数组时,默认从原数组的第一个元素(索引值为 0)开始复制,目标数组的长度将为 length。如果 length 大于 srcArray.length,则目标数组中采用默认值填充;如果 length 小于 srcArray.length,则复制到第 length 个元素(索引值为 length-1)即止。

    目标数组如果已经存在,将会被重构

    public class CopyOfTest {
      public static void main(String[] args) {
        // 定义长度为5的数组
        int[] scores = new int[] {57, 81, 68, 75, 91};
        System.out.println("原数组内容如下:");
        // 循环遍历原数组
        for (int i = 0; i < scores.length; i++) {
          // 将原数组元素输出
          System.out.print(scores[i] + "\t");
        }
        // 定义一个新的数组,将scores数组中的5个元素赋值过来
        // 同时留3个内存空间供以后开发
        int[] newScores =
            (int[])
                Arrays.copyOf(
                    // Arrays.copyOf(scores,8) 方法复制数组之后返回的是 Object[] 类型 强制转换int[] 类型
                    scores, 8);
        System.out.println("\n赋值的新数组内容如下:");
        // 循环遍历复制后的新数组
        for (int j = 0; j < newScores.length; j++) {
          // 将新数组的元素输出
          System.out.print(newScores[j] + "\t");
        }
      }
    }
💨:CopyOfRange()
Arrays.copyOfRange(dataType[] srcArray,int startIndex,int endIndex)
  • srcArray 表示原数组。

  • startIndex 表示开始复制的起始索引,目标数组中将包含起始索引对应的元素,另外,startIndex 必须在 0 到 srcArray.length 之间。

  • endIndex 表示终止索引,目标数组中将不包含终止索引对应的元素,endIndex 必须大于等于 startIndex,可以大于 srcArray.length,如果大于 srcArray.length,则目标数组中使用默认值填充。

    目标数组如果已经存在,将会被重构

    public class copyOfRangeTest {
      public static void main(String[] args) {
        // 定义长度为8的数组
        int[] scores = new int[] {57, 81, 68, 75, 91, 66, 75, 84};
        System.out.println("原数组内容如下:");
        for (int i = 0; i < scores.length; i++) {
          System.out.print(scores[i] + "\t");
        }
        int[] newScores = (int[]) Arrays.copyOfRange(scores, 0, 5);
        System.out.println("\n复制的新数组内容如下:");
        // 循环遍历目标数组,即复制后的新数组
        for (int j = 0; j < newScores.length; j++) {
          System.out.print(newScores[j] + "\t");
        }
      }
    }

2:arraycopy()

  • 定义:arraycopy() 方法位于 java.lang.System 类中

    System.arraycopy(dataType[] srcArray,int srcIndex,int destArray,int destIndex,int length)
  • srcArray 表示原数组;srcIndex 表示原数组中的起始索引;destArray 表示目标数组;destIndex 表示目标数组中的起始索引;length 表示要复制的数组长度

    • 使用此方法复制数组时,length+srcIndex 必须小于等于 srcArray.length,同时 length+destIndex 必须小于等于 destArray.length

    目标数组必须已经存在,且不会被重构,相当于替换目标数组中的部分元素

    public class arraycopyTest {
      public static void main(String[] args) {
        // 定义原数组,长度为8
        int[] scores = new int[] {100, 81, 68, 75, 91, 66, 75, 100};
        // 定义目标数组
        int[] newScores = new int[] {80, 82, 71, 92, 68, 71, 87, 88, 81, 79, 90, 77};
        System.out.println("原数组中的内容如下:");
        // 遍历原数组
        for (int i = 0; i < scores.length; i++) {
          System.out.print(scores[i] + "\t");
        }
        System.out.println("\n目标数组中的内容如下:");
        // 遍历目标数组
        for (int j = 0; j < newScores.length; j++) {
          System.out.print(newScores[j] + "\t");
        }
        System.arraycopy(scores, 0, newScores, 2, 8);
        // 复制原数组中的一部分到目标数组中
        System.out.println("\n替换元素后的目标数组内容如下:");
        // 循环遍历替换后的数组
        for (int k = 0; k < newScores.length; k++) {
          System.out.print(newScores[k] + "\t");
        }
      }
    }

3:clone()

  • 定义:clone() 方法也可以实现复制数组。该方法是类Object中的方法,可以创建一个有单独内存空间的对象。因为数组也是一个 Object 类,因此也可以使用数组对象的 clone() 方法来复制数组。

    clone() 方法的返回值是 Object 类型,要使用强制类型转换为适当的类型

    array_name.clone()
    // 示例
    int[] targetArray=(int[])sourceArray.clone();

    目标数组如果已经存在,将会被重构

    public class CloneTest {
      public static void main(String[] args) {
        // 定义原数组,长度为8
        int[] scores = new int[] {100, 820, 68, 75, 91, 66, 75, 100};
        System.out.println("原数组中的内容如下:");
        // 遍历原数组
        for (int i = 0; i < scores.length; i++) {
          System.out.print(scores[i] + "\t");
        }
        // 复制数组,将Object类型强制转换为int[]类型
        int[] newScores = (int[]) scores.clone();
        System.out.println("\n目标数组内容如下:");
        // 循环遍历目标数组
        for (int k = 0; k < newScores.length; k++) {
          System.out.print(newScores[k] + "\t");
        }
      }
    }

以上几种方法都是浅拷贝(浅复制)。浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化。深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变

12):sort数组排序

1:升序

  • 使用 java.util.Arrays 类中的 sort() 方法对数组进行升序分为以下两步:

    1. 导入 java.util.Arrays 包。
    2. 使用 Arrays.sort(数组名) 语法对数组进行排序,排序规则是从小到大,即升序。
    public class SortTest {
      public static void main(String[] args) {
        // 定义含有5个元素的数组
        double[] scores = new double[] {78, 45, 65, 97, 87};
        System.out.println("排序前数组内容如下:");
        // 对scores数组进行循环遍历
        for (int i = 0; i < scores.length; i++) {
          System.out.print(scores[i] + "\t");
        }
        System.out.println("\n排序后的数组内容如下:");
        // 对数组进行排序
        Arrays.sort(scores);
        // 遍历排序后的数组
        for (int j = 0; j < scores.length; j++) {
          System.out.print(scores[j] + "\t");
        }
      }
    }

2:降序

  • 利用 Collections.reverseOrder() 方法(Collections 是一个包装类)

    public class CollectionsTest {
      public static void main(String[] args) {
        Integer[] a = {4, 5, 3, 7, 8, 9, 3};
        Arrays.sort(a, Collections.reverseOrder());
        for (int arr : a) {
          System.out.println(arr + "");
        }
      }
    }
  • 实现 Comparator 接口的复写 compare() 方法

    public class DescendingOrderTest1 {
      public static void main(String[] args) {
        /*
         * 注意,要想改变默认的排列顺序,不能使用基本类型(int,double,char)而要使用它们对应的类
         */
        Integer[] a = {9, 8, 7, 2, 3, 4, 1, 0, 6, 5};
        // 定义一个自定义类MyComparator的对象
        Comparator cmp = new MyComparator();
        Arrays.sort(a, cmp);
        for (int arr : a) {
          System.out.print(arr + " ");
        }
      }
    }
    // 实现Comparator接口
    class MyComparator implements Comparator<Integer> {
      @Override
      public int compare(Integer o1, Integer o2) {
        /*
         * 如果o1小于o2,我们就返回正值,如果o1大于o2我们就返回负值, 这样颠倒一下,就可以实现降序排序了,反之即可自定义升序排序了
         */
        return o2 - o1;
      }
    }

13):冒泡排序法

  • 定义:对比相邻的元素值,如果满足条件就交换元素值,把较小的元素值移动到数组前面,把大的元素值移动到数组后面(也就是交换两个元素的位置),这样数组元素就像气泡一样从底部上升到顶部。

    • 缺点:冒泡排序在双层循环中实现,其中外层循环控制排序轮数,总循环次数为要排序数组的长度减 1。而内层循环主要用于对比相邻元素的大小,以确定是否交换位置,对比和交换次数依排序轮数而减少
    public class bubblingTest {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        double[] score = new double[5];
        for (int i = 0; i < score.length; i++) {
          System.out.println("请输入第" + (i + 1) + "个成绩:");
          score[i] = input.nextDouble();
        }
        System.out.println("排序前的元素值:");
        for (double val : score) {
          System.out.print(val + "\t");
        }
        System.out.println();
        System.out.println("通过冒泡排序方法对数组进行排序:");
        for (int i = 0; i < score.length - 1; i++) {
          // 比较相邻两个元素,较大的数往后冒泡
          for (int j = 0; j < score.length - 1 - i; j++) {
            if (score[j] > score[j + 1]) {
              // 把第一个元素值保存到临时变量中
              double temp = score[j + 1];
              // 把第二个元素值转移到第一个元素变量中
              score[j + 1] = score[j];
              // 把临时变量(第一个元素的原值)保存到第二个元素中
              score[j] = temp;
              // 对排序后的数组元素进行输出
              System.out.print(score[j] + " ");
            } else {
              // 对排序后的数组元素进行输出
              System.out.print(score[j] + " ");
            }
          }
          System.out.print("【");
          for (int j = score.length - 1 - i; j < score.length; j++) {
            System.out.print(score[j] + " ");
          }
          System.out.println("】");
        }
      }
    }

14):快速排序法

  • 定义:通过一趟排序,将要排序的数据分隔成独立的两部分,其中一部分的所有数据比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此使整个数据变成有序序列。
  • 例子:假设要对某个数组进行排序,首先需要任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它小的数都放到它的前面,所有比它大的数都放到它的后面。这个过程称为一趟快速排序;递归调用此过程,即可实现数据的快速排序。
  1. 第一种不包含重复元素

    public class FastTest {
      public static int getMiddle(int[] list, int low, int high) {
        // 数组的第一个值作为中轴(分界点或关键数据)
        int tmp = list[low];
        while (low < high) {
          while (low < high && list[high] > tmp) {
            high--;
          }
          // 比中轴小的记录移到低端
          list[low] = list[high];
          while (low < high && list[low] < tmp) {
            low++;
          }
          // 比中轴大的记录移到高端
          list[high] = list[low];
        }
        // 中轴记录到尾
        list[low] = tmp;
        /* 返回中轴的位置 */
        return low;
      }
      /**
       * 创建静态的 unckSort() 方法,在该方法中判断 low 参数是否小于 high 参数,如果是则调用 getMiddle() 方法,将数组一分为二,并且调用自身的方法进行递归排序
       */
      public static void unckSort(int[] list, int low, int high) {
        if (low < high) {
          // 将list数组一分为二
          int middle = getMiddle(list, low, high);
          // 对低字表进行递归排序
          unckSort(list, low, middle - 1);
          // 对高字表进行递归排序
          unckSort(list, middle + 1, high);
        }
      }
      /** 声明静态的 quick() 方法,在该方法中判断传入的数组是否为空,如果不为空,则调用 unckSort() 方法进行排序 */
      public static void quick(int[] str) {
        if (str.length > 0) {
          // 查看数组是否为空
          unckSort(str, 0, str.length - 1);
        }
      }
    
      public static void main(String[] args) {
        int[] number = {13, 15, 24, 99, 14, 11, 1, 2, 3, 0, 12, 22, 4, 5};
        System.out.println("排序前:");
        for (int val : number) {
          System.out.print(val + " ");
        }
        quick(number);
        System.out.println("\n排序后:");
        for (int val : number) {
          System.out.print(val + " ");
        }
      }
    }
  2. 第二种包含重复元素

    public class FastTest1 {
      public static int partition(int[] array, int lo, int hi) {
        swap(array, lo, hi);
        int key = array[lo];
        while (lo < hi) {
          while (lo < hi) {
            if (key < array[hi]) {
              hi--;
            } else {
              array[lo++] = array[hi];
              break;
            }
          }
          while (lo < hi) {
            if (array[lo] < key) {
              lo++;
            } else {
              array[hi--] = array[lo];
              break;
            }
          }
        }
        array[lo] = key;
        return lo;
      }
    
      public static void swap(int[] array, int lo, int hi) {
        int temp = 0;
        Random r = new Random();
        int k = r.nextInt(10) % (hi - lo + 1) + lo;
        temp = array[lo];
        array[lo] = array[k];
        array[k] = temp;
      }
    
      public static void quickSort(int[] array, int lo, int hi) {
        if (lo >= hi) {
          return;
        }
        int mid = partition(array, lo, hi);
        quickSort(array, lo, mid - 1);
        quickSort(array, mid + 1, hi);
      }
    
      public static void main(String[] args) {
        int[] array = {13, 15, 24, 99, 14, 11, 1, 2, 3, 1, 2, 11, 99, 24};
        System.out.println("排序前:");
        for (int val : array) {
          System.out.print(val + " ");
        }
        quickSort(array, 0, array.length - 1);
        System.out.println("\n排序后:");
        for (int val : array) {
          System.out.print(val + " ");
        }
      }
    }

15):选择排序法

冒泡排序法

  • 定义:选择排序是指每一趟从待排序的数据元素中选出最大(或最小)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。

    选择排序法

    public class SelectSortTest {
      public static void main(String[] args) {
        int[] number = {13, 15, 21, 10, 32, 2, 3, 12, 15};
        String end = "\n";
        int index;
        for (int i = 1; i < number.length; i++) {
          index = 0;
          for (int j = 1; j <= number.length - i; j++) {
            if (number[j] > number[index]) {
              // 查找最大值
              index = j;
            }
          }
          // 定位已排好的数组元素
          end = number[index] + " " + end;
          int temp = number[number.length - i];
          number[number.length - 1] = number[index];
          number[index] = temp;
          System.out.print("【");
          for (int j = 0; j < number.length - i; j++) {
            System.out.print(number[j] + " ");
          }
          System.out.print("】" + end);
        }
      }
    }

16):直接插入排序法

  • 定义:将n个有序数存放在数组中,要插入的数为x,首先确定x插在数组中的位置p,然后将p之后的元素都向后移一个位置,空出a(p),将x放入a(p),这样可实现插入x后仍然有序。

    public class InsertionSortingTest {
      public static void main(String[] args) {
        int[] number = {13, 15, 24, 99, 4, 1};
        System.out.println("排序前:");
        // 遍历数组元素
        for (int val : number) {
          // 输出数组元素
          System.out.print(val + " ");
        }
        int temp, j;
        for (int i = 1; i < number.length; i++) {
          temp = number[i];
          for (j = i - 1; j >= 0 && number[j] > temp; j--) {
            number[j + 1] = number[j];
          }
          number[j + 1] = temp;
        }
        System.out.println("\n排序后:");
        // 遍历数组元素
        for (int val : number) {
          // 输出数组元素
          System.out.print(val + " ");
        }
      }
    }

17):数组总结

1:一维数组

  1. 声明

    type[] arrayName;    // 数据类型[] 数组名; 推荐
  2. 分配空间

    arrayName = new type[size];    // 数组名 = new 数据类型[数组长度];
  3. 初始化

  • Java 中初始化数组有以下 3 种方式:

    • 使用 new 指定数组大小后进行初始化
    • 使用 new 指定数组元素的值
    • 直接指定数组元素的值
    // 使用 new 指定数组大小后进行初始化
    int[] number = new int[5];
    number[0] = 1;
    number[1] = 2;
    number[2] = 3;
    number[3] = 5;
    number[4] = 8;
    // 使用 new 指定数组元素的值(2种方式)
    int[] number = new int[] { 1, 2, 3, 5, 8 };
    int[] number = new int[5] { 1, 2, 3, 5, 8 };
    // 直接指定数组元素的值(2种方式)
    int[] number = { 1, 2, 3, 5, 8 };
    int[] number;
    number={1,2,3,5,8};
  • 使用

    • 获取单个元素
    • 获取全部元素
    // 获取单个元素
    int[] number = { 1, 2, 3, 5, 8 };
    System.out.println("获取第一个元素:" + number[0]);
    // 使用for语句获取全部元素
    int[] number = { 1, 2, 3, 5, 8 };
    for (int i = 0; i < number.length; i++) {
        System.out.println("第" + (i + 1) + "个元素的值是:" + number[i]);
    }
    // 使用foreach语句获取全部元素
    for (int val : number) {
        System.out.print("元素的值依次是:" + val + "\t");
    }

2:二维数组

  • 声明

    type[][] arrayName;    // 数据类型[][] 数组名; 推荐
  • 初始化

    type[][] arrayName = new type[][]{1,2,3,,值 n};    // 在定义时初始化
    type[][] arrayName = new type[size1][size2];    // 给定空间,在赋值
    type[][] arrayName = new type[size][];    // 数组第二维长度为空,可变化
    // 定义时初始化
    int[][] temp = new int[][]{{1,2},{3,4}};
    // 给定空间在赋值
    int[][] temp = new int[2][2];
    // 数组第二维长度为空,可变化
    int[][] temp = new int[2][];
  • 使用

    • 获取单个元素:arrayName[i-1][j-1];arrayName 表示数组名称,i 表示数组的行数,j 表示数组的列数。
    • 获取全部元素:使用嵌套 for 循环或 for each 循环语句。
    • 获取整行元素:需要将行数固定,然后只遍历该行中的全部列即可。
    • 获取整列元素:获取指定列的元素与获取指定行的元素相似,保持列不变,遍历每一行的该列即可。

3:数组操作

  • 比较数组

    Arrays.equals(arrayA, arrayB);  // arrayA 是用于比较的第一个数组,arrayB 是用于比较的第二个数组。
  • 填充数组

    Arrays.fill(array,value); // array 表示数组,value 表示填充的值。只能使用同一个数值进行填充。
  • 数组查找

  • 查询指定位置的元素,或者查询某元素在指定数组中的位置

    binarySearch(Object[] a,Object key);
    // a 表示要搜索的数组,key 表示要搜索的值。
      
    - 数组中指定范围内查找
    
      ```java
      binarySearch(Object[] a,int fromIndex,int toIndex,Object key);
      // a 表示要进行查找的数组,fromIndex 指定范围的开始处索引(包含开始处),toIndex 指定范围的结束处索引(不包含结束处),key 表示要搜索的元素。
  • 复制数组

    • copyOf

      Arrays.copyOf(dataType[] srcArray,int length);
      // srcArray 表示要进行复制的数组,length 表示复制后的新数组的长度
    • CopyOfRange

      Arrays.copyOfRange(dataType[] srcArray,int startIndex,int endIndex)
      // srcArray 表示原数组,startIndex 表示开始复制的起始索引,endIndex 表示终止索引。
    • arraycopy

      System.arraycopy(dataType[] srcArray,int srcIndex,int destArray,int destIndex,int length)
      // srcArray 表示原数组,srcIndex 表示源数组中的起始索引,destArray 表示目标数组,destIndex 表示目标数组中的起始索引,length 表示要复制的数组长度。
    • clone

      array_name.clone()

4:数组排序

  • Java 数组中有 5 种常见排序方法,分别是:
    • Arrays.sort()
    • 冒泡排序
    • 快速排序
    • 选择排序
    • 直接插入

18):数组最大值和最小值

  • 构建数组,比较判断

    public class CompareTest {
      public static void main(String[] args) {
        int[] scores = {67, 21, 43, 56, 76, 56, 45, 74, 99};
        // 定义变量保存到最大值
        int max = 0;
        // 定义变量保存到最小值
        int min = 0;
        // 把第1个元素的内容赋值给max和min
        max = min = scores[0];
        // 循环求岀最大和最小
        for (int score : scores) {
          // 依次判断后续元素是否比max大
          if (score > max) {
            // 如果大,则修改max内容
            max = score;
          }
          // 依次判断后续的元素是否比min小
          if (score < min) {
            // 如果小,则修改min内容
            min = score;
          }
        }
        // 输出最大值
        System.out.println("最高成绩:" + max);
        // 输出最小值
        System.out.println("最低成绩:" + min);
      }
    }

19):数字转人民币读法

  1. 构建构造方法(Rmb)

    public class Rmb {
      double number; // 人民币的数量
      private String[] hanArr = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}; // 汉字计数符号
      private String[] unitArr = {"", "拾", "佰", "仟"}; // 汉字计数单位
      private String[] unitArrs = {"万", "亿", "万", "兆", "万", "亿", "万", "圆"}; // 顺序添加单位
      // private String[] unitsArr = {"万", "亿"}; //汉字计数大单位
      public Rmb() {}
      /**
       * 构造器初始化人民币数量
       *
       * @param 给定的初始化人民币数
       */
      public Rmb(double number) {
        this.number = number;
      }
    
      /**
       * 把一个浮点数分解成long型部分和小数部分字符串,把人民币换成了整数,浮点数在取小数部分时临界值有误差
       *
       * @return 返回分解后的字符串数组,第一个数组元素是整数部分,第二个是小数部分字符串
       */
      public String[] divideNum() {
        double num = Math.round(number * 100); // 对number进行四舍五入取整
        long integerPart = (long) num; // 连同小数点后两位取成整数 ,避免使用减法
        // double decimalsPart = num-integerPart;//小数部分,临界时产生了误差
        String decimalsPartStr;
        long b = integerPart % 10; // 小数点后第二位
        long a = (integerPart / 10) % 10; // 小数点后第一位
        integerPart /= 100;
        if (a == 0 && b == 0) {
          decimalsPartStr = null;
        } else {
          decimalsPartStr = "" + a + b;
        }
        return new String[] {String.valueOf(integerPart), decimalsPartStr};
      }
    
      /**
       * 把一个数字字符转换成汉语的人民币读法
       *
       * @return 返回汉语人民币读法的字符串形式
       */
      public String toHanStr() {
        String[] results = new String[9]; // 用来暂时存储每四位分段后的数字字符串
        String[] resultStrs = new String[9]; // 用来暂时存储每四位分段后转换成的人民币读法
        String result = ""; // 最终的转换结果
        String[] divideStr = divideNum(); // 得到浮点数分解成的long型部分和小数部分字符串,第一个数组元素是整数部分字符串,第二个是小数部分字符串
        results[8] = divideStr[1];
        for (int i = divideStr[0].length(), j = 8; i > 0 && j > 0; i -= 4, j--) {
          try {
            results[j - 1] = divideStr[0].substring(i - 4, i);
          } catch (Exception e) {
            results[j - 1] = divideStr[0].substring(0, i);
            break;
          }
        }
        if (results[8] == null) {
          resultStrs[8] = "整";
        } else if (results[8].charAt(1) == '0') {
          resultStrs[8] = hanArr[results[8].charAt(0) - 48] + "角"; // 根据ASCII码和hanArr数组吧数字改成汉语大写
        } else {
          resultStrs[8] =
              hanArr[results[8].charAt(0) - 48] + "角" + hanArr[results[8].charAt(1) - 48] + "分";
        }
        for (int i = 0; i < 8; i++) {
          if (results[i] != null) {
            resultStrs[i] = "";
            resultStrs[i] +=
                hanArr[results[i].charAt(0) - 48]
                    + unitArr[results[i].length() - 1]; // 根据ASCII码和数组长度选择数的单位
            for (int j = 1; j < results[i].length(); j++)
              if (results[i].charAt(j - 1) == '0' && results[i].charAt(j) != '0')
                resultStrs[i] +=
                    "零"
                        + hanArr[results[i].charAt(j) - 48]
                        + unitArr[results[i].length() - 1 - j]; // 根据ASCII码和数组长度选择数的单位
              else if (results[i].charAt(j) != '0')
                resultStrs[i] +=
                    hanArr[results[i].charAt(j) - 48] + unitArr[results[i].length() - 1 - j];
          }
        }
        for (int i = 0; i < 8; i++) {
          if (resultStrs[i] != null) {
            result += resultStrs[i] + unitArrs[i];
          }
        }
        result += resultStrs[8];
        return result;
      }
  2. 测试

    public class RmbTest {
      public static void main(String[] args) {
        double l;
        byte[] bye = new byte[50];
        System.out.println("请输入要转换的人民币数量:");
        try {
          System.in.read(bye);
        } catch (Exception e) {
    
        }
        String s = new String(bye);
        l = Double.parseDouble(s);
        Rmb r = new Rmb(l);
        s = r.toHanStr();
        System.out.println(s);
      }
    }

20):统计商品信息

  • 计算出最高价、总价格、平均价格

    public class StatisticalTest {
      public static void main(String[] args) {
        int[] prices = new int[5];
        int maxPrice = 0, avgPrice = 0, sumPrice = 0;
        Scanner input = new Scanner(System.in);
        System.out.println("请输入5件商品的价格:");
        for (int i = 0; i < 5; i++) {
          // 循环向数组中元素赋值
          prices[i] = input.nextInt();
        }
        // 计算出最大值
        maxPrice = prices[0];
        for (int index = 1; index < prices.length; index++) {
          // 总价格
          sumPrice += prices[index];
          if (prices[index] > maxPrice) {
            maxPrice = prices[index];
          }
        }
        // 平均价格=总价/商品数量
        avgPrice = sumPrice / prices.length;
        System.out.println("本货架上商品的总价格为:" + sumPrice + " 平均价格为:" + avgPrice + " 最高价格为:" + maxPrice);
      }
    }

21):商品信息查询

  • 构建数组,排序并查询

    public class EnquiryTest {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        String[] products = {"洗发水", "纸巾", "水杯", "牙膏", "香皂"};
        int[][] amounts = {{50, 43, 30}, {43, 54, 87}, {43, 21, 72}, {56, 32, 88}, {23, 65, 76}};
        System.out.println("*************** 库存系统 ***************");
        System.out.println("请输入要查询库存信息的商品名称:");
        String name = input.next();
        for (int i = 0; i < products.length; i++) {
          if (products[i].equals(name)) {
            System.out.println("商品【" + products[i] + "】的库存信息如下:");
            System.out.println("入库\t出库\t库存");
            for (int j = 0; j < 3; j++) {
              System.out.print(amounts[i][j] + "\t");
            }
            break;
          }
        }
        System.out.println("\n*************** 查询库存不足 100 的商品 ***************");
        System.out.println("1.入库 \t2.出库 \t3.库存");
        System.out.println("请输入序号:");
        int no = input.nextInt();
        int[] temp = new int[5]; // 定义数组,存储该类别的所有商品
        System.out.println("该类别下的数量较少的商品有:");
        for (int i = 0; i < 5; i++) {
          temp[i] = amounts[i][no - 1]; // 指定类别的所有商品名称存储到temp数组中
          if (amounts[i][no - 1] < 60) {
            System.out.print(products[i] + "\t");
          }
        }
        // 使用冒泡排序,将商品的库存量以从低到高的顺序排列
        for (int i = 1; i < temp.length; i++) {
          for (int j = 0; j < temp.length - i; j++) {
            if (temp[j] > temp[j + 1]) {
              int x = temp[j];
              temp[j] = temp[j + 1];
              temp[j + 1] = x;
            }
          }
        }
        System.out.println("\n该类别的商品库存信息从低到高的排列如下:");
        for (int i = 0; i < temp.length; i++) {
          System.out.print(temp[i] + "\t");
        }
      }
    }

22):输出杨辉三角(二维数组)

  • 数组方式输出

    public class TriangleTest {
      public static void main(String[] args) {
        // 从控制台获取行数
        Scanner sc = new Scanner(System.in);
        System.out.print("打印杨辉三角形的行数:");
        int row = sc.nextInt();
        // 根据行数定义好二维数组,由于每一行的元素个数不同,所以不定义每一行的个数
        int[][] arr = new int[row][];
        // 遍历二维数组
        for (int i = 0; i < row; i++) {
          // 初始化每一行的这个一维数组
          arr[i] = new int[i + 1];
          for (int j = 1; j <= row - i; j++) {
            System.out.print(" ");
          }
          // 遍历这个一维数组,添加元素
          for (int j = 0; j <= i; j++) {
            // 每一列的开头和结尾元素为1,开头的时候,j=0,结尾的时候,j=i
            if (j == 0 || j == i) {
              arr[i][j] = 1;
            } else {
              // 每一个元素是它上一行的元素和斜对角元素之和
              arr[i][j] = arr[i - 1][j] + arr[i - 1][j - 1];
            }
            System.out.print(arr[i][j] + " ");
          }
          System.out.println();
        }
      }
    }

23):五子棋游戏

八:Java类和对象

1):面向对象概述

1:对象的概念

  • 定义:Java 是面向对象的编程语言,对象就是面向对象程序设计的核心。所谓对象就是真实世界中的实体,对象与实体是一一对应的,也就是说现实世界中每一个实体都是一个对象,它是一种具体的概念。对象有以下特点:

    • 对象具有属性和行为。
    • 对象具有变化的状态。
    • 对象具有唯一性。
    • 对象都是某个类别的实例。
    • 一切皆为对象,真实世界中的所有事物都可以视为对象。
  • 在真实世界的学校里,会有学生和老师等实体,学生有学号、姓名、所在班级等属性(数据),学生还有学习、提问、吃饭和走路等操作。学生只是抽象的描述,这个抽象的描述称为“类”。在学校里活动的是学生个体,即张同学、李同学等,这些具体的个体称为“对象”,“对象”也称为“实例”

2:面向对象的三大核心特性

  • 面向对象开发模式更有利于人们开拓思维,在具体的开发过程中便于程序的划分,方便程序员分工合作,提高开发效率。面向对象程序设计有以下优点。
    1. 可重用性:代码重复使用,减少代码量,提高开发效率。
    2. 可扩展性:指新的功能可以很容易地加入到系统中来,便于软件的修改。
    3. 可管理性:能够将功能与数据结合,方便管理。
  • 该开发模式之所以使程序设计更加完善和强大,主要是因为面向对象具有继承、封装和多态 3 个核心特性
👀:继承性
  • 定义:程序中的继承性是指子类拥有父类的全部特征和行为,这是类之间的一种关系。Java 只支持单继承。

    例:定义一个语文老师类和数学老师类,如果不采用继承方式,定义的属性和方法如图👇

    总结以上:语文老师类和数学老师类中的许多属性和方法相同,这些相同的属性和方法可以提取出来放在一个父类中,这个父类用于被语文老师类和数学老师类继承。当然父类还可以继承别的类,如图👇

    总结以上图例,树行图概括,如图👇

👀:封装性
  • 定义:封装是将代码及其处理的数据绑定在一起的一种编程机制,该机制保证了程序和数据都不受外部干扰且不被误用。封装的目的在于保护信息,使用它的主要优点如下。
    • 保护类中的信息,它可以阻止在外部定义的代码随意访问内部代码和数据。
    • 隐藏细节信息,一些不需要程序员修改和使用的信息,比如取款机中的键盘,用户只需要知道按哪个键实现什么操作就可以,至于它内部是如何运行的,用户不需要知道。
    • 有助于建立各个系统之间的松耦合关系,提高系统的独立性。当一个系统的实现方式发生变化时,只要它的接口不变,就不会影响其他系统的使用。例如 U 盘,不管里面的存储方式怎么改变,只要 U 盘上的 USB 接口不变,就不会影响用户的正常操作。
    • 提高软件的复用率,降低成本。每个系统都是一个相对独立的整体,可以在不同的环境中得到使用。例如,一个 U 盘可以在多台电脑上使用。
  • Java 语言的基本封装单位是类。由于类的用途是封装复杂性,所以类的内部有隐藏实现复杂性的机制。Java 提供了私有和公有的访问模式,类的公有接口代表外部的用户应该知道或可以知道的每件东西,私有的方法数据只能通过该类的成员代码来访问,这就可以确保不会发生不希望的事情
👀:多态性
  • 定义:面向对象的多态性,即“一个接口,多个方法”。多态性体现在父类中定义的属性和方法被子类继承后,可以具有不同的属性或表现方式。多态性允许一个接口被多个同类使用,弥补了单继承的不足。多态概念可以用树形关系来表示,如图👇

3:认识类和对象

  • 定义:类实际上是表示一个客观世界某类群体的一些基本特征抽象。对象就是表示一个个具体的东西。类是对象的抽象,对象是类的具体。

  • 类是概念模型,定义对象的所有特性和所需的操作,对象是真实的模型,是一个具体的实体。类是描述了一组有相同特性(属性)和相同行为(方法)的一组对象的集合

  • 对象或实体所拥有的特征在类中表示时称为类的属性。``对象执行的操作称为类的方法。比如,“人”这个对象都具有的行为是“吃饭”,因此,吃饭就是“人”类的一个方法

    类和对象的示例
    对象
    正在清洁的环卫工人小刘
    教室里的学生张丽
    汽车 一辆黄色的宝马跑车
    一辆白色的林肯轿车
    动物 一只叫“猫咪”的小花猫
    一只叫“欢欢”的贵宾犬
  • 类是构造面向对象程序的基本单位,是抽取了同类对象的共同属性和方法所形成的对象或实体的“模板”。而对象是现实世界中实体的描述,对象要创建才存在,有了对象才能对对象进行操作。类是对象的模板,对象是类的实例。

2):Java类

1:类的定义

  • 定义:类是Java中的一种重要的引用数据类型,也是组成Java程序的基本要素,因为所有的Java程序都是基于类的。

  • Java 中定义一个类,需要使用 class 关键字、一个自定义的类名和一对表示程序体的大括号。完整语法如下👇:

    [public][abstract|final]class<class_name>[extends<class_name>][implements<interface_name>] {
        // 定义属性部分
        <property_type><property1>;
        <property_type><property2>;
        <property_type><property3>;// 定义方法部分
        function1();
        function2();
        function3();}

    上述语法中,中括号“[]”中的部分表示可以省略,竖线“|”表示“或关系”,例如 abstract|final,说明可以使用 abstract 或 final 关键字,但是两个关键字不能同时出现

  • 上述语法中各关键字的描述如下:

    • public:表示“共有”的意思。如果使用 public 修饰,则可以被其他类和程序访问。每个 Java 程序的主类都必须是 public 类,作为公共工具供其他类和程序使用的类应定义为 public 类。
    • abstract:如果类被 abstract 修饰,则该类为抽象类,抽象类不能被实例化,但抽象类中可以有抽象方法(使用 abstract 修饰的方法)和具体方法(没有使用 abstract 修饰的方法)。继承该抽象类的所有子类都必须实现该抽象类中的所有抽象方法(除非子类也是抽象类)。
    • final:如果类被 final 修饰,则不允许被继承。
    • class:声明类的关键字。
    • class_name:类的名称。
    • extends:表示继承其他类。
    • implements:表示实现某些接口。
    • property_type:表示成员变量的类型。
    • property:表示成员变量名称。
    • function():表示成员方法名称
  • Java 类名的命名规则:

    1. 类名应该以下划线(_)或字母开头,最好以字母开头。
    2. 第一个字母最好大写,如果类名由多个单词组成,则每个单词的首字母最好都大写。
    3. 类名不能为 Java 中的关键字,例如 boolean、this、int 等。
    4. 类名不能包含任何嵌入的空格或点号以及除了下划线(_)和美元符号($)字符之外的特殊字符。

    例1:

    • 声明类。编写类的最外层框架,声明一个名称为 Person 的类。

      public class Person {
          // 类的主体
      }
    • 编写类的属性。类中的数据和方法统称为类成员。其中,类的属性就是类的数据成员。通过在类的主体中定义变量来描述类所具有的特征(属性),这里声明的变量称为类的成员变量。

    • 编写类的方法。类的方法描述了类所具有的行为,是类的方法成员。可以简单地把方法理解为独立完成某个功能的单元模块。定义一个Person类👇

      public class Person {
        // 姓名
        private String name;
        // 年龄
        private int age;
      
        public void tell() {
          // 定义说话的方法
          System.out.println(name + "今年" + age + "岁!");
        }
      }

2:类的属性:

  • 定义:Java中类的成员变量定义了类的属性。例如,一个学生类中一般需要有姓名、性别和年龄等属性,这时就需要定义姓名、性别和年龄 3 个属性。声明成员变量的语法如👇:

    [public|protected|private][static][final]<type><variable_name>
  • 各参数的含义如下:

    • public、protected、private:用于表示成员变量的访问权限。
    • static:表示该成员变量为类变量,也称为静态变量。
    • final:表示将该成员变量声明为常量,其值无法更改。
    • type:表示变量的类型。
    • variable_name:表示变量名称。

    可以在声明成员变量的同时对其进行初始化,如果声明成员变量时没有对其初始化,则系统会使用默认值初始化成员变量

  • 初始化的默认值如下:

    • 整数型(byte、short、int 和 long)的基本类型变量的默认值为 0。
    • 单精度浮点型(float)的基本类型变量的默认值为 0.0f。
    • 双精度浮点型(double)的基本类型变量的默认值为 0.0d。
    • 字符型(char)的基本类型变量的默认值为 “\u0000”。
    • 布尔型的基本类型变量的默认值为 false。
    • 数组引用类型的变量的默认值为 null。如果创建了数组变量的实例,但没有显式地为每个元素赋值,则数组中的元素初始化值采用数组数据类型对应的默认值。
  • 定义类的成员变量如👇:

    public class Student {
      /** 姓名 */
      public String name;
      /** 性别:0表示女孩,1表示男孩 */
      final int sex = 0;
      /** 年龄 */
      private int age;
    }

    Student 类中定义了 3 个成员变量:String 类型的 name、int 类型的 sex 和 int 类型的 age。其中,name 的访问修饰符为 public,初始化值为 null;sex 的访问修饰符为 friendly(默认),初始化值为 0,表示性别为女,且其值无法更改;age 的访问修饰符为 private,初始化值为 0

    public class Counter {
      static int sum;
    
      public static void main(String[] args) {
        System.out.println(sum);
      }
    }

3:学生类(案例)

1:定义一个student类
public class Student {
    // 学生类
}
2:定义学生、性别、年龄
public class Student {
  /** 学生姓名 */
  public String Name;
  /** 学生年龄 */
  public int Age;
  /** 学生性别 */
  private boolean Sex;
}
3:学生性别属性 Sex 设置为 private 作用域
public boolean isSex() {
  return Sex;
}

public void setSex(boolean sex) {
  this.Sex = sex;
}
4:main()方法,输出
public class StudentTest {
  public static void main(String[] args) {
    // 创建第一个实例
    Student zhang = new Student();
    zhang.Name = "你好";
    String isMan = zhang.isSex() ? "女" : "男";
    System.out.println("姓名:" + zhang.Name + " " + "性别:" + isMan + " " + "年龄:" + zhang.Age);
    // 创建第二个实例
    Student li = new Student();
    li.Name = "李子文";
    li.Sex = true;
    li.Age = 15;
    String isWoman = li.isSex() ? "女" : "男";
    System.out.println("姓名:" + li.Name + " " + "性别:" + isWoman + " " + "年龄:" + li.Age);
  }
}

3):成员方法

  • 定义:声明成员方法可以定义类的行为,行为表示一个对象能够做的事情或者能够从一个对象取得的信息。类的各种功能操作都是用方法来实现的,属性只不过提供了相应的数据。

    • 一个完整的方法通常包括方法名称、方法主体、方法参数和方法返回值类型

  • 成员方法一旦被定义,便可以在程序中多次调用,提高了编程效率。声明成员方法的语法格式如👇

    public class Test {
      [public|private|protected][static]<void|return_type><method_name>([paramList]) {
          // 方法体
      }
    }

    除了这些,一个方法还可以没有返回值,即返回类型为 void,像 main() 方法。method_name 表示自定义的方法名称,方法的名称首先要遵循标识符的命名约定,除此之外,方法的名称第一个单词的第一个字母是小写,第二单词的第一个字母是大写,

  • paramList 表示参数列表,这些变量都要有自己的数据类型,可以是原始数据类型,也可以是复杂数据类型,一个方法主要依靠参数来传递消息。方法主体是方法中执行功能操作的语句。其他各修饰符的含义如下。

    • public、private、protected:表示成员方法的访问权限。
    • static:表示限定该成员方法为静态方法。
    • final:表示限定该成员方法不能被重写或重载。
    • abstract:表示限定该成员方法为抽象方法。抽象方法不提供具体的实现,并且所属类型必须为抽象类。
  • Student类返回学生信息

    public class Student {
      public StringBuffer printInfo(Student st) {
        StringBuffer sb = new StringBuffer();
        sb.append("学生姓名:" + st.Name + "\n 学生年龄:" + st.Age + "\n 学生性别:" + st.isSex());
        return sb;
      }
    }

    创建了一个名称为 printInfo 的方法,其返回值类型为 StringBuffer(引用数据类型)。该方法需要传递一个 Student 类型的参数,最后需要将一个 StringBuffer 类型的数据返回。

1:成员方法的返回值

  • 定义:若方法有返回值,则在方法体中用return语句指明要返回的值

    return 表达式
    其中,表达式可以是常量、变量、对象等。表达式的数据类型必须与声明成员方法时给出的返回值类型一致。

2:形参、实参及成员方法的调用

  • 一般来说,可以通过以下方式来调用成员方法:

    methodName({paramList})
  • 形参是定义方法时参数列表中出现的参数,实参是调用方法时为方法传递的参数

  • 下面 retumMin() 方法中的 m 和 n 是形参,调用 retumMin() 方法时的 x 和 y 是实参。

    public class DemoTest {
      public int returnMin(int m, int n) {
        // m和n是形参
        return Math.min(m, n);
      }
    
      public static void main(String[] args) {
        int x = 50;
        int y = 100;
        DemoTest t = new DemoTest();
        // x和y是实参
        int i = t.returnMin(x, y);
        System.out.println(i);
      }
    }
  • 方法的形参和实参具有以下特点:

    • 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只有在方法内部有效,方法调用结束返回主调方法后则不能再使用该形参变量。
    • 实参可以是常量、变量、表达式、方法等,无论实参是何种类型的量,在进行方法调用时,它们都必须具有确定的值,以便把这些值传送给形参。因此应预先用赋值、输入等办法使实参获得确定值。
    • 实参和形参在数量、类型和顺序上应严格一致,否则会发生“类型不匹配” 的错误。
    • 方法调用中发生的数据传送是单向的,即只能把实参的值传送绐形参,而不能把形参的值反向地传送给实参。因此在方法调用过程中,形参的值发生改变,而实参中的值不会变化。
    public class DemoTest {
      public int add(int x) {
        x += 30;
        System.out.println("形参 x 的值:" + x);
        return x;
      }
    
      public static void main(String[] args) {
        int x = 150;
        System.out.println("调用 add() 方法之前 x 的值:" + x);
        DemoTest t = new DemoTest();
        int i = t.add(x);
        System.out.println("实参 x 的值:" + x);
        System.out.println("调用 add() 方法的返回值:" + i);
      }
    }
  • 在调用成员方法时应注意以下 4 点:

    1. 对无参成员方法来说,是没有实际参数列表的(即没有 paramList),但方法名后的括号不能省略。
    2. 对带参数的成员方法来说,实参的个数、顺序以及它们的数据类型必须与形式参数的个数、顺序以及它们的数据类型保持一致,各个实参间用逗号分隔。实参名与形参名可以相同,也可以不同。
    3. 实参也可以是表达式,此时一定要注意使表达式的数据类型与形参的数据类型相同,或者使表达式的类型按 Java 类型转换规则达到形参指明的数据类型。
    4. 实参变量对形参变量的数据传递是“值传递”,即只能由实参传递给形参,而不能由形参传递给实参。程序中执行到调用成员方法时,Java 把实参值复制到一个临时的存储区(栈)中,形参的任何修改都在栈中进行,当退出该成员方法时,Java 自动清除栈中的内容。

3:方法体中的局部变量

  • 在方法体内可以定义本方法所使用的变量,这种变量是局部变量。它的生存期与作用域是在本方法内,也就是说,局部变量只能在本方法内有效或可见,离开本方法则这些变量将被自动释放。

    在方法体内定义变量时,变量前不能加修饰符。局部变量在使用前必须明确赋值,否则编译时会出错。另外,在一个方法内部,可以在复合语句(把多个语句用括号{}括起来组成的一个语句称复合语句)中定义变量,这些变量只在复合语句中有效。

4):this关键字

  • 定义:this关键字是Java常用的关键字,可用于任何实例方法内指向当前对象,也可指向对其调用当前方法的对象,或者在需要当前类型对象引用时使用。

1:this.属性名

  • 定义:普通方法访问其他方法、成员变量时无须使用this前缀,但如果方法里有个局部变量和成员变量同名,但程序又需要在该方法里访问这个被覆盖的成员变量,则必须使用this前缀。

    public class Teacher {
      private String name;    // 教师名称
      private double salary;    // 工资
      private int age;    // 年龄
    }

    name、salary 和 age 的作用域是 private,因此在类外部无法对它们的值进行设置。构造一个Teacher方法,

    // 创建构造方法,为上面的3个属性赋初始值
    public Teacher(String name,double salary,int age) {
      this.name = name;    // 设置教师名称
      this.salary = salary;    // 设置教师工资
      this.age = age;    // 设置教师年龄
    }

    Teacher 类的构造方法中使用了this关键字对属性name、salary和age赋值,this表示当前对象。this.name=name语句表示一个赋值语句,等号左边的this.name是指当前对象具有的变量name,等号右边的name表示参数传递过来的数值。

    public static void main(String[] args) {
      Teacher teacher = new Teacher("王刚",5000.0,45);
      System.out.println("教师信息如下:");
      System.out.println("教师名称:"+teacher.name+"\n教师工资:"+teacher.salary+"\n教师年龄:"+teacher.age);
    }

    当一个类的属性(成员变量)名与访问该属性的方法参数名相同时,则需要使用 this 关键字来访问类中的属性,以区分类的属性和方法中的参数

2:this.方法名

  • 定义:this关键字最大的作用就是让类中一个方法,访问该类里的另一个方法或实例变量。

    定义了一个Dog类,这个Dog对象的run()方法需要调用它的jump()方法

    /**
     * 第一种定义Dog类方法
     **/
    public class Dog {
      /** 定义一个jump()方法 */
      public void jump() {
        System.out.println("正在执行jump方法");
      }
      /** 定义一个run()方法,run()方法需要借助jump()方法 */
      public void run() {
        Dog d = new Dog();
        d.jump();
        System.out.println("正在执行 run 方法");
      }
    }

    使用这种方式来定义这个 Dog 类,确实可以实现在 run( ) 方法中调用 jump( ) 方法

    public class DogTest {
      public static void main(String[] args) {
        // 创建Dog对象
        Dog dog = new Dog();
        // 调用Dog对象的run()方法
        dog.run();
      }
    }

    ❓:run()方法中调用jump()方法时是否一定需要一个Dog对象

    • 是肯定的,因为没有使用 static 修饰的成员变量和方法都必须使用对象来调用

    ❓:是否一定需要重新创建一个Dog对象

    • 不一定,因为当程序调用run()方法时,一定会提供一个Dog对象,无需创建,通过this关键字调用即可。this可以代表任何对象,当this出现在某个方法体中时,它所代表的对象是不确定的,但它的类型是确定的,它所代表的只能是当前类的实例。只有当这个方法被调用时,它所代表的对象才被确定下来,谁在调用这个方法,this就代表谁。
    /**
     * 第二种定义Dog类方法
     **/
    // 定义一个run()方法,run()方法需要借助jump()方法
    public void run() {
      // 使用this引用调用run()方法的对象
      this.jump();
      System.out.println("正在执行run方法");
    }
    public void run() {
      jump();
      System.out.println("正在执行run方法");
    }

    大部分时候,一个方法访问该类中定义的其他方法、成员变量时加不加this前缀的效果是完全一样的。

    对于 static 修饰的方法而言,可以使用类来直接调用该方法,如果在 static 修饰的方法中使用 this 关键字,则这个关键字就无法指向合适的对象。所以,static 修饰的方法中不能使用 this 引用。并且 Java 语法规定,静态成员不能直接访问非静态成员=

3:this()访问构造方法

  • 定义:this()用来访问本类的构造方法(构造方法是类的一种特殊方法,方法名称和类名相同,没有返回值)括号中可以有参数,如果有参数就是调用指定的有参构造方法。

    public class Student {
      String name;
      // 无参构造方法(没有参数的构造方法)
      public Student() {
          this("张三");
      }
      // 有参构造方法
      public Student(String name) {
          this.name = name;
      }
      // 输出name和age
      public void print() {
          System.out.println("姓名:" + name);
      }
      public static void main(String[] args) {
          Student stu = new Student();
          stu.print();
      }
    }
    • this()不能在普通方法中使用,只能写在构造方法中
    • 在构造方法中使用时,必须是第一条语句

5):对象的创建

  • 定义:对象是对类的实例化。对象具有状态和行为,变量用来表明对象的状态,方法表明对象所具有的行为。Java对象的生命周期包括创建、使用和清除,本文详细介绍对象的创建,在Java语言中创建对象分显式创建与隐含创建两种情况。

1:显示创建对象

  • 对象的显示创建方式有4种
  1. 使用new关键字创建对象

    类名 对象名 = new 类名()
  2. 调用java.lang.Class或者java.lang.reflect.Constuctor类的newInstance实例方法

    java.lang.Class Class 类对象名称 = java.lang.Class.forName(要实例化的类全称);
    类名 对象名 = (类名)Class类对象名称.newInstance();
    • 调用 java.lang.Class 类中的 forName() 方法时,需要将要实例化的类的全称(比如 com.mxl.package.Student)作为参数传递过去,然后再调用 java.lang.Class 类对象的 newInstance() 方法创建对象
  3. 调用对象的clone的方法

    • 该方法不常用,使用该方法创建对象时,要实例化的类必须继承java.lang.Cloneable接口

      类名对象名 = (类名)已创建好的类对象名.clone();
  4. 调用java.io.ObjectlnputStream对象的readObject()方法

    例1 (常用的前三种对象创建方法)

    public class StudentTest implements Cloneable {
      // 定义Cloneable接口
      private String name; // 学生名字
      private int age; // 学生年龄
    
      public StudentTest(String name, int age) {
        // 有参构造方法
        this.name = name;
        this.age = age;
      }
    
      public StudentTest() {
        this.name = "name";
        this.age = 0;
      }
      @Override
      public String toString() {
        return "学生姓名:" + name + "年龄:" + age;
      }
    
      public static void main(String[] args)
          throws ClassNotFoundException, InstantiationException, IllegalAccessException,
              CloneNotSupportedException {
        System.out.println("---------使用 new 关键字创建对象---------");
        // 使用new关键字创建对象
        StudentTest studentTest = new StudentTest("小刘", 22);
        System.out.println(studentTest);
    
        System.out.println("-----------调用 java.lang.Class 的 newInstance() 方法创建对象-----------");
        // 调用 java.lang.Class 的 newInstance() 方法创建对象
        Class<?> c1 = Class.forName("Demo.StudentTest");
        StudentTest student2 = (StudentTest) c1.newInstance();
        System.out.println(student2);
    
        System.out.println("-------------------调用对象的 clone() 方法创建对象----------");
        // 调用对象的 clone() 方法创建对象
        StudentTest student3 = (StudentTest) student2.clone();
        System.out.println(student3);
      }
    }
  • 对上述示例的说明如下:

    • 使用 new 关键字或 Class 对象的 newInstance() 方法创建对象时,都会调用类的构造方法。
    • 使用 Class 类的 newInstance() 方法创建对象时,会调用类的默认构造方法,即无参构造方法。
    • 使用 Object 类的 clone() 方法创建对象时,不会调用类的构造方法,它会创建一个复制的对象,这个对象和原来的对象具有不同的内存地址,但它们的属性值相同。
    • 如果类没有实现 Cloneable 接口,则 clone。方法会抛出 java.lang.CloneNotSupportedException 异常,所以应该让类实现 Cloneable 接口。

2:隐含创建对象

  • 定义:除了显式创建对象以外,在 Java 程序中还可以隐含地创建对象,

    • String strName = “strValue”,其中的“strValue”就是一个String对象,由Java虚拟机隐含地创建。

    • 字符串的“+”运算符运算的结果为一个新的String对象

    String str1 = "Hello";
    String str2 = "Java";
    String str3 = str1+str2;    // str3引用一个新的String对象
    • 当 Java 虚拟机加载一个类时,会隐含地创建描述这个类的 Class 实例

    类的加载是指把类的.class 文件中的二进制数据读入内存中,把它存放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构

  • 无论釆用哪种方式创建对象,Java 虚拟机在创建一个对象时都包含以下步骤:

    • 给对象分配内存。
    • 将对象的实例变量自动初始化为其变量类型的默认值。
    • 初始化对象,给实例变量赋予正确的初始值。

    每个对象都是相互独立的,在内存中占有独立的内存地址,并且每个对象都具有自己的生命周期,当一个对象的生命周期结束时,对象就变成了垃圾,由 Java 虚拟机自带的垃圾回收机制处理

6):深入理解new运算符

  • 定义:new寓意”新的”,即内存开辟新的空间,及分配到内存的堆区,

    堆是用来存放由 new 创建的对象和数组,即动态申请的内存都存放在堆区。栈是用来存放在方法中定义的一些基本类型的变量和对象的引用变量。

  • new及创建对象,动态地为一个对象分配地址

    classname obj = new classname( );
    • obj 是创建的对象,classname 是类的名字,类名后边的( )指明了类的构造方法。构造方法定义了当创建一个对象时要进行的操作
    public class Test {
      public static void main(String[] args) {
        String a = "C语言中文网";
        String b = new String("C语言中文网");
        String c = "C语言中文网";
        String d = new String("C语言中文网");
        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(d == b);
        System.out.println(a);
        a = "Java";
        System.out.println(a);
      }
    }

    不同方式定义字符串时堆和栈的变化:

    1. String a; 只是在栈中创建了一个 String 类的对象引用变量 a。
    2. String a = "C语言中文网";在栈中创建一个 String 类的对象引用变量 a,然后查找栈中有没有存放“C语言中文网”,如果有则直接指向“C语言中文网”,如果没有,则将”C语言中文网“存放进栈,再指向。
    3. String a = new String("C语言中文网");不仅在栈中创建一个 String 类的对象引用变量 a,同时也在堆中开辟一块空间存放新建的 String 对象“C语言中文网”,变量 a 指向堆中的新建的 String 对象”C语言中文网“。

    ==用来比较两个对象在堆区存放的地址是否相同。根据上面的输出结果,我们可以看出:

    • 使用 new 运算符创建的 String 对象进行==操作时,两个地址是不同的。这就说明,每次对象进行 new 操作后,系统都为我们开辟堆区空间,虽然值是一样,但是地址却是不一样的。
    • 当我们没有使用 new 运算符的时候,系统会默认将这个变量保存在内存的栈区。如果变量的值存放在栈中,使用==比较时,比较的是具体的值。如果变量的值存放在堆中,使用==比较时,比较的是值所在的地址。因此在变量 a 与变量 c 进行==操作的时候,返回 true,因为变量 a 和变量 c 比较的是具体的值,即“C语言中文网”。
    • 在改变变量 a 的值后(如 a = “Java”),再次输出时,我们发现输出的结果是”Java“。事实上原来的那个“C语言中文网”在内存中并没有清除掉,而是在栈区的地址发生了改变,这次指向的是”Java“所在的地址。

    注意:如果需要比较两个使用 new 创建的对象具体的值,则需要通过“equal()”方法去实现,这样才是比较引用类型变量具体值的正确方式。

7):匿名对象

  • 定义:如果每一个对象只需要使用唯一的一次,使用匿名对象,匿名对象还可以作为实际参数传递

    匿名对象就是没有明确的给出名字的对象,是对象的一种简写形式。一般匿名对象只使用一次,而且匿名对象只在堆内存中开辟空间,而不存在栈内存的引用

    public class Person {
      public String name; // 姓名
      public int age; // 年龄
      // 定义构造方法,为属性初始化
      public Person(String name, int age) {
          this.name = name;
          this.age = age;
      }
      // 获取信息的方法
      public void tell() {
          System.out.println("姓名:" + name + ",年龄:" + age);
      }
      public static void main(String[] args) {
          new Person("张三", 30).tell(); // 匿名对象
      }
    }

8):访问对象的属性和行为

  • 要引用对象的属性和行为,需要使用点(.)操作符来访问。对象名在圆点左边,而成员变量或成员方法的名称在圆点的右边

    对象名.属性(成员变量)    // 访问对象的属性
    对象名.成员方法名()    // 访问对象的方法
    Student stu = new Student();    // 创建 Student 类的对象 stu
    stu.Name = "李子文";    // 调用stu对象的Name属性并赋值
    stu.Sex = true;    // 调用stu对象的Sex属性并赋值
    stu.Age = 15;    // 调用stu对象的Age属性并赋值

    如果一个对象没有被实例化而直接调用了对象的属性或方法,则会抛出异常,如👇

    Student stu = null;
    stu.Name = "李子文";
    stu.Sex = true;
    stu.Age = 15;
    // 抛出异常
    Exception in thread "main" java.lang.NullPointerException

9):对象的销毁

  • 定义:对象的销毁及释放占用的内存,在此之前,必须new 分配内存,在清除对象时,由系统自动进行内存回收,不需要用户额外处理
  • 内存自动回收称为垃圾回收机制。简称GC,垃圾回收机制是指 JVM 用于释放那些不再使用的对象所占用的内存。

1:一个对象被当作垃圾回收的情况主要有两种

  • 对象的引用超过其作用范围

    {
        Object o = new Object();    // 对象o的作用范围,超过这个范围对象将被视为垃圾
    }
  • 对象被赋值为null

    {
        Object o = new Object();
        o = null;    // 对象被赋值为null将被视为垃圾
    }

    在Java的Object类中还提供了一个protected类型的finalize()方法,因此任何Java类都可以覆盖这个方法,在这个方法中进行释放对象所占有的相关资源的操作。

  • 在Java虚拟机的堆区,每个对象都可能处于以下三种状态之一:

    1)可触及状态:当一个对象被创建后,只要程序中还有引用变量引用它,那么它就始终处于可触及状态。

    2)可复活状态:当程序不再有任何引用变量引用该对象时,该对象就进入可复活状态。在这个状态下,垃圾回收器会准备释放它所占用的内存,在释放之前,会调用它及其他处于可复活状态的对象的 finalize()方法,这些finalize()方法有可能使该对象重新转到可触及状态。

    3)不可触及状态:当Java虚拟机执行完所有可复活对象的finalize()方法后,如果这些方法都没有使该对象转到可触及状态,垃圾回收器才会真正回收它占用的内存。

    注意:调用 System.gc() 或者 Runtime.gc() 方法也不能保证回收操作一定执行,它只是提高了 Java 垃圾回收器尽快回收垃圾的可能性。

12):空对象(null)是怎么回事?

10):用户修改密码

  • 创造方法,什么变量

    // 用户类
    private String username; // 用户名
    private String password; // 密码
    
    public String getUsername() {
      return username;
    }
    
    public void setUsername(String username) {
      this.username = username;
    }
    
    public String getPassword() {
      return password;
    }
    
    public void setPassword(String password) {
      this.password = password;
    }
    
    public Member(String username, String password) {
      this.username = username;
      this.password = password;
    }
    
    @Override
    public String toString() {
      return "用户名:" + username + "密码:" + password;
    }
    
    public static void main(String[] args) {
      Member admin = new Member("admin", "123456"); // 创建用户对象
      Scanner input = new Scanner(System.in);
      System.out.println("请输入原密码:");
      String pwd = input.next();
      if (pwd.equals(admin.getPassword())) {
        // 对用户输入的密码进行验证
        System.out.println("请输入新密码:");
        admin.setPassword(input.next());
      } else {
        System.out.println("输入的密码错误,无法进行修改!");
      }
      System.out.println("--------用户信息---------\n" + admin);
    }

11):注释(类、方法和字段)

1:类注释

  • 类注释一般必须放在所有的“import”语句之后,类定义之前,主要声明该类可以做什么,以及创建者、创建日期、版本和包名等一些信息

    /**
     * @projectName(项目名称): project_name
     * @package(包): package_name.file_name
     * @className(类名称): type_name
     * @description(类描述): 一句话描述该类的功能
     * @author(创建人): user 
     * @createDate(创建时间): datetime  
     * @updateUser(修改人): user 
     * @updateDate(修改时间): datetime
     * @updateRemark(修改备注): 说明本次修改内容
     * @version(版本): v1.0
     */
    /**
     * @author: zhangsan
     * @createDate: 2018/10/28
     * @description: this is the student class.
     */
    public class student{
        .................
    }

2:方法注释

  • 方法注释必须紧靠在方法定义的前面,主要声明方法参数、返回值、异常等信息。除了可以使用通用标签外,还可以使用下列的以@开始的标签。

    • @param 变量描述:对当前方法的参数部分添加一个说明,可以占据多行。一个方法的所有 @param 标记必须放在一起。
    • @return 返回类型描述:对当前方法添加返回值部分,可以跨越多行。
    • @throws 异常类描述:表示这个方法有可能抛出异常。
    /**
     * @param num1: 加数1
     * @param num2: 加数2
     * @return: 两个加数的和
     */
    public int add(int num1,int num2) {
        int value = num1 + num2;
        return value;
    }
  • 为类的构造方法添加注释时,一般声明该方法的参数信息

    public class Student {
       String name;
       int age;
       /**
        * @description: 构造方法
        * @param name: 学生姓名
        * @param age: 学生年龄
        */
       public Student(String name,int age) {
        this.name = name;
        this.age = age;
       }
    }

3:字段注释

  • 定义:字段注释在定义字段的前面,用来描述字段的含义

    /**
     * 用户名
     */
    public String name;

12):访问控制修饰符

  • 常用修饰符:public、private、protected、final、abstract、static、transient 和 volatile,这些修饰符有类修饰符、变量修饰符和方法修饰符

  • 信息隐藏是 OOP 最重要的功能之一,也是使用访问修饰符的原因。在编写程序时,有些核心数据往往不希望被用户调用,需要控制这些数据的访问。

    通过使用访问控制修饰符来限制对对象私有属性的访问,可以获得 3 个重要的好处:

    • 防止对封装数据的未授权访问。
    • 有助于保证数据完整性。
    • 当类的私有实现细节必须改变时,可以限制发生在整个应用程序中的“连锁反应”。
  • 访问控制符是一组限定类、属性或方法是否可以被程序里的其他部分访问和调用的修饰符。类的访问控制符只能是空或者 public,方法和属性的访问控制符有 4 个,分别是 public、 private、protected 和 friendly,其中 friendly 是一种没有定义专门的访问控制符的默认情况。访问控制修饰符的权限如👇

    各种访问修饰符的可访问性
    访问范围 private friendly(默认) protected public
    同一个类 可访问 可访问 可访问 可访问
    同一包中的其他类 不可访问 可访问 可访问 可访问
    不同包中的子类 不可访问 不可访问 可访问 可访问
    不同包中的非子类 不可访问 不可访问 不可访问 可访问

    合理地使用访问控制符,可以通过降低类和类之间的耦合性(关联性)来降低整个项目的复杂度

1:private

  • 定义:用private修饰的类成员,只能被该类自身的方法访问和修改,而不能被任何其他类(包括该类的子类)访问和引用。因此,private修饰符具有最高的保护级别。例如,设 PhoneCard 是电话卡类,电话卡都有密码,因此该类有一个密码域,可以把该类的密码域声明为私有成员。

2:friendly(默认)

  • 定义:如果一个类没有访问控制符,说明它具有默认的访问控制特性。这种默认的访问控制权规定,该类只能被同一个包中的类访问和引用,而不能被其他包中的类使用,即使其他包中有该类的子类。这种访问特性又称为包访问性(package private)
  • 同样,类内的成员如果没有访问控制符,也说明它们具有包访问性,或称为友元(friend)。定义在同一个文件夹中的所有类属于一个包,所以前面的程序要把用户自定义的类放在同一个文件夹中(Java 项目默认的包),以便不加修饰符也能运行

3:protected

  • 定义:用保护访问控制符 protected 修饰的类成员可以被三种类所访问:该类自身、与它在同一个包中的其他类以及在其他包中的该类的子类。使用 protected 修饰符的主要作用,是允许其他包中它的子类来访问父类的特定属性和方法,否则可以使用默认访问控制符。

4:public

  • 定义:当一个类被声明为 public 时,它就具有了被其他包中的类访问的可能性,只要包中的其他类在程序中使用 import 语句引入 public 类,就可以访问和引用这个类

  • 类中被设定为 public 的方法是这个类对外的接口部分,避免了程序的其他部分直接去操作类内的数据,实际就是数据封装思想的体现。每个 Java 程序的主类都必须是 public 类,也是基于相同的原因

    1. 修饰符的使用 -创建属性和方法

      class Student {
          // 姓名,其访问权限为默认(friendly)
          String name;
          // 定义私有变量,身份证号码
          private String idNumber;
          // 定义受保护变量,学号
          protected String no;
          // 定义共有变量,邮箱
          public String email;
          // 定义共有方法,显示学生信息
          public String info() {
              return"姓名:"+name+",身份证号码:"+idNumber+",学号:"+no+",邮箱:"+email;
          }
      }
  1. 访问类
    public class StudentTest{
      public static void main(String[] args) {
        // 创建Student类对象
        Student stu = new Student();
        // 向Student类对象中的属性赋值
        stu.name = "zhht";
        // stu.idNumber="043765290763137806";
        // 这是不允许的。提示stu.idNumber是不可见的,必须注释掉才可运行
        stu.no = "20lil01637";
        stu.email = "zhht@qq.com";
        System.out.println(stu.info());
      }
    }

13):static关键字

  • 定义:使用 static 修饰符修饰的属性(成员变量)称为静态变量,也可以称为类变量,常量称为静态常量,方法称为静态方法或类方法,它们统称为静态成员,归整个类所有,静态成员不依赖于类的特定实例,被类的所有实例共享,就是说 static 修饰的方法或者变量不需要依赖于对象来进行访问

    类名.静态成员
    • static 修饰的成员变量和方法,从属于类。
    • 普通变量和方法从属于对象。
    • 静态方法不能调用非静态成员,编译会报错。

1:静态变量

  • 类的成员变量可以分为以下两种:

    1. 静态变量(或称为类变量),指被 static 修饰的成员变量。
    2. 实例变量,指没有被 static 修饰的成员变量。
  • 静态变量与实例变量的区别如👇:

    1)静态变量

    • 运行时,Java 虚拟机只为静态变量分配一次内存,在加载类的过程中完成静态变量的内存分配。
    • 在类的内部,可以在任何方法内直接访问静态变量。
    • 在其他类中,可以通过类名访问该类中的静态变量。

    2)实例变量

    • 每创建一个实例,Java 虚拟机就会为实例变量分配一次内存。
    • 在类的内部,可以在非静态方法中直接访问实例变量。
    • 在本类的静态方法或其他类中则需要通过类的实例对象进行访问。
  • 静态变量在类中的作用如👇:

    • 静态变量可以被类的所有实例共享,因此静态变量可以作为实例之间的共享数据,增加实例之间的交互性。

    • 如果类的所有实例都包含一个相同的常量属性,则可以把这个属性定义为静态常量类型,从而节省内存空间。例如,在类中定义一个静态常量 PI。

      public static double PI = 3.14159256;
      public class StaticVar {
        public static String str1 = "Hello";
        public static void main(String[] args) {
          String str2 = "World!";
          // 直接访问str1
          String accessVar1 = str1+str2;
          System.out.println("第 1 次访问静态变量,结果为:"+accessVar1);
          // 通过类名访问str1
          String accessVar2 = StaticVar.str1+str2;
          System.out.println("第 2 次访问静态变量,结果为:"+accessVar2);
          // 通过对象svt1访问str1
          StaticVar svt1 = new StaticVar();
          svt1.str1 = svt1.str1+str2;
          String accessVar3 = svt1.str1;
          System.out.println("第 3 次访向静态变量,结果为:"+accessVar3);
          // 通过对象svt2访问str1
          StaticVar svt2 = new StaticVar();
          String accessVar4 = svt2.str1+str2;
          System.out.println("第 4 次访问静态变量,结果为:"+accessVar4);
        }
      }
    • 在类中定义静态的属性(成员变量),在 main() 方法中可以直接访问,也可以通过类名访问,还可以通过类的实例对象来访问

    静态变量是被多个实例所共享的

2:静态方法

  • 与成员变量类似,成员方法也可以分为以下两种:

    1. 静态方法(或称为类方法),指被 static 修饰的成员方法。
    2. 实例方法,指没有被 static 修饰的成员方法。
  • 静态方法与实例方法的区别如👇:

    • 静态方法不需要通过它所属的类的任何实例就可以被调用,因此在静态方法中不能使用 this 关键字,也不能直接访问所属类的实例变量和实例方法,但是可以直接访问所属类的静态变量和静态方法。另外,和 this 关键字一样,super 关键字也与类的特定实例相关,所以在静态方法中也不能使用 super 关键字。
    • 在实例方法中可以直接访问所属类的静态变量、静态方法、实例变量和实例方法。
    public static int count = 1;    // 定义静态变量count
    public int method1() {    
      // 实例方法method1
      count++;    // 访问静态变量count并赋值
      System.out.println("在静态方法 method1()中的 count="+count);    // 打印count
      return count;
    }
    public static int method2() {    
      // 静态方法method2
      count += count;    // 访问静态变量count并赋值
      System.out.println("在静态方法 method2()中的 count="+count);    // 打印count
      return count;
    }
    public static void PrintCount() {    
      // 静态方法PrintCount
      count += 2;
      System.out.println("在静态方法 PrintCount()中的 count="+count);    // 打印count
    }
    public static void main(String[] args) {
      StaticMethod sft = new StaticMethod();
      // 通过实例对象调用实例方法
      System.out.println("method1() 方法返回值 intro1="+sft.method1());
      // 直接调用静态方法
      System.out.println("method2() 方法返回值 intro1="+method2());
      // 通过类名调用静态方法,打印 count
      StaticMethod.PrintCount();
    }
    • 在访问非静态方法时,需要通过实例对象来访问,而在访问静态方法时,可以直接访问,也可以通过类名来访问,还可以通过实例化对象来访问。

3:静态代码块

  • 定义:静态代码块指 Java 类中的 static{ } 代码块,主要用于初始化类,为类的静态变量赋初始值,提升程序性能。

  • 静态代码块的特点如下:

    • 静态代码块类似于一个方法,但它不可以存在于任何方法体中。
    • 静态代码块可以置于类中的任何地方,类中可以有多个静态初始化块。
    • Java 虚拟机在加载类时执行静态代码块,所以很多时候会将一些只需要进行一次的初始化操作都放在 static 代码块中进行。
    • 如果类中包含多个静态代码块,则 Java 虚拟机将按它们在类中出现的顺序依次执行它们,每个静态代码块只会被执行一次。
    • 静态代码块与静态方法一样,不能直接访问类的实例变量和实例方法,而需要通过类的实例对象来访问。
    public static int count = 0;
    {
        count++;
        System.out.println("非静态代码块 count=" + count);
    }
    static {
        count++;
        System.out.println("静态代码块1 count=" + count);
    }
    static {
        count++;
        System.out.println("静态代码块2 count=" + count);
    }
    public static void main(String[] args) {
        System.out.println("*************** StaticCode1 执行 ***************");
        StaticCode sct1 = new StaticCode();
        System.out.println("*************** StaticCode2 执行 ***************");
        StaticCode sct2 = new StaticCode();
    }

14):final修饰符

  • 定义:final应用于类、方法和变量时意义是不同的,但本质是一样的,都表示不可改变
  • 使用final关键字声明类、变量和方法需要注意以下几点:
    • final 用在变量的前面表示变量的值不可以改变,此时该变量可以被称为常量。
    • final 用在方法的前面表示方法不可以被重写(子类中如果创建了一个与父类中相同名称、相同返回值类型、相同参数列表的方法,只是方法体中的实现不同,以实现不同于父类的功能,这种方式被称为方法重写,又称为方法覆盖)。
    • final 用在类的前面表示该类不能有子类,即该类不可以被继承。

1:final修饰变量

  • final 修饰的变量即成为常量,只能赋值一次,但是 final 所修饰局部变量和成员变量有所不同。

    1. final 修饰的局部变量必须使用之前被赋值一次才能使用。
    2. final 修饰的成员变量在声明时没有赋值的叫“空白 final 变量”。空白 final 变量必须在构造方法或静态代码块中初始化。

    final 修饰的变量不能被赋值这种说法是错误的,严格的说法是,final 修饰的变量不可被改变,一旦获得了初始值,该 final 变量的值就不能被重新赋值

    public class FinalDemo {
      void doSomething() {
        // todo 没有在声明的同时赋值
        final int e;
        // 只能赋值一次
        e = 100;
        System.out.print(e);
        // 声明的同时赋值
        final int f = 200;
      }
      // 实例常量
      final int a = 5; // 直接赋值
      final int b; // 空白final变量
      // 静态常量
      static final int c = 12; // 直接赋值
      static final int d; // 空白final变量
      // 静态代码块
      static {
        // 初始化静态变量
        d = 32;
      }
      // 构造方法
      FinalDemo() {
        // 初始化实例变量
        b = 3;
        // 第二次赋值,会发生编译错误
        // b = 4;
      }
    }
  • final 修饰基本类型变量和引用类型变量的区别

    当使用 final 修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。 但对于引用类型变量而言,它保存的仅仅是一个引用,final 只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变

    class PersonDemo {
      private int age;
    
      public PersonDemo() {}
      // 有参数的构造器
      public PersonDemo(int age) {
        this.age = age;
      }
    
      public void setAge(int i) {}
    
      public boolean getAge() {
        return false;
      }
      // 省略age的setter和getter方法
    }
    
    public class FinalReferenceTest {
      public static void main(String[] args) {
        // final修饰数组变量,iArr是一个引用变量
        final int[] iArr = {5, 6, 12, 9};
        System.out.println(Arrays.toString(iArr));
        // 对数组元素进行排序,合法
        Arrays.sort(iArr);
        System.out.println(Arrays.toString(iArr));
        // 对数组元素赋值,合法
        iArr[2] = -8;
        System.out.println(Arrays.toString(iArr));
        // 下面语句对iArr重新赋值,非法
        // iArr = null;
        // final修饰Person变量,p是一个引用变量
        final PersonDemo p = new PersonDemo(45);
        // 改变Person对象的age实例变量,合法
        p.setAge(23);
        System.out.println(p.getAge());
        // 下面语句对P重新赋值,非法
        // p = null;
      }
    }
  • 如果一个程序中的变量使用 public static final 声明,则此变量将称为全局变量

    public static final String SEX= “女”;

2:final修饰方法

  • 定义:final 修饰的方法不可被重写,如果出于某些原因,不希望子类重写父类的某个方法,则可以使用 final 修饰该方法。

    public class FinalMethodTest {
      public final void test() {}
    }
    
    class Sub extends FinalMethodTest { // 下面方法定义将出现编译错误,不能重写final方法
      public void test() {}
    }
  • 对于一个 private 方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法——如果子类中定义一个与父类 private 方法有相同方法名、相同形参列表、相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。因此,即使使用 final 修饰一个 private 访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名、相同形参列表、相同返回值类型的方法 重写和重载

    public class PrivateFinalMethodTest {
      private final void test() {
      }
    }
    class Sub extends PrivateFinalMethodTest {
      // 下面的方法定义不会出现问题
      public void test() {
      }
    }

3:final修饰符

  • 定义:final 修饰的类不能被继承。当子类继承父类时,将可以访问到父类内部数据,并可通过重写父类方法来改变父类方法的实现细节,这可能导致一些不安全的因素。为了保证某个类不可被继承,则可以使用 final 修饰这个类

  • final修饰的类不可被继承,👇

    final class SuperClass {
    }
    class SubClass extends SuperClass {    //编译错误
    }

4:final修饰符使用总结

1:final修饰类中的变量
  • 表示该变量一旦被初始化便不可改变,这里不可改变的意思对基本类型变量来说是其值不可变,而对对象引用类型变量来说其引用不可再变。其初始化可以在两个地方:一是其定义处,也就是说在 final 变量定义时直接给其赋值;二是在构造方法中。这两个地方只能选其一,要么在定义时给值,要么在构造方法中给值,不能同时既在定义时赋值,又在构造方法中赋予另外的值。
2:final修饰类中的方法
  • 说明这种方法提供的功能已经满足当前要求,不需要进行扩展,并且也不允许任何从此类继承的类来重写这种方法,但是继承仍然可以继承这个方法,也就是说可以直接使用。在声明类中,一个 final 方法只被实现一次
3:final修饰符
  • 定义:表示该类是无法被任何其他类继承的,意味着此类在一个继承树中是一个叶子类,并且此类的设计已被认为很完美而不需要进行修改或扩展。

15):main方法

  • 定义:main()方法是Java应用程序的入口方法,程序在运行的时候,第一个执行的方法就是main()方法

    public class HelloWorld {
      public static void main(String args[]) {
          System.out.println("Hello World!");
      }
    }

    其中,使用main()方法时应该注意如下几点:

    • 访问控制权限是公有的(public)
    • main()方法是静态的。如果要在main()方法中调用本类中的其他方法,则该方法也必须是静态的,否则需要先创建本类的实例对象,然后再通过对象调用成员方法。
    • main()方法没有返回值,只能使用void
    • main()方法具有一个字符串数组参数,用来接收执行Java程序的命令行参数。命令行参数作为字符串,按照顺序依次对应字符串数组中的元素。
    • 字符串中数组的名字(代码中的 args)可以任意设置,但是根据习惯,这个字符串数组的名字一般和Java规范范例中 main()参数名保持一致,命名为 args,而方法中的其他内容都是固定不变的。
    • main()方法定义必须是“public static void main(String[] 字符串数组参数名)”
    • 一个类只能有一个main()方法,这是一个常用于对类进行单元测试(对软件中的最小可测试单元进行检查和验证)的技巧。
    public class Student {
      public void Speak1() {
          System.out.println("你好!");
      }
      public static void Speak2() {
          System.out.println("Java!");
      }
      public static void main(String[] args) {
          // Speak1();    // 错误调用
          Speak2();    // 可以直接调用静态方法Speak2()
          Student t = new Student();
          t.Speak1();    // 调用非静态方法,需要通过类的对象来调用
      }
    }

    在main()方法中只能直接调用静态方法,如果想调用非静态方法,需要将当前类实例化,然后通过类的对象来调用

  • 例👇:统计传递参数的数量及每个参数值

    public class TestMain {
      public static void main(String[] args) {
        int n = args.length;    // 获取参数数量
        System.out.println("一共有 "+n+" 个参数");
        if(n > 0) {   
            // 判断参数个数是否大于0
            for(int i = 0;i < n;i++) {
                System.out.println(args[i]);
            }
        }
      }
    }

16):方法的可变参数

  • 格式如👇

    methodName({paramList},paramType…paramName)
    • methodName 表示方法名称;paramList 表示方法的固定参数列表;paramType 表示可变参数的类型;… 是声明可变参数的标识;paramName 表示可变参数名称

    可变参数必须定义在参数列表的最后

    public void print(String... names) {
      int count = names.length; // 获取总个数
      System.out.println("本次参加考试的有" + count + "人,名单如下:");
      for (String name : names) {
        System.out.println(name);
      }
    }
    
    public static void main(String[] args) {
      StudentTestMethod student = new StudentTestMethod();
      student.print("张强", "你好", "五一");
      student.print("张强", "你好");
    }

17):构造方法

  • 定义:构造方法是类的一种特殊方法,用来初始化类的一个新的对象,在创建对象(new 运算符)之后自动调用。Java中的每个类都有一个默认的构造方法,并且可以有一个以上的构造方法。

  • 7Java 构造方法有以下特点:

    • 方法名必须与类名相同
    • 可以有 0 个、1 个或多个参数
    • 没有任何返回值,包括 void
    • 默认返回类型就是对象类型本身
    • 只能与 new 运算符结合使用

    构造方法不是没有返回值吗?为什么不能用 void 声明呢?

    简单的说,这是 Java 的语法规定。实际上,类的构造方法是有返回值的,当使用 new 关键字来调用构造方法时,构造方法返回该类的实例,可以把这个类的实例当成构造器的返回值,因此构造器的返回值类型总是当前类,无须定义返回值类型。但必须注意不要在构造方法里使用 return 来返回当前类的对象,因为构造方法的返回值是隐式的。

    注意:构造方法不能被 static、final、synchronized、abstract 和 native(类似于 abstract)修饰。构造方法用于初始化一个新对象,所以用 static 修饰没有意义。构造方法不能被子类继承,所以用 final 和 abstract 修饰没有意义。多个线程不会同时创建内存地址相同的同一个对象,所以用 synchronized 修饰没有必要。

  • 语法如👇

    class class_name {
      public class_name(){}    // 默认无参构造方法
      public ciass_name([paramList]){}    // 定义构造方法// 类主体
    }

    在一个类中,与类名相同的方法就是构造方法。每个类可以具有多个构造方法,但要求它们各自包含不同的方法参数。

  • 例👇 有无参构造方法和有参构造方法

    public class MyClass {
      private int m; // 定义私有变量
    
      MyClass() {
        // 定义无参的构造方法
        m = 0;
      }
    
      MyClass(int m) {
        // 定义有参的构造方法
        this.m = m;
      }
    }

    定义了两个构造方法,分别是无参构造方法和有参构造方法。在一个类中定义多个具有不同参数的同名方法,这就是方法的重载。这两个构造方法的名称都与类名相同,均为 MyClass。在实例化该类时可以调用不同的构造方法进行初始化。

  • 注意:类的构造方法不是要求必须定义的。如果在类中没有定义任何一个构造方法,则 Java 会自动为该类生成一个默认的构造方法。默认的构造方法不包含任何参数,并且方法体为空。如果类中显式地定义了一个或多个构造方法,则 Java 不再提供默认构造方法

    提示:无参数的构造方法也被称为 Nullary 构造方法。只有编译程序自动加入的构造方法,才称为默认构造函数。如果自行编写无参数、没有内容的构造函数,就不称为默认构造函数了(只是 Nullary 构造函数)

  • 演示

    • 定义员工类的构造方法

      public class Worker {
        public String name;
        public int age;
        // todo 定义一个有参数的构造方法
        public Worker(String name) {
          this.name = name;
        }
        /* todo 定义2个有参数的构造方法 */
        public Worker(String name, int age) {
          this.name = name;
          this.age = age;
        }
      
        @Override
        public String toString() {
          return "大家好!我是新来的员工,我叫" + name + ",今年" + age + "岁";
        }
      }
    • 调用构造方法

      public class TestWorker {
        public static void main(String[] args) {
          System.out.println("一个参数的构造方法");
          Worker worker1 = new Worker("张强");
          System.out.println(worker1);
          System.out.println("两个参数的构造方法");
          Worker worker2 = new Worker("小麦", 22);
          System.out.println(worker2);
        }
      }

18):查询个人信息

  • 定义个人基本信息属性

    public class PersonInfo {
      private String name; // 姓名
      private int age; // 年龄
      private String sex; // 性别
      private String birthday; // 出生日期
      private String constellation; // 星座
    
      public PersonInfo(String name, int age, String sex, String birthday, String constellation) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.birthday = birthday;
        this.constellation = constellation;
      }
    
      public String info() {
        return "姓名:"
            + name
            + "\n年龄:"
            + age
            + "\n性别:"
            + sex
            + "\n出生日期:"
            + birthday
            + "\n星座:"
            + constellation;
      }
    }
  • 创建main出口

    public class PersonTest {
      public static void main(String[] args) {
        PersonInfo person = new PersonInfo("王杰", 21, "男", "2020-01-01", "狮子座");
        String info = person.info();
        System.out.println(info);
      }
    }

19):析构方法

  • 定义:析构方法与构造方法相反,当对象脱离其作用域时(例如对象所在的方法已调用完毕),系统自动执行析构方法(清理垃圾碎片的工作)

  • Object类中提供了一个protected类型的finalize()方法,因此任何Java类都可以覆盖这个方法,在这个方法中进行释放对象所占有的相关资源的操作

  • 对象的 finalize() 方法具有如下特点:

    • 垃圾回收器是否会执行该方法以及何时执行该方法,都是不确定的。
    • finalize() 方法有可能使用对象复活,使对象恢复到可触及状态。
    • 垃圾回收器在执行 finalize() 方法时,如果出现异常,垃圾回收器不会报告异常,程序继续正常运行。
    protected void finalize() {
        // 对象的清理工作
    }
  • 例👇 实例化对象的个数

    • Counter 类在构造方法中增值,在析构方法中减值

      public class Counter {
        private static int count = 0; // 计数器变量
      
        public Counter() {
          this.count++; // 创建实例时增加值
        }
      
        public int getCount() {
          // 获取计数器的值
          return this.count;
        }
      
        @Override
        protected void finalize() {
          // 析构方法
          this.count--; // 实例销毁时减少值
          System.out.println("对象销毁");
        }
      }
    • main出口

      public class TestCounter {
        public static void main(String[] args) {
          Counter cnt1 = new Counter(); // 建立第一个实例
          System.out.println("数量:" + cnt1.getCount()); // 输出1
          Counter cnt2 = new Counter(); // 建立第二个实例
          System.out.println("数量:" + cnt2.getCount()); // 输出2
          cnt2 = null; // 销毁实例2
          try {
            System.gc(); // 清理内存
            Thread.sleep(1000); // 延时1000毫秒
            System.out.println("数量:" + cnt1.getCount());
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
      }

      由于 finalize() 方法的不确定性,所以在程序中可以调用 System.gc() 或者 Runtime.gc() 方法提示垃圾回收器尽快执行垃圾回收操作。

20):包(package)

  • 包的 3 个作用如下:
    1. 区分相同名称的类。
    2. 能够较好地管理大量的类。
    3. 控制访问范围。
  1. 包定义
  • 定义:package 语句应该放在源文件的第一行,在每个源文件中只能有一个包定义语句,package 语句适用于所有类型(类、接口、枚举和注释)的文件。

    package 包名;
  • Java 包的命名规则如👇:

    • 包名全部由小写字母(多个单词也全部小写)。
    • 如果包名包含多个层次,每个层次用“.”分割。
    • 包名一般由倒置的域名开头,比如 com.baidu,不要有 www。
    • 自定义包不能 java 开头。
  1. 包导入
  • 如果使用不同包中的其它类,需要使用该类的全名(包名+类名)

    example.Test test = new example.Test();
    example 是包名,Test 是包中的类名,test 是类的对象。
  • 使用 import 导入单个类的语法

    import 包名+类名;
  1. 系统包
  • 系统包就是Java开发中常用的基础类(开发可以自定义包,也可以使用系统包)

    Java中常用的系统包
    说明
    java.lang Java 的核心类库,包含运行 Java 程序必不可少的系统类,如基本数据类型、基本数学函数、 字符串处理、异常处理和线程类等,系统默认加载这个包
    java.io Java 语言的标准输入/输出类库,如基本输入/输出流、文件输入/输出、过滤输入/输出流等
    java.util 包含如处理时间的 Date 类,处理动态数组的 Vector 类,以及 Stack 和 HashTable 类
    java.awt 构建图形用户界面(GUI)的类库,低级绘图操作 Graphics 类、图形界面组件和布局管理 (如 Checkbox 类、Container 类、LayoutManger 接口等),以及用户界面交互控制和事 件响应(如 Event 类)
    java.awt.image 处理和操纵来自网上的图片的 Java 工具类库
    java.wat.peer 很少在程序中直接用到,使得同一个 Java 程序在不同的软硬件平台上运行
    java.net 实现网络功能的类库有 Socket 类、ServerSocket 类
    java.lang.reflect 提供用于反射对象的工具
    java.util.zip 实现文件压缩功能
    java.awt.datatransfer 处理数据传输的工具类,包括剪贴板、字符串发送器等
    java.sql 实现 JDBC 的类库
    java.rmi 提供远程连接与载入的支持
    java. security 提供安全性方面的有关支持
  1. 自定义包
  • 创建com.dao包,返回一个String数组的getAll方法

    public class Student {
      public static String[] getAll() {
        return new String[] {"李潘", "邓国良", "任玲玲", "许月月", "欧阳娜", "赵晓慧"};
      }
    }
  • 创建com.test包,导入com.dao包,遍历getAll方法

    public class Test {
      public static void main(String[] args) {
        System.out.println("学生信息如下");
        for (String str : Student.getAll()) {
          System.out.println(str);
        }
      }
    }

21):递归算法

  • 定义:调用自身的编程技巧称为递归,递归是允许方法调用自身调用的属性

  • 例👇 计算阶乘

    public static int Wayone(int num) {
      int sum = 1;
      if (num < 0) {
        throw new IllegalArgumentException("需要计算的参数必须为正数!"); // 抛出不合理参数异常
      }
      for (int i = 1; i <= num; i++) {
        sum *= i;
      }
      return sum;
      }
    
      /**
       * 运用递归算法求阶乘
       *
       * @param num
       * @return
       */
      public static int Waytwo(int num) {
        int sum = 1;
        if (num < 0) {
          throw new IllegalArgumentException("需要计算的参数必须为正数!"); // 抛出不合理参数异常
        }
        if (num == 1) {
          return 1; // 跳出循环
        } else {
          sum = num * Waytwo(num - 1); // 递归
          return sum;
        }
      }
    
      /**
       * 数组添加计算阶乘
       *
       * @param num
       * @return
       */
      public static long Waythree(int num) {
        long[] arr = new long[21];
        arr[0] = 1;
        int last = 0;
        if (num >= arr.length) {
          throw new IllegalArgumentException("传入的值太大了!"); // 抛出不合理参数异常
        }
        if (num < 0) {
          throw new IllegalArgumentException("需要计算的参数必须为正数!"); // 抛出不合理参数异常
        }
        while (last < num) {
          arr[last + 1] = arr[last] * (last + 1);
          last++;
        }
        return arr[num];
      }
    
      /**
       * 用BigInteger类计算阶乘
       *
       * @param num
       * @return
       */
      public static synchronized BigInteger Wayfour(int num) {
        ArrayList list = new ArrayList();
        list.add(BigInteger.valueOf(1));
        for (int i = list.size(); i <= num; i++) {
          BigInteger lastfact = (BigInteger) list.get(i - 1);
          BigInteger nextfact = lastfact.multiply(BigInteger.valueOf(i));
          list.add(nextfact);
        }
        return (BigInteger) list.get(num);
      }
    }

九:Java继承和多态

1):类的封装

  • 定义:封装将类的某些信息隐藏在类内部,不允许外部程序直接访问,只能通过该类提供的方法来实现对隐藏信息的操作和访问

  • 封装的特点:

    • 只能通过规定的方法访问数据。
    • 隐藏类的实例细节,方便修改和实现。
  • 实现封装的具体步骤如下:

    1. 修改属性的可见性来限制对属性的访问,一般设为 private。
    2. 为每个属性创建一对赋值(setter)方法和取值(getter)方法,一般设为 public,用于属性的读写。
    3. 在赋值和取值方法中,加入属性控制语句(对属性值的合法性进行判断)。
  1. 例1 (封装员工类Employee)

    public class Employee {
      private String name; // 姓名
      private int age; // 年龄
      private String phone; // 联系电话
      private String address; // 家庭住址
    
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public int getAge() {
        return age;
      }
    
      public void setAge(int age) {
        if (age < 18 || age > 40) {
          System.out.println("年龄必须在18到40岁之间!");
          this.age = 20;
        } else {
          this.age = age;
        }
      }
    
      public String getPhone() {
        return phone;
      }
    
      public void setPhone(String phone) {
        this.phone = phone;
      }
    
      public String getAddress() {
        return address;
      }
    
      public void setAddress(String address) {
        this.address = address;
      }
    }
  • main导出

    public class EmployeeTest {
      public static void main(String[] args) {
        Employee employee = new Employee();
        employee.setName("小明");
        employee.setAge(20);
        employee.setPhone("18161369452");
        employee.setAddress("成都");
        System.out.println("姓名:" + employee.getName());
        System.out.println("年龄:" + employee.getAge());
        System.out.println("电话:" + employee.getPhone());
        System.out.println("住址:" + employee.getAddress());
      }
    }
  1. 例2 (封装图书信息类)
  • 需求

    • 基本信息包括图书名称(bookName)、总页数(pagelbtalNum),其中页数不能少于 200 页,否则输出错误信息,并赋予默认值 200。
    • 为各个属性设置赋值和取值方法。
    • 具有 details() 方法,该方法在控制台输出每本图书的名称和总页数。
  • 创建书本信息类

    public class Book {
      private String bookName; // 图书名称
      private int bookTotalNum;
    
      public String getBookName() {
        return bookName;
      }
    
      public void setBookName(String bookName) {
        this.bookName = bookName;
      }
    
      public int getBookTotalNum() {
        return bookTotalNum;
      }
    
      public void setBookTotalNum(int bookTotalNum) {
        if (bookTotalNum < 200) {
          System.out.println(this.bookName + "这本书的页数不能少于200页");
          this.bookTotalNum = 200;
        } else {
          this.bookTotalNum = bookTotalNum;
        }
      }
    
      public void details() {
        System.out.println(this.bookName + "这本书的总页数是:" + this.bookTotalNum);
      }
    }
  • BookTest输出

    public class BookTest {
      public static void main(String[] args) {
        Book book = new Book();
        book.setBookName("大红花");
        book.setBookTotalNum(300);
        book.details();
        Book book1 = new Book();
        book1.setBookName("大红花1");
        book1.setBookTotalNum(100);
        book1.details();
      }
    }

2):继承

  • 定义:继承就是在已经存在类的基础上进行扩展,从而产生新的类。已经存在的类称为父类、基类或超类,而新产生的类称为子类或派生类。在子类中,不仅包含父类的属性和方法,还可以增加新的属性和方法

    修饰符 class class_name extends extend_class {
        // 类的主体
    }

    class_name 表示子类(派生类)的名称;extend_class 表示父类(基类)的名称;extends 关键字直接跟在子类名之后,其后面是该类要继承的父类名称 (java继承都是公有继承)

    public class Student extends Person{}

    类的继承不改变类成员的访问权限,也就是说,如果父类的成员是公有的、被保护的或默认的,它的子类仍具有相应的这些特性,并且子类不能获得父类的构造方法。

  • 例👇 教师和学生之间的继承关系

    • 创建People类

      public class People {
        public String name; // 姓名
        public int age; // 年龄
        public String sex; // 性别
        public String sn; // 身份证号
        /*构造方法*/
        public People(String name, int age, String sex, String sn) {
          this.name = name;
          this.age = age;
          this.sex = sex;
          this.sn = sn;
        }
      
        @Override
        public String toString() {
          return "姓名:" + this.name + "\n年龄:" + this.age + "\n性别:" + this.sex + "\n身份证号:" + this.sn;
        }
      }
    • 创建People的子类Student

      public class Student extends People {
        private String stuNo; // 学号
        private String department; // 所学专业
      
        public Student(String name, int age, String sex, String sn, String stuno, String department) {
          super(name, age, sex, sn); // 调用父类中的构造方法
          this.stuNo = stuno;
          this.department = department;
        }
      
        @Override
        public String toString() {
          return "姓名:"
              + name
              + "\n年龄:"
              + age
              + "\n性别:"
              + sex
              + "\n身份证号:"
              + sn
              + "\n学号:"
              + stuNo
              + "\n所学专业:"
              + department;
        }
      }

      注意:如果在父类中存在有参的构造方法而并没有重载无参的构造方法,那么在子类中必须含有有参的构造方法,因为如果在子类中不含有构造方法,默认会调用父类中无参的构造方法,而在父类中并没有无参的构造方法,因此会出错。

    • 创建People的子类Teacher

      public class Teacher extends People {
        private int tYear; // 教龄
        private String tDept; // 所教专业
      
        public Teacher(String name, int age, String sex, String sn, int tYear, String tDept) {
          super(name, age, sex, sn); // 调用父类中的构造方法
          this.tYear = tYear;
          this.tDept = tDept;
        }
      
        @Override
        public String toString() {
          return "姓名:" + name + "\n年龄:" + age + "\n性别:" + sex + "\n身份证号:" + sn + "\n教龄:" + tYear
              + "\n所教专业:" + tDept;
        }
      }
    • 打印输出

      public class PeopleTest {
        public static void main(String[] args) {
          // 创建Student类对象
          People stuPeople = new Student("王丽丽", 23, "女", "410521198902145589", "00001", "计算机应用与技术");
          System.out.println("----------------学生信息---------------------");
          System.out.println(stuPeople);
          // 创建Teacher类对象
          People teaPeople = new Teacher("张文", 30, "男", "410521198203128847", 5, "计算机应用与技术");
          System.out.println("----------------教师信息----------------------");
          System.out.println(teaPeople);
        }
      }

1:单继承

  • 定义:Java 不支持多继承,只允许一个类直接继承另一个类,即子类只能有一个直接父类,extends 关键字后面只能有一个类名

    class Student extends Person,Person1,Person2{}
    class Student extends Person,extends Person1,extends Person2{} // 编译错误
  • 一个类只能有一个直接父类,但是它可以有多个间接的父类 例:Student 类继承 Person 类,Person 类继承 Person1 类,Person1 类继承 Person2 类,那么 Person1 和 Person2 类是 Student 类的间接父类

  • 使用继承的注意点:

    1. 子类一般比父类包含更多的属性和方法。
    2. 父类中的 private 成员在子类中是不可见的,因此在子类中不能直接使用它们。
    3. 父类和其子类间必须存在“是一个”即“is-a”的关系,否则不能用继承。但也并不是所有符合“is-a”关系的都应该用继承。例如,正方形是一个矩形,但不能让正方形类来继承矩形类,因为正方形不能从矩形扩展得到任何东西。正确的继承关系是正方形类继承图形类。
    4. Java 只允许单一继承(即一个子类只能有一个直接父类),C++ 可以多重继承(即一个子类有多个直接父类)

2:继承的优缺点

在面向对象语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:

  1. 实现代码共享,减少创建类的工作量,使子类可以拥有父类的方法和属性。
  2. 提高代码维护性和可重用性。
  3. 提高代码的可扩展性,更好的实现父类的方法。

自然界的所有事物都是优点和缺点并存的,继承的缺点如下:

  1. 继承是侵入性的。只要继承,就必须拥有父类的属性和方法。
  2. 降低代码灵活性。子类拥有父类的属性和方法后多了些约束。
  3. 增强代码耦合性(开发项目的原则为高内聚低耦合)。当父类的常量、变量和方法被修改时,需要考虑子类的修改,有可能会导致大段的代码需要重构。

3):super关键字

  • 定义:由于子类不能继承父类的构造方法,因此,如果要调用父类的构造方法,可以使用 super 关键字。super 可以用来访问父类的构造方法、普通方法和属性。
  • super 关键字的功能:
    • 在子类的构造方法中显式的调用父类构造方法
    • 访问父类的成员方法和变量。

1:super调用父类构造方法

  • super 关键字可以在子类的构造方法中显式地调用父类的构造方法

    super(parameter-list);

    parameter-list 指定了父类构造方法中的所有参数。super()必须是在子类构造方法的方法体的第一行。

  • 声明父类 Person 和子类 Student,在 Person 类中定义一个带有参数的构造方法,

    public class Person {
        public Person(String name) {
        }
    }
    public class Student extends Person {
    } // 报错 Implicit super constructor Person() is undefined for default constructor. Must define an explicit constructor
  • 声明父类Person,定义两个构造方法

    public class Person {
        public Person(String name, int age) {
        }
        public Person(String name, int age, String sex) {
        }
    }
  • 子类 Student 继承了 Person 类,使用 super 语句来定义 Student 类的构造方法。

    public class Student extends Person {
        public Student(String name, int age, String birth) {
            super(name, age); // 调用父类中含有2个参数的构造方法
        }
        public Student(String name, int age, String sex, String birth) {
            super(name, age, sex); // 调用父类中含有3个参数的构造方法
        }
    }

2:super访问父类成员

  • 定义:使用 super 访问父类中的成员与 this 关键字的使用相似,只不过它引用的是子类的父类

    super.member

    member 是父类中的属性或方法。使用 super 访问父类的属性和方法时不用位于第一行。

✔:super调用成员属性
  • 当父类和子类具有相同的数据成员时,JVM 可能会模糊不清,以👇示例

    class Person {
        int age = 12;
    }
    class Student extends Person {
        int age = 18;
        void display() {
            System.out.println("学生年龄:" + super.age);
        }
    }
    class Test {
        public static void main(String[] args) {
            Student stu = new Student();
            stu.display();
        }
    }
✔:super调用成员方法
  • 当父类和子类都具有相同的方法名时,可以使用 super 关键字访问父类的方法,以👇示例

    class Person {
        void message() {
            System.out.println("This is person class");
        }
    }
    class Student extends Person {
        void message() {
            System.out.println("This is student class");
        }
        void display() {
            message();
            super.message();
        }
    }
    class Test {
        public static void main(String args[]) {
            Student s = new Student();
            s.display();
        }
    }
✔:super和this的区别
  • this 指的是当前对象的引用,super 是当前对象的父对象的引用。

  • super 关键字的用法:

    • super.父类属性名:调用父类中的属性
    • super.父类方法名:调用父类中的方法
    • super():调用父类的无参构造方法
    • super(参数):调用父类的有参构造方法

    如果构造方法的第一行代码不是 this() 和 super(),则系统会默认添加 super()。

  • this 关键字的用法:

    • this.属性名:表示当前对象的属性
    • this.方法名(参数):表示调用当前对象的方法
  • 关于 Java super 和 this 关键字的异同,可简单总结为以下几条。

    1. 子类和父类中变量或方法名称相同时,用 super 关键字来访问。可以理解为 super 是指向自己父类对象的一个指针。在子类中调用父类的构造方法。
    2. this 是自身的一个对象,代表对象本身,可以理解为 this 是指向对象本身的一个指针。在同一个类中调用其它方法。
    3. this 和 super 不能同时出现在一个构造方法里面,因为 this 必然会调用其它的构造方法,其它的构造方法中肯定会有 super 语句的存在,所以在同一个构造方法里面有相同的语句,就失去了语句的意义,编译器也不会通过。
    4. this( ) 和 super( ) 都指的是对象,所以,均不可以在 static 环境中使用,包括 static 变量、static 方法和 static 语句块。
    5. 从本质上讲,this 是一个指向对象本身的指针, 然而 super 是一个 Java 关键字
  • 定义父类Animal

    public class Animal {
      public String name; // 动物名字
    }
  • 定义子类Cat

    public class Cat extends Animal {
      private String dname;
    
      public Cat(String name, String dname) {
        super.name = name; // super关键字来访问父类中的name属性
        this.dname = dname; // this关键字来访问奔雷的dname的属性
      }
    
      @Override
      public String toString() {
        return "我是" + super.name + ",我的名字叫" + this.dname;
      }
    
      public static void main(String[] args) {
        Animal a = new Cat("动物", "喵星人");
        System.out.println(a);
      }
    }

4):对象类型转换

  • 定义:对象类型转换,是指存在继承关系的对象,不是任意类型的对象当对不存在继承关系的对象进行强制类型转换时,会抛出 Java 强制类型转换(java.lang.ClassCastException)异常。Java 语言允许某个类型的引用变量引用子类的实例,而且可以对这个引用变量进行类型转换

1:向上转型

  • 定义:父类引用指向子类对象为向上转型

    fatherClass obj = new sonClass();

    fatherClass 是父类名称或接口名称,obj 是创建的对象,sonClass 是子类名称。

  • 向上转型就是把子类对象直接赋给父类引用,不用强制转换。使用向上转型可以调用父类类型中的所有成员,不能调用子类类型中特有成员,

2:向下转型

  • 定义:与向上转型相反,子类对象指向父类引用为向下转型

    sonClass obj = (sonClass) fatherClass;

    fatherClass 是父类名称,obj 是创建的对象,sonClass 是子类名称。

  • 向下转型可以调用子类类型中所有的成员,不过需要注意的是如果父类引用对象指向的是子类对象,那么在向下转型的过程中是安全的,也就是编译是不会出错误。但是如果父类引用对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错,但是运行时会出现我们开始提到的 Java 强制类型转换异常,一般使用 instanceof 运算符来避免出此类错误

    Animal animal = new Dog();    // 向上转型,把Dog类型转换为Animal类型
    Dog dog = (Dog) animal; // 向下转型,把Animal类型转换为Dog类型
  • 演示对象类型的转换

    • 父类Animal

      public class Animal {
        public String name = "Animal:动物";
        public static String staticName = "Aniaml:可爱的动物";
      
        public void eat() {
          System.out.println("Animal:吃饭");
        }
      
        public static void staticEat() {
          System.out.println("Animal:动物在吃饭");
        }
      }
    • 子类Cat

      public class Cat extends Animal {
        public String name = "Cat:猫";
        public String str = "Cat:可爱的小猫";
        public static String staticName = "Cat:我是喵星人";
      
        @Override
        public void eat() {
          System.out.println("cat:吃饭");
        }
      
        public static void staticEat() {
          System.out.println("Cat:猫在吃饭");
        }
      
        public void eatMethod() {
          System.out.println("Cat:猫喜欢吃饭");
        }
      
        public static void main(String[] args) {
          Animal animal = new Cat();
          Cat cat = (Cat) animal; // 向下转型
          System.out.println(animal.name); // 输出Animal类的name变量
          System.out.println(animal.staticName); // 输出Animal类的staticName的变量
          animal.eat(); // 输出Cat类的eat()方法
          animal.staticEat(); // 输出Animal类的staticEat()方法
          System.out.println(cat.str); // 调用Cat类的str变量
          cat.eatMethod(); // 调用Cat类的eatMethod()方法
          cat.staticEat();
        }
      }
  • 通过引用类型变量来访问所引用对象的属性和方法时,Java 虚拟机将采用以下绑定规则:

    • 实例方法与引用变量实际引用的对象的方法进行绑定,这种绑定属于动态绑定,因为是在运行时由 Java 虚拟机动态决定的。例如,animal.eat() 是将 eat() 方法与 Cat 类绑定。
    • 静态方法与引用变量所声明的类型的方法绑定,这种绑定属于静态绑定,因为是在编译阶段已经做了绑定。例如,animal.staticEat() 是将 staticEat() 方法与 Animal 类进行绑定。
    • 成员变量(包括静态变量和实例变量)与引用变量所声明的类型的成员变量绑定,这种绑定属于静态绑定,因为在编译阶段已经做了绑定。例如,animal.name 和 animal.staticName 都是与 Animal 类进行绑定。

3:强制类型转型

  • 定义:Java 编译器允许在具有直接或间接继承关系的类之间进行类型转换。对于向下转型,必须进行强制类型转换;对于向上转型,不必使用强制类型转换。

    animal.str = "";    // 编译出错,提示Animal类中没有str属性
    animal.eatMethod();    // 编译出错,提示Animal类中没有eatMethod()方法
  • 如果要访问 Cat 类的成员,必须通过强制类型转换

    ((Cat)animal).str = "";    // 编译成功
    ((Cat)animal).eatMethod();    // 编译成功
    Cat cat = (Cat)animal;    // 编译成功,将Animal对象类型强制转换为Cat对象类型
  • 类型强制转换时想运行成功就必须保证父类引用指向的对象一定是该子类对象,最好使用 instanceof 运算符判断后,再强转

    Animal animal = new Cat();
    if (animal instanceof Cat) {
        Cat cat = (Cat) animal; // 向下转型
        ...
    }
  • 子类的对象可以转换成父类类型,而父类的对象实际上无法转换为子类类型。因为通俗地讲,父类拥有的成员子类肯定也有,而子类拥有的成员,父类不一定有。因此,对于向上转型,不必使用强制类型转换

    Cat cat = new Cat();
    Animal animal = cat;    // 向上转型,不必使用强制类型转换
  • 如果两种类型之间没有继承关系,那么将不允许进行类型转换

    Dog dog = new Dog();
    Cat cat = (Cat)dog;    // 编译出错,不允许把Dog对象类型转换为Cat对象类型
  • 案例 为什么使用向上转型而不直接创建子类对象?

    public class Animal {
    
      public void sleep() {
        System.out.println("小动物在睡觉");
      }
    
      public static void doSleep(Animal animal) {
        // 此时的参数是父类对象,但是实际调用时传递的是子类对象,就是向上转型。
        animal.sleep();
      }
    
      public static void main(String[] args) {
        Animal.doSleep(new Cat());
        Animal.doSleep(new Dog());
      }
    }
  • 子类Cat

    public class Cat extends Animal {
      @Override
      public void sleep() {
        System.out.println("猫正在睡觉");
      }
    }
  • 子类Dog

    public class Dog extends Animal {
      @Override
      public void sleep() {
        System.out.println("狗正在睡觉");
      }
    }

    如果不用向上转型则必须写两个 doSleep 方法,一个传递 Cat 类对象,一个传递 Dog 类对象。这还是两个子类,如果有多个子类就要写很多相同的方法,造成重复。可以看出向上转型更好的体现了类的多态性,增强了程序的间接性以及提高了代码的可扩展性。当需要用到子类特有的方法时可以向下转型,这也就是为什么要向下转型。

  • 总结如下:

    1. 把子类对象直接赋给父类引用是向上转型,向上转型自动转换。如 Father father = new Son();
    2. 指向子类对象的父类引用赋给子类引用是向下转型,要强制转换。使用向下转型,必须先向上转型,为了安全可以用 instanceof 运算符判断。 如 father 是一个指向子类对象的父类引用,把 father 赋给子类引用 son,即Son son =(Son)father;。其中 father 前面的(Son)必须添加,进行强制转换。
    3. 向上转型不能使用子类特有的属性和方法,只能引用父类的属性和方法,但是子类重写父类的方法是有效的。
    4. 向上转型时会优先使用子类中重写父类的方法,如例 1 中调用的 sleep 方法。
    5. 向上转型的作用是减少重复代码,可以将父类作为参数,这样使代码变得简洁,也更好的体现了多态

5):方法重载

  • 定义:如果同一个类中包含了两个或两个以上方法名相同的方法,但形参列表不同,这种情况被称为方法重载(overload)

  • 例👇 在 JDK 的 java.io.PrintStream 中定义了十多个同名的 println() 方法

    public void println(int i){}
    public void println(double d){}
    public void println(String s){}

    根据参数的不同来区分它们,以进行不同的格式化处理和输出。它们之间就构成了方法的重

    System.out.println(102);    // 调用println(int i)方法
    System.out.println(102.25);    // 调用println(double d)方法
    System.out.println("价格为 102.25");    // 调用println(String s)方法
  • 方法重载的要求是两同一不同:同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法返回值类型、修饰符等,与方法重载没有任何关系。

  • 优点:使用方法重载其实就是避免出现繁多的方法名,有些方法的功能是相似的,如果重新建立一个方法,重新取个方法名称,会降低程序可读性。

  • 示例👇 重载比较两个数值

    public class OverLoading {
      public void max(int a, int b) {
        // 含有两个int类型参数的方法
        System.out.println(a > b ? a : b);
      }
    
      public void max(double a, double b) {
        // 含有两个int类型参数的方法
        System.out.println(a > b ? a : b);
      }
    
      public void max(double a, double b, int c) {
        // 含有两个double类型参数和一个int类型参数的方法
        double max = (double) (a > b ? a : b);
        System.out.println(c > max ? c : max);
      }
    
      public static void main(String[] args) {
        OverLoading ol = new OverLoading();
        System.out.println("1 与 5 比较,较大的是:");
        ol.max(1, 5);
        System.out.println("5.205 与 5.8 比较,较大的是:");
        ol.max(5.205, 5.8);
        System.out.println("2.15、0.05、58 中,较大的是:");
        ol.max(2.15, 0.05, 58);
      }
    }

6):方法重写

  • 定义:在子类中如果创建了一个与父类中相同名称、相同返回值类型、相同参数列表的方法,只是方法体中的实现不同,以实现不同于父类的功能,这种方式被称为方法重写(override),又称为方法覆盖。子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。

  • 在重写方法时,需要遵循下面的规则:

    • 参数列表必须完全与被重写的方法参数列表相同。
    • 返回的类型必须与被重写的方法的返回类型相同(Java1.5 版本之前返回值类型必须一样,之后的 Java 版本放宽了限制,返回值类型必须小于或者等于父类方法的返回值类型)。
    • 访问权限不能比父类中被重写方法的访问权限更低(public>protected>default>private)。
    • 重写方法一定不能抛出新的检査异常或者比被重写方法声明更加宽泛的检査型异常。例如,父类的一个方法声明了一个检査异常 IOException,在重写这个方法时就不能抛出 Exception,只能拋出 IOException 的子类异常,可以抛出非检査异常
  • 另外还要注意以下几条:

    • 重写的方法可以使用 @Override 注解来标识。
    • 父类的成员方法只能被它的子类重写。
    • 声明为 final 的方法不能被重写。
    • 声明为 static 的方法不能被重写,但是能够再次声明。
    • 构造方法不能被重写。
    • 子类和父类在同一个包中时,子类可以重写父类的所有方法,除了声明为 private 和 final 的方法。
    • 子类和父类不在同一个包中时,子类只能重写父类的声明为 public 和 protected 的非 final 方法。
    • 如果不能继承一个方法,则不能重写这个方法。
  • 示例 父类中定义getInfo方法

    public class Animal {
      public String name; // 姓名
      public int age; // 年龄
    
      public Animal(String name, int age) {
        this.name = name;
        this.age = age;
      }
    
      public String getInfo() {
        return "我叫" + name + ",今年" + age + "岁了";
      }
    }
  • 子类cat重写gitInfo方法

    public class Cat extends Animal {
      private String hobby;
    
      public Cat(String name, int age, String hobby) {
        super(name, age);
        this.hobby = hobby;
      }
    
      @Override
      public String getInfo() {
        return "喵,大家好!我叫" + this.name + ",我今年" + this.age + "岁了,我爱吃" + hobby + "。";
      }
    
      public static void main(String[] args) {
        Animal a = new Cat("小白", 2, "鱼");
        System.out.println(a.getInfo());
      }
    }

7):多态性

  • 定义:多态性是面向对象编程的又一个重要特征,它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为

    对面向对象来说,多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是大家通常所说的多态性

  • Java 实现多态有 3 个必要条件:继承、重写和向上转型。只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。

    • 继承:在多态中必须存在有继承关系的子类和父类。
    • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
    • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。
  • 示例 创建Figure类,定义两个构造函数,计算对象的面积

    public class Figure {
      double dim1;
      double dim2;
    
      Figure(double d1, double d2) {
        // 有参的构造方法
        this.dim1 = d1;
        this.dim2 = d2;
      }
    
      double area() {
        // 用于计算对象的面积
        System.out.println("父类中计算对象面积的方法,没有实际意义,需要在子类中重写。");
        return 0;
      }
    }
  • 创建Figure的子类Rectangle,调用父类方法,重写area()

    public class Rectangle extends Figure {
      Rectangle(double d1, double d2) {
        super(d1, d2);
      }
    
      @Override
      double area() {
        System.out.println("长方形的面积:");
        return super.dim1 * super.dim2;
      }
    }
  • 创建Figure的子类Triangle,调用父类方法,重写area()

    public class Triangle extends Figure {
      Triangle(double d1, double d2) {
        super(d1, d2);
      }
    
      @Override
      double area() {
        System.out.println("三角形的面积:");
        return super.dim1 * super.dim2 / 2;
      }
    }
  • 创建Test类,main输出

    public class Test {
      public static void main(String[] args) {
        Figure figure;
        figure = new Rectangle(9, 9);
        System.out.println(figure.area());
        figure = new Triangle(9, 8);
        System.out.println(figure.area());
        figure = new Figure(8, 8);
        System.out.println(figure.area());
      }
    }

8):instanceof

  • 定义:使用 instanceof 关键字判断一个对象是否为一个类(或接口、抽象类、父类)的实例

    boolean result = obj instanceof Class

    obj 是一个对象,Class 表示一个类或接口。obj 是 class 类(或接口)的实例或者子类实例时,结果 result 返回 true,否则返回 false。

  1. 声明一个class类的对象,判断obj是否为class类的实例对象(很普遍的一种用法)

    Integer integer = new Integer(1);
    System.out.println(integer instanceof  Integer);    // true
  2. 声明一个class接口实现类的对象obj,判断obj是否为class接口实现类的实例对象

    • Java 集合中的 List 接口有个典型实现类 ArrayList。

      public class ArrayList<E> extends AbstractList<E>
              implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    • 可以用 instanceof 运算符判断 ArrayList 类的对象是否属于 List 接口的实例,如果是返回 true,否则返回 false

      ArrayList arrayList = new ArrayList();
      System.out.println(arrayList instanceof List);    // true
    • 反之

      List list = new ArrayList();
      System.out.println(list instanceof ArrayList); // true
  3. obj是class类的直接或间接子类

    public class Person {} // Person类
    public class Man extends Person {} // Person类的子类Man
    public class Test2 {
      public static void main(String[] args) {
        Person p1 = new Person();
        Person p2 = new Man();
        Man m1 = new Man();
        System.out.println(p1 instanceof Man); // false
        System.out.println(p2 instanceof Man); // true
        System.out.println(m1 instanceof Man); // true
      }
    }

    obj 必须为引用类型,不能是基本类型

    int i = 0;
    System.out.println(i instanceof Integer);    // 编译不通过
    System.out.println(i instanceof Object);    // 编译不通过
  • 当 obj 为 null 时,直接返回 false,因为 null 没有引用任何对象

    Integer i = 1;
    System.out.println(i instanceof null);    // false
  • 当 class 为 null 时,会发生编译错误

    Syntax error on token "null", invalid ReferenceType
  • 检查 obj 能否转换成右边的 class 类型,如果不能转换则直接报错

    Person p1 = new Person();
    System.out.println(p1 instanceof String);    // 编译报错
    System.out.println(p1 instanceof List);    // false
    System.out.println(p1 instanceof List<?>);    // false
    System.out.println(p1 instanceof List<Person>);    // 编译报错
  • instanceof 也经常和三目(条件)运算符一起使用

    A instanceof B ? A : C
  • 示例

    public class Test {
      public Object animalCall(Animal a) {
        String tip = "这个动物不是牛!";
        // 判断参数a是不是Cow的对象
        return a instanceof Cow ? (Cow) a : tip;
      }
      public static void main(String[] args) {
        Sheep sh = new Sheep();
        Test test = new Test();
        System.out.println(test.animalCall(sh));
      }
    }
    class Animal {
    }
    class Cow extends Animal {
    }
    class Sheep extends Animal {
    }

9):抽象类

  • 定义:如果一个类中没有包含足够的信息来描绘一个具体的对象,那么这样的类称为抽象类。

    <abstract>class<class_name> {
        <abstract><type><method_name>(parameter-iist);
    }

    abstract 表示该类或该方法是抽象的;class_name 表示抽象类的名称;method_name 表示抽象方法名称,parameter-list 表示方法参数列表。

  • 如果一个方法使用 abstract 来修饰,则说明该方法是抽象方法,抽象方法只有声明没有实现。需要注意的是 abstract 关键字只能用于普通方法,不能用于 static 方法或者构造方法中

  • 抽象方法的 3 个特征如下:

    1. 抽象方法没有方法体
    2. 抽象方法必须存在于抽象类中
    3. 子类重写父类时,必须重写父类所有的抽象方法

    在使用 abstract 关键字修饰抽象方法时不能使用 private 修饰,因为抽象方法必须被子类重写,而如果使用了 private 声明,则子类是无法重写的。

  • 抽象类的定义和使用规则如下:

    1. 抽象类和抽象方法都要使用 abstract 关键字声明。
    2. 如果一个方法被声明为抽象的,那么这个类也必须声明为抽象的。而一个抽象类中,可以有 0n 个抽象方法,以及 0n 个具体方法。
    3. 抽象类不能实例化,也就是不能使用 new 关键字创建对象
  • 示例 几何图形的面积计算

    1. 创建图形抽象类,定义长和宽

      public abstract class Shape {
        public int width; // 几何图形的长
        public int height; // 几何图形的宽
      
        public Shape(int width, int height) {
          this.width = width;
          this.height = height;
        }
      
        public abstract double area(); // 定义抽象方法,计算面积
      }
    2. 定义正方形类,重写area()抽象方法

      public class Square extends Shape {
        public Square(int width, int height) {
          super(width, height);
        }
        /** 重写父类中的抽象方法,实现计算正方形面积 */
        @Override
        public double area() {
          return width * height;
        }
      }
    3. 定义三角形类,以此

      public class Triangle extends Shape {
        public Triangle(int width, int height) {
          super(width, height);
        }
        /** 重写父类中的抽象方法,实现计算三角形面积的功能 */
        @Override
        public double area() {
          return 0.5 * width * height;
        }
      }
    4. 输出

      public class ShapeTest {
        public static void main(String[] args) {
          Square square = new Square(5, 4);
          System.out.println("正方形的面积:" + square.area());
          Triangle triangle = new Triangle(5, 4);
          System.out.println("三角形的面积:" + triangle.area());
        }
      }

10):接口

  • 定义:特殊的“抽象类”——接口(Interface)。接口是 Java 中最重要的概念之一,它可以被理解为一种特殊的类,不同的是接口的成员没有执行体,是由全局常量和公共的抽象方法所组成

1:定义接口

  • 方式与类基本相同,只不过关键字是interface

    [public] interface interface_name [extends interface1_name[, interface2_name,]] {
        // 接口体,其中可以包含定义常量和声明方法
        [public] [static] [final] type constant_name = value;    // 定义常量
        [public] [abstract] returnType method_name(parameter_list);    // 声明方法
    }
  • 对以上语法的说明如下:

    • public 表示接口的修饰符,当没有修饰符时,则使用默认的修饰符,此时该接口的访问权限仅局限于所属的包;

    • interface_name 表示接口的名称。接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可。如果要遵守 Java 可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无需任何分隔符。

    • extends 表示接口的继承关系;

    • interface1_name 表示要继承的接口名称;

    • constant_name 表示变量名称,一般是 static 和 final 型的;

    • returnType 表示方法的返回值类型;

    • parameter_list 表示参数列表,在接口中的方法是没有方法体的。

      注意:一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。

  • 接口对于其声明、变量和方法都做了许多限制,这些限制作为接口的特征归纳如下:

    • 具有 public 访问控制符的接口,允许任何类使用;没有指定 public 的接口,其访问将局限于所属的包。

    • 方法的声明不需要其他修饰符,在接口中声明的方法,将隐式地声明为公有的(public)和抽象的(abstract)。

    • 在 Java 接口中声明的变量其实都是常量,接口中的变量声明,将隐式地声明为 public、static 和 final,即常量,所以接口中定义的变量必须初始化。

    • 接口没有构造方法,不能被实例化

      public interface A {
          publicA(){}    // 编译出错,接口不允许定义构造方法
      }
    • 一个接口不能够实现另一个接口,但它可以继承多个其他接口。子接口可以对父接口的方法和常量进行重写

      public interface StudentInterface extends PeopleInterface {
          // 接口 StudentInterface 继承 PeopleInterface
          int age = 25;    // 常量age重写父接口中的age常量
          void getInfo();    // 方法getInfo()重写父接口中的getInfo()方法
      }
    • 定义一个接口 MyInterface,并在该接口中声明常量和方法

      public interface MyInterface {    // 接口myInterface
          String name;    // 不合法,变量name必须初始化
          int age = 20;    // 合法,等同于 public static final int age = 20;
          void getInfo();    // 方法声明,等同于 public abstract void getInfo();
      }

2:实现接口

  • 定义:接口的主要用途就是被实现类实现,一个类可以实现一个或多个接口,继承使用 extends 关键字,实现则使用 implements 关键字

    <public> class <class_name> [extends superclass_name] [implements interface1_name[, interface2_name…]] {
        // 主体
    }
  • 对以上语法的说明如下:

    • public:类的修饰符;
    • superclass_name:需要继承的父类名称;
    • interface1_name:要实现的接口名称。
  • 实现接口需要注意以下几点:

    • 实现接口与继承父类相似,一样可以获得所实现接口里定义的常量和方法。如果一个类需要实现多个接口,则多个接口之间以逗号分隔。
    • 一个类可以继承一个父类,并同时实现多个接口,implements 部分必须放在 extends 部分之后。
    • 一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。
  • 示例 求和

    • 定义一个IMath接口

      public interface IMath {
          public int sum();    // 完成两个数的相加
          public int maxNum(int a,int b);    // 获取较大的数
      }
    • MathClass类

      public class MathClass implements IMath {
        private int num1; // 第 1 个操作数
        private int num2; // 第 2 个操作数
      
        public MathClass(int num1, int num2) {
          // 构造方法
          this.num1 = num1;
          this.num2 = num2;
        }
        // 实现接口中的求和方法
        @Override
        public int sum() {
          return num1 + num2;
        }
        // 实现接口中的获取较大数的方法
        @Override
        public int maxNum(int a, int b) {
          return Math.max(a, b);
        }
      }
    • NumTest输出

      public class NumTest {
        public static void main(String[] args) {
          MathClass mathClass = new MathClass(500, 300);
          System.out.println("100 和 300 相加结果是:" + mathClass.sum());
          System.out.println("301 比较 300,哪个大:" + mathClass.maxNum(301, 300));
        }
      }

11):抽象类和接口的区别

1:抽象类

  • 定义:被关键字 abstract 修饰的类称为抽象类;被 abstract 修饰的方法称为抽象方法,抽象方法只有方法声明没有方法体
  • 抽象类有以下几个特点:
    1. 抽象类不能被实例化,只能被继承。
    2. 包含抽象方法的类一定是抽象类,但抽象类不一定包含抽象方法(抽象类可以包含普通方法)。
    3. 抽象方法的权限修饰符只能为 public、protected 或 default,默认情况下为 public。
    4. 一个类继承于一个抽象类,则子类必须实现抽象类的抽象方法,如果子类没有实现父类的抽象方法,那子类必须定义为抽象类。
    5. 抽象类可以包含属性、方法、构造方法,但构造方法不能用来实例化对象,只能被子类调用

2:接口

  • 定义:接口可以看成是一种特殊的类,只能用 interface 关键字修饰
  • Java 中的接口具有以下几个特点:
    1. 接口中可以包含变量和方法,变量被隐式指定为 public static final,方法被隐式指定为 public abstract(JDK 1.8 之前)。
    2. 接口支持多继承,即一个接口可以继承(extends)多个接口,间接解决了 Java 中类不能多继承的问题。
    3. 一个类可以同时实现多个接口,一个类实现某个接口则必须实现该接口中的抽象方法,否则该类必须被定义为抽象类。

3:抽象类和接口的区别

  • 接口和抽象类很像,它们都具有如下特征。

    • 接口和抽象类都不能被实例化,主要用于被其他类实现和继承。
    • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
  • 差异

    参数 抽象类 接口
    实现 子类使用 extends 关键字来继承抽象类,如果子类不是抽象类,则需要提供抽象类中所有声明的方法的实现。 子类使用 implements 关键字来实现接口,需要提供接口中所有声明的方法的实现。
    访问修饰符 可以用 public、protected 和 default 修饰 默认修饰符是 public,不能使用其它修饰符
    方法 完全可以包含普通方法 只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现
    变量 既可以定义普通成员变量,也可以定义静态常量 只能定义静态常量,不能定义普通成员变量
    构造方法 抽象类里的构造方法并不是用于创建对象,而是让其子类调用这些构造方法来完成属于抽象类的初始化操作 没有构造方法
    初始化块 可以包含初始化块 不能包含初始化块
    main 方法 可以有 main 方法,并且能运行 没有 main 方法
    与普通Java类的区别 抽象类不能实例化,除此之外和普通 Java 类没有任何区别 是完全不同的类型
    运行速度 比接口运行速度要快 需要时间去寻找在类种实现的方法,所以运行速度稍微有点慢

4:抽象类和接口的应用场景

:name_badge::抽象类的应用场景
  1. 父类只知道其子类应该包含怎样的方法,不能准确知道这些子类如何实现这些方法的情况下,使用抽象类。
  2. 从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为子类的模板,从而避免了子类设计的随意性
:name_badge::接口的应用场景
  1. 一般情况下,实现类和它的抽象类之前具有 “is-a” 的关系,但是如果我们想达到同样的目的,但是又不存在这种关系时,使用接口。
  2. 由于 Java 中单继承的特性,导致一个类只能继承一个类,但是可以实现一个或多个接口,此时可以使用接口。
:name_badge::什么时候使用抽象类和接口
  • 如果拥有一些方法并且想让它们有默认实现,则使用抽象类。
  • 如果想实现多重继承,那么必须使用接口。因为 Java 不支持多继承,子类不能继承多个类,但可以实现多个接口,因此可以使用接口。
  • 如果基本功能在不断改变,那么就需要使用抽象类。如果使用接口并不断需要改变基本功能,那么就需要改变所有实现了该接口的类。

12):内部类

  • 定义:如果在类 Outer 的内部再定义一个类 Inner,此时类 Inner 就称为内部类(或称为嵌套类),而类 Outer 则称为外部类(或称为宿主类)。

    内部类拥有外部类的所有元素的访问权限(private和protected)权限,非内部类则没有这些权限

    内部类可以分为:实例内部类、静态内部类和成员内部类

  • 在类 A 中定义类 B,那么类 B 就是内部类,也称为嵌套类,相对而言,类 A 就是外部类。如果有多层嵌套,例如类 A 中有内部类 B,而类 B 中还有内部类 C,那么通常将最外层的类称为顶层类(或者顶级类)

  • 内部类的特点如下:

    1. 内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号。
    2. 内部类不能用普通的方式访问。内部类是外部类的一个成员,因此内部类可以自由地访问外部类的成员变量,无论是否为 private 的。
    3. 内部类声明成静态的,就不能随便访问外部类的成员变量,仍然是只能访问外部类的静态成员变量。
  • 示例

    public class TestDemo {
      public class InnerClass {
        public int getSum(int x, int y) {
          return x + y;
        }
      }
    
      public static void main(String[] args) {
        TestDemo.InnerClass ti = new TestDemo().new InnerClass();
        int i = ti.getSum(2, 3);
        System.out.println(i);
      }
    }
  • 有关内部类的说明有如下几点。

    • 外部类只有两种访问级别:public 和默认;内部类则有 4 种访问级别:public、protected、private和默认。

    • 在外部类中可以直接通过内部类的类名访问内部类。

      InnerClass ic = new InnerClass();    // InnerClass为内部类的类名
    • 在外部类以外的其他类中则需要通过内部类的完整类名访问内部类。

      Test.InnerClass ti = newTest().new InnerClass();    // Test.innerClass是内部类的完整类名
    • 内部类与外部类不能重名。

13):实例内部类

  • 定义:实例内部类是指没有用 static 修饰的内部类,有的地方也称为非静态内部类

    public class Outer {
        class Inner {
            // 实例内部类
        }
    }
  1. 在外部类的静态方法和外部类以外的其他类中,必须通过外部类的实例创建内部类的实例

    public class Outer {
      class Inner1 {}
    
      Inner1 i = new Inner1(); // 不需要创建外部类实例
    
      public void method1() {
        Inner1 i = new Inner1(); // 不需要创建外部类实例
      }
    
      public static void method2() {
        Inner1 i = new Outer().new Inner1(); // 需要创建外部类实例
      }
    
      class Inner2 {}
    }
    
    class OtherClass {
      Outer.Inner1 i = new Outer().new Inner1(); // 需要创建外部类实例
    }
  2. 在实例内部类中,可以访问外部类的所有成员

    public class Outer1 {
      public int a = 100;
      public int b = 100;
      final int c = 100;
      private int d = 100;
    
      public String method1() {
        return "实例方法1";
      }
    
      public static String method2() {
        return "静态方法2";
      }
    
      class Inner {
        int a2 = a + 1; // 访问public的a
        int b2 = b + 1; // 访问static的b
        int c2 = c + 1; // 访问final的c
        int d2 = d + 1; // 访问private的d
        String str1 = method1(); // 访问实例方法method1
        String str2 = method2(); // 访问静态方法method2
      }
    
      public static void main(String[] args) {
        Inner i = new Outer1().new Inner(); // 创建内部类实例
        System.out.println(i.a2); // 输出101
        System.out.println(i.b2); // 输出101
        System.out.println(i.c2); // 输出101
        System.out.println(i.d2); // 输出101
        System.out.println(i.str1); // 输出实例方法1
        System.out.println(i.str2); // 输出静态方法2
      }
    }

    提示:如果有多层嵌套,则内部类可以访问所有外部类的成员

  3. 在外部类中不能直接访问内部类的成员,而必须通过内部类的实例去访问。如果类A包含内部类B,类B中包含内部类C,则在类A中不能直接访问类C,而应该通过类B的实例去访问类C。

  4. 外部类实例与内部类实例是一对多的关系,也就是说一个内部类实例只对应一个外部类实例,而一个外部类实例则可以对应多个内部类实例

    如果实例内部类B与外部类A包含有同名的成员t,则在类B中t和this.t 都表示B中的成员t,而A.this.t表示A中的成员t。

    public class Outer2 {
      int a = 10;
    
      class Inner {
        int a = 20;
        int b1 = a;
        int b2 = this.a;
        int b3 = Outer2.this.a;
      }
    
      public static void main(String[] args) {
        Inner i = new Outer2().new Inner();
        System.out.println(i.b1); // 输出20
        System.out.println(i.b2); // 输出20
        System.out.println(i.b3); // 输出10
      }
    }
  5. 在实例内部类中不能定义static成员,除非同时使用final和static修饰

14):静态内部类

  • 定义:静态内部类是指使用static修饰的内部类

    public class Outer {
        static class Inner {
            // 静态内部类
        }
    }
  1. 在创建静态内部类的实例时,不需要创建外部类的实例

    public class Outer {
        static class Inner {
        }
    }
    class OtherClass {
        Outer.Inner oi = new Outer.Inner();
    }
  2. 静态内部类中可以定义静态成员和实例成员。(外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成员,如果要访问静态内部类中的实例成员,则需要通过静态内部类的实例)

    public class Outer {
        static class Inner {
            int a = 0;    // 实例变量a
            static int b = 0;    // 静态变量 b
        }
    }
    class OtherClass {
        Outer.Inner oi = new Outer.Inner();
        int a2 = oi.a;    // 访问实例成员
        int b2 = Outer.Inner.b;    // 访问静态成员
    }
  3. 静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问

    public class Outer {
        int a = 0;    // 实例变量
        static int b = 0;    // 静态变量
        static class Inner {
            Outer o = new Outer;
            int a2 = o.a;    // 访问实例变量
            int b2 = b;    // 访问静态变量
        }
    }

15):局部内部类

  • 定义:局部内部类是指在一个方法中定义的内部类

    public class Test {
        public void method() {
            class Inner {
                // 局部内部类
            }
        }
    }
  1. 局部内部类与局部变量一样,不能使用访问控制修饰符(public、private 和 protected)和 static 修饰符修饰

  2. 局部内部类只能在当前方法中有效

    public class Test {
        Inner i = new Inner();    // 编译出错
        Test.Inner ti = new Test.Inner();    // 编译出错
        Test.Inner ti2 = new Test().new Inner();    // 编译出错
        public void method() {
            class Inner{
            
            }
            Inner i = new Inner();
        }
    }
  3. 局部内部类中不能定义static成员

  4. 局部内部类中还可以包含内部类,但是这些内部类也不能使用访问控制修饰符(public、private和protected)和static修饰符修饰

  5. 局部内部类可以访问外部类的所有成员

  6. 局部内部类中只可以访问当前方法中final类型的参数与变量。如果方法中的成员与外部类中的成员同名,则可以使用 .this. 的形式访问外部类中的成员

    public class Test4 {
      int a = 0;
      int d = 0;
    
      public void method() {
        int b = 0;
        final int c = 0;
        final int d = 0;
        class Inner {
          int a2 = a; // 访问外部类的成员
          int c2 = c; // 访问方法中的成员
          int d2 = d; // 访问方法中的成员
          int d3 = Test4.this.d; // 访问外部类中的成员
        }
        Inner i = new Inner();
        System.out.println(i.d2);
        System.out.println(i.d3);
      }
    
      public static void main(String[] args) {
        Test4 t = new Test4();
        t.method();
      }
    }

16):匿名类

  • 定义:匿名类是指没有类名的内部类,必须在创建时使用new语句来声明类

    new <类和接口>() {
    	// 类的主体
    }
  • new 语句声明一个新的匿名类,它对一个给定的类进行扩展,或者实现一个给定的接口。使用匿名类可使代码更加简洁、紧凑,模块化程度更高

  • 匿名类有两种实现方式:

    • 继承一个类,重写其方法。
    • 实现一个接口(可以是多个),实现其方法。
    class out {
      void show() {
        System.out.println("调用 Out 类的 show() 方法");
      }
    }
    
    public class TestAnonymousInterClass {
      // 在这个方法中构造一个匿名内部类
      private void show() {
        out anonyInter =
            new out() {
              // 获取匿名内部类的实例
              @Override
              void show() {
                System.out.println("调用匿名类中的 show() 方法");
              }
            };
        anonyInter.show();
      }
    
      public static void main(String[] args) {
        TestAnonymousInterClass test = new TestAnonymousInterClass();
        test.show();
      }
    }
  1. 匿名类和局部内部类一样,可以访问外部类的所有成员,如果匿名类位于一个方法中,则匿名类只能访问方法中final类型的局部变量和参数。

    public class TestDemo2 {
      public static void main(String[] args) {
        int a = 10;
        final int b = 10;
        Out3 anonyInter =
            new Out3() {
              @Override
              void show() {
                System.out.println("调用了匿名类的 show() 方法" + b); // 编译通过
              }
            };
        anonyInter.show();
      }
    }
  2. 匿名类中允许使用非静态代码块进行成员初始化操作

    public class TestDemo2 {
      public static void main(String[] args) {
        int a = 10;
        final int b = 10;
        Out3 anonyInter =
            new Out3() {
              int i;
    
              {
                i = 10;
              }
    
              @Override
              public void show() {
                System.out.println("调用了匿名类的 show() 方法" + i);
              }
            };
        anonyInter.show();
      }
    }
  3. 匿名类的非静态代码块会在父类的构造方法之后被执行

17):使用内部类实现多继承

  • 使用内部类的继承,保存一个内部类的对象,模拟出一种多重继承
  1. 例、一位同学喜欢唱和跳,但唱跳是抽象类,且想实现类似多重继承功能:

    abstract class Sing {
      /** 抽象方法唱歌 */
      abstract void singASong();
    }
    
    abstract class Dance {
      abstract void danceADance();
    }
    
    class Cai extends Sing {
      @Override
      void singASong() {
        System.out.println("唱歌");
      }
    
      Dance d =
          new Dance() {
            @Override
            void danceADance() {
              System.out.println("跳舞");
            } // 匿名内部类继承了dance抽象类
          };
    }
    
    public class multi {
      public static void main(String[] args) {
        Cai c = new Cai();
        c.singASong();
        c.d.danceADance();
      }
    }
    • 模拟出来的实例有一定缺陷,想用sing引用来接只能用c,想用dance引用来接则只能用c.d
    • 通过保持继承另一个抽象类的匿名内部类对象的引用,来实现多重继承,缺点是必须通过c.d间接调用另一个抽象类的方法
  2. 同一种成员内部类,不同实例

    abstract class sing1 {
      abstract void singASong();
    }
    
    abstract class dance1 {
      abstract void danceADance();
    }
    
    class cai1 extends sing1 {
      @Override
      void singASong() {
        System.out.println("唱歌");
      }
    
      class dancer extends dance1 {
        int count = 0;
    
        @Override
        void danceADance() {
          System.out.println("跳舞");
          System.out.println(this + "已经跳了" + (++count) + "次");
        }
      }
    
      dance1 d1 = new dancer();
      dance1 d2 = new dancer();
    }
    
    public class multi1 {
      public static void main(String[] args) {
        cai1 c = new cai1();
        c.singASong();
        c.d1.danceADance();
        c.d1.danceADance();
      }
    }
    • 定义了成员内部类,以方便创建多个实例。从同一个类创建多个实例,但各个实例能保持各自的状态。
    • 新增了成员count,它保持了自己实例的状态。
  3. 不同的方式实现同一个接口

    abstract class sing2 {
      abstract void singAsing2();
    }
    
    abstract class dance2 {
      abstract void danceDance2();
    }
    
    class cai2 extends sing2 {
      @Override
      void singAsing2() {
        System.out.println("唱歌");
      }
    
      class danceWild extends dance2 {
        int count = 0;
    
        @Override
        void danceDance2() {
          System.out.println("跳舞");
          System.out.println(this + "已经跳了" + (++count) + "次");
        }
      }
    
      class danceGentle extends dance2 {
        int count = 0;
    
        @Override
        void danceDance2() {
          System.out.println("跳舞2");
          System.out.println(this + "已经跳了" + (++count) + "次");
        }
      }
    
      dance2 d1 = new danceWild();
      dance2 d2 = new danceGentle();
    }
    
    public class multi2 {
      public static void main(String[] args) {
        cai2 c = new cai2();
        c.singAsing2();
        c.d1.danceDance2();
        c.d2.danceDance2();
      }
    }

18):Java8新特性:Effectively final

  • 定义:Java中局部内部类和匿名内部类访问的局部变量必须由final修饰,以保证内部类和外部类的数据一致性。Java8不需要加final修饰符。
  1. 测试Java7和Java8

    public class Test {
        public static void main(String[] args) {
            String name = "C语言中文网";
            new Runnable() {
                @Override
                public void run() {
                    System.out.println(name);
                }
            }
        }
    }

    Java7运行结果(必须显式的声明这个变量为 final)

    Jav8运行结果(正常编译)

    Java8运行结果(重写赋值会报错)

    一个非 final 的局部变量或方法参数,其值在初始化后就从未更改,那么该变量就是 effectively final

19):Lambda表达式

  • 定义:Lambda 表达式(Lambda expression)是一个匿名函数,基于数学中的λ演算得名,也可称为闭包(Closure)。

  • 例👇 定义计算数值的接口

    public interface Calculable {
      /** 计算两个int数值 */
      int calculateInt(int a, int b);
    }
  1. 第一种方式(匿名内部类实现运算)

    public class Test {
      /**
       * 通过操作符,进行计算
       *
       * @param opr 操作符
       * @return 实现Calculable接口对象
       */
      public static Calculable calculate(char opr) {
        Calculable result;
        if (opr == '+') {
          // 匿名内部类实现Calculable接口
          result =
              new Calculable() {
                /** 实现加法运算 */
                @Override
                public int calculateInt(int a, int b) {
                  return a + b;
                }
              };
        } else {
          // 匿名内部类实现Calculable接口
          result =
              new Calculable() {
                /** 实现减法运算 */
                @Override
                public int calculateInt(int a, int b) {
                  return a - b;
                }
              };
        }
        return result;
      }
    }
  2. Java8(lambda表达式)

    public class Test1 {
      /**
       * 通过操作符,进行计算
       *
       * @param opr 操作符
       * @return 实现Calculable接口对象
       */
      public static Calculable calculate(char opr) {
        Calculable result;
        if (opr == '+') {
          // lambda表达式实现Calculable加法接口
          result =
              (int a, int b) -> {
                return a + b;
              };
        } else {
          // lambda表达式实现Calculable减法接口
          result =
              (int a, int b) -> {
                return a - b;
              };
        }
        return result;
      }
    }
  3. main输出

    import static com.calcu.Test.calculate;
    import static com.calcu.Test1.calculates;
    
    /**
     * @author Admin
     */
    public class CalculableTest {
      public static void main(String[] args) {
        int n1 = 10;
        int n2 = 15;
        // 实现加法运算Calculable对象
        Calculable f1 = calculate('+');
        // 实现加法运算Calculable对象
        Calculable f2 = calculate('-');
        // 调用calculateInt方法进行加法计算
        System.out.println(n1 + "+" + n2 + "=" + f1.calculateInt(n1, n2));
        // System.out.printf("%d + %d = %d \n", n1, n2, f1.calculateInt(n1, n2));
        // 调用calculateInt方法进行减法计算
        System.out.println(n1 + "-" + n2 + "=" + f2.calculateInt(n1, n2));
        // System.out.printf("%d - %d = %d \n", n1, n2, f2.calculateInt(n1, n2));
        int c1 = 100;
        int c2 = 101;
        // 实现加法运算Calculable对象
        Calculable d1 = calculates('+');
        // 实现加法运算Calculable对象
        Calculable d2 = calculates('-');
        // 调用calculateInt方法进行加法计算
        System.out.println(c1 + "+" + c2 + "=" + d1.calculateInt(c1, c2));
        // System.out.printf("%d + %d = %d \n", n1, n2, f1.calculateInt(n1, n2));
        // 调用calculateInt方法进行减法计算
        System.out.println(c1 + "-" + c2 + "=" + d2.calculateInt(c1, c2));
        // System.out.printf("%d - %d = %d \n", n1, n2, f2.calculateInt(n1, n2));
      }
    }
  • lambda表达式

    (参数列表) -> {
        // Lambda表达式体
    }
  • ->被称为箭头操作符或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分:

    • 左侧:Lambda 表达式的参数列表。
    • 右侧:Lambda 表达式中所需执行的功能,用{ }包起来,即 Lambda 体。
  • Java Lambda 表达式的优缺点

    • 优点:
      1. 代码简洁,开发迅速
      2. 方便函数式编程
      3. 非常容易进行并行计算
      4. Java 引入 Lambda,改善了集合操作(引入 Stream API)
    • 缺点:
      1. 代码可读性变差
      2. 在非并行计算中,很多计算未必有传统的 for 性能要高
      3. 不容易进行调试
  • 函数式接口

    • 定义:如果一个接口中,有且只有一个抽象的方法(Object 类中的方法不包括在内),那这个接口就可以被看做是函数式接口。这种接口只能有一个方法。如果接口中声明多个抽象方法,那么 Lambda 表达式会发生编译错误

      The target type of this expression must be a functional interface
    • 该接口不是函数式接口,为了防止在函数式接口中声明多个抽象方法,Java 8 提供了一个声明函数式接口注解 @FunctionalInterface

      // 可计算接口
      @FunctionalInterface
      public interface Calculable {
          // 计算两个int数值
          int calculateInt(int a, int b);
      }

    Lambda 表达式是一个匿名方法代码,Java 中的方法必须声明在类或接口中,那么 Lambda 表达式所实现的匿名方法是在函数式接口中声明的

1:Lambda表达式简介

💨:省略参数类型
  • 定义:Lambda 表达式可以根据上下文环境推断出参数类型

    public static Calculable calculate(char opr) {
        Calculable result;
        if (opr == '+') {
            // Lambda表达式实现Calculable接口
            result = (a, b) -> {
                return a + b;
            };
        } else {
            // Lambda表达式实现Calculable接口
            result = (a, b) -> {
                return a - b;
            };
        }
        return result;
    }
💨:省略参数小括号
  • 定义:如果 Lambda 表达式中的参数只有一个,可以省略参数小括号

    // 可计算接口
    @FunctionalInterface
    public interface Calculable {
        // 计算一个int数值
        int calculateInt(int a);
    }
    public static void main(String[] args) {
        int n1 = 10;
        // 实现二次方计算Calculable对象
        Calculable f1 = calculate(2);
        // 实现三次方计算Calculable对象
        Calculable f2 = calculate(3);
        // 调用calculateInt方法进行加法计算
        System.out.printf("%d二次方 = %d \n", n1, f1.calculateInt(n1));
        // 调用calculateInt方法进行减法计算
        System.out.printf("%d三次方 = %d \n", n1, f2.calculateInt(n1));
    }
    /**
    * 通过幂计算
    *
    * @param power 幂
    * @return 实现Calculable接口对象
    */
    public static Calculable calculate(int power) {
        Calculable result;
        if (power == 2) {
            // Lambda表达式实现Calculable接口
            // 标准形式
            result = (int a) -> {
                return a * a;
            };
        } else {
            // Lambda表达式实现Calculable接口
            // 省略形式
            result = a -> {
                return a * a * a;
            };
        }
        return result;
    }
💨:省略return和大括号
  • 如果 Lambda 表达式体中只有一条语句,那么可以省略 return 和大括号

    public static Calculable calculate(int power) {
        Calculable result;
        if (power == 2) {
            // Lambda表达式实现Calculable接口
            // 标准形式
            result = (int a) -> {
                return a * a;
            };
        } else {
            // Lambda表达式实现Calculable接口
            // 省略形式
            result = a -> a * a * a;
        }
        return result;
    }

2:lambda表达式的使用

💨:作为参数使用lambda表达式
  • 定义:Lambda 表达式一种常见的用途就是作为参数传递给方法,这需要声明参数的类型声明为函数式接口类型

    public class display {
      /**
       * @param calc lambda表示
       * @param n1 操作数1
       * @param n2 操作数2
       */
      public static void display(Calculable calc, int n1, int n2) {
        System.out.println(calc.calculateInt(n1, n2));
      }
    
      public static void main(String[] args) {
        int n1 = 10;
        int n2 = 5;
        // 加法计算结果
        display(Integer::sum, n1, n2);
        // 减法计算结果
        display((a, b) -> a - b, n1, n2);
      }
    }
💨:访问变量
  • 定义:Lambda 表达式可以访问所在外层作用域定义的变量,包括成员变量和局部变量
  1. 方法成员变量

    • 成员变量包括实例成员变量和静态成员变量。在 Lambda 表达式中可以访问这些成员变量,此时的 Lambda 表达式与普通方法一样,可以读取成员变量,也可以修改成员变量

      public class LambdaDemo {
        /** 实例成员变量 */
        private int value = 10;
        /* 静态成员变量 */
        private static int staticValue = 5;
        /** 静态方法,进行加法运算 */
        public static Calculable add() {
          return (int a, int b) -> {
            // 访问静态成员变量,不能访问实例成员变量
            staticValue++;
            return a + b + staticValue;
          };
        }
      
          public Calculable sub() {
          return (int a, int b) -> {
            // 访问静态成员变量和实例成员变量
            staticValue++;
            this.value++;
            return a - b - staticValue - this.value;
          };
        }
      }
  2. 访问局部变量

  • 定义:对于成员变量的访问 Lambda 表达式与普通方法没有区别,但是访问局部变量时,变量必须是 final 类型的(不可改变)

    public class LambdaDemo1 {
      /** 实例成员变量 */
      private int value = 10;
      // 静态成员变量
      private static int staticValue = 5;
      // 静态方法,进行加法运算
      public static Calculable add() {
        // 局部变量
        int localValue = 20;
        return (int a, int b) -> {
          // localValue++;
          // 编译错误
          return a + b + localValue;
        };
      }
      /** 实例方法,进行减法运算 */
      public Calculable sub() {
        // final局部变量
        final int localValue = 20;
        return (int a, int b) -> {
          // localValue = c;
          // 编译错误
          return a - b - staticValue - this.value;
        };
      }
    }

    Lambda 表达式只能访问局部变量而不能修改,否则会发生编译错误,但对静态变量和成员变量可读可写。

  1. 方法引用
  • 定义:方法引用可以理解为 Lambda 表达式的快捷写法,它比 Lambda 表达式更加的简洁,可读性更高,有很好的重用性。如果实现比较简单,复用的地方又不多,推荐使用 Lambda 表达式,否则应该使用方法引用。

    Java 8 之后增加了双冒号::运算符,该运算符用于“方法引用”,注意不是调用方法。“方法引用”虽然没有直接使用 Lambda 表达式,但也与 Lambda 表达式有关,与函数式接口有关

    ObjectRef::methodName
    // ObjectRef 是类名或者实例名,methodName 是相应的方法名

    💨:被引用方法的参数列表和返回值类型,必须与函数式接口方法参数列表和方法返回值类型一致

    public class LambdaDemo {
        // 静态方法,进行加法运算
        // 参数列表要与函数式接口方法calculateInt(int a, int b)兼容
        public static int add(int a, int b) {
            return a + b;
        }
        // 实例方法,进行减法运算
        // 参数列表要与函数式接口方法calculateInt(int a, int b)兼容
        public int sub(int a, int b) {
            return a - b;
        }
    }

    💨:LambdaDemo 类中提供了一个静态方法 add,一个实例方法 sub。这两个方法必须与函数式接口方法参数列表一致,方法返回值类型也要保持一致。

    public class HelloWorld {
        public static void main(String[] args) {
            int n1 = 10;
            int n2 = 5;
            // 打印加法计算结果
            display(LambdaDemo::add, n1, n2);
            LambdaDemo d = new LambdaDemo();
            // 打印减法计算结果 
            display(d::sub, n1, n2);
        }
        /**
         * 打印计算结果
         *
         * @param calc Lambda表达式
         * @param n1   操作数1
         * @param n2   操作数2
         */
        public static void display(Calculable calc, int n1, int n2) {
            System.out.println(calc.calculateInt(n1, n2));
        }
    }

    代码第 18 行声明 display 方法,第一个参数 calc 是 Calculable 类型,它可以接受三种对象:Calculable 实现对象、Lambda 表达式和方法引用。代码第 6 行中第一个实际参数LambdaDemo::add是静态方法的方法引用。代码第 9 行中第一个实际参数d::sub,是实例方法的方法引用,d 是 LambdaDemo 实例

    • 代码第 6 行的LambdaDemo::add和第 9 行的d::sub是方法引用,此时并没有调用方法,只是将引用传递给 display 方法,在 display 方法中才真正调用方法

3:Lambda表达式和匿名内部类的联系和区别

  • 定义:Lambda 表达式的一个重要用法是简化某些匿名内部类的写法,因此它可以部分取代匿名内部类的作用
  • Lambda 表达式与匿名内部类的相同点如下:
    • Lambda 表达式与匿名内部类一样,都可以直接访问 effectively final 的局部变量(如果不了解 Effectively final,可先阅读《Java8新特性之Effectively final》一节),以及外部类的成员变量(包括实例变量和类变量)。
    • Lambda 表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
  1. 示例 Lambda表达式和匿名内部类的相似之处

    interface Displayable {
      /** 定义一个抽象方法和默认方法 */
      void display();
    
      default int add(int a, int b) {
        return a + b;
      }
    }
    
    public class LambdaAndInner {
      private int age = 12;
      private static String name = "你好";
    
      public void test() {
        String url = "https://pflink.top";
        Displayable dis =
            () -> {
              // 访问的局部变量
              System.out.println("url局部变量为:" + url);
              // 访问外部类的实例变量和类变量
              System.out.println("外部类的age实例变量为:" + age);
              System.out.println("外部类的name类变量为:" + name);
            };
        dis.display();
        // 调用dis对象从接口中继承的add()方法
        System.out.println(dis.add(3, 4));
      }
    
      public static void main(String[] args) {
        LambdaAndInner lambda = new LambdaAndInner();
        lambda.test();
      }
    }
    • 当程序使用 Lambda 表达式创建了 Displayable 的对象之后,该对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法,如上面程序代码第 26 行所示
  • Lambda 表达式与匿名内部类主要存在如下区别。

    • 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但 Lambda 表达式只能为函数式接口创建实例。
    • 匿名内部类可以为抽象类甚至普通类创建实例;但 Lambda 表达式只能为函数式接口创建实例。
    • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但 Lambda 表达式的代码块不允许调用接口中定义的默认方法。

    对于 Lambda 表达式的代码块不允许调用接口中定义的默认方法的限制,可以尝试对上面的 LambdaAndInner.java 程序稍做修改

    // 尝试调用接口中的默认方法,编译器会报错
    System.out.println(add(3, 5));
  • 虽然 Lambda 表达式的目标类型 Displayable 中包含了 add() 方法,但 Lambda 表达式的代码块不允许调用这个方法;如果将上面的 Lambda 表达式改为匿名内部类的写法,当匿名内部类实现 display() 抽象方法时,则完全可以调用这个 add() 方法

    public void test() {
      String url = "https://pflink.top";
      Displayable dis =
          new Displayable() {
            @Override
            public void display() {
              // 访问的局部变量
              System.out.println("url 局部变量为:" + url);
              // 访问外部类的实例变量和类变量
              System.out.println("外部类的 age 实例变量为:" + age);
              System.out.println("外部类的 name 类变量为:" + name);
              System.out.println(add(3, 5));
            }
          };
      dis.display();
    }

十:Java异常处理

1):异常处理

1:异常简介

  • 定义:异常又称为例外,是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流,为了能够及时有效地处理程序中的运行错误,必须使用异常类,这可以让程序具有极好的容错性且更加健壮。

    • 异常产生的原因:
      1. Java 内部错误发生异常,Java 虚拟机产生的异常。
      2. 编写的程序代码中的错误所产生的异常,例如空指针异常、数组越界异常等。
      3. 通过 throw 语句手动生成的异常,一般用来告知该方法的调用者一些必要信息。
    • 通过面向对象的方法来处理异常。方法运行中发生了异常,则产生该异常一个对象,并提交给系统,则寻找相应的代码处理异常。
    • 生成异常对象,并提交给运行时的系统的过程成为抛出(throw)异常。运行时系统在方法的调用栈中查找,直到找到能够处理该类型异常的对象,这一个过程称为捕获(catch)异常
  • 示例 允许只输入1~3整数,其他情况提示输入错误

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

2:异常类型

  • Java异常类,在所有异常类型都是内置类 java.lang.Throwable 类的子类,即 Throwable 位于异常类层次结构的顶层,Throwable 类下有两个异常分支 Exception 和 Error,如👇图

  • Throwable 类是所有异常和错误的超类,下面有 Error 和 Exception 两个子类分别表示错误和异常,其中异常类 Exception 又分为运行时异常和非运行时异常,这两种异常有很大的区别,也称为不检查异常(Unchecked Exception)和检查异常(Checked Exception)

    • Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类。
    • Error 定义了在通常环境下不希望被程序捕获的异常。一般指的是 JVM 错误,如堆栈溢出。
  1. 运行时异常都是 RuntimeException 类及其子类异常,如 NullPointerException、IndexOutOfBoundsException 等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常的发生

  2. 非运行时异常是指 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、ClassNotFoundException 等以及用户自定义的 Exception 异常(一般情况下不自定义检查异常)

    • 常见运行时异常
    异常类型 说明
    ArithmeticException 算术错误异常,如以零做除数
    ArraylndexOutOfBoundException 数组索引越界
    ArrayStoreException 向类型不兼容的数组元素赋值
    ClassCastException 类型转换异常
    IllegalArgumentException 使用非法实参调用方法
    lIIegalStateException 环境或应用程序处于不正确的状态
    lIIegalThreadStateException 被请求的操作与当前线程状态不兼容
    IndexOutOfBoundsException 某种类型的索引越界
    NullPointerException 尝试访问 null 对象成员,空指针异常
    NegativeArraySizeException 再负数范围内创建的数组
    NumberFormatException 数字转化格式异常,比如字符串到 float 型数字的转换无效
    TypeNotPresentException 类型未找到
    • 常见非运行时异常
    异常类型 说明
    ClassNotFoundException 没有找到类
    IllegalAccessException 访问类被拒绝
    InstantiationException 试图创建抽象类或接口的对象
    InterruptedException 线程被另一个线程中断
    NoSuchFieldException 请求的域不存在
    NoSuchMethodException 请求的方法不存在
    ReflectiveOperationException 与反射有关的异常的超类

2):Error和Exception的异同

  • 定义:Error(错误)和 Exception(异常)都是 java.lang.Throwable 类的子类,在 Java 代码中只有继承了 Throwable 类的实例才能被 throw 或者 catch

    • Exception 是程序正常运行过程中可以预料到的意外情况,并且应该被开发者捕获,进行相应的处理
    • Error 是指正常情况下不大可能出现的情况,绝大部分的 Error 都会导致程序处于非正常、不可恢复状态。所以不需要被开发者捕获
  • Error 错误是任何处理技术都无法恢复的情况,肯定会导致程序非正常终止。并且 Error 错误属于未检查类型,大多数发生在运行时。Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源码里必须显示的进行捕获处理,这里是编译期检查的一部分。不检查异常就是所谓的运行时异常,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译器强制要求。

  • 常见Error和Exception

    1. 运行时异常(RuntimeException)

      • NullPropagation:空指针异常;
      • ClassCastException:类型强制转换异常
      • IllegalArgumentException:传递非法参数异常
      • IndexOutOfBoundsException:下标越界异常
      • NumberFormatException:数字格式异常
    2. 非运行时异常

      • ClassNotFoundException:找不到指定 class 的异常
      • IOException:IO 操作异常
    3. 错误(Error)

      • NoClassDefFoundError:找不到 class 定义异常
      • StackOverflowError:深递归导致栈被耗尽而抛出的异常
      • OutOfMemoryError:内存溢出异常(解决方案)
    4. 示例 堆栈溢出

      class StackOverflow {
        public static void test(int i) {
          if (i != 0) {
            test(i += 1);
          }
        }
      }
      
      public class ErrorEg {
        public static void main(String[] args) {
          StackOverflow.test(1);
        }
      }

3):异常处理机制

  • 定义:异常处理通过5个关键字实现:trycatchthrowthrowsfinallytry catch 语句用于捕获并处理异常,finally 语句用于在任何情况下(除特殊情况外)都必须执行的代码,throw 语句用于拋出异常,throws 语句用于声明可能会出现的异常

  • Java 的异常处理机制提供了一种结构性和控制性的方式来处理程序执行期间发生的事件。异常处理的机制如下:

    • 在方法中用 try catch 语句捕获并处理异常,catch 语句可以有多个,用来匹配多个异常。
    • 对于处理不了的异常或者要转型的异常,在方法的声明处通过 throws 语句拋出异常,即由上层的调用方法来处理。
    try {
        逻辑程序块
    } catch(ExceptionType1 e) {
        处理代码块1
    } catch (ExceptionType2 e) {
        处理代码块2
        throw(e);    // 再抛出这个"异常"
    } finally {
        释放资源代码块
    }

4):try catch语句

  • 定义:通常采用try catch语句捕获异常并处理

    try {
        // 可能发生异常的语句
    } catch(ExceptionType e) {
        // 处理异常语句
    }
    • 如果 try 语句块中发生异常,那么一个相应的异常对象就会被拋出,然后 catch 语句就会依据所拋出异常对象的类型进行捕获,并处理。处理之后,程序会跳过 try 语句块中剩余的语句,转到 catch 语句块后面的第一条语句开始执行
    • 如果 try 语句块中没有异常发生,那么 try 块正常结束,后面的 catch 语句块被跳过,程序将从 catch 语句块后的第一条语句开始执行
    • try 后面的花括号{ }不可以省略,即使 try 块里只有一行代码,也不可省略这个花括号。与之类似的是,catch 块后的花括号{ }也不可以省略。另外,try 块里声明的变量只是代码块内的局部变量,它只在 try 块内有效,其它地方不能访问该变量
  • 可以使用以下 3 个方法输出相应的异常信息。

    • printStackTrace() 方法:指出异常的类型、性质、栈层次及出现在程序中的位置。
    • getMessage() 方法:输出错误的性质。
    • toString() 方法:给出异常的类型与性。
  • 示例 学习基本信息,try catch

    public class Test02 {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("学生信息录入");
        // 捕获学生姓名
        String name = "";
        // 捕获学生年龄
        int age = 0;
        // 捕获学生性别
        String sex = "";
        try {
          System.out.println("请输入学生姓名:");
          name = input.next();
          System.out.println("请输入学生年龄:");
          age = input.nextInt();
          System.out.println("请输入学生性别:");
          sex = input.next();
        } catch (Exception e) {
          e.printStackTrace();
          System.out.println("输入有误!");
        }
        System.out.println("姓名:" + name);
        System.out.println("年龄:" + age);
      }
    }

1:多重catch语句

  • 如果 try 代码块中有很多语句会发生异常,而且发生的异常种类又很多。那么可以在 try 后面跟有多个 catch 代码块

    try {
        // 可能会发生异常的语句
    } catch(ExceptionType e) {
        // 处理异常语句
    } catch(ExceptionType e) {
        // 处理异常语句
    } catch(ExceptionType e) {
        // 处理异常语句
    ...
    }
    • 当捕获的多个异常类之间存在父子关系时,捕获异常时一般先捕获子类,再捕获父类。所以子类异常必须在父类异常的前面,否则子类捕获不到(当catch捕获到一个异常时,其余catch不再执行)
    public class Test03 {
      public static void main(String[] args) {
        Date date = readDate();
        System.out.println("读取的日期 = " + date);
      }
    
      public static Date readDate() {
        FileInputStream readfile = null;
        InputStreamReader ir = null;
        BufferedReader in = null;
        try {
          readfile = new FileInputStream("readme.txt");
          ir = new InputStreamReader(readfile);
          in = new BufferedReader(ir);
          // 读取文件中的一行数据
          String str = in.readLine();
          if (str == null) {
            return null;
          }
          DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
          Date date = df.parse(str);
          return date;
        } catch (FileNotFoundException e) {
          System.out.println("处理FileNotFoundException...");
          e.printStackTrace();
        } catch (IOException e) {
          System.out.println("处理IOException...");
          e.printStackTrace();
        } catch (ParseException e) {
          System.out.println("处理ParseException...");
          e.printStackTrace();
        }
        return null;
      }
    }
    • 在 try 代码块中第 12 行代码调用 FileInputStream 构造方法可能会发生 FileNotFoundException 异常。第 16 行代码调用 BufferedReader 输入流的 readLine() 方法可能会发生 IOException 异常。FileNotFoundException 异常是 IOException 异常的子类,应该先捕获 FileNotFoundException 异常,见代码第 23 行;后捕获 IOException 异常,见代码第 26 行
    • 如果将 FileNotFoundException 和 IOException 捕获顺序调换,那么捕获 FileNotFoundException 异常代码块将永远不会进入,FileNotFoundException 异常处理永远不会执行。 上述代码第 29 行 ParseException 异常与 IOException 和 FileNotFoundException 异常没有父子关系,所以捕获 ParseException 异常位置可以随意放置

5):计算平均成绩

public class Test04 {
  public static void main(String[] args) {
    Scanner input = new Scanner(System.in);
    try {
      System.out.println("请输入班级总人数:");
      int count = input.nextInt();
      System.out.println("请输入总成绩:");
      int score = input.nextInt();
      // 获取平均分
      int avg = score / count;
      System.out.println("本次考试的平均分为:" + avg);
    } catch (InputMismatchException e1) {
      System.out.println("输入数值有误!");
    } catch (ArithmeticException e2) {
      System.out.println("输入的总人数不能为0!");
    } catch (Exception e3) {
      e3.printStackTrace();
      System.out.println("发生错误!" + e3.getMessage());
    }
  }
}

6):try catch finally语句

  • try 语句块和 catch 语句块有可能不被完全执行,而有些处理代码则要求必须执行。例如,程序在 try 块里打开了一些物理资源(如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。

    Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只回收堆内存中对象所占用的内存。

    try {
        // 可能会发生异常的语句
    } catch(ExceptionType e) {
        // 处理异常语句
    } finally {
        // 清理代码块
    }
    • 注:无论是否发生异常(除特殊情况外),finally语句块中的代码都会被执行
    try {
        // 逻辑代码块
    } finally {
        // 清理代码块
    }
  • 使用 try-catch-finally 语句时需注意以下几点:

    1. 异常处理语法结构中只有 try 块是必需的,也就是说,如果没有 try 块,则不能有后面的 catch 块和 finally 块;

    2. catch 块和 finally 块都是可选的,但 catch 块和 finally 块至少出现其中之一,也可以同时出现;

    3. 可以有多个 catch 块,捕获父类异常的 catch 块必须位于捕获子类异常的后面;

    4. 不能只有 try 块,既没有 catch 块,也没有 finally 块;

    5. 多个 catch 块必须位于 try 块之后,finally 块必须位于所有的 catch 块之后。

    6. finally 与 try 语句块匹配的语法格式,此种情况会导致异常丢失,所以不常见。

      一般情况下,无论是否有异常拋出,都会执行 finally 语句块中的语句

    • try catch finally 语句块的执行情况可以细分为以下 3 种情况:
      1. 如果 try 代码块中没有拋出异常,则执行完 try 代码块之后直接执行 finally 代码块,然后执行 try catch finally 语句块之后的语句。
      2. 如果 try 代码块中拋出异常,并被 catch 子句捕捉,那么在拋出异常的地方终止 try 代码块的执行,转而执行相匹配的 catch 代码块,之后执行 finally 代码块。如果 finally 代码块中没有拋出异常,则继续执行 try catch finally 语句块之后的语句;如果 finally 代码块中拋出异常,则把该异常传递给该方法的调用者。
      3. 如果 try 代码块中拋出的异常没有被任何 catch 子句捕捉到,那么将直接执行 finally 代码块中的语句,并把该异常传递给该方法的调用者。
    • 除非在 try 块、catch 块中调用了退出虚拟机的方法System.exit(int status),否则不管在 try 块或者 catch 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会执行。
    • 通常情况下不在 finally 代码块中使用 return 或 throw 等导致方法终止的语句,否则将会导致 try 和 catch 代码块中的 return 和 throw 语句失效
    public class Test05 {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("Windows系统已启动!");
        String[] pros = {"记事本", "计算器", "浏览器"};
        try {
          // 循环输出pros数组中的元素
          for (int i = 0; i < pros.length; i++) {
            System.out.println(i + 1 + ":" + pros[i]);
          }
          System.out.println("是否运行程序:");
          String answer = input.next();
          if (answer.equals("y")) {
            System.out.println("请输入程序编号:");
            int no = input.nextInt();
            System.out.println("正则运行程序[" + pros[no - 1] + "]");
          }
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
          System.out.println("谢谢使用!");
        }
      }
    }

7):finally和return的执行顺序

  • try、catch 和 finally 是按顺序执行的。如果 try 中没有异常,则顺序为 try→finally,如果 try 中有异常,则顺序为 try→catch→finally。

1:try和catch中带有return

  • try中带有return

    public class TryDemo {
      public static int show() {
        try {
          return 1;
        } finally {
          System.out.println("执行finally模块");
        }
      }
    
      public static void main(String[] args) {
        System.out.println(show());
      }
    }
  • try和catch中都带有return

    public class TryDemo1 {
      public static int show() {
        try {
          int a = 8 / 0;
          return 1;
        } catch (Exception e) {
          return 2;
        } finally {
          System.out.println("执行finally模块");
        }
      }
    
      public static void main(String[] args) {
        System.out.println(show());
      }
    }
  • 当 try 代码块或者 catch 代码块中有 return 时,finally 中的代码总会被执行,且 finally 语句 return 返回之前执行

2:finally中带有return

public class TryDemo2 {
  public static int show() {
    try {
      int a = 8 / 0;
      return 1;
    } catch (Exception e) {
      return 2;
    } finally {
      System.out.println("执行finally模块");
      return 0;
    }
  }

  public static void main(String[] args) {
    System.out.println(show());
  }
}
  • 当 finally 有返回值时,会直接返回该值,不会去返回 try 代码块或者 catch 代码块中的返回值 finally 代码块中最好不要包含 return 语句,否则程序会提前退出

3:finally中改变返回值

  • try或catch的返回值时普通变量(不会改变最后返回的内容)

    public class TryDemo3 {
      public static int show() {
        int result = 0;
        try {
          return result;
        } finally {
          System.out.println("执行finally模块");
          result = 1;
        }
      }
    
      public static void main(String[] args) {
        System.out.println(show()); // 执行finally模块 0
      }
    }
  • 当返回值类型是引用类型是,结果是一样的(不会改变其返回值)

    public class TryDemo4 {
      public static Object show() {
        Object obj = new Object();
        try {
          return obj;
        } finally {
          System.out.println("执行finally模块");
          obj = null;
        }
      }
    
      public static void main(String[] args) {
        System.out.println(show()); // 执行finally模块 java.lang.Object@74a14482
      }
    }
  • 总结为以下几条:

    • 当 try 代码块和 catch 代码块中有 return 语句时,finally 仍然会被执行。
    • 执行 try 代码块或 catch 代码块中的 return 语句之前,都会先执行 finally 语句。
    • 无论在 finally 代码块中是否修改返回值,返回值都不会改变,仍然是执行 finally 代码块之前的值。
    • finally 代码块中的 return 语句一定会执行。

8):Java 9增强的自动资源管理

  • 原因:当程序使用 finally 块关闭资源时,程序会显得异常臃肿

    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("a.txt");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 关闭磁盘文件,回收资源
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
  • 定义:新特性,该特性提供了另外一种管理资源的方式,这种方式能自动关闭文件,被称为自动资源管理。该特性是在 try 语句上的扩展,主要释放不再需要的文件或其他资源

  • 自动资源管理替代了 finally 代码块,并优化了代码结构和提高程序可读性

    try (声明或初始化资源语句) {
        // 可能会生成异常语句
    } catch(Throwable e1){
        // 处理异常e1
    } catch(Throwable e2){
        // 处理异常e2
    } catch(Throwable eN){
        // 处理异常eN
    }

    当 try 代码块结束时,自动释放资源。不再需要显式的调用 close() 方法,该形式也称为“带资源的 try 语句”

    • 注意:

      1. try 语句中声明的资源被隐式声明为 final,资源的作用局限于带资源的 try 语句。
      2. 可以在一条 try 语句中声明或初始化多个资源,每个资源以;隔开即可。
      3. 需要关闭的资源必须实现了 AutoCloseable 或 Closeable 接口。

      Closeable 是 AutoCloseable 的子接口,Closeable 接口里的 close() 方法声明抛出了 IOException,因此它的实现类在实现 close() 方法时只能声明抛出 IOException 或其子类;AutoCloseable 接口里的 close() 方法声明抛出了 Exception,因此它的实现类在实现 close() 方法时可以声明抛出任何异常。

    1. 示例

      public class AutoCloseTest {
          public static void main(String[] args) throws IOException {
              try (
                      // 声明、初始化两个可关闭的资源
                      // try语句会自动关闭这两个资源
                      BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
                      PrintStream ps = new PrintStream(new FileOutputStream("a.txt"))) {
                  // 使用两个资源
                  System.out.println(br.readLine());
                  ps.println("C语言中文网");
              }
          }
      }
      • 以上代码分别声明、初始化了两个 IO 流,BufferedReader 和 PrintStream 都实现了 Closeable 接口,并在 try 语句中进行了声明和初始化,所以 try 语句会自动关闭它们。自动关闭资源的 try 语句相当于包含了隐式的 finally 块(这个 finally 块用于关闭资源)

      Java 7 几乎把所有的“资源类”(包括文件 IO 的各种类、JDBC 编程的 Connection 和 Statement 等接口)进行了改写,改写后的资源类都实现了 AutoCloseable 或 Closeable 接口。

      如果程序需要,自动关闭资源的 try 语句后也可以带多个 catch 块和一个 finally 块。

    2. Java 9再次强调try语句。

      public class AutoCloseTest {
          public static void main(String[] args) throws IOException {
              // 有final修饰的资源
              final BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
              // 没有显式使用final修饰,但只要不对该变量重新赋值,该变量就是有效的
              final PrintStream ps = new PrintStream(new FileOutputStream("a. txt"));
              // 只要将两个资源放在try后的圆括号内即可
              try (br; ps) {
                  // 使用两个资源
                  System.out.println(br.readLine());
                  ps.println("C语言中文网");
              }
          }
      }

9):声明和抛出异常

  • 定义:可以通过 throws 关键字在方法上声明该方法要拋出的异常,然后在方法内部通过 throw 拋出异常对象

1:throws声明异常

  • 定义:当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法的外部进行处理。使用 throws 声明的方法表示此方法不处理异常。

    returnType method_name(paramList) throws Exception 1,Exception2,{}

    returnType 表示返回值类型;method_name 表示方法名;paramList 表示参数列表;Exception 1,Exception2,… 表示异常类。

    如果有多个异常类,它们之间用逗号分隔。这些异常类可以是方法中调用了可能拋出异常的方法而产生的异常,也可以是方法体中生成并拋出的异常。

  1. 例1 创建一个readFile()的方法,读取文件的内容,可能会产生IOExceotion异常,但是在该方法中不做任何处理,而可能发生的异常交给调用者处理,在main()方法中try catch捕获异常,输出信心。

    public class Test06 {
      public void readFile() throws IOException {
        // 定义方法时声明异常
        // 创建 FileInputStream 实例对象
        FileInputStream file = new FileInputStream("read.txt");
        int f;
        while ((f = file.read()) != -1) {
          System.out.println((char) f);
          f = file.read();
        }
        file.close();
      }
    
      public static <Throws> void main(String[] args) {
        Throws t = (Throws) new Test06();
        try {
          // 调用 readFile()方法
          ((Test06) t).readFile();
        } catch (IOException e) {
          // 捕获异常
          System.out.println(e);
        }
      }
    }
  2. 方法重写时声明抛出异常的限制

    • 使用 throws 声明抛出异常时有一个限制,是方法重写中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多
    public class OverrideThrows {
      public void test() throws IOException {
        FileInputStream fis = new FileInputStream("a.txt");
      }
    }
    
    class Sub extends OverrideThrows {
      // 子类方法声明抛出比父类方法更大的异常
      // 所以下面方法出错
      @Override
      public void test() throws Exception {}
    }
    • 子类在重写父类带 throws 子句的方法时,子类方法声明中的 throws 子句不能出现父类对应方法的 throws 子句中没有的异常类型,因此 throws 子句可以限制子类的行为。也就是说,子类方法拋出的异常不能超过父类定义的范围

2:throws抛出异常

  • 定义:throws语句用来直接抛出一个异常,后接一个可抛出的异常类对象

    throw ExceptionObject
  • ExceptionObject 必须是 Throwable 类或其子类的对象。如果是自定义异常类,也必须是 Throwable 的直接或间接子类。

    throw new String("拋出异常");    // String类不是Throwable类的子类 编译错误
  1. 例2 用户名由8为以上字母或数字组成。当长度小于8位一下抛出以上,非数字或非字母是抛出异常。

    ublic class ExUsername {
      public boolean validateUsername(String username) {
        boolean con = false;
        if (username.length() > 8) {
          // 判断用户名长度是否大于8位
          for (int i = 0; i < username.length(); i++) {
            // 获取每一位字符
            char c = username.charAt(i);
            if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
              con = true;
            } else {
              con = false;
              throw new IllegalArgumentException("用户名只能由字母和数字组成!");
            }
          }
        } else {
          throw new IllegalArgumentException("用户名长度必须大于8位!");
        }
        return con;
      }
    
      public static void main(String[] args) {
        ExUsername exu = new ExUsername();
        Scanner input = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String username = input.next();
        try {
          boolean con = exu.validateUsername(username);
          if (con) {
            System.out.println("用户名输入正确!");
          }
        } catch (IllegalArgumentException e) {
          System.out.println(e);
        }
      }
    }
  • throws关键字和throw关键字在使用上的几点区别如下:
    • throws用来声明一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会这些异常;throw则是指抛出的一个具体的异常类型,执行throw则一定抛出了某种异常对象。
    • 通常在一个方法(类)的声明处通过throws声明方法(类)可能抛出的异常信息,而在方法(类)内部通过throw声明一个具体的异常信息。
    • throws通过不用显示地捕获异常,可由系统自动将所有捕获的异常信息抛给上级方法;throw则需要用户自己捕获相关的异常,而后再对其进行相关包装,最后将包装后的异常信息抛出。

10):Java 7新特性:多异常捕获

  • catch 代码块虽然客观上提高了程序的健壮性,但是也导致了程序代码量大大增加

    try{
        // 可能会发生异常的语句
    } catch (FileNotFoundException e) {
        // 调用方法methodA处理
    } catch (IOException e) {
        // 调用方法methodA处理
    } catch (ParseException e) {
        // 调用方法methodA处理
    }
  • 多异常捕获技术,可以把这些异常合并处理

    try{
        // 可能会发生异常的语句
    } catch (IOException | ParseException e) {
        // 调用方法methodA处理
    }
    • 由于 FileNotFoundException 属于 IOException 异常,IOException 异常可以捕获它的所有子类异常。所以不能写成 FileNotFoundException | IOException | ParseException
  • 使用一个 catch 块捕获多种类型的异常时需要注意如下两个地方。

    • 捕获多种类型的异常时,多种异常类型之间用竖线|隔开。
    • 捕获多种类型的异常时,异常变量有隐式的 final 修饰,因此程序不能对异常变量重新赋值。
    public class ExceptionTest {
      public static void main(String[] args) {
        try {
          int a = Integer.parseInt(args[0]);
          int b = Integer.parseInt(args[1]);
          int c = a / b;
          System.out.println("您输入的两个数相除的结果是:" + c);
        } catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException e) {
          System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
          // 捕获多异常时,异常变量默认由final修饰
          // 以下代码报错,无法将值赋给 final 变量 'e'
          e = new ArithmeticException("test");
        } catch (Exception e) {
          System.out.println("未知异常");
          // 捕获一种类型的异常时,异常变量没有final修饰
          // 以下代码完全正确
          e = new RuntimeException("test");
        }
      }
    }

11):自定义异常

  • 定义:实现自定义异常类需要继承 Exception 类或其子类,如果自定义运行异常类需继承 RuntimeException 类或其子类。

    <class><自定义异常名><extends><Exception>

    一般将自定义异常类的类名命名为 XXXException,其中 XXX 用来代表该异常的作用

  • 自定义异常类一般包含两个构造方法:一个是无参的默认构造方法,另一个构造方法以字符串的形式接收一个定制的异常消息,并将该消息传递给超类的构造方法

    class IntegerRangeException extends Exception {
        public IntegerRangeException() {
            super();
        }
        public IntegerRangeException(String s) {
            super(s);
        }
    }
  1. 例1 对年龄进行验证

    • 创建自定义异常类MyException,提供两个构造方法

      public class MyException extends Exception {
        public MyException() {
          super();
        }
      
        public MyException(String str) {
          super(str);
        }
      }
    • 测试类

      public class MyExceptionTest {
        public static void main(String[] args) {
          int age;
          Scanner input = new Scanner(System.in);
          System.out.println("请输入您的年龄:");
          try {
            age = input.nextInt();
            if (age < 0) {
              throw new MyException("您输入的年龄为负数!输入有误!");
            } else if (age > 100) {
              throw new MyException("您输入的年龄大于100!输入有误!");
            } else {
              System.out.println("您的年龄为:" + age);
            }
          } catch (InputMismatchException e1) {
            System.out.println("输入的年龄不是数字");
          } catch (MyException e2) {
            System.out.println(e2.getMessage());
          }
        }
      }
    • 因为自定义异常继承自 Exception 类,因此自定义异常类中包含父类所有的属性和方法

12):验证用户信息

  • 用户名和密码,不匹配则为异常,并捕获异常(自定义异常)
  1. 自定义异常LoginException

    public class LoginException extends Exception {
      public LoginException() {
        super();
      }
    
      public LoginException(String str) {
        super(str);
      }
    }
  2. 测试类,定义validateLogin()方法

    public class LoginTest {
      public boolean validateLogin(String username, String pwd) {
        // 用户名和密码是否正确
        boolean con = false;
        // 用户名格式是否正确
        boolean conUname = false;
        try {
          if (username.length() >= 6 && username.length() <= 10) {
            for (int i = 0; i < username.length(); i++) {
              // 获取每一个字符
              char c = username.charAt(i);
              // 判断字符是否为0~9的数字
              if (c >= '0' && c <= '9') {
                // 设置 conUname 变量值为 true
                conUname = true;
              } else {
                // 如果字符不是0~9的数字,则拋出LoginException异常
                conUname = false;
                throw new LoginException("用户名中包含非数字的字符!");
              }
            }
          } else {
            // 如果用户名长度不在6~10为之间,抛出异常
            throw new LoginException("用户名长度必须在6~10位之间!");
          }
          // 如果用户名格式正确,判断密码长度
          if (conUname) {
            if (pwd.length() == 6) {
              // 如果密码长度等于6
              // 设置con变量的值为true,表示登录信息符合要求
              con = true;
              // 如果密码长度不等于6,拋出异常
            } else {
              con = false;
              throw new LoginException("密码长度必须位6位!");
            }
          }
        } catch (LoginException e) {
          System.out.println(e.getMessage());
        }
        return con;
      }
    
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("用户名:");
        String username = input.next();
        System.out.println("密码:");
        String password = input.next();
        LoginTest lt = new LoginTest();
        // 调用 validateLogin() 方法
        boolean con = lt.validateLogin(username, password);
        if (con) {
          System.out.println("登录成功!");
        }
      }
    }

13):完善除法运算的错误信息

  • 用户输出两个数字相除,捕获运算中的异常信心,除数时字符、被除数时零等
  1. 创建Compute计算类

    public class Compute {
      private int[] num = new int[2];
    
      public int[] getNum() {
        return num;
      }
    
      public void setNum(int[] num) {
        this.num = num;
      }
    }
  2. 测试类

    public class ComputeTest {
      public static void main(String[] args) {
        Compute c = new Compute();
        int array[] = c.getNum();
        int res = 0;
        String YorN = null;
        Scanner in = new Scanner(System.in);
        try {
          System.out.println("请输入第一个整数:");
          array[0] = in.nextInt();
          System.out.println("请输入第二个整数:");
          array[1] = in.nextInt();
          res = array[0] / array[1];
          System.out.println("是否保存结果请输入Y或者N");
          YorN = in.next();
          if (YorN.equals("Y") || YorN.equals("y")) {
            array[2] = res;
          }
          System.out.println(array[0] + "除以" + array[1] + "的结果是:" + res);
        } catch (ArrayIndexOutOfBoundsException e) {
          System.out.println("出现数组越界错误,下标过大或者过小。");
        } catch (ArithmeticException e) {
          System.out.println("出现算术运算错误,被除数不能为0。");
        } catch (InputMismatchException e) {
          System.out.println("输入的数据类型不匹配,只能输入数字。");
        } catch (Exception e) {
          System.out.println("发生未知错误。");
        }
      }
    }

14):异常处理规则

  • 成功异常处理
    1. 使程序代码混乱最小化。
    2. 捕获并保留诊断信息。
    3. 通知合适的人员。
    4. 采用合适的方式结束异常活动。

1:不要过渡使用异常

  • 过度使用异常主要有以下两个方面:

    1. 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有的错误处理。
    2. 使用异常处理来代替流程控制。
  • 对于完全已知和普通的错误,应该编写处理这种错误处理代码,增加程序的健壮性;只有对外部的、不能确定和预知的运行时错误才使用异常。

    1. 已有棋子处理方式 1

      // 如果用户试图下棋的坐标点已有棋子了
      if (!gb.board[xPos - 1][yPos - 1].equals("╋")){
          System.out.println ("您输入的坐标点已有棋子了,请重新输入");
          continue;
      }
    2. 已有棋子处理方式 2(抛出异常)

      // 如果用户试图下棋的坐标点己经有棋子了,程序自行抛出异常
      if (!gb.board[xPos - 1][yPos - 1].equals ("╋")) {
          throw new Exception ("您试图下棋的坐标点已经有棋子了");
      }
  • 异常处理机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制

    // 定义一个字符串数组
    String[] arr = { "Hello", "Java", "Spring" };
    // 使用异常处理来遍历arr数组的每个元素
    try {
        int i = 0;
        while (true) {
            System.out.println(arr[i++]);
        }
    } catch (ArrayIndexOutOfBoundsException ae) {
    }
    • 可读性差,运行效率低。不建议使用catch捕获该异常。
  • 改之

    String arr[] = {"Hello","Java","Spring"};
    for (int i = 0; i < arr.length; i++) {
        System.out.println(arr[i]);
    }
    • 异常只应该用于处理非正常的情况,不要使用异常处理来代替正常的流程控制。

2:不要使用过去庞大的try块

  • 定义:因为 try 块里的代码过于庞大,业务过于复杂,就会造成 try 块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加。而且当 try 块过于庞大时,就难免在 try 块后紧跟大量的 catch 块才可以针对不同的异常提供不同的处理逻辑。同一个 try 块后紧跟大量的 catch 块则需要分析它们之间的逻辑关系,反而增加了变成复杂度。

    正确的做法是,把大块的 try 块分割成多个可能出现异常的程序段落,并把它们放在单独的 try 块中,从而分别捕获并处理异常。

3:避免使用Catch All语句

  • 定义:Catch All 语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。

    try {
        // 可能引发Checked异常的代码
    } catch (Throwsble t) {
        // 进行异常处理
        t.printStackTrace();
    }
  • 这种处理方式有如下两点不足之处。

    1. 所有的异常都采用相同的处理方式,这将导致无法对不同的异常分情况处理,如果要分情况处理,则需要在 catch 块中使用分支语句进行控制,这是得不偿失的做法。
    2. 这种捕获方式可能将程序中的错误、Runtime 异常等可能导致程序终止的情况全部捕获到,从而“压制”了异常。如果出现了一些“关键”异常,那么此异常也会被“静悄悄”地忽略。

    实际上,Catch All 语句不过是一种通过避免错误处理而加快编程进度的机制,应尽量避免在实际应用中使用这种语句。

4:不要忽略捕获到的异常

  • 不要忽略异常,既然已捕获到异常,那 catch 块理应处理并修复这个错误。catch 块整个为空,或者仅仅打印出错信息都是不妥的。
  • 对异常进行合适的修复,然后绕过异常发生的地方继续执行;或者用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作等。总之,对于 Checked 异常,程序应该尽量修复。

15):异常跟踪栈

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

    测试printStackTrace

    class SelfException extends RuntimeException {
      SelfException() {}
    
      SelfException(String message) {
        super(message);
      }
    }
    
    public class PrintStackTraceTest {
      public static void main(String[] args) {
        firstMethod();
      }
    
      public static void firstMethod() {
        secondMethod();
      }
    
      public static void secondMethod() {
        thirdMethod();
      }
    
      public static void thirdMethod() {
        throw new SelfException("自定义异常信息");
      }
    }
    • 在面向对象的编程中,大多数复杂操作都会被分解成一系列方法调用。这是因为实现更好的可重用性,将每个可重用的代码单元定义成方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个对象来共同实现,在最终编程模型中,很多对象将通过一系列方法调用来实现通信,执行任务
    • 面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者……,直至最后传到 main 方法,如果 main 方法依然没有处理该异常,则 JVM 会中止该程序,并打印异常的跟踪栈信息。
  • 多线程程序中发生异常

    public class ThreadExceptionTest implements Runnable {
      @Override
      public void run() {
        firstMethod();
      }
    
      public void firstMethod() {
        secondMethod();
      }
    
      public void secondMethod() {
        int a = 5;
        int b = 0;
        int c = a / b;
      }
    
      public static void main(String[] args) {
        new Thread(new ThreadExceptionTest()).start();
      }
    }
    • 虽然 printStackTrace() 方法可以很方便地用于追踪异常的发生情况,可以用它来调试程序,但在最后发布的程序中,应该避免使用它。应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。

16):Java.util.logging:JDK自带记录日志类

  • 定义:日志用来记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题

  • 如果生成简单日志记录,则使用全局日志记录器并调用其info方法

    Logger.getGlobal().info("打印信息");
  • JDK Logging 把日志分为如下表 7 个级别,等级依次降低。

    级别 SEVERE WARNING INFO CONFIG FINE FINER FINEST
    调用方法 severe() warning() info() config() fine() finer() finest()
    含义 严重 警告 信息 配置 良好 较好 最好
  1. Logger 的默认级别是 INFO,比 INFO 级别低的日志将不显示。Logger 的默认级别定义在 jre 安装目录的 lib 下面

    # Limit the message that are printed on the console to INFO and above.
    java.util.logging.ConsoleHandler.level = INFO

    所以在默认情况下,日志只显示前三个级别,对于所有的级别有下面几种记录方法

    logger.warning(message);
    logger.fine(message);

    同时,还可以使用 log 方法指定级别

    logger.log(Level.FINE, message);
    public class Test {
      private static Logger log = Logger.getLogger(Test.class.toString());
    
      public static void main(String[] args) {
        log.finest("finest");
        log.finer("finer");
        log.fine("fine");
        log.config("config");
        log.info("info");
        log.warning("waring");
        log.severe("server");
      }
    }
    • 可以使用 setLevel 方法设置级别,例如logger.setLevel(Level.FINE);可以将 FINE 和更高级别的都记录下来。另外,还可以使用 Level.ALL 开启所有级别的记录,或者使用 Level.OFF 关闭所有级别的记录

1:修改日志管理器配置

  • 可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于 jre 安装目录下“jre/lib/logging.properties”。要想使用另一个配置文件,就要将 java.util.logging.config.file 特性设置为配置文件的存储位置,并用下列命令启动应用程序。

    java -Djava.util.logging.config.file = configFile MainClass
  • 日志管理器在 JVM 启动过程中初始化,这在 main 执行之前完成。如果在 main 中调用System.setProperty("java.util.logging.config.file",file),也会调用LogManager.readConfiguration()来重新初始化日志管理器

  • 要想修改默认的日志记录级别,命令

    .level=INFO
  • 可以通过添加以下内容来指定自己的日志记录级别

    Test.Test.level=FINE
  • 日志记录并不将消息发送到控制台上,这是处理器的任务。另外,处理器也有级别。要想在控制台上看到 FINE 级别的消息,就需要进行下列设置

    java.util.logging.ConsoleHandler.level=FINE

    注意:在日志管理器配置的属性设置不是系统属性,因此,用-Dcom.mycompany.myapp.level=FINE启动应用程序不会对日志记录器产生任何影响。

十一:Java集合、泛型和枚举

1):集合

  • 定义:集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。Java 所有的集合类都位于 java.util 包下,提供了一个表示和操作对象集合的统一构架,包含大量集合接口,以及这些接口的实现类和操作它们的算法。

  • 集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量),而集合里只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)

  • Java集合类型分为Collection和Map,它们是Java集合的根接口,这两个接口包含了一些子接口或实现类。

    Java集合接口的作用
    接口名称 作 用
    Iterator 接口 集合的输出接口,主要用于遍历输出(即迭代访问)Collection 集合中的元素,Iterator 对象被称之为迭代器。迭代器接口是集合接口的父接口,实现类实现 Collection 时就必须实现 Iterator 接口。
    Collection 接口 是 List、Set 和 Queue 的父接口,是存放一组单值的最大接口。所谓的单值是指集合中的每个元素都是一个对象。一般很少直接使用此接口直接操作。
    Queue 接口 Queue 是 Java 提供的队列实现,有点类似于 List。
    Dueue 接口 是 Queue 的一个子接口,为双向队列。
    List 接口 是最常用的接口。是有序集合,允许有相同的元素。使用 List 能够精确地控制每个元素插入的位置,用户能够使用索引(元素在 List 中的位置,类似于数组下标)来访问 List 中的元素,与数组类似。
    Set 接口 不能包含重复的元素。
    Map 接口 是存放一对值的最大接口,即接口中的每个元素都是一对,以 key➡value 的形式保存。
    Java集合实现类的作用
    类名称 作用
    HashSet 为优化査询速度而设计的 Set。它是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,实现比较简单
    TreeSet 实现了 Set 接口,是一个有序的 Set,这样就能从 Set 里面提取一个有序序列
    ArrayList 一个用数组实现的 List,能进行快速的随机访问,效率高而且实现了可变大小的数组
    ArrayDueue 是一个基于数组实现的双端队列,按“先进先出”的方式操作集合元素
    LinkedList 对顺序访问进行了优化,但随机访问的速度相对较慢。此外它还有 addFirst()、addLast()、getFirst()、getLast()、removeFirst() 和 removeLast() 等方法,能把它当成栈(Stack)或队列(Queue)来用
    HsahMap 按哈希算法来存取键对象
    TreeMap 可以对键对象进行排序

1:List集合

  • 定义:List 是一个有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List 集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List 集合默认按元素的添加顺序设置元素的索引,第一个添加到 List 集合中的元素的索引为 0,第二个为 1,依此类推。
  • List 实现了 Collection 接口,它主要有两个常用的实现类:ArrayList 类和 LinkedList 类
1:ArrayList类
  • 定义:ArrayList 类实现了可变数组的大小,存储在内的数据称为元素。它还提供了快速基于索引访问元素的方式,对尾部成员的增加和删除支持较好。使用 ArrayList 创建的集合,允许对集合中的元素进行快速的随机访问,不过,向 ArrayList 中插入与删除元素的速度相对较慢。

  • ArrayList 类的常用构造方法有如下两种重载形式:

    • ArrayList():构造一个初始容量为 10 的空列表。

    • ArrayList(Collection<?extends E>c):构造一个包含指定 Collection 元素的列表,这些元素是按照该 Collection 的迭代器返回它们的顺序排列的。

      ArrayList类的常用方法
      方法名称 说明
      E get(int index) 获取此集合中指定索引位置的元素,E 为集合中元素的数据类型
      int index(Object o) 返回此集合中第一次出现指定元素的索引,如果此集合不包含该元 素,则返回 -1
      int lastIndexOf(Object o) 返回此集合中最后一次出现指定元素的索引,如果此集合不包含该 元素,则返回 -1
      E set(int index, Eelement) 将此集合中指定索引位置的元素修改为 element 参数指定的对象。 此方法返回此集合中指定索引位置的原元素
      List subList(int fromlndex, int tolndex) 返回一个新的集合,新集合中包含 fromlndex 和 tolndex 索引之间 的所有元素。包含 fromlndex 处的元素,不包含 tolndex 索引处的 元素

      注意:当调用 List 的 set(int index, Object element) 方法来改变 List 集合指定索引处的元素时,指定的索引必须是 List 集合的有效索引。例如集合长度为 4,就不能指定替换索引为 4 处的元素,也就是说这个方法不会改变 List 集合的长度

  1. 例1 (ArrayList类集合中添加三个商品信息,包括商品编号、名称和价格),遍历集合输出商品信息

    • 商品类,定义三个属性和toString()方法

      public class Product {
        // 商品类
        // 商品编号
        private int id;
        // 名称
        private String name;
        // 价格
        private float price;
      
        public Product(int id, String name, float price) {
          this.id = id;
          this.name = name;
          this.price = price;
        }
      
        @Override
        public String toString() {
          return "商品编号:" + id + ",名称:" + name + ",价格:" + price;
        }
      }
    • 测试类,调用Product类的构造函数实例化三个对象,并将Product对象保存至ArrayList集合中。

      public class Test1 {
        public static void main(String[] args) {
          Product pd1 = new Product(4, "木糖醇", 10);
          Product pd2 = new Product(5, "木糖醇", 12);
          Product pd3 = new Product(3, "木糖醇", 49);
          // 创建集合
          List list = new ArrayList();
          list.add(pd1);
          list.add(pd2);
          list.add(pd3);
          System.out.println("*************** 商品信息 ***************");
          for (Object li : list) {
            // 循环遍历集合,输出集合元素
            Product product = (Product) li;
            System.out.println(product);
          }
        }
      }
  2. 例2 (List 集合中的indexOf() 方法和 lastIndexOf(),前者是获得指定对象的最小索引位置,而后者是获得指定对象的最大索引位置)

    public class ListTest {
      public static void main(String[] args) {
        List list = new ArrayList();
        list.add("One");
        list.add("|");
        list.add("Two");
        list.add("|");
        list.add("Three");
        list.add("|");
        list.add("Four");
        System.out.println("list 集合中的元素数量:" + list.size());
        System.out.println("list 集合中的元素如下:");
        Iterator it = list.iterator();
        while (it.hasNext()) {
          System.out.print(it.next() + "、");
        }
        System.out.println("\n在 list 集合中'丨'第一次出现的位置是:" + list.indexOf("|"));
        System.out.println("在 list 集合中'丨'最后一次出现的位置是:" + list.lastIndexOf("|"));
      }
    }
  3. 例3 subList()方法截取 List 集合中部分元素时要注意,新的集合中包含起始索引位置的元素,但是不包含结束索引位置的元素

    public class SubListTest {
      public static void main(String[] args) {
        List list = new ArrayList();
        list.add("One");
        list.add("Two");
        list.add("Three");
        list.add("Four");
        list.add("Five");
        list.add("Six");
        list.add("Seven");
        System.out.println("list 集合中的元素数量:" + list.size());
        System.out.println("list 集合中的元素如下:");
        Iterator it = list.iterator();
        while (it.hasNext()) {
          System.out.print(it.next() + "、");
        }
        List sublist = new ArrayList();
        // 从list集合中截取索引2~5的元素,保存到sublist集合中
        sublist = list.subList(2, 5);
        System.out.println("\nsublist 集合中元素数量:" + sublist.size());
        System.out.println("sublist 集合中的元素如下:");
        it = sublist.iterator();
        while (it.hasNext()) {
          System.out.print(it.next() + "、");
        }
      }
    }
2:LinkedList类
  • 定义:LinkedList 类采用链表结构保存对象,这种结构的优点是便于向集合中插入或者删除元素。需要频繁向集合中插入和删除元素时,使用 LinkedList 类比 ArrayList 类效果高,但是 LinkedList 类随机访问元素的速度则相对较慢

    LinkList类中的方法
    方法名称 说明
    void addFirst(E e) 将指定元素添加到此集合的开头
    void addLast(E e) 将指定元素添加到此集合的末尾
    E getFirst() 返回此集合的第一个元素
    E getLast() 返回此集合的最后一个元素
    E removeFirst() 删除此集合中的第一个元素
    E removeLast() 删除此集合中的最后一个元素
  1. 例4 (Linkedlist集合使用)

    public class LinkedListTest {
      public static void main(String[] args) {
        // 创建集合对象
        LinkedList<String> products = new LinkedList<String>();
        String p1 = new String("六角螺母");
        String p2 = new String("10A 电缆线");
        String p3 = new String("5M 卷尺");
        String p4 = new String("4CM 原木方板");
        // 将 p1 对象添加到 LinkedList 集合中
        products.add(p1);
        products.add(p2);
        products.add(p3);
        products.add(p4);
        String p5 = new String("标准文件夹小柜");
        // 向集合的末尾添加p5对象
        products.addLast(p5);
        System.out.print("*************** 商品信息 ***************");
        System.out.println("\n目前商品有:");
        for (String product : products) {
          System.out.println(product + "\t");
        }
        System.out.println("\n第一个商品的名称为:" + products.getFirst());
        System.out.println("最后一个商品的名称为:" + products.getLast());
        products.removeLast(); // 删除最后一个元素
        System.out.println("删除最后的元素,目前商品有:");
        for (String product : products) {
          System.out.print(product + "\t");
        }
      }
    }
3:ArrayList类和LinkedList类的区别
  • 相同点:ArrayList 与 LinkedList 都是 List 接口的实现类,因此都实现了 List 的所有未实现的方法,只是实现的方式有所不同
  • 不同点:
    • ArrayList 是基于动态数组数据结构的实现,访问元素速度优于 LinkedList。
    • LinkedList 是基于链表数据结构的实现,占用的内存空间比较大,但在批量插入或删除数据时优于 ArrayList。
  • 对于快速访问对象的需求,使用 ArrayList 实现执行效率上会比较好。需要频繁向集合中插入和删除元素时,使用 LinkedList 类比 ArrayList 类效果高。

2:Set集合

  • 定义:Set集合类似于一个罐子,程序可以一次把多个对象“丢进”Set集合,而Set集合通常不能记住元素的添加顺序。Set集合中的对象不按特定的方式排序,只是简单地把对象加入集合。Set集合中不能包含重复地对象,并且最多只允许包含一个null元素。
  • Set实现了Collection接口,常用地实现类:HashSet类和TreeSet类
1:HashSet类
  • 定义:HashSet是Set接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。HashSet是按照Hash算法来存储集合中的元素。因此具有更好的存取和查找性能

  • HashSet 具有以下特点:

    • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化
    • HashSet 不是同步的,如果多个线程同时访问或修改一个 HashSet,则必须通过代码来保证其同步
    • 集合元素值可以是 null
  • 当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置。如果有两个元素通过 equals() 方法比较返回的结果为 true,但它们的 hashCode 不相等,HashSet 将会把它们存储在不同的位置,依然可以添加成功(两个对象的hashCode相等且通过equals()方法比较为true,则HashSet集合认为两个元素相等)

  • 在 HashSet 类中实现了 Collection 接口中的所有方法。HashSet 类的常用构造方法重载形式如下。

    • HashSet():构造一个新的空的 Set 集合。
    • HashSet(Collection<? extends E>c):构造一个包含指定 Collection 集合元素的新 Set 集合。其中,“< >”中的 extends 表示 HashSet 的父类,即指明该 Set 集合中存放的集合元素类型。c 表示其中的元素将被存放在此 Set 集合中。
    HashSet hs = new HashSet();    // 调用无参的构造函数创建HashSet对象
    HashSet<String> hss = new HashSet<String>();    // 创建泛型的 HashSet 集合对象
  1. 例1 (HashSet测试)

    public class HashSetTest {
      public static void main(String[] args) {
        // 创建一个空的的Set集合
        HashSet<String> courseSet = new HashSet<String>();
        String course1 = new String("Java入门教程");
        String course2 = new String("Python入门教程");
        String course3 = new String("C语言入门教程");
        String course4 = new String("Golang入门教程");
        // 将course1存储到Set集合中
        courseSet.add(course1);
        courseSet.add(course2);
        courseSet.add(course3);
        courseSet.add(course4);
        System.out.println("C语言中文网教程有:");
        Iterator<String> it = courseSet.iterator();
        while (it.hasNext()) {
          // 输出
          System.out.println("《" + (String) it.next() + "》");
        }
        System.out.println("有" + courseSet.size() + "套精彩教程!");
      }
    }
2:TreeSet类
  • 定义:TreeSet类同时实现了Set接口和SortedSet接口。SortedSet接口是Set接口的子接口,可以实现对集合进行自然排序,(自然排序==升序排序)。

  • TreeSet 只能对实现了 Comparable 接口的类对象进行排序,因为 Comparable 接口中有一个 compareTo(Object o) 方法用于比较两个对象的大小。例如 a.compareTo(b),如果 a 和 b 相等,则该方法返回 0;如果 a 大于 b,则该方法返回大于 0 的值;如果 a 小于 b,则该方法返回小于 0 的值。

    实现Comparable接口类对象的比较方式
    比较方式
    包装类(BigDecimal、Biglnteger、 Byte、Double、 Float、Integer、Long 及 Short) 按数字大小比较
    Character 按字符的 Unicode 值的数字大小比较
    String 按字符串中字符的 Unicode 值的数字大小比较
    TreeSet常用方法
    方法名称 说明
    E first() 返回此集合中的第一个元素。其中,E 表示集合中元素的数据类型
    E last() 返回此集合中的最后一个元素
    E poolFirst() 获取并移除此集合中的第一个元素
    E poolLast() 获取并移除此集合中的最后一个元素
    SortedSet subSet(E fromElement,E toElement) 返回一个新的集合,新集合包含原集合中 fromElement 对象与 toElement 对象之间的所有对象。包含 fromElement 对象,不包含 toElement 对象
    SortedSet headSet<E toElement〉 返回一个新的集合,新集合包含原集合中 toElement 对象之前的所有对象。 不包含 toElement 对象
    SortedSet tailSet(E fromElement) 返回一个新的集合,新集合包含原集合中 fromElement 对象之后的所有对 象。包含 fromElement 对象
  1. 例2 (使用Tree类创建Set集合)

    public class TreeSetTest {
      public static void main(String[] args) {
        // 创建TreeSet集合
        TreeSet<Double> scores = new TreeSet<Double>();
        Scanner input = new Scanner(System.in);
        System.out.println("------------学生成绩管理系统-------------");
        for (int i = 0; i < 5; i++) {
          System.out.println("第" + (i + 1) + "个学生成绩");
          double score = input.nextDouble();
          // 将学生成绩转换为Double类型,添加到TreeSet集合中
          scores.add(Double.valueOf(score));
        }
        // 成绩Iterator对象
        Iterator<Double> it = scores.iterator();
        System.out.println("学生成绩从低到高的排序为:");
        while (it.hasNext()) {
          System.out.println(it.next() + "\t");
        }
        System.out.println("\n请输入要查询的成绩");
        double searchScore = input.nextDouble();
        if (scores.contains(searchScore)) {
          System.out.println("成绩为:" + searchScore + "的学生存在!");
        } else {
          System.out.println("成绩为:" + searchScore + "的学生不存在!");
        }
        // 查询不及格的学生成绩
        SortedSet<Double> score1 = scores.headSet(60.0);
        System.out.println("\n不及格的成绩有:");
        for (int i = 0; i < score1.toArray().length; i++) {
          System.out.print(score1.toArray()[i] + "\t");
        }
        // 查询90分以上的学生成绩
        SortedSet<Double> score2 = scores.tailSet(90.0);
        System.out.println("\n90 分以上的成绩有:");
        for (int i = 0; i < score2.toArray().length; i++) {
          System.out.print(score2.toArray()[i] + "\t");
        }
      }
    }

    在使用自然排序时只能向TreeSet集合中添加相同数据类型的对象,否则会抛出ClassCastException异常。如果向 TreeSet 集合中添加了一个 Double 类型的对象,则后面只能添加 Double 对象,不能再添加其他类型的对象,例如 String 对象等。

3:Map集合

  • 定义:Map是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键(key)对象和一个值(value)对象。用于保存具有映射关系的数据

  • Map 集合里保存着两组值,一组值用于保存 Map 里的 key,另外一组值用于保存 Map 里的 value,key 和 value 都可以是任何引用类型的数据。Map 的 key 不允许重复,value 可以重复,即同一个 Map 对象的任何两个 key 通过 equals 方法比较总是返回 false

  • Map 接口主要有两个实现类:HashMap 类和 TreeMap 类。其中,HashMap 类按哈希算法来存取键对象,而 TreeMap 类可以对键对象进行排序

    Map接口的常用方法
    方法名称 说明
    void clear() 删除该 Map 对象中的所有 key-value 对。
    boolean containsKey(Object key) 查询 Map 中是否包含指定的 key,如果包含则返回 true。
    boolean containsValue(Object value) 查询 Map 中是否包含一个或多个 value,如果包含则返回 true。
    V get(Object key) 返回 Map 集合中指定键对象所对应的值。V 表示值的数据类型
    V put(K key, V value) 向 Map 集合中添加键-值对,如果当前 Map 中已有一个与该 key 相等的 key-value 对,则新的 key-value 对会覆盖原来的 key-value 对。
    void putAll(Map m) 将指定 Map 中的 key-value 对复制到本 Map 中。
    V remove(Object key) 从 Map 集合中删除 key 对应的键-值对,返回 key 对应的 value,如果该 key 不存在,则返回 null
    boolean remove(Object key, Object value) 这是 Java 8 新增的方法,删除指定 key、value 所对应的 key-value 对。如果从该 Map 中成功地删除该 key-value 对,该方法返回 true,否则返回 false。
    Set entrySet() 返回 Map 集合中所有键-值对的 Set 集合,此 Set 集合中元素的数据类型为 Map.Entry
    Set keySet() 返回 Map 集合中所有键对象的 Set 集合
    boolean isEmpty() 查询该 Map 是否为空(即不包含任何 key-value 对),如果为空则返回 true。
    int size() 返回该 Map 里 key-value 对的个数
    Collection values() 返回该 Map 里所有 value 组成的 Collection
    public class HashMapTest {
      public static void main(String[] args) {
        HashMap users = new HashMap();
        // 将学生信息键值对存储到Map中
        users.put("11", "张浩太");
        users.put("22", "刘思诚");
        users.put("33", "王强文");
        users.put("44", "李国量");
        users.put("55", "王路路");
        System.out.println("******** 学生列表 ********");
        Iterator it = users.keySet().iterator();
        while (it.hasNext()) {
          // 遍历 Map
          Object key = it.next();
          Object val = users.get(key);
          System.out.println("学号:" + key + ",姓名:" + val);
        }
        Scanner input = new Scanner(System.in);
        System.out.println("请输入要删除的学号:");
        int num = input.nextInt();
        // 判断是否包含指定键
        if (users.containsKey(String.valueOf(num))) {
          users.remove(String.valueOf(num));
        } else {
          System.out.println("该学生不存在!");
        }
        System.out.println("******** 学生列表 ********");
        it = users.keySet().iterator();
        while (it.hasNext()) {
          Object key = it.next();
          Object val = users.get(key);
          System.out.println("学号:" + key + ",姓名:" + val);
        }
      }
    }

    TreeMap 类的使用方法与 HashMap 类相同,唯一不同的是 TreeMap 类可以对键对象进行排序

💨:遍历Map集合
  • 定义:Map有两组值,因此遍历时可以只遍历值的集合,也可以只遍历键的集合,也可以同时遍历。
  1. for循环中使用entries实现Map的遍历(最常见和最常用的)

    public class EntriesTest {
      public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("Java入门教程", "http://c.biancheng.net/java/");
        map.put("C语言入门教程", "http://c.biancheng.net/c/");
        for (Map.Entry<String, String> entry : map.entrySet()) {
          String mapKey = entry.getKey();
          String mapValue = entry.getValue();
          System.out.println(mapKey + ":" + mapValue);
        }
      }
    }
  2. for-each循环遍历key或者values,一般适用于只需要Map中key或者value时使用。性能上比 entrySet 较好

    public class ForEachTest {
      public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("Java入门教程", "http://c.biancheng.net/java/");
        map.put("C语言入门教程", "http://c.biancheng.net/c/");
        // 打印键集合
        for (String key : map.keySet()) {
          System.out.println(key);
        }
        // 打印值集合
        for (String value : map.values()) {
          System.out.println(value);
        }
      }
    }
  3. 迭代器(Iterator)遍历

    public class IteratorTest {
      public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("Java入门教程", "http://c.biancheng.net/java/");
        map.put("C语言入门教程", "http://c.biancheng.net/c/");
        Iterator<Map.Entry<String, String>> entries = map.entrySet().iterator();
        while (entries.hasNext()) {
          Map.Entry<String, String> entry = entries.next();
          String key = entry.getKey();
          String value = entry.getValue();
          System.out.println(key + ":" + value);
        }
      }
    }
  4. 通过键找值遍历

    for(String key : map.keySet()){
        String value = map.get(key);
        System.out.println(key+":"+value);
    }

2):Collection

1.Collection接口

  • 定义:Collection接口时List、Set和Queue接口的父接口,通常情况下不被直接使用。Collection 接口定义了一些通用的方法,通过这些方法可以实现对集合的基本操作。定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。

    Collection接口的常用方法
    方法名称 说明
    boolean add(E e) 向集合中添加一个元素,如果集合对象被添加操作改变了,则返回 true。E 是元素的数据类型
    boolean addAll(Collection c) 向集合中添加集合 c 中的所有元素,如果集合对象被添加操作改变了,则返回 true。
    void clear() 清除集合中的所有元素,将集合长度变为 0。
    boolean contains(Object o) 判断集合中是否存在指定元素
    boolean containsAll(Collection c) 判断集合中是否包含集合 c 中的所有元素
    boolean isEmpty() 判断集合是否为空
    Iteratoriterator() 返回一个 Iterator 对象,用于遍历集合中的元素
    boolean remove(Object o) 从集合中删除一个指定元素,当集合中包含了一个或多个元素 o 时,该方法只删除第一个符合条件的元素,该方法将返回 true。
    boolean removeAll(Collection c) 从集合中删除所有在集合 c 中出现的元素(相当于把调用该方法的集合减去集合 c)。如果该操作改变了调用该方法的集合,则该方法返回 true。
    boolean retainAll(Collection c) 从集合中删除集合 c 里不包含的元素(相当于把调用该方法的集合变成该集合和集合 c 的交集),如果该操作改变了调用该方法的集合,则该方法返回 true。
    int size() 返回集合中元素的个数
    Object[] toArray() 把集合转换为一个数组,所有的集合元素变成对应的数组元素。
  1. 例1 (Collection 接口向集合中添加方法)

    public class CollectionTest {
      public static void main(String[] args) {
        // 创建集合list1
        ArrayList list1 = new ArrayList();
        // 创建集合list2
        ArrayList list2 = new ArrayList();
        // 向 list1 添加一个元素
        list1.add("one");
        // 向 list1 添加一个元素
        list1.add("two");
        // 将 list1 的所有元素添加到 list2
        list2.addAll(list1);
        // 向 list2 添加一个元素
        list2.add("three");
        System.out.println("list2集合中的元素如下:");
        Iterator it1 = list2.iterator();
        while (it1.hasNext()) {
          System.out.print(it1.next() + "、");
        }
      }
    }
  2. 例2 (Collection 接口集合中size()、remove()和removeAll())

    public class CollectionTest1 {
      public static void main(String[] args) {
        // 创建集合 list1
        ArrayList list1 = new ArrayList();
        // 创建集合 list2
        ArrayList list2 = new ArrayList();
        list1.add("one");
        list1.add("two");
        list1.add("three");
        // 输出list1中的元素数量
        System.out.println("list1 集合中的元素数量:" + list1.size());
        list2.add("two");
        list2.add("four");
        list2.add("six");
        // 输出list2中的元素数量
        System.out.println("list2 集合中的元素数量:" + list2.size());
        // 删除第 3 个元素
        list2.remove(2);
        System.out.println("\nremove() 方法之后 list2 集合中的元素数量:" + list2.size());
        System.out.println("list2 集合中的元素如下:");
        Iterator it1 = list2.iterator();
        while (it1.hasNext()) {
          System.out.print(it1.next() + "、");
        }
        list1.removeAll(list2);
        System.out.println("\nremoveAll() 方法之后 list1 集合中的元素数量:" + list1.size());
        System.out.println("list1 集合中的元素如下:");
        Iterator it2 = list1.iterator();
        while (it2.hasNext()) {
          System.out.print(it2.next() + "、");
        }
      }
    }

    注意:retainAll( ) 方法的作用与 removeAll( ) 方法相反,即保留两个集合中相同的元素,其他全部删除

2.Collection类

  • 定义:Collection类是操作Set、List和Map等集合的工具类,Collections 类提供了许多操作集合的静态方法,借助这些静态方法可以实现集合元素的排序、查找替换和复制等操作
1:排序(正向和逆向)
  • Collections 提供了如下方法用于对 List 集合元素进行排序。
    • void reverse(List list):对指定 List 集合元素进行逆向排序。
    • void shuffle(List list):对 List 集合元素进行随机排序(shuffle 方法模拟了“洗牌”动作)。
    • void sort(List list):根据元素的自然顺序对指定 List 集合的元素按升序进行排序。
    • void sort(List list, Comparator c):根据指定 Comparator 产生的顺序对 List 集合元素进行排序。
    • void swap(List list, int i, int j):将指定 List 集合中的 i 处元素和 j 处元素进行交换。
    • void rotate(List list, int distance):当 distance 为正数时,将 list 集合的后 distance 个元素“整体”移到前面;当 distance 为负数时,将 list 集合的前 distance 个元素“整体”移到后面。该方法不会改变集合的长度。
  1. 例1 (输入商品价格,sort低到高排序)

    public class ShopSortTest {
      public static void main(String[] args) {
     Scanner input = new Scanner(System.in);
        List<Integer> prices = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
          System.out.println("请输入第" + (i + 1) + "个商品价格:");
          int p = input.nextInt();
          // 将录入的价格保存到List集合中
          prices.add(Integer.valueOf(p));
        }
        // 调用sort()方法对集合进行排序
        Collections.sort(prices);
        System.out.println("|价格从低到高的排序为:");
        for (Object price : prices) {
          System.out.println(price + "\t");
        }
      }
    }
  2. 例2 (输入商品名称,降序排序,后录入者先输出)

    public class ShopSortTest1 {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        List<String> students = new ArrayList<String>();
        System.out.println("******** 商品信息 ********");
        for (int i = 0; i < 5; i++) {
          System.out.println("请输入第" + (i + 1) + "个商品的名称:");
          String name = input.next();
          students.add(name);
        }
        Collections.reverse(students);
        System.out.println("按录入时间的先后顺序进行降序排列为:");
        for (Object student : students) {
          System.out.println(student + "\t");
        }
      }
    }
2:查找、替换操作
  • Collections 还提供了如下常用的用于查找、替换集合元素的方法:
    • int binarySearch(List list, Object key):使用二分搜索法搜索指定的 List 集合,以获得指定对象在 List 集合中的索引。如果要使该方法可以正常工作,则必须保证 List 中的元素已经处于有序状态。
    • Object max(Collection coll):根据元素的自然顺序,返回给定集合中的最大元素。
    • Object max(Collection coll, Comparator comp):根据 Comparator 指定的顺序,返回给定集合中的最大元素。
    • Object min(Collection coll):根据元素的自然顺序,返回给定集合中的最小元素。
    • Object min(Collection coll, Comparator comp):根据 Comparator 指定的顺序,返回给定集合中的最小元素。
    • void fill(List list, Object obj):使用指定元素 obj 替换指定 List 集合中的所有元素。
    • int frequency(Collection c, Object o):返回指定集合中指定元素的出现次数。
    • int indexOfSubList(List source, List target):返回子 List 对象在父 List 对象中第一次出现的位置索引;如果父 List 中没有出现这样的子 List,则返回 -1。
    • int lastIndexOfSubList(List source, List target):返回子 List 对象在父 List 对象中最后一次出现的位置索引;如果父 List 中没有岀现这样的子 List,则返回 -1。
    • boolean replaceAll(List list, Object oldVal, Object newVal):使用一个新值 newVal 替换 List 对象的所有旧值 oldVal。
    public class CollectionsTest1 {
      public static void main(String[] args) {
        ArrayList nums = new ArrayList();
        nums.add(2);
        nums.add(-5);
        nums.add(3);
        nums.add(0);
        // 输出
        System.out.println(nums);
        // 输出最大值
        System.out.println(Collections.max(nums));
        // 输出最小值
        System.out.println(Collections.min(nums));
        // nums中的0使用1代替
        Collections.replaceAll(nums, 0, 2);
        // 再次输出
        System.out.println(nums);
        // 判断-5出现的次数
        System.out.println(Collections.frequency(nums, -5));
        Collections.sort(nums);
        System.out.println(nums);
        // 只有排序后的list集合才可以用二分法查找,指定元素的索引
        System.out.println(Collections.binarySearch(nums, -5));
      }
    }
3:复制
  • 定义:Collections类的copy()静态方法用于指定集合中的所有元素复制到另一个集合中,执行copy()方法后,目标集合中每个已复制元素的索引将等同于源集合中该元素的索引

    void copy(List <? super T> dest,list<? extends T> src)
    • dest 表示目标集合对象,src 表示源集合对象

    目标集合的长度至少和源集合的长度相同,如果目标集合的长度更长,则不影响目标集合中的其余元素。如果目标集合长度不够而无法包含整个源集合元素,程序将抛出 IndexOutOfBoundsException 异常

    例 (保存商品名称,使用copy替换)

    public class CopyTest {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        List destList = new ArrayList();
        List srcList = new ArrayList();
        srcList.add("木糖醇");
        srcList.add("方便面");
        srcList.add("火腿肠");
        srcList.add("苏打水");
        srcList.add("冰红茶");
        for (Object o : srcList) {
          System.out.println(o);
        }
        System.out.println("输出替换的商品名称:");
        for (int i = 0; i < 3; i++) {
          System.out.println("第" + (i + 1) + "个商品:");
          String name = input.next();
          destList.add(name);
        }
        // 调用copy()方法将当前商品信息复制到原有商品信息集合中
        Collections.copy(srcList, destList);
        System.out.println("当前商品有:");
        for (Object o : srcList) {
          System.out.print(o + "\t");
        }
      }
    }
4:Lambda表达式遍历Collection集合
  • Iterable接口新增forEach(Consumer action)默认方法,该方法所需参数的类型是一个函数式接口,而Iterable接口是Collection接口的父接口。

    public class CollectionEach {
      public static void main(String[] args) {
        // 创造一个集合
        Collection objs = new HashSet();
        objs.add("C语言1");
        objs.add("C语言2");
        objs.add("C语言3");
        // 调用forEach()方法遍历集合
        objs.forEach(obj -> System.out.println("迭代集合元素:" + obj));
        List list = new ArrayList();
        list.add("Ram");
        list.add("TutorialPoints");
        list.forEach(System.out::print);
      }
    }

3):Map新增的方法

  • Map新增默认方法remove(Object key,Object value)。

    Map接口的常用方法
    名称 说明
    Object compute(Object key, BiFunction remappingFunction) 该方法使用 remappingFunction 根据原 key-value 对计算一个新 value。只要新 value 不为 null,就使用新 value 覆盖原 value;如果原 value 不为 null,但新 value 为 null,则删除原 key-value 对;如果原 value、新 value 同时为 null,那么该方法不改变任何 key-value 对,直接返回 null。
    Object computeIfAbsent(Object key, Function mappingFunction) 如果传给该方法的 key 参数在 Map 中对应的 value 为 null,则使用 mappingFunction 根据 key 计算一个新的结果,如果计算结果不为 null,则用计算结果覆盖原有的 value。如果原 Map 原来不包括该 key,那么该方法可能会添加一组 key-value 对。
    Object computeIfPresent(Object key, BiFunction remappingFunction) 如果传给该方法的 key 参数在 Map 中对应的 value 不为 null,该方法将使用 remappingFunction 根据原 key、value 计算一个新的结果,如果计算结果不为 null,则使用该结果覆盖原来的 value;如果计算结果为 null,则删除原 key-value 对。
    void forEach(BiConsumer action) 该方法是 Java 8 为 Map 新增的一个遍历 key-value 对的方法,通过该方法可以更简洁地遍历 Map 的 key-value 对。
    Object getOrDefault(Object key, V defaultValue) 获取指定 key 对应的 value。如果该 key 不存在,则返回 defaultValue。
    Object merge(Object key, Object value, BiFunction remappingFunction) 该方法会先根据 key 参数获取该 Map 中对应的 value。如果获取的 value 为 null,则直接用传入的 value 覆盖原有的 value(在这种情况下,可能要添加一组 key-value 对);如果获取的 value 不为 null,则使用 remappingFunction 函数根据原 value、新 value 计算一个新的结果,并用得到的结果去覆盖原有的 value。
    Object putIfAbsent(Object key, Object value) 该方法会自动检测指定 key 对应的 value 是否为 null,如果该 key 对应的 value 为 null,该方法将会用新 value 代替原来的 null 值。
    Object replace(Object key, Object value) 将 Map 中指定 key 对应的 value 替换成新 value。与传统 put() 方法不同的是,该方法不可能添加新的 key-value 对。如果尝试替换的 key 在原 Map 中不存在,该方法不会添加 key-value 对,而是返回 null。
    boolean replace(K key, V oldValue, V newValue) 将 Map 中指定 key-value 对的原 value 替换成新 value。如果在 Map 中找到指定的 key-value 对,则执行替换并返回 true,否则返回 false。
    replaceAll(BiFunction function) 该方法使用 BiFunction 对原 key-value 对执行计算,并将计算结果作为该 key-value 对的 value 值。
    public class MapTest {
      public static void main(String[] args) {
        Map map = new HashMap();
        // 成对放入多个key-value对
        map.put("Java入门教程", 10);
        map.put("C语言入门教程", 20);
        map.put("Python基础教程", 30);
        // 尝试替换key为”Go语言入门教程”的 value,由于原 Map 中没有对应的 key
        // 因此Map没有改变,不会添加新的key-value对
        map.replace("Go语言入门教程", 40);
        System.out.println(map);
        // 使用原value与传入参数计算出来的结果覆盖原有的value
        map.merge("C语言入门教程", 25, (oldVal, param) -> (Integer) oldVal + (Integer) param);
        System.out.println(map);
        // 当key为"Java"对应的value为null (或不存在)时,使用计算的结果作为新value
        map.computeIfAbsent("Java", (key) -> ((String) key).length());
        // map 中添加了 Java=4 这组 key-value 对
        System.out.println(map);
        // 当key为"Java"对应的value存在时,使用计算的结果作为新value
        map.computeIfPresent("Java", (key, value) -> (Integer) value * (Integer) value);
        // map 中 Java=4 变成 Java=16
        System.out.println(map);
      }
    }

4):使用迭代器遍历集合元素

  • 定义:Iterator(迭代器)是一个接口,它的作用就是遍历容器的所有元素,也是 Java 集合框架的成员,但它与 Collection 和 Map 系列的集合不一样,Collection 和 Map 系列集合主要用于盛装其他对象,而 Iterator 则主要用于遍历(即迭代访问)Collection 集合中的元素。

  • Iterator 接口隐藏了各种 Collection 实现类的底层细节,向应用程序提供了遍历 Collection 集合元素的统一编程接口。Iterator 接口里定义了如下 4 个方法:

    • boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回 true。
    • Object next():返回集合里的下一个元素。
    • void remove():删除集合里上一次 next 方法返回的元素。
    • void forEachRemaining(Consumer action):这是 Java 8 为 Iterator 新增的默认方法,该方法可使用 Lambda 表达式来遍历集合元素
    public class IteratorTest1 {
      public static void main(String[] args) {
        // 创建一个集合
        Collection<String> objs = new HashSet<String>();
        objs.add("C语言中文网Java教程");
        objs.add("C语言中文网C语言教程");
        objs.add("C语言中文网C++教程");
        // 调用forEach()方法遍历集合
        // 获取books集合对应的迭代器
        Iterator<String> it = objs.iterator();
        while (it.hasNext()) {
          // it.next()方法返回的数据类型是Object类型,印次需要强制类型转换
          String obj = it.next();
          System.out.println(obj);
          if ("C语言中文网C语言教程".equals(obj)) {
            // 从集合中删除上一个next()方法返回的元素
            it.remove();
          }
          // 对book变量赋值,不会改变集合元素本身
          obj = "C语言中文网python语言教程";
        }
        System.out.println(objs);
      }
    }

    Iterator 必须依附于 Collection 对象,若有一个 Iterator 对象,则必然有一个与之关联的 Collection 对象。Iterator 提供了两个方法来迭代访问 Collection 集合里的元素,并可通过 remove() 方法来删除集合中上一次 next() 方法返回的集合元素

  1. 当使用 Iterator 迭代访问 Collection 集合元素时,Collection 集合里的元素不能被改变,只有通过 Iterator 的 remove() 方法删除上一次 next() 方法返回的集合元素才可以,否则将会引发“java.util.ConcurrentModificationException”异常

    public class IteratorErrorTest {
      public static void main(String[] args) {
        // 创建一个集合
        Collection<String> objs = new HashSet<>();
        objs.add("C语言中文网Java教程");
        objs.add("C语言中文网C语言教程");
        objs.add("C语言中文网C++教程");
        // 获取books集合对应的迭代器
        Iterator<String> it = objs.iterator();
        while (it.hasNext()) {
          String obj = it.next();
          System.out.println(obj);
          if (obj.equals("C语言中文网C++教程")) {
            // 使用Iterator迭代过程中,不可修改集合元素,下面代码引发异常
            objs.remove(obj);
          }
        }
      }
    }
    • Iterator 迭代器采用的是快速失败(fail-fast)机制,一旦在迭代过程中检测到该集合已经被修改(通常是程序中的其他线程修改),程序立即引发 ConcurrentModificationException 异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题

    快速失败(fail-fast)机制,是 Java Collection 集合中的一种错误检测机制

    上面程序如果改为删除“C语言中文网C语言教程”字符串,则不会引发异常。这样可能有些读者会“心存侥幸”地想,在迭代时好像也可以删除集合元素啊。实际上这是一种危险的行为。对于 HashSet 以及后面的 ArrayList 等,迭代时删除元素都会导致异常。只有在删除集合中的某个特定元素时才不会抛出异常,这是由集合类的实现代码决定的,程序员不应该这么做

  2. Lambda表达式遍历迭代器

    • Iterator 引入了一个 forEachRemaining(Consumer action) 默认方法,该方法所需的 Consumer 参数同样也是函数式接口。当程序调用 Iterator 的forEachRemaining(Consumer action) 遍历集合元素时,程序会依次将集合元素传给 Consumer 的 accept(T t) 方法(该接口中唯一的抽象方法)

    java.util.function 中的 Function、Supplier、Consumer、Predicate 和其他函数式接口被广泛用在支持 Lambda 表达式的 API 中。“void accept(T t);”是 Consumer 的核心方法,用来对给定的参数 T 执行定义操作

    public class IteratorEach {
      public static void main(String[] args) {
        Collection objs = new HashSet();
        objs.add("1");
        objs.add("2");
        objs.add("3");
        Iterator it = objs.iterator();
        it.forEachRemaining(o -> System.out.println("迭代集合元素:" + o));
      }
    }

5):foreach遍历Collection集合

  • Java5 foreach循环迭代方法集合元素

    public class ForeachTest1 {
      public static void main(String[] args) {
        // 创建一个集合
        Collection<String> objs = new HashSet<String>();
        objs.add("C语言中文网Java教程");
        objs.add("C语言中文网C语言教程");
        objs.add("C语言中文网C++教程");
        for (String obj : objs) {
          // 此处的obj变量也不是集合元素本身
          System.out.println(obj);
          if (obj.equals("C语言中文网Java教程")) {
            // 下面代码会引发 ConcurrentModificationException 异常
            objs.remove(obj);
          }
        }
        System.out.println(objs);
      }
    }

6):Predicate操作Collection集合

  • Collection 集合新增了一个 removeIf(Predicate filter) 方法,该方法将会批量删除符合 filter 条件的所有元素。该方法需要一个 Predicate 对象作为参数,Predicate 也是函数式接口,因此可使用 Lambda 表达式作为参数

    public class ForeachTest2 {
      public static void main(String[] args) {
        Collection<String> objs = new HashSet<>();
        objs.add("C语言中文网Java教程");
        objs.add("C语言中文网Java教程");
        objs.add("C语言中文网C++教程");
        objs.add("C语言中文网C语言教程");
        objs.add("C语言中文网Python教");
        objs.add("C语言中文网Go教程");
        objs.removeIf(ele -> ((String) ele).length() < 12);
        System.out.println(objs);
      }
    }
    • 长度小于12的则删除
  • 需求:

    1. 统计文本相同的字符串数量
    2. 统计特定字符串的数量
    3. 统计字符串长度大于4的数量
    public class ForeachTest3 {
      public static void main(String[] args) {
        Collection<String> objs = new HashSet<>();
        objs.add("学习Java");
        objs.add("学习React");
        objs.add("学习SQL");
        objs.add("学习Spring boot");
        objs.add("学习Ts");
        // 统计集合中出现“学习”字符串的数量
        System.out.println(calAll(objs, ele -> ((String) ele).contains("学习")));
        // 统计集合中出现“Java”字符串的数量
        System.out.println(calAll(objs, ele -> ((String) ele).contains("Java")));
        // 统计集合中出现字符串长度大于 4 的数量
        System.out.println(calAll(objs, ele -> ((String) ele).length() > 4));
      }
    
      public static int calAll(Collection str, Predicate<Object> p) {
        int total = 0;
        for (Object o : str) {
          // 使用Predicate的test()方法判断该对象是否满足Predicate指定的条件
          if (p.test(o)) {
            total++;
          }
        }
        return total;
      }
    }

7):Stream操作Collection集合

  • 定义:Stream 是一个通用的流接口,而 IntStream、LongStream、 DoubleStream 则代表元素类型为 int、long、double 的流。

  • 独立使用 Stream 的步骤如下:

    1. 使用 Stream 或 XxxStream 的 builder() 类方法创建该 Stream 对应的 Builder。
    2. 重复调用 Builder 的 add() 方法向该流中添加多个元素。
    3. 调用 Builder 的 build() 方法获取对应的 Stream。
    4. 调用 Stream 的聚集方法。
    public class IntStreamTest {
      public static void main(String[] args) {
        IntStream is = IntStream.builder().add(20).add(13).add(-2).add(18).build();
        // 下面调用聚集方法的代码每次只能执行一行
        System.out.println("is 所有元素的最大值:" + is.max().getAsInt());
        System.out.println("is 所有元素的最小值:" + is.min().getAsInt());
        System.out.println("is 所有元素的总和:" + is.sum());
        System.out.println("is 所有元素的总数:" + is.count());
        System.out.println("is 所有元素的平均值:" + is.average());
        System.out.println("is所有元素的平方是否都大于20: " + is.allMatch(ele -> ele * ele > 20));
        System.out.println("is是否包含任何元素的平方大于20 : " + is.anyMatch(ele -> ele * ele > 20));
        // 将is映射成一个新Stream,新Stream的每个元素是原Stream元素的2倍+1
        IntStream newIs = is.map(ele -> ele * 2 + 1);
        // 使用方法引用的方式来遍历集合元素
        newIs.forEach(System.out::println);
      }
    }

    上面 5~13 行代码每次只能执行一行,因此需要把其他代码注释掉

  • Stream 提供了大量的方法进行聚集操作,这些方法既可以是“中间的”(intermediate),也可以是 “末端的”(terminal)。

    • 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的 map() 方法就是中间方法。中间方法的返回值是另外一个流。
    • 末端方法:末端方法是对流的最终操作。当对某个 Stream 执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的 sum()、count()、average() 等方法都是末端方法
  • 除此之外,关于流的方法还有如下两个特征。

    • 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
    • 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。
    Stream常用的中间方法
    方法 说明
    filter(Predicate predicate) 过滤 Stream 中所有不符合 predicate 的元素
    mapToXxx(ToXxxFunction mapper) 使用 ToXxxFunction 对流中的元素执行一对一的转换,该方法返回的新流中包含了 ToXxxFunction 转换生成的所有元素。
    peek(Consumer action) 依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。
    distinct() 该方法用于排序流中所有重复的元素(判断元素重复的标准是使用 equals() 比较返回 true)。这是一个有状态的方法。
    sorted() 该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
    limit(long maxSize) 该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。
    Stream常用的末端方法
    方法 说明
    forEach(Consumer action) 遍历流中所有元素,对每个元素执行action
    toArray() 将流中所有元素转换为一个数组
    reduce() 该方法有三个重载的版本,都用于通过某种操作来合并流中的元素
    min() 返回流中所有元素的最小值
    max() 返回流中所有元素的最大值
    count() 返回流中所有元素的数量
    anyMatch(Predicate predicate) 判断流中是否至少包含一个元素符合 Predicate 条件。
    allMatch(Predicate predicate) 判断流中是否每个元素都符合 Predicate 条件
    noneMatch(Predicate predicate) 判断流中是否所有元素都不符合 Predicate 条件
    findFirst() 返回流中的第一个元素
    findAny() 返回流中的任意一个元素
    public class CollectionStream {
      public static void main(String[] args) {
        Collection<String> objs = new HashSet<>();
        objs.add("学习Java");
        objs.add("学习React");
        objs.add("学习SQL");
        objs.add("学习Spring boot");
        objs.add("学习Ts");
        // 统计集合中出现“学习”字符串的数量
        System.out.println(objs.stream().filter(ele -> ((String) ele).contains("学习")).count());
        // 统计集合中出现“Java”字符串的数量
        System.out.println(objs.stream().filter(ele -> ((String) ele).contains("Java")).count());
        // 统计集合中出现字符串长度大于 4 的数量
        System.out.println(objs.stream().filter(ele -> ((String) ele).length() > 4).count());
        // 先调用Collection对象的stream() 方法将集合转换为Stream
        // 再调用Stream的mapToInt()获取原有的Stream对应的InStream
        objs.stream()
            .mapToInt(ele -> ((String) ele).length())
            // 调用forEach()方法遍历IntStream中每个元素
            .forEach(System.out::println);
      }
    }

8):新增的不可变集合(Java 9)

  • 定义:不可变意味着程序不能向集合中添加元素,也不能从集合中删除元素。

  • 方法:使用Set、List、Map的of()方法可创建包含N个元素的不可变集合,即一行代码可创建N个元素的集合。

    public class Java9Collection {
      public static void main(String[] args) {
        // 创建包含4个元素的Set集合
        Set<String> set = Set.of("Java", "Kotlin", "Go", "Swift");
        System.out.println(set);
        // 不可变集合,下面代码导致运行时错误
        // set.add("Ruby");
        // 创建包含4个元素的List集合
        List list = List.of(34, -25, 67, 231);
        System.out.println(list);
        // 不可变集合,下面代码导致运行时错误
        // list.remove(1);
        // 创建包含3个key-value对的Map集合
        Map map = Map.of("语文", 89, "数学", 82, "英语", 92);
        System.out.println(map);
        // 不可变集合,下面代码导致运行时错误
        // map.remove("语文");
        // 使用Map.entry()方法显式构建key-value对
        Map map2 = Map.ofEntries(Map.entry("语文", 89), Map.entry("数学", 82), Map.entry("英语", 92));
        System.out.println(map2);
      }
    }

9):泛型

  • 定义:泛型可以在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率

1:泛型集合

  • 定义:泛型本质上是提供类型的"类型参数",也就是参数化类型。我们可以为类、接口或方法指定一个类型参数,通过这个参数限制操作的数据类型,从而保证类型转换的绝对安全
💨:图书案例
  1. 创建图书实体类Book

    public class Book {
      private int Id;
      private String Name;
      private int Price;
    
      public Book(int Id, String Name, int Price) {
        this.Id = Id;
        this.Name = Name;
        this.Price = Price;
      }
    
      @Override
      public String toString() {
        return this.Id + "," + this.Name + "," + this.Price;
      }
    }
  2. 创建Map和List泛型集合

    public class BookTest {
      public static void main(String[] args) {
        // 创建3个Book对象
        Book book1 = new Book(1, "唐诗三百首", 8);
        Book book2 = new Book(2, "小星星", 22);
        Book book3 = new Book(3, "成语大全", 22);
        // 定义泛型Map集合
        Map<Integer, Book> books = new HashMap<>();
        books.put(1001, book1);
        books.put(1002, book2);
        books.put(1003, book3);
        System.out.println("泛型Map存储的图书信息如下:");
        for (Integer id : books.keySet()) {
          // 遍历键
          System.out.print(id + "--");
          // 不需要类型转换
          System.out.println(books.get(id));
        }
        // 定义泛型的List集合
        List<Book> bookList = new ArrayList<Book>();
        bookList.add(book1);
        bookList.add(book2);
        bookList.add(book3);
        System.out.println("泛型List存储的图书信息如下:");
        for (Book book : bookList) {
          System.out.println(book);
        }
      }
    }

2:泛型类

  • 定义:直接限定泛型类的类型参数。

    public class class_name<data_type1,data_type2,...>{}
    • class_name 表示类的名称,data_ type1 等表示类型参数。Java 泛型支持声明一个以上的类型参数,只需要将类型用逗号隔开即可。
  • 泛型类一般用于类中的属性类型不确定的情况下:

    private data_type1 property_name1;
    private data_type2 property_name2;
    • 该语句中的 data_type1 与类声明中的 data_type1 表示的是同一种数据类型
💨:学生案例
  1. 在实例化泛型类时,需要指明泛型类中的类型参数,并赋予泛型类属性相应类型的值

    public class Stu<N, A, S> {
      /** 姓名 */
      private N name;
      /** 年龄 */
      private A age;
      /** 性别 */
      private S sex;
    
      public Stu(N name, A age, S sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
      }
    
      public N getName() {
        return name;
      }
    
      public void setName(N name) {
        this.name = name;
      }
    
      public A getAge() {
        return age;
      }
    
      public void setAge(A age) {
        this.age = age;
      }
    
      public S getSex() {
        return sex;
      }
    
      public void setSex(S sex) {
        this.sex = sex;
      }
    }
  2. 实例化对象

    public class StuTest {
      public static void main(String[] args) {
        Stu<String, Integer, Character> stu = new Stu<>("小明", 30, '男');
        String name = stu.getName();
        Integer age = stu.getAge();
        Character sex = stu.getSex();
        System.out.println("学生信息如下:");
        System.out.println("学生姓名:" + name + ",年龄:" + age + ",性别:" + sex);
      }
    }

3:泛型方法

  • 是否拥有泛型方法,与其所在的类是不是泛型没有关系

  • 泛型方法使得该方法能够独立于类而产生变化。如果使用泛型方法可以取代类泛型化,那么就应该只使用泛型方法。另外,对一个 static 的方法而言,无法访问泛型类的类型参数。因此,如果 static 方法需要使用泛型能力,就必须使其成为泛型方法

    [访问权限修饰符] [static] [final] <类型参数列表> 返回值类型 方法名([形式参数列表])
    public static <T> List find(Class<T> cs,int userId){}
    • 一般来说编写 Java 泛型方法,其返回值类型至少有一个参数类型应该是泛型,而且类型应该是一致的,如果只有返回值类型或参数类型之一使用了泛型,那么这个泛型方法的使用就被限制了
    public class BookTest1 {
      /** 定义泛型方法 */
      public static <T> void list(T book) {
        if (book != null) {
          System.out.println(book);
        }
      }
    
      public static void main(String[] args) {
        Book book = new Book(1, "java", 20);
        list(book);
      }
    }

4:泛型的高级用法

  • 包括限制泛型可用类型、使用类型通配符、继承泛型类和实现泛型接口
1.限制泛型可用类型
  • 默认可以使用任何类型来实例化一个泛型类对象。也可以对泛型类实例的类型进行限制

    class 类名称<T extends anyClass>
    • anyClass 指某个接口或类。使用泛型限制后,泛型类的类型必须实现或继承 anyClass 这个接口或类。无论 anyClass 是接口还是类,在进行泛型限制时都必须使用 extends 关键字
    import java.util.ArrayList;
    import java.util.LinkedList;
    import java.util.List;
    
    /**
     * 限制ListClass的泛型类型必须实现List接口
     *
     * @author Admin
     */
    public class ListClass<T extends List> {
      public static void main(String[] args) {
        // 实例化使用ArrayList的泛型类ListClass,正确
        ListClass<ArrayList> lc1 = new ListClass<>();
        // 实例化使用LinkedList的泛型类ListClass,正确
        ListClass<LinkedList> lc2 = new ListClass<>();
        // 实例化使用HashMap的泛型类ListClass,错误,因为HasMap没有实现List接口
        // ListClass<HashMap> lc3 = new ListClass<HashMap>();
      }
    }
    • 当没有使用 extends 关键字限制泛型类型时,其实是默认使用 Object 类作为泛型类型。因此,Object 类下的所有子类都可以实例化泛型类对象

2.使用类型通配符
  • 定义:作用是在创建一个泛型类对象时限制这个泛型类的类型必须实现或继承某个接口或类

    泛型类名称<? extends List>a = null;
    • “<? extends List>”作为一个整体表示类型未知,当需要使用泛型对象时,可以单独实例化
    A<? extends List>a = null;
    a = new A<ArrayList> ();    // 正确
    b = new A<LinkedList> ();    // 正确
    c = new A<HashMap> ();    // 错误
3.继承泛型类和实现泛型接口
  • 定义:泛型的类和接口也可以被继承和实现。

    public class FatherClass<T1>{}
    public class SonClass<T1,T2,T3> extends FatherClass<T1>{}
  • 如果要在 SonClass 类继承 FatherClass 类时保留父类的泛型类型,需要在继承时指定,否则直接使用 extends FatherClass 语句进行继承操作,此时 T1、T2 和 T3 都会自动变为 Object,所以一般情况下都将父类的泛型类型保留

    interface interface1<T1>{}
    interface SubClass<T1,T2,T3> implements
    Interface1<T2>{}

10):图书信息查询

  1. 创建图书类别的Category类,包含两个属性:id和name,实现set和get方法

    public class Category {
      /** 类型编号 */
      private int id;
      /** 类型名称 */
      private String name;
    
      public Category(int id, String name) {
        this.id = id;
        this.name = name;
      }
    
      @Override
      public String toString() {
        return "所属分类:" + this.name;
      }
    
      public int getId() {
        return id;
      }
    
      public void setId(int id) {
        this.id = id;
      }
    
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    }
  2. 创建图书信息的BookInfo类,包含id、name、price、author和startTime

    public class BookInfo {
      /** 编号 */
      private int id;
      /** 名称 */
      private String name;
      /** 价格 */
      private int price;
      /** 作者 */
      private String author;
      /** 出版时间 */
      private String startTime;
    
      public BookInfo(int id, String name, int price, String author, String startTime) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.author = author;
        this.startTime = startTime;
      }
    
      public int getId() {
        return id;
      }
    
      public void setId(int id) {
        this.id = id;
      }
    
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public int getPrice() {
        return price;
      }
    
      public void setPrice(int price) {
        this.price = price;
      }
    
      public String getAuthor() {
        return author;
      }
    
      public void setAuthor(String author) {
        this.author = author;
      }
    
      public String getStartTime() {
        return startTime;
      }
    
      public void setStartTime(String startTime) {
        this.startTime = startTime;
      }
    }
  3. 创建CategoryDemo类,定义Map映射,键为Category类型的对象,值List类型的对象,打印图书信息方法printCategoryInfo()

    public class CategoryDemo {
      /* 定义泛型Map,存储图书信息 */
      public static Map<Category, List<BookInfo>> categoryListMap = new HashMap<>();
    
      public static void printDeptmentInfo() {
        for (Category cate : categoryListMap.keySet()) {
          System.out.println("所属类别:" + cate.getName());
          List<BookInfo> books = categoryListMap.get(cate);
          System.out.println("图书编号\t\t图书名称\t\t图书价格\t\t图书作者\t\t出版时间");
          for (BookInfo book : books) {
            System.out.println(
                book.getId()
                    + "\t\t"
                    + book.getName()
                    + "\t\t"
                    + book.getPrice()
                    + "\t\t"
                    + book.getAuthor()
                    + "\t\t"
                    + book.getStartTime());
          }
          System.out.println();
        }
      }
    }
  4. 创建测试BooksTest,分别定义Category对象和BookInfo对象,存储到List集合中,再一一对象的关系存储,最后输出。

    public class BooksTest {
      public static void main(String[] args) {
        // 创建类别信息
        Category category1 = new Category(1, "数据库");
        Category category2 = new Category(2, "程序设计");
        Category category3 = new Category(3, "平面设计");
        BookInfo book1 = new BookInfo(1, "细说 Java 编程", 25, "张晓玲", "2012-01-01");
        BookInfo book2 = new BookInfo(2, "影视后期处理宝典", 78, "刘水波", "2012-10-05");
        BookInfo book3 = new BookInfo(3, "MySQL 从入门到精通", 41, "王志亮", "2012-3-2");
        BookInfo book4 = new BookInfo(4, "Java 从入门到精通", 27, "陈奚静", "2012-11-01");
        BookInfo book5 = new BookInfo(5, "SQL Server 一百例", 68, "张晓玲", "2012-01-01");
        // 向类别1添加图书
        List<BookInfo> pList1 = new ArrayList<BookInfo>();
        pList1.add(book1);
        // 向类别1添加图书
        pList1.add(book4);
        List<BookInfo> pList2 = new ArrayList<BookInfo>();
        pList2.add(book3);
        // 向类别1添加图书
        pList2.add(book5);
        List<BookInfo> pList3 = new ArrayList<BookInfo>();
        pList3.add(book2);
        CategoryDemo.categoryListMap.put(category1, pList1);
        CategoryDemo.categoryListMap.put(category2, pList2);
        CategoryDemo.categoryListMap.put(category3, pList3);
        CategoryDemo.printDeptmentInfo();
      }
    }

11):枚举

  • 定义:枚举是一个被命名的整型常数的集合,用于声明一组带标识符的常数。

1:声明枚举

  • 定义:声明枚举时必须使用enum关键字,然后定义枚举的名称、可访问性、基础类型和成员等。

    enum-modifiers enum enumname:enum-base{
    	enum-body
    }
    • enum-modifiers 表示枚举的修饰包括public、private和internal;enumname 表示声明的枚举名称;enum-base 表示基础类型;enum-bdoy 表示枚举的成员,它是枚举类型的命名常数。

    任意两个枚举成员不能相同的名称,且它的常数值必须在该枚举的基础类型范围之类,多个枚举成员之间使用都好分割

    如果没有显示地声明基础类型的枚举,那么意味着它所对应的基础类型是int

  1. 例1 (定义性别枚举、颜色枚举)

    public enum SexEnum {
      male,
      female
    }
    public enum Color {
      RED,
      BLUE,
      GREEN,
      BLACK
    }
    • 则可以通过枚举类型名直接引用常量,如SexEnum.male,Color.RED
    enum Signal {
      /*定义一个枚举类型*/
      GREEN,
      YELLOW,
      RED
    }
    
    /**
     * @author Admin
     */
    public class EnumerationTest {
      Signal color = Signal.RED;
    
      public void change() {
        switch (color) {
          case RED:
            color = Signal.GREEN;
            break;
          case YELLOW:
            color = Signal.RED;
            break;
          case GREEN:
            color = Signal.YELLOW;
            break;
          default:
            throw new IllegalStateException("Unexpected value: " + color);
        }
      }
    }

2:枚举类

  • Java 中的每一个枚举都继承自java.lang.Enum类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是Enum类的实例,这些枚举成员默认都被final、public,static修饰,当使用枚举类型成员时,直接使用枚举名称调用方法即可

  • 所有枚举实例都可以调用Enum类的方法

    Enum类的常用方法
    方法名称 描述
    values() 以数组形式返回枚举类型的所有成员
    valueOf() 将普通字符串转换为枚举实例
    compareTo() 比较两个枚举成员在定义时的顺序
    ordinal() 获取枚举成员的索引位置
  1. 例2 (通过调用枚举类型实例的 values( ) 方法可以将枚举的所有成员以数组形式返回,也可以通过该方法获取枚举类型的成员)

    enum Color1 {
      GREEN,
      YELLOW,
      RED
    }
    
    public class ValuesTest {
      public static void main(String[] args) {
        for (int i = 0; i < Color1.values().length; i++) {
          System.out.println("枚举成员:" + Color1.values()[i]);
        }
      }
    }
  2. 例3 (调用valueOf()获取枚举成员,再比较compareTo())

    public class EnumTest {
      public enum Sex {
        male,
        female
      }
    
      public static void compare(Sex s) {
        for (int i = 0; i < Sex.values().length; i++) {
          System.out.println(s + "与" + Sex.values()[i] + "的比较结果是:" + s.compareTo(Sex.values()[i]));
        }
      }
    
      public static void main(String[] args) {
        compare(Sex.valueOf("male"));
      }
    }
  3. 例4 (ordinal()方法,获取成员在枚举中的索引位置)

    public class EnumTest1 {
      enum Signal {
        GREEN,
        YELLOW,
        RED
      }
    
      public static void main(String[] args) {
        for (Signal signal : Signal.values()) {
          System.out.println("索引" + signal.ordinal() + ",值:" + signal);
        }
      }
    }

3:枚举添加方法

  • 在枚举实例的最后一个成员后添加分号,而且必须先定义枚举实例
  1. 例5 (定义枚举类型WeekDay,添加自定义方法)

    enum WeekDay {
      /** 枚举成员,必须先定义,而且使用分号结束 */
      Mon("星期一"),
      Tue("星期二"),
      Wed("星期三"),
      Thu("星期四"),
      Fri("星期五"),
      Sat("星期六"),
      Sun("星期天");
      private final String day;
    
      WeekDay(String day) {
        this.day = day;
      }
    
      public static void printDay(int i) {
        switch (i) {
          case 1:
            System.out.println(WeekDay.Mon);
            break;
          case 2:
            System.out.println(WeekDay.Tue);
            break;
          case 3:
            System.out.println(WeekDay.Wed);
            break;
          case 4:
            System.out.println(WeekDay.Thu);
            break;
          case 5:
            System.out.println(WeekDay.Fri);
            break;
          case 6:
            System.out.println(WeekDay.Sat);
            break;
          case 7:
            System.out.println(WeekDay.Sun);
            break;
          default:
            System.out.println("不是数字");
        }
      }
    
      public String getDay() {
        return day;
      }
    
      public static void main(String[] args) {
        for (WeekDay day : WeekDay.values()) {
          System.out.println(day + "==>" + day.getDay());
        }
        WeekDay.printDay(5);
      }
    }
  2. enum和Class一样可以覆盖基类的方法

    public class EnumTest2 {
      public enum Color {
        RED("红色", 1),
        GREEN("绿色", 2),
        WHITE("白色", 3),
        YELLOW("黄色", 4);
        /** 成员遍历 */
        private String name;
    
        private int index;
    
        Color(String name, int index) {
          this.name = name;
          this.index = index;
        }
    
        @Override
        public String toString() {
          return this.index + "-" + this.name;
        }
      }
    
      public static void main(String[] args) {
        System.out.println(Color.RED.toString());
      }
    }

4:EnumMap 与 EnumSet

1:EnumMap 类
  • 定义:HashMap只能节后同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以 EnumMap 使用数组来存放与枚举类型对应的值,使得 EnumMap 的效率非常高。
  1. EnumMap示例。枚举类型DataBaseType存放支持所有的数据库类型。

    public class EnumMapTest {
      public enum DataBaseType {
        MYSQUORACLE,
        DB2,
        MYSQL,
        ORACLE,
        SQLSERVER
      }
      // 某类中定义的获取数据库URL的方法以及EnumMap的声明
      private EnumMap<DataBaseType, String> urls = new EnumMap<>(DataBaseType.class);
    
      public void DataBaseInfo() {
        urls.put(DataBaseType.DB2, "jdbc:db2://localhost:5000/sample");
        urls.put(DataBaseType.MYSQL, "jdbc:mysql://localhost/mydb");
        urls.put(DataBaseType.ORACLE, "jdbc:oracle:thin:@localhost:1521:sample");
        urls.put(DataBaseType.SQLSERVER, "jdbc:microsoft:sqlserver://sql:1433;Database=mydb");
      }
      // 根据不同的数据库类型,返回对应的URL
      // @param type DataBaseType 枚举类新实例
      // @return
      public String getURL(DataBaseType type) {
        return this.urls.get(type);
      }
    }
    • 使用 EnumMap 可以很方便地为枚举类型在不同的环境中绑定到不同的值上
2:EnumSet 类
  • 定义:EnumSet时枚举类型的高性能Set实现,它要求放入它的枚举常量必须属于同一枚举类型

    EnumSet类的常用方法
    方法名称 描述
    allOf(Class element type) 创建一个包含指定枚举类型中所有枚举成员的 EnumSet 对象
    complementOf(EnumSet s) 创建一个与指定 EnumSet 对象 s 相同的枚举类型 EnumSet 对象, 并包含所有 s 中未包含的枚举成员
    copyOf(EnumSet s) 创建一个与指定 EnumSet 对象 s 相同的枚举类型 EnumSet 对象, 并与 s 包含相同的枚举成员
    noneOf(<Class elementType) 创建指定枚举类型的空 EnumSet 对象
    of(E first,e…rest) 创建包含指定枚举成员的 EnumSet 对象
    range(E from ,E to) 创建一个 EnumSet 对象,该对象包含了 from 到 to 之间的所有枚 举成员

    EnumSet 作为 Set 接口实现,它支持对包含的枚举常量的遍历

    for(Operation op:EnumSet.range(Operation.PLUS,Operation.MULTIPLY)) {
        doSomeThing(op);
    }

12):一对多关系示例(学校、学生)

  1. 定义学生类

    public class Student {
      /** student类 */
      private String name;
      /** name属性 */
      private int age;
      /** 应该学生属于一个学校 */
      private School school;
    
      public Student(String name, int age) {
        this.setName(name);
        this.setAge(age);
      }
    
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public int getAge() {
        return age;
      }
    
      public void setAge(int age) {
        this.age = age;
      }
    
      public School getSchool() {
        return school;
      }
    
      public void setSchool(School school) {
        this.school = school;
      }
    
      @Override
      public String toString() {
        return "学生姓名:" + this.name + ":年龄:" + this.age;
      }
    }
  2. 定义学校类

    public class School {
      private String name;
      /** 一个学校有多少个学生 */
      private List<Student> allStudents;
    
      public School() {
        // 实例化list集合
        this.allStudents = new ArrayList<Student>();
      }
    
      public School(String name) {
        this();
        this.setName(name);
      }
    
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public List<Student> getAllStudents() {
        return allStudents;
      }
    
      public void setAllStudents(List<Student> allStudents) {
        this.allStudents = allStudents;
      }
    
      @Override
      public String toString() {
        // 重写toString()方法
        return "学校名称:" + this.name;
      }
    }
  3. 测试输出

    public class Test {
      public static void main(String[] args) {
        // 实例化学校对象
        School sch = new School("清华大学");
        // 实例化学生对象
        Student s1 = new Student("张三", 21);
        Student s2 = new Student("李四", 22);
        Student s3 = new Student("王五", 23);
        // 在学校中加入学生
        sch.getAllStudents().add(s1);
        sch.getAllStudents().add(s2);
        sch.getAllStudents().add(s3);
        // 一个学生属于一个学校
        s1.setSchool(sch);
        s2.setSchool(sch);
        s3.setSchool(sch);
        // 输出学校信息
        System.out.println(sch);
        // 实例化Iterator对象,输出全部的学生信息
        Iterator<Student> ite = sch.getAllStudents().iterator();
        while (ite.hasNext()) {
          System.out.println("\t" + ite.next());
        }
      }
    }

13):多对多关系示例(课程、学生)

  1. 定义学生类

    public class Student {
      private String name;
      private int age;
      /** 定义集合保存全部课程 */
      private List<Course> allCourses;
    
      public Student() {
        // 实例化List集合
        this.allCourses = new ArrayList<>();
      }
    
      public Student(String name, int age) {
        this();
        this.setName(name);
        this.setAge(age);
      }
    
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public int getAge() {
        return age;
      }
    
      public void setAge(int age) {
        this.age = age;
      }
    
      public List<Course> getAllCourses() {
        return allCourses;
      }
    
      public void setAllCourses(List<Course> allCourses) {
        this.allCourses = allCourses;
      }
    
      @Override
      public String toString() {
        return "学生姓名:" + this.name + ":年龄:" + this.age;
      }
    }
  2. 定义课程类

    public class Course {
      private String name;
      private int credit;
      /** 定义集合保存多个学生 */
      private List<Student> allStudents;
    
      public Course() {
        this.allStudents = new ArrayList<>();
      }
    
      public Course(String name, int credit) {
        this();
        this.setName(name);
        this.setCredit(credit);
      }
    
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public int getCredit() {
        return credit;
      }
    
      public void setCredit(int credit) {
        this.credit = credit;
      }
    
      public List<Student> getAllStudents() {
        return allStudents;
      }
    
      public void setAllStudents(List<Student> alStudents) {
        this.allStudents = alStudents;
      }
    
      @Override
      public String toString() {
        return "课程名称" + this.name + ";课程学分" + this.credit;
      }
    }
  3. 测试输出

    public class Test {
      public static void main(String[] args) {
        // 实例化课程对象
        Course c1 = new Course("英语", 3);
        Course c2 = new Course("计算机", 5);
        // 实例化学生对象
        Student s1 = new Student("张三", 20);
        Student s2 = new Student("李四", 21);
        Student s3 = new Student("王五", 22);
        Student s4 = new Student("赵六", 23);
        Student s5 = new Student("孙七", 24);
        Student s6 = new Student("钱八", 25);
        // 第一门课程有3个人参加,向课程中增加3个学生信息,同时向学生中增加课程信息
        c1.getAllStudents().add(s1);
        c1.getAllStudents().add(s2);
        c1.getAllStudents().add(s6);
        s1.getAllCourses().add(c1);
        s2.getAllCourses().add(c1);
        s6.getAllCourses().add(c1);
        // 第二门课程有6个人参加,向课程中增加6个学生信息,同时向学生中添加课程信息
        // 向课程中增加学生信息
        c2.getAllStudents().add(s1);
        c2.getAllStudents().add(s2);
        c2.getAllStudents().add(s3);
        c2.getAllStudents().add(s4);
        c2.getAllStudents().add(s5);
        c2.getAllStudents().add(s6);
        // 像学生中增加课程信息
        s1.getAllCourses().add(c2);
        s2.getAllCourses().add(c2);
        s3.getAllCourses().add(c2);
        s4.getAllCourses().add(c2);
        s5.getAllCourses().add(c2);
        s6.getAllCourses().add(c2);
        // 输出一门课程的信息,观察一门课程有多少个学生参加
        // 输出第一门课程
        System.out.println(c1);
        Iterator<Student> iter1 = c1.getAllStudents().iterator();
        // 迭代输出
        while (iter1.hasNext()) {
          Student s = iter1.next();
          System.out.println("\t" + s);
        }
        // 输出一个学生参加的课程信息,观察有多少门课程
        System.out.println(s6);
        Iterator<Course> iter2 = s6.getAllCourses().iterator();
        while (iter2.hasNext()) {
          // 取得所参加的课程
          Course c = iter2.next();
          // 输出课程信息
          System.out.println("\t" + c);
        }
      }
    }

14):模仿斗地主洗牌发牌小游戏

  • 步骤如👇:
    1. 准备牌:ArrayList集合存储,for循环遍历实现。
    2. 洗牌:使用Collections的shuffle(arrayBox),方法刷新集合中的顺序
    3. 发牌:使用for循环遍历ArrayList中的51张牌,然后用索引值对3取余,余数为0,1,2然后随机分配牌
    4. 看牌:直接输出集合
  1. List方式

    public class Main {
      public static void main(String[] args) {
        // 1.准备牌
        String[] arr1 = {"♠", "♥", "♦", "♣"};
        String[] arr2 = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
        List<String> arrayBox = new ArrayList<>();
        for (String s : arr1) {
          for (String value : arr2) {
            arrayBox.add(s + value);
          }
        }
        arrayBox.add("大王");
        arrayBox.add("小王");
        // 3个人斗地主,分别为张三、李四、王五
        List<String> 张三 = new ArrayList<>();
        List<String> 李四 = new ArrayList<>();
        List<String> 王五 = new ArrayList<>();
        // 2.发牌
        Collections.shuffle(arrayBox);
        // 3.发牌
        for (int i = 0; i < arrayBox.size(); i++) {
          if (i % 3 == 0) {
            张三.add(arrayBox.get(i));
          } else if (i % 3 == 1) {
            李四.add(arrayBox.get(i));
          } else {
            王五.add(arrayBox.get(i));
          }
        }
        // 4.看牌
        pushBoss();
        System.out.println("张三:" + 张三);
        System.out.println("李四:" + 李四);
        System.out.println("王五:" + 王五);
        System.out.print("底牌:[");
        for (int i = 1; i < 4; i++) {
          System.out.print(arrayBox.get(arrayBox.size() - i));
          if (i < 3) {
            System.out.print(",");
          }
        }
        System.out.print("]");
      }
      /** 随机地主 */
      public static void pushBoss() {
        List<String> players = new ArrayList<>();
        players.add("张三");
        players.add("李四");
        players.add("王五");
        Random r = new Random();
        int bossIndex = r.nextInt(3);
        String boss = players.get(bossIndex);
        System.out.println("此局地主是:" + boss);
      }
    }
  2. Map方式

    public class MainMap {
      public static void main(String[] args) {
        // 1.准备牌
        String[] arr1 = {"♠", "♥", "♦", "♣"};
        String[] arr2 = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
        // 定义Map集合存放索引和牌
        HashMap<Integer, String> pokerMap = new HashMap<>();
        // 定义List集合存储索引(索引为0-53)
        List<Integer> indexList = new ArrayList<>();
        // 定义索引值变量
        int index = 0;
        // 将扑克牌与索引建立对应关系放入Map和List集合中
        for (String num : arr2) {
          for (String color : arr1) {
            pokerMap.put(index, color + num);
            indexList.add(index);
            index++;
          }
        }
        pokerMap.put(index, "小王");
        indexList.add(index++);
        pokerMap.put(index, "大王");
        indexList.add(index);
        // 定义玩家
        TreeSet<Integer> zhangsan = new TreeSet<>();
        TreeSet<Integer> lisi = new TreeSet<>();
        TreeSet<Integer> wangwu = new TreeSet<>();
        TreeSet<Integer> buttoms = new TreeSet<>();
        // 2.洗牌
        Collections.shuffle(indexList);
        // 3.发牌
        for (int i = 0; i < indexList.size(); i++) {
          if (i >= indexList.size() - 3) {
            buttoms.add(indexList.get(i));
          } else if (i % 3 == 0) {
            zhangsan.add(indexList.get(i));
          } else if (i % 3 == 1) {
            lisi.add(indexList.get(i));
          } else {
            wangwu.add(indexList.get(i));
          }
        }
        // 4.看牌
        pushBoss();
        check(pokerMap, zhangsan, "张三");
        check(pokerMap, lisi, "李四");
        check(pokerMap, wangwu, "王五");
        check(pokerMap, buttoms, "底牌");
      }
    
      /**
       * 看牌
       *
       * @param pokerMap
       * @param player 玩家扑克牌对应索引集合
       * @param name 玩家昵称
       */
      public static void check(
          HashMap<Integer, String> pokerMap, TreeSet<Integer> player, String name) {
        // 查看手中的牌
        System.out.print(name + ":[");
        for (Integer i : player) {
          System.out.print(pokerMap.get(i) + " ");
        }
        System.out.println("]");
      }
      /** 随机地主 */
      public static void pushBoss() {
        List<String> players = new ArrayList<>();
        players.add("张三");
        players.add("李四");
        players.add("王五");
        Random r = new Random();
        int bossIndex = r.nextInt(3);
        String boss = players.get(bossIndex);
        System.out.println("此局地主是:" + boss);
      }
    }

十二:Java反射机制

1):反射机制基本概念

  • 该概念包含两个方面,编译器和运行期

    • 编译器是指把源码交给编译器编译成计算机可以执行的文件的过程。在 Java 中也就是把 Java 代码编成 class 文件的过程。编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如检查错误

    • 运行期是把编译后的文件交给计算机执行,直到程序运行结束。所谓运行期就把在磁盘中的代码放到内存中执行起来

    • Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。简单来说,反射机制指的是程序在运行时能够获取自身的信息。在 Java 中,只要给定类的名字,就可以通过反射机制来获得类的所有信息

  • 作用:Java 反射机制在服务器程序和中间件程序中得到了广泛运用。在服务器端,往往需要根据客户的请求,动态调用某一个对象的特定方法。此外,在 ORM 中间件的实现中,运用 Java 反射机制可以读取任意一个 JavaBean 的所有属性,或者给这些属性赋值

  • Java 反射机制主要提供了以下功能,这些功能都位于java.lang.reflect包。

    • 在运行时判断任意一个对象所属的类。
    • 在运行时构造任意一个类的对象。
    • 在运行时判断任意一个类所具有的成员变量和方法。
    • 在运行时调用任意一个对象的方法。
    • 生成动态代理
  • 要想知道一个类的属性和方法,必须先获取到该类的字节码文件对象。获取类的信息时,使用的就是 Class 类中的方法。所以先要获取到每一个字节码文件(.class)对应的 Class 类型的对象

  • 所有 Java 类均继承了 Object 类,在 Object 类中定义了一个 getClass() 方法,该方法返回同一个类型为 Class 的对象

    Class labelCls = label1.getClass();    // label1为 JLabel 类的对象
    • 利用 Class 类的对象 labelCls 可以访问 labelCls 对象的描述信息、JLabel 类的信息以及基类 Object 的信息
    反射可访问的常用信息
    类型 访问方法 返回值类型 说明
    包路径 getPackage() Package 对象 获取该类的存放路径
    类名称 getName() String 对象 获取该类的名称
    继承类 getSuperclass() Class 对象 获取该类继承的类
    实现接口 getlnterfaces() Class 型数组 获取该类实现的所有接口
    构造方法 getConstructors() Constructor 型数组 获取所有权限为 public 的构造方法
    getDeclaredContruectors() Constructor 对象 获取当前对象的所有构造方法
    方法 getMethods() Methods 型数组 获取所有权限为 public 的方法
    getDeclaredMethods() Methods 对象 获取当前对象的所有方法
    成员变量 getFields() Field 型数组 获取所有权限为 public 的成员变量
    getDeclareFileds() Field 对象 获取当前对象的所有成员变量
    内部类 getClasses() Class 型数组 获取所有权限为 public 的内部类
    getDeclaredClasses() Class 型数组 获取所有内部类
    内部类的声明类 getDeclaringClass() Class 对象 如果该类为内部类,则返回它的成员类,否则返回 null
    • 在调用 getFields() 和 getMethods() 方法时将会依次获取权限为 public 的字段和变量,然后将包含从超类中继承到的成员变量和方法。而通过 getDeclareFields() 和 getDeclareMethod() 只是获取在本类中定义的成员变量和方法
  • Java 反射机制的优缺点

  • 优点:

    • 能够运行时动态获取类的实例,大大提高系统的灵活性和扩展性。
    • 与 Java 动态编译相结合,可以实现无比强大的功能。
    • 对于 Java 这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象
  • 缺点:

    • 反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
    • 反射调用方法时可以忽略权限检查,获取这个类的私有方法和属性,因此可能会破坏类的封装性而导致安全问题。

2):反射机制API

1:java.lang.Class类

  • 定义:java.lang.Class 类是实现反射的关键所在,Class 类的一个实例表示 Java 的一种数据类型,包括类、接口、枚举、注解(Annotation)、数组、基本数据类型和 void。Class 没有公有的构造方法,Class 实例是由 JVM 在类加载时自动创建的

    // 1. 通过类型class静态变量
    Class clz1 = String.class;
    String str = "Hello";
    // 2. 通过对象的getClass()方法
    Class clz2 = str.getClass();
    • 每一种类型包括类和接口等,都有一个 class 静态变量可以获得 Class 实例。另外,每一个对象都有 getClass() 方法可以获得 Class 实例,该方法是由 Object 类提供的实例方法
    public class ReflectionTest01 {
      public static void main(String[] args) {
        // 获得Class实例
        // 1.通过类型class静态变量
        Class clz1 = String.class;
        String str = "Hello";
        // 2.通过对象的getClass();
        Class clz2 = str.getClass();
        // 获得int类型Class实例
        Class clz3 = int.class;
        // 获得Integer类型Class实例
        Class clz4 = Integer.class;
        System.out.println("clz2类名称:" + clz2.getName());
        System.out.println("clz2是否为接口:" + clz2.isInterface());
        System.out.println("clz2是否为数组对象:" + clz2.isArray());
        System.out.println("clz2父类名称:" + clz2.getSuperclass().getName());
        System.out.println("clz2是否为基本类型:" + clz2.isPrimitive());
        System.out.println("clz3是否为基本类型:" + clz3.isPrimitive());
        System.out.println("clz4是否为基本类型:" + clz4.isPrimitive());
      }
    }

2:java.lang.reflect包

  • java.lang.reflect 包提供了反射中用到类,主要的类说明如下:

    • Constructor 类:提供类的构造方法信息。
    • Field 类:提供类或接口中成员变量信息。
    • Method 类:提供类或接口成员方法信息。
    • Array 类:提供了动态创建和访问 Java 数组的方法。
    • Modifier 类:提供类和成员访问修饰符信息。
    public class ReflectTest02 {
      public static void main(String[] args) {
        try {
          // 动态加载xx类的运行是对象
          Class<?> c = Class.forName("java.lang.String");
          // 获取成员方法集合
          Method[] methods = c.getDeclaredMethods();
          // 遍历成员方法集合
          for (Method method : methods) {
            // 打印权限修饰符,如public、protected、private
            System.out.print(Modifier.toString(method.getModifiers()));
            // 打印返回值名称类型
            System.out.print(" " + method.getReturnType().getName() + " ");
            // 打印方法名称
            System.out.println(method.getName() + "();");
          }
        } catch (ClassNotFoundException e) {
          System.out.println("找不到指定类");
        }
      }
    }

3):通过反射访问构造方法

  • 为了能够动态获取对象构造方法的信息,首先需要通过以下方法之一创建一个Constructor类型的对象或者数组

    • getConstructors()
    • getConstructor(Class<?>…parameterTypes)
    • getDeclaredConstructors()
    • getDeclaredConstructor(Class<?>…parameterTypes)
  • 如果是访问指定的构造方法,需要根据该构造方法的入口参数的类型来访问,例如,访问一个入口参数类型依次为 int 和 String 类型的构造方法,下面的两种方式均可以实现

    objectClass.getDeclaredConstructor(int.class,String.class);
    objectClass.getDeclaredConstructor(new Class[]{int.class,String.class});
    Constructor
    方法名称 说明
    isVarArgs() 查看该构造方法是否允许带可变数量的参数,如果允许,返回 true,否则返回 false
    getParameterTypes() 按照声明顺序以 Class 数组的形式获取该构造方法各个参数的类型
    getExceptionTypes() 以 Class 数组的形式获取该构造方法可能抛出的异常类型
    newInstance(Object … initargs) 通过该构造方法利用指定参数创建一个该类型的对象,如果未设置参数则表示 采用默认无参的构造方法
    setAccessiable(boolean flag) 如果该构造方法的权限为 private,默认为不允许通过反射利用 netlnstance() 方法创建对象。如果先执行该方法,并将入口参数设置为 true,则允许创建对 象
    getModifiers() 获得可以解析出该构造方法所采用修饰符的整数
    • 通过 java.lang.reflect.Modifier 类可以解析出 getMocMers() 方法的返回值所表示的修饰符信息。在该类中提供了一系列用来解析的静态方法,既可以查看是否被指定的修饰符修饰,还可以字符串的形式获得所有修饰符。
    Modifier类的常用方法
    静态方法名称 说明
    isStatic(int mod) 如果使用 static 修饰符修饰则返回 true,否则返回 false
    isPublic(int mod) 如果使用 public 修饰符修饰则返回 true,否则返回 false
    isProtected(int mod) 如果使用 protected 修饰符修饰则返回 true,否则返回 false
    isPrivate(int mod) 如果使用 private 修饰符修饰则返回 true,否则返回 false
    isFinal(int mod) 如果使用 final 修饰符修饰则返回 true,否则返回 false
    toString(int mod) 以字符串形式返回所有修饰符

    判断con所代表的构造方法是否被public修饰,以及一字符串形式获取构造方法的所有修饰符

    int modifiers = con.getModifiers();    // 获取构造方法的修饰符整数
    boolean isPublic = Modifier.isPublic(modifiers);    // 判断修饰符整数是否为public 
    string allModifiers = Modifier.toString(modifiers);
  • 例1 (演示Constructor)

    1. 图书信息类,3个构造方法

      public class Book {
        /** 图书编号和价格 */
        String name;
        /** 无参构造方法 */
        int id, price;
      
        private Book() {}
        /** 带两个参数的构造方法 */
        protected Book(String _name, int _id) {
          this.name = _name;
          this.id = _id;
        }
        /** 带可变参数的构造方法 */
        public Book(String... strings) throws NumberFormatException {
          if (0 < strings.length) {
            id = Integer.parseInt(strings[0]);
          }
          if (1 < strings.length) {
            price = Integer.parseInt(strings[1]);
          }
        }
        /** 输出图书信息 */
        public void print() {
          System.out.println("name=" + name);
          System.out.println("id=" + id);
          System.out.println("price=" + price);
        }
      }
    2. 测试

      public class Test {
        public static void main(String[] args) {
          // 获取动态类Book
          Class book = Book.class;
          // 获取Book类的所有构造方法
          Constructor[] declaredContructors = book.getDeclaredConstructors();
          // 遍历所有构造方法
          for (int i = 0; i < declaredContructors.length; i++) {
            Constructor con = declaredContructors[i];
            // 判断构造方法的参数是否可变
            System.out.println("查看是否允许带可变数量的参数:" + con.isVarArgs());
            System.out.println("该构造方法的入口参数类型依次为:");
            // 获取所有参数类型
            Class[] parameterTypes = con.getParameterTypes();
            for (Class parameterType : parameterTypes) {
              System.out.println(" " + parameterType);
            }
            System.out.println("该构造方法可能拋出的异常类型为:");
            // 获取所有可能拋出的异常类型
            Class[] exceptionTypes = con.getExceptionTypes();
            for (int j = 0; j < exceptionTypes.length; j++) {
              System.out.println(" " + parameterTypes[j]);
            }
            // 创建一个未实例化的Book类实例
            Book book1 = null;
            while (book1 == null) {
              // 如果该成员变量的访问权限为private,则拋出异常
              try {
                if (i == 1) {
                  // 通过执行带两个参数的构造方法实例化book1
                  book1 = (Book) con.newInstance("Java 教程", 10);
                } else if (i == 2) {
                  // 通过执行默认构造方法实例化book1
                  book1 = (Book) con.newInstance();
                } else {
                  // 通过执行可变数量参数的构造方法实例化book1
                  Object[] parameters = new Object[] {new String[] {"100", "200"}};
                  book1 = (Book) con.newInstance(parameters);
                }
              } catch (Exception e) {
                System.out.println("在创建对象时拋出异常,下面执行 setAccessible() 方法");
                // 设置允许访问 private 成员
                con.setAccessible(true);
              }
            }
            book1.print();
            System.out.println("=============================\n");
          }
        }
      }

4):通过反射执行方法

  • 动态获取一个对象方法的信息,首先需通过以下方法之一创建一个Method类型的对象或者数组

    • getMethods()
    • getMethods(String name,Class<?> …parameterTypes)
    • getDeclaredMethods()
    • getDeclaredMethods(String name,Class<?>…parameterTypes)
  • 如果是访问指定的构造方法,需要根据该方法的入口参数的类型来访问。例如,访问一个名称为 max,入口参数类型依次为 int 和 String 类型的方法

    objectClass.getDeclaredConstructor("max",int.class,String.class);
    objectClass.getDeclaredConstructor("max",new Class[]{int.class,String.class});
    Method类的常用方法
    静态方法名称 说明
    getName() 获取该方法的名称
    getParameterType() 按照声明顺序以 Class 数组的形式返回该方法各个参数的类型
    getReturnType() 以 Class 对象的形式获得该方法的返回值类型
    getExceptionTypes() 以 Class 数组的形式获得该方法可能抛出的异常类型
    invoke(Object obj,Object…args) 利用 args 参数执行指定对象 obj 中的该方法,返回值为 Object 类型
    isVarArgs() 查看该方法是否允许带有可变数量的参数,如果允许返回 true,否则返回 false
    getModifiers() 获得可以解析出该方法所采用修饰符的整数
  • 例 (演示Method类)

    1. 创建Book1类,4个不同作用域方法

      public class Book1 {
        /** static作用域方法 */
        static void staticMethod() {
          System.out.println("执行staticMethod()方法");
        }
        /** public作用域方法 */
        public int publicMethod(int i) {
          System.out.println("执行publicMethod()方法");
          return 100 + i;
        }
        /** protected作用域方法 */
        protected int protectedMethod(String s, int i) throws NumberFormatException {
          System.out.println("执行protectedMethod()方法");
          return Integer.parseInt(s) + i;
        }
        /** private作用域方法 */
        private String privateMethod(String... strings) {
          System.out.println("执行privateMethod()方法");
          StringBuffer sb = new StringBuffer();
          for (int i = 0; i < sb.length(); i++) {
            sb.append(strings[i]);
          }
          return sb.toString();
        }
      }
    2. 测试

      public class Test02 {
        public static void main(String[] args) {
          // 获取动态Book1
          Book1 book = new Book1();
          Class<? extends Book1> class1 = book.getClass();
          // 获取Book1类的所有方法
          Method[] declaredMethod = class1.getDeclaredMethods();
          for (int i = 0; i < declaredMethod.length; i++) {
            Method method = declaredMethod[i];
            System.out.println("方法名称为:" + method.getName());
            System.out.println("方法是否带有可变数量的参数:" + method.isVarArgs());
            System.out.println("方法的参数类型依次为:");
            // 获取所有参数类型
            Class[] methodTypes = method.getParameterTypes();
            for (Class methodType : methodTypes) {
              System.out.println(" " + methodType);
            }
            // 获取返回值类型
            System.out.println("方法的返回值类型为:" + method.getReturnType());
            System.out.println("方法可能抛出的异常类型有:");
            // 获取所有可能抛出的异常
            Class[] methodExceptions = method.getExceptionTypes();
            for (Class methodException : methodExceptions) {
              System.out.println(" " + methodException);
            }
            boolean isTurn = true;
            while (isTurn) {
              // 如果该成员变量的访问权限为private,则抛出异常
              try {
                isTurn = false;
                // 调用没有参数的方法
                if (method.getName().equals("staticMethod")) {
                  method.invoke(book);
                  // 调用一个参数的方法
                } else if (method.getName().equals("publicMethod")) {
                  System.out.println("publicMethod(10)的返回值为:" + method.invoke(book, 10));
                  // 调用两个参数的方法
                } else if (method.getName().equals("protectedMethod")) {
                  System.out.println("protectedMethod(\"10\",15)的返回值为:" + method.invoke(book, "10", 15));
                  // 调用可变数量参数的方法
                } else if (method.getName().equals("privateMethod")) {
                  Object[] parameters = new Object[] {new String[] {"J", "A", "V", "A"}};
                  System.out.println("privateMethod()的返回值为:" + method.invoke(book, parameters));
                }
              } catch (Exception e) {
                System.out.println("在设置成员变量值时抛出异常,下面执行setAccessible()方法");
                // 设置为允许访问private方法
                method.setAccessible(true);
                isTurn = true;
              }
            }
            System.out.println("=============================\n");
          }
        }
      }

5):通过反射访问成员变量

  • 通过下列任意一个方法访问成员变量时将返回Field类型的对象或数组

    • getFields()
    • getField(String name)
    • getDeclaredFields()
    • getDeclasredField(String name)
  • 返回的 Field 对象代表一个成员变量。例如,要访问一个名称为 price 的成员变量

    object.getDeciaredField("price")
    Fidle类的常用方法
    方法名称 说明
    getName() 获得该成员变量的名称
    getType() 获取表示该成员变量的 Class 对象
    get(Object obj) 获得指定对象 obj 中成员变量的值,返回值为 Object 类型
    set(Object obj, Object value) 将指定对象 obj 中成员变量的值设置为 value
    getlnt(0bject obj) 获得指定对象 obj 中成员类型为 int 的成员变量的值
    setlnt(0bject obj, int i) 将指定对象 obj 中成员变量的值设置为 i
    setFloat(Object obj, float f) 将指定对象 obj 中成员变量的值设置为 f
    getBoolean(Object obj) 获得指定对象 obj 中成员类型为 boolean 的成员变量的值
    setBoolean(Object obj, boolean b) 将指定对象 obj 中成员变量的值设置为 b
    getFloat(Object obj) 获得指定对象 obj 中成员类型为 float 的成员变量的值
    setAccessible(boolean flag) 此方法可以设置是否忽略权限直接访问 private 等私有权限的成员变量
    getModifiers() 获得可以解析出该方法所采用修饰符的整数
  • 例 (演示Field)

    1. 创建Book2类,不同类型的成员

      public class Book2 {
        String name;
        public int id;
        private float price;
        protected boolean isLoan;
      }
    2. 测试

      public class Test03 {
        public static void main(String[] args) {
          Book2 book = new Book2();
          // 获取动态类Book2
          Class class1 = book.getClass();
          // 获取Book2类的所有成员
          Field[] declaredFields = class1.getDeclaredFields();
          // 遍历所有的成员
          for (Field field : declaredFields) {
            // 获取类中的成员变量
            System.out.println("成员名称为:" + field.getName());
            Class fieldType = field.getType();
            System.out.println("成员类型为:" + fieldType);
            boolean isTurn = true;
            while (isTurn) {
              try {
                // 如果该成员变量的访问权限为private,则抛出异常
                isTurn = false;
                System.out.println("修改前成员的值为:" + field.get(book));
                // 判断成员类型是否为int
                if (fieldType.equals(int.class)) {
                  System.out.println("利用setInt()方法修改成员的值");
                  field.setInt(book, 100);
                } else if (fieldType.equals(float.class)) {
                  // 判断成员变量类型是否为float
                  System.out.println("利用setFloat()方法修改成员的值");
                  field.setFloat(book, 29.815f);
                } else if (fieldType.equals(boolean.class)) {
                  // 判断成员变量是否为boolean
                  System.out.println("利用setBoolean()方法修改成员的值");
                  field.setBoolean(book, true);
                } else {
                  System.out.println("利用set()方法修改成员的值");
                  field.set(book, "Java编程");
                }
                System.out.println("修改后成员的值为:" + field.get(book));
              } catch (Exception e) {
                System.out.println("在设置成员变量值时抛出异常,下面执行setAccessible()方法");
                field.setAccessible(true);
                isTurn = true;
              }
            }
            System.out.println("=============================\n");
          }
        }
      }

6):在远程方法中使用反射机制

  • 如何在客户端通过远程方法调用服务器端的方法
  1. 假设服务端有一个HelloService接口,getTime()和echo()方法

    import java.util.Date;
    
    public interface HelloService {
      public String echoed(String msg);
    
      public Date getTime();
    }
  2. 在服务器端创建一个HelloServiceImple类实现HelloService接口

    public class HelloServiceImpl implements HelloService {
      @Override
      public String echoed(String msg) {
        return "echo:" + msg;
      }
    
      @Override
      public Date getTime() {
        return new Date();
      }
    }
  3. 客户端如何调用:客户端需要把调用的方法名、方法参数类型、方法参数值,以及方法所属类型或接口发送给服务器端。服务器端在返回值。(Call类实现发送)

    public class Call implements Serializable {
      private static final long serialVersionUID = 6659953547331194808L;
      /** 类名或接口名 */
      private String className;
      /** 方法名 */
      private String methodName;
      /** 方法参数类型 */
      private Class[] parameterTypes;
      /** 方法参数值 */
      private Object[] params;
      /** 表示方法的执行结果 如果方法正常执行,则result为方法返回值,如果方法抛出异常,那么result为该异常 */
      private Object result;
    
      public Call() {}
    
      public Call(String className, String methodName, Class[] parameterTypes, Object[] params) {
        this.className = className;
        this.methodName = methodName;
        this.parameterTypes = parameterTypes;
        this.params = params;
      }
    
      public String getClassName() {
        return className;
      }
    
      public void setClassName(String className) {
        this.className = className;
      }
    
      public String getMethodName() {
        return methodName;
      }
    
      public void setMethodName(String methodName) {
        this.methodName = methodName;
      }
    
      public Class[] getParameterTypes() {
        return parameterTypes;
      }
    
      public void setParameterTypes(Class[] parameterTypes) {
        this.parameterTypes = parameterTypes;
      }
    
      public Object[] getParams() {
        return params;
      }
    
      public void setParams(Object[] params) {
        this.params = params;
      }
    
      public Object getResult() {
        return result;
      }
    
      public void setResult(Object result) {
        this.result = result;
      }
    
      @Override
      public String toString() {
        return "className=" + className + "methodName=" + methodName;
      }
    }

    假设客户端为 SimpleClient,服务器端为 SimpleServer。SimpleClient 调用 SimpleServer 的 HelloServiceImpl 对象中 echo() 方法的流程如下:

    1. SimpleClient 创建一个 Call 对象,它包含调用 HelloService 接口的 echo() 方法的信息。
    2. SimpleClient 通过对象输出流把 Call 对象发送给 SimpleServer。
    3. SimpleServer 通过对象输入流读取 Call 对象,运用反射机制调用 HelloServiceImpl 对象的 echo() 方法,把 echo() 方法的执行结果保存到 Call 对象中。
    4. SimpleServer 通过对象输出流把包含方法执行结果的 Call 对象发送给 SimpleClient。
    5. SimpleClient 通过对象输入流读取 Call 对象,从中获得方法执行结果。
  4. SimpleClient类

    public class SimpleClient {
      public void invoke() throws Exception {
        Socket socket = new Socket("localhost", 8000);
        OutputStream out = socket.getOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(out);
        InputStream in = socket.getInputStream();
        ObjectInputStream ois = new ObjectInputStream(in);
        // 创建一个远程调用对象
        Call call =
            new Call("chl2.HelloService", "echo", new Class[] {String.class}, new Object[] {"Java"});
        // 向服务器发送Call对象
        oos.writeObject(call);
        // 接收包含了方法执行结果的Call对象
        call = (Call) ois.readObject();
        System.out.println(call.getResult());
        ois.close();
        oos.close();
        socket.close();
      }
    
      public static void main(String[] args) throws Exception {
        new SimpleClient().invoke();
      }
    }
  5. SimpleServer类

    public class SimpleServer {
      // 存放远程对象的缓存
      private Map remoteObjects = new HashMap();
      /** 把一个远程对象放到缓存中 */
      public void register(String className, Object remoteObject) {
        remoteObjects.put(className, remoteObject);
      }
    
      public void service() throws Exception {
        ServerSocket serverSocket = new ServerSocket(8000);
        System.out.println("服务器启动.");
        while (true) {
          Socket socket = serverSocket.accept();
          InputStream in = socket.getInputStream();
          ObjectInputStream ois = new ObjectInputStream(in);
          OutputStream out = socket.getOutputStream();
          ObjectOutputStream oos = new ObjectOutputStream(out);
          // 接收客户发送的Call对象
          Call call = (Call) ois.readObject();
          System.out.println(call);
          // 调用相关对象的方法
          call = invoke(call);
          // 向客户发送包含了执行结果的Call对象
          oos.writeObject(call);
          ois.close();
          oos.close();
          socket.close();
        }
      }
    
      public Call invoke(Call call) {
        Object result = null;
        try {
          String className = call.getClassName();
          String methodName = call.getMethodName();
          Object[] params = call.getParams();
          Class classType = Class.forName(className);
          Class[] paramTypes = call.getParameterTypes();
          Method method = classType.getMethod(methodName, paramTypes);
          // 从缓存中取出相关的远程对象
          Object remoteObject = remoteObjects.get(className);
          if (remoteObject == null) {
            throw new Exception(className + "的远程对象不存在");
          } else {
            result = method.invoke(remoteObject, params);
          }
        } catch (Exception e) {
          result = e;
        }
        // 设置方法执行结果
        call.setResult(result);
        return call;
      }
    
      public static void main(String args[]) throws Exception {
        SimpleServer server = new SimpleServer();
        // 把事先创建的HelloServiceImpl对象加入到服务器的缓存中
        server.register("ch13.HelloService", new HelloServiceImpl());
        server.service();
      }
    }
    • 由于这是一个网络程序,首先需要运行服务器端 SimpleServer,然后再运行客户端 SimpleClient。运行结果是在客户端看到输出“echoJava”,这个结果是服务器端执行 HelloServicelmpl 对象的 echo() 方法的返回值

7):反射方法操作数组

  • 在 java.lang.reflect 包下提供了一个 Array 类,Array 对象可以代表所有的数组。程序可以通过使用 Array 来动态地创建数组,操作数组元素等
  • Array 提供了如下几类方法。
    • static Object newInstance(Class<?> componentType, int…length):创建一个具有指定的元素类型、指定维度的新数组。
    • static xxx getXxx(Object array, int index):返回 array 数组中第 index 个元素。其中 xxx 是各种基本数据类型,如果数组元素是引用类型,则该方法变为 get(Object array, int index)。
    • static void setXxx(Object array, int index, xxx val):将 array 数组中第 index 个元素的值设为 val.
      • 其中 xxx 是各种基本数据类型,如果数组元素是引用类型,则该方法变成 set(Object array, int index, Object val)
  1. Array生成数组,为指定数组元素赋值

    public class ArrayTest1 {
      public static void main(String[] args) {
        try {
          // 创建一个元素为String,长度为10的数组
          Object arr = Array.newInstance(String.class, 10);
          // 依次为arr数组中index5、6的元素赋值
          Array.set(arr, 5, "你好1");
          Array.set(arr, 6, "你好2");
          // 依次取出arr数组中的index为5、6的元素的值
          Object course1 = Array.get(arr, 5);
          Object course2 = Array.get(arr, 6);
          // 输出arr数组中index为5、6的元素
          System.out.println(course1);
          System.out.println(course2);
        } catch (Throwable e) {
          System.out.println(e);
        }
      }
    }
    • 通过 Array 创建数组,为数组元素设置值,访问数组元素的值的示例代码,程序通过使用 Array 就可以动态地创建并操作数组
  2. Array创建三维数组

    public class ArrayTest2 {
      public static void main(String[] args) {
        /*
         * 创建一个三维数组
         * 三维数组也是一维数组,是数组元素是二维数组的一维数组,因此可以认为arr是长度为3的一维数组
         */
        Object arr = Array.newInstance(String.class, 3, 4, 10);
        // 获取arr数组中index为2的元素,该元素应该是二维数组
        Object arrObj = Array.get(arr, 2);
        // 使用Array为二维数组的数组元素赋值,二维数组的数组元素是一维数组
        // 所以传入Array的set()方法的第三个参数是一维数组
        Array.set(arrObj, 2, new String[] {"C语言中文网Java教程", "C语言中文网C语言教程"});
        // 获取arrObj数组中index为3的元素,该元素应该是一维数组
        Object anArr = Array.get(arrObj, 3);
        Array.set(anArr, 8, "C语言中文网Python教程");
        // 将arr强制类型转换为三维数组
        String[][][] cast = (String[][][]) arr;
        // 获取cast三维数组中指定元素的值
        System.out.println(cast[2][3][8]);
        System.out.println(cast[2][2][0]);
        System.out.println(cast[2][2][1]);
      }
    }
    • 使用 Array 创建了一个三维数组,程序中较难理解的地方是第 12 行代码,使用 Array 为 arrObj 的指定元素赋值,相当于为二维数组的元素赋值。由于二维数组的元素是一维数组,所以程序传入的参数是一个一维数组对象

8):反射操作泛型

1:泛型和 Class 类

  • 使用Class 泛型可以避免强制类型转换。例如,下面提供一个简单的对象工厂,该对象工厂可以根据指定类来提供该类的实例

    public class ObjectFactory {
      public static Object getInstance(String clsName) {
        try {
          // 创建指定类对应的Class对象
          Class cls = Class.forName(clsName);
          return cls.newInstance();
        } catch (Exception e) {
          e.printStackTrace();
          return null;
        }
      }
    }
    • 第 5 、7 行代码根据指定的字符串类型创建了一个新对象,但这个对象的类型是 Object,因此当需要使用 ObjectFactory 的 getInstance() 方法来创建对象时

      // 获取实例后需要强制类型转换
      Date d = (Date)ObjectFactory.getInstance("java.util.Date");
      JFrame f = (JFrame)ObjectFactory .getInstance("java.util.Date");
    • 编译以上代码会抛出ClassCastException(强制类型转换异常),因为程序试图将一个 Date 对象转换成 JFrame 对象。

  • ObjectFactory工厂类修改为泛型后的Class,则避免

    public class ObjectFactory2 {
      public static <T> T getInstance(Class<T> cls) {
        try {
          return cls.newInstance();
        } catch (Exception e) {
          e.printStackTrace();
          return null;
        }
      }
    
      public static void main(String[] args) {
        // 获取失利后无须类型转换
        Date d = ObjectFactory2.getInstance(Date.class);
        JFrame f = ObjectFactory2.getInstance(JFrame.class);
        System.out.println(d);
        System.out.println(f);
      }
    }
  • 使用Array类创建数组是,代码如下:

    // 使用Array的newInstance方法来创建一个数组
    Object arr = Array.newInstance(String.class, 10);
    • 以上代码的使用并不方便,因为 newInstance() 方法返回的确实是一个 String[] 数组,而不是简单的 Object 对象。如果需要将 arr 对象当成 String[] 数组使用,则必须使用强制类型转换,但是这是不安全的操作

    • Array 的 newInstance() 方法签名(方法签名由方法名称和参数列表组成)为如下形式:

      public static Object newInstance(Class<?> componentType, int... dimensions)
    • 在这个方法签名中使用了 Class<?> 泛型,但并没有真正利用这个泛型。如果将该方法签名改为如下形式:

      public static <T> T[] newInstance(Class<T> componentType, int length)
      • 此方法只能创建一维数组.
  • Array的newInstance()方法封装

    public class CrazyitArray {
      /** 对Array的newInstance方法封装 */
      @SuppressWarnings("unchecked")
      public static <T> T[] newInstance(Class<T> componentType, int length) {
        return (T[]) Array.newInstance(componentType, length);
      }
    
      public static void main(String[] args) {
        /* 使用CrazyitArray的newInstance()创建一维数组 */
        String[] arr = CrazyitArray.newInstance(String.class, 10);
        /*
         使用 CrazyitArray 的 newInstance()创建二维数组
         在这种情况下,只要设置数组元素的类型是int[]即可
        */
        int[][] intArr = CrazyitArray.newInstance(int[].class, 5);
        arr[5] = "C语言中文网Java教程";
        /*
         intArr是二维数组,初始化该数组的第二个数组元素
         二维数组的元素必须是一维数组
        */
        intArr[1] = new int[] {23, 12};
        System.out.println(arr[5]);
        System.out.println(intArr[1][1]);
      }
    }

    @SuppressWarnings(“unchecked”) 告诉编译器忽略 unchecked 警告信息,如使用 List,ArrayList 等未进行参数化产生的警告信息。程序在第 5 行代码处将会有一个 unchecked 编译警告,所以程序使用了 @SuppressWarnings 来抑制这个警告信息

2:使用反射来获取泛型信息

  • 通过指定类对应的 Class 对象,可以获得该类里包含的所有成员变量,不管该成员变量是使用 private 修饰,还是使用 public 修饰。获得了成员变量对应的 Field 对象后,就可以很容易地获得该成员变量的数据类型

    // 获取成员变量 f 的类型
    Class<?> a = f.getType();
    • 但这种方式只对普通类型的成员变量有效。如果该成员变量的类型是有泛型类型的类型,如 Map<String, Integer>类型,则不能准确地得到该成员变量的泛型参数
  • 为了获得指定成员变量的泛型类型

    // 获得成员变量f的泛型类型
    Type gType = f.getGenericType();

    将 Type 对象强制类型转换为 ParameterizedType 对象,ParameterizedType 代表被参数化的类型,也就是增加了泛型限制的类型。ParameterizedType 类提供了如下两个方法:

    • getRawType():返回没有泛型信息的原始类型
    • getActualTypeArguments():返回泛型参数的类型
    public class GenericTest {
      private Map<String, Integer> score;
    
      public static void main(String[] args) throws Exception {
        Class<GenericTest> clazz = GenericTest.class;
        Field f = clazz.getDeclaredField("score");
        // 直接使用getType()取出类型只对普通类型的成员变量有效
        Class<?> a = f.getType();
        // 仅输出java.util.Map
        System.out.println("score的类型是:" + a);
        // 获取成员变量f的泛型类型
        Type gType = f.getGenericType();
        // 如果gType类型是ParameterizedType对象
        if (gType instanceof ParameterizedType) {
          // 强制类型转换
          ParameterizedType pType = (ParameterizedType) gType;
          // 获取原始类型
          Type rType = pType.getRawType();
          System.out.println("原始类型是:" + rType);
          // 取得泛型类型的泛型参数
          Type[] tArgs = pType.getActualTypeArguments();
          System.out.println("泛型信息是:");
          for (int i = 0; i < tArgs.length; i++) {
            System.out.println("第" + i + "个泛型类型是:" + tArgs[i]);
          }
        } else {
          System.out.println("获取泛型类型出错!");
        }
      }
    }
    • 使用 getType() 方法只能获取普通类型的成员变量的数据类型。对于增加了泛型的成员变量,应该使用 getGenericType() 方法来取得其类型

    Type 也是 java.lang.reflect 包下的一个接口,该接口代表所有类型的公共高级接口,Class 是 Type 接口的实现类。Type 包括原始类型、参数化类型、数组类型、类型变量和基本类型等

十三、输入/输出流

1):流的概念

  • 根据数据流向的不同,可以分为输入(Input)流和输出(Output)流两种

1:什么是输入/输出流

  • 定义:输入就是将数据从各种输入设备(包括文件、键盘等)中读取到内存中,输出则正好相反,是将数据写入到各种输出设备(比如文件、显示器、磁盘等)。例如键盘就是一个标准的输入设备,而显示器就是一个标准的输出设备,但是文件既可以作为输入设备,又可以作为输出设备

  • 数据流是Java进行I/O操作的对象,它按照不同的标准分为不同的类别:

    • 按照流的方向主要分为输入流和输出流两大类

    • 数据流按照数据单位的不同分为字节流和字符流

    • 按照功能可以划分为节点流和处理流

2:输入流

  • Java 流相关的类都封装在 java.io 包中,而且每个数据流都是一个对象。所有输入流类都是 InputStream 抽象类(字节输入流)和 Reader 抽象类(字符输入流)的子类。其中 InputStream 类是字节输入流的抽象类,是所有字节输入流的父类

  • InputStream 类中所有方法遇到错误时都会引发 IOException 异常。

    InputStream类常用方法
    名称 作用
    int read() 从输入流读入一个 8 字节的数据,将它转换成一个 0~ 255 的整数,返回一个整数,如果遇到输入流的结尾返回 -1
    int read(byte[] b) 从输入流读取若干字节的数据保存到参数 b 指定的字节数组中,返回的字节数表示读取的字节数,如果遇到输入流的结尾返回 -1
    int read(byte[] b,int off,int len) 从输入流读取若干字节的数据保存到参数 b 指定的字节数组中,其中 off 是指在数组中开始保存数据位置的起始下标,len 是指读取字节的位数。返回的是实际读取的字节数,如果遇到输入流的结尾则返回 -1
    void close() 关闭数据流,当完成对数据流的操作之后需要关闭数据流
    int available() 返回可以从数据源读取的数据流的位数。
    skip(long n) 从输入流跳过参数 n 指定的字节数目
    boolean markSupported() 判断输入流是否可以重复读取,如果可以就返回 true
    void mark(int readLimit) 如果输入流可以被重复读取,从流的当前位置开始设置标记,readLimit 指定可以设置标记的字节数
    void reset() 使输入流重新定位到刚才被标记的位置,这样可以重新读取标记过的数据
    • 最后 3 个方法一般会结合在一起使用,首先使用 markSupported() 判断,如果可以重复读取,则使用 mark(int readLimit) 方法进行标记,标记完成之后可以使用 read() 方法读取标记范围内的字节数,最后使用 reset() 方法使输入流重新定位到标记的位置,继而完成重复读取操作

3:输出流

  • 所有输出流类都是 OutputStream 抽象类(字节输出流)和 Writer 抽象类(字符输出流)的子类

  • OutputStream 类是所有字节输出流的超类,用于以二进制的形式将数据写入目标设备,该类是抽象类,不能被实例化

    OutputStream类常用方法
    名称 作用
    int write(b) 将指定字节的数据写入到输出流
    int write (byte[] b) 将指定字节数组的内容写入输出流
    int write (byte[] b,int off,int len) 将指定字节数组从 off 位置开始的 len 字节的内容写入输出流
    close() 关闭数据流,当完成对数据流的操作之后需要关闭数据流
    flush() 刷新输出流,强行将缓冲区的内容写入输出流

2):系统流

  • 每个 Java 程序运行时都带有一个系统流,系统流对应的类为 java.lang.System。Sytem 类封装了 Java 程序运行时的 3 个系统流,分别通过 in、out 和 err 变量来引用。这 3 个系统流如下所示:
    • System.in:标准输入流,默认设备是键盘。
    • System.out:标准输出流,默认设备是控制台。
    • System.err:标准错误流,默认设备是控制台。
  1. 例 (使用 System.in 读取字节数组)

    public class SystemTest {
      public static void main(String[] args) {
        // 声明一个字节数组
        byte[] byteData = new byte[100];
        System.out.println("请输入英文:");
        try {
          System.in.read(byteData);
        } catch (IOException e) {
          e.printStackTrace();
        }
        System.out.println("您输入的内容如下:");
        for (byte byteDatum : byteData) {
          System.out.print((char) byteDatum);
        }
      }
    }
    • System.in是InputStream类的一个对象因此上述代码的 System.in.read() 方法实际是访问 InputStream 类定义的 read() 方法。该方法可以从键盘读取一个或多个字符。对于 System.out 输出流主要用于将指定内容输出到控制台
    • System.out 和 System.error 是 PrintStream 类的对象。因为 PrintStream 是一个从 OutputStream 派生的输出流,所以它还执行低级别的 write() 方法。因此,除了 print() 和 println() 方法可以完成控制台输出以外,System.out 还可以调用 write() 方法实现控制台输出
    void write(int byteval) throws IOException
    • 该方法通过 byteval 参数向文件写入指定的字节。在实际操作中,print() 方法和 println() 方法比 write() 方法更常用

    尽管它们通常用于对控制台进行读取和写入字符,但是这些都是字节流。因为预定义流是没有引入字符流的 Java 原始规范的一部分,所以它们不是字符流而是字节流,但是在 Java 中可以将它们打包到基于字符的流中使用

3):字符编码介绍

  • 常见编码以:IOS8859-1、GBK/GB2312、Unicode、UTF编码

  • Java 中常见编码说明如下:

    • ISO8859-1:属于单字节编码,最多只能表示 0~255 的字符范围。
    • GBK/GB2312:中文的国标编码,用来表示汉字,属于双字节编码。GBK 可以表示简体中文和繁体中文,而 GB2312 只能表示简体中文。GBK 兼容 GB2312。
    • Unicode:是一种编码规范,是为解决全球字符通用编码而设计的。UTF-8 和 UTF-16 是这种规范的一种实现,此编码不兼容 ISO8859-1 编码。Java 内部采用此编码。
    • UTF:UTF 编码兼容了 ISO8859-1 编码,同时也可以用来表示所有的语言字符,不过 UTF 编码是不定长编码,每一个字符的长度为 1~6 个字节不等。一般在中文网页中使用此编码,可以节省空间。
  • 查看本地默认编码使用System类查看

    public static Properties getProperty()
    public static void main(String[] args){
      System.out.println("系统默认编码:"+System.getProperty("file.encoding"));
    }
  • 编码转换使用String类中的getBytes(String charset)

    public byte[] getBytes(String charset);
    public class CodeTest {
      public static void main(String[] args) throws Exception {
        File f = new File("D:" + File.separator + "test.txt");
        // 实例化输出流
        OutputStream out = new FileOutputStream(f);
        // 指定ISO8859-1编码
        byte b[] = "C语言中文网,你好!".getBytes("ISO8859-1");
        // 保存转码之后的数据
        out.write(b);
        // 关闭输出流
        out.close();
      }
    }

4):File类

  • 定义:File类是java.io包中唯一代表磁盘文件本身的对象,File类定义一些方法来操作文件,新建、删除等

  • File类不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流

  • File类提供了如下三种形式构造方法:

    1. File(String path):如果path是实际存在的路径,则该File对象表示的是目录;如果path是文件名,则该File对象表示的是文件
    2. File(String path,String name):paht是路径名,name是文件名
    3. File(File dir,String name):dir是路径对象,name是文件名
    File类的常用方法
    方法名称 说明
    boolean canRead() 测试应用程序是否能从指定的文件中进行读取
    boolean canWrite() 测试应用程序是否能写当前文件
    boolean delete() 删除当前对象指定的文件
    boolean exists() 测试当前 File 是否存在
    String getAbsolutePath() 返回由该对象表示的文件的绝对路径名
    String getName() 返回表示当前对象的文件名或路径名(如果是路径,则返回最后一级子路径名)
    String getParent() 返回当前 File 对象所对应目录(最后一级子目录)的父目录名
    boolean isAbsolute() 测试当前 File 对象表示的文件是否为一个绝对路径名。该方法消除了不同平台的差异,可以直接判断 file 对象是否为绝对路径。在 UNIX/Linux/BSD 等系统上,如果路径名开头是一条斜线/,则表明该 File 对象对应一个绝对路径;在 Windows 等系统上,如果路径开头是盘符,则说明它是一个绝对路径。
    boolean isDirectory() 测试当前 File 对象表示的文件是否为一个路径
    boolean isFile() 测试当前 File 对象表示的文件是否为一个“普通”文件
    long lastModified() 返回当前 File 对象表示的文件最后修改的时间
    long length() 返回当前 File 对象表示的文件长度
    String[] list() 返回当前 File 对象指定的路径文件列表
    String[] list(FilenameFilter) 返回当前 File 对象指定的目录中满足指定过滤器的文件列表
    boolean mkdir() 创建一个目录,它的路径名由当前 File 对象指定
    boolean mkdirs() 创建一个目录,它的路径名由当前 File 对象指定
    boolean renameTo(File) 将当前 File 对象指定的文件更名为给定参数 File 指定的路径名

    File 类中有以下两个常用常量:

    • public static final String pathSeparator:指的是分隔连续多个路径字符串的分隔符,Windows 下指;。例如 java -cp test.jar;abc.jar HelloWorld
    • public static final String separator:用来分隔同一个路径字符串中的目录的,Windows 下指/。例如 C:/Program Files/Common Files

    Windows 的路径分隔符使用反斜线“\”,而 Java 程序中的反斜线表示转义字符,所以如果需要在 Windows 的路径下包括反斜线,则应该使用两条反斜线或直接使用斜线“/”也可以。Java 程序支持将斜线当成平台无关的路径分隔符。

1:获取文件属性

  • 已知File类对象指向一个已存在的文件

    public class FileTest {
      public static void main(String[] args) {
        String path = "D:\\resources\\Java\\LearnJava\\src\\com\\Reflection";
        File f = new File(path, "测试.exe");
        System.out.println("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\测试.exe文件信息如下:");
        System.out.println("============================================");
        System.out.println("文件长度:" + f.length() + "字节");
        System.out.println("文件或者目录:" + (f.isFile() ? "是文件" : "不是文件"));
        System.out.println("文件或者目录:" + (f.isDirectory() ? "是目录" : "不是目录"));
        System.out.println("是否可读:" + (f.canRead() ? "可读取" : "不可读取"));
        System.out.println("是否可写:" + (f.canWrite() ? "可写入" : "不可写入"));
        System.out.println("是否隐藏:" + (f.isHidden() ? "是隐藏文件" : "不是隐藏文件"));
        System.out.println("最后修改日期:" + new Date(f.lastModified()));
        System.out.println("文件名称:" + f.getName());
        System.out.println("文件路径:" + f.getPath());
        System.out.println("绝对路径:" + f.getAbsolutePath());
      }
    }

2:创建和删除文件

  • File 类不仅可以获取已知文件的属性信息,还可以在指定路径创建文件,以及删除一个文件。创建文件需要调用 createNewFile() 方法,删除文件需要调用 delete() 方法。无论是创建还是删除文件通常都先调用 exists() 方法判断文件是否存在

  • 假设存在某个文件,如果不存在则创建,否则就删除再创建

    public class CreateFileTest {
      public static void main(String[] args) throws Exception {
        File f = new File("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\1.txt");
        if (f.exists()) {
          f.delete();
        }
        f.createNewFile();
      }
    }

    运行程序之后可以发现,在某盘中已经创建好了 1.txt 文件。但是如果在不同的操作系统中,路径的分隔符是不一样的,例如:

    • Windows 中使用反斜杠\表示目录的分隔符。
    • Linux 中使用正斜杠/表示目录的分隔符。
  • 根据程序所在的操作系统自动使用符合本地操作系统要求的分隔符

    public class CreateFileTest1 {
      public static void main(String[] args) throws Exception {
        String path = "D:" + File.separator + "1.txt";
        File f = new File(path);
        if (f.exists()) {
          f.delete();
        }
        f.createNewFile();
      }
    }

    在操作文件时一定要使用 File.separator 表示分隔符

3:创建和删除目录

  • 创建目录需要调用 mkdir() 方法,删除目录需要调用 delete() 方法。无论是创建还是删除目录都可以调用 exists() 方法判断目录是否存在

    public class MkdirTest {
      public static void main(String[] args) {
        String path = "D:\\config\\";
        File f = new File(path);
        if (f.exists()) {
          f.delete();
        }
        f.mkdir();
      }
    }

4:遍历目录

  • File类的list()方法提供了遍历目录功能,包含两种重载形式:
💨:String[] list()
  • 定义:该方法表示返回由 File 对象表示目录中所有文件和子目录名称组成的字符串数组,如果调用的 File 对象不是目录,则返回 null

    list() 方法返回的数组中仅包含文件名称,而不包含路径

💨:String[] list(FilenameFilter filter)
  • 定义:该方法的作用与 list() 方法相同,不同的是返回数组中仅包含符合 filter 过滤器的文件和目录,如果 filter 为 null,则接受所有名称
public class Traversal {
  public static void main(String[] args) {
    // 建立File变量,并设定由f变量变数引用
    File f = new File("D:\\");
    System.out.println("文件名称\t\t文件类型\t\t文件大小");
    System.out.println("===================================================");
    // 调用不带参数的list()方法
    String[] fileList = f.list();
    // 遍历返回的字符数组
    assert fileList != null;
    for (String s : fileList) {
      System.out.print(s + "\t\t");
      System.out.print((new File("C:/", s)).isFile() ? "文件" + "\t\t" : "文件夹" + "\t\t");
      System.out.println((new File("C:/", s)).length() + "字节");
    }
  }
}

5):动态读取文件内容

  • 定义:动态读取是指从文件的任意位置开始访问文件,而不是从文件开始位置读取到文件末尾。动态读取则需要RandomccessFile类。

  • RandomAccessFile是Java输入/输出流体系中功能最丰富的文件内容访问类,众多方法可以访问文件内容,它既可以读取文件内容,也可以向文件输出数据

  • RandomAccessFile对象包含一个记录指针,用以表示当前读写处的位置,当程序新创建一个 RandomAccessFile 对象时,该对象的文件记录指针位于文件头(也就是 0 处),当读/写了 n 个字节后,文件记录指针将会向后移动 n 个字节。除此之外,RandonAccessFile 可以自由移动该记录指针,既可以向前移动,也可以向后移动

    RandomAccessFile类的常用方法
    方法名及返回值类型 说明
    boolean readBoolean() 从文件中读取一个 boolean 值
    byte readByte() 从文件中读取一个带符号位的字节
    char readChar() 从文件中读取一个字符
    int readlnt() 从文件中读取一个带符号位的整数
    long readLong() 从文件中读取一个带符号位的 long 值
    String readLine() 从文件中读取下一行文本
    void seek(long pos) 指定从文件起始位置开始的指针偏移量
    void writeBoolean(boolean v) 以字节的形式向文件中写入一个 boolean 值
    void writeByte(int v) 以单字节的形式向文件中写入一个 byte 值
    void writeChar(int v) 以双字节的形式向文件中写入一个 char 值
    void writelnt(int v) 以4字节的形式向文件中写入一个整数
    writeLong(long v) 以8字节的形式向文件中写入一个 long 值
    void writeBytes(String s) 以字节序列的形式向文件中写入一个字符串
    void skipBytes(int n) 以当前文件指针位置为起始点,跳过 n 字节

    RandomAccessFile 类的构造方法有如下两种重载形式。

    1. RandomAccessFile(File file, String mode):访问参数 file 指定的文件,访问形式由参数 mode 指定,mode 参数有两个常用的可选值 r 和 rw,其中 r 表示只读,rw 表示读写。
    2. RandomAccessFile(String name, String mode):访问参数 name 指定的文件,mode 参数的含义同上

    如果使用 rw 方式声明 RandomAccessFile 对象时,要写入的文件不存在,系统将自动进行创建

  • 例 (创建words.txt文件,写入数据,并输出)

    1. 创建RandomAccessFileDemo对象,创建words.txt文件

      public class RandomAccessFileDemo {
        public static void main(String[] args) {
          try {
            File file = new File("D:\\resources\\Java\\LearnJava\\src\\words.txt");
            if (file.exists()) {
              file.delete();
              file.createNewFile();
            }
            file.createNewFile();
          } catch (IOException e) {
            System.out.print(e);
          }
        }
      }

6):字节流的使用

  • InputStream是Java所有字节输入流类的父类,OutStream是Java所有字节输出流类的父类。

1:字节输入流

  • InputStream 类及其子类的对象表示字节输入流,InputStream 类的常用子类如下:

    • ByteArrayInputStream 类:将字节数组转换为字节输入流,从中读取字节。
    • FileInputStream 类:从文件中读取数据。
    • PipedInputStream 类:连接到一个 PipedOutputStream(管道输出流)。
    • SequenceInputStream 类:将多个字节输入流串联成一个字节输入流。
    • ObjectInputStream 类:将对象反序列化。
  • 使用 InputStream 类的方法可以从流中读取一个或一批方法

    InputStream类的常用方法
    方法名及返回值类型 说明
    int read() 从输入流中读取一个 8 位的字节,并把它转换为 0~255 的整数,最后返回整数。 如果返回 -1,则表示已经到了输入流的末尾。为了提高 I/O 操作的效率,建议尽量 使用 read() 方法的另外两种形式
    int read(byte[] b) 从输入流中读取若干字节,并把它们保存到参数 b 指定的字节数组中。 该方法返回 读取的字节数。如果返回 -1,则表示已经到了输入流的末尾
    int read(byte[] b, int off, int len) 从输入流中读取若干字节,并把它们保存到参数 b 指定的字节数组中。其中,off 指 定在字节数组中开始保存数据的起始下标;len 指定读取的字节数。该方法返回实际 读取的字节数。如果返回 -1,则表示已经到了输入流的末尾
    void close() 关闭输入流。在读操作完成后,应该关闭输入流,系统将会释放与这个输入流相关 的资源。注意,InputStream 类本身的 close() 方法不执行任何操作,但是它的许多 子类重写了 close() 方法
    int available() 返回可以从输入流中读取的字节数
    long skip(long n) 从输入流中跳过参数 n 指定数目的字节。该方法返回跳过的字节数
    void mark(int readLimit) 在输入流的当前位置开始设置标记,参数 readLimit 则指定了最多被设置标记的字 节数
    boolean markSupported() 判断当前输入流是否允许设置标记,是则返回 true,否则返回 false
    void reset() 将输入流的指针返回到设置标记的起始处

    在使用 mark() 方法和 reset() 方法之前,需要判断该文件系统是否支持这两个方法,以避免对程序造成影响

2:字节输出流

  • OutputStream 类及其子类的对象表示一个字节输出流。OutputStream 类的常用子类如下:

    • ByteArrayOutputStream 类:向内存缓冲区的字节数组中写数据。
    • FileOutputStream 类:向文件中写数据。
    • PipedOutputStream 类:连接到一个 PipedlntputStream(管道输入流)。
    • ObjectOutputStream 类:将对象序列化。
    OutputStream 类的常用方法
    方法名及返回值类型 说明
    void write(int b) 向输出流写入一个字节。这里的参数是 int 类型,但是它允许使用表达式, 而不用强制转换成 byte 类型。为了提高 I/O 操作的效率,建议尽量使用 write() 方法的另外两种形式
    void write(byte[] b) 把参数 b 指定的字节数组中的所有字节写到输出流中
    void write(byte[] b,int off,int len) 把参数 b 指定的字节数组中的若干字节写到输出流中。其中,off 指定字节 数组中的起始下标,len 表示元素个数
    void close() 关闭输出流。写操作完成后,应该关闭输出流。系统将会释放与这个输出 流相关的资源。注意,OutputStream 类本身的 close() 方法不执行任何操 作,但是它的许多子类重写了 close() 方法
    void flush() 为了提高效率,在向输出流中写入数据时,数据一般会先保存到内存缓冲 区中,只有当缓冲区中的数据达到一定程度时,缓冲区中的数据才会被写 入输出流中。使用 flush() 方法则可以强制将缓冲区中的数据写入输出流, 并清空缓冲区

3:字节数组输入流

  • ByteArrayInputStream 类可以从内存的字节数组中读取数据,该类有如下两种构造方法重载形式:

    1. ByteArrayInputStream(byte[] buf):创建一个字节数组输入流,字节数组类型的数据源由参数 buf 指定。
    2. ByteArrayInputStream(byte[] buf,int offse,int length):创建一个字节数组输入流,其中,参数 buf 指定字节数组类型的数据源,offset 指定在数组中开始读取数据的起始下标位置,length 指定读取的元素个数。
  • ByteArrayInputStream 案例

    public class ByteArrayInputStreamTest {
      public static void main(String[] args) {
        // 创建数组
        byte[] b = new byte[] {1, -1, 25, -22, -5, 23};
        // 创建字节数组输入流
        ByteArrayInputStream bais = new ByteArrayInputStream(b, 0, 6);
        // 从输入流中读取下一个字节,并转换成int型数据
        int i = bais.read();
        // 如果不返回-1,则表示没有输入流的末尾
        while (i != -1) {
          System.out.println("原值=" + (byte) i + "\t\t\t转换为int类型=" + i);
          // 读取下一个
          i = bais.read();
        }
      }
    }

    从上述的运行结果可以看出,字节类型的数据 -1 和 -22 转换成 int 类型的数据后变成了 255 和 234,对这种结果的解释如下:

    • 字节类型的 1,二进制形式为 00000001,转换为 int 类型后的二进制形式为 00000000 00000000 0000000000000001,对应的十进制数为 1。
    • 字节类型的 -1,二进制形式为 11111111,转换为 int 类型后的二进制形式为 00000000 00000000 0000000011111111,对应的十进制数为 255。

    负数的二进制形式以补码形式存在

4:字节数组输出流

  • ByteArrayOutputStream 类可以向内存的字节数组中写入数据,该类的构造方法有如下两种重载形式:

    • ByteArrayOutputStream():创建一个字节数组输出流,输出流缓冲区的初始容量大小为 32 字节。
    • ByteArrayOutputStream(int size):创建一个字节数组输出流,输出流缓冲区的初始容量大小由参数 size 指定。
  • ByteArrayOutputStream 类中除了有前面介绍的字节输出流中的常用方法以外,还有如下两个方法:

    • intsize():返回缓冲区中的当前字节数。
    • byte[] toByteArray():以字节数组的形式返回输出流中的当前内容。
    public class ByteArrayOutStreamTest {
      public static void main(String[] args) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 创建数组
        byte[] b = new byte[] {1, -1, 25, -22, -5, 23};
        // 将字节数组b中的前4个字节元素写到输出流中
        baos.write(b, 0, 6);
        // 输出缓冲区中的字节数
        System.out.println("数组中一共包含:" + baos.size() + "字节");
        // 将输出流中的当前内容转换成字节数组
        byte[] newByteArray = baos.toByteArray();
        System.out.println(Arrays.toString(newByteArray));
      }
    }

5:文件输入流

  • FileInputStream 是从文件系统中获取输出字节。

    在创建 FileInputStream 类的对象时,如果找不到指定的文件将抛出 FileNotFoundException 异常,该异常必须捕获或声明抛出

  • FileInputStream 常用的构造方法主要有如下两种重载形式:

    • FileInputStream(File file):通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定。
    • FileInputStream(String name):通过打开一个到实际文件的链接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定。
    public class FileInputStreamTest {
      public static void main(String[] args) {
        try {
          // 以File对象作为参数创建FileInputStream对象
          FileInputStream fis1 =
              new FileInputStream(
                  new File("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\word.txt"));
          FileInputStream fil2 =
              new FileInputStream(
                  new File("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\word1.txt"));
        } catch (FileNotFoundException e) {
          System.out.println("指定的文件找不到!");
        }
      }
    }
  • 使用FileInputStrea, 读取并输出文件内容

    public class FileInputStreamTest1 {
      public static void main(String[] args) {
        File f = new File("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\word1.txt");
        FileInputStream fis = null;
        try {
          // file没有读写能力,则需要InputStream
          fis = new FileInputStream(f);
          // 定义字节数组
          byte[] bytes = new byte[1024];
          // 得到实际读取的字节数
          int n = 0;
          System.out.println("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\word1.txt内容如下:");
          // 循环读取
          while ((n = fis.read(bytes)) != -1) {
            // 将数组中从1下表0到n的内容给s
            String s = new String(bytes, 0, n);
            System.out.println(s);
          }
        } catch (Exception e) {
          System.out.println("文件未找到!");
          e.printStackTrace();
        } finally {
          try {
            fis.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }

    FileInputStream 类重写了父类 InputStream 中的 read() 方法、skip() 方法、available() 方法和 close() 方法,不支持 mark() 方法和 reset() 方法

6:文件输出流

  • FileOutStream 类继承自 OutputStream 类,重写和实现了父类中的所有方法。FileOutputStream 类的对象表示一个文件字节输出流,可以向流中写入一个字节或一批字节。在创建FileOutputStream 类的对象时,如果指定的文件不存在,则创建一个新文件;如果文件已存在,则清除原文件的内容重新写入。

  • FileOutputStream 类构造方法如下4种:

    • FileOutputStream(File file):创建一个文件输出流,参数 file 指定目标文件。
    • FileOutputStream(File file,boolean append):创建一个文件输出流,参数 file 指定目标文件,append 指定是否将数据添加到目标文件的内容末尾,如果为 true,则在末尾添加;如果为 false,则覆盖原有内容;其默认值为 false。
    • FileOutputStream(String name):创建一个文件输出流,参数 name 指定目标文件的文件路径信息。
    • FileOutputStream(String name,boolean append):创建一个文件输出流,参数 name 和 append 的含义同上。

    使用构造方法 FileOutputStream(String name,boolean append) 创建一个文件输出流对象,它将数据附加在现有文件的末尾。该字符串 name 指明了原文件,如果只是为了附加数据而不是重写任何已有的数据,布尔类型参数 append 的值应为 true

  • 对文件输出流有如下四点说明:

    • 在 FileOutputStream 类的构造方法中指定目标文件时,目标文件可以不存在。
    • 目标文件的名称可以是任意的,例如 D:\abc、D:\abc.de 和 D:\abc.de.fg 等都可以,可以使用记事本等工具打开并浏览这些文件中的内容。
    • 目标文件所在目录必须存在,否则会拋出 java.io.FileNotFoundException 异常。
    • 目标文件的名称不能是已存在的目录。例如 D 盘下已存在 Java 文件夹,那么就不能使用 Java 作为文件名,即不能使用 D:\Java,否则抛出 java.io.FileNotFoundException 异常
    public class FileOutputStreamTest {
      public static void main(String[] args) {
        // 声明FileInputStream对象fis
        FileInputStream fis = null;
        // 声明FileOutputStream对象fos
        FileOutputStream fos = null;
        try {
          File srcFile = new File("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\Test.java");
          // 实例化FileInputStream对象
          fis = new FileInputStream(srcFile);
          // 创建目标文件对象,该文件不存在
          File targetFile = new File("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\Word.txt");
          // 实例化FileOutputStream对象
          fos = new FileOutputStream(targetFile);
          // 每次读取1024字节
          byte[] bytes = new byte[1024];
          int i = fis.read(bytes);
          while (i != -1) {
            // 向Word.text文件写入内容
            fos.write(bytes, 0, i);
            i = fis.read(bytes);
          }
          System.out.println("写入结束!");
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
          try {
            assert fis != null;
            fis.close();
            assert fos != null;
            fos.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }

    在创建 FileOutputStream 对象时,如果将 append 参数设置为 true,则可以在目标文件的内容末尾添加数据,此时目标文件仍然可以暂不存在

7):字符流的使用

1:字符输入流

  • Reader 类表示所有字符流输入的父类

  • 常用子类如👇:

    1. CharArrayReader 类:将字符数组转换为字符输入流,从中读取字符
    2. StringReader 类:将字符串转换成字符输入流,从中读取字符
    3. BufferedReader 类:为其他字符输入流提供读缓冲区
    4. PipedReader 类:连接到一个 PipedWriter
    5. InputStreamReader 类:将字节输入流转换为字符输入流,可以指定字符编码
    Reader类中的read()方法
    方法名及返回值类型 说明
    int read() 从输入流中读取一个字符,并把它转换为 0~65535 的整数。如果返回 -1, 则表示 已经到了输入流的末尾。为了提高 I/O 操作的效率,建议尽量使用下面两种 read() 方法
    int read(char[] cbuf) 从输入流中读取若干个字符,并把它们保存到参数 cbuf 指定的字符数组中。 该方 法返回读取的字符数,如果返回 -1,则表示已经到了输入流的末尾
    int read(char[] cbuf,int off,int len) 从输入流中读取若干个字符,并把它们保存到参数 cbuf 指定的字符数组中。其中, off 指定在字符数组中开始保存数据的起始下标,len 指定读取的字符数。该方法返 回实际读取的字符数,如果返回 -1,则表示已经到了输入流的末尾

2:字符输出流

  • 与 Reader 类相反,Write 类是所有字符输出流的父类

  • 常用子类如👇:

    1. CharArrayWriter 类:向内存缓冲区的字符数组写数据
    2. StringWriter 类:向内存缓冲区的字符串(StringBuffer)写数据
    3. BufferedWriter 类:为其他字符输出流提供写缓冲区
    4. PipedWriter 类:连接到一个 PipedReader
    5. OutputStreamReader 类:将字节输出流转换为字符输出流,可以指定字符编码
    Writer类中的write()方法和append()方法
    方法名及返回值类型 说明
    void write(int c) 向输出流中写入一个字符
    void write(char[] cbuf) 把参数 cbuf 指定的字符数组中的所有字符写到输出流中
    void write(char[] cbuf,int off,int len) 把参数 cbuf 指定的字符数组中的若干字符写到输出流中。其中,off 指定 字符数组中的起始下标,len 表示元素个数
    void write(String str) 向输出流中写入一个字符串
    void write(String str, int off,int len) 向输出流中写入一个字符串中的部分字符。其中,off 指定字符串中的起 始偏移量,len 表示字符个数
    append(char c) 将参数 c 指定的字符添加到输出流中
    append(charSequence esq) 将参数 esq 指定的字符序列添加到输出流中
    append(charSequence esq,int start,int end) 将参数 esq 指定的字符序列的子序列添加到输出流中。其中,start 指定 子序列的第一个字符的索引,end 指定子序列中最后一个字符后面的字符 的索引,也就是说子序列的内容包含 start 索引处的字符,但不包括 end 索引处的字符

    Writer 类所有的方法在出错的情况下都会引发 IOException 异常。关闭一个流后,再对其进行任何操作都会产生错误

3:字符文件输入流

  • FileReader用来读取字符串文件。该类有两种构造方式:

    • FileReader(File file):在给定要读取数据的文件的情况下创建一个新的 FileReader 对象。其中,file 表示要从中读取数据的文件
    • FileReader(String fileName):在给定从中读取数据的文件名的情况下创建一个新 FileReader 对象。其中,fileName 表示要从中读取数据的文件的名称,表示的是一个文件的完整路径

    在创建 FileReader 对象时可能会引发一个 FileNotFoundException 异常,因此需要使用 try catch 语句捕获该异常

    public class FileReaderTest {
      public static void main(String[] args) {
        FileReader fr = null;
        try {
          fr = new FileReader("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\Test.java");
          int i = 0;
          System.out.println("D:\\resources\\Java\\LearnJava\\src\\com\\Reflection\\Test.java内容如下:");
          while ((i = fr.read()) != -1) {
            System.out.println((char) i);
          }
        } catch (Exception e) {
          System.out.print(e);
        } finally {
          try {
            fr.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }

4:字符文件输出流

  • FileWriter 写入字符文件,该类有四种构造方法:

    1. FileWriter(File file):在指定 File 对象的情况下构造一个 FileWriter 对象。其中,file 表示要写入数据的 File 对象
    2. FileWriter(File file,boolean append):在指定 File 对象的情况下构造一个 FileWriter 对象,如果 append 的值为 true,则将字节写入文件末尾,而不是写入文件开始处
    3. FileWriter(String fileName):在指定文件名的情况下构造一个 FileWriter 对象。其中,fileName 表示要写入字符的文件名,表示的是完整路径
    4. FileWriter(String fileName,boolean append):在指定文件名以及要写入文件的位置的情况下构造 FileWriter 对象。其中,append 是一个 boolean 值,如果为 true,则将数据写入文件末尾,而不是文件开始处

    FileWriter 类的创建不依赖于文件存在与否,如果关联文件不存在,则会自动生成一个新的文件。在创建文件之前,FileWriter 将在创建对象时打开它作为输出。如果试图打开一个只读文件,将引发一个 IOException 异常

    public class FileWriterTest {
      public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        FileWriter fw = null;
        try {
          fw = new FileWriter("D:\\resources\\Java\\LearnJava\\book.txt");
          for (int i = 0; i < 4; i++) {
            System.out.println("请输入第" + (i + 1) + "个字符串:");
            String name = input.next();
            fw.write(name + "\r\n");
          }
          System.out.println("录入完成!");
        } catch (Exception e) {
          System.out.println(e.getMessage());
        } finally {
          try {
            fw.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }

5:字符缓冲区输入流

  • BufferedReader 类主要用于辅助其他字符输入流,它带有缓冲区,可以先将一批数据督导内存缓冲区。接下来的读操作就可以直接从缓冲区中获取数据,而不需要每次都从数据源读取数据并进行字符编码转换,这样就可以提高数据的读取效率。

  • BufferedReader 两种构造方法:

    1. BufferedReader(Reader in):创建一个 BufferedReader 来修饰参数 in 指定的字符输入流
    2. BufferedReader(Reader in,int size):创建一个 BufferedReader 来修饰参数 in 指定的字符输入流,参数 size 则用于指定缓冲区的大小,单位为字符
  • 除字符输入流外,还包含readLine()方法,该方法返回包含该行内容的字符串,但该字符串中不包含任何终止符,如果已到达流末尾,则返回 null。readLine() 方法表示每次读取一行文本内容,当遇到换行(\n)、回车(\r)或回车后直接跟着换行标记符即可认为某行已终止

    public class BufferedReaderTest {
      public static void main(String[] args) {
        FileReader fr = null;
        BufferedReader br = null;
        try {
          // 创建 FileReader 对象
          fr = new FileReader("D:\\resources\\Java\\LearnJava\\Book.txt");
          // 创建 BufferedReader 对象
          br = new BufferedReader(fr);
          System.out.println("D:\\resources\\Java\\LearnJava\\Book.txt内容如下:");
          String strLine = "";
          while ((strLine = br.readLine()) != null) {
            System.out.println(strLine);
          }
        } catch (IOException e1) {
          e1.printStackTrace();
        } finally {
          try {
            fr.close();
            br.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }

6:字符缓冲区输出流

  • BufferedWriter 类主要用于辅助其他字符输出流,它同样带有缓冲区,可以先将一批数据写入缓冲区,当缓冲区满了以后,再将缓冲区的数据一次性写到字符输出流,其目的是为了提高数据的写效率
  • BufferedWriter 两种构造方法:
    1. BufferedWriter(Writer out):创建一个 BufferedWriter 来修饰参数 out 指定的字符输出流
    2. BufferedWriter(Writer out,int size):创建一个 BufferedWriter 来修饰参数 out 指定的字符输出流,参数 size 则用于指定缓冲区的大小,单位为字符

8):字符流和字节流的区别,区分输入流和输出流

1:字符流和字节流的区别

  • 总结
    1. 以Stream 结尾都是字节流,Reader 和Writer 结尾都是字符流
    2. InputStream 是所有字节输入流的父类,OutputStream 是所有字节输出流的父类
    3. Reader 是字符输入流的父类,Writer 是字符输出流的父类
  • 字节流
    • 文件流:FileOutputStream 和 FileInputStream
    • 缓冲流:BufferedOutputStream 和 BufferedInputStream
    • 对象流:ObjectOutputStream 和 ObjectInputStream
  • 字符流
    • 转换流:InputStreamReader 和 OutputStreamWriter
    • 缓冲字符流:PrintWriter 和 BufferedReader
  • 区别
    1. 读写的时候字节流是按字节读写,字符流按字符读写。
    2. 字节流适合所有类型文件的数据传输,因为计算机字节(Byte)是电脑中表示信息含义的最小单位。字符流只能够处理纯文本数据,其他类型数据不行,但是字符流处理文本要比字节流处理文本要方便。
    3. 在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。
    4. 只是读写文件,和文件内容无关时,一般选择字节流。

2:区分输出流和输出流

  • 文件在磁盘上,把文件从磁盘上读入内存中来,这就需要输入流。反之,把内存中的数据写到磁盘上的文件里就需要输出流。

  • 写(将内容写入到文件里,如:存盘)是输入,读(把内容从文件里读出来,如:显示)是输出

  • 不管从磁盘、网络还是键盘读,读到内存中就是 InputStream

    BufferedReader in =  new BufferedReader(new InputStreamReader(new FileInputStream("infilename")));
  • 不管写到磁盘、网络,或者写到屏幕,都是使用 OutputStream

    BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("outfilename")));

9):转换流

  • InputStreamReader 用于将字节输入流转换为字符输入流 OutputStreamWriter 用于将字节输出流转换为字符输出流

  • 编写txt文件 通过字节流方式运行

    public class InputStreamReaderTest {
      public static void main(String[] args) {
        try {
          FileInputStream fis = new FileInputStream("D:\\resources\\Java\\LearnJava\\Java.txt");
          int b = 0;
          while ((b = fis.read()) != -1) {
            System.out.print((char) b); // 包含中文则乱码
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  • 使用字节数组方式运行

    public class InputStreamReaderTest {
      public static void main(String[] args) {
        try {
          FileInputStream fis = new FileInputStream("D:\\resources\\Java\\LearnJava\\Java.txt");
          byte[] b = new byte[1024];
          int len = 0;
          while ((len = fis.read(b)) != -1) {
            System.out.print(new String(b, 0, len, "UTF-8"));
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  • 当存储文字较多时,则出现解码错乱,且字节长度不一,所以需转换

    public class InputStreamReaderTest {
      public static void main(String[] args) {
        try {
          FileInputStream fis = new FileInputStream("D:\\resources\\Java\\LearnJava\\java.txt");
          InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
          int b = 0;
          while ((b = isr.read()) != -1) {
            System.out.print((char) b);
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  • 获取键盘输入将其转换成字符输入流,BufferedReader类的readLine()方法实现

    public class readLineTest {
      public static void main(String[] args) {
        try {
          // 将 System.in 对象转换成 Reader 对象
          InputStreamReader reader = new InputStreamReader(System.in);
          // 将普通的 Reader 包装成 BufferedReader
          System.out.println("请输入内容:");
          BufferedReader br = new BufferedReader(reader);
          String line = null;
          // 循环方式逐行的读取
          while ((line = br.readLine()) != null) {
            // 如果读取的字符串为"exit" 则退出
            if (line.equals("exit")) {
              System.exit(1);
            }
            // 打印读取的内容
            System.out.println("输入内容为:" + line);
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }

10):对象序列化控制输入输出

1:对象序列化是什么

  • 对象序列化(Serialize)指将一个Java对象写入 IO 流中,与此对应的是,对象的反序列化(Deserialize)则指从 IO 流中恢复该 Java 对象。如果想让某个 Java 对象能够序列化,则必须让它的类实现 Java.io.Serializable接口

    public interface Serializable {}
    • Serializable 接口是一个空接口,实现该接口无须实现任何方法,它只是告诉 JVM 该类可以被序列化机制处理。通常建议程序创建的每个 JavaBean 类都实现 Serializable
  • ObjectInput 接口与 ObjectOutput 接口分别继承了 DataInput 和 DataOutput 接口,主要提供用于读写基本数据和对象数据的方法。 ObjectInput 接口提供了 readObject() 方法,此方法用于将对象从流中读出。ObjectOutput 提供了 writeObject() 方法,此方法用于将对象写入流中。因为 ObjectInput 与 ObjectOutput 都是接口,所以不能创建对象,只能使用分别实现了这两个接口的 ObjectInputStream 类和 ObjectOutputStream 类来创建对象

2:序列化

  • ObjectOutputStream 类继承了 OutputStream 类,同时实现了 ObjectOutput 接口,提供将对象序列化并写入流中的功能

    public ObjectOutputStream (OutputStream out)
    • 需传入一个OutputStream对象,表示将对象二进制写入到指定的OutputStream
  1. 创建一个 ObjectOutputStream 对象

    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
  2. 调用 ObjectOutputStream 对象的writeObject() 方法输出序列化对象

    // 将一个 Person 对象输出到输出流中
    oos.writerObject(per);
  • 定义person类,实现Serializable接口

    public class Person implements Serializable {
      private String name;
      private int age;
    
      public Person(String name, int age) {
        System.out.println("有参数的构造器");
        this.name = name;
        this.age = age;
      }
    }

    Person 类的两个成员变量分别是 String 类型和 int 类型的。如果某个类的成员变量的类型不是基本类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的

  • 利用 ObjectOutputStream 将一个 Person 对象写入到磁盘文件

    public class WriteObject {
      public static void main(String[] args) throws Exception {
        // 创建一个 ObjectOutputStream 输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
        Person per = new Person("C语言中文网", 7);
        // 将 per对象写入输出流
        oos.writeObject(per);
      }
    }

3:反序列化

  • ObjectInputStream 类继承了 InputStream 类,同时实现了 ObjectInput 接口,提供了将对象序列化从流中读取出来

    public ObjectInputStream(InputStream out)
    • 传入一个 InputStream 对象,用来创建从指定 InputStream 读取的 ObjectInputStream
  1. 创建一个 ObjectInputStream 输入流,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上。

    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
  2. 调用 ObjectInputStream 对象的 readObject() 方法读取流中的对象,该方法返回一个 Object 类型的 Java 对象,如果程序知道该 Java 对象的类型,则可以将该对象强制类型转换成其真实的类型

    Person P = (Person)ois.readObject();
  • 从 Object.txt 文件 Person 对象

    public class ReadObject {
      public static void main(String[] args) throws Exception {
        // 创建一个ObjectInputStream输入流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        // 从输入流中读取一个 Java对象,并将其强制类型转换为Person类
        Person p = (Person) ois.readObject();
        System.out.println("名字为:" + p.getName() + "\n年龄为:" + p.getAge());
      }
    }

    如果使用序列化机制向文件中写入了多个 Java 对象,使用反序列化机制恢复对象时必须按实际写入的顺序读取

  • 当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造方法,要么也是可序列化的,否则反序列化时将抛出 InvalidClassException 异常。如果父类是不可序列化的,只是带有无参数的构造方法,则该父类中定义的成员变量值不会序列化到 IO 流中

4:Java序列化编号

  • Java 序列化机制是通过类的 序列化编号(serialVersionUID)来验证版本一致性的。在反序列化时,JVM 会把传来字节流中的序列化编号和本地相应实体类的序列化编号进行比较,如果相同就认为一致,可以进行反序列化,否则会抛出 InvalidCastException 异常
  • 序列化编号有两种显式生成方式:
    1. 默认的1L,比如:private static final long serialVersionUID = 1L
    2. 根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段
  • 当实现 Serializable 接口的对象没有显式定义一个序列化编号时,Java 序列化会根据编译的 Class 自动生成一个序列化编号,这种情况下只要 class 文件发生变化,序列化号就会改变,否则一直不变

11):保存图书信息

  1. 创建 Book 类,包含no,name和price 3个属性,包含write()和read()方法,将图书信息写入磁盘文件,再从磁盘文件读取打印于控制台

    public class Book {
      /** 编号 */
      private int no;
      /** 名称 */
      private String name;
      /** 单价 */
      private double price;
    
      public Book(int no, String name, double price) {
        this.no = no;
        this.name = name;
        this.price = price;
      }
    
      @Override
      public String toString() {
        return "图书编号:" + this.no + ", 图书名称:" + this.name + ", 图书单价:" + this.price + "\n";
      }
    
      public static void write(List<Book> books) {
        FileWriter fw = null;
        try {
          fw = new FileWriter("D:\\resources\\Java\\LearnJava\\book.txt");
          for (Book book : books) {
            // 循环写入
            fw.write(book.toString());
          }
        } catch (Exception e) {
          System.out.println(e.getMessage());
        } finally {
          try {
            assert fw != null;
            fw.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    
      public static void read() {
        FileReader fr = null;
        BufferedReader br = null;
        try {
          fr = new FileReader("D:\\resources\\Java\\LearnJava\\book.txt");
          br = new BufferedReader(fr);
          String str = "";
          // 循环读取每行数据
          while ((str = br.readLine()) != null) {
            System.out.println(str);
          }
        } catch (Exception e) {
          System.out.println(e.getMessage());
        } finally {
          try {
            br.close();
            fr.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }
  2. 测试

    public class BookTest {
      public static void main(String[] args) {
        Book book1 = new Book(1001, "你好", 200);
        Book book2 = new Book(1002, "哈利波特", 500);
        Book book3 = new Book(1005, "安徒生童话", 100);
        List<Book> books = new ArrayList<>();
        books.add(book1);
        books.add(book2);
        books.add(book3);
        Book.write(books);
        System.out.println("图书信息");
        Book.read();
      }
    }

12):猜数字小游戏

  1. 创建 count.txt 文件,存储游戏次数

    count=0
  2. 创建 way.txt 文件,存储支付状态(1 为已付费,0 为未付费)

    way=0
  3. 创建 BullCows 类

    public class BullCows {
    
      /**
       * 获取已经玩过的次数
       *
       * @return temp count.txt 文件中的游戏次数
       * @throws Exception
       */
      private static int getCount() throws IOException {
        // 创建Properties 对象
        Properties props = new Properties();
        // 使用FileReader对获取count文件中的游戏次数
        props.load(new FileInputStream("D:\\resources\\Java\\LearnJava\\src\\com\\guess\\count.txt"));
        String proper = props.getProperty("count");
        return Integer.parseInt(proper);
      }
    
      /**
       * 支付方法,支付成功则把支付状态改为 "1" 并存入数据库,则可以无限次玩游戏
       *
       * @throws Exception
       */
      private static void getMoney() throws IOException {
        System.out.println("请支付5元!");
        // 获取键盘录入数据
        Scanner scanner = new Scanner(System.in);
        int nextInt = scanner.nextInt();
        if (nextInt == 5) {
          // 创建Properties对象
          Properties props = new Properties();
          props.setProperty("way", "1");
          // 使用FileWriter类将支付状态写入到way文件
          props.store(new FileWriter("D:\\resources\\Java\\LearnJava\\src\\com\\guess\\way.txt"), null);
        }
      }
    
      /**
       * 将试玩的次数写入文档并保存
       *
       * @throws Exception
       */
      private static void writeCount() throws IOException {
        // 创建Properties对象
        Properties props = new Properties();
        int count = getCount();
        // 写入文件
        props.setProperty("count", (count + 1) + "");
        props.store(new FileWriter("D:\\resources\\Java\\LearnJava\\src\\com\\guess\\count.txt"), null);
      }
    
      private static boolean getCondition() throws FileNotFoundException, IOException {
        boolean flag = false;
        Properties props = new Properties();
        // 读取way.txt文件,获取支付状态
        props.load(new FileReader("D:\\resources\\Java\\LearnJava\\src\\com\\guess\\way.txt"));
        String property = props.getProperty("way");
        int parseInt = Integer.parseInt(property);
        // way的值等于1时,为已付费
        if (parseInt == 1) {
          flag = true;
        } else {
          flag = false;
        }
        return flag;
      }
    
      private static void game() {
        // 产生随机数1~100
        int random = (int) (Math.random() * 100 + 1);
        // 获取键盘录入数据
        Scanner sc = new Scanner(System.in);
        System.out.println("欢迎来到猜数字小游戏!");
        while (true) {
          System.out.println("请输入你猜的数据:");
          int guess = sc.nextInt();
          if (guess > random) {
            System.out.println("大了");
          } else if (guess < random) {
            System.out.println("小了");
          } else {
            System.out.println("猜对了哦!");
            break;
          }
        }
      }
    
      /**
       * 负责调用对应的方法,实现整个案例的逻辑关系
       *
       * @param args
       * @throws IOException
       */
      public static void main(String[] args) throws IOException {
        while (true) {
          // 获取游戏次数!
          int count = getCount();
          // 获取付费状态
          boolean flag = getCondition();
          // 如果已付费,提示用户游戏次数解封可以继续游戏
          if (flag) {
            System.out.println("游戏已经付费,由此次数无限!");
            game();
          } else {
            // 未付费且游戏次数超过5次,提示游戏结束,要付费
            if (count >= 5) {
              System.out.println("试玩已经结束,请付费!");
              getMoney();
            } else {
              // 未付费且游戏次数未超过5次时,继续游戏,游戏次数加1
              System.out.println("-----" + "试玩第" + (count + 1) + "次" + "-----");
              game();
              writeCount();
            }
          }
        }
      }
    }
    • 示例中用到 Properties 类的几个方法,方法说明如下:
      1. getProperty (String key):用指定的键在此属性列表中搜索属性。也就是通过参数 key,得到 key 所对应的 value。
      2. load (InputStream inStream):从输入流中读取属性列表(键和元素对)。通过对指定的文件进行装载来获取该文件中的所有键值对。以供 getProperty (String key) 来搜索。
      3. setProperty (String key, String value) :调用 Hashtable 的方法 put,通过调用基类的 put 方法来设置键值对。
      4. store (OutputStream out, String comments):与 load 方法相反,该方法是将键值对写入到指定的文件中。

十四、注解

1):注解概念及作用

  • 定义:在源代码中嵌入一些补充信息,这种补充信息称为注解(Annotation)。注解都是 @ 符号开头的

    Annotation 可以翻译为“注解”或“注释”,一般翻译为“注解”,因为“注释”一词已经用于说明“//”、“/**…/”和“/…*/”等符号了,这里的“注释”是英文 Comment 翻译。

    注解并不能改变程序的运行结果,也不会影响程序运行的性能。有些注解可以在编译时给用户提示或警告,有的注解可以在运行时读写字节码文件信息

  • 注解就是源代码的元数据

    @Override
    public String toString() {
        return "你好";
    }
  • 注解常见的作用有以下几种:

    1. 生成帮助文档。这是最常见的,也是 Java 最早提供的注解。常用的有 @see、@param 和 @return 等;
    2. 跟踪代码依赖性,实现替代配置文件功能。比较常见的是 Spring 2.5 开始的基于注解配置。作用就是减少配置。现在的框架基本都使用了这种配置来减少配置文件的数量;
    3. 在编译时进行格式检查。如把 @Override 注解放在方法前,如果这个方法并不是重写了父类方法,则编译时就能检查出
  • 基本注解包括:@Override、@Deprecated、@SuppressWarnings、@SafeVarargs 和 @FunctionalInterface

2):@Override注解

  • 定义:用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其它的元素。它可以强制一个子类必须重写父类方法或者实现接口的方法

    public class Person {
      private String name = "";
      private int age;
      ...
      @Override
      public String t0String() { //toString()
         return "Person [name=" + name + ", age=" + age + "]";
      }
    }

    类型为 Person 的方法t0String()必须覆盖或实现超类型方法

  • @Override 的作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错

3):@Deprecated注解

  • 定义:可以用来注解类、接口、成员方法和成员变量等,用于表示某个元素(类、方法等)已过时

    @Deprecated
    public class Person {
      @Deprecated 
      protected String name;
      private int age;
    
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public int getAge() {
        return age;
      }
    
      public void setAge(int age) {
        this.age = age;
      }
    
      @Deprecated
      public void setNameAndAge(String name, int age) {
        this.name = name;
        this.age = age;
      }
    
      @Override
      public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
      }
    }
    • 使用@Deprecated 注解则 被画上删除线
    public class PersonTest {
      public static void main(String[] args) {
        Person person = new Person();
        person.setNameAndAge("你好", 30);
        person.name = "教程";
      }
    }
  • Java 9 为 @Deprecated 注解增加了以下两个属性:

    1. forRemoval:该 boolean 类型的属性指定该 API 在将来是否会被删除。
    2. since:该 String 类型的属性指定该 API 从哪个版本被标记为过时
    class Test {
      /** since属性指定从哪个版本开始被标记成过时,forRemoval指定该API将来会被删除 */
      @Deprecated(since = "9", forRemoval = true)
      public void print() {
        System.out.println("java教程");
      }
    }
    
    /**
     * @author Admin
     */
    public class DeprecatedTest {
      public static void main(String[] args) {
        /* 下面使用info()方法时将会被编译器警告 */
        new Test().print();
      }
    }
    • @Deprecated 的作用与文档注释中的 @deprecated 标记的作用基本相同,但它们的用法不同,前者是 Java 5 才支持的注解,无须放在文档注释语法(/** … */部分)中,而是直接用于修饰程序中的程序单元,如方法、类和接口等

4):@SuppressWarnings

  • 定义:指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告,且会一直作用于该程序元素的所有子元素

  • 注解的使用有以下三种:

    1. 抑制单类型的警告:@SuppressWarnings(“unchecked”)
    2. 抑制多类型的警告:@SuppressWarnings(“unchecked”,”rawtypes”)
    3. 抑制所有类型的警告:@SuppressWarnings(“all”)
    抑制警告的关键字
    关键字 用途
    all 抑制所有警告
    boxing 抑制装箱、拆箱操作时候的警告
    cast 抑制映射相关的警告
    dep-ann 抑制启用注释的警告
    deprecation 抑制过期方法警告
    fallthrough 抑制在 switch 中缺失 breaks 的警告
    finally 抑制 finally 模块没有返回的警告
    hiding 抑制相对于隐藏变量的局部变量的警告
    incomplete-switch 忽略不完整的 switch 语句
    nls 忽略非 nls 格式的字符
    null 忽略对 null 的操作
    rawtypes 使用 generics 时忽略没有指定相应的类型
    restriction 抑制禁止使用劝阻或禁止引用的警告
    serial 忽略在 serializable 类中没有声明 serialVersionUID 变量
    static-access 抑制不正确的静态访问方式警告
    synthetic-access 抑制子类没有按最优方法访问内部类的警告
    unchecked 抑制没有进行类型检查操作的警告
    unqualified-field-access 抑制没有权限访问的域的警告
    unused 抑制没被使用过的代码的警告
  • 使用 @SuppressWarnings 注解示例

    public class HelloWorld {
      @SuppressWarnings({"deprecation"})
      public static void main(String[] args) {
        Person person = new Person();
        person.setNameAndAge("你好", 50);
        person.name = "Java教程";
      }
    }

5):@SafeVarargs注解

  • 定义:抑制编译器警告

    public class HelloWorldTest {
      public static void main(String[] args) {
        // 传递可变参数,参数是泛型集合
        display(10, 20, 30);
        // 传递可变参数,参数是非泛型集合
        // 会有编译警告
        display("10", 20, 30);
      }
    
      public static <T> void display(T... array) {
        for (T arg : array) {
          System.out.println(arg.getClass().getName() + ":" + arg);
        }
      }
    }
  • 使用 @SafeVarargs 注解抑制编译器警告

    public class HelloWorldTest {
      public static void main(String[] args) {
        // 传递可变参数,参数是泛型集合
        display(10, 20, 30);
        // 传递可变参数,参数是非泛型集合
        // 会有编译警告
        display("10", 20, 30);
      }
    
      @SafeVarargs
      public static <T> void display(T... array) {
        for (T arg : array) {
          System.out.println(arg.getClass().getName() + ":" + arg);
        }
      }
    }

6):@FunctionalInterface

  • @FunctionalInterface 就是用来指定某个接口必须是函数式接口,所以 @FunInterface 只能修饰接口,不能修饰其它程序元素

    函数式接口就是为 Java 8 的 Lambda 表达式准备的,Java 8 允许使用 Lambda 表达式创建函数式接口的实例,因此 Java 8 专门增加了 @FunctionalInterface

    @FunctionalInterface
    public interface FunInterface {
      static void print() {
        System.out.println("C语言中文网");
      }
    
      default void show() {
        System.out.println("我正在学习C语言中文网Java教程");
      }
    
      // 只定义一个抽象方法
      void test();
    }

    @FunctionalInterface 注解的作用只是告诉编译器检查这个接口,保证该接口只能包含一个抽象方法

    • 如果 FunInterface 接口中再增加一个抽象方法 abc(),编译程序时将出现如下错误

      @FunctionInterface”批注无效;FunInterface不是functional接口

7):元注解作用及使用

  • 定义:元注解是负责对其它注解进行说明的注解,自定义注解时可以使用元注解

1:@Documented

  • @Documented 是一个标记注解,没有成员变量。用 @Documented 注解修饰的注解类会被 JavaDoc 工具提取成文档。默认情况下,JavaDoc 是不包括注解的,但如果声明注解时指定了 @Documented,就会被 JavaDoc 之类的工具处理,所以注解类型信息就会被包括在生成的帮助文档中。

    @MyDocumented
    public class DocumentedTest {
      @MyDocumented
      public String Test() {
        return "Java教程";
      }
    }
  • 测试

    @MyDocumented
    public class DocumentedTest {
      @MyDocumented
      public String Test() {
        return "Java教程";
      }
    }

2:@Target

  • 定义:注解用来指定一个注解的使用范围,即被 @Target 修饰的注解可以用在什么地方。@Target 注解有一个成员变量(value)用来设置适用目标,value 是 java.lang.annotation.ElementType 枚举类型的数组

    ElementType常用的枚举常量
    名称 说明
    CONSTRUCTOR 用于构造方法
    FIELD 用于成员变量(包括枚举常量)
    LOCAL_VARIABLE 用于局部变量
    METHOD 用于方法
    PACKAGE 用于包
    PARAMETER 用于类型参数(JDK 1.8新增)
    TYPE 用于类、接口(包括注解类型)或 enum 声明
  • 自定义一个MyTarger注解,使用范围为方法

    @Target({ElementType.METHOD})
    public @interface MyTarget {}
    
    class Test {
      @MyTarget String name;
    }
    • 则有错误信息 The annotation @MyTarget is disallowed for this location
    • @MyTarget 不能修饰成员变量,只能修饰方法

3:@Retention

  • 定义:用于描述注解的生命周期,也就是该注解被保留的时间长短。@Retention 注解中的成员变量(value)用来设置保留策略,value 是 java.lang.annotation.RetentionPolicy 枚举类型。
  • RetentionPolicy 有 3 个枚举常量:
    1. SOURCE:在源文件中有效(即源文件保留)
    2. CLASS:在 class 文件中有效(即 class 保留)
    3. RUNTIME:在运行时有效(即运行时保留)
  • 生命周期大小排序为 SOURCE < CLASS < RUNTIME,前者能使用的地方后者一定也能使用。如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS 注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解

4:@Inherited

  • 定义:一个标记注解,用来指定该注解可以被继承。用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。就是说如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解。
  1. 创建自定义注解

    @Target({ElementType.METHOD})
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyInherited {}
  2. 测试

    @MyDocumented
    public class TestA {
      public static void main(String[] args) {
        System.out.println(TestA.class.getAnnotation(MyInherited.class));
        System.out.println(TestB.class.getAnnotation(MyInherited.class));
        System.out.println(TestC.class.getAnnotation(MyInherited.class));
      }
    }
    
    class TestB extends TestA {}
    
    class TestC extends TestB {}

5:@Repertable

  • (Java8新增)允许在相同的程序元素中重复注解,在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解。Java 8 版本以前,同一个程序元素前最多只能有一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”。
  1. Java8新增前

    public @interface Roles {
      Role[] roles();
    }
    
    public @interface Roles {
      Role[] value();
    }
    
    public class RoleTest {
      @Roles(roles = {@Role(roleName = "role1"), @Role(roleName = "role2")})
      public String doString() {
        return "这是C语言中国网Java教程";
      }
    }
  2. Java8新增后(重复注解)

    public @interface Roles {
      Role[] value();
    }
    
    @Repeatable(Roles.class)
    public @interface Roles {
      String roleName();
    }
    
    public class RoleTest {
      @Role(roleName = "role1")
      @Role(roleName = "role2")
      public String doString() {
        return "这是C语言中文网Java教程";
      }
    }
    • 重复注解只是一种简化写法,这种简化写法是一种假象,多个重复注解其实会被作为“容器”注解的 value 成员的数组元素处理

8):自定义注解

  • 声明自定义注解使用 @interface 关键字(interface 关键字前加 @ 符号)实现。定义注解与定义接口非常像

    public @interface Test{
    
    }
    • 定义注解和定义类相似,注解前面的访问修饰符和类一样有两种,分别是公有访问权限(public)和默认访问权限(默认不写)。一个源程序文件中可以声明多个注解,但只能有一个是公有访问权限的注解。且源程序文件命名和公有访问权限的注解名一致。
  • 不包含任何成员变量的注解称为标记注解,例如上面声明的 Test 注解以及基本注解中的 @Override 注解都属于标记注解。根据需要,注解中可以定义成员变量,成员变量以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型

    public @interface MyTag {
      // 定义带两个成员变量的注解
      // 注解中的成员变量以方法的形式来定义
      String name();
      int age();
    }
  • 如果在注解里定义了成员变量,那么使用该注解时就应该为它的成员变量指定值

    public class Test {
      // 使用带成员变量的注解时,需要为成员变量赋值
      @MyTag(name="xx", age=6)
      public void info() {
         ...
      }
      ...
    }
  • 注解中的成员变量也可以有默认值,可使用 default 关键字。如下代码定义了 @MyTag 注解,该注解里包含了 name 和 age 两个成员变量

    public @interface MyTag {
      // 定义了两个成员变量的注解
      // 使用default为两个成员变量指定初始值
      String name() default "C语言中文网";
      int age() default 7;
    }
  • 如果为注解的成员变量指定了默认值,那么使用该注解时就可以不为这些成员变量赋值,而是直接使用默认值

    public class Test {
      // 使用带成员变量的注解
      // MyTag注释的成员变量有默认值,所以可以不为它的成员变量赋值
      @MyTag
      public void info() {
        ...
      }
      ...
    }
  • 根据注解是否包含成员变量,可以分为如下两类。

    1. 标记注解:没有定义成员变量的注解类型被称为标记注解。这种注解仅利用自身的存在与否来提供信息,如前面介绍的 @Override、@Test 等都是标记注解。
    2. 元数据注解:包含成员变量的注解,因为它们可以接受更多的元数据,所以也被称为元数据注解。

9):通过反射获取注解信息

  • 使用注解修饰了类、方法、变量等成员之后,这些注解不会自己生效,必须由开发者提供相应的工具来提取处理。要想获取类、方法或变量的注解信息,必须通过 Java 的反射技术来获取 Annotation 对象

  • 所有 Java 注解类型都继承于 java.lang.annotation.Annotation 接口,该接口代表程序中可以接收注解的程序元素。该接口主要有如下几个实现类:

    1. Class:类定义。
    2. Constructor:构造方法定义。
    3. Field:类的成员变量定义。
    4. Method:类的方法定义。
    5. Package:类的包定义。
  • java.lang.reflect 包所提供的反射 API 增加了读取运行时注解的能力。只有当定义注解时使用了 @Retention(RetentionPolicy.RUNTIME) 修饰,该注解才会在运行时可见。

  • AnnotatedElement 接口是所有程序元素(如 Class、Method、Constructor 等)的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象(如 Class、Method、 Constructor 等)之后,程序就可以调用该对象的如下几个方法来访问注解信息

    注解名称和作用
    方法名 作用
    A getAnnotation(Class annotationClass) 如果该元素存在 annotationClass 类型的注解,则返回注解,否则返回 null
    A getDeclaredAnnotation(Class annotationClass) 这是 Java 8 新增的方法,该方法尝试获取直接修饰该程序元素、指定类型的注解。如果该类型的注解不存在,则返回 null
    Annotation[] getAnnotations() 返回该元素上存在的所有注解
    Annotation[] getDeclaredAnnotations() 返回直接存在于该元素的所有注解(和 getAnnotations() 的区别在于该方法将不返回继承的注释)
    boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) 判断该元素上是否存在 annotationClass 类型的注解,如果存在则返回 true,否则返回 false。
    A[] getAnnotationsByType(Class annotationClass) 该方法与前面介绍的 getAnnotation() 方法基本相似。但由于 Java 8 增加了重复注解功能,因此需要使用该方法获取该元素存在 annotationClass 类型的多个注解。
    A[] getDeclaredAnnotationsByType(Class annotationClass) 该方法与前面介绍的 getDeclaredAnnotations() 方法基本相似。但由于 Java 8 增加了重复注解功能,因此需要使用该方法获取该元素存在 annotationClass 类型的多个注解。
  • 根据官方的命名规则,可以总结出以下几条:

    1. getDeclaredAnnotationXXXX:只可以获取直接存在的注解信息,即直接修饰在某个元素(类、属性和方法)上的注解。
    2. getXXXXByType:可以获取间接存在的注解信息
    3. getAnnotationXXX:可以获取继承的注解信息

    所有的方法都可以获取直接注解信息

  • getAnnnotation() 实现SpringMVC注解方式@RequestMapping(value="")

    1. 实现SpringMVC注解方式

      @Target({ElementType.TYPE, ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface MyRequestMapping {
        // 这是注解的一个属性字段,也就是在使用注解时填写在括号里的参数
        String value();
      }
    2. 创建TestController类,使用@MyRequestMapping注解

      @MyRequestMapping("/test")
      public class TestController {
        public void test() {
          System.out.println("进入test方法");
        }
      }
    3. 测试类

      public class MyRequestTest {
        public static void main(String[] args) {
          Class<TestController> c = TestController.class;
          MyRequestMapping baseRequestMapping = c.getAnnotation((MyRequestMapping.class));
          // 输出value的值
          System.out.println(baseRequestMapping.value());
        }
      }
  • 使用 getAnnotations() 方法获取多个注解

    1. 自定义 Person 注解

      @Target({ElementType.TYPE})
      @Inherited
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Person1 {
        String value();
      }
    2. 创建TestController1类

      @MyRequestMapping("/test")
      @Person1("C")
      public class TestController1 {
        public void test() {
          System.out.println("进入Test方法");
        }
      }
    3. 测试类

      public class Person1Test {
        public static void main(String[] args) {
          Class<?> c = TestController1.class;
          Annotation[] atnsArray = c.getAnnotations();
          for (Annotation an : atnsArray) {
            System.out.println(an);
          }
        }
      }

文章作者: Ponzio
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ponzio !
  目录