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

您所在的位置:网站首页 java数组的定义和使用思路 java之数组的定义和使用(附加练习题)

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

2024-07-16 18:11| 来源: 网络整理| 查看: 265

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

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

下面先来看一个图:

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

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

首先我们在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)

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 内存区域划分(重点)

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] } }

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

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] } }

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

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

此时我们并没有改变原有数组,如图所示,最终我们所输出的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方法的源码实现:

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

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

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

首先我们在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方法的声明:

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

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

注意复制的时候包括下标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的取值最多(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[][] array3=new int[][]{{1,2,3},{4,5,6},{7,8,9}}; 打印数组  方法1

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

代码示例

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

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


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3