手把手教你基於SqlSugar4編寫一個可視化代碼生成器(生成實體,以SqlServer為例,文末附源碼)

  在開發過程中免不了創建實體類,字段少的表可以手動編寫,但是字段多還用手動創建的話不免有些浪費時間,假如一張表有100多個字段,手寫有些不現實。

這時我們會藉助一些工具,如:動軟代碼生成器、各種ORM框架自帶的代碼生成器等等,都可以使用。

我們現在就基於SqlSugar(ORM框架)自己動手製造一個輪子,以SqlServer為例。我們先看一下成品效果,

 

使用流程:

  配置好數據庫鏈接,點擊【鏈接數據庫】獲取指定服務器上的數據庫名,點擊數據庫名,動態獲取數據庫下面的所有表,

點擊數據表,如果生成過了的會自動獲取生成的實體,如果沒有生成過,點擊【生成實體】自動生成显示,直接複製即可使用。

注:server=xxx.xxx.x.xxx這裏如果是本地沒有配置的話直接server=.即可。

 

 

 

 

開發環境:

編譯器:Visual Studio 2017

運行環境:windows7 x64

數據庫:SqlServer2012

 

代碼實現步驟:

一、創建一個ASP.NET Web應用,命名為GenerateEntity

 

 

 

 

 

 

二、應用SqlSugar動態鏈接庫

 

 

 

三、編寫代碼

這裏分為前端和後端,前端頁面展示,後端後台邏輯(注:由於我們是代碼展示,所以就不搞三層架構、工廠模式這些,直接在控制器中完成,有需要的同學可以根據項目需求進行更改

內部實現邏輯:

  • 在頁面上配置數據庫鏈接,點擊【鏈接數據庫】按鈕獲取指定數據庫的所有數據庫名显示在左邊;
  • 點擊左邊的數據庫名稱,動態獲取指定數據庫下面所有的表显示出來;
  • 點擊表名,生成過的就显示生成的實體,沒有的則點擊【生成實體】按鈕生成(支持生成單表和數據庫表全部生成);

這裏我直接貼出代碼,直接拷貝即可使用:

前端html頁面

@{
    ViewBag.Title = "Home Page";
}

<script src="~/Scripts/jquery-3.3.1.js"></script>

<div style="margin-top:10px;font-family:'Microsoft YaHei';font-size:18px; ">
    <div style="height:100px;width:100%;border:1px solid gray;padding:10px">
        <div>
            <span>鏈接數據庫:</span>
            <input style="width:800px;max-width:800px;" id="Link" value="server=xxx.xxx.x.xxx;uid=sa;pwd=xxx" />
            <a href="javascript:void(0)" onclick="LinkServer()">鏈接數據庫</a>
        </div>
        <div style="margin-top:10px">
            <span>數據庫名:</span>
            <input style="color:red;font-weight:600" id="ServerName" />

            <span>表名:</span>
            <input style="color:red;font-weight:600" id="TableName" />

            <span>生成類型:</span>
            <select id="type">
                <option value="0">生成單個表</option>
                <option value="1">生成所有表</option>
            </select>
            <a  href="javascript:void(0)" onclick="GenerateEntity()" style="margin-left:20px;font-weight:600;">生成實體</a>
            <br />

        </div>
    </div>
    <div style="height:720px;width:100%;">
        <div style="height:100%;width:40%;float:left; border:1px solid gray;font-size:20px">

            <div id="leftserver" style="float:left;border:1px solid gray;height:100%;width:40%;padding:10px;overflow: auto;">

            </div>
            <div id="lefttable" style="float:left;border:1px solid gray;height:100%;width:60%;padding:10px;overflow: auto;">

            </div>
        </div>
        <div  style="height:100%;width:60%;float:left;border:1px solid gray;overflow: auto;">
            <textarea style="width:100%;height:100%;max-width:10000px" id="righttable"></textarea>
        </div>
    </div>
</div>

<script type="text/javascript">

    //鏈接數據庫
    function LinkServer() {
        $.ajax({
            url: "/Home/LinkServer",
            data: { Link: $("#Link").val() },
            type: "POST",
            async: false,
            dataType: "json",
            success: function (data) {
                if (data.res) {
                    if (data.info != "") {
                        $("#leftserver").html("");
                        var leftserver = "<span>數據庫名</span><hr />";
                        var info = eval("(" + data.info + ")");
                        for (var i = 0; i < info.length; i++) {
                            leftserver += "<a onclick=\"leftserver('" + info[i].Name + "')\">" + info[i].Name + "</a><br />";
                        }

                        $("#leftserver").html(leftserver);
                    }
                }
                else {
                    alert(data.msg);
                }
            }
        });
    }

    //查詢指定數據庫的表
    function leftserver(Name) {
        $("#ServerName").val(Name)
        $.ajax({
            url: "/Home/GetTable",
            data: { Link: $("#Link").val(), Name: Name },
            type: "POST",
            async: false,
            dataType: "json",
            success: function (data) {
                if (data.res) {
                    if (data.info != "") {
                        $("#lefttable").html("");
                        var lefttable = "<span>表名</span><hr />";
                        var info = eval("(" + data.info + ")");
                        for (var i = 0; i < info.length; i++) {
                            lefttable += "<a onclick=\"lefttable('" + info[i].Name + "')\">" + info[i].Name + "</a><br />";
                        }

                        $("#lefttable").html(lefttable);
                    }
                }
                else {
                    alert(data.msg);
                }
            }
        });
    }

    //查詢指定數據庫的表
    function lefttable(Name) {
        $("#TableName").val(Name);
        $.ajax({
            url: "/Home/GetGenerateEntity",
            data: { TableName: Name },
            type: "POST",
            async: false,
            dataType: "json",
            success: function (data) {
                if (data.res) {
                    document.getElementById("righttable").innerHTML = data.info;
                }
                else {
                    alert(data.msg);
                }
            }
        });
    }

    //生成實體
    function GenerateEntity() {

        $.ajax({
            url: "/Home/GenerateEntity",
            data: {
                Link: $("#Link").val(),
                Name: $("#ServerName").val(),
                TableName: $("#TableName").val(),
                type: $("#type").val()
            },
            type: "POST",
            async: false,
            dataType: "json",
            success: function (data) {
                if (data.res) {
                    document.getElementById("righttable").innerHTML = data.info;
                }
                else {
                    alert(data.msg);
                }
            }
        });
    }

</script>

 

後端控制器數據

using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace GenerateEntity.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }


     
        //鏈接數據庫
        public JsonResult LinkServer(string Link)
        {
            ResultInfo result = new ResultInfo();
            try
            {
                //配置數據庫連接
                SqlSugarClient db = new SqlSugarClient(
                                    new ConnectionConfig()
                                    {
                                        ConnectionString = ""+ Link + ";database=master",
                                        DbType = DbType.SqlServer,//設置數據庫類型
                                    IsAutoCloseConnection = true,//自動釋放數據務,如果存在事務,在事務結束后釋放
                                    InitKeyType = InitKeyType.Attribute //從實體特性中讀取主鍵自增列信息
                                });
                string sql = @"SELECT top 100000 Name FROM Master..SysDatabases ORDER BY Name";  //查詢所有鏈接的所有數據庫名
                var strList = db.SqlQueryable<databaseName>(sql).ToList();
                result.info = Newtonsoft.Json.JsonConvert.SerializeObject(strList);
                result.res = true;
                result.msg = "鏈接成功!";
            }
            catch (Exception ex)
            {
                result.msg = ex.Message;
            }

            return Json(result, JsonRequestBehavior.AllowGet);
        }

        //根據數據庫名查詢所有表
        public JsonResult GetTable(string Link,string Name)
        {

            ResultInfo result = new ResultInfo();
            try
            {
                //配置數據庫連接
                SqlSugarClient db = new SqlSugarClient(
                                    new ConnectionConfig()
                                    {
                                        ConnectionString = "" + Link + ";database="+ Name + "",
                                        DbType = DbType.SqlServer,//設置數據庫類型
                                        IsAutoCloseConnection = true,//自動釋放數據務,如果存在事務,在事務結束后釋放
                                        InitKeyType = InitKeyType.Attribute //從實體特性中讀取主鍵自增列信息
                                    });

                string sql = @"SELECT top 10000 Name FROM SYSOBJECTS WHERE TYPE='U' ORDER BY Name";  //查詢所有鏈接的所有數據庫名
                var strList = db.SqlQueryable<databaseName>(sql).ToList();
                result.info = Newtonsoft.Json.JsonConvert.SerializeObject(strList);
                result.res = true;
                result.msg = "查詢成功!";
            }
            catch (Exception ex)
            {
                result.msg = ex.Message;
            }

            return Json(result, JsonRequestBehavior.AllowGet);
        }

        //生成實體
        public JsonResult GenerateEntity(string Link, string Name,string TableName,string type)
        {

            ResultInfo result = new ResultInfo();
            try
            {
                //配置數據庫連接
                SqlSugarClient db = new SqlSugarClient(
                                    new ConnectionConfig()
                                    {
                                        ConnectionString = "" + Link + ";database=" + Name + "",
                                        DbType = DbType.SqlServer,//設置數據庫類型
                                        IsAutoCloseConnection = true,//自動釋放數據務,如果存在事務,在事務結束后釋放
                                        InitKeyType = InitKeyType.Attribute //從實體特性中讀取主鍵自增列信息
                                    });

                string path = "C:\\Demo\\2";

                if (type == "0")
                {
                    path = "C:\\Demo\\2";
                    db.DbFirst.Where(TableName).CreateClassFile(path);
                    result.info = System.IO.File.ReadAllText(@"" + path + "\\" + TableName + ".cs" + "", Encoding.UTF8);
                }
                else if (type == "1")
                {
                    path = "C:\\Demo\\3";
                    db.DbFirst.IsCreateAttribute().CreateClassFile(path);
                    result.info = "";
                }

                
                
                result.res = true;
                result.msg = "生成成功!";
            }
            catch (Exception ex)
            {
                result.msg = ex.Message;
            }

            return Json(result, JsonRequestBehavior.AllowGet);
        }

        //生成全部表時查看
        public JsonResult GetGenerateEntity(string TableName)
        {

            ResultInfo result = new ResultInfo();
            try
            {
                string path = "C:\\Demo\\3";
                result.info = System.IO.File.ReadAllText(@"" + path + "\\" + TableName + ".cs" + "", Encoding.UTF8);
                result.res = true;
                result.msg = "查詢成功!";
            }
            catch (Exception ex)
            {
                result.msg = ex.Message;
                try
                {
                    if (result.msg.Contains("未能找到文件"))
                    {
                       string path = "C:\\Demo\\2";
                        result.info = System.IO.File.ReadAllText(@"" + path + "\\" + TableName + ".cs" + "", Encoding.UTF8);
                        result.res = true;
                        result.msg = "查詢成功!";
                    }
                }
                catch (Exception)
                {
                    result.msg = ex.Message;
                }
            }

            return Json(result, JsonRequestBehavior.AllowGet);
        }

        //數據庫名
        public class databaseName
        {
            public string Name { get; set; }
        }

        //封裝返回信息數據
        public class ResultInfo
        {
            public ResultInfo()
            {
                res = false;
                startcode = 449;
                info = "";
            }
            public bool res { get; set; }  //返回狀態(true or false)
            public string msg { get; set; }  //返回信息
            public int startcode { get; set; }  //返回http的狀態碼
            public string info { get; set; }  //返回的結果(res為true時返回結果集,res為false時返回錯誤提示)
        }

    }
}

 

 

 

這樣一套可視化代碼生成器就出來了,我們把他發布到IIS上面,然後設置為瀏覽器標籤(收藏),這樣就可以快捷使用了。

我們運行一下看看,是不是感覺很方便呀!

 

 

 

歡迎關注訂閱我的微信公眾平台【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂
公眾號:熊澤有話說
出處: https://www.cnblogs.com/xiongze520/p/13181241.html
創作不易,版權歸作者和博客園共有,轉載或者部分轉載、摘錄,請在文章明顯位置註明作者和原文鏈接。  

 

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

網頁設計最專業,超強功能平台可客製化

程序員如何高效學Python,如何高效用Python掙錢

    本人在1年半之前,不熟悉Python(不過有若干年Java開發基礎),由於公司要用Python,所以學習了一通。現在除了能用Python做本職工作外,還出了本Python書,《基於股票大數據分析的Python入門實戰 視頻教學版》,京東鏈接:https://item.jd.com/69241653952.html,還在某網站錄製了視頻課,後面還有其它線上線下課的機會。

    

    本人的感受是,哪怕上班用不到Python,程序員也應該學Python,因為Python能給大家帶來更多的主業副業機會,而且現在做Python的人還沒Java多。在本文里將結合本人的經驗,一方面分享下如何高效學Python,另一方面分享下用Python掙錢的經驗。

1 先說Python的尷尬地位,首先要明確掙錢方式

    尷尬體現在哪裡?一些大廠雖然有專門做Python的高薪崗位,但一般會直接找些深度學習機器學習方向的碩士博士,而且是要名校的,而小公司一般限於成本的原因,無法直接設置單做Python的崗位,而且也就是做些技術含量較低的應用, 比如做個爬蟲或者簡單調個機器學習的庫,所以一般是讓做其它語言的人順帶做掉。

    而且Python用庫和方法的形式包裝掉了一些很複雜的算法,程序員一般只需要調用方法就可以實現基本的機器學習和深度學習之類的活,而在大廠里高薪的Python崗,絕非是簡單地調用Python,而是需要深入了解算法,從而根據業務定製模型,所以一般社會上的程序員很難通過自學,達到大廠里高薪Python程序員的標準。

    總之,你在工作后通過自學Python,未必能達到大廠高薪職位的標準,因為由於你數學基礎不行,未必能深入算法,而一般公司也不會單獨開設Python崗位,所以對應地,學Python之前大家應該明確靠Python的掙錢方式。

    1 主業上,還得以Java等語言為主,但如果你能在簡歷和面試中證明自己很精通一般的Python爬蟲、數據分析和機器學習等方面的應用,絕對能幫你更好地找到工作,並且個人提升也會很快。

    2 雖然Python底層包含的深度學習等方面的算法很難,但用Python做案例並不難,大家可以通過Python做些副業的活。

2 再說Python該怎麼學,該學哪些技能?

    第一步,了解Python的基本語法,比如集合,讀寫文件,讀寫數據庫和異常處理等,如果大家有Java等語言的開發基礎,這塊很簡單,本人也就用了2個星期。但正是因為簡單,所以這些技能很不值錢,別人學起來也快。

    第二步,了解數據分析三劍客,具體來說就是Numpy, Pandas和matplotlib,用Numpy和Pandas清洗數據,用Pandas的DataFrame存儲數據,再用matplotlib繪製柱狀圖餅圖之類的圖形。

    第三步,了解爬蟲技能,這裏除了需要了解自帶的urllib庫之外,還需要了解一種框架,比如Scrapy,需要到能根據需求定製爬蟲代碼的程序。

    其實數據分析和爬蟲相關的語法技能,也不複雜,本人用1個半月也就達到能幹活的程度了,相信大家應該更快。而且,學到這種程序,應該就可以去做些案例以此掙錢了,比如寫分析xx網站的案例,錄成視頻去賣了,而且也能完成公司里大多數數據獲取和數據分析的功能需求了。

    第四步去了解機器學習庫,具體而言就去學習sklearn庫,這個庫里不僅包含了線性回歸嶺回歸和SVM分類等機器學習算法,還包含了波士頓房價、鳶尾花和手寫體識別等的數據集,而且由於已經包裝了相關算法,用sklearn庫學習機器學習的過程並不難,不需要過多的數學知識。學好這個庫,外帶結合爬蟲和數據分析的技能,就更在某個領域幹活掙錢了,比如本人在股票分析領域出了本書,並且也出了些視頻,後繼還可以繼續深入股票量化分析領域。

    第五步,可以去了解深度學習,無非是人工神經網絡,自然語言分析,圖像識別等,這方面雖然包含的數學知識更複雜,但由於也經過包裝,所以直接用接口也不難。這方面學好以後,雖然說高不成低不就,即沒法進大廠,同時小公司也用不到,但用這些知識準備些案例,出書講課錄視頻,甚至做企業培訓,還是能帶來一定的收益的。

    在學上述知識的時候,千萬不能只學語法,因為沒用,一定得結合實例,同時把這些知識變現的時候,也不能單講語法,也是要準備若干案例,比如像我這樣的股票分析,或者是scrapy+數據分析+深度學習的xx網站數據分析案例,這些技能雖然很高大上,但其實做到調用接口實踐案例的程度就能掙錢,如果再有機緣以此進入大廠,那就真的前途無量了。

3 可以先從公眾號做起

    之前講的是如何學,學什麼,這裏就開始講如何掙錢。當然最簡單的就是建個公眾號,在上面發文,吸引粉絲,這個門檻相對低。

    但注意如果僅僅發表入門級的文章,比如numpy庫怎麼用,怎麼用matplotlib庫繪製基本圖形,這絕對不夠,因為此類文章太多,哪些文章能吸引人?

    1 綜合應用類,比如scrapy+數據分析三劍客。

    2 實戰案例類,比如用scrapy爬個網站數據,然後分析。

    3 專業領域類,比如量化分析股票,分析房價等。

    4 深度學習機器學習這些領域現在還很火,這些領域如果把某個算法通俗易懂地講透也行,或者這些方面給寫案例,比如用自然語言分析技術分析某網站的評論等。

    如果能定期發表此類文章,公眾號一定能聚集到不少粉絲,同樣也可以做視頻的up主。

4 更可以出本屬於你的書

    如何讓別人認為你是python某個領域的大牛?要麼有大廠架構師職位加持,這不是每個人都能達到,或者是著名博主公眾號主,但似乎這也需要經歷來積澱,不過如果你在python數據分析和機器學習等方面出本書,那說服力自然就上來了。

    出書可以偏重案例,比如講爬蟲數據分析的書,在合法的前提下給出爬取分析若干知名網頁的案例,如果講機器學習的書,甚至可以結合sklearn庫自帶的數據集,講清楚常用算法的案例應用,如果有時間有機會,我甚至打算再出版本基於python股票量化的書。

    相對而言,寫一本講述包括語法、結合小案例講(機器學習等)庫的用法和結合綜合案例講機器學習算法和數據分析綜合應用的書,並不難。對於一個有5年開發經驗的程序員而言,從零基礎積累個半年,就完全可以達到出書的地步,如果資歷稍微弱些,只有2,3年開發經歷,估計學個1年也應該可以達到出書的地步。

    還是這句話,出書掙的錢不多,但絕對能證明你在python某個領域的能力,小到聯繫副業,大到以此找工作,一定能幫到你。具體操作的話,可以直接在清華出版社,机械工業出版社,人民郵電出版社和电子工業出版社的官網找聯繫方式,然後直接和編輯溝通,至於一些有中介性質的圖書公司,大家自己看着辦。

5 也可以做其它副業

    包括到各大視頻網站去錄製數據分析、爬蟲、機器學習和深度學習等方面的系列課,也可以找你所在城市的線下培訓班去講課,如果你有相關大公司背景,有自己的書,或者業內知名,你就可以聯繫些做企業培訓的公司。這樣做下個半年後,月入1萬應該不是問題。

    在做各種副業的時候,一般來說也是要偏重案例,比如你有若干個深度學習的案例外帶相關算法的說明,再加上些好的文案,應該很能吸引人。當然也可以直接找項目做,目前python方面比較熱門的項目可能還是用爬蟲,但這塊做的時候就要非常慎重了,不能做違法的事情,而當前用到深度學習機器學習技術的項目倒不多,可能因為這些應用更集中在大公司吧。

6 總結:下班后不能總放鬆,更得找點事干

    說實話,python方面的活,哪怕門檻最低的做公眾號,要做好也不簡單,更何況出書了。至於,自己聯繫平台出視頻,或者做線下培訓或者做項目,就不僅得靠技術,更得靠人脈了,經營這類活需要的時間更多。

    不過掙錢拿有容易的,況且,如果下班后總是看手機或者混日子,可能一天天就很快過去了,與其下班做些沒收益的消遣,還不如學些python幹些活,這樣多少好歹也有收益,或者指不定無心插柳柳成蔭,你經營python一段時間后,或者真就以此進了大廠,或者也通過各種途徑成為業內知名人事,拓展了不少副業渠道,也算是不負好時光吧。

    感謝大家看完此文,如果感覺有一定道理,請點贊此文。如果要轉載,也請全文轉載,別刪節本人辛苦寫成的文章。

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

大文件上傳、斷點續傳、秒傳、beego、vue

大文件上傳

0、項目源碼地址

源碼地址 :https://github.com/zhuchangwu/large-file-upload

前端基於 vue-simple-uploader (感謝這個大佬)實現: https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md

vue-simple-uploader底層封裝了uploader.js : https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md

1、如何唯一標識一個文件?

文件的信息後端會存儲在mysql數據庫表中。

在上傳之前,前端通過 spark-md5.js 計算文件的md5值以此去唯一的標示一個文件。

spark-md5.js 地址:https://github.com/satazor/js-spark-md5

README.md中有spark-md5.js的使用demo,可以去看看。

2、斷點續傳是如何實現的?

斷點續傳可以實現這樣的功能,比如用戶上傳200M的文件,當用戶上傳完199M時,斷網了,有了斷點續傳的功能,我們允許RD再次上傳時,能從第199M的位置重新上傳。

實現原理:

實現斷點續傳的前提是,大文件切片上傳。然後前端得問後端哪些chunk曾經上傳過,讓前端跳過這些上傳過的chunk就好了。

前端的上傳器(uploader.js)在上傳時會先發送一個GET請求,這個請求不會攜帶任何chunk數據,作用就是向後端詢問哪些chunk曾經上傳過。 後端會將這些數據保存在mysql數據庫表中。比如按這種格式:1:2:3:5表示,曾經上傳過的分片有1,2,3,5。第四片沒有被上傳,前端會跳過1,2,3,5。 僅僅會將第四個chunk發送給後端。

3、秒傳是如何實現的?

秒傳實現的功能是:當RD重複上傳一份相同的文件時,除了第一次上傳會正常發送上傳請求后,其他的上傳都會跳過真正的上傳,直接显示秒成功。

實現方式:

後端存儲着當前文件的相關信息。為了實現秒傳,我們需要搞一個字段(isUploaded)表示當前md5對應的文件是否曾經上傳過。 後端在處理 前端的上傳器(uploader.js)發送的第一個GET請求時,會將這個字段發送給前端,比如 isUploaded = true。前端看到這個信息后,直接跳過上傳,显示上傳成功。

4、上傳暫停是如何實現的?

上傳的暫停:並不是去暫停一個已經發送出去的正在進行數據傳輸的http請求~

而是暫停發送起發送下一個http請求。

就我們的項目而言,因為我們的文件本來就是先切片,對於我們來說,暫停文件的上傳,本質上就是暫停發送下一個chunk。

5、前端上傳併發數是多少?

前端的uploader.js中默認會三條線程啟動併發上傳,前端會在同一時刻併發 發送3個chunk,後端就會相應的為每個請求開啟三個協程處理上傳的過來的chunk。

在我們的項目中,會將前端併發數調整成了1。原因如下:

因為考慮到了斷點續傳的實現,後端需要記錄下曾經上傳過哪些切片。(這個記錄在mysql的數據庫表中,以 ”1:2:3:4:5“ )這種格式記錄。

Mysql5.7默認的存儲引擎是innoDB,默認的隔離級別是RR。如果我們將前端的併發數調大,就會出現下面的異常情況:

1. goroutine1 獲取開啟事物,讀取當前上傳到記錄是 1:2 (未提交事物)
2. goroutine1 在現有的記錄上加上自己處理的分片3,並和現有的1:2拼接在一起成1:2:3 (未提交事物)
3. goroutine2 獲取開啟事物,(因為RR,所以它讀不到1:2:3)讀取當前上傳到記錄是 1:2 (未提交事物)
4. goroutine1 提交事物,將1:2:3寫回到mysql
5. goroutine2 在現有的記錄上加上自己處理的分片4,並和現有的1:2拼接在一起成1:2:4 (提交事物)

可以看到,如果前端併發上傳,後端就會出現分片丟失的問題。 故前端將併發數置為1。

6、單個chunk上傳失敗怎麼辦?

前端會重傳chunk?

由於網絡問題,或者時後端處理chunk時出現的其他未知的錯誤,會導致chunk上傳失敗。

uploaded.js 中有如下的配置項, 每次uploader.js 在上傳每一個切片實際上都是在發送一次post請求,後端根據這個post請求是會給前端一個狀態嗎。 uploader.js 就是根據這個狀態碼去判斷是失敗了還是成功了,如果失敗了就會重新發送這個上傳的請求。

那uploader.js是如何知道有哪些狀態嗎是它應該重傳chunk的標記呢? 看看下面uploader.js需要的options 就明白了,其中的permantErrors中配置的狀態碼標示:當遇到這個狀態碼時整個上傳直接失敗~

successStatuses中配置的狀態碼錶示chunk是上傳成功的~。 其他的狀態嗎uploader.js 就會任務chunk上傳的有問題,於是重新上傳~

        options: {
          target: 'http://localhost:8081/file/upload',
          maxChunkRetries: 3,
          permanentErrors:[502], // 永久性的上傳失敗~,會認為整個文件都上傳失敗了
          successStatuses:[200], // 當前chunk上傳成功后的狀態嗎
          ...
        }

7、超過重傳次數后,怎麼辦?

比如我們設置出錯后重傳的次數為3,那麼無論當前分片是第幾片,整個文件的上傳狀態被標記為false,這就意味着會終止所有的上傳。

肯定不會出現這種情況:chunk1重傳3次后失敗了,chunk2還能再去上傳,這樣的話數據肯定不一致了。

8、如何控制上傳多大的文件?

目前了解到nginx端的限制上單次上傳不能超過1M。

前端會對大文件進行切片突破nginx的限制。

        options: {
          target: 'http://localhost:8081/file/upload',
          chunkSize: 512000, // 單次上傳 512KB 
        }     

如果後續和nginx負責的同學達成一致,可以把這個值進行調整。前端可以後續將這個chunk的閾值加大。

9、如何保證上傳文件的百分百正確?

在上傳文件前,前端會計算出當前RD選擇的這個文件的 md5 值。

當後端檢測到所有的分片全部上傳完畢,這時會merge所有分片匯聚成單個文件。計算這個文件的md5 同 RD在前端提供的文件的md5值比對。 比對結果一致說明RD正確的完成了上傳。結果不一致,說明文件上傳失敗了~返回給前端任務失敗,提示RD重新上傳。

10、其他細節問題:

如何判斷文件上傳失敗了,給RD展示紅色?

如何控制上傳什麼類型的文件?

如何控制不能上傳空文件?

上面說過了,當 uploader.js 遇到了permanentErrors這種狀態碼時會認為文件上傳失敗了。

前端想在上傳失敗后,將進度條轉換成紅色,其實改一下CSS樣式就好了,問題就在於,根據什麼去修改?在哪裡去修改?

前端會將每一個file封裝成一個組件:如下圖中的files就是file的集合

整個的fileList會將會被渲染成下面這樣。

我們上傳的文件被vue-simple-uploader的作者封裝成一個file.vue組件,這個對象中會有個配置參數, 比如它會長下面這樣。

     options: {
        target: 'http://localhost:8081/file/upload',
        statusText: {
          success: '上傳成功',
          error: '上傳出錯,請重試',
          typeError: '暫不支持上傳您添加的文件格式',
          uploading: '上傳中',
          emptyError:'不能上傳空文件',
          paused: '請確認文件後點擊上傳',
          waiting: '等待中'
        }
      }
    },

我們將上面的配置添加給Uploader.js

      const uploader = new Uploader(this.options)

在file組件中有如下計算屬性的,分別是status和statusText

    computed: {
      // 計算出一個狀態信息
      status () {
        const isUploading = this.isUploading // 是否正在上傳
        const isComplete = this.isComplete // 是否已經上傳完成
        const isError = this.error // 是否出錯了
        const isTypeError = this.typeError // 是否出錯了
        const paused = this.paused // 是否暫停了
        const isEmpty = this.emptyError // 是否暫停了
        // 哪個屬性先不為空,就返回哪個屬性
        if (isComplete) {
          return 'success'
        } else if (isError) {
          return 'error'
        } else if (isUploading) {
          return 'uploading'
        } else if (isTypeError) {
          return 'typeError'
        } else if (isEmpty) {
          return 'emptyError'
        } else if (paused) {
          return 'paused'
        } else {
          return 'waiting'
        }
      },
      // 狀態文本提示信息
      statusText () {
        // 獲取到計算出的status屬性(相當於是個key,具體的值在下面的fileStatusText中獲取到)
        const status = this.status
        // 從file的uploader對象中獲取到 fileStatusText,也就是用自己定義的名字
        const fileStatusText = this.file.uploader.fileStatusText
        let txt = status
        if (typeof fileStatusText === 'function') {
          txt = fileStatusText(status, this.response)
        } else {
          txt = fileStatusText[status]
        }
        return txt || status
      },
    },

status綁定在html上

	<div class="uploader-file" :status="status">

對應的CSS樣式入下:

  .uploader-file[status="error"] .uploader-file-progress {
    background: #ffe0e0;
  }

綜上:有了上面代碼的編寫,我們可以直接像下面這樣控制就好了

  file.typeError = true // 表示文件的類型不符合我們的預期,不允許RD上傳
  file.error = true // 表示文件上傳失敗了
  file.emptyError = true // 表示文件為空,不允許上傳

11、後端數據庫表設計

CREATE TABLE `file_upload_detail` (                                                                               
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',                                                           
  `username` varchar(64) NOT NULL COMMENT '上傳文件的用戶賬號',                                                            
  `file_name` varchar(64) NOT NULL COMMENT '上傳文件名',                                                               
  `md5` varchar(255) NOT NULL COMMENT '上傳文件的MD5值',                                                                
  `is_uploaded` int(11) DEFAULT '0' COMMENT '是否完整上傳過 \n0:否\n1:是',                                                 
  `has_been_uploaded` varchar(1024) DEFAULT NULL COMMENT '曾經上傳過的分片號',                                             
  `url` varchar(255) DEFAULT NULL COMMENT 'bos中的url,或者是本機的url地址',                                                 
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  COMMENT '本條記錄創建時間',     
  `update_time` timestamp NULL DEFAULT NULL  COMMENT '本條記錄更新時間',                                                  
  `total_chunks` int(11) DEFAULT NULL COMMENT '文件的總分片數',                                                          
  PRIMARY KEY (`id`)                                                                                              
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8                                                             

12、關於什麼時候mergechunk

在本文中給出的demo中,merge是後端處理完成所有的chunk后,像前端返回 merge=1,這個表示來實現的。

前端拿着這個字段去發送/merge請求去合併所有的chunk。

值得注意的地方是:這個請求是在uploader.js認為所有的分片全部成功上傳后,在單個文件成功上傳的回調中執行的。我想了一下,感覺這麼搞其實不太友好,萬一merge的過程中失敗了,或者是某個chunk丟失了,chunk中的數據缺失,最終merge的產物的md5值其實並不等於原文件。當這種情況發生的時候,其實上傳是失敗的。但是後端既然告訴uploader.js 可以合併了,說明後端的upload函數認為任務是成功的。vue-simple-uploader上傳完最後一個chunk得到的狀態碼是200,它也會覺得任務是成功的,於是在前端段展示綠色的上傳成功給用戶看~(然而上傳是失敗的), 這麼看來,整個過程其實控制的不太好~

我現在的實現:直接幹掉merge請求,前端1條線程發送請求,將chunk依次發送到後端。後端檢測到所有的chunk都上傳過來後主動merge,merge完成后馬上校驗文件的md5值是否符合預期。這個處理過程在上傳最後一個chunk的請求中進行,因此可以實現的控制前端上傳成功還是失敗的樣式~
如果偏偏想追求極致的速度,可以考慮將後端更新isUpload字段的SQL換成 “select for update” 他可以鎖住你要更新的數據行
以及這一行上下的間隙,這樣就不會出現併發修改異常。前端也可以重新更換成多線程併發上傳的機制。理論上只要網絡帶寬允許你開啟五條線程,速度就快5倍。至於什麼時候merge,加個if判斷一下,當上傳過的分片數 == totalChunks 就可以merge了。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

特斯拉向企業客戶推出電動車租賃計劃 以提振需求

特斯拉(Tesla)於8日推出面向企業客戶的S型豪華跑車租賃計劃,以爭取更多的美國國內客戶。消費者可分期支付車款,在租期截止後可選擇支付餘款並擁有汽車,也可選擇將汽車歸還。有意願者在網上僅需5分鐘就可提交一份租賃申請。

此舉正逢S型豪華跑車在美國的銷量開始放慢增長。今年第一季度,S型車在美國市場售出4700輛,比去年同期僅增長1%。特斯拉公司表示,相比去年向個人客戶推出的一種功能相當於租賃計劃的推銷模式,面向中小企業的租賃計劃更為直接透明,租賃費可在企業納稅時扣抵。

S型全電動跑車是特斯拉的主打產品,去年登上美國最暢銷豪華車榜單。但市場分析人士認為,美國消費者對該系列汽車的興趣開始消退,需求進入穩定階段,因此開拓國際市場對特斯拉來說尤為重要。他們預計,中國消費者早期的強勁需求可能是提升S型車銷量的最大因素。

特斯拉電池工廠計畫面臨危機

另外,特斯拉早前宣佈將耗資數十億美元建造全球最大的電池工廠,以求大幅降低電動車成本,幫助特斯拉進入中端市場。不過,特斯拉新聞發言人西恩曾表示,特斯拉電動車電池的兩項重要原料資源,現在均面臨缺乏危機。

石墨是特斯拉電動車電池最重要的原料之一,特斯拉目前使用的石墨原料絕大部分來自日本和歐洲市場,且都是化學合成石墨,而並非天然開採的石墨。美國稀有金屬諮詢分析公司Technology Metal預計,未來需要新增6個石墨礦才能夠滿足不斷增長的電池生產對原材料的需求,石墨的價格則會繼續上漲。

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

【其他文章推薦】

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

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

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

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

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

特斯拉與阿里及租車公司合作 方便中國民眾購買電動車

電動汽車大廠特斯拉(Tesla)日前分別與阿里巴巴旗下支付寶及一嗨租車合作,中國大陸客戶不僅可用支付寶訂車付款,還可以租用特斯拉的電動車。首批兩輛Model S電動車已經正式交付,下一批將會在5月份到達。

除了阿里巴巴與一嗨租車搶著與特斯拉合作,電信巨頭之一的中國聯通日前也傳出與特斯拉中國合作的消息,將由聯通為特斯拉的電動車提供車載資訊服務。

其實電動車在大陸已經銷售多年,長城、比亞迪等大陸本土車廠都有製造電動汽車,但整體銷量始終未見起色。特斯拉卻在大陸獲得了超高人氣。

根據中國汽車工業協會的統計,2013年全國的純電動車銷量只有14,604輛,僅占全年汽車銷量的不到1%。此外,在大陸的電動車銷售業績中,大多屬於商用載客或公務車輛,民眾主動購買的純電動車數量相當稀少。

特斯拉去年全年銷售了23,500輛電動汽車。特斯拉創辦人Elon Musk日前表示,預估今年公司在全球的銷量將超過30,000輛,其中有5,000輛會來自大陸市場。該公司更計畫在大陸設立超級充電站與電動車產線。

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

【其他文章推薦】

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

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

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

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

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

明年京港澳高速全線將建成電動汽車充電服務區

據新華社報導,中國大陸湖北省即將啟動京港澳高速湖北段電動汽車快速充電設施建設。到2015年,京港澳高速公路全線將建成智慧充電服務區,屆時電動汽車可從北京開到香港。

全長2285公里的京港澳高速是中國南北交通大動脈。從今年起,國家電網公司將在京港澳高速北京至湖南段全長1500公里內,平均每38公里建設一對電動汽車智慧充電服務區,並將聯合南方電網公司和社會資本在2015年實現全線覆蓋。

目前,按照國家電網統一部署,湖北省電力公司已與湖北省交通投資有限公司對接,雙方將於近期簽署戰略合作協議,儘快啟動湖北高速公路沿線充電網路建設。

據瞭解,在京港澳高速湖北段,每個快充站將部署4臺120千瓦分體式充電機,可同時供8臺電動車充電,一般只需20分鐘即可充電80%,可提供150至200公里續航里程。

按照國家電網公司智慧充換電網路發展規劃,2020年將全面建成以「四縱四橫」(四縱:沈海、京滬、京臺、京港澳,四橫:青銀、連霍、滬蓉、滬昆)為支撐的、覆蓋國家電網公司經營區內所有示範城市的城際快充網路。

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

【其他文章推薦】

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

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

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

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

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

Model S需求來到高原區 面臨BMW電動車威脅

巴克萊銀行6日發表分析報告指出,以特斯拉目前當紅的電動轎車「Model S」在美國、挪威的需求都逐漸來到高原區的情況來看,該公司未來恐怕會面臨愈來愈多風險,尤其是將面臨來自德國知名車商BMW即將出款的電動車的威脅。

本週稍晚,BMW計劃在加州聖莫尼卡(Santa Monica)的法說會上發表插電式混合動力跑車「i8」。除了i8之外,BMW已推出了另一款電動轎車「i3」。根據報告,BMW的i系列車款不但顛覆了動力工程學的傳統概念,對車體設計、配送系統以及品牌形象也有全新理念。此外,BMW在整個製造過程中對續航力也特別重視。

BMW的i8售價為136,000美元,而i3售價則為41,350美元。相較之下,特斯拉Model S系列車款售價則在70,000-93,000美元之間。i8、i3近期內可能會侵蝕部分Model S客戶層,而BMW中期要推的i5也可能對特斯拉大眾豪華車款「Gen III」構成威脅。

巴克萊認為,BMW已將自身定位為超越特斯拉等同業的科技領航員。特斯拉的目標顧客可能剛好夾在BMW這兩款i系列車款中間。i3的車體較小,每次充電後的續航里程不如Model S,但其主打的客戶層卻與特斯拉類似;而i8則是較為高階、更為昂貴的跑車款式。

本站,BMW公司針對旗下i系列車款,還設計了專屬的太陽能充電車棚。由BMW集團所屬BMW Designworks USA所設計、安置於美國加州Malibu的太陽能充電車棚(Solar Carport)目前已展開運作,安裝於車棚頂上的太陽能面板可為BMW i車款供電。

圖:i3電動汽車

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案

TOYOTA將與FirstElement合作建立加氣站

據AUTONET報導,TOYOTA將與能源公司FirstElement攜手合作加氫站網絡的架設,預計2015-2016年能夠建成20多處的加氣站,並能在2024年實現擴建至100座的計畫。

TOYOTA於2013年底東京車展發表準量產版的FCV Concept,此後又藉由年初的2014 CES消費電子展預告2015年將率先投入北美市場販售,因此對於加氫站等後勤網絡的建設排上日程。

其實在過去的20年來,TOYOTA就已持續針對氫燃料電池進行研發,自2002年以來便投入一系列的測試車輛於北美進行實測,隨著技術的突破,使得氫燃料電池技術成本大幅下修,終於達到原廠量產的預期。

儘管FCV(氫燃料電池車)最終量產版本的相關數據原廠尚未公開,但參考FCV Concept所呈現的數據就相當令人滿意。

動力來源為分別藏在車身底下的氫燃料儲存槽及電池,具備等同於136hp馬力的100kW輸出功率,需花費10秒才能完成100km/h的加速衝刺,滿電的情況下續航力約可達到482km,而且僅需3-5分鐘的時間便能將儲存槽加滿,省去了傳統電池充電耗時的問題。

重點是氫燃料電池是以氫或含氫物質結合氧所形成的一種燃料電池,在化學反應之後僅會產生電流及水且完全沒有污染。

動力來源為分別藏在車身底下的氫燃料儲存槽及電池(圖片來源:AUTONET)

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

面試官:換人!他連哈希扣的都不懂

前言

相信你面試的時候,肯定被問過 hashCode 和 equals 相關的問題 。如:

  • hashCode 是什麼?它是怎麼得來的?有什麼用?
  • 經典題,equals 和 == 有什麼區別?
  • 為什麼要重寫 equals 和 hashCode ?
  • 重寫了 equals ,就必須要重寫 hashCode 嗎?為什麼?
  • hashCode 相等時,equals 一定相等嗎?反過來呢?

好的,上面就是靈魂拷問環節。其實,這些問題仔細想一下也不難,主要是平時我們很少去思考它。

正文

下面就按照上邊的問題順序,一個一個剖析它。扒開 hashCode 的神秘面紗。

什麼是 hashCode?

我們通常說的 hashCode 其實就是一個經過哈希運算之後的整型值。而這個哈希運算的算法,在 Object 類中就是通過一個本地方法 hashCode() 來實現的(HashMap 中還會有一些其它的運算)。

public native int hashCode();

可以看到它是一個本地方法。那麼,想要了解這個方法到底是用來幹嘛的,最直接有效的方法就是,去看它的源碼註釋。

下邊我就用我蹩腳的英文翻譯一下它的意思。。。

返回當前對象的一個哈希值。這個方法用於支持一些哈希表,例如 HashMap 。

通常來講,它有如下一些約定:

  • 若對象的信息沒有被修改,那麼,在一個程序的執行期間,對於相同的對象,不管調用多少次 hashCode 方法,都應該返回相同的值。當然,在相同程序的不同執行期間,不需要保持結果一致。
  • 若兩個對象的 equals 方法返回值相同,那麼,調用它們各自的 hashCode 方法時,也必須返回相同的結果。(ps: 這句話解答了上邊的一些問題,後面會用例子來證明這一點)
  • 當兩個對象的 equals 方法返回值不同時,那麼它們的 hashCode 方法不用保證必須返回不同的值。但是,我們應該知道,在這種情況下,我們最好也設計成 hashCode 返回不同的值。因為,這樣做有助於提高哈希表的性能。

在實際情況下,Object 類的 hashCode 方法在不同的對象中確實返回了不同的哈希值。這通常是通過把對象的內部地址轉換為一個整數來實現的。

ps: 這裏說的內部地址就是指物理地址,也就是內存地址。需要注意的是,雖然 hashCode 值是依據它的內存地址而得來的。但是,不能說 hashCode 就代表對象的內存地址,實際上,hashCode 地址是存放在哈希表中的。

上邊的源碼註釋真可謂是句句珠璣,把 hashCode 方法解釋的淋漓盡致。一會兒我通過一個案例說明,就能明白我為什麼這樣說了。

什麼是哈希表?

上文中提到了哈希表。什麼是哈希表呢?我們直接看百度百科的解釋。

用一張圖來表示它們的關係。

左邊一列就是一些關鍵碼(key),通過哈希函數,它們都會得到一個固定的值,分別對應右邊一列的某個值。右邊的這一列就可以認為是一張哈希表。

而且,我們會發現,有可能有些 key 不同,但是它們對應的哈希值卻是一樣的,例如 aa,bb 都指向 1001 。但是,一定不會出現同一個 key 指向不同的值。

這也非常好理解,因為哈希表就是用來查找 key 的哈希地址的。在 key 確定的情況下,通過哈希函數計算出來的 哈希地址,一定也是確定的。如圖中的 dd 已經確定在 1002 位置了,那麼就不可能再佔據 1003 位置。

思考一下,如果有另外一個元素 ee 來了,它的哈希地址也落在 1002 位置,怎麼辦呢?

hashCode 有什麼用?

其實,上圖就已經可以說明一些問題了。我們通過一個 key 計算出它的 hashCode 值,就可以唯一確定它在哈希表中的位置。這樣,在查詢時,就可以直接定位到當前元素,提高查詢效率。

現在我們假設有這樣一個場景。我們需要在內存中的一塊兒區域存放 10000 個不同的元素(以aa,bb,cc,dd 等為例)。那怎麼實現不同的元素插入,相同的元素覆蓋呢?

我們最容易想到的方法就是,每當存一個新元素時,就遍歷一遍已經存在的元素,看有沒有相同的。這樣雖然也是可以實現的,但是,如果已經存在了 9000 個元素,你就需要去遍歷一下這 9000 個元素。很明顯,這樣的效率是非常低下的。

我們轉換一種思路,還是以上圖為例。若來了一個新元素 ff,首先去計算它的 hashCode 值,得出為 1003 。發現此處還沒有元素,則直接把這個新元素 ff 放到此位置。

然後,ee 來了,通過計算哈希值得到 1002 。此時,發現 1002 位置已經存在一個元素了。那麼,通過 equals 方法比較它們是否相等,發現只有一個 dd 元素,很明顯和 ee 不相等。那麼,就把 ee 元素放到 dd 元素的後邊(可以用鏈表形式存放)。

我們會發現,當有新元素來的時候,先去計算它們的哈希值,再去確定存放的位置,這樣就可以減少比較的次數。如 ff 不需要比較, ee 只需要和 dd 比較一次。

當元素越來越多的時候,新元素也只需要和當前哈希值相同的位置上,已經存在的元素進行比較。而不需要和其他哈希值不同的位置上的元素進行比較。這樣就大大減少了元素的比較次數。

圖中為了方便,畫的哈希表比較小。現在假設,這個哈希表非常的大,例如有這麼非常多個位置,從 1001 ~ 9999。那麼,新元素插入的時候,有很大概率會插入到一個還沒有元素存在的位置上,這樣就不需要比較了,效率非常高。但是,我們會發現這樣也有一個弊端,就是哈希表所佔的內存空間就會變大。因此,這是一個權衡的過程。

有心的同學可能已經發現了。我去,上邊的這個做法好熟悉啊。沒錯,它就是大名鼎鼎的 HashMap 底層實現的思想。對 HashMap 還不了解的,趕緊看這篇文章理一下思路。HashMap 底層實現原理及源碼分析

所以,hashCode 有什麼用。很明顯,提高了查詢,插入元素的效率呀。

equals 和 == 有什麼區別?

這是萬年不變,經久不衰的經典面試題了。讓我油然想起,當初為了面試,背誦過的面經了,簡直是一把心酸一把淚。現在還能記得這道題的標準答案:equals 比較的是內容, == 比較的是地址。

當時,真的就只是背答案,知其然而不知其所以然。再往下問,為什麼要重寫 equals ,就懵逼了。

首先,我們應該知道 equals 是定義在所有類的父類 Object 中的。

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

可以看到,它的默認實現,就是 == ,這是用來比較內存地址的。所以,如果一個對象的 equals 不重寫的話,和 == 的效果是一樣的。

我們知道,當創建兩個普通對象時,一般情況下,它們所對應的內存地址是不一樣的。例如,我定義一個 User 類。

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User() {

    }
}

public class TestHashCode {
    public static void main(String[] args) {
        User user1 = new User("zhangsan", 20);
        User user2 = new User("lisi", 18); 

        System.out.println(user1 == user2);
        System.out.println(user1.equals(user2));
    }
}
// 結果: false	false

很明顯,zhangsan 和 lisi 是兩個人,兩個不同的對象。因此,它們所對應的內存地址不同,而且內容也不相等。

注意,這裏我還沒有對 User 重寫 equals,實際此時 equals 使用的是父類 Object 的方法,返回的肯定是不相等的。因此,為了更好地說明問題,我僅把第二行代碼修改如下:

//User user2 = new User("lisi", 18);
User user2 = new User("zhangsan", 20);

讓 user1 和 user2 的內容相同,都是 zhangsan,20歲。按我們的理解,這雖然是兩個對象,但是應該是指的同一個人,都是張三。但是,打印結果,如下:

這有悖於我們的認知,明明是同一個人,為什麼 equals 返回的卻不相等呢。因此,此時我們就需要把 User 類中的 equals 方法重寫,以達到我們的目的。在 User 中添加如下代碼(使用 idea 自動生成代碼):

public class User {
    ... //省略已知代碼
        
    @Override
    public boolean equals(Object o) {
        //若兩個對象的內存地址相同,則說明指向的是同一個對象,故內容一定相同。
        if (this == o) return true;
        //類都不是同一個,更別談相等了
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        //比較兩個對象中的所有屬性,即name和age都必須相同,才可認為兩個對象相等
        return age == user.age &&
                Objects.equals(name, user.name);
    }
   
}
//打印結果:  false 	true

再次執行程序,我們會發現此時 equals 返回 true ,這才是我們想要的。

因此,當我們使用自定義對象時。如果需要讓兩個對象的內容相同時,equals 返回 true,則需要重寫 equals 方法。

為什麼要重寫 equals 和 hashCode ?

在上邊的案例中,其實我們已經說明了為什麼要去重寫 equals 。因為,在對象內容相同的情況下,我們需要讓對象相等。因此,不能用 Object 類的默認實現,只去比較內存地址,這樣是不合理的。

那 hashCode 為什麼要重寫呢? 這就涉及到集合,如 Map 和 Set (底層其實也是 Map)了。

我們以 HashMap JDK1.8的源碼來看,如 put 方法。

我們會發現,代碼中會多次進行 hash 值的比較,只有當哈希值相等時,才會去比較 equals 方法。當 hashCode 和 equals 都相同時,才會覆蓋元素。get 方法也是如此(先比較哈希值,再比較equals),

只有 hashCode 和 equals 都相等時,才認為是同一個元素,找到並返回此元素,否則返回 null。

這也對應 “hashCode 有什麼用?”這一小節。 重寫 equals 和 hashCode 的目的,就是為了方便哈希表這樣的結構快速的查詢和插入。如果不重寫,則無法比較元素,甚至造成元素位置錯亂。

重寫了 equals ,就必須要重寫 hashCode 嗎?

答案是肯定的。首先,在上邊的 JDK 源碼註釋中第第二點,我們就會發現這句說明。其次,我們嘗試重寫 equals ,而不重寫 hashCode 看會發生什麼現象。

public class TestHashCode {
    public static void main(String[] args) {
        User user1 = new User("zhangsan", 20);
        User user2 = new User("zhangsan", 20);

        HashMap<User, Integer> map = new HashMap<>();
        map.put(user1,90);
        System.out.println(map.get(user2));
    }
}
// 打印結果: null

對於代碼中的 user1 和 user2 兩個對象來說,我們認為他是同一個人張三。定義一個 map ,key 存儲 User 對象, value 存儲他的學習成績。

當把 user1 對象作為 key ,成績 90 作為 value 存儲到 map 中時,我們肯定希望,用 key 為 user2 來取值時,得到的結果是 90 。但是,結果卻大失所望,得到了 null 。

這是因為,我們自定義的 User 類,雖然重寫了 equals ,但是沒有重寫 hashCode 。當 user1 放到 map 中時,計算出來的哈希值和用 user2 去取值時計算的哈希值不相等。因此,equals 方法都沒有比較的機會。認為他們是不同的元素。然而,其實,我們應該認為 user1 和 user2 是相同的元素的。

用圖來說明就是,user1 和 user2 存放在了 HashMap 中不同的桶裡邊,導致查詢不到目標元素。

因此,當我們用自定義類來作為 HashMap 的 key 時,必須要重寫 hashCode 和 equals 。否則,會得到我們不想要的結果。

這也是為什麼,我們平時都喜歡用 String 字符串來作為 key 的原因。 因為, String 類默認就幫我們實現了 equals 和 hashCode 方法的重寫。如下,

// String.java
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        //從前向後依次比較字符串中的每個字符
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
		//把字符串中的每個字符都取出來,參与運算
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        //把計算出來的最終值,存放在hash變量中。
        hash = h;
    }
    return h;
}

重寫 equals 時,可以使用 idea 提供的自動代碼,也可以自己手動實現。

public class User {
    ... //省略已知代碼
        
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
   
}
//此時,map.get(user2) 可以得到 90 的正確值

在重寫了 hashCode 后,使用自定義對象作為 key 時,還需要注意一點,不要在使用過程中,改變對象的內容,這樣會導致 hashCode 值發生改變,同樣得不到正確的結果。如下,

public class TestHashCode {
    public static void main(String[] args) {
        User user = new User("zhangsan", 20);

        HashMap<User, Integer> map = new HashMap<>();
        map.put(user,90);
        System.out.println(map.get(user));
        user.setAge(18); //把對象的年齡修改為18
        System.out.println(map.get(user));
    }
}
// 打印結果:
// 90
// null

會發現,修改后,拿到的值是 null 。這也是,hashCode 源碼註釋中的第一點說明的,hashCode 值不變的前提是,對象的信息沒有被修改。若被修改,則有可能導致 hashCode 值改變。

此時,有沒有聯想到其他一些問題。比如,為什麼 String 類要設計成不可以變的呢?這裏用 String 作為 HashMap 的 key 時,可以算作一個原因。你肯定不希望,放進去的時候還好好的,取出來的時候,卻找不到元素了吧。

String 類內部會有一個變量(hash)來緩存字符串的 hashCode 值。只有字符串不可變,才可以保證哈希值不變。

hashCode 相等時,equals 一定相等嗎?

很顯然不是的。在 HashMap 的源碼中,我們就能看到,當 hashCode 相等時(產生哈希碰撞),還需要比較它們的 equals ,才可以確定是否是同一個對象。因此,hashCode 相等時, equals 不一定相等 。

反過來,equals 相等的話, hashCode 一定相等嗎? 那必須的。equals 都相等了,那說明在 HashMap 中認為它們是同一個元素,所以 hashCode 值必須也要保證相等。

結論:

  • hashCode 相等,equals 不一定相等。
  • hashCode 不等,equals 一定不等。
  • equals 相等, hashCode 一定相等。
  • equals 不等, hashCode 不一定不等。

關於最後這一點,就是 hashCode 源碼註釋中提到的第三點。當 equals 不等時,不用必須保證它們的 hashCode 也不相等。但是為了提高哈希表的效率,最好設計成不等。

因為,我們既然知道它們不相等了,那麼當 hashCode 設計成不等時。只要比較 hashCode 不相等,我們就可以直接返回 null,而不必再去比較 equals 了。這樣,就減少了比較的次數,無疑提高了效率。

結尾

以上就是 hashCode 和 equals 相關的一些問題。相信已經可以解答你心中的疑惑了,也可以和面試官侃侃而談。再也不用擔心,面試官說換人了。

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

【其他文章推薦】

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

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

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

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

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

談談我對 Flutter 未來發展 和 “嵌套地獄” 的淺顯看法

Flutter 未來發展

提到 Flutter 就不得不提到 Fuchsia 系統,這是一個尚未正式發布的操作的系統,引用 Android 和 Chrome 的高級副總裁 Hiroshi Lockheimer 在一檔播客節目中對 Fuchsia 的介紹是:

不僅僅是手機和個人電腦,在物聯網的世界里,越來越多的設備需要操作系統、新的軟件運行環境等支持。我認為,在具有不同優勢和專業化的諸多操作系統中還存在很大的發展空間。Fuchsia 就是其中之一,所以,請繼續保持關注。

是的,Fuchsia 系統是為物聯網研發的操作系統,物聯網簡稱 IoT,現在全世界都在押注 IoT,包括華為、小米等國內公司。

那 Flutter 和 Fuchsia 又有什麼關係呢?

Flutter 是 Fuchsia 官方指定的唯一UI開發框架。

現在有很多物聯網操作系統 ,Fuchsia 就一定可以脫穎而出嗎?

不一定,未來的事情誰說的准呢,但在我看來 Fuchsia 是最有可能發展起來的物聯網操作系統,因為一個操作系統的發展除了本身優秀以外,最大的阻礙其實是生態,而 Fuchsia 在生態方面具有天然的優勢, 國外的一篇報道曾說:

Google 希望將 Android App 無縫移植到 Fuchsia 上,而且一直在做相關工作。

試想一下,一旦 Google 將 Android App 無縫移植到 Fuchsia 上,其他物聯網操作系統如何與之抗衡。

這裏引用 Google 公眾號底部的一句話送給大家:

預測未來不如創造未來

在跨平台技術上 Flutter 還有很多競爭對手,比如 HTML5、React Native、Weex、快應用、小程序等,我曾在跨平台技術發展簡介 中詳細說明了各個跨平台技術的發展歷史及優缺點。

Flutter 的出現會終結其他跨平台技術?我想不會的, React Native 發展了這麼多年也沒有完全乾掉 HTML5,應為 HTML5 有其獨特的應用場景,比如 營銷活動場景、新聞或者博客詳情頁面等,這些場景非常適合 HTML5。因此 Flutter 也不可能終結其他跨平台技術,總結一句話就是:

未來很長一段時間,將會是跨平台技術共存的時代,但 Flutter 適用場景更為廣闊。

Flutter 嵌套地獄

現在網絡上對 Flutter 吐槽最多大概就是 Flutter “嵌套地獄”寫法了,為什麼會出現這種現象?個人認為最大的原因就是目前大部分開源的 Flutter 項目都是這種嵌套寫法(包括我自己以前也是如此),導致後來的初學者認為這麼寫沒有問題,當項目越來越複雜時,這種嵌套寫法給項目的維護帶來了巨大的挑戰。下面說說如何避免這種嵌套寫法?

比如實現如下效果:

嵌套地獄 的寫法:

@override
Widget build(BuildContext context) {
  return Column(
    children: <Widget>[
      Container(
        height: 45,
        child: Row(
          children: <Widget>[
            SizedBox(
              width: 30,
            ),
            Icon(
              Icons.notifications,
              color: Colors.blue,
            ),
            SizedBox(
              width: 30,
            ),
            Expanded(
              child: Text('消息中心'),
            ),
            Container(
              padding: EdgeInsets.symmetric(horizontal: 10),
              decoration: BoxDecoration(
                  shape: BoxShape.rectangle,
                  borderRadius: BorderRadius.all(Radius.circular(50)),
                  color: Colors.red),
              child: Text(
                '2',
                style: TextStyle(color: Colors.white),
              ),
            ),
            SizedBox(
              width: 15,
            ),
          ],
        ),
      ),
      Divider(),
      //類似上面的布局寫6個
    ],
  );
}

上面還僅僅是第一項的布局,下面還有7個,一個30多行代碼,7個就是200多行的布局代碼,這還僅僅是布局代碼,如果加上邏輯,都不敢想象啊。

或許有一點封裝思想開發者會將每一個 Item封裝為一個方法,寫法如下:

_buildItem(IconData iconData, Color iconColor, String title, Widget widget) {
  return Container(
    height: 45,
    child: Row(
      children: <Widget>[
        SizedBox(
          width: 30,
        ),
        Icon(
          iconData,
          color: iconColor,
        ),
        SizedBox(
          width: 30,
        ),
        Expanded(
          child: Text('$title'),
        ),
        widget,
        SizedBox(
          width: 15,
        ),
      ],
    ),
  );
}

@override
Widget build(BuildContext context) {
  return Column(
    children: <Widget>[
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
    ],
  );
}

這樣看起來好多了,基本解決了嵌套地獄問題,但這樣寫還存在一個非常大的問題-性能問題,一旦其中一個数字發生變化,整個頁面都要重建,Flutter 開發中非常重要的一個原則就是 盡可能少的重建組件,因此將上面封裝到方法中組件變為一個 Widget。

class SettingDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        _SettingItem(
          iconData: Icons.notifications,
          iconColor: Colors.blue,
          title: '消息中心',
          suffix: _NotificationsText(
            text: '2',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.thumb_up,
          iconColor: Colors.green,
          title: '我贊過的',
          suffix: _Suffix(
            text: '121篇',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.grade,
          iconColor: Colors.yellow,
          title: '收藏集',
          suffix: _Suffix(
            text: '2個',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.shopping_basket,
          iconColor: Colors.yellow,
          title: '已購小冊',
          suffix: _Suffix(
            text: '100個',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.account_balance_wallet,
          iconColor: Colors.blue,
          title: '我的錢包',
          suffix: _Suffix(
            text: '10萬',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.location_on,
          iconColor: Colors.grey,
          title: '閱讀過的文章',
          suffix: _Suffix(
            text: '1034篇',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.local_offer,
          iconColor: Colors.grey,
          title: '標籤管理',
          suffix: _Suffix(
            text: '27個',
          ),
        ),
      ],
    );
  }
}

class _SettingItem extends StatelessWidget {
  const _SettingItem(
      {Key key, this.iconData, this.iconColor, this.title, this.suffix})
      : super(key: key);

  final IconData iconData;
  final Color iconColor;
  final String title;
  final Widget suffix;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 45,
      child: Row(
        children: <Widget>[
          SizedBox(
            width: 30,
          ),
          Icon(iconData,color: iconColor,),
          SizedBox(
            width: 30,
          ),
          Expanded(
            child: Text('$title'),
          ),
          suffix,
          SizedBox(
            width: 15,
          ),
        ],
      ),
    );
  }
}

class _NotificationsText extends StatelessWidget {
  final String text;

  const _NotificationsText({Key key, this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 10),
      decoration: BoxDecoration(
          shape: BoxShape.rectangle,
          borderRadius: BorderRadius.all(Radius.circular(50)),
          color: Colors.red),
      child: Text(
        '$text',
        style: TextStyle(color: Colors.white),
      ),
    );
  }
}

class _Suffix extends StatelessWidget {
  final String text;

  const _Suffix({Key key, this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      '$text',
      style: TextStyle(color: Colors.grey.withOpacity(.5)),
    );
  }
}

封裝為一個個單獨的小組件,將有變化的組件盡量單獨封裝,這樣就不會重建整個控件樹,增強了可讀性和可維護性,而且對性能有很大的提升。

最後總結一句:

雖然 Flutter 一切皆是組件,但並不代表一切都要寫在組件中。

當然這僅僅是我個人的看法,如果您有更好的方法歡迎一起討論,從我做起,規範寫法,為 Flutter 發展貢獻做出一點微不足道的貢獻。

交流

老孟Flutter博客地址(330個控件用法):http://laomengit.com

歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:

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

【其他文章推薦】

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

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

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

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

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