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();
        }
    }
}

对于数组元素为引用类型在内存中的存储与基本类型不一样,此时数组元素仍然存放引用,指向另一块内存,在其中存放有效的数据。

谈到这里,不知是否有朋友要问:Java的多维数组是什么样的?我的回答是:可以有。为什么呢?从底层来看,数组元素可以存放引用类型,包含数组。也就是说在数组元素的内部还可以包含数组(如int[][] numbers = new int[length][]),也即二维数组可当作一维数组(数组长度为length)来处理,也可以同时指定多个维度的长度(如int[][] matrix = new int[length][width]),不过必须至少指定最左端的数组长度length。由此我们得出结论: 任何多维数组(维度为n,n>1)都当作一维数组,其数组元素为n-1维数组

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
        }
    }
}

最后,简单介绍一下Arrays(位于java.util下)的静态方法:binarySearch、copyOf、copyOfRange、equals、fill、sort、toString等方法(具体用法参见JDK)。

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表示不处理任何异常,将在后续的篇章中继续讲解。

时间: 2024-10-24 22:05:00

Java学习之旅基础知识篇:数组及引用类型内存分配的相关文章

java基础知识回顾之javaIO类--内存操作流ByteArrayInputStream和ByteArrayOutputSteam(操作字节数组)

直接看代码: package cn.itcast.io.p6.bytestream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; public class ByteArrayStreamDemo { /** * @param args * @throws IOException * 特点 * 1.内存操作流 * 2.不操作底层资源,不调

c++基础知识篇:指针

从面试的反馈来看,这部分可以问的很难. 1.指针与引用的区别 指针是一个变量,用来存放地址的变量.引用是原来变量的存储空间的别名. ? 2.指针作为参数的要点 a.需要进行指针的合法性检验,防止空指针. b.需要修改指针本身指向的地址时,参数需要是该指针的引用. ? 3.c++程序运行空间 数据区(Data Area):全局变量.静态变量.常量存放在数据区. 代码区(Code Area):所有类成员函数和非成员函数 栈区(Stack Area):为运行函数分配的局部变量.函数参数.返回数据.返回

C#基础知识篇(五)-----------C#笔记

一.值类型和引用类型 1>值类型和引用类型将我们学过的数据类型划分成了两部分. 划分的依据是不同类型的数据在内存中(堆栈)存储的结构不同. 2>值类型:所有的数值类型:long int short byte ulong uint ushort sbyte decimal duoble float char bool 枚举 结构 3>引用类型:string,arry(数组),类(class) 4>不管是值类型还是引用类型赋值都是将数据copy一份将副本赋给变量,不同的是值类型拷贝的是

C#基础知识篇(二)-----------C#笔记

1.关系运算符(比较运算符) 1.关系运算符有哪些? >,< ==,!= >=,<= 2.关系运算符的作用? 用于比较两个事物之间的关系. 3.什么叫关系表达式? 由关系运算符连接起来的式子叫关系表达式. 注意:所有的关系表达式最终都能计算成一个bool类型的值. 2.逻辑运算符 1.逻辑表达式有哪些? 逻辑与:&& ,逻辑或:||  ,逻辑非:!(又叫取反) 2.逻辑运算 语法:表达式1 逻辑运算符  表达式2 逻辑运算符连接的两个表达式,要最终能求解成一个boo

C#基础知识篇(三)-----------C#笔记

一.方法 1. 什么叫做方法? 方法就是对一段代码的重用的机制. 2. 方法的定义: [访问修饰符] [static] 返回值类型 方法名() { 方法体; } 注意:用[]修饰的都是可选的. 3. 需要注意的细节: 命名规则:方法名开头大写,参数名开头小写,参数名.变量名要有意义. 4. 方法的参数: 1>在方法名后面括号内定义变量就叫做定义这个方法的参数(形参). 2>在方法()中我们定义多个参数时,参数之间用逗号分隔,不管参数之间的类型是否相同,都不能像定义同类型的多个变量时:如:int

Java中String的基础知识

Java中String的基础知识 ==与equal的区别 基本数据类型,指的是java中的八种基本数据结构(byte,short,char,int,long,float,double,boolean),一般的比较是使用的 ==,比较的是他们的值. 复合数据类型(类) ==比较的是两个对象的引用,可以理解为在内存中的地址,除非是同一个new出来的对象,他们的 ==为true,否则,都为false. equal是object中的方法.object中的实现如下,内部还是使用==实现,也就是说,如果一个

[基础] Java目录(摘自Java核心技术·卷1 基础知识)

Java核心技术·卷1 基础知识(原书第9版) 第1章 Java程序设计概述 1.1 Java程序设计平台 1.2 Java"白皮书"的关键术语 1.2.1 简单性 1.2.2 面向对象 1.2.3 网络技能 1.2.4 健壮性 1.2.5 安全性 1.2.6 体系结构中立 1.2.7 可移植性 1.2.8 解释型 1.2.9 高性能 1.2.10 多线程 1.2.11 动态性 1.3 Java applet与Internet 1.4 Java发展简史 1.5 关于Java的常见误解

Java多线程完整版基础知识

Java多线程完整版基础知识 (翟开顺由厚到薄系列) 1.前言 线程是现代操作系统中一个很重要的概念,多线程功能很强大,java语言对线程提供了很好的支持,我们可以使用java提供的thread类很容易的创建多个线程.线程很不难,我对之前学习过的基础,在这做了一个整理,本文主要参考的是Java研究组织出版的j2se进阶和张孝祥-java就业培训教材这两本书 2.概述 2.1线程是什么 主要是线程与进程的区别,这里不再阐述,自行网上搜索 为什么使用线程:操作系统切换多个线程要比调度进程在速度上快很

Objective-C学习之旅 第三篇

Objective-C学习之旅 第三篇 Objective-C 类声明,定义,实例,初始化 类声明 //类声明的编译处理指令以@interface开始,以@end结尾,在这之间代码便是类变量的定义和方法的声明. //类的声明和定义比其他语言复杂,这会让刚开始学习Objective-C的人非常困惑 //准确的说,其他语言如Java只需定义类而不用声明,而Objective-C需要先声明再定义. @interface 类名 : 父类名 { 声明成员变量 ... } 声明方法 @end 实例: @in