斯柯達混動/純電動汽車未來幾年將相繼上市

近日,據海外媒體報導,斯柯達在未來幾年計畫推出一系列新能源車,其中包括或將於2019年上市的插電式混動車型以及最早2020年才能推出的純電動版車型。

斯柯達未來的純電動車型就將基於大眾MEB電動車模組化平臺進行打造,目前在考慮推出晶銳和明銳的純電動版車型,不過即使推出的話,最早也要等到2020年,不知道那時候某些城市購買純電動車搖號的話會不會很費勁了。

在推出純電動車之前,斯柯達計畫先推出插電式混動車型,首先考慮的是旗艦車型速派,最早能在2019年正式推出。

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

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

麥格納攜手格特拉克將亮相第六屆中國國際新能源汽車論壇2016

由希邁商務諮詢(上海)有限公司主辦的2016年第六屆中國國際新能源汽車論壇即將於4月20日-4月22日在上海隆重舉行。此次論壇獲得了亞太電動車學會、中國國家新能源機動車產品品質監督檢驗中心、上海交大密西根學院、賽迪顧問及中國綠色能源產業技術創新戰略聯盟的大力支持。截止3月14日,論壇已經確認40位演講嘉賓出席本次論壇並做高品質學術演講。演講嘉賓分別來自菲律賓電動車協會、國家新能源機動車產品品質監督檢驗中心、美國國家能源局、中國工程院等在內的政府單位與研究機構,以及包括寶馬、通用汽車、特斯拉、長安汽車、宇通客車、奇瑞、比亞迪戴姆勒、上汽、北汽、觀致等在內的多個知名整車商,將在論壇上共同研討新能源汽車行業政策趨勢、技術路線及難點、基礎設施建設、商業模式,延續以往的豐碩成果,繼續為新能源汽車行業作出貢獻。

作為全球首屈一指的汽車零部件供應商,麥格納旗下動力總成事業部此次將攜手格特拉克成為本屆論壇的鉑金贊助商,不僅會分享電子驅動和混合驅動控制及新能源汽車熱管理研究的技術報告,還將同期展示新能源汽車領域的前沿產品。麥格納動力總成是全球領先的汽車動力總成系統供應商,集傳動系統設計、研發、試驗和製造能力為一體,以世界級的製造水準不斷實現產品和工藝的創新,為國內外客戶傳遞卓越價值。

本屆論壇相比歷屆舉辦規模最大的第六屆新能源汽車論壇,涉及主論壇及三個分論壇、考察活動、頒獎典禮和交流晚宴。屆時將誠邀全球範圍內的整車製造商、電網電力公司、電池廠商、零部件供應商、核心技術提供商和政府官員近500位行業人士一起,對新能源汽車產業面臨的挑戰,機遇與對策各方面進行為期三天更深層次並具有建設和戰略性的探討。

如需更多詳情,請諮詢:
大會連絡人:Hill ZENG(曾先生)
聯繫電話:0086 21-6045 1760 或郵箱
我們期待與貴單位一起出席於2016年4月20-22日在上海舉辦的第六屆中國國際新能源汽車論壇2016,以利決策!

 

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

【其他文章推薦】

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

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

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

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

雪佛蘭Bolt電動車進入試生產階段

據報導,雪佛蘭Bolt電動車已經進入到試生產階段,在這通過道橫在經銷商的大門之前,要測試產品和生產設備的穩定性,確保所有下線的汽車都能達標。目前,已經有車輛從美國密歇根州Orion Township的生產車間下線。

在很多方面,Bolt都被認為是Model 3的強有力競爭對手,性能相當定價也在3萬美元左右的水準。兩家公司都致力於將電動汽車的成本降低到普通消費者可以接受的範圍內,並承諾充滿電之後續航里程能達到320公里。

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

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

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

flink 流式處理中如何集成mybatis框架

flink 中自身雖然實現了大量的connectors,如下圖所示,也實現了jdbc的connector,可以通過jdbc 去操作數據庫,但是flink-jdbc包中對數據庫的操作是以ROW來操作並且對數據庫事務的控制比較死板,有時候操作關係型數據庫我們會非常懷念在java web應用開發中的非常優秀的mybatis框架,那麼其實flink中是可以自己集成mybatis進來的。 我們這裏以flink 1.9版本為例來進行集成。

如下圖為flink內部自帶的flink-jdbc:

 

創建一個flink的流式處理項目,引入flink的maven依賴和mybatis依賴(注意這裏引入的是非spring版本,也就是mybatis的單機版):

<properties>

<flink.version>1.9.0</flink.version>
</properties>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.2</version>
</dependency>
<!-- flink java 包 -->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-streaming-java_2.11</artifactId>
    <version>${flink.version}</version>
</dependency>

maven依賴引入以後,那麼需要在resources下面定義mybatis-config.xml 配置:

mybatis-config.xml 需要定義如下配置:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <typeAlias alias="BankBillPublic" type="xxxx.xx.xx.BankBillPublic" />
    </typeAliases>
    <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://xx.xx.xx.xx:3306/hue?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&autoReconnect=true" />
                <property name="username" value="xxxx" />
                <property name="password" value="xxxx*123%" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/xxxxxMapper.xml" />
    </mappers>
</configuration>

typeAlias 標籤中為自定義的數據類型,然後在xxxxxMapper.xml 中parameterType或者resultType就可以直接用這種定義的數據類型。

dataSource type=”POOLED” 我們使用的是mybatis中的POOLED 類型,也就是連接池的方式去使用。默認支持如下這三種類型。

 我們也可以使用阿里巴巴開源的druid連接池,那麼就需要引入對應的maven依賴,如下所示:

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.14</version>
        </dependency>  

 然後定義一個對應的druid的DataSource,如下所示:

import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.datasource.DataSourceFactory;
import com.alibaba.druid.pool.DruidDataSource;

public class DruidDataSourceFactory implements DataSourceFactory {
    private Properties props;

    @Override
    public DataSource getDataSource() {
        DruidDataSource dds = new DruidDataSource();
        dds.setDriverClassName(this.props.getProperty("driver"));
        dds.setUrl(this.props.getProperty("url"));
        dds.setUsername(this.props.getProperty("username"));
        dds.setPassword(this.props.getProperty("password"));
        // 其他配置可以根據MyBatis主配置文件進行配置
        try {
            dds.init();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return dds;
    }

    @Override
    public void setProperties(Properties props) {
        this.props = props;
    }
}

之後就可以mybatis的配置中使用了,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <typeAlias alias="BankBillPublic" type="xxxx.xx.xx.BankBillPublic" />
        <typeAlias alias="DRUID" 
 type="com.xx.mybatis.druid.utils.DruidDataSourceFactory" />
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="DRUID">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://xx.xx.xx.xx:3306/hue?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&autoReconnect=true" />
                <property name="username" value="xxxx" />
                <property name="password" value="xxxx*123%" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/xxxxxMapper.xml" />
    </mappers>
</configuration>

<mappers> 下面為定義的mybatis 的xxxxxMapper文件。裏面放置的都是sql語句。

本文作者張永清,轉載請註明出處:

xxxxxMapper.xml 中的sql示例:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xx.xx.bigdata.flink.xx.xx.mapper.UserRelaInfoMapper">
    <!--查詢關鍵字匹配 -->
    <select id="queryUserRelaInfo" parameterType="String" resultType="UserRelaInfo">
        SELECT id AS id,
        USER_NAME AS userName,
        APPL_IDCARD AS applIdCard,
        PEER_USER AS peerUser,
        RELA_TYPE AS relaType,
        CREATE_USER AS createUser,
        CREATE_TIME AS createTime
        FROM USER_RELA_INFO
        <where>
            <if test="applIdCard != null">
                APPL_IDCARD=#{applIdCard}
            </if>
            <if test="peerUser != null">
            AND PEER_USER=#{peerUser}
            </if>
        </where>
    </select>
</mapper>

 定義Mapper,一般可以定義一個interface ,和xxxxxMapper.xml中的namespace保持一致

注意傳入的參數一般加上@Param 註解,傳入的參數和xxxxxMapper.xml中需要的參數保持一致

public interface UserRelaInfoMapper {
    List<UserRelaInfo> queryUserRelaInfo(@Param("applIdCard")String applIdCard,@Param("peerUser") String peerUser);
}

定義SessionFactory工廠(單例模式):

/**
 *
 *  sqlsession factory 單例  事務設置為手動提交
 */
public class MybatisSessionFactory {
    private static final Logger LOG = LoggerFactory.getLogger(MybatisSessionFactory.class);
    private static SqlSessionFactory sqlSessionFactory;
    private MybatisSessionFactory(){
        super();
    }
    public synchronized static SqlSessionFactory getSqlSessionFactory(){
        if(null==sqlSessionFactory){
            InputStream inputStream=null;
            try{
                inputStream = MybatisSessionFactory.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            }
            catch (Exception e){
                LOG.error("create MybatisSessionFactory read mybatis-config.xml cause Exception",e);
            }
            if(null!=sqlSessionFactory){
                LOG.info("get Mybatis sqlsession sucessed....");
            }
            else {
                LOG.info("get Mybatis sqlsession failed....");
            }
        }
        return sqlSessionFactory;
    }
}

  

使用mybatis 對數據庫進行操作:

        SqlSession sqlSession = MybatisSessionFactory.getSqlSessionFactory().openSession();
        UserRelaInfoMapper  userRelaInfoMapper  = sqlSession.getMapper(UserRelaInfoMapper .class);
		//調用對應的方法
		userRelaInfoMapper.xxxx();
		//提交事務
		sqlSession.commit();
		//回滾事務,一般可以捕獲異常,在發生Exception的時候,事務進行回滾
		sqlSession.rollback();
		
		
		

這裏以mysql為示例,寫一個flink下mysql的sink示例,可以自己來靈活控制事務的提交:

public class MysqlSinkFunction<IN> extends RichSinkFunction {
    private static final Logger LOG = LoggerFactory.getLogger(MysqlSinkFunction.class);
    @Override
    public void invoke(Object value, Context context) throws Exception{
        SqlSession sqlSession = MybatisSessionFactory.getSqlSessionFactory().openSession();
        try{
                            //插入
                            LOG.info("MysqlSinkFunction start to do insert data...");
                            xxx.xxx();
							//更新
                            LOG.info("MysqlSinkFunction start to do update data...");
							xxx.xxx();
                            //刪除
                            LOG.info("MysqlSinkFunction start to do delete data...");
							xxx.xxx();

                    
                
                sqlSession.commit();
                LOG.info("MysqlSinkFunction commit transaction success...");
        }
        catch (Throwable e){
            sqlSession.rollback();
            LOG.error("MysqlSinkFunction cause Exception,sqlSession transaction rollback...",e);
        }
    }
}  
相信您如果以前在spring中用過mybatis的話,對上面的這些操作一定不會陌生。由此你也可以發現,在大數據中可以完美的集成mybatis,這樣可以發揮mybatis框架對數據庫操作的優勢,使用起來也非常簡單方便。
一旦集成了mybaitis后,在flink中就可以方便的對各種各樣的關係型數據庫進行操作了。

本文作者張永清,轉載請註明出處:

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

一個類GraphQL的ORM數據訪問框架發布

Zongsoft.Data 發布公告

很高興我們的 ORM 數據訪問框架()在歷經兩個 SaaS 產品的應用之後,今天正式宣布對外推廣!
這是一個類  風格的 ORM(Object/Relational Mapping) 數據訪問框架。

又一個輪子?

在很長時間里,.NET 陣營似乎一直缺乏一個被普遍使用的 ORM 數據訪問框架,從最早的原生 ADO.NET 到舶來品 iBatis.NETHibernate.NET,後來又經歷了 Linq for SQL 與 Entity Framework 的混戰,可能是因為 Entity Framework 早期版本的模糊定位和反覆變更的設計導致了它失之霸主之位,進而造就了一段百舸爭流、群雄共逐的戰國時代。在歷經漫長而反覆的期待、失望、糾結和痛苦之後,我終於決定動手造一個輪子。

設計理念

在開始動手之前,先確定以下基本設計原則:

  • 數據庫優先(Database First)
  • 嚴格的 POCO/POJO 支持
  • 映射模型與代碼完全隔離
  • 禁止業務層出現 SQL 和類 SQL 代碼

在一個業務系統中,數據結構及其關係毋庸置疑是最底層的基礎性結構,數據庫應由系統架構師或開發負責人進行仔細設計 No Schema/Weakly Schema 的思潮是塗抹了蜂蜜的毒藥),數據訪問映射以數據庫表結構關係為基石,在此之上業務層亦以概念映射模型為準繩,層級之間相互隔離。

領域模型實體避免通過註解 (標籤) 來進行元數據定義,應確保嚴格符合 POCO/POJO 範式。通過語義化的 Schema 來聲明訪問的數據結構關係,禁止應用層的 SQLLinq 式的類 SQL 代碼可降低業務層對數據層的依賴、提升代碼可維護性外,還具備更加統一可控的便利性,併為數據訪問引擎的實現提供了更大的優化空間和自由度。

範例說明

下面通過三個的例子 (注:例子均基於 項目) 來佐證上面的部分設計理念,更多示例和闡述請參考  項目的  文檔和  項目的代碼。

提示: 下面的範例均基於 開源項目,該項目是一個完整的論壇社區的後台程序。你可能需要預先閱讀一下該項目的文檔,以便更好的理解範例代碼的業務邏輯。

示例一

導航查詢及導航過濾

var forums = this.DataAccess.Select<Forum>(
    Condition.Equal("SiteId", this.User.SiteId) &
    Condition.In("Visibility", Visibility.Internal, Visibility.Public) |
    (
        Condition.Equal("Visibility", Visibility.Specified) &
        Condition.Exists("Users",
                  Condition.Equal("UserId", this.User.UserId) &
                  (
                      Condition.Equal("IsModerator", true) |
                      Condition.NotEqual("Permission", Permission.None)
                  )
        )
    ),
    "*, MostRecentThread{ThreadId,Title,Creator{Name,Nickname,Avatar}}"
);

上述數據訪問的查詢方法大致生成如下SQL腳本:

SELECT
    t.*,
    t1.ThreadId AS 'MostRecentThread.ThreadId',
    t1.Title AS 'MostRecentThread.Title',
    t1.CreatorId AS 'MostRecentThread.CreatorId',
    t2.UserId AS 'MostRecentThread.Creator.UserId',
    t2.Name AS 'MostRecentThread.Creator.Name',
    t2.Nickname AS 'MostRecentThread.Creator.Nickname',
    t2.Avatar AS 'MostRecentThread.Creator.Avatar'
FROM Forum t
    LEFT JOIN Thread AS t1 ON
        t.MostRecentThreadId=t1.ThreadId
    LEFT JOIN UserProfile AS t2 ON
        t1.CreatorId=t2.UserId
WHERE
    t.SiteId = @p1 AND
    t.Visibility IN (@p2, @p3) OR
    (
        t.Visibility = @p4 AND
        EXISTS
        (
            SELECT u.SiteId, u.ForumId, u.UserId
            FROM ForumUser u
            WHERE u.SiteId = t.SiteId AND
                  u.ForumId = t.ForumId AND
                  u.UserId = @p5 AND
                  (
                      u.IsModerator = @p6 OR
                      u.Permission != @p7
                  )
        )
    );

上述示例通過 Select 查詢方法的 schema 參數 (即值為 *, MostRecentThread{ThreadId,Title,Creator{Name,Nickname,Avatar}} 的參數) 從數據結構關係的層次指定了查詢數據的形狀,因而不再需要 SQL 或類 SQL 語法中 JOIN 這樣命令式的語法元素,它不光提供了更簡潔且語義化的 API 訪問方式,而且還給數據訪問引擎底層提供了更大的優化空間和自由度。

如果將 Select 查詢方法的 schema 參數值改為 *,Moderators{*},MostRecentThread{ThreadId,Title,Creator{Name,Nickname,Avatar}} 后,數據訪問引擎會將查詢內部分解為一對多的兩條 SQL 語句進行迭代執行,而這些都不需要業務層進行分拆處理,因而提升了效率並降低了業務層的複雜度。

注:Schema 模式表達式通過 Web API 提供給前端應用,將大大減少後端開發的工作量,提升前後端的工作效率。

示例二

一對多的關聯新增

// 構建待新增的實體對象
var forum = new
{
    SiteId = this.User.SiteId,
    GroupId = 100,
    Name = "xxxx",

    // 一對多的導航屬性
    Users = new ForumUser[]
    {
      new ForumUser { UserId = 1001, IsModerator = true },
      new ForumUser { UserId = 1002, Permission = Permission.Read },
      new ForumUser { UserId = 1003, Permission = Permission.Write },
    }
}

// 執行數據新增操作
this.DataAccess.Insert<Forum>(forum, "*, Users{*}");

上述數據訪問的新增方法大致生成如下SQL腳本:

/* 主表插入語句,執行一次 */
INSERT INTO Forum (SiteId,ForumId,GroupId,Name,...) VALUES (@p1,@p2,@p3,@p4,...);

/* 子表插入語句,執行多次 */
INSERT INTO ForumUser (SiteId,ForumId,UserId,Permission,IsModerator) VALUES (@p1,@p2,@p3,@p4,@p5);

上述示例通過 Insert 新增方法的 schema 參數(即值為 *,User{*} 的參數)指定了新增數據的形狀,由數據訪問引擎根據映射定義自動處理底層的 SQL 執行方式,確保業務層代碼的簡潔和更高的執行效率。

示例三

一對一和一對多的關聯更新,對於“一對多”的導航屬性,還能確保該屬性值 (集合類型) 以 UPSERT 模式寫入。

public bool Approve(ulong threadId)
{
    //構建更新的條件
    var criteria =
        Condition.Equal(nameof(Thread.ThreadId), threadId) &
        Condition.Equal(nameof(Thread.Approved), false) &
        Condition.Equal(nameof(Thread.SiteId), this.User.SiteId) &
        Condition.Exists("Forum.Users",
            Condition.Equal(nameof(Forum.ForumUser.UserId), this.User.UserId) &
            Condition.Equal(nameof(Forum.ForumUser.IsModerator), true));

    //執行數據更新操作
    return this.DataAccess.Update<Thread>(new
    {
        Approved = true,
        ApprovedTime = DateTime.Now,
        Post = new
        {
            Approved = true,
        }
    }, criteria, "*,Post{Approved}") > 0;
}

上述數據訪問的更新方法大致生成如下SQL腳本:

/* 以下代碼為支持 OUTPUT/RETURNING 子句的數據庫(如:SQLServer,Oracle,PostgreSQL) */

/* 根據更新的關聯鍵創建臨時表 */
CREATE TABLE #TMP
(
    PostId bigint NOT NULL
);

/* 更新主表,並將更新的關聯鍵輸出到內存臨時表 */
UPDATE T SET
    T.[Approved]=@p1,
    T.[ApprovedTime]=@p2
OUTPUT DELETED.PostId INTO #TMP
FROM [Community_Thread] AS T
    LEFT JOIN [Community_Forum] AS T1 ON /* Forum */
        T1.[SiteId]=T.[SiteId] AND
        T1.[ForumId]=T.[ForumId]
WHERE
    T.[ThreadId]=@p3 AND
    T.[Approved]=@p4 AND
    T.[SiteId]=@p5 AND EXISTS (
        SELECT [SiteId],[ForumId]
        FROM [Community_ForumUser]
        WHERE [SiteId]=T1.[SiteId] AND
              [ForumId]=T1.[ForumId] AND
              [UserId]=@p6 AND
              [IsModerator]=@p7
    );

/* 更新關聯表 */
UPDATE T SET
    T.[Approved]=@p1
FROM [Community_Post] AS T
WHERE EXISTS (
    SELECT [PostId]
    FROM #TMP
    WHERE [PostId]=T.[PostId]);

上述示例通過 Update 更新方法的 schema 參數(即值為 *,Post{Approved} 的參數)指定了更新數據的形狀,數據訪問引擎將根據數據庫類型生成高效的 SQL 語句,對於業務層而言這一切都是無感的、透明的。

對於一對多的導航屬性,數據訪問引擎默認將以 UPSERT 模式處理子集的寫入,關於 UPSERT 更多信息請參考  項目文檔。

性能

我們希望提供最佳的綜合性價比,對於一個 ORM 數據訪問引擎來說,性能的關注點主要 (不限) 有這些要素:

  1. 生成簡潔高效的 SQL 腳本,並盡可能利用特定數據庫的最新 SQL 語法;
  2. 數據查詢結果的實體組裝(Populate)過程必須高效;
  3. 避免反射,有效的語法樹緩存。

實現層面我們採用 Emitting 動態編譯技術對實體組裝(Populate)、數據參數綁定等進行預熱處理,可查閱  等相關類的源碼深入了解。

其他

得益於 “以聲明方式來表達數據結構關係” 的語義化設計理念,相對於命令式設計而言,它使得程序意圖更加聚焦,天然地對底層數據的表達和優化更加寬容與自由。

更多詳細內容 (譬如:讀寫分離、繼承表、數據模式、映射文件、過濾器、驗證器、類型轉換、數據隔離) 請查閱相關文檔。

支持贊助

我們歡迎並期待任何形式的推廣支持!

如果你認同我們的設計理念請為這個項目點贊(Star),如果你認為該項目很有用,並且希望支持它未來的發展,請給予必要的資金來支持它:

  1. 關注 Zongsoft 微信公眾號,對我們的文章進行打賞;
  2. 加入 ,可以獲得在線問答和技術支持;
  3. 如果您的企業需要現場技術支持與輔導,又或者需要開發新功能、即刻的錯誤修復等請給我。

提醒: 本文可能會更新,請閱讀原文:,以避免因內容陳舊而導致的謬誤,同時亦有更好的閱讀體驗。

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

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

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

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

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

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

整數的表示與編碼

補一下CSAPP的筆記

計算機中整數的表示與編碼

計算機中的整數主要包括兩種:有符號數與無符號數。

無符號數的編碼

其中有符號數的表示方法與傳統二進制一致。
假設有一個整數數據類型有w位。我們可以將位向量寫成

在這個編碼中,每個xi都取值為0或1。我們用一個函數來表示B2Uw來表示:

無符號數的編碼方式實際上與我們所知道的二進制編碼方式是一致的。唯一要注意的是無符號數的編碼具有唯一性,也就是說一個数字只能有一個無符號數編碼。這是因為B2Uw是一個雙射。

有符號數的編碼

有符號數的編碼主要有三種方式:原碼、補碼與反碼。我曾經寫過一篇博客來進行探究,這裏不贅述。

需要說明的是:補碼也具有唯一性,原碼與反碼不具備這種性質,因為0在原碼與反碼中有兩種解釋。

有符號數與無符號數之間的轉換

有符號數轉無符號數

C語言中提供了在不同數據類型中做強制類型轉換的方法,對於無符號整數與有符號整數之間的轉換方式,大多數系統上默認的是底層的位不變,由此我們能推出有符號數與無符號數之間的轉換。
關於這些的轉換的的過程和原理,在此不贅述。這裏直接給出公式:
一個數的編碼方式從無符號編碼(補碼)轉換為有符號編碼后的數值公式為:

如果有符號數的真值小於0那麼,把真值加上2w即為其無符號真值,如果真值大於0,那麼不變。
我們用一段C語言代碼舉例:

#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
int main(void)
{
    int i = -1;
    unsigned int j = (unsigned int)i;
    printf("%u\n", j);
    printf("%u\n", UINT_MAX);
    system("pause");
}

VS2017下的運行結果:

數據類型int的大小為8字節,32位,把-1轉換成無符號數需要加上232,結果為232-1,正好為無符號數編碼的最大值,所以與UINT_MAX的值一致。

無符號數轉有符號數

直接給出公式:

C語言代碼測試實例:

#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
int main(void)
{
    unsigned int i = UINT_MAX;
    int j=(int)i;
    printf("%d", j);
    system("pause");
}

VS2017下的運行結果:

需要說明的是,在VS2017的環境下,上面兩個程序經過測試即使不使用強制類型轉換也可以得到正確的結果,其一是C語言中如果發現左右兩邊數據類型不一致會自動把數據往左邊的類型轉換,其二是,printf中的格式說明符也會自動執行類型轉換,這裏使用強制類型轉換隻是為了讓轉換看起來更加清晰。

無符號整數與有符號整數互相轉換可能遇到的問題

由於C語言對同時包含有符號和無符號數表達式的這種處理方式,出現了一些奇特的行為。當執行一個運算時如果它的一個運算數是有符號的而另一個是無符號的,那麼C語言會隱式地把有符號參數強制類型轉換為無符號,並假設這兩個數都是非負的
對於 這樣的關係運算符來說,它會導致非直觀的結果。我們同樣用一個C語言程序來作為測試:

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    printf("%d", -1 < 0U);
    printf("%d",(unsigned)-1 > -2);
}

VS2017運行結果:

第一個表達式中,由於0是無符號數,所以-1默認變成無符號數,即為232-1,這個數必然比0要大。所以第一個表達式為假。
第二個表達式中,通過把-1強制轉換成無符號數,-1變為232-1,-2變為232-2,所以第二個表達式為真。

擴展一個数字的位表示

有時我們會把一個佔用空間較小的數據類型轉換為佔用空間較大的數據類型(如果把佔用空間較大的數據類型轉換為佔用空間較小的數據類型,可能會丟失數據,我們一般不推薦這麼做)。

無符號數的零擴展

定義寬度為w位的位向量:

和寬度為w’的位向量:

其中w’>w。則:

要將一個無符號數轉換為一個更大的無符號數數據類型,我們只要簡單的在前面加上足夠的0即可,這種運算被稱為零擴展。

有符號數的符號拓展

定義寬度為w位的位向量:

和寬度為w’的位向量:

其中w’>w。則:

要將補碼数字轉換為一個更大的數據類型,可以執行一個符號擴展(sign-extension),在前面添加最高有效位的值。
具體證明略。

值得注意的點:在C語言中,把類型不同、大小不同的兩個數據類型相互轉換,先改變數據類型的大小,然後在執行類型轉換。
比如說:在C語言中,把一個short類型的變量轉換為unsigned類型的變量,我們要先把short類型的變量擴展到8個字節,然後再執行有符號數到無符號數的轉換。

截斷数字

一些特殊情況下,儘管這樣做會帶來風險,但我們仍然有時候會需要把一個高位的數據類轉換為低位的數據類型,這時候我們就需要截斷這個数字。

無符號數的截斷

定義寬度為w位的位向量:

而它截斷為k位的結果為:

令x=B2U_w(\vec x),x’=B2U_(\vec x’),則x’=x mod 2^k。
截斷為k為實際上就是對原數的真值用2^k取模。具體證明過程略。

有符號數的截斷

要理解有符號數的截斷,我們首先要明白,無論是有符號數還是無符號數真正區別他們的不是他們的真值,而是他們的編碼方式,實際上無論是有符號數,還是無符號數,在內存中都表示為串二進制數,有了編碼對他們真值的解釋,他們才能表示不同的數據。
我們都知道,截斷實際上就是截去前面冗餘的位,只留下我們需要的位,既然無符號數和有符號數在內存中表示的方法實際上都是一串二進制數,我們為什麼不可以把一個有符號數的位模式,看做是無符號數的編碼,用無符號數的方式將其截斷後得到的真值,再用把無符號數轉換為有符號數,最終得到將有符號數階段的真值。
總而言之,有符號數編碼的截斷結果是:

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

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

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

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

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

Unity – Cinemachine實現相機抖動

普通相機抖動腳本較易實現,但在使用cinemachine相機下,其Transform組件不可被代碼修改,那麼Cinemachine的相機抖動如何實現呢?本文結合實際項目,對實現相機抖動的三大步驟進行系統講解:

  • 項目地址:

配置流程

項目背景:一款2D像素動作遊戲,我們操控着Player(必須帶有Collider組件),遊戲相機為Cinemachine 2DCamera(關於其配置方法此處不做贅述,推薦文章在末尾參考處)

1. 在相機上添加監聽腳本

在我們使用的虛擬相機 CM vcam1 上添加組件:AddComponent->CinemachineImpulse Listener

  • CinemachineImpulse Listener監聽腳本內震動信號(方法調用),使得抖動在此相機上發生
    • Channel Mask:通道遮罩,此處最好默認為EveryThing
    • Gain:可獲得震動信號的數目,0為屏蔽,1表示某時段僅能進行一個抖動運動
    • Use 2D Distance:用於2D遊戲,忽略相機Z軸的抖動

2. 在震動信號發生物體上添加腳本

震動信號發生物體(調用震動函數的物體)為Player,因此需要在Player上添加組件:AddComponent->Cinemachine Collision Impulse Source(注:必須掛到含Collider的物體上),然後在Raw Signal右側齒輪->New Noise Settings 添加震動配置器,默認名CM vcam1 Raw Signal

  • Cinemachine Collision Impulse Source:含有抖動函數震動配置器的關鍵腳本
    • Raw Signal:震動配置器,配置震動參數的關鍵部件,我們打開剛上面新建的CM vcam1 Raw Signal,可看到震動方式的各類參數。我們以Position Y,即上下抖動為例,添加Components后可設置其Frequency震頻Amplitude震幅,並且勾選右側方框可將其設置為正弦波
    • Attack:抖動開始的變化曲線及時間
    • Sustain Time:抖動的持續時間
    • Decay:抖動衰退的變化曲線及時間

3. 調用震動方法:

在Player內引用震動核心腳本,並在合適位置調用震動方法

private Cinemachine.CinemachineCollisionImpulseSource MyInpulse;

private void Start()
{
    MyInpulse = GetComponent<Cinemachine.CinemachineCollisionImpulseSource>();
}

private void Update()
{
    //按下右鍵產生相機抖動,抖動方式依照上面CM vcam1 Raw Signal內配置信息
    if (Input.GetMouseButtonDown(1))
        MyInpulse.GenerateImpulse();       
}

至此,我們在遊戲內操控Player,按下右鍵即可實現相機抖動。當然除了上面無參的GenerateImpulse()方法,還有兩個帶參的方法:

//假若使用傳遞velocity的方法,其震動方式為velocity和CM vcam1 Raw Signal的混合
public void GenerateImpulse(Vector3 velocity);
public void GenerateImpulse(Vector3 position, Vector3 velocity);

此外,還有可能出現bug:還尚未調用震動函數,遊戲開始時就自動產生抖動。其產生原因博主尚未在對應腳本內發現,但解決方式為關閉Player上的Cinemachine Collision Impulse Source腳本

總述

Cinemachine中實現相機抖動的基本流程:

  • 在虛擬相機上添加監聽腳本 CinemachineImpulse Listener
  • 在Player上添加震動核心腳本Cinemachine Collision Impulse Source,並添加、設置震動配置器
  • Player腳本合適位置調用震動函數

本例僅介紹了單Position方向上的抖動,讀者可按需配置抖動的Position、Rotation、發生時間、維持時間、衰退時間等,實現自己想要的效果

參考

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

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

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

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

小三通海運與一般國際貿易有何不同?

小三通快遞通關作業有哪些?

心裏有點樹

why 樹形結構

  • 順序存儲

順序存儲的特點是各個存儲單位在邏輯和物理內存上都是相鄰的,典型的就是代表就是數組,物理地址相鄰因此我們可以通過下標很快的檢索出一個元素

我們想往數組中添加一個元素最快的方式就是往它的尾部添加.如果往頭部添加元素的話,效率就很低,因為需要將從第一個元素開始依次往後移動一位,這樣就能空出第一位的元素,然後才能我們指定的數據插入到第一個的位置上

  • 鏈式存儲

鏈式存儲的特點是,各個節點之間邏輯是相鄰的,但是物理存儲上不相鄰,每一個節點都存放一個指針或者是引用用來指向它的前驅或者後繼節點, 因此我們想插入或者刪除一個元素時速度就會很塊,只需要變動一下指針的指向就行

但是對鏈表來說查找是很慢的, 因此對任意一個節點來書,他只知道自己的下一個節點或者是上一個節點在哪裡,再多的他就不不知道了,因此需要從頭結點開始遍歷…

樹型存儲結構有很多種,比如什麼二叉樹,滿二叉樹,紅黑樹,B樹等, 對於樹形結構來說,它會相對中和鏈式存儲結構和順序存儲結構的優缺點 (其中二叉排序樹最能直接的體會出樹中和鏈式存儲和線性存儲的特性,可以通過右邊的導航先去看看二叉排序樹)

樹的概述

如上圖是一個二叉樹, 當然樹還能有三叉,四叉等等…

  • 根節點: 最頂上的節點 即A
    層: 根節點在第一層 BE在第二層
    高度: 最大的層數
    森林: 多個樹的組合
    權: 節點上的值 如根節點的權是 A
    恭弘=叶 恭弘子節點: 下層上的節點是上一層的恭弘=叶 恭弘子節點
    雙親節點: 上層的節點是下層的節點的雙親節點(單個節點又是爸又是媽)
    路徑: 找到C的路徑是 A-B-C
    度: 就是直接子節點的個數

普通二叉樹

  • 什麼是二叉樹?

顧名思義就是度最大為2的樹就是二叉樹.而且對二叉樹來說,是嚴格區分左子樹和右子樹的,看上圖,雖然兩個樹的根節點都是1,但是他們的左右子樹不同,因此他們並不是相同的樹

  • 什麼是滿二叉樹?

像上圖這樣,所有的恭弘=叶 恭弘子節點都在最後一層,所有的且除了最後一層其他層的節點都有兩個子節點

二叉樹的全部節點計算公式是 2^n-1 , n是層數

  • 什麼是完全二叉樹?

像上圖這樣, 所有的恭弘=叶 恭弘子點都在最後一層或者是倒數第二層, 並且從左往右數是連續的

java&二叉樹

  • 封裝二叉樹節點
public class TreeNode {
    // 權
    private int value;
    // 左節點
    private TreeNode leftNode;
    // 右節點
    private TreeNode rightNode;
}
  • 封裝二叉樹
public class BinaryTree {
    TreeNode root;

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

    public TreeNode getRoot() {
        return this.getRoot();
    }
}

遍歷

像這樣一顆二叉樹,通過不同的書序遍歷會得到不同的結果

前中后的順序說的是root節點的順序,前序的話就是先遍歷父節點, 中序就是左父右 後續就是左右父

  • 前序遍歷
 public void frontShow() {
        System.out.println(this.value);
        if (leftNode != null)
            leftNode.frontShow();

        if (rightNode != null)
            rightNode.frontShow();
    }
  • 中序遍歷
    public void middleShow() {
        if (leftNode != null)
            leftNode.middleShow();

        System.out.println(value);

        if (rightNode != null)
            rightNode.middleShow();
    }
  • 後續遍歷
    public void backShow() {
        if (leftNode != null)
            leftNode.backShow();

        if (rightNode != null)
            rightNode.backShow();

        System.out.println(value);
    }

查找

其實有了上面三種遍歷的方式, 查找自然存在三種, 一遍遍歷一遍查找

    public TreeNode frontSeach(int num) {
        TreeNode node = null;
        // 當前節點不為空,返回當前節點
        if (num == this.value) {
            return this;
        } else {
            // 查找左節點
            if (leftNode != null) {
                node = leftNode.frontSeach(num);
            }
            if (node != null)
                return node;
            // 查找右節點
            if (rightNode != null)
                node = rightNode.frontSeach(num);
        }
        return node;
    }

刪除節點

刪除節點也是, 不考慮特別複雜的情況, 刪除節點就有兩種情況, 第一種要刪除的節點就是根節點, 那麼讓根節點=null就ok, 第二種情況要刪除的節點不是根節點,就處理它的左右節點, 左右節點還不是需要刪除的元素的話那麼就得遞歸循環這個過程

   // 先判斷是否是根節點,在調用如下方法
    
   public void deleteNode(int i) {
        TreeNode parent = this;
        // 處理左樹
        if (parent.leftNode!=null&&parent.leftNode.value==i){
            parent.leftNode=null;
            return;
        }
        // 處理左樹
        if (parent.rightNode!=null&&parent.rightNode.value==i){
            parent.rightNode=null;
            return;
        }
        // 遞歸-重置父節點
        parent=leftNode;
        if (parent!=null)
            parent.deleteNode(i);
        // 遞歸-重置父節點
        parent=rightNode;
        if (parent!=null)
            parent.deleteNode(i);

    }

順序存儲二叉樹

文章一開始剛說了, 順序存儲的數據結構的典型代表就是數組, 就像這樣

[1,2,3,4,5,6,7]

什麼是順序存儲的二叉樹呢? 其實就是將上面的數組看成了一顆樹,就像下圖這樣

數組轉換成二叉樹是有規律的, 這個規律就體現在他們的 下標的關聯上, 比如我們想找2節點的左子節點的下標就是 2*n -1 = 3 , 於是我們從數組中下標為3的位置取出4來

  • 第n個元素的左子節點是 2n-1
  • 第n個元素的右子節點是 2n-2
  • 第n個元素的父節點是 (n-1)/2

  • 遍歷順序存儲的二叉樹

    public void frontShow(int start){
        if (data==null||data.length==0){
            return;
        }
        // 遍歷當前節點
        System.out.println(data[start]);
        // 遍歷左樹
        if (2*start+1<data.length)
            frontShow(2*start+1);
        // 遍歷右樹
        if (2*start+2<data.length)
            frontShow(2*start+2);
    }

線索二叉樹

假設我們有下面的二叉樹, 然後我們可以使用中序遍歷它, 中序遍歷的結果是 4,2,5,1,3,6 但是很快我們就發現了兩個問題, 啥問題呢?

  • 問題1: 雖然可以正確的遍歷出 4,2,5,1,3,6 , 但是當我們遍歷到2時, 我們是不知道2的前一個是誰的,(哪怕我們剛才遍歷到了它的前一個節點就是4)

  • 問題2: node4,5,6,3的左右節點的引用存在空閑的情況

針對這個現狀做出了改進就是線索化二叉樹, 它可以充分利用各個節點中剩餘的node這個現狀…線索化后如下圖

  • 如果這個節點的右節點為空,我們就讓它讓它指向自己的後繼節點, 例如上圖的紅線
  • 如何節點的左節點為空, 就讓這個空閑的節點指向它的前驅節點,例如上圖的藍色線

這樣的話, 就實現了任意獲取出一個節點我們都能直接的得知它的前驅節點后後繼節點到底是誰

java&中序化二叉樹;

思路: 按照原來中序遍歷樹的思路,對樹進行中序遍歷,一路遞歸到4這個節點, 檢查到它的左節點為空,就將他的左節點指向它的前驅節點, 可是4本來就是最前的節點,故4這個節點的左節點自然指向了null

然後看它的右節點也為空,於是將他的右節點指向它的後繼節點, 可是這時依然沒獲取到2節點的引用怎麼辦呢? 於是先找個變量將4節點臨時存起來, 再往後遞歸,等遞歸到2節點時,取出臨時變量的4節點, 4節點.setRightNode(2節點)

然後重複這個過程

    // 臨時保存上一個節點
    private TreeNode preNode;

    // 中序線索化二叉樹
    void threadNode(TreeNode node) {
        if (node == null)
            return;

        // 處理左邊
        threadNode(node.getLeftNode());

        // 左節點為空,說明沒有左子節點, 讓這個空出的左節點指向它的上一個節點
        if (node.getLeftNode() == null) {
            // 指向上一個節點
            node.setLeftNode(preNode);
            // 標識節點的類型
            node.setLeftType(1);
        }

        // 處理前驅節點的右指針
        // 比如現在遍歷到了1, 1的上一個節點是5, 5的右邊空着了, 於是讓5的有節點指向1
        if (preNode != null && preNode.getRightNode() == null) {
            preNode.setRightNode(node);
            preNode.setRightType(1);
        }

        // 每次遞歸調用一次這個方法就更新前驅節點
        preNode = node;
        // 處理右邊
        threadNode(node.getRightNode());
    }

遍歷二叉樹

    public void threadIterator() {
        TreeNode node = root;
        while (node != null) {
            // 循環找
            while (node.getLeftType() == 0)
                node = node.getLeftNode();
            // 打印當前節點
            System.out.println(node.getValue());
            // 如果當前的節點的右type=1說明它有指針指向自己的前一個節點
            // 比如現在位置是4, 通過下面的代碼可以讓node=2
            while (node.getRightType() == 1) {
                node = node.getRightNode();
                System.out.println(node.getValue());
            }

            // 替換遍歷的節點, 可以讓 node從2指向 5, 或者從3指向1
            node = node.getRightNode();
        }

    }

赫夫曼樹(最優二叉樹)

定義: 什麼是赫夫曼樹

赫夫曼樹又稱為最優二叉樹

定義: 在N個帶權的恭弘=叶 恭弘子節點的所組成的所有二叉樹中,如果你能找出那個帶權路徑最小的二叉樹,他就是赫夫曼樹

一說起來赫夫曼樹,其實我們可以只關心它的恭弘=叶 恭弘子節點, 權, 路徑這三個要素

  • 什麼是恭弘=叶 恭弘子節點的帶權路徑?

所謂權,其實就是節點的值, 比如上圖中node4的權是8 , node5的權是6 ,node3的權是1, 而且我們只關心恭弘=叶 恭弘子節點的權

啥是帶權路徑呢? 比如上圖中 node4的帶權路徑是 1-2-4

  • 樹的帶權路徑長度(weight path length) 簡稱 WPL

其實就是這個樹所有的恭弘=叶 恭弘子節點的帶權路徑長度之和,

計算左樹的WPL =2*8+2*6+1*1 = 29

計算左樹的WPL =2*1+2*6+1*8 = 22

總結: 權值越大的節點,離根節點越近的節點是最優二叉樹

### 實戰: 將數組轉換為赫夫曼樹

  • 思路:

假設我們現在已經有了數組 [3,5,7,8,11,14,23,29], 如何將這個數組轉換成赫夫曼樹呢?

取出這裏最小的node3 和 倒數第二小的node5 ,構建成新的樹, 新樹的根節點是 node3,5的權值之和, 將構建完成的樹放回到原數組中

重複這個過程, 將最小的node7,node8取出,構建新樹, 同樣新樹的權重就是 node7,8的權重之和, 再將構建完成的樹放回到原數組中

如此往複,最終得到的樹就是huffman樹

  • java實現:

封裝TreeNode, 看上面的過程可以看到,需要比較權重的大小,因此重寫它的compareTo方法

public class TreeNode implements Comparable{
    // 權
    private int value;
    private TreeNode leftNode;
    private TreeNode rightNode;

    @Override
    public int compareTo(Object o) {
        TreeNode node = (TreeNode) o;
        return this.value-node.value;
    }

構建赫夫曼樹, 思路就是上圖的過程, 將數組中的各個元素轉換成Node. 然後存放在List容器中,每輪構建新樹時需要排序, 當集合中僅剩下一個節點,也就是根節點時完成樹的構建

    // 創建赫夫曼樹
    private static TreeNode buildHuffmanTree(int[] arr) {
        // 創建一個集合,存放將arr轉換成的二叉樹
        ArrayList<TreeNode> list = new ArrayList<>();
        for (int i : arr) {
            list.add(new TreeNode(i));
        }
        // 開始循環, 當集合中只剩下一棵樹時
        while (list.size() > 1) {
            // 排序
            Collections.sort(list);
            // 取出權值最小的數
            TreeNode leftNode = list.get(list.size() - 1);
            // 取出權值次要小的數
            TreeNode rightNode = list.get(list.size() - 2);
            // 移除取出的兩棵樹
            list.remove(leftNode);
            list.remove(rightNode);

            // 創建新的樹根節點
            TreeNode parentNode = new TreeNode(leftNode.getValue() + rightNode.getValue(), leftNode, rightNode);
            // 將新樹放到原樹的集合中
            list.add(parentNode);
        }
        return list.get(0);
    }

實戰: 赫夫曼樹與數據壓縮

通過上面的介紹我們能直觀的看出來,赫夫曼樹很顯眼的特徵就是它是各個節點能組成的樹中,那顆WPL,帶權路徑長度最短的樹, 利用這條性質常用在數據壓縮領域, 即我們將現有的數據構建成一個赫夫曼樹, 其中出現次數越多的字符,就越靠近根節點, 經過這樣的處理, 就能用最短的方式表示出原有字符

假設我們有這條消息can you can a can as a canner can a can.

數據對計算機來說不過是0-1這樣的数字, 我們看看將上面的字符轉換成01這樣的二進制數它長什麼樣子

1. 將原字符串的每一個char強轉換成 byte == ASCII
99 97 110 32 121 111 117 32 99 97 110 32 97 32 99 97 110 32 97 115 32 97 32 99 97 110 110 101 114 32 99 97 110 32 97 32 99 97 110
    
2. 將byte toBinaryString 轉換成01串如下:
1100011110000111011101000001111001110111111101011
0000011000111100001110111010000011000011000001100
0111100001110111010000011000011110011100000110000
1100000110001111000011101110100000110001111000011
1011101101110110010111100101000001100011110000111
011101000001100001100000110001111000011101110101110

也就是說,如果我們不對其進行壓縮時, 它將會轉換成上面那一大坨在網絡上進行傳輸

使用赫夫曼進行編碼:

思路: 我們將can you can a can as a canner can a can. 中的每一個符號,包括 點 空格,全部封裝進TreeNode

TreeNode中屬性如下: 包含權重: 也就是字符出現的次數, 包含data: 字符本身

public class TreeNode implements Comparable{
    // 存放權重就是字符出現的次數
    private int weight;
    // 存放英文數值
    private Byte data; //
    private TreeNode leftNode;
    private TreeNode rightNode;

封裝完成后, 按照權重的大小倒序排序,各個節點長成這樣:

a:11  :11   n:8   c:7   o:1  .:1  y:1   e:1  u:1  s:1  r:1  

將赫夫曼樹畫出來長這樣:

特徵,我們讓左側的路徑上的值是0, 右邊是1. 因此通過這個赫夫曼樹其實我們可以得到一張赫夫曼編碼錶,

比如像下面這樣:

n: 00
 : 01
a: 10
c: 111
// 每一個字符的編碼就是從根節點到它的路徑

有了這樣編碼錶, 下一步就是對數據進行編碼, 怎麼編碼呢? 不就是做一下替換嗎? 我們現在開始循環遍歷一開始的字符串, 挨個取出裏面的字符, 比如我們取出第一個字符是c, 拿着c來查詢這個表發現,c的編碼是111,於是我們將c替換成111, 遍歷到第二個字符是a, 拿着a查詢表,發現a的值是10, 於是我們將a替換成10, 重複這個過程, 最終我們得到的01串明顯比原來短很多

怎麼完成解碼呢? 解碼也不複雜, 前提也是我們得獲取到huffman編碼錶, 使用前綴匹配法, 比如我們現在接收到了

1111000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

使用前綴就是先取出1 去查查編碼錶有沒有這個數? 有的話就返回對應的字符, 沒有的話就用11再去匹配

大家可以看看上面的那顆霍夫曼樹, 所有的data都在恭弘=叶 恭弘子節點上,所以使用前綴匹配完全可以,絕對不會出現重複的情況

  • 使用java實現這個過程

思路概覽:

  1. 將原生的字節數組轉化成一個個的TreeNode
  2. 取出所有的TreeNode封裝成赫夫曼樹
  3. 通過赫夫曼樹踢去出赫夫曼編碼錶
  4. 使用這個編碼錶進行編碼
  5. 解碼

  private static byte[] huffmanZip(byte[] bytes) {
        // 先統計每個byte出現的次數,放入集合中
        List<TreeNode> treeNodes = buildNodes(bytes);
        // 創建赫夫曼樹
        TreeNode node = createHuffmanTree(treeNodes);
        // 創建huffman編碼錶
        Map<Byte, String> codes = createHuffmanCodeTable(node);
        // 編碼, 將每一個byte替換成huffman編碼錶中的V
        byte[] encodeBytes = encodeHuffmanByte(bytes, codes);
        
        // 使用huffman編碼進行解碼
        byte[] decodeBytes = decode(encodeBytes);
        return decodeBytes;
    }

將原生的byte數組,封裝成一個個的TreeNode節點,保存在一個容器中,並且記錄下這個節點出現的次數, 因此我們需要將出現次數多的節點靠近根節點

    /**
     * 將byte轉換成node集合
     *
     * @param bytes
     * @return
     */
    private static List<TreeNode> buildNodes(byte[] bytes) {
        ArrayList<TreeNode> list = new ArrayList<>();
        HashMap<Byte, Integer> countMap = new HashMap<>();
        // 統計每一個節點的出現的次數
        for (byte aByte : bytes) {
            Integer integer = countMap.get(aByte);
            if (integer == null) {
                countMap.put(aByte, 1);
            } else {
                countMap.put(aByte, integer + 1);
            }
        }
        // 將k-v轉化成node
        countMap.forEach((k, v) -> {
            list.add(new TreeNode(v, k));
        });
        return list;
    }

構建赫夫曼樹

  /**
     * 創建huffman樹
     *
     * @param treeNodes
     * @return
     */
    private static TreeNode createHuffmanTree(List<TreeNode> treeNodes) {
        // 開始循環, 當集合中只剩下一棵樹時
        while (treeNodes.size() > 1) {
            // 排序
            Collections.sort(treeNodes);
            // 取出權值最小的數
            TreeNode leftNode = treeNodes.get(treeNodes.size() - 1);
            // 取出權值次要小的數
            TreeNode rightNode = treeNodes.get(treeNodes.size() - 2);
            // 移除取出的兩棵樹
            treeNodes.remove(leftNode);
            treeNodes.remove(rightNode);

            // 創建新的樹根節點
            TreeNode parentNode = new TreeNode(leftNode.getWeight() + rightNode.getWeight(), leftNode, rightNode);
            // 將新樹放到原樹的集合中
            treeNodes.add(parentNode);
        }
        return treeNodes.get(0);
    }

從赫夫曼樹中提取出編碼錶, 思路: 下面是完了個遞歸, 我們規定好左樹是0,右邊是1, 通過一個SpringBuilder, 每次迭代都記錄下原來走過的路徑,當判斷到它的data不為空時,說明他就是恭弘=叶 恭弘子節點,立即保存這個節點曾經走過的路徑,保存在哪裡呢? 保存在一個map中, Key就是byte value就是走過的路徑

  static StringBuilder stringBuilder = new StringBuilder();
  static Map<Byte, String> huffCode = new HashMap<>();

    /**
     * 創建huffman便編碼錶
     *
     * @param node
     * @return
     */
    private static Map<Byte, String> createHuffmanCodeTable(TreeNode node) {
        if (node == null)
            return null;
        getCodes(node.getLeftNode(), "0", stringBuilder);
        getCodes(node.getRightNode(), "1", stringBuilder);
        return huffCode;
    }

    /**
     * 根據node, 獲取編碼
     *
     * @param node
     * @param code
     * @param stringBuilder
     */
    private static void getCodes(TreeNode node, String code, StringBuilder stringBuilder) {
        StringBuilder sb = new StringBuilder(stringBuilder);
        sb.append(code);
        // 如果節點的data為空,說明根本不是恭弘=叶 恭弘子節點,接着遞歸
        if (node.getData() == null) {
            getCodes(node.getLeftNode(), "0", sb);
            getCodes(node.getRightNode(), "1", sb);
        } else {
            // 如果是恭弘=叶 恭弘子節點,就記錄它的data和路徑
            huffCode.put(node.getData(), sb.toString());
        }
    }

根據赫夫曼編碼錶進行編碼:

思路:

舉個例子: 比如,原byte數組中的一個需要編碼的字節是a

a的ASCII==97

97正常轉成二進制的01串就是 0110 0001

但是現在我們有了編碼錶,就能根據97從編碼錶中取出編碼: 10

換句話說,上面 0110 0001 和 10 地位相同

若干個需要編碼的數append在一起,於是我們就有了一個比原來短一些的01串, 但是問題來了,到這裏就結束了嗎? 我們是將這些01串轉換成String, 在getBytes()返回出去嗎? 其實不是的,因為我們還需要進行解碼,你想想解碼不得編碼map中往外取值? 取值不得有key? 我們如果在這裏將這個01串的byte數組直接返回出去了,再按照什麼樣的方式將這個byte[]轉換成String串呢? ,因為我們要從這個String串中解析出key

然後這裏我們進行約定, 將現在得到的01串按照每8位為一組轉換成int數, 再將這個int強轉成byte, 解碼的時候我們就知道了.就按照8位一組進行解碼. 解析出來數組再轉換成01串,我們就重新拿到了這個編碼后的01串,它是個String串

每遇到8個0或者1,就將它強轉成Int, 再強轉成type, 經過這樣的轉換可能會出現負數,因此01串的最前面有個符號位,1表示負數

比如說: 如果你打印一下面代碼中的encodeByte,你會發現打印的第一個數是-23, 這個-23被保存在新創建的byte數組的第一個位置上, 後續解碼時,就從這個byte數組中的第一個位置上獲取出這個-23, 將它轉換成01二進制串

怎麼轉換呢? 比如不是-23, 而是-1
真值 1
原碼:1,0001
補碼: 2^(4+1) +1 = 100000 + (-1) = 1,1111
我們獲取到的結果就是1111
 /**
     * 進行編碼
     *
     * @param bytes
     * @param codes
     * @return
     */
    private static byte[] encodeHuffmanByte(byte[] bytes, Map<Byte, String> codes) {
        StringBuilder builder = new StringBuilder();
        for (byte aByte : bytes) {
            builder.append(codes.get(aByte));
        }

        // 將這些byte按照每8位一組進行編碼
        int length = 0;
        if (builder.length() % 8 == 0) {
            length = builder.length() / 8;
        } else {
            length = builder.length() / 8 + 1;
        }
        // 用於存儲壓縮后的byte
        byte[] resultByte = new byte[length];
        // 記錄新byte的位置
        int index = 0;
        // 遍歷新得到的串
        for (int i = 0; i < builder.length(); i += 8) {
            String str = null;
            if (i + 8 > builder.length()) {
                str = builder.substring(i);
            } else {
                str = builder.substring(i, i + 8);
            }
            // 將八位的二進制轉換成byte
            // 這裏出現負數了....  涉及到補碼的問題
            byte encodeByte = (byte) Integer.parseInt(str, 2);
            // 存儲起來
            resultByte[index] = encodeByte;
            index++;
        }
        return resultByte;
    }

解碼: 前面我們知道了,約定是按照8位轉換成的int 再轉換成type[] , 現在按照這個約定,反向轉換出我們一開始的01串

/**
     * 按照指定的赫夫曼編碼錶進行解碼
     *
     * @param encodeBytes
     * @return
     */
    private static byte[] decode(byte[] encodeBytes) {
        List<Byte> list = new ArrayList();
        StringBuilder builder = new StringBuilder();
        for (byte encodeByte : encodeBytes) {
            // 判斷是否是最後一個,如果是最後一次不用用0補全, 因此最後一位本來就不夠8位
            boolean flag = encodeByte == encodeBytes[encodeBytes.length - 1];
            String s = byteToBitStr(!flag, encodeByte);
            builder.append(s);
        }
        // 調換編碼錶的k-v
        Map<String, Byte> map = new HashMap<>();
        huffCode.forEach((k, v) -> {
            map.put(v, k);
        });
        // 處理字符串
        for (int i = 0; i < builder.length(); ) {
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag){
                String key = builder.substring(i,i+count);
                b=map.get(key);
                if (b==null){
                    count++;
                }else {
                    flag=false;
                }
            }
            list.add(b);
            i+=count;
        }

        // 將list轉數組
        byte[] bytes = new byte[list.size()];
        int i=0;
        for (Byte aByte : list) {
            bytes[i]=aByte;
            i++;
        }
        return bytes;
    }

    /**
     * 將byte轉換成二進制的String
     *
     * @param b
     * @return
     */
    public static String byteToBitStr(boolean flag, byte b) {
        /**
         * 目標: 全部保留八位.正數前面就補零, 負數前面補1
         * 為什麼選256呢?  因為我們前面約定好了, 按照8位進行分隔的
         * 256的二進製表示是  1 0000 0000
         * 假設我們現在是 1
         * 計算              1 0000 0000
         *               或  0 0000 0001
         *              ----------------------
         *                   1 0000 0001
         *                   結果截取8位就是 0000 0001
         *
         * 假設我們現在是   -1
         * 轉換成二進制:    1111 1111 1111 1111 1111 1111 1111 1111
         *
         * 計算                            1 0000 0000
         * 或  1111 1111 1111 1111 1111 1111 1111 1111
         *              ----------------------
         *                        1 1111 1111
         *                   結果截取8位就是 1111 1111
         *
         *
         */
        int temp = b;
        if (flag) {
            temp |= 256;
        }
        String str = Integer.toBinaryString(temp);
        if (flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }

    }

二叉排序樹

二叉排序樹, 又叫二叉搜索樹 , BST (Binary Search Tree)

  • 線性存儲和鏈式存儲的優缺點

比如我們有一個數組 [7,3,10,12,5,1,9]

雖然我們可以直接取出下標為幾的元素,但是卻不能直接取出值為幾的元素, 比如,我們如果想取出值為9的元素的話,就得先去遍歷這個數組, 然後挨個看看當前位置的數是不是9 , 就這個例子來說我們得找7次

假設我們手裡的數組已經是一個有序數組了 [1,3,5,7,9,11,12]

我們可以通過二分法快速的查找到想要的元素,但是對它依然是數組,如果想往第一個位置上插入元素還是需要把從第一個位置開始的元素,依次往後挪. 才能空出第一個位置,把新值放進去

假設我們將這一行數轉換成鏈式存儲, 確實添加, 刪除變的異常方便, 但是查找還是慢, 不管是查詢誰, 都得從第一個開始往後遍歷

  • 我們的主角: 二叉搜索樹

二叉排序樹有如下的特點:

  • 對於二叉排序樹中的任意一個非恭弘=叶 恭弘子節點都要求他的左節點小於自己, 右節點大於自己
  • 空樹也是二叉排序樹

將上面的無序的數組轉換成二叉排序樹長成下圖這樣

如果我們按照中序遍歷的話結果是: 1 3 5 7 9 11 12 , 正好是從小到大完成排序

再看他的特徵: 如果我們想查找12 , 很簡單 7-10-12 , 如果我們想插入也很簡單,它有鏈表的特性

java&二叉排序樹

封裝Node和Tree

// tree
public class BinarySortTree {
    Node root;
}

// node
public class Node {
    private int value;
    private Node leftNode;
    private Node rightNode;
}

構建一顆二叉排序樹, 思路是啥呢? 如果沒有根節點的話,直接返回,如果存在根節點, 就調用根節點的方法,將新的node添加到根節點上, 假設我們現在遍歷到的節點是NodeA. 新添加的節點是NodeB, 既然想添加就得比較一下NodeA和NodeB的值的大小, 將如果NodeB的值小於NodeA,就添加在NodeA的右邊, 反之就添加在NodeA的左邊

-----------BinarySortTree.class--------------- 
/**
     * 向二叉排序樹中添加節點
     */
    public void add(Node node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

-------------Node.class------------
/**
     * 添加節點
     *
     * @param node
     */
    public void add(Node node) {
        if (node == null)
            return;
        //判斷需要添加的節點的值比傳遞進來的節點的值大還是小
        // 添加的節點小於當前節點的值
        if (node.value < this.value) {
            if (this.leftNode == null) {
                this.leftNode = node;
            } else {
                this.leftNode.add(node);
            }
        } else {
            if (this.rightNode == null) {
                this.rightNode = node;
            } else {
                this.rightNode.add(node);
            }
        }
    }

刪除一個節點

刪除一節點如如下幾種情況, 但是無論是哪種情況,我們都的保存當前節點的父節點, 通過他的父節點對應節點=null實現節點的刪除

情況1: 如圖

這是最好處理的情況, 就是說需要刪除的元素就是單個的子節點

情況2: 如圖

這種情況也不麻煩,我們讓當前比如我們想上刪除上圖中的3號節點, 我們首先保存下node3的父節點 node7, 刪除node3時發現node3有一個子節點,於是我們讓 node7 的 leftNode = node3

情況3: 如圖

比如我們想刪除7, 但是7這個節點還有一個子樹 按照中序遍歷這個樹的順序是 1,3,5,7,9,11,13, 想刪除7的話,其實

  1. 臨時存儲node9
  2. 刪除node9
  3. 用臨時存儲的node9替換node7

如果node9還有右節點怎麼辦呢?

  1. 臨時保存node9
  2. 刪除node9
  3. 讓node9的右節點替換node9
  4. 讓臨時存儲的node9替換node7
/**
     * 刪除一個節點
     *
     * @param value
     * @return
     */
    public void delete(int value) {
        if (root == null) {
            return;
        } else {
            // 找到這個節點
            Node node = midleSearch(value);
            if (node == null)
                return;
            // 找到他的父節點
            Node parentNode = searchParent(value);

            // todo 當前節點是恭弘=叶 恭弘子節點
            if (node.getLeftNode() == null && node.getRightNode() == null) {
                if (parentNode.getLeftNode().getValue() == value) {
                    parentNode.setLeftNode(null);
                } else {
                    parentNode.setRightNode(null);
                }
                // todo 要刪除的節點存在兩個子節點
            } else if (node.getLeftNode() != null && node.getRightNode() != null) {
                // 假設就是刪除7
                //1. 找到右子樹中最小的節點,保存它的值,然後刪除他
                int minValue = deleteMin(node.getRightNode());
                //2.替換被刪除的節點值
                node.setValue(minValue);

            } else { // todo 要刪除的節點有一個左子節點或者是右子節點
                // 左邊有節點
                if (node.getLeftNode() != null) {
                    // 要刪除的節點是父節點的左節點
                    if (parentNode.getLeftNode().getValue() == value) {
                        parentNode.setLeftNode(node.getLeftNode());
                    } else {// 要刪除的節點是父節點的右節點
                        parentNode.setRightNode(node.getLeftNode());
                    }
                } else { // 右邊有節點
                    // 要刪除的節點是父節點的右節點
                    if (parentNode.getLeftNode().getValue() == value) {
                        parentNode.setLeftNode(node.getRightNode());
                    } else {// 要刪除的節點是父節點的右節點
                        parentNode.setRightNode(node.getRightNode());
                    }
                }
            }
        }
    }

 /**
     * 刪除並保存以當前點為根節點的樹的最小值節點
     * @param node
     * @return
     */
    private int deleteMin(Node node) {
        // 情況1: 值最小的節點沒有右節點
        // 情況2: 值最小的節點存在右節點
        // 但是下面我們使用delete,原來考慮到了
        while(node.getLeftNode()!=null){
            node=node.getLeftNode();
        }
        delete(node.getValue());
        return node.getValue();
    }

    /**
     * 搜索父節點
     *
     * @param value
     * @return
     */
    public Node searchParent(int value) {
        if (root == null) {
            return null;
        } else {
            return root.searchParent(value);
        }
    }

缺點

二叉排序樹其實對節點權是有要求的, 比如我們的數組就是[1,2,3,4] 那麼畫成平衡二叉樹的話長下面這樣

它不僅沒有二叉排序樹的優點,而且還不如單鏈表的速度快

AVL樹(平衡二叉樹)

定義: 什麼是平衡二叉樹

平衡二叉樹的出現就是為了 解決上面二叉排序樹[1,2,3,4,5,6]這樣成單條鏈的略勢的情況,它要求,每個樹的左子樹和右子樹的高度之差不超過1, 如果不滿足這種情況了,馬上馬對各個節點進行調整,這樣做保證了二叉排序樹的優勢

如何調整

  • 情況1: 對於node1來說, 它的左邊深度0 , 右邊的深度2 , 於是我們將它調整成右邊的樣子
  • 情況2: 在1234的情況下, 添加node5,導致node2不平衡, 進行如下的調整
  • 情況3: 在12345的基礎上添加node6,導致node4不平衡, 對node4進行調整, 其實就和情況1相同了
  • 情況4: 在1234567的情況下,進行添加8. 打破了node5的平衡, 因此進行旋轉

一個通用的旋轉規律

看這個典型的有旋轉的例子

node4的出現,使用node8的平衡被打破, 因此我們需要進行調整, 按照下面的步驟進行調整

下面說的this是根節點node8, 按照下面的步驟在紙上畫一畫就ok

  1. 創建新node, 使新node.value = this.value
  2. 新節點的rightNode = this.rightNode
  3. 新節點的leftNode = this.leftNode.rightNode
  4. this.value = this.LeftNode.value
  5. this.leftNode = this.leftNode .leftNode
  6. this.leftNode = 新創建的node

需要注意的情況:

新添加6使得node8不再平衡,但是如果你按照上面的步驟進行旋轉的話,會得到右邊的結果, 但是右邊的結果中對於node4還是不平衡的,因此需要預處理一下

再進行右旋轉時,提前進行檢驗一下,當前節點的左子樹是否存在右邊比左邊高的情況, 如果右邊比較高的話,就先將這個子樹往左旋轉, 再以node8為根,整體往右旋轉

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

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

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

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

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

小三通物流營運型態?

※快速運回,大陸空運推薦?

Prometheus監控有所思:多標籤埋點及Mbeana

  使用 grafana+prometheus+jmx 作為普通的監控手段,是比較有用的。我之前的文章介紹了相應的實現辦法。

  但是,按照之前的實現,我們更多的只能是監控 單值型的數據,如請求量,tps 等等,對於複雜組合型的指標卻不容易監控。

  這種情況一般帶有一定的業務屬性,比如想監控mq中的每個topic的消費情況,每類產品的實時訂單情況等等。當然,對於看過完整的 prometheus 的監控數據的同學來說,會覺得很正常,因為你會看到如下的數據:

# HELP java_lang_MemoryPool_PeakUsage_max java.lang.management.MemoryUsage (java.lang<type=MemoryPool, name=Metaspace><PeakUsage>max)
# TYPE java_lang_MemoryPool_PeakUsage_max untyped
java_lang_MemoryPool_PeakUsage_max{name="Metaspace",} -1.0
java_lang_MemoryPool_PeakUsage_max{name="PS Old Gen",} 1.415053312E9
java_lang_MemoryPool_PeakUsage_max{name="PS Eden Space",} 6.96778752E8
java_lang_MemoryPool_PeakUsage_max{name="Code Cache",} 2.5165824E8
java_lang_MemoryPool_PeakUsage_max{name="Compressed Class Space",} 1.073741824E9
java_lang_MemoryPool_PeakUsage_max{name="PS Survivor Space",} 5242880.0

  這裏面的 name 就是普通標籤嘛,同理於其他埋點咯。應該是可以實現的。

  是的,prometheus 是方便實現這玩意的,但是我們之前不是使用 jmx_exportor 作為導出工具嘛,使用的埋點組件是 io.dropwizard.metrics:metrics-core 。

  而它則是重在單值的監控,所以,用它我們是實現不了帶指標的數據的監控了。

  那怎麼辦呢?三個辦法!

1. 直接替換原有的 metrics-core 組件為 prometheus 的client 組件,因為官方是支持這種操作的;
2. 使用 prometheus-client 組件與 metrics-core 組件配合,各自使用各自的功能;
3. 自行實現帶標籤的埋點,這可能是基於 MBean 的;

 

  以上這幾種方案,各有優劣。方案1可能改動太大,而且可能功能不兼容不可行; 方案2可能存在整合不了或者功能衝突情況,當然如果能整合,絕對是最好的; 方案3實現複雜度就高了,比如監控值維護、線程安全、MBean數據吐出方式等等。

  好吧,不管怎麼樣,我們還是都看看吧。

 

一、 使用 prometheus-client 埋點實現帶標籤的監控

  1. 引入 pom 依賴

        <dependency>
            <groupId>io.prometheus</groupId>
            <artifactId>simpleclient</artifactId>
            <version>0.8.0</version>
        </dependency>
        <dependency>
                <groupId>io.prometheus</groupId>
                <artifactId>simpleclient_hotspot</artifactId>
                <version>0.8.0</version>
        </dependency>
        <dependency>
                <groupId>io.prometheus</groupId>
                <artifactId>simpleclient_servlet</artifactId>
                <version>0.8.0</version>
        </dependency>

  2. 框架註冊監控

        @Configuration
        public class PrometheusConfig {
            @Bean
            public ServletRegistrationBean servletRegistrationBean(){
                // 將埋點指標吐出到 /metrics 節點
                return new ServletRegistrationBean(new MetricsServlet(), "/metrics");
            }
        }

  3. 業務埋點數據

        // 註冊指標實例
        io.prometheus.client.Counter c = io.prometheus.client.Counter.build()
                .name("jmx_test_abc_ffff")
                .labelNames("topic")
                .help("topic counter usage.")
                .register();
        public void incTopicMetric(String topic) {
            // c.labels("test").inc();  // for test
        }

  4. 獲取埋點數據信息

        curl http://localhost:8080/metrics
        # 對外暴露http接口調用,結果如下
        # HELP jmx_test_abc_ffff counter usage.
        # TYPE jmx_test_abc_ffff counter
        jmx_test_abc_ffff{topic="bbb",} 1.0
        jmx_test_abc_ffff{topic="2",} 2.0
        jmx_test_abc_ffff{topic="test",} 1.0

  可以看出,效果咱們是實現了。但是,對於已經運行的東西,要改這玩意可能不是那麼友好。主要有以下幾點:

    1. 暴露數據方式變更,原來由javaagent進行統一處理的數據,現在可能由於應用端口的不一,導致收集的配置會變更,不一定符合運維場景;
    2. 需要將原來的埋點進行替換;

 

二、 prometheus-client 與 metrics-core 混合埋點

  不處理以前的監控,將新監控帶標籤數據吐入到 jmx_exportor 中。

  我們試着使用如上的埋點方式:

        // 註冊指標實例
        io.prometheus.client.Counter c = io.prometheus.client.Counter.build()
                .name("jmx_test_abc_ffff")
                .labelNames("topic")
                .help("topic counter usage.")
                .register();
        public void incTopicMetric(String topic) {
            // c.labels("test").inc();  // for test
        }

  好像數據是不會進入的到 jmx_exportor 的。這也不奇怪,畢竟咱們也不了解其原理,難道想靠運氣取勝??

  細去查看 metrics-core 組件的埋點實現方案,發現其是向 MBean 中吐入數據,從而被 jmx_exportor 抓取的。

        // com.codahale.metrics.jmx.JmxReporter.JmxListener#onCounterAdded
        @Override
        public void onCounterAdded(String name, Counter counter) {
            try {
                if (filter.matches(name, counter)) {
                    final ObjectName objectName = createName("counters", name);
                    registerMBean(new JmxCounter(counter, objectName), objectName);
                }
            } catch (InstanceAlreadyExistsException e) {
                LOGGER.debug("Unable to register counter", e);
            } catch (JMException e) {
                LOGGER.warn("Unable to register counter", e);
            }
        }
        // 向 mBeanServer 註冊監控實例
        // 默認情況下 mBeanServer = ManagementFactory.getPlatformMBeanServer();
        private void registerMBean(Object mBean, ObjectName objectName) throws InstanceAlreadyExistsException, JMException {
            ObjectInstance objectInstance = mBeanServer.registerMBean(mBean, objectName);
            if (objectInstance != null) {
                // the websphere mbeanserver rewrites the objectname to include
                // cell, node & server info
                // make sure we capture the new objectName for unregistration
                registered.put(objectName, objectInstance.getObjectName());
            } else {
                registered.put(objectName, objectName);
            }
        }

  而 prometheus-client 則是通過 CollectorRegistry.defaultRegistry 進行註冊實例的。

    // io.prometheus.client.SimpleCollector.Builder#register()
    /**
     * Create and register the Collector with the default registry.
     */
    public C register() {
      return register(CollectorRegistry.defaultRegistry);
    }
    /**
     * Create and register the Collector with the given registry.
     */
    public C register(CollectorRegistry registry) {
      C sc = create();
      registry.register(sc);
      return sc;
    }

  所以,好像原理上來講是不同的。至於到底為什麼不能監控到數據,那還不好說。至少,你可以學習 metrics-core 使用 MBean 的形式將數據導出。這是我們下一個方案要討論的事。

  這裏我可以給到一個最終簡單又不失巧合的方式,實現兩個監控組件的兼容,同時向 jmx_exportor 進行導出。如下:

  1. 引入 javaagent 依賴包

        <!-- javaagent 包,與 外部使用的 jmx_exportor 一致 -->
        <dependency>
            <groupId>io.prometheus.jmx</groupId>
            <artifactId>jmx_prometheus_javaagent</artifactId>
            <version>0.12.0</version>
        </dependency>

  2. 使用 agent 的工具類進行埋點

  因為 javaagent 裏面提供一套完整的 client 工具包,所以,我們可以使用。

        // 註冊指標實例
        // 將 io.prometheus.client.Counter 包替換為 io.prometheus.jmx.shaded.io.prometheus.client.Counter
        io.prometheus.client.Counter c = io.prometheus.client.Counter.build()
                .name("jmx_test_abc_ffff")
                .labelNames("topic")
                .help("topic counter usage.")
                .register();
        public void incTopicMetric(String topic) {
            // c.labels("test").inc();  // for test
        }

  3. 原樣使用 jmx_exportor 就可以導出監控數據了

  為什麼換一個包這樣就可以了?

  因為 jmx_exportor 也是通過註冊 CollectorRegistry.defaultRegistry 來進行收集數據的,我們只要保持與其實例一致,就可以做到在同一個jvm內共享數據了。

 

三、 基於 MBean自行實現帶標籤的埋點

// 測試類
public class PrometheusMbeanMetricsMain {
    private static ConcurrentHashMap<String, AtomicInteger> topicContainer = new ConcurrentHashMap<>();
    private static MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();

    public static void main(String[] args) throws Exception {
        // 模擬某個topic
        String commingTopic = "test_topic";
        AtomicInteger myTopic1Counter = getMetricCounter(commingTopic);
        System.out.println("jmx started!");
        while(true){
            System.out.println("---");
            // 計數增加
            myTopic1Counter.incrementAndGet();
            Thread.sleep(10000);
        }
    }

    private static AtomicInteger getMetricCounter(String topic) throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
        AtomicInteger myTopic1Counter = topicContainer.get(topic);
        if(myTopic1Counter == null) {
            myTopic1Counter = new AtomicInteger(0);
            Hashtable<String, String> tab = new Hashtable<>();
            tab.put("topic", topic);
            // 佔位符,雖然不知道什麼意思,但是感覺很厲害的樣子
            tab.put("_", "_value");
            ObjectName objectName = new ObjectName("mydomain_test", tab);
            // 註冊監控實例 到 MBeanServer 中
            ObjectInstance objectInstance = mBeanServer.registerMBean(new JmxCounter(myTopic1Counter, objectName), objectName);
        }
        return myTopic1Counter;
    }
}
// JmxCounter, MBean 要求: 1. 接口必須定義成Public的;  2. 接口命名規範符合要求, 即接口名叫 XYZMBean ,那麼實現名就必須一定是XYZ;
// DynamicMBean
public interface JmxCounterMBean {
    public Object getCount() throws Exception;
}
public class JmxCounter implements JmxCounterMBean {
    private AtomicInteger metric;
    private ObjectName objectName;

    public JmxCounter(AtomicInteger metric, ObjectName objectName) {
        this.objectName = objectName;
        this.metric = metric;
    }

    @Override
    public Object getCount() throws Exception {
        // 返回監控結果
        return metric.get();
    }

}

  最後,見證奇迹的時刻。結果如下:

# HELP mydomain_test_value_Count Attribute exposed for management (mydomain_test<_=_value, topic=b_topic><>Count)
# TYPE mydomain_test_value_Count untyped
mydomain_test_value_Count{topic="b_topic",} 1.0
mydomain_test_value_Count{topic="a_topic",} 88.0

  很明顯,這是一個糟糕的實現,不要學他。僅為了演示效果。

  所以,總結下來,自然是使用方案2了。兩個組件兼容,實現簡單,性能也不錯。如果只是為了使用,到此就可以了。不過你得明白,以上方案有取巧的成分在。

 

四、 原理: jmx_exportor 是如何獲取數據的?

  jmx_exportor 也是可以通過 http_server 暴露數據。

    // io.prometheus.client.exporter.HTTPServer
    /**
     * Start a HTTP server serving Prometheus metrics from the given registry.
     */
    public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException {
        server = HttpServer.create();
        server.bind(addr, 3);
        // 使用 HTTPMetricHandler 處理請求
        HttpHandler mHandler = new HTTPMetricHandler(registry);
        // 綁定到 /metrics 地址上
        server.createContext("/", mHandler);
        server.createContext("/metrics", mHandler);
        executorService = Executors.newFixedThreadPool(5, DaemonThreadFactory.defaultThreadFactory(daemon));
        server.setExecutor(executorService);
        start(daemon);
    }    
    /**
     * Start a HTTP server by making sure that its background thread inherit proper daemon flag.
     */
    private void start(boolean daemon) {
        if (daemon == Thread.currentThread().isDaemon()) {
            server.start();
        } else {
            FutureTask<Void> startTask = new FutureTask<Void>(new Runnable() {
                @Override
                public void run() {
                    server.start();
                }
            }, null);
            DaemonThreadFactory.defaultThreadFactory(daemon).newThread(startTask).start();
            try {
                startTask.get();
            } catch (ExecutionException e) {
                throw new RuntimeException("Unexpected exception on starting HTTPSever", e);
            } catch (InterruptedException e) {
                // This is possible only if the current tread has been interrupted,
                // but in real use cases this should not happen.
                // In any case, there is nothing to do, except to propagate interrupted flag.
                Thread.currentThread().interrupt();
            }
        }
    }

  所以,可以主要邏輯是 HTTPMetricHandler 處理。來看看。

        // io.prometheus.client.exporter.HTTPServer.HTTPMetricHandler#handle
        public void handle(HttpExchange t) throws IOException {
            String query = t.getRequestURI().getRawQuery();

            ByteArrayOutputStream response = this.response.get();
            response.reset();
            OutputStreamWriter osw = new OutputStreamWriter(response);
            // 主要由該 TextFormat 進行格式化輸出
            // registry.filteredMetricFamilySamples() 進行數據收集
            TextFormat.write004(osw,
                    registry.filteredMetricFamilySamples(parseQuery(query)));
            osw.flush();
            osw.close();
            response.flush();
            response.close();

            t.getResponseHeaders().set("Content-Type",
                    TextFormat.CONTENT_TYPE_004);
            if (shouldUseCompression(t)) {
                t.getResponseHeaders().set("Content-Encoding", "gzip");
                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
                final GZIPOutputStream os = new GZIPOutputStream(t.getResponseBody());
                response.writeTo(os);
                os.close();
            } else {
                t.getResponseHeaders().set("Content-Length",
                        String.valueOf(response.size()));
                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.size());
                // 寫向客戶端
                response.writeTo(t.getResponseBody());
            }
            t.close();
        }

    }

 

五、 原理: jmx_exportor 是如何獲取Mbean 的數據的?

  jmx_exportor 有一個 JmxScraper, 專門用於處理 MBean 的值。

    // io.prometheus.jmx.JmxScraper#doScrape
    /**
      * Get a list of mbeans on host_port and scrape their values.
      *
      * Values are passed to the receiver in a single thread.
      */
    public void doScrape() throws Exception {
        MBeanServerConnection beanConn;
        JMXConnector jmxc = null;
        // 默認直接獲取本地的 jmx 信息
        // 即是通過共享 ManagementFactory.getPlatformMBeanServer() 變量來實現通信的
        if (jmxUrl.isEmpty()) {
          beanConn = ManagementFactory.getPlatformMBeanServer();
        } else {
          Map<String, Object> environment = new HashMap<String, Object>();
          if (username != null && username.length() != 0 && password != null && password.length() != 0) {
            String[] credent = new String[] {username, password};
            environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credent);
          }
          if (ssl) {
              environment.put(Context.SECURITY_PROTOCOL, "ssl");
              SslRMIClientSocketFactory clientSocketFactory = new SslRMIClientSocketFactory();
              environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory);
              environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory);
          }
          // 如果是遠程獲取,則會通過 rmi 進行遠程通信獲取
          jmxc = JMXConnectorFactory.connect(new JMXServiceURL(jmxUrl), environment);
          beanConn = jmxc.getMBeanServerConnection();
        }
        try {
            // Query MBean names, see #89 for reasons queryMBeans() is used instead of queryNames()
            Set<ObjectName> mBeanNames = new HashSet<ObjectName>();
            for (ObjectName name : whitelistObjectNames) {
                for (ObjectInstance instance : beanConn.queryMBeans(name, null)) {
                    mBeanNames.add(instance.getObjectName());
                }
            }

            for (ObjectName name : blacklistObjectNames) {
                for (ObjectInstance instance : beanConn.queryMBeans(name, null)) {
                    mBeanNames.remove(instance.getObjectName());
                }
            }

            // Now that we have *only* the whitelisted mBeans, remove any old ones from the cache:
            jmxMBeanPropertyCache.onlyKeepMBeans(mBeanNames);

            for (ObjectName objectName : mBeanNames) {
                long start = System.nanoTime();
                scrapeBean(beanConn, objectName);
                logger.fine("TIME: " + (System.nanoTime() - start) + " ns for " + objectName.toString());
            }
        } finally {
          if (jmxc != null) {
            jmxc.close();
          }
        }
    }
    
    // io.prometheus.jmx.JmxScraper#scrapeBean
    private void scrapeBean(MBeanServerConnection beanConn, ObjectName mbeanName) {
        MBeanInfo info;
        try {
          info = beanConn.getMBeanInfo(mbeanName);
        } catch (IOException e) {
          logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
          return;
        } catch (JMException e) {
          logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
          return;
        }
        MBeanAttributeInfo[] attrInfos = info.getAttributes();

        Map<String, MBeanAttributeInfo> name2AttrInfo = new LinkedHashMap<String, MBeanAttributeInfo>();
        for (int idx = 0; idx < attrInfos.length; ++idx) {
            MBeanAttributeInfo attr = attrInfos[idx];
            if (!attr.isReadable()) {
                logScrape(mbeanName, attr, "not readable");
                continue;
            }
            name2AttrInfo.put(attr.getName(), attr);
        }
        final AttributeList attributes;
        try {
            // 通過 MBean 調用對象,獲取所有屬性值,略去不說
            attributes = beanConn.getAttributes(mbeanName, name2AttrInfo.keySet().toArray(new String[0]));
        } catch (Exception e) {
            logScrape(mbeanName, name2AttrInfo.keySet(), "Fail: " + e);
            return;
        }
        for (Attribute attribute : attributes.asList()) {
            MBeanAttributeInfo attr = name2AttrInfo.get(attribute.getName());
            logScrape(mbeanName, attr, "process");
            // 處理單個key的屬性值, 如 topic=aaa,ip=1 將會進行再次循環處理
            processBeanValue(
                    mbeanName.getDomain(),
                    // 獲取有效的屬性列表, 我們可以簡單看一下過濾規則, 如下文
                    jmxMBeanPropertyCache.getKeyPropertyList(mbeanName),
                    new LinkedList<String>(),
                    attr.getName(),
                    attr.getType(),
                    attr.getDescription(),
                    attribute.getValue()
            );
        }
    }
    // 處理每個 mBean 的屬性,寫入到 receiver 中
    // io.prometheus.jmx.JmxScraper#processBeanValue
    /**
     * Recursive function for exporting the values of an mBean.
     * JMX is a very open technology, without any prescribed way of declaring mBeans
     * so this function tries to do a best-effort pass of getting the values/names
     * out in a way it can be processed elsewhere easily.
     */
    private void processBeanValue(
            String domain,
            LinkedHashMap<String, String> beanProperties,
            LinkedList<String> attrKeys,
            String attrName,
            String attrType,
            String attrDescription,
            Object value) {
        if (value == null) {
            logScrape(domain + beanProperties + attrName, "null");
        } 
        // 單值情況,数字型,字符串型,可以處理
        else if (value instanceof Number || value instanceof String || value instanceof Boolean) {
            logScrape(domain + beanProperties + attrName, value.toString());
            // 解析出的數據存入 receiver 中,可以是 jmx, 或者 控制台
            this.receiver.recordBean(
                    domain,
                    beanProperties,
                    attrKeys,
                    attrName,
                    attrType,
                    attrDescription,
                    value);
        } 
        // 多值型情況
        else if (value instanceof CompositeData) {
            logScrape(domain + beanProperties + attrName, "compositedata");
            CompositeData composite = (CompositeData) value;
            CompositeType type = composite.getCompositeType();
            attrKeys = new LinkedList<String>(attrKeys);
            attrKeys.add(attrName);
            for(String key : type.keySet()) {
                String typ = type.getType(key).getTypeName();
                Object valu = composite.get(key);
                processBeanValue(
                        domain,
                        beanProperties,
                        attrKeys,
                        key,
                        typ,
                        type.getDescription(),
                        valu);
            }
        } 
        // 更複雜型對象
        else if (value instanceof TabularData) {
            // I don't pretend to have a good understanding of TabularData.
            // The real world usage doesn't appear to match how they were
            // meant to be used according to the docs. I've only seen them
            // used as 'key' 'value' pairs even when 'value' is itself a
            // CompositeData of multiple values.
            logScrape(domain + beanProperties + attrName, "tabulardata");
            TabularData tds = (TabularData) value;
            TabularType tt = tds.getTabularType();

            List<String> rowKeys = tt.getIndexNames();

            CompositeType type = tt.getRowType();
            Set<String> valueKeys = new TreeSet<String>(type.keySet());
            valueKeys.removeAll(rowKeys);

            LinkedList<String> extendedAttrKeys = new LinkedList<String>(attrKeys);
            extendedAttrKeys.add(attrName);
            for (Object valu : tds.values()) {
                if (valu instanceof CompositeData) {
                    CompositeData composite = (CompositeData) valu;
                    LinkedHashMap<String, String> l2s = new LinkedHashMap<String, String>(beanProperties);
                    for (String idx : rowKeys) {
                        Object obj = composite.get(idx);
                        if (obj != null) {
                            // Nested tabulardata will repeat the 'key' label, so
                            // append a suffix to distinguish each.
                            while (l2s.containsKey(idx)) {
                              idx = idx + "_";
                            }
                            l2s.put(idx, obj.toString());
                        }
                    }
                    for(String valueIdx : valueKeys) {
                        LinkedList<String> attrNames = extendedAttrKeys;
                        String typ = type.getType(valueIdx).getTypeName();
                        String name = valueIdx;
                        if (valueIdx.toLowerCase().equals("value")) {
                            // Skip appending 'value' to the name
                            attrNames = attrKeys;
                            name = attrName;
                        } 
                        processBeanValue(
                            domain,
                            l2s,
                            attrNames,
                            name,
                            typ,
                            type.getDescription(),
                            composite.get(valueIdx));
                    }
                } else {
                    logScrape(domain, "not a correct tabulardata format");
                }
            }
        } else if (value.getClass().isArray()) {
            logScrape(domain, "arrays are unsupported");
        } else {
            // 多半會返回不支持的對象然後得不到jmx監控值
            // mydomain_test{3=3, topic=aaa} java.util.Hashtable is not exported
            logScrape(domain + beanProperties, attrType + " is not exported");
        }
    }
    
    // 我們看下prometheus 對 mbeanName 的轉換操作,會將各種特殊字符轉換為 屬性列表
    // io.prometheus.jmx.JmxMBeanPropertyCache#getKeyPropertyList
    public LinkedHashMap<String, String> getKeyPropertyList(ObjectName mbeanName) {
        LinkedHashMap<String, String> keyProperties = keyPropertiesPerBean.get(mbeanName);
        if (keyProperties == null) {
            keyProperties = new LinkedHashMap<String, String>();
            // 轉化為 string 格式
            String properties = mbeanName.getKeyPropertyListString();
            // 此處為 prometheus 認識的格式,已經匹配上了
            Matcher match = PROPERTY_PATTERN.matcher(properties);
            while (match.lookingAt()) {
                keyProperties.put(match.group(1), match.group(2));
                properties = properties.substring(match.end());
                if (properties.startsWith(",")) {
                    properties = properties.substring(1);
                }
                match.reset(properties);
            }
            keyPropertiesPerBean.put(mbeanName, keyProperties);
        }
        return keyProperties;
    }
        // io.prometheus.jmx.JmxMBeanPropertyCache#PROPERTY_PATTERN
        private static final Pattern PROPERTY_PATTERN = Pattern.compile(
            "([^,=:\\*\\?]+)" + // Name - non-empty, anything but comma, equals, colon, star, or question mark
                    "=" +  // Equals
                    "(" + // Either
                    "\"" + // Quoted
                    "(?:" + // A possibly empty sequence of
                    "[^\\\\\"]*" + // Greedily match anything but backslash or quote
                    "(?:\\\\.)?" + // Greedily see if we can match an escaped sequence
                    ")*" +
                    "\"" +
                    "|" + // Or
                    "[^,=:\"]*" + // Unquoted - can be empty, anything but comma, equals, colon, or quote
                    ")");

 

六、 原理: jmx_exportor 為什麼輸出的格式是這樣的?

  prometheus 的數據格式如下,如何從埋點數據轉換?

# HELP mydomain_test_value_Count Attribute exposed for management (mydomain_test<_=_value, topic=b_topic><>Count)
# TYPE mydomain_test_value_Count untyped
mydomain_test_value_Count{topic="b_topic",} 1.0
mydomain_test_value_Count{topic="a_topic",} 132.0

  是一個輸出格式問題,也是一協議問題。

  // io.prometheus.client.exporter.common.TextFormat#write004
  public static void write004(Writer writer, Enumeration<Collector.MetricFamilySamples> mfs) throws IOException {
    /* See http://prometheus.io/docs/instrumenting/exposition_formats/
     * for the output format specification. */
    while(mfs.hasMoreElements()) {
      Collector.MetricFamilySamples metricFamilySamples = mfs.nextElement();
      writer.write("# HELP ");
      writer.write(metricFamilySamples.name);
      writer.write(' ');
      writeEscapedHelp(writer, metricFamilySamples.help);
      writer.write('\n');

      writer.write("# TYPE ");
      writer.write(metricFamilySamples.name);
      writer.write(' ');
      writer.write(typeString(metricFamilySamples.type));
      writer.write('\n');

      for (Collector.MetricFamilySamples.Sample sample: metricFamilySamples.samples) {
        writer.write(sample.name);
        // 帶 labelNames 的,依次輸出對應的標籤
        if (sample.labelNames.size() > 0) {
          writer.write('{');
          for (int i = 0; i < sample.labelNames.size(); ++i) {
            writer.write(sample.labelNames.get(i));
            writer.write("=\"");
            writeEscapedLabelValue(writer, sample.labelValues.get(i));
            writer.write("\",");
          }
          writer.write('}');
        }
        writer.write(' ');
        writer.write(Collector.doubleToGoString(sample.value));
        if (sample.timestampMs != null){
          writer.write(' ');
          writer.write(sample.timestampMs.toString());
        }
        writer.write('\n');
      }
    }
  }

 

  done.

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

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

花了幾個小時總結了一些容易出錯的 Java 知識點!

本文已經收錄自 (61k+Star!【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。歡迎 Star!歡迎騷擾!)
原文地址:https://javaguide.cn/2019/08/20/java/java%E5%9F%BA%E7%A1%80/Java%E7%96%91%E9%9A%BE%E7%82%B9/

1. 基礎

1.1. 正確使用 equals 方法

Object的equals方法容易拋空指針異常,應使用常量或確定有值的對象來調用 equals。

舉個例子:

// 不能使用一個值為null的引用類型變量來調用非靜態方法,否則會拋出異常
String str = null;
if (str.equals("SnailClimb")) {
  ...
} else {
  ..
}

運行上面的程序會拋出空指針異常,但是我們把第二行的條件判斷語句改為下面這樣的話,就不會拋出空指針異常,else 語句塊得到執行。:

"SnailClimb".equals(str);// false 

不過更推薦使用 java.util.Objects#equals(JDK7 引入的工具類)。

Objects.equals(null,"SnailClimb");// false

我們看一下java.util.Objects#equals的源碼就知道原因了。

public static boolean equals(Object a, Object b) {
        // 可以避免空指針異常。如果a==null的話此時a.equals(b)就不會得到執行,避免出現空指針異常。
        return (a == b) || (a != null && a.equals(b));
    }

注意:

Reference:

  • 每種原始類型都有默認值一樣,如int默認值為 0,boolean 的默認值為 false,null 是任何引用類型的默認值,不嚴格的說是所有 Object 類型的默認值。
  • 可以使用 == 或者 != 操作來比較null值,但是不能使用其他算法或者邏輯操作。在Java中null == null將返回true。
  • 不能使用一個值為null的引用類型變量來調用非靜態方法,否則會拋出異常

1.2. 整型包裝類值的比較

所有整型包裝類對象值的比較必須使用equals方法。

先看下面這個例子:

Integer x = 3;
Integer y = 3;
System.out.println(x == y);// true
Integer a = new Integer(3);
Integer b = new Integer(3);
System.out.println(a == b);//false
System.out.println(a.equals(b));//true

當使用自動裝箱方式創建一個Integer對象時,當數值在-128 ~127時,會將創建的 Integer 對象緩存起來,當下次再出現該數值時,直接從緩存中取出對應的Integer對象。所以上述代碼中,x和y引用的是相同的Integer對象。

注意:如果你的IDE(IDEA/Eclipse)上安裝了阿里巴巴的p3c插件,這個插件如果檢測到你用 ==的話會報錯提示,推薦安裝一個這個插件,很不錯。

1.3. BigDecimal

1.3.1. BigDecimal 的用處

《阿里巴巴Java開發手冊》中提到:浮點數之間的等值判斷,基本數據類型不能用==來比較,包裝數據類型不能用 equals 來判斷。 具體原理和浮點數的編碼方式有關,這裏就不多提了,我們下面直接上實例:

float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false

具有基本數學知識的我們很清楚的知道輸出並不是我們想要的結果(精度丟失),我們如何解決這個問題呢?一種很常用的方法是:使用使用 BigDecimal 來定義浮點數的值,再進行浮點數的運算操作。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1
System.out.println(x.equals(y));// true 

1.3.2. BigDecimal 的大小比較

a.compareTo(b) : 返回 -1 表示小於,0 表示 等於, 1表示 大於。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.compareTo(b));// 1

1.3.3. BigDecimal 保留幾位小數

通過 setScale方法設置保留幾位小數以及保留規則。保留規則有挺多種,不需要記,IDEA會提示。

BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN);
System.out.println(n);// 1.255

1.3.4. BigDecimal 的使用注意事項

注意:我們在使用BigDecimal時,為了防止精度丟失,推薦使用它的 BigDecimal(String) 構造方法來創建對象。《阿里巴巴Java開發手冊》對這部分內容也有提到如下圖所示。

1.3.5. 總結

BigDecimal 主要用來操作(大)浮點數,BigInteger 主要用來操作大整數(超過 long 類型)。

BigDecimal 的實現利用到了 BigInteger, 所不同的是 BigDecimal 加入了小數位的概念

1.4. 基本數據類型與包裝數據類型的使用標準

Reference:《阿里巴巴Java開發手冊》

  • 【強制】所有的 POJO 類屬性必須使用包裝數據類型。
  • 【強制】RPC 方法的返回值和參數必須使用包裝數據類型。
  • 【推薦】所有的局部變量使用基本數據類型。

比如我們如果自定義了一個Student類,其中有一個屬性是成績score,如果用Integer而不用int定義,一次考試,學生可能沒考,值是null,也可能考了,但考了0分,值是0,這兩個表達的狀態明顯不一樣.

說明 :POJO 類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何 NPE 問題,或者入庫檢查,都由使用者來保證。

正例 : 數據庫的查詢結果可能是 null,因為自動拆箱,用基本數據類型接收有 NPE 風險。

反例 : 比如显示成交總額漲跌情況,即正負 x%,x 為基本數據類型,調用的 RPC 服務,調用不成功時,返回的是默認值,頁面显示為 0%,這是不合理的,應該显示成中劃線。所以包裝數據類型的 null 值,能夠表示額外的信息,如:遠程調用失敗,異常退出。

2. 集合

2.1. Arrays.asList()使用指南

最近使用Arrays.asList()遇到了一些坑,然後在網上看到這篇文章: 感覺挺不錯的,但是還不是特別全面。所以,自己對於這塊小知識點進行了簡單的總結。

2.1.1. 簡介

Arrays.asList()在平時開發中還是比較常見的,我們可以使用它將一個數組轉換為一個List集合。

String[] myArray = { "Apple", "Banana", "Orange" }; 
List<String> myList = Arrays.asList(myArray);
//上面兩個語句等價於下面一條語句
List<String> myList = Arrays.asList("Apple","Banana", "Orange");

JDK 源碼對於這個方法的說明:

/**
 *返回由指定數組支持的固定大小的列表。此方法作為基於數組和基於集合的API之間的橋樑,與           Collection.toArray()結合使用。返回的List是可序列化並實現RandomAccess接口。
 */ 
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

2.1.2. 《阿里巴巴Java 開發手冊》對其的描述

Arrays.asList()將數組轉換為集合后,底層其實還是數組,《阿里巴巴Java 開發手冊》對於這個方法有如下描述:

2.1.3. 使用時的注意事項總結

傳遞的數組必須是對象數組,而不是基本類型。

Arrays.asList()是泛型方法,傳入的對象必須是對象數組。

int[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//數組地址值
System.out.println(myList.get(1));//報錯:ArrayIndexOutOfBoundsException
int [] array=(int[]) myList.get(0);
System.out.println(array[0]);//1

當傳入一個原生數據類型數組時,Arrays.asList() 的真正得到的參數就不是數組中的元素,而是數組對象本身!此時List 的唯一元素就是這個數組,這也就解釋了上面的代碼。

我們使用包裝類型數組就可以解決這個問題。

Integer[] myArray = { 1, 2, 3 };

使用集合的修改方法:add()remove()clear()會拋出異常。

List myList = Arrays.asList(1, 2, 3);
myList.add(4);//運行時報錯:UnsupportedOperationException
myList.remove(1);//運行時報錯:UnsupportedOperationException
myList.clear();//運行時報錯:UnsupportedOperationException

Arrays.asList() 方法返回的並不是 java.util.ArrayList ,而是 java.util.Arrays 的一個內部類,這個內部類並沒有實現集合的修改方法或者說並沒有重寫這些方法。

List myList = Arrays.asList(1, 2, 3);
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList

下圖是java.util.Arrays$ArrayList的簡易源碼,我們可以看到這個類重寫的方法有哪些。

  private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        ...

        @Override
        public E get(int index) {
          ...
        }

        @Override
        public E set(int index, E element) {
          ...
        }

        @Override
        public int indexOf(Object o) {
          ...
        }

        @Override
        public boolean contains(Object o) {
           ...
        }

        @Override
        public void forEach(Consumer<? super E> action) {
          ...
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
          ...
        }

        @Override
        public void sort(Comparator<? super E> c) {
          ...
        }
    }

我們再看一下java.util.AbstractListremove()方法,這樣我們就明白為啥會拋出UnsupportedOperationException

public E remove(int index) {
    throw new UnsupportedOperationException();
}

2.1.4. 如何正確的將數組轉換為ArrayList?

stackoverflow:https://dwz.cn/vcBkTiTW

1. 自己動手實現(教育目的)

//JDK1.5+
static <T> List<T> arrayToList(final T[] array) {
  final List<T> l = new ArrayList<T>(array.length);

  for (final T s : array) {
    l.add(s);
  }
  return (l);
}
Integer [] myArray = { 1, 2, 3 };
System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList

2. 最簡便的方法(推薦)

List list = new ArrayList<>(Arrays.asList("a", "b", "c"))

3. 使用 Java8 的Stream(推薦)

Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本類型也可以實現轉換(依賴boxed的裝箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());

4. 使用 Guava(推薦)

對於不可變集合,你可以使用類及其與工廠方法:(參數不能為空)

List<String> il = ImmutableList.of("string", "elements");  // from varargs
List<String> il = ImmutableList.copyOf(aStringArray);      // from array

對於可變集合,你可以使用類及其工廠方法:

List<String> l1 = Lists.newArrayList(anotherListOrCollection);    // from collection
List<String> l2 = Lists.newArrayList(aStringArray);               // from array
List<String> l3 = Lists.newArrayList("or", "string", "elements"); // from varargs

5. 使用 Apache Commons Collections

List<String> list = new ArrayList<String>();
CollectionUtils.addAll(list, str);

2.2. Collection.toArray()方法使用的坑&如何反轉數組

該方法是一個泛型方法:<T> T[] toArray(T[] a); 如果toArray方法中沒有傳遞任何參數的話返回的是Object類型數組。

String [] s= new String[]{
    "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
s=list.toArray(new String[0]);//沒有指定類型的話會報錯

由於JVM優化,new String[0]作為Collection.toArray()方法的參數現在使用更好,new String[0]就是起一個模板的作用,指定了返回數組的類型,0是為了節省空間,因為它只是為了說明返回的類型。詳見:

2.3. 不要在 foreach 循環里進行元素的 remove/add 操作

如果要進行remove操作,可以調用迭代器的 remove方法而不是集合類的 remove 方法。因為如果列表在任何時間從結構上修改創建迭代器之後,以任何方式除非通過迭代器自身remove/add方法,迭代器都將拋出一個ConcurrentModificationException,這就是單線程狀態下產生的 fail-fast 機制

fail-fast 機制 :多個線程對 fail-fast 集合進行修改的時,可能會拋出ConcurrentModificationException,單線程下也會出現這種情況,上面已經提到過。

java.util包下面的所有的集合類都是fail-fast的,而java.util.concurrent包下面的所有的類都是fail-safe的。

開源項目推薦

作者的其他開源項目推薦:

  1. :【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。
  2. : 適合新手入門以及有經驗的開發人員查閱的 Spring Boot 教程(業餘時間維護中,歡迎一起維護)。
  3. : 我覺得技術人員應該有的一些好習慣!
  4. :從零入門 !Spring Security With JWT(含權限驗證)後端部分代碼。

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

【其他文章推薦】

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

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

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

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!