Java基础(四)异常、枚举、日期、反射、泛型

文章目录

  本系列文章:
    Java基础(一)基本数据类型、变量类型、修饰符、表达式、数组、分支循环、关键字
    Java基础(二)字符串、四种引用、内存泄漏、克隆、语法糖、IO
    Java基础(三)面向对象、封装继承多态、重写和重载、内部类、包装类
    Java基础(四)异常、枚举、日期、反射、泛型
    Java基础(五)Lambda表达式、Stream流、正则表达式

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

一、异常分类

  Throwable是Java中处理异常情况的最顶级父类,该类下面有两个子类:Error和Exception。从继承关系来看,如下:

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

1.1 Java异常架构

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

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

  • 2、Error
      定义:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误
      特点:此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。
      这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的。
  • 3、Exception
      程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
  1. 运行时异常
      定义RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常
      特点:Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。比如NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、ClassCastException类型转换异常、ArithmeticExecption算术异常。此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!
      RuntimeException 异常会由 Java 虚拟机自动抛出并自动捕获(就算我们没写异常捕获语句运行时也会抛出错误!!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。
  2. 编译时异常
      定义: Exception 中除 RuntimeException 及其子类之外的异常。
      特点: Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常。

1.2 受检异常与非受检异常

  Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。

  1. 受检异常
      编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。
  2. 非受检异常
      编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。

1.3 常见异常

1.3.1 常见RuntimeException

  • 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、SecurityException:安全异常。由安全管理器抛出,用于指示违反安全情况的异常。示例:
  • 4、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);

  此时就会抛出异常:

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

  此时就会抛出异常:

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

  此时就会抛出异常:

1.3.2 IOException类

  • 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、FileNotFoundException:文件未找到异常,示例:
		File absoluteFile = new File("F:/demo.txt");
		BufferedReader br = new BufferedReader(new FileReader(absoluteFile));
		StringBuilder contentHolder = new StringBuilder();
		String lineContent = null;
		while ((lineContent = br.readLine()) != null){
    
    
			contentHolder.append(lineContent);
		}
		br.close();
		System.out.println("content=" + contentHolder);

  此时就会抛出异常:

1.3.3 其他类

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

  此时就会抛出异常:

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

  此时就会抛出异常:

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

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

1.5 NoClassDefFoundError 和 ClassNotFoundException 区别?

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

二、异常处理机制

2.1 异常处理中的关键字

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

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

2.2 异常处理的语句形式

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

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

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

  测试结果为:
          

2.2.2 try…catch使用的注意事项

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

2.3 Java异常处理过程

2.3.1 声明异常

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

2.3.2 抛出异常

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

2.3.3 捕获异常

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

2.3.4 常见异常处理方式

  • 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 方法会打印这些详细信息,调试时很有用)。

2.4 Error与Exception的区别

  Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
   Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

2.5 在catch捕获异常时,为什么不考虑使用Throwable类型,而只是使用Exception来进行接收?

  Throwable表示的范围要比Exception大。实际上程序使用Throwable来进行处理,没有任何语法问题,但是却会存在逻辑问题。因为此时出现的(或者说用户能够处理的)只有Exception类型,而如果使用Throwable接收,还会表示可以处理Error的错误,而用户是处理不了Error错误的,所以在开发中用户可以处理的异常都要求以Exception类为主。

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

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

2.7 throw和throws的区别?

  两者的区别如下:

  1. 位置不同
      throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象。
  2. 功能不同
      throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。
      throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。
      两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

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

  所有的检查性异常都继承自Exception;所有的非检查性异常都继承自RuntimeException。
  检查性异常和非检查性异常最主要的区别在于其处理异常的方式:检查性异常必须使用try catch或者throws等关键字进行处理,否则编译器会报错;非检查性异常一般是程序代码写的不够严谨而导致的问题,可以通过修改代码来规避。

2.9 JVM 是如何处理异常的?

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

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

  catch 或finally可以省略:

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

2.11 try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

  会执行,在 return 前执行。
  在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。

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

  开发者应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。用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);
            }
        }
    }

  Java 7 的 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);
    }

2.13 优先捕获最具体的异常

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

2.14 不要捕获 Throwable 类

  Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做!
  如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。

2.15 异常会影响性能

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

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

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

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

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

三、自定义异常

3.1 自定义异常的实现

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

  如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。

四、final/finally/finalize

4.1 final/finally/finalize的区别

  简单来说,三者区别如下:

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

4.2 final/finally/finalize的使用差异

  先看final关键字,如果final修饰的是一个基本类型,就表示这个变量被赋予的值是不可变的,即它是个常量;如果final修饰的是一个对象,就表示这个变量被赋予的引用是不可变的。也就是说,不可改变的只是这个变量所保存的引用,并不是这个引用所指向的对象
  如果一个变量或方法参数被final修饰,就表示它只能被赋值一次,但是JAVA虚拟机为变量设定的默认值不记作一次赋值。被final修饰的变量必须被初始化。初始化的方式以下几种:

  • 1、在定义的时候初始化。
  • 2、final变量可以在初始化块中初始化,不可以在静态初始化块中初始化。
  • 3、静态final变量可以在定义时初始化,也可以在静态初始化块中初始化,不可以在初始化块中初始化。
  • 4、final变量还可以在类的构造器中初始化,但是静态final变量不可以。
      当final用来定义一个方法时,它表示这个方法不可以被子类重写,但是并不影响它被子类继承
      finally只能用在try/catch语句中并且附带着一个语句块,表示这段语句最终总是被执行。关于try、catch、finally块中代码的执行顺序,可以看下面的示例代码:
        try{
    
    
            throw new NullPointerException();
        }catch(NullPointerException e){
    
    
            System.out.println("程序抛出了异常");
        }finally{
    
    
            System.out.println("执行了finally语句块");
        }

  测试结果为:

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

4.3 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)之前被执行的。
  接下来看下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()方法供客户端手动调用。
  此时,可以总结一下finally与return语句的执行顺序的关系:

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

  当try块中有return语句时,try语句在返回前,将其他所有的操作执行完,保留好要返回的值,而后转入执行finally中的语句,而后分为以下三种情况:

  1. 如果finally中有return语句,则会将try中的return语句”覆盖“掉,直接执行finally中的return语句,得到返回值,这样便无法得到try之前保留好的返回值。
  2. 如果finally中没有return语句,也没有改变要返回值,则执行完finally中的语句后,会接着执行try中的return语句,返回之前保留的值。
  3. 如果finally中没有return语句,但是改变了要返回的值,这里有点类似与引用传递和值传递的区别,分以下两种情况,:

  1)如果return的数据是基本数据类型或文本字符串,则在finally中对该基本数据的改变不起作用,try中的return语句依然会返回进入finally块之前保留的值。
   2)如果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是在finally中改变后的该属性的值。

五、可变参数

  在Java中,如果一个方法中传入的参数个数不固定,此时就可以使用可变参数的写法了,该方法并不常用,了解用法即可。
  可变参数的使用特点:

  • 1、只能出现在参数列表的最后;
  • 2、 …位于变量类型和变量名之间,前后有无空格都可以;
  • 3、调用可变参数的方法时,编译器为该可变参数隐含创建一个数组,在方法体中以数组的形式访问可变参数。如果传入的可变参数为null,便不会形成一个null数组,而是只有一个null。

  看一个例子:

public class HelloWorld {
    
    
	public static void main(String[] args) {
    
    
		addElements(1,2,3);
		addElements(null);
	}
	
	static void addElements(int ... arrs){
    
    
		System.out.println(arrs);
		for(int arr : arrs){
    
    
			System.out.println(arr);
		}
	}
}

  测试结果为:

  这个结果可以看出:可变参数并不是一个只含有null的单个元素的数组,而是null。

六、枚举

6.1 枚举的基本使用

  枚举是一个整型常数的集合,用于声明一组带标识符的常数,常用于整数数量固定的场景,比如月份、星期、性别等。JDK1.5中引入了枚举,其语法为:

修饰符 enum 枚举名称:枚举变量类型
{
    
    
	枚举成员
}

  需要注意的是:

  • 1、枚举变量类型可以不写,默认的是int型,具体的枚举变量对应的值从0开始,逐渐递增;
  • 2、任意两个枚举成员不能具有相同的名称,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔;
  • 3、引用枚举变量时的格式是:枚举名称.枚举变量。

  总的来说,除了不能继承,基本上可以将 enum 看做一个常规的类
  看个具体的枚举的例子:

public enum Color {
    
    
	RED,BLUE,GREEN,BLACK;
}

public class EnumTest {
    
    
	public static void main(String[] args) {
    
    
		Color color = Color.BLACK;
		switch(color){
    
    
			case BLACK:
				System.out.println("黑色");
				break;
			case RED:
				System.out.println("红色");
				break;	
			case GREEN:
				System.out.println("绿色");
				break;
			case BLUE:
				System.out.println("蓝色");
				break;	
			
		}
	}
}

  测试结果为:

黑色

  枚举类稍微高级的用法:

public enum EventEnum {
    
    

    LAUNCH("launch"),PAGEVIEW("pageview"),EVENT("event");

    EventEnum(String name){
    
    
        this.name = name;
    }
    private String name;

    public void show(){
    
    
        System.out.println(this.name);
        EventEnum[] ee = values();
        for(int i = 0;i<ee.length;i++){
    
    
            System.out.println(ee[i]);
        }
    }
}

public class Test {
    
    

    public static void main(String[] args) {
    
    
        EventEnum ee = EventEnum.LAUNCH;
        ee.show();
        String name = EventEnum.PAGEVIEW.name();
        System.out.println(name);
    }
}

6.2 Enum类的常用方法

   枚举继承自java.lang.Enum 类,所以枚举不能再继承其他类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是 Enum 类的实例。Enum类的常用方法如下:

  • 1、values()
      该方法作用是:以数组形式返回枚举类型的所有成员,数组中元素的顺序就是在枚举中声明的顺序。
      仍以上面的Color枚举为例,测试代码改为:
	    for(int i=0;i<Color.values().length;i++){
    
    
	        System.out.println(Color.values()[i]);
	    }

  测试结果为:

RED
BLUE
GREEN
BLACK

  • 2、valueOf()
      该方法作用是:将普通字符串转换为枚举实例。
  • 3、compareTo()
      该方法作用是:比较两个枚举成员在定义时的顺序。
      将valueOf和compareTo方法联合起来使用,示例代码:
	public static void main(String[] args) {
    
    
		compare(Color.valueOf("RED"));
	}
	
    public static void compare(Color color){
    
    
        for(int i=0;i<Color.values().length;i++){
    
    
            System.out.println(color+"与"+Color.values()[i]+"的比较结果是:"+color.compareTo(Color.values()[i]));
        }
    }

  测试结果为:

RED与RED的比较结果是:0
RED与BLUE的比较结果是:-1
RED与GREEN的比较结果是:-2
RED与BLACK的比较结果是:-3

  • 4、ordinal()
      该方法作用是:获取枚举成员的索引位置。
      示例代码:
        for(int i=0;i<Color.values().length;i++){
    
    
            System.out.println("索引"+Color.values()[i].ordinal()
            	+",值:"+Color.values()[i]);
        }

  测试结果为:

索引0,值:RED
索引1,值:BLUE
索引2,值:GREEN
索引3,值:BLACK

七、日期

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

7.1 Date类

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

  • 1、构造方法
      public Date()和public Date(long date)
  • 2、获取自 1970 年 1 月 1 日 00:00:00 GMT 以来此 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

7.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 代表毫秒

7.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中有特定的字段来表示,如下:

  看例子:

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

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

八、反射

8.1 反射是什么

  在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制
  由上面的定义可以看出,反射是在程序运行时获取类型信息的,而用来表示运行时类型信息对应类就是Class类。看下JDK文档中对Class类的解释:

  Class类被创建后的对象就是Class对象,Class对象表示的是自己手动编写类的类型信息。
  实际上在java中每个类都有一个Class对象,每编写并且编译一个新创建的类就会产生一个对应Class对象,并且这个Class对象会被保存在同名.class文件里。当new一个新对象或者引用静态成员变量时,Java虚拟机中的类加载器子系统会将对应Class对象加载到JVM中,然后JVM再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。
  需要注意的是:手动编写的每个Class类,无论创建多少个实例对象,在JVM中都只有一个Class对象,即在内存中每个类有且只有一个相对应的Class对象。
  Class类的特点:

  • 1、Class类也是类的一种,与class关键字是不一样的。
  • 2 、手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且对象保存在同名.class的文件中(字节码文件)。
  • 3、每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。
  • 4、Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载。
  • 5、Class类的对象作用是运行时提供或获得某个对象的类型信息。

8.2 反射机制的相关类

  与Java反射相关的类如下:

8.2.1 Class类

  Class代表类的实体,在运行的Java应用程序中表示类和接口。在这个类中提供了很多有用的方法,这里对他们简单的分类介绍。

  • 1、获得类相关的方法
  • 2、获得类中属性相关的方法
  • 3、获得类中注解相关的方法
  • 4、获得类中构造器相关的方法
  • 5、获得类中方法相关的方法

1.2.2 Field类

  Field代表类的成员变量(成员变量也称为类的属性)。
在这里插入图片描述

1.2.3 Method类

  Method代表类的方法。
在这里插入图片描述

1.2.4 Constructor类

  Constructor代表类的构造方法。
在这里插入图片描述

8.3 反射的基本用法

  我们先写一个简单的实例类:

public class Person {
    
    
	
	private String name;
	private int age;
	public Person(){
    
    
		
	}
	public Person(String name,int age){
    
    
		this.name = name;
		this.age = age;
	}
	public void sayHello(String str){
    
    
		System.out.println(name+" say hello");
	}
}

8.3.1 获取Class对象

  使用反射获取Class对象时,方法有三种:

  1. Class.forName()
  2. Person.class
  3. new Person().getClass()

如果是一个基本数据类型,那么可以通过Type的方式来获取Class对象,示例:
Class type = Integer.TYPE;

  这三种方法的使用,示例:

	    Class pClass1 = Class.forName("Basic.Person");
	    Class pClass2 = Person.class;
	    Class pClass3 = new Person().getClass();  

  这三种方式获取到的Class对象是同一个。那么在实际使用中,常用第几个呢?答案是第一个。第二种需要导入类的包,依赖性强,不导包就抛编译错误。第三种方法都可以直接创建对象,自然能获取到对象中的属性和方法。
  在使用 Class.forName 和 new Person().getClass() 方式获取Class对象时,都会导致静态属性被初始化,而且只会执行一次。为了验证,可以将Person稍微修改下:

public class Person {
    
    
	
	static String name;
	private int age;
	
	static {
    
    
	    System.out.println("初始化name");
	    name = "小明";
	}
	
	public Person(){
    
    
	}
	public Person(String name,int age){
    
    
		this.name = name;
		this.age = age;
	}
	public void sayHello(String str){
    
    
		System.out.println(name+" say hello");
	}
}

  再次运行上述测试代码,会有如下打印:

初始化name

8.3.2 获取构造方法及创建对象

  为了模拟各种构造方法,我们先将Person类稍微改一下:

package Basic;

public class Person {
    
    
			
	//(默认的构造方法)
	Person(String str){
    
    
		System.out.println("(默认)的构造方法 s = " + str);
	}
		
	//无参构造方法
	public Person(){
    
    
		System.out.println("调用了公有、无参构造方法");
	}
		
	//有一个参数的构造方法
	public Person(char name){
    
    
		System.out.println("姓名:" + name);
	}
		
	//有多个参数的构造方法
	public Person(String name ,int age){
    
    
		System.out.println("姓名:"+name+"年龄:"+ age);
	}
		
	//受保护的构造方法
	protected Person(boolean n){
    
    
		System.out.println("受保护的构造方法 n = " + n);
	}
		
	//私有构造方法
	private Person(int age){
    
    
		System.out.println("私有的构造方法   年龄:"+ age);
	}
}

  使用反射机制创建对象时,分为三步:

  1. 先获取Class对象
  2. 再获取构造器对象
  3. 获取想要的实体类对象

  测试代码如下:

package Basic;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class ReflectTest {
    
    
	public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException  {
    
    
		//1.加载Class对象
		Class clazz = Class.forName("Basic.Person");
		
		//2.获取所有公有构造方法
		System.out.println("所有公有构造方法:");
		Constructor[] conArray = clazz.getConstructors();
		for(Constructor c : conArray){
    
    
			System.out.println(c);
		}
		
		System.out.println("所有的构造方法(包括:私有、受保护、默认、公有):");
		conArray = clazz.getDeclaredConstructors();
		for(Constructor c : conArray){
    
    
			System.out.println(c);
		}
		
		System.out.println("获取公有、无参的构造方法:");
		Constructor con = clazz.getConstructor(null);
		//1>、因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
		//2>、返回的是描述这个无参构造函数的类对象。
		System.out.println("con = " + con);
		//调用构造方法
		Object obj = con.newInstance();
		
		//根据参数来获取对象的构造方法
		System.out.println("获取私有构造方法,并调用:");
		con = clazz.getDeclaredConstructor(int.class);
		System.out.println(con);
		//调用构造方法
		con.setAccessible(true);
		obj = con.newInstance(80);
	}
}

  测试结果为:

所有公有构造方法:
public Basic.Person(java.lang.String,int)
public Basic.Person(char)
public Basic.Person()
所有的构造方法(包括:私有、受保护、默认、公有):
private Basic.Person(int)
protected Basic.Person(boolean)
public Basic.Person(java.lang.String,int)
public Basic.Person(char)
public Basic.Person()
Basic.Person(java.lang.String)
获取公有、无参的构造方法:
con = public Basic.Person()
调用了公有、无参构造方法
获取私有构造方法,并调用:
private Basic.Person(int)
私有的构造方法 年龄:80

  对利用反射机制获取构造器对象的小结:

  • 1、public Constructor[ ] getConstructors()
      获取所有 public 构造方法
  • 2、public Constructor[ ] getDeclaredConstructors()
      获取所有的构造方法
  • 3、public Constructor getConstructor(Class… parameterTypes)
      获取单个参数的 public 构造方法
  • 4、public Constructor getDeclaredConstructor(Class…parameterTypes)
      根据不同参数类型,获取单个构造方法

  在获取了构造器对象之后,就可以继续调用newInstance方法(newInstance方法的参数就是创建对象时所需要传入的初始值)创建实例类的对象。

8.3.3 操作成员变量

  使用反射机制操作成员变量时,分为三步:

  1. 先获取Class对象
  2. 再获取属性(Field)对象
  3. 调用属性对象的set方法修改属性值

   此时再改一下Person类:

public class Person {
    
    
	
	public String name;
	protected int age;
	char sex;
	private String phoneNum;
	
	@Override
	public String toString() {
    
    
		return "Student [name=" + name + ", age=" + age + ", sex=" + sex
				+ ", phoneNum=" + phoneNum + "]";
	}	
}

  测试代码为:

package Basic;

import java.lang.reflect.Field;

public class ReflectTest {
    
    

	public static void main(String[] args) throws Exception {
    
    
		
		Class perClass = Class.forName("Basic.Person");
		/*获取字段*/
		System.out.println("获取所有公有的字段:");
		Field[] fieldArray = perClass.getFields();
		for(Field f : fieldArray){
    
    
			System.out.println(f);
		}
		System.out.println("获取所有的字段(包括私有、受保护、默认的):");
		fieldArray = perClass.getDeclaredFields();
		for(Field f : fieldArray){
    
    
			System.out.println(f);
		}
		System.out.println("获取公有字段并调用:");
		Field f = perClass.getField("name");
		System.out.println(f);
		//获取一个对象
		Object obj = perClass.getConstructor().newInstance();
		//为字段设置值
		f.set(obj, "Jack");
		//验证
		Person per = (Person)obj;
		System.out.println("验证姓名:" + per.name);
		
		
		System.out.println("获取私有字段并调用");
		f = perClass.getDeclaredField("phoneNum");
		System.out.println(f);
		//解除私有限定,设置为true后,就可以访问private属性了
		f.setAccessible(true);
		f.set(obj, "12345678910");
		System.out.println("验证电话:" + per);
	}
}

  测试结果为:

获取所有公有的字段:
public java.lang.String Basic.Person.name
获取所有的字段(包括私有、受保护、默认的):
public java.lang.String Basic.Person.name
protected int Basic.Person.age
char Basic.Person.sex
private java.lang.String Basic.Person.phoneNum
获取公有字段并调用:
public java.lang.String Basic.Person.name
验证姓名:Jack
获取私有字段并调用
private java.lang.String Basic.Person.phoneNum
验证电话:Student [name=Jack, age=0, sex=

  从测试结果可以看出,我们已经成功修改了Person对象的两个属性值。对利用反射机制获取构造器对象的小结:

  • 1、public Field[ ] getFields()
      获取类中所有 public 属性
  • 2、public Field[ ] getDeclaredFields
      获取类中所有属性
  • 3、public Field getField(String name)
      获取某个特定的public属性
  • 4、public Field getDeclaredField(String name)
      获取某个特定属性

  有上面代码也可以看出,获取到某个属性对象后,修改改属性的方式为:
f.set(obj, value),其中f为属性对象,obj为实体类对象,value为属性值。

   需要注意的是:

getDeclaredField 可以获取本类所有的字段,包括private的,但是不能获取继承来的字段。 (只能获取到private的字段,并且要想访问该private字段的值,需要加上setAccessible(true))。

8.3.4 调用成员方法

  使用反射机制调用成员方法时,分为三步:

  1. 先获取Class对象
  2. 再获取方法对象
  3. 调用方法对象的invoke方法

   此时再改一下Person类:

public class Person {
    
    
	
	public void methodOne(String s){
    
    
		System.out.println("调用了public methodOne方法");
	}
	protected void methodTwo(){
    
    
		System.out.println("调用了protected methodTwo方法");
	}
	void methodThree(){
    
    
		System.out.println("调用了default methodThree方法");
	}
	private String methodFour(int age){
    
    
		System.out.println("调用了private methodFour方法");
		return "1234";
	}
}

  此时测试代码:

public class ReflectTest {
    
    

	public static void main(String[] args) throws Exception {
    
    
		Class perClass = Class.forName("Basic.Person");
		//获取该对象的普通方法,包含的方法范围是当前对象及父类对象的
		//所有公共方法,比如Object中的方法
		System.out.println("获取所有的public方法:");
		perClass.getMethods();
		Method[] methodArray = perClass.getMethods();
		for(Method m : methodArray){
    
    
			System.out.println(m);
		}
		
		System.out.println("获取所有的方法:");
		methodArray = perClass.getDeclaredMethods();
		for(Method m : methodArray){
    
    
			System.out.println(m);
		}
		
		System.out.println("获取public methodOne()方法:");
		Method m = perClass.getMethod("methodOne", String.class);
		System.out.println(m);
		
		Object obj = perClass.getConstructor().newInstance();
		m.invoke(obj, "Jack");
				
		System.out.println("获取private methodFour()方法:");
		m = perClass.getDeclaredMethod("methodFour", int.class);
		System.out.println(m);
		//解除私有限定,可以访问private方法
		m.setAccessible(true);
		Object result = m.invoke(obj, 20);
		System.out.println("返回值:" + result);
				
	}
}

  测试结果为:

获取所有的public方法:
public void Basic.Person.methodOne(java.lang.String)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
获取所有的方法:
public void Basic.Person.methodOne(java.lang.String)
private java.lang.String Basic.Person.methodFour(int)
protected void Basic.Person.methodTwo()
void Basic.Person.methodThree()
获取public methodOne()方法:
public void Basic.Person.methodOne(java.lang.String)
调用了public methodOne方法
获取private methodFour()方法:
private java.lang.String Basic.Person.methodFour(int)
调用了private methodFour方法
返回值:1234

  从测试结果可以看出,我们已经成功调用了Person类中的成员方法,对利用反射机制调用成员方法的小结:

  • 1、public Method[ ] getMethods()
      获取所有的public方法
  • 2、public Method[ ] getDeclaredMethods()
      获取所有方法
  • 3、public Method getMethod(String name,Class<?>… parameterTypes)
      获取指定的 public 方法
  • 4、public Method getDeclaredMethods(String name ,Class<?>… parameterTypes)
      获取指定的方法

8.4 反射的使用场景

  在二十三种设计模式中,很多都在提高代码的可维护性、复用性,其实设计模式和反射结合起来使用,是一种更好的方式。

8.4.1 用配置文件运行指定方法

  我们先写两个相似的实现类:

package Basic;

public class Book1 {
    
    
	public void read1(){
    
    
		System.out.println("读《英雄志》");
	}
}

public class Book2 {
    
    
	public void read2(){
    
    
		System.out.println("读《雪中悍刀行》");
	}
}

  在E盘目录下创建一个test.txt文件,内容为:

class=Basic.Book1
method=read1

  然后测试代码为:

package Basic;

import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Properties;

public class ReflectTest {
    
    

	public static void main(String[] args) throws Exception {
    
    
	      //test.txt中获取类名称和方法名称
        File springConfigFile = new File("E:\\test.txt");
        Properties springConfig= new Properties();
        springConfig.load(new FileInputStream(springConfigFile));
        String className = (String) springConfig.get("class");
        String methodName = (String) springConfig.get("method");
         
        Class clazz = Class.forName(className);
        Method m = clazz.getMethod(methodName);
        Constructor c = clazz.getConstructor();
        Object book = c.newInstance();
        //调用对象的指定方法
        m.invoke(book);  
    }
}

  测试结果为:

读《英雄志》

  此时我们将配置文件的内容改一下:

class=Basic.Book2
method=read2

  测试的测试结果为:

读《雪中悍刀行》

8.4.2 越过泛型检查

  泛型是在编译期间起作用的,在编译后的.class文件中是没有泛型的, 所以是可以通过反射越过泛型检查的。

public class ReflectTest {
    
    

	public static void main(String[] args) throws Exception {
    
    
		List<String> strList = new ArrayList<>();
		strList.add("aaa");
		strList.add("bbb");
		
		//直接add(100)会报错:The method add(int, String) in the type List<String> is not applicable for the arguments (int)
		//strList.add(100);
		Class listClass = strList.getClass(); 
		Method m = listClass.getMethod("add", Object.class);
		m.invoke(strList, 100);
		
		for(Object obj : strList){
    
    
			System.out.println(obj);
		}
    }
}

  测试结果如下:

aaa
bbb
100

8.4.3 Spring中的反射机制

  Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:

1)将程序内所有 XML 或 Properties 配置文件加载入内存中;
2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
3)使用反射机制,根据这个字符串获得某个类的Class实例;
4)动态配置实例的属性。

8.5 Java反射创建对象效率高还是通过new创建对象的效率高?

  通过new创建对象的效率比较高。通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低。

九、泛型

  泛型,即“参数化类型”。泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
  在JDK源码中,经常能看到泛型的踪迹,比如ArrayList的继承关系:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

9.1 泛型的特性

  泛型的特性是:只在编译阶段有效,这句话如何理解理解呢?看下面的例子:

		List<String> sList = new ArrayList<String>();
		List<Integer> iList = new ArrayList<Integer>();

		Class classStringArrayList = sList.getClass();
		Class classIntegerArrayList = iList.getClass();

		if(classStringArrayList.equals(classIntegerArrayList)){
    
    
		    System.out.println("泛型测试,类型相同");
		}

  这个测试程序的输出结果是:“泛型测试,类型相同”。这个例子说明了:在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息去掉,泛型型信息不会进入到运行时阶段。
  此处需要注意的有两点:

  • 1、泛型类型变量不能是基本数据类型
      比如,没有 ArrayList< double >,只有 ArrayList< Double >。因为当类型擦除后,ArrayList 的原始类中的类型变量(T)替换为 Object,但 Object 类型不能存储 double 值。
  • 2、泛型类中的静态方法和静态变量不可以使用 泛型类所声明的泛型类型参数
      这话直观看上去不太容易理解,看个例子:

      上面的代码会编译报错,原因是:因为泛型类中的泛型参数的实例化是在定义 泛型类型对象(例如 ArrayList< Integer > ) 的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,不能确定这个泛型参数是何种类型。

9.2 泛型的使用

  前面已简单介绍过,泛型可以用在类、接口和方法中。

9.2.1 泛型类

  在定义类的时候在类名的后面添加<E,K,V,A,B>,起到占位的作用,类中的方法的返回值类型和属性的类型都可以使用定义的泛型。
  这个应该是最常见的泛型的使用,比如上面提到的ArrayList:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

  泛型类一般在使用时,会声明要存储的参数类型,示例:

		List<String> sList = new ArrayList<String>();
		List<Integer> iList = new ArrayList<Integer>();
		List<Long> lList = new ArrayList<Long>();
		sList.add("123");
		iList.add(1);
		lList.add(23L);

9.2.2 泛型接口

  泛型接口的使用,与泛型类类似,比如List接口,看下List接口的定义:

public interface List<E> extends Collection<E>

  在定义接口的时候,在接口的名称后添加<E,K,V,A,B>,需注意:

1、子类在进行实现的时候,可以不填写泛型的类型,此时在创建具体的子类对象的时候才决定使用什么类型
2、子类在实现泛型接口的时候,只在实现父类的接口的时候指定父类的泛型类型即可,此时,测试方法中的泛型类型必须要跟子类保持一致(下面的例子)

  再看个例子:

public interface FanXingInterface<B> {
    
    
   public B test();
   public void test2(B b);
}

public  class   FanXingInterfaceSub implements FanXingInterface<String> {
    
    

    @Override
    public String test() {
    
    
        return null;
    }

    @Override
    public void test2(String string) {
    
    
    }
}

     FanXingInterfaceSub fxi = new FanXingInterfaceSub() ;
     fxi.test2("123");

9.2.3 泛型方法

  泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。在JDK源码中,泛型经常使用,比如HashMap.java中:

    final Node<K,V> getNode(int hash, Object key) 

  对泛型方法的一个总结:无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法
  另外对于一个static的方法,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。
  在定义方法的时候,指定方法的返回值和参数是自定义的占位符,可以是类名中的T,也可以是自定义的Q,只不过在使用Q的时候需要使用< Q>定义在返回值的前面。看下面的例子:

public class FanXingMethod<T> {
    
    

    private T t;

    public T getT() {
    
    
        return t;
    }

    public void setT(T t) {
    
    
        this.t = t;
    }

    public <Q> void show(Q q){
    
    
        System.out.println(q);
        System.out.println(t);
    }
}

        FanXingMethod<String> fxm = new FanXingMethod<>();
        fxm.setT("ttt");
        fxm.show(123);
        fxm.show(true);

9.3 泛型的上下边界(不常用)

  泛型可以完全不指定所要存储的元素类型,也可以进行一些限制,比如:ArrayList <? extends ClassA> list 就表示list中存储的元素类型必须是ClassA类型或是其子类;也就是父类确定了,所有的子类都可以直接使用。

  ArrayList <? super ClassA> list 就表示list中存储的元素类型必须是ClassA类型或是其父类。也就是子类确定了,子类的所有父类都可以直接传递参数使用。

  此外,还有’?'通配符,代表任意泛型,此种情况下,就不要向容器里存储对象了,因为不指定容器里存放的什么元素,此时,可以将容器中的元素以Object的形式取出来。

9.4 常见的泛型面试题

  • 1、Java 中的泛型是什么 ? 使用泛型的好处是什么?
      泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码。
      泛型是一种编译时类型确认机制。它提供了编译期的 类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现 ClassCastException。
  • 2、Java 的泛型是如何工作的 ? 什么是类型擦除 ?
      泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。
      编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个 List 类型来表示。
  • 3、Array 中可以用泛型吗?
      Array不支持泛型。
  • 4、 什么是泛型中的限定通配符和非限定通配符 ?
      限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是 T 的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是 T 的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。
  • 5、Java中List<?>和 和 List< Object >之间的区别是什么?
      List<?> 是一个未知类型的 List,而List< Object >其实是任意类型的 List。你可以把 List< String >, List< Integer >赋值给 List<?>,却不能把 List< String >赋值给List< Object >。
  • 6、List< String > 和原始类型 List 之间的区别
      带参数类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型 List 却不是类型安全的。你不能把 String 之外的任何其它类型的 Object 存入String 类型的 List 中,而你可以把任何类型的对象存入原始 List 中。使用泛型的带参数类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换。看个例子:
		List listOfRawTypes = new ArrayList();
		listOfRawTypes.add("abc");
		listOfRawTypes.add(123); //编译器允许这样 - 运行时却会出现异常
		String item = (String) listOfRawTypes.get(0); //需要显式的类型转换
		item = (String) listOfRawTypes.get(1); //抛 ClassCastException,因为 Integer 不能被转换为 String
		List<String> listOfString = new ArrayList();
		listOfString.add("abcd");
		listOfString.add(1234); //编译错误,比在运行时抛异常要好
		item = listOfString.get(0); //不需要显式的类型转换 - 编译器自动转换
  • 7、 List<? extends T>和 和 List <? super T> 之间有什么区别 ?
      List<? extends T>可以接受任何继承自 T 的类型的 List,而 List<? super T>可以接受任何 T 的父类构成的 List。例如 List<?extends Number>可以接受 List< Integer >或 List< Float >。在本段出现的连接中可以找到更多信息。

十、参考:阿里巴巴Java开发手册中异常处理

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

  反例:

try {
    
     
	obj.method(); 
} catch (NullPointerException e) {
    
    
	//todo
}
  • 2、【强制】异常不要用来做流程控制,条件控制。 说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
  • 3、【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。 说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。 正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
  • 4、【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
  • 5、【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务
  • 6、【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。 说明:如果JDK7及以上,可以使用try-with-resources方式。
  • 7、【强制】不要在finally块中使用return。
  • 8、【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类
  • 9、【强制】在调用RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。 说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException。什么情况会抛出NoSuchMethodError呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。
  • 10、【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。 说明:本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。
  • 11、【推荐】防止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问题。
  • 12、【推荐】定义时区分unchecked / checked 异常,避免直接抛出new RuntimeException(),更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException等。
  • 13、【参考】对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess()方法、“错误码”、“错误简短信息”。 说明:关于RPC方法返回方式使用Result方式的理由: 1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。 2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
  • 14、【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。 说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。 正例:一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto) {
    
    
	//todo
}

猜你喜欢

转载自blog.csdn.net/m0_37741420/article/details/107685765