[转载] 什么是Java中的自动拆装箱 integer
參考鏈接: Java中autoboxing自動裝箱整數(shù)對象的比較
本文主要介紹Java中的自動拆箱與自動裝箱的有關(guān)知識。?
??
1、基本數(shù)據(jù)類型? ?
基本類型,或者叫做內(nèi)置類型,是Java中不同于類(Class)的特殊類型。它們是我們編程中使用最頻繁的類型。?
Java是一種強類型語言,第一次申明變量必須說明數(shù)據(jù)類型,第一次變量賦值稱為變量的初始化。?
Java基本類型共有八種,基本類型可以分為三類:?
字符類型char?
布爾類型boolean?
整數(shù)類型byte、short、int、long?
浮點數(shù)類型float、double。?
Java中的數(shù)值類型不存在無符號的,它們的取值范圍是固定的,不會隨著機器硬件環(huán)境或者操作系統(tǒng)的改變而改變。?
實際上,Java中還存在另外一種基本類型void,它也有對應(yīng)的包裝類java.lang.Void,不過我們無法直接對它們進(jìn)行操作。?
基本數(shù)據(jù)類型有什么好處 我們都知道在Java語言中,new一個對象是存儲在堆里的,我們通過棧中的引用來使用這些對象;所以,對象本身來說是比較消耗資源的。?
對于經(jīng)常用到的類型,如int等,如果我們每次使用這種變量的時候都需要new一個Java對象的話,就會比較笨重。?
所以,和C++一樣,Java提供了基本數(shù)據(jù)類型,這種數(shù)據(jù)的變量不需要使用new創(chuàng)建,他們不會在堆上創(chuàng)建,而是直接在棧內(nèi)存中存儲,因此會更加高效。?
整型的取值范圍?
Java中的整型主要包含byte、short、int和long這四種,表示的數(shù)字范圍也是從小到大的,之所以表示范圍不同主要和他們存儲數(shù)據(jù)時所占的字節(jié)數(shù)有關(guān)。?
先來個簡答的科普,1字節(jié)=8位(bit)。java中的整型屬于有符號數(shù)。?
先來看計算中8bit可以表示的數(shù)字:?
最小值:10000000 (-128)(-2^7) 最大值:01111111(127)(2^7-1) 整型的這幾個類型中,?
byte:byte用1個字節(jié)來存儲,范圍為-128(-2^7)到127(2^7-1),在變量初始化的時候,byte類型的默認(rèn)值為0。?
short:short用2個字節(jié)存儲,范圍為-32,768 (-2^15)到32,767 (2^15-1),在變量初始化的時候,short類型的默認(rèn)值為0,一般情況下,因為Java本身轉(zhuǎn)型的原因,可以直接寫為0。?
int:int用4個字節(jié)存儲,范圍為-2,147,483,648 (-2^31)到2,147,483,647 (2^31-1),在變量初始化的時候,int類型的默認(rèn)值為0。?
long:long用8個字節(jié)存儲,范圍為-9,223,372,036,854,775,808 (-2^63)到9,223,372,036, 854,775,807 (2^63-1),在變量初始化的時候,long類型的默認(rèn)值為0L或0l,也可直接寫為0。?
超出范圍怎么辦 上面說過了,整型中,每個類型都有一定的表示范圍,但是,在程序中有些計算會導(dǎo)致超出表示范圍,即溢出。如以下代碼:?
int i = Integer.MAX_VALUE;
int j = Integer.MAX_VALUE;
?
int k = i + j;
System.out.println("i (" + i + ") + j (" + j + ") = k (" + k + ")");?
輸出結(jié)果:i (2147483647) + j (2147483647) = k (-2)?
這就是發(fā)生了溢出,溢出的時候并不會拋異常,也沒有任何提示。所以,在程序中,使用同類型的數(shù)據(jù)進(jìn)行運算的時候,一定要注意數(shù)據(jù)溢出的問題。?
??
2、包裝類型?
Java語言是一個面向?qū)ο蟮恼Z言,但是Java中的基本數(shù)據(jù)類型卻是不面向?qū)ο蟮?#xff0c;這在實際使用時存在很多的不便,為了解決這個不足,在設(shè)計類時為每個基本數(shù)據(jù)類型設(shè)計了一個對應(yīng)的類進(jìn)行代表,這樣八個和基本數(shù)據(jù)類型對應(yīng)的類統(tǒng)稱為包裝類(Wrapper Class)。?
包裝類均位于java.lang包,包裝類和基本數(shù)據(jù)類型的對應(yīng)關(guān)系如下表所示?
基本數(shù)據(jù)類型? ? 包裝類?
byte? ? ? ? ? Byte
boolean? ? ? ?Boolean
short? ? ? ? ?Short
char? ? ? ? ? Character
int? ? ? ? ? ?Integer
long? ? ? ? ? Long
float? ? ? ? ?Float
double? ? ? ? Double?
?在這八個類名中,除了Integer和Character類以后,其它六個類的類名和基本數(shù)據(jù)類型一致,只是類名的第一個字母大寫即可。?
為什么需要包裝類 很多人會有疑問,既然Java中為了提高效率,提供了八種基本數(shù)據(jù)類型,為什么還要提供包裝類呢??
這個問題,其實前面已經(jīng)有了答案,因為Java是一種面向?qū)ο笳Z言,很多地方都需要使用對象而不是基本數(shù)據(jù)類型。比如,在集合類中,我們是無法將int 、double等類型放進(jìn)去的。因為集合的容器要求元素是Object類型。?
為了讓基本類型也具有對象的特征,就出現(xiàn)了包裝類型,它相當(dāng)于將基本類型“包裝起來”,使得它具有了對象的性質(zhì),并且為其添加了屬性和方法,豐富了基本類型的操作。?
??
3、拆箱與裝箱?
那么,有了基本數(shù)據(jù)類型和包裝類,肯定有些時候要在他們之間進(jìn)行轉(zhuǎn)換。比如把一個基本數(shù)據(jù)類型的int轉(zhuǎn)換成一個包裝類型的Integer對象。?
我們認(rèn)為包裝類是對基本類型的包裝,所以,把基本數(shù)據(jù)類型轉(zhuǎn)換成包裝類的過程就是打包裝,英文對應(yīng)于boxing,中文翻譯為裝箱。?
反之,把包裝類轉(zhuǎn)換成基本數(shù)據(jù)類型的過程就是拆包裝,英文對應(yīng)于unboxing,中文翻譯為拆箱。?
在Java SE5之前,要進(jìn)行裝箱,可以通過以下代碼:?
Integer i = new Integer(10);?
??
4、自動拆箱與自動裝箱?
在Java SE5中,為了減少開發(fā)人員的工作,Java提供了自動拆箱與自動裝箱功能。?
自動裝箱: 就是將基本數(shù)據(jù)類型自動轉(zhuǎn)換成對應(yīng)的包裝類。?
自動拆箱:就是將包裝類自動轉(zhuǎn)換成對應(yīng)的基本數(shù)據(jù)類型。?
Integer i =10;? //自動裝箱
int b= i;? ? ?//自動拆箱?
Integer i=10 可以替代 Integer i = new Integer(10); 這就是因為Java幫我們提供了自動裝箱的功能,不需要開發(fā)者手動去new一個Integer對象。?
??
5、自動裝箱與自動拆箱的實現(xiàn)原理?
既然Java提供了自動拆裝箱的能力,那么,我們就來看一下,到底是什么原理,Java是如何實現(xiàn)的自動拆裝箱功能。?
我們有以下自動拆裝箱的代碼:?
public static? void main(String[]args){
? ? Integer integer=1; //裝箱
? ? int i=integer; //拆箱
}
對以上代碼進(jìn)行反編譯后可以得到以下代碼:
?
public static? void main(String[]args){
? ? Integer integer=Integer.valueOf(1);?
? ? int i=integer.intValue();?
}?
?從上面反編譯后的代碼可以看出,int的自動裝箱都是通過Integer.valueOf()方法來實現(xiàn)的,Integer的自動拆箱都是通過integer.intValue來實現(xiàn)的。如果讀者感興趣,可以試著將八種類型都反編譯一遍 ,你會發(fā)現(xiàn)以下規(guī)律:?
自動裝箱都是通過包裝類的valueOf()方法來實現(xiàn)的.自動拆箱都是通過包裝類對象的xxxValue()來實現(xiàn)的。?
6、哪些地方會自動拆裝箱?
我們了解過原理之后,在來看一下,什么情況下,Java會幫我們進(jìn)行自動拆裝箱。前面提到的變量的初始化和賦值的場景就不介紹了,那是最簡單的也最容易理解的。?
我們主要來看一下,那些可能被忽略的場景。?
場景一、將基本數(shù)據(jù)類型放入集合類?
我們知道,Java中的集合類只能接收對象類型,那么以下代碼為什么會不報錯呢??
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i ++){
? ? li.add(i);
}?
將上面代碼進(jìn)行反編譯,可以得到以下代碼:?
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2){
? ? li.add(Integer.valueOf(i));
}?
以上,我們可以得出結(jié)論,當(dāng)我們把基本數(shù)據(jù)類型放入集合類中的時候,會進(jìn)行自動裝箱。?
場景二、包裝類型和基本類型的大小比較?
有沒有人想過,當(dāng)我們對Integer對象與基本類型進(jìn)行大小比較的時候,實際上比較的是什么內(nèi)容呢?看以下代碼:?
I?
Integer a=1;
System.out.println(a==1?"等于":"不等于");
Boolean bool=false;
System.out.println(bool?"真":"假");?
對以上代碼進(jìn)行反編譯,得到以下代碼:?
Integer a=1;
System.out.println(a.intValue()==1?"等于":"不等于");
Boolean bool=false;
System.out.println(bool.booleanValue?"真":"假");?
可以看到,包裝類與基本數(shù)據(jù)類型進(jìn)行比較運算,是先將包裝類進(jìn)行拆箱成基本數(shù)據(jù)類型,然后進(jìn)行比較的。?
場景三、包裝類型的運算?
有沒有人想過,當(dāng)我們對Integer對象進(jìn)行四則運算的時候,是如何進(jìn)行的呢?看以下代碼:?
Integer i = 10;
Integer j = 20;
System.out.println(i+j);?
反編譯后代碼如下:?
Integer i = Integer.valueOf(10);
Integer j = Integer.valueOf(20);
System.out.println(i.intValue() + j.intValue());?
我們發(fā)現(xiàn),兩個包裝類型之間的運算,會被自動拆箱成基本類型進(jìn)行。?
場景四、三目運算符的使用?
這是很多人不知道的一個場景,作者也是一次線上的血淋淋的Bug發(fā)生后才了解到的一種案例。看一個簡單的三目運算符的代碼:?
boolean flag = true;
Integer i = 0;
int j = 1;
int k = flag ? i : j;?
很多人不知道,其實在int k = flag ? i : j;這一行,會發(fā)生自動拆箱。反編譯后代碼如下:?
boolean flag = true;
Integer i = Integer.valueOf(0);
int j = 1;
int k = flag ? i.intValue() : j;?
這其實是三目運算符的語法規(guī)范:當(dāng)?shù)诙?#xff0c;第三位操作數(shù)分別為基本類型和對象時,其中的對象就會拆箱為基本類型進(jìn)行操作。?
因為例子中,flag ? i : j;片段中,第二段的i是一個包裝類型的對象,而第三段的j是一個基本類型,所以會對包裝類進(jìn)行自動拆箱。如果這個時候i的值為null,那么久會發(fā)生NPE。(自動拆箱導(dǎo)致空指針異常)?
場景五、函數(shù)參數(shù)與返回值?
這個比較容易理解,直接上代碼了:?
//自動拆箱
public int getNum1(Integer num) {
?return num;
}
//自動裝箱
public Integer getNum2(int num) {
?return num;
}?
7、自動拆裝箱與緩存?
Java SE的自動拆裝箱還提供了一個和緩存有關(guān)的功能,我們先來看以下代碼,猜測一下輸出結(jié)果:?
public static void main(String... strings) {
?
? ? Integer integer1 = 3; //通過反編譯發(fā)現(xiàn)執(zhí)行的是? Integer.valueOf(1);?
? ? Integer integer2 = 3;
?
? ? if (integer1 == integer2)
? ? ? ? System.out.println("integer1 == integer2");
? ? else
? ? ? ? System.out.println("integer1 != integer2");
?
? ? Integer integer3 = 300;
? ? Integer integer4 = 300;
?
? ? if (integer3 == integer4)
? ? ? ? System.out.println("integer3 == integer4");
? ? else
? ? ? ? System.out.println("integer3 != integer4");
?
}?
我們普遍認(rèn)為上面的兩個判斷的結(jié)果都是false。雖然比較的值是相等的,但是由于比較的是對象,而對象的引用不一樣,所以會認(rèn)為兩個if判斷都是false的。?
在Java中,==比較的是對象應(yīng)用,而equals比較的是值。?
所以,在這個例子中,不同的對象有不同的引用,所以在進(jìn)行比較的時候都將返回false。奇怪的是,這里兩個類似的if條件判斷返回不同的布爾值。?
上面這段代碼真正的輸出結(jié)果:?
integer1 == integer2 integer3 != integer4 原因就和Integer中的緩存機制有關(guān)。在Java 5中,在Integer的操作上引入了一個新功能來節(jié)省內(nèi)存和提高性能。整型對象通過使用相同的對象引用實現(xiàn)了緩存和重用。?
適用于整數(shù)值區(qū)間-128 至 +127。?
只適用于自動裝箱。使用構(gòu)造函數(shù)創(chuàng)建對象不適用。?
具體的代碼實現(xiàn)可以閱讀Java中整型的緩存機制一文,這里不再闡述。?
-- 源碼
?public static Integer valueOf(int i) {
? ? ? ? assert IntegerCache.high >= 127;
? ? ? ? if (i >= IntegerCache.low && i <= IntegerCache.high)
? ? ? ? ? ? return IntegerCache.cache[i + (-IntegerCache.low)];
? ? ? ? return new Integer(i);
? }?
我們只需要知道,當(dāng)需要進(jìn)行自動裝箱時,如果數(shù)字在-128至127之間時,會直接使用緩存中的對象,而不是重新創(chuàng)建一個對象。?
擴展:??
? ? ? ? int i =50;
? ? ? ? Integer i1 =50;
? ? ? ? Integer i2 =50;
? ? ? ? Integer i3 = new Integer(50);
? ? ? ? Integer i4 = new Integer(50);
? ? ? ? Integer i5 = 300;
? ? ? ? Integer i6 = 300;
? ? ? ? System.out.println(i == i1);// true;i1 自動拆箱變成基本類型,兩基本類型比較值
? ? ? ? System.out.println(i == i3);// true; i3自動拆箱變成基本類型,兩基本類型比較值
? ? ? ? System.out.println(i1 == i2);// true; i1和i3都指向常量池中同一個地址
? ? ? ? System.out.println(i1 == i3);// false; 兩個不同的對象
? ? ? ? System.out.println(i3 == i4);// false; 兩個不同的對象
? ? ? ? System.out.println(i5 == i6);// false; 自動裝箱時,如果值不在-128到127,就會創(chuàng)建一個新的對象
? ? System.out.println( "11".equals( 11 ));//false; 不同類型之間的額比較?
其中的javadoc詳細(xì)的說明了緩存支持-128到127之間的自動裝箱過程。最大值127可以通過-XX:AutoBoxCacheMax=size修改。緩存通過一個 for 循環(huán)實現(xiàn)。從小到大的創(chuàng)建盡可能多的整數(shù)并存儲在一個名為 cache 的整數(shù)數(shù)組中。這個緩存會在 Integer 類第一次被使用的時候被初始化出來。以后就可以使用緩存中包含的實例對象,而不是創(chuàng)建一個新的實例(在自動裝箱的情況下)。?
實際上這個功能在Java 5中引入的時候,范圍是固定的-128 至 +127。后來在Java 6中,可以通過java.lang.Integer.IntegerCache.high設(shè)置最大值。這使我們可以根據(jù)應(yīng)用程序的實際情況靈活地調(diào)整來提高性能。到底是什么原因選擇這個-128到127范圍呢?因為這個范圍的數(shù)字是最被廣泛使用的。 在程序中,第一次使用Integer的時候也需要一定的額外時間來初始化這個緩存。?
/**?
?* Cache to support the object identity semantics of autoboxing for values between?
?* -128 and 127 (inclusive) as required by JLS.?
?*?
?* The cache is initialized on first usage.? The size of the cache?
?* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.?
?* During VM initialization, java.lang.Integer.IntegerCache.high property?
?* may be set and saved in the private system properties in the?
?* sun.misc.VM class.?
?*/??
??
private static class IntegerCache {??
? ? static final int low = -128;??
? ? static final int high;??
? ? static final Integer cache[];??
??
? ? static {??
? ? ? ? // high value may be configured by property??
? ? ? ? int h = 127;??
? ? ? ? String integerCacheHighPropValue =??
? ? ? ? ? ? sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");??
? ? ? ? if (integerCacheHighPropValue != null) {??
? ? ? ? ? ? try {??
? ? ? ? ? ? ? ? int i = parseInt(integerCacheHighPropValue);??
? ? ? ? ? ? ? ? i = Math.max(i, 127);??
? ? ? ? ? ? ? ? // Maximum array size is Integer.MAX_VALUE??
? ? ? ? ? ? ? ? h = Math.min(i, Integer.MAX_VALUE - (-low) -1);??
? ? ? ? ? ? } catch( NumberFormatException nfe) {??
? ? ? ? ? ? ? ? // If the property cannot be parsed into an int, ignore it.??
? ? ? ? ? ? }??
? ? ? ? }??
? ? ? ? high = h;??
??
? ? ? ? cache = new Integer[(high - low) + 1];??
? ? ? ? int j = low;??
? ? ? ? for(int k = 0; k < cache.length; k++)??
? ? ? ? ? ? cache[k] = new Integer(j++);??
??
? ? ? ? // range [-128, 127] must be interned (JLS7 5.1.7)??
? ? ? ? assert IntegerCache.high >= 127;??
? ? }??
??
? ? private IntegerCache() {}??
}? ?
在Boxing Conversion部分的Java語言規(guī)范(JLS)規(guī)定如下:?
如果一個變量p的值是:?
-128至127之間的整數(shù)(§3.10.1)? ?true 和 false的布爾值 (§3.10.3)? ?‘\u0000’至 ‘\u007f’之間的字符(§3.10.4) 范圍內(nèi)的時,將p包裝成a和b兩個對象時,可以直接使用a==b判斷a和b的值是否相等。?
補充:??
?注意,Integer、Short、Byte、Character、Long這幾個類的valueOf方法的實現(xiàn)是類似的。? Double、Float的valueOf方法的實現(xiàn)是類似的。?
而其中double和float又有所不同,我們就以double為例子,貼出代碼討論:?
public class Test {
? /**
? ?* Double
? ?*/
? public static void first() {
? ? Double i1 = 100.0;
? ? Double i2 = 100.0;
? ? Double i3 = 200.0;
? ? Double i4 = 200.0;
? ? System.out.println(i1 == i2);//false
? ? System.out.println(i3 == i4);//false
? }
? /**
? ?* 測試方法
? ?*/
? public static void main(String[] args) {
? ? first();
? }
}
?
注意為什么上面的代碼的輸出結(jié)果都是false呢?同樣的我們依舊以Double類中的valueOf方法來討論,貼出源碼就一目了然了:
?
??
? public static Double valueOf(double d) {
? ? return new Double(d);
? }
也就是說不管你的double是什么范圍的值,他都是給你返回一個新的對象。float同double,就不過多贅述了。?
??
8、自動拆裝箱帶來的問題? ?
當(dāng)然,自動拆裝箱是一個很好的功能,大大節(jié)省了開發(fā)人員的精力,不再需要關(guān)心到底什么時候需要拆裝箱。但是,他也會引入一些問題。包裝對象的數(shù)值比較,不能簡單的使用==,雖然-128到127之間的數(shù)字可以,但是這個范圍之外還是需要使用equals比較。?
前面提到,有些場景會進(jìn)行自動拆裝箱,同時也說過,由于自動拆箱,如果包裝類對象為null,那么自動拆箱時就有可能拋出NPE。?
如果一個for循環(huán)中有大量拆裝箱操作,會浪費很多資源。?
性能問題
首先在堆內(nèi)存中創(chuàng)建對象的消耗肯定是要比使用棧內(nèi)存要多的,同時在自動拆裝箱的時候,也有一定的性能消耗,如果在數(shù)據(jù)量比較大,或者是循環(huán)的情況下,頻繁的拆裝箱并且生成包裝類的時候,對性能的影響就是一星半點了,所以不是特殊的需求,例如上述被集合持有的情況,還是使用基本類型而不是包裝類?
在循環(huán)的時候?
Integer sum = 0;
?for(int i=1000; i<5000; i++){
? ?sum+=i;
}?
上面的代碼sum+=i可以看成sum = sum + i,在sum被+操作符操作的時候,會對sum進(jìn)行自動拆箱操作,進(jìn)行數(shù)值相加操作,最后發(fā)生自動裝箱操作轉(zhuǎn)換成Integer對象。其內(nèi)部變化如下?
sum = sum.intValue() + i;
Integer sum = new Integer(result);?
sum為Integer類型,在上面的循環(huán)中會創(chuàng)建4000個無用的Integer對象,在這樣龐大的循環(huán)中,會降低程序的性能并且加重了垃圾回收的工作量。因此在我們編程時,需要注意到這一點,正確地聲明變量類型,避免因為自動裝箱引起的性能問題?
再舉一個例子,在Java中的HashMap的性能也受到自動拆裝箱的影響 因為HashMap接收的參數(shù)類型是HashMap <Object, Object>,所以在增刪改查的時候,都會對Key值進(jìn)行大量的自動拆裝箱,為了解決這個問題,Java提供了SparseArray,包括SparseBoolMap, SparseIntMap, SparseLongMap, LongSparseMap,所接受的Key值都是基本類型的值,例如SparseIntMap就是SparseIntMap<int, Object>,在避免了大量自動拆裝箱的同時,還降低的內(nèi)存消耗。這里就點到為止,具體的數(shù)據(jù)結(jié)構(gòu)的問題我們就不深入了?
2. 重載與自動裝箱?
在Java5之前,value(int i)和value(Integer o)是完全不相同的方法,開發(fā)者不會因為傳入是int還是Integer調(diào)用哪個方法困惑,但是由于自動裝箱和拆箱的引入,處理重載方法時稍微有點復(fù)雜,例如在ArrayList中,有remove(int index)和remove(Object o)兩個重載方法,如果集合持有三個Integer類型值為3,1,2的對象,我們調(diào)用remove(3), 是調(diào)用了remove的哪個重載方法?remove掉的是值為3的對象,還是remove了index為3,值為2的那個對象呢?其實問題就是,參數(shù)3是否會被自動打包呢?答案是:不會,在這種情況下,編譯器不會進(jìn)行自動拆裝箱,所以調(diào)用的是remove(int index),index為3值為2的這個Integer對象會被remove?
通過以下例子我們可以驗證?
public void testAutoBoxing(int i){
? ? System.out.println("primitive argument");
?
}
?
public void testAutoBoxing(Integer integer){
? ? System.out.println("wrapper argument");
?
}
?
//calling overloaded method
int value = 1;
test(value); //no autoboxing?
Integer iValue = value;
test(iValue); //no autoboxing
?
Output:
primitive argument
wrapper argument?
3. 緩存值問題?
4. 警惕NullPointerException?
我們在使用基本類型的時候,在聲明的時候即使我們沒有對變量進(jìn)行賦值,編譯器也會自動的為其賦予初始值,比如int值就是0,boolean就是flase,所以我們在使用基本類型的時候,是不會出現(xiàn)NullPointerException的,但在使用包裝類的時候,我們就要注意這個問題了,不能因為有自動拆裝箱這個語法糖,就忘記了包裝類和基本類型的區(qū)別,將其同等對待了,如果你沒有在使用包裝類的時候通過顯式、或是通過自動裝箱機制為其賦值,在你取出值、或是通過自動拆箱使用該值的時候,就會發(fā)生NullPointerException,這個是大家要注意的?
?鏈接:https://www.jianshu.com/p/547b36f04239? ? https://www.jianshu.com/p/547b36f04239
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的[转载] 什么是Java中的自动拆装箱 integer的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [转载] JVM中对象的回收过程
- 下一篇: xampp for mac mysql_