顯示具有 JAVA設計師的絕境與哀愁 標籤的文章。 顯示所有文章
顯示具有 JAVA設計師的絕境與哀愁 標籤的文章。 顯示所有文章

2019年4月10日 星期三

【Android AES】SHA1PRING + Crypto

Google提供的範例檔案


好像是Android6以後,AES加解密如果是使用SecureRandom這個物件搭配演算法「SHA1PRING」和Provider「Crypto」來取得RawKey(用來產生SecretKeySpec時需要的導入參數),會發生失敗的情況。

雖然Google提供了一組叫做CryptoProvider的物件在網路上讓人下載(或複製貼上),但在Cipher進行解密(執行dofinal這個函數)時一樣會出錯。

Google這個超級天才就繼續提供了一組物件叫做「InsecureSHA1PRNGKeyDerivator」在網路上,(載點在上面,)來解決這個問題。


這個物件的用法很單純,就是呼叫一組static物件取得Instance然後再導出 RawKey即可。

方法大概是這樣...

byte[] raw = InsecureSHA1PRNGKeyDerivator.deriveInsecureKey(seed, 16);

比較詭異、比較巧妙的是這個「16」,因為大多數使用這個物件的範例都使用「32」,可是解密部分一樣會在Cipher的地方出錯。這似乎是看你使用的Key長短而定,或是你設定的加密位元。

(32是256位元?16是128位元?)

其他部分都可以循著一般標準範例。

2019年2月19日 星期二

【Android】Mac Adress的可用求法(純Java、不含Android API)

根據官方文件的說法,所有可行的Android API都已經不可以取得Mac Address。

所以要取得Mac Address,必須要回歸原始Java方法。


public static String getMacAddr() {
    try {
        List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
        for (NetworkInterface nif : all) {
            if (!nif.getName().equalsIgnoreCase("wlan0")) continue;

            byte[] macBytes = nif.getHardwareAddress();
            if (macBytes == null) {
                return "";
            }

            StringBuilder res1 = new StringBuilder();
            for (byte b : macBytes) {
                String hex = Integer.toHexString(b & 0xFF);
                if (hex.length() == 1)
                    hex = "0".concat(hex);
                res1.append(hex.concat(":"));
            }

            if (res1.length() > 0) {
                res1.deleteCharAt(res1.length() - 1);
            }
            return res1.toString();
        }
    } catch (Exception ex) {
    }
    return "";

簡單來說就是去取得無法閱讀的編碼然後轉換成Mac Address格式。(但也不是真的無法閱讀,只是送出的資料為數值,要轉16進位、還要轉成統一文字格式。

方法很簡單,但是困死許多人。



2015年2月6日 星期五

這些年在Android上,我看過的網路傳輸並接收資料的模組架構,還有Intent、SingleTon模組、和Adapter機制

【我不是要寫文教大家怎麼去POST/GET資料,也不是要自以為是的跟大家分享Socket經驗(雖然我很想......),我是想要分享我在設計Android APP時,設計「接收資料並呈獻到畫面上給使用者看」的過程中遭遇到的種種狀況。

這文對工程師意義可能不大,因為裡頭會談到一些Android架構的限制,工程師可能比較好奇怎麼凌駕這些限制,但我個人選擇和這些限制和平共處,──所以我的經驗和意見可能是不中看也不中用,但對非技術底的人來說可能比較有價值,例如設計或PM,因為在構思功能和流程時,不需要太多知識與經驗也知道要避開這些現象。】



Android的問題在於除了「Activity生命週期」的幾個階段被底層呼叫後,畫面一旦生成就不能隨便更動。要更動就必須要經由Handler、BroadCastReciever、或另起Activity。不然就必須要使用本來就可隨意更動的SurfaceView。

所以當畫面原本設計目的就是「不時要被網路資料更動」時,...注意!網路資料進出的時間點本來就不可能會等APP的「生命週期」,結果不是造成APP掛點,不然就是畫面跟實際接收到的資訊有落差。(前者是因為在生命周期外更動畫面,後者是因為生命周期外接收到的資訊不會反映在畫面上。)

要處理這個,我見到的兩大派作法:
1.大量使用Adapter,將資訊裝入Adapter中後進行簡單的「自動更新」。(給非技術底的:如果簡單描述Adapter,它就是個會針對資訊的內容快速產生畫面的工具,例如一百筆記錄了連絡資料的JSON。它有些限制,例如每筆資料間要最低限度的共通點、針對每筆資料產生的畫面通常也只能操做該筆資料...等。說它簡單,因為要啟動它的「自動更新」很簡單,而且「自動更新」的內容要多複雜就可以有多複雜,幾乎可以完全無視起動的時機點──因為系統會自動幫APP管理。)
2.在上一個Activity中使用一個Thread確保網路資料都已經接收完畢後,再啟動新的Activity、並直接使用剛接收到的網路資訊來決定畫面內容。(給非技術底的:這些字數完全無法描繪它實際上需要做的事情。)

第一種是Google在設計Android基本功能時常用的手法,不管是GMail、Google Play、或一些管理功能這類進行「系統控制」的「APP」。(對!一樣是給非技術底的:那些都是APP,Android底層是Linux,本來是只有文字的東西而已,凡是圖形介面、包含解鎖畫面都是種APP。)連FaceBook也是使用這樣的方式製作最新、最穩定的APP版本。(注意!這真的是最穩定,而且可能是最完美的APP型態。)

第二種在許多客製化打造的APP中經常使用。例如時不時接收網路傳來的JSON資料,然後將它分散到畫面上的數塊文字訊息欄中,或是變換背景顏色,甚至野心很大的更動它的佈局。



關於第一種,參考GMail、Google Play、還有FaceBook就可以知道這種設計方式的最大特徵。它們的畫面並不單調,程式不管是使用者體驗或功能都可以維持一定的穩定順暢。反觀第二種,就很考驗設計者和QC的耐心了!

以下是個範例客題:如果畫面被要求動態的、隨機的產生無數個欄位,每個欄位的內容功能都要隨網路資訊而有不同變化,而且網路資料的量、規則都無法預估.......

如果是第一種設計法,只要確保Adapter有呈現每種資料的能力,而網路接收資料正確、傳遞給Adapter的順序也如網路資料預估.......然後要求系統去自行「更新」即可。不管重新接收多少次資料、刪除單筆、插入多筆.........,都可以維持這個簡單的線性流程。(操作資料......操作一次、操作兩次、操作三次......操作N次,然後要求Adapter更新內容,一定可以精準的看到結果。)

如果是第二種設計法,就要先把畫面維持一個空白的大框架,然後接收完網路資料後開始數算數量,然後預估會需要在大框架中塞入幾個欄位,接著就開始逐一生成欄位。........

聽起來沒什麼、兩種差異不大,問題是用第一種方案中一行程式碼就可以做到的事情,為何要像第二種方案這樣寫數十、甚至數百行程式碼呢?

很多專案遲遲無法收尾,就是因為工程師被迫使用第二種。最後不管他們寫再多程式碼,都會發生「為何第一次新增條目時不會當機,刪除後新增就當機了,你的新增功能真的有做好嗎?」這類的Bug。因為第一種方法強制要求APP的畫面一律跟資料看齊,而第二種方式其實是寫很多修改畫面的程式碼,然後隨時判斷資料內容來決定要使用哪段程式碼修改畫面,只是這些方法即使相似處很多,但不是「永遠不夠用」,不然就是「會打架、會矛盾、會互扯後腿」。



另一個問題是我身為一個半路出家的技術狂最感興趣的現象。就是程式的「穩定壽命會縮短」。(以下所說其實已經是種理論性的天馬行空胡亂講...只是因為真的發生過,所以我寫下來。)

Activity是Android切割畫面與功能分界的一種機制,意義就有點像網路的「頁面」。

即使是同一個Activity之間,也無法直接交換資料,需要經過一些比較繁瑣的方式去設計它。

例如Intent,即使預設目的並不是要夾帶「畫面資料」,但很多人都會選擇這樣設計它。將下一個畫面的「標題」、「背景」...等資訊夾在Intent中,發送給下一個Activity。聰明的人、經驗夠的人都一定會經歷到Intent沒有忠實的把所有資訊傳給下個Activity的現象,例如前十筆資料都有帶到,後十筆資料就不見了。(這可能是因為Intent是用C/C++實做的,它並不是完美的物件導向定義出來的物件,即使它有嚴謹的建構子,但它其實會被轉換成非物件的型態在Activity和系統間傳遞,轉換回物件時,資料就消失在記憶體中了。──歡迎技術底更強的人吐嘲!)

所以老手應該都會使用Intent以外的方法傳遞資料。最常見的就是SingleTon。

所謂的SingleTon,就是在記憶體中畫置一塊區域,存放一個物件,然後這個物件中有存放資料的功能。因為這個物件嚴格說起來是「人人(任何Activity)都可以摸到裡面,所以只要知道這個物件存在的都可以往裡面塞資料、取資料」。

如果看過我其它文章的人就知道:使用SingleTon的人不見得很懂物件導向,他們只是覺得短短幾行程式碼可以有這個功用,就把它拿來用吧!

不管是SingleTon,或任何一種第二類資料傳輸法,理論上都會有個風險,就是記憶體會不夠用、記憶體管理會出錯。

不懂電腦原理的人一定會質疑「記憶體需求不都開好、規畫好,怎麼會不夠用?不夠用不能改良嗎?」

事實是:資料在記憶體中不會自己判斷自己是不是「已經不被需要了」,必須要程式要求它「請從記憶體中消失」,或是設計者關機斷電。一般來說,C/C++程式設計師都要自己管理這個動作,自己產生的、自己主動讀取的取用的資料,都要自己下指令、寫程式把它們清除;但Java/Android工程師不是如此,只要操作一種類似「索引值」的東西,系統內的某個特定程式就會定時按照索引值來清除記憶體內不要的資料。

不是開玩笑,真的有一支專門的系統程式負責這個工作。

但某些因素讓這支程式工作起來不太精準,經常會發生已經被排入要被清除的資料,清除程式竟然認為自己不可以清除它,但也不知道該拿它怎辦.........(或許是「索引值」管理程式出問題?或其他?)當這些「不知道該怎辦的資料」在記憶體中越堆越多,最終的後果就是程式效能越跑越慢、甚至當機。(所以重開機通常可以讓Android效能恢復,可見這個現象不是神話或笑話。)

可是「因為Android和Java的共通性」這種話講起來是很讓人心虛的。重點是:程式的動作越多、程式碼越多、管理調配的動作越多,就越容易產生這些「不知道該怎辦的資料」。譬如不用Adapter來產生動態畫面的APP在刷新畫面無數次後,最終的命運就是記憶體被那些「淘汰」的畫面元件吃光光.....(被丟掉的畫面變成「不知道該怎辦的資料」。)

使用第一種方法並不會保障「不知道該怎辦的資料」絕不會產生。

但是.......舉例來說,如果使用Intent傳遞資料,結果可能會造成資料傳遞完、Intent的一部份變成「不知道該怎辦的資料」,如果大量、頻繁使用Intent,結果就是造成APP從開始到「因為記憶體被這些資料吃光而無法正常運作」的周期縮短。

SingleTon也如此。

重點在於:第二類方法總是會造成「特定Activity需要的資料必須要在其他Activity中產生,並且使用額外物件管理或傳遞,處理跟接收本身也要產生額外的物件」,──附帶一提,所謂的物件也是種「資料」。這些物件越多,也就表示資料越多......

所以在單一Activity當下接收並管理資料,怎麼看都是王道。



寫這些,是因為公司某前輩離職前寫的程式開始被客戶抱怨「使用久了、操作頻繁了,就會當機」。

大老闆正在四處找更資深的工程師來解這個問題,我雖然知道解法、而且在別家公司也成功處理過類似的狀況,但.........先安靜.........

2014年6月28日 星期六

三張圖,讓人看懂Java Socket真正的用法

Socket的結構、特性......不難查到相關資料跟說明,例如怎樣設定它的Port、怎樣跟特定IP建立聯結、怎樣檢查連線是否成功........

但是實際上會用Socket建立TCP/IP或UDP連線的人很少,要做網路功能,URL和J2EE幾乎把這塊領域吃光光........

怎麼回事?



因為Oracle官方的教學文件本身就是塊爛帳!

要看清楚這塊爛帳,除非老師上課有講清楚、教學文寫者有意識到,不然讀的人最後都會撞死在「為什麼連線建好了卻不能用」、「為什麼連線的效率差到根本無法運作」、「為什麼最後會徹底失去建立Socket和Stream的能力」......上。

把「網際網路通訊原理」丟一邊,Java設計師/工程師其實需要理解到的本來就只有「這樣就可以跟遠端裝置進行通訊」這個概念就好,(而且這不正是「Socket的精神」嗎?)但是Oracle的官方範例僅止於「一條龍式的示範可以丟入怎樣的參數來建立Socekt,建立之後可以優先使用哪些參數或得哪些回傳物件........」但是對於真正重要的Stream使用就幾乎支字未提。

而且我可以保證同樣都是Stream,在網路跟單純的檔案管理上頭,這兩種Sream的使用與操作經驗完全不同。(我現在會這樣說,十年後還是這樣說。「這是一樣的東西啊!」那完全是老鳥的嘴砲與傲慢。)

不管是Oracle,或是網路上、課堂上無數的範例與教學,根本沒想過要讓人搞懂「Java怎麼建立連線環境」的全貌,都是不停丟出片段的知識而已。

(不知道多少初入門的Java設計師死在這上頭,最後只能一把鼻涕一把眼淚的強迫自己接受J2EE的強暴、放棄自己滿腦子本來可以用J2SE實現的應用與設計。真是殺死人的絕境、與欲哭無淚的哀愁啊!)



第一步,建立Socket。
不管是用直接指定IP的方式,或用ServerSocket。
總之,想去「取得」一個Socket。 


 從第一步中獲得的Socket產生兩個InputStream和OutputStream。
可能是Thread中產生兩個全域變數Stream;
或是直接產生兩個Thread裡面帶有Stream變數。

將兩個Stream丟給不同的Thread(不是負責生成Socket的同一個Thread),來進行資料的讀寫操作。

這裡頭牽涉到一些細節,例如負責生成Socket的Thread要怎麼管理已經生成的Socket?

提示:絕對不是馬上關掉它。Stream一樣也不用馬上關閉,除非在測試中發現機器沒辦法同時負荷那麼多Stream(原理...待查中...)否則Stream做好,除非要結束連線......建議:就放著吧。(不然連線的效能拉不起來。)



為何要獨立一個Thread來管理Socket生成?

當機器要能夠具備跟多個對象溝通的能力時,Socket絕對不會只有一組,可能最後要建立多組,特別是使用ServerSocket時,但問題是ServerSocket的avaliable()指令會霸佔住整個Thread,所以對於已經建立好的Socket,如果要保有繼續操作它們的能力,就要用另外獨立的Thread來操作,另外Socket要有建立流程、自然也要有關閉流程,一個Thread中怎麼同時管理多組Socket、操作多組InputStream/OutputStream?(神人會說「有何難?不就幾百行程式碼而已!」........想當那種神人,我就不會開這個部落格、寫這些文章分享這些經驗了。)

領悟力好的人會理解這點,根本不需要我這三張圖,.......但問題是領悟力好的人有多少呢?況且說這些人領悟力好...這些人又有多少有能力自己從底層去寫自己的Socket物件來管理網路連線?........還不提我看到一家規模中上等級的公司開出「Java工程師,懂Socket使用,會管理TCP/IP連線......」的徵人啟事,薪水45K(起跳),結果過了一個月還沒人應徵.......

2014年1月17日 星期五

[Java/Android]選取Spinner的內容同時改變Button的功能 (我會怎麼用純物件的方式寫這個功能)

(接續上一篇)還是這個功能/介面,資料處理方式可能是「各種加密方法」或「將資料傳向特定網站或資料庫」。

用「進化」的觀點來看,結構化程式設計技巧確實比物件化程式設計技巧更基礎。(前者在60~70年代出現並成熟、被發揚光大,後者雖然同時期出現,但Java卻是90年代的東西了。)

不用物件導向技巧還是可以在物件導向語言裡寫出可以運作良好的程式,只是...「維護」和「擴充」(特別是要由第三者來維護跟擴充)就很讓人「蛋疼」了。(女工程式請不要介意...)




我會先設計一個這樣的介面Interface(不是UI介面)。

看名稱應該就知道,這是擴充後要給Spinner/下拉彈出式選單作為內容的介面。

我擴充這個介面產生一個新類別,並在getText()中設定「return "功能一"」,如果用這個類別宣告實作物件,並將物件塞給Spinner的SpinnerAdapter,則選單中就會有「功能一」這個選項。以此類推...要多少有多少。(中間有一些設定SpinnerAdapter的細節,如果不知道自己推敲應變,那我這段描述會變得很怪。或者你知道使用其他Adapter來取代......)

然後再設計一個OnItemSelectedListener,──給Spinner使用的。這個OnItemSelectedListener具有「偵測」使用者選取了哪個Spinner選單內容的能力。(看到第22行的程式碼,那是個函數,裡頭有個「int position」,這就是「使用者選擇的參數」,──或者說是「選單中的第幾個選項」。)

Button b1就是「送出」鈕。藉著將這個Listener設定給Spinner,程式會即時、動態(使用者每選擇一次選單內容)的變換這個按鈕的OnClickListener。(list就是用來「裝」選單內容的地方/物件。)

(上面這個程式範例有嚴重的錯誤就是「它是OnItemClickListener」。但Spinner雖然保有這個屬性,但卻無法使用,執行Spinner的setOnItemClickListener......還沒成功過。幸運的是,除了名稱不一樣以外,它和OnItemSelectedListener結構與傳入參數都一模一樣。)



如下圖,這是Activity,可以看到我一共產生了三個選項,分別是「Test1」、「Test2」、「Test3」。(恕我省略HsuSpinnerAdapter的內容。因為就是個繼承自SpinnerAdapter的類別)



這支程式只會很可笑的把「Test1」、「Test2」、「Test3」作成小框框內的文字顯示在畫面上而已。但確實是「三個不同的功能」。



如果想要,可以自己設計獨特的SpinnerItem,然後放進list中,未來要擴充功能就會變得很簡單。

「難道不用物件導向技巧會變得很難寫嗎?」

這樣假設吧......今天有個「Test1.5」要放進「Test1」和「Test2」之間,那可能只要在ArrayList<String>的 「Test1」和「Test2」之間放進「Test1.5」,就會出現這個選項。

問題是「送出」按鈕的OnCLickLitener中,要將所有判斷式的position值重新修改,......其實也不難,但是如果又出現了「Test1.4」、「Test1.3」、「Test2.1」.......無限增加中,只要這個程式還活著、專案管理和企劃人員腦筋動個不停,這種事情就會發生。

當OnClickListener的程式碼長達八九百行、甚至一千五百行時,不停重複修改一支Class就不是讓人很愉快的事情了。

所以為何不在程式剛開始、專案剛起頭時,想好一個將來方便擴充的機制呢?

(文章寫到這裡,我又想到一個修改SpinnerItem的方法,將來就算要直接新增一個選單,──也就是一共會有兩個選單──也可以輕鬆擴充、修改的設計,......物件導向就是這麼迷人。)



上面的那個問題好像有點廢話,但問題是「有些工程師會認為像我這樣寫程式才是廢話。」

因為「物件架構」不是一翻兩瞪眼的東西,如果架構不可行,理論上有時後看不出來,要有具體程式碼才能驗證,而且驗證一次不夠,可能要兩三次。

第一次驗證失敗後,已經有一個功能和三千行程式碼,第二個功能進行設計完成時,要進行第二次驗證,結果又失敗,這時已經有六千行程式碼,結果這六千行幾乎都要打掉重寫,重第一個功能開始驗證......重複這樣的過程,直到確定架構可以重複運作在...「所有功能上」。

可能五六個星期已經過去了。

但是......直接抓起規格書,開始用土法煉鋼的「一個功能就一個物件/Activity」這樣寫,可能兩個禮拜就把初步規格書上所有的功能都完成了。

當這些工程師可以不斷一次又一次超前進度、快速精準完成專案時,「物件導向」就變成了「Java語言不方便的特性」而已。去精熟物件架構、去專研怎樣設計物件架構、累積設計物件架構的經驗.......只是一場笑話罷了。




這就是JAVA設計師的絕境與哀愁啊!物件導向其實好學!只要認真寫教學文、教學範例。甚至可以用Library重新包裝所有JAVA基礎程式指令,例如更精簡、更快速的Socket建立方式。寫JAVA?哪那麼難?

但是一堆人把JAVA當成單純的結構式語言、當成C/C++工程師找工作時的代替品,認為「要精通很多指令」(或說是「同一個指令要有很多次使用的經驗」)、「要能閱讀並掌握大量程式碼」、「要能快速、用直線思考的方式實現規格書的需求」......才是工程師之道。

2014年1月16日 星期四

[Java/Android]選取Spinner的內容同時改變Button的功能 (為何程式設計師流動率高?)

上面(假設)是個Android功能的UI,資料處理方式可能是「各種加密方法」或「將資料傳向特定網站或資料庫」。

EditText和Spinner的使用及設定方式,我不想花太多力氣詳述,(我講概念/心得/天馬行空亂撞的結果,不講別人寫死的技術規格。)

一般來說,程式寫法很多、怎樣寫都行,但我覺得最糟糕的寫法就是用匿名/無實體的類別搭配ArrayList<String>來作為SPinner的Adapter。



首先在會使用到這個介面的Activity中設定一個參數int p,同時設定一個OnItemClickListener,在這個介面的功能onItemClick(AdapterView<?> parent, View view, int position, long id)中,int position參數可以傳給p( p = position )。

「送出」按鈕的OnClickListener則是會判讀參數p的值,如果是0,就表示使用者選擇了方法一,如果是1,就表示使用者選擇了方法二...以此類推,然後在各個判別條件後寫入真正的「方法一/方法二........」的程式碼。



而且,這些Adapter/OnItemClickListener/OnClickListener都是用匿名/無實體的類別來設計。

程式碼全都擠在一支Activity的程式中。 (對這些人來說,Java的「物件導向」只是寫Java程式不得不應付的「規則」和「語法」罷了!類似「每支程式都是用class這串字當開頭」之類的限制。他們很擅長應用別人寫好的物件、把那些物件當成基礎指令套件一樣使用,他們知道怎麼把程式結構化,但他們並沒有真的在思考怎麼把程式物件化。)



我碰到一堆人把「這樣才是成熟的工程師寫程式該有的風格」當成聖經。

即使這個功能將來如果要新增Spinner中的「方法」好達到擴充功能的目的,結果接手的人找不到「方法」寫在什麼位置,而導致整個程式只能無條件要打掉重寫........(一個功能只有一個輸入框、一個彈出選項、兩顆按鈕的功能不太可能發生這種「接手的人找不到舊功能程式碼寫在什麼地方」的窘境,所以大家假設一下這個功能有三個輸入框,同時有兩個彈出選向來交叉決定「功能」,還有三個按鈕可以選取,同時這個功能要能應付至少六種客製化版本........)

怎辦?「換個人來寫程式。」

他們永遠不會發現「自己家的Java工程師流動率非常高,特別是菜鳥陣亡率奇高無比!」他們只會慢慢的歸納出一個結論:真的會寫程式的人好少。



(下一篇......)

2014年1月15日 星期三

[Java]用介面Interface使用「用介面Interface訂出來的參數架構」

 (接續上一篇「用介面Interface設計參數架構」...)

有點饒口的標題。

我相信(因為這也曾經困擾我)一個程式語言的語法好學習,迴圈、判斷式、運算子、函式定義、物件宣告......可是怎麼把這些「規矩」組合成「功能」,就很難學......

因為鮮少有人在教。

如果在網路上、在論壇上,明確的講出自己想要的功能,其實得到的往往是連「模糊」都稱不上的答案。當然不能怪那些程式老鳥們這種態度,畢竟「又沒錢拿」,如果菜鳥們、出學者們都乖乖掏個幾千、甚至幾萬塊去走進補習班和短期課程,學程式設計肯定會好混許多。



可是學習方式應該有很多種。從「規矩」到「具體功能」之間,明顯有個難以跨越的門檻,沒理由要讓「任大家自生自滅,可以自己領悟的人才有資格進入程式設計殿堂」變成放諸四海、天下人接要遵守的規矩。

所以我用我的風格寫這些文章,或許......有人看過這些文章後可以有比讀其他文章、鑽研被公開分享的範例更有效率的學會程式設計。



接續上一篇「用介面Interface設計參數架構」...

(別告訴我你不懂何謂參數......我還沒準備好寫那麼基礎的東西。)

用「介面設計參數」這話直覺上違反Java特性,因為寫在介面中的參數會變成final形態,是不能被修改的。

可是 (請注意接下來這段話特指物件導向,所以看完立刻以為「你挑出了明顯的錯誤和例外」的人請去重讀物件導向) ...「物件導向的特性讓參數和函式在記憶體中都只是個位址,差別在於參數的位置可能導向另一個存放數值的記憶體區塊,函數的位置則是導向存放一連串的運算式、指令、判斷式、迴圈的記憶體區塊, 共通點在於它們都是個「另一段記憶體區段的索引」罷了,(主要的差異在於:函數存的索引值不能改,可是參數可以。另外就是程式碼的寫法不同。)

所以.......「為何不直接把函數拿來當參數用?反正在多型性的程式世界中,編譯器才不會好奇為何設計者設計的父類別沒有參數只有函數。」

但這都是 「用介面Interface設計參數架構」中的內容。



這篇講的其實是「多形性」到底怎麼應用來寫程式。或是說「用多型性寫程式」到底是會寫出什麼樣的程式。

這個介面只有定義一個函式。而這個函式要求傳入一個屬性為ValueInterface的參數。

為何只定義一個函數?

這是因為「大家習慣用IDE編輯器來編寫Java」,而「IDE編輯器(這裡是指Eclipse)讀取註解時有些特性」。

下圖是個「實作」這個介面的程式/類別/物件...(我又迷失在專業術語中了。)

先不看函數中的程式碼,(此為物件導向寫作法的好習慣,)注意一下開頭的註解(第36到39行的程式碼),這個註解會在任何寫了Operate1這個物件名稱的地方跳出來。(只要把滑鼠游標一到上面,就會跳出這段程式碼。

像下圖...可以看到游標一到Operate1上,就自動彈出一個小視窗,顯示註解內容。

有趣的問題是ops這個矩陣中,不管是ops[0]、ops[1]、或ops[2],都有operate()這個函數。

可是如果把註解寫在Operate1的operate()函數上,游標移到接下來的程式碼中出現的operate函數時,什麼都不會顯示。
因為...這個operate()是介面ValueOperatorInterface的operate(),不是Operate1的,而我在介面中沒有寫任何註解,自然不會顯示。

詳細一點說明為何只設計一個函數:當這個物件只有一個函數時,你寫在建構子上的註解自然會是它唯一的功能說明,閱讀性理論上會好一點。可是我工作的地方,大部分的工程師都還是習慣結構導向寫法,物件導向...頂多就是「記得寫個建構子」,多型性?好像沒啥人真得在活用。



基於多型性的理由,我不會考慮直接用Operate1當成參數的型別/屬性,(但是這樣設計程式沒什麼錯,更沒什麼問題,就是有點可惜罷了。而且如果要這樣作,就沒有必要寫介面了。)

但說到所謂的多型性...我可以隨意組合ops矩陣的內容,而且都會成立、都可以精準運作。(除果出錯,絕對是程式出錯,而不是「多型性」這個原則有例外。)

有了多型性,我設計好了各種不同的Operator類別,然後在各種不同的程式中組合它們......我就可以快速擴建出各種不同功能的程式。




一般會反駁我這種寫法、認為這樣很多餘的人,都會認為...「與其我設計三種Operator類別,不如我在ValueInterface中額外增加三個函數,(甚至直接就用物件寫,不需要經過設計ValueInterface的步驟,)然後把本來要寫在三種Operator中的程式碼般到三個函數中。」(參考一下我平常測試的方法...這次不附上測試方法了。不過,如果覺得這樣測試根本不對......根本不重要啦!原因...繼續看下去。)

所以我一開始要解釋參數與函數的差異......

我原始的想法是認為......一個只有兩個函數(一個讀取參數、一個寫入參數)的介面會在記憶體中佔去兩個單位的記憶體,一個有五個函數的介面自然就是在記憶體中佔去五個單位的記憶體。如果用矩陣大量操作這個ValueInterface,例如說「現在要1000個ValueInterface」,想想看「2*1000」和「5*1000」,會有多少差距?

但後來發現......其實不會有差。設計兩個類別,都只有一個參數,然後甲類別有兩個函數,乙類別有五個,分別用一個ArrayList無限量去「產生、並收納」,結果會發現...我在Android模擬器下分別去執行,會發現大概都在89300000次的執行上下時,會出現「OutOfMemory」的錯誤。

反過來,如果將乙類別的函數全砍掉,改成八個參數,(參數的結構不詳述,)結果執行次數一口氣就掉到14300000左右而已。

簡單來說,我對於在物件導向機制下Java應用記憶體的方式假想試錯誤的,「函數並不會隨著類別宣告為實體物件而增加佔去的記憶體空間,只有參數會。」不管類別A被產生了幾個物件實體,函數都導向/共用同一個記憶體區塊。

(所以我上面說「測試方法對不對」並不重要......仔細研究了一下物件導向的機制,確實如此。)

我並不能夠說服自己:這樣寫,程式的效能和記憶體的使用效率比較高。──並不會!但我還是繼續這樣寫程式。



為何要這樣寫程式?因為......

專案方便擴充!提高程式碼利用度!

如果不盡可能徹底使用物件導向,一個程式未來如果要擴充功能,難度明顯會高許多。

上面提到的「與其我設計三種Operator類別,不如我在ValueInterface中額外增加三個函數,(甚至直接就用物件寫,不需要經過設計ValueInterface的步驟,)然後把本來要寫在三種Operator中的程式碼般到三個函數中。」假設一支程式經過無數次的改寫擴充後,已經從三個函數增加到十三個函數,每個函數的平均工能從三十行程式碼增加到六十行程式碼......然後今天又要再增加兩個新函數,然後有至少五個舊函數的功能要修改........請便吧!看到這種程式碼,我每次都有罷工的衝動。

把要新增加的功能額外獨立成一支程式/類別,然後再要擴充的地方增加一個參數。像下圖,我只要新設計一個實作 ValueOperatorInterface的類別,然後在switch中找個地方插入它即可......

雖然這樣沒有徹底作到「舊程式永遠不會膨脹」,但是修改的難度低、幅度永遠不會過大,而且程式的邏輯/模式永遠不會變化太大,不會因為逐次擴充/修改/維護,而失去原樣。

這個範例使用的是switch指令。假想一下今天使用的是「if...else...」然後每一段「...」裡頭都有數百行程式碼,第一次拿到程式碼、又要再上頭擴充功能的人,可能就會迷失在追蹤各種if條件值中,──可能很難搞懂自己新增加的if條件為何沒有順利發揮作用,到底是哪個if、或哪個if中的if把程式的執行判斷權拿走了。(「執行判斷權」?我又發明了一個莫名其妙的名詞。)

當那「數百行程式碼」被縮減成一個物件和它的函數時,if就很好追蹤了。(就像這個switch......天哪!比這個switch還好用。)



另外,如果不盡可能徹底使用物件導向卻又牽涉到「版本控管」......先假定物件導向跟專案管理無關,一般的專案管理就只能期望專案管理軟體夠強、或專案團隊製定的專案管理流程夠嚴謹.......好笑的是:JAVA物件導向的精神之一就包含了解決上述問題。

使用Java撰寫程式不善用物件導向特性,卻期望「專案管理」軟體代勞......我不知道這叫什麼,只是發現這想法/態度/策略挺普遍的。

有機會在介紹一個更明確的「多型性」寫作法。

2014年1月13日 星期一

[Java]用介面Interface「設計參數架構」

標題所謂的「參數架構」是指擁有/繼承自同一個父類別的所有子類別們,都會有相似的參數型態。

如果類別A有個屬性為開放(public)的參數X,那繼承類別A的類別B和類別C都會有參數X。

如果我在程式中宣告了一個參數Y,這個Y的種類為類別A,(這個時候或許應該稱Y為物件Y,或實作Y,.......這方面的名詞我一向很不講究,)那我不管塞進了類別B或類別C,後續的程式都可以在參數Y中另外獲得參數X。(當要讀取或修改「Y.X」時不會發生錯誤。)

或許可以稱這樣的目的為「設計參數架構」。



學Java都會學物件導向。

教學範本大多從Object的extends開始教起,其次才會講到怎麼自己設計介面/接口。

結果......大家最後都直接習慣設計Object、擴充自己寫的(或別人寫的)Object,然後沒啥人在用介面/接口。

我個人用介面的頻率算挺高的...事實上,我覺得寫Java程式就是先訂出Interface,並且已Interface為程式邏輯/流程設計和運作的基礎/核心。

「但是Interface有很多限制,寫在裡頭的參數都會被強制轉成final形態,如此一來不管是開放(public)、私用(private)、限制(protected),都沒有意義了。」

(用前面參數Y作範例來解釋的意思是:如果把參數X1寫在介面D中,則繼承/擴充介面D的類別E和類別F也會有參數X1,但這個X1是個「常數」,只能讀取,對它作修改......不會出錯,但不會有意義,X1的值會永遠保持在設計者寫在程式碼中的值,如果我明確寫了X1=0,不管我後來寫了什麼程式碼,X1就是0。)

不懂我在描述的狀況是什麼?或不曾有過類似的疑惑和需要?

反正.......

介面/接口/Interface也是可以拿來幫參數訂架構的。

這是一個介面。(public interface後面的ValueInterface是這個 介面/接口/Interface的名稱......)

像這樣......  我就可以用a()這個函式/接口來產生一個「參數a」了......

如果要把「參數a」傳給「參數b」,我就用「b = a();」。

如果要把「參數a」加上「參數b」,我就用「a( b );」。

像以下的Value0...



 注意到我想要達成的效果了嗎?可能還不夠明顯......

總之,凡是介面ValueInterface的子類別,都會有參數a。

至於參數a(第91行)的「真面目」,可能可以是(x+y),參數a(第94行)則是(y = i)。

感覺上沒什麼不同,只是想證明「也可以用介面形式來達到設計參數」。這些範例並不是非常有意義、更不用提效率與價值了。



這樣作具體來說有任何好處嗎?

Java的設計是「一個物件只能繼承一個物件」,如果類別甲繼承自類別乙,雖然它會完整的繼承類別乙的數值結構,但如果它同時也需要類別丙的數值結構呢?

如果類別丙的數值結構是在介面之下用我這裡所寫的形式,這就不會是問題了。

一連串的類別可以任意搭配一系列的介面......這裡可以有也是用來「設計參數架構」的介面丙一、丙二、丙三.......

可以用丙一和丙二搭配出類別一,用丙一和丙三搭配出類別二。不管是類別一或類別二,都可以塞進屬性為丙一的參數中。

「能」這樣設計程式不就是物件導向的特性嗎?作為全物件導向語言之一,使用Java卻不善用這種特性與思維......何必呢?它的「可攜性」可沒那麼強大。