本文大量借鉴 李刚老师 编著的《疯狂Java讲义 第五版》,并加入自己的理解,将其概念进行精简。如想更深入了解,建议购买原著。
Java的数组要求所有数组元素具有相同的类型。在一个数组中,数组元素类型是唯一的,只能储存同一种数据类型,不能出现多种数据类型。
定义数组
定义数组的方式有两种:
- type[] arrayName
- type arrayName[]
推荐使用第一种,具有更好的语意;
数组是一种引用类型,使用它定义一个变量时,仅仅表示定义了一个引用变量(定义了一个指针),这个引用变量还未指向任何有效的内存,因此定义数组时不能指定数组的长度。同时这个数组也还不能使用,只有对数组初始化之后才能使用。
一旦数组的初始化完成,数组在内存中所占的空间将被固定,因此数组的长度是不可变的。即使把某个数组的元素清空,但它所占的空间依然被保留,数组长度依然不变。
注意:定义数组的时候不能指定数组的长度,只有数组完成初始化后才具有长度,才可以被使用,同时此长度是不可变的。
数组的初始化
所谓初始化,就是为数组的数组元素分配空间,并为每个数组元素赋初始值。
注意:分配空间则一定赋初始值。不能只分配空间不赋初始值。
只不过初始值的获得有的两种方式:
1.由系统自动分配初始值(当程序员只规定数组的空间而未给元素赋值时),
2.由程序员指定初始值。
数组的初始化有如下两种方式:
- 静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度。
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"};
- 动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。
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讲义 第五版》,并加入自己的理解,将其概念进行精简。如想更深入了解,建议购买原著。