JavaSE(五)异常、断言与日志

本文深入探讨Java中的异常处理机制,包括异常类型、自定义异常、异常捕获及处理流程,同时详述日志系统的设计与使用,涵盖日志级别、日志记录器、日志处理器和日志格式化等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

异常

  所有的异常类都是Throwable类的子类,throw只能抛出异常类,Throwable下有Error和Exception两个系统定义好的直接子类。Error为运行时系统内部错误或资源耗尽等,一般无法彻底解决出现的异常让程序继续运行,只能通知用户并安全退出,Error跟运行时异常相似,可以不被捕获;Exception包括RuntimeException和其他异常(如IOException)。
  RuntimeException是由于程序本身错误而导致的异常,所以在程序中通过检测手段,应该可以解决运行时异常,所以运行时异常通过程序是可以避免的,尽量修改代码不让运行时异常产生,而不是去捕获异常来处理。并不是只有RuntimeException才是出现在运行时。
  未检查异常包括RuntimeException和Error及其子类异常,当异常发生而没在任何地方捕获就会终止程序并打印出错堆栈信息。已检查异常必须被处理(捕获或函数抛出),否则编译都会出错。之所以已检查异常必须被捕获或在函数头抛出,是因为异常必须被处理,而未检查异常可以不捕获也不须在函数头抛出,因为这异常是不应该发生(RuntimeException)或无法处理只能终止程序(Error)。一个方法内部如果有抛出已检查异常(throw Exception对象)却未被捕捉,那么方法必须在其首部声明(public void fun() throws Exception1类, Exception2类 {})所有未被捕捉的已检查异常,而调用该方法的函数必须处理所有声明的已检查异常,到底用哪种方法处理异常,主要看知不知道如何处理异常,如果知道如何处理异常,就对他进行捕获处理,否则就函数抛出让调用函数的对象去处理。子类的方法抛出的异常必须是父类抛出异常的子类。如果希望某个函数抛出某个类型的异常,而函数中执行的过程中抛出的是另一个异常,可以捕获该异常并在异常处理中抛出希望抛出的异常,如我们某个函数是为了获取某个资源,如果未能获取资源,不管什么原因都抛出一个获取资源失败的异常,而函数中抛出异常可能是各种各样的原因,我们捕获这些原因并在处理时抛出获取资源失败的异常,我们也可以将捕获的异常作为获取资源失败的异常的原因。
  一般在找不到系统定义的合适异常类的时候才自定义异常类,它需要继承Throwable或其子类(可以用比较贴近的系统已定义的类做父类),一般实现不带参和带详细描述的字符串参两个构造器,带详细描述字符串参数的实现直接调用父类的构造器,可以通过getMessage获取详细描述字符串。
  异常捕获的步骤为 try {} catch(Exception e) {} finally{},正常情况执行了try块执行finally块,有异常抛出时先执行try块,再执行catch块,最后执行finally块。一个try跟有多个catch捕获多个不同类型的异常,最先匹配的一个catch才有效(符合instanceof就匹配),几个捕获的异常具有继承关系时,必须子在前父在后,否则编译报错,要同时捕获多个异常且处理方式相同,可以catch(Exception1 | Exception2 e){}。有finally时,我们习惯用try { try {} finally{}} catch(Exception e) {},步骤为try->finally->catch,其实这就是一个try-catch里面嵌套一个try-finally结构,这有个问题就是try中抛出异常,finally中也抛出异常,catch到的就是finally抛出的异常,而try块中抛出的异常会丢失。
  finally通常用于资源回收,如关闭文件、数据库解除关联等,一个try最多跟一个finally,他一定会被执行,无异常执行try->finally,异常被捕获执行try->catch->finally,如果try中有return语句 在return后执行finally,所以如果finally中也有return语句,会改变函数返回值,try/finally语句可以没有catch。
  根据返回值来判断是否有问题还是抛出异常?发生异常就抛出异常,比如判断函数返回false就不属于异常。
  如果释放的资源实现了AutoCloseable接口,可以用带资源的try块,如try(AutoCloseable resource1 = …; AutoCloseable resource2=…){},如果还带有catch和finally,那么顺序就是try->resource1.close()->resource2.close()->catch->finally。
  当异常在main函数中都未被捕获时,默认会打印出堆栈跟踪信息和异常信息,我们可以通过静态方法Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)自定义handler处理main函数抛出的异常
  堆栈跟踪是一个方法调用的过程列表,表现为一个StackTraceElement数组,每一个StackTraceElement就表示一个调用,他包含了类名、方法名以及在代码中的行数等等信息。Thread.getAllStackTraces()返回一个Map<Thread, StackTraceElement[]>就是当前应用各个线程的堆栈跟踪。

public final class StackTraceElement implements java.io.Serializable {
  public String getFileName(); // 返回源文件名,源文件信息不存在返回null;
  public String getClassName(); // 类全名;
  public String getMethodName(); // 方法名;
  public int getLineNumber(); // 获取源文件中的行数,行数信息不存在返回-1;
  public String toString(); // 上面所有信息的格式化输出;
}

// 其子类一般都没有别的方法,基本上都只有自己的构造函数,而构造函数也和Throwable的构造函数参数相同
public class Throwable implements Serializable {
  public Throwable();
  public Throwable(String msg);
  public Throwable(Throwable cause);
  public Throwable(String message, Throwable cause);
  public String getMessage();
  public synchronized Throwable getCause(); // 获取引起该异常的原因异常;
  public StackTraceElement[] getStackTrace(); // 获取跟踪堆栈;
  public void printStackTrace(PrintWriter/PrintStream s); // 将跟跟踪堆栈写入指定的流;
  public void printStackTrace(); // 等同于printStackTrace(System.err);
}

断言

  使用格式为assert 条件表达式; 或assert 条件表达式: 信息表达式; 断言表达式不成立会抛出AssertionError错误,第二种方式会在构建AssertionError时,构造函数会将信息表达式转换为消息字符串作为参数。
  对于一个方法,存在对象做参数,参数可以为null或者不可以为null需要在方法注释里面注明,未注明表示参数为null时方法的功能是不确定的,如果注明了可以为null,那么要说明为null时方法的功能,如果注明不能为null,那么在函数中可以首先用assert断言该参数不为null,当然,既然注明了不为null,该函数的调用函数在调用时就应该先判断该参数不为null,若调试时出现了断言异常,说明我们在调用该函数时为进行参数检查。
  使用断言需要在运行时使用-ea选项,如java -ea hello,而且编译好的的应用,在不加-ea时会在加载类时跳过断言语句,不会影响程序的执行效率,所以在开发调试阶段可以使用-ea选项,而在正式环境下关闭断言,断言多用在无法恢复的错误检查

日志系统

  日志级别由高到低分为Level.SERVER、Level.WARNING、Level.INFO、Level.CONFIG、Level.FINE、Level.FINER、Level.FINEST七个级别,日志记录器的记录级别除了日志级别外还有Level.OFF表示不记录,以及Level.ALL表示全记录,如果是日志级别就表示记录该级别以及比该级别高的日志
  日志记录器具有树的层次结构,其根日志记录器我们一般不直接用,在根日志记录器下挂了个全局日志记录器名为global,通过静态方法Logger.getGlobal()获得,我们也可以通过静态方法Logger.getLogger(String name)获得名为name的日志记录器,如果不存在,就新建名为name的日志记录器并将它挂到日志记录器树的相应位置(这个相应位置是根据日志记录器的name来确定的,而name本身就是由点分割的字符串,具有层次结构,如新建一个名为aaa.bbb.ccc.ddd的日志记录器,先找名为aaa.bbb.ccc的日志记录器挂在下面,如果找不到会找名为aaa.bbb的日志记录器挂在下面,如果还是找不到会找名为aaa的日志记录器挂在下面,如果还是找不到就挂在日志系统的根下,假设找到了aaa.bbb日志记录器,那说明没有aaa.bbb.ccc日志记录器,由于也没有aaa.bbb.ccc.ddd记录器,所以aaa.bbb下面可能会有aaa.bbb.ccc.ddd.xxx记录器,我们还需要把aaa.bbb.ccc.ddd.xxx记录器从aaa.bbb上卸下来挂在aaa.bbb.ccc.ddd下面)。日志记录器会继承父日志记录器的一些属性,如记录等级。
  日志记录器的默认配置在配置文件jre/lib/logging.properties中,由LogManager类进行处理,如handlers= java.util.logging.ConsoleHandler配置处理器,多个处理器用逗号隔开;.level= INFO表示默认的记录级别;com.xyz.foo.level = SEVERE表示名为com.xyz.foo日志记录器的记录级别;java.util.logging.ConsoleHandler.level = INFO表示处理器级别为info;java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter表示默认采用的记录格式,另外还有xml格式;对于FileHandler,有下列配置java.util.logging.FileHandler.level默认Level.All/append默认false表示文件不是追加/limit文件的近似最大字节数/pattern日志文件名模式/count循环日志的最大数值(当文件大于limit,循环值+1,大于count又从0开始)/filter/encoding/formatter,日志文件名的模式%h为user.home的值,%g当前循环值,%u为解决不同程序的冲突值,应该都加上。我们可以在启动虚拟机的时候自己指定配置文件,参数为-Djava.util.logging.config.file=configFile。
  我们使用logger.log(Level level, String msg);来记录level等级的信息msg,该方法不但会记录msg,还会记录调用该方法的类名和方法名,以及调用时间,我们也可以用logger.server/warning/info/config/fine/finer/finest(String msg);来简化调用。但是有时候由于虚拟机的优化,比如内联,就会导致得到的类名和方法名不够准确,于是我们可以通过logger.logp(Level level, ,String className, String methodName, String msg);来指定调用的类名和方法名。我们还可以用相应方法来记录throwable的栈信息。
  日志系统中的类主要有日志管理器LogManager、日志记录器Logger、日志记录LogRecord、日志过滤器Filter、日志格式Formatter和日志处理器Handler。我们最常用的类一般只有Logger
  日志记录器log一类函数的处理过程,如果log日志级别低于记录器级别直接返回,否则根据log参数以及日志记录器参数构建LogRecord,并通过Filter进行过滤,然后调用Handler的publish处理,处理完后如果要调用父logger的处理器,就调用父Logger的处理器的publish方法。

// 一个LogRecord对应一条日志记录,其中包括级别、调用类名、调用方法名、信息、日志时间、thrown、参数数组和资源包。
public class LogRecord implements java.io.Serializable {
    public LogRecord(Level level, String msg);
  public void setLoggerName(String name);
  public String getLoggerName();
  public void setResourceBundle(ResourceBundle bundle);
  public ResourceBundle getResourceBundle();
  public void setLevel(Level level);
  public Level getLevel();
  public void setSourceClassName(String sourceClassName);
  public String getSourceClassName();
  public void setSourceMethodName(String sourceMethodName);
  public String getSourceMethodName();
  public void setMessage(String message);
  public String getMessage();
  public void setParameters(Object parameters[]);
  public Object[] getParameters();
  public void setThrown(Throwable thrown);
  public Throwable getThrown();
  public void setMillis(long millis);
  public long getMillis();
}

public abstract class Formatter {
  public abstract String format(LogRecord record); // 返回格式化后的信息;
  public String getHead(Handler h); // 返回在文档头添加的字符串;
  public String getTail(Handler h); // 返回在文档尾添加的字符串;
  public synchronized String formatMessage(LogRecord record); // 返回本地化和格式化后的信息;
}    
    
public interface Filter {
    public boolean isLoggable(LogRecord record); // 根据日志记录信息来决定是否过滤掉;
}

// 日志系统最终的处理类的基类,JRE继承了该类的子类主要有ConsoleHandler、FileHandler和SocketHandler。日志处理器也有处理等级,只有当LogRecord的等级高于它时才处理
public abstract class Handler {
  public abstract void publish(LogRecord record); //该方法负责对日志信息进行处理;
  public abstract void flush();
  public abstract void close(); 
  public synchronized void setFormatter(Formatter newFormatter);
  public Formatter getFormatter();
  public synchronized void setEncoding(String encoding);
  public String getEncoding();
  public synchronized void setFilter(Filter newFilter);
  public Filter getFilter();
  public synchronized void setLevel(Level newLevel);
  public Level getLevel();
  public boolean isLoggable(LogRecord record);
}
    
// 日志记录器,可以记录日志    
public class Logger {
  public static Logger getGlobal(); // 获取日子系统的全局Logger;
  public static getLogger(String name); // 获取名为name的一个logger,我们习惯用一个包名作为logger的名;
  public void log(LogRecord logRecord); // 
  public void log(Level level, String msg); 
  public void log(Level level, String msg, Object param1); 
  public void log(Level level, String msg, Object params[]); // msg会为msg的msg{0}...{params.length),这个{i}等是占位符,实际值为params[i].toString();
  public void log(Level level, String msg, Throwable thrown); 
  public void logp(Level level, String sourceClass, String sourceMethod, String msg);
  public void logp(Level level, String sourceClass, String sourceMethod, String msg, Object param1);
  public void logp(Level level, String sourceClass, String sourceMethod, String msg, Object params[]);
  public void logp(Level level, String sourceClass, String sourceMethod, String msg, Throwable thrown);    
  public void server/warning/info/config/fine/finer/finest(String msg); 
  public void entering(String sourceClass, String sourceMethod); // FINER级别以"ENTRY"为msg,实际调用logp;
  public void entering(String sourceClass, String sourceMethod, Object param1); // msg为ENTRY{0};
  public void entering(String sourceClass, String sourceMethod, Object params[]); // msg为ENTRY{0},{1},...{params.length},
  public void exiting(String sourceClass, String sourceMethod); // FINER级别以"RETURN"为msg;
  public void exiting(String sourceClass, String sourceMethod, Object result); // msg为"RETURN{0}",result为param1; 
  public void setResourceBundle(ResourceBundle bundle); // 设置资源包;
  public ResourceBundle getResourceBundle();
  public String getResourceBundleName();
  public void setFilter(Filter newFilter);
  public Filter getFilter();
  public void setLevel(Level newLevel); 
  public Level getLevel();
  public String getName();
  public void addHandler(Handler handler);
  public void removeHandler(Handler handler);
  public Handler[] getHandlers(); // 获取处理器;
  public void setUseParentHandlers(boolean useParentHandlers); //是否在调用了当前日志记录器的处理器后调用父日志记录器的处理器进行处理;
  public boolean getUseParentHandlers();
  public Logger getParent();
  public void setParent(Logger parent);
}    

调试

  用system.out.println(String str)或日志进行打印调试。
  测试一个类可以在类中加main函数,然后运行该类。
  eclipse集成了调试器比JDB好用,需要run as debug,使用JDB需要加-g选项编译。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值