1. 程式人生 > >Java為什麽需要保留基本數據類型

Java為什麽需要保留基本數據類型

import -o math return ESS 轉化 面向 containe 版本

基本數據類型對以數值計算為主的應用程序來說是必不可少的。

自從1996年Java發布以來,基本數據類型就是Java語言的一部分。John Moore通過對使用基本類型和不使用基本類型做java基準測試給Java中為什麽要保留基本數據類型做了一個很有力的說明。然後,他還在特定類型的應用中把Java和Scala、C++和JavaScript的性能做了對比。在這些應用中,使用基本數據類型應用性能會有很顯著的不同。

問:影響買房最重要的三個因素是什麽?
答:位置!位置!還是位置!!

這是個很古老但卻經常被提及的諺語。意思是,當購買房產的時候,位置因素是絕對的主導因素。與此類似,考慮在Java中使用基本數據類型的3個最重要的因素,那就是性能!性能!還是性能!!房產與基本數據類型有兩個不同之處。首先,位置主導幾乎適用於所有買房的情況。但是使用基本類型帶來的性能提升,對不同類型的應用程序來大不相同。其次,盡管其他的因素與位置相比在買房時都顯得不太重要,但是也是應該被考慮的因素。而使用基本數據類型的原因卻只有一個——那就是性能,並且只適用於那些使用後能提升性能的應用。

基本數據類型對大多數業務相關或網絡應用程序沒有太大的用處,這些應用一般是采用客戶端/服務器模式,後端有數據庫。然而,使用基本數據類型對那種以數值計算為主的應用提升性能有很大好處。

在Java語言中包含基本數據類型是最有爭議的設計決定之一,關於這個決定討論的文章和帖子比比皆是。2011年9月,Simon Ritter在JAX London上的演講說,要認真考慮在Java未來的某個版本中刪除基本數據類型。接下來,我會簡要介紹基本數據類型和Java的雙類型系統。通過示例代碼和簡單的基準測試說明為什麽基本數據類型對於某些類型的應用程序是必須的。我也會對Java與Scala、C++和JavaScript性能做一些比較。

測量軟件性能

軟件性能經常用時間和空間來衡量。時間可以是實際的運行時間,比如3.7分鐘,或者是基於輸入規模的時間復雜度,比如O(n2)。對於空間的衡量也是如此。經常用內存消耗來衡量,有時候也會擴大到磁盤的使用。改善性能經常要做時間和空間的折衷,縮短時間經常會對空間造成損害,反之亦然。時間復雜度取決於算法,把包裝類型切換成基本數據類型對結果不會有任何改變,但是使用基本數據類型取代對象類型能改善時間和空間的性能。

基本數據類型vs對象類型

當你閱讀這篇文章的時候,可能已經知道了Java是雙類型的系統,也就是基本數據類型和對象類型,簡稱基本類型和對象。Java中有8個預定義的基本類型,它們的名字都是保留的關鍵字。常見的基本類型有int、double和boolean。Java中所有其他的類型包括用戶自定義的類型,它們必然也是對象類型(我說”必然”是因為數組類型有點例外,與基本類型比數組更像是對象類型)。每一個基本類型都有一個對應的對象包裝類,比如int的包裝類是Integer,double的包裝類是Double,boolean的包裝類是Boolean。

基本類型基於值,而對象類型則基於引用。與基本類型相關的爭議都源於此。為了說明它們的不同,先來看一下兩個聲明語句。第一個語句使用的是基本類型,第二個使用的是包裝類。

1 2 int n1 = 100; Integer n2 = new Integer(100);

使用新添加到JDK5的特性自動裝箱以後,第二個聲明可以簡化成:

1 Integer n2 = 100;

但是,底層的語義並沒有發生改變。自動裝箱簡化了包裝類的使用,減少了程序員的編碼量,但是對運行時並沒有任何的改變。

圖1展示了基本類型n1和包裝對象類型n2的區別。

技術分享圖片

圖1. 基本類型vs對象類型的內存結構

n1持有一個整數的值,但是n2持有的是對一個對象的引用,即那個對象持有整數的值。除此之外,n2引用的對象也包含了一個對Double對象的引用。

基本數據類型存在的問題

在我試圖說服你需要基本類型之前,首先我應該感謝不同意我觀點的那些人。Sherman Alpert在”基本類型是有害的(Primitive types considered harmful)”這篇文章中說基本類型是有害的,因為“它們把函數式的語義混進了面向對象模型裏面,讓面向對象變得不純。基本類型不是對象,但是它們卻存在於以一流對象為根本的語言中”。基本類型和(包裝類形式的)對象類型提供了兩種處理邏輯上相似的類型的方式,但是在底層的語義上卻有著非常大的不同。比如,兩個實例如何來比較相等性?對於基本類型,使用==操作符,但是對於對象類型,更好的方式是調用equals()方法,而基本類型是沒有這個操作的。相似的,在賦值和傳參的語義上也是不同的。就連默認值也是不一樣的,比如int的默認是值0,但是Integer的默認值是null。

關於這個話題的更多的背景可以參考Eric Bruno的博客”關於基本類型的討論(A modern primitive discussion)”,裏面總結了關於基本類型的正反兩方面的意見。Stack Overflow上也有很多關於基本類型的討論,包括”為什麽人們仍然在Java中使用基本類型?(Why do people still use primitive types in Java?)”,”有沒有只使用對象不使用基本類型的理由?(Is there a reason to always use Objects instead of primitives?)”。Programmers Stack Exchange上也有一個類似的叫做”Java中什麽時候應該使用基本類型和對象類型?(When to use primitive vs class in Java?)”的討論。

內存的使用

Java中的double總是占據內存的64個比特,但是引用類型的字節數取決於JVM。我的電腦運行64位Win7和64位JVM,因此在我的電腦上一個引用占用64個比特。根據圖1,一個double比如n1要占用8個字節(64比特),一個Double比如n2要占用24個字節——對象的引用占8個字節,對象中的double的值占8個字節,對象中對Double對象的引用占8個字節。此外,Java需要使用額外的內存來支持對象的垃圾回收,但是基本類型不需要。下面讓我們來驗證下。

跟Glen McCluskey在”Java基本類型 VS 包裝類型(Java primitive types vs. wrappers)”中使用的方式類似,列表1中的方法會測量一個n*n的double類型的矩陣(二維數組)所占的字節數。

列表1. 計算double類型的內存使用

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static long getBytesUsingPrimitives(int n) { System.gc(); // force garbage collection long memStart = Runtime.getRuntime().freeMemory(); double[][] a = new double[n][n]; // put some random values in the matrix for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) a[i][j] = Math.random(); } long memEnd = Runtime.getRuntime().freeMemory(); return memStart - memEnd; }

修改列表1中的代碼(沒有列出來),改變矩陣的元素類型,我們也可以測量一個nn的Double類型的矩陣所占的字節數。在我的電腦上用10001000的矩陣來測試這兩個方法,我得到了表1的結果。就像之前說的那樣,基本類型版本的double矩陣中每一個元素占8個多字節,跟我預期的差不多,但是,對象類型版本的Double矩陣中每一個元素占28個字節還要多一點。因此,在這個例子中,Double的內存使用是double的3倍還要多,這對那些明白上面圖1說的內存布局的人來說,並不是一件讓人很吃驚的事情。

表1. double和Double的內存使用情況對比

版本總字節數平均字節數
使用double 8,380,768 8.381
使用Double 28,166,072 28.166

運行時性能

為了比較基本類型和對象類型的運行時性能,我們需要一個數值計算占主導的算法。本文中,我選擇了矩陣相乘,然後計算1000*1000的矩陣相乘所需要的時間。我用一種很直觀的方式來編碼double類型的矩陣相乘,就像列表2中展示的那樣。可能會有更快的方式來實現矩陣相乘(比如使用並發),但那個與本文無關。我需要的僅僅是兩個很簡單的方法,一個使用基本類型的double,另一個使用包裝類Double。Double類型的矩陣相乘的代碼跟列表2非常相似,僅僅是改了類型。

列表2. 兩種類型的浮點數的矩陣相乘

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static double[][] multiply(double[][] a, double[][] b) { if (!checkArgs(a, b)) throw new IllegalArgumentException("Matrices not compatible for multiplication"); int nRows = a.length; int nCols = b[0].length; double[][] result = new double[nRows][nCols]; for (int rowNum = 0; rowNum < nRows; ++rowNum) { for (int colNum = 0; colNum < nCols; ++colNum) { double sum = 0.0; for (int i = 0; i < a[0].length; ++i) sum += a[rowNum][i]*b[i][colNum]; result[rowNum][colNum] = sum; } } return result; }

在我的計算機上分別用兩個方法對兩個1000*1000的矩陣做多次乘法,測量執行時間。表2中列出了平均時間。可以看出來,在本例中,double的運行時性能是Double的四倍。這是一個不能忽略的很大的不同!

版本秒數
使用double 11.31
使用Double 48.48

SciMark2.0基準測試

迄今為止,我使用了一個簡單的矩陣相乘的例子來說明基本類型比對象類型有更高的計算性能。為了讓我的觀點更站得住腳,我會用更科學的基準點(來做測試)。SciMark 2.0是NIST的用來測試科學和數值計算能力的Java基準工具。我下載了它的源碼,創建了2個版本的基準測試,一個是使用基本類型,另一個使用包裝類。第二個測試中我把int替換成了Integer,把double替換成了Double,這樣來看包裝類帶來的影響。這兩個版本在本文的源碼中都可以找到。

Java基準測試: 源碼下載

SciMark基準測試會測量許多種常見計算的性能,然後用大概的Mflops(每秒百萬浮點運算數)給出一個綜合的分數。因此,對這樣的基準測試來說數據越大越好。表3給出了這個基準測試在我的計算機上對每個版本多次運行的平均綜合分數。就像表中展示的那樣,這兩個版本的SciMark基準測試的運行時性能跟上面矩陣相乘的結果是一致的,基本類型的性能幾乎比包裝類型快了5倍。

表3. SciMark基準測試的運行時性能

SciMark版本性能(Mflops)
使用double 710.80
使用Double 143.73

你已經見過使用自己的基準測試和一個更科學的方式對Java程序做數值計算的一些方式,但是,Java和其他語言比起來會怎樣呢?看下Java和其他三種編程語言Scala,C++,JavaScript的性能比較,然後我會做出結論。

Scala基準測試

Scale是運行在JVM上的編程語言,貌似因此變得很流行。Scale有統一的類型系統,也就是說它不區分基本類型和對象類型。根據Erik Osheim在Scala的數值類型(第一部分)中說的,Scala在可能的情況下會使用基本類型,但是在必須的時候會使用對象類型(Scala uses primitive types when possible but will use objects if necessary)。與此相似,Martin Odersky對Scale數組的描述說:“Scala的Array[Int]數組對應Java當中的int[],Array[Double]對應Java當中的double[]”。

難道這意味著Scale的統一類型系統和Java的基本類型的運行時性能差不多?讓我們來看一下。

Scala性能改善

當我兩年前第一次使用Scala運行矩陣相乘的基準測試的時候,平均差不多要用超過33秒的時間,性能大概在Java基本類型和對象類型之間。最近我又用新版本的Scale重新編譯一下,然後我被新版本的重大性能改善所震驚了。

我用Scale不像用Java那樣熟練,但是我嘗試把Java版本的矩陣相乘的基準測試直接轉化成Scale版本。結果如下面列表3所示。當我在我的計算機上執行Scale版本的基準測試的時候,平均花費12.30秒,這個跟Java使用基本類型時候的性能非常接近。結果比我預期的要好很多,這個也給Scale聲明的對數值類型的處理做了很好的證明。

Scala基準測試:源碼下載

列表3. Scala語言實現的矩陣相乘

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def multiply(a : Array[Array[Double]], b : Array[Array[Double]]) : Array[Array[Double]] = { if (!checkArgs(a, b)) throw new IllegalArgumentException("Matrices not compatible for multiplication"); val nRows : Int = a.length; val nCols : Int = b(0).length; var result = Array.ofDim[Double](nRows, nCols); for (rowNum <- 0 until nRows) { for (colNum <- 0 until nCols) { val sum : Double = 0.0; for (i <- 0 until a(0).length) sum += a(rowNum)(i)*b(i)(colNum); result(rowNum)(colNum) = sum; } } return result; }

代碼轉化的風險

就像James Roper指出的那樣,當把一種語言直接轉化成另一種語言的時候,總是存在風險的。因為缺少Scala的經驗,使我意識到轉換基準測試的代碼有可能是可行的,這會強制Scala在運行時使用更高效的基本類型,同時還可以保留算法的基本特性。因為它用Java編寫,所以折衷比較也是有意義的。

C++基準測試

因為C++是直接運行在物理機而非虛擬機上,大家自然會認為C++的速度比Java快。此外,為了確保索引是在數組聲明的邊界之內,Java會檢查數組的訪問,這對性能也有輕微的損害,C++不會做這樣的檢查(這是C++的一個特性,它可以導致緩沖區溢出,這可能會被黑客利用)。我發現,C++在處理基本的二維數組的時候多少有點尷尬,幸運的是,可以把這種尷尬隱藏到類內部的私有部分。我創建了一個簡單的C++版本的Matrix類,重載了*操作符,用來做矩陣相乘,基本的矩陣相乘的算法是用Java版本直接轉化過來的。列表4列出了C++的源碼:

C++基準測試:源碼下載

列表4.C++語言實現的矩陣相乘

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Matrix operator*(const Matrix& m1, const Matrix& m2) throw(invalid_argument) { if (!Matrix::checkArgs(m1, m2)) throw invalid_argument("matrices not compatible for multiplication"); Matrix result(m1.nRows, m2.nCols); for (int i = 0; i < result.nRows; ++i) { for (int j = 0; j < result.nCols; ++j) { double sum = 0.0; for (int k = 0; k < m1.nCols; ++k) sum += m1.p[i][k]*m2.p[k][j]; result.p[i][j] = sum; } } return result; }

使用Eclipse CDT和MinGW C++編譯器可以創建調試版和正式版的應用程序。為了測試C++的性能,我是多次運行正式版取平均值。正如預期的那樣,在這個簡單的測試中C++很明顯要快得多,在我的計算機上平均是7.58秒。如果性能是選擇一個編程語言的主要因素的話,那麽C++是數值運算密集型應用的首選。

JavaScript基準測試

好吧,這個測試的結果讓我感到震驚。因為Javascript是一種動態語言,我本以為它的性能是最差的,甚至要比Java包裝類的性能還要差。但是實際上,Javascript的性能跟Java使用基本類型的性能很接近。為了測試Javascript的性能,我安裝了Node.js——它是一個以效率著稱的Javascript引擎。測試的平均結果是15.91秒。列表5展示了運行在Node.js下Javascript版本的矩陣相乘基準測試:

JavaScript基準測試:源碼下載

列表5. JavaScript語言實現的矩陣相乘

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function multiply(a, b) { if (!checkArgs(a, b)) throw ("Matrices not compatible for multiplication"); var nRows = a.length; var nCols = b[0].length; var result = Array(nRows); for(var rowNum = 0; rowNum < nRows; ++rowNum) { result[rowNum] = Array(nCols); for(var colNum = 0; colNum < nCols; ++colNum) { var sum = 0; for(var i = 0; i < a[0].length; ++i) sum += a[rowNum][i]*b[i][colNum]; result[rowNum][colNum] = sum; } } return result; }

結論

當18年前Java第一次登上歷史舞臺的時候,對於以數值計算為主的應用程序從性能的角度來說,它並不是最好的語言。但時過境遷,隨著其他領域技術的發展,比如即時編譯(aka自適應或動態編譯),當使用基本數據類型的時候,這類Java應用的性能已經可以匹敵編譯成本地代碼的那些語言。

並且,基本數據類型不需要垃圾回收,因此比對象類型多了另一個性能優勢。表4總結了矩陣相乘基準測試在我的計算機上的運行時性能。還有其他的諸如可維護性可移植性和開發者擅長等因素讓Java成為許多這類應用的很好地選擇。

表4. 矩陣相乘基準測試的運行時性能

結果(以秒計)

C++Java
(double)
ScalaJavaScriptJava
(Double)
7.58 11.31 12.30 15.91 48.48

就像前面討論的那樣,看上去Oracle似乎很嚴肅的考慮了是否在Java未來的版本中去掉基本數據類型。除非Java編譯器能產生跟基本數據類型性能相當的代碼,我認為把基本數據類型從Java中去掉會妨礙Java在某些類型應用中的使用,即那些以數值計算為主的應用。本文中,我對矩陣相乘做了基準測試,還用更科學的基準測試SciMark2.0來支持這一點。

附錄

在本文發表在JavaWorld上幾周以後,作者收到了來自Brian Goetz的郵件。Brian Goetz是Oracle的Java語言架構師,他說從Java中移除基本數據類型不在考慮範圍之內。移除基本數據類型的說法源自於對Java未來版本的願景討論的誤解或者是不準確的解釋。

關於作者

John I. Moore, Jr.是Citadel的一位數學和計算機教授,他在工業領域和學術上都有很豐富的經驗,在面向對象技術,軟件工程和應用數學方面有獨到的專長。在超過30年的時間裏,他使用關系型數據庫和很多高級語言來設計和開發軟件,工作中廣泛使用從1.1開始的Java的各個版本。他對計算機科學的很多高級話題都開設並教授了許多課程和研討班。

了解更多:

1.Paul Krill在“Oracle的Java長期目標”寫到Oracle對Java的長期的一些計劃(JavaWorld,2012年3月)。那篇文章和相關的評論促使我寫了這篇支持基本類型的文章。

Szymon Guz writes about his results in benchmarking primitive types and wrapper classes in “Primitives and objects benchmark in Java” (SimonOnSoftware, January 2011).
2.Szymon Guz在“Java中基本數據類型和對象類型的基準測試”(SimonOnSoftware,2011年1月)中寫了對基本類型和包裝類型做基準測試的結果。

3.C++編程準則和實踐(Addison-Wesley, 2009)的支持站點上,C++的作者Bjarne提供了一個比本文要完善很多的矩陣類的實現。

4.John Rose,Brian Goetz和Guy Steele在“值的狀態”一文中討論了值的類型這個概念。值的類型可以被認為是沒有標識的不變的用戶自定義的類型集合,因此,可以把對象類型和基本類型的屬性結合起來。值類型的好處是:像引用類型那樣編碼,像基本類型那樣運行。

原文鏈接: javaworld 翻譯: ImportNew.com - miracle1919
譯文鏈接: http://www.importnew.com/11915.html
[ 轉載請保留原文出處、譯者和譯文鏈接。]

Java為什麽需要保留基本數據類型