打怪升级之小白的大数据之旅(十五)
Java基础语法之面向对象的内部类
上次回顾:
上一期,我们对面向对象的接口进行了介绍,接口在jdk8前可以理解为一个特殊一些的100%抽象类,是为了让java也可以实现的多继承的效果而存在.本期我将会带来面向对象最后一章内部类、匿名类,这一章中的匿名类是个重点,他为以后我们大数据相关的语法会有帮助,也是理解lambda表达式的基础。然后我会介绍一下java中的异常。好了,开始进入正题:
内部类
内部类就像名字一样,在一个类的内部定义的一个类,假设将一个类A定义在另一个类B里面,那么,里面的类A就成为内部类,B则称之为外部类
概述
为什么要声明内部类呢?请看下图:
- 准确的描述是这样的: 当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整结构又只为外部事物提供服务,不在其他地方单独使用,那么整个内部的完整结构最好使用内部类
- 就像上图,人体可以理解为一个外部类,那么,各个器官就是由许多的内部类组成,每个内部类都会封装自己独特的属性和行为,这些内部类(器官)只为这个人服务,不在其他地方单独使用(器官移植就相当于还是为人这个类服务)
- 因为内部类在外部类里面,因此可以直接访问外部类的私有成员
- 内部类的分类
根据内部类声明的位置(如同变量的分类),我们可以将内部类分为:- 成员内部类
静态内部类
费静态成员内部类 - 局部内部类
有名字的局部内部类
匿名的内部类
- 成员内部类
静态内部类
语法格式
【修饰符】 class 外部类{
【其他修饰符】 static class 内部类{
}
}
示例代码
public class TestInner{
public static void main(String[] args){
Outer.Inner in= new Outer.Inner();
in.inMethod();
Outer.Inner.inTest();
Outer.Inner.inFun(3);
}
}
class Outer{
private static int a = 1;
private int b = 2;
protected static class Inner{
static int d = 4;//可以
void inMethod(){
System.out.println("out.a = " + a);
// System.out.println("out.b = " + b);//错误的
}
static void inTest(){
System.out.println("out.a = " + a);
}
static void inFun(int a){
System.out.println("out.a = " + Outer.a);
System.out.println("local.a = " + a);
}
}
}
静态内部类的特点
- 和其他类一样,它只是定义在外部类中的另一个完整的类结构,主要是为外部类服务的,但与外部类的耦合度并不高。
- 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
- 可以在静态内部类中声明属性、方法、构造器等结构,包括静态成员
- 可以使用abstract修饰,因此它也可以被其他类继承
- 可以使用final修饰,表示不能被继承
- 编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名和$符号。
- 和外部类不同的是,它可以允许四种权限修饰符:public,protected,缺省,private
- 外部类只允许public或缺省的
- 只可以在静态内部类中使用外部类的静态成员
- 在静态内部类中不能使用外部类的非静态成员
- 在外部类的外面不需要通过外部类的对象就可以创建静态内部类的对象
- 如果在内部类中有变量与外部类的静态成员变量同名,可以使用“外部类名."进行区别
非静态成员内部类
语法格式:
【修饰符】 class 外部类{
【修饰符】 class 内部类{
}
}
示例代码:
public class TestInner{
public static void main(String[] args){
Outer out = new Outer();
Outer.Inner in= out.new Inner();
in.inMethod();
Outer.Inner inner = out.getInner();
inner.inMethod();
}
}
class Father{
protected static int c = 3;
}
class Outer{
private static int a = 1;
private int b = 2;
protected class Inner extends Father{
// static int d = 4;//错误
int b = 5;
void inMethod(){
System.out.println("out.a = " + a);
System.out.println("out.b = " + Outer.this.b);
System.out.println("in.b = " + b);
System.out.println("father.c = " + c);
}
}
public static void outMethod(){
// Inner in = new Inner();//错误的
}
public Inner getInner(){
return new Inner();
}
}
非静态内部类的特点
- 和其他类一样,它只是定义在外部类中的另一个完整的类结构,就是为了方便使用外部类的数据,与外部类的耦合度高。
- 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
- 可以在非静态内部类中声明属性、方法、构造器等结构,但是不允许声明静态成员,但是可以继承父类的静态成员,而且可以声明静态常量。
- 可以使用abstract修饰,因此它也可以被其他类继承
- 可以使用final修饰,表示不能被继承
- 编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名和$符号。
- 和外部类不同的是,它可以允许四种权限修饰符:public,protected,缺省,private
- 外部类只允许public或缺省的
- 还可以在非静态内部类中使用外部类的所有成员,哪怕是私有的
- 在外部类的静态成员中不可以使用非静态内部类哦
- 就如同静态方法中不能访问本类的非静态成员变量和非静态方法一样
- 在外部类的外面必须通过外部类的对象才能创建非静态内部类的对象
- 因此在非静态内部类的方法中有两个this对象,一个是外部类的this对象,一个是内部类的this对象
局部内部类
局部内部类就是在成员方法的内部声明的类
语法格式
修饰符】 class 外部类{
【修饰符】 返回值类型 方法名(【形参列表】){
【final/abstract】 class 内部类{
}
}
}
示例代码
class Outer{
private static int a = 1;
private int b = 2;
public static void outMethod(){
final int c = 3;
class Inner{
public void inMethod(){
System.out.println("out.a = " + a);
// System.out.println("out.b = " + b);//错误的,因为outMethod是静态的
System.out.println("out.local.c = " + c);
}
}
Inner in = new Inner();
in.inMethod();
}
public void outTest(){
final int c = 3;
class Inner{
public void inMethod(){
System.out.println("out.a = " + a);
System.out.println("out.b = " + b);//可以,因为outTest是非静态的
System.out.println("method.c = " + c);
}
}
Inner in = new Inner();
in.inMethod();
}
}
局部内部类的特点
- 和外部类一样,它只是定义在外部类的某个方法中的另一个完整的类结构
- 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
- 可以在局部内部类中声明属性、方法、构造器等结构,但不包括静态成员,除非是从父类继承的或静态常量
- 可以使用abstract修饰,因此它也可以被同一个方法的在它后面的其他内部类继承
- 可以使用final修饰,表示不能被继承
- 编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名、$符号、编号。
- 这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
- 和成员内部类不同的是,它前面不能有权限修饰符等
- 局部内部类如同局部变量一样,有作用域
- 局部内部类中是否能访问外部类的静态还是非静态的成员,取决于所在的方法
- 局部内部类中还可以使用所在方法的局部常量,即用final声明的局部变量
- JDK1.8之后,如果某个局部变量在局部内部类中被使用了,自动加final
- 这里有个小重点提一下,那就是局部内部类中使用外部类方法的局部变量时,是无法修改局部变量的值的,示例代码如下:
public class TestInner{
public static void main(String[] args) {
A obj = Outer.method();
//因为如果c不是final的,那么method方法执行完,method的栈空间就释放了,那么c也就消失了
obj.a();//这里打印c就没有中可取了,所以把c声明为常量,存储在方法区中
}
}
interface A{
void a();
}
class Outer{
public static A method(){
final int c = 3;
class Sub implements A{
@Override
public void a() {
System.out.println("method.c = " + c);
}
}
return new Sub();
}
}
因为外部方法中的局部变量在传递给内部类后就在栈内存中销毁了,为了能访问到局部变量的值,java就利用final关键字,将它存放到了方法区内,所以内部类可以访问但不能修改值
匿名内部类(重点)
- 好了,说了这么多的内部类,都是为了匿名内部类作铺垫,就如同学习抽象类是为了引出接口一样,为什么要有匿名内部类呢?
- 匿名内部类实际就是一个没有名字的局部内部类
- 回忆一下,当我们在开发过程中,需要用到一个抽象类的子类的对象或一个接口的实现类的对象,而且只创建一个对象,而且逻辑代码也不复杂。那么我们原先怎么做的呢?
- 编写抽象父类或编写接口
- 编写子类或编写接口的实现类
- 重写父类或接口的方法
- 创建这个子类或实现类的对象
- 对象调用
- 语法格式:
new 类名或接口名(){子类或实现类的成员}
- 示例代码:
// 编写接口
public interface Runnable{
public abstract void run();
}
//声明接口实现类
public class MyRunnable implements Runnable{
public void run(){
while(true){
System.out.println("大家注意安全");
try
Thread.sleep(1000);
}catch(Exception e){
}
}
}
}
// 在测试类中,创建接口的实例并调用
public class Test{
public static void main(String[] args){
//如果MyRunnable类只是在这里使用一次,并且只创建它的一个对象
//分开两个.java源文件,反而不好维护
Runnable target = new MyRunnable();
Thread t = new Thread("安全提示线程",target);
t.start();
}
}
当我们这个子类,或者实现类是一次性的,那么还需要费尽心机来命名,我们最终所需要的仅仅只是为了使用对象去调用而已,此时就可以使用匿名类来实现这个需求:
public class Test{
public static void main(String[] args){
//MyRunnable类只是在这里使用一次,并且只创建它的一个对象,那么这些写代码更紧凑,更好维护
Runnable target = new Runnable(){
public void run(){
while(true){
System.out.println("大家注意安全");
try
Thread.sleep(1000);
}catch(Exception e){
}
}
}
};
Thread t = new Thread("安全提示线程",target);
t.start();
}
}
是不是感觉清爽了很多,代码也简化了很多?下面开始正式结束匿名内部类的语法格式和使用:
语法格式:
// 有参数
new 父类(【实参列表】){
重写方法...
}
//()中是否需要【实参列表】,看你想要让这个匿名内部类调用父类的哪个构造器,如果调用父类的无参构造,那么()中就不用写参数,如果调用父类的有参构造,那么()中需要传入实参
// 无参数
new 父接口(){
重写方法...
}
//()中没有参数,因为此时匿名内部类的父类是Object类,它只有一个无参构造
使用方式一: 匿名内部类的对象直接调用
// 创建接口
interface A{
void a();
}
public class Test{
public static void main(String[] args){
new A(){
@Override
public void a() {
System.out.println("aaaa");
}
}.a();
}
}
class B{
public void b(){
System.out.println("bbbb");
}
}
public class Test{
public static void main(String[] args){
new B(){
public void b(){
System.out.println("ccccc");
}
}.b();
}
}
使用方式二: 通过父类或父接口的变量多态引用匿名内部类的对象
interface A{
void a();
}
public class Test{
public static void main(String[] args){
A obj = new A(){
@Override
public void a() {
System.out.println("aaaa");
}
};
obj.a();
}
}
class B{
public void b(){
System.out.println("bbbb");
}
}
public class Test{
public static void main(String[] args){
B obj = new B(){
public void b(){
System.out.println("ccccc");
}
};
obj.b();
}
}
使用方式三: 匿名内部类作为实参
interface A{
void method();
}
public class Test{
public static void test(A a){
a.method();
}
public static void main(String[] args){
test(new A(){
@Override
public void method() {
System.out.println("aaaa");
}
});
}
}
我写了一个案例,大家可以通过案例感受一下匿名内部类的丝滑~:
public class Demo1 {
public static void main(String[] args) {
// 正常的接口实现
Danceable cat = new Cat();
cat.dance();
// 使用匿名内部类
Danceable cat1 = new Danceable(){
@Override
public void dance() {
System.out.println("猫跳钢管舞");
}
};
cat1.dance();
// 直接使用匿名内部类
new Danceable(){
@Override
public void dance() {
System.out.println("猫跳广场舞");
}
}.dance();
// 使用匿名内部类的对象
test(new Danceable() {
@Override
public void dance() {
System.out.println("猫跳鬼步舞");
}
});
// 超级简化版
// 使用匿名内部类的对象
test(() -> System.out.println("猫跳鬼步舞"));
}
// 匿名内部类的对象作为实参
public static void test(Danceable danceable){
danceable.dance();
}
}
// 定义一个接口
interface Danceable{
void dance();
}
// 定义一个接口实现类
class Cat implements Danceable{
@Override
public void dance() {
System.out.println("猫跳靴子舞");
}
}
示例中的超级简化版就是lambda表达式,我们后面会详细讲到,它的原理就是匿名内部类
Static关键字
此时,我们终于开始对static进行开刀了,从面向对象开始就使用它,但没有具体提到过,今天,来详细挖一挖static关键字的用法,
static关键字,是一个修饰符,它的作用除了将类的内部成员变为静态属性/方法外还可以:
-
成员变量,我们称为类变量,或静态变量,表示某个类的所有对象共享的数据
-
成员方法,我们称为类方法,或静态方法,表示不需要实例对象就可以调用的方法,使用“类名."进行调用
- 父类的静态方法可以被继承不能被重写
- 父接口的静态方法不能被实现类继承
-
代码块,我们称为静态代码块,或静态初始化块,用于为静态变量初始化,每一个类的静态代码块只会执行一次,在类第一次初始化时执行
-
成员内部类,我们称为静态成员内部类,简称静态内部类,不需要外部类实例对象就可以使用的内部类,在静态内部类中只能使用外部类的静态成员
- static不能修饰top-level的类
静态随着类的加载而加载,实例相关的都是后加载,静态无法访问实例,实例可以访问静态。
静态变量:所有实例共享的一份数据。
静态方法:使用类名直接调用的方法(常用于工具类中,Arrays)
静态代码块:常用于在实例使用前准备一些数据的初始化
- static不能修饰top-level的类
-
静态导入
import static 包.类名.静态成员; import static 包.类名.*;
修饰符在一起使用时的问题:
修饰符名称 | 外部类 | 成员变量 | 代码块 | 构造器 | 方法 | 局部变量 |
---|---|---|---|---|---|---|
public | √ | √ | × | √ | √ | × |
protected | × | √ | × | √ | √ | × |
private | × | √ | × | √ | √ | × |
static | × | √ | √ | × | √ | × |
final | √ | √ | × | × | √ | √ |
abstract | √ | × | × | × | √ | × |
native | × | × | × | × | √ | × |
不能和abstract一起使用的修饰符?
(1)abstract和final不能一起修饰方法和类
(2)abstract和static不能一起修饰方法
(3)abstract和native不能一起修饰方法
(4)abstract和private不能一起修饰方法
static和final一起使用:
(1)修饰方法:可以,因为都不能被重写
(2)修饰成员变量:可以,表示静态常量
(3)修饰局部变量:不可以,static不能修饰局部变量
(4)修饰代码块:不可以,final不能修改代码块
(5)修饰内部类:可以一起修饰成员内部类,不能一起修饰局部内部类
注解
首先提一下我们java开篇提到的注释:
注释: 给人看的说明文字
注解: 给程序看的说明,是一种标记
注解的构成:
- 注解的声明:就如同类、方法、变量等一样,需要先声明后使用
- 注解的使用:用于注解在包、类、方法、属性、构造、局部变量等上面的10个位置中一个或多个位置
- 注解的读取:有一段专门用来读取这些使用的注解,然后根据注解信息作出相应的处理,这段程序称为注解处理流程,这也是注解区别与普通注释最大的不同。
元注解: 使用在注解上的注解
@Target(ElementType.METHOD)//注解可以使用的目标位置,比如类,方法,成员变量等上面
@Retention// 注解的有效周期
@Inherited
@OCUMENTED
public@interface MyAnnotation{}
常用的注解
-
@Override
用于检测被修饰的方法为有效的重写方法,如果不是,则报编译错误!
只能标记在方法上。
它会被编译器程序读取。 -
@Deprecated
用于表示被标记的数据已经过时,不建议使用。
可以用于修饰 属性、方法、构造、类、包、局部变量、参数。
它会被编译器程序读取。 -
@SuppressWarnings
抑制编译警告。
可以用于修饰类、属性、方法、构造、局部变量、参数
它会被编译器程序读取。 -
@Target 元注解,用在注解上的注解,例如@Override只能用在方法上
ElementType.MTHOD用在方法上
ElementType.FIELD用在属性上
ElementType.TYPE用在类上 -
@Retention 表述注解的有效范围,有效周期
RetentionPolicy.SOURCE 只在源码中有效
RetentionPolicy.ClASS 只在字节码文件中有效
RetentionPolicy.RUNTIME 在运行期间有效 -
@iNHERITED 表示加了此注解的类的子类同样有此注解
-
@Documented 表示生成的API文档中有次注解信息
-
自定义注解
public @interface 名称{}
示例说明:
初识注解的时候,我将它理解为python中的装饰器,因为它的作用类似装饰器,但java的注解更加强大一些,比如@override,它可以对代码在书写的时候就进行判断.活用注解,可以减少我们很多很多没有注意到的bug
JUnit单元测试
- 当我们在一个项目中,只需要对某个功能进行测试而不是整体运行时,就可以使用单元测试,对我们所需要运行的模块进行运行测试
- 需要单独导入JUnit.jar包
- 原理,运用的是java的反射原理,后面我会介绍java的反射
- 这个单元测试是为了方便我们日常的代码调试的,会使用它就好了,没有必要了解内部,毕竟我们最终是为了学习大数据
- JUnit的安装:
junit是一个java的jar包,可以自行下载也可以在idea中利用Marven仓库下载(后面会提到)
我就接受最简单的安装方式啦:如图:
在@Test后面按Alt + 回车,选择Add ‘JUnit4’ to classpath即可
设计模式
- 设计模式就是多年来各路大神在大量业务开发实践中针对某些需求,总结的一套开发经验,我的理解就是一套模板,设计模式总共有23种,我们可以根据实际需求套用不同的设计模式完成我们的业务逻辑
- 如果我们想要详细学习设计模式,我推荐一本书<大话设计模式>程杰大大写的,很生动,对于小白也很友好(会java,python等编程语言前提下),我们目前所学习的知识点学习它完全够用了,有需求的可以网上下载电子版或者买本书,这里就不详细介绍了
- 因为是面向对象基础的最终章,所以我把能想到的知识点都填补一下哈
- 单例模式
单例模式是最简单的一个设计模式,它的主要作用就是让一个类只能声明一个实例,举个栗子,我有一个音乐播放器,我第一次打开它时,它会开启软件的客户端,当我再次点击,它会将我已经打开的客户端弹出来而非再次重开一个客户端.面试的时候,很多面试官对设计模式的考点就是手撸一个单例模式:
- 懒汉式 有线程安全问题
public class Singleton {
//私有的静态变量
private static Singleton instance = null;
//私有构造器
private Singleton() {
}
//静态的公共的方法获取实例
public static Singleton getInstance() {
if (instance == null)
instance = new Singleton();//第一次调用时创建对象
return instance;
}
}
- 饿汉式
public class Student {
//3.创建一个静态属性,赋值一个实例
private final static Student student=new Student();//只在类加载过程中执行一次,创建一个对象
//1.私有构造器
private Student() {
}
//2.提供公共的静态的方法返回一个此类的实例
public static Student getInstance() {
return student;//每次调用返回同一个对象
}
}
因为懒汉式的单例实现方式是事先为这个唯一实例开辟了一个null的空间,如果是多线程,那么就可能会创造出很多的实例出来
异常
概念
- 对于任何一款软件,为了了解其在运行中的问题,很多时候会对日志文件进行分析,举个栗子,王者荣耀会推出体验服,在它版本发布后,每天程序运行中,会产生一些日志文件,里面会有有些程序bug,因此你会在正式服更新的时候看到: 修复了xxx的异常,
- 这里的异常就是指程序运行中出现的一些问题,因为即使程序员把代码写的再尽善尽美,在运行过程中总会遇到意料之外的问题,因为许多问题并不是靠代码能够避免的
java中异常的原因
java中把常见的不同异常用不同的类表示,当发生某种异常时,JVM会创建该异常类型的对象(其中包含了异常详细信息),并且抛出来,然后程序员可以catch到这个异常对象,并根据需要进行相应处理,如果无法catch到这个异常对象,说明没有针对这个异常预备处理措施,那么这个异常对象将会导致程序终止
异常体系
异常的根类是java.lang.Throwable,Java提供的所有异常类均继承自此类,其下有两个子类:java.lang.Error与java.lang.Exception,平常所说的异常指java.lang.Exception
Throwable体系:
- Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
- 例如: StackOverflowError、OutOfMemoryError。
- Exception:表示异常,其它因编程错误或偶然的外在因素导致的一般性问题,程序员可以通过相应预防处理措施,使程序发生异常后还可以继续运行。好比感冒、阑尾炎。
- 例如:空指针访问、试图读取不存在的文件、网络连接中断、数组角标越界
Throwable中的常用方法:
- public void printStackTrace():打印异常的详细信息。
包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。 - public String getMessage():获取发生异常的原因。
提示给用户的时候,就提示错误原因。 - 出现异常,不用担心,解决它就是了
异常出现后,我们根据异常的信息反馈,进行分析,如上图,根据分析可以发现,是因为我们数组的长度定义了3个,但是在调用时,传递的脚标是第四个,因此造成了脚标越界异常
异常分类
由于Error情况发生是我们无法处理的,一般因为是内存不足或程序存在严重逻辑问题,只能通过扩大内存或重新修改代码解决。
而我们平常所遇到的大多数是Exception类型的异常,也是我们通常说的异常和异常处理。Exception异常又通常分两大类:
-
运行期异常(unchecked Exception):这类异常的发生多数是因为程序员编写的代码逻辑不够严谨造成的(如数组脚标越界异常),可以选择进行处理或不处理,最好是通过修正、优化代码避免异常的发生(或者使用异常处理简化复杂的逻辑判断代码)。
-
编译期异常(checked Exception):这类异常一般由程序之外的因素引起的(如程序读取的文件不存在、网络中断),而不是程序员写的代码逻辑有问题,所以程序员容易忽略对这类异常的处理,而恰恰这类异常又很常发生,所以Java要求针对这类可能发生的异常必须进行处理,否则编译无法通过。(只有java语言有需强制处理的异常)
常见的异常示例一: 虚拟机栈内存不足 VirtualMachineError
/*tackOverflowError:虚拟机栈内存不足,无法分配栈帧所需空间。
OutOfMemoryError:没有足够的内存空间可以分配。*/
@Test
public void test01(){
//StackOverflowError
digui();
}
public void digui(){
digui();
}
@Test
public void test02(){
//OutOfMemoryError
//方式一:
int[] arr = new int[Integer.MAX_VALUE];
}
@Test
public void test03(){
//OutOfMemoryError
//方式二:
StringBuilder s = new StringBuilder();
while(true){
s.append("atguigu");
}
}
常见的异常示例一: 运行时异常
@Test
public void test01(){
//NullPointerException
int[] arr=null;
System.out.println(arr.length);
}
@Test
public void test02(){
//ClassCastException
Person p = new Man();
Woman w = (Woman) p;
}
@Test
public void test03(){
//ArrayIndexOutOfBoundsException
int[] arr = new int[5];
for (int i = 1; i <= 5; i++) {
System.out.println(arr[i]);
}
}
@Test
public void test04(){
//InputMismatchException
Scanner input = new Scanner(System.in);
System.out.print("请输入一个整数:");
int num = input.nextInt();
}
@Test
public void test05(){
int a = 1;
int b = 0;
//ArithmeticException
System.out.println(a/b);
}
常见的异常示例一: 编译时异常
@Test
public void test06() throws InterruptedException{
Thread.sleep(1000);//休眠1秒
}
@Test
public void test07() throws FileNotFoundException{
FileInputStream fis = new FileInputStream("Java学习秘籍.txt");
}
@Test
public void test08() throws SQLException{
Connection conn = DriverManager.getConnection("....");
}
异常的生成与抛出机制
Java程序的执行过程中如出现异常,会生成一个异常类对象,然后该异常对象会被提交给Java运行时系统,这个过程称为抛出(throw)异常。异常对象的生成与抛出有两种方式:
-
由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,就会在后台自动创建一个对应异常类的实例对象并自动抛出。
我们通过示例分析下一次产生的过程:
//运行以下程序会产生一个数组索引越界异常ArrayIndexOfBoundsException。 // 工具类 public class ArrayTools { // 对给定的数组通过给定的角标获取元素。 public static int getElement(int[] arr, int index) { int element = arr[index]; return element; } } // 测试类 public class ExceptionDemo { public static void main(String[] args) { int[] arr = { 34, 12, 67 }; intnum = ArrayTools.getElement(arr, 4) System.out.println("num=" + num); System.out.println("over"); } }
异常产生过程图解:
由此看出,异常对象被JVM创建后,在产生异常的方法中会自动抛出,抛给方法的调用者,抛给main方法,最后抛给虚拟机,虚拟机打印异常信息后终止程序 -
由开发人员手动创建:Exception exception = new ClassCastException();——创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样,手动创建的异常对象需要手动抛出,才会对程序产生影响。
在Java中,使用关throw关键字手动抛出一个异常对象,throw用在方法内,将这个异常对象传递到方法调用者处,同时结束当前方法的执行。
语法格式:
throw new 异常类名(参数);
示例代码:
public class ThrowDemo {
public static void main(String[] args) {
//创建一个数组
int[] arr = {2,4,52,2};
//根据索引找对应的元素
int index = 4;
int element = getElement(arr, index);
System.out.println(element);
System.out.println("over");
}
/*
* 根据 索引找到数组中对应的元素
*/
public static int getElement(int[] arr,int index){
if(arr == null){
/*
判断条件如果满足,当执行完throw抛出异常对象后,方法已经无法继续运算。
这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。
*/
throw new NullPointerException("要访问的arr数组不存在");
}
//判断 索引是否越界
if(index<0 || index>arr.length-1){
/*
判断条件如果满足,当执行完throw抛出异常对象后,方法已经无法继续运算。
这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。
*/
throw new ArrayIndexOutOfBoundsException("哥们,角标越界了~~~");
}
int element = arr[index];
return element;
}
}
异常的处理机制
异常的产生和过程我们了解清楚了,下面我们来学习如何对异常进行处理:
try…catch
捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。
-
try:捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的代码放在try语句块中。建议:此范围尽量小。
-
catch:用来进行某种异常的捕获,实现对捕获到的异常进行处理。每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常。
- 可以有多个catch块,按顺序匹配。
- 如果多个异常类型有包含关系,那么小上大下
-
获取异常信息:
捕获到了异常对象,就可以获取异常对象中封装的异常信息,Throwable类中定义了一些方法用于获取异常对象中的信息:- public String getMessage():获取异常的描述信息。
- public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。这些信息包含了异常的类型,异常信息,还包括异常出现的位置,在开发和调试阶段,建议使用printStackTrace。
捕获异常语法如下
try{
编写可能会出现异常的代码
}catch(异常类型1 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}catch(异常类型2 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
....
示例代码:
public class TestException {
public static void main(String[] args) {
try {
readFile("不敲代码学会Java秘籍.txt");
} catch (FileNotFoundException e) {
// e.printStackTrace();
// System.out.println("好好敲代码,不要老是想获得什么秘籍");
System.out.println(e.getMessage());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
System.out.println("继续学习吧...");
}
// 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
public static void readFile(String filePath) throws FileNotFoundException, IllegalAccessException{
File file = new File(filePath);
if(!file.exists()){
throw new FileNotFoundException(filePath+"文件不存在");
}
if(!file.isFile()){
throw new IllegalAccessException(filePath + "不是文件,无法直接读取");
}
//...
}
}
finally块
- 在finally代码块中存放的代码都是一定会被执行的。由于异常会引发程序跳转,导致后面有些语句执行不到,如果一定要执行这些语句就可以使用finally,finally常用于释放系统资源。
- 比如:当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),无论异常有没有发生,我们都要在使用完之后关闭已打开的资源,避免系统资源的浪费
- finally不能单独使用
- 语法格式:
try{ }catch(...){ }finally{ 无论try中是否发生异常,也无论catch是否捕获异常,也不管try和catch中是否有return语句,都一定会执行 } 或 try{ }finally{ 无论try中是否发生异常,也不管try中是否有return语句,都一定会执行。 }
示例代码: IO流后面也会介绍
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class TestException {
public static void main(String[] args) {
readFile("不敲代码学会Java秘籍.txt");
System.out.println("继续学习吧...");
}
// 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
public static void readFile(String filePath) {
File file = new File(filePath);
FileInputStream fis = null;
try {
if(!file.exists()){
throw new FileNotFoundException(filePath+"文件不存在");
}
if(!file.isFile()){
throw new IllegalAccessException(filePath + "不是文件,无法直接读取");
}
fis = new FileInputStream(file);
//...
} catch (Exception e) {
//抓取到的是编译期异常 抛出去的是运行期
throw new RuntimeException(e);
}finally{
System.out.println("无论如何,这里的代码一定会被执行");
try {
if(fis!=null){
fis.close();
}
} catch (IOException e) {
//抓取到的是编译期异常 抛出去的是运行期
throw new RuntimeException(e);
}
}
}
}
finally与return
当在try…catch…finally中使用了return会发生什么呢?程序的运行顺序是什么?
形式一:从try回来
public class TestReturn {
public static void main(String[] args) {
int result = test("12");
System.out.println(result);
}
public static int test(String str){
try{
Integer.parseInt(str);
return 1;
}catch(NumberFormatException e){
return -1;
}finally{
System.out.println("test结束");
}
}
}
形式二: 从catch回来
public class TestReturn {
public static void main(String[] args) {
int result = test("a");
System.out.println(result);
}
public static int test(String str){
try{
Integer.parseInt(str);
return 1;
}catch(NumberFormatException e){
return -1;
}finally{
System.out.println("test结束");
}
}
}
形式三:从finally回来
public class TestReturn {
public static void main(String[] args) {
int result = test(“a”);
System.out.println(result);
}
public static int test(String str){
try{
Integer.parseInt(str);
return 1;
}catch(NumberFormatException e){
return -1;
}finally{
System.out.println("test结束");
return 0;
}
}
}
根据上面的案例,我们可以得出,finally虽然最后会执行,但如果finally没有return返回值的时候,方法的返回值是在满足条件的return中,如果finally中return了返回值,那么不论前面return了什么返回值,都会被覆盖掉
自定义异常
- 既然JVM自定义了这么多异常,那么我们为什么还需要自定义异常呢?
- 因为业务逻辑,我们的业务逻辑不可能仅仅使用if流程控制就可以得出现实情况的所有可能性,因此搭配自定异常,可以完成现实生活中的种种业务逻辑
- 保持一个合理的异常体系是很重要的,一般自定义一个异常UserException作为“根异常”,然后在此基础上再派生出不同的异常类型,自定义的“根异常”需要从一个合适的现有异常中派生出来,通常建议派生自java.lang.RuntimeException
- 自定义异常的步骤:
// 第一步,定义"根异常":
public class UserException extends RuntimeException {
}
// 第二步,其他异常从”根异常“派生出来:
public class UserExistedException extends UserException {
}
public class UserNotFoundException extends UserException {
}
...
// 第三步,自定义的“根异常”通常提供多个构造方法,直接调用父类的即可:
//用户异常类
public class UserException extends RuntimeException {
public UserException() {
}
public UserException(String message) {
super(message);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
}
// 使用
//用户已经存在异常类
public class UserExistedException extends UserException {
public UserExistedException() {
}
public UserExistedException(String message) {
super(message);
}
}
案例演示:
public class DemoUserException {
// 模拟数据库中已存在账号
private static String[] names = {"bill","hill","jill"};
public static void main(String[] args) {
//调用方法
try{
// 可能出现异常的代码
checkUsername("bill");
System.out.println("注册成功");//如果没有异常就是注册成功
}catch(UserExistedException e){
//处理异常
e.printStackTrace();
}
}
//判断当前注册账号是否存在
//因为是编译期异常,又想调用者去处理 所以声明该异常
public static boolean checkUsername(String uname) {
for (int i=0; i<names.length; i++) {
if(names[i].equals(uname)){//如果名字在这里面 就抛出登陆异常
throw new UserExistedException("亲"+uname+"已经被注册了!");
}
}
return true;
}
}
异常关键字与注意事项
-
异常处理中的5个关键字
-
异常处理注意事项
- 编译期异常必须处理,要么捕获处理,要么声明在方法上,让调用者处理。
- 运行时异常被抛出可以不处理。即不捕获也不声明抛出。
- try语句范围要尽量小的包围在可能出现异常的一行或几行代码上,不要把大量无异常的代码一起包起来,虽然这样很省事。
- catch语句捕获的异常类型要尽量小,尽量精准,好针对性的做处理。
- 如果finally有return语句,永远返回finally中的结果,但要避免该情况.
- 如果父类方法抛出了多个异常,子类重写父类方法时不能抛出更大异常,可以抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。
- 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。
总结
面向对象的基础到此完结,如果学到此时,你觉得还OK,那么恭喜你,你已经成功度过了零基础小白最艰难的时光,后面的学习会相对轻松一些,本章重点对匿名类以及异常进行介绍,学会使用匿名类,可以大大减少我们日常工作中的代码量,也是未来学习大数据的一个基础,异常通常会在捕获后传递到日志文件中,而我们大数据也会经常操作日志文件.所以,每一个知识点都是有联系的…好了,下一期,我会对java中的常用核心类:另外一个引用数据类型–String进行介绍,欢迎大家后台吐槽~