疯狂Java讲义 第四章 Java流程控制和数组

jefxff 153,588 2020-03-15

1. 顺序结构

  1. Java的顺序结构就是程序从上到下逐行地执行, 中间没有任何判断和跳转;
  2. 如果main方法的多行代码之间没有任何流控制, 则程序总是从上到下依次执行, 排在前面你的代码先执行,排在后面的代码后执行, 这意味着: **如果没有流程控制, Java方法里面的语句是一个顺序执行流, 从上到下依次执行每条语句.**

2. 分支结构

2.1 if 条件语句

if语句使用布尔表达式或布尔值作为分支条件来进行分支控制, 有以下三种情形:


    // 第一种形式
    if(logic expression)
    {
        // statement...
    }
    // 第二种形式
    if(logic expression)
    {
        // statement...
    }
    else
    {
        // statement...
    }
    // 第三种形式
    if(logic expression)
    {
        // statement...
    }
    else if(logic expression)
    {
        // statement...
    }
    // ...   //可以有0个或者多个 else if 语句
    else  // 最后else语句也可以省略
    {
        // statement...
    }
三种形式的说明
  1. 上述条件语句中, if(logic expression), else if(logic expression), else 后花括号括起来的多行代码被称之为代码块, 一个代码块通常被当做一个整体来执行(除非运行过程中遇到 return, break, continue等关键字,或者遇到了异常), 因此这个**代码块也被称为条件执行体**. (例子1)
  2. 如果 if(logic expression), else if(logic expression), else 后的代码只有一行语句时, 可以省略花括号, 因为单行语句本身就是一个整体, 无需用花括号来把他们定义成一个整体!
  3. if 语句容易出现逻辑错误的问题, 不是语法错误, (例子2)
if 语句注意问题
  1. else 的隐含条件是对前面条件取反, 当第一个条件始终满足的情况下, else 的语句判断的结果就始终为false
  2. 记住: 总是优先把包含范围小的条件放在前面处理, 例如: age>60 和 age>20 两个条件, 明显age>60的范围更小, 所以应该先处理 age>60 的情况.(例子3)
if 语句例子
    // 例子1:
    public class ifTest
    {
        public static void main(String[] args)
        {
            int age = 18;
            if (age > 20)
            {
                System.out.println("年龄已经大于20岁了");
                System.out.println("大于20岁的人应该学会自律, 承担更多的责任...");
            }
        }
    }


    // 例子2:
    // 下面的if语句中 if(age > 20)的判断始终为true, 导致后面的else判断始终为false;
    public class ifErrorTest
    {
        public static void main(String[] args)
        {
            int age = 45;
            if (age > 20)
            {
                System.out.println("青年人");  // 执行程序始终输出 "青年人"
            }
            else if (age > 40)
            {
                System.out.println("中年人");
            }
            else if (age > 60)
            {
                System.out.println("老年人");
            }
        }
    }


    // 例子3
    // 改写上面的例子2, 解决错误:
    public class ifErrorTest
    {
        public static void main(String[] args)
        {
            int age = 45;
            if (age <= 20)
            {
                ystem.out.println("小朋友!"); 
            }
            else if (age > 20 && age<=40)
            {
                System.out.println("青年人");
            }
            else if (age > 40 && age <= 60)
            {
                System.out.println("中年人");
            }
            else if (age > 60)
            {
                System.out.println("老年人");
            }
        }
    }

2.2 switch分支语句

switch 语句由一个控制表达式和多个 case 标签组成, 和 if 语句不同的是, switch 语句后面的控制表达式的**数据类型只能是byte, short, char, int四种整数类型, 枚举类型和java.lang.String类型(从java7才允许), 不可以是boolean类型**

语法格式
    switch (expression)
    {
        case condition1:
        {
            statement(s)
            break;
        }
        case condition2:
        {
            statement(s)
            break;
        }
        ...
        case conditionN:
        {
            statement(s)
            break;
        }
        default:
        {
            statement(s)
        }
    }
语法格式说明
  1. 上述这种分支语句的执行是先对 expression 求值, 然后依次匹配condition1, condition2, ..., confitionN等值, 遇到匹配的值即执行对应的执行体; 不再判断与后面 case, default标签的条件是否匹配;
  2. 如果所有的 case 标签后的值都不与 expression 表达式的值相等, 则执行 default 标签后的代码块;
  3. 和if语句不同的是, switch 语句中各 case 标签后的代码块的开始点和结束点都非常清晰, 因此完全可以省略case后代码块的花括号;
  4. switch 语句中的 default 标签看似没有条件, 其实是有条件的, 条件是 expression 表达式的值不能和前面任何一个 case 标签后的值相等;
  5. Java7 增强了switch语句的功能, 允许switch语句的控制表达式是java.lang.String类型的变量或表达式---只能是java.lang.String类型, 不能是StringBuffer 或 StringBuilder 这两种字符串类型.
    // switch 语法例子:
    public class SwitchTest
    {
        public static void main(String[] args)
        {
            /*
             * 下面的代码示例了基本的switch语句的基本语法
             * 再记一遍, switch语句后expression表达式的数据类型只能是四种整型 加枚举类型 加String类型
             */
            // 声明 char 类型变量score, 并为其赋值为'C'
            char score = 'C';
            switch (score)
            {
            case 'A':
                System.out.println("优秀");
                break;
            case 'B':
                System.out.println("良好");
                break;
            case 'C':
                System.out.println("中");
                break;
            case 'D':
                System.out.println("及格");
                break;
            case 'E':
                System.out.println("不及格");
                break;
            default:
                System.out.println("成绩输入有误");
                break;
            }

            /*
             * 下面的代码示例了java.lang.String类型语法
             */
            String season = "夏天";
            switch (season)
            {
                case "春天":
                    System.out.println("春暖花开!");
                    break;
                case "夏天":
                    System.out.println("夏日炎炎!");
                    break;
                case "秋天":
                    System.out.println("秋高气爽!");
                    break;
                case "冬天":
                    System.out.println("冬雪皑皑!");
                    break;
                default:
                    System.out.println("季节输入有误! ");
            }
        }
    }

3. 循环结构

  1. 循环语句可以在满足循环条件的情况下, 反复执行一段代码, 这段被重复执行的代码被称为循环体;
  2. 当反复执行这个循环体的时候, 需要在合适的时候把循环条件改为假, 从而结束循环, 否则循环将一直执行下去, 形成死循环.

循环语句组成

  1. 初始化语句(init_statement):一条或多条语句, 这些语句用于完成一些初始化工作, 初始化语句在循环执行前执行;
  2. 循环条件(test_expression): 这是一个boolean表达式, 这个表达式能决定是否执行循环体;
  3. 循环体(body_statement): 这个部分是循环的主体, 如果循环条件允许, 这个代码块将被重复执行;
  4. 迭代语句(iteration_statement): 这个部分在一次循环体执行结束后, 对循环条件求值之前执行, 通常用于控制循条件中的变量, 使得循环在合适的时候结束;

3.1 while 循环语句

while语法格式
    [init_satement]
    while(test_expression)
    {
        statement;
        [iteration_statement]
    }
while语法格式说明
  1. while循环每次执行循环体之前, 先对 test_expression 循环条件求值,如果循环条件为 true,则执行循环体部分
  2. 迭代语句 iteration_statement 总是位于循环体的最后, 因此只有当循环体能成功执行完成时, while循环才会执行 iteration_statement 迭代语句.
  3. while 循环也可被当成条件语句, 如果 test_expression 条件表达式一开始就为 fasle, 则循环体部分将永远不会获得执行.
    //while 循环例子:
    public class WhileTest
    {
        public static void main(String[] args)
        {
            // 一个简单的while循环程序
            int count = 0;
            while (count < 10)
            {
                System.out.println("这是第 " + count + " 次循环!");
                count++;
            }
            System.out.println("while循环示例结束!");
        }
    }
注意问题
  1. 使用while循环的时候, 一定要保证循环条件有变成false的时候, 否则这个循环就是一个死循环.
  2. 记清楚上面的语法格式, while(test_expression) 循环条件后面是没有分号(;)的, 如果加了分号, 就表明循环体是一个空语句(也就是这个多余的分号), 空语句作为循环体的时候, 循环条件的返回值没有任何改变, 这就成了一个死循环. 也导致分号后面的代码和循环没有任何关系.

3.2 do while 循环语句

do while 语法格式
    [init_statement]
    do
    {
        statement;
            [iteration_statement]
    }while (test_expression);
do while 和 while 的区别
  1. while循环是先判断循环条件, 如果条件为真则执行循环体;
  2. do while 循环则先执行循环体,(即使循环条件为false, 循环体也会执行一次,) 然后才判断循环条件,如果循环条件为真, 则执行下一次循环, 否则中止循环.
  3. 由于 do while 最后判断循环条件, 所以 do while 循环条件后面要加一个分号(;)
    // do while 例子:
    public class DoWhileTest
    {
        public static void main(String[] args)
        {
            // 一个简单的 do while循环程序
            int count = 1;
            do 
            {
                System.out.println("这是第 " + count + " 次循环!");
                count++;
            }while (count < 10);
            System.out.println("Do while循环示例结束!");
        }
    }

3.3 for 循环

for循环语法格式
    for ([init_statement]; [test_expression]; [iteration_statement])
    {
        statement
    }
for循环语法格式说明
  1. 程序执行for循环时, 先执行循环的初始化语句 init_statement, 初始化语句只在循环开始前执行一次; 每次执行循环体之前, 先计算 test_expression 循环条件的值, 如果循环条件返回true, 则执行循环体, 循环体执行结束后执行循环迭代语句,
  2. for循环, 循环条件总是比循环体要多执行一次, 是因为最后一次执行循环条件返回false, 将不再执行循环体.
    // for循环例子1:
    public class ForTest1
    {
        public static void main(String[] args)
        {
            // 一个简单的 for 循环, 初始化语句只有一个, 循环条件也只是一个boolean表达式
            // 循环的初始条件, 循环条件, 循环迭代语句都在下面一行
            for (int count = 0; count < 10; count++)
            {
                System.out.println("这是第 " + count + " 次循环!"); 
            }
            System.out.println("for 循环示例结束!");
        }
    }

    // for循环例子2:
    public class ForTest2
    {
        public static void main(String[] args)
        {
            // 复杂点的for循环, 初始化3个变量, 
            for (int b = 0, s = 0, p = 0; b<10 && s<4 && p<10; p++)
            {
                System.out.println("b: " + b++); 
                System.out.println( "s: " + ++s + "  p: " + p);
            }
            System.out.println(" for 循环示例2 结束!");
        }
    }
    // out: b: 0
    //         s: 1  p: 0
    //         b: 1
    //         s: 2  p: 1
    //         b: 2
    //         s: 3  p: 2
    //         b: 3
    //         s: 4  p: 3
    //         for 循环示例2 结束!

    // 例子3:(错误示例, 这是一个死循环)
    // 程序是一个死循环, 将一直输出 "-----"
    for( ; ; )
    {
        System.out.println("-----");
    }

    //例子4:
    // 把for循环的初始化条件提出来单独定义
    int count = 0;
    // for 循环只放循环条件
    for (; count < 10;  )
    {
        System.out.println("这是第 " + count + " 次循环!");
        // 把循环迭代语句放在循环体之后定义
        count++;  
    }

    // 例子5:
    int tmp = 0;
    for (int i=0; i<10; i++)
    {
        System.out.println("这是第 " + count + " 次循环!");
        tmp = i;
    }
    // 可以通过 tmp 来访问 i 变量的值
    System.out.println(tmp);
    // 说明:
    //   使用这种方法, 使得变量 i 和变量 tmp 的责任更加清晰, 反之, 使用前一种方
    //   法, 则变量 i 的作用域被扩大了, 功能也被扩大了; 扩大的后果就是: 如果该方法还有另
    //   外一个循环也需要定义循环变量, 则不能再次使用 i 变量作为循环变量.

示例代码说明及注意问题
  1. 上面的(例子2)中初始化变量有三个, 但是只能有一个声明语句, 因此如果需要在初始化表达式中声明多个变量, 那么这些变量应该具有相同的数据类型.
  2. 不要在循环体内修改循环变量(也叫循环计数器)的值, 否则会增加程序出错的可能性, 万一程序真的需要访问, 修改循环变量的值, 那就重新定义一个临时变量, 先将循环变量的值赋给临时变量, 然后对临时变量的值进行修改(例子5)
  3. for循环圆括号中的两个分号(;)是必需的, 初始化语句, 循环变量, 迭代语句部分都可以省略, 如果省略了循环条件, 则这个循环条件默认为true, 将会产生一个死循环.(例子3)
  4. 还可以把for循环的初始化条件定义在循环体之外, 把循环迭代语句放在循环体内, 这样的for循环非常类似与while循环. 但是循环体部分使用 continue 语句来结束本次循环的话, 将会导致循环迭代语句得不到执行.(例子4)
  5. 将for循环的初始化语句放在循环之前定义还有一个作用, 就是可以扩大初始话语句中定义变量的作用域,在for循环里面定义的变量, 其作用域仅在该循环内有效, for循环终止之后, 这些变量就不可以访问了,如果想要在for循环之外的地方使用这些变量, 那就将初始化语句在循环体之前定义; 或者额外定义一个变量来保存这个循环变量的值.(例子5)

3.4 循环嵌套

循环嵌套理解
  1. 自我理解就是, 循环中套着循环, 可以是for中套着while, 也可以是for中套着while, 也可以是for中套着for或者while中套着for或者while等等
  2. 嵌套循环执行的次数, 是各层循环体执行次数的乘积
  3. 无论嵌套多少层, 都可以**把内层循环当成外层循环的循环体来对待**
    // 循环嵌套例子:
    // Java代码写正序九九乘法表, 使用两层for循环
    public class NestedLoopTest1
    {
        public static void main(String[] args)
        {
            // 正序输出九九乘法表
            for (int i = 1; i < 10; i++)
            {
                for (int j = 1; j <= i; j++)
                {
                    // print 不换行输出; println 换行输出
                    System.out.print(j + " * " + i + " = " + j*i + "    ");
                }
                System.out.print("\n");
            }
        }
    }
    // Java代码写倒序九九乘法表, 使用两层for循环
    public class NestedLoopTest2
    {
        public static void main(String[] args)
        {
            // 倒序输出九九乘法表
            for (int i = 9; i >= 1; i--)
            {
                for (int j = i; j > 0 && j <= i; j--)
                {
                    // print 不换行输出; println 换行输出
                    System.out.print(i + " * " + j + " = " + j*i + "    ");
                }
                System.out.print("\n");
            }
        }
    }

4. 控制循环语句

4.1 break 语句

break 用于完全跳出一个循环, 跳出当前的循环体. 不管是那种循环, 一旦在循环中遇到break, 系统将完全结束该循环, 开始执行循环之后的代码.

break语句注意问题
  1. break 语句不仅可以结束其所在的循环, 还可以直接结束外层循环. 此时需要在 break 后紧跟一个标签, 这个标签用于表示一个外层循环;
  2. Java标签: Java 中的标签就是一个紧跟着英文冒号(:)的标识符, Java中标签只有放在循环语句之前才起作用
  3. break 后的标签必须是一个有效标签, 即这个标签必须在 break 语句所在的循环之前定义, 或者在其所在循环的外层循环之前定义
    // break语句结束外层循环的例子:
    public class BreakTest
    {
        public static void main(String[] args)
        {
            // 外层循环, outer: 作为标识符, 也就是Java标签
            outer:
            for(int i = 0; i < 5; i++)
            {
                for (int j = 0; j < 3; j++)
                {
                    System.out.println("i的值为: " + i + "  j的值为: " + j);
                    if (j == 2)
                    {
                        // 跳出 outer的标签所标识的循环
                        break outer;
                    }
                }
            }
        }
    }

4.2 continue 语句

continue 的功能和break有点类似, 区别是continue只是忽略本次循环剩下的语句, 接着开始下一次循环,并不会终止循环; 而break则完全终止循环本身; continue语句理解就是忽略本次循环剩下的语句

continue 语句注意问题
  1. 与 break 类似, continue 后面也可以紧跟一个标签, 用于直接跳过标签所标识循环的当次循环的剩下语句, 重新开始下一次循环
  2. continue后面的标签也可以有一个必须是有效的标签, 即这个标签通常应该放在continue所在循环的外层循环之前定义
    // continue语句例子:
    public class ContinueTest
    {
        public static void main(String[] args)
        {
            for (int  i < 0; i < 3; i++)
            {
                System.out.println("i的值是: " + i);
                if (i == 1)
                {
                    // 忽略本次循环剩下的语句
                    continue;
                }
                System.out.println("continue后的输出语句");
            }
        }
    }

4.3 return 返回值

  1. return关键字的功能是结束一个方法, 当一个方法执行到一个return语句时(return关键后面还可以跟变量,常量和表达式), 这个方法将被结束
  2. Java程序中大部分循环都放在方法中执行, 一旦在循环体内执行到一个return语句, return语句就会结束该方法(整个方法, 不管return处于多少层循环中), 循环也就结束了.
    // return 语句例子:
    public class ReturnTest
    {
        public static void main(String[] args)
        {
            for (int i = 0; i < 3; i++)
            {
                System.out.println("i的值是 " + i);
                if ( i == 1)
                {
                    return;
                }
                System.out.println("return 后面输出的语句!");
            }
        }
    }

5. 数组

5.1 Java数组的含义

  1. Java数组要求所有的数组元素具有相同的数据类型, 即一个数组里只能存储一种数据类型的数据, 而不能存储多种类型的数据.
  2. 一但数组的初始化完成, 数组在内存中所占的空间将被固定下来, 因此数组的长度将不可改变. 即使把某个数组元素的数据清空, 但它所占的内存空间依然被保留, 依然属于该数组, 数组的长度依然不变.
  3. Java的数组既可以存储基本类型的数据, 也可以存储引用类型的数据, 只要存储该数据元素具有相同类型即可.
  4. 数组是一种数据类型,可用于存储多个数据,每个数组元素存放一个数据,通常可通过数组元素的索引来访问数组元素, 包括为数组元素赋值和取出数组元素的值.

5.2 定义数组

定义数组的格式
    type[] arrayName;   // 记住这一种方式, 平时使用的是这一种
    // type arrayName[];
数组格式说明
  1. 记住第一种方式, 即 type[] arrayName 这种形式, 因为语意及可读性更好
  2. type 和 type[] 是两类型, type 是基本类型, type[] 是引用类型
  3. 数组是一种引用的类型变量, 使用数组定义一个变量时, 仅仅表示定义了一个引用变量(也就是定义了一个指针), 这个引用变量还未指向任何有效内存, 因此定义数组时不能指定数组的长度
  4. 由于定义数组的时候只是定义了一个引用变量, 并未指向任何有效内存空间, 所以还没有内存空间来存储元素数组, 因此这个数组还不能使用, 需要对数组初始化后才可以使用.

5.3 初始化数组

初始化, 就是为数组的数组元素分配内存空间, 并为每个数组元素赋值, 分为静态初始化,动态初始化两种


5.4 静态初始化数组

初始化时由程序员显式指定每个数组元素的值, 由系统决定数组长度(定值不定长)

语法格式
    arrayName = new type[]{element1, element2, element3, element4 ...}
    // 也可以定义数组及初始化数组一起执行
    type[] arrayName = new type[]{element1, element2, element3, element4 ...}
    // 也可以简化为:
    type[] arrayName = {element1, element2, element3, element4 ...}
格式说明

上面的格式语法中, 前面的 type 就是数组元素的数据类型, 此处的 type 必须与定义数组变量时所使用的 type 相同, 也可以是定义数组时所指定的 type 的子类, 并使用花括把所有的数组元素括起来多个数据元素使用逗号(,)隔开, 定义初始化值的花括号紧跟[]之后.

// 静态初始化数组的例子:
// 定义一个 int 数组类型的变量, 变量名为intArr
int[] intAr;
// 使用静态初始化, 初始化数组时只指定数组元素的初始值, 不指定数组的长度
intArr = new int[]{5, 6, 8, 20};

// 定义一个object数组类型的变量, 变量objArr
Object[] objArr;
// 使用静态初始化, 初始化数组时数组元素的类型是定义数组时所指定的数组元素类型的子类
// String 类型是 Object 类型的子类, 即字符串是一种特殊的 Object 实例
objArr = new String[]{"Java", "python"};

Object[] objArr2;
// 使用静态初始化,
objArr2 = new Object[]{"Java", "python"};

// 使用简化初始化格式:
// 数组的定义和初始化同时完成, 使用简化的静态初始化写法
int[] a = {5, 6, 7, 9};

5.5 动态初始化数组

初始化时程序员只指定数组的长度, 由系统为数组元素分配初始值(定长不定值)

语法格式
    arrayName = new type[length];
    // 也可以简化(定义及初始化一起完成):
    type[] arrayName = new type[int类型的数值]
格式说明
  1. 上面的语法格式需要指定一个int类型的length参数, 这个参数决定了数组的长度, 也就是可以容纳数组元素的个数.此处的type必须与定义数组变量时所使用的type相同, 或者是定义数组时所指定的type的子类
  2. 动态初始化时, 程序只需要指定数组的长度, 即为每个数组元素指定所需的内存空间, 系统将负责为这些数组元素分配初始值
    • 数组元素的类型是基本类型中的整数类型(byte, short, int, long), 则数组元素的值是0
    • 数组元素的类型是基本类型中的浮点类型(float, double), 则数组元素的值是0.0
    • 数组元素的类型是基本类型中的字符类型(char), 则数组元素的值是'\u0000'
    • 数组元素的类型是基本类型中的布尔类型(boolean), 则数组元素的值是false
    • 数组元素的类型是引用类型(类, 接口, 数组), 则元素的值是null
    //例子:
    // 数组的定义个初始化同时完成, 使用动态初始化语法
    int[] prices = new int[5];
    // 数组的定义及初始化同时完成, //初始化数组时数组元素的类型是定义数组时所指定的数组元素类型的子类
    Object[] books = new String[4];

5.6 使用数组

  1. 使用数组包括对数组进行赋值和取出数组元素的值
  2. 使用 "数组引用变量[索引]" 这种方式来访问数组元素
  3. 访问到数组元素后, 就可以把这个数组元素当成普通变量来操作了
注意问题
  1. 如果访问数组元素时指定的索引值小于0, 或者大于等于数组的长度, 编译的时候不会报错, 但是运行的时候会出现异常: java.lang.ArrayIndexOutOfBoundsException:N(数组索引越界异常), 异常后面的" N "就是试图访问的数组的索引.
  2. 所有的数组都有 length 属性, 通过这个属性可以获取数组的长度
  3. 初始化一个数组的时候, 就相当于同时初始化了多个相同类型的变量, 通过数组元素的索引就可以自由的访问这些变量(实际上都是数组元素).
    // 使用数组例子: 
    // 1. 静态定义及初始化一个数组
    Object[] objArr1 = {"java", "python", "html", "CSS", "JS"};
    // 访问 objArr 数组的第一个元素的值
    System.out.println(objArr1[0]);  // out: java
    // 修改 objArr 数组的第一个元素的值
    objArr1[0] = "JAVA";
    System.out.println(objArr1[0]);  // out: JAVA
    // 2. 动态定义及初始化一个数组
    int[] objArr2 = new int[5];
    System.out.println(objArr2[2]);  // out: 0
    objArr2[0] = 9527;
    objArr2[1] = 9070;
    System.out.println(objArr2[0]);  // out: 9527
    System.out.println(objArr2[1]);  // out: 9070
    System.out.println(objArr2[2]);  // out: 0
    // 3. for循环遍历输出每个数组元素, 调用数组的length属性
    // 3.1 使用静态方法定义及初始化一个 int 数组
    int[] intArr = {1, 19, 36, 100, 9070, 1991887, 2019, 88, 2};
    // 3.2 for循环遍历
    for(int i = 0; i < intArr.length; i++)
    {
        System.out.print(intArr[i] + " ");  // out: 1 19 36 100 9070 1991887 2019 88 2
    }
    // 使用数组的length属性获取数组的长度
    System.out.println(intArr.length);      // out: 9

foreach循环

foreach 循环遍历数组和集合比上面使用for循环和length属性更加简洁. 使用 foreach 循环遍历数组和集合时, 无需获取数组和集合的长度, (无需循环条件, 无需循环迭代语句, 这部分由系统来完成) 无需根据索引来访问数组元素和集合元素, foreach循环自动遍历数组和集合的每个元素.

foreach 语法格式
    for (type variableName : array | collection)
    {
        // variableName 自动迭代访问每个元素
    }
    // foreach 语法例子:
    // 静态定义及初始化一个数组
    String[] strArrs = {"java", "python", "html", "CSS", "JS", "mysql"};
    // 使用foreach循环来遍历数组元素
    // 其中 strArr 将会自动迭代每个数组元素
    for(String strArr : strArrs)
    {
        System.out.print(strArr + " ");     // out: java python html CSS JS mysql
    }

6. 深入理解数组

6.1 内存中的数组

  1. 数组引用变量只是一个引用, 只有当该引用指向有效内存之后, 才可以通过该数组变量来访问数组元素, 也就是说如果在程序中访问数组对象本身, 则只能通过这个数组的引用变量来访问它
  2. 实际数组对象被存储在堆(heap)内存中, 如果引用该数组对象的数组引用变量时一个局部变量, 那么他被存储在栈(stack)内存中.
  3. 所有在方法中定义的局部变量都是放在栈内存中
  4. 如果对内存中的数组不再有任何引用变量指向自己, 则这个数组将成为垃圾, 该数组所占的内存将会被系统的垃圾回收机制回收, 所以可以将该数组的变量赋给 null 来使系统回收该数组所占的堆内存
  5. 只要类型相互兼容, 就可以让一个数组变量指向另外一个实际的数组, 这种操作会误让人感觉数组长度可变.
    // 例子:
    // 使用静态方法定义并初始化一个数组 a
    int[] a = {1, 2, 3, 4};
    // 使用动态方法定义并初始化一个数组 b
    int[] b = new int[5];
    // 输出 a 和 b 的length属性值
    System.out.println("a数组的长度为: " + a.length);   // out: a数组的长度为: 4
    System.out.println("b数组的长度为: " + b.length);   // out: b数组的长度为: 5
    // 因为 a 和 b 都是int类型, 所以可以将 a 赋值给 b
    // 实际上, 也就是让 b 的指针指向 a 引用指向的数组
    b = a;
    // 再次输出 a 和 b 的length属性值
    System.out.println("a数组的长度为: " + a.length);   // out: a数组的长度为: 4
    System.out.println("b数组的长度为: " + b.length);   // out: b数组的长度为: 4
程序说明
  1. 上面的代码两次输出b的长度不一样, 并不是长度可变; 而是引用变量改变了而已;
  2. 程序定义并初始化一个数组的时, 系统内存中实际上产生了2个内存区, 其中栈内存有一个引用变量a, 堆内存中有一块内存区, 存储a变量引用所指向的数组本身.

6.2 基本类型数组的初始化

  1. 基本数据类型数组元素的值直接存储在对应的数组元素中, 初始化数组时, 先为该数组分配内存空间, 然后直接将数组元素的值存入对应数组元素中.
    // 基本类型数组例子:
    // 定义一个 int 类型的数组变量
    int[] intArrs;
    // 动态初始化数组, 数组长度为 5
    intArrs = new int[5];
    // 使用for循环为每个数组元素赋值
    for(int i = 0; i < intArrs.length; i++)
    {
        intArrs[i] = i + 10;
    }
    // 使用 foreach 方式来访问数组元素
    for(int intArr : intArrs)
    {
        System.out.print(intArr + " " );   //out: 10 11 12 13 14
    }

6.3 引用类型数组的初始化

  1. 引用类型数组的数组元素是引用, 每个数组元素里存储的还是引用, 它指向另一块内存, 这块内存里存储了有效数据.
    // 引用类型数组例子:
    // 定义一个person类
    class Person
    {
        public int age;
        public double height;
        public void info()
        {
            System.out.println("我的年龄是: "+ age + ", 我的身高是: " + height);
        }
    }
    // 定义一个Person[] 数组,并动态初始化数组, 为数组指定值
    public class ReferenceArrayTest
    {
        public static void main(String[] args)
        {
            // 定义一个students数组变量, 其类型是 Person[]
            Person[] students;
            // 执行动态初始化, 为数组指定长度
            students = new Person[2];
            // 创建一个Person实例, 并将这个Person实例赋值给 zhang 变量
            Person zhang = new Person();
            // 为zhang所引用的Person对象的 age, height 赋值
            zhang.age = 18;
            zhang.height = 170;
            // 创建一个Person实例, 并将这个Person实例赋值给 lee 变量
            Person lee = new Person();
            // 为lee所引用的Person对象的 age, height 赋值
            lee.age = 16;
            lee.height = 168;
            // 将zhang变量的赋值给第一个数组元素
            students[0] = zhang;
            // 将lee变量的赋值给第二个数组元素
            students[1] = lee;
            // 下面两行代码的结果完全一样, 因为lee和students[1]指向的是同一个Person实例
            lee.info();
            students[1].info();
        }
    }

6.4 没有多维数组

  1. Java语言支持多维数组的语法, 但是没有多维数组---如果从底层的运行机制上来讲的话.
  2. Java语言里的数组类型是引用类型, 因此数组变量其实就是一个引用, 这个引用指向真实的数组内存. 数组元素的类型也可以是引用, 如果数组元素的引用再次指向真实的数组内存, 这种情形看上去很像多维数组.

二维数组语法

语法格式

    type[][] arrName;

语法格式说明

  1. Java语言采用上面的语法格式定义二维数组, 但它实质上还是一维数组, 只是其数组元素也是引用, 数组元素里保存的引用指向一维数组.

二维数组初始化

    // (指定最左边维的大小)
    arrName = new type[length][]
    // (指定每一维的大小)
    arrName = new type[length][length]
    // 上面的初始化相当于初始化了一个一维数组, 这个一维数组的长度是length, 因为这
    //  个一维数组的数组元素也是一个引用类型(数组类型)的, 所以系统为每个数组元素都分
    //  配初始值: null


扩展多维

  1. 在Java中想实现无限扩展的数组, 可以定义一个Object[]类型的数组, 这个数组的元素是Object类型, 因此可以再次指向一个Object[]类型的数组, 从而实现从一维扩展到二维, 多维数组的功能
    // 例子:
    // 1. 示范如何将二维数组当成一维数组处理
    // 定义一个二维数组
    int[][] a;
    // 把a当成一维数组进行初始化, 初始化 a 是一个长度为 4 的数组
    // a 数组的数组元素又是引用类型
    a = new int[4][];
    // 把a当成一维数组, 遍历 a 数组的每个数组元素
    for(int i = 0, len = a.length; i < len; i++)
    {
        System.out.println(a[i]);   // out: 4个 null (因为引用类型, 默认值是null)
    }
    // 初始化 a 数组的第一个元素
    a[0] = new int[2]; 
    // 指定 a 数组的第一个元素所指数组的第二个元素的元素值为6
    a[0][1] = 6;
    // a 数组的第一个元素时一个一维数组, 遍历这个数组
    for (int i = 0, len = a[0].length; i < len; i++)
    {
        System.out.println(a[0][i]);   // out: 0, 6 
    }
    // 2. 动态方法同时定义并初始化二维数组的两个维数
    int [][] b = new int[3][4];
    // 3. 静态方法定义并初始化一个二维数组
    String[][] str1 = new String[][]{new String[3], new String[]{"hello"}};
    // 使用简化的静态方法初始化二维数组
    String[][] str2 = {new string[3], new String[]{"hello"}};

Java8 增强的工具类: Arrays(结合下面的例子理解)

Arrays类包含的static修饰方法:
  1. int binarySearch(type[] a, type key)

    • 使用二分法查询key元素值在a数组中出现的索引; 如果a数组不包含key元素值, 则返回负数. 调用该方法时要求数组元素已经按升序排列,(二分法查找的前提是有序数列), 这样才能得到正确的结果
  2. int binarySearch(type[] a, int fromIndex, int toIndex, type key)

    • 这个方法与上一个方法类似, 但它只搜索a数组中 fromIndex 到 toIndex 索引的元素, 调用该方法时要求数组元素已经按升序排列
  3. type[] copyOf(type[] original, int length)

    • 这个方法将会把original数组复制成一个新数组, 其中length时新数组的长度, 如果length小于original数组的长度, 则新数组就是源数组的前面length个元素; 如果length大于original数组的长度, 则新数组的前面元素就是原数组的所有元素, 后面补充0(数值类型), false(布尔类型)或者null(引用类型)
  4. type[] copyOfRange(type[] original, int from, int to)

    • 和上面方法类似, 区别在于,这个方法之复制original数组的from索引到to索引的元素
  5. boolean equals(type[] a, type[] a2)

    • 如果a数组和a2数组的长度相等, 而且a数组和a2数组的数组元素也一一相同, 则返回 true
  6. void fill(type[] a, type val)

    • 该方法将会把a数组的所有元素都赋值为 val
  7. void fill(type[] a, int fromIndex, int toIndex, type val)

    • 该方法与前一个方法作用相同, 区别只是该方法仅仅将数组中 fromIndex 到 toIndex 索引的数组元素赋值为 val
  8. void sort(type[] a)

    • 该方法对a数组的数组元素进行排序
  9. void sort(type[] a, int fromIndex, int toIndex)

    • 与上面方法类似, 区别在于该方法仅仅将a数组的fromIndex 到 toIndex索引的元素进行排序
  10. String toString(type[] a)

    • 该方法将一个数组转换成一个字符串, 该方法按顺序把多个数组元素连缀在一起, 多个数组元素使用英文逗号(,)和空格隔开
    例子:
    public class ArraysTest
    {
        public static void main(String[] args)
        {
            /// 定义一个a数组
            int[] a = new int[]{3, 4, 5, 6, 7};
            // 定义一个 a2 数组
            int[] a2 = new int[]{3, 4, 5, 6, 7};
            // a数组和a2数组的长度相等, 每个元素依次相等, 将输出true
            System.out.println("a数组和a2数组是否相等: " + Arrays.equals(a, a2));
            // 通过复制a数组, 生成一个新的b数组
            int[] b = Arrays.copyOf(a, 6);
            System.out.println("a数组和b数组是否相等: " + Arrays.equals(a, b));
            // 输出b数组的元素, 将输出[3, 4, 5, 6, 7, 0]
            System.out.println("b数组的元素为: " + Arrays.toString(b));
            // 将b数组的第3个元素(包含)到第5个元素(不包含)赋值为1
            Arrays.fill(b, 2, 4, 1);
            // 输出b数组的元素, 将输出[3, 4, 1, 1, 7, 0]
            System.out.println("b数组的元素为: " + Arrays.toString(b));
            // 对b数组进行排序
            Arrays.sort(b);
            // 输出b数组的元素, 
            System.out.println("b数组的元素为: " + Arrays.toString(b));
        }
    }

Java8为Arrarys类增加的工具方法

  1. void parallelPrefix(xxx[] array, XxxBinaryOperator op)

    • 该方法使用op参数指定的计算公式计算的都到的结果作为新元素, op计算公式包含 left, right两个形参, 其中left代表数组中前一个索引处的元素, right 代表数组中当前索引处的元素, 当计算第一个欣数组元素时, left的值默认为1
  2. void parallePrefix(xxx[] array, int fromIndex, int toIndex, XxxBinaryOperator op)

    • 该方法和上面的方法相似, 区别在于该方法仅重新计算 formIndex 到 toIndex索引的元素
  3. void setAll(xxx[] array, IntToXxxFunction generator)

    • 该方法使用指定的生成器(generator)为所有的数组元素设置值,该生成器控制数组元素的值生成算法
  4. void parallelSetAll(xxx[] array, IntToXxxFunction generator)

    • 该方法和上面的方法相似, 只是该方法增加了并行功能, 可以利用多CPU并行来提高性能
  5. void parallelSort(xxx[] a)

    • 该方法的功能与Arrays类以前就有的sort()防范相似, 只是增加了并行功能, 可以利用多CPU并行来提高性能
  6. void parallelSort(xxx[] a, int fromIndex, int toIndex)

    • 该方法与前一个方法作用类似, 区别是该方法仅对fromIndex 到 toIndex索引的元素进行排序
  7. Spliterator.OfXxx spliterator(xxx[] array)

    • 该方法将数组的所有元素转换成对应的 Spliterator 对象
  8. Spliterator.OfXxx spliterator(xxx[] array, int startInclusive, int endExclusive)

    • 该方法与前一个方法类似, 区别是该方法仅对fromIndex 到 toIndex 索引的元素进行排序
  9. XxxStream stream(xxx[] array)

    • 该方法将数组转换成Stream, Stream 是Java8新增的流式变成的API
  10. XxxStream stream(xxx[] array, int startInclusive, int endExclusive)

    • 该方法与上一个方法相似, 区别是该方法仅将fromIndex 到 toIndex 索引的元素转换成Stream
说明
  1. 上述方法中所有以parallel开头的方法都表示该方法可利用CPU并行的能力来提高性能,
  2. xxx代表不同的数据类型, 比如处理int[]型数组对应将xxx换成int, 处理long[]型数组时应将xxx换成 long
    // 例子:(先了解, 往后学完基础再来看)
    public class ArraysTest2
    {
        public static void main(String[] args)
        {
            int[] arr1 = new int[]{3, -4, 25, 16, 30, 18};
            //对数组arr1进行排序
            Arrays.parallelSort(arr1);
            System.out.println(Arrays.toString(arr1));
            int[] arr2 = new int[]{3, -4, 25, 16, 30, 18};
            Arrays.parallePrefix(arr2, new IntBinaryOperator()
            {
                // left代表数组中前一个索引处的元素, 计算第一个元素时, left 为1
                // right代表数值中当前索引处的元素
                public int applyAsInt(int left, int right)
                {
                    reutrn left * right ;
                }
            });
            System.out.println(Ayyars.toString(Arr2));
            int[] arr3 = new int[5];
            Arrays.parallelSetAll(arr3, new IntUnaryOperator()
            {
                // operand 代表正在计算的元素索引
                public int applyAsInt(int operand)
                {
                    return operand * 5;
                }
            });
            System.out.println(Arrays.toString(arr3));
        }
    }

数组的应用举例

    // 例子1: 将一个四位数字转换成人名币读法
    public class Num2Rmb
    {
        private String[] hanArr = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
        private String[] unitArr = {"十", "百", "千"};
        /**
         * 把一个浮点数分解成整数部分和小数部分
         * @param num 需要被分解的浮点数
         * @return 分解出来的整数部分和小数部分, 第一数组元素时整数部分, 第二个数组元素是小数部分
         */
            private String[] divide(double num)
            {
            // 将一个浮点数强制类型转换为long类型, 即得到它的整数部分
            long zheng = (long)num;
            // 浮点数减去整数部分就是得到小数部分, 小数部分乘以100后再取整得到2位小数
            long xiao = Math.round((num - zheng) * 100);
            // 下面用了2中方法把整数转换成字符串
            return new String[] {zheng + "", String.valueOf(xiao)};
            }
        /**
         * 把一个四位数的数字字符串变成汉字字符串
         * @param numStr 需要被转换的四位的数字字符串
         * @return 四位数字字符串被转换成汉字字符串
         */
        private String toHanStr(String numStr)
        {
            String result = "";
            int numLen = numStr.length();
            // 一次遍历数字字符串的每一位数字
            for (int i = 0; i < numLen; i++)
            {
                // 把char型数组转换成int型数字, 因为他们的ASCII码值恰好相差48
                // 因此把char型数字减去48得到int 型数字, 例如: '4' 被转换成 4
                int num = numStr.charAt(i) - 48;
                // 如果不是最后一位数字, 而且数字不是零, 则需要添加单位(千, 百, 十
                if (i != numLen - 1 && num != 0)
                {
                    result += hanArr[num] + unitArr[numLen - 2 - i];
                }
                // 否则不需要添加单位
                else
                {
                    result += hanArr[num]; 
                }
            }
            return result;
        }
        public static void main(String[] args)
        {
            Num2Rmb nr = new Num2Rmb();
            // 把一个浮点数分解成整数部分和小数部分
            // System.out.println(Arrays.toString(nr.divide(236711125.123)));
            // 把一个四位数的数字字符串变成汉字字符串
            System.out.println(nr.toHanStr("7015"));
        }
    }
    例子2: 初步五子棋功能
    import java.io.*;

    public class WuZiQi
    {
        // 定义棋盘的大小
        private static int BOARD_SIZE = 15;
        // 定义二维数组来充当棋盘
        private String[][] board;
        public void initBoard()
        {
            // 初始化棋盘数组
            board = new String[BOARD_SIZE][BOARD_SIZE];
            // 双层for循环为元素赋值为"╋", 用于在控制台画出棋盘
            for (int i = 0; i < BOARD_SIZE; i++)
            {
                for (int  j = 0; j < BOARD_SIZE; j++)
                {
                    board[i][j] = "╋";
                }
            }
        }
        //在控制台输出棋盘方法
        public void printBpard()
        {
            // 打印每个数组元素
            for (int i = 0; i < BOARD_SIZE; i++)
            {
                for (int j = 0; j < BOARD_SIZE; j++)
                {
                    System.out.print(board[i][j]);
                }
                // 每次打印完一行数组元素后输出一个换行符
                System.out.print("\n");
            }
        }
        public static void main(String[] args) throws Exception
        {
            WuZiQi wzq = new WuZiQi();
            wzq.initBoard();
            wzq.printBpard();
            // 这是用于获取键盘输入的方法
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            String inputStr = null;
            // br.readLine(); // 每当在键盘上输入一行内容后按回车键, 刚输入的内容将被wzq读取到
            while((inputStr = br.readLine()) != null)
            {
                // 将用户输入的字符串以逗号(,)作为分隔符, 分割为2个字符串
                String[] posStrArr = inputStr.split(",");
                // 将2个字符串转换成用户下棋的坐标
                int xPos = Integer.parseInt(posStrArr[0]);
                int yPos = Integer.parseInt(posStrArr[1]);
                // 把对应的数组元素赋给"●"
                wzq.board[yPos - 1][xPos - 1] = "●";
                /*
                    * 电脑随机生成2个整数, 作为电脑的坐标, 赋给 board 数组  
                    * 1. 坐标的有效性, 只能是数字, 不能超出棋盘的范围
                    * 2. 下棋的点, 不能重复下棋
                    * 3. 每次下棋后, 需要扫描谁赢了
                    */
                    wzq.printBpard();
                    System.out.println("输入下棋的坐标, 应为x, y的格式:");
            }
        }
    }

# Java