day05_数组

容器概念

  • 容器:是将多个数据存储到一起,每个数据称为该容器的元素生活中的容器:水杯,衣柜,教室

数组的概述

  • 数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式 对这些数据进行统一管理。 数组就是用于存储数据的长度固定的容器。

百度百科中对数组的定义:

所谓数组(array),就是相同数据类型的元素按一定顺序排列的集合,就是把有限个类型相同的变量用一个名字命名,以便统一管理他们,然后用编号区分他们,这个名字称为数组名,编号称为下标或索引(index)。组成数组的各个变量称为数组的元素(element)。组中元素的个数称为数组的长度(length)。

特点:

  • 数组本身是引用数据类型,而数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型。
  • 创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续空间的首地址。
  • 数组的长度一旦确定,就不能修改。
  • 我们可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快。

数组的分类:

  • 按照维度:一维数组、二维数组、三维数组、…
  • 按照元素的数据类型分:基本数据类型元素的数组、引用数据类型元素的数组(即对象数组)

一维数组

一维数组的声明格式有以下两种:

  • 数组元素的类型[ ] 变量名称;
  • 数组元素的类型 变量名称[ ];

数组元素的类型,可以是 java 中的任意类型,变量名称可以是任意合法的标识符,上面两种格式推荐是第一种,例如:

int [ ] a;
Student[ ] stu
int[ ] a, b, c  // 在一行中也可以声明多个数组

我们单单在程序中声明了数据,而不给他初始化是无法使用的。数组的初始化:在内存当中创建一个数组,并且向其中赋予一些默认值。两种常见的初始化方式:

  • 动态初始化(指定长度):在创建数组的时候,直接指定数组当中的数据元素个数。
  • 静态初始化(指定内容):在创建数组的时候,不直接指定数据个数多少,而是直接将具体的数据内容进行指定。

动态初始化数组的格式:

  • 数据类型[ ] 数组名称 = new 数据类型[数组长度];

解析含义:

  • 左侧数据类型:创建的数组容器可以存储什么数据类型的数据。 元素的类型可以是任意的Java的数据类型。例如:int, String, Student等
  • 左侧的中括号[ ] :代表我是一个数组
  • 左侧数组名称:为定义的数组起个变量名,满足标识符规范,可以使用名字操作数组。
  • 右侧的new:代表创建数组的动作。关键字,创建数组使用的关键字。因为数组本身是引用数据类型,所以要用new创建数组对象。
  • 右侧数据类型:必须和左边的数据类型保持一致
  • 右侧中括号的长度:也就是数组当中,到底可以保存多少个元素 ,是一个int数字。

代码示例

        // 创建一个数组,里面可以存放300个int数据
        int[] arrayA = new int[300];

        // 创建一个数组,能存放10个double类型的数据
        double[] arrayB = new double[10];

        // 创建一个数组,能存放5个字符串
        String[] arrayC = new String[5];

静态初始化基本格式:

  • 数据类型[] 数组名称 = new 数据类型[] { 元素1, 元素2, ... };
      // 直接创建一个数组,里面装的全都是int数字,具体为:5、15、25
        int[] arrayA = new int[] { 5, 15, 25, 40 };

        // 创建一个数组,用来装字符串:"Hello"、"World"、"Java"
        String[] arrayB = new String[] { "Hello", "World", "Java" };

静态初始化省略格式:

  • 数据类型[] 数组名称 = { 元素1, 元素2, ... };
        // 省略格式的静态初始化
        int[] arrayA = { 10, 20, 30 };

注意事项:

  • 静态初始化没有直接指定长度,但是仍然会自动推算得到长度。
  • 静态初始化标准格式可以声明和赋值分开进行
  • 动态初始化也可以声明和赋值分开进行
  • 静态初始化一旦使用省略格式,就不能声明和赋值分开进行
  • 如果不确定数组当中的具体内容,用动态初始化;否则,已经确定了具体的内容,用静态初始化。

代码示例

public class Demo03Array {
    public static void main(String[] args) {
        // 静态初始化的标准格式,声明和赋值分开进行
        int[] arrayB;
        arrayB = new int[] { 11, 21, 31 };

        // 动态初始化也可以拆分成为两个步骤,声明和赋值分开进行
        int[] arrayC;
        arrayC = new int[5];

        // 静态初始化的省略格式,声明和赋值分开进行
        //  int[] arrayD;
        // arrayD = { 10, 20, 30 };
    }

}

如何调用数组的指定位置的元素

  • 索引:每一个存储到数组的元素,都会自动的拥有一个编号,从0开始,这个自动编号称为数组索引 (index)特点:连续,逐一增加,每次加1。可以通过数组的索引访问到数组中的元素。 格式:数组名[索引]。
  • 也可以通过数组的索引给数组指定位置赋值
  • 数组元素下标可以是整型常量或整型表达式。如a[3] , b[i] , c[6*i];
  • 数组元素下标从0开始;长度为n的数组合法下标取值范围: 0到 n-1。

代码示例

/*
直接打印数组名称,得到的是数组对应的:内存地址哈希值。

思考:打印array 为什么是[I@75412c2f,它是数组的地址吗?

答:它不是数组的地址。

问?不是说array 中存储的是数组对象的首地址吗?

答:
array 中存储的是数组的首地址,但是因为数组是引用数据类型,打印array 时,会自动调用arr数组对象的toString()方法,
默认该方法实现的是对象类型名@该对象的hashCode()值的十六进制值。

问?对象的hashCode值是否就是对象内存地址?

答:不一定,因为这个和不同品牌的JVM产品的具体实现有关。
例如:Oracle的OpenJDK中给出了5种实现,其中有一种是直接返回对象的内存地址,
但是OpenJDK默认没有选择这种方式。
 */
public class Demo04ArrayUse {

    public static void main(String[] args) {
        // 静态初始化的省略格式
        int[] array = { 10, 20, 30 };

        System.out.println(array); // [I@75412c2f

        // 直接打印数组当中的元素
        System.out.println(array[0]); // 10
        System.out.println(array[1]); // 20
        System.out.println(array[2]); // 30
        System.out.println("=============");

        // 也可以将数组当中的某一个单个元素,赋值交给变量
        int num = array[1];
        System.out.println(num); // 20
    }

}

如何获取数组的长度

  •  每个数组都具有长度,而且是固定的,Java中赋予了数组的一个属性,可以获取到数组的长度,语法: 数组名.length ,属性length的执行结果是数组的长度,int类型结果。由次可以推断出,数 组的最大索引值为数组名.length-1

代码示例

/*
如何获取数组的长度,格式:
数组名称.length

这将会得到一个int数字,代表数组的长度。

数组一旦创建,程序运行期间,长度不可改变。
 */
public class Demo03ArrayLength {

    public static void main(String[] args) {
        int[] arrayA = new int[3];

        int[] arrayB = {10, 20, 30, 3, 5, 4, 6, 7, 8, 8, 65, 4, 44, 6, 10, 3, 5, 4, 6, 7, 8, 8, 65, 4};
        int len = arrayB.length;
        System.out.println("arrayB数组的长度是:" + len);
        System.out.println("=============");

        int[] arrayC = new int[3];
        System.out.println(arrayC.length); // 3
        arrayC = new int[5];
        System.out.println(arrayC.length); // 5
    }

}

如何遍历数组

​​

代码示例

/*
遍历数组,说的就是对数组当中的每一个元素进行逐一、挨个儿处理。默认的处理方式就是打印输出。
 */

public class Demo07Array {
    /*
        数组的遍历: 通过循环获取数组中的所有元素(数据)

        动态获取数组元素个数 : 数组名.length
     */
    public static void main(String[] args) {
        int[] arr = {11, 22, 33, 44, 55};

        // 数组名.length
        System.out.println("arr数组中元素的个数为:" + arr.length);
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

增强for 遍历数组,代码示例

/*
遍历方式二: 增强for循环
    for(数组的类型 标识符: 数组名){
            标识符就是数组内的元素
    }
 */

public class Demo03 {
    public static void main(String[] args) {
        String [] strArr = new String[]{"蔡旭坤","特朗普","肖战","郭德纲"};
        for (String s : strArr) {
            System.out.print(s+ " ");//蔡旭坤 特朗普 肖战 郭德纲 
        }
    }
}

 一维数组元素的默认初始化值

  • 数组是引用类型,它的元素相当于类的成员变量,因此数组一经 分配空间,其中的每个元素也被按照成员变量同样的方式被隐式初始化。
  • 对于基本数据类型而言,默认初始化值各有不同
  • 对于引用数据类型而言,默认初始化值为null(注意与0不同!)       

代码示例

public class Demo {
    public static void main(String[] args) {
        // 动态初始化一个数组
        int[] array = new int[3];
        Object[] objects = new Object[4];
        System.out.println(array); // 内存地址值
        System.out.println(array[0]); // 0
        System.out.println(objects[0]);//null
    }
}

Java虚拟机的内存划分

为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。 JVM的内存划分如下图所示:

解释上图的每个区域名称的含义:  

​​

注意事项:

  • 每new一次就必定会在堆空间开辟一个新的空间
  • 数组的变量名称存在于栈空间中。而且保存了存储在数组中的首地址

一个数组的内存图

 多个数组指向相同内存图

public static void main(String[] args) {
    // 定义数组,存储3个元素
    int[] arr = new int[3];
    //数组索引进行赋值
    arr[0] = 5;
    arr[1] = 6;
    arr[2] = 7;
    //输出3个索引上的元素值
    System.out.println(arr[0]);
    System.out.println(arr[1]);
    System.out.println(arr[2]);
    //定义数组变量arr2,将arr的地址赋值给arr2
    int[] arr2 = arr;
    arr2[1] = 9;
    System.out.println(arr[1]);
}

结论:引用类型传递的是内存地址值。

练习题

import java.util.Scanner;

/*
 * 2. 从键盘读入学生成绩,找出最高分,并输出学生成绩等级。
        成绩>=最高分-10    等级为’A’   
        成绩>=最高分-20    等级为’B’
        成绩>=最高分-30    等级为’C’   
        其余                               等级为’D’
        
        提示:先读入学生人数,根据人数创建int数组,存放学生成绩。

 * 
 */
public class ArrayDemo {
    public static void main(String[] args) {
        //1.使用Scanner,读取学生个数
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入学生人数:");
        int number = scanner.nextInt();
        
        //2.创建数组,存储学生成绩:动态初始化
        int[] scores = new int[number];
        //3.给数组中的元素赋值
        System.out.println("请输入" + number + "个学生成绩:");
        int maxScore = 0;
        for(int i = 0;i < scores.length;i++){
            scores[i] = scanner.nextInt();
            //4.获取数组中的元素的最大值:最高分
            if(maxScore < scores[i]){
                maxScore = scores[i];
            }
        }
        
        //5.根据每个学生成绩与最高分的差值,得到每个学生的等级,并输出等级和成绩
        char level;
        for(int i = 0;i < scores.length;i++){
            if(maxScore - scores[i] <= 10){
                level = 'A';
            }else if(maxScore - scores[i] <= 20){
                level = 'B';
            }else if(maxScore - scores[i] <= 30){
                level = 'C';
            }else{
                level = 'D';
            }
            
            System.out.println("student " + i + 
                    " score is " + scores[i] + ",grade is " + level);
        }
        
    }
}

 用一个数组存储26个小写英文字母,并遍历显示,显示要求如:a->A

public class Demo04 {
    public static void main(String[] args) {
            //有一个数组 长度为26 char类型
            char[] charArr = new char[26];

        /*
         将26个英文字母填充到数组内 a  b  c  d  e f g ........
          a:97   b:98  0: 48  A:65 z:122
          A:65   B:66
          小写字母-32 可以拿到对应的大写字母 的ASCII值
        */
            for(int i = 0;i<charArr.length;i++){
                //  从int转换到char可能会有损失
                //小的数据类型 标识符 = (小的数据类型)大的数据类型的数值
                charArr[i] = (char)(97+i);
            }

            for(char e:charArr){
                //获取小写字母对应的大写字母的编码值
                int num = e-32;
                //并 将其转为 大写字母
                char daXieZiMu = (char)num;
                System.out.println(e+"->"+daXieZiMu);
            }

        }
    }

需求:已知一个数组 arr = {19, 28, 37, 46, 50}; 键盘录入一个数据,查找该数据在数组中的索引,并在控制台输出找到的索引值。

import java.util.Scanner;

public class Test4Array {
    /*
        需求:
            已知一个数组 arr = {19, 28, 37, 46, 50}; 键盘录入一个数据,查找该数据在数组中的索引,并在控
            制台输出找到的索引值。

       思路:
            1.定义一个数组,用静态初始化完成数组元素的初始化
            2.键盘录入要查找的数据,用一个变量接收
            3.定义一个索引变量,初始值为-1
            4.遍历数组,获取到数组中的每一个元素
            5.拿键盘录入的数据和数组中的每一个元素进行比较,如果值相同,就把该值对应的索引赋值给索引变量,并结束循环
            6.输出索引变量
     */
    public static void main(String[] args) {
        // 1.定义一个数组,用静态初始化完成数组元素的初始化
        int[] arr = {19, 28, 37, 46, 50};
        // 2.键盘录入要查找的数据,用一个变量接收
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入您要查找的元素:");
        int num = sc.nextInt();
        // 3.定义一个索引变量,初始值为-1
        // 假设要查找的数据, 在数组中就是不存在的
        int index = -1;
        // 4.遍历数组,获取到数组中的每一个元素
        for (int i = 0; i < arr.length; i++) {
            // 5.拿键盘录入的数据和数组中的每一个元素进行比较,如果值相同,就把该值对应的索引赋值给索引变量,并结束循环
            if(num == arr[i]){
                // 如果值相同,就把该值对应的索引赋值给索引变量,并结束循环
                index = i;
                break;
            }
        }
        //  6.输出索引变量
        System.out.println(index);
    }
}

需求:在编程竞赛中,有6个评委为参赛的选手打分,分数为0-100的整数分。选手的最后得分为:去掉一个最高分和一个最低分后 的4个评委平均值 (不考虑小数部分)。

import java.util.Scanner;

public class Test5Array {
    /*
        需求:在编程竞赛中,有6个评委为参赛的选手打分,分数为0-100的整数分。
                选手的最后得分为:去掉一个最高分和一个最低分后 的4个评委平均值 (不考虑小数部分)。

        思路:
            1.定义一个数组,用动态初始化完成数组元素的初始化,长度为6
            2.键盘录入评委分数
            3.由于是6个评委打分,所以,接收评委分数的操作,用循环
            4.求出数组最大值
            5.求出数组最小值
            6.求出数组总和
            7.按照计算规则进行计算得到平均分
            8.输出平均分

     */
    public static void main(String[] args) {
        // 1.定义一个数组,用动态初始化完成数组元素的初始化,长度为6
        int[] arr = new int[6];
        // 2.键盘录入评委分数
        Scanner sc = new Scanner(System.in);
        //  3.由于是6个评委打分,所以,接收评委分数的操作,用循环
        for (int i = 0; i < arr.length; i++) {
            System.out.println("请输入第" + (i+1) + "个评委的打分:");
            int score = sc.nextInt();
            if(score >= 0 && score <= 100){
                // 合法的分值
                arr[i] = score;
            }else{
                // 非法的分值
                System.out.println("您的打分输入有误, 请检查是否是0-100之间的");
                i--;
            }
        }

        // 4.求出数组最大值
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if(max < arr[i]){
                max = arr[i];
            }
        }

        // 5.求出数组最小值
        int min = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if(min > arr[i]){
                min = arr[i];
            }
        }

        // 6.求出数组总和
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }

        // 7.按照计算规则进行计算得到平均分
        int avg = (sum - max - min ) / 4;

        // 8.输出平均分
        System.out.println(avg);
    }
}

二维数组

对于二维数组的理解,我们可以看成是一维数组 array1又作为另一个一维数组array2的元素而存在。其实,从数组底层的运行机制来看,其实没有多维数组。

二维数组的声明格式如下:

注意:

int[] x, y[];
//x是一维数组,y是二维数组

和一维数组一样,如果只声明不赋值是不能使用的。下面介绍几种二维数组初始化的方式:

静态初始化

 如果是静态初始化,右边new 数据类型[ ][ ]中不能写数字,因为行数和列数,由{ }的元素个数决定

/*
初始化:
    静态初始化
        int [] arr = {1,2,3,4,5};
      方式一: 数据类型[][] 标识符 = {{一维数组的元素},{一维数组的的元素},{一维数组的元素}};
      
      方式二: 数据类型[][] 标识符 = new 数据类型[][]{{一维数组的元素},{一维数组的元素}};
    
    动态初始化
        
        1.动态初始化方式一:
           数据类型[][] 标识符 = new 数据类型[容量1][容量2];
           
           容量1:代表此二维数组内 有多少一维数组
           
           容量2:带表一维数组内  有多少个元素
           
           生成一个等长的一维数组
          
        2.动态初始化方式二:
         数据类型[][] 标识符 = new 数据类型[容量1][];
         
         使用前 需要给每一个一维数组进行手动指定大小 
         

*/


class Test4{

    public static void main(String [] args){
        int [][] arr = new int[5][6];
        //查找第一个一维数组的长度
        
        System.out.println(arr.length);
        
        System.out.println("--->"+arr[0].length);
        //获取第一个一维数组内第二个元素
        System.out.println("--->"+arr[0][1]);//0
        
        System.out.println("--->"+arr[1].length);
        System.out.println("--->"+arr[2].length);
        System.out.println("--->"+arr[3].length);
        System.out.println("--->"+arr[4].length);
        
        System.out.println("--------------------------------------------------");//0
        
        
        
        double [][] doubleArr = new double[3][];
        
        System.out.println(doubleArr.length);
        //静态初始化给第一个一维数组赋值
        doubleArr[0] = new double[]{3.14,6.28};
        
        //Exception in thread "main" java.lang.NullPointerException
        System.out.println(doubleArr[0][0]);
        
        //动态初始化 给第二个一维数组赋值
        doubleArr[1] = new double[5];
        System.out.println(doubleArr[1][2]);
        
        
        
    }
}

动态初始化(规则二维表:每一行的列数是相同的)

​​

代码示例

public class Demo10Array {
    public static void main(String[] args) {
        //定义一个二维数组
        int[][] arr = new int[3][2];

        //定义了一个二维数组arr
        //这个二维数组有3个一维数组的元素
        //每一个一维数组有2个元素
        //输出二维数组名称
        System.out.println(arr); //地址值    [[I@175078b

        //输出二维数组的第一个元素一维数组的名称
        System.out.println(arr[0]); //地址值    [I@42552c
        System.out.println(arr[1]); //地址值    [I@e5bbd6
        System.out.println(arr[2]); //地址值    [I@8ee016

        //输出二维数组的元素
        System.out.println(arr[0][0]); //0
        System.out.println(arr[0][1]); //0
    }
}

 动态初始化(不规则:每一行的列数可能不一样)

​​

代码示例

public class Demo11Array {
    public static void main(String[] args) {
        //定义数组
        int[][] arr = new int[3][];

        System.out.println(arr);    //[[I@175078b

       //  System.out.println(arr[1][0]); NullPointerException
        System.out.println(arr[0]); //null
        System.out.println(arr[1]); //null
        System.out.println(arr[2]); //null

        //动态的为每一个一维数组分配空间
        arr[0] = new int[2];
        arr[1] = new int[3];
        arr[2] = new int[1];

        System.out.println(arr[0]); //[I@42552c
        System.out.println(arr[1]); //[I@e5bbd6
        System.out.println(arr[2]); //[I@8ee016

        System.out.println(arr[0][0]); //0
        System.out.println(arr[0][1]); //0
        //ArrayIndexOutOfBoundsException
        //System.out.println(arr[0][2]); //错误

        arr[1][0] = 100;
        arr[1][2] = 200;
    }
}

 如何调用数组的指定位置的元素

 //  定义一个名称为arr的二维数组,二维数组中有三个一维数组
        //  每一个一维数组中具体元素也都已初始化
        String[][] arr = new String[][]{{"a", "b", "c"}, {"e", "f"}, {"w", "|", "wed", "|wre"}};
        //找到二维数组中的index为0一维数组 arr[0]
        //访问一维数组index 为2 第三个元素
        System.out.println(arr[2][3]); //|wre

 获取数组的长度

public class Demo0 {
    public static void main(String[] args) {
        //  定义一个名称为arr的二维数组,二维数组中有三个一维数组
        //  每一个一维数组中具体元素也都已初始化
        String[][] arr = new String[][]{{"a", "b", "1","c"}, {"e", "f"}, {"w", "|", "wed", "|wre"}};
        //获取二维数组中一维数组的个数
        System.out.println(arr.length);//3
        //获取第一个一维数组的元素个数
        System.out.println(arr[0].length); //4 

    }
}

如何遍历二维数组

public class Demo0 {
    public static void main(String[] args) {
        //  定义一个名称为arr的二维数组,二维数组中有三个一维数组
        //  每一个一维数组中具体元素也都已初始化
        String[][] arr = new String[][]{{"a", "b", "1","c"}, {"e", "f"}, {"w", "|", "wed", "|wre"}};
        //获取二维数组中一维数组的个数
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[i].length; j++) {
                System.out.print(arr[i][j]+ "  ");
            }
            System.out.println(" ");
        }
    }
}

二维数组中保存的是一维数组的地址值

 求二维数组中的最大值和最小值

public class Demo12Array {
    public static void main(String[] args) {
                int [][] arr = {{10,20,30},{1,2000,3},{-100,200,300}};

                //假设第一个一维数组的第一个元素是最大值
                int maxNum = arr[0][0];//10
                //假设第一个一维数组的第一个元素是最小值
                int minNum = arr[0][0];

                for(int i = 0;i<arr.length;i++){// 0 1 2
                    //遍历每一个一维数组

                    for(int j = 0;j<arr[i].length;j++){
                        //如果比最大值还大  说明 此数是最大值
                        if(arr[i][j] >maxNum){

                            maxNum = arr[i][j];

                        }

                        //如果一个数比最小值还小 则 此数是最小值

                        if(arr[i][j]<minNum){

                            minNum = arr[i][j];

                        }

                    }

                }

                System.out.println("最大值是:"+maxNum+"最小值是:"+minNum);

    }
}

Arrays工具类

java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比 如排序和搜索)的各种方法。常见的如下所示:

 代码示例

import java.util.Arrays;

/*
 * java.util.Arrays:操作数组的工具类,里面定义了很多操作数组的方法
 * 
 * 
 */
public class ArraysTest {
    public static void main(String[] args) {
        
        //1.boolean equals(int[] a,int[] b):判断两个数组是否相等。
        int[] arr1 = new int[]{1,2,3,4};
        int[] arr2 = new int[]{1,3,2,4};
        boolean isEquals = Arrays.equals(arr1, arr2);
        System.out.println(isEquals);
        
        //2.String toString(int[] a):输出数组信息。
        System.out.println(Arrays.toString(arr1));
        
            
        //3.void fill(int[] a,int val):将指定值填充到数组之中。
        Arrays.fill(arr1,10);
        System.out.println(Arrays.toString(arr1));
        

        //4.void sort(int[] a):对数组进行排序。
        Arrays.sort(arr2);
        System.out.println(Arrays.toString(arr2));
        
        //5.int binarySearch(int[] a,int key)
        int[] arr3 = new int[]{-98,-34,2,34,54,66,79,105,210,333};
        int index = Arrays.binarySearch(arr3, 210);
        if(index >= 0){
            System.out.println(index);
        }else{
            System.out.println("未找到");
        }
        
        
    }
}

注意事项:

  • 索引越界:访问了数组中不存在的索引对应的元素,造成索引越界问题 。数组角标越界的异常:ArrayIndexOutOfBoundsExcetion
  • 空指针异常:访问的数组已经不再指向堆内存的数据,造成空指针异常。空指针异常:NullPointerException
  • null:空值,引用数据类型的默认值,表示不指向任何有效对象
原文地址:https://www.cnblogs.com/wurengen/p/15354029.html