Java8 中用法優雅的 Stream,性能也""優雅""嗎?

2022-01-02 輕鬆編程社

我們已經學會如何使用Stream API,用起來真的很爽,但簡潔的方法下面似乎隱藏著無盡的秘密,如此強大的API是如何實現的呢?


比如Pipeline是怎麼執行的,每次方法調用都會導致一次迭代嗎?自動並行又是怎麼做到的,線程個數是多少?本節我們學習Stream流水線的原理,這是Stream實現的關鍵所在。


首先回顧一下容器執行Lambda表達式的方式,以ArrayList.forEach()方法為例,具體代碼如下:


// ArrayList.forEach()
public void forEach(Consumersuper E> action) {
    ...
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);// 回調方法
    }
    ...
}


我們看到ArrayList.forEach()方法的主要邏輯就是一個for循環,在該for循環裡不斷調用action.accept()回調方法完成對元素的遍歷。


這完全沒有什麼新奇之處,回調方法在Java GUI的監聽器中廣泛使用。Lambda表達式的作用就是相當於一個回調方法,這很好理解。


Stream API中大量使用Lambda表達式作為回調方法,但這並不是關鍵。理解Stream我們更關心的是另外兩個問題:流水線和自動並行。


使用Stream或許很容易寫入如下形式的代碼:


int longestStringLengthStartingWithA
        = strings.stream()
              .filter(s -> s.startsWith("A"))
              .mapToInt(String::length)
              .max();


上述代碼求出以字母A開頭的字符串的最大長度,一種直白的方式是為每一次函數調用都執一次迭代,這樣做能夠實現功能,但效率上肯定是無法接受的。


類庫的實現著使用流水線(Pipeline)的方式巧妙的避免了多次迭代,其基本思想是在一次迭代中儘可能多的執行用戶指定的操作。為講解方便我們匯總了Stream的所有操作。


Stream操作分類中間操作(Intermediate operations)無狀態(Stateless)unordered() filter() map() mapToInt() mapToLong() mapToDouble() flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() peek()有狀態(Stateful)distinct() sorted() sorted() limit() skip()結束操作(Terminal operations)非短路操作forEach() forEachOrdered() toArray() reduce() collect() max() min() count()短路操作(short-circuiting)anyMatch() allMatch() noneMatch() findFirst() findAny()


Stream上的所有操作分為兩類:中間操作和結束操作,中間操作只是一種標記,只有結束操作才會觸發實際計算。中間操作又可以分為無狀態的(Stateless)和有狀態的(Stateful),無狀態中間操作是指元素的處理不受前面元素的影響,而有狀態的中間操作必須等到所有元素處理之後才知道最終結果。


比如排序是有狀態操作,在讀取所有元素之前並不能確定排序結果;結束操作又可以分為短路操作和非短路操作,短路操作是指不用處理全部元素就可以返回結果,比如找到第一個滿足條件的元素。之所以要進行如此精細的劃分,是因為底層對每一種情況的處理方式不同。


為了更好的理解流的中間操作和終端操作,可以通過下面的兩段代碼來看他們的執行過程。


IntStream.range(1, 10)
   .peek(x -> System.out.print("\nA" + x))
   .limit(3)
   .peek(x -> System.out.print("B" + x))
   .forEach(x -> System.out.print("C" + x));


輸出為:


A1B1C1
A2B2C2
A3B3C3


中間操作是懶惰的,也就是中間操作不會對數據做任何操作,直到遇到了最終操作。而最終操作,都是比較熱情的。他們會往前回溯所有的中間操作。也就是當執行到最後的forEach操作的時候,它會回溯到它的上一步中間操作,上一步中間操作,又會回溯到上上一步的中間操作,...,直到最初的第一步。


第一次forEach執行的時候,會回溯peek 操作,然後peek會回溯更上一步的limit操作,然後limit會回溯更上一步的peek操作,頂層沒有操作了,開始自上向下開始執行,輸出:A1B1C1 第二次forEach執行的時候,然後會回溯peek 操作,然後peek會回溯更上一步的limit操作,然後limit會回溯更上一步的peek操作,頂層沒有操作了,開始自上向下開始執行,輸出:A2B2C2


... 當第四次forEach執行的時候,然後會回溯peek 操作,然後peek會回溯更上一步的limit操作,到limit的時候,發現limit(3)這個job已經完成,這裡就相當於循環裡面的break操作,跳出來終止循環。


再來看第二段代碼:


IntStream.range(1, 10)
   .peek(x -> System.out.print("\nA" + x))
   .skip(6)
   .peek(x -> System.out.print("B" + x))
   .forEach(x -> System.out.print("C" + x));


輸出為:


A1
A2
A3
A4
A5
A6
A7B7C7
A8B8C8
A9B9C9


第一次forEach執行的時候,會回溯peek操作,然後peek會回溯更上一步的skip操作,skip回溯到上一步的peek操作,頂層沒有操作了,開始自上向下開始執行,執行到skip的時候,因為執行到skip,這個操作的意思就是跳過,下面的都不要執行了,也就是就相當於循環裡面的continue,結束本次循環。輸出:A1


第二次forEach執行的時候,會回溯peek操作,然後peek會回溯更上一步的skip操作,skip回溯到上一步的peek操作,頂層沒有操作了,開始自上向下開始執行,執行到skip的時候,發現這是第二次skip,結束本次循環。輸出:A2


...


第七次forEach執行的時候,會回溯peek操作,然後peek會回溯更上一步的skip操作,skip回溯到上一步的peek操作,頂層沒有操作了,開始自上向下開始執行,執行到skip的時候,發現這是第七次skip,已經大於6了,它已經執行完了skip(6)的job了。這次skip就直接跳過,繼續執行下面的操作。輸出:A7B7C7


...直到循環結束。


一種直白的實現方式

仍然考慮上述求最長字符串的程序,一種直白的流水線實現方式是為每一次函數調用都執一次迭代,並將處理中間結果放到某種數據結構中(比如數組,容器等)。


具體說來,就是調用filter()方法後立即執行,選出所有以A開頭的字符串並放到一個列表list1中,之後讓list1傳遞給mapToInt()方法並立即執行,生成的結果放到list2中,最後遍歷list2找出最大的數字作為最終結果。程序的執行流程如如所示:



這樣做實現起來非常簡單直觀,但有兩個明顯的弊端:


迭代次數多。迭代次數跟函數調用的次數相等。

頻繁產生中間結果。每次函數調用都產生一次中間結果,存儲開銷無法接受。


這些弊端使得效率底下,根本無法接受。如果不使用Stream API我們都知道上述代碼該如何在一次迭代中完成,大致是如下形式:


int longest = 0;
for(String str : strings){
    if(str.startsWith("A")){// 1. filter(), 保留以A開頭的字符串
        int len = str.length();// 2. mapToInt(), 轉換成長度
        longest = Math.max(len, longest);// 3. max(), 保留最長的長度
    }
}


採用這種方式我們不但減少了迭代次數,也避免了存儲中間結果,顯然這就是流水線,因為我們把三個操作放在了一次迭代當中。只要我們事先知道用戶意圖,總是能夠採用上述方式實現跟Stream API等價的功能,但問題是Stream類庫的設計者並不知道用戶的意圖是什麼。


如何在無法假設用戶行為的前提下實現流水線,是類庫的設計者要考慮的問題。


Stream流水線解決方案

我們大致能夠想到,應該採用某種方式記錄用戶每一步的操作,當用戶調用結束操作時將之前記錄的操作疊加到一起在一次迭代中全部執行掉。沿著這個思路,有幾個問題需要解決:


用戶的操作如何記錄?

操作如何疊加?

疊加之後的操作如何執行?

執行後的結果(如果有)在哪裡?

>> 操作如何記錄

注意這裡使用的是「操作(operation)」一詞,指的是「Stream中間操作」的操作,很多Stream操作會需要一個回調函數(Lambda表達式),因此一個完整的操作是<數據來源,操作,回調函數>構成的三元組。


Stream中使用Stage的概念來描述一個完整的操作,並用某種實例化後的PipelineHelper來代表Stage,將具有先後順序的各個Stage連到一起,就構成了整個流水線。跟Stream相關類和接口的繼承關係圖示。



還有IntPipeline, LongPipeline, DoublePipeline沒在圖中畫出,這三個類專門為三種基本類型(不是包裝類型)而定製的,跟ReferencePipeline是並列關係。


圖中Head用於表示第一個Stage,即調用調用諸如Collection.stream()方法產生的Stage,很顯然這個Stage裡不包含任何操作;StatelessOp和StatefulOp分別表示無狀態和有狀態的Stage,對應於無狀態和有狀態的中間操作。


Stream流水線組織結構示意圖如下:



圖中通過Collection.stream()方法得到Head也就是stage0,緊接著調用一系列的中間操作,不斷產生新的Stream。


這些Stream對象以雙向鍊表的形式組織在一起,構成整個流水線,由於每個Stage都記錄了前一個Stage和本次的操作以及回調函數,依靠這種結構就能建立起對數據源的所有操作。這就是Stream記錄操作的方式。


>> 操作如何疊加

以上只是解決了操作記錄的問題,要想讓流水線起到應有的作用我們需要一種將所有操作疊加到一起的方案。你可能會覺得這很簡單,只需要從流水線的head開始依次執行每一步的操作(包括回調函數)就行了。


這聽起來似乎是可行的,但是你忽略了前面的Stage並不知道後面Stage到底執行了哪種操作,以及回調函數是哪種形式。換句話說,只有當前Stage本身才知道該如何執行自己包含的動作。這就需要有某種協議來協調相鄰Stage之間的調用關係。


這種協議由Sink接口完成,Sink接口包含的方法如下表所示:


方法名作用void begin(long size)開始遍曆元素之前調用該方法,通知Sink做好準備。void end()所有元素遍歷完成之後調用,通知Sink沒有更多的元素了。boolean cancellationRequested()是否可以結束操作,可以讓短路操作儘早結束。void accept(T t)遍曆元素時調用,接受一個待處理元素,並對元素進行處理。Stage把自己包含的操作和回調方法封裝到該方法裡,前一個Stage只需要調用當前Stage.accept(T t)方法就行了。


有了上面的協議,相鄰Stage之間調用就很方便了,每個Stage都會將自己的操作封裝到一個Sink裡,前一個Stage只需調用後一個Stage的accept()方法即可,並不需要知道其內部是如何處理的。


當然對於有狀態的操作,Sink的begin()和end()方法也是必須實現的。比如Stream.sorted()是一個有狀態的中間操作,其對應的Sink.begin()方法可能創建一個盛放結果的容器,而accept()方法負責將元素添加到該容器,最後end()負責對容器進行排序。


對於短路操作,Sink.cancellationRequested()也是必須實現的,比如Stream.findFirst()是短路操作,只要找到一個元素,cancellationRequested()就應該返回true,以便調用者儘快結束查找。


Sink的四個接口方法常常相互協作,共同完成計算任務。


實際上Stream API內部實現的的本質,就是如何重寫Sink的這四個接口方法。


有了Sink對操作的包裝,Stage之間的調用問題就解決了,執行時只需要從流水線的head開始對數據源依次調用每個Stage對應的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了。


一種可能的Sink.accept()方法流程是這樣的:


void accept(U u){
    1. 使用當前Sink包裝的回調函數處理u
    2. 將處理結果傳遞給流水線下遊的Sink
}


Sink接口的其他幾個方法也是按照這種[處理->轉發]的模型實現。


下面我們結合具體例子看看Stream的中間操作是如何將自身的操作包裝成Sink以及Sink是如何將處理結果轉發給下一個Sink的。


先看Stream.map()方法:


// Stream.map(),調用該方法將產生一個新的Stream
public final  Stream map(Functionsuper P_OUT, ? extends R> mapper) {
    ...
    return new StatelessOp(this, StreamShape.REFERENCE,
                                 StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
        @Override /*opWripSink()方法返回由回調函數包裝而成Sink*/
        Sink opWrapSink(int flags, Sink downstream) {
            return new Sink.ChainedReference(downstream) {
                @Override
                public void accept(P_OUT u) {
                    R r = mapper.apply(u);// 1. 使用當前Sink包裝的回調函數mapper處理u
                    downstream.accept(r);// 2. 將處理結果傳遞給流水線下遊的Sink
                }
            };
        }
    };
}


上述代碼看似複雜,其實邏輯很簡單,就是將回調函數mapper包裝到一個Sink當中。由於Stream.map()是一個無狀態的中間操作,所以map()方法返回了一個StatelessOp內部類對象(一個新的Stream),調用這個新Stream的opWripSink()方法將得到一個包裝了當前回調函數的Sink。


再來看一個複雜一點的例子。Stream.sorted()方法將對Stream中的元素進行排序,顯然這是一個有狀態的中間操作,因為讀取所有元素之前是沒法得到最終順序的。拋開模板代碼直接進入問題本質,sorted()方法是如何將操作封裝成Sink的呢?sorted()一種可能封裝的Sink代碼如下:


// Stream.sort()方法用到的Sink實現
class RefSortingSink<T> extends AbstractRefSortingSink<T> {
    private ArrayList list;// 存放用於排序的元素
    RefSortingSink(Sinksuper T> downstream, Comparatorsuper T> comparator) {
        super(downstream, comparator);
    }
    @Override
    public void begin(long size) {
        ...
        // 創建一個存放排序元素的列表
        list = (size >= 0) ? new ArrayList((int) size) : new ArrayList();
    }
    @Override
    public void end() {
        list.sort(comparator);// 只有元素全部接收之後才能開始排序
        downstream.begin(list.size());
        if (!cancellationWasRequested) {// 下遊Sink不包含短路操作
            list.forEach(downstream::accept);// 2. 將處理結果傳遞給流水線下遊的Sink
        }
        else {// 下遊Sink包含短路操作
            for (T t : list) {// 每次都調用cancellationRequested()詢問是否可以結束處理。
                if (downstream.cancellationRequested()) break;
                downstream.accept(t);// 2. 將處理結果傳遞給流水線下遊的Sink
            }
        }
        downstream.end();
        list = null;
    }
    @Override
    public void accept(T t) {
        list.add(t);// 1. 使用當前Sink包裝動作處理t,只是簡單的將元素添加到中間列表當中
    }
}


上述代碼完美的展現了Sink的四個接口方法是如何協同工作的:


首先begin()方法告訴Sink參與排序的元素個數,方便確定中間結果容器的的大小;

之後通過accept()方法將元素添加到中間結果當中,最終執行時調用者會不斷調用該方法,直到遍歷所有元素;

最後end()方法告訴Sink所有元素遍歷完畢,啟動排序步驟,排序完成後將結果傳遞給下遊的Sink;

如果下遊的Sink是短路操作,將結果傳遞給下遊時不斷詢問下遊cancellationRequested()是否可以結束處理。

>> 疊加之後的操作如何執行


Sink完美封裝了Stream每一步操作,並給出了[處理->轉發]的模式來疊加操作。這一連串的齒輪已經咬合,就差最後一步撥動齒輪啟動執行。


是什麼啟動這一連串的操作呢?也許你已經想到了啟動的原始動力就是結束操作(Terminal Operation),一旦調用某個結束操作,就會觸發整個流水線的執行。


結束操作之後不能再有別的操作,所以結束操作不會創建新的流水線階段(Stage),直觀的說就是流水線的鍊表不會在往後延伸了。


結束操作會創建一個包裝了自己操作的Sink,這也是流水線中最後一個Sink,這個Sink只需要處理數據而不需要將結果傳遞給下遊的Sink(因為沒有下遊)。對於Sink的[處理->轉發]模型,結束操作的Sink就是調用鏈的出口。


我們再來考察一下上遊的Sink是如何找到下遊Sink的。一種可選的方案是在PipelineHelper中設置一個Sink欄位,在流水線中找到下遊Stage並訪問Sink欄位即可。


但Stream類庫的設計者沒有這麼做,而是設置了一個Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法來得到Sink,該方法的作用是返回一個新的包含了當前Stage代表的操作以及能夠將結果傳遞給downstream的Sink對象。為什麼要產生一個新對象而不是返回一個Sink欄位?


這是因為使用opWrapSink()可以將當前操作與下遊Sink(上文中的downstream參數)結合成新Sink。


試想只要從流水線的最後一個Stage開始,不斷調用上一個Stage的opWrapSink()方法直到最開始(不包括stage0,因為stage0代表數據源,不包含操作),就可以得到一個代表了流水線上所有操作的Sink,用代碼表示就是這樣:


// AbstractPipeline.wrapSink()
// 從下遊向上遊不斷包裝Sink。如果最初傳入的sink代表結束操作,
// 函數返回時就可以得到一個代表了流水線上所有操作的Sink。
final  Sink wrapSink(Sink sink) {
    ...
    for (AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
        sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
    }
    return (Sink) sink;
}


現在流水線上從開始到結束的所有的操作都被包裝到了一個Sink裡,執行這個Sink就相當於執行整個流水線,執行Sink的代碼如下:


// AbstractPipeline.copyInto(), 對spliterator代表的數據執行wrappedSink代表的操作。
final  void copyInto(Sink wrappedSink, Spliterator spliterator) {
    ...
    if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
        wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知開始遍歷
        spliterator.forEachRemaining(wrappedSink);// 迭代
        wrappedSink.end();// 通知遍歷結束
    }
    ...
}


上述代碼首先調用wrappedSink.begin()方法告訴Sink數據即將到來,然後調用spliterator.forEachRemaining()方法對數據進行迭代,最後調用wrappedSink.end()方法通知Sink數據處理結束。邏輯如此清晰。


>> 執行後的結果在哪裡

最後一個問題是流水線上所有操作都執行後,用戶所需要的結果(如果有)在哪裡?首先要說明的是不是所有的Stream結束操作都需要返回結果,有些操作只是為了使用其副作用(Side-effects),比如使用Stream.forEach()方法將結果列印出來就是常見的使用副作用的場景(事實上,除了列印之外其他場景都應避免使用副作用),對於真正需要返回結果的結束操作結果存在哪裡呢?


特別說明:副作用不應該被濫用,也許你會覺得在Stream.forEach()裡進行元素收集是個不錯的選擇,就像下面代碼中那樣,但遺憾的是這樣使用的正確性和效率都無法保證,因為Stream可能會並行執行。大多數使用副作用的地方都可以使用歸約操作更安全和有效的完成。

// 錯誤的收集方式
ArrayList results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
      .forEach(s -> results.add(s));  // Unnecessary use of side-effects!
// 正確的收集方式
Listresults =
     stream.filter(s -> pattern.matcher(s).matches())
             .collect(Collectors.toList());  // No side-effects!


回到流水線執行結果的問題上來,需要返回結果的流水線結果存在哪裡呢?這要分不同的情況討論,下表給出了各種有返回結果的Stream結束操作。


返回類型對應的結束操作booleananyMatch() allMatch() noneMatch()OptionalfindFirst() findAny()歸約結果reduce() collect()數組toArray()

對於表中返回boolean或者Optional的操作(Optional是存放 一個 值的容器)的操作,由於值返回一個值,只需要在對應的Sink中記錄這個值,等到執行結束時返回就可以了。

對於歸約操作,最終結果放在用戶調用時指定的容器中(容器類型通過收集器指定)。collect(), reduce(), max(), min()都是歸約操作,雖然max()和min()也是返回一個Optional,但事實上底層是通過調用reduce()方法實現的。

對於返回是數組的情況,毫無疑問的結果會放在數組當中。這麼說當然是對的,但在最終返回數組之前,結果其實是存儲在一種叫做Node的數據結構中的。Node是一種多叉樹結構,元素存儲在樹的葉子當中,並且一個葉子節點可以存放多個元素。這樣做是為了並行執行方便。關於Node的具體結構,我們會在下一節探究Stream如何並行執行時給出詳細說明。

結語

本文詳細介紹了Stream流水線的組織方式和執行過程,學習本文將有助於理解原理並寫出正確的Stream代碼,同時打消你對Stream API效率方面的顧慮。如你所見,Stream API實現如此巧妙,即使我們使用外部迭代手動編寫等價代碼,也未必更加高效。


註:留下本文所用的JDK版本,以便有考究癖的人考證:


$ java -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) Server VM (build 25.101-b13, mixed mode)

相關焦點

  • "生孩子"也要AA制嗎?
    她是這樣寫的:"我懷孕六個月了,沒上班在家待著,自己也沒多少存款,我老公今天說生孩子的時候花的醫療費用我倆一人出一半,平攤,我很震驚,想了一天都沒想通他怎麼會這樣說,為什麼呢?"看了之後,我也驚著了。難道是自己落伍了,跟不上潮流了嗎?還是現在的夫妻樣樣都實行AA制,包括生孩子呢?左思右想不得其解,於是便將這故事當新聞說與朋友聽。朋友聽後一陣哈哈大笑,我被朋友笑懵了,問他很好笑嗎?
  • Emoji表情符號添72新丁:"打臉"、"流口水","孕婦"...你一定要試試
    蘋果和微軟反對"步槍"emoji制定新的表情符過程中一個值得玩味細節是,作為奧運會運動項目之一的"步槍射擊"emoji最後被踢出了列表。 "病毒式傳播"大神Buzzfeed News的透露的內幕是,只是蘋果公司和微軟反對的結果。這個消息在社交網絡上引起了討論。
  • 華玉振:與"擺地攤"感同身受
    當共和國總理在莊嚴的全國人大會議上,宣布城市管理要為"擺地攤"寬容時,我的眼睛溼了。三十四年前剛畢業的那年夏天,在家鄉小集鎮的稅務所,因拒絕"出外勤"向"地攤"收稅的風波又勾起我的回憶。所長安排我"出外勤"上街。所謂"出外勤",就是上街丶入村丶進廠,挨家挨戶收稅。我從小厭煩稅務官。據父母講太祖父、祖父都曾經因為挑擔擺攤賣杏、賣黃花菜而被稅務官盤剝過。
  • I love You的真正意思不是"我愛你"!你知道嗎?
    知道"Ilove you"是什麼意思嗎?肯定會有人說:"這個啊,地球人都是知道!"我愛你"唄." 是的,我們知道這是一句告白的話,是我們表達愛的方式,它是一句放之四海皆準的宣言,既是世界最美的語言,也是情人之間表達情感最直接的語言.但是,你知道嗎,這8個字母其實是有特殊意義的.當你聽到或當你說出這句話的時候,是否知道它還有更深一層的涵義呢?
  • "記事簿"的提醒
    教師也應該有"記事簿",我開學前曾對家長和孩子們說:"我會每天分享孩子們在學校的生活和學習狀態。"我也是這樣做的,從開學前一天開始,堅持分享。可是昨天太累了,眼睛都睜不開了,只想睡覺。        睡覺前讓我很是愧疚,只因我食言了。最重要的是昨天有讓我非寫不可的事,而我沒有毅力支撐自己不睡。
  • "兒臣" 的帳單與 "額娘" 的神回復火了 !
    兩封信,幽默的語句裡,藏著深深的愛……《兒子的信》額娘:兒臣熬夜複習中,只是深知"父母之愛子,則為之計深遠",你的起點是父母懷抱,你的未來是修齊治平,弘毅致遠。所以額娘才狠心讓你歷練,吾兒可知"有多殘忍有多愛"。當然,吾兒自立並非始於今年今日,是家風使然。額娘一介弱女,尚能十九歲離家三千裡獨自求學,深信吾兒勝之。
  • 你知道嗎?「I love You」的真正意思其實不是 "我愛你"
    肯定會有人說:"這個啊,地球人都是知道!"我愛你"唄是的,我們知道這是一句告白的話,是我們表達愛的方式,它是一句放之四海皆準的宣言,既是世界最美的語言,也是情人之間表達情感最直接的語言. 但是,你知道嗎,這8個字母其實是有特殊意義的.當你聽到或當你說出這句話的時候,是否知道它還有更深一層的涵義呢?
  • "我,36歲,和甜蜜男談了一場戀愛,傾家蕩產!"
    揭秘騙局本文由英語口語(ID:kouyu8)原創,如需轉載請後臺授權。並且隨著"殺豬盤"騙局的逐步升級,一開始只針對單身女性的"殺豬盤",開始轉向大齡男性。今年8月,宿城公安分局龍河派出所成功破獲了一起涉案價值15萬餘元的"殺豬盤"案件,這次被騙的是一位男性。
  • 網絡"遊戲"的泛濫,你孩子的人生是否被偷換了概念?
    人類的生命本質是公平的,但是這種本質的公平已經被"經濟"所打破。這"經濟"當然不僅僅指的是金錢和物質,它當然包含一種無形的、也是比金錢更具威力的"經濟"~那就是"名"。所謂的"名",當然是指那些出了名的人名,統一稱呼為"名人"。名人當然也就不同於一般的人名,他們具備一呼百應的能力。特別是在當下的網絡時代,這名人效應尤為明顯。
  • 自閉患兒幹預路上最重要的一項"關係"
    家有特殊孩讓許多父母總是埋頭於孩子的康復教育,心裡唯一的盼望就是希望孩子可以早日恢復成"正常"的樣子,每天有不同的幹預課程,天天在機構與家裡間不斷的來回波奔。在忙碌中甚至無法關注與特殊孩子間的親子關係。可是,特殊孩子與父母之間的親子關係難道不重要嗎?當然不是。
  • "我談了一場戀愛卻傾家蕩產!"比仙人跳悲劇 10000倍的,居然是這種騙局!
    揭秘騙局本文由英語口語(ID:kouyu8)原創,如需轉載請後臺授權。並且隨著"殺豬盤"騙局的逐步升級,一開始只針對單身女性的"殺豬盤",開始轉向大齡男性。今年8月,宿城公安分局龍河派出所成功破獲了一起涉案價值15萬餘元的"殺豬盤"案件,這次被騙的是一位男性。
  • 新高考"3+1+2"模式下,怎麼選科?
    新高考"3+1+2"模式,把哪些學科救上岸,哪些學科推下水?我認為,談不到把哪個學科"救上岸"或"推下水",改革是大勢所趨,高考也不例外。新高考模式下,對語文、數學、外語沒有什麼影響,突出了物理、歷史兩個科目,對化學科目是一個弱化。
  • 從心理學上看程璐思文離婚:兩個"好人"是很難維繫婚姻的
    這個回應問題的視頻我看了好幾遍,可以說通篇都是"乾貨"。兩人思路清晰,真誠而又直率地做了各自的回答。與其說他們是在給觀眾做交待。更像是再次給自己的婚姻做總結。倆人都是脫口秀演員,在節目中慣性職業素養使然"輕鬆"的開聊。但卻無法掩飾她們對分開的傷感和遺憾。
  • 戰"疫"響了,情感深了!
    2020,註定是多事之秋,註定是不平凡的一年,因為新年的第一天給大家特殊的拜年方式"居家拜年"。作為一名普通的老師我聽從安排,作為一名黨員我絕對服從命令聽指揮。 形式越來越緊,學校要求上報的數據越來越多,越來越細,所以每天都要在班級群裡發不同的通知,分享不同的文件精神,不停地催促家長朋友回覆信息,填寫表格……有時感覺家長也挺累的但是沒有辦法,特殊時期不能掉鏈子。
  • 新高考"3+1+2"選科之人間清醒
    大家應該也都看了不少關於新高一選科的指導文章和資料,那明確選什麼學科了嗎?還是腦瓜子嗡嗡嗡呢?其實對大部份人來說光是靠一些數據、百分佔比、競爭大小等還是不太明白自己該怎麼選。下面樂嚴會幫你介紹一個選科的思路供你參考,包括以下幾個環節。
  • 遠離心理疾病,就要大膽的和這類人說"不"
    它的可怕之處就是指通過"學習"而形成的一種對現實的無望和無可奈何的行為和心理狀態。我們要警醒失敗對自己的影響,同時一定要時刻的記住,失敗都是暫時性的,我們無法預測未來,現在覺得困難事,在明天都可能會有轉機,一切皆有可能。培養這種樂觀的去看待事情的心態,就能避免"習得性無助"心理的形成。
  • 醉了,這個美國女生竟然因為"愛吃披薩"被耶魯大學錄取!
    美國許多青少年喜歡吃披薩,但田納西州一個聰明的女學生在大學申請短文中,闡述自己對披薩的喜愛,從而獲得耶魯大學的錄取。3月,在主題為"我喜歡做的事"大學申請短文中,她撰寫了一篇自己最喜歡從"Papa John’s"訂購披薩的文章,吸引了被耶魯大學招生官員的注意。威廉士表示,當時在看到這個作文題目時,她只是寫下頭腦中閃現的第一個念頭。不過,威廉士的成績名列畢業生排名前10位,她還參加了數個課外活動社團,以及社會上的一些團體。
  • 防疫情宅家,延期開學,你這樣管教"神獸"也許是上策!
    更有甚者,還要面對一個撓頭事,那就是對"神獸"管理教育問題!",不少朋友調侃自家"神獸"作呀,打遊戲上網熬夜,早上不起床,上網課隔靴撓癢不頂事!大人天天手機不離手,幾天不洗漱,床鋪亂糟糟,"神獸"會暗自竊喜:彼此彼此!家長要克服這些問題,做個愛看書學習、愛生活愛乾淨的"上進生",裝樣子也要象。
  • 記住 |「車牌號」英文可不是"car number"
    記住 | 「車牌號」英文可不是"car number"! 「熟詞偏義」是英語中常見的情況,看似每個單詞都認識,連在一起就不知道是什麼意思A、傳統B、落伍大家答對了嗎?留言答案喲~上期回顧「hand up」表達什麼意思?
  • 兩歲半寶寶和她的動物"牧場",二十多隻動物友好相處
    最近幾天,又玩上了動物"牧場"的小遊戲,常常把我們逗的哈哈大笑。",都特別忙,她要讓動物們排好隊,還要給它們分配食物,忙起來叫她也聽不到,喊她也喊不走。看"牧場"裡的動物們排著整整齊齊的隊伍在吃飯呢,看起來聽話的很。