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