Java学习之旅基础知识篇:数组及引用类型内存分配
在上一篇中,我们已经了解了数组,它是一种引用类型,本篇将详细介绍数组的内存分配等知识点。数组用来存储同一种数据类型的数据,一旦初始化完成,即所占的空间就已固定下来,即使某个元素被清空,但其所在空间仍然保留,因此数组长度将不能被改变。当仅定义一个数组变量(int[] numbers)时,该变量还未指向任何有效的内存,因此不能指定数组的长度,只有对数组进行初始化(为数组元素分配内存空间)后才可以使用。数组初始化分为静态初始化(在定义时就指定数组元素的值,此时不能指定数组长度)和动态初始化(只指定数组长度,由系统分配初始值)。
//静态初始化 int[] numbers = new int[] { 3, 5, 12, 8, 7 }; String[] names = { "Miracle", "Miracle He" };//使用静态初始化的简化形式
//动态初始化 int[] numbers = new int[5]; String[] names = new String[2];
建议不要混用静态初始化和动态初始化,即不要既指定数组的长度的同时又指定每个元素的值。当初始化完毕后,就可以按索引位置(0~array.length-1)来访问数组元素了。当使用动态初始化时,如在对应的索引位未指定值的话,系统将指定相应数据类型对应的默认值(整数为0,浮点数为0.0,字符为'\u0000',布尔类型为false,引用类型为null)。
public class TestArray { public static void main(String[] args) { String[] names = new String[3]; names[0] = "Miracle"; names[1] = "Miracle He"; //以下代码将输出Miracle Miracle He null /* for(int i = 0; i < names.length;i++) { System.out.print(names[i] + " "); } */ //还可以使用foreach来遍历 for(String name : names) { System.out.print(name + " "); } } }
请注意:java中是没有foreach这个关键字的,其语法是for(type item : items)来表示,但foreach只能用于遍历元素的值而不能改变,必须使用for才能实现。
public class TestForEach { public static void main(String[] args) { int[] numbers = { 3, 5, 12, 8, 7 }; for(int number : numbers) { int num = number * 10; System.out.print(num + ","); } System.out.println(""); //numbers仍然未发生变化(如果换成for将改变) for(int i = 0;i < numbers.length;i++) { System.out.print(numbers[i] + ","); } } }
以上简单的介绍了数组的初始化和应用,接下来讲详细介绍数组(数组引用和数组元素)在内存中的存放形式。首先给出结论:数组引用变量是存放在栈内存(stack)中,数组元素是存放在堆内存(heap)中,通过栈内存中的指针指向对应元素的在堆内存中的位置来实现访问,以下图来说明数组此时的存放形式。
那什么是栈内存和堆内存呢?我举例作一一解释。当执行方法时,该方法都会建立自身的内存栈,以用来将该方法内部定义的变量逐个加入到内存栈中,当执行结束时方法的内存栈也随之销毁,我们说所有变量存放在栈内存中,即随着寄存主体的消亡而消亡;反之,当我们创建一个对象时,这个对象被保存到运行时数据区中,以便反复利用(因为创建成本很高),此时不会随着执行方法的结束而消亡,同时该对象还可被其他对象所引用,只有当这个对象没有被任何引用变量引用时,才会在垃圾回收在合适的时间点回收,我们说此时变量所指向的运行时数据区存在堆内存中。
只有类型兼容(即属于同一数据类型体系且遵守优先级由低到高原则),才能将数组引用传递给另一数组引用,但仍然不能改变数组长度(仅仅只是调整数组引用指针的指向)。
public class TestArrayLength { public static void main(String[] args) { int[] numbers = { 3, 5, 12 }; int[] digits = new int[4]; System.out.println("digits数组长度:" + digits.length);//4 for(int number : numbers) { System.out.print(number + ",");//3,5,12, } System.out.println(""); for(int digit : digits) { System.out.print(digit + ",");//0,0,0,0, } System.out.println(""); digits = numbers; System.out.println("digits数组长度:" + digits.length);//3 } }
虽然看似digits的数组长度看似由4变成3,其实只是numbers和digits指向同一个数组而已,而digits本身失去引用而变成垃圾,等待垃圾回收来回收(但其长度仍然为4),但其内部运行机制如下图所示。
因此当我们看一个数组时(或者其他引用变量),通常看成两部分:数组引用变量和数组元素本身,而数据元素是存放在堆内存中,只能通过数组引用变量来访问。
从上述的示例中看出数组中存放的是基本类型,其实数组中还可以存放引用类型的。而存放基本类型的内存分布已经解释了,而存放引用类型的内存分布则相对复杂了。来看一段非常简单的程序。
public class TestPrimitiveArray { public static void main(String[] args) { //1.定义数组 int[] numbers; //2.分配内存空间 numbers = new int[4]; //3.为数组元素指定值 for(int i = 0;i < numbers.length;i++) { numbers[i] = i * 10; } } }
按以上步骤的内存分布示意图:
从图中可看出数组元素直接存放在堆内存中,当操作数组元素时,实际上是操作基本类型的变量。接下来再看一段程序:
class Person { public int age; public String name; public void display() { System.out.println(name + "的年龄是: " + age); } } public class TestReferenceArray { public static void main(String[] args) { //1.定义数组 Person[] persons; //2.分配内存空间 persons = new Person[2]; //3.为数组元素指定值 Person p1 = new Person(); p1.age = 28; p1.name = "Miracle"; Person p2 = new Person(); p2.age = 30; p2.name = "Miracle He"; persons[0] = p1; persons[1] = p2; //输出元素的值 for(Person p : persons) { p.display(); } } }
对于数组元素为引用类型在内存中的存储与基本类型不一样,此时数组元素仍然存放引用,指向另一块内存,在其中存放有效的数据。
public class TestMultiArray { public static void main(String[] args) { //1.定义二维数组 int[][] numbers; //2.分配内存空间 numbers = new int[3][]; //可以把numbers看作一维数组来处理 for(int i = 0;i < numbers.length;i++) { System.out.print(numbers[i] + ",");//null,null,null } System.out.println(""); //3.为数组元素指定值 numbers[0] = new int[2]; numbers[0][1] = 1; for(int i = 0;i < numbers[0].length;i++) { System.out.print(numbers[0][i] + ",");//0,1 } } }
import java.util.Arrays; public class TestArrays { public static void main(String[] args) { int[] a = {3, 4, 5, 6}; int[] b = {3, 4, 5, 6}; System.out.println("a和b是否相等:" + Arrays.equals(a, b));//true System.out.println("5在a中的位置:" + Arrays.binarySearch(a, 5));//2 int[] c = Arrays.copyOf(a, 6); System.out.println("a和c是否相等:" + Arrays.equals(a, c));//false System.out.println("c的元素:" + Arrays.toString(c));//3,4,5,6,0,0 Arrays.fill(c, 2, 4, 1);//将c中第3个到第5个元素(不包含)赋值为1 System.out.println("c的元素:" + Arrays.toString(c));//3,4,1,1,0,0 Arrays.sort(c); System.out.println("c的元素:" + Arrays.toString(c));//0,0,1,1,3,4 } }
接下来,给出两个数组实际应用场景的示例。
import java.util.Arrays; public class NumberToRMB { private String[] numbers = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; private String[] units = { "拾", "佰","仟" }; /** * 把一个浮点数分成整数部分和小数部分 * @param number 要进行分割的浮点数 * @return 由整数部分和小数部分组成的字符串数组 */ private String[] divide(double number) { long zheng = (long)number; long xiao = Math.round((number - zheng) * 100); return new String[] { zheng + "", String.valueOf(xiao) }; } /** * 把一个四位数字字符串转化四位人民币大写字符串 * @param str 要转化的四位数字字符串 * @return 四位人民币大写字符串 */ private String toRMBString(String str) { String money = ""; for(int i = 0, len = str.length(); i < len; i++) { int num = str.charAt(i) - 48; if(i != len - 1 && num != 0) { money += numbers[num] + units[len - 2 - i]; } else { money += numbers[num]; } } return money; } public static void main(String[] args) { NumberToRMB rmb = new NumberToRMB(); System.out.println(Arrays.toString(rmb.divide(2346.789))); System.out.println(rmb.toRMBString("2346")); } }
import java.io.*; public class WZQ { //定义一个二维数组当作棋盘 private String[][] board; //定义棋盘大小 private static int BOARD_SIZE = 15; //初始化棋盘 private void initBoard() { board = new String[BOARD_SIZE][BOARD_SIZE]; for(int i = 0; i < BOARD_SIZE; i++) { for(int j = 0; j < BOARD_SIZE; j++) { board[i][j] = "+"; } } } //打印棋盘 private void printBoard() { for(int i = 0; i < BOARD_SIZE; i++) { for(int j = 0; j < BOARD_SIZE; j++) { System.out.print(board[i][j]); } System.out.println(""); } } //开始下棋 public void play() throws Exception { initBoard(); printBoard(); //获取键盘输入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String input = null; do { if(input != null) { String[] pos = input.split(","); int x = Integer.parseInt(pos[0]); int y = Integer.parseInt(pos[1]); board[x - 1][y - 1] = "●"; printBoard(); } System.out.print("请输入你下棋的坐标(以x,y的形式):"); } while((input = br.readLine()) != null); } public static void main(String[] args) throws Exception { WZQ wzq = new WZQ(); wzq.play(); } }
数字转化为人民币大写程序中,利用了一维数组表示大写及单位;五子棋游戏中,利用了二维数组表示棋盘。从程序中可看到throws Exception表示不处理任何异常,将在后续的篇章中继续讲解。