1、异常
1.1、定义
异常通常指的是程序在执行过程中遇到的非正常情况或错误条件,这些问题可能导致程序不能按照预期的方式继续执行。
在Java等面向对象的语言中,异常通常被看作是一类特殊的对象,这些对象是从java.lang.Throwable类派生出来的。Throwable有两个主要的子类:
1、Error:表示严重的问题,这些问题通常是程序无法处理的,比如内存不足或系统错误。一般情况下,这类错误不应该被捕获或处理,因为它们通常表明程序处于不可恢复的状态。
2、Exception:表示应用程序可以尝试捕获和处理的异常情况。Exception又分为两种类型:
● 编译时异常(Checked Exceptions):这些异常必须在编译时被声明或处理,否则编译器将拒绝编译代码。例如,IOException 和ClassNotFoundException
● 运行时异常(Unchecked Exceptions):这些异常不需要在编译时显式声明,因为它们通常是由于编程错误引起的,这类异常通常在编码时进行处理
常见的五大运行时异常如下:
1、NullPointerException:空指针异常
2、ArithmeticException:数字运算异常
3、ArrayIndexOutOfBoundsException:数组下标越界异常
4、ClassCastException:类型转换异常
5、NumberFormatException:数字格式异常
1.2、异常处理
异常处理机制是一种在程序中处理意外情况的方法,它允许程序在遇到错误时继续执行,而不是直接终止。在Java中,异常处理机制主要包括以下几个关键字:try, catch, finally, throw, 和 throws
- try:用于包围可能抛出异常的代码块
- catch:用于捕获try块中抛出的特定类型的异常
- finally:无论是否发生异常,finally块中的代码都会被执行,通常用于释放资源,且优先return
- throw:用于抛出一个异常,最终会被抛给JVM(此时就会由于异常未被处理而强制退出程序)
- throws:用于声明一个方法可能会抛出的异常,通知调用者需要处理这些异常
在一整个try...catch...finally 语句块中,如果在try块中的代码抛出了一个异常,那么控制权将传递给与该异常匹配的第一个catch块。如果没有异常被抛出,那么catch块将被跳过。并且不论是否有异常发生,finally块(如果存在的话)中的代码总是会被执行(类似于之前提到的分支控制中的switch 语句)
比如一个读取文件流程的异常处理如下:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ExceptionHandlingExample {
public static void main(String[] args) {
String fileName = "example.txt";
try {
BufferedReader br = new BufferedReader(new FileReader(fileName));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
System.out.println("文件未找到: " + fileName);
} catch (IOException e) {
System.out.println("读取文件时发生错误: " + e.getMessage());
} finally {
System.out.println("无论是否成功读取文件,此消息都将显示。");
}
}
}
对于异常的处理除了使用try...catch...finally 之外,还可以使用throw 或throws 抛出具体的或者可能出现的异常,该异常将由方法调用者进行处理。比如对上例代码修改如下:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ExceptionHandlingExample {
public static void main(String[] args) throws IOException {
String fileName = "example.txt";
BufferedReader br = new BufferedReader(new FileReader(fileName));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
1.3、Throw 和Throws
throw 和 throws 在 Java 中都是用来处理异常的关键字,区别如下:
throw
1、使用位置:throw 关键字通常用在方法体内部
2、功能:用来显式地抛出一个异常对象。一旦执行到 throw 语句,当前方法就会立即停止执行,并且将指定的异常对象传递给上层调用者。throw 后面跟着的是一个具体的异常对象
3、特点:执行 throw 语句时,一定会抛出异常,程序控制流会跳转到异常处理代码块(如 catch 块)或者如果未被处理则可能导致程序终止
throws
1、使用位置:throws 关键字用在方法签名之后
2、功能:用来声明一个方法可能会抛出的异常类型。这样做的目的是通知方法的调用者,该方法有可能抛出某些类型的异常,调用者需要准备好处理这些异常
3、特点:throws 后面跟着的是异常类的类型,可以是多个,用逗号分隔。使用 throws 并不意味着方法一定会抛出异常,而是说有这种可能性。调用者可以选择捕获并处理这些异常,或者再次声明抛出异常
总的来说,throw 是用来抛出具体的异常实例,而 throws 是用来声明方法可能抛出的异常类型。前者通常用于方法体内,后者用于方法声明
1.4、使用细节
1、对于编译时异常,必须在程序中进行处理。比如使用try..catch 或者throws
2、对于运行时异常,如果程序中没有特殊处理,则默认为throws 方式进行处理(JVM 兜底,中止程序并打印异常信息)
3、子类在重写父类方法时,要求抛出的异常与父类方法抛出的异常类型一致或者为其子类型
4、如果异常被手动捕获,即try..catch 进行处理了,就不用throws 抛出
1.4、自定义异常
自定义异常是在编程中为了更好地处理错误情况而创建的特定异常类。在Java中,自定义异常是指程序员根据自己的需要定义的异常类,这些类通常是java.lang.Exception类或者java.lang.RuntimeException类的子类
使用自定义异常的好处如下:
1、提供更具体的错误信息:当标准库提供的异常不足以表达程序中出现的错误情况时,自定义异常可以提供更详细的错误信息
2、区分应用程序特有的错误:自定义异常可以帮助区分应用程序特有的错误与系统级别的错误
3、改善代码可读性和可维护性:使用自定义异常可以使代码更加清晰易懂,便于维护
使用自定义异常的步骤如下:
1、定义一个新的类,这个类继承自Exception(检查异常)或RuntimeException(运行时异常)
2、为这个新类添加构造器,通常至少会有一个接受String参数的构造器,用于描述异常信息
3、根据需要添加其他成员变量或方法来提供更多关于异常的信息
// 自定义一个CustomException 异常类,继承自Exception
public class CustomException extends Exception {
// 包含一个String 类型参数的构造器,以提供异常描述信息
public CustomException(String message) {
// 最终会调用Throwable 类的构造方法,并输出该异常信息
super(message);
}
}
// 使用自定义的CustomException 异常
public void someMethod() throws CustomException {
// 如果someCondition 条件满足,则抛出该异常
if (someCondition) {
throw new CustomException("This is a custom exception.");
}
}
2、包装类
2.1、定义
在Java中,包装类(Wrapper Class)是一种特殊的类,它为Java的基本数据类型提供了对象包装。这意味着每一个基本数据类型都有一个对应的类,使得基本数据类型可以像对象一样被使用。这对于需要对象的地方(例如集合框架、泛型等)是非常有用的,因为基本数据类型本身不是对象,无法直接放入这些容器中
Java中的包装类包括:
1、Boolean 对应 boolean
2、Character 对应 char
3、Byte 对应 byte
4、Short 对应 short
5、Integer 对应 int
6、Long 对应 long
7、Float 对应 float
8、Double 对应 double
这些包装类位于 java.lang 包中,并且提供了多种有用的功能,比如:
1、装箱和拆箱:允许基本数据类型自动转换为相应的包装类对象,反之亦然。例如,Integer a = 1; 实际上是 Integer a = Integer.valueOf(1);
2、常量池:某些包装类(如 Integer)提供了一个常量池来存储常用的数值,从而提高性能
3、方法和属性:包装类提供了许多静态方法和属性,如最大值、最小值、转换方法等,使得操作基本数据类型变得更加方便
2.2、自动装箱和拆箱
自动装箱和拆箱是JDK 5.0 推出的特性,其特点如下:
自动装箱是指编译器自动将基本数据类型转换为对应的包装类对象。例如,当你尝试将一个 int 类型的值赋给 Integer 类型的变量时,编译器会自动调用 Integer.valueOf() 方法来创建一个 Integer 对象
int i = 10;
Integer integer = i; // 自动装箱
// 等价于以下代码
Integer integer = Integer.valueOf(i);
自动拆箱则是相反的过程,即编译器自动将包装类对象转换为基本数据类型。例如,当你尝试将一个 Integer 对象的值赋给 int 类型的变量时,编译器会自动调用 intValue() 方法来获取基本数据类型的值
Integer integer = 20;
int i = integer; // 自动拆箱
// 等价于以下代码
int i = integer.intValue();
使用自动装箱或自动拆箱的优点是:
1、简化代码:自动装箱和拆箱减少了手动调用 valueOf 和 intValue 等方法的需要,使得代码更简洁
2、提高开发效率:由于不需要显式地进行转换,开发人员可以更快地编写代码
尽管自动装箱和拆箱带来了便利,但也有一些需要注意的地方:
1、空指针异常:由于自动装箱会创建对象,所以包装类对象可以为 null。这意味着在使用自动装箱时,需要考虑到可能出现的空指针异常
2、性能影响:频繁的装箱和拆箱操作可能会导致额外的内存消耗和垃圾回收的压力,尤其是在大量使用这些操作的情况下
总之,自动装箱和拆箱是Java语言为了简化基本类型和其包装类之间的转换而引入的特性,使得编写代码更加方便和直观
2.3、Integer 缓存
Integer 类的缓存机制是为了优化小范围整数对象的创建和使用。在Java中,Integer 类提供了一个内部类 IntegerCache,用于缓存一定范围内的 Integer 对象。这一机制可以避免频繁创建和销毁相同值的对象,从而提高性能
缓存范围
默认情况下,Integer 类的缓存范围是从 -128 到 127。这意味着在这个范围内的所有整数对象都会被缓存起来,当需要创建这个范围内的整数对象时,会直接从缓存中获取,而不是新建对象。例如,Integer a = 100; 实际上是 Integer a = Integer.valueOf(100);,如果 100 在缓存范围内,那么 a 将指向缓存中的对象
缓存机制实现
Integer 类的缓存机制主要通过 IntegerCache 内部类实现。IntegerCache 包含一个静态数组,用于存储整数对象。这个数组在类加载时初始化,并且只初始化一次。当调用 Integer.valueOf(int i) 时,如果 i 在缓存范围内,那么就直接返回缓存中的对象;否则,创建一个新的 Integer 对象
高版本Java中的调整
在较新的Java版本中,Integer 类的缓存范围可以通过系统属性 java.lang.Integer.IntegerCache.high 来调整。这意味着你可以通过设置这个系统属性来改变缓存的最大值。例如,可以通过 -Djava.lang.Integer.IntegerCache.high=256 来设置缓存的最大值为 256
public class IntegerCacheExample {
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // 输出 true,因为 100 在缓存范围内
Integer c = 150; // 假设默认缓存范围是 -128 到 127
Integer d = 150;
System.out.println(c == d); // 输出 false,因为 150 超出了缓存
// 范围,每次调用 valueOf 会创建新对象
}
}
虽然使用缓存可以提高性能,但也需要注意以下几点:
1、缓存范围的选择应该基于应用程序的需求和性能考虑。
2、过大的缓存范围可能会导致内存占用增加。
3、缓存机制仅适用于 Integer 类,如果你使用的是基本数据类型 int,那么不会涉及对象缓存的问题。
2.4、方法
在Java中,String 是常被使用到的,而包装类和String提供了相互转换的方法。这些方法使得在处理基本数据类型和字符串时更加方便
1、包装类转字符串:toString()
2、字符串转包装类:parseXXX()
3、包装类和字符串相互转换:valueOf()
public class WrapperClassExample {
public static void main(String[] args) {
// 将基本数据类型转换为字符串
int i = 10;
String strInt = Integer.toString(i); // "10"
System.out.println("Integer to String: " + strInt);
// 将字符串转换为基本数据类型
String str = "100";
int parsedInt = Integer.parseInt(str); // 100
System.out.println("String to Integer: " + parsedInt);
// 使用 valueOf 方法
String strDouble = "3.14";
Double parsedDouble = Double.valueOf(strDouble); // 3.14
System.out.println("String to Double: " + parsedDouble);
// 使用 toString 方法
Double d = 3.14;
String strDouble2 = d.toString(); // "3.14"
System.out.println("Double to String: " + strDouble2);
}
}
除了与String 的相互转换之外,包装类中还提供了一些实用的方法,比如:
1、Integer 中的compare、min、max分别用于比较两个整数的大小以及获取较小、较大值
2、Character 中的toUpperCase、toLowerCase、isDigit分别用于转大写、小写以及判断是否为数字