中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java中值對象的作用是什么

發布時間:2021-07-01 15:39:54 來源:億速云 閱讀:606 作者:Leah 欄目:編程語言

本篇文章給大家分享的是有關Java中值對象的作用是什么,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。

值類型與值對象


我們都知道,Java 語言中的類型分為兩種:基本類型(primitive type)和引用類型(reference type),這不僅是語言層面的特性,也由 JVM 內在實現支持  [1]  。

其中,基本類型指是的 8 種基本的數值類型:boolean、byte、char、int、short、long、float、double;而引用類型,指的是對程序中創建的對象的引用,可以理解為指向對象的指針或句柄。Java 號稱一切皆是對象,很可惜,這并不是事實,基本類型就不是對象。

那么,值類型又是什么呢?

在你編寫程序時,是否經常會遇到一些需要表達數值或其它類型值的場景?比如復數、向量、顏色值、坐標點、時間、日期等。這些值通常無法用基本類型來表達,一則它可能是多個屬性構成,二則針對值的一些操作或邏輯我們希望跟數據封裝在一起,比如向量的點乘、叉乘、取模等。但如果使用對象來表達同樣也會產生很多問題:

?  相等性比較
對這些對象的比較是有意義的,但是默認情況下 Java 對象比較的是地址,因此直接比較的結果通常不是我們期待的行為:

Java中值對象的作用是什么

?  可變性

對引用類型的賦值、方法傳參等會生成多個引用,這些引用都指向同一個對象。這在一些情況下是沒有問題的,但在某些場景下可能導致對象發生預期之外的變化。如:

Java中值對象的作用是什么

上面的 case 比較簡單,只要對 Date 的特性有些了解就不會犯這樣的錯誤。但如果對象經過多次傳遞,使用的位置離創建的位置很遠的話,我們就未必能這么謹慎了。這種問題,Martin Flower 稱之為   aliasing bug[2]  。

?  性能

上面兩點其實都容易解決,只是每個實現需要寫很多樣板代碼。需要比較的對象只要重寫   equals()   和   hashCode   方法即可;對于可變性問題,可以將對象設計為不可變對象,在修改時返回一個深拷貝副本來供客戶端操作。滿足上述兩種條件的對象,我們可以稱之為值對象。

那么,通過“對象”來實現我們對這種數據結構的訴求,是否是最好的方式呢?

我們知道,Java 中的對象通常是分配在堆上,通過引用來進行操作,不過這不是必然的。JVM 有一項技術叫  逃逸分析[3]  ,可以在運行時分析出一個方法中創建的對象是否會逃逸到方法或線程外部,如果沒有逃逸,可以進而執行一些編譯優化,比如棧上分配、同步消除、標量替換等。如果一個對象被分配到棧上,就意味著當方法結束后就會自動銷毀,省去了 GC 的開銷,這對于優化應用內存占用和 GC 停頓時間來說,無疑是個好消息;而標量替換意味著壓根就不會創建對象,相關數據被替換成基本類型數據直接分配到棧上,不僅省去了對象操作相關開銷,也更利于 CPU 高速緩存或寄存器進行優化。

對于值對象來說,一般極少有共享的需求,假如能直接在棧上進行分配,那么將省去對象的存儲、訪問和 GC 的成本,對程序性能非常有利。不過進行逃逸分析也是有成本的,如果在語言層面直接支持的話,就可以進一步減少編譯時分析的開銷。不過,目前 Java 語言還做不到這一點。

當一門編程語言為上述類型的數據結構提供內在支持時,該類型可稱之為值類型。而對于滿足上述訴求的實例,無論是基于值類型實現還是普通對象類型實現,我們都可以稱之為值對象。

不同編程語言對值類型的支持

?  Java
上面已經說過,Java 語言層面原生并不支持值類型。不過,它提供了許多具有值類型特點的類,比如:8個基本類型對應的封裝類、String、BigDecimal 等,這些類的共同特點之一就是不可變性,同時也都對比較操作做了實現,因此都可看作值對象。另外一個應該設計為不可變、但實際可變的類是 java.util.Date 類,也因為如此,Date 類飽受詬病。在 Java 8 中官方正式推出新的 時間/日期 API,試圖取代 Date 相關接口,這些新的類全部被設計成了不可變類。

對于Java 是否應該從語言層面支持值類型的討論由來已久,比如這篇  JEP提案[4]  早在 2012 時就提議支持值對象;oracle 論壇上的這篇  博客[5]  也對如何實現值對象做了探討。最近有兩篇提案,一個提出了   Primitive Object  [6]  的概念,可算是值類型的一種實現;另外一篇提議  基于Primitive Object統一基本類型與對象類型[7]  。不過,這兩個提案仍處于   Submitted   階段(JEP 提案從提出到發布的流程有幾個階段,可以看     這里  [8]   Process states 一節),能否被采納、實現乃至發布到正式版本,還是未知之數。

?  C++

C++ 中沒有值對象這一概念,不過在創建對象時,允許開發者選擇在堆上還是在棧上創建。比如下面的示例代碼,直接通過   A a;   的方式創建的對象是分配在棧上的,而通過   new A();   的方式創建的對象分配在堆上,并且返回一個指向該對象的指針。在棧上創建的對象在函數執行結束時會自動銷毀。

更進一步,對 A 類型的對象進行賦值(34行)或方法傳參(38行)時,會產生一次拷貝操作,生成一個新的對象,新對象的作用域分別為當前函數和被調函數,相應函數執行結束時也會被銷毀。而對指針類型的對象進行賦值(43行)和方法傳參(45行)時,盡管創建了新的指針對象,新的指針仍然指向相同的對象。

可見 C++ 中對類類型和指針類型的使用,分別具有值類型和引用類型的一些特點。

Java中值對象的作用是什么

?  C#
C# 語言中是明確的提出了  值類型[9]  這一概念的,struct 就是一種值類型。MSDN文檔中說明:“默認情況下,在分配中,通過將實參傳遞給方法并返回方法結果來復制變量值。” 在賦值操作時,也同樣會對對象進行拷貝。如下面的代碼所示,我們可以看到將 p1 賦值給 p2,p2 修改狀態后,p1 中的數據仍然保持不變。

另外,在 C# 中值類型是分配在棧上的,值類型與引用類型之間可以進行轉化,稱之為裝箱和拆箱,上面的 Java Primitive Object 提案似乎也借鑒了 C# 的設計思想。

Java中值對象的作用是什么

?  其它語言

其它編程語言對值類型的支持不盡相同。以函數式編程為例,大多數函數式編程語言中變量都是不可變的,因此在函數式語言中定義的數據結構都可看作是值類型。

DDD 中的值對象

盡管 Java 并沒有對值對象提供語言層面的類型支持,但這并不妨礙我們在自己的代碼中創建事實上的值對象。實際上值對象[10]的定義可以并不僅限于類似向量、顏色值、坐標點這樣一些使用范圍。Martin Flower 認為,  值對象  在編程中的作用被極大的忽視了,善于值對象可以非常有效的簡化你的系統代碼;Vaughn Vernon 在《實現領域驅動設計》一書中甚至說,我們應該盡量使用值對象建模而不是實體對象。實際上,當提到“值對象”這個概念時,最常見的就是在 DDD(領域驅動設計)這個上下文中。

Eric Evans 在《領域驅動設計 軟件核心復雜性應對之道》一書中提出了實體(Enity)與值對象(Value Object)的概念。Vaughn Vernon 在《實現領域驅動設計》中做了進一步闡述。

在 DDD 中,實體代表具有個性特征或需要區分不同個體的對象,它具有唯一標識和可變性。對于實體對象,我們首要考慮的并不是其屬性,而是能代表其本質特征的唯一標識,無論對象屬性如何變化,它都是同一個對象,它的生命周期具有連續性,甚至對對象進行持久化存儲然后基于存儲來重建對象,它仍然是同一個對象的延續。

而值對象,它通常是一些屬性的集合,是對對象的度量和描述。值對象應該是不可變的,當度量和描述改變時,可以用另外一個值對象替換。值可以跟其它值對象進行相等性比較。

可以看到,在 DDD 中的值對象的定義跟我們上面的描述非常相似。《實現領域驅動設計》對于值對象的闡述非常詳盡,想要進一步了解的可以閱讀該書第 6 章內容。

使用值對象的好處

因為值對象通常設計為不可變對象,因此值對象的好處首先就是不可變對象的好處。另外在支持值類型的語言中,值對象的創建、操作、銷毀會有更好的性能。

?  線程安全
在 Java 編程語言中,出現線程安全問題的必要條件有兩個:對象狀態被多個線程共享;對象狀態可變。因此解決線程安全問題的思路也主要從幾個方向出發:無狀態;狀態不可變;不共享狀態;通過同步機制來序列化對象狀態的訪問。

而不可變對象狀態是不變的,因此是線程安全的,可以放心應用到并發環境中,無需額外的同步機制在多個線程中共享。

?  避免 Alias Bug

Aliasing bug 的概念上文已經講過,主要是指多個對象的引用被分享到多個環境中后,在某個環境的改動會導致從另外一個環境中看到預期之外的變化。

最近我們的項目中就遇到這樣一個 bug,某個對象會被緩存到本地內存中,取出對象后,返回給 UI 層的某個屬性值需要根據請求環境做一些判斷與變更,由于未做防御性拷貝,導致變化污染了緩存對象,后面的請求出現錯誤的結果。

而不可變對象不允許修改屬性值,任何狀態的變化必須通過創建副本來實現,因此可以有效的避免該類 bug。

?  簡化邏輯復雜程度
  • 任何使用到值對象的地方,它的狀態始終是合法的。通常不可變對象會在創建時進行自校驗,因此一旦創建完成,它始終處于合法有效的狀態之中,沒有任何行為能使破壞它的一致性狀態。

  • 可以安全的共享給其它對象、其它線程,而不用擔心狀態發生變化,簡化了代碼維護者對流程、邏輯的理解。

  • 可以作為構件簡化其它對象的狀態管理。當其它對象使用不可變對象作為其構件時,由于不可變對象自身狀態不變,使得它在被傳入和獲取時不需要進行防御性拷貝,簡化了對象狀態的跟蹤。

     
?  使你的設計更清晰
值對象與基礎類型數據相比,富含業務語義,在任何使用到它的地方,其含義一看便知。它還可以封裝跟數據相關的業務邏輯,避免為了復用代碼而創建 util 類,更符合面向對象的思想。

?  可比較、可以被集合類使用

相信這一點不需要再說明了。

值對象 Java 實踐


那么,如何在我們的代碼中創建不可變對象呢?我們分為部分內容來講,第一部分是指導思想,第二部分是如何進行實踐。

?  值對象創建指南
  • 創建不可變對象
在 《Effective Java 第三版》 第 17 條 最小化可變性一節中,將不可變類的設計歸納為五條原則:
  • 不要提供修改對象狀態的方法
  • 確保這個類不能被繼承
  • 把所有屬性設置為 final
  • 把所有的屬性設置為 private
  • 確保對任何可變組件的互斥訪問

第 2、3、4 點很容易理解。對第 1 點,也就是說對任何涉及狀態變更的操作,都不能直接修改原始對象的狀態,而是通過創建對象的副本,比如下面對復數對象的“加”操作:

Java中值對象的作用是什么

對于第 2 點,確保類不能被繼承,除了將類設為 final,還有一種方式是將構造方法設為 private,并向外提供靜態工廠方法來創建實例。

Java中值對象的作用是什么

而第 5 點的意思是,“如果你的類有任何引用可變對象的屬性,請確保該類的客戶端無法獲得 對這些對象的引用”。舉例而言,下面的 Period 類,盡管滿足上面的 1~4 點,但由于其狀態變量中包含了引用對象,引用對象通過構造方法與訪問方法與外界共享,導致它的狀態也會發生變化(第 7 行、第 10 行):

Java中值對象的作用是什么

一個解決方案是,不使用 Date 對象,而是使用 Java 8 中提供的 LocalDate 對象,該對象是不可變的。另一種方案,在引用共享的位置對對象進行拷貝。

由此可以延伸出:
  • 盡可能使用不可變對象作為構建對象的組件;

  • 必要時對構造方法參數和方法返回值進行防御性拷貝:(第 6、7、14、18 行)


Java中值對象的作用是什么

這里還要注意幾點:
  • 進行防御性拷貝應在參數檢查之前執行,以避免參數檢查可拷貝期間受其它線程對參數更改的影響。

  • 必要時,對實現 serializable 接口的類進行反序列化重寫 readObject 方法,以避免字節碼攻擊。對于這一點,簡單來講就是由于 Java 對象的反序列默認通過 readObject 方法重建對象,而不會調用我們提供的構造方法,這使得攻擊者可以通過修改字節碼數據,從而繞開構造方法中的參數校驗的防御性拷貝。具體可以看 《Effective Java 第三版》 第 88 條 保護性的編寫 readObject 方法。

  • 當構造方法參數過多時,可以借助 builder 設計模式
這一點可參照《Effective Java 第三版》 第 2 條。這里不展開了。

  • 盡可能重用實例
由于不變對象在修改數據時會進行拷貝,因此它的一個主要問題就是可能會創建過多的對象,這會帶來性能問題。一個方案是,對可能會經常用到的對象提供公共的靜態 final 常量。這一點,既可以通過公共的常量字段來實現,也可以通過靜態工廠方法來實現。

  • 相等性判斷
需要重寫 equals() 和 hashCode() 方法。至于為什么以及如何實現,相信大家都知道了,就不展開講了。

  • 創建即合法
這一點也很好理解,既然值對象是不可變的,那么創建完成之后沒有任何方法可以改變的狀態,因此必須在構造時進行必要的合法性校驗,使創建出來的對象滿足其所有的不變性條件(Invariants)。

?  如何實現
  • 手寫代碼
有了指導思想,如何實現其實就一目了然了。只不過,要實現不可變對象,需要創建大量的樣板代碼,比如 equals() 和 hashCode() 方法的重寫、builder 模式的創建等等。這些重復代碼不僅寫起來費力,而且會使類的核心業務邏輯隱藏在大量的樣板代碼中,降低了類的可讀性。因此,最好實現方式還是借且代碼生成工具。

  • 基于代碼生成工具
(i) lombok @value 注解
lombok 庫的 @value 注解可以很方便的幫我們生成一個不可變的值對象類型。如:

Java中值對象的作用是什么

如果我們使用 Intellij IDEA 工具,并且安裝了 lombok 插件,可以在源代碼處 右鍵 -> Refactor -> Delombok -> All lombok annotations,來查看 lombok 注解處理器處理過后生成的字節碼對應的源代碼大概是什么樣子。

Java中值對象的作用是什么

這里有一點需要注意,lombok 工具對于引用類型不會幫我們做防御性拷貝,因此假如我們的構成組件包含可變對象,需要我們自己去做防御性拷貝。做法很簡單,只要提供我們自己的構造方法和 get 方法,lombok 就不會再幫我們生成對應的方法。

Java中值對象的作用是什么

如果我們要對參數進行合法性校驗,也同樣需要提供自定義的構造方法,在構造方法中添加校驗邏輯。
(ii) lombok @Builder 注解
lombok 的 @Builder 注解非常強大,可以應用在類上、構造方法上,也可以應用在靜態工廠方法上。在構建時未傳入的參數為該類型的默認值。同樣的,如果你需要校驗,可提供自定義的全參數構造方法。

Java中值對象的作用是什么

上面我們提到過,對值對象的實例盡可能的重用。如果我們使用靜態工廠方法,就可以實現這一點:

Java中值對象的作用是什么

注意我們把 @Builder 注解放在了   of()   靜態工廠方法上面,同時將構造方法設為 private。通過查看生成的代碼,發現 builder 的   build()   方法直接調用了該工廠方法。

(iii) lombok @With 注解
@Value 注解會將生成的類設為不可變,如果我們需要修改對象的狀態,怎么辦?上面說過,修改狀態需要創建拷貝。使用 @With 注解可以很方便的做到這一點。

Java中值對象的作用是什么
(iv) 與 mapstruct 配合使用
在進行領域驅動設計時,我們經常會在不同的層或者模塊之間使用不同的對象,比如持久化層使用跟數據庫紀錄進行映射的 DO 對象,而在領域層使用更具有業務意義的領域對象。如何在對象之間進行屬性的拷貝呢?可以有很多種選擇,我最常用的是 mapstruct 工具,該工具非常強大,不僅支持不同名稱、不同類型字段的映射,還可以使用表達式、方法調用等。

對于它我們不做過多介紹,有興趣可以看  這里[11]  。

在進行屬性拷貝時,通常基于無參構造函數創建對象,然后設置對應屬性。但是上面的類,我們在實現不可變特性時,不再提供無參構造函數。如何讓 mapstruct 支持這種類呢?恭喜你,只要加了 @Builder 注解,什么都不需要做,mapstruct 已經內置提供了對 lombok @Builder 注解的支持。

至于使用其它手段的屬性拷貝,我暫時沒有去了解,熟悉的同學可以參與討論。

(v) json 反序列化

我們知道,當使用 json 反序列化工具生成自定義類型的實例時,通常也是使用該類型的默認無參構造方法。假如沒有該構造方法,運行時就會拋出異常。但是,我們不希望提供該構造方法來破壞對象的不可變性。怎么辦呢?

這里又要祭出 lombok 的另一法寶,@Jacksonized 注解。加上這一注解后,我們的不可變對象就可以被 jackson json 庫順利的創建出來了(需要跟 @Builder 一起使用)。其實這個注解沒什么復雜之處,能實現這點得益于 jackson json 庫本身對 builder 模式的支持,@Jacksonized 注解只是按照 jackson json 的相關要求生成相關的 builder 類和方法而已。目前 fastjson 庫似乎不支持使用 builder 模式來創建對象,不知道后面有沒有相關的計劃。

以上就是Java中值對象的作用是什么,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

新乡市| 遂平县| 德庆县| 车致| 衡阳市| 朝阳县| 军事| 泸西县| 威信县| 平南县| 通榆县| 宾阳县| 榆社县| 项城市| 廊坊市| 沁阳市| 惠水县| 邯郸县| 禹州市| 盐亭县| 迁西县| 聊城市| 京山县| 铜梁县| 温宿县| 黄龙县| 台中市| 六盘水市| 原平市| 枣庄市| 舒城县| 泰安市| 微山县| 虹口区| 炉霍县| 万年县| 乌海市| 称多县| 海阳市| 宁都县| 济南市|