1. 枚举
1.1 为什么需要枚举
- 有些业务的类需要包含固定数量的对象,普通类的实现无法限制对象的数量,可随意创建非法对象
- 提供 set 方法,属性可被修改,无法保证只读
- 因此需要有一种方法可以创建一种特殊的类,里面只包含有限的、固定的对象。
class Season{ private String name; private String desc; public Season(String name, String desc) { this.name = name; this.desc = desc; } // getter和setter public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } } public class Enumeration01 { public static void main(String[] args) { Season spring = new Season("春天", "温暖"); Season winter = new Season("冬天", "寒冷"); // 致命问题1:可以随意创建非法对象,比如"红天" Season other = new Season("红天", "~~~"); // 致命问题2:可以随意修改属性,破坏只读特性 autumn.setDesc("非常的热.."); } }1.2 自定义类实现枚举
- 构造器私有化,禁止外部 new 对象
- 移除 set 方法,保证枚举对象只读不可修改
- 类内部创建固定的
public static final对象,对外暴露 - 可以提供 get 方法,不提供 set 方法
package com.hspedu.enum_; public class Enumeration02 { public static void main(String[] args) { // 只能通过类名获取固定对象,无法new System.out.println(Season.AUTUMN); System.out.println(Season.SPRING); } } // 自定义枚举类 class Season { private String name; private String desc; // 1. 构造器私有化:防止外部new对象 private Season(String name, String desc) { this.name = name; this.desc = desc; } // 2. 去掉set方法:防止属性被修改,保证只读 public String getName() { return name; } public String getDesc() { return desc; } // 3. 类内部创建固定的对象,用public static final修饰,对外暴露 public static final Season SPRING = new Season("春天", "温暖"); public static final Season WINTER = new Season("冬天", "寒冷"); public static final Season AUTUMN = new Season("秋天", "凉爽"); public static final Season SUMMER = new Season("夏天", "炎热"); // 重写toString,方便打印 @Override public String toString() { return "Season{" + "name='" + name + '\'' + ", desc='" + desc + '\'' + '}'; } }1.3 enum 关键字实现枚举
JDK5.0 提供了enum关键字,简化枚举类的开发,用于表示一组固定且有限的常量。
- 使用
enum定义的枚举类,默认隐式继承Enum类,且是final类(通过javap反编译可验证) - 传统的
public static final ...简化为常量名(实参列表),本质还是调用构造器 - 无参构造器创建枚举对象时,实参列表和小括号可以省略,例如:
BOY, GIRL; - 多个枚举对象用
,分隔,最后一个对象结尾用; - 枚举对象必须放在枚举类的行首
- 因为已经继承了
Enum类,Java 是单继承机制,所以enum枚举类不能再继承其他类 enum枚举类和普通类一样,可以实现多个接口
package com.hspedu.enum_; public class Enumeration03 { public static void main(String[] args) { System.out.println(Season2.AUTUMN); System.out.println(Season2.SUMMER); } } // 使用enum关键字定义枚举类 enum Season2 { // 1. 枚举对象必须写在类的最前面,多个用逗号分隔,分号结尾 // 2. 简化写法:常量名(实参列表) 等价于 public static final Season2 SPRING = new Season2("春天", "温暖") SPRING("春天", "温暖"), WINTER("冬天", "寒冷"), AUTUMN("秋天", "凉爽"), SUMMER("夏天", "炎热"); private String name; private String desc; // 构造器默认private,可省略不写 private Season2(String name, String desc) { this.name = name; this.desc = desc; } // getter方法,无setter public String getName() { return name; } public String getDesc() { return desc; } @Override public String toString() { return "Season2{" + "name='" + name + '\'' + ", desc='" + desc + '\'' + '}'; } }1.4 enum 枚举的 6 个常用方法
| 方法名 | 作用 |
|---|---|
toString() | 返回当前枚举常量的名称,子类可重写,用于返回对象属性信息 |
name() | 返回当前枚举常量的名称(定义时的常量名),子类不能重写 |
ordinal() | 返回当前枚举常量的声明次序(编号),默认从 0 开始 |
values() | 返回当前枚举类中所有的枚举常量,返回值为枚举数组 |
valueOf(String name) | 将字符串转换成枚举对象,要求字符串必须是已有的常量名,否则抛异常 |
compareTo(E o) | 比较两个枚举常量,比较的是声明编号的差值 |
package com.hspedu.enum_; public class EnumMethod { public static void main(String[] args) { Season2 autumn = Season2.AUTUMN; // 1. name():返回枚举常量名 System.out.println(autumn.name()); // 输出:AUTUMN // 2. ordinal():返回声明次序,从0开始(SPRING=0,WINTER=1,AUTUMN=2,SUMMER=3) System.out.println(autumn.ordinal()); // 输出:2 // 3. values():返回所有枚举常量数组 Season2[] values = Season2.values(); System.out.println("===遍历枚举常量==="); for (Season2 season : values) { System.out.println(season); } // 4. valueOf():字符串转枚举对象 Season2 autumn1 = Season2.valueOf("AUTUMN"); System.out.println("autumn1=" + autumn1); System.out.println(autumn == autumn1); // 输出:true // 5. compareTo():比较编号,AUTUMN编号2 - SUMMER编号3 = -1 System.out.println(Season2.AUTUMN.compareTo(Season2.SUMMER)); // 输出:-1 } }2. 注解
注解(Annotation)也叫元数据(Metadata),用于修饰、解释包、类、方法、属性、构造器、局部变量等程序元素。
2.1 注解和注释的区别
| 特性 | 注解 | 注释 |
|---|---|---|
| 作用对象 | 编译器、JVM、框架 | 给程序员看的 |
| 生效阶段 | 编译期 / 运行期 | 仅源码阶段 |
| 功能 | 可被程序读取,实现逻辑控制 | 仅代码说明,无程序功能 |
| 语法 | 以@开头,有固定语法 | 单行//、多行/* */、文档/** */ |
2.2 @Override
限定某个方法,是重写父类的方法,该注解只能用于方法。
- 写了
@Override注解,编译器会编译期校验该方法是否真的重写了父类的方法,若没有构成重写,直接编译报错 - 不写
@Override,只要方法符合重写规则,仍然构成重写 - 只能修饰方法,不能修饰类、包、属性等
- 源码中
@Target(ElementType.METHOD),说明只能修饰方法
package com.hspedu.annotation_; public class Override_ { public static void main(String[] args) { Son son = new Son(); son.fly(); } } class Father { public void fly() { System.out.println("Father fly..."); } public void say() {} } class Son extends Father { // 标记该方法是重写父类的fly方法 @Override public void fly() { System.out.println("Son fly...."); } @Override public void say() {} }2.3 @Deprecated
标记某个程序元素(类、方法、字段、构造器等)已过时,不推荐使用,但仍然可以使用。
- 用于版本升级的兼容过渡,提示开发者该元素有新的替代方案
- 可以修饰类、方法、字段、包、参数等几乎所有程序元素
- 被标记的元素,IDE 中会显示删除线
package com.hspedu.annotation_; public class Deprecated_ { public static void main(String[] args) { A a = new A(); a.hi(); // 调用过时方法,IDE会显示删除线 System.out.println(a.n1); } } // 标记类已过时 @Deprecated class A { // 标记字段已过时 @Deprecated public int n1 = 10; // 标记方法已过时 @Deprecated public void hi() { System.out.println("hi"); } }2.4 @SuppressWarnings
抑制编译器警告,消除代码中黄色的警告提示,让代码更整洁。
- 作用范围和放置位置相关:放在类上,抑制整个类的警告;放在方法上,抑制方法内的警告
- 可以通过
value参数指定要抑制的警告类型,常用的有:all:抑制所有警告unchecked:抑制未检查的泛型警告rawtypes:抑制未指定泛型的 raw 类型警告unused:抑制未使用变量 / 代码的警告deprecation:抑制过时元素的警告
package com.hspedu.annotation_; import java.util.ArrayList; import java.util.List; // 抑制整个类的rawtypes、unchecked、unused警告 @SuppressWarnings({"rawtypes", "unchecked", "unused"}) public class SuppressWarnings_ { public static void main(String[] args) { List list = new ArrayList(); list.add("jack"); list.add("tom"); // 未使用的变量,不会报警告 int i; System.out.println(list.get(1)); } public void f1() { // 仅抑制当前方法的rawtypes警告 @SuppressWarnings({"rawtypes"}) List list = new ArrayList(); list.add("jack"); } }2.5 元注解
元注解是修饰注解的注解,用于给自定义注解指定规则,开发中主要用于看源码,不用深入研究。
| 元注解 | 作用 |
|---|---|
@Retention | 指定注解的保留范围,3 个取值:1.SOURCE:仅源码阶段,编译后丢弃2.CLASS:编译到 class 文件,JVM 运行时不保留(默认值)3.RUNTIME:编译到 class 文件,JVM 运行时保留,可通过反射获取 |
@Target | 指定注解可以修饰哪些程序元素(类、方法、字段等),通过ElementType枚举指定 |
@Documented | 指定该注解会被javadoc工具提取成 API 文档 |
@Inherited | 指定该注解具有继承性,父类使用了该注解,子类会自动继承 |
3. 异常
Java 中,程序执行过程中发生的不正常情况(非语法错误、非逻辑错误)称为异常。
3.1 异常的体系结构
- 异常分为两大类:Error(错误)和Exception(异常)
- Exception 又分为:运行时异常(程序运行时触发)和编译时异常(编译期就必须处理)
- 运行时异常是
RuntimeException及其子类,编译时异常是其他 Exception 子类
java.lang.Object └── java.lang.Throwable ├── Error(错误:JVM级别的严重问题,无法处理) │ ├── StackOverflowError(栈溢出) │ └── OutOfMemoryError(OOM,内存溢出) └── Exception(异常:可通过代码处理的一般性问题) ├── RuntimeException(运行时异常,非受检异常) │ ├── NullPointerException(空指针异常) │ ├── ArithmeticException(算术异常) │ ├── ArrayIndexOutOfBoundsException(数组下标越界) │ ├── ClassCastException(类型转换异常) │ └── NumberFormatException(数字格式异常) └── 编译时异常(受检异常,CheckedException) ├── IOException(IO异常) ├── FileNotFoundException(文件不存在异常) ├── ClassNotFoundException(类不存在异常) └── SQLException(数据库操作异常)3.2 运行异常
- 定义:
java.lang.RuntimeException及其子类,编译器不会检查,即使不处理也能编译通过,运行时才会触发。通常是编程逻辑错误,程序员应该主动避免。 - 特点:Java 不要求必须显式处理,默认会以
throws的方式向上抛出,最终交给 JVM 处理(打印异常栈信息,终止程序)。
常见运行异常
| 异常类型 | 触发场景 | 代码案例 |
|---|---|---|
NullPointerException空指针异常 | 对null的对象调用方法 / 属性 | java String name = null; System.out.println(name.length()); // 触发空指针 |
ArithmeticException算术异常 | 错误的数学运算,最常见整数除以 0 | java int num1 = 10; int num2 = 0; int res = num1 / num2; // 触发算术异常 |
ArrayIndexOutOfBoundsException数组下标越界异常 | 使用非法索引访问数组(索引为负 / 大于等于数组长度) | java int[] arr = {1,2,4}; // 数组长度3,索引最大为2 for (int i = 0; i <= arr.length; i++) { System.out.println(arr[i]); // i=3时触发越界 } |
ClassCastException类型转换异常 | 强制类型转换时,对象不是目标类型的实例 | java A b = new B(); // 向上转型 C c2 = (C)b; // B和C无继承关系,触发类型转换异常 class A {} class B extends A {} class C extends A {} |
NumberFormatException数字格式异常 | 字符串无法转换为对应数值类型 | java String name = "韩顺平教育"; int num = Integer.parseInt(name); // 非数字字符串转int,触发异常 |
3.3 编译异常
- 定义:也叫受检异常(CheckedException),编译器在编译期就会检查,必须显式处理(try-catch/throws),否则代码无法编译通过。
- 特点:通常是外部因素导致的异常(如文件不存在、网络中断),不是程序逻辑错误,必须提前处理。
常见的编译时异常
IOException:操作文件 / 流时发生的 IO 异常FileNotFoundException:操作不存在的文件时触发,是 IOException 的子类ClassNotFoundException:加载类时,类不存在触发SQLException:操作数据库时发生的异常
3.4 异常处理方式
为了保证当异常发生时,不让程序直接崩溃,而是针对性处理,保证程序继续运行,Java 提供了两种处理方式:try-catch-finally和throws。
3.4.1 try-catch-finally 异常捕获
程序员在代码中捕获异常,自行处理,是最常用的异常处理方式。
基本语法
try { // 可疑代码:可能发生异常的代码 } catch (异常类型1 异常对象名) { // 捕获到异常类型1时,执行的处理逻辑 } catch (异常类型2 异常对象名) { // 捕获到异常类型2时,执行的处理逻辑 } finally { // 可选:不管try中是否发生异常,始终会执行的代码 // 通常用于释放资源:关闭文件、关闭数据库连接等 }使用细节
- 异常发生后的执行逻辑:异常发生后,try 块中异常位置之后的代码不会执行,直接进入匹配的 catch 块。
- 无异常的执行逻辑:try 块中代码全部正常执行,不会进入任何 catch 块。
- 多个 catch 的规则:可以有多个 catch 块捕获不同异常,子类异常必须写在前面,父类异常写在后面,否则编译报错。异常发生后,只会匹配第一个符合的 catch 块。
try { Person person = new Person(); person = null; System.out.println(person.getName()); // 空指针 int res = 10 / 0; // 算术异常 } catch (NullPointerException e) { // 子类异常在前 System.out.println("空指针异常"); } catch (ArithmeticException e) { // 子类异常在前 System.out.println("算术异常"); } catch (Exception e) { // 父类异常在后 System.out.println("其他异常"); }- finally 的执行特性:不管 try 中是否发生异常、是否有 return,finally 块始终会执行(唯一不执行的情况:
System.exit(0)终止 JVM)。 - try-finally 配合使用:可以没有 catch 块,只使用 try-finally。这种用法不会捕获异常,程序会直接崩溃,但能保证必须执行的业务逻辑(如释放资源)。
try { int res = 10 / 0; } finally { System.out.println("必须执行的代码"); } // 异常发生后,finally执行完毕,程序依然会崩溃- try/catch 中的 return 语句,会先计算返回值并保存临时变量,然后执行 finally 块,最后再 return,此时return的是临时变量。
- 但是如果 finally 块中有 return 语句,会直接覆盖 try/catch 中的 return,最终返回 finally 中的值。
3.4.2 throws 异常抛出
如果一个方法执行时可能生成异常,但无法确定如何处理,该方法可以显式声明抛出异常,将异常交给方法的调用者处理,最顶级的处理者是 JVM,JVM 处理异常时会打印异常堆栈信息,并终止程序。
基本语法
// 在方法声明处,通过throws关键字声明抛出的异常类型,多个异常用逗号分隔。 // 声明方法会抛出FileNotFoundException,交给调用者处理 public static void readFile(String file) throws FileNotFoundException { // 读文件操作,可能抛出FileNotFoundException FileInputStream fis = new FileInputStream("d://aa.txt"); }使用细节
- 编译异常必须处理:对于编译时异常,程序中必须处理,要么
try-catch,要么throws,否则编译报错。 - 运行时异常默认处理:对于运行时异常,程序中如果没有处理,默认就是
throws的方式向上抛出,无需显式声明。 - 子类重写规则:子类重写父类的方法时,抛出的异常类型必须和父类一致,或为父类抛出异常的子类型,不能抛出父类没有声明的更大范围的异常。
- 异常传递规则:方法 A 调用了声明抛出异常的方法 B,方法 A 要么处理这个异常,要么继续向上抛出。
3.4.3 自定义异常
当程序中出现了特定的业务错误,而该错误没有在 JDK 的 Throwable 子类中描述时,我们可以自己设计异常类,用于描述该业务错误信息。
实现步骤
- 定义自定义异常类,继承
Exception或RuntimeException - 如果继承
Exception,属于编译时异常,必须显式处理 - 如果继承
RuntimeException,属于运行时异常,推荐使用(可以使用默认的处理机制,更灵活) - 编写构造器,通过
super(message)传递异常提示信息
package com.hspedu.customexception_; public class CustomException { public static void main(String[] args) { int age = 180; if(!(age >= 18 && age <= 120)) { // 手动抛出自定义异常 throw new AgeException("年龄需要在18~120 之间"); } System.out.println("你的年龄范围正确."); } } // 自定义异常:继承RuntimeException,运行时异常 class AgeException extends RuntimeException { // 构造器,接收异常信息 public AgeException(String message) { super(message); } }3.4.4 throw 和 throws 关键字的区别
| 维度 | throws | throw |
|---|---|---|
| 核心意义 | 异常处理的一种方式,声明方法会抛出的异常 | 手动生成并抛出异常对象的关键字 |
| 位置 | 方法声明处,跟在方法名后面 | 方法体内部 |
| 后面跟的内容 | 异常类型(可以多个,逗号分隔) | 异常对象(只能一个) |
| 作用 | 告诉调用者方法可能发生的异常,让调用者提前处理 | 主动触发异常,将异常对象交给调用者 |