Bootstrap

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

文章目录

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

一、内部类

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

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.1 内部类的优点*
  • 1、内部类可以实现和外部类不同的接口,也可以继承和外部类不同的类,间接完成功能扩展
  • 2、内部类有效实现了“多重继承”,即用内部类去继承其他类,优化Java单继承的缺陷。
  • 3、内部类中的属性、方法可以和外部类重名,但并不冲突,因为内部类是具有类的基本特征的独立实体。
  • 4、内部类利用访问修饰符隐藏内部类的实施细节,提供了更好的封装,除外部类,都不能访问。
  • 5、静态内部类使用时可直接使用,不需先创造外部类。
  • 6、 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据
  • 7、匿名内部类可以很方便的定义回调。匿名内部类往往是做为一个内部类(接口)的具体实现。
1.2.2 内部类的应用场景
  • 1、一些多算法场合
      将部分实现转移给使用者,让使用者决定算法的实现。
  • 2、解决一些非面向对象的语句块
      也是类似于模板方法模式的使用,将if…else if…else语句、case语句等转移到子类中去实现。
  • 3、适当使用内部类,使得代码更加灵活和富有扩展性
      常见的是使用模板方法模式的时候,将部分接口的实现转移到子类中实现
  • 4、当某个类除了它的外部类,不再被其他的类使用时
      一个内部类依附于它的外部类而存在,可能的原因有:
  1. 不可能为其他的类使用;
  2. 出于某种原因,不能被其他类引用,可能会引起错误。
1.2.3 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?

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

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

  静态内部类相对于外部类是独立存在的,在静态内部类中无法直接访问外部类中变量、方法。如果要访问的话,必须要new一个外部类的对象,使用new出来的对象来访问。但是可以直接访问静态的变量、调用静态的方法。
  普通内部类作为外部类一个成员而存在,在普通内部类中可以直接访问外部类属性,调用外部类的方法
  如果外部类要访问内部类的属性或调用内部类的方法,必须要创建一个内部类的对象,使用该对象访问属性或调用方法。
  如果其他的类要访问普通内部类的属性或调用普通内部类的方法,必须要在外部类中创建一个普通内部类的对象作为一个属性。
  如果其他的类要访问静态内部类的属性或调用静态内部类的方法,直接创建一个静态内部类对象即可。

  • 非静态内部类
      1、变量和方法不能声明为静态的。
      2、实例化的时候需要依附在外部类上面。比如:B是A的非静态内部类,实例化B,则:A.B b = new A().new B()。
      3、内部类可以引用外部类的静态或者非静态属性或者方法。
  • 静态内部类
      1、属性和方法可以声明为静态的或者非静态的。
      2、实例化静态内部类:比如:B是A的静态内部类,A.B b = new A.B()。
      3、内部类只能引用外部类的静态的属性或者方法。
      4、如果属性或者方法声明为静态的,那么可以直接通过类名直接使用。比如B是A的静态内部类,b()是B中的一个静态属性,则可以:A.B.b()。

二、包装类

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种:

基本类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
booleanCharacter
charBoolean

  Number是所有数字包装类的父类。

2.2 包装类和基本类型的转换

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

基本类型基本类型–>包装类包装类–>基本类型
bytenew Byte / valueOfbyteValue
shortnew Short / valueOfshortValue
intnew Integer / valueOfintValue
longnew Long / valueOflongValue
floatnew Float / valueOffloatValue
doublenew Double / valueOfdoubleValue
booleannew Boolean / valueOfbooleanValue
charnew Character / valueOfcharValue

  以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.3 自动装箱和自动拆箱*

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

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

  在自动装箱的时候,基本类型要与包装类类型一一对应;自动拆箱的时候,包装类类型 <= 基本类型就可以
  自动装箱和拆箱发生的场景:

  1. 赋值操作(装箱或拆箱);
  2. 进行加减乘除混合运算 (拆箱);
  3. 进行>,<,==比较运算(拆箱);
  4. 调用equals进行比较(装箱);
  5. ArrayList、HashMap等集合类添加基础类型数据时(装箱)。
  • 包装类的原理
      其实装箱调用包装类的valueOf方法,拆箱调用的是包装类的xxxValue方法(如下图中的Integer.valueOf和Integer.intValue),示例:

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并没有实现常量池技术。
  示例:

	 Integer i1 = 40;
	 Integer i2 = 40;
	 Integer i3 = 0;
	 Integer i4 = new Integer(40);
	 Integer i5 = new Integer(40);
	 Integer i6 = new Integer(0);
	 System.out.println("i1=i2  " + (i1 == i2));
	 System.out.println("i1=i2+i3  " + (i1 == i2 + i3));
	 System.out.println("i1=i4  " + (i1 == i4));
	 System.out.println("i4=i5  " + (i4 == i5));
	 System.out.println("i4=i5+i6  " + (i4 == i5 + i6)); 
	 System.out.println("40=i5+i6  " + (40 == i5 + i6));

  结果:

i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true

  语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。

2.4.3 包装类和基本类型的不同
  1. 声明方式不同:基本类型不使用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
  2. 存储方式及位置不同:基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
  3. 初始值不同:基本类型的初始值如int为0,boolean为false,而包装类型的初始值null;
  4. 使用方式不同:基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。

三、异常

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

3.1 异常简介

  Throwable是Java中处理异常情况的最顶级父类,只有继承于Throwable的类或其子类才能够被抛出,还有一种方式是带有Java中throw注解的类也可以抛出。
  Throwable有两个子类:Error和Exception。

  Error是错误,对于所有的编译时期的错误以及系统错误都是通过Error抛出的。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误、类定义错误等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况
  Exception是另外一个非常重要的异常子类,Exception规定的异常是程序本身可以处理的异常。异常可以分为编译时异常或者检查时异常。

3.2 异常相关类

3.2.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.2.2 Error

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

3.2.3 Exception

  程序本身可以捕获并且可以处理的异常。Exception这种异常又分为两类:运行时异常和编译时异常。
  除了RuntimeException和其子类,以及Error和其子类,其他的所有异常都是checkedException

3.3 异常分类*

3.3.1 运行时异常(非受检异常)

  定义RuntimeException类及其子类,表示JVM在运行期间可能出现的异常
  特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。比如NullPointerException、ArrayIndexOutBoundException、ClassCastException、ArithmeticExecption。
  此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然Java编译器不会检查运行时异常,但是我们也可以通过throws进行声明抛出,也可以通过try-catch对它进行捕获处理。
  如果产生运行时异常,则需要通过修改代码来进行避免。
  常见非受检异常:

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

  此时就会抛出异常:

  • 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异常
	}
}

  此时就会抛出异常:

3.3.2 编译时异常(受检异常)

  定义: Exception中除RuntimeException及其子类之外的异常
  特点: Java编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException、IOException,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常。
  常见受检异常:

  • 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();
	}

  此时就会抛出异常:

  • 4、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.3.4 非受检异常和受检异常的区别
  • 1、从类看
      运行时异常包括RuntimeException类及其子类,表示JVM在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。
      受检异常是Exception中除RuntimeException及其子类之外的异常。 Java编译器会检查受检异常。
  • 2、:是否强制要求调用者必须处理此异常
      如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常
3.3.5 NoClassDefFoundError和ClassNotFoundException的区别
  • NoClassDefFoundError
      一个Error类型的异常,是由JVM引起的,不应该尝试捕获这个异常。引起该异常的原因是JVM或ClassLoader尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是编译后被删除了等原因导致。
      NoClassDefFoundError的错误是因为在运行时类加载器在classpath下找不到需要加载的类,所以我们需要把对应的类加载到classpath中,或者检查为什么类在classpath中是不可用的,这个发生可能的原因如下:
  1. 对应的Class在java的classpath中不可用。
  2. 你可能用jar命令运行你的程序,但类并没有在jar文件的manifest文件中的classpath属性中定义。
  3. 可能程序的启动脚本覆盖了原来的classpath环境变量。
  4. 因为NoClassDefFoundError是java.lang.LinkageError的一个子类,所以可能由于程序依赖的原生的类库不可用而导致。
  5. 检查日志文件中是否有java.lang.ExceptionInInitializerError这样的错误,NoClassDefFoundError有可能是由于静态初始化失败导致的。
  6. 如果你工作在J2EE的环境,有多个不同的类加载器,也可能导致NoClassDefFoundError。
  • ClassNotFoundException
      一个受查异常,需要显式地使用try-catch对其进行捕获和处理,或在方法签名中用throws关键字进行声明。引起该异常的常见原因:1)当使用Class.forName、ClassLoader.loadClass或 ClassLoader.findSystemClass动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;2)另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。

3.4 异常处理机制*

3.4.1 异常处理中的关键字

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

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

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

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

  在try代码块里有外部服务调用代码时,catch里常常返回不同的数据,示例:

	ResponseDTO responseDTO = new ResponseDTO();
	try{
	    //……
	    responseDTO.setCode("0000");
	    responseDTO.setMsg("正常");
	}catch(Exception e){
	    responseDTO.setCode("0001");
	    responseDTO.setMsg("异常");
	}
3.4.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代码块");
		}
	}

  测试结果为:

  • 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代码块");
		}
	}	

  测试结果为:

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

  如果原生的异常类不能满足功能要求,开发者可以写自己的异常类。如果要自定义非检查异常,则继承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.6 final/finally/finalize

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

  测试结果:

  很明显,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()方法供客户端手动调用。
  finalize()方法的一些注意事项:

  1. Java语言规范并不保证该方法会被及时地执行,更根本不会保证它们一定会被执行。
  2. 该方法可能会带来性能问题,因为JVM通常在单独的低优先级线程中执行该方法。
  3. 该方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的。
  4. 该方法最多由GC执行一次。

3.7 异常处理注意事项

3.7.1 阿里巴巴Java开发手册中的异常处理
  • 1、【强制】Java类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch的方式来处理。
      比如:NullPointerException、IndexOutOfBoundsException等等。 说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。
	//正例
	if (obj != null) {} 
	//反例
	try { 
		obj.method(); 
	} catch(NullPointerException e) {}
  • 2、【强制】异常不要用来做流程控制,条件控制。
      异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
  • 3、【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。
      对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。
  • 4、【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者,外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
  • 5、【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
  • **6、【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。 **
      说明:如果JDK1.7及以上,可以使用try-with-resources方式。
      当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单:通过使用分号分隔,可以在 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问题。示例:

     int day = Optional.ofNullable(accountInfo.getWriteoffDay()).orElse(0);
3.7.2 常见异常处理方式
  • 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语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。
3.7.3 异常是一起处理好还是分开处理好*

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

@RestControllerAdvice
public class ResponseHandler implements ResponseBodyAdvice<Object>{
     @ExceptionHandler(RetryableException e)
     @ResponseBody
     public ResponseDTO exceptionHandler(RetryableException){
        //……
     }

}
3.7.4 关闭资源的两种方式

  在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.5 优先捕获最具体的异常

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

3.7.6 异常会影响性能

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

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

3.8 异常相关的问题

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

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

3.8.2 JVM是如何处理异常的?

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

1)首先,它查找线程对象的未捕获异常处理器
2)如果找不到,JVM继续查找线程对象所在的线程组(ThreadGroup)的未捕获异常处理器
3)如果还是找不到,如同本节所讲的,JVM将继续查找默认的未捕获异常处理器
4)如果没有一个处理器存在,JVM则将堆栈异常记录打印到控制台,并退出程序

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

  catch或finally可以省略:

  1. 一般省略catch时,都在方法声明将异常继续往上层抛出。
  2. finally一般用来关闭资源,可以不用在finally块中关闭资源,比如try-with-resources用法。
3.8.4 finally语句什么时候不会执行
  1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行。
  2. 在try块中有System.exit(0);这样的语句,System.exit(0);是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。
  3. 程序所在的线程死亡。
  4. 关闭CPU。
3.8.5 try-catch放在循环体内执行会影响性能吗

  在没有发生异常的情况下,try-catch无论是在for循环内还是for循环外,它们的性能相同,几乎没有任何差别。
  在循环体内的try-catch在发生异常之后,可以继续执行循环;循环外的try-catch在发生异常之后会终止循环。因此在决定try-catch究竟是应该放在循环内还是循环外,不取决于性能(因为性能几乎相同),而是应该取决于具体的业务场景。

3.8.6 try里有return,finally还执行么

  执行,并且finally的执行早于try里面的return。

  1、不管有木有出现异常,finally块中代码都会执行;
  2、当try和catch中有return时,finally仍然会执行;
  3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;
  4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。

3.8.7 NullPointerException异常怎么解决*

  NullPointerException是Java编程语言中最常见的异常之一。它通常在程序中出现,当试图访问一个空对象的属性或调用一个空对象的方法时,就会抛出这个异常。
  些常见的解决方法如下:

  • 1、检查空引用
      访问对象/集合的属性或调用对象/集合的方法之前,应该先检查对象/集合是否为空。可以使用if语句或三元运算符来检查对象/集合是否为null。例如:
if (object != null) {
     // 访问对象的属性或调用对象的方法
}
  • 2、初始化对象
      在创建对象时,应该确保对象被正确地初始化。如果对象没有被正确地初始化,就会抛出NullPointerException异常。可以使用构造函数或初始化块来初始化对象。例如:
public class MyClass {
    private String name;
    public MyClass() {
      this.name = "default";
    }
    // 其他代码
}
  • 3、使用默认值
      如果一个对象可能为空,并且在使用它之前没有被初始化,可以为对象设置一个默认值。这样,即使对象为空,也不会抛出NullPointerException异常。例如:
public class MyClass {
    private String name = "default";
    private MyClass obj = new MyClass();
    // 其他代码
}
  • 4、使用Optional类
      Java 8引入了Optional类,它可以避免NullPointerException。Optional类允许您在值存在时使用它,否则返回一个默认值。例如:
    Optional<String> optional = 
          Optional.ofNullable(str);optional.ifPresent(System.out::println);
  • 5、调试代码
      如果无法确定NullPointerException异常的原因,可以使用调试工具来检查代码。

四、日期

  简单来说,Date类用来获取时间,SimpleDateFormat类用来处理日期格式,Calendar类用来计算时间。

4.1 Date类

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

    //构造方法
	public Date()
	public Date(long date)
    
    //获取自1970年1月1日00:00:00GMT以来,Date对象表示的毫秒数
    public long getTime()

    //用自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是线程不安全的。
  该类用于日期的格式化,常用于字符串和时间类对象之间的互相转换。示例:

   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
H24进制的小时
h12进制的小时
m分钟
s
S毫秒

4.3 Calendar类

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

    //获取Calendar对象(默认是当前时间)
	public static Calendar getInstance()
	
    //设置年月日
	public final void set(int year,int month,int date)

    //设置某个维度的时间(年、月、日)
	public void set(int field,int value)
    
    //对某个维度的日期进行加减
	public void add(int field, int amount)

  关于时间的不同维度(年、月、日),Calendar中有特定的字段来表示,如下:

  示例:

		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之间的相互转换

  • 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);

4.5 解决SimpleDateFormat类的线程安全问题*

  以下是几种方法供参考。

  • 1、局部变量法
      最简单的一种方式就是将SimpleDateFormat类对象定义成局部变量,即每个线程里都创建一个SimpleDateFormat对象,这样自然不会发生线程安全问题。不过,这种方式在高并发下会创建大量的SimpleDateFormat类对象,影响程序的性能。
  • 2、synchronized锁方式
      即用synchronized修饰并发代码块。当然,这种方式也会导致性能下降,不推荐。示例:
public class DateUtil {
  // 创建 ThreadLocal 对象,并设置默认值(new SimpleDateFormat)
  private static ThreadLocal<SimpleDateFormat> threadLocal =
      ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

  public static String formatDate(Date date) {
      return threadLocal.get().format(date);
  }
}

  • 3、Lock锁方式
      Lock锁方式与synchronized锁方式实现原理相同,缺点也和synchronized锁方式相似,性能并不好。示例:
          try {
            lock.lock();
            simpleDateFormat.parse("2020-01-01");
         } catch (ParseException e) {
            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
         }finally {
            lock.unlock();
         }
  • 4、ThreadLocal方式
      使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题。
      使用该方式时,将每个线程使用的SimpleDateFormat副本保存在ThreadLocal中,各个线程在使用时互不干扰,从而解决了线程安全问题。此种方式运行效率比较高,推荐在高并发业务场景的生产环境使用。示例:
	private static ThreadLocal<SimpleDateFormat> yyyyMMddFormat =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
 
	public static SimpleDateFormat yyyyMMdd() {
        return yyyyMMddFormat.get();
    }
  • 5、使用JDK8中线程安全的相关时间类
      DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。使用示例:
	private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
	LocalDate.parse("2020-01-01", formatter);

  使用DateTimeFormatter类来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。

;