環保署長張子敬、嘉義縣政府秘書長羅木興與張根穆,共同宣布溪口鄉成為全國第一個畜牧糞尿資源示範鄉

環保署長張子敬、嘉義縣政府秘書長羅木興與張根穆,共同宣布溪口鄉成為全國第一個畜牧糞尿資源示範鄉。嘉義縣府環保局長張根穆指出,溪口鄉目前人口數約1萬2000多人,飼養豬隻約7萬2000多頭,鄉內豬比人多,豬隻排放的廢棄物,令人困擾。

張子敬說,有別於過去處理畜牧業排放糞水需耗費許多心力,現今改變處理方式,先將豬糞做沼氣發電,剩餘的沼液沼渣經妥善處理後,成為天然有機肥取代傳統化學肥料,創造循環經濟,農業永久發展。

張根穆說,溪口鄉內種植經濟作物「紫色蘆筍」一公斤高達800元,此農作物的特性需要「吃重肥」。每15日需施灑1包肥料,若改用畜牧糞尿產出的肥料,以鄉內種植約2.6公傾計算,每年約可省下39萬元的肥料費,未來將協助農民擴大紫色蘆筍栽種面積。

本站聲明:網站內容來源於https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

飲水機設備有哪些?

※如何選購橡膠製品橡膠按鍵規格有哪些?該如何應用在商品上?  

AVX DistributorAVX TPSAVX鉭質電容器 規格有哪些?各別作用在於?

nbr乳膠手套可適用在什麼環境?

※空壓機何時可換油? 空壓機保養的正確觀念與維護 !

真空封口機該不該買?使用心得分享

根據通路業者針對超過千名消費者的進行調查結果顯示,近八成的民眾在意雞蛋的生產方式

根據通路業者針對超過千名消費者的進行調查結果顯示,近八成的民眾在意雞蛋的生產方式,而有近九成的民眾擔心用藥及藥物殘留、生產環境髒亂的問題,然而受限於目前的產銷狀況,民眾很難清楚知道雞蛋背後的生產真相。

家樂福企業社會責任暨溝通總監蘇小真表示,為了發揮企業社會責任,特與非營利組織台灣動物社會研究會共同推動「食品轉型——從蛋開始」的計畫,經過一年的時間親自拜訪無數牧場,首款自有品牌非籠飼雞蛋誕生,這也是亞洲通路第一盒自有品牌非籠飼雞蛋,讓家樂福「食物轉型——從蛋開始」的非籠飼雞蛋承諾再向前邁進一步。

什麼是「非籠飼」雞蛋?指的是以「放牧、平飼」飼養,重視母雞天性與身心需求的飼養方法,讓母雞可以安心棲息,產下優質健康蛋。而已逐漸被國際淘汰,視為漠視經濟動物福利、殘虐產蛋母雞身心的「格子籠」(battery cage)飼養方式,則是一種將母雞一輩子三到四隻囚禁在約莫A4大小的窄小籠子內,剝奪母雞安全就巢產蛋、沙浴洗澡、棲息高處、展翅梳理羽翼的天性與本能,讓母雞在不見天日、擁擠不堪的鐵籠中成為產蛋機器的飼養法。

本站聲明:網站內容來源於https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道台中食品倉有哪些?  

堆高機基本駕駛指導教學(影片)

廢氣洗滌塔,叫得動, 找得到的專業廠商‎

※市面十大品牌封口機!該如何選購?

塑膠射出成型加工商品有哪些?

交通部公路總局宣布,「西濱快速公路八棟寮至九塊厝工程碳管理及環境減輕策略」

交通部公路總局宣布,「西濱快速公路八棟寮至九塊厝工程碳管理及環境減輕策略」榮獲國際道路協會(International Road Federation,IRF)全球唯一的2019年度「全球道路成就獎」(Global Road Achievement Awards, GRAA)環境減輕類首獎。

這次的獲獎工程是台灣第一個獲得ISO/TS 14067碳足跡查證聲明的公路工程,從規劃設計階段便導入環境減輕措施,如綠化植披、地下水入滲、既有紅樹林保護與棲地補償等,並檢討工程施作與後續車流轉移所產生的碳排放量。

公路總局也分析不同設計對碳排放的影響,如路線調整、替代材料、結構體減量等手段。經透過實際盤查成果發現,工程材料碳排放量佔比雖高達94%,機具與運輸各約佔6%。但透過以飛灰、爐石粉取代水泥、加勁擋土牆取代一般擋土牆、機具運用最佳化、場電取代柴油發電機及多孔隙瀝青混凝土取代密級配瀝青混凝土等措施,並配合交通量的轉移、樹木與植披的種植,可減少約164座大安森林公園年吸附量,發揮減碳最大效益。

公路總局說,希望藉由西濱快速公路工程卓越的成果,讓台灣工程建設得以躍登國際舞台外。

本站聲明:網站內容來源於https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※使用真空封口機常見問題?

※高價位跟低價位的示波器又有何差異?

※各大百貨每波促銷贈品活動,限量知名LOGOL型資料夾,獨家販售中!!

※哪裡有合版印刷優惠,尋找L夾客製化印刷廠商?

※如何正確使用開飲機?

洗滌塔運作原理介紹

南投縣環保局為遏止工廠偷排廢氣,降低空氣污染

南投縣環保局為遏止工廠偷排廢氣,降低空氣污染,運用「無人機攜帶攝影機」或「紅外線熱顯像儀」巡查,並於南崗工業區制高點裝設監視器。陸空雙管齊下監測,全天候、全方位掌握工廠排煙狀況。

環保局指出,監視器易有監測死角或距離較遠,出現難以判​​斷舉證情形,該局再輔以空拍機(UAV)攜帶攝影機或紅外線熱顯像儀,進行機動式污染監測,直接於工廠上方進行蒐證,有效判斷污染排放源,已執行六架次無人機空拍作業。

透過監視器及空拍機雙管齊下交互監測、蒐證,查獲金屬鑄造業及食品油炸業等四家工廠有排放廢氣情形,其中兩家未設置有效處理空氣污染物防制設備,將依違反空污法開罰十萬元。

本站聲明:網站內容來源於https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※一般小吃店常見使用的nbr耐油手套,是否有含耐酸鹼作用?

迴轉式空壓機性能介紹 !

海線倉儲類人員薪水待遇最新情報

※(全省)堆高機租賃保養一覽表

※高效率洗滌塔活性碳設備,能去除多少有機溶劑?

射出成型技師工作甘苦談

SSE圖像算法優化系列三十:GIMP中的Noise Reduction算法原理及快速實現。

  GIMP源代碼鏈接:

  GEGL相關代碼鏈接:

  最近因為要研究下色溫算法,順便下載了最新的GIMP軟件,色溫算法倒是找到了(有空單獨來講下),也順便看看GIMP都有些什麼更新,嗯,更新還是蠻多的,界面UI上有很多改動,有些已經改的面目全非了。隨便瞄了一下Enhance菜單,發現裏面有一個Nosie Reduction算法,試了下,還有點效果。於是在github上下載了GIMP的源代碼,可是在源代碼里搜索相關的關鍵詞確沒有發現任何的相關代碼,後來才發現很多東西都有個GEGL關鍵詞,結果一百度,原來他是一個單獨的軟件包,於是有下載了GEGL的源代碼,終於在gegl-master\operations\common\裏面看到了noise-reduction.c文件。

  其核心的代碼如下:

static void
noise_reduction (float *src_buf,     /* source buffer, one pixel to the left
                                        and up from the starting pixel */
                 int    src_stride,  /* stridewidth of buffer in pixels */
                 float *dst_buf,     /* destination buffer */
                 int    dst_width,   /* width to render */
                 int    dst_height,  /* height to render */
                 int    dst_stride)  /* stride of target buffer */
{
  int c;
  int x,y;
  int dst_offset;

#define NEIGHBOURS 8
#define AXES       (NEIGHBOURS/2)

#define POW2(a) ((a)*(a))
/* core code/formulas to be tweaked for the tuning the implementation */
#define GEN_METRIC(before, center, after) \
                   POW2((center) * 2 - (before) - (after))

/* Condition used to bail diffusion from a direction */
#define BAIL_CONDITION(new,original) ((new) > (original))

#define SYMMETRY(a)  (NEIGHBOURS - (a) - 1) /* point-symmetric neighbour pixel */

#define O(u,v) (((u)+((v) * src_stride)) * 4)
  int   offsets[NEIGHBOURS] = {  /* array of the relative distance i float
                                  * pointers to each of neighbours
                                  * in source buffer, allows quick referencing.
                                  */
              O( -1, -1), O(0, -1), O(1, -1),
              O( -1,  0),           O(1,  0),
              O( -1,  1), O(0, 1),  O(1,  1)};
#undef O

  dst_offset = 0;
  for (y=0; y<dst_height; y++)
    {
      float *center_pix = src_buf + ((y+1) * src_stride + 1) * 4;
      dst_offset = dst_stride * y;
      for (x=0; x<dst_width; x++)
        {
          for (c=0; c<3; c++) /* do each color component individually */
            {
              float  metric_reference[AXES];
              int    axis;
              int    direction;
              float  sum;
              int    count;

              for (axis = 0; axis < AXES; axis++)
                { /* initialize original metrics for the horizontal, vertical
                     and 2 diagonal metrics */
                  float *before_pix  = center_pix + offsets[axis];
                  float *after_pix   = center_pix + offsets[SYMMETRY(axis)];

                  metric_reference[axis] =
                    GEN_METRIC (before_pix[c], center_pix[c], after_pix[c]);
                }

              sum   = center_pix[c];
              count = 1;

              /* try smearing in data from all neighbours */
              for (direction = 0; direction < NEIGHBOURS; direction++)
                {
                  float *pix   = center_pix + offsets[direction];
                  float  value = pix[c] * 0.5 + center_pix[c] * 0.5;
                  int    axis;
                  int    valid;

                  /* check if the non-smoothing operating check is true if
                   * smearing from this direction for any of the axes */
                  valid = 1; /* assume it will be valid */
                  for (axis = 0; axis < AXES; axis++)
                    {
                      float *before_pix = center_pix + offsets[axis];
                      float *after_pix  = center_pix + offsets[SYMMETRY(axis)];
                      float  metric_new =
                             GEN_METRIC (before_pix[c], value, after_pix[c]);

                      if (BAIL_CONDITION(metric_new, metric_reference[axis]))
                        {
                          valid = 0; /* mark as not a valid smoothing, and .. */
                          break;     /* .. break out of loop */
                        }
                    }
                  if (valid) /* we were still smooth in all axes */
                    {        /* add up contribution to final result  */
                      sum += value;
                      count ++;
                    }
                }
              dst_buf[dst_offset*4+c] = sum / count;
            }
          dst_buf[dst_offset*4+3] = center_pix[3]; /* copy alpha unmodified */
          dst_offset++;
          center_pix += 4;
        }
    }
}

   這個代碼看上去比較混亂,沒辦法,大型軟件沒有哪一個代碼看上去能讓人省心的,而且也不怎麼講究效率,我測試了一個3K*2K的彩色圖,在GIMP里大概在4S左右處理完成,屬於很慢的了,看這個代碼,也知道大概有width * height * 3 * 8 * 4 * Iter個循環,計算量確實是相當的大。

  我試着嘗試優化這個算法。

  優化的第一步是弄明白算法的原理,在GIMP的UI界面上可當鼠標停留在Noise Reduction菜單上時,會出現Anisotroic smoothing operation字樣,所以初步分析他是屬於各項異性擴散類的算法。稍微分析下代碼,也確實是。明顯這屬於一個領域濾波器,對每一個像素,求取其3*3領域內的累加值,但是3*3領域的權重並不是平均分佈或者是高斯分佈,而是和領域的值有關的,如果領域的值是一個邊緣點,他將不參与到累加中,權重為0,否則權重為1。

  具體一點,對於領域里的任何一點,我們先求取其和中心點的平均值,對應 float value = pix[c] * 0.5 + center_pix[c] * 0.5; 這條語句,然後計算這個值在2個45度對角線及水平和垂直方向的梯度(4個梯度值)是否比中心點在四個方向的梯度都小,如果都小,說明這個領域點不屬於邊緣點,可以往這個方向擴散,把他計入到統計值中,如果有任何一個方向小了,則不參与最終的計算。

  上面的過程可以看成是標準的各項異性擴散的特殊在特殊處理,他具有各項異性擴散的特性,也具有一些特殊性。

  下一步,稍微分析下最簡單的優化方法。第一,我們知道,在大循環里一般不建議嵌套入小的循環,這樣是很低效的。我們觀察到上面代碼里的

      for (c=0; c<3; c++) /* do each color component individually */

  這個語句主要是為了方便表達3通道的處理的方便,但是其實三通道之間的處理時沒有任何聯繫的,對於這樣的算法,很明顯,我們可以一次性當然處理R G B R G B R G B ,而不需要像GIMP這個代碼這樣按照 RRR  GGG  BBB這樣的順序來寫,GIMP這種寫法浪費了很多CPU的CACHE,畢竟R和G和B在內存類分佈本來就是連續的。這樣就減少了一個小循環。

  第二個優化的點是,對於普通的圖像數據,我們可以考慮不用浮點數來處理,畢竟上述計算里只有*0.5這樣的浮點操作,我們考慮將原先的圖像數據放大一定的倍數,然後用整形來玩,在處理完后,在縮小到原來的範圍,比如使用short類型應該就足夠了,我把數據放大16倍或者32倍,甚至8倍應該都能獲得足夠的精度。

  第三個優化點,程序中是使用的Pow來判斷梯度的大小的,其實可以不用,直接使用絕對值的結果和Pow是完全一樣的,而絕對值的計算量比pow要小很多,對於整數則更為如此(還可以不考慮pow數據類型的改變,比如short的絕對值還是short類型,但是其pow可能就需要用int來表示了,這在SIMD優化會產生不同的結果)。

  第四個優化點是 for (axis = 0; axis < AXES; axis++)這個小循環我們應該把它直接展開。

  第五點,我們還可以考慮我在其他文章里提到的支持Inplace操作的方式,這樣noise_reduction這個函數的輸入和輸出就可以是同一個內存。

  第六點還有小點上的算法改進,比如一些中間計算沒必要重複進行,有些可以提到外部來等。

  綜合上面的描述,我整理除了一個優化的C語言版本的程序,如下所示:

void IM_AnisotropicDiffusion3X3(short *Src, short *Dest, int Width, int Height, int SplitPos, int Stride)
{
    int Channel = Stride / Width;

    short *RowCopy = (short *)malloc((Width + 2) * 3 * Channel * sizeof(short));
    short *First = RowCopy;
    short *Second = RowCopy + (Width + 2) * Channel;
    short *Third = RowCopy + (Width + 2) * 2 * Channel;
    memcpy(Second, Src, Channel * sizeof(short));
    memcpy(Second + Channel, Src, Width * Channel * sizeof(short));                                                    //    拷貝數據到中間位置
    memcpy(Second + (Width + 1) * Channel, Src + (Width - 1) * Channel, Channel * sizeof(short));

    memcpy(First, Second, (Width + 2) * Channel * sizeof(short));                                                    //    第一行和第二行一樣

    memcpy(Third, Src + Stride, Channel * sizeof(short));                                                            //    拷貝第二行數據
    memcpy(Third + Channel, Src + Stride, Width * Channel* sizeof(short));
    memcpy(Third + (Width + 1) * Channel, Src + Stride + (Width - 1) * Channel, Channel* sizeof(short));

    for (int Y = 0; Y < Height; Y++)
    {
        short *LinePD = Dest + Y * Stride;
        if (Y != 0)
        {
            short *Temp = First; First = Second; Second = Third; Third = Temp;
        }
        if (Y == Height - 1)
        {
            memcpy(Third, Second, (Width + 2) * Channel * sizeof(short));
        }
        else
        {
            memcpy(Third, Src + (Y + 1) * Stride, Channel * sizeof(short));
            memcpy(Third + Channel, Src + (Y + 1) * Stride, Width * Channel * sizeof(short));                            //    由於備份了前面一行的數據,這裏即使Src和Dest相同也是沒有問題的
            memcpy(Third + (Width + 1) * Channel, Src + (Y + 1) * Stride + (Width - 1) * Channel, Channel * sizeof(short));
        }
        for (int X = 0; X < SplitPos * Channel; X++)
        {
            short LT = First[X], T = First[X + Channel], RT = First[X + 2 * Channel];
            short L = Second[X], C = Second[X + Channel], R = Second[X + 2 * Channel];
            short LB = Third[X], B = Third[X + Channel], RB = Third[X + 2 * Channel];
            short LT_RB = LT + RB,    RT_LB = RT + LB;
            short T_B = T + B,        L_R = L + R,        C_C = C + C;
            short Dist1 = IM_Abs(C_C - LT_RB),        Dist2 = IM_Abs(C_C - T_B);
            short Dist3 = IM_Abs(C_C - RT_LB),        Dist4 = IM_Abs(C_C - L_R);
            
            int Sum = C_C, Amount = 2;

            short LT_C = LT + C;
            if ((IM_Abs(LT_C - LT_RB) < Dist1) && (IM_Abs(LT_C - T_B) < Dist2) && (IM_Abs(LT_C - RT_LB) < Dist3) && (IM_Abs(LT_C - L_R) < Dist4))
            {
                Sum += LT_C;
                Amount += 2;
            }
            short T_C = T + C;
            if ((IM_Abs(T_C - LT_RB) < Dist1) && (IM_Abs(T_C - T_B) < Dist2) && (IM_Abs(T_C - RT_LB) < Dist3) && (IM_Abs(T_C - L_R) < Dist4))
            {
                Sum += T_C;
                Amount += 2;
            }
            short RT_C = RT + C;
            if ((IM_Abs(RT_C - LT_RB) < Dist1) && (IM_Abs(RT_C - T_B) < Dist2) && (IM_Abs(RT_C - RT_LB) < Dist3) && (IM_Abs(RT_C - L_R) < Dist4))
            {
                Sum += RT_C;
                Amount += 2;
            }
            short L_C = L + C;
            if ((IM_Abs(L_C - LT_RB) < Dist1) && (IM_Abs(L_C - T_B) < Dist2) && (IM_Abs(L_C - RT_LB) < Dist3) && (IM_Abs(L_C - L_R) < Dist4))
            {
                Sum += L_C;
                Amount += 2;
            }
            short R_C = R + C;
            if ((IM_Abs(R_C - LT_RB) < Dist1) && (IM_Abs(R_C - T_B) < Dist2) && (IM_Abs(R_C - RT_LB) < Dist3) && (IM_Abs(R_C - L_R) < Dist4))
            {
                Sum += R_C;
                Amount += 2;
            }
            short LB_C = LB + C;
            if ((IM_Abs(LB_C - LT_RB) < Dist1) && (IM_Abs(LB_C - T_B) < Dist2) && (IM_Abs(LB_C - RT_LB) < Dist3) && (IM_Abs(LB_C - L_R) < Dist4))
            {
                Sum += LB_C;
                Amount += 2;
            }
            short B_C = B + C;
            if ((IM_Abs(B_C - LT_RB) < Dist1) && (IM_Abs(B_C - T_B) < Dist2) && (IM_Abs(B_C - RT_LB) < Dist3) && (IM_Abs(B_C - L_R) < Dist4))
            {
                Sum += B_C;
                Amount += 2;
            }
            short RB_C = RB + C;
            if ((IM_Abs(RB_C - LT_RB) < Dist1) && (IM_Abs(RB_C - T_B) < Dist2) && (IM_Abs(RB_C - RT_LB) < Dist3) && (IM_Abs(RB_C - L_R) < Dist4))
            {
                Sum += RB_C;
                Amount += 2;
            }
            LinePD[X] = Sum / Amount;
        }
    }
    free(RowCopy);
}

  調用函數

int IM_ReduceNoise(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int SplitPos,  int Strength)
{
    int Channel = Stride / Width;
    if ((Src == NULL) || (Dest == NULL))                        return IM_STATUS_NULLREFRENCE;
    if ((Width <= 0) || (Height <= 0))                            return IM_STATUS_INVALIDPARAMETER;
    if ((Channel != 1) && (Channel != 3))                         return IM_STATUS_INVALIDPARAMETER;

    Strength = IM_ClampI(Strength, 1, 10);
    SplitPos = IM_ClampI(SplitPos, 0, Width);
    int Status = IM_STATUS_OK;
    short *Temp = (short *)malloc(Height * Stride * sizeof(short));
    if (Temp == NULL)    return IM_STATUS_OUTOFMEMORY;
    for (int Y = 0; Y < Height * Stride; Y++)
    {
        Temp[Y] = Src[Y] << 3;
    }
    for (int Y = 0; Y < Strength; Y++)
    {
        IM_AnisotropicDiffusion3X3(Temp, Temp, Width, Height, SplitPos, Stride);
    }
    for (int Y = 0; Y < Height * Stride; Y++)
    {
        Dest[Y] = Temp[Y] >> 3;
    }
    free(Temp);
    return IM_STATUS_OK;
}

  是不是看起來比上面的GIMP得要舒服些,而且中間也大概只要原始圖像2倍的一個臨時內存了。在速度和內存佔用方面都前進了很多。

  我測試前面提到的那副3K*2K的圖像,耗時要7S多,但是我測試表面GIMP用了多核的,如果論單核,我這裏的速度要比他快2倍多。

  很明顯,這個速度是不可以接受的,我們需要繼續優化。

      我還是老套路,使用SIMD指令做處理,看到上面的代碼,其實真的覺得好容易改成SIMD的。

     short LT_RB = LT + RB,    RT_LB = RT + LB;
     short T_B = T + B,        L_R = L + R,        C_C = C + C;
     short Dist1 = IM_Abs(C_C - LT_RB),        Dist2 = IM_Abs(C_C - T_B);
     short Dist3 = IM_Abs(C_C - RT_LB),        Dist4 = IM_Abs(C_C - L_R);
 
這些加減絕對值都有完全對應的SSE指令。 _mm_add_epi16、 _mm_sub_epi16、_mm_abs_epi16,基本上就是照着寫。
  稍微複雜一點就是這裏:
  if ((IM_Abs(LT_C - LT_RB) < Dist1) && (IM_Abs(LT_C - T_B) < Dist2) && (IM_Abs(LT_C - RT_LB) < Dist3) && (IM_Abs(LT_C - L_R) < Dist4))
   {
     Sum += LT_C;
    Amount += 2;
   }
  在C語言里,這裏判斷會進行短路計算,即如果前一個條件已經不滿足了,後續的計算就不會進行。但是在SIMD指令里,是沒有這樣的機制的。我們只能全部計算,然後在通過某一種條件組合。
  在合理,要實現符合條件就進行累加,不符合條件就不做處理的需求,我們需要稍作修改,即不符合條件不是不做處理,而是加0,加0對結果沒有影響的。主要藉助下面的_mm_blendv_epi8來實現。
    __m128i LT_C = _mm_add_epi16(LT, C);
    Flag1 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, LT_RB)), Dist1);        //    只能全部都計算,但還是能提速
    Flag2 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, T_B)), Dist2);
    Flag3 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, RT_LB)), Dist3);
    Flag4 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, L_R)), Dist4);
    Flag = _mm_and_si128(_mm_and_si128(Flag1, Flag2), _mm_and_si128(Flag3, Flag4));
    Sum = _mm_adds_epu16(Sum, _mm_blendv_epi8(Zero, LT_C, Flag));
    Amount = _mm_adds_epu16(Amount, _mm_blendv_epi8(Zero, Two, Flag));

  注意到我們這裏用到了_mm_adds_epu16,無符號的16位加法,這是因為我需要盡量的提速,因此需要減少類型轉換的次數。同時,我們看到在統計累加值時,我們並沒有求平均值,而是直接用的累加值,這樣理論上最大的累加值就是 255 * n * (8 + 1) * 2 < 65535, 這樣n最大能取15,但是15不是個好數據,在放大和縮小都不能用移位來實現,所以我最後取得放大係數為8。

  另外,在最後還有個16位的整數的除法問題,這個沒有辦法,SSE指令沒有提供整數的除法計算方法,還只能轉換到浮點后,再次轉換回來。

  這樣用SSE處理后,還是同一幅測試圖像,在同一台PC上速度能提升到400ms(4次迭代),比之前的普通的C語言提高了約17倍的速度。

  在現代CPU中,具有AVX2指令集已經是很普遍的了,單AVX2能同時處理256字節的數據,比SSE還要多一倍,我也常使用AVX2進行優化處理,速度能達到250ms,相當於普通C語言的28倍之多(但是AVX編程里有很多坑,這些坑都拜AVX不是完全的按照SSE的線性擴展導致的,這個後續有時間我單獨提出)。

  經過測試,1080P的圖像使用4次迭代大約需要80ms,3次迭代55ms,2次迭代月40ms,也就是說前面的一些方法和縮小所使用的時間幾乎可以忽略。

  選了幾幅有特點的圖進行了去燥測試,其中分界線左側的位處理的效果,右側為未處理的。

 

  但是,這個算法也還是不是很好,他對於圖像容易出現輕微的油畫效果,對於一些細節特別豐富的圖像非常明顯,比如下圖:

  這個應該是不太可以接受的,也許可以通過修改部分權重的規則來改變這個現象。這個屬於後期研究的問題了。

     另外,在GIMP里也提供了這個算法的OPENCL實現,有興趣的可以源代碼里找一找,不曉得速度怎麼樣。

  本文Demo下載地址:  ,見其中的Denoise -> Anisotroic Diffusion 菜單。

  寫博不易,歡迎土豪打賞讚助。

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

別翻了,這篇文章絕對讓你深刻理解java類的加載以及ClassLoader源碼分析【JVM篇二】

目錄

@
前言
你是否真的理解java的類加載機制?點進文章的盆友不如先來做一道非常常見的面試題,如果你能做出來,可能你早已掌握並理解了java的類加載機制,若結果出乎你的意料,那就很有必要來了解了解java的類加載機制了。代碼如下

package com.jvm.classloader;

class Father2{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son2 extends Father2{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitativeUseTest2 {
    public static void main(String[] args) {

       System.out.println(Son2.strSon);
    }
}
運行結果:
        Father靜態代碼塊
        Son靜態代碼塊
        HelloJVM_Son

嗯哼?其實上面程序並不是關鍵,可能真的難不倒各位,不妨做下面一道面試題可好?如果下面這道面試題都做對了,那沒錯了,這篇文章你就不用看了,真的。

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe靜態代碼塊");
    }
}

class Father extends YeYe{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather); 
    }
}

各位先用“畢生所學”來猜想一下運行的結果是啥…

注意了…
注意了…
注意了…

運行結果:
    YeYe靜態代碼塊
    Father靜態代碼塊
    HelloJVM_Father

是對是錯已經有個數了吧,我就不拆穿各位的小心思了…

以上的面試題其實就是典型的java類的加載問題,如果你對Java加載機制不理解,那麼你可能就錯了上面兩道題目的。這篇文章將通過對Java類加載機制的講解,讓各位熟練理解java類的加載機制。

其實博主還是想在給出一道題,畢竟各位都已經有了前面兩道題的基礎了,那麼請看代碼:

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe靜態代碼塊");
    }
}

class Father extends YeYe{
    public final static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather);
    }
}

注意了
注意了
注意了

運行結果:HelloJVM_Father

衝動的小白童鞋看到了運行結果,果斷的註銷了博客賬戶….

1、什麼是類的加載(類初始化)

JVM重要的一個領域:類加載

當程序主動使用某個類時,如果該類還未被加載到內存中,則JVM會通過加載、連接、初始化3個步驟來對該類進行初始化。如果沒有意外,JVM將會連續完成3個步驟,所以有時也把這個3個步驟統稱為類加載或類初始化。

而類加載必然涉及類加載器,下面我們先來了解一下類的加載。

類的加載(類初始化):

1、在java代碼中,類型加載連接、與初始化過程都是在程序運行期間完成的(類從磁盤加載到內存中經歷的三個階段)【牢牢記在心裏】

 

2、提供了更大的靈活性,增加了更多的可能性

雖然上面的第一句話非常簡短,但是蘊含的知識量卻是巨大的!包含兩個重要的概念:

1、類型

定義的類、接口或者枚舉稱為類型而不涉及對象,在類加載的過程中,是一個創建對象之前的一些信息

2、程序運行期間

程序運行期間完成典型例子就是動態代理,其實很多語言都是在編譯期就完成了加載,也正因為這個特性給Java程序提供了更大的靈活性,增加了更多的可能性

1、1.類加載注意事項

1、類加載器並不需要等到某個類被 “首次主動使用” 時再加載它~關於首次主動使用這個重要概念下文將講解~
2、JVM規範允許類加載器在預料某個類將要被使用時就預先加載它
3、如果在預先加載的過程中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。

首先給各位打個預防針:可能沒有了解過JVM的童鞋可能看的很蒙,感覺全是理論的感覺,不勉強一字一句的“死看”,只要達到一種概念印象就好!等到有一定理解認識之後再回頭看一遍就好很多了,畢竟學習是一種循進漸進的過程,記住沒有捷徑!

2、類的生命周期

從上圖可知,類從被加載到虛擬機內存開始,到卸載出內存為止,它的整個生命周期包括 7 個階段,而驗證、準備、解析 3 個階段統稱為連接。

加載、驗證、準備、初始化和卸載這 5 個階段的順序是固定確定的,類的加載過程必須按照這種順序開始(注意是“開始”,而不是“進行”),而解析階段則不一定:它在某些情況下可以在初始化后再開始,這是為了支持 Java 語言的運行時綁定【也就是java的動態綁定/晚期綁定】。

2、1.加載

在上面已經提到過,加載階段是類加載的第一個階段!類的加載過程就是從加載階段開始~

加載階段指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個 java.lang.Class對象(JVM規範並未說明Class對象位於哪裡,HotSpot虛擬機將其放在方法區中),用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的 Class對象, Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

Class對象是存放在堆區的,不是方法區,這點很多人容易犯錯。類的元數據才是存在方法區的。【元數據並不是類的Class對象。Class對象是加載的最終產品,類的方法代碼,變量名,方法名,訪問權限,返回值等等都是在方法區的】

JDK7創建Class實例存在堆中;因為JDK7中JavaObjectsInPerm參數值固定為false。
JDK8移除了永久代,轉而使用元空間來實現方法區,創建的Class實例依舊在java heap(堆)中

編寫一個新的java類時,JVM就會幫我們編譯成class對象,存放在同名的.class文件中。在運行時,當需要生成這個類的對象,JVM就會檢查此類是否已經裝載內存中。若是沒有裝載,則把.class文件裝入到內存中。若是裝載,則根據class文件生成實例對象。

怎麼理解Class對象與new出來的對象之間的關係呢?

new出來的對象以car為例。可以把carClass類看成具體的一個人,而new car則是人物映像,具體的一個人(Class)是唯一的,人物映像(new car)是多個的。鏡子中的每個人物映像都是根據具體的人映造出來的,也就是說每個new出來的對象都是以Class類為模板參照出來的!為啥可以參照捏?因為Class對象提供了訪問方法區內的數據結構的接口哇,上面提及過了喔!

算了參照下面這張圖理解吧,理解是其次,重點是話說這妹砸蠻好看的。

總結:
加載階段簡單來說就是:
.class文件(二進制數據)——>讀取到內存——>數據放進方法區——>堆中創建對應Class對象——>並提供訪問方法區的接口

相對於類加載的其他階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。

加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,而且在Java堆中也創建一個 java.lang.Class類的對象,這樣便可以通過該對象訪問方法區中的這些數據。

加載.calss文件的方式:
類的加載由類加載器完成,類加載器通常由JVM提供,這些類加載器也是前面所有程序運行的基礎,JVM提供的這些類加載器通常被稱為系統類加載器。除此之外,開發者可以通過繼承ClassLoader基類來創建自己的類加載器。通過使用不同的類加載器,可以從不同來源加載類的二進制數據,二進制數據通常有如下幾種來源:

(1)從本地系統中直接加載
(2)通過網絡下載.class文件
(3)從zip,jar等歸檔文件中加載.class文件
(4)從專用數據庫中提取.class文件
(5)將java源文件動態編譯為.class文件

2、2.驗證

驗證:確保被加載的類的正確性。
關於驗證大可不必深入但是了解類加載機制必須要知道有這麼個過程以及知道驗證就是為了驗證確保Class文件的字節流中包含的信息符合當前虛擬機的要求即可。
所以下面關於驗證的內容作為了解即可!

驗證是連接階段的第一階段,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致會完成4個階段的檢驗動作:

文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以 0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。

元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了 java.lang.Object之外。

字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

符號引用驗證:確保解析動作能正確執行。

驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用 -Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

2、3.準備【重點】

當完成字節碼文件的校驗之後,JVM 便會開始為類變量分配內存並初始化。準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。

這裏需要注意兩個關鍵點,即內存分配的對象以及初始化的類型

內存分配的對象:要明白首先要知道Java 中的變量有類變量以及類成員變量兩種類型,==類變量指的是被 static 修飾的變量==,而==其他所有類型的變量都屬於類成員變量==。在準備階段,JVM 只會為類變量分配內存,而不會為類成員變量分配內存。類成員變量的內存分配需要等到初始化階段才開始(初始化階段下面會講到)。

舉個例子:例如下面的代碼在準備階段,只會為 LeiBianLiang屬性分配內存,而不會為 ChenYuanBL屬性分配內存。

public static int LeiBianLiang = 666;
public String ChenYuanBL = "jvm";

初始化的類型:在準備階段,JVM 會為類變量分配內存,併為其初始化(JVM 只會為類變量分配內存,而不會為類成員變量分配內存,類成員變量自然這個時候也不能被初始化)。==但是這裏的初始化指的是為變量賦予 Java 語言中該數據類型的默認值,而不是用戶代碼里初始化的值。==

例如下面的代碼在準備階段之後,LeiBianLiang 的值將是 0,而不是 666。

public static int LeiBianLiang = 666;

注意了!!!
注意了!!!
注意了!!!

但如果一個變量是常量(被 static final 修飾)的話,那麼在準備階段,屬性便會被賦予用戶希望的值。例如下面的代碼在準備階段之後,ChangLiang的值將是 666,而不再會是 0。

public static final int ChangLiang = 666;

之所以 static final 會直接被複制,而 static 變量會被賦予java語言類型的默認值。其實我們稍微思考一下就能想明白了。

兩個語句的區別是一個有 final 關鍵字修飾,另外一個沒有。而 final 關鍵字在 Java 中代表不可改變的意思,意思就是說 ChangLiang的值一旦賦值就不會在改變了。既然一旦賦值就不會再改變,那麼就必須一開始就給其賦予用戶想要的值,因此被 final 修飾的類變量在準備階段就會被賦予想要的值。而沒有被 final 修飾的類變量,其可能在初始化階段或者運行階段發生變化,所以就沒有必要在準備階段對它賦予用戶想要的值。

如果還不是很清晰理解final和static關鍵字的話建議參閱下面博主整理好的文章,希望對你有所幫助!

2、4.解析

當通過準備階段之後,進入解析階段。解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。

直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

==其實這個階段對於我們來說也是幾乎透明的,了解一下就好==。

2、5.初始化【重點】

到了初始化階段,用戶定義的 Java 程序代碼才真正開始執行。

Java程序對類的使用方式可分為兩種:主動使用被動使用。一般來說只有當對類的==首次主動使用==的時候才會導致類的初始化,所以主動使用又叫做類加載過程中“初始化”開始的時機。那啥是主動使用呢?類的主動使用包括以下六種【超級重點】

1、 創建類的實例,也就是new的方式

 

2、 訪問某個類或接口的靜態變量,或者對該靜態變量賦值(凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外)

 

3、 調用類的靜態方法

 

4、 反射(如 Class.forName(“com.gx.yichun”))

 

5、 初始化某個類的子類,則其父類也會被初始化

 

6、 Java虛擬機啟動時被標明為啟動類的類( JavaTest ),還有就是Main方法的類會
首先被初始化

 

最後注意一點對於靜態字段,只有直接定義這個字段的類才會被初始化(執行靜態代碼塊),這句話在繼承、多態中最為明顯!為了方便理解下文會陸續通過例子講解

2、6.使用

當 JVM 完成初始化階段之後,JVM 便開始從入口方法開始執行用戶的程序代碼。這個使用階段也只是了解一下就可以了。

2、7.卸載

當用戶程序代碼執行完畢后,JVM 便開始銷毀創建的 Class 對象,最後負責運行的 JVM 也退出內存。這個卸載階段也只是了解一下就可以了。

2、8.結束生命周期

在如下幾種情況下,Java虛擬機將結束生命周期

1、 執行了 System.exit()方法

2、 程序正常執行結束

3、 程序在執行過程中遇到了異常或錯誤而異常終止

4、 由於操作系統出現錯誤而導致Java虛擬機進程終止

3、接口的加載過程

接口加載過程與類加載過程稍有不同。

==當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,當真正用到父接口的時候才會初始化。==

4、解開開篇的面試題

package com.jvm.classloader;

class Father2{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son2 extends Father2{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitativeUseTest2 {
    public static void main(String[] args) {

       System.out.println(Son2.strSon);
    }
}

運行結果:
        Father靜態代碼塊
        Son靜態代碼塊
        HelloJVM_Son

再回頭看這個題,這也太簡單了吧,由於Son2.strSon是調用了Son類自己的靜態方法屬於主動使用,所以會初始化Son類,又由於繼承關係,類繼承原則是初始化一個子類,會先去初始化其父類,所以會先去初始化父類!

再看開篇的第二個題

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe靜態代碼塊");
    }
}

class Father extends YeYe{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather); 
    }
}

運行結果:
    YeYe靜態代碼塊
    Father靜態代碼塊
    HelloJVM_Father

這個題就稍微要注意一下,不過要是你看懂這篇文章,這個題也很簡單。這個題要注意什麼呢?要注意子類Son類沒有被初始化,也就是Son的靜態代碼塊沒有執行!發現了咩?那我們來分析分析…

首先看到Son.strFather,你會發現是子類Son訪問父類Father的靜態變量strFather,這個時候就千萬要記住我在歸納主動使用概念時特別提到過的一個注意點了:對於靜態字段,只有直接定義這個字段的類才會被初始化(執行靜態代碼塊),這句話在繼承、多態中最為明顯!

嗯哼,對吧,Son.strFather中的靜態字段是屬於父類Father的對吧,也就是說直接定義這個字段的類是父類Father,所以在執行 System.out.println(Son.strFather); 這句代碼的時候會去初始化Father類而不是子類Son!是不是一下子明白了?如果明白了就支持一下博主點個讚唄,謝謝~

再看開篇的第三個題

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe靜態代碼塊");
    }
}

class Father extends YeYe{
    public final static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather);
    }
}

運行結果:HelloJVM_Father

這個題唯一的特點就在於final static !是的Son.strFather所對應的變量便是final static修飾的,依舊是在本篇文章中歸納的類的主動使用範疇第二點當中:訪問某個類或接口的靜態變量,或者對該靜態變量賦值(凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外)

所以,這個題並不會初始化任何類,當然除了Main方法所在的類!於是僅僅執行了System.out.println(Son.strFather);所以僅僅打印了Son.strFather的字段結果HelloJVM_Father,嗯哼,是不是又突然明白了?如果明白了就再支持一下博主點個讚唄,謝謝~

實際上上面的題目並不能完全說明本篇文章中歸納的類的主動使用範疇第二點!這話怎麼說呢?怎麼理解呢?再來一個程序各位就更加明了了

package com.jvm.classloader;

import sun.applet.Main;

import java.util.Random;
import java.util.UUID;

class Test{
    static {
        System.out.println("static 靜態代碼塊");
    }

//    public static final String str= UUID.randomUUID().toString();
    public static final double str=Math.random();  //編譯期不確定
}


public class FinalUUidTest {
    public static void main(String[] args) {
        System.out.println(Test.str);
    }
}

請試想一下結果,會不會執行靜態代碼塊里的內容呢?

重點來了
重點來了
重點來了
重點來了

運行結果

static 靜態代碼塊
0.7338688977344875

上面這個程序完全說明本篇文章中歸納的類的主動使用範疇第二點當中的這句話:凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外!

分析:==其實final不是重點,重點是編譯器把結果放入常量池!當一個常量的值並非編譯期可以確定的,那麼這個值就不會被放到調用類的常量池中,這時在程序運行時,會導致主動使用這個常量所在的類,所以這個類會被初始化==

到這裏,能理解完上面三個題已經很不錯了,但是要想更加好好的學習java,博主不得不給各位再來一頓燒腦盛宴,野心不大,只是單純的想巔覆各位對java代碼的認知,當然還望大佬輕拍哈哈哈,直接上代碼:

package com.jvm.classloader;

public class ClassAndObjectLnitialize {

        public static void main(String[] args) {
            System.out.println("輸出的打印語句");
        }

      public ClassAndObjectLnitialize(){

            System.out.println("構造方法");
            System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
        }

        {
            System.out.println("普通代碼塊");
        }

        int ZhiShang = 250;
        static int QingShang = 666;
        
        static
        {
            System.out.println("靜態代碼塊");
        }     

}

建議這個題不要花太多時間思考,否則看了結果你會發現自己想太多了,導致最後可能你看到結果想砸電腦哈哈哈

隔離運行結果專業跑龍套…

隔離運行結果專業跑龍套…

隔離運行結果專業跑龍套…

隔離運行結果專業跑龍套…

隔離運行結果專業跑龍套…

運行結果
        靜態代碼塊
        輸出的打印語句

怎麼樣,是不是沒有你想的那麼複雜呢?

下面我們來簡單分析一下,首先根據上面說到的觸發初始化的(主動使用)的第六點:Java虛擬機啟動時被標明為啟動類的類( JavaTest ),還有就是Main方法的類會首先被初始化

嗯哼?小白童鞋就有疑問了:不是說好有Main方法的類會被初始化的么?那怎麼好多東西都沒有執行捏?

那麼類的初始化順序到底是怎麼樣的呢?在我們代碼中,我們只知道有一個構造方法,但實際上Java代碼編譯成字節碼之後,最開始是沒有構造方法的概念的,只有==類初始化方法== 和 ==對象初始化方法== 。

這個時候我們就不得不深入理解了!那麼這兩個方法是怎麼來的呢?

類初始化方法:編譯器會按照其出現順序,收集:類變量(static變量)的賦值語句靜態代碼塊,最終組成類初始化方法。==類初始化方法一般在類初始化的時候執行。==

所以,上面的這個例子,類初始化方法就會執行下面這段代碼了:

 static int QingShang = 666;  //類變量(static變量)的賦值語句

  static   //靜態代碼塊
   {
       System.out.println("靜態代碼塊");
   }

而不會執行普通賦值語句以及普通代碼塊了

對象初始化方法:編譯器會按照其出現順序,收集:成員變量的賦值語句普通代碼塊,最後收集構造函數的代碼,最終組成對象初始化方法值得特別注意的是,如果沒有監測或者收集到構造函數的代碼,則將不會執行對象初始化方法。==對象初始化方法一般在實例化類對象的時候執行。==

以上面這個例子,其對象初始化方法就是下面這段代碼了:

    {                        
       System.out.println("普通代碼塊");    //普通代碼塊
    }
 
    int ZhiShang = 250;   //成員變量的賦值語句
    
    System.out.println("構造方法");  //最後收集構造函數的代碼
    System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);

明白了類初始化方法 和 對象初始化方法 之後,我們再來看這個上面例子!是的!正如上面提到的:如果沒有監測或者收集到構造函數的代碼,則將不會執行對象初始化方法。上面的這個例子確實沒有執行對象初始化方法。忘了嗎?我們根本就沒有對類ClassAndObjectLnitialize 進行實例化!只是單純的寫了一個輸出語句。

如果我們給其實例化,驗證一下,代碼如下:

package com.jvm.classloader;

public class ClassAndObjectLnitialize {

        public static void main(String[] args) {
            new ClassAndObjectLnitialize();
            System.out.println("輸出的打印語句");
        }

      public ClassAndObjectLnitialize(){

            System.out.println("構造方法");
             System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
        }

        {
            System.out.println("普通代碼塊");
        }

        int ZhiShang = 250;
        static int QingShang = 666;
        
        static
        {
            System.out.println("靜態代碼塊");
        }      
}

運行結果:
        靜態代碼塊
        普通代碼塊
        構造方法
        我是熊孩子我的智商=250,情商=666
        輸出的打印語句     

到這裏博主必須要聲明一點了!我為什麼要用這些面試題作為這篇文章的一部分?因為關於學習有一定的方法,你可以設想一下,如果博主不涉及並分析這幾個面試題,你還有耐心看到這裏嗎?小白杠精童鞋說有。。。好的,就算有,大篇大篇的理論各位扣心自問,能掌握所有知識嗎?小白杠精童鞋說說能。。。額,就算能,那你能保證光記理論一個月不遺忘嗎?小白杠精童鞋說可以。。。我特么一老北京布鞋過去頭給你打歪(我這暴脾氣我天)。所以呢學習要帶着興趣、“目的”、“野心”!希望我這段話能對你有所幫助,哪怕是一點點…

5、理解首次主動使用

我在上面提到過Java程序對類的使用方式可分為兩種:主動使用與被動使用。一般來說只有當對類的首次主動使用的時候才會導致類的初始化,其中首次關鍵字很重要,因此特地用一小結將其講解!

怎麼理解呢?老規矩看個題:

package com.jvm.classloader;

class Father6{
    public static int a = 1;
    static {
        System.out.println("父類粑粑靜態代碼塊");
    }
}
class Son6{
    public static int b = 2;
    static {
        System.out.println("子類熊孩子靜態代碼塊");
    }
}

public class OverallTest {
    static {
        System.out.println("Main方法靜態代碼塊");
    }

    public static void main(String[] args) {
        Father6 father6;
        System.out.println("======");

         father6=new Father6();
        System.out.println("======");

        System.out.println(Father6.a);
        System.out.println("======");

        System.out.println(Son6.b);

    }
}

請試想一下運行結果

運行結果:
        Main方法靜態代碼塊
        ======
        父類粑粑靜態代碼塊
        ======
        1
        ======
        子類熊孩子靜態代碼塊
        2

分析:
首先根據主動使用概括的第六點:Main方法的類會首先被初始化。 所以最先執行Main方法靜態代碼塊,而 Father6 father6;只是聲明了一個引用不會執行什麼,當運行到father6=new Father6();的時候,看到關鍵字new並且將引用father6指向了Father6對象,說明主動使用了,所以父類Father6將被初始化,因此打印了:父類粑粑靜態代碼塊 ,之後執行 System.out.println(Father6.a);屬於訪問靜態變量所以也是主動使用,這個時候注意了,因為在上面執行father6=new Father6();的時候父類已經主動使用並且初始化過一次了,這次不再是首次主動使用了,所以Father6不會在被初始化,自然它的靜態代碼塊就不再執行了,所以直接打印靜態變量值1,而後面的System.out.println(Son6.b);同樣,也是只初始化自己,不會去初始化父類,只因為父類Father6以及不再是首次主動使用了!明白了沒?如果有疑問歡迎留言,絕對第一時間回復!

6、類加載器

喔o,終於到類加載器內容了!我們之前講的類加載都是給類加載器做的一個伏筆,在這之前講的所有類被加載都是由類加載器來完成的,可見類加載器是多麼重要。由於上面的面試題並不涉及類加載器的相關知識,所以到這裏再涉及涉及類加載器的知識!

類加載器負責加載所有的類,其為所有被載入內存中的類生成一個java.lang.Class實例對象。一旦一個類被加載入JVM中,同一個類就不會被再次載入了。正如一個對象有一個唯一的標識一樣,一個載入JVM的類也有一個唯一的標識。

關於唯一標識符:

在Java中,一個類用其全限定類名(包括包名和類名)作為標識;

 


但在JVM中,一個類用其全限定類名和其類加載器作為其唯一標識。

類加載器的任務是根據一個類的全限定名來讀取此類的二進制字節流到JVM中,然後轉換為一個與目標類對應的java.lang.Class對象實例,在虛擬機提供了3種類加載器,啟動(Bootstrap)類加載器、擴展(Extension)類加載器、系統(System)類加載器(也稱應用類加載器),如下:

站在Java開發人員的角度來看,類加載器可以大致劃分為以下三類:

啟動類加載器BootstrapClassLoader,啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用C++語言實現的,是虛擬機自身的一部分,負責加載存放在 JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被 -Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如rt.jar,所有的java.開頭的類均被 BootstrapClassLoader加載)。啟動類加載器是無法被Java程序直接引用的。==總結一句話:啟動類加載器加載java運行過程中的核心類庫JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的類,也就是JDK提供的類等常見的比如:Object、Stirng、List…==

擴展類加載器: ExtensionClassLoader,該加載器由 sun.misc.Launcher$ExtClassLoader實現,它負責加載 JDK\jre\lib\ext目錄中,或者由 java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.開頭的類),開發者可以直接使用擴展類加載器。

應用程序類加載器: ApplicationClassLoader,該類加載器由 sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。==總結一句話:應用程序類加載器加載CLASSPATH變量指定路徑下的類 即指你自已在項目工程中編寫的類==

線程上下文類加載器:除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器。類似Thread.currentThread().getContextClassLoader()獲取線程上下文類加載器

在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,我們還可以自定義類加載器,因為JVM自帶的類加載器(ClassLoader)只是懂得從本地文件系統加載標準的java class文件,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:

1、在執行非置信代碼之前,自動驗證数字簽名。

2、動態地創建符合用戶特定需要的定製化構建類。

3、從特定的場所取得java class,例如數據庫中和網絡中。

需要注意的是,Java虛擬機對class文件採用的是按需加載的方式,也就是說當需要使用該類時才會將它的class文件加載到內存生成class對象,而且加載某個類的class文件時,Java虛擬機默認採用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,下面將會詳細講到!

下面我們看一個程序:

package com.jvm.classloaderQi;

public class ClassloaderTest {
    public static void main(String[] args) {
        //獲取ClassloaderTest類的加載器
        ClassLoader classLoader= ClassloaderTest.class.getClassLoader(); 
        
        System.out.println(classLoader);
        System.out.println(classLoader.getParent()); //獲取ClassloaderTest類的父類加載器
        System.out.println(classLoader.getParent().getParent());
    }
}

運行結果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null

從上面的結果可以看出,並沒有獲取到ExtClassLoader的父Loader,原因是Bootstrap Loader(啟動類加載器)是用C++語言實現的(這裏僅限於Hotspot,也就是JDK1.5之後默認的虛擬機,有很多其他的虛擬機是用Java語言實現的),找不到一個確定的返回父Loader的方式,於是就返回null。至於$符號就是內部類的含義。

7、關於命名空間

我覺得講類加載器,還是很有必要知道命名空間這個概念!實際上類加載器的一個必不可少的前提就是命名空間!

命名空間概念:

每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成。

 

特別注意:

在同一個命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類。

在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。
由子加載器加載的類能看見父加載器的類,由父親加載器加載的類不能看見子加載器加載的類

我們已經知道每個類只能被加載一次,其實這樣說是不夠準確的,怎樣才算是準確的呢?那就涉及到命名空間的概念了!只有在相同的命名空間中,每個類才只能被加載一次,反過來說就是一個類在不同的命名空間中是可以被加載多次的,而被加載多次的Class對象是互相獨立的!

7.1、如何理解?

當然,直接把命名空間的概念直接拋給大家,如果沒有接觸過,100%是看不懂其中的含義的,我敢打包票,假一賠100萬。。。那麼博主就舉出寫例子讓各位深刻體會一下!當然這些例子涉及自定義加載器的一些知識,建議先對自定義加載器有一定了解在看!

例子必知前提:
1、 自己在idea或者eclipse中創建的工程項目中只要編譯之後都會有對應的class文件成在classPath目錄中
2、 而這些目錄是由ApplicationClassLoader應用加載器加載
3、 我之後會將class文件放到系統桌面地址上,而這些系統地址由自定義加載器指定,所以由自定義加載器加載

7.2、準備

事先編譯好,然後將項目工程中的兩個字節碼class文件【File1和File2】拷貝到系統桌面路徑上,編譯main方法就會出現在項目工程(ClassPath)下,注意以下例子情況中系統桌面路徑的class文件一直都存在!

Main方法情況:

  • 1、創建一個自定義加載器classloader2,並聲明桌面class文件路徑,接着加載File1

  • 2、打印File1的加載器
  • 3、newInstanceFile1的實例

File1類的方法情況:

  • 1、File1的構造方法中存在一行代碼:new File2new實例代碼

File2類的方法情況:

  • 1、打印File2的加載器

7.3、測試代碼情景一

刪除File1File2項目工程中的class文件,工程項目的兩個class文件都刪除(只存在系統桌面路徑下的class文件)

結果:File1File2的加載器都是自定義加載器

7.4、測試代碼情景二

只刪除File1項目工程中的class文件

結果:File1的加載器是自定義加載器,而執行到File2實例的加載器是App應用加載器

7.5、測試代碼情景三

只刪除File2項目工程中的class文件

結果:File1的加載器都是APP應用加載器,而執行到File2實例的時候報NoClassDefFoundError異常

得出結論:加載一個類(File1)的時候,這個類裏面調用了其他的類(File2)或者其他類方法的初始化代碼,那麼這裏面的類也會試着從這個類的加載器開始向上委託加載,如果全都加載不了加載不了就報NoClassDefFoundError異常

當然這樣理解命名空間和類加載機制還是遠遠不夠的!

File2類中發生改變情況如下:

  • 1、File1的構造方法中存在一行new File2的實例這沒變
  • 2、在File2的構造方法中,打印(訪問)File1的class文件

7.6、測試代碼情景四

只刪除項目工程中File1的class文件

結果:File1的加載器都是自定義加載器,而執行到File2實例的加載器是App應用加載器,當運行到File2構造方法中的打印(訪問)File1的class文件的時候報NoClassDefFoundError異常

得出結論:父親加載器加載的類(File2)不能看見子加載器加載的類(File1

File1方法發生改變情況如下:
1、Main方法中newInstanceFile1的實例,File1的構造方法中存在一行new File2的實例這都沒變
2、在File1的構造方法中,打印File2的class文件

7.7、測試代碼情景五

只刪除File1項目工程中的class文件

結果:File1的加載器都是自定義加載器,而執行到File2實例的加載器是App應用加載器,當運行到File1構造方法中的打印File2的class文件的時候沒問題

得出結論:由子加載器加載的類(File1)能看見父加載器的類(File2)

當然還要注意知道的一點的是:如果兩個加載器之間沒有直接或間接的父子關係,那麼它們各自加載類相互不可見。

當然整對上面的情況還是相當比較抽象,畢竟沒上代碼,如果有任何疑問,歡迎留言,宜春絕對第一時間回復!

8、JVM類加載機制

JVM的類加載機制主要有如下3種。

全盤負責:當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非显示使用另外一個類加載器來載入

父類委託:先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類,通俗講就是兒子們都他么是懶豬,自己不管能不能做,就算能加載也先不幹,先給自己的父親做,一個一個往上拋,直到拋到啟動類加載器也就是最頂級父類,只有父親做不了的時候再沒辦法由下一個子類做,直到能某一個子類能做才做,之後的子類就直接返回,實力坑爹!

緩存機制:緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統才會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是為什麼修改了Class后,必須重啟JVM,程序的修改才會生效

9、雙親委派模型

雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。也就是實力坑爹!

雙親委派機制:

1、當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。

 

2、當 ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。

 

3、如果 BootStrapClassLoader加載失敗(例如在 $JAVA_HOME/jre/lib里未查找到該class),會使用

ExtClassLoader來嘗試加載;

 

4、若ExtClassLoader也加載失敗,則會使用 AppClassLoader來加載,如果

AppClassLoader也加載失敗,則會報出異常 ClassNotFoundException。

從代碼層面了解幾個Java中定義的類加載器及其雙親委派模式的實現,它們類圖關係如下:

從圖可以看出頂層的類加載器是
抽象類ClassLoader類,其後
所有的類加載器都繼承自ClassLoader(不包括啟動類加載器),為了更好理解雙親委派模型,ClassLoader源碼中的loadClass(String)方法該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2之後不再建議用戶重寫但用戶可以直接調用該方法,
loadClass()方法是ClassLoader類自己實現的,該方法中的邏輯就是雙親委派模式的實現,loadClass(String name, boolean resolve)是一個重載方法,resolve參數代表是否生成class對象的同時進行解析相關操作。源碼分析如下::

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先從緩存查找該class對象,找到就不用重新加載
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,則委託給父類加載器去加載
                      c = parent.loadClass(name, false);
                  } else {
                  //如果沒有父類,則委託給啟動加載器去加載
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都沒有找到,則通過自定義實現的findClass去查找並加載
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否需要在加載時進行解析
              resolveClass(c);
          }
          return c;
      }
  }

既然存在這個雙親委派模型,那麼就一定有着存在的意義,其意義主要是:Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係,通過這種層級關可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素,java核心api中定義類型不會被隨意替換,假設通過網絡傳遞一個名為java.lang.Integer的類,通過雙親委託模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字的類,發現該類已被加載,並不會重新加載網絡傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。

雙親委派模型意義總結來講就是:
1、系統類防止內存中出現多份同樣的字節碼
2、保證Java程序安全穩定運行

10、ClassLoader源碼分析

ClassLoader類是一個抽象類,所有的類加載器都繼承自ClassLoader(不包括啟動類加載器),因此它顯得格外重要,分析ClassLoader抽象類也是非常重要的!

簡單小結一下ClassLoader抽象類中一些概念:

二進制概念(Binary name):格式如下

把二進制名字轉換成文件名字,然後在文件系統中磁盤上讀取其二進制文件(class文件),每一個class對象都包含了定義了這個類的classload對象,class類都是由類加載器加載的只有數組類型是有JVM根據需要動態生成。

特別注意數組類型

1、 數組類的類對象不是由類加載器創建的,而是根據Java運行時的需要自動創建的。
2、 數組類的類加載器getClassLoader()與它的元素類型的類加載器相同;如果元素類型是基本類型,則數組類沒有類加載器也就是null,而這個null不同於根類加載器返回的null,它是單純的null。

到這裏,下面就主要分析ClassLoader抽象類中幾個比較重要的方法。

10、1.loadClass

該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2之後不再建議用戶重寫但用戶可以直接調用該方法,loadClass()方法是ClassLoader類自己實現的,該方法中的邏輯就是雙親委派模式的實現,其源碼如下,loadClass(String name, boolean resolve)是一個重載方法,resolve參數代表是否生成class對象的同時進行解析相關操作:

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先從緩存查找該class對象,找到就不用重新加載
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,則委託給父類加載器去加載
                      c = parent.loadClass(name, false);
                  } else {
                  //如果沒有父類,則委託給啟動加載器去加載
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都沒有找到,則通過自定義實現的findClass去查找並加載
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否需要在加載時進行解析
              resolveClass(c);
          }
          return c;
      }
  }

正如loadClass方法所展示的,當類加載請求到來時,先從緩存中查找該類對象,如果存在直接返回,如果不存在則交給該類加載去的父加載器去加載,倘若沒有父加載則交給頂級啟動類加載器去加載,最後倘若仍沒有找到,則使用findClass()方法去加載(關於findClass()稍後會進一步介紹)。從loadClass實現也可以知道如果不想重新定義加載類的規則,也沒有複雜的邏輯,只想在運行時加載自己指定的類,那麼我們可以直接使用this.getClass().getClassLoder.loadClass(“className”),這樣就可以直接調用ClassLoader的loadClass方法獲取到class對象。

10、2.findClass

在JDK1.2之前,在自定義類加載時,總會去繼承ClassLoader類並重寫loadClass方法,從而實現自定義的類加載類,但是在JDK1.2之後已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被調用的,當loadClass()方法中父加載器加載失敗后,則會調用自己的findClass()方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委託模式。需要注意的是ClassLoader類中並沒有實現findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應該知道的是findClass方法通常是和defineClass方法一起使用的(稍後會分析),ClassLoader類中findClass()方法源碼如下:

//直接拋出異常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

10、3.defineClass(byte[] b, int off, int len)

defineClass()方法是用來將byte字節流解析成JVM能夠識別的Class對象(ClassLoader中已實現該方法邏輯),通過這個方法不僅能夠通過class文件實例化class對象,也可以通過其他方式實例化class對象,如通過網絡接收一個類的字節碼,然後轉換為byte字節流創建對應的Class對象,defineClass()方法通常與findClass()方法一起使用,一般情況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法並編寫加載規則,取得要加載類的字節碼後轉換成流,然後調用defineClass()方法生成類的Class對象,簡單例子如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
      // 獲取類的字節數組
      byte[] classData = getClassData(name);  
      if (classData == null) {
          throw new ClassNotFoundException();
      } else {
          //使用defineClass生成class對象
          return defineClass(name, classData, 0, classData.length);
      }
  }

需要注意的是,如果直接調用defineClass()方法生成類的Class對象,這個類的Class對象並沒有解析(也可以理解為鏈接階段,畢竟解析是鏈接的最後一步),其解析操作需要等待初始化階段進行。

10、4.resolveClass (Class<?>c)

使用該方法可以使用類的Class對象創建完成也同時被解析。前面我們說鏈接階段主要是對字節碼進行驗證,為類變量分配內存並設置初始值同時將字節碼文件中的符號引用轉換為直接引用。

10、5.ClassLoader小結

以上上述4個方法是ClassLoader類中的比較重要的方法,也是我們可能會經常用到的方法。接看SercureClassLoader擴展了 ClassLoader,新增了幾個與使用相關的代碼源(對代碼源的位置及其證書的驗證)和權限定義類驗證(主要指對class源碼的訪問權限)的方法,一般我們不會直接跟這個類打交道,更多是與它的子類URLClassLoader有所關聯,前面說過,ClassLoader是一個抽象類,很多方法是空的沒有實現,比如 findClass()、findResource()等。而URLClassLoader這個實現類為這些方法提供了具體的實現,並新增了URLClassPath類協助取得Class字節碼流等功能,在編寫自定義類加載器時,如果沒有太過於複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。

檢查完父類加載器之後loadClass會去默認調用findClass方法,父類(ClassLoader)中的findClass方法主要是拋出一個異常。

findClass根據二進制名字找到對應的class文件,返回值為Class對象Class<?>

defineClass這個方法主要是將一個字節數組轉換成Class實例,會拋三個異常,但只是threws一個,因為其他兩個是運行時異常。

loadClass方法是一個加載一個指定名字的class文件,調用findLoadedClass (String)檢查類是否已經加載…如果已經加裝就不再加載而是直接返回第一次加載結果 所以一個類只會加載一次

11、自定義類加載器

自定義核心目的是擴展java虛擬機的動態加載類的機制,JVM默認情況是使用雙親委託機制,雖然雙親委託機制很安全極高但是有些情況我們需要自己的一種方式加載,==比如應用是通過網絡來傳輸 Java類的字節碼,為保證安全性,這些字節碼經過了加密處理,這時系統類加載器就無法對其進行加載,這樣則需要自定義類加載器來實現==。因此自定義類加載器也是很有必要的。

自定義類加載器一般都是繼承自 ClassLoader類,從上面對 loadClass方法來分析來看,我們只需要重寫 findClass 方法即可。自定義加載器中點:重寫findClass,下面直接看自定義類加載器代碼的流程:

package com.yichun.classloader;
import java.io.*;

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\dirtemp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.yichun.classloader.Demo1");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

自定義類加載器的核心在於對字節碼文件的獲取,如果是加密的字節碼則需要在該類中對文件進行解密。上面代碼程序只是簡單Demo,並未對class文件進行加密,因此省略了解密的過程。這裡有幾點需要注意:

1、這裏傳遞的文件名需要是類的全限定性名稱,即com.yichun.test.classloading.Test格式的,因為
defineClass 方法是按這種格式進行處理的。

 

2、最好不要重寫loadClass方法,因為這樣容易破壞雙親委託模式。

 

3、這類Test 類本身可以被 AppClassLoader類加載,因此我們不能把com/yichun/test/classloading/Test.class放在類路徑下。否則,由於雙親委託機制的存在,會直接導致該類由AppClassLoader加載,而不會通過我們自定義類加載器來加載。

12、加載類的三種方式

到這裏,相信大家已經對類的加載以及加載器有一定的了解了,那麼你知道嗎,其實加載類常見的有三種方式,如下:

1、靜態加載,也就是通過new關鍵字來創建實例對象。

 

2、動態加載,也就是通過Class.forName()方法動態加載(反射加載類型),然後調用類的newInstance()方法實例化對象。

 

3、動態加載,通過類加載器loadClass()方法來加載類,然後調用類的newInstance()方法實例化對象

12、1.三種方式的區別:

1、第一種和第二種方式使用的類加載器是相同的,都是當前類加載器。(this.getClass.getClassLoader)。而3由用戶指定類加載器。

2、如果需要在當前類路徑以外尋找類,則只能採用第3種方式。第3種方式加載的類與當前類分屬不同的命名空間

3、第一種是靜態加載,而第二、三種是動態加載。

12、2.兩種異常(exception)

1、靜態加載的時候如果在運行環境中找不到要初始化的類,拋出的是NoClassDefFoundError,它在JAVA的異常體系中是一個Error

2、動態態加載的時候如果在運行環境中找不到要初始化的類,拋出的是ClassNotFoundException,它在JAVA的異常體系中是一個checked異常

12、3.理解Class.forName

Class.forName()是一種獲取Class對象的方法,而且是靜態方法。

Class.forName()是一個靜態方法,同樣可以用來加載類,Class.forName()返回與給定的字符串名稱相關聯類或接口的Class對象。注意這是一種獲取Class對象的方法

官方給出的API文檔如下

publicstatic Class<?> forName(String className)

Returns the Class object associated withthe class or interface with the given string name. Invokingthis method is equivalent to:

Class.forName(className,true, currentLoader)

where currentLoader denotes the definingclass loader of the current class.

For example, thefollowing code fragment returns the runtime Class descriptor for theclass named java.lang.Thread:

Class t =Class.forName("java.lang.Thread")

A call to forName("X") causes theclass named X to beinitialized.

Parameters:

className - the fully qualifiedname of the desired class.

Returns:

the Class object for the classwith the specified name.

可以看出,Class.forName(className)實際上是調用Class.forName(className,true, this.getClass().getClassLoader())。第二個參數,是指Classloading后是不是必須被初始化。可以看出,使用Class.forName(className)加載類時則已初始化。所以Class.forName()方法可以簡單的理解為:獲得字符串參數中指定的類,並初始化該類。

12、4.Class.forName與ClassLoader.loadClass區別

首先,我們必須先明確類加載機制的三個過程主要是:加載 –> 連接 –> 初始化。

  • Class.forName():將類的.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊;

  • ClassLoader.loadClass():只干一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。

  • Class.forName(name, initialize, loader):帶參函數也可控制是否加載static塊。並且只有調用了newInstance()方法採用調用構造函數,創建類的對象 。

這個時候,我們再來看一個程序:

package com.jvm.classloader;

class Demo{
    static {
        System.out.println("static 靜態代碼塊");
    }
}

public class ClassLoaderDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader=ClassLoaderDemo.class.getClassLoader();
        //1、使用ClassLoader.loadClass()來加載類,不會執行初始化塊
        classLoader.loadClass("com.jvm.classloader.Demo");
        
        //2、使用Class.forName()來加載類,默認會執行初始化塊
        Class.forName("com.jvm.classloader.Demo");
        
        //3、使用Class.forName()來加載類,並指定ClassLoader,初始化時不執行靜態塊 
        Class.forName("com.jvm.classloader.Demo",false,classLoader);
    }
}

記得一個一個測試!我上面的程序是一次寫了三個的並且已經標明了標號1、2、3!!!各位再自個電腦上跑一遍,思路就很會清晰了!

13、總結

類的加載、連接與初始化:

1、加載:查找並加載類的二進制數據到java虛擬機中

 

2、 連接:

驗證: 確保被加載的類的正確性
準備:為類的靜態變量分配內存,並將其初始化為默認值,但是到達初始化之前類變量都沒有初始化為真正的初始值(如果是被 final 修飾的類變量,則直接會被初始成用戶想要的值。)
解析:把類中的符號引用轉換為直接引用,就是在類型的常量池中尋找類、接口、字段和方法的符號引用,把這些符號引用替換成直接引用的過程

3、 初始化:為類的靜態變量賦予正確的初始值

 

類從磁盤上加載到內存中要經歷五個階段:加載、連接、初始化、使用、卸載

Java程序對類的使用方式可分為兩種

(1)主動使用
(2)被動使用

所有的Java虛擬機實現必須在每個類或接口被Java程序“首次主動使用”時才能初始化他們
主動使用
(1)創建類的實例
(2)訪問某個類或接口的靜態變量 getstatic(助記符),或者對該靜態變量賦值 putstatic
(3)調用類的靜態方法 invokestatic
(4)反射(Class.forName(“com.test.Test”))
(5)初始化一個類的子類
(6)Java虛擬機啟動時被標明啟動類的類以及包含Main方法的類
(7)JDK1.7開始提供的動態語言支持(了解)

被動使用
除了上面七種情況外,其他使用java類的方式都被看做是對類的被動使用,都不會導致類的初始化

14、特別注意

初始化入口方法。當進入類加載的初始化階段后,JVM 會尋找整個 main 方法入口,從而初始化 main 方法所在的整個類。當需要對一個類進行初始化時,會首先初始化類構造器(),之後初始化對象構造器()。

初始化類構造器:JVM 會按順序收集類變量的賦值語句、靜態代碼塊,最終組成類構造器由 JVM 執行。
初始化對象構造器:JVM 會按照收集成員變量的賦值語句、普通代碼塊,最後收集構造方法,將它們組成對象構造器,最終由 JVM 執行。值得特別注意的是,如果沒有監測或者收集到構造函數的代碼,則將不會執行對象初始化方法。對象初始化方法一般在實例化類對象的時候執行。

如果在初始化 main 方法所在類的時候遇到了其他類的初始化,那麼就先加載對應的類,加載完成之後返回。如此反覆循環,最終返回 main 方法所在類。

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

[UWP]使用Win2D的BorderEffect實現圖片的平鋪功能

1. WPF有,而UWP沒有的圖片平鋪功能

在WPF中只要將ImageSource的TileMode屬性設置為Tile即可實現圖片的平鋪,具體可見WPF的這些文檔:

WPF圖片平鋪功能我幾乎沒用過,只是作為基礎中的基礎知識記住了用法。我以為那麼基礎的功能在UWP肯定有,根本不用懷疑,所以當我在UWP中發現這麼基礎的東西居然沒有時真的嚇了一跳。

上圖左面是WPF版本的TileBrush,右邊是UWP版本,可以看到UWP版本功能少了一大半。

這麼小的一個類,我覺得沒必要在這裏做簡化吧。幸好圖片平鋪可以使用Win2D里的實現。

2. UWP中的圖片平鋪功能

<Grid>
    <Rectangle x:Name="Background" />
</Grid>

假設有以上的XAML,要在名為Background的元素上應用合成畫筆,首先引用 nuget包,然後參考官方文檔中 的部分使用圖片創建一個合成畫筆:

var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var imageBrush = compositor.CreateSurfaceBrush();
var loadedSurface = LoadedImageSurface.StartLoadFromUri(new Uri("ms-appx:///110Strawberry.png"));
imageBrush.Surface = loadedSurface;
imageBrush.Stretch = CompositionStretch.None;

現在就差創建一個SpriteVisual並把它應用到Background的VisualTree上了,順便一提,是這張圖片:

不過要實現平鋪功能還需要創建一個BorderEffect:

var borderEffect = new BorderEffect
{
    Source = new CompositionEffectSourceParameter("source")
};


var effectFactory = compositor.CreateEffectFactory(borderEffect);
var effectBrush = effectFactory.CreateBrush();
effectBrush.SetSourceParameter("source", imageBrush);


var sprite = compositor.CreateSpriteVisual();
sprite.Brush = effectBrush;

var backgroundVisual = ElementCompositionPreview.GetElementVisual(Background);
var bindSizeAnimation = compositor.CreateExpressionAnimation("backgroundVisual.Size");
bindSizeAnimation.SetReferenceParameter("backgroundVisual", backgroundVisual);
sprite.StartAnimation("Size", bindSizeAnimation);

ElementCompositionPreview.SetElementChildVisual(Background, sprite);

總之BorderEffect以imageBrush為Source,其它都保留默認值,將它它應用到Background的VisualTree上後效果如下:

這還不是我想要的平鋪效果。這是因為這時候ExtendXExtendY保持默認值的Clamp,這個類型會讓BorderEffect重複圖像邊緣的屬性。如果要實現我想要的平鋪需要將這兩個屬性設置為Wrap

borderEffect.ExtendX = CanvasEdgeBehavior.Wrap;
borderEffect.ExtendY = CanvasEdgeBehavior.Wrap;

居然不是從左上角開始平鋪的,和我的想法還是有出入,不過這種細節就算了。順便一提ExtendXExtendY還可以設置為Mirror,效果如下:

3. 綁定Size

var backgroundVisual = ElementCompositionPreview.GetElementVisual(Background);
var bindSizeAnimation = compositor.CreateExpressionAnimation("backgroundVisual.Size");
bindSizeAnimation.SetReferenceParameter("backgroundVisual", backgroundVisual);
sprite.StartAnimation("Size", bindSizeAnimation);

最後順便提一下,上面的代碼中有這麼一段代碼沒介紹到,這是用來動態地設置SpriteVisual的尺寸。ExpressionAnimation有一直運行和永不停止這兩個特性,創建ExpressionAnimation並在SpriteVisual上運行動畫,實際上將SpriteVisual的Size永遠地綁定為backgroundVisual 的Size的值。其實簡單地訂閱SizeChanged事件也能達到這個效果,代碼好像還少些。

4. 結語

這麼簡單的功能居然都要這麼多代碼,或者有更簡單的實現?不過凡事都有要辯證地看,幸好它這麼複雜,又讓我水了一篇博客。

有給出其它的方案,可以參考一下。

5. 參考

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

[視頻演示].NET Core開發的iNeuOS物聯網平台,實現從設備&PLC、雲平台、移動APP數據鏈路閉環

目       錄

1.      概述… 1

2.      登陸信息… 2

3.      設備驅動… 3

4.      組態建模… 3

5.      手機APP. 5

6.      視頻演示… 6

1.   概述

此次我們團隊人員對iNeuOS進行了全面升級,主要升級內容包括:

(1)    設備容器增加設備驅動,包括:西門子(S7-200smart、S7-300、S7-400、S7-1200、S7-1500)、三菱(FxSerial)、MQTT協議等。

(2)    組態建模可以設置背景,作為開發大屏展示使用,背景可以為一個獨立的圖元信息。

(3)    組態建模可以按瀏覽器大小按比例縮放,以適應移動APP显示。

(4)    組態建模標題框和文本框可以設置背景和字體顏色。

(5)    組態建模文本框綁定數據點后,可以設置上下限值,以用於判斷,進行顏色報警显示。

(6)    組態建模文本框可以設置數據保留小數點位數。

(7)    組態建模可以自定義畫任意圖形,並且填充顏色等。

(8)    組態建模開發好的視圖,可以右鍵單擊數值文本框,查看數據曲線趨勢。

(9)    手機移動APP,組態建模開發的視圖,直接可以显示在手機APP上。

(10)進行其他優化。

2.   演示信息

在線演示:  (注:服務器比較慢,請耐心等待。用戶自已註冊用戶,體驗系統功能)

視頻演示:

驅動開發: (v2.1版本)

手機APP:

登陸界面,如下圖:

3.   設備驅動

   主要了主流的PLC驅動,包括:西門子(S7-200smart、S7-300、S7-400、S7-1200、S7-1500)、三菱(FxSerial)。增加了MQTT協議,MQTT協議基本與阿里雲IOT的alink協議保持一致,方便在私有雲建設過程中遷移。如下圖:

      iNeuKernel現在完全支持跨平台部署,至此iNeuOS的前台和後台全部支持跨平台。

4.   組態建模

   雲端組態建模改動和完善的地方比較多,主要依據用戶提出的建議進行了修改。

(1)增加編輯區域設置背景功能,主要用於開發數據大屏展示的效果,可以上傳多個背景圖元,並且作為一個獨立的圖元,隨時可以更換背景。

  (2)編輯數據的文本框,可以設置背景和字體顏色、報警運作、保留小數位數等,更具有交互感。

  (3)任意畫圖形,儘管我們提供了3500多個SVG圖元信息,但是難免缺少特殊領域的圖形,可以使用這個功能彌補。也可以畫大屏展示的特定區域形式。

  (4)組態建模開發完成后,可以右鍵單擊文本框,查看趨勢,以展示當前數據的曲線圖。

5.   手機APP

    手機APP可以使數據離用戶最近,組態建模完成后,通過手機進行縮放显示。

      打開手機APP显示已經開發完成的視圖列表,選擇任意視圖,显示效果如下圖。

6.   視頻演示


如果你播放不了該視頻, 那是你的設備不支持該文件格式

參見:

《》

《》

《》

 物聯網&集成技術 QQ群:54256083 

 物聯網&集成合作 QQ群:727664080

 網站:

聯繫QQ:504547114

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

還看不懂同事的代碼?超強的 Stream 流操作姿勢還不學習一下

Java 8 新特性系列文章索引。

前言

我們都知道 Lambda 和 Stream 是 Java 8 的兩大亮點功能,在前面的文章里已經介紹過 Lambda 相關知識,這次介紹下 Java 8 的 Stream 流操作。它完全不同於 java.io 包的 Input/Output Stream ,也不是大數據實時處理的 Stream 流。這個 Stream 流操作是 Java 8 對集合操作功能的增強,專註於對集合的各種高效、便利、優雅的聚合操作。藉助於 Lambda 表達式,顯著的提高編程效率可讀性。且 Stream 提供了并行計算模式,可以簡潔的編寫出并行代碼,能充分發揮如今計算機的多核處理優勢。

在使用 Stream 流操作之前你應該先了解 Lambda 相關知識,如果還不了解,可以參考之前文章: 。

1. Stream 流介紹

Stream 不同於其他集合框架,它也不是某種數據結構,也不會保存數據,但是它負責相關計算,使用起來更像一個高級的迭代器。在之前的迭代器中,我們只能先遍歷然後在執行業務操作,而現在只需要指定執行什麼操作, Stream 就會隱式的遍歷然後做出想要的操作。另外 Stream 和迭代器一樣的只能單向處理,如同奔騰長江之水一去而不復返。

由於 Stream 流提供了惰性計算并行處理的能力,在使用并行計算方式時數據會被自動分解成多段然後并行處理,最後將結果匯總。所以 Stream 操作可以讓程序運行變得更加高效。

2. Stream 流概念

Stream 流的使用總是按照一定的步驟進行,可以抽象出下面的使用流程。

數據源(source) -> 數據處理/轉換(intermedia) -> 結果處理(terminal )

2.1. 數據源

數據源(source)也就是數據的來源,可以通過多種方式獲得 Stream 數據源,下面列舉幾種常見的獲取方式。

  • Collection.stream(); 從集合獲取流。
  • Collection.parallelStream(); 從集合獲取并行流。
  • Arrays.stream(T array) or Stream.of(); 從數組獲取流。
  • BufferedReader.lines(); 從輸入流中獲取流。
  • IntStream.of() ; 從靜態方法中獲取流。
  • Stream.generate(); 自己生成流

2.2. 數據處理

數據處理/轉換(intermedia)步驟可以有多個操作,這步也被稱為intermedia(中間操作)。在這個步驟中不管怎樣操作,它返回的都是一個新的流對象,原始數據不會發生任何改變,而且這個步驟是惰性計算處理的,也就是說只調用方法並不會開始處理,只有在真正的開始收集結果時,中間操作才會生效,而且如果遍歷沒有完成,想要的結果已經獲取到了(比如獲取第一個值),會停止遍歷,然後返回結果。惰性計算可以顯著提高運行效率。

數據處理演示。

@Test
public void streamDemo(){
    List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter");
    // 1. 篩選出名字長度為4的
    // 2. 名字前面拼接 This is
    // 3. 遍歷輸出
    nameList.stream()
            .filter(name -> name.length() == 4)
            .map(name -> "This is "+name)
            .forEach(name -> System.out.println(name));
}
// 輸出結果
// This is Jack
// This is Poul

數據處理/轉換操作自然不止是上面演示的過濾 filtermap映射兩種,另外還有 map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered 等。

2.3. 收集結果

結果處理(terminal )是流處理的最後一步,執行完這一步之後流會被徹底用盡,流也不能繼續操作了。也只有到了這個操作的時候,流的數據處理/轉換等中間過程才會開始計算,也就是上面所說的惰性計算結果處理也必定是流操作的最後一步。

常見的結果處理操作有 forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator 等。

下面演示了簡單的結果處理的例子。

/**
 * 轉換成為大寫然後收集結果,遍歷輸出
 */
@Test
public void toUpperCaseDemo() {
    List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter");
    List<String> upperCaseNameList = nameList.stream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());
    upperCaseNameList.forEach(name -> System.out.println(name + ","));
}
// 輸出結果
// DARCY,CHRIS,LINDA,SID,KIM,JACK,POUL,PETER,

2.4. short-circuiting

有一種 Stream 操作被稱作 short-circuiting ,它是指當 Stream 流無限大但是需要返回的 Stream 流是有限的時候,而又希望它能在有限的時間內計算出結果,那麼這個操作就被稱為short-circuiting。例如 findFirst 操作。

3. Stream 流使用

Stream 流在使用時候總是藉助於 Lambda 表達式進行操作,Stream 流的操作也有很多種方式,下面列舉的是常用的 11 種操作。

3.1. Stream 流獲取

獲取 Stream 的幾種方式在上面的 Stream 數據源里已經介紹過了,下面是針對上面介紹的幾種獲取 Stream 流的使用示例。

@Test
public void createStream() throws FileNotFoundException {
    List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter");
    String[] nameArr = {"Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"};
    // 集合獲取 Stream 流
    Stream<String> nameListStream = nameList.stream();
    // 集合獲取并行 Stream 流
    Stream<String> nameListStream2 = nameList.parallelStream();
    // 數組獲取 Stream 流
    Stream<String> nameArrStream = Stream.of(nameArr);
    // 數組獲取 Stream 流
    Stream<String> nameArrStream1 = Arrays.stream(nameArr);
    // 文件流獲取 Stream 流
    BufferedReader bufferedReader = new BufferedReader(new FileReader("README.md"));
    Stream<String> linesStream = bufferedReader.lines();
    // 從靜態方法獲取流操作
    IntStream rangeStream = IntStream.range(1, 10);
    rangeStream.limit(10).forEach(num -> System.out.print(num+","));
    System.out.println();
    IntStream intStream = IntStream.of(1, 2, 3, 3, 4);
    intStream.forEach(num -> System.out.print(num+","));
}

3.2. forEach

forEach 是 Strean 流中的一個重要方法,用於遍歷 Stream 流,它支持傳入一個標準的 Lambda 表達式。但是它的遍歷不能通過 return/break 進行終止。同時它也是一個 terminal 操作,執行之後 Stream 流中的數據會被消費掉。

如輸出對象。

List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numberList.stream().forEach(number -> System.out.println(number+","));
// 輸出結果
// 1,2,3,4,5,6,7,8,9,

3.3. map / flatMap

使用 map 把對象一對一映射成另一種對象或者形式。

/**
 * 把数字值乘以2
 */
@Test
public void mapTest() {
    List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
    // 映射成 2倍数字
    List<Integer> collect = numberList.stream()
            .map(number -> number * 2)
            .collect(Collectors.toList());
    collect.forEach(number -> System.out.print(number + ","));
    System.out.println();

    numberList.stream()
            .map(number -> "数字 " + number + ",")
            .forEach(number -> System.out.println(number));
}
// 輸出結果
// 2,4,6,8,10,12,14,16,18,
// 数字 1,数字 2,数字 3,数字 4,数字 5,数字 6,数字 7,数字 8,数字 9,

上面的 map 可以把數據進行一對一的映射,而有些時候關係可能不止 1對 1那麼簡單,可能會有1對多。這時可以使用 flatMap。下面演示使用 flatMap把對象扁平化展開。

/**
 * flatmap把對象扁平化
 */
@Test
public void flatMapTest() {
    Stream<List<Integer>> inputStream = Stream.of(
            Arrays.asList(1),
            Arrays.asList(2, 3),
            Arrays.asList(4, 5, 6)
    );
    List<Integer> collect = inputStream
            .flatMap((childList) -> childList.stream())
            .collect(Collectors.toList());
    collect.forEach(number -> System.out.print(number + ","));
}
// 輸出結果
// 1,2,3,4,5,6,

3.4. filter

使用 filter 進行數據篩選,挑選出想要的元素,下面的例子演示怎麼挑選出偶數数字。

/**
 * filter 數據篩選
 * 篩選出偶數数字
 */
@Test
public void filterTest() {
    List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
    List<Integer> collect = numberList.stream()
            .filter(number -> number % 2 == 0)
            .collect(Collectors.toList());
    collect.forEach(number -> System.out.print(number + ","));
}

得到如下結果。

2,4,6,8,

3.5. findFirst

findFirst 可以查找出 Stream 流中的第一個元素,它返回的是一個 Optional 類型,如果還不知道 Optional 類的用處,可以參考之前文章 。

/**
 * 查找第一個數據
 * 返回的是一個 Optional 對象
 */
@Test
public void findFirstTest(){
    List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
    Optional<Integer> firstNumber = numberList.stream()
            .findFirst();
    System.out.println(firstNumber.orElse(-1));
}
// 輸出結果
// 1

findFirst 方法在查找到需要的數據之後就會返回不再遍歷數據了,也因此 findFirst 方法可以對有無限數據的 Stream 流進行操作,也可以說 findFirst 是一個 short-circuiting 操作。

3.6. collect / toArray

Stream 流可以輕鬆的轉換為其他結構,下面是幾種常見的示例。

 /**
 * Stream 轉換為其他數據結構
 */
@Test
public void collectTest() {
    List<Integer> numberList = Arrays.asList(1, 1, 2, 2, 3, 3, 4, 4, 5);
    // to array
    Integer[] toArray = numberList.stream()
            .toArray(Integer[]::new);
    // to List
    List<Integer> integerList = numberList.stream()
            .collect(Collectors.toList());
    // to set
    Set<Integer> integerSet = numberList.stream()
            .collect(Collectors.toSet());
    System.out.println(integerSet);
    // to string
    String toString = numberList.stream()
            .map(number -> String.valueOf(number))
            .collect(Collectors.joining()).toString();
    System.out.println(toString);
    // to string split by ,
    String toStringbJoin = numberList.stream()
            .map(number -> String.valueOf(number))
            .collect(Collectors.joining(",")).toString();
    System.out.println(toStringbJoin);
}
// 輸出結果
// [1, 2, 3, 4, 5]
// 112233445
// 1,1,2,2,3,3,4,4,5

3.7. limit / skip

獲取或者扔掉前 n 個元素

/**
 * 獲取 / 扔掉前 n 個元素
 */
@Test
public void limitOrSkipTest() {
    // 生成自己的隨機數流
    List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26);
    ageList.stream()
            .limit(3)
            .forEach(age -> System.out.print(age+","));
    System.out.println();
    
    ageList.stream()
            .skip(3)
            .forEach(age -> System.out.print(age+","));
}
// 輸出結果
// 11,22,13,
// 14,25,26,

3.8. Statistics

數學統計功能,求一組數組的最大值、最小值、個數、數據和、平均數等。

/**
 * 數學計算測試
 */
@Test
public void mathTest() {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
    IntSummaryStatistics stats = list.stream().mapToInt(x -> x).summaryStatistics();
    System.out.println("最小值:" + stats.getMin());
    System.out.println("最大值:" + stats.getMax());
    System.out.println("個數:" + stats.getCount());
    System.out.println("和:" + stats.getSum());
    System.out.println("平均數:" + stats.getAverage());
}
// 輸出結果
// 最小值:1
// 最大值:6
// 個數:6
// 和:21
// 平均數:3.5

3.9. groupingBy

分組聚合功能,和數據庫的 Group by 的功能一致。

/**
 * groupingBy
 * 按年齡分組
 */
@Test
public void groupByTest() {
    List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26);
    Map<String, List<Integer>> ageGrouyByMap = ageList.stream()            
        .collect(Collectors.groupingBy(age -> String.valueOf(age / 10)));
    ageGrouyByMap.forEach((k, v) -> {
        System.out.println("年齡" + k + "0多歲的有:" + v);
    });
}
// 輸出結果
// 年齡10多歲的有:[11, 13, 14]
// 年齡20多歲的有:[22, 25, 26]

3.10. partitioningBy

/**
 * partitioningBy
 * 按某個條件分組
 * 給一組年齡,分出成年人和未成年人
 */
public void partitioningByTest() {
    List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26);
    Map<Boolean, List<Integer>> ageMap = ageList.stream()
            .collect(Collectors.partitioningBy(age -> age > 18));
    System.out.println("未成年人:" + ageMap.get(false));
    System.out.println("成年人:" + ageMap.get(true));
}
// 輸出結果
// 未成年人:[11, 13, 14]
// 成年人:[22, 25, 26]

3.11. 進階 – 自己生成 Stream 流

/**
 * 生成自己的 Stream 流
 */
@Test
public void generateTest(){
    // 生成自己的隨機數流
    Random random = new Random();
    Stream<Integer> generateRandom = Stream.generate(random::nextInt);
    generateRandom.limit(5).forEach(System.out::println);
    // 生成自己的 UUID 流
    Stream<UUID> generate = Stream.generate(UUID::randomUUID);
    generate.limit(5).forEach(System.out::println);
}

// 輸出結果
// 793776932
// -2051545609
// -917435897
// 298077102
// -1626306315
// 31277974-841a-4ad0-a809-80ae105228bd
// f14918aa-2f94-4774-afcf-fba08250674c
// d86ccefe-1cd2-4eb4-bb0c-74858f2a7864
// 4905724b-1df5-48f4-9948-fa9c64c7e1c9
// 3af2a07f-0855-455f-a339-6e890e533ab3

上面的例子中 Stream 流是無限的,但是獲取到的結果是有限的,使用了 Limit 限制獲取的數量,所以這個操作也是 short-circuiting 操作。

4. Stream 流優點

4.1. 簡潔優雅

正確使用並且正確格式化的 Stream 流操作代碼不僅簡潔優雅,更讓人賞心悅目。下面對比下在使用 Stream 流和不使用 Stream 流時相同操作的編碼風格。

/**
 * 使用流操作和不使用流操作的編碼風格對比
 */
@Test
public void diffTest() {
    // 不使用流操作
    List<String> names = Arrays.asList("Jack", "Jill", "Nate", "Kara", "Kim", "Jullie", "Paul", "Peter");
    // 篩選出長度為4的名字
    List<String> subList = new ArrayList<>();
    for (String name : names) {
        if (name.length() == 4) {
            subList.add(name);
        }
    }
    // 把值用逗號分隔
    StringBuilder sbNames = new StringBuilder();
    for (int i = 0; i < subList.size() - 1; i++) {
        sbNames.append(subList.get(i));
        sbNames.append(", ");
    }
    // 去掉最後一個逗號
    if (subList.size() > 1) {
        sbNames.append(subList.get(subList.size() - 1));
    }
    System.out.println(sbNames);
}
// 輸出結果
// Jack, Jill, Nate, Kara, Paul

如果是使用 Stream 流操作。

// 使用 Stream 流操作
String nameString = names.stream()
       .filter(num -> num.length() == 4)
       .collect(Collectors.joining(", "));
System.out.println(nameString);

4.2. 惰性計算

上面有提到,數據處理/轉換(intermedia) 操作 map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered 等這些操作,在調用方法時並不會立即調用,而是在真正使用的時候才會生效,這樣可以讓操作延遲到真正需要使用的時刻。

下面會舉個例子演示這一點。

 /**
  * 找出偶數
  */
 @Test
 public void lazyTest() {
     // 生成自己的隨機數流
     List<Integer> numberLIst = Arrays.asList(1, 2, 3, 4, 5, 6);
     // 找出偶數
     Stream<Integer> integerStream = numberLIst.stream()
             .filter(number -> {
                 int temp = number % 2;
                 if (temp == 0 ){
                     System.out.println(number);
                 }
                 return temp == 0;
             });

     System.out.println("分割線");
     List<Integer> collect = integerStream.collect(Collectors.toList());
 }

如果沒有 惰性計算,那麼很明顯會先輸出偶數,然後輸出 分割線。而實際的效果是。

分割線
2
4
6

可見 惰性計算 把計算延遲到了真正需要的時候。

4.3. 并行計算

獲取 Stream 流時可以使用 parallelStream 方法代替 stream 方法以獲取并行處理流,并行處理可以充分的發揮多核優勢,而且不增加編碼的複雜性。

下面的代碼演示了生成一千萬個隨機數后,把每個隨機數乘以2然後求和時,串行計算和并行計算的耗時差異。

  /**
  * 并行計算
  */
 @Test
 public void main() {
     // 生成自己的隨機數流,取一千萬個隨機數
     Random random = new Random();
     Stream<Integer> generateRandom = Stream.generate(random::nextInt);
     List<Integer> numberList = generateRandom.limit(10000000).collect(Collectors.toList());

     // 串行 - 把一千萬個隨機數,每個隨機數 * 2 ,然後求和
     long start = System.currentTimeMillis();
     int sum = numberList.stream()
         .map(number -> number * 2)
         .mapToInt(x -> x)
         .sum();
     long end = System.currentTimeMillis();
     System.out.println("串行耗時:"+(end - start)+"ms,和是:"+sum);

     // 并行 - 把一千萬個隨機數,每個隨機數 * 2 ,然後求和
     start = System.currentTimeMillis();
     sum = numberList.parallelStream()
         .map(number -> number * 2)
         .mapToInt(x -> x)
         .sum();
     end = System.currentTimeMillis();
     System.out.println("并行耗時:"+(end - start)+"ms,和是:"+sum);
 }

得到如下輸出。

串行耗時:1005ms,和是:481385106
并行耗時:47ms,和是:481385106

效果顯而易見,代碼簡潔優雅。

5. Stream 流建議

5.1 保證正確排版

從上面的使用案例中,可以發現使用 Stream 流操作的代碼非常簡潔,而且可讀性更高。但是如果不正確的排版,那麼看起來將會很糟糕,比如下面的同樣功能的代碼例子,多幾層操作呢,是不是有些讓人頭大?

// 不排版
String string = names.stream().filter(num -> num.length() == 4).map(name -> name.toUpperCase()).collect(Collectors.joining(","));
// 排版
String string = names.stream()
        .filter(num -> num.length() == 4)
        .map(name -> name.toUpperCase())
        .collect(Collectors.joining(","));

5.1 保證函數純度

如果想要你的 Stream 流對於每次的相同操作的結果都是相同的話,那麼你必須保證 Lambda 表達式的純度,也就是下面亮點。

  • Lambda 中不會更改任何元素。
  • Lambda 中不依賴於任何可能更改的元素。

這兩點對於保證函數的冪等非常重要,不然你程序執行結果可能會變得難以預測,就像下面的例子。

@Test
public void simpleTest(){
    List<Integer> numbers = Arrays.asList(1, 2, 3);
    int[] factor = new int[] { 2 };
    Stream<Integer> stream = numbers.stream()
            .map(e -> e * factor[0]);
    factor[0] = 0;
    stream.forEach(System.out::println);
}
// 輸出結果
// 0
// 0
// 0

文中代碼都已經上傳到

個人網站:
如果你喜歡這篇文章,可以關注公眾號,一起成長。
關注公眾號回復資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試資料。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

系統分析師-軟件水平考試(高級)-開篇

系統分析師-軟件水平考試(高級)-開篇

前言

時隔一年,我開始了系統分析師的博客寫作。回過頭翻看一下,一年前的系統架構設計師系列的第一篇博客-需求理論,還是比較有感觸的。

其實系統分析師的考試早在上邊年五月份就參与了,也在六月份就知道自己通過了考試。但是一方面系統分析師與系統架構設計師有很多內容上的重複,另一方面自己確實工作也比較忙,所以相關的博客就擱置下來了。

正好最近有點空閑時間,正好一方面整理所學,一方面輸出一些博客,幫助大家。

分析師與架構師

首先,就是探討一下,系統分析師與系統架構設計師的關聯與區別。

兩者都是軟件考試的高級考試科目,並且也是相似對最高的兩門高級科目。畢竟早期軟考只有系統分析師的考試,而系統架構設計師是由於系統架構設計內容不斷增多,然後分離出來單獨成為一個科目的。

很多朋友都無法把握住兩門考試科目的區別,導致學習無法集中注意力,從而造成考試失利。

考試角度

首先從考試角度來說,系統分析師中有關架構的部分,分值比較低,可以說幾乎與企業信息化等章節一樣,就是個普通公民,不再享受系統架構設計師考試中一等公民的待遇了。其次,系統分析師由於在架構方面的分值大幅下降,所以提高了所有章節的分值。簡單說,就是所有章節的考試內容變多了。雖然深度不再深挖,但是考試範圍的擴大,導致考生覺得系統分析師內容太過繁雜,準備困難,難以把握重點。

那麼,分析師有沒有類似架構師的重點呢?答案是有的。從考試的分值散布(客觀,案例,論文綜合起來看),以及考試名稱——系統分析師,可以知道重點在分析。就像系統架構師的重心在架構,高項的重心在管理,分析師的重心在分析。當然了,由於系統分析師的特殊性(所有高級科目的源頭),所以它的重心不會如架構師,管理師那樣突出。

那麼落在考試章節中,分析又落實在哪裡呢?那就是系統規劃,需求分析,以及一些零散的涉及分析的內容。當然,如果你是第一次參与高級考試,可千萬別只看這兩個章節啊。

現實角度

老規矩,從考試的角度分析后,我們來從現實角度分析一下。當公司規模不大的時候,公司技術方面往往就一個技術部。技術部會有一個負責人,他會負責所有技術相關的問題,包括但不限於:

  • 技術顧問:負責解答公司高層對技術的疑惑
  • 技術評估:參与公司項目,產品的技術評估
  • 技術規劃:為公司的戰略目標,提供技術方面的長遠規劃
  • 技術管理:為領導的技術部門進行有效管理
  • 技術支持:為領導的項目提供技術的直接幫助

公司規模不大的時候(百人以內,技術部門二十人以內),負責人尚且還能支撐得住,能夠在各方,各個工作間周轉開。不要問我怎麼知道的,問就是我在上家公司就是做這些的。

但是隨着公司規模增大,技術部門的人數增長。技術負責就不可能面面俱到了(某些牛人,就算了,咱只說正常情況)。

到了這個時候,原有技術負責的工作必須進行拆分。在中型公司,比較常見的是採用矩陣型的組織結構,原技術負責的職責拆分為:

  • 技術總監:負責技術顧問,技術規劃
  • 項目經理:負責項目管理工作
  • 技術負責:負責單個項目的技術支持,配合項目經理與技術總監,完成技術評估,配合技術總監完成技術規劃的落地。

(這其中的需求,往往是三方的協調,妥協的一個結果。如果不懂得這句話,可以等到我開啟項目管理的分支,再細談。或者私聊我)

不要問我怎麼知道的,問就是因為年中有一個以前的上司來挖我的時候,和我提到了他們公司的情況。

可能你們要說,還是沒有看到系統分析師啊?別急,馬上就到了。

隨着項目規模的擴大,項目內的技術負責壓力就比較大了。一方面需要技術上司,業務方,項目經理打交道,了解具體需求,進而進行分析,另一方面還需要進行項目信息系統的架構設計,搭建,為下屬提供技術支持。所以,部分大型公司就再次將技術負責拆分為業務分析師與技術架構師,也就是大家說的BA和架構師。不要問我怎麼知道的,問就是這個月,打電話挖我的公司就有這麼做。當然,也有人注意到,需求這個東西不是應該交由項目經理處理嘛?怎麼說呢?一方面,有些需求只有技術人員才有那個敏感性,另一方面,項目經理雖然也有獲取需求這一過程,但並不表示只靠項目經理自己去獲取,更多的是需要依靠具體的人落實,後者具體的人配合落實。項目經理本身更多是一個協調整合的人員,而不一定就是具體落實的人。

學習必要性

可能有些朋友就要問了,大型公司才用到,那是不是對於很多人來說,這個考試的學習就沒有意義了。

當然不是。

首先,即使是在中小公司,分析師的學習會補全架構師在業務方面,商業層面的不足。在一家中小公司,一個幾乎只會談論技術的(雖然有着非常高超的架構水平,但不是每個公司都有“伯樂”的)與一個可以談論公司業務,可以為公司戰略發展提出一個考慮了商業內涵的技術方案的,相信後者會更得Boss的歡心。

其次,不想當將軍的士兵不是好士兵。不想去大公司露一手的,不是好員工。人嘛,總是要有一顆上進的心。

最後,我們需要提升自己視野,如果只局限於技術的維度,很容易把自己的職業道路走窄了。舉個例子,馬雲評價行癲,不僅有足夠的技術,更有着敏銳的商業視野。後面的故事,大家也都知道了,行癲上位(甚至現有的公司紛紛提出公司組成要有八成的技術人員,也不知道有沒有這方面的原因。囧)。

學習困難性

分析師學習難不難?

從數據角度。系統架構設計師的考試就比較困難了,其通過率接近8%,而分析師的通過率就只有系統架構設計師的一半不到,其通過率約為3%-4%。

從內容角度。套用一位老師說的話,從內容的深度而言,分析師的內容深度與系統架構設計師差不多。但是內容的量級上,分析師的內容量級比系統架構師要多(大概1.5倍吧,但是如果從架構師轉過來的話,只需要再學習0.7左右的內容)。

從抽象角度。對於有開發經驗的人而言,架構師中提到的技術,以及架構思想,起碼在經手的項目中能夠比較直觀的感受到。而分析師提到的系統規劃,需求分析等內容就不是每個開發人員能經手到的了。當然,對於沒有開發經驗的,那麼兩者幾乎是沒有什麼差別的。

XMIND

給出一個XMIND,讓大家比較直觀地感受到系統分析師的知識體系。

學習方法

那麼有沒有什麼辦法可以提高學習效率呢?

當然是有的。雖然我在架構師考試博客中推薦了許多書籍,但是分析師的書籍真的幾乎沒有,所以就不推薦了(畢竟也有一些人認為沒有時間看那麼多的書籍)。

說一下我的學習方法:

  • 首先,看一下教材的目錄,了解往年考試情況與分值分佈情況。然後有目的性地快速看一遍教材,不求甚解,只求留個印象。
  • 其次,配合XMIND,寫下各個章節的重點內容,從而建立知識體系(我十分看重建立知識體系,包括面試別人的時候)。
  • 然後,配合XMIND,按照重要程度,去細看教材。不大清楚的地方,還會查閱資料,詢問群友什麼的。
  • 最後,就是做題啦,每個章節學習完,都會做章節練習,判斷自己對這一章節的認識,並了解題型。另外學完所有章節(起碼是自己認為應該學習的章節)后,還會做模擬題(盡量還原出考場的感覺)。最重要的,別忘了錯題集,真的有用的。

總結

如果你想要參加考試,第一件事情就是需要明確自己是為了知識而來,還是為了考試而來,抑或是兩者都有的。

另外,我這邊確實有一個關於系統架構師/分析師的群,但是是邀請制的,也就是說給你群號也沒用。如果有參与考試的想法,可以私信@我。

最後,只想說一下,軟考高級是個好東西,但是也不可能讓你立馬上天的。它只是一個加速器,一個倍增器。就像架構師的考試,給了我一個很好的知識體系,雖然非常空蕩蕩的,但是我可以不斷向其中填充具體的技術。目測架構師考試的紅利,我至少還可以吃個三年。至於後續的分析師與管理師就更不用說了。最重要的是提供了非常好的視野,而視野這個東西,無法直觀地帶來薪水,職位的提升。但是這個東西的好處真的很多,關鍵其它途徑很難如此快速地獲得它。

最後,希望我的博客可以為大家提供幫助。謝謝。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

南投搬家前需注意的眉眉角角,別等搬了再說!