1. 前言
寫這基礎復習系列是覺得工作中自己的基礎太差了,很多東西都沒想透徹,沒研究明白。看了《Java基礎16課》總結出其中的一些知識點,用于以后自己復習用,以前的一些知識盲點也明白了。當然,基礎這東西很難說,什么是基礎?有人認為將Java的SDK源碼中重要的類研究一遍,并且能按其規(guī)范(接口)實現(xiàn)了自己的類才算是真正掌握了基礎。其實一點都沒錯,只有通過去看微觀的實現(xiàn),才能提升自己的認識。
2. 數(shù)組在內(nèi)存中的存儲狀態(tài)
先看看數(shù)組,數(shù)組咱們平時經(jīng)常用,從用法來看,數(shù)組相當于普通變量,只不過它可以狀態(tài)多個相同類的多個對象容器而已。在內(nèi)存中,數(shù)組向內(nèi)存申請的空間是一段連續(xù)的物理空間。
寫這基礎復習系列是覺得工作中自己的基礎太差了,很多東西都沒想透徹,沒研究明白。看了《Java基礎16課》總結出其中的一些知識點,用于以后自己復習用,以前的一些知識盲點也明白了。當然,基礎這東西很難說,什么是基礎?有人認為將Java的SDK源碼中重要的類研究一遍,并且能按其規(guī)范(接口)實現(xiàn)了自己的類才算是真正掌握了基礎。其實一點都沒錯,只有通過去看微觀的實現(xiàn),才能提升自己的認識。
2. 數(shù)組在內(nèi)存中的存儲狀態(tài)
先看看數(shù)組,數(shù)組咱們平時經(jīng)常用,從用法來看,數(shù)組相當于普通變量,只不過它可以狀態(tài)多個相同類的多個對象容器而已。在內(nèi)存中,數(shù)組向內(nèi)存申請的空間是一段連續(xù)的物理空間。
public class ArrayTest {
public static void main(String[] args) {
String[] array = new String[] { "1", "2", "3" };
for (String str : array) {
System.out.println(str.hashCode());
}
}
}
public static void main(String[] args) {
String[] array = new String[] { "1", "2", "3" };
for (String str : array) {
System.out.println(str.hashCode());
}
}
}
這3個字符串實際上占用的是一段連續(xù)的內(nèi)存空間地址。需要說明的一點就是數(shù)組是引用型變量,數(shù)組中的元素僅僅是指向內(nèi)存地址的指針,而指針指向的目的地才是實際的數(shù)據(jù)對象。內(nèi)存中的情況如下:
所謂的數(shù)組聲明,實際上就是按照指定長度,為數(shù)組在內(nèi)存開辟了一段連續(xù)的空間,如果不是Java基本原型數(shù)據(jù)則附給這些內(nèi)存空間的指針與默認初始地址null,如果是原型數(shù)據(jù),則這些空間不再是指針,而是實實在在的原型值(例如int是0)。
而實際上多維數(shù)組的實現(xiàn)也是基于上面一維數(shù)組實現(xiàn)的,所以二維數(shù)組在我們來看,邏輯上可以當成矩陣,而在實實在在物理內(nèi)存上則是如下:
例如
而實際上多維數(shù)組的實現(xiàn)也是基于上面一維數(shù)組實現(xiàn)的,所以二維數(shù)組在我們來看,邏輯上可以當成矩陣,而在實實在在物理內(nèi)存上則是如下:
例如
public static void main(String[] args) {
String[][] str2 = new String[3][4];
for (int i = 0; i < str2.length; i++) {
int numOuter = i + 1;
System.out.println("外層執(zhí)行了" + numOuter + "次");
for (int j = 0; j < str2[i].length; j++) {
int numIuter = j + 1;
System.out.println("內(nèi)層執(zhí)行了" + numIuter + "次");
str2[i][j] = "素" + i + j;
}
}
for (String[] strArray1 : str2) {
for (String str : strArray1) {
System.out.print(str + " ");
}
System.out.println();
}
}
String[][] str2 = new String[3][4];
for (int i = 0; i < str2.length; i++) {
int numOuter = i + 1;
System.out.println("外層執(zhí)行了" + numOuter + "次");
for (int j = 0; j < str2[i].length; j++) {
int numIuter = j + 1;
System.out.println("內(nèi)層執(zhí)行了" + numIuter + "次");
str2[i][j] = "素" + i + j;
}
}
for (String[] strArray1 : str2) {
for (String str : strArray1) {
System.out.print(str + " ");
}
System.out.println();
}
}
看似二維數(shù)組存儲的元素是這樣的矩陣形式
素00 素01 素02 素03
素10 素11 素12 素13
素20 素21 素22 素23
素10 素11 素12 素13
素20 素21 素22 素23
實際上這個二維數(shù)組的分配如下圖
再復雜的三維數(shù)組、多維數(shù)組同樣以此類推,呈現(xiàn)出一個倒樹結構,以根擴展,葉子節(jié)點才是真正存儲數(shù)據(jù)(原型)或者是真正指向應用數(shù)據(jù)對象的指針(復雜對象)。
3.對象的產(chǎn)生
對象的產(chǎn)生和JVM的運行機制息息相關,我們使用一個對象為我們服務實際上歸根結底最后都是得用new出來的對象為我們所用,而這個對象是通過類對象產(chǎn)生的,這就是Java思想中的萬事萬物接對象的概念。首先得有一個模板對象,這個模板對象就是類對象,每一個new出來的實例對象實際上都是由這個模板對象而產(chǎn)生出來的,所以我們定義類的時候如果具有類變量,那么所有因它而創(chuàng)建的實例對象中的static變量都會因為類變量的改變而改變。因為static本身就是類對象所擁有的,模板都變了,你實例對象中的相關變量當然要變嘍。
無論是通過哪個實例對象去訪問類變量,底層都是用類對象直接訪問該類變量,所以大家使用static變量時得出來的值都是一樣的。
3.對象的產(chǎn)生
對象的產(chǎn)生和JVM的運行機制息息相關,我們使用一個對象為我們服務實際上歸根結底最后都是得用new出來的對象為我們所用,而這個對象是通過類對象產(chǎn)生的,這就是Java思想中的萬事萬物接對象的概念。首先得有一個模板對象,這個模板對象就是類對象,每一個new出來的實例對象實際上都是由這個模板對象而產(chǎn)生出來的,所以我們定義類的時候如果具有類變量,那么所有因它而創(chuàng)建的實例對象中的static變量都會因為類變量的改變而改變。因為static本身就是類對象所擁有的,模板都變了,你實例對象中的相關變量當然要變嘍。
無論是通過哪個實例對象去訪問類變量,底層都是用類對象直接訪問該類變量,所以大家使用static變量時得出來的值都是一樣的。
還要說明的一點就是final變量,如果在編譯時就能確定該變量的值,則此值在程序運行時不再是個變量,而是一個定值常量。至于實例變量的初始化時機以及JVM的一些初始化內(nèi)幕
4. 父子對象
使用Java不可能不使用繼承機制,現(xiàn)在來看看new一個子類的時候是如何初始化父類的。假如有如下的類結構
使用Java不可能不使用繼承機制,現(xiàn)在來看看new一個子類的時候是如何初始化父類的。假如有如下的類結構
所有類如果不指定父類那么就都是Object的子類,如果指定了父類,則間接地會繼承Object的,可能是它的孫子,也可能是它的曾孫子,也可能是它的孫子的孫子。如下例
class Parent{
static{
System.out.println("老子的靜態(tài)塊");
}
{
System.out.println("老子的非靜態(tài)塊");
}
public Parent(){
System.out.println("老子的無參構造函數(shù)");
}
}
class Sub extends Parent{
static{
System.out.println("兒子的靜態(tài)塊");
}
{
System.out.println("兒子的非靜態(tài)塊");
}
public Sub(){
System.out.println("兒子的無參構造函數(shù)");
}
}
public class ParSubTest {
/**
* @param args
*/
public static void main(String[] args) {
new Sub();
}
}
static{
System.out.println("老子的靜態(tài)塊");
}
{
System.out.println("老子的非靜態(tài)塊");
}
public Parent(){
System.out.println("老子的無參構造函數(shù)");
}
}
class Sub extends Parent{
static{
System.out.println("兒子的靜態(tài)塊");
}
{
System.out.println("兒子的非靜態(tài)塊");
}
public Sub(){
System.out.println("兒子的無參構造函數(shù)");
}
}
public class ParSubTest {
/**
* @param args
*/
public static void main(String[] args) {
new Sub();
}
}
執(zhí)行之后的結果是
老子的靜態(tài)塊
兒子的靜態(tài)塊
老子的非靜態(tài)塊
老子的無參構造函數(shù)
兒子的非靜態(tài)塊
兒子的無參構造函數(shù)
兒子的靜態(tài)塊
老子的非靜態(tài)塊
老子的無參構造函數(shù)
兒子的非靜態(tài)塊
兒子的無參構造函數(shù)
由此可以得出結論:
0.靜態(tài)代碼塊總會在實例對象創(chuàng)建之前執(zhí)行,因為它是屬于類對象級別的代碼塊,JVM先在內(nèi)存中分配好了類對象的空間,執(zhí)行完靜態(tài)塊后再去理會實例對象作用域的東西。
1.總是執(zhí)行父類的非靜態(tài)塊
2.隱式調用父類的無參構造函數(shù),或者現(xiàn)實調用父類的有參構造函數(shù)
3.執(zhí)行子類的非靜態(tài)塊
4.根據(jù)程序需要(就是new后面的構造器函數(shù))調用子類的構造函數(shù)
下面來看看一個不太規(guī)范的父子程序引發(fā)的問題。
0.靜態(tài)代碼塊總會在實例對象創(chuàng)建之前執(zhí)行,因為它是屬于類對象級別的代碼塊,JVM先在內(nèi)存中分配好了類對象的空間,執(zhí)行完靜態(tài)塊后再去理會實例對象作用域的東西。
1.總是執(zhí)行父類的非靜態(tài)塊
2.隱式調用父類的無參構造函數(shù),或者現(xiàn)實調用父類的有參構造函數(shù)
3.執(zhí)行子類的非靜態(tài)塊
4.根據(jù)程序需要(就是new后面的構造器函數(shù))調用子類的構造函數(shù)
下面來看看一個不太規(guī)范的父子程序引發(fā)的問題。
package se01;
class Par1 {
private int num = 20;
public Par1() {
System.out.println("par-num:" + num);
this.display();
}
public void display() {
System.out.println("num:" + num + " class:"
+ this.getClass().getName());
}
}
class Sub1 extends Par1 {
private int num = 40;
public Sub1() {
num = 4000;
}
public void display() {
System.out.println("sub-num:" + num + " class:"
+ this.getClass().getName());
}
}
public class ParSubErrorTest {
public static void main(String[] args) {
new Sub1();
}
}
class Par1 {
private int num = 20;
public Par1() {
System.out.println("par-num:" + num);
this.display();
}
public void display() {
System.out.println("num:" + num + " class:"
+ this.getClass().getName());
}
}
class Sub1 extends Par1 {
private int num = 40;
public Sub1() {
num = 4000;
}
public void display() {
System.out.println("sub-num:" + num + " class:"
+ this.getClass().getName());
}
}
public class ParSubErrorTest {
public static void main(String[] args) {
new Sub1();
}
}
當然,一般在實際項目開發(fā)中也不會這么寫代碼,不過這代碼給咱們的啟示是揭示了JVM的一些內(nèi)幕。執(zhí)行結果是
par-num:20
sub-num:0 class:se01.Sub1
sub-num:0 class:se01.Sub1
就像剛剛得出的5條結論一樣,在new Sub1();的時候先要對父類進行構造函數(shù)的調用,而父類的構造函數(shù)又調用了方法display(),這個時候問題就出現(xiàn)了,父類究竟調用的是誰的構造方法?是父類自己的,還是子類重寫的?結論很簡單了,就是子類若重寫了該方法,那么直接調用子類的重寫方法,如果沒有重寫該方法,那么直接由父類對象直接調用自己的方法即可。由上面程序可以看出子類重寫了該display()方法,那么在調用子類的構造函數(shù)之前是先調用了父類的無參構造函數(shù),之后在父類無參構造函數(shù)中調用了子類重寫后的display()方法,而此時,子類對象還沒實例化完畢呢,僅僅在內(nèi)存中分配了相應的空間而已,實例變量僅僅有系統(tǒng)默認值而已,并沒有完成賦值的過程,所以,此時子類的實例變量num是默認值0,導致調用子類方法時顯示num也是0。而父類的實例變量當然此時已經(jīng)初始化完畢了,實例對象也有了,自然它的num是賦予初始值后的20嘍。
而這程序的問題,或者說不規(guī)范的地方在哪里呢?就是它將構造函數(shù)用于了其他用途,構造函數(shù)實際上就是為了初始化數(shù)據(jù)用的,而不是用于調用其他方法用的,此程序在構造函數(shù)中調用了自己聲明的一個public方法,無異于扭曲了構造函數(shù)本身的作用,雖然說這么寫編譯器不會報錯,但是無異于給繼承機制帶來了隱患。
5.繼承機制在處理成員變量和方法時的區(qū)別
而這程序的問題,或者說不規(guī)范的地方在哪里呢?就是它將構造函數(shù)用于了其他用途,構造函數(shù)實際上就是為了初始化數(shù)據(jù)用的,而不是用于調用其他方法用的,此程序在構造函數(shù)中調用了自己聲明的一個public方法,無異于扭曲了構造函數(shù)本身的作用,雖然說這么寫編譯器不會報錯,但是無異于給繼承機制帶來了隱患。
5.繼承機制在處理成員變量和方法時的區(qū)別
package se01;
class Parent2 {
int a = 1;
public void test01() {
System.out.println(a);
}
}
class Sub2 extends Parent2 {
int a = 2;C#字符串比較Compare使用指南
public void test01() {
System.out.println(a);
}
}
public class ParSubPMTest {
public static void main(String[] args) {
Parent2 sub2 = new Sub2();
Sub2 sub3 = (Sub2)sub2;
System.out.println(sub2.a);
sub2.test01();
System.out.println(sub3.a);
sub3.test01();
}
}
class Parent2 {
int a = 1;
public void test01() {
System.out.println(a);
}
}
class Sub2 extends Parent2 {
int a = 2;C#字符串比較Compare使用指南
public void test01() {
System.out.println(a);
}
}
public class ParSubPMTest {
public static void main(String[] args) {
Parent2 sub2 = new Sub2();
Sub2 sub3 = (Sub2)sub2;
System.out.println(sub2.a);
sub2.test01();
System.out.println(sub3.a);
sub3.test01();
}
}
輸出結果是
1
2
2
2
2
2
2
也就是說通過直接訪問實例變量的時候是顯示父類特性的,當使用方法的時候則顯示運行時特性。實際上父子關系在內(nèi)存中存儲是這樣的
就是說實例對象雖然都是同一個,但是這個實例實際上既存儲了自己的變量,也存儲了父類的變量,當使用父類聲明的對象訪問變量時呈現(xiàn)父親的變量值,使用子類的對象直接訪問變量時呈現(xiàn)子類的值。也就是說當我們初始化一個子類對象時,會將它所有的父類(這里是單繼承的意思,所有的父類就是說父親、爺爺、曾祖、曾曾祖父……)的實例變量分配內(nèi)存空間。如果子類定義的實例變量與父類同名,那么會隱藏父類的變量,并不是完全覆蓋,通過父類.變量依然能夠獲得父類的實例變量。6. Java內(nèi)存管理技巧1:盡量使用直接量,而盡量不要用new的方式建立這些對象,比如
就是說實例對象雖然都是同一個,但是這個實例實際上既存儲了自己的變量,也存儲了父類的變量,當使用父類聲明的對象訪問變量時呈現(xiàn)父親的變量值,使用子類的對象直接訪問變量時呈現(xiàn)子類的值。也就是說當我們初始化一個子類對象時,會將它所有的父類(這里是單繼承的意思,所有的父類就是說父親、爺爺、曾祖、曾曾祖父……)的實例變量分配內(nèi)存空間。如果子類定義的實例變量與父類同名,那么會隱藏父類的變量,并不是完全覆蓋,通過父類.變量依然能夠獲得父類的實例變量。6. Java內(nèi)存管理技巧1:盡量使用直接量,而盡量不要用new的方式建立這些對象,比如
String string = "1";
Long longlong = 1L;
Byte bytebyte = 1;
Short shortshort = 1;
Integer integer = 22;
Float floatfloat = 2.2F;
Double doubledouble = 0.333333;
Boolean booleanboolean = false;
Character character = 'm';
Long longlong = 1L;
Byte bytebyte = 1;
Short shortshort = 1;
Integer integer = 22;
Float floatfloat = 2.2F;
Double doubledouble = 0.333333;
Boolean booleanboolean = false;
Character character = 'm';
2:盡量使用StringBuffer和StringBuilder來進行字符串的的鏈接和使用,這個就不用解釋了吧,很常用,尤其是拼接SQL的時候。
3:養(yǎng)成習慣,盡早釋放無用對象
例如如下程序:
3:養(yǎng)成習慣,盡早釋放無用對象
例如如下程序:
public void test(){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder = null;
//很消耗時間………………………………
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder = null;
//很消耗時間………………………………
}
在很消耗時間的程序執(zhí)行前將變量就盡量釋放掉,讓JVM垃圾回收期去回收去。
4:不到萬不得以,不要輕易使用static變量,雖然static變量很常用,不過這個類變量會常駐內(nèi)存,從對象復用的角度講,倒是省了資源了,但是如果不是經(jīng)常復用的對象而聲明了static變量就會常駐內(nèi)存,只要程序還在運行就永不會回收。
5:避免創(chuàng)建重復對象變量
4:不到萬不得以,不要輕易使用static變量,雖然static變量很常用,不過這個類變量會常駐內(nèi)存,從對象復用的角度講,倒是省了資源了,但是如果不是經(jīng)常復用的對象而聲明了static變量就會常駐內(nèi)存,只要程序還在運行就永不會回收。
5:避免創(chuàng)建重復對象變量
for(int i=0;i<10;i++){
Use use = new Use();
}
Use use = new Use();
}
如上代碼創(chuàng)建了很多個臨時對象變量use,實際上可以改進成
Use use = null;
for(int i=0;i<10;i++){
use = new Use();
use = null;
}
for(int i=0;i<10;i++){
use = new Use();
use = null;
}
6:盡量不要自己使用對象的finalize方法
不到萬不得以,千萬不要在此方法中進行變量回收等等操作。
7:如果運行時環(huán)境要求空間資源很嚴格,那么可以考慮使用軟引用SoftReference對象進行引用。當內(nèi)存不夠時,它會犧牲自己,釋放軟引用對象。軟引用對象適用于比較瞬時的處理程序,處理完了就完了,內(nèi)存不夠會先將此對象控件騰出來而不回內(nèi)存溢出的報錯誤。(關于垃圾回收和對象各種方式的引用會在之后學習筆記中體現(xiàn))
7.總結
主要復習了數(shù)組的內(nèi)存形式、父子對象的一些調用陷阱、父子關系在內(nèi)存中的形式、內(nèi)存的使用技巧。
不到萬不得以,千萬不要在此方法中進行變量回收等等操作。
7:如果運行時環(huán)境要求空間資源很嚴格,那么可以考慮使用軟引用SoftReference對象進行引用。當內(nèi)存不夠時,它會犧牲自己,釋放軟引用對象。軟引用對象適用于比較瞬時的處理程序,處理完了就完了,內(nèi)存不夠會先將此對象控件騰出來而不回內(nèi)存溢出的報錯誤。(關于垃圾回收和對象各種方式的引用會在之后學習筆記中體現(xiàn))
7.總結
主要復習了數(shù)組的內(nèi)存形式、父子對象的一些調用陷阱、父子關系在內(nèi)存中的形式、內(nèi)存的使用技巧。