zoukankan      html  css  js  c++  java
  • 《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制

    很早以前就听过李刚老师的疯狂java系列很不错,所以最近找一本拿来拜读,再此做下读书笔记,促进更好的消化。

    使用Java数组之前必须先对数组对象进行初始化。当数组的所有元素都被分配了合适的内存空间,并指定了初始值时,数组初始化完成。程序以后将不能重新改变数组对象在内存中的位置和大小。

    从用法角度来看,数组元素相当于普通变量,程序既可把数组元素的值赋给普通变量,也可把普通变量的值赋给数组元素。

    1.1数组初始化

    1.1.1 Java数组是静态的

    Java语言是典型的静态语言,因此Java的数组是静态的,即当数组被初始化之后,该数组的长度是不可变的。

    Java程序中的数组必须经过初始化才可使用。所谓初始化,就是为数组对象的元素分配内存空间,并为每个数组元素指定初始值。

    数组的初始化有以下两种方式。

    (1)静态初始化:初始化时由程序员显示指定每个数组元素的初始值,由系统决定数组长度。

    (2)动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。

    不管采用哪种方式初始化Java数组,一旦初始化完成,该数组的长度就不可改变,Java语言允许通过数组的length属性来访问数组的长度。示例如下:

    package cn.zhouyu.array;
    
    public class ArrayTest
    {
    	public static void main(String[] args)
    	{
    		//采用静态初始化方式初始化第1个数组
    		String[] books = new String[]{"疯狂java","轻量级java ee企业应用实战","疯狂ajax","疯狂XML"};
    		
    		//采用静态初始化的简化形式初始化第2个数组
    		String[] names = {"孙悟空","白骨精","猪八戒"};
    		
    		//采用动态初始化的方式初始化第3个数组
    		String[] strArray = new String[5];
    		
    		System.out.println("第1个数组的长度:" + books.length);
    			//输出4
    		System.out.println("第2个数组的长度:" + names.length);
    			//输出3
    		System.out.println("第3个数组的长度:" + strArray.length);
    			//输出5
    	}
    }
    

    books,names,strArray这3个变量以及各自引用的数组在内存中的分配图:

    从图中可以看出,对于静态初始化方式而言,程序员无需指定数组长度,指定该数组的数组元素,有系统来决定该数组的长度即可。

    例如books数组,为它指定了4个数组元素,那它的长度就是4;对于names数组,为它指定了3个元素,那它的长度就是3.

    执行动态初始化时,程序员只需指定数组的长度,即为每个数组元素指定所需的内存空间,系统将负责为这些数组元素分配初始值。指定初始值时,系统将按照如下规则分配初始值。

    整型(byte,short,int,long)是0

    浮点型(float,double)是0.0

    字符型(char)是'u0000'

    布尔型是false

    引用类型(类,接口,数组)是null

    不要同时使用静态初始化和动态初始化。也就是说,不要在进行数组初始化时,即指定数组的长度,也为每个数组元素分配初始值。

    Java的数组变量是一种引用类型的变量,数组变量并不是数组本身,它只是指向堆内存中的数组对象。因此,可以改变一个数组变量所引用的数组,这样可以造成数组长度可变的假象。

    //让books数组变量,stArr数组变量指向names所引用的数组

    books = names;

    strArray = names;

    System.out.println("books数组的长度:" + books.length);

    System.out.println("strArray数组的长度:" + strArray.length);

            

    //改变books数组变量所引用的数组的第2个元素值。

    books[1] = "白骨精";

    System.out.println("names数组的第2个元素是:" + books[1]);

    现在books数组变量,strArray数组变量都指向names数组变量所引用的数组,这样做的结果就是books,strArray,names这3个变量引用同一个数组对象。如图:

    从图中可以看出,此时strArr,names,books数组变量实际上引用同一个数组对象。因此,当访问books数组,strArr数组的长度时,将看到输出3.这很容易造成一个假想,books的数组长度从4变成3。实际上,数组对象本身的长度并没有发生改变,变的是books数组变量。books数组变量原本指向堆内存下面的数组,当执行了books=names后,books数组将改为指向堆内存中间的数组,而原来books变量所引用的数组长度依然是4。

    从图中可以看出,原来books变量所引用的数组长度依然是4,但不再有任何引用变量引用该数组,因此它将会变成垃圾,等着垃圾回收机制来回收。此时,程序使用books,names,strArr这3个变量时,将会访问同一个数组对象,隐藏把books数组的第2个元素赋值为“白骨精”时,names数组的第2个元素的值也会随之改变。

    1.1.2 数组一定要初始化吗

    在使用Java数组之前必须先初始化数组,也就是为数组元素分配内存空间,并指定初始值。实际上,如果真正掌握了Java数组在内存中分配机制,那么完全可以换一个方式来初始化数组,也就是说,数组无需经过初始化。

    package cn.zhouyu.array;
    
    public class ArrayTest3
    {
    	public static void main(String[] args)
    	{
    		int[] nums = new int[]{3,5,20,12};
    		int[] prices;
    		prices = nums;
    		
    		for(int i=0;i<prices.length;i++)
    		{
    			System.out.println(prices[i]);
    		}
    		
    		prices[2] = 34;
    		System.out.println("nums数组的第3个元素是:" + nums[2]);
    	}
    }
    

    程序定义了prices数组之后,并未对prices数组进行初始化。当执行int[] prices之后,如图

    从图中可以看出,此时的prices数组变量还未指向任何有效的内存,未指向任何数组对象,此时的程序还不可使用prices数组变量。

    当程序执行prices=nums后,prices变量将指向nums变量所引用的数组,此时prices变量和nums变量引用同一个数组对象。执行这条语句之后,prices变量已经指向有效的内存及一个长度为4的数组对象,因此程序完全可以正常使用prices变量了。

    常常说使用java数组之前必须先进行初始化,可是现在prices变量却无需初始化,这不是互相矛盾吗?其实一点都不矛盾,关键是大部分时候,我们把数组变量和数组对象搞混了,数组变量只是一个引用变量(有点类似于C里的指针),通常存放在栈内存中(也可被放入堆内存中),而数组对象就是保存在堆内存中的连续内存空间。对数组执行初始化,其实并不是对数组变量执行初始化,而是要对数组对象执行初始化,也就是为该数组对象分配一块连续的内存空间,这块连续内存空间的长度就是数组的长度。虽然上面程序中的prices变量看似没有经过初始化,但执行prices=nums就会让prices变量直接指向一个已经执行初始化的数组。

    对于数组变量来说,它并不需要进行所谓的初始化,只要让数组变量指向一个有效的数组对象,程序即可正常使用该数组变量。

    对于Java程序中所有的引用变量,它们都不需要经过所谓的初始化操作,需要进行初始化操作的是该引用变量所引用的对象。比如,数组变量不需要进行初始化操作,而数组对象本身需要进行初始化;对象的引用变量也不需要进行初始化,而对象本身才需要进行初始化。

    1.1.3基本类型数组的初始化

    对于基本类型数组而言,数组元素的值直接存储在对应的数组元素中,因此基本类型数组的初始化比较简单:程序直接先为数组分配内存空间,再将数组元素的值 对应内存里。

    下面程序采用静态初始化的方式初始化了一个基本类型的数组对象。

    public class PrimitiveArrayTest
    {
    	public static void main(String[] args)
    	{
    		//定义一个int[]类型的数组变量
    		Int[] iArr;
    		//静态初始化数组,数组长度为4
    		iArr = new int[]{2,5,-12,20};
            }
    }
    

    上面代码的执行过程代表了基本类型数组初始化的典型过程。下面将结合示意图详细介绍这段代码的执行过程。

    执行第一行代码int[] iArr;时,仅定义一个数组变量,此时内存中的存储如图1.4所示。

    执行了int[] iArr;代码后,仅在main方法栈中定义了一个iArr数组变量,它是一个引用类型的变量,并未指向任何有效的内存,没有真正指向实际的数组对象。此时还不能使用该数组对象。

    当执行iArr = new int[]{2,5,-12,20};静态初始化后,系统会根据程序员指定的数组元素来决定数组的长度。此时指定了4个数组元素,系统将创建一个长度为4的数组对象,一旦该数组对象创建成功,该数组的长度将不可改变,程序只能改变数组元素的值。此时内存中的存储如图1.5所示。

    静态初始化完成后,iArr数组变量所引用的数组所占用的内存空间被固定下来,程序员只能改变各数组元素内的值,但不能移动该数组所占用的内存空间,既不能扩大该数组对象所占用的内存,也不能缩减该数组对象所占用的内存。

    有些书籍中总是不断地重复:基本类型变量的值存储在栈内存中,其实这句话是完全错误的。例如图1.5中的2,5,-12,20,他们都是基本类型的值,但实际上它们却存储在堆内存中。实际上应该说:所有局部变量都是存放在栈内存里保存的,不管其是基本类型的变量,还是引用类型的变量,都是存储在各自的方法栈区中;但引用类型变量所引用的对象(包括数组,普通java对象)则总是存储在堆内存中。

             对于Java语言而言,堆内存中的对象(不管是数组对象,还是普通的Java对象)通常不允许直接访问,为了访问堆内存中的对象,通常只能通过引用变量。这也是很容易混淆的地方。例如,iArr本质上只有main栈区的引用变量,但使用iArr.length,iArr[2]时,系统将会自动变为访问堆内存中的数组对象。

             对于很多Java程序员而言,他们最容易混淆的是:引用类型变量何时只是栈内存中的变量本身,何时又变为引用实际的Java对象。其实规则很简单:引用变量本质上只是一个指针,只要程序通过引用变量访问属性,或者通过调用引用变量来调用方法,该引用变量将会由它所引用的对象代替。

    1.1.4 引用类型数组的初始化

             引用类型数组的数组元素依然是引用类型的,因此数组元素里存储的还是引用,它指向另一块内存,这块内存里存储了该引用变量所引用的对象(包括数组和Java对象)。

             为了说明引用类型数组的运行过程,下面程序先定义了一个Person类,然后定义了一个Person[]数组,并动态初始化了该Person[]数组,再显式为数组的不同数组元素指定值。

    class Person
    {
    	//年龄
    	public int age;
    	//身高
    	public double height;
    	//定义一个info方法
    	public void info()
    	{
    		System.out.println("我的年龄是:" + age + ",我的身高是:" + height);
    	}
    }
    public class ReferenceArrayTest
    {
    	public static void main(String[] args) 
    	{
    		//定义一个students数组变量,其类型是Person[]
    		Person[] students;
    		//执行动态初始化
    		students = new Person[2];
    		System.out.println("students所引用的数组的长度是:" + students.length);
    		//创建一个Person实例,并将这个Person实例赋给zhang变量
    		Person zhang = new Person();
    		//为zhang所引用的Person对象的属性赋值
    		zhang.age = 15;
    		zhang.height = 158;
    		//创建一个Person实例,并将这个Person实例赋给lee变量
    		Person lee = new Person();
    		//为lee所引用的Person对象的属性赋值
    		lee.age = 16;
    		lee.height = 161;
    		//将zhang变量的值赋给第一个数组元素
    		students[0] = zhang;
    		//将lee变量的值赋给第二个数组元素
    		students[1] = lee;
    		//下面两行代码的结果完全一样,因为lee和students[1]指向的是同一个Person实例。
    		lee.info();
    		students[1].info();
    	}
    }
    

    上面代码的执行过程代表了引用类型数组的初始化的典型过程。下面将结合示意图详细介绍这段代码的执行过程。

             执行Person[] students;代码时,这行代码仅仅在栈内存中定义了一个引用变量,也就是一个指针,这个指针并未指向任何有效的内存区。此时内存中的存储如图:

    上图中的栈内存中定义了一个students变量,它仅仅是一个空引用,并未指向任何有效的内存。直到执行初始化,本程序对students数组执行动态初始化。动态初始化由系统为数组元素分配默认的初始值null,即每个数组元素的值都是null。执行动态初始化后的存储如图:

    从上图可以看出,students数组的2个数组元素都是引用,而这2个引用并未指向任何有效的内存,因此,每个数组元素的值都是null。此时,程序可以通过students来访问它所引用的数组的属性。

             Students数组是引用类型的数组,因此students[0],students[1]两个数组元素相当于两个引用类型的变量。如果程序只是直接输出这两个引用类型的变量,那程序完全正常。但程序依然不能通过students[0],students[1]来调用属性或方法,因此它们还未指向任何有效的内存区,所以这两个连续的Person变量(students数组的数组元素)还不能被使用。

             接着,程序定义了zhang和lee两个引用变量,并让它们指向堆内存中的两个Person对象,此时的zhang,lee两个引用变量存储在main方法栈区中,而两个Person对象则存储在堆内存中。此时的内存存储如图:

    对于zhang,lee两个引用变量来说,它们可以指向任何有效的Person对象,而students[0],students[1]也可指向任何有效的Person对象。从本质上来看,zhang,lee,students[0],students[1]所能存储的内容完全相同。接着,程序执行students[0] = zhang;student[1] = lee;两行代码,也就是让zhang和students[0]指向同一个Person对象,让lee和students[1]指向同一个Person对象,此时的内存存储如图:

    从上图可以看出,此时zhang和student[0]指向同一个内存区,而它们都是引用类型变量,因此通过zhang和students[0]来访问Person实例的属性和方法的效果完全一样,不论修改students[0]所指向的Person实例的属性,还是修改zhang变量所指向的Person实例的属性,所修改的其实是同一个内存区,所以必然互相影响。同理,lee和student[1]也是引用到同一个Person对象,也有相同的效果。

    1.2使用数组

             当数组引用变量指向一个有效的数组对象之后,程序就可以通过该数组引用变量来访问数组对象。Java语言不允许直接访问堆内存中的数据,因此无法直接访问堆内存中的数组对象,程序将通过数组引用变量来访问数组。

    1.2.1 数组元素就是变量

             只要在已有数据类型之后增加方括号,就会产生一个新的数组类型:例如

    int -> int[],String -> String[],Person -> Person[]。

             当程序需要多个类型相同的变量来保存程序状态时,可以考虑使用数组来保存这些变量。当一个数组初始化完成,就相当于定义了多个类型相同的变量。

             无论哪种类型的数组,其数组元素其实想当于一个普通变量,把数组类型之后的方括号去掉后得到的类型就是该数组元素的类型。

             当通过索引来使用数组元素时,将该数组元素当初普通变量使用即可,包括访问该数组元素的值,为数组元素赋值,等等。

    1.2.2 没有多维数组

             前面已经指出:只要在已有数据类型之后增加方括号,就会产生一个新的数组类型。如果已有的类型是int,增加后是int[]类型,这是一个数组类型,如果再增加就是int[][],这依然是数组类型,如果再增加就是int[][][],这依然是数组类型。反过来也是一样。

             从上面分析可以看出,所谓多维数组,其实只是数组元素依然是数组的1维数组:2维数组是数组元素是1维数组的数组,3维数组是数组元素是2维数组的数组,4维数组是数组元素是3维数组的数组……N维数组是数组元素是N-1维数组的数组。

             Java允许将多维数组当成1维数组处理。初始化多维数组时可以先只初始化最左边的维数,此时该数组的每一个元素都相当于一个数组引用变量,这些数组元素还需要进一步初始化。

    public class TwoDimensionTest
    {
    	public static void main(String[] args) 
    	{
    		//定义一个二维数组
    		int[][] a;
    		//把a当成一维数组进行初始化,初始化a是一个长度为3的数组
    		//a数组的数组元素又是引用类型
    		a = new int[4][];
    		//把a数组当成一维数组,遍历a数组的每个数组元素
    		for (int i = 0; i < a.length ; i++ )
    		{
    			System.out.println(a[i]);
    		}
    		//初始化a数组的第一个元素
    		a[0] = new int[2];
    		//访问a数组的第一个元素所指数组的第二个元素
    		a[0][1] = 6;
    		//a数组的第一个元素是一个一维数组,遍历这个一维数组
    		for (int i = 0 ; i < a[0].length ; i ++ )
    		{
    			System.out.println(a[0][i]);
    		}
    	}
    }
    
  • 相关阅读:
    codeforces 616B Dinner with Emma
    codeforces 616A Comparing Two Long Integers
    codeforces 615C Running Track
    codeforces 612C Replace To Make Regular Bracket Sequence
    codeforces 612B HDD is Outdated Technology
    重写父类中的成员属性
    子类继承父类
    访问修饰符
    方法的参数
    实例化类
  • 原文地址:https://www.cnblogs.com/zyaizz/p/3508179.html
Copyright © 2011-2022 走看看