Java中的数组跟c语言的数组几乎不一样,我们要区分对待。在之后你就能理解到我为什么说这句话了。
1.java中数组的创建与初始化
数组的创建
如下,皆为数组的创建。
double[] a;
int[] b;
创建时的[]里面绝对不能填数字。
数组的初始化
主要分为动态初始化以及静态初始化。
1. 动态初始化:在创建数组时,直接指定数组中元素的个数
int[] array = new int[10];
动态初始化只是分配了一个数组空间,里面的值并没初始化赋值,像平时如果创建一个变量没将其初始化就使用,Java是会直接报错的。
而在这数组里其值也没初始化,但系统却会自动帮助其数组给它们一个基础值。
如果数组中存储元素类型为基类类型,默认值为基类类型对应的默认值,比如:
如果数组中存储元素类型为引用类型(类型于c语言的指针),默认值为null 。
在动态初始化时,java语法允许可以new int[n] :n为变量,这样就更加方便。
2. 静态初始化:在创建数组时不直接指定数据元素个数,而直接将具体的数据内容进行指定。
int[] array1 = new int[]{0,1,2,3,4,5,6,7,8,9};
double[] array2 = new double[]{1.0, 2.0, 3.0, 4.0, 5.0};
String[] array3 = new String[]{"hell", "Java", "!!!"};
【注意事项】
静态初始化虽然没有指定数组的长度,编译器在编译时会根据{}中元素个数来确定数组的长度。
静态初始化时, {}中数据类型必须与[]前数据类型一致。
静态初始化可以简写,省去后面的new T[]。 本质还是一样的。
// 注意:虽然省去了new T[], 但是编译器编译代码时还是会还原
int[] array1 = {0,1,2,3,4,5,6,7,8,9};
double[] array2 = {1.0, 2.0, 3.0, 4.0, 5.0};
String[] array3 = {"hell", "Java", "!!!"};
但是简写在一些时候也是用不了的, 如我们数组的创建和初始化可以分成两步,在分为两步时,静态初始化的简写就用不了了。
int[] array1;
array1 = new int[10];
int[] array2;
array2 = new int[]{10, 20, 30};
// 注意省略格式不可以拆分, 否则编译失败
// int[] array3;
// array3 = {1, 2, 3};
数组也可以按照依照C语言创建数组的方法去创建,但不推荐,不要这么写
/*
该种定义方式不太友好,容易造成数组的类型就是int的误解
[]如果在类型之后,就表示数组类型,因此int[]结合在一块写意思更清晰
*/
int arr[] = {1, 2, 3};
作者自己的思考理解
对于a和b这些数组名是引用变量,它们都是存地址的变量(数组是引用类型)。所以为了方便理解记忆,我们可以把[]理解成C语言的*,从而类型就是 double *或int *。
从而还可以这么理解,在初始化时,如new int[]{1,2}或着 new int[10]就在系统已经分配了一个数组空间,其还返回了这数组的最起始地址,从而让数组名(接收地址的变量)去接收,从而就创建了一个完整的数组。
不知道我这个思路理解是不是正确的,但这样确实更方便理解记忆。
2.遍历数组
第一种方法
这是第一种方法,很简单。值得注意的是 数组对象名.length就可以得到数组所含的元素个数
第二种方法
我们可以使用 for-each遍历数组,for-each就是一个加强版的for循环,其专门用在数组上(目前来看)。
其语法格式是这样。
for(数据类型 变量;数组名)
{}
其最开始是讲数组第一个元素赋值给变量。从而之后就是第二个元素赋值给变量。直到最后一个元素赋值给变量。然后就结束循环。
从而可以用该for-each循环遍历数组。代码如下:
int[] array = {1, 2, 3};
for (int x : array) {
System.out.println(x);
}
for-each 是 for 循环的另外一种使用方式(专门用于数组). 能够更方便的完成对数组的遍历. 可以避免循环条件和更新语句写错.
3.数组是引用类型
初始JVM的内存分布
程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址
虚拟机栈(JVM Stack): 与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量等. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的(native方法是使用其他语言如c/c++编写的方法,它可以在java程序中被调用),我们现在使用的方法创建的栈帧都是在虚拟机栈中。
堆(Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2, 3} ),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁。
在c语言中堆中申请的内存在使用完后要用free释放。而在java中当我们申请的内存没有引用类型引用时(可以理解为没指针指向其申请的内存区域),它就会自动销毁。
方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. 方法编译出的的字节码就是保存在这个区域
现在我们只简单关心堆 和 虚拟机栈这两块空间,后序JVM中还会更详细介绍。
基本类型变量与引用类型变量的区别
基本数据类型创建的变量,称为基本变量,该变量空间中直接存放的是其所对应的值;
而引用数据类型创建的变量,一般称为对象的引用,其空间中存储的是对象所在空间的地址。
public static void func() {
int a = 10;
int b = 20;
int[] arr = new int[]{1,2,3};
}
再谈引用变量
public static void func() {
int[] array1 = new int[3];
array1[0] = 10;
array1[1] = 20;
array1[2] = 30;
int[] array2 = new int[]{1,2,3,4,5};
array2[0] = 100;
array2[1] = 200;
array1 = array2;
array1[2] = 300;
array1[3] = 400;
array2[4] = 500;
for (int i = 0; i < array2.length; i++) {
System.out.println(array2[i]);
}
}
4.认识null
null 在 Java 中表示 "空引用" , 也就是一个不指向对象的引用.
int[] arr = null;
System.out.println(arr[0]);
// 执行结果
Exception in thread "main" java.lang.NullPointerException
at Test.main(Test.java:6)
null 的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操作. 一旦尝试读写, 就会抛出 NullPointerException.
注意: Java 中并没有约定 null 和 0 号地址的内存有任何关联.
4.数组的应用场景
保存数据
public static void main(String[] args) {
int[] array = {1, 2, 3};
for(int i = 0; i < array.length; ++i){
System.out.println(array[i] + " ");
}
}
作为函数的参数
之前已经讲过这个作为函数的参数,但由于当时没学习数组,所以参数传数组类型并没很清楚的讲述,现在学了,就清楚的讲述一遍
参数传基本数据类型
public static void main(String[] args) {
int num = 0;
func(num);
System.out.println("num = " + num);
}
public static void func(int x) {
x = 10;
System.out.println("x = " + x);
}
// 执行结果
x = 10
num = 0
发现在func方法中修改形参 x 的值, 不影响实参的 num 值.
参数传数组类型(引用数据类型)
public static void main(String[] args) {
int[] arr = {1, 2, 3};
func(arr);
System.out.println("arr[0] = " + arr[0]);
}
public static void func(int[] a) {
a[0] = 10;
System.out.println("a[0] = " + a[0]);
}
// 执行结果
a[0] = 10
arr[0] = 10
发现在func方法内部修改数组的内容, 方法外部的数组内容也发生改变.
因为数组是引用类型,按照引用类型来进行传递,是可以修改其中存放的内容的。(其实将其看作c语言的指针就更好理解了,将int []看作int*不就一下子就理解了)
总结: 所谓的 "引用" 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实 只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).
作为函数的返回值
在c语言中不存在将数组类型当作返回值类型处理,但java可以。(同理,将其看作指针就好理解了)
获取斐波那契数列的前N项就是一个很好的例子
public class TestArray {
public static int[] fib(int n){
if(n <= 0){
return null;
}
int[] array = new int[n];
array[0] = array[1] = 1;
for(int i = 2; i < n; ++i){
array[i] = array[i-1] + array[i-2];
}
return array;
}
public static void main(String[] args) {
int[] array = fib(10);
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
}
5.数组练习
Java 中提供了 java.util.Arrays 包, 其中包含了一些操作数组的常用方法,里面就有sort方法,toString方法,full方法,equals方法。
现在我们先了解下fill方法和equals方法
fill 方法
public class one {
public static void main(String[] args) {
int[] a=new int[10];
Arrays.fill(a,9);
for(int x :a){
System.out.println(x);
}
在这代码中fill是将9全部填充到数组中 ,当然也可以部分填充,如下在中间添加了两个参数,从而就实现了部分填充。
public class one {
public static void main(String[] args) {
int[] a=new int[10];
Arrays.fill(a,0,4,9);
//第二个是起始位置,第三个是最终位置,左闭右开
for(int x :a){
System.out.println(x);
}
equals方法
即使不用Arrays它也存在equals方法,之前讲过,那个是针对字符串去比较的。而Arrays中的equals方法是针对于数组去比较的。其区别如下
public class one {
public static void main(String[] args) {
int[] a=new int[10];
int[] b=new int[10];
System.out.println("das".equals("das"));//不属于Arrays里的
System.out.println(Arrays.equals(a,b));//属于Arrays里的
所以两者存在区别,一个只适用于字符串,一个只适用于数组 。
1.数组转字符串
toString其参数类型为数组类型,返回值为字符串类型。所以能通过它将数组转为字符串类型。
import java.util.Arrays
int[] arr = {1,2,3,4,5,6};
String newArr = Arrays.toString(arr);
System.out.println(newArr);
// 执行结果
[1, 2, 3, 4, 5, 6]
使用这个方法后续打印数组就更方便一些.
2.数组拷贝
copyOf
该函数返回值为拷贝出的数组类型,所以需要用数组去接收。
public class one {
public static void main(String[] args) {
int[]arr=new int[]{0,1,5,4};
int[] arr2= Arrays.copyOf(arr,arr.length);
int[]arr3= Arrays.copyOf(arr,arr.length-1);
int[]arr4= Arrays.copyOf(arr,arr.length*2);
//拷贝的数组在堆区分配内存
//当新数组长度等于原数组时,完全拷贝
//当新数组长度小于原数组时,原数组会将最前面一部分拷贝到新数组中
//当新数组长度大于原数组时,原数组会将其全部都拷贝到新数组中,新数组的其余部分为0(基础值)
for(int x:arr2){
System.out.print(x);
}
System.out.println();
for(int x:arr3){
System.out.print(x);
}
System.out.println();
for(int x:arr4){
System.out.print(x);
}
}
}
copyOfRange
同理返回值为数组类型,要用数组接收。
该函数作用是拷贝数组的某个范围。如下应该简而易懂。
public class one {
public static void main(String[] args) {
int[]arr=new int[]{1,4,6,8,5};
int[]arr2= Arrays.copyOfRange(arr,2,5);
for (int x:arr2
) {
System.out.println(x);
}
}
}
代码及文案
文案中提及拷贝分为深浅拷贝,其拷贝需要考虑深浅拷贝的问题。关于这个问题我们等到讲到接口时再讲 。
3.求数组中元素的平均值
给定一个整型数组, 求平均值。(这个较简单,就直接看代码吧)
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
System.out.println(avg(arr));
}
public static double avg(int[] arr) {
int sum = 0;
for (int x : arr) {
sum += x;
}
return (double)sum / (double)arr.length;
}
// 执行结果
3.5
4. 查找数组中指定元素(顺序查找)
很简单,就不详细讲述了,直接看文案
5.查找数组中指定元素(二分查找)
这个在c语言里也学过,这里就不过多讲述了,直接看文案。
6.数组排序(冒泡排序)
之前在c语言里学过了,这里直接看文案,就不讲了。
冒泡排序性能较低. Java 中内置了更高效的排序算法,其中sort用的就是更高效的排序算法。
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
sort还可以指定部分进行排序。如
Arrays.sort(a,0,6);
java中都是左闭右开,所以在这里是[0,6),从而是对数组中的下标为0到下标为5中的这部分进行排序。
关于 Arrays.sort 的具体内部实现算法, 我们在后面的排序算法课上再详细介绍. 到时候我们会介绍很多种常见排序算法.
sort对数组是进行升序排列,sort并不能对数组进行降序排列 (如果要实现降序可以先用sort进行升序排列,再将数组逆序)
7.数组逆序
这个很简单,在c语言中学过,这里直接看文案吧
6.二维数组
二维数组的内存图
此时创建3个一维数组,这三个一维数组并不是连续分布的,三个一维数组分别有三个内存地址值,此时二维数组存放的就是这3个内存地址值。在二维数组通过3个地址值就可以找到3块空间,此时二维数组才算创建完毕,也会有一个对应的地址值(图上的0x0011),并把这个地址值赋值给arr。
此时如果输出arr[0]就会出现第一个一维数组的地址值,输出arr即输出二维数组的地址值。
所以说二维数组是个特殊的一维数组。
通过这java的二维数组的内存图也就能很好解释之后的二维数组代码了。
在c语言中二维数组的内存图也跟java的内存图差不多。(之前我对c语言二维数组的内存图理解有误,现在改正跟这个Java的内存图分布差不多,只是c语言数组是全部分布在栈区)
二维数组的创建和初始化
这是二维数组的正常初始化 :分为三种,实则两种。
不规则的二维数组
这是java特有的,c语言中二维数组不可能存在这种不规则的。
public class one {
public static void main(String[] args) {
int[][] a = new int[3][]; //不规则二维数组必须要用这样的格式,列不能显示出来
//该代码先创建好存放一维数组地址的二维数组,子数组并没创建
for(int i = 0; i < 3; i++)//再创建并初始化每个子数组
{
a[i] = new int[i + 1];
}
}//之后的代码就可以使用不规则的二维数组了,否则不能使用。
这就是不规则的二维数组。
个人对于二维数组的理解
int[][] arr=new int[4][3]
此时arr类型为int[][] 。[]可以理解为c语言的*,所以可以理解arr类型为int**,根据内存图不难发现arr是二维数组的地址,而二维数组存放的是存放整形的一维数组的地址,所以可以用int**表示.从而在java中arr类型是int[][].
同理, 还存在 "三维数组", "四维数组" 等更复杂的数组, 只不过出现频率都很低. 其道理跟二维数组是一样的(用二维数组可以推出来)