1、Java基础


自学 Java 如何从入门到精通
https://www.zhihu.com/question/25255189?sort=created

一、准备知识

Java 语言发展

特点:
简单: 针对 C++简化了很多
跨平台:

Java 平台体系:

Java SE
Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为 Java Platform,Enterprise Edition(Java EE)提供基础。
Java ME
这个版本以前称为 J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端 Java 应用程序。
Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web 2.0 应用程序。
JavaEE
Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。

几种名词的概念:

注释

单行注释

// 单行注释. 这一行内容会被jvm运行的时候过滤掉.

多行注释

/*
 多行注释
 一次注释多行代码
 */

文档注释

/**
  * 文档注释
  * 后期可以使用javadoc命令生成api文档
  */

JDK 安装和 IDEA 的安装

https://book.apeland.cn/details/81/

Java 文件的编译和运行

在 cmd 命令行中运行:

  • 首先进入文件所在目录
  • 编译:javac Hello.java 默认 cmd 命令行使用的 ANSI 编码,中文会乱码报错
    • 生成一个Hello.class 文件
  • 运行:java Hello

    我们使用编译器 IEDA 写的代码都是 utf8 编码的,直接使用java Hello.java编译会报“编码 GBK 的不可映射字符” 的错误,有两种解决办法
    1)在 notepad++中全选剪切代码,改变编码为 ANSI,然后粘贴保存
    2)javac -encoding utf8 Hello.java,指定编译.java 文件的编码方法,但后面文件输出中有中文在 cmd 中显示还是会乱码,该方案适用于打印输入无中文的情况

在 IDEA 中编译运行:

注意:
IDEA 编写的代码第一行是包的依赖,如 package com.xjt.demo01;
如果使用命令行的方式运行会发现报错,找不到或无法加载主类
这时需要注释掉第一行

二、Java 数据类型

1、基本数据类型

  • 整数
    • byte 字节. 1 个字节, 范围: -128~127
    • short 短整数. 2 个 byte, 范围: -32768~32767
    • int 整数. 4 个 byte, 范围: -21 亿~21 亿-1
    • long 长整数. 8 个 byte
  • 浮点数
    • float 单精度浮点数 精度低
    • double 双精度浮点数 精度高
  • 字符
    • char 字符类型 表示单个字符. 2 个字节 byte
    • 必须使用单引号,字符串使用的是双引号
    • char 类型可以存放数字,比如 char c = 20037; 实际上是 Unicode 码中汉字 ‘久’
  • 布尔
    • boolean 布尔类型 两个取值(true, false),长度是 8bit 1byte
    • Java 中所有的数据类型的最小单元是 byte(1 byte==1B,1KB=1024B)

1.1 数据类型转化

首先, 这里的转化跟多的发生在数字身上,char 和 boolean 一般不参与转化,谁也不会闲的把一个文字变成数字来干活。我们把数据类型进行排序, 按照能表示的数据量的范围进行排序.

  • byte -> short -> int -> long -> float -> double

为什么 long 排在 float 前面?因为整数是有数据量的范围的,而小数是没有的,很简单的例子, 0~1 有多少个小数? 无数个.
从小数据类型向大数据类型转化是安全的 ->直接转化

byte a = 10;
int b = a;
System.out.println(b); // 10
long c = b;
System.out.println(c);  // 10

我们可以看到非常方便直接用就可以了,但是如果是大的数据类型向小数据类型转化的话,就需要强制类型转换(俗称强转).

int a = 100;
short b = (short) a;
System.out.println(b);  // 100

强制类型转换的语法:
(转换之后的数据类型) 变量

**小结: 小 -> 大 : 自动转换 **
大 -> 小 : 强制类型转换

1.2 数据类型之间隐式转化

运算过程中,byte short char 都会自动先转化为 int

  1. 相同数据类型之间
    相同数据类型之间计算, 得到的一定是这个数据类型
    int + int = int

  2. 不同数据类型之间
    首先, 把小的数据类型自动转换成大的数据类型, 然后再进行计算, 得到的结果一定是大的数据类型.
    int + long = long

  3. 特殊的 byte, short, char
    在计算的时候,首先会转化成 int 类型然后再进行计算,这样是安全的。
    byte + byte = int
    结果至少是 int

结论:

  1. 在执行算数运算的时候,byte, short 会自动的转化成 int 然后再进行计算。
  2. 如果不同数据类型之间进行计算,比如 int+long. 此时, 程序会自动的把 int 转化成 long,然后再进行计算. 所以结果一定是大的数据类型

恶心人的题:

short s1 = 1;
short s2 = s1 + 1; //s1 + 1 会先将s1转化为int,相加之后得到的是int 在赋值给short会报错
System.out.println(s2);

解析: 此时第 2 行代码一定会报错,因为 s1 是 short 类型,而 short 类型计算的时候会自动转换成 int 进行计算,并且,所有的数字默认都可以看做是 int 类型,默认的小数都可以看做是 double 类型,所以第二行计算的结果应该是 int 类型,把 int 类型的数据赋值给 short 类型的变量一定会报错的,此处必须要进行强制类型转换。
short s2 = (short) (s1 + 1);

2、引用数据类型

引用数据类型非常多,大致包括:
类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型
例如,String 类型就是引用类型。
简单来说,所有的非基本数据类型都是引用数据类型。
1.存储位置不同:
基本变量类型:

  • 在方法中定义的非全局基本数据类型变量的具体内容是存储在栈中的

引用变量类型:

  • 只要是引用数据类型变量,其具体内容都是存放在堆中的,而栈中存放的是其具体内容所在内存的地址
    ps:通过变量地址可以找到变量的具体内容,就如同通过房间号可以找到房间

2.传递方式
基本变量类型:

  • 在方法中定义的非全局基本数据类型变量,调用时按数值传递

引用变量类型:

  • 引用数据类型变量,调用时按引用传递

3、字符串

注意区别字符(单引号括起来的单个字符),被双引号引起来的内容都是字符串

String name = "周杰伦";
System.out.println(name);  //周杰伦

字符串可以执行加法运算. 表示字符串拼接.

String a = "你好";
String b = "赛利亚";
System.out.println(a+b); //你好赛利亚
System.out.println(1+a); //1你好

当出现非字符串和字符串进行相加的时候. 首先把非字符串自动转化成字符串然后再执行拼接操作

System.out.println("1" + 1);    # 11

转义字符: 具有特定含义的字符串
\n : 换行
\t : 制表符
. : .
\’ : ‘
\”: “

System.out.println(“你好啊, 我叫\n周润发”); //换行
 你好啊, 我叫
 周润发
System.out.println("玛丽说: \"她喜欢你\""); // 玛丽说: "她喜欢你"
System.out.println("hello\t world"); // hello    world

4、数组

数组: 具有相同数据类型的集合。int 数组装一堆 int,String 数组装一堆 String。
数组对数据类型是非常敏感的, 所以在声明数组的时候, 就需要给定数组的数据类型。
Java 的数组有几个特点:

  • 数组所有元素初始化为默认值,整型都是0,浮点型是0.0,布尔型是false
  • 数组一旦创建后,大小就不可改变。

要访问数组中的某一个元素,需要使用索引。数组索引从0开始,例如,5 个元素的数组,索引范围是0~`4。 可以修改数组中的某一个元素,使用赋值语句,例如,ns[1] = 79;。 可以用数组变量.length`获取数组长度

基本用法

  • 创建数组
String[] arr = new String[10];  // 创建一个10个格子的数组. 有10个位置装数据. 都是空的
String[] games = {"LOL", "DNF", "绝地求生", "男友4"};
String[] smallGames = new String[]{"CS", "红色警戒", "war3", "扫雷"};
int[] ns = new int[5];		//长度为5的整型数组
int[] ns = {1,2,3,4,5,6,7,8};		//长度不定的整型数组

第一种, 只开辟内存空间,但不放入数据。
第二种和第三种, 开辟 4 个空间, 并放入数据

  • 数组的使用
String[] arr = new String[5];
arr[0] = "红色警戒";
arr[1] = "war3";
arr[2] = "CS";
arr[3] = "英雄联盟";
arr[4] = "男友5";
// arr[5] = "扫雷和埋雷";  // 报错. 数组下标越界 ArrayIndexOutOfBoundsException
System.out.println(arr[0]);	//红色警戒
System.out.println(arr[1]);	//war3
System.out.println(arr[2]);	//CS
System.out.println(arr[3]);	//英雄联盟
System.out.println(arr[4]);	//男友5
System.out.println(arr[5]); // ArrayIndexOutOfBoundsException 要我说几遍. 没有第5个

数组的遍历

1.取索引的方式遍历数组

String[] arr = {"CS", "红色警戒", "war3", "扫雷"};
 for(int i = 0; i < arr.length; i++){		//数组.length就是数组的长度
     System.out.println(arr[i]);
 }

2.forEach循环,直接迭代数组的每个元素

int[] ns = { 1, 4, 9, 16, 25 };
for (int n : ns) {
    System.out.println(n);
}

注意:在for (int n : ns)循环中,变量n直接拿到ns数组的元素,而不是索引。
显然for each循环更加简洁。但是,for each循环无法拿到数组的索引,因此,到底用哪一种for循环,取决于我们的需要。

  • 打印数组内容

直接打印数组变量,得到的是数组在 JVM 中的引用地址:

int[] ns = { 1, 1, 2, 3, 5, 8 };
System.out.println(ns); // 类似 [I@7852e922

这并没有什么意义,因为我们希望打印的数组的元素内容。因此,使用for each循环来打印它:

int[] ns = { 1, 1, 2, 3, 5, 8 };
for (int n : ns) {
    System.out.print(n + ", ");
}

使用for each循环打印也很麻烦。幸好 Java 标准库提供了Arrays.toString(),可以快速打印数组内容:

import java.util.Arrays;
public class Main {
    public static void main(String[] args) {
        int[] ns = { 1, 1, 2, 3, 5, 8 };
        System.out.println(Arrays.toString(ns));	//[1, 1, 2, 3, 5, 8]
    }
}

多维数组

较常见的多维数组就是二维数组,定义一个二维数组如下:

int[][] ns = {
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 }
};
System.out.println(ns[0]);      //[I@10f87f48
int[] arr0 = ns[0];     //arr0和ns[0] 指向相同的内存区域
System.out.println(arr0);       //[I@10f87f48
ns[0][1]=20;
System.out.println(Arrays.deepToString(ns)); //[[1, 20, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
System.out.println(Arrays.toString(arr0));  //[1, 20, 3, 4]
System.out.println(arr0.length); // 4

命令行参数

Java 程序的入口是main方法,而main方法可以接受一个命令行参数,它是一个String[]数组。这个命令行参数由 JVM 接收用户输入并传给main方法:

public class Main {
    public static void main(String[] args) {
        for (String arg : args) {
            System.out.println(arg);
        }
    }
}

我们可以利用接收到的命令行参数,根据不同的参数执行不同的代码。例如,实现一个-version参数,打印程序版本号:

public class Main {
    public static void main(String[] args) {
        for (String arg : args) {
            if ("-version".equals(arg)) {
                System.out.println("v 1.0");
                break;
            }
        }
    }
}

上面这个程序必须在命令行执行,我们先编译它,会生成一个 Main.class 文件:

$ javac Main.java

然后,执行的时候,给它传递一个-version参数:

$ java Main -version
v 1.0

这样,程序就可以根据传入的命令行参数,作出不同的响应。
小结:
命令行参数类型是String[]数组,保存在 args 中,可以通过索引或遍历取值;
注意:package com.xjt.demo01; IDEA 编辑时第一行是引用的包名,如果使用命令行执行时需要注释掉,否则找不到该类。

  • 练习:

    1.倒序打印数组每个元素

int[] ns = { 1, 4, 9, 16, 25 };
// 倒序打印数组元素:
for (int i=ns.length-1;i>=0;i--) {
    System.out.println(ns[i]);
}

2.数组冒泡排序

int[] ns = { 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 };
for (int i=0;i<ns.length-1;i++){
    for (int j=i+1;j<ns.length;j++){
        if(ns[i]>ns[j]){
            int a = ns[i];
            ns[i] = ns[j];
            ns[j] = a;
        }
    }
}
System.out.println(Arrays.toString(ns));    //[8, 12, 18, 28, 36, 50, 65, 73, 89, 96]

3.判断一个数是否是质数

质数: 只能被 1 和自身整除的数

System.out.println("请输入一个数:");
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
boolean flag=true;
for (int i=2;i<num;i++){
    if(num%i == 0){
        flag=false;
        break;
    }
}
if(flag){
    System.out.println("num="+num+",是质数");
}else{
    System.out.println("num="+num+",不是质数");
}

4.数组中最大数

int[] arr = {1,55,2,17, 8,12,5};
int max = arr[0];
for (int i=1;i<arr.length;i++){
    if(max<arr[i]){
        max = arr[i];
    }
}
System.out.println("数组arr中最大数为:"+max);

4.计算数组平方和

int[] arr = {1,55,2,17, 8,12,5};
long sum=0;
for (int item:arr){
    sum+=item*item;
}
System.out.println("数组arr各项的平方和="+sum)

5.鸡兔同笼问题:用户输入脚数目 判断鸡兔的所有可能情况

Scanner sc = new Scanner(System.in);
System.out.println("请输入鸡兔脚数目:");
int foots = sc.nextInt();
for (int i=0;i<=foots/2;i++){
    //有i只鸡
    if((foots-2*i)%4 == 0){
        System.out.println("鸡有"+i+"只,兔子有"+((foots-2*i)/4)+"只");
    }
}
//请输入鸡兔脚数目:
//10
//鸡有1只,兔子有2只
//鸡有3只,兔子有1只
//鸡有5只,兔子有0只

6.水仙花数是指一个 3 位数,它的每个位上的数字的 3 次幂之和等于它本身(例如:1^3 + 5^3+ 3^3 = 153).

Scanner sc = new Scanner(System.in);
System.out.println("请输入一个三位数:");
int num = sc.nextInt();
if(num<100 || num >999){
    System.out.println("该数不是水仙花数");
}
int baiwei = num/100;
int shiwei = num%100/10;
int gewei = num%10;
if(baiwei*baiwei*baiwei+shiwei*shiwei*shiwei+gewei*gewei*gewei == num){
    System.out.println("该数是水仙花数");
}

7.命令行执行.java 文件

package com.xjt.demo01;

import java.util.Arrays;

public class Demo01 {
    public static void main(String[] args) {
        if(args.length == 0 || args[0].equals("-h")){
            System.out.println(Arrays.toString(args));
        }else if (args[0].equals("-g")){
            System.out.println("goodbey,");
            for (String s:args
                 ) {
                System.out.println(" "+s);
            }
            System.out.println("!");
        }
    }
}

javac Demo01.java
java Demo01 -h xiong taop

三、条件和循环控制语句

1.if 语句

语法 3(多分支语句):
if(条件 1){
语句块 1
} else if (条件 2){
语句块 2
} else if(条件 3){
语句块 3
}…. else {
else 语句
}
执行流程:
判断条件 1 是否成立. 如果成立. 执行语句块 1, 否则, 如果条件 2 成立, 执行语句 2, 否则, 如果条件 3 成立, 执行语句 3……如果所有条件都不成立. 执行 else 语句.

// 输入考试分数
 Scanner sc = new Scanner(System.in);
 int score = sc.nextInt();
 if(score >= 100){
     System.out.println("优秀");
 } else if (score >= 80 ){
     System.out.println("良好");
 } else if (score >= 70 ){
     System.out.println("中等");
 } else if (score >= 60 ){
     System.out.println("及格");
 } else {
     System.out.println("不及格");
 }

2.while 语句

int a = 0;
 while (a < 10){
     System.out.println("还我钱");
     a = a + 1 ;
 }

3.switch 语句

示例:
输入月份, 进行判断. 如果是 1,2,3 月. 输出第一季度, 如果是 4,5,6 输出第二季度. 以此类推输出第三季度和第四季度

Scanner sc = new Scanner(System.in);
int month = sc.nextInt();
switch (month){
     case 1:
     case 2:
     case 3:
         System.out.println("第一季度");
         break;

     case 4:
     case 5:
     case 6:
         System.out.println("第二季度");
         break;

     case 7:
     case 8:
     case 9:
         System.out.println("第三节度");
         break;

     case 10:
     case 11:
     case 12:
         System.out.println("第四季度");
         break;

     default:
         System.out.println("您输入的月份有问题. ");
         break;
 }

注意: break 表示跳出 switch. 如果不写 break 则会发生 case 穿透现象
case 穿透: 如果有一个 case 匹配成功, 则后面的 case 不会继续判断而是直接执行 case 中的语句

4.for 语句

语法:
for(语句 1; 语句 2; 语句 3){
循环体
}
解释:
语句 1: 一般初始化我们的循环变量
语句 2: 条件判断, 是否继续循环
语句 3: 一般做循环变量的改变
执行流程: 首先执行语句 1, 然后判断语句 2 是否成立,如果成立(true)执行循环体,如果不成立(false),停止循环(for 循环结束),最后执行语句 3,改变循环变量。然后继续判断语句 2 是否成立. 如果成立, 继续执行循环体 再执行语句 3….以此类推,直到语句 2 为假循环跳出。

案例: 从 0 数到 9

for(int i = 0; i < 10; i++ ){
     System.out.println(i);
 }

特殊的 for 循环

for(int i = 0; i < 10; i--){
    System.out.println(i);
}

由于 i—的存在. i 越来越小. i<10 恒成立. 所以次循环将会永远执行下去. 像这样的循环, 被称为死循环
另类的死循环

for(;;){
 	System.out.println("傻x");
 }

for each 语法

这是 for 循环的增强语法,能直接取出数组/集合中的每一项,缺点是 不能获得下标

for(类型 变量:可迭代对象){
循环体
}

示例:

5.do-while 语句

while 循环是先判断条件,条件满足继续执行循环体,
do-while 是先执行循环体,再判断条件是否满足,满足则进入下一次循环即执行循环体,判断条件。。。

do {
// 循环体
} while (条件判断);

int i = 0 ;
do{
    System.out.println(i);
    i++;
} while (i < 10);

6.break 和 continue

break:终止循环

for (int i = 0 ; i < 10; i++){
     if(i == 7){
         break;
     }
     System.out.println(i);
 }
//0 1 2 3 4 5 6

continue:终止当前的循环,进入下一个循环

for (int i = 0 ; i < 10; i++){
     if(i == 7){
         continue; // 我不喜欢7
     }
     System.out.println(i);
 }
//0 1 2 3 4 5 6 8 9

四、函数

函数也叫做方法

1.定义方法

语法:

//static:普通的静态方法 void:该方法没有 return 返回值或返回值为空
public static void 方法名(){
方法体
}
方法名() // 这里是调用方法

public static void yue() {
    System.out.println("1. 拿出手机");
    System.out.println("2. 打开陌陌");
    System.out.println("3. 找一个美眉");
    System.out.println("4. 谈谈人生理想");
    System.out.println("5. 约出来看个电影");
}
public static void count100(){
    for(int i = 1; i <= 100; i++){
        System.out.println(i);
    }
}
public static void main(String[] args) {
    yue(); // 调用上面定义好的方法
    System.out.println("爽啊...约完之后数100个数之后继续约");
    count100();
    yue(); // 调用上面定义好的方法
}

带参数的方法定义:

public static void 方法名(参数){
方法体
}
方法名(参数)

参数两类:

  1. 在方法声明的位置的括号里写的内容被称为形参. 形式上. 你在调用该方法的时候需要给一个数据. 那接收数据的话, 咱么说过, 变量是用来保存数据的. 所以, 所谓的形参, 实际上就是声明一个变量
  2. 在方法被调用的地方给方法传递的具体的信息, 这个叫实参.

形参就是一个变量声明就可以了. 表示可以接受 xxx 类型的数据
实参必须是具体的数据, 可以是变量, 也可以直接是值.

public class Func_test {
    public static void yue(String tools,String doWhat) {
        System.out.println("1. 拿出手机");
        System.out.println("2. 打开"+tools);
        System.out.println("3. 找一个美眉");
        System.out.println("4. 谈谈人生理想");
        System.out.println("5. 约出来"+doWhat);
    }
    public static void count100(){
        for(int i = 1; i <= 100; i++){
            System.out.println(i);
        }
    }
    public static void main(String[] args) {
        yue("陌陌","看个电影"); // 调用上面定义好的方法
        System.out.println("爽啊...约完之后数100个数之后继续约");
        count100();
        yue("soul","聚餐"); // 调用上面定义好的方法
    }
}

接收不定参数:

//接收不定参数的整数相加
public static int addToSum(int a,int b,int... args){
    int sum = a+b;
    for (int item:args){
        sum+=item;
    }
    return sum;
}
//主函数中调用
System.out.println(addToSum(1,2,3,4,5,6,7));    //28

方法的返回值:
带有返回值的方法:

public static 返回值类型 方法(参数){
方法体
return xxx;
}

public static int addTwoNum(int a,int b){
    int sum = a+b;
    return sum;
}
//主函数中调用
int ret = addTwoNum(1,2);
System.out.println(ret);

注意:

  1. 如果方法没有返回值, 那么在方法声明的地方必须要写 void. 不可以空着.
  2. 如果方法有返回值. 那么在方法声明的地方必须写出方法的返回值类型.
  3. 如果方法有返回值. 在方法内部必须使用 return 语句来返回数据.
  4. 方法的返回值必须和返回值类型一致.
  5. return 语句执行之后, 方法就结束了. 后面不能执行任何代码.

2.方法重载

重载:方法的名字相同,参数的个数或者类型不同。调用方法时传入不同的参数/类型 就会调用不同的执行方法;

public class TestMethod6 {
     public static void chi(String fan){
         System.out.println(fan);
     }
     public static void chi(String fan, String cai){
         System.out.println(fan);
         System.out.println(cai);
     }
     public static void chi(String fan, String cai , String tiandian){
         System.out.println(fan);
         System.out.println(cai);
         System.out.println(tiandian);
     }
     public static void main(String[] args) {
         chi("大米饭");
         chi("大米饭", "西红柿炒鸡蛋");
         chi("大米饭", "尖椒土豆丝", "哈根达斯");
     }
 }

我们发现, 在方法调用的时候. 程序会自动根据你给的参数类型和个数选择你要执行的方法.
再看一个

public class TestMethod7 {
     public static int add(int a, int b){
         System.out.println("两个int");
         return a + b;
     }
     public static long add(int a, long b){
         System.out.println("一个int, 一个long");
         return a + b;
     }
     public static double add(int a, double b){
         System.out.println("一个int, 一个double");
         return a + b;
     }
     public static void main(String[] args) {
         System.out.println(add(10, 20));
         System.out.println(add(10, 20L));
         System.out.println(add(10, 1.25));
     }
 }

五、输入输出

读取输入

前面已经看到,打印输出到’标准输出流’(即控制台窗口) 是一件非常容易的事情,只要
调用 System.out.println 即可,然而读取”标准输人流” 就没有那么简单了,首先需要构建输入流 Scanner in

Scanner in = new Scanner(System.in);
System.out.println("please input your name:");
String name = "";
if(in.hasNext()){
    name = in.nextLine();
}

System.out.println("please input your age:");
int age = in.nextInt();

System.out.println("please input your score:");
double score = in.nextDouble();

double avg = score / 3.0;
System.out.printf("student:%s,age:%d,the score avg= %.2f",name,age,avg);

image.png

格式化输出

https://www.cnblogs.com/kunlbc/p/4518977.html

System.out.println(x);
System.out.printf("%,10.2f\r\n",x);
System.out.printf("%-,10.2f\r\n",x);

//输出
-3333.3333333333335
 -3,333.33
-3,333.33

image.png

格式化输出日期时间


文件输入与输出


文章作者: CoderXiong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CoderXiong !
  目录