Exception

Java 中的异常

Java 中异常分为两类:受检异常、非受检异常(RuntimeException, 运行时异常)。

两种异常的处理方式:

  1. 非受检异常
    1. 捕获
    2. 抛出
    3. 不处理
  2. 受检异常(除了 RuntimeException 都是受检异常)
    1. 继续抛出,消极的方式,一致抛出到 JVM 来处理
    2. 使用try..catch块来处理

受检异常必须处理,否则不能编译通过。

异常原理

异常,即程序运行期键发生的不正常事件,它会打断指令的正常流程。异常均出现在程序运行期,编译期的问题成为语法错误。

异常的处理机制:

  1. 当程序在运行过程中出现异常,JVM 会创建一个该类型的异常对象。同时把这个异常对象交给运行时系统,即抛出异常。
  2. 运行时系统接收到一个异常时,它会在异常产生的代码上下文附近查找对应的处理方式。
  3. 异常的处理方式有两种:
    1. 捕获并处理:在抛出异常的代码附近显式使用try..catch进行处理,运行时系统捕获后会查询相应的catch处理块,在catch处理块中对异常进行处理。
    2. 查看异常发生的方法是否向上声明异常,有向上声明,向上级查询处理语句,如果没有向上声明, JVM 中断程序的运行并处理,即使用throws向外声明

异常分类

所有的错误和异常均继承自java.lang.Throwable

  1. Error:错误,JVM 内部的严重问题,无法恢复,程序人员不用处理。
  2. Exception:异常,普通的问题,通过合理的处理,程序还可以回到正常的处理流程,要求编程人员进行处理。
  3. RuntimeException:非受检异常,这类异常是编程人员的逻辑问题,即程序编写 BUG。Java 编译器不强制要求处理,因此这类异常在程序中可以处理也可以不处理。比如:算术异常、除零异常等。
  4. 非 RuntimeException:受检异常,这类异常由外部的偶然因素导致。Java 编译器强制要求处理,程序人员必须对这类异常进行处理。比如:Exception、FileNotFoundException、IOException等。

除了受检异常,都是非受检异常。

异常处理方式

try/catch

try{
  // 可能会出现异常的代码块
} catch(异常类型1 变量名1){
  // 对该类型异常的处理代码块
} catch(异常类型2 变量名2){
  // ...
} finally{
  // 无论是否发生异常都会执行的代码块
  // 常用来释放资源,比如关闭文件
}

向上声明

即使用thorws关键字,将异常向上抛出,声明一个方法可能会抛出的异常列表。

... methodName(参数列表) throws 异常类型1, 异常类型2 {
  // 方法体
}

这种方式通过声明,告诉本方法的调用者,在使用本方法时,应该对那些异常进行处理。

手动抛出

当程序逻辑不符合预期,要终止后面的代码执行时使用这种方式。

在方法的代码段中,可以使用throw关键字手动抛出一个异常。

如果手动抛出的是一个受检异常,那么本方法必须处理(应该采用向上抛出这个异常),如果是非受检异常,则处理是可选的。

自定义异常

当需要一些跟特定业务相关的异常信息类时,可以根据实际的需求,继承Exception来定义受检异常,或者继承RuntimeException来定义非受检异常

最佳实践

捕获那些已知如何处理的异常,即使用try/catch来处理已知类型的异常。

向上抛出那些不知如何处理的异常。

减少异常处理的嵌套。

Scala 中的异常

Scala 中定义所有的异常为非受检异常,即便是SQLExceptionIOException

最简单的处理方式是定义一个偏函数:

val input = new BufferReader(new FileReader(file))
try{
  for(line <- Iteratro.continually(input.readLine()).takeWhile(_ != null))
      println(line) 
} catch{
  case e:IOException => errorHandler(e)
  // case SomeOtherException(e) => ???
} finally{
  imput.close()
}

或者使用 control.Exception来组合需要处理的多个异常:

Exception.handling(classOf[RuntimeException], classOf[IOException]) by println apply {
  throw new IOException("foo")
}

更上面的最佳实践里提到的一样,不能以通配的方式捕获所有异常,这会捕获到类似内存溢出这样的异常:

try{	
  ...
} catch{
  case _ => ...	// Don't do this!
}

如果需要捕获大部分可能出现的异常,并且不是严重致命的,可以使用NonFatal

try{
  ...
} catch{
  case NonFatal(e) => println(e.getMessage)
}

NonFatal意为非致命错误,不会捕获类似VirtualMachineError这样的虚拟机错误。

object NonFatal {
   /**
    * Returns true if the provided `Throwable` is to be considered non-fatal, or false if it is to be considered fatal
    */
   def apply(t: Throwable): Boolean = t match {
     // VirtualMachineError includes OutOfMemoryError and other fatal errors
     case _: VirtualMachineError | _: ThreadDeath | _: InterruptedException | _: LinkageError | _: ControlThrowable => false
     case _ => true
   }
  /**
   * Returns Some(t) if NonFatal(t) == true, otherwise None
   */
  def unapply(t: Throwable): Option[Throwable] = if (apply(t)) Some(t) else None
}

在使用NonFatal捕获异常时定义的偏函数case NonFatal(e) => ???,这类似于模式匹配的构造器模式,会调用NonFatalunapply方法,在unapply中会对异常进行判断,即调用apply方法,如果不属于虚拟机错误则进行捕获,否则将不进行捕获。这是一种捕获异常的快捷方式。

通常还有一些为了逻辑处理而主动抛出的异常需要处理,比如assertrequireassume

Option

在处理异常时,一般讲结果置为一个Option,成功时返回Some(t),失败时返回None

Either

Either是一个封闭抽象类,表示两种可能的类型,它只有两个终极子类,LeftRright,因此Either的实例要么是一个Left要么是一个Right

类似于Option,通常也可以用于异常的处理,Option只能表示有结果或没有结果,Either则可以表示有结果时结果是什么,没有结果时“结果”又是什么,比如失败时的结果是一个异常信息等等。

sealed abstract class Either[+A, +B]
final case class Left[+A, +B](a: A) extends Either[A, B]
final case class Right[+A, +B](b: B) extends Either[A, B]

通常,Left表示失败,Right表示成功。伴生对象中提供了多种转换方法,比如toOption:

// 如果是 Left 实例
def toOption = e match {
  case Left(a) => Some(a)
  case Right(_) => None
}
// 如果是 Right 实例
def toOption = e match {
  case Left(_) => None
  case Right(b) => Some(b)
}

一个使用的实例:

case class FailResult(reason:String)

def parse(input:String) : Either[FailResult, String] = {
  val r = new StringTokenizer(input)
  if (r.countTokens() == 1) {
    Right(r.nextToken())
  } else {
    Left(FailResult("Could not parse string: " + input))
  }
}

这时如果只想要处理成功结果:

val rightFoo = for (outputFoo <- parse(input).right) yield outputFoo

或者使用fold

parse(input).fold(
  error => errorHandler(error),
  success => { ... }
)

或者模式匹配:

parse(input) match {
  case Left(le) => ???
  case Riggt(ri) => ???
}

并不限制于用在解析或验证,也可以用在业务场景:

case class UserFault
case class UserCreatedEvent

def createUser(user:User) : Either[UserFault, UserCreatedEvent]

或者二选一的时候:

def whatShape(shape:Shape) : Either[Square, Circle]

或者与Option进行嵌套,返回一个异常,或者成功时包含有值或无值两种情况:

def lookup() : Either[FooException,Option[Foo]]

这种方式比较冗余,可以直接返回一个异常或结果:

def modify(inputFoo:Foo) : Either[FooException,Foo]

**不要在 Either 中返回异常,而是创建一个 case 类来表示异常的结果。**比如:

Either[FailResult,Foo]

Try

TryEither类似,但它不像Either将一些结果类包装在LeftRight中,它会直接返回一个Failure[Throwable]Succese[T]。它是try/catch的一种简写方式,内部仍然是对NonFatal的处理。

它实现了flatMap方法,因此可以使用下面的方式,任何一个Try失败都会返回Failure

val sumTry = for {
  int1 <- Try(Integer.parseInt("1"))
  int2 <- Try(Integer.parseInt("2"))
} yield {
  int1 + int2
}

或者通过模式匹配的方式对Try的结果进行处理:

sumTry match {
  case Failure(thrown) => Console.println("Failure: " + thrown)
  case Success(s) => Console.println(s)
}

或者获取失败时的异常值:

if (sumTry.isFailure) {
  val thrown = sumTry.failed.get
}

如果是成功的结果,get方法会返回对应的值。

可以使用recover方法处理多个Try链接中任意位置的异常:

val sum = for {
  int1 <- Try(Integer.parseInt("one"))
  int2 <- Try(Integer.parseInt("two"))
} yield {
  int1 + int2
} recover {
  case e => 0
}
// or
val sum = for {
  int1 <- Try(Integer.parseInt("one")).recover { case e => 0 }
  int2 <- Try(Integer.parseInt("two"))
} yield {
  int1 + int2
}

使用toOption方法将Try[T]转换为一个Option[T]

或者与Either混合使用:

val either : Either[String, Int] = Try(Integer.parseInt("1")).transform(
  { i => Success(Right(i)) }, { e => Success(Left("FAIL")) }
).get
Console.println("either is " + either.fold(l => l, r => r))

将方法的返回值声明为 Try 可以告诉调用者该方法可能会抛出异常,可以达到受检异常的效果,即调用者必须要处理对应的异常,因此可以使代码更安全。虽然使用常规的 try/catch 也可以做到,但是这样更清晰。

与 Future 组合使用

Try 的存在意义就是为了用于 Future,参考 Future 对应的整理记录。

使用Future包装阻塞的Try代码块:

def blockMethod(x: Int): Try[Int] = Try {
    // Some long operation to get an Int from network or IO
    Thread.sleep(10000)
    100
  }

def tryToFuture[A](t: => Try[A]): Future[A] = {
    future {
      t
    }.flatMap {
      case Success(s) => Future.successful(s)
      case Failure(fail) => Future.failed(fail)
    }
  }

// Initiate long operation
val f = tryToFuture(blockMethod(1))

或者如果经常需要将FutureTry进行链接:

object FutureTryHelpers{
  implicit def tryToFuture[T](t:Try[T]):Future[T] = {
    t match{
      case Success(s) => Future.successful(s)
      case Failure(ex) => Future.failed(ex)
    }
  }
}

def someFuture:Future[String] = ???
def processResult(value:String):Try[String] = ???

import FutureTryHelpers._
val result = for{
  a <- someFuture
  b <- processResult(a)
} yield b
result.map { /* Success Block */ } recover { /* Failure Block */ }

或者使用PromsefromTry方法来构建Future

implicit def tryToFuture[T](t:Try[T]):Future[T] = Promise.fromTry(t).future

用法总结

  • 在纯函数代码中将异常抛出到单独的非预期错误
  • 使用Option返回可选的值
  • 使用Either返回预期的错误
  • 返回异常时使用Try而不是Either
  • 捕获非预期错误时使用Try而不是trt/catch
  • 在处理Future时使用Try
  • 在公共接口暴露Try类似于受检异常,直接使用异常替代

自定义异常

case class PGDBException(message:Option[String] = None, cause:Option[Throwable] = None) extends RuntimeException(PGDBException.defaultMessage(message, cause))

object PGDBException{
    def defaultMessage(message:Option[String], cause:Option[Throwable]) = {
      (message, cause) match {
        case (Some(msg), _) => msg
        case (_, Some(thr)) => thr.toString
        case _ => null
      }
    }

    def apply(message:String) = new PGDBException(Some(message), None)

    def apply(throwable: Throwable) = new PGDBException(None,Some(throwable))
  }

// usage
throw PGDBException("Already exist.")
throw PGDBException(new Throwable("this is a throwable"))