1、数组
数组是编程语言中最常见的一种数据结构,可以用于存储多个数据,每个数组元素存放一个数据,通常可通过数组元素的索引来访问元素,包括为数组元素赋值和取出数组元素的值。
1.1 数组也是一种类型
Java 的数组要求所有的数组元素具有相同的数据类型。因此,在一个数组中,数组元素的类型是唯一的,即一个数组里只能存储一种类型的数据,而不能存储多种数据类型的数据。
因为 Java 语言是面向对象的语言,而类与类之间可以支持继承关系,这样可能产生一个数组里可以存放多种数据类型的假象。例如有一个水果数组,要求每个数组元素都是水果,实际上数组元素既可以是水果,也可以是香蕉,但这个数组的数组元素的类型还是唯一的,只能是水果类型。
一旦数组的初始化完成,其所占空间就是固定的不可变。
Java 的数组既可以存储基本类型的数据,也可以存储引用类型的数据,只要所有的数组元素具有相同的类型即可。
值得注意的是,数组也是一种数据类型,它本身是一种引用类型。
1.2 定义数组
定义数组格式:
type[] array_name; type array_name[];
对这两种语法格式而言,通常推荐使用第一种格式。
第一种格式更符合数组的语意,而且具有更好的可读性。type[] array_name 方式很容易理解这是定义一个变量,变量名是 array_name ,而变量类型是 type[]。
第二种格式 type array_name[] 的可读性就差了,看起来像是定义了一个类型为 type 的变量,而变量名字是 array_name[] ,和它真实的含义相差甚远。
数组是一种引用类型的变量,因此使用它定义的变量是,仅仅表示了一个引用变量(可以理解为指针),这个引用变量还未指出任何有效内存,因此定义数组时不能指定数组的长度。
注意数组的定义只是定义了一个变量,还没有对数组进行初始化,所以不能使用。
1.3 数组的初始化
Java 语言中数组必须先初始化,然后才可以使用。
所谓初始化,就是为数组的数组元素分配内存空间,并为每个数组元素赋初始值。
数组的初始化有两种:
- 静态初始化:初始化时有程序员显示指定每个数组元素的初始值,有系统决定数组长度。
- 动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。
1、静态初始化
type arrayName; arrayName = type[]{值1,值2,值3,...};
type 为数组元素的类型,且指定的数组元素值得类型必须与 new 关键字后的 type 类型相同,或者是其子类型的实例。
下面案例代码定义使用了三种形式来进行初始化。
// 定义一个int数组类型变量,变量名 intArray int[] intArray; // 使用静态初始化,初始化数组时只指定数组元素的初始值,不指定数组长度 intArray = new int[]{1,2,3,4,5,6}; // 定义一个 Object 数组类型的变量,变量名 objectArray Object[] objectArray; // 使用功能静态初始化 objectArray = new Object[]{"J3", "白起", "西行"}; // 定义一个 Object 数组类型的变量,变量名 stringArray Object[] stringArray; // 使用功能静态初始化,初始化数组时数组元素的类型是定义数组元素类型的子类型 stringArray = new String[]{"J3", "白起", "西行"};
因为 Java 语言是面向对象编程语言,能很好的支持子类和父类的继承关系:子类实例是一种特殊的父类实例。
上面程序中,String 类型是 Object 类型的子类型,既字符串是一种特殊的 Object 实例。
除此之外,静态初始化还有如下简化的语法格式:
arrayName = {值1,值2,值3,...};
在实际开发中,我们通常将数组的定义和初始化同时完成,如下示例:
int[] intArray = {1, 2, 3, 4, 5, 6, 7};
2、动态初始化
动态初始化只指定数组的长度,由系统为每个数组元素指定初始值,格式如下:
type arrayName; arrayName = type[length];
在上面语法中,需要指定一个 int 类型的 length 参数,这个参数指定了数组的长度,也就是可以容纳数组元素的个数。
与静态初始化类似的是,此处对额 type 必须与定义数组时使用的 type 类型相同,或者是定义数组时使用的 type 类型的子类。下面代码示范了如何动态初始化:
int[] intArray =new int[5]; Object[] objects = new String[5];
执行动态初始化时,程序员只需要指定数组的长度,既为每个数组元素指定所需的内存空间,系统将负责为这些数组元素分配初始值。指定初始值时,系统按如下规则分配初始值。
- 数组元素的类型是基本类型中的整数类型(byte、short、int 和 long),则数组元素的值是 0。
- 数组元素的类型是基本类型中的浮点类型(float、double),则数组元素的值是 0.0。
- 数组元素的类型是基本类型中的字符类型(char),则数组元素的值是‘\u000’。
- 数组元素的类型是基本类型中的布尔类型(boolean),则数组元素的值是 false。
- 数组元素的类型是引用类型(类、接口和数组),则数组元素的值是 null。
注意,数组的初始化不能同时使用静态初始化和动态初始化,也就是说不要在进行数组初始化时,既指定数组的长度,也为每个数组元素分配初始值。
数组初始化完成后,其长度固定不可改变。
1.4 使用数组
数组最常用的用法就是访问数组元素,包括对数组元素进行赋值和取出数组元素的值。
下面代码演示了访问数组元素及对数组元素进行赋值。
int[] intArray =new int[5]; // 访问数组下标对应的元素 System.out.println(intArray[0]); // 赋值 intArray[4] = 100;
注意:数组的下标是从 0 开始的。
如果访问数组元素时指定的索引值小于 0 ,或者大于等于数组的长度,编译器程序不会出现任何错误,但运行时会出现异常:java.lang.ArrayIndexOutOfBoundsException:N (数组索引越界异常),异常信息后 N 就是程序员师徒访问的数组索引。
所有的数组都提供了一个 length 属性,通过这个属性可以访问到数组的长度,一旦获得了数组的长度,就可通过循环来遍历该数组的每个数组元素,案例代码如下。
int[] intArray =new int[5]; // 根据下标,for 循环遍历数组 for (int i = 0; i < intArray.length; i++) { System.out.println(intArray[i]); }
以上不难看出,初始化一个数组后,相当于同时初始化了多个相同类型的变量,通过数组元素的索引就可以自由访问这些变量。
使用数组元素与使用普通变量并没有什么不同,一样可以对数组元素进行赋值,或者取出数组元素的值。
1.5 foreach 循环
从 Java5 之后,Java 提供了一种更简单的循环:foreach 循环,这种循环遍历数组和集合更加简单。
使用 foreach 循环遍历数组和集合元素是,无须获得数组和集合长度,无须根据索引来访问数组元素和集合元素,foreach 循环自动玄幻遍历数组和集合的每个元素。
foreach 循环的语法格式如下:
for(type variableName : array | collection){ // variableName 自动迭代访问每个元素 }
语法格式中,type 是数组元素或集合元素的类型,variableName 是一个形参名,foreach 循环将自动将数组元素、集合元素依次赋值给该变量。
使用案例如下:
public class ForeachTest { public static void main(String[] args) { String[] names = new String[]{"J3", "西行", "白起"}; for (String name : names) { System.out.println(name); } } }
在使用 foreach 循环来迭代输出数组元素或集合时,通常不要对循环遍历进行赋值,虽然这种赋值在语法上是允许的,但却不会改变数组原本元素的值。案例代码如下:
public class ForeachTest { public static void main(String[] args) { String[] names = new String[]{"J3", "西行", "白起"}; for (String name : names) { // 给中间变量赋值 name="哈哈"; System.out.println(name); } System.out.println(names[0]); } }
执行你会发现,遍历的结果都是 哈哈 而且数组元素的值并没有改变,如果希望遍历的时候改变数组元素的值,那不建议使用 foreach。
2、深入数组
深入数组前我们要牢记前面讲的,数组是一种引用类型。
定义的数值变量名称只是一个引用,数组元素和数组变量在内存中是分开存放的。
2.1 内存中的数组
数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效的内存后,才可以通过该数组变量来访问数组元素。
程序中访问真实对象的唯一途径就是通过引用变量进行访问。
实际中,数组对象被存储在堆(heap)内存中,如果引用该数组对象的数组引用变量是一个局部变量,那么它被存储在栈(stack)内存中。数组在内存中的存储示意图如下。
如果堆内存中的数组不在有任何引用变量指向自己,则这个数组将成为垃圾,改数组所占用的内存将会被系统的垃圾回收机制回收。因此,为了让垃圾回收机制回收一个数组所占的内存空间,可以将该数组变量赋为 null ,也就切断了数组引用变量和实际数组之间的引用关系,实际数组也就成为了垃圾。
2.2 基本类型数组的初始化
对于基本类型数组而言,数组元素的值直接存储在对应的数组元素中,因此,初始化数组时,宣威该数组分配内存空间,然后直接将数组元素的值存入对应数组元素中。
下面的代码定义了一个 int [] 类型的数组变量,采用动态初始化的方式初始化该数组,并显示为每个数组元素赋值。
public class ArrayTest { public static void main(String[] args) { int[] intArray =new int[5]; // 循环赋值 for (int i = 0; i < intArray.length; i++) { intArray[i] = i + 1; } } }
下面来结合一下示意图详细介绍这段代码执行过程。
第一段代码 int[] intArray =new int[5]
包括两个部分,定义数组变量和动态初始化数组,内存存储示意图如下。
此时每个数组元素的值都是 0 ,当循环为该数组每个数组元素一次赋值后,此时每个数组元素的值编程程序显示指定的值。显示指定每个数组元素值后的存储示意图如下。
从基本类型数组的存储示意图中可知,每个数组元素的值直接存储在对应的内存中,操作基本类型数组的元素时,实际上就是操作基本类型的变量。
2.3 引用类型数组的初始化
引用类型数组的数组元素是引用,因此情况有点复杂。每个数组元素存储的还是引用,它指向另一块内存,这块内存里存储了有效数据。
为了更好说明引用来下数组的运行过程,先看下面定义的 Person 类(所有类都是引用类型)。
public class Person { private String name; private Integer age; public void info() { System.out.println("姓名是:" + name + " ,年龄是:" + age); } }
接着将定义一个 Person[] 数组,动态初始化这个 Person[] 数组,并为这个数组的每个元素指定值,代码如下。
public class ArrayTest { public static void main(String[] args) { // 定义 person 类型数组,并初始化长度为 5 Person[] students = new Person[5]; // 创建 person 对象并赋值相关值 Person person1 = new Person(); person1.name = "J3"; person1.age = 18; // 调用 person 对象方法 person1.info(); // 将 person 对象赋值给数组第一个元素 students[0] = person1; students[0].info(); // 和上面雷同 Person person2 = new Person(); person2.name = "西行"; person2.age = 3256; person2.info(); students[1] = person2; students[1].info(); } }
这段代码的执行过程就是经典的引用类型初始化,下面将结合示意图详细分析。
执行 Person[] students = new Person[5]; 代码时,会在栈内存和堆内存分别开辟对应的内存空间,栈中开辟Person[] 类型的引用,名称为 students ;堆中开辟大小为五的空内存块。存储示意图如下。
接着执行代码 Person person1 = new Person();
将在栈中和堆中分别开辟对应空间,如图。
接着往下执行赋值操作,对 person1 对象的成员变量赋值,并将 person1 对象赋值给数组元素,在内存中的示意图如下。
可以看到,最终 students 中的元素中存的地址和栈中引用变量存的地址是相同的,所以通过引用变量修改 person 的值 students 数组元素指向的对象也会相应修改互相影响。
2.4 没有多维数组
Java 语言支持多维数组语法,但其本质还是一维数组。
下面我们定义一个二维数组并写一段代码,看看如何将其理解成一维数组。
public class ArrayTest { public static void main(String[] args) { // 这是一个二维数组 int[][] ints = new int[5][]; // 给二维数组的一维数组的第一个元素初始化值,值:一个长度位 10 的数组 ints[0] = new int[10]; // 给二维数组的一维数组的第二个元素初始化值,值:一个长度位 15 的数组 ints[1] = new int[15]; // 赋值 ints[0][0] = 1; ints[1][0] = 2; ints[1][1] = 3; } }
程序第一行代码 int[][] ints = new int[5][]
只是定义了一个数组(一维数组)初始值都是 null 的引用类型,示例图如下。
因为 ints 类型必须是数组类型,所以给 ints[0]、ints[1]等元素位置分别初始化一个数组类型数据,如图。
最后分别给 ints[0]、ints[1] 元素上引用类型(从程序看是一个数组)对应的位置赋值,所以最终示意图如下。
不难看出,二维数组其实就是在一维数组上将其元素中的值赋了一个一维数组,这样就形成了二维数组,那我们完全可以往后推理,三维数组就是在一维数组的元素上赋值个二维数组从而形成一个三维数组、四维数组则是在一维数组的元素上赋值个三维数组从而形成四维数组等以此类推,所以从这个角度来看,Java 语言里没有多维数组。
2.5 Arrays工具类
Arrays 是一个操作数组的工具类,在原生数组上进行操作时,我们往往会觉得很多不便,所以 Java 就提供了一个操作数据的工具类 Arrays ,里面包含了一些 static 修饰的方法可以直接操作数组,方便我们操作数组中元素。
下面介绍 Arrays 中几个常用方法:
- String toString(type[] a):该方法将一个数据转为成一个字符串。该方法按顺序吧多个数组元素连接在一起,多个数组元素使用英文逗号和空格隔开。
- void sort(type[] a): 该方法对 a 数组的数组元素进行排序。
- void fill(type[] a,type val):该方法会将数组的所有元素都赋值为 val。
- type[] copyOf(type[] original,int newLength):这个方法将会把 original 数组赋值成一个新数组,其中 length 是新数组的长度。如果 length 小于 original 数组的长度,则新数组的前面元素就是原数组的所有元素,后面补充 0 (数值类型)、false (布尔类型)或者 null (引用类型)。
下面代码示范了 Arrays 类中方法的用法。
public class ArrayTest { public static void main(String[] args) { // 定义一个字符串数组,并初始化 String[] names = new String[]{"J3", "西行", "白起", "悟空"}; // 将 names 数组变成字符串打印 System.out.println(Arrays.toString(names)); // 定义数组 int[] ints = new int[]{1, 5, 2, 4, 6, 3, 15, 22, 9}; // 排序,默认自然排序 Arrays.sort(ints); // 输出 System.out.println(Arrays.toString(ints)); // 定义数组 int[] intValues = new int[10]; // 将数组元素值统一设置成 8 Arrays.fill(intValues, 8); // 输出 System.out.println(Arrays.toString(intValues)); // 复制 names 数组的 前 2 个元素值, 然后生成新数组 String[] copyOf = Arrays.copyOf(names, 2); // 输出复制后的数组 System.out.println(Arrays.toString(copyOf)); } }
好了,今天的内容到这里就结束了,关注我,我们下期见