第一节 Java程序的结构

Java 程序的结构通常由以下几个关键部分组成

  1. 包声明 (Package Declaration)
  • 每个 Java 类都可以属于一个包,通过包来组织类和接口。包声明在文件的第一行。
  • 例如:package com.example.myapp;
  1. 导入语句 (Import Statements)
  • 用于导入其他包中的类,以便在当前类中使用。
  • 例如:import java.util.List;
  1. 类定义 (Class Definition)
  • 每个 Java 程序至少有一个类,类是 Java 程序的基本单位。类定义包含类名、成员变量、构造方法和方法。
  • 例如:public class MyClass { ... }
  1. 成员变量 (Fields)
  • 定义类的属性。
  • 例如:private int number;
  1. 构造方法 (Constructors)
  • 用于创建类的实例。
  • 例如:public MyClass(int number) { this.number = number; }
  1. 方法 (Methods)
  • 定义类的行为。
  • 例如:public void setNumber(int number) { this.number = number; }
  1. 主方法 (Main Method)
  • 程序的入口点。每个 Java 应用程序都需要一个 main 方法来启动程序。
  • 例如:public static void main(String[] args) { ... }

下面是一个简单的 Java 程序示例,展示了这些部分如何组合在一起:

package com.example.myapp; // 包声明

import java.util.ArrayList; // 导入语句
import java.util.List;

public class MyClass { // 类定义
    private int number; // 成员变量
    private List<String> names;

    // 构造方法
    public MyClass(int number) {
        this.number = number;
        this.names = new ArrayList<>();
    }

    // 方法
    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public void addName(String name) {
        names.add(name);
    }

    public List<String> getNames() {
        return names;
    }

    // 主方法
    public static void main(String[] args) {
        MyClass myClass = new MyClass(10);
        myClass.addName("Alice");
        myClass.addName("Bob");

        System.out.println("Number: " + myClass.getNumber());
        System.out.println("Names: " + myClass.getNames());
    }
}

Java 程序结构的详细说明

  1. 包声明 (Package Declaration)
  • 包声明用于将类组织到命名空间中,可以避免命名冲突,并使类更易于管理。
  • 包名通常是小写的,并且遵循反向域名系统命名约定。
  1. 导入语句 (Import Statements)
  • import 语句使我们可以使用其他包中的类,而无需指定它们的完全限定名。
  • 例如,import java.util.ArrayList; 允许我们直接使用 ArrayList 而不是 java.util.ArrayList
  1. 类定义 (Class Definition)
  • public class MyClass 表示定义了一个公共类 MyClass
  • 类定义的主体包含成员变量、构造方法和方法。
  1. 成员变量 (Fields)
  • 成员变量是类的属性,每个对象都具有这些属性。
  • 它们可以有不同的访问修饰符(如 private, public, protected)。
  1. 构造方法 (Constructors)
  • 构造方法在创建对象时被调用,用于初始化对象。
  • 构造方法的名称必须与类名相同,并且没有返回类型。
  1. 方法 (Methods)
  • 方法定义了类的行为或功能。
  • 方法可以有返回值,也可以是 void 类型,没有返回值。
  • 访问修饰符(如 public, private)定义了方法的可见性。
  1. 主方法 (Main Method)
  • public static void main(String[] args) 是 Java 程序的入口点。
  • main 方法必须是 publicstatic,返回类型为 void,参数是一个 String 数组。

代码示例说明

  • 包声明package com.example.myapp; 将类 MyClass 放在 com.example.myapp 包中。
  • 导入语句import java.util.ArrayList;import java.util.List; 导入了 ArrayListList 类,方便在程序中使用。
  • 类定义public class MyClass 定义了一个公共类 MyClass
  • 成员变量private int number;private List<String> names; 定义了 numbernames 成员变量。
  • 构造方法public MyClass(int number) { ... } 是类的构造方法,用于初始化 numbernames
  • 方法getNumber, setNumber, addName, getNames 是类的方法,提供了对成员变量的访问和操作。
  • 主方法public static void main(String[] args) 是程序的入口点,创建了 MyClass 的实例,并调用其方法。

通过这种结构,Java 程序可以清晰地组织代码,方便维护和扩展。

第二节 流程控制

Java 条件语句 - if...else

Java 中的条件语句允许程序根据条件的不同执行不同的代码块。

if语句

一个 if 语句包含一个布尔表达式和一条或多条语句。

语法:

if 语句的语法如下:

if(布尔表达式) {
	//如果布尔表达式为true将执行的语句 
}

如果布尔表达式的值为 true,则执行 if 语句中的代码块,否则执行 else 语句块后面的代码。

if...else语句

if 语句后面可以跟 else 语句,当 if 语句的布尔表达式值为 false 时,else 语句块会被执行。

语法:

if…else 的用法如下:

if(布尔表达式){
   //如果布尔表达式的值为true
}else{
   //如果布尔表达式的值为false
}
if...else if...else 语句

if 语句后面可以跟 else if…else 语句,这种语句可以检测到多种可能的情况。

使用 if,else if,else 语句的时候,需要注意下面几点:

  • if 语句至多有 1 个 else 语句,else 语句在所有的 else if 语句之后。
  • if 语句可以有若干个 else if 语句,它们必须在 else 语句之前。
  • 一旦其中一个 else if 语句检测为 true,其他的 else if 以及 else 语句都将跳过执行。

语法:

if...else 语法格式如下:

if(布尔表达式 1){
   //如果布尔表达式 1的值为true执行代码
}else if(布尔表达式 2){
   //如果布尔表达式 2的值为true执行代码
}else if(布尔表达式 3){
   //如果布尔表达式 3的值为true执行代码
}else {
   //如果以上布尔表达式都不为true执行代码
}
嵌套的 if…else 语句

使用嵌套的 if…else 语句是合法的。也就是说你可以在另一个 if 或者 else if 语句中使用 if 或者 else if 语句。

语法:

嵌套的 if…else 语法格式如下:

if(布尔表达式 1){
   ////如果布尔表达式 1的值为true执行代码
   if(布尔表达式 2){
      ////如果布尔表达式 2的值为true执行代码
   }
}

Java switch case 语句

switch case 语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支。

语法:

switch case 语句语法格式如下:

switch(expression){
    case value :
       //语句
       break; //可选
    case value :
       //语句
       break; //可选
    //你可以有任意数量的case语句
    default : //可选
       //语句
}

【添油加醋的Java基础】第三章 流程控制语句_夏明亮

switch case 语句有如下规则:

  • switch 语句中的变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。
  • switch 语句可以拥有多个 case 语句。每个 case 后面跟一个要比较的值和冒号。
  • case 语句中的值的数据类型必须与变量的数据类型相同,而且只能是常量或者字面常量。
  • 当变量的值与 case 语句的值相等时,那么 case 语句之后的语句开始执行,直到 break 语句出现才会跳出 switch 语句。
  • 当遇到 break 语句时,switch 语句终止。程序跳转到 switch 语句后面的语句执行。case 语句不必须要包含 break 语句。如果没有 break 语句出现,程序会继续执行下一条 case 语句,直到出现 break 语句。
  • switch 语句可以包含一个 default 分支,该分支一般是 switch 语句的最后一个分支(可以在任何位置,但建议在最后一个)。default 在没有 case 语句的值和变量值相等的时候执行。default 分支不需要 break 语句。

switch case 执行时,一定会先进行匹配,匹配成功返回当前 case 的值,再根据是否有 break,判断是否继续输出,或是跳出判断。

public class Test {
   public static void main(String args[]){
      //char grade = args[0].charAt(0);
      char grade = 'C';
 
      switch(grade)
      {
         case 'A' :
            System.out.println("优秀"); 
            break;
         case 'B' :
         case 'C' :
            System.out.println("良好");
            break;
         case 'D' :
            System.out.println("及格");
            break;
         case 'F' :
            System.out.println("你需要再努力努力");
            break;
         default :
            System.out.println("未知等级");
      }
      System.out.println("你的等级是 " + grade);
   }
}

以上代码编译运行结果如下:

良好
你的等级是 C

如果 case 语句块中没有 break 语句时,JVM 并不会顺序输出每一个 case 对应的返回值,而是继续匹配,匹配不成功则返回默认 case。

public class Test {
   public static void main(String args[]){
      int i = 5;
      switch(i){
         case 0:
            System.out.println("0");
         case 1:
            System.out.println("1");
         case 2:
            System.out.println("2");
         default:
            System.out.println("default");
      }
   }
}

以上代码编译运行结果如下:

default

如果 case 语句块中没有 break 语句时,匹配成功后,从当前 case 开始,后续所有 case 的值都会输出。

public class Test {
   public static void main(String args[]){
      int i = 1;
      switch(i){
         case 0:
            System.out.println("0");
         case 1:
            System.out.println("1");
         case 2:
            System.out.println("2");
         default:
            System.out.println("default");
      }
   }
}

以上代码编译运行结果如下:

1
2
default

如果当前匹配成功的 case 语句块没有 break 语句,则从当前 case 开始,后续所有 case 的值都会输出,如果后续的 case 语句块有 break 语句则会跳出判断。

public class Test {
   public static void main(String args[]){
      int i = 1;
      switch(i){
         case 0:
            System.out.println("0");
         case 1:
            System.out.println("1");
         case 2:
            System.out.println("2");
         case 3:
            System.out.println("3"); break;
         default:
            System.out.println("default");
      }
   }
}

以上代码编译运行结果如下:

1
2
3

Java 循环结构 - for, while 及 do...while

顺序结构的程序语句只能被执行一次。

如果您想要同样的操作执行多次,就需要使用循环结构。

Java中有三种主要的循环结构:

  • while 循环:判断条件,进入循环
  • do…while 循环:先做一次,再判断条件,进入循环
  • for 循环:可以精确控制循环次数

在 Java5 中引入了一种主要用于数组的增强型 for 循环。

while 循环

while是最基本的循环,它的结构为:

while( 布尔表达式 ) {
	//循环内容 
}

只要布尔表达式为 true,循环就会一直执行下去。

do…while 循环

对于 while 语句而言,如果不满足条件,则不能进入循环。但有时候我们需要即使不满足条件,也至少执行一次。

do…while 循环和 while 循环相似,不同的是,do…while 循环至少会执行一次。

do {
	//代码语句
}while(布尔表达式);

**注意:**布尔表达式在循环体的后面,所以语句块在检测布尔表达式之前已经执行了。 如果布尔表达式的值为 true,则语句块一直执行,直到布尔表达式的值为 false。

for循环

虽然所有循环结构都可以用 while 或者 do...while表示,但 Java 提供了另一种语句 —— for 循环,使一些循环结构变得更加简单。

for循环执行的次数是在执行前就确定的。语法格式如下:

for(初始化; 布尔表达式; 更新) {
	//代码语句 
}

关于 for 循环有以下几点说明:

  • 最先执行初始化步骤。可以声明一种类型,但可初始化一个或多个循环控制变量,也可以是空语句。
  • 然后,检测布尔表达式的值。如果为 true,循环体被执行。如果为false,循环终止,开始执行循环体后面的语句。
  • 执行一次循环后,更新循环控制变量。
  • 再次检测布尔表达式。循环执行上面的过程。
Java 增强 for 循环

Java5 引入了一种主要用于数组的增强型 for 循环。

Java 增强 for 循环语法格式如下:

for(声明语句 : 表达式) {
    //代码句子 
}

**声明语句:**声明新的局部变量,该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句块,其值与此时数组元素的值相等。

**表达式:**表达式是要访问的数组名,或者是返回值为数组的方法。

public class Test {
   public static void main(String[] args){
      int [] numbers = {10, 20, 30, 40, 50};
 
      for(int x : numbers ){
         System.out.print( x );
         System.out.print(",");
      }
      System.out.print("\n");
      String [] names ={"James", "Larry", "Tom", "Lacy"};
      for( String name : names ) {
         System.out.print( name );
         System.out.print(",");
      }
   }
}

以上实例编译运行结果如下:

10,20,30,40,50,
James,Larry,Tom,Lacy,
break 关键字

break 主要用在循环语句或者 switch 语句中,用来跳出整个语句块。

break 跳出最里层的循环,并且继续执行该循环下面的语句。

语法:

break 的用法很简单,就是循环结构中的一条语句:

break;

例子:

public class Test {
   public static void main(String[] args) {
      int [] numbers = {10, 20, 30, 40, 50};
 
      for(int x : numbers ) {
         // x 等于 30 时跳出循环
         if( x == 30 ) {
            break;  // 跳过30及以后全部循环,也就是跳出循环体
         }
         System.out.print( x );
         System.out.print("\n");
      }
   }
}

以上实例编译运行结果如下:

10
20
continue 关键字

continue 适用于任何循环控制结构中。作用是让程序立刻跳转到下一次循环的迭代。

在 for 循环中,continue 语句使程序立即跳转到更新语句。

在 while 或者 do…while 循环中,程序立即跳转到布尔表达式的判断语句。

语法:

continue 就是循环体中一条简单的语句:

continue;

例子:

public class Test {
   public static void main(String[] args) {
      int [] numbers = {10, 20, 30, 40, 50};
 
      for(int x : numbers ) {
         if( x == 30 ) {
        continue;  // 只跳过30这一次循环
         }
         System.out.print( x );
         System.out.print("\n");
      }
   }
}

以上实例编译运行结果如下:

10
20
40
50

第三节 简单的输入与输出

Scanner类

java.util.Scanner 是 Java5 的新特征,我们可以通过 Scanner 类来获取用户的输入。

下面是创建 Scanner 对象的基本语法:

Scanner s = new Scanner(System.in);

接下来我们演示一个最简单的数据输入的示例,并通过 Scanner 类的 next() 与 nextLine() 方法获取输入的字符串,在读取前我们一般需要 使用 hasNext 与 hasNextLine 判断是否还有输入的数据:

使用next方法
import java.util.Scanner; 
 
public class ScannerDemo {
    public static void main(String[] args) {
        System.out.println("next方式接收:");
        // 从键盘接收数据
        // next方式接收字符串
        Scanner scan = new Scanner(System.in);
        // 判断是否还有输入
        if (scan.hasNext()) {
            String str1 = scan.next();
            System.out.println("输入的数据为:" + str1);
        }
        scan.close();
    }
}

执行结果:

next方式接收:     //输出
hello            //输入
输入的数据为:hello    //输出

截图如下:这个程序只是能从输入中读取一行并输出而已。

【添油加醋的Java基础】第三章 流程控制语句_流程控制_02

并且空格隔开的是数据也无法全部读取到:

【添油加醋的Java基础】第三章 流程控制语句_输入输出_03

使用 nextLine 方法
package com.xml.a;

import java.util.Scanner;

public class Test31 {
	public static void main(String[] args) {
		System.out.println("next方式接收:");
        // 从键盘接收数据
        // next方式接收字符串
        Scanner scan = new Scanner(System.in);
        // 判断是否还有输入
        if (scan.hasNext()) {
            String str1 = scan.next();
            System.out.println("输入的数据为:" + str1);
        }
        //scan.close();
        
        System.out.println("----------");
        // nextLine方式接收字符串
        System.out.println("nextLine方式接收:");
        // 判断是否还有输入
        if (scan.hasNextLine()) {
            String str2 = scan.nextLine();
            System.out.println("输入的数据为:" + str2);
        }
        scan.close();  
	}
}

执行结果:

//情况1
next方式接收:
hello Word!     // 输入
输入的数据为:hello  //输出
----------
nextLine方式接收:
输入的数据为: Word!  // 输出;当第一次输入了带有空格的字符串后,这里就自动直接输出空格及其后面的部分


// 情况2
next方式接收:
hello, world! xxx    //输入
输入的数据为:hello,  //输出
----------
nextLine方式接收:
输入的数据为: world! xxx  //输出

我们修改下程序:

package com.xml.a;

import java.util.Scanner;

public class Test31 {
	public static void main(String[] args) {   
        System.out.println("----------");
        // nextLine方式接收字符串
        System.out.println("nextLine方式接收:");
        Scanner scan1 = new Scanner(System.in);
        // 判断是否还有输入
        if (scan1.hasNextLine()) {
            String str2 = scan1.nextLine();
            System.out.println("输入的数据为:" + str2);
        }
        scan1.close();  
	}
}

执行结果:

----------
nextLine方式接收:
hello world 123;
输入的数据为:hello world 123;
next() 与 nextLine() 区别

next():

  • 1、一定要读取到有效字符后才可以结束输入。
  • 2、对输入有效字符之前遇到的空白,next() 方法会自动将其去掉。
  • 3、只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。
  • next() 不能得到带有空格的字符串。

nextLine():

  • 1、以Enter为结束符,也就是说 nextLine()方法返回的是输入回车之前的所有字符。
  • 2、可以获得空白。

如果要输入 int 或 float 类型的数据,在 Scanner 类中也有支持,但是在输入之前最好先使用 hasNextXxx() 方法进行验证,再使用 nextXxx() 来读取:

public class ScannerDemo {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        // 从键盘接收数据
        int i = 0;
        float f = 0.0f;
        System.out.print("输入整数:");
        if (scan.hasNextInt()) {
            // 判断输入的是否是整数
            i = scan.nextInt();
            // 接收整数
            System.out.println("整数数据:" + i);
        } else {
            // 输入错误的信息
            System.out.println("输入的不是整数!");
        }
        System.out.print("输入小数:");
        if (scan.hasNextFloat()) {
            // 判断输入的是否是小数
            f = scan.nextFloat();
            // 接收小数
            System.out.println("小数数据:" + f);
        } else {
            // 输入错误的信息
            System.out.println("输入的不是小数!");
        }
        scan.close();
    }
}

执行以上程序输出结果为:

$ javac ScannerDemo.java
$ java ScannerDemo
输入整数:12
整数数据:12
输入小数:1.2
小数数据:1.2

我们可以输入多个数字,并求其总和与平均数,每输入一个数字用回车确认,通过输入非数字来结束输入并输出执行结果:

import java.util.Scanner;
 
class Test {
    public static void main(String[] args) {
        System.out.println("请输入数字:");
        Scanner scan = new Scanner(System.in);
 
        double sum = 0;
        int m = 0;
 
        while (scan.hasNextDouble()) {
            double x = scan.nextDouble();
            m = m + 1;
            sum = sum + x;
        }
 
        System.out.println(m + "个数的和为" + sum);
        System.out.println(m + "个数的平均值是" + (sum / m));
        scan.close();
    }
}

执行结果:

$ javac ScannerDemo.java
$ java ScannerDemo
请输入数字:
12
23
15
21.4
end
4个数的和为71.4
4个数的平均值是17.85

System.out类

不换行与换行

System.out.print("Hello, World!");
System.out.println("This is a new line.");

与输入结合并实现字符串的拼接

Scanner scanner = new Scanner(System.in);
System.out.print("Enter your name: ");
String name = scanner.nextLine();
System.out.println("Hello, " + name + "!");

以下是一些有关 System 类 的其他信息:

  • System 类是最终类,这意味着它不能被子类化。
  • System 类的对象是单例的,这意味着只有一个 **System` 对象存在于 JVM 中。
  • System 类的方法都是静态的,这意味着无需创建对象即可调用它们。

希望这些信息对您有所帮助。如果您还有其他问题,请随时提出。

第四节 处理异常

概念

很多事件并非总是按照人们自己设计意愿顺利发展的,经常出现这样那样的异常情况。例如: 你计划周末郊游,计划从家里出发→到达目的→游泳→烧烤→回家。但天有不测风云,当你准备烧烤时候突然天降大雨,只能终止郊游提前回家。“天降大雨”是一种异常情况,你的计划应该考虑到这样的情况,并且应该有处理这种异常的预案。

在程序设计和运行的过程中,发生错误是不可避免的。尽管 Java 语言的设计从根本上提供了便于写出整洁、安全代码的方法,并且程序员也尽量地减少错误的产生,但是使程序被迫停止的错误的存在仍然不可避免。为此,Java 提供了异常处理机制来帮助程序员检查可能出现的错误,以保证程序的可读性和可维护性。

Java 将异常封装到一个类中,出现错误时就会拋出异常。

比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error,如果你用 System.out.println(11/0),那么你是因为你用 0 做了除数,会抛出 java.lang.ArithmeticException 的异常。

异常发生的原因有很多,通常包含以下几大类:

  • 用户输入了非法数据。
  • 要打开的文件不存在。
  • 网络通信时连接中断,或者JVM内存溢出。

这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。

要理解 Java 异常处理是如何工作的,你需要掌握以下三种类型的异常:

  • **检查性异常:**最具代表的检查性异常是用户错误或问题引起的异常,这些异常在编译时强制要求程序员处理。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
    这类异常通常使用 try-catch 块来捕获并处理异常,或者在方法声明中使用 throws 子句声明方法可能抛出的异常。
try {
    // 可能会抛出异常的代码
} catch (IOException e) {
    // 处理异常的代码
}

或者:

public void readFile() throws IOException {
    // 可能会抛出IOException的代码
}
  • 运行时异常: 这些异常在编译时不强制要求处理,通常是由程序中的错误引起的,例如 NullPointerException、ArrayIndexOutOfBoundsException 等,这类异常可以选择处理,但并非强制要求。
try {
    // 可能会抛出异常的代码
} catch (NullPointerException e) {
    // 处理异常的代码
}
  • 错误: 错误不是异常,而是脱离程序员控制的问题,错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

Java 提供了以下关键字和类来支持异常处理:

  • try:用于包裹可能会抛出异常的代码块。
  • catch:用于捕获异常并处理异常的代码块。
  • finally:用于包含无论是否发生异常都需要执行的代码块。
  • throw:用于手动抛出异常。
  • throws:用于在方法声明中指定方法可能抛出的异常。
  • Exception类:是所有异常类的父类,它提供了一些方法来获取异常信息,如 getMessage()、printStackTrace() 等。

Exception 类的层次

所有的异常类是从 java.lang.Exception 类继承的子类。

Exception 类是 Throwable 类的子类。除了Exception类外,Throwable还有一个子类Error 。

Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。

Error 用来指示运行时环境发生的错误。

例如,JVM 内存溢出。一般地,程序不会从错误中恢复。

异常类有两个主要的子类:IOException 类和 RuntimeException 类。

【添油加醋的Java基础】第三章 流程控制语句_异常_04

在 Java 内置类中,有大部分常用检查性非检查性异常(也称为受检异常Checked Exception和非受检异常Unchecked Exception)。

  • 受检异常是那些在编译时必须处理的异常。这意味着如果代码可能抛出受检异常,程序员必须在代码中显式地捕获和处理这些异常,或者在方法签名中声明它们。
  • 编译时检查:编译器会检查代码中是否对受检异常进行了处理。
  • 处理方式:必须使用 try-catch 语句捕获并处理,或者使用 throws 关键字在方法签名中声明抛出。
  • 典型例子
  • IOException
  • SQLException
  • ClassNotFoundException
  • 非受检异常是那些在编译时不需要强制处理的异常。它们包括运行时异常(Runtime Exception)及其子类,以及错误(Error)及其子类。
  • 编译时不检查:编译器不要求程序员必须捕获或声明非受检异常。
  • 处理方式:可以选择捕获和处理,但不强制。如果程序不处理,异常将传播到运行时。
  • 典型例子
  • NullPointerException
  • ArrayIndexOutOfBoundsException
  • ArithmeticException
  • IllegalArgumentException

如何区分?

  1. 继承层次
  • 所有从 java.lang.Exception 继承的异常都是受检异常,除了那些从 java.lang.RuntimeException 继承的异常。
  • 所有从 java.lang.RuntimeException 继承的异常都是非受检异常。
  • 所有从 java.lang.Error 继承的错误也是非受检异常。
  1. 编译器行为
  • 如果编译器在编译时要求你必须捕获或声明某个异常,它就是受检异常。
  • 如果编译器不强制你捕获或声明某个异常,它就是非受检异常。

使用建议

  • 受检异常:通常用于程序无法恢复的情况,例如文件未找到、网络连接失败等。这些异常应该通过适当的错误处理机制处理。
  • 非受检异常:通常用于编程错误,例如访问数组越界、空指针引用等。通常这些错误是由编程错误引起的,不应通过异常处理机制进行恢复,而应通过纠正代码来预防。

Java 内置异常类

Java 语言定义了一些异常类在 java.lang 标准包中。

标准运行时异常类的子类是最常见的异常类。由于 java.lang 包是默认加载到所有的 Java 程序的,所以大部分从运行时异常类继承而来的异常都可以直接使用。

Java 根据各个类库也定义了一些其他的异常,下面的表中列出了 Java 的非受检异常。

异常

描述

ArithmeticException

当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。

ArrayIndexOutOfBoundsException

用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。

ArrayStoreException

试图将错误类型的对象存储到一个对象数组时抛出的异常。

ClassCastException

当试图将对象强制转换为不是实例的子类时,抛出该异常。

IllegalArgumentException

抛出的异常表明向方法传递了一个不合法或不正确的参数。

IllegalMonitorStateException

抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。

IllegalStateException

在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。

IllegalThreadStateException

线程没有处于请求操作所要求的适当状态时抛出的异常。

IndexOutOfBoundsException

指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。

NegativeArraySizeException

如果应用程序试图创建大小为负的数组,则抛出该异常。

NullPointerException

当应用程序试图在需要对象的地方使用 null 时,抛出该异常

NumberFormatException

当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。

SecurityException

由安全管理器抛出的异常,指示存在安全侵犯。

StringIndexOutOfBoundsException

此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。

UnsupportedOperationException

当不支持请求的操作时,抛出该异常。

下面的表中列出了 Java 定义在 java.lang 包中的受检异常类。

异常

描述

ClassNotFoundException

应用程序试图加载类时,找不到相应的类,抛出该异常。

CloneNotSupportedException

当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。

IllegalAccessException

拒绝访问一个类的时候,抛出该异常。

InstantiationException

当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。

InterruptedException

一个线程被另一个线程中断,抛出该异常。

NoSuchFieldException

请求的变量不存在

NoSuchMethodException

请求的方法不存在

异常方法

下面的列表是 Throwable 类的主要方法:

序号

方法及说明

1

public String getMessage() 返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了。

2

public Throwable getCause() 返回一个 Throwable 对象代表异常原因。

3

public String toString() 返回此 Throwable 的简短描述。

4

public void printStackTrace() 将此 Throwable 及其回溯打印到标准错误流。

5

public StackTraceElement [] getStackTrace() 返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。

6

public Throwable fillInStackTrace() 用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。

捕获异常try/catch

try/catch

使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方。

try/catch代码块中的代码称为保护代码,使用 try/catch 的语法如下:

try
{
   // 程序代码
}catch(ExceptionName e1)
{
   //Catch 块
}

Catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。

如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数的方法是一样。

下面的例子中声明有两个元素的一个数组,当代码试图访问数组的第四个元素的时候就会抛出一个异常。

import java.io.*;
public class ExceptTest{
 
   public static void main(String args[]){
      try{
         int a[] = new int[2];
         System.out.println("Access element three :" + a[3]);
      }catch(ArrayIndexOutOfBoundsException e){
         System.out.println("Exception thrown  :" + e);
      }
      System.out.println("Out of the block");
   }
}

执行结果:

Exception thrown  :java.lang.ArrayIndexOutOfBoundsException: 3
Out of the block
多重捕获块

一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。

多重捕获块的语法如下所示:

try{
   // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}catch(异常类型3 异常的变量名3){
  // 程序代码
}

上面的代码段包含了 3 个 catch块。

可以在 try 语句后面添加任意数量的 catch 块。

如果保护代码中发生异常,异常被抛给第一个 catch 块。

如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。

如果不匹配,它会被传递给第二个 catch 块。

如此,直到异常被捕获或者通过所有的 catch 块。

因此,catch的顺序很重要,更精确的异常类型应该排在更前面的位置;否则,他们可能永远也不可能被执行到。

下面的示例展示了怎么使用多重 try/catch:

try {
    file = new FileInputStream(fileName);
    x = (byte) file.read();
} catch(FileNotFoundException f) { // Not valid!
    f.printStackTrace();
    return -1;
} catch(IOException i) {
    i.printStackTrace();
    return -1;
}
finally关键字

finally 关键字用来创建在 try 代码块后面执行的代码块。

无论是否发生异常,finally 代码块中的代码总会被执行。

在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。

finally 代码块出现在 catch 代码块最后,语法如下:

try{
  // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}finally{
  // 程序代码
}

例子:

public class ExceptTest1{
  public static void main(String args[]){
    int a[] = new int[2];
    try{
       System.out.println("Access element three :" + a[3]);
    }catch(ArrayIndexOutOfBoundsException e){
       System.out.println("Exception thrown  :" + e);
    }
    finally{
       a[0] = 6;
       System.out.println("First element value: " +a[0]);
       System.out.println("The finally statement is executed");
    }
  }
}

执行结果:

Exception thrown  :java.lang.ArrayIndexOutOfBoundsException: 3
First element value: 6
The finally statement is executed

注意下面事项:

  • catch 不能独立于 try 存在。
  • 在 try/catch 后面添加 finally 块并非强制性要求的。
  • try 代码后不能既没 catch 块也没 finally 块。
  • try, catch, finally 块之间不能添加任何代码。
try-with-resources

JDK7 之后,Java 新增的 try-with-resource 语法结构,旨在自动管理资源,确保资源在使用后能够及时关闭,避免资源泄露 。

try-with-resources 是一种异常处理机制,它能够自动关闭在 try 块中声明的资源,无需显式地在 finally 块中关闭。

try-with-resources 语句中,你只需要在 try 关键字后面声明资源,然后跟随一个代码块。无论代码块中的操作是否成功,资源都会在 try 代码块执行完毕后自动关闭。

try (resource declaration) {
  // 使用的资源
} catch (ExceptionType e1) {
  // 异常块
}

以上的语法中 try 用于声明和实例化资源,catch 用于处理关闭资源时可能引发的所有异常。

**注意:**try-with-resources 语句关闭所有实现 AutoCloseable 接口的资源。

import java.io.*;

public class Test {

    public static void main(String[] args) {
    String line;
        try(BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
            while ((line = br.readLine()) != null) {
                System.out.println("Line =>"+line);
            }
        } catch (IOException e) {
            System.out.println("IOException in try block =>" + e.getMessage());
        }
    }
}

输出结果为:

IOException in try block =>test.txt (No such file or directory)

以上实例中,我们实例一个 BufferedReader 对象从 test.txt 文件中读取数据。

在 try-with-resources 语句中声明和实例化 BufferedReader 对象,执行完毕后实例资源,不需要考虑 try 语句是正常执行还是抛出异常。

如果发生异常,可以使用 catch 来处理异常。

再看下不使用 try-with-resources 而改成 finally 来关闭资源,整体代码量多了很多,而且更复杂繁琐了:

import java.io.*;

class Test1 {
    public static void main(String[] args) {
        BufferedReader br = null;
        String line;

        try {
            System.out.println("Entering try block");
            br = new BufferedReader(new FileReader("test.txt"));
            while ((line = br.readLine()) != null) {
            System.out.println("Line =>"+line);
            }
        } catch (IOException e) {
            System.out.println("IOException in try block =>" + e.getMessage());
        } finally {
            System.out.println("Entering finally block");
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                System.out.println("IOException in finally block =>"+e.getMessage());
            }
        }
    }
}

输出结果为:

Entering try block
IOException in try block =>test.txt (No such file or directory)
Entering finally block
try-with-resources 处理多个资源

try-with-resources 语句中可以声明多个资源,方法是使用分号 ; 分隔各个资源:

import java.io.*;
import java.util.*;
class Test2 {
    public static void main(String[] args) throws IOException{
        try (Scanner scanner = new Scanner(new File("testRead.txt")); 
            PrintWriter writer = new PrintWriter(new File("testWrite.txt"))) {
            while (scanner.hasNext()) {
                writer.print(scanner.nextLine());
            }
        }
    }
}

实例使用 Scanner 对象从 testRead.txt 文件中读取一行并将其写入新的 testWrite.txt 文件中。

多个声明资源时,try-with-resources 语句以相反的顺序关闭这些资源。 在本例中,PrintWriter 对象先关闭,然后 Scanner 对象关闭。

throws/throw 关键字

在Java中, throwthrows 关键字也是用于处理异常的。

throw 关键字用于在代码中抛出异常(一般是逻辑上判断出来的异常),而 throws 关键字用于在方法声明中指定可能会抛出的异常类型。

throw 关键字

throw 关键字用于在当前方法中抛出一个异常。

通常情况下,当代码执行到某个条件下无法继续正常执行时,可以使用 throw 关键字抛出异常,以告知调用者当前代码的执行状态。

例如,下面的代码中,在方法中判断 num 是否小于 0,如果是,则抛出一个 IllegalArgumentException 异常。

public void checkNumber(int num) {
  if (num < 0) {
    throw new IllegalArgumentException("Number must be positive");
  }
}
throws 关键字

throws 关键字用于在方法声明中指定该方法可能抛出的异常。当方法内部抛出指定类型的异常时,该异常会被传递给调用该方法的代码,并在该代码中处理异常。

例如,下面的代码中,当 readFile 方法内部发生 IOException 异常时,会将该异常传递给调用该方法的代码。在调用该方法的代码中,必须捕获或声明处理 IOException 异常。

public void readFile(String filePath) throws IOException {
  BufferedReader reader = new BufferedReader(new FileReader(filePath));  //后面输入输出流章节里会讲
  String line = reader.readLine();
  while (line != null) {
    System.out.println(line);
    line = reader.readLine();
  }
  reader.close();
}

一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。

例如,下面的方法声明抛出 RemoteException 和 InsufficientFundsException:

import java.io.*;
public class className{
   public void withdraw(double amount) throws RemoteException, InsufficientFundsException{
       // Method implementation
   }
   //Remainder of class definition
}

声明自定义异常

在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。

  • 所有异常都必须是 Throwable 的子类。
  • 如果希望写一个检查性异常类,则需要继承 Exception 类。
  • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。

可以像下面这样定义自己的异常类:

class MyException extends Exception{
    // my code
}

只继承Exception 类来创建的异常类是检查性异常类

下面的 InsufficientFundsException 类是用户定义的异常类,它继承自 Exception

一个异常类和其它任何类一样,包含有变量和方法。

以下实例是一个银行账户的模拟,通过银行卡的号码完成识别,可以进行存钱和取钱的操作。

文件1:InsufficientFundsException.java

// 文件名InsufficientFundsException.java
import java.io.*;
 
//自定义异常类,继承Exception类
public class InsufficientFundsException extends Exception
{
  //此处的amount用来储存当出现异常(取出钱多于余额时)所缺乏的钱
  private double amount;
  public InsufficientFundsException(double amount)
  {
    this.amount = amount;
  } 
  public double getAmount()
  {
    return amount;
  }
}

为了展示如何使用我们自定义的异常类,

在下面的 CheckingAccount 类中包含一个 withdraw() 方法抛出一个 InsufficientFundsException 异常。

文件2:CheckingAccount.java

// 文件名称 CheckingAccount.java
import java.io.*;
 
//此类模拟银行账户
public class CheckingAccount
{
  //balance为余额,number为卡号
   private double balance;
   private int number;
   public CheckingAccount(int number)
   {
      this.number = number;
   }
  //方法:存钱
   public void deposit(double amount)
   {
      balance += amount;
   }
  //方法:取钱
   public void withdraw(double amount) throws InsufficientFundsException // 可能会抛出InsufficientFundsException异常
   {
      if(amount <= balance) // 取钱金额小于等于余额
      {
         balance -= amount;
      }
      else  // 取钱金额大于余额或者其他情况
      {
         double needs = amount - balance;  // 缺少的金额
         throw new InsufficientFundsException(needs);  // 携带缺少的金额抛出异常InsufficientFundsException
      }
   }
  //方法:返回余额
   public double getBalance()
   {
      return balance;
   }
  //方法:返回卡号
   public int getNumber()
   {
      return number;
   }
}

下面的 BankDemo 程序示范了如何调用 CheckingAccount 类的 deposit()withdraw() 方法。

文件3:BankDemo.java

//文件名称 BankDemo.java
public class BankDemo
{
   public static void main(String [] args)
   {
      CheckingAccount c = new CheckingAccount(101);  // 实例化CheckingAccount类的c;并通过构造函数指定卡号number为101
      System.out.println("Depositing $500...");
      c.deposit(500.00);  // 存款500
      try
      {
         System.out.println("\nWithdrawing $100...");
         c.withdraw(100.00);  // 先取款100
         System.out.println("\nWithdrawing $600...");
         c.withdraw(600.00); // 再取款600,这里会发生余额不足异常
      }catch(InsufficientFundsException e)  // 捕获到余额不足的异常
      {
         System.out.println("Sorry, but you are short $"
                                  + e.getAmount());  // 处理异常
         e.printStackTrace();
      }
    }
}

编译上面三个文件,并运行程序 BankDemo,得到结果如下所示:

Depositing $500...

Withdrawing $100...

Withdrawing $600...
Sorry, but you are short $200.0
InsufficientFundsException
        at CheckingAccount.withdraw(CheckingAccount.java:25)
        at BankDemo.main(BankDemo.java:13)

这里示例包含:

  1. InsufficientFundsException.java自定义异常类
  2. CheckingAccount.java模拟的银行账户类
  3. BankDemo.java使用上面2个文件中的类

异常处理的最佳实践

  • 在合适的位置捕获异常,并对异常进行适当的处理,以确保程序的稳定性和可靠性。
  • 避免过度捕获异常,应该尽量精确捕获特定类型的异常。
  • 使用finally块来释放资源,例如关闭文件或数据库连接等,以确保资源的正确释放。
  • 优先处理受检异常,避免将受检异常转换为非受检异常。

异常处理是编写健壮的 Java 应用程序的重要组成部分,合理地处理异常可以提高程序的可维护性和可靠性。

本章小结

习题