String、StringBuffer、StringBuilder的区别详解

虽然印象中记得StringBuffer是线程安全,所以性能比StringBuilder慢一丢丢,但是实话说对于它们3个的了解还是很浅,本文我们就深入♂一些,彻底搞明白这三兄贵。

 

首先我们要清楚一个知识:String是不可变的。

1.不可变的String

这是啥意思呢,就是一个String对象,它所存储的具体字符串值,是不可修改的。String本质上也是一个类,它里面有很多属性和方法,而存储的字符串值在它里面也只是一个char数组的属性而已,但是这个属性却被final修饰了,不可更改,所以这个属性只会随着String对象的创建而初始化一次。也就是一个String对象,它存储的字符串是固定死的,直到这个对象被回收也不会更改。

一句话:一旦你通过new或其他手段创建了一个String对象,那么它存储的字符串值就是固定的,不会再改变了。

话虽如此,我们偶尔还是可以看到字符串拼接操作:

        String str = "a";
        str = str + "b";

看起来str对象的值由【a】改变成了【ab】,实际上它已经不是那个“它”了,第一行的str和第二行的str指向的已经不是同一个String对象了。

详细且废话点说,就是第一行时,变量str指向了一个String对象,它的值是“a”。而第二行str+"b"中,新new了一个String对象,并且它的值是"ab",同时str重新指向了这个对象。而原本的值为"a"的对象,还是存在的,只是现在已经没有变量指向它了。

验证

验证方法也很简单,查看第一行和第二行的str指向的内存地址就可以了,由于String已经重写了hashCode()方法,所以我们可以通过System.identityHashCode(object)获取它的原始hashCode,这个hash值就是根据内存地址获取的,如果是同一个对象,自然取出来的值也是一样的。

代码:

package com.lzh.array;


public class Test1 {
    public static void main(String[] args) {
        String str = "a";
        System.out.println("字符串 a 的String对象hash值:"+System.identityHashCode(str));
        str = str + "b";
        System.out.println("字符串 ab 的String对象hash值:"+System.identityHashCode(str));
        String str1 = new String("a");
        System.out.println("虽然是字符串a,但是是new出来的对象,所以hash值为:"+System.identityHashCode(str1));
    }
}


结果是

字符串 a 的String对象hash值:1265094477
字符串 ab 的String对象hash值:2125039532
虽然是字符串a,但是是new出来的对象,所以hash值为:312714112

 

 

2.有String不够吗,为什么要有StringBuffer和StringBuilder?

其实,通过上面的知识,我们就知道为什么需要StringBuffer和StringBuilder了,正是因为String是不可变的

如果我们需要频繁的操作同一个字符串,那必然会创建很多String对象,然后不停的让变量指向新的String对象。但是实际上我们需要用的就只有一个对象,那么就会产生很大的资源浪费,如果你更改了10次字符串,那就会创建10次String对象,效率低不说,浪费的内存空间更多。

如果代码里这样的操作多一些或来几十个循环,估计就麻烦了,一下子就可能创建了成百上千个无用的String对象。

所以java必须有一个可变长的字符串类,这就是StringBuffer和StringBuilder的作用,它们都可以更改自身所存储的字符串值,当需要对字符串频繁操作时,我们就可以用它们代替String对象了。不用担心转换问题,它们存储字符串的方式和String是相同的,都是char数组,只是没有加final修饰,并且也都重写了toString方法。

 

 

3.为什么String要设计成不可变的?

这时可能我们会有一个疑问,为什么最开始要把String设计成不可变的呢?如果它一开始就是可变的,那不就没这么多事了吗?

这里我们就说一下String类是不可变的好处:

①建立字符串常量池

java中,String的使用可以说是最多的,而且很多是作为常量反复使用。像基本类型Integer、Long这些,也都设置了各自的常量池(通常是-128~127),覆盖一些常用的数字范围,目的就是避免创建大量无意义的对象。String作为使用最多的对象,也自然得设置一个常量池。

而String的常量池由于不能预判用户经常会使用哪些字符串,所以不能像Integer一样初始化一个范围。所以String的常量池是这样实现的:在声明一个String时,它会进入常量池中这个字符串,如果没有,就直接new一个String对象,同时将这个对象投入到常量池。那么如果后面又有其他地方用到了这个字符串,就会直接使用第一次new出来的对象。

这就是String常量池的原理,但如果String是可变长的,那就实现不了常量池了。如果常量池中的String可以被任意改动它实际存储的值,那还是常量池吗?所以说个题外话,Integer那些包装类,也是不可变的。

②其他性能问题

其实①就是为了提升使用性能而创建的常量池,但是还有一些其他方面的性能问题,例如HashMap等容器,它们的Key大多是String,当然HashMap已经利用hashcode进行性能上的优化了,但是如果对象的hashcode不能保持稳定不变,也会造成很大的性能浪费。

如果String是可变的,那么每次你修改String对象,它的hashcode都不得不重新计算一次,反复计算新的hashcode就已经够麻烦了,更麻烦的是如果你把已经加入到Map里的一个数据的key改重复了,那同一个Map就有两个key相同的数据了,为了避免这点又不知道要做多少设计和限制。

③安全问题

一旦容易发生变化,就很容易引起各种各样的问题。

如果String随随便便就可以把它的值改了,那涉及到线程的地方肯定又是个大麻烦,要做到线程安全,又是一大笔性能开销(怎么又是性能,看来性能真的很重要)。

不仅是线程安全,其他地方例如在写代码的时候,不小心将String的value操作变化了,但是却没发现,也是一种风险。

 

可以说官方只是选择了最快和最安全的方式表达字符串,并且将这种方式锁定设为了默认选择。但如果我们想用可变的字符串,官方也为我们留了一扇门:StringBufferStringBuilder

 

4.StringBuffer和StringBuilder的区别

说好的一扇门呢,这怎么有两扇?

别担心,两个门各有各的特色,先让我们搞清楚两个门的区别:

StringBuffer是线程安全的(但是也因为这点,牺牲了一些性能),StringBuilder不是线程安全的(所以效率比前者高)。

好了,没了,结束。

。。

。。

。。

呃,的确就只是这个区别而已。

如果只是想知道它们两个的“区别”,那到这里为止就结束了,不过你们可能想了解一下这两个类的其他知识,我就继续讲解一下好了。

 

5.StringBuffer和StringBuilder身世之谜

在前面,我说了它们的区别就只是线程是否安全,以及由于这个区别产生的性能效率区别。

的确没有其他区别,包括怎么使用,怎么初始化,都是一样的。

相信看到这里,大家就猜到它们这么相似的原因了,因为它们实现了同一个抽象类AbstractStringBuilder。这个抽象类的描述是:可变的字符序列。简单粗暴的说明了它的特点,可变的字符串。

关于这个抽象类,我们后面详细说说,先说个一个小知识:StringBuffer的诞生比AbstractStringBuilder

这是很正常的,StringBufferJDK1.0开始就存在了,它是线程安全的,但是也因此牺牲了一些性能。在JDK1.5的时候,线程不安全但是效率更高的StringBuilder就和它们的抽象类AbstractStringBuilder一起诞生了。这个时候StringBuffer也被迫继承了这个抽象类。

所以AbstractStringBuilder其实就是对可变长字符串专门提取出来的抽象类,也是对这一概念的描述。

image-20210224213732774

 

6.AbstractStringBuilder抽象类介绍

接口

关于AbstractStringBuilder,它定义了一些可变字符串的属性和方法实现,同时它还实现了两个接口

AppendableCharSequence

abstract class AbstractStringBuilder implements Appendable, CharSequence {

Appendable(翻译:可追加)接口也是一同推出的接口,内容很简单,就是3个append的方法,append方法用过的人应该懂,就是StringBuffer和StringBuilder进行字符串拼接的方法

package java.lang;


import java.io.IOException;


public interface Appendable {


    //允许append拼接实现了CharSequence接口的类
    Appendable append(CharSequence csq) throws IOException;


    //允许append拼接实现了CharSequence接口的类,并指定要拼接的字符串范围,只取其开始到结束位置的字符
    Appendable append(CharSequence csq, int start, int end) throws IOException;


    //允许append拼接基本类型char字符
    Appendable append(char c) throws IOException;
}


而其中眼熟的CharSequence(翻译:字符序列)接口,就是说明实现了它的类是一个字符序列。当然最常见的实现类就是String,StringBuffer和StringBuilder了。由于这两个接口的组合使用,才让我们可以进行字符串的拼接,甚至可以跨类进行拼接(StringBuffer拼接StringBuilder对象),只要这个类实现了CharSequence接口即可。

CharSequence接口,也定义了一些字符序列的方法,例如最常用的length()获取字符串长度,charAt(index)获取单个字符,subSequence(start,end)截取字符串,这三个方法是实现类必须实现的。

这两个接口我们大致明白了,简单总结一下:Appendable是关于拼接字符串的接口,而实现了CharSequence接口则是表明自身也是属于字符串类型的类。

属性

    //字符数组,即存储字符串的值,不过和Spring不同的是它没有用final修饰,所以可以修改,据说JDK9之后,采用的就是byte[]了。
    char[] value;


    //字符数组的长度,length()方法其实就是直接返回这个值。
    int count;


    //字符串的最大值,实际上这个值是直接从数组的最大长度直接取的,毕竟字符串的值也是数组,数组能有多长,字符串就有多长
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

其中比较关键的,当然就是value属性了,本质上它和String是一样的,只是Spring的value属性被private final修饰了,才导致它是不可变的。而没有任何修饰的value,就可以修改了,这个value也就是可变长字符串的核心属性了,所以定义在了父类的抽象类中,子类StringBuffer和StringBuilder是没有这个属性的。

方法

本来想要不把方法都讲解一下,但是看了一下里面的方法数。。?抱歉,是我不知天高地厚了。数量还是有亿点多的,包括常用的对字符串进行操作的方法(毕竟是可变长的),获取字符串的方法。还有一些是针对字符数组的操作(即value属性),因为java中数组是定长的,显然我们不可能每次都初始化一个最大长度的字符数组,而是应该随着字符数量的增多,对数组进行扩容。最后还有一些是兼容String的方法,像indexOf,substring这些String里有的方法。

所以里面的方法,这里就先不讲了,还是重点关注一下StringBufferStringBuilder类吧,结合它们会顺带带出来一些AbstractStringBuilder中的方法。

 

7.StringBuffer和StringBuilder代码的区别

前面也说过了,其实它们最大的区别就是:是否线程安全。并且由于这个原因导致了线程不安全的StringBuilder可以有更快的效率。这里我们看看源码,通过源码查看一下两者的区别。

当然前提你得知道synchronized关键字是啥和它的作用,如果不知道还要继续看的话,就先记住它的功能是线程保护。加了它修饰的方法,同一时间只能有一个线程执行(所以会降低性能)。

 

①相同的继承结构

首先它们两的继承结构当然是一样的,都继承了父类AbstractStringBuilder,这意味着它们是可变长的。然后都实现了CharSequence接口,同时也实现了Serializable接口,表明自己支持序列化。

StringBuffer:
public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
}


StringBuilder:
public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
}

 

②toStringCache属性和toString方法

先声明,这块内容意义不是很大,但是可以涨涨知识,可选择跳过。

    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

toStringCache属性是StringBuffer特有的属性,注释的意思大概是:toString返回的最后一次缓存值,会随着StringBuffer的修改而清空。

所以这个字段也就是个缓存,并且是专门用于toString方法的缓存,另外如果StringBuffer的value值进行了任何修改,它都会被直接设为null

既然它是为toString服务的,那么我们就看看两个类的toString方法区别:

StringBuffer的toString方法:

    @Override
    public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        return new String(toStringCache, true);
    }

StringBuilder的toString方法:

    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }
  • 区别1:StringBuffer使用了synchronized关键字修饰
  • 区别2:StringBuffer判断了toStringCache属性是否为空,如果为空,就从value值中新复制一个字符数组给它。而StringBuilder没有这个属性。
  • 区别3:new String方法不同,StringBuffer直接将char数组传递给了String的value属性,StringBuilder的new String却是复制了一个数组出来。

toStringCache的作用和理解

可以看到,它的确是为toString方法服务的一个属性。当使用者调用toString方法时,逻辑会先判断它是否为null。如果为null(说明被改过),就拷贝当前的value数组值作为一个新数组存给toStringCache,然后将它new直接传递给String的value数组。这里的重点是它直接传递了过去,这说明这个数组在这个时刻,StringBuffer的toStringCache和String的value指向的是同一个数组也就是只要一个地方改了,另一个地方也会自动改变

那这不就不符合String的不可变性了吗?答案是不会,首先String的value值是不可能改的,因为final修饰了,唯一可能变化的就是StringBuffer,但是StringBuffer只要字符串有任何改动,toStringCache属性都会立即设为null,也就是和之前的char数组撇开关系

所以这里唯一提高了效率的地方,就是new String,由于直接将数组值传递了过去,当然一行就搞定啦。

String的构造方法源码:

    String(char[] value, boolean share) {
        // assert share : "unshared not supported";
        this.value = value;
    }

可是StringBuillder却用的不是这个构造方法,因为StringBuillder没有toStringCache属性,为了避免它发生上面所说的,违反String不可变性的问题,所以它调用的String构造方法是重新复制了一个数组,加上一堆有的没的逻辑判断,自然就相对慢一点。

toStringCache的意义

虽然看起来高大上,但有时候不要就以为它是100%实用的。上面我们所说的优化,隐藏了一个大前提,就是需要连续调用未更改的StringBuffer的toString方法。毕竟如果你修改了StringBuffer,那么toStringCache就会被设为null,那同样也要完全复制value的char数组给它,只是这个复制操作从String的构造方法挪到了StringBuffer的toString中。所以这种情况下,性能完全不见得会有啥区别,没准儿还更慢。

但是连续调用未修改的StringBuffer的toString方法,更是极其罕见的操作,如果真有人会这样写代码,那我就有点好奇他想干啥了。。

所以我个人觉得它的实用性并不大,这也是我开头所说的,意义不大,但是可以涨涨姿势。网上有网友说,有些代码可能从JDK1.0开始就存在了,像这种可能优势并不大的代码,很少会发生改动,有一定的缺陷是正常的。有时候改起来的成本和影响,远超过它本身存在所造成的负面影响。

所以没准,它也是个有一点点冗余的小功能,只是不方便修改代码而已?当然我也只能画个问号,因为我也不知道。。

 

③构造方法

构造方法两者是完全一致的,两者都有4个构造方法,且这4个代码都是一样的。所以这里不会过多的说明。

无参构造方法:

    public StringBuffer() {
        super(16);
    }

可以看到调用了父类的构造方法,并默认传了数字16。

父类AbstractStringBuilder的构造方法:

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

所以,这个16的意义就是初始化了一个长度为16的char数组设为值。前面已经了解过了,数组的最大长度是Integer.MAX_VALUE - 8,但是显然不可能每次都初始化这么大的,这里我们可以理解了。默认情况下,StringBuffer和StringBuilder是先创建一个长度为16的字符数组。

当然,如果是有参的初始化,它也是往字符串的长度再延长一个16,再进行初始化。

    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }

 

④append方法

两者的append方法原理是类似的,虽然有一些细微差别,但是由于数量太多了,StringBuffer有14个append方法,StringBuilder有13个,所以只节选两个简单讲解。

StringBuffer的append方法:

    @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }


    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuilder的append方法:

    @Override
    public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
    }


    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }


  • 区别1:StringBuffer所有的append方法都加了synchronized关键字
  • 区别2:StringBuffer所有的append方法在开头都将toStringCache属性设为了null
  • 区别3:StringBuffer没有调用其他的重载append方法,而StringBuilder的append方法调用了它的一个重载(虽然只有这一个地方)。

首先synchronized关键字,即是StringBuffer对多线程操作的预防,嗯,虽然之前以为线程安全很高深,但是好像也就是靠这个关键字做到的。然后toStringCache属性设为null的理由前面也说明的很清楚了。最后一个细微的小区别,我觉得可能是synchronized的影响,不过本身也是无关痛痒的变化。

它们都选择调用了父类的append方法,说明核心的内容还是在AbstractStringBuilder父类抽象类中的,虽然我们目的是想了解它们的区别,但是这个时候很适合插入一些知识,所以我们简单的看看append方法的源码学习一下。

AbstractStringBuilder的其中一个append方法:

    public AbstractStringBuilder append(String str) {
        //校验要append的字符是否为空
        if (str == null)
            //如果为空,调用appendNull方法,会添加null四个字符到尾部,并不会抛出异常哦
            return appendNull();
        //获取要添加的字符串长度
        int len = str.length();
        //确保数组容量够用,不够用的话会进行扩容
        ensureCapacityInternal(count + len);
        //通过String的getChars方法,将String对象中的char数组复制到StringBuffer的char数组尾部。
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

就不详解方法内部了,对每个函数的操作都注释了。值得关注的是ensureCapacityInternal,它是检查当前数组容量的方法,当然里面还嵌套了好几个不同方法,目的只有一个:检查当前数组的长度是否足够,如果不够则扩容。

其中 str.getChars(0, len, value, count);内部实际上使用了System.arraycopy,这是System提供了的一个native静态方法,专门用于拷贝数组的。

关于StringBuffer和StringBuilder的扩容:

通过查看源码,并不难理解,前面我们已经知道它一般情况下的初始化大小为16(可以自己指定这个大小初始化)。当使用append方法添加字符时,就会检查其容量是否足够,不足时首先会扩容至当前数组长度*2+2,乘2好理解,就是翻倍当前的长度。至于加2,据说是因为拼接字符串通常末尾都会有个多余的字符。

当然有时候一次加的字符串太长,翻倍+2也不足以装下它,这时候就会直接将长度设置为添加的字符串加上原本字符串的长度,也就是刚刚好装的下的程度。

要查看StringBuffer和StringBuilder的字符容量,可以用capacity方法,它会返回char数组的长度,而length方法实际返回的是存储的字符数量。

 

⑤其他区别

其他还有不少区别,但是就不细讲了,因为这些区别有个共同点,就是这些方法只有StringBuffer有,而StringBuilder没有,但是两者的对象都可以使用这些方法。

没错,就是父类的方法,StringBuffer额外重写了好几个父类的方法,但是却没有作多少 改动,几乎全都加了synchronized,有些方法还会在第一行加上一个toStringCache = null;,所以目的只是兼容它的线程安全,所以没有什么必要进行比较。

 

8.StringBuffer和StringBuilder的应用场景

看了这么多,相信大家最关心的就是这个了,先说结论:一般用StringBuilder,除非可能有线程问题

StringBuffer对线程安全的处理比较简单粗暴,就是为大部分方法都上个synchronized,不管你是加是减还是查,很多方法都直接用synchronized修饰,自然可以保证线程安全。但是效率可想而知。。。比较低下。

而且我们一般也不常需要在多线程的情况下操作StringBuffer或StringBuilder。

就算是多线程,还要要求不能是高并发的,因为StringBuffer是直接用synchronized的,很容易堵塞。所以有些时候会选择用StringBuilder搭配其他手段解决高并发情况下的线程问题(自己在外部加锁之类的)。

所以不管怎么看,StringBuilder都用的比StringBuffer多,当然除非你是低并发下的多线程操作。

9.StringBuffer和StringBuilder的使用

感觉都讲到这个地步了,不贴几个方法好像也过不去了,以下会列一些StringBuffer和StringBuilder的常用方法。

当然通过了源码分析,我们都知道这大部分方法都是从它们的爸爸:AbstractStringBuilder抽象类父类中来的。

append(String s)

将指定的字符串追加到此字符序列,同时有各种各样的重载方法。

reverse()

将字符序列翻转,就是123翻转变成了321这样。

delete(int start, int end)

删除字符序列中指定位置的子字符串

insert(int offset, String str)

将字符串插入此字符序列的指定位置,有很多格式的重载方法。

replace(int start, int end, String str)

使用给定 String 中的字符,替换此字符序列中指定位置的字符。

capacity()

返回当前字符数组的容量(即char[]的容量)。

indexOf(String str)

返回第一次出现的指定子字符串在该字符串中的索引下标。

lastIndexOf(String str, int fromIndex)

返回指定子字符串最后一次出现在字符串中的索引。

String substring(int start, int end)

截取指定位置的字符串,然后返回为一个新的String对象。

 

10.总结

通过这9节内容,我想如果认真看完了,应该学到的不仅只有StringBuffer和StringBuilder的区别而已。

StringBuffer虽然是从JDK1.0就开始出现的,但是目前来看,最常用的应该是JDK1.5出的StringBuilder。

(StringBuffer:为什么会变成这样呢?明明。。明明是我先的来的。。

这两个类我刚开始在用的时候不太容易分得清哪个是线程安全的,当时是用Buffer这个单词记忆的,Buffer有缓冲的意思,因为是处理多线程的类,所以需要缓冲。。。。我大概就是这样记的,虽然听起来有点不太靠谱。。

 

 

 

参考资料:

JDK源码之AbstractStringBuilder类分析:

https://www.cnblogs.com/houzheng/p/12153734.html

[十三]基础数据类型之AbstractStringBuilder:

https://www.cnblogs.com/noteless/

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

推荐阅读更多精彩内容