面试官:说下对 Java 中异常的理解(详解 Java 异常机制)

引言

Java 中的异常与异常处理机制也是面试中常见的考察点。面试官不仅关注求职者对 Java 异常体系结构的理解,如区分 Exception 以及 Error 的能力,更希望通过了解你对异常处理的方式,来评估你的编程功底和实际项目中的处理经验。

所以,我们今天来看一下,Java 中的异常体系结构以及面试经常被问到的相关知识。

Java 中异常体系的层次结构

Java 的异常体系基于 Throwable 类构建,它有两个主要的子类:ErrorException。这两个类分别代表了不同类型的异常情况。请看图:

Java 异常类层次结构

Throwable 类中的常用 API

  • String getMessage():返回异常发生时的简要描述
  • Throwable getCause():返回此异常原因,即导致此异常发生的一个 Throwable 对象。
  • String toString():返回异常发生时的详细信息,通常包括异常类的名称和详细消息。
  • void printStackTrace():使用标准错误流打印 Throwable 对象封装的异常信息,包括异常的类型、详细消息和调用堆栈。

Exception 与 Error

ExceptionError 是 Java 中异常和错误的两个顶层父类。

Exception 和 Error 的主要区别

它们两个的主要区别,我们从以下几个方面进行分析:

  • 严重程度
    • Exception:表示程序可以捕获并能够处理的异常情况。这类异常通常是可预见的,可以通过合理的异常处理机制来应对。例如,文件找不到 (FileNotFoundException) 或者网络连接失败 (SocketException)。
    • Error:表示应用程序无法处理的故障,出现此类异常 JVM 一般都会停止执行,如 OutOfMemoryErrorStackOverflowError。这些问题通常不在应用程序的控制范围内,因此不建议被应用程序捕获或处理。
  • 处理方式
    • Exception:我们应当对 Exception 进行适当的处理,以确保应用程序能够继续正常运行。编码时可以通过 try-catch 块来捕获异常,或者通过 throws 关键字声明方法抛出异常,让调用方自行处理。
    • Error:一般情况下,在程序中不建议尝试捕获 Error,因为 Error 代表问题的严重级别很高,通常意味着应用程序已经处于不健康的状态。当系统出现 Error 时,可能需要配合我们的监控系统,采取快速响应措施,减少对业务的影响。
  • 适用场景
    • Exception:适用于开发过程中可能出现的异常情况,特别是那些可以通过合理设计和编码避免的情况。比如请求输入错误、资源不可用、文件找不到等。
    • Error:适用于超出应用控制范围的严重问题,如硬件故障、JVM 内部错误等。这些问题是编码时难以预见且难以修复的。

什么是 Checked Exception 与 Unchecked Exception

Java 中的 Exception 分为两大类:Checked Exception(检查异常) 和 Unchecked Exception(非检查异常)。

  • Checked Exception:Checked Exception 是指那些必须在编译时被显式处理的异常,如果不处理这类异常,IDE 中的编译器一般会给出错误提示。如果一个方法可能会抛出 Checked Exception,那么该方法要么通过 throws 声明抛出异常,要么在其内部使用 try-catch 捕获异常。

[图片上传失败...(image-d9608b-1736213978262)]

  • Unchecked Exception:Unchecked Exception 是指那些不需要在编译时显式处理的异常。RuntimeException 及其子类都是 Unchecked Exception。这类通常由编码错误引起,如空指针异常 (NullPointerException) 或数组越界访问异常 (ArrayIndexOutOfBoundsException)。编译器虽然不要求显示处理这些异常,但优秀的编码应该尽量避免抛出此类异常。

阿里开发手册中这样要求:

强制】Java 类库中定义的一类 RuntimeException 可以通过预先检查进行规避,而不应该通过 catch 来处理,比如:IndexOutOfBoundsExceptionNullPointerException 等等。

说明:无法通过预检查的异常除外,如在解析一个外部传来的字符串形式数字时,通过 catch NumberFormatException 来实现。

正例if (obj != null) {...}

反例try { obj.method() } catch (NullPointerException e) {...}

常见的 Checked Exception 与 Unchecked Exception

这里整理下常见的这两类异常,面试时有可能会被问到开发中常见的有哪些。

常见的 Checked Exception

  • IOException 相关的:与输入输出操作相关的异常。例如,读写文件或网络连接失败时抛出。包括子类如 FileNotFoundExceptionEOFException
  • SQLException:在执行数据库操作时可能发生的异常。
  • ClassNotFoundException:当应用程序尝试加载某个类而在类路径中找不到对应的类文件时抛出。
  • ……

常见的 Unchecked Exception

  • NullPointerException:空指针异常,当访问对象方法或者字段而对象为 null 时抛出。
  • ArrayIndexOutOfBoundsException:数组越界访问异常,访问数组中不存在的索引时抛出。
  • ClassCastException:类型转换异常,当将一个对象强制转换为不是其实际类型的类时抛出。
  • IllegalArgumentException:非法参数异常,当接收到非法或不适合的参数时抛出,可在编码中手动抛出该异常提示参数问题。
  • NumberFormatException:数值格式化异常,比如使用 Long.parseLong 解析一个字符串数值"3.14"时会抛出该异常。
  • ArithmeticException:算术异常,比如在执行 1/0 时抛出 by zero 除零算术异常。
  • ConcurrentModificationException:在迭代集合的同时修改它(没有通过迭代器自身的移除方法),或者检测到并发修改时抛出。
  • ……

NoClassDefFoundError 和 ClassNotFoundException 有什么区别

首先,这两者从名字上就可以看出本质的不同:ClassNotFoundExceptionException,而 NoClassDefFoundErrorError

看下 JDK 源码中的解释(摘抄自 JDK 1.8):

/**
 * Thrown when an application tries to load in a class through its
 * string name using:
 * <ul>
 * <li>The <code>forName</code> method in class <code>Class</code>.
 * <li>The <code>findSystemClass</code> method in class
 *     <code>ClassLoader</code> .
 * <li>The <code>loadClass</code> method in class <code>ClassLoader</code>.
 * </ul>
 * <p>
 * but no definition for the class with the specified name could be found.
 */
public class ClassNotFoundException {}

当应用程序尝试使用以下字符串类名加载类,但未找到指定名称的类的定义时抛出该异常:

  • Class.forName() 方法。
  • ClassLoader.findSystemClass() 方法。
  • ClassLoader.loadClass() 方法。
/**
 * Thrown if the Java Virtual Machine or a <code>ClassLoader</code> instance
 * tries to load in the definition of a class (as part of a normal method call
 * or as part of creating a new instance using the <code>new</code> expression)
 * and no definition of the class could be found.
 * <p>
 * The searched-for class definition existed when the currently
 * executing class was compiled, but the definition can no longer be
 * found.
 */
public class NoClassDefFoundError {}

Java 虚拟机或类加载器在尝试加载某个类的定义时(例如作为方法调用的一部分或使用 new 关键字创建新实例时),无法找到该类的定义,则会抛出 NoClassDefFoundError

在当前执行的代码在编译时能找到并引用了所需的类定义,但在运行时,JVM 却找不到这个类的定义,则会抛出 NoClassDefFoundError

下面,我们结合场景示例加深一下这两种异常的理解:

  • ClassNotFoundException 通常是在类加载阶段,使用 Class.forName() 等方式加载类时,找不到类的字节码文件(.class 文件)导致的。

场景示例:加载一个不存在的类时

public class ClassNotFoundDemo {
    public static void main(String[] args) {
        try {
            // 不存在的类 com.johnny.NonExistentClass
            Class.forName("com.johnny.NonExistentClass");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

java.lang.ClassNotFoundException: com.johnny.NonExistentClass
    at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:359)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at com.tender.ClassNotFoundDemo.main(ClassNotFoundDemo.java:7)
  • NoClassDefFoundError 是在编译时类是存在的,但在运行时 JVM 无法找到该类的定义。

场景示例:编译时存在,运行时不存在

class HelperClass {
}
public class MainClass {
    public static void main(String[] args) {
        HelperClass h = new HelperClass();
    }
}

运行结果:

Exception in thread "main" java.lang.NoClassDefFoundError: com/johnny/HelperClass
    at com.johnny.MainClass.main(MainClass.java:7)
Caused by: java.lang.ClassNotFoundException: com.johnny.HelperClass
    at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:359)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
    ... 1 more

MainClassHelperClass 编译后会生成两个 class 文件 MainClass.classHelperClass.class,现手动将编译后的 HelperClass.class 文件删除,然后执行 MainClassmain 方法,执行结果会抛出 NoClassDefFoundError,因为 JVM 无法找到 HelperClass 的定义,因为它对应的 .class 文件已经被删除。

场景示例:类的静态初始化失败,后续继续引用该类时

class FaultyClass {
    static {
        // 模拟类静态初始化失败的情况
        System.out.println(1/0);
    }

    public static void printMessage() {
        System.out.println("这里输出一个消息。");
    }
}

public class NoClassDefFoundErrorExample {
    static {
        try {
            // 加载 FaultyClass
            Class.forName("com.johnny.FaultyClass");
        } catch (ClassNotFoundException e) {
            // 这里不会触发,因为类确实存在
            e.printStackTrace();
        } catch (ExceptionInInitializerError e) {
            // 类静态初始化失败,会抛出该异常
            System.out.println("静态初始化失败: " + e.getCause().getMessage());
        }
    }

    public static void main(String[] args) {
        try {
            // 这里再次使用 FaultyClass 去调用它的静态方法,此时会抛出 NoClassDefFoundError
            System.out.println("类静态初始化失败后,再次使用 FaultyClass 的静态方法...");
            FaultyClass.printMessage();
        } catch (NoClassDefFoundError e) {
            // 捕获 NoClassDefFoundError
            System.out.println("捕获到 NoClassDefFoundError: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

运行结果:

静态初始化失败: / by zero
类静态初始化失败后,再次使用 FaultyClass 的静态方法...
捕获到 NoClassDefFoundError: Could not initialize class com.johnny.FaultyClass
java.lang.NoClassDefFoundError: Could not initialize class com.johnny.FaultyClass
    at com.johnny.NoClassDefFoundErrorExample.main(NoClassDefFoundErrorExample.java:32)
Caused by: java.lang.ExceptionInInitializerError: Exception java.lang.ArithmeticException: / by zero [in thread "main"]
    at com.johnny.FaultyClass.<clinit>(NoClassDefFoundErrorExample.java:6)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at com.johnny.NoClassDefFoundErrorExample.<clinit>(NoClassDefFoundErrorExample.java:18)

从这个示例中,我们可以看到,当我们尝试使用 Class.forName() 去加载 FaultyClass 时,由于在该类的静态代码块中出现了除零异常,所以会导致类静态初始化失败,从而抛出 ExceptionInInitializerError 错误,我们 catch 了这个异常却没有做任何的处理,因此在后续的逻辑中再次引用该类时,便会抛出 NoClassDefFoundError

总结:

  • ClassNotFoundException 是在加载阶段发生的,当 JVM 无法从外存储器找到并加载指定的类时抛出。
  • NoClassDefFoundError 是在链接阶段发生的,当 JVM 在内存中无法找到已经加载过的类的定义时抛出。

补充一下:
根据《Java 虚拟机规范》,类加载分为三个主要阶段:加载(Loading)、链接(Linking)、初始化(Initialization)。其中链接又细分为验证(Verification)、准备(Preparation)、解析(Resolution)。

Java 中的异常处理机制

在 Java 中,异常处理机制为:抛出异常,捕获异常。

try-catch-finally 语句

try-catch-finally 语句用于捕获异常,是 Java 中处理异常的核心机制,由 trycatchfinally 3 个语句块组合而成。

每个语句块的作用

  • try 块:是 try-catch-finally 语句结构中的必选部分,用于包裹可能出现异常的代码,后边可接零或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch 块:捕获并处理由 try 块中抛出的特定类型的异常,可以有多个 catch 块来处理不同类型的异常,多个 catch 块时,按异常类型逐一匹配,找到与之对应的 catch 块进行处理,如果没有匹配的 catch 块,则异常将传播到调用栈上的更高层。
  • finally 块:是可选的,无论是否发生异常都会执行的代码块,通常用于释放资源(如关闭文件流或数据库连接)。

语法示例

try {
    // 包裹可能会发生异常的程序代码
} catch (ExceptionType1 e1){
    // 捕获并处理 try 块中抛出的异常 e1
} catch (ExceptionType2 e2){
     // 捕获并处理 try 块中抛出的异常 e2
} finally {
    // 无论是否发生异常都会执行的语句块
}

finally 块中的代码一定会执行吗?

理论上,finally 块中的代码是无论是否发生异常都会执行的代码块,但是在一些特殊情况下,finally 块中的代码不会被执行。这些情况包括:

  • JVM 强制终止:比如 JVM 内存不足或其他严重的系统问题导致 JVM 非正常退出。
  • 线程死亡:如果应用程序所在的线程死亡或被中断,finally 块的代码也不会执行。
  • System.exit() 调用:代码中如果在 finally 块前显示调用了 System.exit()finally 块也不会执行。

不要在 finally 块中使用 return

阿里开发手册中要求:

【强制】不能在 finally 块中使用 returnfinally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。

因为在 trycatch 块中有 return 语句时,这个返回值会被暂时保存在本地变量中,当执行到 finally 块中的 return 时,它会覆盖之前本地变量中保存的返回值,并在方法执行结束时将这个返回值返回。

代码示例:

public class ReturnInFinallyExample {
    public static int methodWithReturnInFinally() {
        try {
            return 1; // 这个返回值会被记住,但不会立即返回
        } catch (Exception e) {
            return 2; // 如果异常发生,这个返回值也会被记住
        } finally {
            System.out.println("Finally block executed");
            return 3; // 最终这个返回值会覆盖之前的返回值
        }
    }

    public static void main(String[] args) {
        System.out.println("Returned value: " + methodWithReturnInFinally()); // 输出:Returned value: 3
    }
}

运行结果:

Finally block executed
Returned value: 3

throw 与 throws 关键字

throwthrows 关键字在 Java 中用来抛出异常。

  • throw:用来显式地抛出一个异常对象。当程序检测到错误条件时,可以通过 throw 来创建并抛出一个异常实例,从而中断当前方法或语句块的执行,并将控制权传递给调用栈上的更高层来处理该异常。

语法:

throw new ExceptionType("Exception Message");

ExceptionType 是需要抛出的异常类型(如 IllegalArgumentExceptionRuntimeException 等)。"Exception Message" 是可选的消息字符串,描述异常的具体原因。

代码示例:

public class ThrowExample {
    public static void validateAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative.");
        }
        System.out.println("Valid age: " + age);
    }

    public static void main(String[] args) {
        try {
            validateAge(-5); // 将抛出异常
        } catch (IllegalArgumentException e) {
            System.err.println("Caught exception: " + e.getMessage());
        }
    }
}

在这个例子中,如果传入的年龄为负数,则会抛出 IllegalArgumentException 异常。

  • throws:用于声明一个方法可能抛出的异常列表。它用来告诉编译器和调用者,这个方法内部可能会抛出某些类型的异常,调用者需要准备好处理这些异常。throws 只是声明,而不是实际抛出异常,实际抛出由 throw 实现。

Checked Exception(检查异常)必须在方法声明使用 throws 进行声明,或者在其方法体内捕获;而 Unchecked Exception(非检查异常)不需要。

语法:

public returnType methodName(parameters) throws ExceptionType1, ExceptionType2 {
    // 方法体
}

ExceptionType1ExceptionType2 是声明的方法可能抛出的异常类型。

代码示例:

public class ThrowsExample {
    // 声明该方法可能会抛出 IOException
    public void readFile(String filePath) throws IOException {
        // 模拟文件读取操作,抛出 IOException
        if (!filePath.endsWith(".txt")) {
            throw new IOException("File must be a text file.");
        }
        System.out.println("Reading file: " + filePath);
    }

    public static void main(String[] args) {
        ThrowsExample example = new ThrowsExample();
        try {
            example.readFile("data.csv"); // 可能抛出异常
        } catch (IOException e) {
            System.err.println("Caught exception: " + e.getMessage());
        }
    }
}

在这个例子中,readFile 方法声明了它可能会抛出 IOException。在 main 方法中调用 readFile 时,必须处理这个潜在的异常,否则代码无法通过编译。

throw vs. throws

关键字 throw throws
目的 显式抛出一个异常实例 声明一个方法可能抛出的异常类型
使用位置 在方法体内 在方法声明上
影响 直接导致方法或语句块的执行被中断 不影响方法体内的逻辑,仅作为接口契约的一部分

try-with-resources 语法

try-with-resources 语法是 Java 7 引入的一项重要特性,为了简化资源管理并确保资源的自动关闭。适用于那些实现了 java.lang.AutoCloseable 接口的资源(如文件流、数据库连接等),从而避免了手动关闭这些资源的繁琐操作,并可以避免因忘记关闭资源而导致的内存泄漏的问题。它是一种语法糖,对使用 try-with-resources 包裹的代码进行反编译后,可以看到仍然是 try-catch-finally 结构。

try-with-resources 的基本用法

try (ResourceType resource = new ResourceType()) {
    // 使用资源
} catch (ExceptionType e) {
    // 处理异常
}

ResourceType 需实现 AutoCloseable 接口,资源声明和初始化直接放在 try 括号内,当 try 块执行完毕后,无论是否发生异常,所有声明的资源都将被自动关闭。

为什么需要 try-with-resources

在 Java 7 之前,我们使用像 InputStreamOutputStream 这样的资源时,通常需要遵循以下步骤:

  • 打开资源。
  • 使用资源。
  • finally 块中关闭资源。

比如:

public static void main(String[] args) {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new FileReader("data.txt"));
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        // 处理异常
    } finally {
        if (br != null) {
            try {
                br.close();
            } catch (IOException ex) {
                // 处理 close() 方法抛出的异常
            }
        }
    }
}

这种编码不仅冗长,而且容易出错。如果我们在开启资源后立即抛出异常的话,则可能会跳过 finally 块中的资源关闭逻辑;另外,我们还需要处理 close() 方法本身可能抛出的异常。

try-with-resources 通过引入更简洁的语法来解决这些问题,确保每个资源都会被正确关闭,即使发生异常也不会遗漏。使用 try-with-resources 后,代码简化如下:

public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Caught exception: " + e.getMessage());
        }
    }

同时,try-with-resources 语句可以同时管理多个资源,只需将它们放在 try 括号内并用分号隔开即可,每个资源都会按照声明的逆序依次关闭

什么是逆序关闭,比如程序中先开启了 ResourceA ,又开启了 ResourceB,那么执行关闭的时候,先关闭 ResourceB,再关闭 ResourceA,这就像我们回家开门进屋一样,从大门依次开门进屋,出去的时候要先从里边开始向外锁门。

为什么逆序关闭?

因为开启的资源间可能会有依赖关系,有依赖关系时,通常是先关闭依赖于其他资源的资源。

比如操作数据库时:打开数据库连接 (Connection)-->创建语句对象 (Statement)-->执行查询并获取结果集 (ResultSet)。

在这种情况下,ResultSet 依赖于 Statement,而 Statement 又依赖于 Connection。如果按照打开资源的顺序关闭这些资源时,比如先关闭了 Connection,那么当我们再去关闭 Statement 时,可能已经失去了对 Statement 的访问权限,这很可能导致资源泄露。

try-with-resources 的优势

  • 自动资源管理:不再需要显式地调用 close() 方法,简化了代码结构。
  • 减少错误风险:即使发生异常,资源也会被正确关闭,避免了资源泄露的风险。
  • 提高可读性:代码更加紧凑且易于理解,减少了不必要的样板代码。

结语

好了,写到这里,关于 Java 中异常体系相关的知识基本就介绍完了,希望通过这篇文章能帮助大家更好地理解和运用 Java 中的 ExceptionError。如果你有任何进一步的问题或需要更详细的解释,欢迎评论区留言!

您的鼓励对我持续创作非常关键,如果本文对您有帮助,请记得点赞、分享、在看哦~~~谢谢!


更多精彩内容,请微信搜索并关注【Java驿站】公众号。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容