基本工具(guava)

1.1-使用和避免null

轻率地使用null可能会导致很多令人惊愕的问题。通过学习Google底层代码库,我们发现95%的集合类不接受null值作为元素。我们认为, 相比默默地接受null,使用快速失败操作拒绝null值对开发者更有帮助。

此外,Null的含糊语义让人很不舒服。Null很少可以明确地表示某种语义,例如,Map.get(key)返回Null时,可能表示map中的值是null,亦或map中没有key对应的值。Null可以表示失败、成功或几乎任何情况。使用Null以外的特定值,会让你的逻辑描述变得更清晰。

Null确实也有合适和正确的使用场景,如在性能和速度方面Null是廉价的,而且在对象数组中,出现Null也是无法避免的。但相对于底层库来说,在应用级别的代码中,Null往往是导致混乱,疑难问题和模糊语义的元凶,就如同我们举过的Map.get(key)的例子。最关键的是,Null本身没有定义它表达的意思。

鉴于这些原因,很多Guava工具类对Null值都采用快速失败操作,除非工具类本身提供了针对Null值的因变措施。此外,Guava还提供了很多工具类,让你更方便地用特定值替换Null值。

具体案例

不要在Set中使用null,或者把null作为map的键值。使用特殊值代表null会让查找操作的语义更清晰。

如果你想把null作为map中某条目的值,更好的办法是 不把这一条目放到map中,而是单独维护一个”值为null的键集合” (null keys)。Map 中对应某个键的值是null,和map中没有对应某个键的值,是非常容易混淆的两种情况。因此,最好把值为null的键分离开,并且仔细想想,null值的键在你的项目中到底表达了什么语义。

如果你需要在列表中使用null——并且这个列表的数据是稀疏的,使用Map<Integer, E>可能会更高效,并且更准确地符合你的潜在需求。

此外,考虑一下使用自然的null对象——特殊值。举例来说,为某个enum类型增加特殊的枚举值表示null,比如java.math.RoundingMode就定义了一个枚举值UNNECESSARY,它表示一种不做任何舍入操作的模式,用这种模式做舍入操作会直接抛出异常。

如果你真的需要使用null值,但是null值不能和Guava中的集合实现一起工作,你只能选择其他实现。比如,用JDK中的Collections.unmodifiableList替代Guava的ImmutableList

Optional

大多数情况下,开发人员使用null表明的是某种缺失情形:可能是已经有一个默认值,或没有值,或找不到值。例如,Map.get返回null就表示找不到给定键对应的值。

Guava用Optional<T>表示可能为null的T类型引用。一个Optional实例可能包含非null的引用(我们称之为引用存在),也可能什么也不包括(称之为引用缺失)。它从不说包含的是null值,而是用存在或缺失来表示。但Optional从不会包含null值引用。

Optional<Integer> possible = Optional.of(5);
possible.isPresent(); // returns true`
possible.get(); // returns 5` 

Optional无意直接模拟其他编程环境中的”可选” or “可能”语义,但它们的确有相似之处。

Optional最常用的一些操作被罗列如下:

创建Optional实例(以下都是静态方法):

方法 详情
Optional.of(T) 创建指定引用的Optional实例,若引用为null则快速失败
Optional.absent() 创建引用缺失的Optional实例
Optional.fromNullable(T) 创建指定引用的Optional实例,若引用为null则表示缺失

用Optional实例查询引用(以下都是非静态方法):

方法 详情
boolean isPresent() 如果Optional包含非null的引用(引用存在),返回true
T get() 返回Optional所包含的引用,若引用缺失,则抛出java.lang.IllegalStateException
T or(T) 返回Optional所包含的引用,若引用缺失,返回指定的值
T orNull() 返回Optional所包含的引用,若引用缺失,返回null
Set<T> asSet() 返回Optional所包含引用的单例不可变集,如果引用存在,返回一个只有单一元素的集合,如果引用缺失,返回一个空集合。

使用****Optional****的意义在哪儿?

使用Optional除了赋予null语义,增加了可读性,最大的优点在于它是一种傻瓜式的防护。Optional迫使你积极思考引用缺失的情况,因为你必须显式地从Optional获取引用。直接使用null很容易让人忘掉某些情形,尽管FindBugs可以帮助查找null相关的问题,但是我们还是认为它并不能准确地定位问题根源。

如同输入参数,方法的返回值也可能是null。和其他人一样,你绝对很可能会忘记别人写的方法method(a,b)会返回一个null,就好像当你实现method(a,b)时,也很可能忘记输入参数a可以为null。将方法的返回类型指定为Optional,也可以迫使调用者思考返回的引用缺失的情形。

其他处理null的便利方法

当你需要用一个默认值来替换可能的null,请使用Objects.firstNonNull(T, T) 方法。如果两个值都是null,该方法会抛出NullPointerException。Optional也是一个比较好的替代方案,例如:Optional.of(first).or(second).

还有其它一些方法专门处理null或空字符串:emptyToNull(String)nullToEmpty(String)<tt>,</tt>isNullOrEmpty(String)。我们想要强调的是,这些方法主要用来与混淆null/空的API进行交互。当每次你写下混淆null/空的代码时,Guava团队都泪流满面。(好的做法是积极地把null和空区分开,以表示不同的含义,在代码中把null和空同等对待是一种令人不安的坏味道。

1.2-前置条件

前置条件:让方法调用的前置条件判断更简单。

Guava在Preconditions类中提供了若干前置条件判断的实用方法,我们强烈建议在Eclipse中静态导入这些方法。每个方法都有三个变种:

  • 没有额外参数:抛出的异常中没有错误消息;
  • 有一个Object对象作为额外参数:抛出的异常使用Object.toString() 作为错误消息;
  • 有一个String对象作为额外参数,并且有一组任意数量的附加Object对象:这个变种处理异常消息的方式有点类似printf,但考虑GWT的兼容性和效率,只支持%s指示符。例如:
checkArgument(i >= 0, "Argument was %s but expected nonnegative", i);
checkArgument(i < j, "Expected i < j, but %s > %s", i, j);
方法声明(不包括额外参数) 描述 检查失败时抛出的异常
checkArgument(boolean) 检查boolean是否为true,用来检查传递给方法的参数。 IllegalArgumentException
checkNotNull(T) 检查value是否为null,该方法直接返回value,因此可以内嵌使用checkNotNull<tt>。</tt> NullPointerException
checkState(boolean) 用来检查对象的某些状态。 IllegalStateException
checkElementIndex(int index, int size) 检查index作为索引值对某个列表、字符串或数组是否有效。index>=0 && index<size * IndexOutOfBoundsException
checkPositionIndex(int index, int size) 检查index作为位置值对某个列表、字符串或数组是否有效。index>=0 && index<=size * IndexOutOfBoundsException
checkPositionIndexes(int start, int end, int size) 检查[start, end]表示的位置范围对某个列表、字符串或数组是否有效* IndexOutOfBoundsException

译者注:

****索引值常用来查找列表、字符串或数组中的元素,如*List.get(int), String.charAt(int)

*位置值和位置范围常用来截取列表、字符串或数组,如List.subList(int,int), String.substring(int)

相比Apache Commons提供的类似方法,我们把Guava中的Preconditions作为首选。Piotr Jagielski在他的博客中简要地列举了一些理由:

  • 在静态导入后,Guava方法非常清楚明晰。checkNotNull清楚地描述做了什么,会抛出什么异常;
  • checkNotNull直接返回检查的参数,让你可以在构造函数中保持字段的单行赋值风格:this.field = checkNotNull(field)
  • 简单的、参数可变的printf风格异常信息。鉴于这个优点,在JDK7已经引入Objects.requireNonNull的情况下,我们仍然建议你使用checkNotNull。

在编码时,如果某个值有多重的前置条件,我们建议你把它们放到不同的行,这样有助于在调试时定位。此外,把每个前置条件放到不同的行,也可以帮助你编写清晰和有用的错误消息。

1.3-常见Object方法

equals

当一个对象中的字段可以为null时,实现Object.equals方法会很痛苦,因为不得不分别对它们进行null检查。使用<tt>Objects.equal</tt>帮助你执行null敏感的equals判断,从而避免抛出NullPointerException。例如:

Objects.equal("a", "a"); // returns true

Objects.equal(null, "a"); // returns false

Objects.equal("a", null); // returns false

Objects.equal(null, null); // returns true

注意:JDK7引入的Objects类提供了一样的方法<tt>Objects.equals</tt>

hashCode

用对象的所有字段作散列[hash]运算应当更简单。Guava的<tt>Objects.hashCode(Object...)</tt>会对传入的字段序列计算出合理的、顺序敏感的散列值。你可以使用Objects.hashCode(field1, field2, …, fieldn)来代替手动计算散列值。

注意:JDK7引入的Objects类提供了一样的方法<tt>Objects.hash(Object...)</tt>

toString

好的toString方法在调试时是无价之宝,但是编写toString方法有时候却很痛苦。使用 Objects.toStringHelper可以轻松编写有用的toString方法。例如:

   // Returns "ClassName{x=1}" 

   Objects.toStringHelper(this).add("x", 1).toString(); 

   // Returns "MyObject{x=1}" 

   Objects.toStringHelper("MyObject").add("x", 1).toString(); 

compare/compareTo

实现一个比较器[Comparator],或者直接实现Comparable接口有时也伤不起。考虑一下这种情况:

   class Person implements Comparable<Person> { 

   private String lastName; 

   private String firstName; 

   private int zipCode; 

     

   public int compareTo(Person other) { 

   int cmp = lastName.compareTo(other.lastName); 

   if (cmp != 0) { 

   return cmp; 

   } 

   cmp = firstName.compareTo(other.firstName); 

   if (cmp != 0) { 

   return cmp; 

   } 

   return Integer.compare(zipCode, other.zipCode); 

   } 

   } 

这部分代码太琐碎了,因此很容易搞乱,也很难调试。我们应该能把这种代码变得更优雅,为此,Guava提供了<tt>ComparisonChain</tt>

ComparisonChain执行一种懒比较:它执行比较操作直至发现非零的结果,在那之后的比较输入将被忽略。

   public int compareTo(Foo that) { 

   return ComparisonChain.start() 

   .compare(this.aString, that.aString) 

   .compare(this.anInt, that.anInt) 

   .compare(this.anEnum, that.anEnum, Ordering.natural().nullsLast()) 

   .result(); 

   } 

这种Fluent接口风格的可读性更高,发生错误编码的几率更小,并且能避免做不必要的工作。更多Guava排序器工具可以在下一节里找到。

排序: Guava强大的”流畅风格比较器”

排序器[Ordering]是Guava流畅风格比较器[Comparator]的实现,它可以用来为构建复杂的比较器,以完成集合排序的功能。

从实现上说,Ordering实例就是一个特殊的Comparator实例。Ordering把很多基于Comparator的静态方法(如Collections.max)包装为自己的实例方法(非静态方法),并且提供了链式调用方法,来定制和增强现有的比较器。

创建排序器:常见的排序器可以由下面的静态方法创建

方法 描述
natural() 对可排序类型做自然排序,如数字按大小,日期按先后排序
usingToString() 按对象的字符串形式做字典排序[lexicographical ordering]
from(Comparator) 把给定的Comparator转化为排序器

实现自定义的排序器时,除了用上面的from方法,也可以跳过实现Comparator,而直接继承Ordering:

   Ordering<String> byLengthOrdering = new Ordering<String>() { 

   public int compare(String left, String right) { 

   return Ints.compare(left.length(), right.length()); 

   } 

   };

链式调用方法:通过链式调用,可以由给定的排序器衍生出其它排序器

方法 描述
reverse() 获取语义相反的排序器
nullsFirst() 使用当前排序器,但额外把null值排到最前面。
nullsLast() 使用当前排序器,但额外把null值排到最后面。
compound(Comparator) 合成另一个比较器,以处理当前排序器中的相等情况。
lexicographical() 基于处理类型T的排序器,返回该类型的可迭代对象Iterable<T>的排序器。
onResultOf(Function) 对集合中元素调用Function,再按返回值用当前排序器排序。

例如,你需要下面这个类的排序器。

   class Foo { 

   @Nullable String sortedBy; 

   int notSortedBy; 

   } 

考虑到排序器应该能处理sortedBy为null的情况,我们可以使用下面的链式调用来合成排序器:

   Ordering<Foo> ordering = Ordering.natural().nullsFirst().onResultOf(new Function<Foo, String>() { 

   public String apply(Foo foo) { 

   return foo.sortedBy; 

   } 

   }); 

当阅读链式调用产生的排序器时,应该从后往前读。上面的例子中,排序器首先调用apply方法获取sortedBy值,并把sortedBy为null的元素都放到最前面,然后把剩下的元素按sortedBy进行自然排序。之所以要从后往前读,是因为每次链式调用都是用后面的方法包装了前面的排序器。

注:用compound方法包装排序器时,就不应遵循从后往前读的原则。为了避免理解上的混乱,请不要把compound写在一长串链式调用的中间,你可以另起一行,在链中最先或最后调用compound。

超过一定长度的链式调用,也可能会带来阅读和理解上的难度。我们建议按下面的代码这样,在一个链中最多使用三个方法。此外,你也可以把Function分离成中间对象,让链式调用更简洁紧凑。

Ordering<Foo> ordering = Ordering.natural().nullsFirst().onResultOf(sortKeyFunction)

运用排序器:Guava的排序器实现有若干操纵集合或元素值的方法

方法 描述 另请参见
greatestOf(Iterable iterable, int k) 获取可迭代对象中最大的k个元素。 leastOf
isOrdered(Iterable) 判断可迭代对象是否已按排序器排序:允许有排序值相等的元素。 isStrictlyOrdered
sortedCopy(Iterable) 判断可迭代对象是否已严格按排序器排序:不允许排序值相等的元素。 immutableSortedCopy
min(E, E) 返回两个参数中最小的那个。如果相等,则返回第一个参数。 max(E, E)
min(E, E, E, E...) 返回多个参数中最小的那个。如果有超过一个参数都最小,则返回第一个最小的参数。 max(E, E, E, E...)
min(Iterable) 返回迭代器中最小的元素。如果可迭代对象中没有元素,则抛出NoSuchElementException。 max(Iterable), min(Iterator), max(Iterator)

1.5-Throwables:简化异常和错误的传播与检查

有时候,你会想把捕获到的异常再次抛出。这种情况通常发生在Error或RuntimeException被捕获的时候,你没想捕获它们,但是声明捕获Throwable和Exception的时候,也包括了了Error或RuntimeException。Guava提供了若干方法,来判断异常类型并且重新传播异常。例如:

   try { 

   someMethodThatCouldThrowAnything(); 

   } catch (IKnowWhatToDoWithThisException e) { 

   handle(e); 

   } catch (Throwable t) { 

   Throwables.propagateIfInstanceOf(t, IOException.class); 

   Throwables.propagateIfInstanceOf(t, SQLException.class); 

   throw Throwables.propagate(t); 

   } 

所有这些方法都会自己决定是否要抛出异常,但也能直接抛出方法返回的结果——例如,throw Throwables.propagate(t);—— 这样可以向编译器声明这里一定会抛出异常。

Guava中的异常传播方法简要列举如下:

方法 描述
RuntimeException propagate(Throwable) 如果Throwable是Error或RuntimeException,直接抛出;否则把Throwable包装成RuntimeException抛出。返回类型是RuntimeException,所以你可以像上面说的那样写成<tt>throw Throwables.propagate(t)</tt>,Java编译器会意识到这行代码保证抛出异常。
void propagateIfInstanceOf( Throwable, Class<X extends Exception>) throws X Throwable类型为X才抛出
void propagateIfPossible( Throwable) Throwable类型为Error或RuntimeException才抛出
void propagateIfPossible( Throwable, Class<X extends Throwable>) throws X Throwable类型为X, Error或RuntimeException才抛出

Throwables.propagate的用法

模仿Java7的多重异常捕获和再抛出

通常来说,如果调用者想让异常传播到栈顶,他不需要写任何catch代码块。因为他不打算从异常中恢复,他可能就不应该记录异常,或者有其他的动作。他可能是想做一些清理工作,但通常来说,无论操作是否成功,清理工作都要进行,所以清理工作可能会放在finallly代码块中。但有时候,捕获异常然后再抛出也是有用的:也许调用者想要在异常传播之前统计失败的次数,或者有条件地传播异常。

当只对一种异常进行捕获和再抛出时,代码可能还是简单明了的。但当多种异常需要处理时,却可能变得一团糟:

   @Override public void run() { 

   try { 

   delegate.run(); 

   } catch (RuntimeException e) { 

   failures.increment(); 

   throw e; 

   }catch (Error e) { 

   failures.increment(); 

   throw e; 

   } 

   } 

Java7用多重捕获解决了这个问题:

   } catch (RuntimeException  Error e) { 

   failures.increment(); 

   throw e; 

   } 

非Java7用户却受困于这个问题。他们想要写如下代码来统计所有异常,但是编译器不允许他们抛出Throwable(译者注:这种写法把原本是Error或RuntimeException类型的异常修改成了Throwable,因此调用者不得不修改方法签名):

  } catch (Throwable t) { 

  failures.increment(); 

  throw t; 

  } 

解决办法是用throw Throwables.propagate(t)替换throw t。在限定情况下(捕获Error和RuntimeException),Throwables.propagate和原始代码有相同行为。然而,用Throwables.propagate也很容易写出有其他隐藏行为的代码。尤其要注意的是,这个方案只适用于处理RuntimeException 或Error。如果catch块捕获了受检异常,你需要调用propagateIfInstanceOf来保留原始代码的行为,因为Throwables.propagate不能直接传播受检异常。

总之,Throwables.propagate的这种用法也就马马虎虎,在Java7中就没必要这样做了。在其他Java版本中,它可以减少少量的代码重复,但简单地提取方法进行重构也能做到这一点。此外,使用propagate会意外地包装受检异常。

非必要用法:把抛出的Throwable转为Exception

有少数API,尤其是Java反射API和(以此为基础的)Junit,把方法声明成抛出Throwable。和这样的API交互太痛苦了,因为即使是最通用的API通常也只是声明抛出Exception。当确定代码会抛出Throwable,而不是Exception或Error时,调用者可能会用Throwables.propagate转化Throwable。这里有个用Callable执行Junit测试的范例:

  public Void call() throws Exception { 

  try { 

  FooTest.super.runTest(); 

  } catch (Throwable t) { 

  Throwables.propagateIfPossible(t, Exception.class); 

  Throwables.propagate(t); 

  } 

    

  return null; 

  } 

在这儿没必要调用propagate()方法,因为propagateIfPossible传播了Throwable之外的所有异常类型,第二行的propagate就变得完全等价于throw new RuntimeException(t)。(题外话:这个例子也提醒我们,propagateIfPossible可能也会引起混乱,因为它不但会传播参数中给定的异常类型,还抛出Error和RuntimeException)

这种模式(或类似于throw new RuntimeException(t)的模式)在Google代码库中出现了超过30次。(搜索’propagateIfPossible[^;]* Exception.class[)];’)绝大多数情况下都明确用了”throw new RuntimeException(t)”。我们也曾想过有个”throwWrappingWeirdThrowable”方法处理Throwable到Exception的转化。但考虑到我们用两行代码实现了这个模式,除非我们也丢弃propagateIfPossible方法,不然定义这个throwWrappingWeirdThrowable方法也并没有太大必要。

Throwables.propagate的有争议用法

争议一:把受检异常转化为非受检异常

原则上,非受检异常代表bug,而受检异常表示不可控的问题。但在实际运用中,即使JDK也有所误用——如Object.clone()、Integer. parseInt(String)、URI(String)——或者至少对某些方法来说,没有让每个人都信服的答案,如URI.create(String)的异常声明。

因此,调用者有时不得不把受检异常和非受检异常做相互转化:

  try { 

  return Integer.parseInt(userInput); 

  } catch (NumberFormatException e) { 

  throw new InvalidInputException(e); 

  } 

  try { 

  return publicInterfaceMethod.invoke(); 

  } catch (IllegalAccessException e) { 

  throw new AssertionError(e); 

  } 

有时候,调用者会使用Throwables.propagate转化异常。这样做有没有什么缺点?最主要的恐怕是代码的含义不太明显。throw Throwables.propagate(ioException)做了什么?throw new RuntimeException(ioException)做了什么?这两者做了同样的事情,但后者的意思更简单直接。前者却引起了疑问:”它做了什么?它并不只是把异常包装进RuntimeException吧?如果它真的只做了包装,为什么还非得要写个方法?”。应该承认,这些问题部分是因为”propagate”的语义太模糊了(用来抛出未声明的异常吗?)。也许”wrapIfChecked”更能清楚地表达含义。但即使方法叫做”wrapIfChecked”,用它来包装一个已知类型的受检异常也没什么优点。甚至会有其他缺点:也许比起RuntimeException,还有更合适的类型——如IllegalArgumentException。
我们有时也会看到propagate被用于传播可能为受检的异常,结果是代码相比以前会稍微简短点,但也稍微有点不清晰:

  } catch (RuntimeException e) { 

  throw e; 

  }catch (Exception e) { 

  throw new RuntimeException(e); 

  } 

  } catch (Exception e) { 

  throw Throwables.propagate(e); 

  } 

然而,我们似乎故意忽略了把检查型异常转化为非检查型异常的合理性。在某些场景中,这无疑是正确的做法,但更多时候它被用于避免处理受检异常。这让我们的话题变成了争论受检异常是不是坏主意了,我不想对此多做叙述。但可以这样说,Throwables.propagate不是为了鼓励开发者忽略IOException这样的异常。

争议二:异常穿隧

但是,如果你要实现不允许抛出异常的方法呢?有时候你需要把异常包装在非受检异常内。这种做法挺好,但我们再次强调,没必要用propagate方法做这种简单的包装。实际上,手动包装可能更好:如果你手动包装了所有异常(而不仅仅是受检异常),那你就可以在另一端解包所有异常,并处理极少数特殊场景。此外,你可能还想把异常包装成特定的类型,而不是像propagate这样统一包装成RuntimeException。

争议三:重新抛出其他线程产生的异常

 try { 

 return future.get(); 

 } catch (ExecutionException e) { 

 throw Throwables.propagate(e.getCause()); 

 } 

对这样的代码要考虑很多方面:

  • ExecutionException的cause可能是受检异常,见上文”争议一:把检查型异常转化为非检查型异常”。但如果我们确定future对应的任务不会抛出受检异常呢?(可能future表示runnable任务的结果——译者注:如ExecutorService中的submit(Runnable task, T
    result)方法
    )如上所述,你可以捕获异常并抛出AssertionError。尤其对于Future,请考虑 Futures.get方法。(TODO:对future.get()抛出的另一个异常InterruptedException作一些说明)
  • ExecutionException的cause可能直接是Throwable类型,而不是Exception或Error。(实际上这不大可能,但你想直接重新抛出cause的话,编译器会强迫你考虑这种可能性)见上文”用法二:把抛出Throwable改为抛出Exception”。
  • ExecutionException的cause可能是非受检异常。如果是这样的话,cause会直接被Throwables.propagate抛出。不幸的是,cause的堆栈信息反映的是异常最初产生的线程,而不是传播异常的线程。通常来说,最好在异常链中同时包含这两个线程的堆栈信息,就像ExecutionException所做的那样。(这个问题并不单单和propagate方法相关;所有在其他线程中重新抛出异常的代码都需要考虑这点)

异常原因链

Guava提供了如下三个有用的方法,让研究异常的原因链变得稍微简便了,这三个方法的签名是不言自明的:

方法
Throwable getRootCause(Throwable)
List<Throwable> getCausalChain(Throwable)
String getStackTraceAsString(Throwable)

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: [Google Guava] 1.5-Throwables:简化异常和错误的传播与检查

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

推荐阅读更多精彩内容