Java的数组类型的进阶要点

本文大量借鉴 李刚老师 编著的《疯狂Java讲义 第五版》,并加入自己的理解,将其概念进行精简。如想更深入了解,建议购买原著。


Java的数组要求所有数组元素具有相同的类型。在一个数组中,数组元素类型是唯一的,只能储存同一种数据类型,不能出现多种数据类型。


定义数组

定义数组的方式有两种:

  1. type[] arrayName
  2. type arrayName[]

推荐使用第一种,具有更好的语意;

数组是一种引用类型,使用它定义一个变量时,仅仅表示定义了一个引用变量(定义了一个指针),这个引用变量还未指向任何有效的内存,因此定义数组时不能指定数组的长度。同时这个数组也还不能使用,只有对数组初始化之后才能使用。

一旦数组的初始化完成,数组在内存中所占的空间将被固定,因此数组的长度是不可变的。即使把某个数组的元素清空,但它所占的空间依然被保留,数组长度依然不变。

注意:定义数组的时候不能指定数组的长度,只有数组完成初始化后才具有长度,才可以被使用,同时此长度是不可变的。


数组的初始化

所谓初始化,就是为数组的数组元素分配空间,并为每个数组元素赋初始值。

注意:分配空间则一定赋初始值。不能只分配空间不赋初始值。
只不过初始值的获得有的两种方式:
1.由系统自动分配初始值(当程序员只规定数组的空间而未给元素赋值时),
2.由程序员指定初始值。

数组的初始化有如下两种方式:

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

arrayName = new type[] {elem1,elem2,elem3,elem4,…}
简化写法:arrayName = {elem1,elem2,elem3,elem4,…}

//定义一个数组类型
int[] intArr;
String[] stringArr;
//使用静态初始化
intArr = new int[] {5,6,2,1,3};
stringArr = {"Alex","Leon","Lisa","Jeff","Steven"};
  1. 动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。

arrayName = new type[length];

//使用动态初始化时,数组的定义和初始化可同时完成
int[] intArr = new int[5];
//初始化数组时元素的类型是定义数组时元素类型的子类
Object[] books = new String[5];

动态初始化时,系统按如下规则分配初始值:

  • 元素为 基本类型中的整型(byte, short, int, long), 初始值为0;
  • 元素为 基本类型中的浮点类型(float,double), 初始值为0.0;
  • 元素为 基本类型中的字符类型(char), 初始值为’\u0000’;
  • 元素为 基本类型中的布尔类型(boolean),初始值为 false;
  • 元素为 引用类型 (String,类,接口和数组),初始值为 null;

使用数组

数组最常用的用法 就是访问数组元素,包括对数组元素进行赋值和取值
Java语言的数组索引是从0开始的,第一个数组元素的下标(索引值)为0,最后一个数组元素的下标(索引值)为数组长度减1.

system.out.println(stringArr[2]);
stringArr[0]="Alex";

如果试图访问的数组元素的下标小于0,或者大于等于数组长度,编译不会报错,但运行时会报数组下标越界异常:java.lang.ArrayIndexOutOfBoundsException: N(运行时异常)


数组的遍历和foreach

每一个数组都提供了length属性

注意
String类型提供的是length()方法
list集合类型提供的是size()方法

通过这个length属性可以访问到数组的长度,一旦获得了数组长度就可以对其进行遍历

for(int i = 0; i<stringArr.length; i++){
	//可以取值或者对数组元素进行赋值
	system.out.println(stringArr[i]);
}

java 5之后提供了更简单的遍历方式,可以使用foreach循环对数组元素或者集合元素进行遍历。

for(type variableName : array/collection){}

for(String book : books){
	system.out.println(book);
}

注意:

  • foreach无需循环条件,无需迭代语句,无需获取数组长度,无需通过下标索引访问数组元素
  • foreach中的循环变量(如例子中的String book),相当于一个临时变量,对其进行赋值不可改变数组元素的值,因此不要使用foreach方式对数组元素进行赋值。

深入数组

内存中的数组

数组是一种引用数据类型。数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用变量指向有效内存后,才可通过数组变量来访问数组元素。

与所有引用变量相同的是,引用变量是访问真实对象的根本方式。如果想在程序中访问数组对象本身,则只能通过这个数组的引用变量来访问它。

数组对象被存储在堆内存(heap)中,如果引用该数组对象的引用变量是一个局部变量,那么它被存储在栈内存(stack)中
数组在内存中存储示意图

堆内存和栈内存

  • 每个方法执行时都会建立自己的内存栈,定义的局部变量会逐个放入这个内存栈,方法执行结束,这个方法的内存栈也会随之销毁。
  • 创建对象时,对象会被保存到运行时数据区,以便反复利用(对象的创建成本比较高),这个运行时数据区就是堆内存。
  • 堆内存中的对象不会随方法结束而销毁,它还有可能被其他引用变量所引用,当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在合适的时机回收它。

如下代码所示,可以让一个数组变量指向另一个数组对象,这种操作会让人产生数组长度可变的错觉,而实际上它只是引用了另一个数组对象

public class ArrayInRam
{
	public static void main(String[] args)
	{
		// 定义并初始化数组,使用静态初始化
		int[] a = {5, 7, 20};
		// 定义并初始化数组,使用动态初始化
		var b = new int[4];
		// 输出b数组的长度
		System.out.println("b数组的长度为:" + b.length);
		// 循环输出a数组的元素
		for (int i = 0, len = a.length; i < len; i++)
		{
			System.out.println(a[i]);
		}
		// 循环输出b数组的元素
		for (int i = 0, len = b.length; i < len; i++)
		{
			System.out.println(b[i]);
		}
		// 因为a是int[]类型,b也是int[]类型,所以可以将a的值赋给b。
		// 也就是让b引用指向a引用指向的数组
		b = a;
		// 再次输出b数组的长度
		System.out.println("b数组的长度为:" + b.length);
	}
}
/*
b数组的长度为:4
5
7
20
0
0
0
0
b数组的长度为:3
*/

在这里插入图片描述


引用类型数组的初始化

基本数据类型的数组在进行初始化时,静态初始化直接赋值,动态初始化为其分配空间然后给每个元素赋初始值。这时候操作数组元素实际上是直接操作基本数据类型

而引用类型的数组就复杂多了,因为它的每一个元素也是引用数据类型,每一个数组元素都指向另一块内存,这块内存里存放了有效数据。

如下代码:

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];
		// 创建一个Person实例,并将这个Person实例赋给zhang变量
		var zhang = new Person();
		// 为zhang所引用的Person对象的age、height赋值
		zhang.age = 15;
		zhang.height = 158;
		// 创建一个Person实例,并将这个Person实例赋给lee变量
		var lee = new Person();
		// 为lee所引用的Person对象的age、height赋值
		lee.age = 16;
		lee.height = 161;
		// 将zhang变量的值赋给第一个数组元素
		students[0] = zhang;
		// 将lee变量的值赋给第二个数组元素
		students[1] = lee;
		// 下面两行代码的结果完全一样,因为lee
		// 和students[1]指向的是同一个Person实例。
		lee.info();
		students[1].info();
		System.err.println(students[0]==zhang);
		System.err.println(students[1]==lee);
	}
}
/*
我的年龄是:16,我的身高是:161.0
我的年龄是:16,我的身高是:161.0
true
true
*/

定义了一个Person类,并申明一个引用变量Person[] student = new Person[2]

此时Person数组动态初始化分配空间并为每个元素赋默认值null,实际上两个数组元素student[0],和student[1]是引用变量,指向了null

而lee和zhang两个引用变量,指向了两个不同属性的Person对象

当给studen[0] (数组元素)赋值zhang时,实际上它们两个引用变量指向的是同一块内存(地址相同),同理,student[1]和lee指向的也是同一块内存

由于相同对象的内存地址相同,因此对他们做==判断会得到true

在这里插入图片描述

(没有?)多维数组

虽然Java语言里提供了支持多维数组的语法,但从数组底层运行机制上来看,其实是引用类型数组的元素再次引用了数组对象

二维数组的定义和初始化

  • type[][] arrayName
  • arrayName = new type[length][] //此语法动态初始化了第一维数组
  • arrayName = new type[length1][length2]//同时初始化二维数组
package com.codes.chapter04.section06;


/**
 * Description:
 * 网站: <a href="http://www.crazyit.org">疯狂Java联盟</a><br>
 * Copyright (C), 2001-2020, Yeeku.H.Lee<br>
 * This program is protected by copyright laws.<br>
 * Program Name:<br>
 * Date:<br>
 * @author Yeeku.H.Lee kongyeeku@163.com
 * @version 5.0
 */
public class TwoDimensionTest
{
	public static void main(String[] args)
	{
		// 定义一个二维数组
		int[][] a;
		// 把a当成一维数组进行初始化,初始化a是一个长度为4的数组
		// a数组的数组元素又是引用类型
		a = new int[4][];
		// 把a数组当成一维数组,遍历a数组的每个数组元素
		for (int i = 0, len = a.length; i < len; i++)
		{
			System.out.println(a[i]);
		}
		// 初始化a数组的第一个元素
		a[0] = new int[2];
		// 访问a数组的第一个元素所指数组的第二个元素
		a[0][1] = 6;
		// a数组的第一个元素是一个一维数组,遍历这个一维数组
		for (int i = 0, len = a[0].length; i < len; i++)
		{
			System.out.println(a[0][i]);
		}

		// 同时初始化二维数组的2个维数
		int[][] b = new int[3][4];
		for (int i = 0; i < b.length; i++) {
			System.out.println(b[i]);
		}
		for (int i = 0; i < b.length; i++) {
			for (int j = 0; j < b[i].length; j++) {
				System.err.println(b[i][j]);
			}
		}
		// 使用静态初始化的语法来初始化一个二维数组
		String[][] str1 = new String[][] {new String[3],
			new String[] {"hello"}};
		// 使用简化的静态初始化语法来初始化二维数组
		String[][] str2 = {new String[3],
			new String[] {"hello"}};
		System.out.println(str1[1][0]);
		System.out.println(str2[1][0]);
	}
}
/*
null
null
null
null
0
6
[I@65b3120a
[I@6f539caf
[I@79fc0f2f
0
0
0
0
0
0
0
0
0
0
0
0
hello
hello
*/

当只动态初始化一维数组时,每个引用类型的数组元素都被赋值null

对数组元素再进行初始化时它们才指向了二维int类型的数组

当同时为两维动态初始化时,一维数组的元素引用指向了对象,因此打印出来的是地址值

再次遍历一维历数组元素(二维数组)的元素时,发现都初始化自动赋值0(因为是基本类型int)
在这里插入图片描述
在这里插入图片描述

上述代码中的二位数组无法再扩展为三维数组
原因:
Java是强类型语言,上述代码中定义a数组的时候就已经确定了其数组元素为int[]类型, 因此int[]的数组元素无法拓展成引用类型(数组)
如果定义一个Object[]类型的数组,则可以无限拓展为多维数组

结论:二维数组是一维数组,其数组元素是一维数组;三维数组是一维数组,其数组元素为二维数组…从这个角度看,java语言里没有多维数组。


操作数组的工具类:Arrays

Java提供的Arrays类里包含的一些static修饰的方法可以直接操作数组

  • int binarySearch(type[] a, type key):
    使用二分法查询key元素在a数组中出现的索引;
    如果不包含key元素值,则返回负数;
    要求数组元素已经按升序排列;
  • int binarySearch(type[] a, int fromIndex, int toIndex, type key):
    与上一个方法类似,但只搜索a数组中fromIndex到toIndex索引的元素
    要求数组元素已经按升序排列;
  • type[] copyOf(type[] original, int length):
    这个方法将会把original数组复制成一个新数组
    length是新数组的长度
    如果length小于original数组的长度,则新数组就是原数组前面length个元素
    如果length大于original数组的长度,则新数组前面的元素就是原数组的所有元素,后面补充0,false或者null
  • type[] copyOfRange(type[] orginial, int from, int to):
    与前面方法相似,但这个方法只复制original数组的from索引到to索引的元素
  • boolean eqauls(type[] a, type[] a2):
    如果a数组和a2数组的长度相等,而且a数组和a2数组的数组元素也一一相同,该方法将返回true
  • void fill(type[] a, type val):
    该方法将会把a数组的所有元素复制为val
  • void fill(type[] a, int fromIndex, int toIndex, type val):
    该方法与前一个方法类似,仅仅将a数组的fromIndex到toIndex索引的数组元素赋值为val
  • void sort(type[] a):
    该方法对a数组的数组元素进行排序
  • void sort(type[] a, inr fromIndex, int toIndex):
    与前面方法类似,该方法仅仅对fromIndex到toIndex索引的元素进行排序
  • String toString(type[] a):
    该方法将一个数组转换成一个字符串。
    该方法按顺序把多个数组元素连缀在一起,多个数组元素使用英文逗号(,)和空格隔开
public class ArraysTest
{
	public static void main(String[] args)
	{
		// 定义一个a数组
		var a = new int[] {3, 4, 5, 6};
		// 定义一个a2数组
		var a2 = new int[] {3, 4, 5, 6};
		// a数组和a2数组的长度相等,每个元素依次相等,将输出true
		System.out.println("a数组和a2数组是否相等:"
			+ Arrays.equals(a, a2));
		// 通过复制a数组,生成一个新的b数组
		var b = Arrays.copyOf(a, 6);
		System.out.println("a数组和b数组是否相等:"
			+ Arrays.equals(a, b));
		// 输出b数组的元素,将输出[3, 4, 5, 6, 0, 0]
		System.out.println("b数组的元素为:"
			+ Arrays.toString(b));
		// 将b数组的第3个元素(包括)到第5个元素(不包括)赋为1
		Arrays.fill(b, 2, 4, 1);
		// 输出b数组的元素,将输出[3, 4, 1, 1, 0, 0]
		System.out.println("b数组的元素为:"
			+ Arrays.toString(b));
		// 对b数组进行排序
		Arrays.sort(b);
		// 输出b数组的元素,将输出[0, 0, 1, 1, 3, 4]
		System.out.println("b数组的元素为:"
			+ Arrays.toString(b));
	}
}
  • System类里包含的static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)方法
    可以将src数组里的元素值赋给dest数组的元素
    其中srcPos指定从src数组的第几个元素开始赋值
    length参数指定将src数组的多少个元素赋值给dest数组的元素

Java8增强了Arrays类的功能,下面是Java8增加的方法

  • void parallelPrefix(xxx[] array, XxxBinaryOperator op):
    该方法使用op参数指定的计算公式计算得到的结果作为新的数组元素
    op计算公式包括left、right两个形参
    left代表新数组中前一个索引处的元素
    right代表array数组中当前索引处的元素
    新数组的第一个元素无需计算,直接等于array数组的第一个元素
  • void parallelPrefix(xxx[] array, int fromIndex, int toIndex, XxxBinaryOperator op):
    该方法仅重新计算从fromIndex到toIndex索引的元素
  • void setAll(xxx[] array, IntToXxxFunction generator):
    该方法使用指定的生成器(generator)为所有数组元素设置值,该生成器控制数组元素的值的生成算法
  • void parallelSetAll(xxx[] array, IntToXxxFunction generator):
    与上一个方法相同,只是增加了并行能力,可以利用多CPU并行提高性能
  • void parallelSort(xxx[] a):
    该方法的功能与sort()方法类似,只是增加了并行能力,可以利用多CPU并行提高性能
  • void parallelSort(xxx[] a, int fromIndex, int toIndex):
    与上一个方法类似,仅对fromIndex到toIndex索引的元素进行排序
  • Spliterator.OfXxx spliterator(xxx[] array):
    将该数组的所有元素转换成对应的Spliterator对象
  • Spliterator.OfXxx spliterator(xxx[] array, int startInclusive, int endEclusive):
    与上一个方法类似,区别是仅转换startInclusive到endEclusive索引的元素
  • XxxStream stream(xxx[] array):
    该方法将数组转换为Stream, Stream是Java8新增的流式编程API
  • XxxStream stream(xxx[] array, int startInclusive, int endEclusive):
    与上一个方法类似,区别是仅转换startInclusive到endEclusive索引的元素

示范

public class ArraysTest2
{
	public static void main(String[] args)
	{
		var arr1 = new int[] {3, -4, 25, 16, 30, 18};
		// 对数组arr1进行并发排序
		Arrays.parallelSort(arr1);
		System.out.println(Arrays.toString(arr1));
		var arr2 = new int[] {3, -4, 25, 16, 30, 18};
		Arrays.parallelPrefix(arr2, new IntBinaryOperator()
		{
			// left代表新数组中前一个索引处的元素,right代表原数组中当前索引处的元素
			// 新数组的第一个元素总等于原数组第一个元素
			public int applyAsInt(int left, int right)
			{
				return left * right;
			}
		});
		System.out.println(Arrays.toString(arr2));
		var arr3 = new int[5];
		Arrays.parallelSetAll(arr3, new IntUnaryOperator()
		{
			// operand代表正在计算的元素索引
			public int applyAsInt(int operand)
			{
				return operand * 5;
			}
		});
		System.out.println(Arrays.toString(arr3));
	}
}
/*
//排序得
[-4, 3, 16, 18, 25, 30]

//代码中使用的公式是left*right, left代表新数组中前一个索引处的元素,right代表当前索引
//因此计算方式[1*3=3,3*-4=-12,-12*25=-300,-300*16=-4800,-4800*30=-144000,-144000*18=-2592000]
[3, -12, -300, -4800, -144000, -2592000]

//代码中使用operand*5公式来设置数组元素,operand代表正在计算得数组元素得索引
[0, 5, 10, 15, 20]
*/

本文大量借鉴 李刚老师 编著的《疯狂Java讲义 第五版》,并加入自己的理解,将其概念进行精简。如想更深入了解,建议购买原著。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值