java之数组的定义和使用(附加练习题)

简介: java之数组的定义和使用(附加练习题)

1. 数组基本用法

1.1 什么是数组

数组本质上就是让我们能 "批量" 创建相同类型的变量.

数组是一块连续的内存,存放相同数据类型的集合

在java当中,数组也称为数组对象

注意事项: 在 Java 中, 数组中包含的变量必须是相同类型.


1.2 创建数组

// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };
// 静态初始化
数据类型[] 数组名称 = { 初始化数据 };


代码示例:数组的定义(三种定义方式)

int[] arr = new int[]{1, 2, 3};//动态初始化
int[] arr = {1, 2, 3};//静态初始化

思考:此时若不给数组赋值的话,数组的默认值是多少呢?

//定义了数组,但没有初始化,默认值为0
int[] arr1 = new int[6];
经过调试可以发现若不初始化的话默认值为0

2.png


1.3 数组的使用

代码示例1: 获取长度 & 访问元素

int[] arr = {1, 2, 3};
// 获取数组长度
System.out.println("length: " + arr.length); // 执行结果: 3
// 访问数组中的元素
System.out.println(arr[1]); // 执行结果: 2
System.out.println(arr[0]); // 执行结果: 1
arr[2] = 100;
System.out.println(arr[2]); // 执行结果: 100


注意事项

1. 使用 arr.length 能够获取到数组的长度.  length为数组对象的一个属性。

2. 使用 [ ] 按下标取数组元素. 需要注意, 下标从 0 开始计数

3. 使用 [ ] 操作既能读取数据, 也能修改数据.

4. 下标访问操作不能超出有效范围 [0, length - 1] , 如果超出有效范围, 会出现下标越界异常


代码示例2:数组越界异常的示例

int[] arr = {1, 2, 3};
System.out.println(arr[100]);
// 执行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100 at Test.main(Test.java:4)

抛出了 java.lang.ArrayIndexOutOfBoundsException 异常. 使用数组一定要下标谨防越界.

代码示例3: 遍历数组(三种方法)

所谓 "遍历" 是指将数组中的所有元素都访问一遍, 不重不漏. 通常需要搭配循环语句.


遍历数组一共有三种方法:


方法1:利用循环遍历输出数组,此方法可以获取数组下标值

int[] arr = {1, 2, 3, 4, 5, 6};
for (int i = 0; i < arr.length; i++) {
    System.out.print(arr[i] + " ");
}
//输出结果为1 2 3 4 5 6

方法2:利用foreach循环进行遍历输出数组,此方法只能获得数组中的值

int[] arr = {1, 2, 3, 4, 5, 6};
for (int x : arr) {
    System.out.print(x + " ");
}
//输出结果为1 2 3 4 5 6

方法3:利用Array工具类(注:Array工具类是专门用来操作数组的工具类)的toString方法进行遍历输出数组,即将数组转化成字符串进行输出,但是toString方法只能遍历输出一维数组,且toString方法的返回值为String


如果还想知道Arrays工具类中其他对数组操作的方法,可以直接利用JAVA 的API文档进行查询。

int[] arr = {1, 2, 3, 4, 5, 6};
String str = Arrays.toString(arr);
System.out.print(str + " ");
//输出结果为[1, 2, 3, 4, 5, 6],此输出结果的格式是toString方法所固有的格式

2. 数组作为方法的参数

2.1 基本用法

代码示例: 打印数组内容

        public static void main (String[]args){
            int[] arr = {1, 2, 3};
            printArray(arr);
        }
        public static void printArray ( int[] a){
            for (int x : a) {
                System.out.println(x);
            }
        }
        // 执行结果
        1
        2
        3

     

2.2 理解引用类型(重点/难点)

2.2.1

下面先来看一个图:


2.png

其中我们可以清楚的看到等号左边首先是数组类型,然后是引用,等号右边为数组对象,在JAVA数组中,引用所存放的是其所指向的数组对象在堆中的地址。例如下图所示,arr2这个引用存储的就是所指对象在堆中的地址。


下面我们再用另外一张图来具体解析下其在内存中的存储:


2.png

首先我们在main方法中定义了三个数组,则这三个局部变量全部存储于栈上的main方法的栈帧中,此时对arr进行初始化,初始化的值我们称之为数组对象,则arr中此时存储的便是arr所指向的数组对象在堆中的地址,假设其为0x999,在逻辑上来说此时引用指向了对象,同理arr2这个引用也是相同的逻辑,只是arr2为动态初始化,而arr1为静态初始化。此时我们会发现arr1并没有进行初始化,不同于c语言的是,java中没有初始化的数组的值默认为0,并不是随机值,此处虽然没有存储地址,但是arr1这个引用同样指向了自己的数组对象。

此处要注意一个问题:引用是不是一定都在栈上呢?

答:引用不一定都在栈上的,之所以在栈上是因为其是一个局部变量,若是一个全局变量的话则未必在栈上,但是对象一定在堆上。


2.2.2一个引用只能指向一个对象


例如下面的代码此时arr这个引用表面上看起来指向了三个数组对象,实际上只能指向一个对象为{11,12,3,4,5},其余的两个对象被垃圾回收器回收掉了。

int[] arr= {1, 2, 3, 4, 5, 6, 7};
arr= new int[]{3, 4, 5, 6};
arr= new int[]{11, 12, 3, 4, 5};
String str3 = Arrays.toString(arr);
System.out.println(str3);
//输出结果为[11, 12, 3, 4, 5]

2.2.3 代码示例:如何正确理解下面的代码:

int[] arr7 = {1, 2, 3, 4};
int[] arr8 = arr7;
String str2 = Arrays.toString(arr8);
System.out.println(str2);
//输出结果为[1, 2, 3, 4]

此处不能理解成为arr8引用指向arr7引用,因为引用是不能指向引用的,正确理解为arr8这个引用指向了arr7这个引用所指向的数组对象。

此时arr7中存储的是其数组对象在堆中的地址,现在通过int[] arr8 = arr7语句将其地址赋给了arr8这个引用,那么此时arr8这个引用指向了arr7这个引用所指向的数组对象,最终两个引用同时指向了{1,2,3,4}这个对象。

注意:当两个引用同时指向了一个对象,最终当一个引用修改了对象中的某个值,那么当另一个引用去访问这个对象的时候,也是会收到牵连的,举例:

public class suanshu {
    public static void func(int[] array2) {
        array2[2] = 6;
    }
    public static void main(String[] args) {
        int[] array={1,2,3,4,5,6};
        func(array);
        System.out.println(array[2]);
    }
}

此时输出的结果为6,原因是我们的array和array2这两个引用同时指向了{1,2,3,4,5,6}这个对象,当array2这个引用修改了对象中的2下标所对应的值后,array这个引用再次访问2这个下标对应的值便成了修改过后的值。


2.3 认识 null  

null 在 Java 中表示 "空引用" , 也就是一个无效的引用,此时这个引用不指向任何一个对象

int[] arr={1,2,4,5};
arr = null; 
System.out.println(arr[0]);
System.out.println(arr.length); 
// 执行结果 
Exception in thread "main" java.lang.NullPointerException at Test.main(Test.java:6)

Exception in thread "main" java.lang.NullPointerException at Test.main(Test.java:6)

null 的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操作. 一旦尝试读写, 就会抛出 NullPointerException.即空指针异常

所以说如果发生了空指针异常,首先检查我们的引用是否为null.

注意: Java 中并没有约定 null 和 0 号地址的内存有任何关联.

思考一个问题:当arr不指向任何对象时,原来的数组对象去了哪里?

答:当arr等于null时,此时原来的数组对象被jvm中的垃圾回收器回收了

总结:当一个对象不再被任何引用所引用的时候,此时会被垃圾回收器回收掉

例如  int arr={1,2,3,4,5,6};

       arr=null;

那么{1,2,3,4,5,6}这个对象如果没有被任何一个引用所指向的话,在不久后会被垃圾回收器回收掉。

同时当我们的引用不知道指向谁的时候,可以赋值为null.但是不能赋值为0.


2.4 初识 JVM 内存区域划分(重点)

1.webp.jpg


JVM 的内存被划分成了几个区域, 如图所示:

1.程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址.

2.虚拟机栈(JVM Stack): 重点是存储局部变量表(当然也有其他信息). 我们刚才创建的 int[] arr 这样的存储地址的引用就是在这里保存.

3.本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的.

在这里简单介绍下native方法:JVM 是一个基于 C++ 实现的程序. 在 Java 程序执行过程中, 本质上也需要调用 C++ 提供的一些函数进行和操作系统底层进行一些交互. 因此在 Java 开发中也会调用到一些 C++ 实现的函数.这里的 Native 方法就是指这些 C++ 实现的, 再由 Java 来调用的函数.

4.堆(Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2, 3} )

5.方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. 方法编译出的的字节码就是保存在这个区域.

6.运行时常量池(Runtime Constant Pool): 是方法区的一部分, 存放字面量(字符串常量)与符号引用. (注意 从 JDK 1.7 开始, 运行时常量池在堆上).

我们发现, 在上面的图中, 程序计数器, 虚拟机栈, 本地方法栈被很多个原谅色的, 名叫 Thread(线程) 的方框圈起来了,并且存在很多份. 而 堆, 方法区, 运行时常量池, 只有一份. (关于线程, 这是我们后面重点讲解的内容).


关于上面的划分方式, 我们随着后面的学习慢慢理解. 此处我们重点理解 虚拟机栈 和 堆


3. 数组作为方法的返回值

代码示例: 写一个方法, 将数组中的每个元素都 * 2

 public class ShuZu {
        //写一个方法,将数组中的每一个元素扩大2倍(不改变原数组)
        public static int[] print2(int[] arr) {
            for (int i = 0; i < arr.length; i++) {
                arr[i] = arr[i] * 2;
            }
            //注意返回的是一个数组名
            return arr;
        }
        public static void main(String[] args) {
            int[] arr= {1, 2, 3, 4, 5, 6};
            int[] arr1 = print2(arr);
            System.out.println(Arrays.toString(arr1));
            //输出结果为[2, 4, 6, 8, 10, 12]
        }
    }

这个代码固然可行, 但是破坏了原有数组. 有时候我们不希望破坏原数组, 就需要在方法内部创建一个新的数组, 并由方法返回出来.,上述代码示意图如下所示:

2.png


 public class ShuZu {
        //写一个方法,将数组中的每一个元素扩大2倍(改变原数组)
        public static int[] print3(int[] arr) {
            int[] ret = new int[arr.length];
            for (int i = 0; i < arr.length; i++) {
                ret[i] = arr[i] * 2;
            }
            return ret;
        }
        public static void main(String[] args) {
            int[] arr = {1, 2, 3, 4, 5, 6};
            int[] arr1 = print3(arr);
            System.out.println(Arrays.toString(arr1));
            //输出结果为[2, 4, 6, 8, 10, 12]
        }
    }

这样的话就不会破坏原有数组了.

另外由于数组是引用类型, 返回的时候只是将这个数组的首地址返回给函数调用者, 没有拷贝数组内容, 从而比较高效.上述代码示意图如下所示:

2.png此时我们并没有改变原有数组,如图所示,最终我们所输出的arr1指向的是新的对象{2,4,6,8,10,12},并不是原有对象{1,2,3,4,5,6}。

代码示例:匿名数组作为返回值

   public static int[] func(int[] arr) {
        return new int[]{1, 2};
    }
此时的new int[]{1,2}就是匿名数组。


4. 数组练习

4.1 数组转字符串

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]

使用这个方法后续打印数组就更方便一些.


Java 中提供了 java.util.Arrays 包, 其中包含了一些操作数组的常用方法.


在这里科普下什么是包?

例如做一碗油泼面, 需要先和面, 擀面, 扯出面条, 再烧水, 下锅煮熟, 放调料, 泼油. 但是其中的 "和面, 擀面, 扯出面条" 环节难度比较大, 不是所有人都能很容易做好. 于是超市就提供了一些直接已经扯好的面条, 可以直接买回来下锅煮. 从而降低了做油泼面的难度, 也提高了制作效率. 程序开发也不是从零开始, 而是要站在巨人的肩膀上. 像我们很多程序写的过程中不必把所有的细节都自己实现, 已经有大量的标准库(JDK提供好的代码)和海量的 第三方库(其他机构组织提供的代码)供我们直接使用. 这些代码就放在一个一个的 "包" 之中. 所谓的包就相于卖面条的超市. 只不过, 超市的面条只有寥寥几种, 而我们可以使用的 "包" , 有成千上万.


我们实现一个自己版本的数组转字符串:即重写我们的toString方法


public class suanshu {
    public static String myToString(int[] array) {
     //要考虑到我们这个数组假如为空的情况
        if(array==null){
            return "[]";
        }
        String ret = "[";
        for (int i = 0; i < array.length; i++) {
            ret += array[i];
     //如果没到最后一个,则一个数字后面跟一个逗号
            if (i != array.length - 1) {
                ret += ",";
            }
        }
        ret += "]";
        return ret;
    }
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        System.out.println(myToString(array));
    }
}
//输出结果为【1,2,3,4,5】

4.2 数组拷贝(重要)

下面一共介绍四种数组拷贝的方法:


方法1:利用for循环来进行拷贝

代码示例:

        int[] array = {1, 2, 3, 4, 5, 6};
        int[] array2 = new int[array.length];
        for (int i = 0; i < array.length; i++) {
            array2[i] = array[i];
        }
        //输出结果为[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array));
        //输出结果为[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array2));

此方法为最简单的数组拷贝方法,利用for循环来进行拷贝


方法2:利用copyof方法进行拷贝

代码示例:

        //定义一个数组array3
        int[] array = {1, 2, 3, 4, 5, 6};
        //copyOf方法是用来拷贝数组的一种方法,括号中放入的分别为引用和想要拷贝多长的数组的长度值
        int[] array2 = Arrays.copyOf(array, 10);
        //输出结果为[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array));
        //输出结果为[1, 2, 3, 4, 5, 6, 0, 0, 0, 0]
        System.out.println(Arrays.toString(array2));

此处我们利用copyof方法来进行数组的拷贝,此处我们可以看下copyof方法的源码实现:


2.png

Arrays的copyOf()方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。(在下面的图中将做具体解释)

copyOf()方法中有两个形参,第一个形参是代表被拷贝的数组,第二个形参newLength指定要建立的新的数组的长度,如果新数组的长度超过原数组的长度,则新数组剩余坑位保留数组的默认值(默认值为0)

下面对此段代码在内存中的存储做一个图示:


2.png

首先我们在main方法中定义了一个数组array并进行了初始化,那么首先在栈上开辟main方法的栈帧,并为引用array开辟内存,此时引用array的数组对象的地址存储在其内存中,逻辑上引用array指向了其数组对象,此时我们定义了一个新的数组array2,并且调用了copyof方法,那么在栈上为copyof开辟一个新的栈帧并在main方法的栈帧中再为array2开辟一个新的内存,然后再为copyof方法内部的局部变量copy在copyof方法的栈帧上分配内存,此时通过copeof方法的调用后我们的引用copy有了一个新的数组对象,此时同样copy引用指向了这个新的数组对象,并且当我们将copyof方法的返回值(即copy数组)赋给我们所定义的新的数组array2后,同样array2这个引用此时也指向了这个新的数组对象(原因是地址传递).


同时在这里介绍下copefOf方法的变形也就是copeOfRange方法,首先我们来看下代码示例:

        int[] array= {1, 2, 3, 4, 5, 6};
        int[] ret=Arrays.copyOfRange(array,1,5);
        int[] ret1=Arrays.copyOfRange(array,1,7);
        //输出结果为[2, 3, 4, 5]
        System.out.println(Arrays.toString(ret));
        //输出结果为[2, 3, 4, 5, 6, 0]
        System.out.println(Arrays.toString(ret1));

首先我们来看下copyOfRange方法的声明:

2.png将一个原始的数组original,从下标from开始复制,复制到下标to,生成一个新的数组。

新拷贝的数组的长度为to-from,并且我们可以看到copyOfRange方法的底层代码实现本质上还是arraycopy方法


2.png

注意复制的时候包括下标from,但不包括上标to。

例如在上面的代码中,我们从array数组中下标为1的地方开始复制,复制到下标为5的地方,但并不复制下标为5处的本身的数字(左闭右开),那么此时复制的数字有2,3,4,5

而当to处的的下标值超过了被复制数组array下标的最大值时,例如上面的代码中,array的下标值最大到5,而此时from下标值为1,to的下标值为7,7>5,相当于当我们复制完array[5]处的数字后,此时array不能再提供数字让我们复制了,则此时ret1[6]的值为0,即为默认值,所以总结结论得当to处的的下标值超过了被复制数组array下标的最大值时,其后面所放的数字统一为0.例如执行int[] ret1=Arrays.copyOfRange(array,1,7);语句后,最终的ret1实际值为[2, 3, 4, 5, 6, 0].

方法3:利用arraycopy方法进行拷贝(注意与copyOf方法的对比)

代码示例:

        int[] array5 = {1, 2, 3, 4, 5, 6};
        int[] array6 = new int[array5.length];
        /*表示从array5数组的0下标开始复制6个数字到array6数组当中,并且在放入arrray6数组当中时
        也是从0下标开始放入*/
        System.arraycopy(array5, 0, array6, 0, 6);
        //输出结果为[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array5));
        //输出结果为[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array6));

首先观察先System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)的声明:

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

src - 源数组(即被复制的数组)。

srcPos - 源数组中的起始位置(表示可以从源数组的任意一个下标处开始复制)。

dest - 目标数组(即复制后的数组)。

destPos - 目标数组中的起始位置(表示可以让复制过来的数字按照下标顺序从其要在新的目标数组中所放入的位置开始有序插入)。

同时还需注意:arraycopy方法使用了native关键字,说明这个方法是c/c++代码实现的,我们是看不到具体实现的。同时被native关键字所修饰的方法具有速度快这一特性。

并且在jvm内存的划分中,有一部分为本地方法栈,其存储的就是被native所修饰的方法.

下面再来看两种需要注意的情况:

情况一:此时我们修改下代码:当我们要复制的数组长度大于原数组长度时,例如我们设为10

     

int[] array5 = {1, 2, 3, 4, 5, 6};
        int[] array6 = new int[10];
        /*表示从array5数组的1下标开始复制5个数字到array6数组当中,并且在放入arrray6数组当中时,是从2下标开始放入的,那么此时array6[0],array6[1]的默认值变为0,并且此时已经占用两个位置后,array6中能放入的数字变为了8个,所以 此时我们复制过来的数字能放入array6这个数组当中的有2,3,4,5,6.接着array6[6]=array6[7]=array6[8]=array6[9]=0.

 此处一定要注意如果srcPos+length>src.length,那么此时便会发生数组下标越界异常,例如此时length值若变为6,1+6>6,那么运行代码时便会出现数组下标越界异常,原因是从原数组下标为1处开始往后复制6个时,当我们复制到下标为5处的地方,此时已经没有数组再让我们复制了。

     

System.arraycopy(array5, 1, array6, 2, 5);
        //输出结果为[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array5));
        //输出结果为[0, 0, 2, 3, 4, 5,0,0,0,0]
        System.out.println(Arrays.toString(array6));
情况二:此时我们修改下代码:当我们要复制的数组长度等于原数组长度时
        int[] array5 = {1, 2, 3, 4, 5, 6};
        int[] array6 = new int[array5.length];
        /*表示从array5数组的1下标开始复制4个数字到array6数组当中,并且在放入arrray6数组当中时,是从2下标开始放入的,那么此时array6[0],array6[1]的默认值变为0,并且此时已经占用两个位置后,array6中能放入的数字变为了4个,所以 此时我们复制过来的数字能放入array6这个数组当中的只有2,3,4,5.

         此处一定要注意length的取值最多(<=)为array5.length-destPos,如果超过此时便会发生数组下标越界异常,例如此时length值若变为5,那么运行代码时便会出现数组下标越界异常,原因是array[6]数组中只能再放入4个了,而length的值是小于等于能插入的个数的.

   

System.arraycopy(array5, 1, array6, 2, 4);
        //输出结果为[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array5));
        //输出结果为[0, 0, 2, 3, 4, 5]
        System.out.println(Arrays.toString(array6));

length - 要复制的数组元素的数量。(容易出错,需要注意)

面试题目:System.arraycopy方法和copyOf方法哪个拷贝速度更快呢?

答:arraycopy方法会比较快,原因是当我们浏览arraycopy方法源码时会发现其被native所修饰,我们都知道native方法是由C/C++代码来实现的,我们是看不到具体实现的,而且被native所修饰的方法一般有个特点就是速度块

所以arraycopy方法会速度更快些

方法4:利用clone方法进行拷贝

代码示例:

        int[] array={1,2,3,4,5,6};
        int[] array1=array.clone();
        //输出结果为[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array));
        //输出结果为[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array1));

此时运用clone方法相当于在堆中对数组对象进行拷贝,如下图所示:


2.png


自主实现数组拷贝的方法:

import java.util.Arrays;
/**
 * 自主实现数组的拷贝方法
 * @author SongBiao
 * @Date 2021/1/06
 */
public class ShuZu3 {
    public static int[] KaoBei(int[] array) {
        int[] ret=new int[array.length];
        for (int i = 0; i <array.length ; i++) {
            ret[i]=array[i];
        }
        return ret;
    }
    public static void main(String[] args) {
        int[] array={1,2,3,4,5,6};
        int[] ret=KaoBei(array);
        System.out.println(Arrays.toString(ret));
    }
}

面试题:这四种拷贝方法是深拷贝还是浅拷贝?

答:这四种方法若是针对基本类型或者是简单类型为深拷贝,对于引用类型来说就是浅拷贝了

深拷贝:当拷贝结束后,通过一个新的引用修改所拷贝的新的对象的其中的某个类型(此类型为简单类型或者基本类型,引用类型的分情况,后续会详细介绍)的值时,并不影响原来引用所对应的相同对象中的相同类型的值,那么此时便为深拷贝。

例如假设此时我们拷贝array1数组后生成了array2数组,当修改了array2[0]处的值后,array1[0]处的值会发生改变吗,答案当然是不,原因是array1和array2这两个数组存储的都是基本数据类型,array2拷贝一份array1后修改自己的值,并不影响array1中的值.

2.png

浅拷贝:当拷贝结束后,此时两个引用指向了同一个数组对象,当通过其中一个引用去改变数组对象的某个值后,另一个引用在访问完数组对象后得到了另一个引用修改后的值,并非原来的值时,则此时便为浅拷贝

例如此时我们有一个array1数组存储的是引用数组类型,即每个对象在堆中的地址,并且每个对象中都存储了对应的整数值,如下所示:


2.png

当我们拷贝一份array1后得到了一个新的数组array2,但是我们此时的拷贝只是拷贝了引用,并没有拷贝对象,所以新拷贝的array2数组还是指向了之前array1所指向的对象。

所以当我们修改array1[0]所对应的对象中的值后,array2再度访问array2[0]下标的值时,便会得到修改后的值.

当然上述情况的浅拷贝要是变为深拷贝的话,也是可以实现的,只需要我们拷贝对象就好了

2.png

后续当我们学完接口后,便可以实现我们的深拷贝了


4.3找出数组中最大的元素

代码示例:

public class suanshu {
    public static int findMax(int[] array){
        //假定此时数组第一个值为最大值
        int max=array[0];
        for(int i=1;i<array.length;i++){
            if(array[i]>max){
                max=array[i];
            }
        }
        return max;
    }
    public static void main(String[] args) {
        int[] arr={1,4,7,9,4,8};
        int maxNum=findMax(arr);
        System.out.println(maxNum);
    }
}

注意:题目思路为假设数组下标为0的元素为最大值,然后从下标为1的元素开始进行比较,假如比下标为0的元素大,那么当前最大值就进行替换,依次往复.


4.4求数组中元素的平均值

代码示例:

public class suanshu {
    public static double findAverge(int[] array) {
        //求数组中元素的平均值
        double sum=0.0;
        double averge=0.0;
        for (int i = 0; i <array.length ; i++) {
            sum+=array[i];
            averge=1.0*sum/array.length;
        }
        return averge;
    }
    public static void main(String[] args) {
        int[] arr={1,4,7,9,4,8};
        double averge=findAverge(arr);
        System.out.println(averge);
    }
}

注意给整形数组求平均值的时候要进行数组元素和或者数组个数的强转。


4.5查找数组中指定元素(顺序查找)

给定一个数组,给定一个元素,找出该元素在数组中的位置

代码示例:

public class suanshu {
    public static int findNum(int[] array, int num) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] == num) {
                return i;
            }
        }
        return -1;
    }
    public static void main(String[] args) {
        int[] arr = {1, 3, 5, 10, 9, 8};
        int num = findNum(arr, 9);
        System.out.println(num);
    }
}

注意: 顺序查找数组中的某个数的下标是最低效的方法,并且不要漏掉查不到下标返回-1这种情况.


4.6查找数组中指定元素(二分查找)

注意:数组的二分查找是建立在这个数组是一个有序数组的前提下


代码示例:

public class suanshu {
    public static int binarySerach(int[] array, int num) {
        int left = 0;
        int right = array.length - 1;
        while (left <= right) {
            int mid = (left + right) / 2;
            if (array[mid] == num) {
                return mid;
            } else if (num > array[mid]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        //没有找到的话返回-1
        return -1;
    }
    public static void main(String[] args) {
        int[] arr = {1, 4, 7, 9, 4, 8};
        //因为二分法建立在有序数组上,所以要先进行排序
        Arrays.sort(arr);
        int num = binarySerach(arr, 8);
        System.out.println(num);
    }
}

使用二分法查找数组中的某个数字的时候,要注意首先这个数组必须是有序数组,其次需要考虑三种情况:

1.如果直接查询到的话,返回下标即可。

2.经过几轮查询后查询到,返回下标。

3.如果查询不到,就返回-1.


当然上述是我们自己实现了我们二分查找的方法,我们的Arrays工具类也为我们提供了二分查找的方法,也就是我们的Arrays.binarySearch方法,代码如下:

public class suanshu {
    public static void main(String[] args) {
        int[] arr = {1, 3, 5, 10, 9, 8};
        int num= Arrays.binarySearch(arr,9);
        System.out.println(num);
    }
}

输出结果为4,结果无误。


4.7检查数组的有序性

给定一个数组,判断这个数组是否是升序还是降序


代码示例:

public class suanshu {
    public static boolean isSorted(int[] array){
        for (int i = 0; i <array.length-1 ; i++) {
             if(array[i]>array[i+1]){
                 return false;
             }
        }
        return true;
    }
    public static void main(String[] args) {
        int[] arr = {1, 3, 5, 10, 9, 8};
        System.out.println(isSorted(arr));
    }
}

上述代码判断的是一个数组是否为升序,是的话返回true,不是true返回false

注意:i此时小于的是array.length-1,原因是假如i小于的是array.length的话,当i=array.length-1,那么i+1=array.length,便会发生越界冲突异常。


4.8数组排序(冒泡排序)

算法思路

每次尝试找到当前待排序区间中最小 ( 或最大 ) 的元素 , 放到数组最前面 ( 或最后面 ).

那么下面的代码我们将以升序为例,也就是将最大元素放到最后面这种方式来进行比较

冒泡排序有三种解法,我们会依次给出优化解法:

解法一:无优化解法

public static int[] bubbleSorted(int[] array) {
        //i表示趟数
        for (int i = 0; i < array.length - 1; i++) {
            //j代表每次比较完后结束的位置
            for (int j = 0; j < array.length - 1; j++) {
                if (array[j] > array[j + 1]) {
                     int tmp=array[j];
                     array[j]=array[j+1];
                     array[j+1]=tmp;
                }
            }
        }
        return array;
    }


冒泡排序中 最容易出错的点在于确定两个for循环中i和j的边界,我们可以看到i是从0开始计数的,说明此时 i=0代表第一趟,i=1代表第二趟,而最终i小于的值 应为array.length-1,并不是array.length

举个例子,假设此时有四个数字,冒泡排序是要比较三趟的,那么当i=0,i

而 j的 边界有 两种取法, 无优化的取法是j小于array.length-1.j也是从0下标开始取

j的思想为首先进行arrray[j]与array[j+1]的比较,如果array[j]>arrray[j+1],那么两两交换,不大于就不交换,然后j+1,继续进行array[j+1]与array[j+2]的比较,依次往复执行即可。

解法二:优化解法一(只优化j的边界)

此时我们j的边界其实还可以继续优化,这样就减少了两两元素比较的频率,


2.png


根据这个图我们可以分析出j其实不但要小于array.length-1,还小于array.length-1-i;

从图中我们来进行分析:

第一趟,i=0,j此时走到了下标2处      array.length-1-i=4-1-0=3>j

第二趟,i=1,j此时走到了下标1处      array.length-1-i=4-1-1=2>j

第三趟,i=2,j此时走到了下标0处      array.length-1-i=4-1-2=1>j

我们可以发现j此时不但小于array.length-1,还小于array.length-1-i

所以此时我们j的边界值可以改为array.length-1-i,这样每次就会少比较一次.

代码示例:

 public static void bubbleSorted(int[] array) {
        //i表示趟数
        for (int i = 0; i < array.length - 1; i++) {
            //j代表每次比较完后结束的位置
            for (int j = 0; j < array.length - 1 - i; j++) {
                if (array[j] > array[j + 1]) {
                    int tmp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = tmp;
                }
            }
        }
    }

 

解法三:优化解法二(优化j的边界+标志控制)

我们紧接着优化解法二继续看,我们会发现其实在第二趟的最后,我们的数组已经是一个有序数组了,第三趟的出现根本是不需要的,但是没有一个标志去告诉for循环,数组已经有序,该退出for循环了。


此时就需要加上我们的标志控制,即定义一个boolean类型变量tag,初始值为false,false代表数组已经有序,如果后面进行了数组元素的交换,tag置为true,说明此时数组还无序。


如果最终数组已经是有序的话,那么tag肯定为false。


代码示例:

 public static void bubbleSorted(int[] array) {
        //i表示趟数
        boolean tag = false;
        for (int i = 0; i < array.length - 1; i++) {
            tag = false;
            //j代表每次比较完后结束的位置
            for (int j = 0; j < array.length - 1 - i; j++) {
                if (array[j] > array[j + 1]) {
                    int tmp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = tmp;
                    tag = true;
                }
            }
            if(tag==false){
                return ;
            }
        }
    }

总规律:

1:一共有五个数字,那么冒泡排序就要比较4趟,即此时我们用i来表示趟数

2.   j用来表示数组下标,同时也代表每次比较完成后结束的位置,我们会发现j每次都小于arr.length-1-i.

3.我们会发现最后一趟已经不用比较了,原因是倒数第二趟的末尾此时已经是有序的了,就无需在进行最后一次比较.


4.9 数组逆置

给定一个数组 , 将里面的元素逆序排列

算法思路

设定两个下标 , 分别指向第一个元素和最后一个元素 . 交换两个位置的元素 .

然后让前一个下标自增 , 后一个下标自减 , 循环继续即可 .

代码示例

 public static void reverse(int[] array) {
        if(array == null) {
            return;
        }
        int i = 0;
        int j = array.length-1;
        while (i < j) {
            int tmp = array[i];
            array[i] = array[j];
            array[j] = tmp;
            i++;
            j--;
        }
    }

注意: 1.首先一定要考虑到数组为空的情况

        2.当不为空的时候,要注意i是永远小于j的

4.10数组数字排列

给定一个整型数组, 将所有的偶数放在前半部分, 将所有的奇数放在数组后半部分


举例


{1, 2, 3, 4}

调整后得到

{4, 2, 3, 1}

算法思路

代码示例

5.二维数组

基本语法

数据类型 [][] 数组名称 = new 数据类型 [ 行数 ][ 列数 ] {初始化数据};

定义方式

方式1

定义一个三行三列且有具体值的数组


int[][] array = {{1,2,3},{4,5,6},{7,8,9}};

方式2

定义一个两行三列的数组,但是没有具体值,所以默认都值是0


int[][] array1=new int[2][3];

方式3

同方式2的定义方法,但是可以不定义列数,只定义行数,这样的二维数组我们称之为不规则的二维数组。如果不定义行数,只定义列数的话会报错。


int[][] array2=new int[2][];

方式4

定义一个三行三列且有具体值的数组,但是定义方式不同于方式1中的方法


int[][] array2=new int[2][];

打印数组

方法1

使用for循环遍历输出二维数组


代码示例

假设此时要遍历输出二维数组array,其定义如下,请遍历数组这个array数组


int[][] array = {{1,2,3},{4,5,6},{7,8,9}};
        for (int i = 0; i <array.length ; i++) {
            //j代表每一行元素的下标
            for (int j = 0; j <array[i].length ; j++) {
                System.out.print(array[i][j]+" ");
            }
            //换行
            System.out.println();
        }
        //输出结果
        1 2 3 
        4 5 6 
        7 8 9   

可以看到i和j的边界各不相同,i此时小于的是array.length,可以看出来外围二维数组array的长度就代表我们二维数组的行数,而j此时小于的是array[i].length,说明array[i].length获取到的就是二维数组中每一行元素的个数,可以借助如下图帮助理解,可以发现二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组.


2.png


方法2

使用for each循环来遍历输出数组:


代码示例

       for(int[] arr:array){
            for(int a:arr){
                System.out.print(a+" ");
            }
        //换行
            System.out.println();
        }
        //输出结果
        1 2 3  
        4 5 6 
        7 8 9 

可以看到使用for(int[] arr:array)语句获取到的是每一行的一维数组.


然后使用for(int a:arr)获取到的是每一行一维数组中具体的元素值.


方法3

使用工具类Arrays中的deepToString方法,输出结果也有所不同


代码示例

        String ret=Arrays.deepToString(array);
        System.out.println(ret);
        //输出结果
        [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

特殊情况

假设此时我们定义一个不规则二维数组如下所示:


int[][] array2=new int[2][];

那么当我们遍历输出这个数组的时候,会发生空指针异常(NullPointerException),这是为什么呢?


答:原因是二维数组的每一个元素其实都是引用,我们可以看到array2数组此时只定义了行,并没有定义列,说明每一行所对应的一维数组默认都是null,因为引用类型的默认值就是null,如下图所示:


2.png


像这种只有行,没有列的不规则二维数组,可以手动指定其每一行的一维数组,如下所示

int[][] array2=new int[2][];
array2[0]=new int[10];
array2[1]=new int[]{1,2,3,4}

注意:如果要给某一行手动赋值,不能直接赋值,例如 array2[1]={1,2,3,4}这样的赋值方法就是错误的。


遍历array2这个数组

        int[][] array = new int[2][];
        array[0]=new int[10];
        array[1]=new int[]{1,2,3,4};
        for (int i = 0; i <array.length ; i++) {
            for (int j = 0; j <array[i].length ; j++) {
                System.out.print(array[i][j]+" ");
            }
            //换行
            System.out.println();
        }
        //输出结果
        0 0 0 0 0 0 0 0 0 0 
        1 2 3 4 


相关文章
|
1月前
|
Java 编译器
Java重复定义变量详解
这段对话讨论了Java中变量作用域和重复定义的问题。学生提问为何不能重复定义变量导致编译错误,老师通过多个示例解释了编译器如何区分不同作用域内的变量,包括局部变量、成员变量和静态变量,并说明了使用`this`关键字和类名来区分变量的方法。最终,学生理解了编译器在逻辑层面检查变量定义的问题。
Java重复定义变量详解
|
2月前
|
存储 缓存 算法
Java 数组
【10月更文挑战第19天】Java 数组是一种非常实用的数据结构,它为我们提供了一种简单而有效的方式来存储和管理数据。通过合理地使用数组,我们能够提高程序的运行效率和代码的可读性。更加深入地了解和掌握 Java 数组的特性和应用,为我们的编程之旅增添更多的精彩。
33 4
|
2月前
|
存储 缓存 算法
提高 Java 数组性能的方法
【10月更文挑战第19天】深入探讨了提高 Java 数组性能的多种方法。通过合理运用这些策略,我们可以在处理数组时获得更好的性能表现,提升程序的运行效率。
33 2
|
23天前
|
Java
在Java中定义一个不做事且没有参数的构造方法的作用
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
|
2月前
|
存储 Java
Java“(array) <X> Not Initialized” (数组未初始化)错误解决
在Java中,遇到“(array) &lt;X&gt; Not Initialized”(数组未初始化)错误时,表示数组变量已被声明但尚未初始化。解决方法是在使用数组之前,通过指定数组的大小和类型来初始化数组,例如:`int[] arr = new int[5];` 或 `String[] strArr = new String[10];`。
88 2
|
2月前
|
Oracle Java 关系型数据库
重新定义 Java 对象相等性
本文探讨了Java中的对象相等性问题,包括自反性、对称性、传递性和一致性等原则,并通过LaptopCharger类的例子展示了引用相等与内容相等的区别。文章还介绍了如何通过重写`equals`方法和使用`Comparator`接口来实现更复杂的相等度量,以满足特定的业务需求。
28 3
|
2月前
|
存储 Java
什么是带有示例的 Java 中的交错数组?
什么是带有示例的 Java 中的交错数组?
53 9
|
2月前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
19 1
|
2月前
|
Java
Java数组动态扩容和动态缩减
Java数组动态扩容和动态缩减
25 3
|
2月前
|
存储 Java 程序员
【一步一步了解Java系列】:何为数组,何为引用类型
【一步一步了解Java系列】:何为数组,何为引用类型
30 1