標籤: 網頁設計

  • 為取得專利,小米持股 Ninebot 收購電動代步車始祖 Segway

    為取得專利,小米持股 Ninebot 收購電動代步車始祖 Segway

      小米投資的電動代步車廠商 Ninebot 今天宣布與小米、順為資本、紅杉資本聯合出資收購電動代步車始祖 Segway,交易目前已全數完成。身為 Ninebot 的股東,這項收購除了意味著小米持續拓展海外市場外,對小米來說最大的收益,便是可取得 Segway 手中大量的專利。   總部位於美國的 Segway 成立於 2001 年,產品主要為雙輪的電動代步車,而 Ninebot 則是中國電動代步車品牌。Segway 在全球每年約只有 1 萬台左右的銷量,對增加 Ninebot 銷售量並沒有太大的直接幫助,據知情人士透露,先前 Segway 曾與包括法拉利、香奈兒等國際知名品牌合作,Ninebot 買下 Segway 主要是衝著其在海外的知名度,更重要的是可取得 Segway 手中握有的專利。   有趣的是,Segway 創辦人 Dean Kamen 曾在 2014 年控告 Ninebot 及其他中國廠商侵犯智慧財產權,美國國際貿易委員會於 11 月受理調查,而今調查結果尚未出爐,Segway 卻先被 Ninebot 收購,成為旗下全資子公司。   此外,Ninebot 執行長高祿峰同時也對外宣布已完成 8,000 萬美元的 A 輪融資,由小米、紅杉資本、順為資本共同出資,並表示未來將開發應用電動代步車、移動互聯網及人車互動技術的產品。     本文全文授權轉載自《科技新報》─〈〉

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※別再煩惱如何寫文案,掌握八大原則!

  • 推薦一種通過刷leetcode來增強技術功底的方法

    推薦一種通過刷leetcode來增強技術功底的方法

    背景

    如果前人認為這個一種學習提高或者檢驗能力的成功實踐。而自己目前又沒有更好的方法,那就不妨試一試。

    而不管作為面試官還是被面試者,編碼題最近越來越流行。而兩種角色都需要思考的問題是希望考察什麼能力,通過什麼題目,需要達到怎樣的程度可以說明面試者具有了這樣的能力。

    而要找到上面這些問題的答案,比較好的方式除了看一些理論性文章和接受培訓之外,自己動手刷一刷leetcode切身實踐一下不失為一個不錯的方式。而既然要花精力去做這件事情,那就需要解決一個問題:我從中可以獲得什麼提高。以下是個人的一些經驗和感受。

     

    收益

    對底層有了更深入的了解和思考

    leetcode一些常見題也是平時工作中常用的一些底層數據結構的實現方法。 

    先舉個大家使用比較多的算法:LRU(最近最少使用),在Java的實現中實現特別簡單。只是使用了LinkedHashMap這種數據結構。而如果看一些開源包里LRU的實現,發現也是這樣實現的。實際上動手實現一遍,LRU就再也不會忘了。

    再舉個數據結構的例子:字典樹又叫前綴樹。它是搜索和推薦的基礎。標準點的定義是:

    字典樹又稱單詞查找樹,Tire樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。

    因為之前做過搜索引擎,一直也對這塊很有興趣,所以對它底層知識的補充對個人而言,感覺深度有增加。

     

      

    養成評估時空開銷的習慣

    我刷leetcode必看官方解答里估算的時間和空間複雜度。這也是作為一個架構師的必備基本能力。

    數組、哈希這些因為數據的位置不需要進行查找,只需要算數計算就可以得到,所以它們的時間複雜度是O(1)。

    鏈表如果直接在頭部或者尾部插入,因為不需要查找,所以時間複雜度也是O(1),但是定位的話因為涉及查找,按遍歷查找來算是log(n)。所以對於jdk1.7之前,hashmap底層採用的是數組+鏈表的數據結構。所以如果不算哈希衝突和擴容的話,獲取和插入數據的時間複雜度是O(1);如果出現了哈希衝突,則插入因為是頭部插入,時間複雜度還是O(1);獲取時間複雜度因為涉及先查找,所以是O(n),這個n代表衝突的數量。

    對於在有序數據中進行查找,因為可採用二分查找等優化,時間複雜度可降到log(n).

    對於遞歸調用,如果遞歸方法內進行2次調用。對於層數n來說,時間複雜度是2的n次方。舉個例子就是一個數等於前面兩個數之和。當然,如果是前面3個數之和,不進行優化的情況下時間複雜度就是3的n次方。

    對於一個n*m的二維數組等需要進行嵌套循環遍歷的,時間複雜度是O(n*m),有個特殊情況是n*m,這時候時間複雜度是n的平方。

    對於全排列的情況,時間複雜度是O(n!)。

     

    代碼簡化的方法

     

    我習慣的一種學習方法是先做題,有了一定自己的總結和思考之後,再看書學習別人的總結思考方法。對於刷leetcode相關性高,也比較受認可的書是《Cracking the Coding interview(6th)》,中文版翻譯是《程序員面試金典》。這本書對於面試官和面試者來說讀了都會有一定的收穫。

     

    我讀了這本書,對我印象最深的是介紹了兩種代碼優化的方法:BUD和BCR。

     

    BUD 

    BUD是瓶頸、不必要工作、重複工作 三個詞組首字母的縮寫。

     

    作者提出拿到一道編程題,可先嘗試用暴力解法將題目寫出來,之後找到解法的性能瓶頸,針對瓶頸進行優化,之後在去掉不必要的工作,最後去掉重複的工作。

    這個經典的編程優化方法不只可應用於編程,還可應用於整個程序的優化,也是最常規的優化方法。

     

    BCR

    BCR是Best Conceivable Runtime的縮寫,意思是想知道自己可以優化到什麼程度,先估算可達到的最優情況。

    比如:在一個無序數組中,查找兩個兩個相同的數。直覺來說如果找到這兩個數,最起碼需要將每個數都遍歷一遍,所以可達到的最優情況是O(n),無論怎麼優化,都不可能比這個更好。所以這就是優化的上限。

    這本書里還介紹了其他的優化方法如:使用額外數據結構、通過構建測試用例、根據題目的限制和提示來尋找線索,大家看這本書的時候可以了解下。

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

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    新北清潔公司,居家、辦公、裝潢細清專業服務

    ※推薦評價好的iphone維修中心

  • 作為一個Java開發你用過Jib嗎

    作為一個Java開發你用過Jib嗎

    1. 前言

    JibGoogle開發的可以直接構建 Java應用的DockerOCI鏡像的類庫,以MavenGradle插件形式提供。它最騷操作的是可以在沒有Docker守護程序的情況下構建,也就是說,您不必在計算機上安裝docker守護程序!儘管Spring Boot 2.3.0.RELEASE已經推出了構建鏡像的功能,胖哥還是忍不住要試試Jib

    其實最騷的還是名字。

    2. Docker構建流程和Jib的構建流程

    沒有對比就沒有傷害。我們還是要對比一下這兩者的構建流程。

    Docker構建流程需要我們先把項目打成Jar然後編寫Dockerfile,然後使用Docker構建功能進行構建鏡像、運行容器。流程如下:

    而Jib是這樣構建的:

    作為一個Java開發者,不用再關心各種無關的命令和操作,只需要專註於Java,而且高效穩定以及可復用的增量構建。為什麼Jib能這麼快而高效?

    傳統上,將Java應用程序與應用程序Jar一起構建為單個圖像層,而Jib的構建策略將Java應用程序分為多層,以進行更細化的增量構建。更改代碼時,僅重建更改,而不重建整個應用程序。

    3. Jib構建Spring Boot應用

    接下來我將演示如何將Spring Boot 應用打成鏡像並上傳到Dockerhub倉庫。

    Maven工程為例,我們只需要在pom.xml中引入Jib Maven 插件。默認情況下Jib會把我們打好的鏡像上傳到Googlegcr.io倉庫,實際中我們會把打好的鏡像上傳到私有倉庫,所以我們要加一些個性化配置。這裏我以dockerhub倉庫為例添加一些個性化配置:

    <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.4.0</version>
        <configuration>
            <!-- 相當於 Dockerfile 中的 FROM -->
            <from>
                <image>amazoncorretto:8</image>
            </from>
            <to>
                <!--構建鏡像名稱,這裏我使用maven中定義的項目名稱-->
                <image>daxus/${project.name}</image>
                <!--私有倉庫的賬號密碼-->
                <auth>
                    <username>felordcn</username>
                    <password>yourpassword</password>
                </auth>
                <!--Docker 鏡像的 tag 這裏使用maven定義的版本號-->
                <tags>
                    <tag>
                        ${project.version}
                    </tag>
                </tags>
            </to>
        </configuration>
    </plugin>
    

    然後在項目根目錄執行mvn clean compile jib:build就可以了。

    其實也可以簡單引入Jib插件:

    <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.4.0</version>
    </plugin>
    

    只不過我們的命令會更複雜一些,需要指定一些必要的參數,例如:

    mvn clean compile jib:build \
        -Djib.to.image=myregistry/myimage:latest \
        -Djib.to.auth.username=$USERNAME \
        -Djib.to.auth.password=$PASSWORD
    

    更多的定製命令可參考官方文檔:

    https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#extended-usage

    4. 總結

    Jib使用起來非常簡單,讓開發人員以Java的風格來完成Docker鏡像的構建,能夠大大改善編程的體驗。多多關注:碼農小胖哥 獲取更多有用的編程乾貨教程。

    關注公眾號:Felordcn 獲取更多資訊

    個人博客:https://felord.cn

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

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

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

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • 重學 Java 設計模式:實戰中介者模式「按照Mybatis原理手寫ORM框架,給JDBC方式操作數據庫增加中介者場景」

    重學 Java 設計模式:實戰中介者模式「按照Mybatis原理手寫ORM框架,給JDBC方式操作數據庫增加中介者場景」

    作者:小傅哥
    博客:https://bugstack.cn – 原創系列專題文章

    沉澱、分享、成長,讓自己和他人都能有所收穫!

    一、前言

    同齡人的差距是從什麼時候拉開的

    同樣的幼兒園、同樣的小學、一樣的書本、一樣的課堂,有人學習好、有人學習差。不只是上學,幾乎人生處處都是賽道,發令槍響起的時刻,也就把人生的差距拉開。編程開發這條路也是很長很寬,有人跑得快有人跑得慢。那麼你是否想起過,這一點點的差距到遙不可及的距離,是從哪一天開始的。摸摸肚子的肉,看看遠處的路,別人講的是故事,你想起的都是事故

    思想沒有產品高才寫出一片的ifelse

    當你承接一個需求的時候,比如;交易、訂單、營銷、保險等各類場景。如果你不熟悉這個場景下的業務模式,以及將來的拓展方向,那麼很難設計出良好可擴展的系統。再加上產品功能初建,說老闆要的急,儘快上線。作為程序員的你更沒有時間思考,整體一看現在的需求也不難,直接上手開干(一個方法兩個if語句),這樣確實滿足了當前需求。但老闆的想法多呀,產品也跟着變化快,到你這就是改改改,加加加。當然你也不客氣,回首掏就是1024個if語句!

    日積月累的技術沉澱是為了厚積薄發

    粗略的估算過,如果從上大學開始每天寫200行,一個月是6000行,一年算10個月話,就是6萬行,第三年出去實習的是時候就有20萬行的代碼量。如果你能做到這一點,找工作難?有時候很多事情就是靠時間積累出來的,想走捷徑有時候真的沒有。你的技術水平、你的業務能力、你身上的肉,都是一點點積累下來的,不要浪費看似很短的時間,一年年堅持下來,留下印刻青春的痕迹,多給自己武裝上一些能力。

    二、開發環境

    1. JDK 1.8
    2. Idea + Maven
    3. mysql 5.1.20
    4. 涉及工程一個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
    工程 描述
    itstack-demo-design-16-01 使用JDBC方式連接數據庫
    itstack-demo-design-16-02 手寫ORM框架操作數據庫

    三、中介者模式介紹

    中介者模式要解決的就是複雜功能應用之間的重複調用,在這中間添加一層中介者包裝服務,對外提供簡單、通用、易擴展的服務能力。

    這樣的設計模式幾乎在我們日常生活和實際業務開發中都會見到,例如;飛機降落有小姐姐在塔台喊話、無論哪個方向來的候車都從站台上下、公司的系統中有一个中台專門為你包裝所有接口和提供統一的服務等等,這些都運用了中介者模式。除此之外,你用到的一些中間件,他們包裝了底層多種數據庫的差異化,提供非常簡單的方式進行使用。

    四、案例場景模擬

    在本案例中我們通過模仿Mybatis手寫ORM框架,通過這樣操作數據庫學習中介者運用場景

    除了這樣的中間件層使用場景外,對於一些外部接口,例如N種獎品服務,可以由中台系統進行統一包裝對外提供服務能力。也是中介者模式的一種思想體現。

    在本案例中我們會把jdbc層進行包裝,讓用戶在使用數據庫服務的時候,可以和使用mybatis一樣簡單方便,通過這樣的源碼方式學習中介者模式,也方便對源碼知識的拓展學習,增強知識棧。

    五、用一坨坨代碼實現

    這是一種關於數據庫操作最初的方式

    基本上每一個學習開發的人都學習過直接使用jdbc方式連接數據庫,進行CRUD操作。以下的例子可以當做回憶。

    1. 工程結構

    itstack-demo-design-16-01
    └── src
        └── main
            └── java
                └── org.itstack.demo.design
                    └── JDBCUtil.java
    
    • 這裏的類比較簡單隻包括了一個數據庫操作類。

    2. 代碼實現

    public class JDBCUtil {
    
        private static Logger logger = LoggerFactory.getLogger(JDBCUtil.class);
    
        public static final String URL = "jdbc:mysql://127.0.0.1:3306/itstack-demo-design";
        public static final String USER = "root";
        public static final String PASSWORD = "123456";
    
        public static void main(String[] args) throws Exception {
            //1. 加載驅動程序
            Class.forName("com.mysql.jdbc.Driver");
            //2. 獲得數據庫連接
            Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
            //3. 操作數據庫
            Statement stmt = conn.createStatement();
            ResultSet resultSet = stmt.executeQuery("SELECT id, name, age, createTime, updateTime FROM user");
            //4. 如果有數據 resultSet.next() 返回true
            while (resultSet.next()) {
                logger.info("測試結果 姓名:{} 年齡:{}", resultSet.getString("name"),resultSet.getInt("age"));
            }
        }
    
    }
    
    • 以上是使用JDBC的方式進行直接操作數據庫,幾乎大家都使用過這樣的方式。

    3. 測試結果

    15:38:10.919 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:水水 年齡:18
    15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:豆豆 年齡:18
    15:38:10.922 [main] INFO  org.itstack.demo.design.JDBCUtil - 測試結果 姓名:花花 年齡:19
    
    Process finished with exit code 0
    
    • 從測試結果可以看到這裏已經查詢到了數據庫中的數據。只不過如果在全部的業務開發中都這樣實現,會非常的麻煩。

    六、中介模式開發ORM框架

    `接下來就使用中介模式的思想完成模仿Mybatis的ORM框架開發~

    1. 工程結構

    itstack-demo-design-16-02
    └── src
        ├── main
        │   ├── java
        │   │   └── org.itstack.demo.design
        │   │       ├── dao
        │   │       │	├── ISchool.java
        │   │       │	└── IUserDao.java
        │   │       ├── mediator
        │   │       │	├── Configuration.java
        │   │       │	├── DefaultSqlSession.java
        │   │       │	├── DefaultSqlSessionFactory.java
        │   │       │	├── Resources.java
        │   │       │	├── SqlSession.java
        │   │       │	├── SqlSessionFactory.java
        │   │       │	├── SqlSessionFactoryBuilder.java
        │   │       │	└── SqlSessionFactoryBuilder.java
        │   │       └── po
        │   │         	├── School.java
        │   │         	└── User.java
        │   └── resources
        │       ├── mapper
        │       │   ├── School_Mapper.xml
        │       │   └── User_Mapper.xml
        │       └── mybatis-config-datasource.xml
        └── test
             └── java
                 └── org.itstack.demo.design.test
                     └── ApiTest.java
    

    中介者模式模型結構

    • 以上是對ORM框架實現的核心類,包括了;加載配置文件、對xml解析、獲取數據庫session、操作數據庫以及結果返回。
    • 左上是對數據庫的定義和處理,基本包括我們常用的方法;<T> T selectOne<T> List<T> selectList等。
    • 右側藍色部分是對數據庫配置的開啟session的工廠處理類,這裏的工廠會操作DefaultSqlSession
    • 之後是紅色地方的SqlSessionFactoryBuilder,這個類是對數據庫操作的核心類;處理工廠、解析文件、拿到session等。

    接下來我們就分別介紹各個類的功能實現過程。

    2. 代碼實現

    2.1 定義SqlSession接口

    public interface SqlSession {
    
        <T> T selectOne(String statement);
    
        <T> T selectOne(String statement, Object parameter);
    
        <T> List<T> selectList(String statement);
    
        <T> List<T> selectList(String statement, Object parameter);
    
        void close();
    }
    
    • 這裏定義了對數據庫操作的查詢接口,分為查詢一個結果和查詢多個結果,同時包括有參數和沒有參數的方法。

    2.2 SqlSession具體實現類

    public class DefaultSqlSession implements SqlSession {
    
        private Connection connection;
        private Map<String, XNode> mapperElement;
    
        public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement) {
            this.connection = connection;
            this.mapperElement = mapperElement;
        }
    
        @Override
        public <T> T selectOne(String statement) {
            try {
                XNode xNode = mapperElement.get(statement);
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                ResultSet resultSet = preparedStatement.executeQuery();
                List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
                return objects.get(0);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        public <T> List<T> selectList(String statement) {
            XNode xNode = mapperElement.get(statement);
            try {
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                ResultSet resultSet = preparedStatement.executeQuery();
                return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        // ...
    
        private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
            List<T> list = new ArrayList<>();
            try {
                ResultSetMetaData metaData = resultSet.getMetaData();
                int columnCount = metaData.getColumnCount();
                // 每次遍歷行值
                while (resultSet.next()) {
                    T obj = (T) clazz.newInstance();
                    for (int i = 1; i <= columnCount; i++) {
                        Object value = resultSet.getObject(i);
                        String columnName = metaData.getColumnName(i);
                        String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
                        Method method;
                        if (value instanceof Timestamp) {
                            method = clazz.getMethod(setMethod, Date.class);
                        } else {
                            method = clazz.getMethod(setMethod, value.getClass());
                        }
                        method.invoke(obj, value);
                    }
                    list.add(obj);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return list;
        }
    
        @Override
        public void close() {
            if (null == connection) return;
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 這裏包括了接口定義的方法實現,也就是包裝了jdbc層。
    • 通過這樣的包裝可以讓對數據庫的jdbc操作隱藏起來,外部調用的時候對入參、出參都有內部進行處理。

    2.3 定義SqlSessionFactory接口

    public interface SqlSessionFactory {
    
        SqlSession openSession();
    
    }
    
    • 開啟一個SqlSession, 這幾乎是大家在平時的使用中都需要進行操作的內容。雖然你看不見,但是當你有數據庫操作的時候都會獲取每一次執行的SqlSession

    2.4 SqlSessionFactory具體實現類

    public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
        private final Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            this.configuration = configuration;
        }
    
        @Override
        public SqlSession openSession() {
            return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
        }
    
    }
    
    • DefaultSqlSessionFactory,是使用mybatis最常用的類,這裏我們簡單的實現了一個版本。
    • 雖然是簡單的版本,但是包括了最基本的核心思路。當開啟SqlSession時會進行返回一個DefaultSqlSession
    • 這個構造函數中向下傳遞了Configuration配置文件,在這個配置文件中包括;Connection connectionMap<String, String> dataSourceMap<String, XNode> mapperElement。如果有你閱讀過Mybatis源碼,對這個就不會陌生。

    2.5 SqlSessionFactoryBuilder實現

    public class SqlSessionFactoryBuilder {
    
        public DefaultSqlSessionFactory build(Reader reader) {
            SAXReader saxReader = new SAXReader();
            try {
                saxReader.setEntityResolver(new XMLMapperEntityResolver());
                Document document = saxReader.read(new InputSource(reader));
                Configuration configuration = parseConfiguration(document.getRootElement());
                return new DefaultSqlSessionFactory(configuration);
            } catch (DocumentException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private Configuration parseConfiguration(Element root) {
            Configuration configuration = new Configuration();
            configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
            configuration.setConnection(connection(configuration.dataSource));
            configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
            return configuration;
        }
    
        // 獲取數據源配置信息
        private Map<String, String> dataSource(List<Element> list) {
            Map<String, String> dataSource = new HashMap<>(4);
            Element element = list.get(0);
            List content = element.content();
            for (Object o : content) {
                Element e = (Element) o;
                String name = e.attributeValue("name");
                String value = e.attributeValue("value");
                dataSource.put(name, value);
            }
            return dataSource;
        }
    
        private Connection connection(Map<String, String> dataSource) {
            try {
                Class.forName(dataSource.get("driver"));
                return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
            } catch (ClassNotFoundException | SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        // 獲取SQL語句信息
        private Map<String, XNode> mapperElement(List<Element> list) {
            Map<String, XNode> map = new HashMap<>();
    
            Element element = list.get(0);
            List content = element.content();
            for (Object o : content) {
                Element e = (Element) o;
                String resource = e.attributeValue("resource");
    
                try {
                    Reader reader = Resources.getResourceAsReader(resource);
                    SAXReader saxReader = new SAXReader();
                    Document document = saxReader.read(new InputSource(reader));
                    Element root = document.getRootElement();
                    //命名空間
                    String namespace = root.attributeValue("namespace");
    
                    // SELECT
                    List<Element> selectNodes = root.selectNodes("select");
                    for (Element node : selectNodes) {
                        String id = node.attributeValue("id");
                        String parameterType = node.attributeValue("parameterType");
                        String resultType = node.attributeValue("resultType");
                        String sql = node.getText();
    
                        // ? 匹配
                        Map<Integer, String> parameter = new HashMap<>();
                        Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                        Matcher matcher = pattern.matcher(sql);
                        for (int i = 1; matcher.find(); i++) {
                            String g1 = matcher.group(1);
                            String g2 = matcher.group(2);
                            parameter.put(i, g2);
                            sql = sql.replace(g1, "?");
                        }
    
                        XNode xNode = new XNode();
                        xNode.setNamespace(namespace);
                        xNode.setId(id);
                        xNode.setParameterType(parameterType);
                        xNode.setResultType(resultType);
                        xNode.setSql(sql);
                        xNode.setParameter(parameter);
    
                        map.put(namespace + "." + id, xNode);
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
    
            }
            return map;
        }
    
    }
    
    • 在這個類中包括的核心方法有;build(構建實例化元素)parseConfiguration(解析配置)dataSource(獲取數據庫配置)connection(Map<String, String> dataSource) (鏈接數據庫)mapperElement (解析sql語句)
    • 接下來我們分別介紹這樣的幾個核心方法。

    build(構建實例化元素)

    這個類主要用於創建解析xml文件的類,以及初始化SqlSession工廠類DefaultSqlSessionFactory。另外需要注意這段代碼saxReader.setEntityResolver(new XMLMapperEntityResolver());,是為了保證在不聯網的時候一樣可以解析xml,否則會需要從互聯網獲取dtd文件。

    parseConfiguration(解析配置)

    是對xml中的元素進行獲取,這裏主要獲取了;dataSourcemappers,而這兩個配置一個是我們數據庫的鏈接信息,另外一個是對數據庫操作語句的解析。

    connection(Map<String, String> dataSource) (鏈接數據庫)

    鏈接數據庫的地方和我們常見的方式是一樣的;Class.forName(dataSource.get("driver"));,但是這樣包裝以後外部是不需要知道具體的操作。同時當我們需要鏈接多套數據庫的時候,也是可以在這裏擴展。

    mapperElement (解析sql語句)

    這部分代碼塊內容相對來說比較長,但是核心的點就是為了解析xml中的sql語句配置。在我們平常的使用中基本都會配置一些sql語句,也有一些入參的佔位符。在這裏我們使用正則表達式的方式進行解析操作。

    解析完成的sql語句就有了一個名稱和sql的映射關係,當我們進行數據庫操作的時候,這個組件就可以通過映射關係獲取到對應sql語句進行操作。

    3. 測試驗證

    在測試之前需要導入sql語句到數據庫中;

    • 庫名:itstack-demo-design
    • 表名:userschool
    CREATE TABLE school ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64), address varchar(256), createTime datetime, updateTime datetime, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    insert into school (id, name, address, createTime, updateTime) values (1, '北京大學', '北京市海淀區頤和園路5號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
    insert into school (id, name, address, createTime, updateTime) values (2, '南開大學', '中國天津市南開區衛津路94號', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
    insert into school (id, name, address, createTime, updateTime) values (3, '同濟大學', '上海市彰武路1號同濟大廈A樓7樓7區', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
    CREATE TABLE user ( id bigint(11) NOT NULL AUTO_INCREMENT, name varchar(32), age int(4), address varchar(128), entryTime datetime, remark varchar(64), createTime datetime, updateTime datetime, status int(4) DEFAULT '0', dateTime varchar(64), PRIMARY KEY (id), INDEX idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (1, '水水', 18, '吉林省榆樹市黑林鎮尹家村5組', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200309');
    insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (2, '豆豆', 18, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 1, null);
    insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (3, '花花', 19, '遼寧省大連市清河灣司馬道407路', '2019-12-22 00:00:00', '無', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200310');
    

    3.1 創建數據庫對象類

    用戶類

    public class User {
    
        private Long id;
        private String name;
        private Integer age;
        private Date createTime;
        private Date updateTime;
        
        // ... get/set
    }
    

    學校類

    public class School {
    
        private Long id;
        private String name;
        private String address;
        private Date createTime;
        private Date updateTime;  
      
        // ... get/set
    }
    
    • 這兩個類都非常簡單,就是基本的數據庫信息。

    3.2 創建DAO包

    用戶Dao

    public interface IUserDao {
    
         User queryUserInfoById(Long id);
    
    }
    

    學校Dao

    public interface ISchoolDao {
    
        School querySchoolInfoById(Long treeId);
    
    }
    

    3.3 ORM配置文件

    鏈接配置

    <configuration>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack_demo_design?useUnicode=true"/>
                    <property name="username" value="root"/>
                    <property name="password" value="123456"/>
                </dataSource>
            </environment>
        </environments>
    
        <mappers>
            <mapper resource="mapper/User_Mapper.xml"/>
            <mapper resource="mapper/School_Mapper.xml"/>
        </mappers>
    
    </configuration>
    
    • 這個配置與我們平常使用的mybatis基本是一樣的,包括了數據庫的連接池信息以及需要引入的mapper映射文件。

    操作配置(用戶)

    <mapper namespace="org.itstack.demo.design.dao.IUserDao">
    
        <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.design.po.User">
            SELECT id, name, age, createTime, updateTime
            FROM user
            where id = #{id}
        </select>
    
        <select id="queryUserList" parameterType="org.itstack.demo.design.po.User" resultType="org.itstack.demo.design.po.User">
            SELECT id, name, age, createTime, updateTime
            FROM user
            where age = #{age}
        </select>
    
    </mapper>
    

    操作配置(學校)

    <mapper namespace="org.itstack.demo.design.dao.ISchoolDao">
    
        <select id="querySchoolInfoById" resultType="org.itstack.demo.design.po.School">
            SELECT id, name, address, createTime, updateTime
            FROM school
            where id = #{id}
        </select>
    
    </mapper>
    

    3.4 單個結果查詢測試

    @Test
    public void test_queryUserInfoById() {
        String resource = "mybatis-config-datasource.xml";
        Reader reader;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
            SqlSession session = sqlMapper.openSession();
            try {
                User user = session.selectOne("org.itstack.demo.design.dao.IUserDao.queryUserInfoById", 1L);
                logger.info("測試結果:{}", JSON.toJSONString(user));
            } finally {
                session.close();
                reader.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    • 這裏的使用方式和Mybatis是一樣的,都包括了;資源加載和解析、SqlSession工廠構建、開啟SqlSession以及最後執行查詢操作selectOne

    測試結果

    16:56:51.831 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}
    
    Process finished with exit code 0
    
    • 從結果上看已經滿足了我們的查詢需求。

    3.5 集合結果查詢測試

    @Test
    public void test_queryUserList() {
        String resource = "mybatis-config-datasource.xml";
        Reader reader;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
            SqlSession session = sqlMapper.openSession();
            try {
                User req = new User();
                req.setAge(18);
                List<User> userList = session.selectList("org.itstack.demo.design.dao.IUserDao.queryUserList", req);
                logger.info("測試結果:{}", JSON.toJSONString(userList));
            } finally {
                session.close();
                reader.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    • 這個測試內容與以上只是查詢方法有所不同;session.selectList,是查詢一個集合結果。

    測試結果

    16:58:13.963 [main] INFO  org.itstack.demo.design.demo.ApiTest - 測試結果:[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]
    
    Process finished with exit code 0
    
    • 測試驗證集合的結果也是正常的,目前位置測試全部通過。

    七、總結

    • 以上通過中介者模式的設計思想我們手寫了一個ORM框架,隱去了對數據庫操作的複雜度,讓外部的調用方可以非常簡單的進行操作數據庫。這也是我們平常使用的Mybatis的原型,在我們日常的開發使用中,只需要按照配置即可非常簡單的操作數據庫。
    • 除了以上這種組件模式的開發外,還有服務接口的包裝也可以使用中介者模式來實現。比如你們公司有很多的獎品接口需要在營銷活動中對接,那麼可以把這些獎品接口統一收到中台開發一個獎品中心,對外提供服務。這樣就不需要每一個需要對接獎品的接口,都去找具體的提供者,而是找中台服務即可。
    • 在上述的實現和測試使用中可以看到,這種模式的設計滿足了;單一職責開閉原則,也就符合了迪米特原則,即越少人知道越好。外部的人只需要按照需求進行調用,不需要知道具體的是如何實現的,複雜的一面已經有組件合作服務平台處理。

    八、推薦閱讀

    • 1. 重學 Java 設計模式:實戰工廠方法模式「多種類型商品不同接口,統一發獎服務搭建場景」
    • 2. 重學 Java 設計模式:實戰原型模式「上機考試多套試,每人題目和答案亂序排列場景」
    • 3. 重學 Java 設計模式:實戰橋接模式「多支付渠道(微信、支付寶)與多支付模式(刷臉、指紋)場景」
    • 4. 重學 Java 設計模式:實戰組合模式「營銷差異化人群發券,決策樹引擎搭建場景」
    • 5. 重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」
    • 6. 重學 Java 設計模式:實戰享元模式「基於Redis秒殺,提供活動與庫存信息查詢場景」

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

    【其他文章推薦】

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

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

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

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    新北清潔公司,居家、辦公、裝潢細清專業服務

  • 為什麼建議你使用枚舉?

    為什麼建議你使用枚舉?

    枚舉是 JDK 1.5 新增的數據類型,使用枚舉我們可以很好的描述一些特定的業務場景,比如一年中的春、夏、秋、冬,還有每周的周一到周天,還有各種顏色,以及可以用它來描述一些狀態信息,比如錯誤碼等。

    枚舉類型不止存在在 Java 語言中,在其它語言中也都能找到它的身影,例如 C# 和 Python 等,但我發現在實際的項目中使用枚舉的人很少,所以本文就來聊一聊枚舉的相關內容,好讓朋友們對枚舉有一個大概的印象,這樣在編程時起碼還能想到有“枚舉”這樣一個類型。

    本文的結構目錄如下:

    枚舉的 7 種使用方法

    很多人不使用枚舉的一個重要的原因是對枚舉不夠熟悉,那麼我們就先從枚舉的 7 種使用方法說起。

    用法一:常量

    在 JDK 1.5 之前,我們定義常量都是 public static final... ,但有了枚舉,我們就可以把這些常量定義成一個枚舉類了,實現代碼如下:

    public enum ColorEnum {  
      RED, GREEN, BLANK, YELLOW  
    } 
    

    用法二:switch

    將枚舉用在 switch 判斷中,使得代碼可讀性更高了,實現代碼如下:

    enum ColorEnum {
        GREEN, YELLOW, RED
    }
    public class ColorTest {
        ColorEnum color = ColorEnum.RED;
    
        public void change() {
            switch (color) {
                case RED:
                    color = ColorEnum.GREEN;
                    break;
                case YELLOW:
                    color = ColorEnum.RED;
                    break;
                case GREEN:
                    color = ColorEnum.YELLOW;
                    break;
            }
        }
    }
    

    用法三:枚舉中增加方法

    我們可以在枚舉中增加一些方法,讓枚舉具備更多的特性,實現代碼如下:

    public class EnumTest {
        public static void main(String[] args) {
            ErrorCodeEnum errorCode = ErrorCodeEnum.SUCCESS;
            System.out.println("狀態碼:" + errorCode.code() + 
                               " 狀態信息:" + errorCode.msg());
        }
    }
    
    enum ErrorCodeEnum {
        SUCCESS(1000, "success"),
        PARAM_ERROR(1001, "parameter error"),
        SYS_ERROR(1003, "system error"),
        NAMESPACE_NOT_FOUND(2001, "namespace not found"),
        NODE_NOT_EXIST(3002, "node not exist"),
        NODE_ALREADY_EXIST(3003, "node already exist"),
        UNKNOWN_ERROR(9999, "unknown error");
    
        private int code;
        private String msg;
    
        ErrorCodeEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
        public int code() {
            return code;
        }
    
        public String msg() {
            return msg;
        }
    
        public static ErrorCodeEnum getErrorCode(int code) {
            for (ErrorCodeEnum it : ErrorCodeEnum.values()) {
                if (it.code() == code) {
                    return it;
                }
            }
            return UNKNOWN_ERROR;
        }
    }
    

    以上程序的執行結果為:

    狀態碼:1000 狀態信息:success

    用法四:覆蓋枚舉方法

    我們可以覆蓋一些枚舉中的方法用於實現自己的業務,比如我們可以覆蓋 toString() 方法,實現代碼如下:

    public class EnumTest {
        public static void main(String[] args) {
            ColorEnum colorEnum = ColorEnum.RED;
            System.out.println(colorEnum.toString());
        }
    }
    
    enum ColorEnum {
        RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
        //  成員變量
        private String name;
        private int index;
    
        //  構造方法
        private ColorEnum(String name, int index) {
            this.name = name;
            this.index = index;
        }
    
        //覆蓋方法
        @Override
        public String toString() {
            return this.index + ":" + this.name;
        }
    }
    

    以上程序的執行結果為:

    1:紅色

    用法五:實現接口

    枚舉類可以用來實現接口,但不能用於繼承類,因為枚舉默認繼承了 java.lang.Enum 類,在 Java 語言中允許實現多接口,但不能繼承多個父類,實現代碼如下:

    public class EnumTest {
        public static void main(String[] args) {
            ColorEnum colorEnum = ColorEnum.RED;
            colorEnum.print();
            System.out.println("顏色:" + colorEnum.getInfo());
        }
    }
    
    interface Behaviour {
        void print();
    
        String getInfo();
    }
    
    enum ColorEnum implements Behaviour {
        RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
        private String name;
        private int index;
    
        private ColorEnum(String name, int index) {
            this.name = name;
            this.index = index;
        }
    
        @Override
        public void print() {
            System.out.println(this.index + ":" + this.name);
        }
    
        @Override
        public String getInfo() {
            return this.name;
        }
    }
    

    以上程序的執行結果為:

    1:紅色

    顏色:紅色

    用法六:在接口中組織枚舉類

    我們可以在一個接口中創建多個枚舉類,用它可以很好的實現“多態”,也就是說我們可以將擁有相同特性,但又有細微實現差別的枚舉類聚集在一個接口中,實現代碼如下:

    public class EnumTest {
        public static void main(String[] args) {
            // 賦值第一個枚舉類
            ColorInterface colorEnum = ColorInterface.ColorEnum.RED;
            System.out.println(colorEnum);
            // 賦值第二個枚舉類
            colorEnum = ColorInterface.NewColorEnum.NEW_RED;
            System.out.println(colorEnum);
        }
    }
    
    interface ColorInterface {
        enum ColorEnum implements ColorInterface {
            GREEN, YELLOW, RED
        }
        enum NewColorEnum implements ColorInterface {
            NEW_GREEN, NEW_YELLOW, NEW_RED
        }
    }
    

    以上程序的執行結果為:

    RED

    NEW_RED

    用法七:使用枚舉集合

    在 Java 語言中和枚舉類相關的,還有兩個枚舉集合類 java.util.EnumSetjava.util.EnumMap,使用它們可以實現更多的功能。

    使用 EnumSet 可以保證元素不重複,並且能獲取指定範圍內的元素,示例代碼如下:

    import java.util.ArrayList;
    import java.util.EnumSet;
    import java.util.List;
    
    public class EnumTest {
        public static void main(String[] args) {
            List<ColorEnum> list = new ArrayList<ColorEnum>();
            list.add(ColorEnum.RED);
            list.add(ColorEnum.RED);  // 重複元素
            list.add(ColorEnum.YELLOW);
            list.add(ColorEnum.GREEN);
            // 去掉重複數據
            EnumSet<ColorEnum> enumSet = EnumSet.copyOf(list);
            System.out.println("去重:" + enumSet);
    
            // 獲取指定範圍的枚舉(獲取所有的失敗狀態)
            EnumSet<ErrorCodeEnum> errorCodeEnums = EnumSet.range(ErrorCodeEnum.ERROR, ErrorCodeEnum.UNKNOWN_ERROR);
            System.out.println("所有失敗狀態:" + errorCodeEnums);
        }
    }
    
    enum ColorEnum {
        RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
        private String name;
        private int index;
    
        private ColorEnum(String name, int index) {
            this.name = name;
            this.index = index;
        }
    }
    
    enum ErrorCodeEnum {
        SUCCESS(1000, "success"),
        ERROR(2001, "parameter error"),
        SYS_ERROR(2002, "system error"),
        NAMESPACE_NOT_FOUND(2003, "namespace not found"),
        NODE_NOT_EXIST(3002, "node not exist"),
        NODE_ALREADY_EXIST(3003, "node already exist"),
        UNKNOWN_ERROR(9999, "unknown error");
    
        private int code;
        private String msg;
    
        ErrorCodeEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
        public int code() {
            return code;
        }
    
        public String msg() {
            return msg;
        }
    }
    

    以上程序的執行結果為:

    去重:[RED, GREEN, YELLOW]

    所有失敗狀態:[ERROR, SYS_ERROR, NAMESPACE_NOT_FOUND, NODE_NOT_EXIST, NODE_ALREADY_EXIST, UNKNOWN_ERROR]

    EnumMapHashMap 類似,不過它是一個專門為枚舉設計的 Map 集合,相比 HashMap 來說它的性能更高,因為它內部放棄使用鏈表和紅黑樹的結構,採用數組作為數據存儲的結構。

    EnumMap 基本使用示例如下:

    import java.util.EnumMap;
    
    public class EnumTest {
        public static void main(String[] args) {
            EnumMap<ColorEnum, String> enumMap = new EnumMap<>(ColorEnum.class);
            enumMap.put(ColorEnum.RED, "紅色");
            enumMap.put(ColorEnum.GREEN, "綠色");
            enumMap.put(ColorEnum.BLANK, "白色");
            enumMap.put(ColorEnum.YELLOW, "黃色");
            System.out.println(ColorEnum.RED + ":" + enumMap.get(ColorEnum.RED));
        }
    }
    
    enum ColorEnum {
        RED, GREEN, BLANK, YELLOW;
    }
    

    以上程序的執行結果為:

    RED:紅色

    使用注意事項

    阿里《Java開發手冊》對枚舉的相關規定如下,我們在使用時需要稍微注意一下。

    【強制】所有的枚舉類型字段必須要有註釋,說明每個數據項的用途。

    【參考】枚舉類名帶上 Enum 後綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。說明:枚舉其實就是特殊的常量類,且構造方法被默認強制是私有。正例:枚舉名字為 ProcessStatusEnum 的成員名稱:SUCCESS / UNKNOWN_REASON。

    假如不使用枚舉

    在枚舉沒有誕生之前,也就是 JDK 1.5 版本之前,我們通常會使用 int 常量來表示枚舉,實現代碼如下:

    public static final int COLOR_RED = 1;
    public static final int COLOR_BLUE = 2;
    public static final int COLOR_GREEN = 3;
    

    但是使用 int 類型可能存在兩個問題:

    第一, int 類型本身並不具備安全性,假如某個程序員在定義 int 時少些了一個 final 關鍵字,那麼就會存在被其他人修改的風險,而反觀枚舉類,它“天然”就是一個常量類,不存在被修改的風險(原因詳見下半部分);

    第二,使用 int 類型的語義不夠明確,比如我們在控制台打印時如果只輸出 1…2…3 這樣的数字,我們肯定不知道它代表的是什麼含義。

    那有人就說了,那就使用常量字符唄,這總不會還不知道語義吧?實現示例代碼如下:

    public static final String COLOR_RED = "RED";
    public static final String COLOR_BLUE = "BLUE";
    public static final String COLOR_GREEN = "GREEN";
    

    但是這樣同樣存在一個問題,有些初級程序員會不按套路出牌,他們可能會直接使用字符串的值進行比較,而不是直接使用枚舉的字段,實現示例代碼如下:

    public class EnumTest {
        public static final String COLOR_RED = "RED";
        public static final String COLOR_BLUE = "BLUE";
        public static final String COLOR_GREEN = "GREEN";
        public static void main(String[] args) {
            String color = "BLUE";
            if ("BLUE".equals(color)) {
                System.out.println("藍色");
            }
        }
    }
    

    這樣當我們修改了枚舉中的值,那程序就涼涼了。

    枚舉使用場景

    枚舉的常見使用場景是單例,它的完整實現代碼如下:

    public class Singleton {
        // 枚舉類型是線程安全的,並且只會裝載一次
        private enum SingletonEnum {
            INSTANCE;
            // 聲明單例對象
            private final Singleton instance;
            // 實例化
            SingletonEnum() {
                instance = new Singleton();
            }
            private Singleton getInstance() {
                return instance;
            }
        }
        // 獲取實例(單例對象)
        public static Singleton getInstance() {
            return SingletonEnum.INSTANCE.getInstance();
        }
        private Singleton() {
        }
        // 類方法
        public void sayHi() {
            System.out.println("Hi,Java.");
        }
    }
    class SingletonTest {
        public static void main(String[] args) {
            Singleton singleton = Singleton.getInstance();
            singleton.sayHi();
        }
    }
    

    因為枚舉只會在類加載時裝載一次,所以它是線程安全的,這也是《Effective Java》作者極力推薦使用枚舉來實現單例的主要原因。

    知識擴展

    枚舉為什麼是線程安全的?

    這一點要從枚舉最終生成的字節碼說起,首先我們先來定義一個簡單的枚舉類:

    public enum ColorEnumTest {
        RED, GREEN, BLANK, YELLOW;
    }
    

    然後我們再將上面的那段代碼編譯為字節碼,具體內容如下:

    public final class ColorEnumTest extends java.lang.Enum<ColorEnumTest> {
      public static final ColorEnumTest RED;
      public static final ColorEnumTest GREEN;
      public static final ColorEnumTest BLANK;
      public static final ColorEnumTest YELLOW;
      public static ColorEnumTest[] values();
      public static ColorEnumTest valueOf(java.lang.String);
      static {};
    }
    

    從上述結果可以看出枚舉類最終會被編譯為被 final 修飾的普通類,它的所有屬性也都會被 staticfinal 關鍵字修飾,所以枚舉類在項目啟動時就會被 JVM 加載並初始化,而這個執行過程是線程安全的,所以枚舉類也是線程安全的類。

    小貼士:代碼反編譯的過程是先用 javac 命令將 java 代碼編譯字節碼(.class),再使用 javap 命令查看編譯的字節碼。

    枚舉比較小技巧

    我們在枚舉比較時使用 == 就夠了,因為枚舉類是在程序加載時就創建了(它並不是 new 出來的),並且枚舉類不允許在外部直接使用 new 關鍵字來創建枚舉實例,所以我們在使用枚舉類時本質上只有一個對象,因此在枚舉比較時使用 == 就夠了。

    並且我們在查看枚舉的 equlas() 源碼會發現,它的內部其實還是直接調用了 == 方法,源碼如下:

    public final boolean equals(Object other) {
        return this==other;
    }
    

    總結

    本文我們介紹了枚舉類的 7 種使用方法:常量、switch、枚舉中添加方法、覆蓋枚舉方法、實現接口、在接口中組織枚舉類和使用枚舉集合等,然後講了如果不使用枚舉類使用 int 類型和 String 類型存在的一些弊端:語義不夠清晰、容易被修改、存在被誤用的風險,所以我們在適合的環境下應該盡量使用枚舉類。並且我們還講了枚舉類的使用場景——單例,以及枚舉類為什麼是安全的,最後我們講了枚舉比較的小技巧,希望本文對你有幫助。

    查看 & 鳴謝

    https://www.iteye.com/blog/softbeta-1185573

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

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※別再煩惱如何寫文案,掌握八大原則!

  • 特斯拉將公布儲能電池計畫 F-貿聯可望受惠

    特斯拉傳將於 4 月 30 日公布儲能電池計畫,市場推測,該計畫將包括住宅與公共規模市場,業界傳出,本來就是特斯拉重要夥伴的 F-貿聯,有機會獲新電力線訂單。而今年在特斯拉需求持續強勁,與 Type C 等外接式擴充介面的需求爆發下,法人預期,該公司今年營運將逐季走高,全年營收看增 15%。   F-貿聯首季營收 19.04 億元,年增 10%,創下歷年同期新高。而特斯拉傳出將於下周公布最新的儲能電池計畫,據市場傳言表示,貿聯本來就是特斯拉在電動車電池模組用線與超級充電樁的線束的主力供應商,未來不排除大型公共規模用儲能電池,將與現有充電站結合。而貿聯有機會順勢切入取得新產品,並以公用市場應用的電力線為主,近日有望已小量出貨。   法人預估,F-貿聯今年除來自特斯拉的需求成長外,另在其他車用佈局方面,包括全地形車大客戶全車線束、擴大歐美客戶於倒車雷達等新產品的開發;另外針對美國官方對公營單位節能環保的需求,也配合客戶開發出得以改裝現有車為電動車的配備裝置,隨未來該商業模式成型,貢獻將逐步擴大。  

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    新北清潔公司,居家、辦公、裝潢細清專業服務

    ※推薦評價好的iphone維修中心

  • 最新的一波Vue實戰技巧,不用則已,一用驚人

    Vue中,不同的選項有不同的合併策略,比如 data,props,methods是同名屬性覆蓋合併,其他直接合併,而生命周期鈎子函數則是將同名的函數放到一個數組中,在調用的時候依次調用

    Vue中,提供了一個api, Vue.config.optionMergeStrategies,可以通過這個api去自定義選項的合併策略。

    在代碼中打印

    console.log(Vue.config.optionMergeStrategies)
    

      

     通過合併策略自定義生命周期函數

    背景

    最近客戶給領導反饋,我們的系統用一段時間,瀏覽器就變得有點卡,不知道為什麼。問題出來了,本來想甩鍋到後端,但是瀏覽器問題,沒法甩鍋啊,那就排查吧。

    後來發現頁面有許多定時器,ajax輪詢還有動畫,打開一個瀏覽器頁簽沒法問題,打開多了,瀏覽器就變得卡了,這時候我就想如果能在用戶切換頁簽時候將這些都停掉,不久解決了。百度裏面上下檢索,找到了一個事件visibilitychange,可以用來判斷瀏覽器頁簽是否显示。

    有方法了,就寫唄

    export default {
      created() {
        window.addEventListener('visibilitychange', this.$_hanldeVisiblityChange)
        // 此處用了hookEvent,可以參考小編前一篇文章
        this.$on('hook:beforeDestroy', () => {
          window.removeEventListener(
            'visibilitychange',
            this.$_hanldeVisiblityChange
          )
        })
      },
      methods: {
        $_hanldeVisiblityChange() {
          if (document.visibilityState === 'hidden') {
            // 停掉那一堆東西
          }
          if (document.visibilityState === 'visible') {
            // 開啟那一堆東西
          }
        }
      }
    }
    

    通過上面的代碼,可以看到在每一個需要監聽處理的文件都要寫一堆事件監聽,判斷頁面是否显示的代碼,一處兩處還可以,文件多了就頭疼了,這時候小編突發奇想,定義一個頁面显示隱藏的生命周期鈎子,把這些判斷都封裝起來

    自定義生命周期鈎子函數

    定義生命周期函數 pageHidden 與 pageVisible

    import Vue from 'vue'
    
    // 通知所有組件頁面狀態發生了變化
    const notifyVisibilityChange = (lifeCycleName, vm) => {
      // 生命周期函數會存在$options中,通過$options[lifeCycleName]獲取生命周期
      const lifeCycles = vm.$options[lifeCycleName]
      // 因為使用了created的合併策略,所以是一個數組
      if (lifeCycles && lifeCycles.length) {
        // 遍歷 lifeCycleName對應的生命周期函數列表,依次執行
        lifeCycles.forEach(lifecycle => {
          lifecycle.call(vm)
        })
      }
      // 遍歷所有的子組件,然後依次遞歸執行
      if (vm.$children && vm.$children.length) {
        vm.$children.forEach(child => {
          notifyVisibilityChange(lifeCycleName, child)
        })
      }
    }
    
    // 添加生命周期函數
    export function init() {
      const optionMergeStrategies = Vue.config.optionMergeStrategies
      // 定義了兩個生命周期函數 pageVisible, pageHidden
      // 為什麼要賦值為 optionMergeStrategies.created呢
      // 這個相當於指定 pageVisible, pageHidden 的合併策略與 created的相同(其他生命周期函數都一樣)
      optionMergeStrategies.pageVisible = optionMergeStrategies.beforeCreate
      optionMergeStrategies.pageHidden = optionMergeStrategies.created
    }
    
    
    // 將事件變化綁定到根節點上面
    // rootVm vue根節點實例
    export function bind(rootVm) {
      window.addEventListener('visibilitychange', () => {
        // 判斷調用哪個生命周期函數
        let lifeCycleName = undefined
        if (document.visibilityState === 'hidden') {
          lifeCycleName = 'pageHidden'
        } else if (document.visibilityState === 'visible') {
          lifeCycleName = 'pageVisible'
        }
        if (lifeCycleName) {
          // 通過所有組件生命周期發生變化了
          notifyVisibilityChange(lifeCycleName, rootVm)
        }
      })
    }
    

    應用

    1. main.js主入口文件引入
    import { init, bind } from './utils/custom-life-cycle'
    
    // 初始化生命周期函數, 必須在Vue實例化之前確定合併策略
    init()
    
    const vm = new Vue({
      router,
      render: h => h(App)
    }).$mount('#app')
    
    // 將rootVm 綁定到生命周期函數監聽裏面
    bind(vm)
    

      2. 在需要的地方監聽生命周期函數

    export default {
      pageVisible() {
        console.log('頁面显示出來了')
      },
      pageHidden() {
        console.log('頁面隱藏了')
      }
    }
    

      

    provideinject,不止父子傳值,祖宗傳值也可以

    Vue相關的面試經常會被面試官問道,Vue父子之間傳值的方式有哪些,通常我們會回答,props傳值,$emit事件傳值,vuex傳值,還有eventbus傳值等等,今天再加一種provideinject傳值,離offer又近了一步。(對了,下一節還有一種)

    使用過React的同學都知道,在React中有一個上下文Context,組件可以通過Context向任意後代傳值,而Vueprovideinject的作用於Context的作用基本一樣

    先舉一個例子

    使用過elemment-ui的同學一定對下面的代碼感到熟悉

    <template>
      <el-form :model="formData" size="small">
        <el-form-item label="姓名" prop="name">
          <el-input v-model="formData.name" />
        </el-form-item>
        <el-form-item label="年齡" prop="age">
          <el-input-number v-model="formData.age" />
        </el-form-item>
        <el-button>提交</el-button>
      </el-form>
    </template>
    <script>
    export default {
      data() {
        return {
          formData: {
            name: '',
            age: 0
          }
        }
      }
    }
    </script>
    

      

    看了上面的代碼,貌似沒啥特殊的,天天寫啊。在el-form上面我們指定了一個屬性size="small",然後有沒有發現表單裏面的所有表單元素以及按鈕的 size都變成了small,這個是怎麼做到的?接下來我們自己手寫一個表單模擬一下

    自己手寫一個表單

    自定義表單custom-form.vue

    <template>
      <form class="custom-form">
        <slot></slot>
      </form>
    </template>
    <script>
    export default {
      props: {
        // 控製表單元素的大小
        size: {
          type: String,
          default: 'default',
          // size 只能是下面的四個值
          validator(value) {
            return ['default', 'large', 'small', 'mini'].includes(value)
          }
        },
        // 控製表單元素的禁用狀態
        disabled: {
          type: Boolean,
          default: false
        }
      },
      // 通過provide將當前表單實例傳遞到所有後代組件中
      provide() {
        return {
          customForm: this
        }
      }
    }
    </script>
    

      

    在上面代碼中,我們通過provide將當前組件的實例傳遞到後代組件中,provide是一個函數,函數返回的是一個對象

    自定義表單項custom-form-item.vue

    沒有什麼特殊的,只是加了一個label,element-ui更複雜一些

    <template>
      <div class="custom-form-item">
        <label class="custom-form-item__label">{{ label }}</label>
        <div class="custom-form-item__content">
          <slot></slot>
        </div>
      </div>
    </template>
    <script>
    export default {
      props: {
        label: {
          type: String,
          default: ''
        }
      }
    }
    </script>

    自定義輸入框 custom-input.vue

    <template>
      <div
        class="custom-input"
        :class="[
          `custom-input--${getSize}`,
          getDisabled && `custom-input--disabled`
        ]"
      >
        <input class="custom-input__input" :value="value" @input="$_handleChange" />
      </div>
    </template>
    <script>
    /* eslint-disable vue/require-default-prop */
    export default {
      props: {
        // 這裏用了自定義v-model
        value: {
          type: String,
          default: ''
        },
        size: {
          type: String
        },
        disabled: {
          type: Boolean
        }
      },
      // 通過inject 將form組件注入的實例添加進來
      inject: ['customForm'],
      computed: {
        // 通過計算組件獲取組件的size, 如果當前組件傳入,則使用當前組件的,否則是否form組件的
        getSize() {
          return this.size || this.customForm.size
        },
        // 組件是否禁用
        getDisabled() {
          const { disabled } = this
          if (disabled !== undefined) {
            return disabled
          }
          return this.customForm.disabled
        }
      },
      methods: {
        // 自定義v-model
        $_handleChange(e) {
          this.$emit('input', e.target.value)
        }
      }
    }
    </script>
    

      


    form中,我們通過
    provide返回了一個對象,在
    input中,我們可以通過
    inject獲取
    form中返回對象中的項,如上代碼
    inject:['customForm']所示,然後就可以在組件內通過
    this.customForm調用
    form實例上面的屬性與方法了

    在項目中使用

    <template>
      <custom-form size="small">
        <custom-form-item label="姓名">
          <custom-input v-model="formData.name" />
        </custom-form-item>
      </custom-form>
    </template>
    <script>
    import CustomForm from '../components/custom-form'
    import CustomFormItem from '../components/custom-form-item'
    import CustomInput from '../components/custom-input'
    export default {
      components: {
        CustomForm,
        CustomFormItem,
        CustomInput
      },
      data() {
        return {
          formData: {
            name: '',
            age: 0
          }
        }
      }
    }
    </script>
    

      執行上面代碼,運行結果為:

    <form class="custom-form">
      <div class="custom-form-item">
        <label class="custom-form-item__label">姓名</label>
        <div class="custom-form-item__content">
          <!--size=small已經添加到指定的位置了-->
          <div class="custom-input custom-input--small">
            <input class="custom-input__input">
          </div>
        </div>
      </div>
    </form>
    

      

    通過上面的代碼可以看到,input組件已經設置組件樣式為custom-input--small

    inject格式說明

    除了上面代碼中所使用的inject:['customForm']寫法之外,inject還可以是一個對象。且可以指定默認值

    修改上例,如果custom-input外部沒有custom-form,則不會注入customForm,此時為customForm指定默認值

    {
      inject: {
        customForm: {
          // 對於非原始值,和props一樣,需要提供一個工廠方法
          default: () => ({
            size: 'default'
          })
        }
      }
    }
    

      

    使用限制

    1.provideinject的綁定不是可響應式的。但是,如果你傳入的是一個可監聽的對象,如上面的customForm: this,那麼其對象的屬性還是可響應的。

    2.Vue官網建議provideinject 主要在開發高階插件/組件庫時使用。不推薦用於普通應用程序代碼中。因為provideinject在代碼中是不可追溯的(ctrl + f可以搜),建議可以使用Vuex代替。 但是,也不是說不能用,在局部功能有時候用了作用還是比較大的。

     

    插槽,我要鑽到你的懷裡

    插槽,相信每一位Vue都有使用過,但是如何更好的去理解插槽,如何去自定義插槽,今天小編為你帶來更形象的說明。

    默認插槽

    <template>
      <!--這是一個一居室-->
      <div class="one-bedroom">
        <!--添加一個默認插槽,用戶可以在外部隨意定義這個一居室的內容-->
        <slot></slot>
      </div>
    </template>
    

      

    <template>
      <!--這裏一居室-->
      <one-bedroom>
        <!--將傢具放到房間裏面,組件內部就是上面提供的默認插槽的空間-->
        <span>先放一個小床,反正沒有女朋友</span>
        <span>再放一個電腦桌,在家還要加班寫bug</span>
      </one-bedroom>
    </template>
    <script>
    import OneBedroom from '../components/one-bedroom'
    export default {
      components: {
        OneBedroom
      }
    }
    </script>

    具名插槽

    <template>
      <div class="two-bedroom">
        <!--這是主卧-->
        <div class="master-bedroom">
          <!---主卧使用默認插槽-->
          <slot></slot>
        </div>
        <!--這是次卧-->
        <div class="secondary-bedroom">
          <!--次卧使用具名插槽-->
          <slot name="secondard"></slot>
        </div>
      </div>
    </template>
    

      

    <template>
      <two-bedroom>
        <!--主卧使用默認插槽-->
        <div>
          <span>放一個大床,要結婚了,嘿嘿嘿</span>
          <span>放一個衣櫃,老婆的衣服太多了</span>
          <span>算了,還是放一個電腦桌吧,還要寫bug</span>
        </div>
        <!--次卧,通過v-slot:secondard 可以指定使用哪一個具名插槽, v-slot:secondard 也可以簡寫為 #secondard-->
        <template v-slot:secondard>
          <div>
            <span>父母要住,放一個硬一點的床,軟床對腰不好</span>
            <span>放一個衣櫃</span>
          </div>
        </template>
      </two-bedroom>
    </template>
    <script>
    import TwoBedroom from '../components/slot/two-bedroom'
    export default {
      components: {
        TwoBedroom
      }
    }
    </script>

    作用域插槽

    <template>
      <div class="two-bedroom">
        <!--其他內容省略-->
        <div class="toilet">
          <!--通過v-bind 可以向外傳遞參數, 告訴外面衛生間可以放洗衣機-->
          <slot name="toilet" v-bind="{ washer: true }"></slot>
        </div>
      </div>
    </template>
    

      

    <template>
      <two-bedroom>
        <!--其他省略-->
        <!--衛生間插槽,通過v-slot="scope"可以獲取組件內部通過v-bind傳的值-->
        <template v-slot:toilet="scope">
          <!--判斷是否可以放洗衣機-->
          <span v-if="scope.washer">這裏放洗衣機</span>
        </template>
      </two-bedroom>
    </template>  

    插槽默認值

    <template>
      <div class="second-hand-house">
        <div class="master-bedroom">
          <!--插槽可以指定默認值,如果外部調用組件時沒有修改插槽內容,則使用默認插槽-->
          <slot>
            <span>這裡有一張水床,玩的夠嗨</span>
            <span>還有一個衣櫃,有點舊了</span>
          </slot>
        </div>
        <!--這是次卧-->
        <div class="secondary-bedroom">
          <!--次卧使用具名插槽-->
          <slot name="secondard">
            <span>這裡有一張嬰兒床</span>
          </slot>
        </div>
      </div>
    </template>
    

      

    <second-hand-house>
        <!--主卧使用默認插槽,只裝修主卧-->
        <div>
          <span>放一個大床,要結婚了,嘿嘿嘿</span>
          <span>放一個衣櫃,老婆的衣服太多了</span>
          <span>算了,還是放一個電腦桌吧,還要寫bug</span>
        </div>
      </second-hand-house>
    

    dispatchbroadcast,這是一種有歷史的組件通信方式

    dispatch
    broadcast是一種有歷史的組件通信方式,為什麼是有歷史的,因為他們是
    Vue1.0提供的一種方式,在
    Vue2.0中廢棄了。但是廢棄了不代表我們不能自己手動實現,像許多UI庫內部都有實現。本文以
    element-ui實現為基礎進行介紹。同時看完本節,你會對組件的
    $parent,
    $children,
    $options有所了解。

    方法介紹

    $dispatch: $dispatch會向上觸發一個事件,同時傳遞要觸發的祖先組件的名稱與參數,當事件向上傳遞到對應的組件上時會觸發組件上的事件偵聽器,同時傳播會停止。

    $broadcast: $broadcast會向所有的後代組件傳播一個事件,同時傳遞要觸發的後代組件的名稱與參數,當事件傳遞到對應的後代組件時,會觸發組件上的事件偵聽器,同時傳播會停止(因為向下傳遞是樹形的,所以只會停止其中一個恭弘=叶 恭弘子分支的傳遞)。

    $dispatch實現與應用

    1. 代碼實現

     // 向上傳播事件
     // @param {*} eventName 事件名稱
     // @param {*} componentName 接收事件的組件名稱
     // @param {...any} params 傳遞的參數,可以有多個
     
    function dispatch(eventName, componentName, ...params) {
      // 如果沒有$parent, 則取$root
      let parent = this.$parent || this.$root
      while (parent) {
        // 組件的name存儲在組件的$options.componentName 上面
        const name = parent.$options.name
        // 如果接收事件的組件是當前組件
        if (name === componentName) {
          // 通過當前組件上面的$emit觸發事件,同事傳遞事件名稱與參數
          parent.$emit.apply(parent, [eventName, ...params])
          break
        } else {
          // 否則繼續向上判斷
          parent = parent.$parent
        }
      }
    }
    
    // 導出一個對象,然後在需要用到的地方通過混入添加
    export default {
      methods: {
        $dispatch: dispatch
      }
    }  

    2. 代碼應用

    • 在子組件中通過$dispatch向上觸發事件

      import emitter from '../mixins/emitter'
      export default {
        name: 'Chart',
        // 通過混入將$dispatch加入進來
        mixins: [emitter],
         mounted() {
           // 在組件渲染完之後,將組件通過$dispatch將自己註冊到Board組件上
          this.$dispatch('register', 'Board', this)
        }
      }
    • Board組件上通過$on監聽要註冊的事件

    $broadcast實現與應用

    1. 代碼實現

      //向下傳播事件
      // @param {*} eventName 事件名稱
      // @param {*} componentName 要觸發組件的名稱
      // @param  {...any} params 傳遞的參數
     
    function broadcast(eventName, componentName, ...params) {
      this.$children.forEach(child => {
        const name = child.$options.name
        if (name === componentName) {
          child.$emit.apply(child, [eventName, ...params])
        } else {
          broadcast.apply(child, [eventName, componentName, ...params])
        }
      })
    }
    
    // 導出一個對象,然後在需要用到的地方通過混入添加
    export default {
      methods: {
        $broadcast: broadcast
      }
    }  

    2. 代碼應用

    在父組件中通過$broadcast向下觸發事件

    import emitter from '../mixins/emitter'
    export default {
      name: 'Board',
      // 通過混入將$dispatch加入進來
      mixins: [emitter],
      methods:{
      	//在需要的時候,刷新組件
      	$_refreshChildren(params) {
      		this.$broadcast('refresh', 'Chart', params)
      	}
      }
    }
    

    在後代組件中通過$on監聽刷新事件

    export default {
      name: 'Chart',
      created() {
        this.$on('refresh',(params) => {
          // 刷新事件
        })
      }
    }
    

    總結

    通過上面的例子,同學們應該都能對$dispatch$broadcast有所了解,但是為什麼Vue2.0要放棄這兩個方法呢?官方給出的解釋是:”因為基於組件樹結構的事件流方式實在是讓人難以理解,並且在組件結構擴展的過程中會變得越來越脆弱。這種事件方式確實不太好,我們也不希望在以後讓開發者們太痛苦。並且 $dispatch$broadcast 也沒有解決兄弟組件間的通信問題。“

    確實如官網所說,這種事件流的方式確實不容易讓人理解,而且後期維護成本比較高。但是在小編看來,不管黑貓白貓,能抓老鼠的都是好貓,在許多特定的業務場景中,因為業務的複雜性,很有可能使用到這樣的通信方式。但是使用歸使用,但是不能濫用,小編一直就在項目中有使用。

     

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

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    新北清潔公司,居家、辦公、裝潢細清專業服務

    ※推薦評價好的iphone維修中心

  • Java 反射簡介

    本文部分內容參考博客。點擊鏈接可以查看原文。

    1. 反射的概念

    反射是指在運行時將類的屬性、構造函數和方法等元素動態地映射成一個個對象。通過這些對象我們可以動態地生成對象實例,調用類的方法和更改類的屬性值。

    2. 使用場景

    什麼情況下運用JAVA反射呢?如果編譯時根本無法預知對象和類可能屬於哪些類,程序只依靠運行時信息來發現該對象和類的真實信息,此時就必須使用反射。

    使用反射可以實現下面的功能:

    • 在運行時判斷任意一個對象所屬的類
    • 在運行時構造任意一個類的對象
    • 在運行時判斷任意一個類所具有的方法和屬性
    • 在運行時調用任意一個對象的方法
    • 生成動態代理

    3. 獲得Class對象的幾種方式

    前面已經介紹過了,每個類被加載之後,系統就會為該類生成一個對應的Class對象,通過該Class對象就可以訪問到JVM中的這個類。在Java程序中獲得Class對象通常有如下3種方式。

    • 使用Class類的forName(String clazzName)靜態方法。該方法需要傳入字符串參數,該字符串參數的值是某個類的全限定類名(必須添加完整包名)。
    • 調用某個類的class屬性來獲取該類對應的Class對象。例如,Person.class將會返回Person類對應的Class對象。
    • 調用某個對象的getClass()方法。該方法是java.lang.Object類中的一個方法,所以所有的Java對象都可以調用該方法,該方法將會返回該對象所屬類對應的Class對象。

    對於第一種方式和第二種方式都是直接根據類來取得該類的Class對象,相比之下,第二種方式有如下兩種優勢。

    • 代碼更安全。程序在編譯階段就可以檢查需要訪問的Class對象是否存在。
    • 程序性能更好。因為這種方式無須調用方法,所以性能更好。

    也就是說,大部分時候我們都應該使用第二種方式來獲取指定類的Class對象。但如果我們只有一個字符串,例如“java.lang.String”,若需要獲取該字符串對應的Class對象,則只能使用第一種方式,使用Class的forName(String clazzName)方法獲取Class對象時,該方法可能拋出一個ClassNotFoundException異常。一旦獲得了某個類所對應的Class對象之後,程序就可以調用Class對象的方法來獲得該對象和該類的真實信息了。

    4. Class類 API介紹

    通過class類我們能夠獲取大量的信息:

    1. 獲取構造函數
    • Connstructor getConstructor(Class<?>… parameterTypes):返回此Class對象對應類的指定public構造器。
    • Constructor<?>[] getConstructors():返回此Class對象對應類的所有public構造器。
    • Constructor getDeclaredConstructor(Class<?>… parameterTypes):返回此Class對象對應類的指定構造器,與構造器的訪問權限無關。
    • Constructor<?>[] getDeclaredConstructors():返回此Class對象對應類的所有構造器,與構造器的訪問權限無關。
    1. 獲取方法
    • Method getDeclaredMethod(String name, Class<?>… parameterTypes):返回此Class對象對應類的指定方法,與方法的訪問權限無關。
    • Method[] getDeclaredMethods():返回此Class對象對應類的全部方法,與方法的訪問權限無關。
    1. 獲取屬性
    • Field getField(String name):返回此Class對象對應類的指定public Field。
    • Field[] getFields():返回此Class對象對應類的所有public Field。
    • Field getDeclaredField(String name):返回此Class對象對應類的指定Field,與Field的訪問權限無關。
    • Field[] getDeclaredFields():返回此Class對象對應類的全部Field,與Field的訪問權限無關。
    1. 獲取Class對應類上所包含的Annotation。
    • A getAnnotation(Class annotationClass):試圖獲取該Class對象對應類上指定類型的Annotation;如果該類型的註釋不存在,則返回null。
    • Annotation[] getAnnotations():返回該Class對象對應類上的所有Annotation。
    • Annotation[] getDeclaredAnnotations():返回直接修飾該Class對應類的所有Annotation。
    1. 獲取Class對象對應類包含的內部類。
    • Class<?>[] getDeclaredClasses():返回該Class對象對應類里包含的全部內部類。
      如下方法用於訪問該Class對象對應類所在的外部類。
    • Class<?> getDeclaringClass():返回該Class對象對應類所在的外部類。
      如下方法用於訪問該Class對象對應類所繼承的父類、所實現的接口等。
    • Class<?>[] getInterfaces():返回該Class對象對應類所實現的全部接口。
    1. 獲取Class對象對應類所繼承的父類
    • Class<? super T> getSuperclass():返回該Class對象對應類的超類的Class對象。
    1. 獲取Class對象對應類的修飾符、所在包、類名等基本信息。
    • int getModifiers():返回此類或接口的所有修飾符。修飾符由public、protected、private、final、static、abstract等對應的常量組成,返回的整數應使用Modifier工具類的方法來解碼,才可以獲取真實的修飾符。
    • Package getPackage():獲取此類的包。
    • String getName():以字符串形式返回此Class對象所表示的類的名稱。
    • String getSimpleName():以字符串形式返回此Class對象所表示的類的簡稱。
    1. 判斷該類是否為接口、枚舉、註釋類型等
    • boolean isAnnotation():返回此Class對象是否表示一個註釋類型(由@interface定義)。
    • boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判斷此Class對象是否使用了Annotation註釋修飾。
    • boolean isAnonymousClass():返回此Class對象是否是一個匿名類。
    • boolean isArray():返回此Class對象是否表示一個數組類。
    • boolean isEnum():返回此Class對象是否表示一個枚舉(由enum關鍵字定義)。
    • boolean isInterface():返回此Class對象是否表示一個接口(使用interface定義)。
    • boolean isInstance(Object obj):判斷obj是否是此Class對象的實例,該方法可以完全代替instanceof操作符。

    上面的多個getMethod()方法和getConstructor()方法中,都需要傳入多個類型為Class<?>的參數,用於獲取指定的方法或指定的構造器。關於這個參數的作用,假設某個類內包含如下3個info方法簽名:

    • public void info()
    • public void info(String str)
    • public void info(String str , Integer num)

    這3個同名方法屬於重載,它們的方法名相同,但參數列表不同。在Java語言中要確定一個方法光有方法名是不行的,例如,我們指定info方法——實際上可以是上面3個方法中的任意一個!如果需要確定一個方法,則應該由方法名和形參列表來確定,但形參名沒有任何實際意義,所以只能由形參類型來確定。例如,我們想要確定第二個info方法,則必須指定方法名為info,形參列表為String.class——因此在程序中獲取該方法使用如下代碼:

    clazz.getMethod("info",String.class);
    

    使用反射生成對象

    1. 利用構造函數生成對象
    • 使用Class對象的newInstance()方法來創建該Class對象對應類的實例,這種方式要求該Class對象的對應類有默認構造器,而執行newInstance()方法時實際上是利用默認構造器來創建該類的實例。
    • 先使用Class對象獲取指定的Constructor對象,再調用Constructor對象的newInstance()方法來創建該Class對象對應類的實例。通過這種方式可以選擇使用指定的構造器來創建實例。
    Constructor c = clazz.getConstructor(String.class);
    c.newInstance("xx");
    

    調用方法

    Method setProName = aClass.getDeclaredMethod("setProName",String.class);
    setProName.setAccessible(true);
    etProName.invoke(product,"我是一個產品");
    

    操作屬性

    Field[] declaredFields = aClass.getDeclaredFields();
            for (Field declaredField : declaredFields) {
                System.out.println("fieldName:"+declaredField.getName()+" filedType:"+declaredField.getType());
            }
            Field proName = aClass.getDeclaredField("proName");
            proName.setAccessible(true);
            proName.set(product,"我是一個產品");
            System.out.println("修稿屬性:"+product);
    

    操作數組

    //使用反射動態地創建數組
    //創建一個元素類型為String,長度為3的數組
    Object arr = Array.newInstance(String.class, 3);
    //依次為arr數組中index為0,1,2的元素賦值
    Array.set(arr, 0, "榮耀盒子");
    Array.set(arr, 1, "榮耀8手機");
    Array.set(arr, 2, "華為mate9保時捷版");
    Object o1= Array.get(arr, 0);
    Object o2= Array.get(arr, 1);
    Object o3= Array.get(arr, 2);
    System.out.println(o1);
    System.out.println(o2);
    System.out.println(o3);
    

    5. 使用Demo

    public class ReflectDemo {
        public static void main(String[] args) throws Exception {
            Class<Product> aClass = Product.class;
            Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
            for (Constructor<?> declaredConstructor : declaredConstructors) {
                System.out.println(declaredConstructor.getName());
            }
            Constructor<Product> constructor = aClass.getConstructor(int.class, String.class);
            Product product = constructor.newInstance(10, "ds");
            System.out.println("創建對象:"+product);
    
            //獲取方法並調用
            Method setProName = aClass.getDeclaredMethod("setProName", String.class);
            setProName.setAccessible(true);
            setProName.invoke(product,"我是一個產品");
            System.out.println("調用方法:"+product);
    
            //獲取屬性,並設置屬性的值
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field declaredField : declaredFields) {
                System.out.println("fieldName:"+declaredField.getName()+" filedType:"+declaredField.getType());
            }
            Field proName = aClass.getDeclaredField("proName");
            proName.setAccessible(true);
            proName.set(product,"我是一個產品");
            System.out.println("修稿屬性:"+product);
    
            //使用反射動態地創建數組
            //創建一個元素類型為String,長度為3的數組
            Object arr = Array.newInstance(String.class, 3);
            //依次為arr數組中index為0,1,2的元素賦值
            Array.set(arr, 0, "榮耀盒子");
            Array.set(arr, 1, "榮耀8手機");
            Array.set(arr, 2, "華為mate9保時捷版");
            Object o1= Array.get(arr, 0);
            Object o2= Array.get(arr, 1);
            Object o3= Array.get(arr, 2);
            System.out.println(o1);
            System.out.println(o2);
            System.out.println(o3);
            
            System.out.println("end...");
        }
    }
    

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

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

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

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • 解Bug之路-中間件”SQL重複執行”

    解Bug之路-中間件”SQL重複執行”

    前言

    我們的分庫分表中間件在線上運行了兩年多,到目前為止還算穩定。在筆者將精力放在處理各種災難性事件(例如中間件物理機宕機/數據庫宕機/網絡隔離等突發事件)時。竟然發現還有一些奇怪的corner case。現在就將排查思路寫成文章分享出來。

    Bug現場

    應用拓撲

    應用通過中間件連後端多個數據庫,sql會根據路由規則路由到指定的節點,如下圖所示:

    錯誤現象

    應用在做某些數據庫操作時,會發現有比較大的概率失敗。他們的代碼邏輯是這樣:

    	int count = updateSql(sql1);
    	...
    	// 偽代碼
    	int count = updateSql("update test set value =1 where id in ("100","200") and status = 1;
    	if( 0 == count ){
    		throw new RuntimeException("更新失敗");
    	}
    	......
    	int count = updateSql(sql3);
    	...
    

    即每做一次update之後都檢查下是否更新成功,如果不成功則回滾並拋異常。
    在實際測試的過程中,發現經常報錯,更新為0。而實際那條sql確實是可以更新到的(即報錯回滾后,我們手動執行sql可以執行並update count>0)。

    中間件日誌

    筆者根據sql去中間件日誌裏面搜索。發現了非常奇怪的結果,日誌如下:

    2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>0;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24266;
    2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>2;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24591;
    

    由於中間件對每條sql都標識了唯一的一個sqlID,在日誌表現看來就好像sql執行了兩遍!由於sql中有一個in,很容易想到是否被拆成了兩條執行了。如下圖所示:

    這條思路很快被筆者否決了,因為筆者explain並手動執行了一下,這條sql確實只路由到了一個節點。真正完全否決掉這條思路的是筆者在日誌裏面還發現,同樣的SQL會打印三遍!即看上去像執行了三次,這就和僅僅只in了兩個id的sql在思路上相矛盾了。

    數據庫日誌

    那到底數據真正執行了多少條呢?找DBA去撈一下其中的sql日誌,由於線下環境沒有日誌切割,日誌量巨大,搜索時間太慢。沒辦法,就按照現有的數據進行分析吧。

    日誌如何被觸發

    由於當前沒有任何思路,於是筆者翻看中間件的代碼,發現在update語句執行后,中間件會在收到mysql okay包后打印上述日誌。如下圖所示:

    注意到所有出問題的update出問題的時候都是同一個NIOREACTOR線程先後打印了兩條日誌,所以筆者推斷這兩個okay大概率是同一個後端連接返回的。

    什麼情況會返回多個okay?

    這個問題筆者思索了很久,因為在筆者的實際重新執行出問題的sql並debug時,永遠只有一個okay返回。於是筆者聯想到,我們中間件有個狀態同步的部分,而這些狀態同步是將set auto_commit=0等sql拼接到應用發送的sql前面。即變成如下所示:

    sql可能為
    set auto_commit=0;set charset=gbk;>update test set value =1 where id in ("1","2") and status = 1;
    

    於是筆者細細讀了這部分的代碼,發現處理的很好。其通過計算出前面拼接出的sql數量,再在接收okay包的時候進行遞減,最後將真正執行的那條sql處理返回。其處理如下圖所示:

    但這裏確給了筆者一個靈感,即一條sql文本確實是有可能返回多個okay包的。

    真相大白

    在筆者發現(sql1;sql2;)這樣的拼接sql會返回多個okay包后,就立刻聯想到,該不會業務自己寫了這樣的sql發給中間件,造成中間件的sql處理邏輯錯亂吧。因為我們的中間件只有在對自己拼接(同步狀態)的sql做處理,明顯是無法處理應用傳過來即為拼接sql的情況。
    由於看上去有問題的那條sql並沒有拼接,於是筆者憑藉這條sql打印所在的reactor線程往上搜索,發現其上面真的有拼接sql!

    2020-03-1311:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;
    sql=>update test_2 set value =1 where id=1 and status = 1;update test_2 set value =1 where id=2 and status = 1;
    

    如上圖所示,(update1;update2)中update1的okay返回被驅動認為是所有的返回。然後應用立即發送了update3。前腳剛發送,update2的okay返回就回來了而其剛好是0,應用就報錯了(要不是0,這個錯亂邏輯還不會提前暴露)。那三條”重複執行”也很好解釋了,就是之前的拼接sql會有三條。

    為何是概率出現

    但奇怪的是,並不是每次拼接sql都會造成update3″重複執行”的現象,按照筆者的推斷應該前面只要是多條拼接sql就會必現才對。於是筆者翻了下jdbc驅動源碼,發現其在發送命令之前會清理下接收buffer,如下所示:

    MysqlIO.java
    final Buffer sendCommand(......){
    	......
    	// 清理接收buffer,會將殘存的okay包清除掉
    	clearInputStream();
    	......
    	send(this.sendPacket, this.sendPacket.getPosition());
    	......
    }
    

    正是由於clearInputStream()使得錯誤非必現(暴露),如果okay(update2)在應用發送第三條sql前先到jdbc驅動會被驅動忽略!
    讓我們再看一下不會讓update3″重複執行”的時序圖:

    即根據okay(update2)返回的快慢來決定是否暴露這個問題,如下圖所示:

    同時筆者觀察日誌,確實這種情況下”update1;update2″這條語句在中間件裏面日誌有兩條。

    臨時解決方案

    讓業務開發不用這些拼接sql的寫法后,再也沒出過問題。

    為什麼不連中間件是okay的

    業務開發這些sql是就在線上運行了好久,用了中間件后才出現問題。
    既然不連中間件是okay的,那麼jdbc必然有這方面的完善處理,筆者去翻了下mysql-connect-java(5.1.46)。由於jdbc裏面存在大量的兼容細節處理,筆者這邊只列出一些關鍵代碼路徑:

    MySQL JDBC 源碼
    MySQLIO
    stack;
    executeUpdate
    	|->executeUpdateInternel
    		|->executeInternal
    			|->execSQL
    				|->sqlQueryDirect
    					|->readAllResults (MysqlIO.java)
    readAllResults: //核心在這個函數的處理裏面
    ResultSetImpl readAllResults(......){
    		......
           while (moreRowSetsExist) {
    			  ......
    			  // 在返回okay包的保中其serverStatus字段中如果SERVER_MORE_RESULTS_EXISTS置位
    			  // 表明還有更多的okay packet
                moreRowSetsExist = (this.serverStatus & SERVER_MORE_RESULTS_EXISTS) != 0;
            }
            ......
    }
    

    正確的處理流程如下圖所示:

    而我們中間件的源碼確實這麼處理的:

    @Override
    public void okResponse(byte[] data, BackendConnection conn) {
    	......
    	// 這邊僅僅處理了autocommit的狀態,沒有處理SERVER_MORE_RESULTS_EXISTS
    	// 所以導致了不兼容拼接sql的現象
    	ok.serverStatus = source.isAutocommit() ? 2 : 1;
    	ok.write(source);
    	......
    }
    

    select也”重複執行”了

    解決完上面的問題后,筆者在日誌里竟然發現select盡然也有重複的,這邊並不會牽涉到okay包的處理,難道還有問題?日誌如下所示:

    2020-03-13 12:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;select abc;
    2020-03-13 12:21:01:045[NIOREACTOR-21RW]frontIP=>ip2;sqlID=>12345678;rows=>1;select abc;
    

    從不同的REACTOR線程號(20RW/21RW)和不同的frontIP(ip1,ip2)來看是兩個連接執行了同樣的sql,但為何sqlID是一樣的?任何一個詭異的現象都必須一查到底。於是筆者登錄到應用上看了下應用日誌,確實應用有兩個不同的線程運行了同一條sql。
    那肯定是中間件日誌打印的問題了,筆者很快就想通了其中的關竅,我們中間件有個對同樣sql緩存其路由節點結構體的功能(這樣下一次同樣sql就不必解析,降低了CPU),而sqlID信息正好也在那個路由節點結構體裏面。如下圖所示:

    這個緩存功能感覺沒啥用(因為線上基本是沒有相同sql的),於是筆者在筆者優化的閃電模式下(大幅度提高中間件性能)將這個功能禁用掉了,沒想到為了排查問題而開啟的詳細日誌碰巧將這個功能開啟了。

    總結

    任何系統都不能說百分之百穩定可靠,尤其是不能立flag。在線上運行了好幾年的系統也是如此。只有對所有預料外的現象進行細緻的追查與深入的分析並解決,才能讓我們的系統越來越可靠。

    公眾號

    關注筆者公眾號,獲取更多乾貨文章:

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

    【其他文章推薦】

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

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

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

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    新北清潔公司,居家、辦公、裝潢細清專業服務

  • 多圖解釋Redis的整數集合intset升級過程

    多圖解釋Redis的整數集合intset升級過程

    redis源碼分析系列文章

    [Redis源碼系列]在Liunx安裝和常見API 

    為什麼要從Redis源碼分析 

    String底層實現——動態字符串SDS 

    雙向鏈表都不懂,還說懂Redis?

    面試官:說說Redis的Hash底層 我:……(來自閱文的面試題)

    Redis的跳躍表確定不了解下

     

    前言

    大噶好,今天仍然是元氣滿滿的一天,拋開永遠寫不完的需求,拒絕要求賊變態的客戶,單純的學習技術,感受技術的魅力。(哈哈哈,皮一下很開森)

    前面幾周我們一起看了Redis底層數據結構,如動態字符串SDS雙向鏈表Adlist字典Dict跳躍表,如果有對Redis常見的類型或底層數據結構不明白的請看上面傳送門。

    今天來說下set的底層實現整數集合,如果有對set不明白的,常見的API使用這篇就不講了,看上面的傳送門哈。

    整數集合概念

    整數集合是Redis設計的一種底層結構,是set的底層實現,當集合中只包含整數值元素,並且這個集合元素數據不多時,會使用這種結構。但是如果不滿足剛才的條件,會使用其他結構,這邊暫時不講哈。

    下圖為整數集合的實際組成,包括三個部分,分別是編碼格式encoding,包含元素數量length,保存元素的數組contents。(這邊只需要簡單看下,下面針對每個模塊詳細說明哈)

    整數集合的實現

    我們看下intset.h裏面關於整數集合的定義,上代碼哈:

    //整數集合結構體
    typedef struct intset {
        uint32_t encoding;  //編碼格式,有如下三種格式,初始值默認為INTSET_ENC_INT16
        uint32_t length;    //集合元素數量
        int8_t contents[];  //保存元素的數組,元素類型並不一定是ini8_t類型,柔性數組不佔intset結構體大小,並且數組中的元素從小到大排列。
    } intset;               
    
    #define INTSET_ENC_INT16 (sizeof(int16_t))   //16位,2個字節,表示範圍-32,768~32,767
    #define INTSET_ENC_INT32 (sizeof(int32_t))   //32位,4個字節,表示範圍-2,147,483,648~2,147,483,647
    #define INTSET_ENC_INT64 (sizeof(int64_t))   //64位,8個字節,表示範圍-9,223,372,036,854,775,808~9,223,372,036,854,775,807

     

     

    編碼格式encoding

    包括INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64三種類型,其分別對應着不同的範圍,具體看上面代碼的註釋信息。

    因為插入的數據的大小是不一樣的,為了盡可能的節約內存(畢竟都是錢,平時要省着點用),所以我們需要使用不同的類型來存儲數據。

    集合元素數量length

    記錄了保存數據contents的長度,即有多少個元素。

    保存元素的數組contents

    真正存儲數據的地方,數組是按照從小到大有序排序的,並且不包含任何重複項(因為set是不含重複項,所以其底層實現也是不含包含項的)。

    整數集合升級過程(重點,手動標星)

    上面的圖我們重新看下,編碼格式encoding為INTSET_ENC_INT16,即每個數據佔16位。長度length為4,即數組content裏面有四個元素,分別是1,2,3,4。如果我們要添加一個数字位40000,很明顯超過編碼格式為INTSET_ENC_INT16的範圍-32,768~32,767,應該是編碼格式為INTSET_ENC_INT32。那麼他是如何升級的呢,從INTSET_ENC_INT16升級到INTSET_ENC_INT32的呢?

    1.了解舊的存儲格式

    首先我們看下1,2,3,4這四個元素是如何存儲的。首先要知道一共有多少位,計算規則為length*編碼格式的位數,即4*16=64。所以每個元素佔用了16位。

    2.確定新的編碼格式

    新的元素為40000,已經超過了INTSET_ENC_INT16的範圍-32,768~32,767,所以新的編碼格式為INTSET_ENC_INT32。

    3.根據新的編碼格式新增內存

    上面已經說明了編碼格式為INTSET_ENC_INT32,計算規則為length*編碼格式的位數,即5*32=160。所以新增的位數為64-159。

    4.根據編碼格式設置對應的值

    從上面知道按照新的編碼格式,每個數據應該佔用32位,但是舊的編碼格式,每個數據佔用16位。所以我們從後面開始,每次獲取32位用來存儲數據。

    這樣說太難懂了,看下圖。

    首先,那最後32位,即128-159存儲40000。那麼第49-127是空着的。

    接着,取空着的49-127最後的32位,即96到127這32位,用來存儲4。那麼之前4存儲的位置48-6349-127剩下的64-95這兩部分組成了一個大部分,即48-95,現在空着啦。

    在接着在48-95這個大部分,再取后32位,即64-95,用來存儲3。那麼之前3存儲位置32-4748-95剩下的48-63這兩部分組成了一個大部分,即32-63,現在空着啦。

    再接着,將32-63這個大部分,再取后32位,即還是32-63,用來存儲2。那麼之前2存儲位置16-31空着啦。

    最後,將16-31和原來0-31合起來,存儲1。

    至此,整個升級過程結束。整體來說,分為3步,確定新的編碼格式,新增需要的內存空間,從后往前調整數據。

    這邊有個小問題,為啥要從后往前調整數據呢?

    原因是如果從前往後,數據可能會覆蓋。也拿上面個例子來說,數據1在0-15位,數據2在16-31位,如果從前往後,我們知道新的編碼格式INTSET_ENC_INT32要求每個元素佔用32位,那麼數據1應該佔用0-31,這個時候數據2就被覆蓋了,以後就不知道數據2啦。

    但是從后往前,因為後面新增了一些內存,所以不會發生覆蓋現象。

    升級的優點

     節約內存

    整數集合既可以讓集合保存三種不同類型的值,又可以確保升級操作只在有需要的時候進行,這樣就節省了內存。 

    不支持降級

    一旦對數組進行升級,編碼就會一直保存升級后的狀態。即使後面把40000刪掉了,編碼格式還是不會將會INTSET_ENC_INT16。

    整數集合的源碼分析

    創建一個空集合 intsetnew

    這個方法比較簡單,是初始化整數集合的步驟,即下圖部分。

    主要的步驟是分配內存空間,設置默認編碼格式,以及初始化數組長度length。

    intset *intsetNew(void) {
        intset *is = zmalloc(sizeof(intset));//分配內存空間 
        is->encoding = intrev32ifbe(INTSET_ENC_INT16);//設置默認編碼格式INTSET_ENC_INT16 
        is->length = 0;//初始化length 
        return is;
    }

    添加元素並升級insetAdd流程圖(重點)

    添加元素並升級insetAdd源碼分析

    可以根據上面的流程圖,對照着下面的源碼分析,這邊就不寫啦哈。

    //添加元素
    //輸入參數*is為原整數集合
    //value為要添加的元素
    //*success為是否添加成功的標誌量 ,1表示成功,0表示失敗 
    intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
        //確定要添加的元素的編碼格式 
        uint8_t valenc = _intsetValueEncoding(value);
        
        uint32_t pos;
        //如果success沒有初始值,則初始化為1 
        if (success) *success = 1;
    
       //如果新的編碼格式大於現在的編碼格式,則升級並添加元素 
        if (valenc > intrev32ifbe(is->encoding)) {
            //調用另一個方法 
            return intsetUpgradeAndAdd(is,value);
        } else {
            //如果編碼格式不變,則調用查詢方法 
            //輸入參數is為原整數集合 
            //value為要添加的數據
            //pos為位置 
            if (intsetSearch(is,value,&pos)) {//如果找到了,則直接返回,因為數據是不可重複的。 
                if (success) *success = 0;
                return is;
            }
    
            //設置length 
            is = intsetResize(is,intrev32ifbe(is->length)+1);
            if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
        }
        //設置數據 
        _intsetSet(is,pos,value);
        is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
        return is;
    }
    
    
    //#define INT8_MAX 127
    //#define INT16_MAX 32767
    //#define INT32_MAX 2147483647
    //#define INT64_MAX 9223372036854775807LL 
    static uint8_t _intsetValueEncoding(int64_t v) {
        if (v < INT32_MIN || v > INT32_MAX)
            return INTSET_ENC_INT64;
        else if (v < INT16_MIN || v > INT16_MAX)
            return INTSET_ENC_INT32;
        else
            return INTSET_ENC_INT16;
    }
    
    
    //根據輸入參數value的編碼格式,對整數集合is的編碼格式升級 
    static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
        //當前集合的編碼格式 
        uint8_t curenc = intrev32ifbe(is->encoding);
        //根據對value解析獲取新的編碼格式 
        uint8_t newenc = _intsetValueEncoding(value);
        //獲取集合元素數量 
        int length = intrev32ifbe(is->length);
        //如果要添加的數據小於0,則prepend為1,否則為0 
        int prepend = value < 0 ? 1 : 0;
    
       //設置集合為新的編碼格式,並根據編碼格式重新設置內存 
        is->encoding = intrev32ifbe(newenc);
        is = intsetResize(is,intrev32ifbe(is->length)+1);
    
        //逐步循環,直到length小於0,挨個重新設置每個值,從后往前 
        while(length--)
            _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    
        //如果value為負數,則放在最前面 
        if (prepend)
            _intsetSet(is,0,value);
        else//如果value為整數,設置最末尾的元素為value 
            _intsetSet(is,intrev32ifbe(is->length),value);
        //重新設置length 
        is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
        return is;
    }
    
    
    //找到is集合中值為value的下標,返回1,並保存在pos中,沒有找到返回0,並將pos設置為value可以插入到數組的位置
    static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
        int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
        int64_t cur = -1;
    
        //如果集合為空,那麼位置pos為0 
        if (intrev32ifbe(is->length) == 0) { 
            if (pos) *pos = 0;
            return 0;
        } else {
            //因為數據是有序集合,如果要添加的數據大於最後一個数字,那麼直接把要添加的值放在最後即可,返回最大值下標 
            if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
                if (pos) *pos = intrev32ifbe(is->length);
                return 0;
            } else if (value < _intsetGet(is,0)) { //如果這個數據小於數組下標為0的數據,即為最小值 ,返回0 
                if (pos) *pos = 0;
                return 0;
            }
        }
        //有序集合採用二分法 
        while(max >= min) {
            mid = ((unsigned int)min + (unsigned int)max) >> 1;
            cur = _intsetGet(is,mid);
            if (value > cur) {
                min = mid+1;
            } else if (value < cur) {
                max = mid-1;
            } else {
                break;
            }
        }
    
        //確定找到 
        if (value == cur) {
            if (pos) *pos = mid;//設置參數pos,返回1,即找到位置 
            return 1;
        } else {//如果沒找到,則min和max相鄰,隨便設置都行,並返回0 
            if (pos) *pos = min; 
            return 0;
        }
    }

     

    結語

    該篇主要講了Redis的SET數據類型的底層實現整數集合,先從整數集合是什麼,,剖析了其主要組成部分,進而通過多幅過程圖解釋了intset是如何升級的,最後結合源碼對整數集合進行描述,如創建過程,升級過程,中間穿插例子和過程圖。

    如果覺得寫得還行,麻煩給個贊,您的認可才是我寫作的動力!

    如果覺得有說的不對的地方,歡迎評論指出。

    好了,拜拜咯。

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

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※別再煩惱如何寫文案,掌握八大原則!