Effective Java——类和接口

Android.jpg

本系列文章是总结Effective Java文章中我认为最重点的内容,给很多没时间看书的朋友以最短的时间看到这本书的精华。
第一篇《Effective Java——创建和销毁对象》
第二篇《Effective Java——对于所有对象都通用的方法》

第四章 类和接口

目录.png

第13条:使类和成员的可访问性最小化

该条规则尽可能使每个类或者成员不被外界访问,只对外暴露有用的API接口且永远支持它。
开发系统每个模块之前的实现细节全部隐藏,只是通过API去掉用,那么会大大增加这个系统的稳定性和并行开发能力,每个模块都可以单独的运行、调试、测试。
四种访问级别按照访问性的递增顺序如下:

  1. 私有的(private)——只要在声明该成员的顶层类内部才可以访问。
  2. 包级私有(package-private)——声明该成员的包内部任何类都可以访问这个成员。又被成为“缺省(default)访问级别”,如果没有为成员指定访问修饰符,就采用这个访问级别。
  3. 受保护(protected)——声明该成员的包内部任何类和该类的子类都可以访问这个成员。
  4. 共有的(public)——任何地方都可以访问。

protected和public都属于导出API的一部分,需要永久维护。

实例域决不能是共有的
如果域是非final得,或者是一个指向可变对象final引用,那么一旦这个域成为共有的,就放弃了对存储在这个域中的值进行限制的能力。
用代码说话:

public static final class ClassA{
       //非final
        public String string = "A";
        //可变对象final
        public final StringBuilder stringBuilder = new StringBuilder();
}
//在外部都可以修改这个域,所以就放弃了对存储在这个域中的值进行限制的能力。
ClassA classA = new ClassA();
classA.string = "B";
classA.stringBuilder.append("BBBB");

长度非零的数组总是可变的
用代码说话:

//类暴露这个字段,客户端程序员可以任意修改数组中的值
public static final Integer[] VALUES = {0,1,2,3,4,5,6,7,8,9};
//解决方法1
public static final Integer[] PRIVATE_VALUES = {0,1,2,3,4,5,6,7,8,9};
public static final List<Integer> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
//解决方法2
public static final Integer[] PRIVATE_VALUES = {0,1,2,3,4,5,6,7,8,9};
public static final Integer[] values(){
            return PRIVATE_VALUES.clone();
}

第14条:在公有类中使用访问方法而非公有域

这条说的很简单:
如果类可以在它所在的包的外部进行访问,就提供访问方法。
如果类是包级私有的,或者是私有嵌套类,直接暴露他的数据域并没有本质的错误。
用代码说话:

//如下类是不符合规范的
public class Point{
        public double x;
        public double y;
}
//需要提供getter和setter方法
public class Point{
        public double x;
        public double y;
        public double getX() {
            return x;
        }
        public void setX(double x) {
            this.x = x;
        }
        public double getY() {
            return y;
        }
        public void setY(double y) {
            this.y = y;
        }
}

第15条:使可变性最小化

不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。例如StringBigInteger,BigDecimal,基本类型的包装类。
使类不可变的五条原则:
1.不要提供任何会修改对象状态的方法。
2.保证类不会被扩展。防止子类化:final修饰类,private构造方法
3.使所有的域都成为私有的。
4.确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法(accessor)中返回该对象对的引用。在构造器、访问方法和readObject方法中请使用保护性拷贝(defensive copy)技术。
函数的:类的方法进行运算之后返回一个新创建的实例,而不是修改这个实例。

String s = "string test";
String rep = s.replace("str","");
System.out.println(s);//打印结果string test
System.out.println(rep); //打印结果ing test
对比发现字符串实例`s`调用`replace`之后打印结果还是原来的没变,表示没有修改当前实例内部状态,而是重新创建一个实例,查看字符串实例`rep`的打印结果可以证明这点。

过程的或者命令式的:类的方法进行运算之后不产生新的实例,导致当前实例内部状态发生了改变。

函数的

优点:

  1. 不可变对象比较简单,只有一种状态创建时期的状态,在整个生命周期内永远不再发生变化。
  2. 不可变对象本质上是线程安全的,不需要同步。
  3. 不可变对象可以被自由的共享。无需提供clone方法或者拷贝构造器。但是String是个反例它仍然具有拷贝构造器。
  4. 不仅可以共享不可变对象,还可以共享它们的内部信息。例如:BigInteger用int类型表示符号,用int数组表示数值。negate方法方法产生一个新的实例数值一样但是符号相反,在内部它并不需要拷贝数组,新建的实例也指向原来的数组来优化内存。
    缺点:
    每个不同的值都会创建一个单独的对象。如果频繁调用会造成内存紧张或者频繁GC影响系统的性能。
    解决方法:
  5. 将频繁用到的值提供静态final常量。
  6. 提供静态工厂方法,把频繁被请求的实例缓存起来。从而降低内存占用和垃圾回收的成本。
  7. 提供可变配套类,例如;String的可变配套类为StringBuilder
不允许被子类化方案
1. 用final关键字修饰类
2. 将类的所有构造器都声明为private

用代码说话:

 public class Complex{
        private final double re;
        private final double im;
        //构造器声明为private 
        private Complex(double re,double im){
            this.re = re;
            this.im = im;
        }
        public static final Complex valueOf(double re,double im){
            return new Complex(re,im);
        }
}

这种方式相对于第一种方式更为灵活,优点:

  1. 它允许使用多个实现类。
  2. 使用静态工厂方式创建对象有非常多的好处,可以增加缓存对象的能力,避免重载构造方法造成的功能不清晰。(可以会看本本书第一条规则)
    用代码说话:
public static class Complex{
        private final double re;
        private final double im;
        private static SubComplex subComplex = null;
        private Complex(double re,double im){
            this.re = re;
            this.im = im;
        }
        //每个不同的功能用不同的静态方法,避免构造方法重载
        public static final Complex valueOf(double re,double im){
            return new Complex(re,im);
        }
        //可以对频繁创建的对象进行缓存
        public static final Complex valueOfSub(double re,double im){
            if(null == subComplex){
                subComplex =  new SubComplex(re,im);
            }
            return subComplex;
        }
        //允许子类化
        private static class SubComplex extends Complex{
            private SubComplex(double re, double im) {
                super(re, im);
            }
        }
}
不可变类实现Serializable接口

如果选择让不可变类实现Serializable接口,那么会涉及到一下几个方法,如下代码解释

public static class CustomSerializable implements Serializable{
        public static final CustomSerializable INSTANCE = new CustomSerializable();
        private String name;
        private String age;
        public String getName() { return name;}
        public void setName(String name) { this.name = name;}
        public String getAge() {return age; }
        public void setAge(String age) {this.age = age;}
        //如下两个方法可以自定义序列化对象那些字段可以序列化,那些对象不可以序列化
        private void writeObject(ObjectOutputStream out) throws IOException {
//            out.defaultWriteObject();
            out.writeObject(age);
        }
        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
//            in.defaultReadObject();
            age = (String) in.readObject();
        }
        //实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。
        private Object readResolve() throws ObjectStreamException {
            return INSTANCE;
        }
        @Override
        public String toString() {
            return "CustomSerializable{" +
                    "name='" + name + '\'' +
                    ", age='" + age + '\'' +
                    '}';
        }
}

try{
            INSTANCE.setName("aaaaaaaa");
            INSTANCE.setAge("30");
            File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/person.out");
            ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
            oout.writeObject(INSTANCE); // 保存单例对象
            oout.close();
            ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
            Object newPerson = oin.readObject();
            oin.close();
            Log.e("TAG",newPerson.toString());
            Log.e("TAG",String.valueOf(INSTANCE == newPerson));
        }catch (Exception e){
            e.printStackTrace();
        }
//打印结果
04-19 22:09:49.644 22007-22007/? E/TAG: CustomSerializable{name='aaaaaaaa', age='30'}
04-19 22:09:49.644 22007-22007/? E/TAG: true

这篇文章讲解的非常详细:http://developer.51cto.com/art/201202/317181.htm

第16条:复合优先于继承

在包的内部使用继承是非常安全的,子类和超类的实现都处在同一个程序员的控制下。对于专门为了继承而设计、并且具有很好文档说明的类来说继承也是非常安全的。然而对于普通类进行跨域包边界的继承则是非常危险的
继承打破了封装性,子类依赖其超类中特定功能的实现。如果超类的实现随着发布的新版本发生了变化,那么子类有可能会遭到破坏,即使他的代码完全没有改变。
如下例子:
检测一个Set从创建依赖一共增加了多少个元素:如下代码:

public static class InstrumentedHashSet<E> extends HashSet<E>{
        private int addCount = 0;
        public InstrumentedHashSet(){}
        public InstrumentedHashSet(int intCap,float loadFactor){
            super(intCap,loadFactor);
        }
        @Override
        public boolean add(E e) {
            addCount++;
            return super.add(e);
        }
        @Override
        public boolean addAll(Collection<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
        public int getAddCount() {
            return addCount;
        }
}
 InstrumentedHashSet hashSet = new InstrumentedHashSet();
hashSet.addAll(Arrays.asList("aaa","bbb","ccc"));
Log.e("TAG","count : " + hashSet.getAddCount());

如上我们期望返回3,但实际上返回6。出现这种情况就是因为HashSet.addAll内部调用了add()方法,所以总共增加了6。
子类的功能需要依赖于父类的内部实现。由于父类的内部实现是不对外承诺的,不能保证java的每个版本内部实现都一样,所以不能保证子类的功能一定是正确的。
使用“复合”来解决这个问题,也就是包装器模式
如下代码:

public static class InstrumentedHashSet<E> extends ForwardingSet<E>{
        private int addCount = 0;
        public InstrumentedHashSet(Set<E> set) {
            super(set);
        }
        @Override
        public boolean add(E e) {
            addCount++;
            return super.add(e);
        }
        @Override
        public boolean addAll(Collection<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
        public int getAddCount() {
            return addCount;
        }
    }
//包装器类非常稳固它不依赖于任何类的实现细节,包装器中的方法称为转发方法
    public  static class ForwardingSet<E> implements Set<E>{
        private Set<E> mSet;
        public ForwardingSet(Set<E> set){
            mSet = set;
        }
        @Override
        public int size() {
            return mSet.size();
        }
        @Override
        public boolean add(E e) {
            return mSet.add(e);
        }
        @Override
        public boolean addAll(@NonNull Collection<? extends E> c) {
            return mSet.addAll(c);
        }
    //此处省略好多转发方法,因为篇幅有限
}
//而且所有继承`Set`接口的对象都可以用这个包装器类来实现增加对象计数功能
InstrumentedHashSet hashSet = new InstrumentedHashSet(new HashSet());
InstrumentedHashSet treeSet = new InstrumentedHashSet(new TreeSet());

如上代码包装类不依赖任何类的实现细节,只是通过转发方法来实现类的功能,非常完美的解决了这个问题,而且所有继承Set接口的对象都可以用这个包装器类来实现增加对象计数功能。
包装器模式也叫装饰模式动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。

第17条:要么为继承而设计,并提供文档说明,要么就禁止继承

专门为继承设计的类,该类的文档必须精确地描述每个方法所带来的影响,换句话说,该类必须有文档说明它的可覆盖的方法的自用性。对于每个共有的或受保护的方法或者构造器,它的文档必须指明该方法或者构造器调用了那些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何响应后续的处理过程的。类必须在文档中说明,在哪些情况下它会调用可覆盖方法。

例如:java.util.AbstractCollectionpublic boolean remove(Object o)这个方法的注释,非常清楚。
源码注释片段:该实现遍历整个集合来查找制定元素,如果找到该元素,将会利用迭代器的remove方法将之从集合中删除,注意,如果该集合的iterator方法返回的迭代器没有实现remove方法,改实现就会抛出UnsupportOperationException
该文档清楚地说明了,覆盖iterator方法将会影响remove方法的行为。而且,它确切的描述了iterator返回的Iterator的行为将会怎样影响remove方法的行为。

  1. 对于为了继承而设计的类,唯一的测试方法就是编写子类。
  2. 为了继承而设计的类的构造器决不允许调用可被覆盖的方法,如果实现了CloneableSerializable接口,就应该意识到clonereadObject,方法在行为上非常类似构造器,所以无论是clone还是readObject都不可以调用可覆盖方法,不管是直接还是间接。
  3. 不可变类禁止继承
  4. 继承一个类消除可覆盖方法的自用性,而不改变它的行为。将每个可覆盖方法的代码体移到一个私有的“辅助方法”中,并且让每个可覆盖的方法调用它的私有辅助方法。然后,用“直接调用可覆盖方法的私有辅助方法”来代替“可覆盖方法的每个自用调用”。

第18条:接口优于抽象类

  1. 现有类可以很容易被更新,以实现新的接口。例如,当Comparable接口引入Java平台时,会更新许多现有类,以实现Comparable接口。一般来说无法更新现有类实现新的抽象类,由于Java只允许单继承,所以会破坏类的层级关系,这样做非常危险。
  2. 接口是定义mixin(混合类型)的理想选择。mixin是指这样的类型:类除了实现他的“基本类型”之外,还可以实现这个mixin类型,以表示它提供了某些可供选择的行为。例如,实现Comparable这个接口类的实例,除了表示他本身的类型外,还可以表示,它的实例可以和任何其他实现这个接口的类型的实例相比较。
  3. 接口允许我们构造非层次结构的的类的框架。主要说的就是接口可以多重继承,一个类可以同时实现多个接口,实现多种功能。
  4. 包装类模式,接口使得安全地增强类的功能成为可能。如果使用抽象类,那么只能使用继承手段来增加功能。
  5. 骨架类。接口定义类型,骨架实现类接管了所有与接口实现相关的工作。例如,AbstractList、AbstractMap等。可以自行查看源码。
  6. 模拟多重继承,实现了这个接口的类可以把对于接口方法的调用,转发到一个内部类私有类的实力上,这个内部私有类扩展了骨架实现类。
  7. 抽象类的演变比接口的演变要容易得多。如果在后续版本中,希望在抽象类中增加新的方法,始终可以增加具体的方法,并且可以包含具体的默认实现。然后,该抽象类的所有现有实现都将提供这个新的方法。对于接口是行不通的。
  8. 接口一旦被公开发行,并且已被广泛实现,在想改变这个接口几乎是不可能的。它会影响所有实现这个接口的类。但是实现这个接口骨架类的类不会受到影响,因为可以直接在骨架类中增加新的方法。最佳途径,为每个借口都实现一个骨架类。
  9. 如果演变比灵活性更加重要的情况下,应该使用抽象类而非接口。因为抽象类在后期非常容易添加方法。

第19条:接口只用于定义类型

  1. 避免常量接口。接口没有任何方法,只包含静态的final域。
  2. 如果这些常量最好被看做枚举类型,就应该使用枚举常量
  3. 如果这些常量与某个现有的类或者接口机密相关,就应该把这些常量添加到这个接口或者类中。例如,Java平台中的Integer.MIN_VALUEInteger.MAX_VALUE
  4. 使用不可实例化工具类来导出这些常量。例如,final类,或者private构造方法类

第20条:层次优先于标签类

标签类
public static class Figure{
       enum Shape{ RECTANGLE, CIRCLE};
        
        final Shape shape;
        
        double lenght;
        double width;
        
        double radius;
        
        Figure(double radius){
            shape = Shape.CIRCLE;
            this.radius = radius;
        }

        Figure( double lenght, double width){
            this.shape = Shape.RECTANGLE;
            this.lenght = lenght;
            this.width = width;
        }
        
        double area(){
            switch (shape){
                case RECTANGLE: {
                    return lenght * width;
                }
                case CIRCLE:{
                    return Math.PI*(radius*radius);
                }
                default:{return -1;}
            }
        }
}

如上代码被称为标签类。他有很多缺点:

  1. 充斥着样本代码,包括枚举声明、标签域(final Shape shape;)以及条件语句。
  2. 单个类中存在了多个实现,通过标签域进行区分,破坏了可读性。
  3. 内存占用增加,因为实例承担着属于其他风格的不相关的域
  4. 无法给标签类增加风格,除非修改源代码。如果一定要添加风格,就必须记得给每个条件语句都添加一个条件,否则会运行失败。
    一句话,标签类过于冗长,容易出错,并且效率低下。
类层次
public static abstract class Figure{
        abstract double area();
    }

    public static class Circle extends Figure{
        final double radius;
        public Circle(double radius){
            this.radius = radius;
        }

        @Override
        double area() {
            return Math.PI*(radius*radius);
        }
}
public static class Rectangle extends Figure {

        final double length;
        final double width;

        public Rectangle(double length, double width){
            this.length = length;
            this.width = width;
        }
        
        @Override
        double area() {
            return lenght * width;
        }
}

如上代码被称为类层次,他纠正了标签类的所有缺点:

  1. 代码简单清除,没有样板代码。
  2. 每个类型都有自己的类,这些类都没有收到不想管数据域的拖累。
  3. 所有域都是final
  4. 杜绝了switch case这种形式的语句,防止扩展时忘记写case造成的运行失败。
  5. 多个程序员可以独立的扩展层次结构,并且不用访问根类的远代码就能相互操作。6. 在不修改源文件的情况下增加类型。

标签类尽量少用,需要被类层次来代替。

第21条:用函数对象表示策略

用函数对象表示策略模式

  1. 函数对象:如果一个类只导出一个方法,那么他的实例就相当于指向该方法的一个指针,这样的实例被称为函数对象
  2. 策略模式(Strategy Pattern):一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

策略模式的典型应用就是java.util.Collections.sort(List<T> list, Comparator<? super T> c)
查看源码:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
        // BEGIN Android-changed: Compat behavior for apps targeting APIs <= 25.
        // list.sort(c);
        int targetSdkVersion = VMRuntime.getRuntime().getTargetSdkVersion();
        if (targetSdkVersion > 25) {
            list.sort(c);
        } else {
            // Compatibility behavior for API <= 25. http://b/33482884
            if (list.getClass() == ArrayList.class) {
                Arrays.sort((T[]) ((ArrayList) list).elementData, 0, list.size(), c);
                return;
            }

            Object[] a = list.toArray();
            Arrays.sort(a, (Comparator) c);
            ListIterator<T> i = list.listIterator();
            for (int j = 0; j < a.length; j++) {
                i.next();
                i.set((T) a[j]);
            }
        }
        // END Android-changed: Compat behavior for apps targeting APIs <= 25.
}

由源码可见这是一个比较方法,具体的比较策略是由函数的参数决定的,也就是Comparator<? super T> c这个参数。所有实现Comparator接口的对象都可以作为这个比较方法的策略。

查看Comparator源码:

public interface Comparator<T> {
      int compare(T o1, T o2);
      boolean equals(Object obj);
}

如上代码,这就是策略接口,实现这个接口,实现int compare(T o1, T o2);方法,就可以定义自己的策略了。实现这个接口需要使用函数对象的形式。

第22条:优先考虑静态成员类

嵌套类:被定义在另一个类内部的类。嵌套类存在的目的应该只是为了他的外围类提供服务。
嵌套类四种:静态成员类非静态成员类匿名类局部类

1. 静态成员类,静态成员类使用static定义的内部类,由于静态成员类内部没有保存外围类的实例,所以它只能访问外围类的静态成员变量或者方法。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性。静态成员的一个典型应用——建造者模式,如下代码:
public class TestBuilder {
    private final String mName; 
    private TestBuilder(Builder builder){
        this.mName = builder.mName;
    }
    public static final class  Builder{
        private String mName;
        public Builder setName(String name){
            this.mName = name;
            return this;
        }        
        public TestBuilder builder(){
            return new TestBuilder(this);
        }
    }
    public static final void main(){
        TestBuilder testBuilder = new TestBuilder
                .Builder()
                .setName("heiheihei")
                .builder();
    }
}
2. 非静态成员类,和静态类唯一区别是它的定义去掉static关键字。非静态成员类的每个实例都隐含着外围类的一个外围实例。

优点:它可以访问外围类的所有成员变量和方法。
缺点:

  1. 创建非静态成员类浪费内存,并且增加了时间开销(由于隐含着外围类的实例)。
  2. 会导致外围类实例在符合垃圾回收时仍然得以保留,出现内存泄漏(例如Android中Handler的内存泄漏)。

典型应用:ArrayList.class中的Iterator非静态内部类,可以自行查看源码。

  1. 想要创建非静态成员类实例必须先创建外围类的实例,enclosingInstance.new MembnerClass()
  2. 如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类必须是静态成员类。
  3. 如果声明成员类不需要访问外围实例,就要始终定义成静态成员类。
3. 匿名类,没有名字,不是外围类的一个成员。
  1. 它并不与其他成员一起被声明,而是在使用的同时被声明和实例化。
  2. 匿名类可以出现在代码中任何允许存在表达式的地方。
  3. 匿名类出现在非静态环境中,才有外围实例。
  4. 匿名类中不能有任何静态成员变量,无论是否在静态环境中。
  5. 匿名类除了在它们被声明的时候之外,是无法将他们实例化的。
  6. 不能执行instanceof测试,或者做任何需要命名类的其他事情。
  7. 匿名类无法实现接口、无法继承类。

典型应用

  1. 动态的创建函数对象,例如:java.util.Collections.sort(List<T> list, Comparator<? super T> c)方法中的函数对象。
  2. 创建过程对象例如:RunnableThreadTimerTask
  3. 静态工厂内部例如:18条中创建的匿名骨架类。
3. 局部类,在任何“可以声明局部变量”的地方,都可以声明局部类。

局部类与其他三种嵌套类一样有一些共同属性,只是它声明的位置不太一样。

总结:

  1. 嵌套类需要在单个方法之外仍然可见,或者太长,就应该定义成成员类。
  2. 如果成员类每个实例都需要指向一个外围类的引用,就应该定义成非静态成员类,否则就应该定义成静态成员类。
  3. 假设嵌套类属于一个方法内部,如果你只需要在一个地方创建实例,并且已经有一个预置的类型可以说明这个类的特征,就要把它做成匿名类,否则,就做成局部类。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容