Java(四)内部类、包装类、异常、日期

文章目录

    • 一、内部类
      • 1.1 内部类的分类
        • 1.1.1 成员内部类
        • 1.1.2 静态内部类
        • 1.1.3 局部内部类(了解即可)
        • 1.1.4 匿名内部类
      • 1.2 内部类的优点
      • 1.3 内部类有哪些应用场景
      • 1.4 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?
    • 二、包装类
      • 2.1 包装类的由来
      • 2.2 自动装箱和自动拆箱
      • 2.3 包装类和基本类型的互相转换
      • 2.4 包装类的相关问题
        • 2.4.1 空指针问题
        • 2.4.2 常量池问题
    • 三、异常
      • 3.1 异常的分类
        • 3.1.1 Throwable
        • 3.1.2 Error
        • 3.1.3 Exception
        • 3.1.4 常见非受检异常
        • 3.1.5 常见受检异常
        • 3.1.6 运行时异常和一般异常(受检异常)区别是什么?【重要】
        • 3.1.7 NoClassDefFoundError和ClassNotFoundException 区别?
      • 3.2 异常处理机制
        • 3.2.1 异常处理中的关键字
        • 3.2.2 异常处理的语句形式
        • 3.2.3 try...catch的处理顺序
        • 3.2.4 try...catch使用的注意事项
        • 3.2.5 try-with-resource
      • 3.3 Java异常处理过程
        • 3.3.1 声明异常
        • 3.3.2 抛出异常
        • 3.3.3 捕获异常
      • 3.4 自定义异常
      • 3.5 final/finally/finalize
        • 3.5.1 final/finally/finalize的区别
        • 3.5.2 final和finally的使用差异
        • 3.5.3 finally块和finalize方法的执行时机
      • 3.6 异常处理-阿里巴巴Java开发手册
      • 3.7 一些异常相关的问题
        • 3.7.1 常见异常处理方式
        • 3.7.2 异常是一起处理好还是分开处理好?
        • 3.7.3 throw和throws的区别?
        • 3.7.4 检查型异常与非检查型异常区别?
        • 3.7.5 JVM是如何处理异常的?
        • 3.7.6 try-catch-finally中哪个部分可以省略?
        • 3.7.7 关闭资源的两种方式
        • 3.7.8 优先捕获最具体的异常
        • 3.7.9 异常会影响性能
        • 3.7.10 finally语句什么时候不会执行
        • 3.7.11 异常会影响性能
    • 四、日期
      • 4.1 Date类
      • 4.2 SimpleDateFormat类
      • 4.3 Calendar类
      • 4.4 Date和Calendar之间的相互转换

  本系列文章:
    Java(一)数据类型、变量类型、修饰符、运算符
    Java(二)分支循环、数组、字符串、方法
    Java(三)面向对象、封装继承多态、重写和重载、枚举
    Java(四)内部类、包装类、异常、日期
    Java(五)反射、克隆、泛型、语法糖
    Java(六)IO
    Java(七)Lambda、Stream、新日期类、Optional

一、内部类

  将一个类的定义放在里另一个类的内部,就是内部类。可以将内部类看作类的一个属性,与其他属性定义方式一致。

1.1 内部类的分类

  内部类可以分为四种:成员内部类、静态内部类、局部内部类、匿名内部类

1.1.1 成员内部类

  成员内部类是最常见的内部类,即一个类中嵌套了另一个类,无特殊修饰符。成员内部类的语法:

	new 外部类().new 内部类()

  成员内部类示例:

package Inner;

public class OutClass {
    private int outerVariable = 1;
    private int commonVariable = 2;
    private static int outerStaticVariable = 3;
    
    public class Inner {
        
        private int commonVariable = 20;
        public Inner() {
        }

        public void innerShow() {
            /*当和外部类属性名相同时,直接引用属性名,访问的是内部类的成员属性*/
            System.out.println("内部类、外部类中变量同名时,直接访问的是内部的变量:" + commonVariable);
            /*不同名情况下,内部类可直接访问外部属性*/
            System.out.println("outerVariable:" + outerVariable+",outerStaticVariable:"+outerStaticVariable);
            /*当和外部类属性名相同时,可通过外部类名.this.属性名来访问外部变量*/
            System.out.println("内部类、外部类中变量同名时,需要用外部类类名来访问外部的变量:" + OutClass.this.commonVariable);
        }
    }
    
    /*将内部类中的接口,包装成外部类中的方法,这样其他类可方便地调用内部类中的接口*/
    public void outerShow() {
        Inner inner = new Inner();
        inner.innerShow();
    }
}

  测试代码:

public class InnerTest {
	public static void main(String[] args){
		OutClass outClass=new OutClass();
		outClass.outerShow();
	}
}

  这个例子中可以看出内部类和外部类访问的一些简单规则:成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式外部类实例.new 内部类()
  当内部类和外部类中有相同名称的变量时,在内部类中需要用“外部类.this.变量名”的形式才能访问。
  当然,在其他类中,也可以创建内部类对象,调用内部类中的方法,示例:

public class InnerTest {
	public static void main(String[] args){		
		OutClass outer = new OutClass();
		OutClass.Inner inner = outer.new Inner();
	    inner.innerShow();
	}
}

  上面测试代码的输出结果与之前测试结果一致,并且这也是创建成员内部类对象的固定格式,即:

  1. 先用new的方式,创建外部类对象,如OutClass outer = new OutClass();
  2. 然后用 “外部类类名.内部类类名 内部类变量名 = 外部类对象.new 内部类类名()” 的方式创建内部类对象。

 成员内部类的特点:

  1. 可以是任何的访问修饰符。
  2. 成员内部类的内部不能有静态信息
  3. 成员内部类也是类,具有普通类的特性,如继承、重写、重载等。
  4. 外部类要访问内部类信息,需要先创建内部类对象,才能访问
  5. 成员内部类可以直接使用外部类的任何信息,如果属性或者方法同名,调用外部类.this.属性或者方法

1.1.2 静态内部类

  定义在类内部的静态类,就是静态内部类。在静态内部类中,只能访问外部类中static方法和static变量,其他用法与成员内部类相似。静态内部类实例创建的语法:

	new 外部类.静态内部类()

  静态内部类示例:

/*外部类*/
public class OutClass {
    private int outerVariable = 1;
    private int commonVariable = 2;
    
    private static int outerStaticVariable = 3;

    static {
        System.out.println("OutClass-->静态块");
    }

    public static void outerStaticMethod() {
        System.out.println("外部类-->静态方法");
    }

    public static class Inner {
        private int innerVariable = 10;
        private int commonVariable = 20;

        static {
            System.out.println("Inner-->静态块");
        }

        private static int innerStaticVariable = 30;

        public void innerShow() {
            System.out.println("内部类中变量innerVariable:" + innerVariable);
            System.out.println("内部类中与外部类同名变量commonVariable:" + commonVariable);
            System.out.println("外部类中变量outerStaticVariable:"+outerStaticVariable);
            outerStaticMethod();
        }

        public static void innerStaticShow() {
        	//被调用时会先加载Outer类
            //outerStaticMethod();
        }
    }

    public static void callInner() {
        System.out.println(Inner.innerStaticVariable);
        Inner.innerStaticShow();
    }
}
/*测试类*/
public class InnerTest {
    public static void main(String[] args) {
        //访问静态内部类的静态方法,Inner类被加载,此时外部类未被加载,独立存在,不依赖于外围类。
    	OutClass.Inner.innerStaticShow();
        //访问静态内部类的成员方法
//    	OutClass.Inner oi = new OutClass.Inner();
//        oi.innerShow();
    }
}

  此时的测试结果为:

Inner–>静态块

  从这个例子可以看出,当静态内部类不访问外部类中的static变量或static方法时,是不会调用外部类的静态代码块的。将上述外部类代码稍微修改:

public class OutClass {
    private int outerVariable = 1;
    private int commonVariable = 2;
    
    private static int outerStaticVariable = 3;

    static {
        System.out.println("OutClass-->静态块");
    }

    public static void outerStaticMethod() {
        System.out.println("外部类-->静态方法");
    }

    public static class Inner {
        private int innerVariable = 10;
        private int commonVariable = 20;

        static {
            System.out.println("Inner-->静态块");
        }

        private static int innerStaticVariable = 30;

        public void innerShow() {
            System.out.println("内部类中变量innerVariable:" + innerVariable);
            System.out.println("内部类中与外部类同名变量commonVariable:" + commonVariable);
            System.out.println("外部类中变量outerStaticVariable:"+outerStaticVariable);
            outerStaticMethod();
        }

        public static void innerStaticShow() {
        	//被调用时会先加载OutClass类
            outerStaticMethod();
        }
    }

    public static void callInner() {
        System.out.println(Inner.innerStaticVariable);
        Inner.innerStaticShow();
    }
}

  此时的测试结果:

Inner–>静态块
OutClass–>静态块
外部类–>静态方法

  可以看出,在内部类中访问外部static变量或static方法时,就会加载外部类的静态代码块,不过是在加载完内部类的静态代码块之后
  静态内部类的特点:

1>静态内部类的方法只能访问外部类的static变量或static方法。
2>访问内部类的静态信息的形式是:直接外部类.内部类.静态信息。
3>静态内部类可以独立存在,不依赖于其他外部类。

1.1.3 局部内部类(了解即可)

  局部内部类的位置和之前的两个类不一样,不再是在一个类内部,而是在方法内部
  定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法
  局部内部类的使用,和之前的两种内部类差别主要有两点:

  1. 访问方法内的变量时,变量需要用final修饰
  2. 局部内部类只能在方法内使用

  示例:

package Inner;
/*外部类*/
public class OutClass {
    private int outerVariable = 1;
    private int commonVariable = 2;
    private static int outerStaticVariable = 3;

    public void outerMethod() {
        System.out.println("外部类-->普通方法");
    }

    public static void outerStaticMethod() {
        System.out.println("外部类-->静态方法");
    }
    
    public void outerCreatMethod(final int value) {
        final boolean inOut = false;

        class Inner {

            private int innerVariable = 10;
            private int commonVariable = 20;

            public void innerShow() {
                System.out.println("内部类-->变量innerVariable:" + innerVariable);
                /*局部变量*/
                System.out.println("是否直接在外部类中:" + inOut);
                System.out.println("内部类所在方法的参数value:" + value);
                /*访问外部类的变量、方法*/
                System.out.println("外部类中的普通变量outerVariable:" + outerVariable);
                System.out.println("访问内部类的同名变量commonVariable:" + commonVariable);
                System.out.println("访问外部类的同名变量commonVariable:" + OutClass.this.commonVariable);
                System.out.println("外部类中的静态变量outerStaticVariable:" + outerStaticVariable);
                outerMethod();
                outerStaticMethod();
            }
        }
        /*局部内部类只能在方法内使用*/
        Inner inner = new Inner();
        inner.innerShow();
    }
}

  测试类代码如下:

public class InnerTest {
    public static void main(String[] args) {
    	OutClass outer = new OutClass();
        outer.outerCreatMethod(100);
    }
}

  测试结果:

内部类–>变量innerVariable:10
是否直接在外部类中:false
内部类所在方法的参数value:100
外部类中的普通变量outerVariable:1
访问内部类的同名变量commonVariable:20
访问外部类的同名变量commonVariable:2
外部类中的静态变量outerStaticVariable:3
外部类–>普通方法
外部类–>静态方法

  局部内部类的特点:

1>类前不能有访问修饰符。
2>使用范围为当前方法内。
3>不能声明static变量和static方法。
4>JDK8以前(不包括8)只能访问被final修饰的变量,不论是方法接收的参数,还是方法内的参数,JDK8后隐式地加上final
5>可以随意的访问外部类的变量和方法。

1.1.4 匿名内部类

  匿名内部类就是没有名字的内部类,本质上是一个重写或实现了父类或接口的子类对象。
  匿名内部类创建方式:

	new/接口{
		//匿名内部类实现部分
	}

  匿名内部类的使用场景:一般是只使用一次某个接口的实现类时。示例:

/*定义一个接口*/
public interface Sport{
	void play();
}
/*测试类*/
public class OutClass {
	
    public static void main(String[] args){
    	OutClass.getInnerInstance("打篮球").play();
    }
	
    public static Sport getInnerInstance(final String sport){
        return new Sport(){
            @Override
            public void play(){
                System.out.println(sport);
            }
        };
    }
}

  测试结果:

打篮球

  匿名内部类的特点:

1>匿名内部类无访问修饰符。
2>使用匿名内部类的主要目的重写new后的类的某个或某些方法(匿名内部类必须继承一个抽象类或者实现一个接口)。
3>匿名内部类访问方法参数时也有和局部内部类同样的限制。
4>匿名内部类没有构造方法。
5>当所在的方法的形参需要被匿名内部类使用时,必须声明为final
6>匿名内部类不能定义任何静态成员和静态方法
7>匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。

  当然最常见的匿名内部类就是线程:

        new Thread( new Runnable() {  
            public void run(){                         
                System.out.println("test");
            }
        }).start(); 

1.2 内部类的优点

  • 1、内部类可以实现和外部类不同的接口,也可以继承和外部类不同的类,间接完成功能扩展
  • 2、内部类有效实现了“多重继承”,即用内部类去继承其他类,优化 java 单继承的缺陷。
  • 3、内部类中的属性、方法可以和外部类重名,但并不冲突,因为内部类是具有类的基本特征的独立实体。
  • 4、内部类利用访问修饰符隐藏内部类的实施细节,提供了更好的封装,除外部类,都不能访问。
  • 5、静态内部类使用时可直接使用,不需先创造外部类。
  • 6、 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据
  • 7、匿名内部类可以很方便的定义回调
     匿名内部类往往是做为一个内部类(接口)的具体实现。

1.3 内部类有哪些应用场景

  • 1、一些多算法场合
      将部分实现转移给使用者,让使用者决定算法的实现。
  • 2、解决一些非面向对象的语句块
      也是类似于模板方法模式的使用,将if…else if…else语句,case语句等转移到子类中去实现。
  • 3、适当使用内部类,使得代码更加灵活和富有扩展性
      常见的是使用模板方法模式的时候,将部分接口的实现转移到子类中实现
  • 4、当某个类除了它的外部类,不再被其他的类使用时
      一个内部类依附于它的外部类而存在,可能的原因有:
  1. 不可能为其他的类使用;
  2. 出于某种原因,不能被其他类引用,可能会引起错误。

1.4 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?

  先说结论:在JDK8之前,如果我们在匿名内部类中需要访问局部变量,那么这个局部变量必须用final修饰符修饰;在JDK8中,如果我们在匿名内部类中需要访问局部变量,那么这个局部变量不需要用final修饰符修饰,其原因是:看似是一种编译机制的改变,实际上就是一个语法糖(底层还是帮你加了final)。
  这种现象的原因是:用final修饰实际上就是为了保护数据的一致性。这里所说的数据一致性,对引用变量来说是引用地址的一致性,对基本类型来说就是值的一致性。

  • 为什么需要用final保护数据的一致性呢?
      因为将数据拷贝完成后,如果不用final修饰,则原先的局部变量可以发生变化。如果局部变量发生变化后,匿名内部类是不知道的(因为他只是拷贝了局部变量的值,并不是直接使用的局部变量)。这里举个例子:原先局部变量指向的是对象A,在创建匿名内部类后,匿名内部类中的成员变量也指向A对象。但过了一段时间局部变量的值指向另外一个B对象,但此时匿名内部类中还是指向原先的A对象。那么程序再接着运行下去,可能就会导致程序运行的结果与预期不同。
    Java(四)内部类、包装类、异常、日期_第1张图片

二、包装类

2.1 包装类的由来

  Java是一个面向对象的语言,同时Java中存在着8种基本数据类型,为每个基本数据类型设计一个对应的类进行代表,这种方式增强了Java面向对象的性质。
  很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,无法将int 、double等类型放进去的,因为集合的容器要求元素是Object类型。而包装类型的存在使得向集合中传入数值成为可能,包装类的存在弥补了基本数据类型的不足
  此外,包装类还为基本类型添加了属性和方法,丰富了基本类型的操作。比如int类型的最大值和最小值,直接用哪个Integer.MAX_VALUE和Integer.MIN_VALUE表示即可。
  Java有8种基本数据类型:byte、short、int、long、float、double、boolean、char,因此包装类也有8种:

基本类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Character
char Boolean
  Number 是所有数字包装类的父类。

2.2 自动装箱和自动拆箱

  因为包装类是对象,而基本数据不是对象,所以预想中,两者应该是有转换机制的,示例:

		/*基本数据类型转为包装类*/
		Integer num1 = new Integer(1);	
		/*包装类型转为基本数据类型*/
		int num2 = num1.intValue();		
		System.out.println("包装类值:"+num1+",基本类型值:"+num2);

  所谓的自动装箱和自动拆箱,就是说不用这么明显的转换,系统会默认装换,示例:

		/*自动装箱,编译器会改为 new Integer(1)*/
		Integer num1 = 1;	
		/*自动拆箱,编译器会修改为new Integer(1).intValue()*/
		int num2 = num1;		
		System.out.println("包装类值:"+num1+",基本类型值:"+num2);  //包装类值:1,基本类型值:1

  基本类型和包装类型为什么可以直接相互赋值呢?这其实是Java中的一种“语法糖”。“语法糖”是指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
  在自动装箱的时候,基本类型要与包装类类型一一对应;自动拆箱的时候,包装类类型 <= 基本类型就可以
  自动装箱和拆箱发生的场景:

  1. 赋值操作(装箱或拆箱);
  2. 进行加减乘除混合运算 (拆箱);
  3. 进行>,<,==比较运算(拆箱);
  4. 调用equals进行比较(装箱);
  5. ArrayList、HashMap等集合类添加基础类型数据时(装箱)。

2.3 包装类和基本类型的互相转换

  由于自动装箱和拆箱的操作,包装类和基本类型的互相转换其实不需要开发者手动进行。以下是相互转换的方式:

基本类型 基本类型–>包装类 包装类–>基本类型
byte new Byte / valueOf byteValue
short new Short / valueOf shortValue
int new Integer / valueOf intValue
long new Long / valueOf longValue
float new Float / valueOf floatValue
double new Double / valueOf doubleValue
boolean new Boolean / valueOf booleanValue
char new Character / valueOf charValue
  以Integer为例,我们看一下它的valueOf 方法,示例:
    public static Integer valueOf(int i) {
        return  i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128];
    }

  从上面代码可以看出,其实包装类的valueOf方法,还是通过 ‘new 包装类()’ 的方式来创建包装类对象的。
  基本类型和包装类互相转换,示例:

		/*基本型转换为包装类对象*/
		Byte num1 = new Byte((byte) 1);
		Short num2 = new Short((short) 2); 
		Integer num3 = new Integer(3);	
		Long num4 = new Long(4);
		Float num5 = new Float(5.0);
		Double num6 = new Double(6.0);
		Character num7 = new Character((char) 99);
		Boolean bool1 = new Boolean(true);
		//包装类值,Byte型:1,Short型:2,Integer型:3,Long型:4,Float型:5.0,Double型:6.0,Character型:c,Boolean型:true
		System.out.println("包装类值,Byte型:"+num1+",Short型:"+num2+",Integer型:"+num3+",Long型:"+num4
				+",Float型:"+num5+",Double型:"+num6+",Character型:"+num7+",Boolean型:"+bool1);
		
		/*包装类转换为基本类型*/
		byte num11 = num1.byteValue();
		short num12 = num2.shortValue();
		int num13 = num3.intValue();
		long num14 = num4.longValue();
		float num15 = num5.floatValue();
		double num16 = num6.doubleValue();
		char num17 = num7.charValue();
		boolean bool2 = bool1.booleanValue();
		//基本类型值,byte型:1,short型:2,int型:3,long型:4,float型:5.0,double型:6.0,char型:c,boolean型:true
		System.out.println("基本类型值,byte型:"+num11+",short型:"+num12+",int型:"+num13+",long型:"+num14
				+",float型:"+num15+",double型:"+num16+",char型:"+num17+",boolean型:"+bool2);	

2.4 包装类的相关问题

2.4.1 空指针问题

  常见的形式是:

		Integer num1 = null;
		int num2 = num1;

  此时运行代码,会提示空指针,原因是:将num1的值赋给num2时,会先进行自动拆箱,也就是num1.intValue(),此时num1是null,所以报了空指针异常。

2.4.2 常量池问题

  先看个例子:

		Integer int1 = 1;
		Integer int2 = 1;
		System.out.println(int1 == int2);  //true

		Integer int3 = 200;
		Integer int4 = 200;
		System.out.println(int3 == int4);  //false

  用int值创建Integer对象时,有个默认装箱的操作,不过对int的值是有要求的:

	public static Integer valueOf(int i) {
	    // 判断实参是否在可缓存范围内,默认为[-128, 127]
	    if (i >= IntegerCache.low && i <= IntegerCache.high) 
	        return IntegerCache.cache[i + (-IntegerCache.low)]; 
	    return new Integer(i); 
	}

  从这段源码可以看出,当用[-128, 127]范围内的值作为参数创建Integer对象时,会创建相同的对象;否则会创建出不同的对象。
  起始Java基本类型的包装类,大部分都实现了常量池技术,即Byte、Short、Integer、Long、Character、Boolean,前4种包装类默认创建了数值[-128,127]的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean直接返回True或False。如果超出这些范围,就会正常地创建新的对象。
  两种浮点数类型的包装类Float、Double并没有实现常量池技术。

三、异常

  Java异常,就是程序出现了预期之外的情况,这个出现异常的时间可能是编译期或运行期。Java中,针对这种意外情况,存在一种专业的机制来处理:异常处理机制。Java异常是Java提供的一种识别及响应错误的一致性机制,该机制的最大作用是让程序尽可能恢复到正常状态。
  Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。

3.1 异常的分类

  Throwable是Java中处理异常情况的最顶级父类,该类下面有两个子类:Error和Exception。
Java(四)内部类、包装类、异常、日期_第2张图片
  Error是错误,对于所有的编译时期的错误以及系统错误都是通过Error抛出的。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误、类定义错误等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况
  Exception是另外一个非常重要的异常子类,Exception规定的异常是程序本身可以处理的异常。异常可以分为编译时异常或者检查时异常。

3.1.1 Throwable

  Throwable是所有错误与异常的超类。
  Throwable包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。
  Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。
  Throwable 类常用方法:

	//返回异常发生时的详细信息
	public string getMessage()
	//返回异常发生时的简要描述
    public string toString()
    //返回异常对象的本地化信息。使用Throwable 的子类覆盖这个方法,可以生成本地化信息。
    //如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
	public string getLocalizedMessage()
	//在控制台上打印Throwable对象封装的异常信息
 	public void printStackTrace()

3.1.2 Error

  定义:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误
  特点:此类错误一般表示代码运行时JVM出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)、OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。
  当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,开发者是不应该实现任何新的Error子类的。

3.1.3 Exception

  程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。

  • 1、运行时异常(非受检异常)
      定义RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常
      特点:Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。比如NullPointerException、ArrayIndexOutBoundException、ClassCastException、ArithmeticExecption。
      此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。
  • 2、编译时异常(受检异常)
      定义: Exception 中除 RuntimeException 及其子类之外的异常。
      特点: Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundExceptio、IOException,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常。

3.1.4 常见非受检异常

  • 1、ArrayIndexOutOfBoundsException
      数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。示例:
        int[] arr = {1,2,3,4,5};
        for(int i=0;i<=arr.length;i++) {
        	System.out.println(arr[i]);
        }

  此时就会抛出异常:
Java(四)内部类、包装类、异常、日期_第3张图片

  • 2、ArithmeticException
      算术条件异常。譬如:整数除零等。示例:
	System.out.println(5/0);

  此时就会抛出异常:

  • 3、Illegalargumentexception
      非法参数异常,进行非法调用时,传递的参数不合规。 示例:
        Date day = new Date();   
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
        String date = df.format(day);

        SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy-MM");
        String format = dateFormat.format(date);
        System.out.println(format);

  此时就会抛出异常:

  • 4、NullPointerException
      空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。示例:
        String str = null;
        System.out.println(str.length());

  此时就会抛出异常:

  • 5、IndexOutOfBoundsException
      索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。示例:
        ArrayList<String> arrayList = new ArrayList<>();
        System.out.println(arrayList.get(2));

  此时就会抛出异常:

  • 6、ClassCastException
      类转型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。示例:
public interface Animal {
	 abstract void eat();
}

public class Cat implements Animal {
	@Override
    public void eat() {
        System.out.println("吃鱼");
    }
     
    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}

public class Dog implements Animal{
	@Override
    public void eat() {
        System.out.println("吃骨头");
    }
     
    public void watchHouse() {
        System.out.println("看家");
    }
}

//测试类
public class JavaTest {
	public static void main(String[] args) throws IOException {
        Animal a = new Cat();
        a.eat();
         
        Cat c = (Cat)a;
        c.catchMouse();    
        Dog d = (Dog)a;
        d.watchHouse(); // ClassCastException异常
	}
}

  此时就会抛出异常:

  • 7、NoSuchMethodException
      方法不存在异常。当访问某个类的不存在的方法时抛出该异常。示例:
public class Person {
	public void methodOne(String s){
		System.out.println("调用了public methodOne方法");
	}
}

		Class perClass = null;
		try {
			perClass = Class.forName("com.test.Person");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		perClass.getMethods();
		Method[] methodArray = perClass.getMethods();
		
		Method m;
		try {
			m = perClass.getMethod("methodTwo", String.class);
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}

  此时就会抛出异常:

3.1.5 常见受检异常

  • 1、IOException
      输入输出异常,示例:
        File file = new File("F:/123.txt");
        OutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            outputStream.close();
            outputStream.write("456".getBytes());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
            	outputStream.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
        }

  此时就会抛出异常:

  • 2、EOFException
      文件已结束异常,示例:
		 File f0 = new File("F:/kkk.out");
		 FileInputStream fis = null;
		 FileOutputStream fos = null;
		 ObjectInputStream dis = null;
		 ObjectOutputStream dos = null;
		 try{
		     if(!f0.exists())f0.createNewFile();

		     fos = new FileOutputStream(f0);
		     fis = new FileInputStream(f0);

		     // 1. 初始化Object流语句
		     dis = new ObjectInputStream(fis);
		     dos = new ObjectOutputStream(fos);

		     // 2. 写"对象"语句
		     dos.writeInt(1);
		     dos.writeObject(new Integer(3));

		     // 3. 读取,输出语句
		     System.out.println(dis.readInt() + ","+ dis.readInt());
		 } catch (Exception e){
		     e.printStackTrace();
		     if(fos != null) fos.close();
		     if(fis != null) fis.close();
		     if(dos != null) dos.close();
		     if(dis != null) dis.close();
		 }

  此时就会抛出异常:

  • 3、ClassNotFoundException
      找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。基于上面的Person继续演示,示例:
		Class perClass = null;
		try {
			perClass = Class.forName("com.test.Person1");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

  此时就会抛出异常:
Java(四)内部类、包装类、异常、日期_第4张图片

3.1.6 运行时异常和一般异常(受检异常)区别是什么?【重要】

  运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。
  受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常。
  RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处理此异常。如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。

3.1.7 NoClassDefFoundError和ClassNotFoundException 区别?

  • NoClassDefFoundError
      一个Error 类型的异常,是由JVM引起的,不应该尝试捕获这个异常。引起该异常的原因是JVM或ClassLoader尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;
  • ClassNotFoundException
      一个受查异常,需要显式地使用try-catch对其进行捕获和处理,或在方法签名中用throws关键字进行声明。当使用Class.forName、ClassLoader.loadClass或 ClassLoader.findSystemClass动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。

3.2 异常处理机制

3.2.1 异常处理中的关键字

  Java异常机制重用到的关键字:

关键字 作用
try try后面的{ }中,是有可能抛出异常的代码块。如果这些代码块中出现了异常,就可以被及时发现,进行下一阶段处理
catch 用于捕获异常。catch后的{ }中,是针对某一类异常的具体处理
finally 不管代码运行时有没有异常,finally后的{ }语句总会被执行,一般用于一些IO的终止操作等。
throw 在代码中主动抛出异常
throws 用于声明一个方法可能抛出的异常

3.2.2 异常处理的语句形式

  常见的语句有两种:try…catch和try…catch…finally,通用一点的写法是:

try { 
	可能出现异常的代码
} catch(异常类名A e){ 
	如果出现了异常类A类型的异常,那么执行该代码
} ...(catch可以有多个){
}finally { 
	最终肯定必须要执行的代码(例如释放资源的代码)
}

3.2.3 try…catch的处理顺序

  此时有两种情况:try代码块中出现了异常和没出现异常。

  • 1、出现异常
      try内的代码从出现异常的那一行开始,中断执行;执行对应的catch块内的代码;继续执行try…catch结构之后的代码。示例:
	public static void main(String[] args){
		int[] arr = {1,2,3,4,5};
		try {
			for(int i=0;i<=arr.length;i++)
				System.out.println(arr[i]);
			System.out.println("try代码块中的数组元素输出完毕");
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			System.out.println("进入catch代码块");
		}finally{
			System.out.println("进入finally代码块");
		}
	}

  测试结果为:
Java(四)内部类、包装类、异常、日期_第5张图片

  • 2、未出现异常
      try内的代码执行完;不执行catch代码块里的语句;继续执行try…catch结构之后的代码。示例:
	public static void main(String[] args){
		int[] arr = {1,2,3,4,5};
		try {
			for(int i=0;i<arr.length;i++)
				System.out.println(arr[i]);
			System.out.println("try代码块中的数组元素输出完毕");
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			System.out.println("进入catch代码块");
		}finally{
			System.out.println("进入finally代码块");
		}
	}	

  测试结果为:
Java(四)内部类、包装类、异常、日期_第6张图片

3.2.4 try…catch使用的注意事项

  1. 如果catch内的异常类存在子父类的关系,那么子类应该在前,父类在后
  2. 如果finally语句块中有return语句,那么最后返回的结果肯定以finally中的返回值为准,因为出现异常时,优先执行finally中的语句。
  3. catch不能独立于try存在。
  4. 在try…catch后面的finally语句块并非强制性要求的。
  5. try代码块里面越少越好。
  6. 如果程序可能存在多种异常,需要多个catch进行捕获

3.2.5 try-with-resource

  在try和finally块中都抛出异常时,finally块中的异常会覆盖掉try块中的异常。为了解决这种情形,JDK1.7引入了try-with-resource机制,这种机制可以实现资源的自动释放,自动释放的资源需要是实现了AutoCloseable接口的类。示例:

	private static void tryWithResourceTest(){
		try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF8")){
			// code
		} catch (IOException e){
			// handle exception
		}
	}

  在上面的代码中,当try代码块中抛出异常时,会自动调用scanner.close方法,和把scanner.close方法放在finally代码块中不同的是,若scanner.close抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由addSusppressed方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用getSuppressed方法来获取。
  被抑制的异常会出现在抛出的异常的堆栈信息中,也可以通过getSuppressed方法来获取这些异常。这样做的好处是不会丢失任何异常,方便开发人员进行调试。

3.3 Java异常处理过程

3.3.1 声明异常

  通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用throws关键字声明可能会抛出的异常

3.3.2 抛出异常

  如果你觉得解决不了某些异常问题,且不需要调用者处理,那么你可以抛出异常。
  throw关键字作用是在方法内部抛出一个Throwable类型的异常。任何Java代码都可以通过throw语句抛出异常。

3.3.3 捕获异常

  程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。

3.4 自定义异常

  如果原生的异常类不能满足功能要求,开发者可以写自己的异常类。如果要自定义非检查异常,则继承RuntimeException;如果要自定义检查异常,则继承Exception。此处可以借鉴一下IOException的写法:

public class IOException extends Exception {

    private static final long serialVersionUID = 7818375828146090155L;

    public IOException() {
    }

    public IOException(String detailMessage) {
        super(detailMessage);
    }

    public IOException(String message, Throwable cause) {
        super(message, cause);
    }

    public IOException(Throwable cause) {
        super(cause == null ? null : cause.toString(), cause);
    }
}

  此处以Student为例,如果Student的分数不在[1,100]范围内,我们就抛出一个异常。示例:

/*自定义异常类*/
public class GradeException extends Exception{
    public GradeException() {
    }

    public GradeException(String detailMessage) {
        super(detailMessage);
    }

    public GradeException(String message, Throwable cause) {
        super(message, cause);
    }

    public GradeException(Throwable cause) {
        super(cause == null ? null : cause.toString(), cause);
    }
}
/*学生类*/
public class Student {
	private String name;
	private int grade;
	
	public Student(){	
	}
	
	public String getName(){
		return this.name;
	}
	
	public void setName(String name){
		if(name.length()!=0){
			this.name = name;
		}
	}
	
	public int getGrade(){
		return this.grade;
	}
	
	public void setGrade(int grade) throws GradeException {
		if(grade > 100 || grade < 0){
			throw new GradeException("分数参数不合法,应该是0-100的整数");
		}else{
			this.grade = grade;
		}
	}
}
/*测试类*/
public class BasicTest {
	public static void main(String[] args){
		Student student = new Student();
		try {
			student.setGrade(101);
		} catch (GradeException e) {
			e.printStackTrace();
		}
	}	
}

  测试结果:
    

3.5 final/finally/finalize

3.5.1 final/finally/finalize的区别

  简单来说,三者区别:

  • 1、final可以用于修饰变量,方法,类,被修饰的变量的值不能被改变,被修饰的方法不能被重写,被修饰的类不能被继承。
  • 2、finally通常放在try…catch…的后面,这就意味着程序无论正常运行还是发生异常,finally块中的代码都会执行,finally块中一般写释放资源的操作。
  • 3、finalize()是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。再详细地说,finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没被引用时对这个对象调用的。它是在Object类中定义的,因此所的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

3.5.2 final和finally的使用差异

  • final
      先看关键字,如果final修饰的是一个基本类型,就表示这个变量被赋予的值是不可变的,即它是个常量;如果final修饰的是一个对象,就表示这个变量被赋予的引用是不可变的。也就是说,不可改变的只是这个变量所保存的引用,并不是这个引用所指向的对象
      如果一个变量或方法参数被final修饰,就表示它只能被赋值一次,但是JAVA虚拟机为变量设定的默认值不记作一次赋值。被final修饰的变量必须被初始化。初始化的方式以下几种:
  1. 在定义的时候初始化。
  2. final变量可以在初始化块中初始化,不可以在静态初始化块中初始化。
  3. 静态final变量可以在定义时初始化,也可以在静态初始化块中初始化,不可以在初始化块中初始化。
  4. final变量还可以在类的构造器中初始化,但是静态final变量不可以。
  • finally
      finally只能用在try/catch语句中并且附带着一个语句块,表示这段语句最终总是被执行。关于try、catch、finally块中代码的执行顺序,示例:
        try{
            throw new NullPointerException();
        }catch(NullPointerException e){
            System.out.println("程序抛出了异常");
        }finally{
            System.out.println("执行了finally语句块");
        }

  测试结果为:

程序抛出了异常
执行了finally语句块

3.5.3 finally块和finalize方法的执行时机

  • finally块
      在Java中,return、continue、break这个可以打乱代码顺序执行语句的规律,那么这些能影响finally语句块的执行吗?可以看下面的例子:
public class BasicTest {
	
    public static void main(String[] args) {
        // 测试return语句对finally块代码执行的影响
        testReturn();
        System.out.println();
        // 测试continue语句对finally块代码执行的影响
        testContinue();
        System.out.println();
        // 测试break语句对finally块代码执行的影响
        testBreak();
    }
    
   	static ReturnClass testReturn() {
        try {
            return new ReturnClass();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        	System.out.println("testReturn方法中,执行了finally语句");
        }
        return null;
    }

    static void testContinue(){
        for(int i=0; i<3; i++){
            try {
                System.out.println(i);
                if(i == 1){
                    System.out.println("con");
                }
            } catch(Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println("testContinue方法中,执行了finally语句");
            }
        }
    }
    
    static void testBreak() {
        for (int i=0; i<3; i++) {
            try {
                System.out.println(i);
                if (i == 1) {
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println("testBreak方法中,执行了finally语句");
            }
        }
    }
}

public class ReturnClass {
    public ReturnClass() {
        System.out.println("创建ReturnClass对象");
    }
}

  测试结果:
Java(四)内部类、包装类、异常、日期_第7张图片
  很明显,return、continue和break都没能阻止finally语句块的执行。从结果上直观地看,return语句似乎在finally语句块之前执行了,其实不然,return语句的作用是退出当前的方法,并将值或对象返回。如果 finally语句块是在return语句之后执行的,那么return语句被执行后就已经退出当前方法了,finally语句块执行不了。
  因此,正确的执行顺序应该是这样的:编译器在编译return new ReturnClass();时,将它分成了两个步骤,new ReturnClass()和return,前一个创建对象的语句是在finally语句块之前被执行的,而后一个return语句是在finally语句块之后执行的,也就是说finally语句块是在程序退出方法之前被执行的。同样,finally语句块是在循环被跳过(continue)和中断(break)之前被执行的。
  此时,可以总结一下finally与return语句的执行顺序的关系:

  1. 如果try块中有return,finally块的代码仍会执行,并且finally的执行早于try里面的return。
  2. 当try和catch中有return时,finally仍然会执行。
  3. finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的。
  4. finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
  • finalize方法
      该方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,会首先调用finalize()方法,该方法在Object类中的定义为:
	protected void finalize() throws Throwable { }

  看一个调用该方法的demo:

public class BasicTest {
	public static void main(String[] args) {
		BasicTest bs = new BasicTest();
		bs = null;
		System.gc();
	}
	
	@Override
	protected void finalize() throws Throwable {
		System.out.println("执行了finalize方法");
	}
}

  测试结果为:

执行了finalize方法

  finalize()方法中一般用于释放非Java 资源(如打开的文件资源、数据库连接等)。同时,finalize()方法的调用时机具有不确定性,导致开发者并不能依赖finalize()方法能及时的回收占用的资源,可能出现的情况是在耗尽资源之前,gc却仍未触发,因而通常的做法是提供显式的close()方法供客户端手动调用。

3.6 异常处理-阿里巴巴Java开发手册

  • 1、【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理。
      比如:NullPointerException,IndexOutOfBoundsException等等。 说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。
      正例:
	if (obj != null) {} 

  反例:

	try { obj.method(); } catch
	(NullPointerException e) {}
  • 2、【强制】异常不要用来做流程控制,条件控制。
      异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
  • 3、【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。
      说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。 正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
  • 4、【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者,外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
  • 5、【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
  • 6、【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。 说明:如果JDK7及以上,可以使用try-with-resources方式。
  • 7、【强制】不要在finally块中使用return。 说明:try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。
      反例:
	private int x = 0;
	public int checkReturn() {
		try {
			// x等于1,此处不返回
			return ++x;
		} finally {
			// 返回的结果是2
			return ++x;
		}
	}
  • 8、【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
  • 9、【推荐】防止NPE,是程序员的基本修养。
      注意NPE产生的场景:

1) 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。 反例:public int f() { return Integer对象}, 如果为null,自动解箱抛NPE
2) 数据库的查询结果可能为null。
3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止NPE。
5) 对于Session中获取的数据,建议进行NPE检查,避免空指针。
6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。

  正例:使用JDK8的Optional类来防止NPE问题。

3.7 一些异常相关的问题

3.7.1 常见异常处理方式

  • 1、直接抛出异常
      通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。
  • 2、封装异常再抛出
      有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。例如:
private static void readFile(String filePath) throws MyException {    
    try {
        // code
    } catch (IOException e) {
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    }
}
  • 3、捕获异常
      在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。
  • 4、自定义异常
      习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用)。

3.7.2 异常是一起处理好还是分开处理好?

  根据实际的开发要求是否严格来决定。在实际的项目开发项目工作中,所有的异常是统一使用Exception处理还是分开处理,完全根据开发者的项目开发标准来决定。如果项目开发环境严谨,基本上要求针对每一种异常分别进行处理,并且要详细记录下异常产生的时间以及产生的位置,这样可以方便程序开发人员进行代码的维护。
  注意:处理多个异常时,捕获范围小的异常要放在捕获范围大的异常之前处理。

3.7.3 throw和throws的区别?

throws throw
位置 用在函数上,后面跟的是异常类,可以跟多个 用在函数内,后面跟的是异常对象
功能 声明异常 抛出具体的问题对象
意义 表示出现异常的一种可能性,并不一定会发生这些异常 已经抛出了某种异常对象

  两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

3.7.4 检查型异常与非检查型异常区别?

检查型异常 非检查型异常
继承谁 Exception RuntimeException
处理方式 必须使用try…catch或者throws等关键字进行处理,否则编译器会报错 可以通过修改代码来规避

3.7.5 JVM是如何处理异常的?

  在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
  JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。

3.7.6 try-catch-finally中哪个部分可以省略?

  catch或finally可以省略:

  1. 一般省略catch 时,都在方法声明将异常继续往上层抛出。
  2. finally一般用来关闭资源,可以不用在finally块中关闭资源,比如try-with-resources用法。

3.7.7 关闭资源的两种方式

在finally块中清理资源或者使用try-with-resource语句

  • 1、finally块
      用finally关闭资源示例:
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        // use the inputStream to read a file
    } catch (FileNotFoundException e) {
        log.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error(e);
            }
        }
    }
  • 2、try-with-resource
      用try-with-resource关闭资源示例:
    File file = new File("./tmp.txt");
    try (FileInputStream inputStream = new FileInputStream(file);) {
        // use the inputStream to read a file
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }

3.7.8 优先捕获最具体的异常

  在异常处理机制中,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获 IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的catch块,因为它是 IllegalArgumentException 的子类。所以应该总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。

3.7.9 异常会影响性能

  异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。所以应该在这两种情况下使用异常:

  1. 仅在异常情况下使用异常;
  2. 在可恢复的异常情况下使用异常;

3.7.10 finally语句什么时候不会执行

  至少有两种情况下finally语句是不会被执行的:

  1. try语句没有被执行到,如在try语句之前就返回了,这样finally语句就不会执行,这也说明了finally语句被执行的必要而非充分条件是:相应的try语句一定被执行到
  2. 在try块中有System.exit(0);这样的语句,System.exit(0);是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。
  3. 程序所在的线程死亡。
  4. 关闭CPU。

3.7.11 异常会影响性能

  异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。因此,对异常的合理使用方式是:

  • 1、仅在异常情况下使用异常;
  • 2、在可恢复的异常情况下使用异常;尽管使用异常有利于 Java 开发,但是在应用中 好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。

四、日期

  简单来说,Date类用来获取时间,SimpleDateFormat类用来显示时间,Calendar类用来计算时间。

4.1 Date类

  java.util 包提供了Date类来封装当前的日期和时间。该类中的常用方法:

  • 1、构造方法
	public Date()
	public Date(long date)
  • 2、获取自1970年1月1日00:00:00GMT以来,Date对象表示的毫秒数
`public long getTime()
  • 3、用自1970年1月1日00:00:00 GMT以后time毫秒数设置时间和日期
	public void setTime(long time)

  上面方法的示例:

	       Date date = new Date();
	       System.out.println("当前时间:");
	       System.out.println(date.toString());  //Thu Oct 29 10:06:51 CST 2020
	       System.out.println(date.getTime());   //1603937211568
	       System.out.println("设置后的时间:");
	       date.setTime(1503937115121L);
	       System.out.println(date.toString()); //Tue Aug 29 00:18:35 CST 2017
	       System.out.println(date.getTime());  //1503937115121

4.2 SimpleDateFormat类

  该类用于日期的格式化,常用于字符串和时间类对象之间的互相转换。示例:

        SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS" );
        Date d= new Date();
        String str = sdf.format(d);
        System.out.println(str);  //2021-08-29 16:10:39 517
         
        SimpleDateFormat sdf1 =new SimpleDateFormat("yyyy-MM-dd" );
        Date d1= new Date();
        String str1 = sdf1.format(d1);
        System.out.println(str1);  //2021-08-29
         

  上面是日期转字符串的例子,下面看下字符串转日期的:

        SimpleDateFormat sdf =new SimpleDateFormat("yyyy/MM/dd HH:mm:ss" );
        String str = "2020/10/30 10:12:00";
          
        try {
            Date d = sdf.parse(str);
            System.out.printf(d.toString()); //Fri Oct 30 10:12:00 CST 2020
        } catch (ParseException e) {
            e.printStackTrace();
        }

  在使用SimpleDateFormat类进行转换时,常用的转换规则为:

字符 含义
y
M
d
H 24进制的小时
h 12进制的小时
m 分钟
s
S 毫秒

4.3 Calendar类

   Calendar功能比Date多一些,使用上也更复杂一些,常用的方法有:

  • 1、获取Calendar对象(默认是当前时间)
	public static Calendar getInstance()
  • 2、设置年月日
	public final void set(int year,int month,int date)
  • 3、设置某个维度的时间(年、月、日)
	public void set(int field,int value)
  • 4、对某个维度的日期进行加减
	public void add(int field, int amount)

   关于时间的不同维度(年、月、日),Calendar中有特定的字段来表示,如下:
Java(四)内部类、包装类、异常、日期_第8张图片
  示例:

		Calendar c1 = Calendar.getInstance();
		c1.set(2009, 6 - 1, 12);
		//2009,5,12
		System.out.println(c1.get(Calendar.YEAR)+","
				+c1.get(Calendar.MONTH)+","
				+c1.get(Calendar.DATE));
		c1.set(Calendar.YEAR,2008);
		c1.set(Calendar.MONTH,8);
		c1.set(Calendar.DATE,8);
		//2008,8,8
		System.out.println(c1.get(Calendar.YEAR)+","
					+c1.get(Calendar.MONTH)+","
				+c1.get(Calendar.DATE));
		c1.add(Calendar.DATE, 10);
		//2008,8,18
		System.out.println(c1.get(Calendar.YEAR)+","
				+c1.get(Calendar.MONTH)+","
				+c1.get(Calendar.DATE));

4.4 Date和Calendar之间的相互转换

  前面介绍的Date和SimpleDateFormat是可以相互转换的,那么Date和Calendar可以相互转换吗?答案肯定是可以的。

  • 1、Calendar转化为Date
Calendar cal1=Calendar.getInstance();  
Date date1=cal1.getTime(); 
  • 2、Date转化为Calendar
Date date2=new Date();  
Calendar cal2=Calendar.getInstance();  
cal2.setTime(date2);

你可能感兴趣的:(【Java】,java,4种内部类,自动拆箱装箱,异常)