Java 集合框架_ArrayList(源码解析)

上一章分析了AbstractList抽样类,这一章我们将全面分析最常用的集合ArrayList。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

ArrayList继承自AbstractList类,实现了List接口,RandomAccess这个可随机访问的标记接口,Cloneable可克隆标记接口,和Serializable可序列化标记接口。

一.成员属性

    // 默认ArrayList集合的大小。
    private static final int DEFAULT_CAPACITY = 10;

    // 如果是空集合,那么都指向这个空数组常量,减少创建多个空数组变量。
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 如果是默认集合,先用这个空数组常量代替,当添加元素时,再创建DEFAULT_CAPACITY长度的数组
    // 作用就是减少内存使用。
    // 例如我们new 多个ArrayList集合时,只要我们还没有添加元素,它们内部elementData都指向这个空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 采用Object[]数组来存储集合中的元素
    transient Object[] elementData;

    // 集合中元素的数量
    private int size;

它有两个重要属性,elementData Object数组用来存储集合中元素,size表示集合中元素的数量。还有一个重要属性就是继承自AbstractList的modCount属性。

二. 构造函数

2.1 默认构造函数

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

注意这里和老版本不一样,当我们调用ArrayList空参构造时,它仅仅只是将空数组赋值给elementData,而不是创建一个DEFAULT_CAPACITY大小的新数组。当我们真正调用添加元素方法时,才会创建新数组。延迟创建默认大小的数组。

2.2 设置数组初始长度的构造函数

     public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

根据initialCapacity的值创建对应大小的Object数组,如果小于0就抛出异常。

注意这里没有使用延迟创建数组的方法,因为这里数组初始大小是由用户自定义的。当然也可以用一个成员变量记录,然后延迟创建,只不过它这里没有这么做。

2.3 通过Collection实例构建集合

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray 返回的可能不是Object[]数组,所以进行处理一下
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 如果c是一个空集合,那么这里就设置成EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

通过toArray方法,将集合c转成数组,如果数组长度为0,那么就将 this.elementData设置成空数组常量EMPTY_ELEMENTDATA,而不是集合c转成那个空数组,就可以让这个新创建的空数组内存释放了。
还要注意一点就是,c.toArray()方法返回的可能并不是Object[]数组,所以这里做了判断。

三. 重要方法

3.1 数组扩容方法

我们知道使用数组中存放元素的大小是固定的,而ArrayList集合好像是无限大小的,但是ArrayList集合又是通过数组来实现的,所以它一定帮我们做了数组扩容。

   public void ensureCapacity(int minCapacity) {
        // 如果是默认构造函数创建的ArrayList集合,那么它的最小数组长度都是DEFAULT_CAPACITY。
        // 如果不是的话,那么它最小数组长度就是0
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            ? 0
            : DEFAULT_CAPACITY;

        // 当minCapacity > minExpand,那么就有可能要对数组进行扩容。
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

这个是供给外部调用的方法,来确保数组的长度大于这个minCapacity值。

    private void ensureCapacityInternal(int minCapacity) {
        // 如果相等,表示通过默认构造函数创建的ArrayList集合,而且还没有创建新数组。
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 取minCapacity和DEFAULT_CAPACITY中较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

这是供给类内部使用的方法,在集合的添加元素方法中都会调用这个方法,确保数组长度不小于minCapacity值。

   private void ensureExplicitCapacity(int minCapacity) {
        modCount++; // 表示集合修改了

        // 当数组长度小于minCapacity时,就要进行扩容了
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

会调用grow方法,进行数组扩容操作。

    private void grow(int minCapacity) {
        // 获取老数组长度
        int oldCapacity = elementData.length;
        // 先进行1.5倍扩容, oldCapacity >> 1 与oldCapacity/2 效果一样,但是采用右移操作,速度比除法快得多。
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果进行扩容之后,还是小于minCapacity值,那么就直接用minCapacity值当成数组长度
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 处理newCapacity值大于MAX_ARRAY_SIZE情况,集合容量也是有上限的。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 使用Arrays.copyOf方法,将老数组的元素拷贝到newCapacity长度的新数组中,并返回这个新数组。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

进行数组扩容,先将新数组长度扩充至老数组1.5倍,如果还是小于minCapacity,那么就直接使用minCapacity作为新数组长度,然后再处理超出最大数组长度问题。最后使用Arrays.copyOf方法,将老数组元素拷贝到新数组中。

3.2 添加元素

    public boolean add(E e) {
        // 确保数组容量
        ensureCapacityInternal(size + 1); 
        elementData[size++] = e;
        return true;
    }

在集合末尾添加元素,先确保数组容量,然后再在集合末尾添加元素,最后将集合数量size加一。

     public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

在集合指定索引位置添加元素。先检查索引位置是否越界,确保数组容量满足要求,然后调用System.arraycopy方法,将从索引位置开始的元素全部右移一位,再将索引位置设置成新元素element,最后集合数量size自增。

   public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew); 
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
  1. 将集合c转成数组a,并得到数组长度numNew
  2. 确保集合数组长度不小于size + numNew
  3. 通过System.arraycopy方法,将数组a的全部元素拷贝到数组elementData的size位置之后,就是集合末尾。
  4. 将集合数量size加上numNew大小。
     public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }
  1. 检查索引位置是否越界。
  2. 将集合c转成数组a,并得到数组长度numNew。
  3. 确保集合数组长度不小于size + numNew。
  4. numMoved 表示原数组要移动元素的数量
  5. 将原数组index之后的元素(包括index),都向后移动numNew个位置

我们要向一个数组从index位置插入numNew个元素,那么这个数组从index位置开始到结尾的都要移动numNew个位置,一共移动size - index个元素。

  1. 将数组a中全部元素插入到elementData数组index之后位置(包括index)。
  2. 将集合数量size加上numNew大小。

3.3 删除元素

   public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 清理引用,让垃圾回收器可以回收无用的对象内存
        elementData[--size] = null; 
    }

遍历数组,找到与o相等的索引位置,然后调用fastRemove方法删除这个索引位置的元素。
要删除数组index位置元素,就是将index之后的元素都向前移动一位,即使用System.arraycopy(elementData, index+1, elementData, index, numMoved)方法完成。

     public E remove(int index) {
        rangeCheck(index);
        modCount++;
        E oldValue = elementData(index);
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 清理引用,让垃圾回收器可以回收无用的对象内存
        elementData[--size] = null;
        return oldValue;
    }

先通过elementData(index)方法得到index位置的元素,便于返回这个被删除的元素。然后通过System.arraycopy方法将数组index位置之后的元素都向前移动一位,最后将数组最后位置的元素置位null,并将集合容量size自减。

    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }

    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }

移除与集合c相同元素,和只保留与集合c相同元素都调用batchRemove方法。

private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                // 如果complement为true,表示本集合只保留集合c中包含的元素
                // 如果complement为false,就删除集合c中包含的元素,所以集合c中不包含的元素才保留到本集合中
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // 发生异常之后的补救处理
            // 如果r不等于size,表示数组没有遍历完成,将数组r位置之后的元素拷贝到数组w位置之后。
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                // w就表示现在集合的数量
                w += size - r;
            }
            // 如果不相等,说明本集合删除元素了,就要进行一些处理
            if (w != size) {
                // 将w位置之后的元素置位null,方便垃圾回收器回收。
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                // 将w的值赋值给size。
                size = w;
                modified = true;
            }
        }
        return modified;
    }

还记得我们在AbstractCollection类中是怎么操作的么?是调用迭代器的remove方法,但是在这里就不适用,因为数组删除一个元素会导致数组的移动,很浪费时间。这里用了很巧妙的方法。

想一想我们平常怎么删除数组中特定元素的。

  1. 第一种是新建一个长度一样的数组,然后遍历原数组,将满足条件的元素才加入到新数组中,这样就可以删除不满足条件的元素了。这种方式效率很快,但是需要多余的内存。
  2. 第二种是反向遍历数组,如果发现不满足条件的元素,将这个位置之后的元素前移一位,这样就覆盖不满足条件的元素了。这种方式不需要多余内存,但是需要进行数组的移动,耗费时间。
  3. 第三种就采用非常巧妙的方法了,这里用到两个位置索引r和w,用r索引来遍历整个数组,用w表示保留下来元素的索引。所以遍历数组,如果满足条件,就将这个元素elementData[r]存放到elementData[w] w位置,并将w数量加一。因为r一定大于或者等于w,所以不存在前面位置的元素覆盖后面位置的元素。这种方式速度快,而且不用多余内存。
   public void clear() {
        modCount++;
        // 清理引用,让垃圾回收器可以回收无用的对象内存
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }

清除集合中元素。

protected void removeRange(int fromIndex, int toIndex) {
        modCount++;
        int numMoved = size - toIndex;
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                         numMoved);

        // 清理引用,让垃圾回收器可以回收无用的对象内存
        int newSize = size - (toIndex-fromIndex);
        for (int i = newSize; i < size; i++) {
            elementData[i] = null;
        }
        size = newSize;
    }

删除集合中fromIndex位置到toIndex位置的元素(包括fromIndex,但是不包括toIndex)。通过 System.arraycopy方法将toIndex(包括toIndex位置)之后的元素全部向前移动到fromIndex位置,所以toIndex位置的元素还是保留在集合中。然后将无效位置的元素全部置位null,方便垃圾回收器回收。

3.4 替换元素

    public E set(int index, E element) {
        rangeCheck(index);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

就是将数组对应位置替换成新元素element

3.5 查询方法

    E elementData(int index) {
        return (E) elementData[index];
    }

返回对应索引位置元素,并强转成E类型。

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }

通过elementData方法获取元素。

   public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

正向遍历数组,查找与o相同元素,如果查找到就返回对应下标位置,如果没有查到就返回-1。

    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

反向遍历数组,查找与o相同元素,如果查找到就返回对应下标位置,如果没有查到就返回-1。

    public Iterator<E> iterator() {
        return new Itr();
    }
    public ListIterator<E> listIterator() {
        return new ListItr(0);
    }
    public ListIterator<E> listIterator(int index) {
        if (index < 0 || index > size)
            throw new IndexOutOfBoundsException("Index: "+index);
        return new ListItr(index);
    }

与AbstractList一样,ArrayList提供两个迭代器,都是ArrayList内部类。

四. ArrayList的内部类Itr

4.1 成员属性

       // 光标索引位置,0表示在集合开始位置(因为这是一个list集合,所以可以用索引表示开始位置)
        int cursor;
        // 表示读取到的当前元素位置
        int lastRet = -1;
        // 用来判断原集合是否被修改,抛出ConcurrentModificationException异常。
        int expectedModCount = modCount;

4.2 遍历集合

       final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

如果集合中modCount的值与迭代器中expectedModCount的值不相等,就说明在迭代器期间集合被修改了,那么遍历的数据已经失效,就抛出异常。

        public boolean hasNext() {
            return cursor != size;
        }

判断集合中是否还有未读取的元素,即cursor光标已经移动到最后了。

       public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
  1. 先检查集合有没有没被修改。
  2. 将cursor光标位置赋值给变量i
  3. 然后判断i位置是否有效
  4. 将cursor光标位置加1,指向下一个元素。
  5. 将i赋值给lastRet,并返回i位置的元素,表示当前遍历到的元素。

4.3 删除元素

     public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
  1. 如果lastRet < 0表示当前位置元素无效,不能进行删除操作,抛出异常。
  2. 检查集合有没有没被修改。
  3. 调用ArrayList集合remove(lastRet)方法删除集合当前元素。
  4. 将当前元素位置索引lastRet赋值给cursor,然后将lastRet置位-1,表示当前位置已失效。

注意这里的处理方法与AbstractList不一样,但是实现的效果是一样的。都是将cursor重置成当前位置坐标。

  1. 重新设置expectedModCount的值

五. ArrayList的内部类 ListItr

它继承自Itr类,实现了ListIterator接口。它实现了对List集合的反向遍历,以及添加和替换集合中元素的方法。

5.1 构造函数

        ListItr(int index) {
            super();
            cursor = index;
        }

表示从index - 1位置开始,反向遍历集合元素。

5.2 反向遍历集合

        public boolean hasPrevious() {
            return cursor != 0;
        }

判断集合中是否还有未读取的元素。当cursor==0,表示已经读取到第一元素了,前面以及没有元素了。

      public E previous() {
            checkForComodification();
            int i = cursor - 1;
            if (i < 0)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i;
            return (E) elementData[lastRet = i];
        }
  1. 先检查集合有没有没被修改。
  2. 将cursor-1赋值给变量i
  3. 然后判断i位置是否有效
  4. 将i的值赋值给cursor和lastRet;
  5. 并返回i位置的元素,表示当前遍历到的元素。

5.3 返回索引位置。

        // 如果是正向遍历集合,nextIndex返回值表示集合中下一个元素的索引位置。
        // 如果是反向遍历集合,nextIndex返回值表示集合中当前元素的索引位置。
        public int nextIndex() {
            return cursor;
        }

        // 如果是正向遍历集合,previousIndex返回值表示集合中当前元素的索引位置。
        // 如果是反向遍历集合,previousIndex返回值表示集合中前一个元素的索引位置。
        public int previousIndex() {
            return cursor-1;
        }

5.4 替换当前元素

       public void set(E e) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.set(lastRet, e);
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
  1. lastRet < 0 表示当前位置无效,不能更换元素,抛出异常。
  2. checkForComodification方法检查集合有没有没被修改。
  3. 调用List集合的set(lastRet, e)方法,替换当前元素。
  4. 因为修改了集合,那么重新赋值expectedModCount = modCount

5.5 添加元素

       public void add(E e) {
            checkForComodification();

            try {
                int i = cursor;
                ArrayList.this.add(i, e);
                cursor = i + 1;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
  1. 调用checkForComodification方法检查集合有没有没被修改。
  2. 调用List的add(i, e)方法,在cursor光标位置插入元素。

那么就有两种情况了,正向遍历的时候,就是在当前元素下一个索引位置插入,而反向遍历时,就是在当前元素索引位置插入。

  1. lastRet = -1 设置当前元素索引位置无效。
  2. cursor = i + 1 将光标位置加1

这里就有问题了,它只能保证正向遍历的时候,不会遍历到刚刚插入的元素。但是反向遍历的时候,因为将cursor光标位置加一,那么下次获取前一个元素正好是刚刚添加的元素。

  1. 因为修改了集合,那么重新赋值expectedModCount = modCount。

总结

ArrayList内部是通过数组储存元素的,数组的长度是固定,所以集合中元素数量超出数组长度时,要对数组进行扩容。

  • 一般先将长度扩大到原数组长度的1.5倍。
  • 与集合元素数量进行比较,如果还是小于,就直接将元素数量当成数组长度。
  • 考虑最大上限问题。
  • 最后利用 Arrays.copyOf(elementData, newCapacity),创建一个新数组,将原数组元素都拷贝到新数组中,返回新数组赋值给elementData

数组扩容操作一般都是在添加元素中判断的,因为只有添加元素,才能超出原数组储存大小的情况。

添加元素

我们就以最复杂的一种情况为例,在索引index位置添加一个集合c。

  1. 检查索引位置是否越界。
  2. 通过集合c的toArray方法,将集合转成数组。
  3. 确保集合数组长度不小于本集合元素数量与集合c元素数量之和。(调用ensureCapacityInternal方法)
  4. 先通过System.arraycopy方法,将index位置之后的元素,全部右移集合c元素数量的位数。

这样就将index 到index+ numNew位置全部空出来,正好可以存放集合c全部元素了。

  1. 再使用System.arraycopy方法,将集合c中元素拷贝到本集合数组index 到index+ numNew位置。
  2. 最后将集合数量size加上numNew大小。

删除元素

从源码中看,ArrayList删除元素分两种情况。

  1. 删除某一位置index的单个元素。就是通过System.arraycopy来实现index之后元素全部左移一位,实现了删除效果。

记着将无效位置元素置位null,方便垃圾回收器回收内存。

  1. 删除多个元素。这里用了很巧妙的方法,具体请看文章中关于batchRemove(Collection<?> c, boolean complement)方法的详解。

查询方法

主要就是遍历数组,很简单。

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

推荐阅读更多精彩内容