第一章、异常处理机制(主要研究Exception异常)
一、概述
在Java中,异常处理机制用于处理程序运行过程中出现的异常情况,以保证程序的稳定性和可靠性。异常是程序运行时的一种不正常的情况,可能导致程序终止或产生不可预料的结果。Java提供了一套强大的异常处理机制,使开发人员能够捕获、处理和抛出异常。
异常就是程序执行过程中出现了不正常的情况,java是一个很完善的语言,提供了异常的处理方式,程序在执行过程中出现了不正常的情况,java就会把该异常信息输出到控制台,供程序员参考。程序员看到异常信息之后,可以对程序进行修改,让程序更健壮
package com.javase.exception;
/**
* 以下程序执行时再控制台出现了:
* Exception in thread "main" java.lang.ArithmeticException: / by zero
* at com.javase.exception.ExceptionTest01.main(ExceptionTest01.java:7)
* 这个信息就是异常信息,是JVM打印输出的
*/
public class ExceptionTest01 {
public static void main(String[] args) {
/**
* Exception in thread "main" java.lang.ArithmeticException: / by zero
* at com.javase.exception.ExceptionTest01.main(ExceptionTest01.java:7)
*/
int a = 10;
int b = 0;
//实际上JVM再执行到此处的时候,会new异常对象:new ArithmeticException("/ by zero")
//并且JVM将new的异常对象抛出,打印输出到控制台了
int c = a / b;
System.out.println(a + "/" + b + "=" + c);
//程序执行到此处又会new ArithmeticException对象
System.out.println(10 / 0);
/*//程序员在看到异常信息后,就会对代码进行调试
int a = 10;
int b = 0;
if(b == 0){
System.out.println("除数不能为0");
return;
}
//程序执行到此处,说明b不为0
int c = a / b;
System.out.println(a + "/" + b + "=" + c);*/
}
}
Java的异常处理机制主要涉及以下几个关键词和概念:
1、异常类(Exception Class)
在Java中,异常是通过异常类的实例来表示的。Java提供了一些内置的异常类,例如NullPointerException
、ArrayIndexOutOfBoundsException
等,同时也可以自定义异常类。
2、异常处理语句(Exception Handling Statement)
用于捕获和处理异常的语句块。主要包括try-catch
语句和finally
语句。
try
块:包含可能会抛出异常的代码块。当在try
块中发生异常时,异常对象会被抛出。catch
块:用于捕获和处理异常的代码块。可以在catch
块中指定捕获特定类型的异常,并执行相应的处理逻辑。finally
块:无论是否发生异常,finally
块中的代码都会被执行。通常用于释放资源或进行清理操作。
3、异常处理机制的工作流程
当发生异常时,程序会中断当前的执行流程,转而查找匹配的
catch
块来处理异常。如果找到匹配的catch
块,则执行相应的处理逻辑;如果找不到匹配的catch
块,则异常将被传递给上层调用栈,直到找到合适的异常处理机制。
4、异常处理的方式:
- 捕获异常(Catch Exception):使用
try-catch
语句捕获异常,提供异常处理逻辑,并阻止异常的继续传播。 - 上抛异常(Throw Exception):使用
throw
关键字抛出异常,将异常throws传递给调用者处理。 - 创建自定义异常类(Create Custom Exception):通过继承现有的异常类,创建自定义的异常类以满足特定需求。
5、异常处理的层级结构
Java中的异常类形成了一个层级结构,
Throwable
是所有异常类的根类,它有两个子类:Error
和Exception
。Error
表示严重的系统错误,通常不需要捕获和处理;Exception
表示程序中可能出现的异常情况,分为可检查异常(Checked Exception)和运行时异常(Runtime Exception)两种。
- 可检查异常(Checked Exception)编译时异常:编译器要求必须进行处理或声明抛出的异常,例如
IOException
。 - 运行时异常(Runtime Exception):不需要进行强制处理或声明的异常,通常由程序错误导致,例如
NullPointerException
、ArrayIndexOutOfBoundsException
。
Java的异常处理机制使得开发人员能够及时捕获和处理异常情况,避免程序崩溃或产生不可预料的结果。通过合理地使用异常处理,可以增加程序的稳定性和可靠性,并提供更好的错误提示和处理方式
二、异常的存在形式(类和对象的形式)
1、异常在java中以类的形式存在,每一个异常类都可以创建异常对象
2、当程序运行不正常时,JVM就会就会根据异常类创建异常对象并抛出,若程序中有多个相同的异常,则JVM会根据该异常类分别new出异常对象(虽然是同一个异常类,但是异常对象并不是同一个)并抛出
1、异常对应现实生活的关系
火灾(异常类)
- 2008年8月8日,小明家着火了(异常对象)
- 2008年8月9日,小刚家着火了(异常对象)
- 2008年10月8日,小李家着火了(异常对象)
类是模版,对象是实际中存在的个体
钱包丢了(异常类)
- 2009年1月2日 小明钱包丢了(异常对象)
- 2009年1月10日 小方钱包丢了(异常对象)
package com.javase.exception;
public class ExceptionTest02 {
public static void main(String[] args) {
NumberFormatException numberFormatException = new NumberFormatException("数字格式化异常");
System.out.println(numberFormatException); //java.lang.NumberFormatException: 数字格式化异常
NullPointerException nullPointerException = new NullPointerException("空指针异常");
System.out.println(nullPointerException); //java.lang.NullPointerException: 空指针异常
}
}
三、异常类的继承结构
异常在java中以类和对象的形式存在。那么异常的继承结构是怎样的?
我们可以使用UML图来描述一下继承结构。
画UML图有很多工具,例如: Rational Rose (收费的)、starum等...
1、UML图
- UML是一种统一建模语言,一种图标式语言(画图的)
- UML不是只有java中使用。只要是面向对象的编程语言,都有UML。
- 一般画UML图的都是软件架构师或者说是系统分析师。这些级别的人员使用的
- 软件设计人员使用UML。
- 在UML图中可以描述类和类之间的关系,程序执行的流程,对象的状态等
- 盖大楼和软件开发一样,一个道理。
- 盖楼之前,会先由建筑师画图纸。图纸上一个一个符号都是标准符号。这个图纸画完,只要是搞建筑的都能看懂,因为这个图纸上标注的这些符号都是一种“标准的语言”。
- 在java软件开发当中,软件分析师/设计师负责设计类,java软件开发人员必须要能看懂。
2、异常类继承结构图示
Error(错误)和Exception(异常)都统称为异常,不正常的现象
Object是顶级类
Object下有Throwable(可抛出的)
Throwable下有两个分支:Error(不可处理)和Exception(可处理的)
Exception下有两个分支:
- Exception的直接子类,编译时报错(要求程序员在编写程序阶段必须预先对这些异常进行处理)
- RuntimeException:运行时异常。(在编写程序阶段程序员可以预先处理,也不可不处理)
3、所有异常都是发生在运行阶段
1、编译时异常和运行时异常,都是发生在运行阶段,编译阶段异常时不会发生的。
2、因为编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器报错,因此得名。
3、所有异常都是在运行阶段发生的。因为只有程序运行阶段才可以new对象
4、编译时异常和运行时异常的区别
4.1、编译时异常
编译时异常又称:受检异常(CheckedException)或受控异常。
编译时异常一般发生的概率比较高
编译时异常在编写程序阶段必须预先对这些异常进行处理,否则编译报错
举个例子:
- 看到外面下雨了,倾盆大雨的。你出门之前会预料到:如果不打伞,我可能会生病(生病是一种异常)。而且这个异常发生的概率很高,所以我们出门之前要拿一把伞。
- “拿一把伞”就是对“生病异常”发生之前的一种处理方式
- 对于一些发生概率较高的异常,需要在运行之前对其进行预处理
4.2、运行时异常
运行时异常又称:未受检异常(UnCheckedException)或非受控异常
运行时异常一般发生的概率比较低
运行时异常在编写程序阶段程序员可以预先处理,也不可不处理
举个例子:
- 小明走在大街上,可能会被天上的飞机轮子砸到。被飞机轮子砸到也算一种异常。
- 但是这种异常发生概率较低。在出门之前没必要对这种发生概率较低的异常进行预处理。
- 如果你预处理这种异常,你将活的很累
假设java中没有对异常进行划分,没有分为:编译时异常和运行时异常,所有的异常都需要再编写程序阶段对其进行预处理。将是怎样的效果尼?
首先,如果这样的话,程序肯定是绝对的安全的。但是程序员编写程序太累,代码到处都是异常处理的代码(就像你在出门之前,你把能够发生的异常都预先处理,你这个人会更加的安全,但是你这个人会活的很累)
5、方法级别默认上抛RuntimeException(运行时异常)
所有方法在方法声明处都默认上抛运行时异常(RuntimeException)
6、异常的产生和处理都是以方法级别为基本单位的
- 异常产生
- 异常都是在方法内部产生的
- 异常处理
- 1、在方法内部进行异常捕获处理
- 2、在方法声明处将异常上抛
四、throws和throw关键字
在Java中,异常处理机制使用throws
和throw
关键字来处理异常,它们有不同的作用和用法。
1、throws
关键字(方法级别:上抛异常的类型)
将异常上抛给方法的调用者,调用者中没有捕获处理,则调用者方法终止,继续将异常上抛给调用者的调用者,直到JVM,JVM只有一种处理方式,那就是终止程序运行
- 用法:
throws
关键字用于方法声明中,用于声明方法可能抛出的异常。在方法声明中使用throws
关键字后面跟随异常类型,表示该方法可能会抛出指定的异常(多为编译时异常)。 - throws后面可以写多个异常,使用逗号隔开
- 作用:当方法内部出现异常,但不想在方法内部进行处理时,可以使用
throws
关键字将异常抛出给方法的调用者处理。调用该方法的代码需要使用try-catch
块或继续使用throws
关键字将异常继续传递给上层调用者。
示例:
public void readFile() throws IOException {
// 读取文件的代码,可能会抛出IOException
}
2、throw
关键字(代码级别:抛出异常的实例对象)
throw比return影响还要大,
- 当执行到return时,方法结束
- 当执行到throw时,方法结束,如不手动捕获处理,异常上抛,程序终止
总结:只要程序执行到return或者throw方法必然结束
- 用法:
throw
关键字用于在代码块中手动抛出异常。它后面跟随一个异常对象,表示抛出指定的异常。 - 作用:
throw
关键字用于在代码中主动抛出异常,通常用于自定义异常或在特定条件下抛出异常。
实例:
public void divide(int dividend, int divisor) {
if (divisor == 0) {
throw new ArithmeticException("Divisor cannot be zero");
}
int result = dividend / divisor;
System.out.println("Result: " + result);
}
3、异常对象只有使用throw抛出才是真正的异常对象,否则就是个普通java对象
package com.javase.exception;
import java.io.FileNotFoundException;
public class ExceptionTest17 {
public static void main(String[] args) {
//实例化一个异常对象,但并未抛出,所以也不用任何处理,就是一个普通的java对象
FileNotFoundException fileNotFoundException = new FileNotFoundException("找不到文件");
/**
* 找不到文件
* 此处代码也可以执行
* java.io.FileNotFoundException: 找不到文件
* at com.javase.exception.ExceptionTest17.main(ExceptionTest17.java:8)
*/
//调用对象的实例方法
System.out.println(fileNotFoundException.getMessage());
fileNotFoundException.printStackTrace();
System.out.println("此处代码也可以执行");
}
}
4、throw和throws关键字的联系
throws
关键字用于方法声明,表示可能抛出的异常类型,而throw
关键字用于在代码中手动抛出异常对象。throws
关键字是方法级别的,用于声明方法可能抛出的异常,而throw
关键字是在代码块中使用,用于主动抛出异常。- 使用throw在方法体内手动抛出异常对象,必须要在方法级别进行上抛(因为在方法体内主动抛出的异常就是为了让调用者处理,没必要在自己抛出异常,又自己处理,没有意义)
- 两者都与异常处理相关,但
throws
是方法级别的,用于告知调用者可能的异常情况,而throw
是在方法内部使用的,用于抛出具体的异常对象。
总结:
throws
关键字是方法声明中用于声明可能抛出的异常,而throw
关键字是在代码块中手动抛出异常对象。它们都是Java异常处理机制的一部分,用于提供更好的异常处理方式
五、Java会自动实例化内置异常类对象
- 无论是使用内置的异常类还是自定义的异常类,实例化异常对象都允许您在代码中捕获和处理错误情况,以确保程序可以优雅地处理异常而不会崩溃。
- 输出异常对象时会自动调用java中的toString方法,输出异常的信息
在 Java中,运行时会自动实例化内置的异常类来创建异常对象。异常类是 Java的预定义类,用于表示不同类型的错误和异常情况。要实例化异常对象,只需创建异常类的实例并传递适当的参数(如果有的话)。
以下是实例化异常对象的示例:
try{
//一些可能引发异常的代码
System.out.println(100 / 0); //除数为0错误
}catch (ArithmeticException e){ //自动实例化异常类ArithmeticException对象,并捕获,赋值给引用e
// e.printStackTrace();
System.out.println(e); //java.lang.ArithmeticException: / by zero
}
在这个示例中,我们捕获了一个 ArithmeticException异常,然后将其赋值给变量 e
。 通过这个实例对象,您可以访问异常的信息,如错误消息、错误码等。
六、try catch finally语句(异常处理语句)
try-catch
语句是Java中异常处理机制的一部分,用于捕获和处理异常。
1、语法结构
try {
// 可能会抛出异常的代码块
} catch (ExceptionType1 exception1) {
// 处理 ExceptionType1 异常的代码块
} catch (ExceptionType2 exception2) {
// 处理 ExceptionType2 异常的代码块
} finally {
// 最终会执行的代码块,可选
}
try
块中包含可能会抛出异常的代码。当try
块中的代码抛出异常时,会根据异常的类型进行匹配,如果匹配到对应的catch
块,则执行相应的处理代码。(如果没有匹配到合适的catch
块,异常将被传递到上一级调用者进行处理,或者如果没有上级调用者处理该异常,则程序将终止并输出异常信息 指的是运行时异常,因为方法默认上抛RuntimeException,如果异常没有在catch块中被捕获,则会传递到上一级调用者处理。编译时异常不存在这种情况,必须在编写程序阶段全部处理)。
2、执行流程
- 执行
try
块中的代码。 - 如果
try
块中的代码抛出了异常,会与catch
块中的异常类型进行匹配。 - 如果找到与抛出的异常类型匹配的
catch
块,则执行该catch
块中的代码,并跳过其他catch
块。 - 如果没有找到与抛出的异常类型匹配的
catch
块,则异常将被传递给上一级调用者。 - 如果存在
finally
块,不论是否有异常抛出,finally
块中的代码都会被执行。 - 程序继续执行后续的代码(只有使用try catch语句将异常处理后)。
可以理解为
1、try和finally分别是两个线程,当try线程中检测到异常还是有return语句,则会开启另一个线程来执行finally语句块,所以无论try中有异常还是有return语句,finally都会执行
2、catch是另一个线程,专门用于捕获异常
3、3线程同时运行,异步操作
3、try代码块(必选)
1、将有可能出现异常的代码放到try代码块中
2、try代码块中的语句出现异常,则try代码块中后续代码不会执行
try
语句块包含您希望尝试执行的代码,这些代码可能会引发异常。如果在try
块中引发了异常,程序将跳转到相应的catch
块来处理异常。
try{
System.out.println(10 / 0); //此处有异常
System.out.println(123); //此行代码并不会执行
}catch (ArithmeticException e){
// e.printStackTrace();
System.out.println("异常信息:" + e); //异常信息:java.lang.ArithmeticException: / by zero
}
4、catch代码块(可选)
catch
语句块用于捕获并处理在try
块中引发的异常。您可以为不同类型的异常编写多个catch
块,以便根据异常类型执行不同的处理逻辑。如果没有异常发生,catch
块将被跳过。
- 可以有多个
catch
块,每个块可以捕获不同类型的异常,并进行相应的处理(精确处理异常)。 catch
块中的异常类型必须与抛出的异常类型相匹配或是其父类或接口。- catch写多个的时候,从上到下捕获的异常必须是从小到大的顺序
- 可以嵌套使用
try-catch
语句,内层的try-catch
语句可以捕获并处理内部代码块中抛出的异常。
5、finally代码块(可选)
finally
语句块无论是否发生异常,都会在try
块中的代码执行完毕后执行。它通常用于确保资源的释放,例如关闭文件或释放网络连接,无论是否发生异常都要执行这些操作。
finally
块是可选的,用于定义无论是否有异常抛出,都必须执行的代码。- 在finally代码块中的代码是最后执行的,并且一定会执行的,即使try代码块中的代码出现了异常,也会执行
- finally代码块必须和try 一起出现,(没有catch也可以)不能单独编写
- finally代码块使用的情况:
- 通常在finally语句块中完成资源的释放/关闭
- 因为finally中的代码比较有保障
- 即使try语句块中的代码出现异常,finally中的代码也会正常执行
5.1、finally用法示例一:finally代码块关闭流
package com.javase.exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* 关于try catch 中的finally字句:
* 1、在finally字句中的代码是最后执行的,并且一定会执行的,即使try语句块中的代码出现了异常
* finally子句必须和try一起出现,不能单独编写
* 2、finally语句通常使用在哪些情况下尼?
* 通常在finally语句块中完成资源的释放/关闭
* 因为finally中的代码比较有保障
* 即使try语句块中的代码出现异常,finally中的代码也会正常执行
*/
public class ExceptionTest10 {
public static void main(String[] args) {
FileInputStream fileInputStream = null;
try {
//创建输入流对象
fileInputStream = new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\JavaSE\\异常练习.txt");
//开始读文件。。。
String s = null;
//这里一定会出现空指针异常
s.toLowerCase();
System.out.println("hello");
//流使用完需要关闭,因为流是占用资源的
//即使以上代码出现异常,流也必须要关闭
//放在这里流有可能关闭不了
//fileInputStream.close();
} catch (FileNotFoundException e) {
// throw new RuntimeException(e);
e.printStackTrace();
}catch (NullPointerException e){
e.printStackTrace();
}
finally {
System.out.println("finally代码块执行了");
//放在这里 关闭流 比较保险
//因为finally中的代码是一定会执行的
//即使try中出现了异常
if(fileInputStream != null){
try {
//close方法有可能异常,采用捕捉的方式
fileInputStream.close();
} catch (IOException e) {
// throw new RuntimeException(e);
e.printStackTrace();
}
}
}
System.out.println("异常捕获后执行");
}
}
5.2、finally用法示例二:try块中有return语句,异常语句,finally代码块一定会执行,除了退出JVM外
1、没有catch,try和finally可以联合使用
2、理解try和finally线程的执行顺序
- 先执行try线程
- 在执行finally线程
3、finally比较特殊,无论try中有return语句还是有异常,都会先执行finally块,在执行try中的return或者异常语句
4、若是try中System.exit(0); 退出JVM,则finally不会执行
package com.javase.exception;
public class ExceptionTest11 {
public static void main(String[] args)throws Exception {
/**
* 没有catch,try和finally可以联合使用
* try不能单独使用
*
* 以下代码的执行顺序
* 先执行try
* 再执行finally
* 最后执行return(return语句只要执行方法必然结束)
*/
// throw new Exception("123");
try{
System.out.println("try...");
// return;
throw new Exception("123");
// return;
}finally {
//finally中的语句会执行
System.out.println("finally...");
}
//若是try中有return或者有可能抛出异常的语句,则这里不能写语句
//因为此处没有catch来捕获异常,处理异常,一旦有异常的话,方法结束,程序终止
//所以这个代码是无法执行的
// System.out.println("这里不能写语句");
}
}
package com.javase.exception;
public class ExceptionTest12 {
public static void main(String[] args) {
try{
System.out.println("try...");
//退出JVM
System.exit(0);
}finally {
System.out.println("finally...");
}
System.out.println("这里可以写代码,但是JVM已经退出了,不会执行");
}
}
5.3、finally面试题:try,catch,finally多线程和反编译
- try中的return语句一定会在finally之后执行
- try块中的return语句有闭包立即绑定特性
- return的值是其变量名中最初声明的值,即使finally中对原始值修改,也会返回原始的值
package com.javase.exception;
/**
* finally面试题
*/
public class ExceptionTest13 {
public static void main(String[] args) {
int result = m();
System.out.println(result); //100
}
/**
* java语法规则(有一些规则是不能破坏的)
* 方法体中的代码必须遵守自上而下的顺序依次执行
* return语句一旦执行,整个方法必须结束
* @return
*/
public static int m (){
int i = 100;
try{
//这行代码出现在int i = 100;的下面,所以最终结果必须放回100
//return语句还必须保证是最后执行的。一旦执行,整个方法结束
//return语句必须是最后执行的
return i;
}finally {
i++;
}
}
/**
* 反编译效果:
* public static int m()
* {
* int i = 100;
* int j = i;
* i++;
* return j;
* }
*/
}
5.4、finally面试题:final,finally,finalize的区别
package com.javase.exception;
/**
* final finally finalize的区别
* final关键字
* final修饰的类无法继承
* final修饰的方法无法重写
* final修饰的变量无法重新赋值
* finally关键字
* 和try联合使用
* 在finall语句块中的代码,除了try语句块中有System.exit(0);关闭JVM的代码外,一定会执行
* finalize标识符
* 是Object类中的一个方法,已弃用
* 这个方法是由垃圾回收机制GC负责调用的
*/
public class ExceptionTest14 {
public static void main(String[] args) {
//final是一个关键字,表示最终的,不变的
final int a = 10;
//finally也是一个关键字,和try联合使用,使用在异常处理机制中
//在finall语句块中的代码,除了try语句块中有System.exit(0);关闭JVM的代码外,一定会执行
try {
} finally {
System.out.println("finally...");
}
//finalize()是Object类中的一个方法,作为方法名出现
//所以finalize是标识符
//finalize方法是由JVM的GC垃圾回收器进行调用
//不能这样写,是有GC负责调用的,直接重写该方法即可
// ExceptionTest14 exceptionTest14 = new ExceptionTest14();
// exceptionTest14.finalize();
for (int i = 0; i < 1000000; i++) {
ExceptionTest14 obj = new ExceptionTest14();
obj = null;
System.gc();
}
}
protected void finalize() throws Throwable {
System.out.println("即将被销毁");
}
public final void m() {
}
public static final double PI = 3.1415926;
}
6、语句组合形式
- 可组合
- try catch可组合
- try finally可组合
- try catch finally可组合
- 不可组合
- catch finally不可组合
- 任意单独一个不可以
7、JDK8新特性(catch捕获多个异常,可用 | 链接异常类型)
//JDK8的新特性
try{
//创建输入流
FileInputStream fis = new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\JavaSE\\异常练习.txt");
//进行数学运算
System.out.println(10 / 0);
}catch (FileNotFoundException | ArithmeticException | NullPointerException e){ //多态:IOException e = new FileNotFoundException();
System.out.println("文件不存在? 数学异常? 空指针异常? 都有可能!");
}
通过使用try-catch
语句,我们可以优雅地处理可能发生的异常,防止程序崩溃并提供适当的错误处理逻辑。
8、案例:结合IO流演示try catch finally异常处理语句的用法
try {
int result = divide(10, 0); // 可能会抛出异常的方法调用
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Error: " + e.getMessage());
} finally {
System.out.println("Finally block executed.");
}
// 自定义的方法,可能会抛出 ArithmeticException 异常
public static int divide(int dividend, int divisor) {
if (divisor == 0) {
throw new ArithmeticException("Divisor cannot be zero");
}
return dividend / divisor;
}
在上述示例中,divide()
方法可能会抛出ArithmeticException
异常,而在try-catch
语句中,我们捕获了该异常并输出错误消息。无论是否抛出异常,finally
块中的代码都会被执行。
package com.javase.exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* 1、catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型
* 2、catch可以写多个,建议catch的时候,精确地一个一个的处理,这样有利于程序的调试
* 3、catch写多个的时候,从上到下,必须遵守从小到大的原则
*/
public class ExceptionTest07 {
//throws方法级别,上抛异常,可以多个,也可以一个
// public static void main(String[] args) throws Exception, RuntimeException, NullPointerException {
// }
public static void main(String[] args) {
//并没有捕获到,编译器还是报错
/*try{
new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\机考辅导.docx");
}catch (NullPointerException e){
System.out.println(e);
}*/
/*try{
new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\机考辅导123.docx");
System.out.println("以上出现异常,这里无法执行!");
}catch (FileNotFoundException e){
System.out.println(e.toString());
}
System.out.println("异常捕获到了,此处代码可执行");*/
/*try{
new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\机考辅导123.docx");
System.out.println("以上出现异常,这里无法执行!");
//IOException是FileNotFoundException的父类
}catch (IOException e){ //多态:IOException e = new FileNotFoundException();
System.out.println(e.toString());
}
System.out.println("异常捕获到了,此处代码可执行");*/
/*try{
new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\机考辅导123.docx");
}catch (Exception e){ //多态:IOException e = new FileNotFoundException();
System.out.println(e.toString());
}*/
//精确处理异常
/*try{
//创建输入流
FileInputStream fis = new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\JavaSE\\异常练习.txt");
//读文件
fis.read();
}catch (FileNotFoundException e){ //多态:IOException e = new FileNotFoundException();
System.out.println("文件不存在");
}catch (IOException e){
System.out.println("读文件报错了");
}*/
//编译报错:因为顺序错了,从上到下异常应该是由小到大的过程
/*try{
//创建输入流
FileInputStream fis = new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\JavaSE\\异常练习.txt");
//读文件
fis.read();
}catch (IOException e){ //多态:IOException e = new FileNotFoundException();
System.out.println("文件不存在");
}catch (FileNotFoundException e){
System.out.println("读文件报错了");
}*/
//JDK8的新特性
try{
//创建输入流
FileInputStream fis = new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\JavaSE\\异常练习.txt");
//进行数学运算
System.out.println(10 / 0);
}catch (FileNotFoundException | ArithmeticException | NullPointerException e){ //多态:IOException e = new FileNotFoundException();
System.out.println("文件不存在? 数学异常? 空指针异常? 都有可能!");
}
}
}
9、try-with-resources语法
在 Java 中,try
后面的圆括号(()
)的写法是在 Java 7 引入的一种称为“try-with-resources”的异常处理语法。这种写法用于自动关闭实现了 AutoCloseable
接口的资源,如文件、套接字、数据库连接等。当 try
块结束时,系统会自动调用这些资源的 close
方法,无需显式关闭资源。
try-with-resources
的语法如下:
try (ResourceType1 resource1 = new ResourceType1(); ResourceType2 resource2 = new ResourceType2()) {
// 使用资源的代码
} catch (ExceptionType e) {
// 异常处理代码
}
其中,ResourceType1
和 ResourceType2
表示需要被自动关闭的资源,这些资源必须实现 AutoCloseable
接口。在 try
块中,你可以使用这些资源,而不必手动关闭它们。一旦 try
块结束(无论是正常结束还是异常结束),Java 将自动关闭这些资源。
以下是一个示例,演示如何使用 try-with-resources
来自动关闭文件资源:
import java.io.*;
public class Example {
public static void main(String[] args) {
try (FileWriter writer = new FileWriter("example.txt")) {
writer.write("Hello, World!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,FileWriter
是实现了 AutoCloseable
接口的资源,因此可以在 try
块中使用它,而不必在 finally
块中显式关闭它。当 try
块结束时,FileWriter
会自动关闭。
try-with-resources
是一种优雅且安全的方式来管理资源,可以减少资源泄漏的可能性,并提高代码的可读性。它在处理需要关闭的资源时非常有用。
六、异常处理的方式(主要针对Exception)
1、概述
java中异常发生共有两种处理方式:异常上抛和异常捕获
举个例子
- 我是集团的一个销售员,因为我的失误,到时公司损失了1000元。
- “损失1000元”可以看做是一个异常发生了。我有两种处理方式。
- 第一种方式:我把这件事告诉我的领导(异常上抛)
- 张三 --》李四 --》 王五 --》 CEO
- 第二种方式:我自己掏腰包把这个钱补上(异常的捕捉)
异常发生之后,如果选择了上抛,抛给了调用者,调用者需要对这个异常继续处理,调用者处理该异常同样有两种方式:异常上抛和异常捕获,就这样一级一级的处理,直到将这个异常解决。
1.1、异常和处理方式之间的关系
1.2、处理异常时不论是捕获还是上抛目标异常对象类型应为实际异常对象类型或其父类型(否则没有意义,还是没有真正处理异常)
例如:编译时异常
FileNotFoundException --> IOException --> Exception --> Throwable
异常处理顺序如下:
package exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest06 {
public static void main(String[] args) {
try {
m1();
} catch (Throwable e) {
// throw new RuntimeException(e); //一般不再重新抛出
System.out.println("找不到指定文件");
}
}
private static void m1() throws Exception{
m2();
}
private static void m2() throws IOException {
m3();
}
private static void m3() throws FileNotFoundException {
FileInputStream fis = new FileInputStream("test.txt");
}
}
2、上抛异常(Throws Exception)
1、在方法声明的位置上,使用throws关键字,抛给上一级方法调用者
2、谁调用我,我就抛给谁,抛给上一级
3、注意:java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道这个异常发生,只有一个结果。终止java程序的执行
4、异常上抛类似于推卸责任,继续把异常传递给调用者
2.1、运行时异常自动上抛
所有方法都默认在方法声明处默认上抛RuntimeException(throws RuntimeException)
package com.javase.exception;
public class ExceptionTest03 {
public static void main(String[] args) {
/**
* 程序执行到此处时发生了java.lang.ArithmeticException异常
* 底层new了一个ArithmeticException异常对象,然后抛出了
* 由于是main方法调用 100 / 0 ,所以这个异常ArithmeticException抛给了main方法
* main方法没有处理,将这个异常自动抛给了JVM,JVM最终终止程序的运行
*
* ArithmeticException 继承于 RuntimeException ,属于运行时异常
* 在编写程序阶段不需要对这种异常进行处理
*/
System.out.println(100 / 0);
//这里的helloworld没有输出,没有执行
System.out.println("hello world!");
}
}
2.2、编译时异常手动上抛
制造编译时异常
ClassNotFoundException --> ReflectiveOperationException --> Exception
package com.javase.exception;
/**
* 以下代码报错的原因是什么?
* 因为doSome()方法声明位置上使用了:throws ClassNoFoundException
* 而ClassNoFoundException是编译时异常,必须编写代码是处理,没有处理,编译器报错
*/
public class ExceptionTest04 {
public static void main(String[] args) {
//main方法中调用doSome方法
//因为doSome()方法声明位置上有:throws ClassNoFoundException
//我们在调用doSome()方法的时候必须对这种异常进行预先处理。
//如果不处理,编译器就报错
//编译器报错:Unhandled exception: java.lang.ClassNotFoundException
// doSome();
}
/**
* doSome方法在方法声明的位置上使用了 throws ClassNoFoundException
* 这个代码表示doSome()方法在执行过程汇总,有可能会出现ClassNoFoundException异常。
* 叫做 类没有找到异常 这个异常直接父类是Exception,所以ClassNoFoundException属于编译时异常
* @throws ClassNotFoundException
*/
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!!");
}
}
处理方式,上抛给调用者
ackage com.javase.exception;
public class ExceptionTest05 {
//第一种处理方式:在方法声明的位置上继续使用:throws,在完成异常的继续上抛,抛给调用者
public static void main(String[] args) throws ClassNotFoundException {
doSome();
}
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!!");
}
}
3、捕获异常(Catch Exception)
1、使用try.. ..catch语句进行异常的捕获
2、这件事发生了,但谁也不知道,因为我给抓住了
3、异常捕获等于把异常拦下了,异常真正的解决了,调用者是不知道的
3.1、编译时异常手动捕获
package com.javase.exception;
public class ExceptionTest05 {
//第二种处理方式:try catch 进行异常捕获
public static void main(String[] args) {
try {
doSome();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public static void doSome() throws ClassNotFoundException {
System.out.println("doSome!!!");
}
}
3.2、运行时异常手动捕获
package com.javase.exception;
public class ExceptionTest03 {
public static void main(String[] args) {
try{
System.out.println(100 / 0);
}catch (ArithmeticException arithmeticException){
System.out.println("计算出错了"); //计算出错了
// throw new RuntimeException(arithmeticException); //一般情况下异常捕获后就不会再抛出了,否则白捕获了,也没有处理该异常
}
System.out.println("hello world!"); //hello world!
}
}
4、异常处理方式的选择
- 如果希望调用者来处理,选择throws上抛
- 其他情况使用捕获,增强程序的健壮性(一般方法第一级调用处使用异常捕获,避免上抛到JVM,导致程序终止)
七、出现异常后的代码执行情况
- 1、代码执行过程中出现异常,则自出现异常处起,后续代码均不会执行,在该异常被捕获到后,才可执行后续代码(上抛异常不行,必须要被捕获,消化掉,真正的解决该异常)
- 2、只要异常没有捕获,采用了上抛的方式,此方法的后续代码不会执行
- 定义可能抛出异常的方法时,由于执行了throw语句,抛出了异常,该方法终止,进行了异常上抛
- 调用可能抛出异常的方法时,由于没有进行异常捕获处理,调用者方法终止,进行了异常上抛
- 3、另外需要注意,try语句块中的某一行出现异常,该行后面的代码不会执行
- 4、try catch捕获异常后,后续代码可以执行
1、案例:演示方法多次调用异常上抛(throw执行方法终止)
package com.javase.exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* 处理异常的第一种方式,
* 在方法声明的位置上使用throws关键字抛出,谁调用我这个方法,我就抛给谁,抛给调用者来处理
* 这种处理异常的态度:上报
*
* 处理异常的第二种方式:
* 使用try catch语句对异常进行捕获
* 这个异常不会上报,自己把这个异常处理了
* 异常到此处为止,不再上抛了
*
*
*/
public class ExceptionTest06 {
//一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM,JVM只有终止程序
//异常处理机制的作用就是增强程序的健壮性,怎么能做到,异常发生了也不影响程序的执行
//所以,一般main方法中的异常建议使用try catch进行捕捉,main就不要继续往上抛了
public static void main(String[] args){
System.out.println("main begin");
//100 / 0 是算数异常,这个异常是运行时异常,在编译阶段可以处理,也可以不处理,编译器不管
//System.out.println(100 / 0);
//也可以进行处理,如下:
/*try{
System.out.println(100 / 0);
}catch (ArithmeticException e){
System.out.println("算数异常了!!");
}
*/
try {
//try尝试
m1();
//以上代码出现异常,直接进入catch语句块中执行
System.out.println("hello");
} catch (FileNotFoundException e) { //catch后面的好像一个方法的形参
//这个分支中可以使用e引用,e引用保存的内存地址是那个new出来异常对象的内存地址
//发生异常后执行的代码块
System.out.println("文件不存在,可能路径错误,也可能该文件被删除了!");
System.out.println(e.toString()); //java.io.FileNotFoundException: C:\Users\yuliang\Desktop\算法与数据结构\机考辅导12.docx (系统找不到指定的文件。)
// throw new RuntimeException(e);
}
System.out.println("main over");
}
private static void m1() throws FileNotFoundException {
System.out.println("m1 begin");
m2();
System.out.println("m1 over");
}
//抛别的不行 抛ClassNoFoundException说明还是没有对FileNotFoundException进行处理
// private static void m2() throws ClassNotFoundException{
//抛出FileNotFoundException的父类IOException,是可以的,因为IOException包括FileNotFoundException
//同理抛出Exception也是可以的,因为Exception包含所有的异常
// private static void m2() throws IOException {
//throws后面可以写多个异常,使用逗号隔开
// private static void m2() throws IOException,ClassNotFoundException{
private static void m2() throws FileNotFoundException {
System.out.println("m2 begin");
//编译器报错原因是:m3()方法声明位置上有:throws FileNotFoundException
//我们在这里调用m3()没有对异常进行处理,所以编译报错
m3();
System.out.println("m2 over");
}
private static void m3() throws FileNotFoundException {
//创建一个输入流对象,该流需要指向一个文件
/**
* 编译报错的原因是什么?
* 第一:这是调用了一个构造方法:FileInputException(String name)
* 第二:这个构造方法的声明位置处有:throws FileNotFoundException
* 第三:通过类的继承结构看到:FileNotFoundException 父类型 IOException,IOException 的父类型是Exception
* 最终得知:FileNotFoundException是编译时异常
*
* 错误原因:编译时异常要求程序员在编写程序阶段必须对它进行处理,不处理编译器报错
*
*/
System.out.println("m3 begin");
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\机考辅导12.docx");
System.out.println("以上代码出现异常,此处也不会执行");
System.out.println("m3 over");
}
}
执行结果:
八、异常对象的常用方法(实例方法)
Java中,异常对象是通过异常类的实例化来创建的。异常对象提供了一些常用的方法来获取关于异常的信息。下面是一些常用的异常对象方法:
1、getMessage()
: 获取异常的详细描述信息
返回值类型为String,也就是实例化异常对象的时候传入的参数信息
try {
// some code that may throw an exception
} catch (Exception e) {
String message = e.getMessage();
System.out.println(message);
}
2、printStackTrace()
: 使用较多,打印异常堆栈追踪信息
返回值类型为void
- 采用异步线程的方式打印输出
- 打印异常堆栈跟踪信息,包括异常的类型、详细描述以及异常发生的位置和调用堆栈。
- 和直接上抛异常给JVM导致程序终止所输出的堆栈追踪信息还不一样,
printStackTrace()只是异常对象的一个方法,程序并没有终止
try {
// some code that may throw an exception
} catch (Exception e) {
e.printStackTrace();
}
package com.javase.exception;
/**
* 异常对象有两个非常重要的方法
*
* 获取异常简单的描述信息
* String msg = exception.getMessage();
*
* 打印输出异常追踪的堆栈信息
* exception.printStackTrace();
*/
public class ExceptionTest08 {
public static void main(String[] args) {
NullPointerException e = new NullPointerException("空指针异常");
//获取异常简单的描述信息,这个信息实际上就是构造方法上面的String参数
String message = e.getMessage();
System.out.println(message);
//输出异常堆栈信息
//采用异步的方式输出,专门有个线程输出
e.printStackTrace();
System.out.println("Hello");
}
}
这些方法可以帮助我们在异常处理过程中获取异常信息、打印异常堆栈信息以及查找异常的原因。请注意,这些方法是从Throwable
类继承而来,因此在Java中所有的异常类(包括内置的异常类和自定义的异常类)都可以使用这些方法。
3、案例:从上往下查看异常追踪信息
Java中查看异常追踪信息是从上往下,正好和Python中是相反的
package com.javase.exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
/**
* 异常对象的两个方法
* String msg = e.getMessage();
* e.printStackTrace();
*
* 查看异常信息,快速定位问题:
* 异常信息追踪信息,从上往下,一行一行看
* 但需要注意的是,SUN写的代码就不用看了(看包名就知道是SUN的还是自己的),主要的问题是出现在自己写的代码上
*
*/
public class ExceptionTest09 {
public static void main(String[] args) {
try {
n1();
} catch (FileNotFoundException e) {
// throw new RuntimeException(e);
String msg = e.getMessage();
System.out.println(msg);
//打印异常堆栈信息
//在实际的开发中,建议使用这个,养成好习惯
e.printStackTrace();
/**
* C:\Users\yuliang\Desktop\算法与数据结构\JavaSE\异常练习123.txt (系统找不到指定的路径。)
* 123
* java.io.FileNotFoundException: C:\Users\yuliang\Desktop\算法与数据结构\JavaSE\异常练习123.txt (系统找不到指定的路径。)
* at java.base/java.io.FileInputStream.open0(Native Method)
* at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
* at java.base/java.io.FileInputStream.<init>(FileInputStream.java:158)
* at java.base/java.io.FileInputStream.<init>(FileInputStream.java:112)
* at com.javase.exception.ExceptionTest09.n3(ExceptionTest09.java:61)
* at com.javase.exception.ExceptionTest09.n2(ExceptionTest09.java:57)
* at com.javase.exception.ExceptionTest09.n1(ExceptionTest09.java:53)
* at com.javase.exception.ExceptionTest09.main(ExceptionTest09.java:19)
*
* 因为61行出问题导致57行出问题
* 57行出问题导致53行出问题
* 53行出问题导致19行出问题
*
* 应该先查看61行的代码,61行时代码错误的根源
*/
//后续代码也可以继续执行,就像服务器不会因为遇到异常而宕机
System.out.println(123);
}
}
private static void n1() throws FileNotFoundException {
n2();
}
private static void n2() throws FileNotFoundException {
n3();
}
private static void n3() throws FileNotFoundException {
new FileInputStream("C:\\Users\\yuliang\\Desktop\\算法与数据结构\\JavaSE\\异常练习123.txt");
}
}
九、自定义异常类
1、概述
SUN提供的JDK内置的异常类肯定是不够用的,在实际的开发中,有很多业务,这些业务出现异常之后,JDK中都是没有的,和业务挂钩的,因此我们要自定义异常类
2、自定义异常类的步骤
第一步:编写一个类继承Exception(编译时异常)或者RuntimeException(运行时异常)
第二步:提供两个构造方法,一个无参数的构造方法,一个带有String参数的构造方法
3、自定义编译时异常(继承Exception)
package com.javase.exception;
public class MyException extends Exception{ //编译时异常
//定义一个无参构造
public MyException(){
}
//定义一个带有String类型参数构造方法
public MyException(String s){
super(s);
}
}
测试:
package com.javase.exception;
public class ExceptionTest15 {
public static void main(String[] args) {
//创建异常对象(只是new了,并未抛出)
MyException myException = new MyException("自定义编译时异常");
//打印异常堆栈信息
myException.printStackTrace();
/**
* com.javase.exception.MyException: 自定义编译时异常
* at com.javase.exception.ExceptionTest15.main(ExceptionTest15.java:6)
*/
//获取异常简单描述信息
String msg = myException.getMessage();
System.out.println(msg); //自定义编译时异常
}
}
4、自定义运行时异常(继承RuntimeException)
package com.javase.exception;
public class MyException extends RuntimeException{ //运行时异常
//定义一个无参构造
public MyException(){
}
//定义一个带有String类型参数构造方法
public MyException(String s){
super(s);
}
}
测试:
package com.javase.exception;
public class ExceptionTest15 {
//方法中都默认上抛运行时异常
public static void main(String[] args) throws MyException,RuntimeException{
//创建异常对象(只是new了,并未抛出)
//实例化一个异常对象,但并未抛出,所以也不用任何处理,就是一个普通的java对象
MyException myException = new MyException("自定义运行时异常");
//打印异常堆栈信息
myException.printStackTrace();
/**
* com.javase.exception.MyException: 自定义运行时异常
* at com.javase.exception.ExceptionTest15.main(ExceptionTest15.java:8)
* 自定义运行时异常
*/
//获取异常简单描述信息
String msg = myException.getMessage();
System.out.println(msg); //自定义运行时异常
}
}
5、自定义异常在实际开发中的作用(以编译时异常为主)
自定义异常类
package com.javase.exception;
/**
* 栈操作异常:自定义编译时异常
*/
public class MyStackOperationException extends Exception{
public MyStackOperationException(){
}
public MyStackOperationException(String s){
super(s);
}
}
改良MyStack类
package com.javase.exception;
/**
* 编写程序:使用一维数组,模拟栈数据结构
* 要求:
* 1、这个栈可以存储java中的任何引用数据类型
* 2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息)
* 3、在栈中提供pop方法模拟弹栈。(栈空了,也要有提示信息)
* 4、编写测试程序,new栈对象,调用push,pop方法模拟压栈和弹栈动作。
*/
public class MyStack {
/**
* 向栈当中存储元素,我们这里使用一维数组模拟,存到栈中,就表示存储到数组中
* 因为数组是我们学习java的第一个容器
* 为什么选择Object类型数组?因为这个栈可以存储java中任何引用类型数据
*/
private Object[] objects;
//setters and getters
public Object[] getObjects() {
return objects;
}
public void setObjects(Object[] objects){
this.objects = objects;
}
/**
* 模拟栈顶指针,永远指向栈顶元素
* 默认初始值为0,注意:最初的栈是空的,一个元素也没有
*
* 如果默认初始化为0,则表示栈顶指针指向了顶部元素的上方
* 如果默认初始化为-1,则表示栈顶指针指向了顶部元素
*/
private int index;
//setters and getters
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
/**
* 构造方法,构造方法执行的时候进行初始化
*/
public MyStack(){
//默认初始化容量为10
this.objects = new Object[10];
//默认初始化栈顶指针为-1
this.index = -1;
}
//有参构造
public MyStack(int num){
this.objects = new Object[num];
}
/**
* 提供一个压栈的方法
* @param obj 要压的元素
*/
public void push(Object obj) throws MyStackOperationException{
if(this.getIndex() >= this.getObjects().length - 1){
//=============================================================修改代码===============================================
//原代码
// System.out.println("压栈失败,栈已满!");
// return;
// //创建异常对象
// MyStackOperationException e = new MyStackOperationException("压栈失败,栈已满!");
// //手动将异常抛出
// //这里捕捉异常没有意义,自己new一个异常,自己抓,没有意义,应该将这个异常上抛
// throw e;
//合并代码
throw new MyStackOperationException("压栈失败,栈已满!");
//=============================================================修改代码===============================================
}
this.setIndex(this.getIndex() + 1);
this.getObjects()[this.getIndex()] = obj;
//所有的System.out.println方法执行时,如果输出的引用的话,会自动调用引用的toString方法
System.out.println("压栈" + obj + "成功,栈顶指针指向:" + this.getIndex());
}
/**
* 弹栈的方法,往外取元素
* @return 返回当前弹出的元素
*/
public Object pop()throws MyStackOperationException {
if(this.getIndex() < 0){
//=============================================================修改代码===============================================
//原代码
// System.out.println("弹栈失败,栈已空");
// return null;
throw new MyStackOperationException("弹栈失败,栈已空");
//=============================================================修改代码===============================================
}
System.out.print("弹栈" + this.getObjects()[this.getIndex()] + "元素成功,");
//栈顶指针向下移动一位
this.setIndex(this.getIndex() - 1);
System.out.print("栈顶指针指向" + this.getIndex());
System.out.println();
//程序执行到此处说明栈没有空
return this.getObjects()[this.getIndex() + 1];
}
}
/**
* 测试改良版的MyStack
*/
class TestMyStack{
public static void main(String[] args) {
//创建一个栈对象
MyStack ms = new MyStack();
for (int i = 0; i < 11; i++) {
try {
ms.push(new Object());
} catch (MyStackOperationException e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
for (int i = 0; i < 11; i++) {
try {
ms.pop();
} catch (MyStackOperationException e) {
e.printStackTrace();
}
}
}
}
6、用户注册案例
编写程序模拟用户注册:
1、程序开始执行时,提示用户输入“用户名”和“密码”信息
2、输入信息之后,后台java程序模拟用户注册
3、注册时用户要求长度在[6,14]之间,小于或者大于都表示异常
package com.javase.exception.异常作业;
/**
* 自定义异常
* 编译时异常
*/
public class InvalidNameException extends Exception{
public InvalidNameException(){
}
public InvalidNameException(String s){
//idea中紫色的表示实例变量
super(s);
}
}
package com.javase.exception.异常作业;
import java.util.Scanner;
/**
* 用户业务类:处理用户相关的业务,例如登录,注册等
*/
public class UserService {
/**
* 用户注册
* @param username 用户名
* @param password 密码
* @throws InvalidNameException 用户名为null或者名户名长度小于6,或者用户名长度大于14,则出现该异常
*/
public void register(String username,String password) throws InvalidNameException{
/**
* 引用 == null的这个判断最好放到所有条件的最前面
*/
// if(username == null || username.length() < 6 || username.length() > 14){}
/**
* 在分享一个经验: username == null 不如写成 null == username
*
* "abc".equals(username) 比 username.equals("abc") 好
*/
if(null == username || username.length() < 6 || username.length() > 14){
//以前的做法
/*System.out.println("用户名不合法,长度必须在[6,14]之间");
return;*/
throw new InvalidNameException("用户名不合法,长度必须在[6,14]之间");
}
//程序能执行到此处,说明username合法
System.out.println("注册成功,欢迎" + username);
}
}
class Test{
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("请输入用户名:");
String userName = input.next();
// System.out.println(userName.length());
System.out.print("请输入密码:");
String password = input.next();
UserService userService = new UserService();
try {
userService.register(userName,password);
} catch (InvalidNameException e) {
e.printStackTrace();
}
}
}
十、综合案例:武器库数组案例(注意导包时采用绝对方式,完整类名)
写一个类Army,代表一支军队,这个类有一个属性Weapon数组w(用来存储该军队所拥有的武器),该类提供一个构造方法,在构造方法中通过传一个int类型的参数来限定该类型所能拥有的最大武器数量
该类还提供一个方法addWeapon(Weapon weapon),表示吧参数weapon所代表的武器加入到数组w中。在这个类中还定义两个方法attackAll() 让w数组中的所有武器攻击。以及moveAll()让w数组中的所有可移动的武器移动
写一个主方法去测试以上程序。
提示:
Weapon类是一个父类,应该有很多子武器
这些武器应该有一些事可移动的,有一些是可攻击的
注意和Python中的解法做对比
1、武器父类:Weapon.java
package com.javase.数组.武器数组作业;
/**
* 所有武器的父类
*/
public class Weapon {
public String toString(){
//获取当前实例对象的类名信息
return this.getClass().getName();
}
}
2、接口:Moveable.java
package com.javase.数组.武器数组作业;
/**
* 可移动的接口
*/
public interface Moveable {
/**
* 移动的方法
*/
public abstract void move();
}
3、接口:Shootable.java
package com.javase.数组.武器数组作业;
/**
* 射击接口
*/
public interface Shootable {
/**
* 射击行为
*/
void shoot();
}
4、武器:Fighter.java
package com.javase.数组.武器数组作业;
/**
* 战斗机,是武器,可移动,可射击
*/
public class Fighter extends Weapon implements Moveable,Shootable{
@Override
public void move() {
System.out.println("战斗机起飞");
}
@Override
public void shoot() {
System.out.println("战斗机开炮");
}
}
5、武器:GaoShePao.java
package com.javase.数组.武器数组作业;
/**
* 高射炮,是武器,可射击,不能移动
*/
public class GaoShePao extends Weapon implements Shootable{
@Override
public void shoot() {
System.out.println("高射炮开炮");
}
}
6、武器:Tank.java
package com.javase.数组.武器数组作业;
/**
* 坦克是一个武器,可以移动,可以射击
*/
public class Tank extends Weapon implements Moveable,Shootable {
@Override
public void move() {
System.out.println("坦克移动");
}
@Override
public void shoot() {
System.out.println("坦克开炮");
}
}
7、武器:WuziFeiJI.java
package com.javase.数组.武器数组作业;
/**
* 物资飞机,是武器,可移动,不能射击
*/
public class WuziFeiJI extends Weapon implements Moveable{
@Override
public void move() {
System.out.println("运输机起飞");
}
}
8、自定义异常类:AddWeaponException.java
package com.javase.数组.武器数组作业;
/**
* 添加武器异常
*/
public class AddWeaponException extends Exception {
public AddWeaponException(){
}
public AddWeaponException(String s){
super(s);
}
}
9、军队:Army.java
package com.javase.数组.武器数组作业;
import java.util.Arrays;
import java.util.zip.CheckedOutputStream;
/**
* 军队
*/
public class Army {
/**
* 武器数组
*/
Weapon[] weapons;
public Army() {
}
/**
* 创建军队的构造方法
* @param count 武器数量
*/
public Army(int count) {
//动态初始化数组中的每一个元素默认为null
//武器数组是有了,但是武器数组中没有放武器
this.weapons = new Weapon[count];
}
/**
* 将武器加入数组
* @param weapon
*/
public void addWeapon(Weapon weapon) throws AddWeaponException {
for (int i = 0; i < weapons.length; i++) {
if(null == weapons[i]){
//在数组空的位置处添加武器
weapons[i] = weapon;
System.out.println(weapon.toString() + "添加成功");
return;
}
}
// Arrays.sort(weapons);
//程序执行到此处说明数组已经添加完全
throw new AddWeaponException("武器数量已达到上线");
}
/**
* 所有可攻击的武器攻击
*/
public void attackAll(){
for (int i = 0; i < weapons.length; i++) {
//判断weapons数组中的实例对象是否诶Shootable的实例
if(weapons[i] instanceof Shootable){
//强制转到指定类型
Shootable shootable = (Shootable) weapons[i];
shootable.shoot();
}
}
}
/**
* 所有可移动的武器移动
*/
public void moveAll(){
for (int i = 0; i < weapons.length; i++) {
if(weapons[i] instanceof Moveable){
Moveable moveable = (Moveable) weapons[i];
moveable.move();
}
}
}
}
10、测试程序:Test.java
package com.javase.数组.武器数组作业;
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
//创建军队对象
Army army = new Army(4);
//创建武器对象
Fighter fighter = new Fighter();
Fighter fighter1 = new Fighter();
Tank tank = new Tank();
WuziFeiJI wuziFeiJI = new WuziFeiJI();
GaoShePao gaoShePao = new GaoShePao();
//添加武器
try{
army.addWeapon(fighter);
army.addWeapon(fighter1);
army.addWeapon(tank);
army.addWeapon(wuziFeiJI);
army.addWeapon(gaoShePao);
}catch (AddWeaponException e){
e.printStackTrace();
System.out.println(e.getMessage());
}
//让所有可移动的武器移动
army.moveAll();
System.out.println("================");
//让所有可攻击的武器攻击
army.attackAll();
}
}
第二章、集合
一、概述
集合是对象的容器,只能存储对象的引用
1、集合的概念及作用
Java中的集合是一组对象的容器,用于存储、操作和传输数据。它们提供了各种不同类型的集合类,可以根据需求选择适当的集合实现。数组其实就是一个集合。集合实际上就是一个容器。可以来容纳其它类型的数据。
集合为什么说在开发中使用较多?
集合是一个容器,是一个载体,可以一次容纳多个对象。在实际开发中,假设连接数据库,数据库当中有10条记录,那么假设把这10条记录查询出来,在java程序中会将10条数据封装成10个java对象,然后将10个java对象放到某一个集合当中,将集合传到前端,然后遍历集合,将一个数据一个数据展现出来。
2、 集合存储的都是引用数据类型(实例对象的引用)
集合中不能存储基本数据,另外对于泛型,类型参数也不能指定基本数据类型
基本数据类型进行自动装箱,存储的也是引用
Java中的集合(Collection)只能存储引用数据类型,而不能直接存储基本数据类型,是因为Java的设计原则和语言特性导致了这样的情况。
Java是一门面向对象的编程语言,它将所有的数据都看作对象。基本数据类型(例如int、char、boolean等)不是对象,它们是直接存储在内存中的简单数据,没有面向对象的属性和方法。然而,Java的集合框架是建立在面向对象的基础上的,它要求集合中存储的元素必须是对象,因此无法直接存储基本数据类型。
为了解决这个问题,Java引入了包装类(Wrapper Classes),这些类用于将基本数据类型包装成对象。例如,将int包装成Integer,将char包装成Character等。这样,基本数据类型就可以通过包装类的实例来在集合中存储了。
为什么不直接在集合中存储基本数据类型呢?这涉及到Java内存管理和性能方面的考虑:
-
一致性: Java的设计强调一致性和面向对象的思想,通过使用包装类,使得所有数据在集合中都以对象的形式存在,保持了代码的一致性。
-
泛型和类型安全: 集合框架中引入了泛型,使得集合能够在编译时检查类型安全。如果允许集合直接存储基本数据类型,会导致泛型的复杂性增加,可能会破坏类型安全。
-
自动装箱和拆箱: Java提供了自动装箱(Autoboxing)和拆箱(Unboxing)功能,使得基本数据类型和包装类之间的转换更加方便。但是这也带来了一些性能开销,因为在装箱时需要创建包装对象,在拆箱时需要从包装对象中提取基本数据类型。
-
性能考虑: 直接存储基本数据类型可能会造成额外的内存和性能开销,因为包装类会引入一定的对象头和额外的内存消耗。此外,频繁的装箱和拆箱操作也会影响性能。
综上所述,尽管不能直接在Java集合中存储基本数据类型,但通过包装类以及自动装箱和拆箱功能,可以在一定程度上弥补这个限制,并保持代码的一致性和类型安全。如果对性能有严格要求,可以考虑使用一些针对基本数据类型优化的第三方库或数据结构。
集合不能直接存储基本数据类型(自动装箱),另外集合也不能直接存储java对象。集合当中存储的都是java对象的内存地址。(或者说是集合中存储的是引用)
注意:
- 集合在java中本身是一个容器,是一个对象
- 集合中任何时候存储的都是“引用”。
3、集合内存图示
4、集合与数据结构
在java中每一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构中。
为什么是数据结构?
数据存储的结构就是数据结构。不同的数据结构,数据存储方式不同。例如:
数组,二叉树,链表,哈希表...
以上这些都是常见的数据结构
- 你往集合c1中放数据,可能是放到数组上了。
- 你往集合c2中放数据,可能是放到二叉树上了
- ......
- 使用不同的集合等于使用了不同的数据结构
在java集合这一章中,你需要掌握的不是精通数据结构,java中已经将数据结构实现了,已经写好了这些常用的集合类,你只需要掌握怎么使用?什么情况下选择哪一种合适的集合去使用即可
- new ArrayList(); 创建一个集合对象,底层是数组
- new LinkedList(); 创建一个集合对象,底层是链表
- new TreeSet(); 创建一个集合对象,底层是二叉树
5、集合在java.util包中
java.util.*
所有的集合类和集合接口都在java.util包下
二、集合继承结构
1、集合分类
1.1、单个方式存储元素
单个方式存储元素,这一类集合中超级父接口:java.util.Collection(接口)
1.2、键值对方式存储元素
以键值对的方式存储元素,这一类集合中的超级父接口:java.util.Map(接口)
2、相关概念
2.1、有序
有序:有索引,按照一定顺序将元素存进去,取出元素还是该顺序,并不是排序
2.2、无序
无序,无索引,按照一定顺序将元素存进去,取出元素不一定还是该顺序
2.3、排序
放到该集合中的元素自动按照大小顺序进行排序
3、单个方式存储元素
3.1、继承结构图示
3.2、实现类
ArrayList:底层是数组数据结构,非线程安全的
LinkedList:底层是双向链表数据结构
Vector:底层是数组数据结构,线程安全的,效率较低,使用较少
HashSet:底层是HashMap,放到HashSet集合中的元素等同于放到HashMap集合的Key部分
TreeSet:底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合的Key部分
3.3、集合接口特点
List集合存储元素特点:
- 有序:存进去的元素的顺序和取出元素的顺序相同,每一个元素都有索引
- 可重复:存进去1,可以再存进去1
- 可索引:集合中的元素有索引,从0开始,依次递增
Set集合存储元素特点;
Set对应Map
- 无序:存进去的顺序和取出来的顺序不一定相同
- 不可重复:存进去1,不能再存进去1
- 不可索引:Set集合中元素没有索引
SortedSet集合存储元素特点:
SortedSet对象SortedMap
- 无序:存进去的顺序和取出来的顺序不一定相同
- 不可重复:存进去1,不能再存进去1
- 不可索引:Set集合中元素没有索引
- 可排序:可以按照元素大小顺序排列
3.4、toString重写位置分析
Set集合和List集合的toString方法都是重写在AbstractCollection类中
表现样式相同:
toString()
方法,该方法会遍历集合中的元素,并以类似 "[element1, element2, ...]" 的形式返回一个表示集合内容的字符串。
4、 键值对方式存储元素
4.1、继承结构图示
4.2、实现类
HashMap:底层是哈希表数据结构,非线程安全的
Hashtable:底层也是哈希表数据结构,线程安全的,效率较低,使用较少
Properties:线程安全的,并且Key和value只能存储字符串String
TreeMap:底层是二叉树数据结构,TreeMap集合中的Key可以自动按照大小顺序排序
5、总结(所有的接口)
Map集合的Key,就是一个Set集合
往Set集合中放数据,实际上是放到了Map集合的Key部分
三、集合数据结构,List数据结构,数组数据结构
1、集合
在Java中,集合是一种用于存储和操作数据的抽象概念。它不仅仅是一种特定的数据结构,而是一个更广义的概念,代表一组对象的容器。
集合框架(Collection Framework)是Java提供的一组接口和类,用于处理和操作集合。它提供了许多实现了不同数据结构的集合类,如List、Set、Queue和Map等。
集合框架的设计目标是为了提供统一的接口和一致的操作方式,以方便开发者进行集合的管理和操作。通过使用集合框架,可以更加灵活和高效地处理数据,而无需关注具体的数据结构实现细节。
因此,集合本身不是特定的数据结构,而是一种抽象的概念,代表了一组对象的容器。不同的集合类可以使用不同的数据结构来实现,例如数组、链表、哈希表等。通过使用集合框架,我们可以选择适合特定需求的具体集合类,并使用统一的接口来进行操作。
总结:集合是一种抽象的概念,代表一组对象的容器。它不是具体的数据结构,而是通过集合框架提供的接口和类来操作和管理数据。集合框架提供了不同的集合类,它们可以使用不同的数据结构来实现。
2、List
在Java中,List是一种接口,定义了一组有序的元素集合,并且可以包含重复的元素。它是集合框架中最常用的一种数据结构之一。
List接口继承自Collection接口,它定义了许多用于操作和访问元素的方法,例如添加元素、删除元素、获取元素数量、访问指定位置的元素等。
List是一种有序的数据结构,它保留了元素插入的顺序。每个元素在List中都有一个索引,可以通过索引来访问或修改元素。List允许元素的重复,这意味着可以向List中添加相同的元素。
在Java中,List是一个接口,因此不能直接实例化,需要使用List的实现类来创建List对象。常见的List实现类有ArrayList、LinkedList和Vector等。
这些List实现类使用不同的数据结构来存储元素。例如,ArrayList使用数组作为底层数据结构,而LinkedList使用链表。不同的实现类在性能和适用场景上有所差异,可以根据具体需求选择合适的实现类。
总结:List是Java中的一个接口,定义了一组有序的元素集合,并且允许元素重复。它是集合框架中最常用的数据结构之一。List保留了元素插入的顺序,并且可以通过索引访问和修改元素。在Java中,需要使用List的实现类来创建List对象,常见的实现类有ArrayList、LinkedList和Vector等。
3、数组(检索效率极高)
在Java中,数组是一种数据结构,用于存储相同类型的元素序列。它是一种固定长度的数据结构,即在创建数组时需要指定数组的长度,且长度不能改变。
在Java中,数组可以包含基本数据类型(如int、double、boolean等)或对象类型(如String、Person等)的元素。数组中的每个元素通过索引访问,索引从0开始,依次递增。
数组是一种简单而有效的数据结构,它在内存中以连续的方式存储元素。通过索引,我们可以快速访问和修改数组中的元素。然而,由于数组的长度是固定的,一旦创建后,无法动态地添加或删除元素。如果需要动态调整大小并支持元素的增删操作,可以使用Java集合框架中的List接口及其实现类。
总结:在Java中,数组是一种数据结构,用于存储相同类型的元素序列。它是一种固定长度的结构,在创建时需要指定长度,并且长度不能改变。数组通过索引访问和修改元素。它是一种简单而有效的数据结构,但不支持动态调整大小和元素的增删操作。
4、集合,List,数组之间的关系
数组是基本实现数据结构,List是半抽象数据结构,集合是完全抽象数据结构
集合和数组都可以被看作是数据结构,用于存储和组织多个相关元素的数据集合。它们都提供了在程序中处理和操作多个元素的机制,但它们在实现和使用上有一些差异。
数组是一种简单的数据结构,它是一块连续的内存空间,用于存储一组具有相同类型的元素。数组的大小在创建时确定,可以通过索引快速访问元素。数组在内存中的存储效率高,适用于需要频繁访问特定位置的元素的场景。
集合是更为复杂和灵活的数据结构,它提供了丰富的功能和操作,包括添加、删除、查找、修改,遍历等。集合可以存储不同类型的元素,可以动态调整大小,并提供了各种不同的接口和实现,以满足不同的需求。集合类在内部使用不同的数据结构实现,如数组、链表、哈希表等。
总之,集合和数组都是用来存储和操作多个相关元素的数据结构,但集合类提供了更高级和更灵活的功能,适用于大多数的数据处理需求,而数组更适合于需要直接访问特定位置的元素的简单场景。
在Java中,集合、List和数组之间存在关系,但它们并非简单的包含关系。
数组是一种基本的数据结构,它可以存储相同类型的元素,并且具有固定的长度。数组在内存中以连续的方式存储元素,通过索引可以快速访问和修改元素。
集合和List是Java集合框架提供的抽象概念和接口。它们提供了更高级的数据结构和操作方法,用于存储和操作一组元素。
List是集合框架中的一种接口,它表示有序的元素序列,并且允许元素重复。List接口继承自Collection接口,定义了许多操作元素的方法,如添加元素、删除元素、获取元素数量等。List可以通过索引来访问和修改元素。
因此,可以说List是集合的一种特殊类型,它提供了更丰富的操作和访问元素的方式。List接口的常见实现类有ArrayList、LinkedList等,它们使用不同的数据结构来实现List的功能。
在某种程度上,List可以看作是数组的一种更灵活和功能更丰富的替代品。List提供了动态调整长度和元素增删的能力,相对于数组更适合处理动态变化的数据。
综上所述,数组是一种基本的数据结构,而集合和List是在数组基础上提供了更高级的抽象和功能。集合是一个更广义的概念,List是集合中的一种特殊类型,提供了有序且可重复的元素序列。
第三章、泛型
在Java中,泛型(Generics)是一种在编译时期提供类型安全和代码复用的机制。它允许在定义类、接口和方法时使用类型参数,从而使得代码可以处理多种不同的数据类型,同时提供编译时类型检查的好处。
JDK5.0之后的新特性
泛型这种语法机制,只在程序编译阶段起作用,只是给编译器作参考,(运行阶段没用)
因此对于一些没有编译期的解释性语言,例如Python,是没有泛型机制的
一、概述
1、泛型语法
<类型参数> //类型参数只是一个标识符,随便写
2、类型参数(Type Parameters,传入的类型参数不能为基本数据类型)
类型参数是使用泛型的关键部分。它是在类、接口或方法的声明中用尖括号(<>)括起来的标识符。例如,List<T>
中的 T
就是一个类型参数。类型参数可以用作变量类型、方法参数类型、方法返回类型等。
在Java的泛型中,类型参数(Type Parameters)是在泛型类、接口或方法的声明中使用的占位符。它表示一个未知的类型,在实例化或调用时可以被具体的类型替代。类型参数使用尖括号(<>)括起来,并通常用单个大写字母表示,例如
T
、E
、K
等T是type单词首字母
E是element单词首字符
2.1、类型参数不能直接使用基本数据类型
集合框架确实不能直接存储基本数据类型,而且在泛型中也不能使用基本数据类型作为类型参数。
Java集合框架中的集合(如List、Set、Map等)只能存储对象,而基本数据类型不是对象。为了在集合中存储基本数据类型,需要使用对应的包装类(如Integer、Double、Boolean等)来包装这些基本数据类型,然后将包装类的对象存储在集合中。
关于泛型,是的,类型参数也不能直接使用基本数据类型。例如,以下代码是不合法的:
List<int> intList = new ArrayList<>(); // 错误,不能使用基本数据类型作为类型参数
您必须使用包装类作为泛型类型参数:
List<Integer> intList = new ArrayList<>(); // 正确
这样就可以在集合中存储整数对象而不是基本的int值。
总之,基本数据类型在集合框架和泛型中有一些限制,需要通过包装类来间接处理。数组是直接支持基本数据类型存储的一种数据结构。
3、已声明泛型但不指定,默认为Object类型
可以将泛型理解成参数,需要指定类型时,传入指定类型,否则默认为Object
4、类型参数不能直接实例化,不能直接new
不能直接new T( );
5、类型参数只能指代一个类型
类型参数只能指代一个类型,不能指代像 String[ ] ,List<String>,可以写为T [ ] 和List<T>
5、泛型优点
泛型的主要优点是提供了类型安全和代码复用的好处。它可以帮助在编译时捕获类型错误,并减少类型转换的需要。通过使用泛型,可以编写更通用、灵活和可读性更强的代码,同时提高代码的重用性和可维护性。
例如,使用泛型可以创建一个通用的数据结构,如列表(List),可以存储不同类型的元素。在使用列表时,不需要进行类型转换,因为列表的类型参数已经确定。
1、集合中存储的元素统一了
2、从集合中取出的元素类型是泛型指定的类型,不需要进行大量的“向下转型”。
总结来说,Java中的泛型是一种在编译时期提供类型安全和代码复用的机制。它允许使用类型参数来创建泛型类、泛型接口和泛型方法。泛型提供了类型检查、灵活性和代码重用的好处,帮助编写更通用、可读性更强的代码。
6、泛型缺点
也不算是缺点:导致集合中存储的元素缺乏多样性
大多数情况下,集合中元素的类型还是统一的,所以这种泛型机制被大家认可
7、泛型的意义
package com.javase.collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* JDK5.0之后推出的特性:泛型
* 泛型优点:
* 集合中存储的元素类型统一了
* 从集合中取出的元素都泛型指定的类型,不需要进行大量 强制类型转换了
* 泛型缺点:
* 导致集合中缺乏元素类型多样性
*/
public class GenericTest01 {
public static void main(String[] args) {
/**
* 不使用泛型,分析程序的缺点
*/
/*//创建集合
List list = new ArrayList();
//准备对象
Cat c = new Cat();
Bird b = new Bird();
//将元素添加到集合
list.add(c);
list.add(b);
//[com.javase.collection.Cat@15aeb7ab, com.javase.collection.Bird@7b23ec81]
System.out.println(list.toString());
//遍历集合,取出Cat抓老鼠,取出Bird飞翔
// Iterator it = list.iterator();
// while (it.hasNext()){
// Object a = it.next();
// if(a instanceof Cat){
// Cat cat = (Cat) a;
// cat.catchMouse();
// }
// if(a instanceof Bird){
// Bird bird = (Bird) a;
// bird.fly();
// }
// }
//遍历集合取出每个Animal,让其move
Iterator it = list.iterator();
while (it.hasNext()){
Object obj = it.next();
if(obj instanceof Animal){
Animal a = (Animal) obj;
a.move();
}
}*/
/**
* 使用JDK5的泛型机制
*/
//使用泛型 List<Animal> 之后,表示List集合中只允许存储Animal类型的数据
//使用方形来制定集合中存储的数据类型
List<Animal> list = new ArrayList<Animal>();
//指定List集合中只能存储Animal,那么存储String就编译报错
//这样用了泛型之后,集合中元素的数据类型就更加统一了
//list.add("a");
Cat c = new Cat();
Bird b = new Bird();
list.add(c);
list.add(b);
//遍历集合
//这个表示迭代器迭代的是Animal类型数据
Iterator<Animal> it = list.iterator();
while (it.hasNext()){
//使用泛型之后,每一次迭代的数据类型都是Animal类型
Animal a = it.next();
//调用父类的方法不用进行强制类型转换了,可以直接调用
a.move();
//调用子类的特有方法还得向下转型
if(a instanceof Cat){
Cat cat = (Cat) a;
cat.catchMouse();
}
if(a instanceof Bird){
Bird bird = (Bird) a;
bird.fly();
}
}
}
}
class Animal{
public void move(){
System.out.println("动物在移动!");
}
}
class Cat extends Animal{
public void catchMouse(){
System.out.println("猫抓老鼠!");
}
}
class Bird extends Animal{
public void fly(){
System.out.println("鸟儿在飞翔!");
}
}
二、自动类型推断
自动类型推断(Automatic Type Inference)是Java 7引入的一个特性,它允许编译器根据上下文环境推断变量的类型,而无需显式地指定类型。这使得代码更简洁、易读,并减少了类型的冗余。
自动类型推断主要应用于以下几个方面:
1、局部变量类型推断
在声明局部变量时,可以使用关键字var
代替显式的类型声明,编译器会根据变量的初始值推断出变量的类型。
例如:
var name = "John"; // 类型推断为String
var age = 25; // 类型推断为int
var list = new ArrayList<String>(); // 类型推断为ArrayList<String>
2、泛型类型推断:
在使用泛型时,编译器可以根据上下文环境和方法参数的类型推断出泛型的实际类型。
例如:
List<String> names = new ArrayList<>(); // 类型推断为ArrayList<String>
Optional<Integer> result = someMethod(); // 类型推断为Optional<Integer>
package com.javase.collection;
import java.util.*;
/**
* JDK7之后引入了:自动类型推断机制(又称钻石表达式)
*
*/
public class GenericTest02 {
public static void main(String[] args) {
// var name = "jzq";
// System.out.println(name.length());
// var list = new ArrayList<String>();
// list.add()
//ArrayList<这里的类型会自动推断>,前提是JDK7之后
List<Animal> myList = new ArrayList<>();
myList.add(new Animal());
myList.add(new Cat());
myList.add(new Bird());
//遍历
Iterator<Animal> it = myList.iterator();
while (it.hasNext()){
Animal animal = it.next();
animal.move();
}
List<String> stringList = new ArrayList<>();
//类型不匹配
//stringList.add(new Cat());
stringList.add("http://www.baidu.com");
stringList.add("http://www.bjpowernode.com");
System.out.println(stringList.size());
for (int i = 0; i < stringList.size(); i++) {
var str = stringList.get(i);
String substr = str.substring(7);
System.out.println(substr);
}
Iterator<String> it2 = stringList.iterator();
while (it2.hasNext()){
//不使用泛型
/*Object obj = it2.next();
if(obj instanceof String){
String s = (String) obj;
System.out.println(s.substring(7));
}*/
//直接通过迭代器获取到了String类型数据
String s = it2.next();
//直接通过调用String类的subString方法截取字符串
System.out.println(s.substring(7));
}
}
}
3、自动类型推断的优点
自动类型推断的优点包括:
- 减少了冗余的类型声明,使代码更简洁、易读。
- 提高了代码的可维护性,当变量类型需要更改时,只需更改初始值的类型而无需修改变量声明。
- 降低了代码的复杂性,特别是在使用复杂的泛型类型时,可以减少冗长的类型声明。
需要注意的是,自动类型推断并不意味着完全放弃类型声明,仍然建议在某些情况下显式地指定类型,以增加代码的可读性和明确性。
三、泛型分类
1、泛型类(Generic Class):类型参数只能在实例变量或实例方法中
泛型类中的类型参数只能在实例变量或实例方法中
泛型类是具有一个或多个类型参数的类。它在实例化时,可以指定实际的类型参数来确定其具体的数据类型。例如,ArrayList<E>
是一个泛型类,E
是类型参数,可以在实例化时指定为具体的数据类型,如 ArrayList<String>
。
// 定义一个泛型类Box,使用类型参数T
class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 创建一个Box对象,指定类型参数为String
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
// 创建一个Box对象,指定类型参数为Integer
Box<Integer> intBox = new Box<>();
intBox.setContent(42);
// 获取存储的内容,类型推断为String和Integer
String str = stringBox.getContent();
System.out.println(str); //Hello
int num = intBox.getContent(); //自动拆箱
System.out.println(num); //42
在上面的示例中,我们定义了一个泛型类Box<T>
,其中T
是类型参数。通过在实例化Box
对象时指定具体的类型参数,我们可以创建存储不同类型对象的Box
实例。在使用时,类型参数T
被替换为实际的类型,从而使得编译器能够进行类型检查和类型推断。
1.2、泛型类中类型参数不能用于静态变量和静态方法上
在 Java 中,泛型类的类型参数(type parameter)不能直接应用于静态变量和静态方法,这涉及到泛型类型擦除(type erasure)和静态上下文的一些限制。下面我将解释其中的原因:
-
泛型类型擦除:在 Java 中,泛型在编译后会被擦除,这意味着在运行时并不保留泛型的具体类型信息。在泛型类中,类型参数在编译后会被擦除为 Object 或其它边界类型,以便保持与泛型之前的非泛型代码的兼容性。
-
静态上下文:静态变量和静态方法是属于类本身而不是实例的。由于泛型类的类型参数是与实例化对象相关的,而静态成员是与类相关的,这导致泛型类型参数在静态上下文中的使用会有一些限制。
由于泛型类型参数的擦除和静态上下文的限制,将类型参数用于静态变量和静态方法可能会导致类型模糊性和编译器难以解析的问题。
为了解决这个问题,您可以考虑以下几种方法:
-
不使用类型参数:如果您的静态变量或静态方法不需要涉及泛型类型,那么可以在这些成员中使用非泛型类型。
-
将泛型应用于类或实例级别:如果您需要在静态上下文中使用泛型类型,可以将泛型应用于整个类或实例级别,而不是仅应用于静态成员。这可能需要对您的设计进行重新考虑。
-
使用通配符(Wildcard):在某些情况下,您可以使用通配符来表示未知的泛型类型,这可能允许在一些特定场景中使用泛型类型。
总之,Java 中泛型的擦除和静态上下文的特性决定了泛型类型参数不能直接应用于静态变量和静态方法。如果您在静态上下文中需要使用泛型类型参数,可能需要考虑不同的设计和实现方式。
2、泛型接口(Generic Interface)
在Java中,泛型接口(Generic Interface)是具有类型参数的接口。它使用类型参数来定义接口中的方法、属性或其他成员,从而使接口可以适用于多种不同的类型。它的使用方式和泛型类类似,可以在实现该接口时指定实际的类型参数。
// 定义一个泛型接口List,使用类型参数T
interface MyList<T> {
void add(T element);
T get(int index);
}
// 实现泛型接口List,并指定类型参数为String
class StringList implements MyList<String> {
private final String[] elements;
private int size;
public StringList() {
elements = new String[10];
size = 0;
}
public void add(String element) {
elements[size] = element;
size++;
}
public String get(int index) {
return elements[index];
}
}
// 实现泛型接口List,并指定类型参数为Integer
class IntegerList implements MyList<Integer> {
private final Integer[] elements;
private int size;
public IntegerList() {
elements = new Integer[10];
size = 0;
}
public void add(Integer element) {
elements[size] = element;
size++;
}
public Integer get(int index) {
return elements[index];
}
}
// 使用泛型接口List进行操作
MyList<String> stringList = new StringList();
stringList.add("Hello");
stringList.add("World");
String str = stringList.get(0);
System.out.println(str); //Hello
MyList<Integer> intList = new IntegerList();
intList.add(42);
intList.add(10);
int num = intList.get(0);
System.out.println(num); //42
在上面的示例中,我们定义了一个泛型接口List<T>
,其中T
是类型参数。该接口定义了两个方法add
和get
,可以用于向列表中添加元素和获取指定位置的元素。然后,我们分别创建了实现泛型接口的StringList
和IntegerList
类,分别指定了类型参数为String
和Integer
。
通过实现泛型接口,我们可以根据需要创建不同类型的列表,并使用泛型接口中定义的方法进行操作。在使用时,可以根据具体的类型参数进行类型推断,从而使代码更加灵活和类型安全。
泛型接口的使用可以提高代码的通用性和可重用性。它允许在接口中定义方法或其他成员时使用未知类型,从而使接口适用于多种类型的数据结构。
3、泛型方法(Generic Method)
泛型方法中的类型参数是独立于泛型类中的泛型参数的
泛型方法是定义在类或接口中的方法,可以在方法签名中使用类型参数。它可以独立于类的泛型参数而存在,也可以有自己的类型参数。泛型方法可以在调用时根据传递的参数类型进行类型推断,从而实现参数类型的灵活性。
// 定义一个泛型方法printArray,使用类型参数E
public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.println(element);
}
}
// 使用泛型方法打印不同类型的数组
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"Apple", "Banana", "Orange"};
printArray(intArray); // 打印整数数组
printArray(strArray); // 打印字符串数组
在上面的示例中,我们定义了一个泛型方法printArray
,使用类型参数T
。该方法接受一个泛型数组作为参数,并打印数组中的元素。在方法的签名中,使用了<T>
来声明类型参数。
在调用泛型方法时,编译器会根据传递的实际参数类型进行类型推断,并将类型参数T
替换为实际类型。因此,我们可以使用泛型方法printArray
来打印不同类型的数组,无需编写多个具体类型的打印方法。
泛型方法的优点包括:
- 提供更大的灵活性:可以在方法级别上使用类型参数,使方法可以处理不同类型的参数。
- 增加代码的重用性:可以编写一次代码,适用于多种类型的数据。
- 提高类型安全性:编译器可以在编译时进行类型检查,避免在运行时出现类型不匹配的错误。
总之,泛型方法是使用类型参数的方法,可以在调用时适应不同类型的参数。它提供了更灵活、更可重用和类型安全的代码编写方式。
3.1、泛型方法的返回值类型
泛型方法的返回值类型可以根据您的需求来指定,可以是泛型类型参数,也可以是使用泛型类型参数进行操作后得到的具体类型。下面是关于泛型方法返回值类型的一些解释:
返回泛型类型参数:
- 泛型方法的返回类型可以是与方法参数中的泛型类型参数相同的类型。
- 这样的泛型方法可以灵活地返回与输入类型相同的结果。
public <T> T identity(T value) {
return value; // 返回与输入类型相同的值
}
返回经过泛型类型参数处理后的具体类型:
- 泛型方法可以在方法体内对泛型类型参数进行操作,然后返回经过处理后的具体类型。
- 这样的泛型方法可以根据输入的泛型类型参数计算得到不同类型的结果。
public <T> String toStringAndType(T value) {
return value.toString() + " (" + value.getClass().getSimpleName() + ")";
}
返回受限制的类型:
- 泛型方法的返回类型可以是受限制的类型,例如使用通配符来表示返回的类型具有一定的限制。
public <T extends Number> T add(T a, T b) {
// 这里的返回类型可以是 T 或其子类,因为泛型类型参数 T 是 Number 的子类
return (T) (Double) (a.doubleValue() + b.doubleValue());
}
请注意,泛型方法的返回类型取决于您的设计需求。您可以根据需要,结合泛型类型参数进行操作,以获得符合预期的返回类型。在泛型方法中,您可以使用泛型类型参数来增强代码的灵活性和可重用性。
3.2、泛型方法中的类型参数可以在静态数据中也可以在实例数据中
//实例方法
public <E> void m1(E para){
}
//静态方法
public static <E> void m2(E para){
}
4、泛型类和泛型接口相结合
泛型类和泛型接口都是一套类型参数,在泛型类实例化时传入指定类型,则泛型接口中的类型会自动推断
// 定义一个泛型接口List,使用类型参数T
interface MyList<T> {
void add(T element);
T get(int index);
}
// 实现泛型接口List,并指定类型参数为String
class JzqList<T> implements MyList<T> {
private final T[] elements;
private int size;
public JzqList() {
elements = (T[]) new Object[10];
size = 0;
}
public void add(T element) {
elements[size] = element;
size++;
}
public T get(int index) {
return elements[index];
}
}
// 使用泛型接口List进行操作
MyList<String> myList = new JzqList<>();
myList.add("Hello");
myList.add("World");
String str = myList.get(0);
System.out.println(str); //Hello
MyList<Integer> myList1 = new JzqList<>();
myList1.add(1);
myList1.add(2);
Integer i = myList1.get(0);
System.out.println(i); //1
四、通配符(Wildcard)
通配符是用于表示未知类型的符号。在泛型中,可以使用 ?
作为通配符来表示任意类型。通配符可以用于限制或扩展泛型类型的范围。
泛型通配符(Wildcard)是一种在泛型类型中使用的特殊符号,用于表示未知类型或某个范围内的类型。它在泛型代码中用于增加灵活性,并允许处理不同类型的参数。
在Java中,有三种使用泛型通配符的形式:
1、通配符类型
1.1、?
通配符:表示未知类型
?
通配符表示任意类型,可以用作方法参数、方法返回类型或泛型类的类型参数。使用?
通配符时,表示不关心具体的类型,可以接受任何类型的参数或返回任何类型的结果。
例如:
public void process(List<?> list) {
// 处理未知类型的列表
}
public List<?> getList() {
// 返回未知类型的列表
}
1.2、? extends "某个类" 上限
通配符:表示某个类型及其子类或实现类
? extends
"某个类"
通配符表示参数或返回值的类型是"某个类"
的子类或实现类。它限制了通配符的上限,只能接受"某个类"
或"某个类"
的子类作为参数。
extends
"某个类" 只使用来修饰、限定 ?, 最终还是?去传入 声明的 T(类型参数)泛型类型擦除只适用于声明时的类型参数T,与此调用时的?无关
例如:
public void process(List<? extends Number> list) {
// 处理Number及其子类的列表
}
public List<? extends Number> getList() {
// 返回Number及其子类的列表
}
1.3、? super "某个类"
通配符:表示某个类型及其父类或接口
? super
"某个类"
通配符表示参数或返回值的类型是"某个类"
的父类或接口。它限制了通配符的下限,只能接受"某个类"
或"某个类"
的父类作为参数。
super
"某个类" 只使用来修饰、限定 ?, 最终还是?去传入 声明的 T(类型参数)泛型类型擦除只适用于声明时的类型参数T,与此调用时的?无关
例如:
public void process(List<? super Integer> list) {
// 处理Integer及其父类的列表
}
public List<? super Integer> getList() {
// 返回Integer及其父类的列表
}
使用泛型通配符可以增加泛型代码的灵活性和适用性。它使得代码可以处理更广泛的类型,并提供了更好的扩展性。泛型通配符是一种强大的工具,用于处理不同类型的参数和返回值。
2、通配符用法
泛型通配符可以用于方法参数、方法返回类型、泛型类的类型参数以及其他泛型相关的地方。除了方法,还可以在泛型类、接口、变量声明等地方使用通配符。
2.1、方法参数中的通配符
在方法参数中的通配符就相当于在方法参数中又定义了一个小泛型一样
public void process(List<?> list) {
// 处理未知类型的列表
}
public void printElements(List<? extends Number> list) {
// 打印Number及其子类的元素
}
public void processList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
public static void main(String[] args) {
GenericTest02 g = new GenericTest02();
List<? super Number> list = new ArrayList<>();
list.add(1);
list.add(2);
g.processList(list);
/**
* 1
* 2
*/
}
2.2、方法返回类型中的通配符
public List<?> getList() {
// 返回未知类型的列表
}
public List<? extends Number> getNumberList() {
// 返回Number及其子类的列表
}
2.3、泛型类的类型参数中的通配符
class MyGenericClass<T> {
// 泛型类中的类型参数
}
MyGenericClass<?> genericObj; // 创建未知类型的泛型类对象
MyGenericClass<? extends Number> numberObj; // 创建Number及其子类的泛型类对象
2.4、其他泛型相关的地方
List<?> unknownList; // 声明一个未知类型的列表变量
List<? extends Number> numberList; // 声明一个Number及其子类的列表变量
通过在这些不同的场景中使用通配符,可以处理各种类型的参数和结果,并使代码更具灵活性和可重用性。通配符不仅仅限于方法,而是广泛应用于各种泛型相关的场景。
3、通配符相关只能在具体类型传入时使用,不能再泛型声明时使用
通配符?的这种语法只能在传入具体类型的时候使用
通配符 ?
的语法通常用于泛型类型的实际类型传入中,以表示一种未知的类型,或者用于灵活地处理泛型类型的情况。然而,在实际使用中,通配符泛型通常更多用于方法的参数和返回值上,而不是在集合中。
在集合的声明和使用中,通配符 ?
的限定通常被用来表示某种类型的子类型或父类型,以适应更广泛的场景。但是,使用通配符的限制是,您不能对带有通配符的泛型类型进行写操作,例如添加元素。这是因为在使用通配符时,编译器无法确切知道集合中的实际类型,因此无法安全地进行写操作。
通配符泛型在处理某些复杂的泛型场景中很有用,但在普通的集合操作中,通常更常见的是直接使用具体的泛型类型,以便进行更灵活的操作。
总之,通配符 ?
在传入具体类型的情况下使用得更多,而在集合中使用时需要谨慎考虑其限制和用途。
4、集合中使用通配符传入类型添加元素原理
Collection<? extends SomeType> 都添加限定了为什么还无法添加元素尼
我对之前的回答进行了补充,希望能够更清晰地解释这个问题。
使用 Collection<? extends SomeType>
的通配符限定表示集合中的元素是某个类型(SomeType
)的子类型,但具体是哪个子类型是不确定的。这就导致了一个问题,您无法向这样的集合中添加元素,因为编译器不知道添加的元素是否符合集合元素的实际类型。
这是由于 PECS 原则(Producer Extends Consumer Super)的原因。通配符 ? extends SomeType
表示生产者,即从集合中获取元素。您可以从集合中读取元素,但不能添加元素,因为您无法确定要添加的元素是否与集合中元素的实际类型相匹配。
如果您需要向集合中添加元素,您可以使用 ? super SomeType
的通配符限定,表示消费者。这将允许您添加类型为 SomeType
或其子类型的元素。
以下是使用 ? super SomeType
的示例:
Collection<? super String> c = new ArrayList<>();
c.add("1"); // 可以添加元素 "1" 或其子类型
总结起来:
? extends SomeType
:适用于从集合中读取元素。? super SomeType
:适用于向集合中添加元素。
5、通配符原理补充
类型验证通过原则:
第一维度:类型参数处类型必须相同或具有继承关系(只有 ? 通配符系列才和具体类型有继承关系,除此之外对于第一维度来说,其他具体类型包括 Object和String都没有继承关系)
第二维度:泛型类或接口本身类型必须相同或具有继承关系
Java 泛型中的通配符详解_java泛型通配符_swadian2008的博客-CSDN博客
Java泛型中通配符的使用详解_泛型通配符_小鲁蛋儿的博客-CSDN博客
6、PECS:生产者和消费模式
Java泛型中的extends和super_function<? super t,? extends u> fn-CSDN博客
PECS是Java中泛型(Generics)中的一个重要概念,它代表"Producer Extends, Consumer Super"。这个概念主要涉及泛型类型在作为参数时的使用规则,以及在集合或者类设计中如何更好地限制或放宽类型的使用。
-
生产者(Producer): 当你希望从一个数据结构(比如集合)中获取数据时,你希望它提供给你的数据类型是你期望的类型或者其子类型。在这种情况下,你应该使用
<? extends T>
,这个表达式表明类型是T或者T的子类型。这样的数据结构能够提供获取数据的操作,但对于放置(写入)数据则是受限制的。 -
消费者(Consumer): 当你希望向一个数据结构中添加数据时,你希望这个数据结构能够接受你提供的数据类型或者其父类型。在这种情况下,你应该使用
<? super T>
,这个表达式表明类型是T或者T的父类型。这样的数据结构能够接受添加数据的操作,但对于获取数据则是受限制的。
这个原则的目的是确保在使用泛型类型作为参数时不会出现类型不匹配的问题。例如,在一个生产者扩展的情况下(<? extends T>
),你可以安全地从集合中读取元素并确保这些元素是T类型或者其子类型,但是你不能向这个集合中添加元素,因为你不知道集合的确切类型是什么。
相反,在一个消费者超级的情况下(<? super T>
),你可以安全地向集合中添加T类型或者其子类型的元素,但是你不能保证从集合中获取的元素的具体类型,因为它可以是T的父类型的任何类型。
这个原则在Java中用于泛型类型的使用,特别是在集合类中。例如,在编写通用代码时,可以使用这个原则来确保代码的灵活性和类型安全性。
五、集合中泛型表示该集合中元素类型为泛型,需要指定。非集合泛型就是普通的泛型,传入类型即可。
六、泛型也可为多个(2个类型参数)
在Java中,泛型可以使用多个类型参数,也就是可以定义具有两个类型参数的泛型类、接口或方法。这样的泛型结构可以更灵活地处理多个类型的数据,并提供类型安全和代码重用。
以下是一个示例,演示了具有两个类型参数的泛型类和泛型方法的用法:
package com.javase.collection.泛型;
public class 测试两个类型参数 {
public static void main(String[] args) {
Pair<String, Integer> pair1 = new Pair<>("One", 1);
String key = pair1.getKey();
System.out.println(key); // 返回 "One"
Integer value = pair1.getValue();
System.out.println(value); // 返回 1
Pair<Boolean, String> pair2 = Pair.create(true, "Yes");
Boolean key2 = pair2.getKey();
System.out.println(key2); // 返回 true
String value2 = pair2.getValue();
System.out.println(value2); // 返回 "Yes"
}
}
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public static <T, E> Pair<T, E> create(T key, E value) {
return new Pair<>(key, value);
}
}
在上述示例中,Pair
是一个泛型类,具有两个类型参数K和V。它有一个构造函数接受一个键和一个值,并提供了相应的访问方法getKey()
和getValue()
来获取键和值。
此外,示例还展示了一个泛型方法create()
,它接受一个键和一个值,并返回一个Pair
对象。这个方法也使用了两个类型参数K和V。
通过定义具有两个类型参数的泛型类和泛型方法,我们可以在不同的场景中使用不同类型的数据,并在编译时进行类型检查和类型安全。这提高了代码的可重用性和灵活性。
七、泛型使用原则
对于泛型方法:
- 若在调用方法时有返回值,则在返回值类型传入指定类型
- 若在调用方法时没有返回值,则在方法传参时传入指定类型
对于泛型类和泛型接口
- 在声明实例对象类型时传入执行类型