很早以前就听过李刚老师的疯狂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]);
}
}
}

《疯狂Java:突破程序员基本功的16课》读书笔记-第一章 数组与内存控制的更多相关文章

  1. 《疯狂java-突破程序员基本功的16课 》笔记总结

    本人最近读完<疯狂java-突破程序员基本功的16课 >读完后,感觉对java基础又有了新的认识,在这里总结一下:一.数组与内存控制 1.1 数组初始化     java语言的数组是静态的 ...

  2. 疯狂Java:突破程序员基本功的16课-李刚编著 学习笔记(未完待续)

    突破程序员基本功(16课) 数组 静态语言: 在编译的时候就能确定数据类型的语言,大多静态语言要求在使用变量之前必须声明数据类型(少数具有强推导能力的现代语言不用) 动态语言: 在程序运行时确定数据类 ...

  3. 《疯狂Java:突破程序员基本功的16课》读书笔记-第二章 对象与内存控制

    Java内存管理分为两个方面:内存分配和内存回收.这里的内存分配特指创建Java对象时JVM为该对象在堆内存中所分配的内存空间.内存回收指的是当该Java对象失去引用,变成垃圾时,JVM的垃圾回收机制 ...

  4. 类变量的初始化时机(摘录自java突破程序员基本功德16课)

    先看书本的一个例子,代码如下: public class Price { final static Price INSTANCE=new Price(2.8); static double initP ...

  5. Android群英传神兵利器读书笔记——第一章:程序员小窝——搭建高效的开发环境

    1.1 搭建高效的开发环境之操作系统 1.2 搭建开发环境之高效配置 基本环境配置 基本开发工具 1.3 搭建程序员的博客平台 开发者为什么要写作 写作平台 第三方博客平台 自建博客平台 开发论坛 1 ...

  6. 《程序员的自我修养》读书笔记——系统调用、API

        系统调用 程序运行的时候,本身是没有权限访问多少系统资源的.系统资源有限,如果操作系统不进行控制,那么各个程序难免会产生冲突.线程操作系统都将可能产生冲突的系统资源保护起来,阻止程序直接访问. ...

  7. 《程序是怎样跑起来的》读书笔记——第一章 对程序员来说CPU是什么

    1 程序的运行流程 2 CPU的组成 3 寄存器的主要种类和功能 "程序计数器"--决定程序流程的 4 条件分支和循环机制 4.1 顺序执行 4.2 选择分支 5 函数的调用机制 ...

  8. pwn学习日记Day21 《程序员的自我修养》读书笔记

    Linux内核装载ELF过程 (1)bash进程调用fork()系统调用创建一个新的进程 (2)新的进程调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚才启动的新进程 ...

  9. pwn学习日记Day20 《程序员的自我修养》读书笔记

    可执行文件的装载与进程 覆盖装入和页映射是两种典型的动态装载方法 进程建立的三步 1.创建一个独立的虚拟地址空间 2.读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系. 3.将CPU的指令寄存 ...

随机推荐

  1. div中的内容垂直居中的五种方法

    一.行高(line-height)法 如果要垂直居中的只有一行或几个文字,那它的制作最为简单,只要让文字的行高和容器的高度相同即可,比如: p { height:30px; line-height:3 ...

  2. sqlzoo.net刷题

    只发后面提升题目的题解,前面的太简单,写下来也没有意义 12.查找尤金•奧尼爾EUGENE O'NEILL得獎的所有細節 Find all details of the prize won by EU ...

  3. 爆炸吧 js dom ---------> boom

    dom-> html css 事件 元素 eventlistener HTML 事件的例子: 当用户点击鼠标时 当网页已加载时 当图像已加载时 当鼠标移动到元素上时 当输入字段被改变时 当提交 ...

  4. php安全配置记录

    Php环境部署完成后,通常我们会进行一些安全设置.除了熟悉各种PHP漏洞外,还可以通过配置php.ini来加固PHP的运行环境.PHP官方也曾经多次修改php.ini的默认设置. 接下来,推荐php. ...

  5. AndroidStudio出现“Plugin is too old, please update to a more recent”问题

    可能原因: 你AS版本不够高....能够更新的话你更新试下,不能更新删了最新的sdk,不要下载4.4以上的版本 解决方法如下 第一种,最简单,但是不推荐这么做 将build.gradle 里的类似 c ...

  6. sql 索引 填充因子(转)

    和索引重建最相关的是填充因子.当创建一个新索引,或重建一个存在的索引时,你可以指定一个填充因子,它是在索引创建时索引里的数据页被填充的数量.填充因子设置为100意味着每个索引页100%填满,50%意味 ...

  7. 【转】【WPF】MVVM模式的3种command

    1.DelegateCommand 2.RelayCommand 3.AttachbehaviorCommand 因为MVVM模式适合于WPF和SL,所以这3种模式中也有一些小差异,比如RelayCo ...

  8. shell案例

    调用同目录下的ip.txt内容: 路径 [root@lanny ~]# pwd /root txt文件 [root@lanny ~]# cat ip.txt 10.1.1.1 10.1.1.2 10. ...

  9. MVC中利用ActionFilterAttribute过滤关键字

    在开发过程中,有时候会对用户输入进行过滤,以便保证平台的安全性.屏蔽的方法有很多种,但是今天我说的这种主要是利用MVC中的ActionFilterAttribute属性来实现.由于MVC天然支持AOP ...

  10. ThinkPHP项目CMS内容管理系统开发视频教程【20课】(3.02GB)

    ThinkPHP背景介绍:     ThinkPHP是一个免费开源的,快速.简单的面向对象的轻量级PHP开发框架,遵循Apache2开源协议发布,是为了敏捷WEB应用开发和简化企业级应用开发而诞生的. ...