4.数组与流程控制


关于Java数组,我们说几点需要注意的地方:

  • 数组是一种引用类型,它是若干相同类型(既可以是基本类型也可以是引用类型)的数据的集合
  • 数组从下标0开始计数,且定义数组不能指定长度,一旦初始化完成数组长度不可改变
  • 数组必须初始化,可以动态/静态初始化
  • 数组使用length成员获取其长度,数组越界将会抛出异常:java.lang.ArrayIndexOutOfBoundsException

定义&&初始化数组

Java支持两种方式定义数组,如:

type[] arrayName;

或者是:

type arrayName[];

为了保证程序可读性和良好的语义,建议使用第一种方式初始化数组

再次强调定义数组不能指定长度,且必须初始化再使用,原因是:

数组是一个引用类型,用引用类型定义一个变量时,相当于只定义了一个变量,实质上定义了一个指针,Java没有指针的概念,不支持将引用类型的变量指向无效地址(即没有初始化过的地址空间),因此定义数组时不能为其指定长度(除非你同时对其初始化).由于定义数组只是定义了一个引用变量,并未指向任何有效地址,因此无法存放数组元素,所以必须通过初始化为其分配空间让其指向空间(隐式,初始化时自动完成)再使用.

初始化也分两种,如:

  • 静态初始化
arrayName = new type[]{elem1,elem2,elem3,...};
    • 简化写法:
type[] arrayName = {elem1,elem2,elem3,...};
  • 动态初始化
arrayName = new type[length];
  • 总结
    • 静态初始化:手动指定数组每个元素的值,数组长度由指定的值的个数决定
    • 动态初始化:指定分配数组长度,系统自动将数组每个元素都置为初值,基本数据类型的初值如下表
类型 初值
整型(byte/short/int/long) 0
浮点型(float/double) 0.0
字符型(char) '\u0000'
布尔型(boolean) false
引用类型 null

使用数组

数组定义完就使用呗,这里没什么好讲的,注意的点还是在定义和初始化上,请看以下示例:

public class ArrayTest {
    public static void main(String[] args) {
        int[] a;    //定义数组;
        a = new int[]{1,2,3,4,5};   //静态初始化;
        char[] b;
        b = new char[20];   //动态初始化;
        String[] c = new String[]{"Hello","World"}; //定义时静态初始化;
        double[] d = new double[20];    //定义时动态初始化;
        // float[20] e;    //错误写法;
        // float f[20];    //错误写法;
        System.out.println(a.length+" "+b.length+" "+c.length+" "+d.length);
        for(int i = 0; i < a.length; i++)
            System.out.print(a[i]+" ");
        System.out.println("");
        for(int i = 0; i < b.length; i++)
            System.out.print(b[i]+" ");
        System.out.println("");
        for(int i = 0; i < c.length; i++)
            System.out.print(c[i]+" ");
        System.out.println("");
        for(int i = 0; i < d.length; i++)
            System.out.print(d[i]+" ");
        System.out.println("");
    }
}

流程控制

流程控制无非三种结构:顺序分支循环.

顺序结构

顺序结构不多说,就是线性执行语句罢了.

分支结构

已经不是第一次学这些基础语法了,没什么新鲜的,主要是if条件语句和switch分支语句,简要说一下:

  • if条件语句
    • 形式1:
if(condition) {
    statement;
}
    • 形式2:
if(condition) {
    statement1;
} else {
    statement2;
}
    • 形式3:
if(condition1) {
    statement1;
} else if(condition2) {
    statement2;
} else if(condition3) {
    statement3;
}
...	//若干个else if
else if(conditionN) {
    statementN;
} else {
    statementN1;
}

使用多重if...else...的时候,优先处理范围小的集合,如:a > 50a > 10,则把a > 50作为前面的条件

  • switch语句
    • 基本形式:
switch (expression) {
    case condition1: {
        statement1;
        break;
    }
    case condition2: {
    	  statement2;
    	  break;
    }
    ...	//若干个case
    case conditionN: {
        statementN;
        break;
    }
    default: {
        statementN1;
        break;
    }
}

注意以下三点:

  • switch中表达式(expression)的类型只能是charbyteshortintString枚举类型几种,不能是boolean
  • default用于处理前面所有case都失配的情况
  • 每一个case代码块都必须有一个break,避免出现错误

循环结构

其他语言已有的while/do...while循环,for循环和Java新增的foreach循环:

  • while循环
    • 基本形式:
while(boolean_Test) {
    statement;
}
    • 注意事项:
    • boolean_Test必须是boolean类型的表达式
    • 只要boolean_Test值为true,就会一直执行statement
  • do…while循环
    • 基本形式:
do {
    statement;
} while(boolean_Test);
    • 注意事项:
    • 同while的注意事项
    • 和while循环不同的地方在于,无论如何statement至少执行一次
  • for循环
    • 基本形式:
for(init_statement;boolean_Test;iteration_statement) {
    statement;
}
    • 注意事项:
    • init_statement用于初始化变量,一般初始化循环迭代器
    • boolean_Test作用同while循环
    • iteration_statement更新循环迭代器,更新到一定程度boolean_Test就会变成false
  • foreach循环
    • 基本形式:
for(type variableName : arrayName | collectionTypeName) {
    //variableName自动遍历数组/集合;
}
    • 注意事项:
    • foreach并不是Java关键字,而是借用了其他语言的foreach特性,对for循环的增强
    • foreach主要用来遍历数组/集合,如List,Set等
    • variableName:引用变量名,作为中间变量用来遍历数组/集合
    • arrayName/collectionTypeName:要遍历的数组名/集合名
    • type应和arrayName/collectionTypeName的类型一致
    • foreach循环不能改变数组/集合元素的值,元素和variableName赋值是单向的

关于流程控制请看以下示例:

public class ProcessControl {
    public static void main(String[] args) {
        int count0 = 0,count1 = 0;
        int[] array = new int[] {1,2,3,4,5,6,7,8,9,10};
        while(true) {   //死循环;
            count0++;
            if(count0 == 10) break;
        }
        do {
            System.out.println(count1);
            count1++;
        } while(count1 < 10);
        for(int x : array) {
            System.out.println(x);
            x = 0;   //企图这样改变数组元素的值是无效的;
            System.out.println(x);
        }
        for(int i = 0; i < array.length; i++)
            System.out.print(array[i]+" ");
        System.out.println("");
    }
}
  • 死循环只能是while(true)for(;;),使用while时不能是while(1),因为boolean不能转换为其它类型也不能由其他类型转换而来,Java不会像C++一样做隐式转化,譬如将0当做false,其余数值是true

深入数组

数组在内存中

我们知道,数组是一个引用类型的变量,我们声明并初始化一个数组以后,它在内存中实际上占有两块空间:

  • 内存分配的数组实际存的空间(用于存储每一个数组元素,一般在堆内存)
  • 内存分配的存放数组名变量的空间(存放指向数组的这个引用变量,一般在栈内存)

堆内存:对于一些需要反复利用,长久占据内存,且可能被多个引用变量引用的数据,一般存入这里.数组的实际对象就在这里.堆内存的特点是使用完以后数据不会被立马销毁,只有等到没有任何引用变量指向它时,JVM才会将其标记为"垃圾",并在合适的时候将其内存收回,将内存空间清空为null.

栈内存:对于局部变量,尤其是引用变量,将存入这里.数组的引用变量一般在这里.栈内存的特点是当一个方法结束以后,方法使用的局部变量内存将全部释放回收.这样该方法修改的堆内存数据将会保证被修改,且方法内使用的引用局部变量将销毁而不占内存.

总结:常驻内存的数据(一般是需反复利用,且开辟内存花销大的数据),存入堆内存,而操作这些数据的"控制器",即引用变量,存入栈内存,随用随删,节省资源.引用变量是复杂对象的控制器,栈内存是堆内存的修改器.

Q:为什么这么做?分两块内存区的好处在哪里?

A:如果没有引用变量,我们无法表示一整片连续的存储空间,如数组这样的形式,那么在每一个方法里需要修改数组的话,得找到数组存储首地址,再一个又一个地址去改,貌似省了引用变量的空间开销,但是在高级语言中,直接和地址打交道往往都是不容易的事情,且在方法传递参数为数组时,没有引用变量也显得异常麻烦.使用引用变量,相当于为这片地址起了一个名字,用同一个名字加上[]符号和索引即可访问整片地址,这样也使得方法传递参数只用传个引用变量即可,只有一个单位的对象内存开销,且在方法结束后自动回收引用地址,何乐而不为呢?

下面的示例将会说明,引用变量和实际数组对象内存上的分离关系:

public class ArrayTestTwo {
    public static void main(String[] args) {
        int[] a = new int[] {1,2,3,4,5};
        int[] b = new int[] {1,2,3,4,5,6,7,8};
        System.out.println(a.length);
        System.out.println(b.length);
        a = b;
        System.out.println(a.length);
    }
}

输出结果:

5
8
8

以上程序中的数组在内存中的示意图为:

Arrays

这里可以直接把a和b看成是C++的指针,就很好理解了.顺带一提,由于长度为5的数组没有引用变量再指向了,如果此时程序没有结束的话,这个数组就会被标记为"垃圾",JVM会在合适的时候清理它.可以把指向数组的所有引用变量都置为null,这样该数组的堆内存将会被标记为"垃圾"并清理

再说初始化

关于初始化,前面只是说了一个大致分类:动态与静态初始化,具体到哪一种数组怎么初始化,我们再往下说.

基本类型数组的初始化

基本类型数组,其初始化在前面几乎已经说完了,静态不用多说,动态初始化需要补充一下:

  • 对于基本数据类型,动态初始化以后将会指定分配的空间大小,并已初值置位
  • 建议使用自定义的值而非初值,将不可控性降低,建议用多少数据分多大空间
import java.util.Scanner;

public class Initial {
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        int[] array = new int[10];	//动态初始化;
        for(int i = 0; i < array.length; i++) {	//用输入的值覆盖系统初值;
            array[i] = cin.nextInt();
        }
    }
}

引用类型数组初始化

引用类型当然也是推荐动态初始化,和C++一样对于每一个数组元素对象,使用构造器进行初始化,基本形式:

SomeObject[] array = new SomeObject[len];
for(int i = 0; i < array.length; i++) {
    //input args;
    array[i] = new SomeObject(args);
}

或者可以使用赋值,每次都实例化一个新的对象,再将这个对象赋值给数组元素即可,如下形式:

SomeObject[] array = new SomeObject[len];
for(int i = 0; i < array.length; i++) {
    //input args;
    SomeObject temp = new SomeObject(args);
    array[i] = temp;
}

使用构造器初始化对象的样例如下:

import java.util.Scanner;

class Complex {
    private double real,imag;
    Complex(double real,double imag) {
        this.real = real;
        this.imag = imag;
    }
    public void show() {
        if(imag > 0)
            System.out.println(real+"+"+imag+"i");
        else if(imag == 0)
            System.out.println(real);
        else
            System.out.println(real+""+imag+"i");
    }
}

public class ObjectInit {
    public static void main(String[] args) {
        double real,imag;
        Scanner cin = new Scanner(System.in);
        Complex[] array = new Complex[10];	//动态初始化;
        for(int i = 0; i < array.length; i++) {	//用输入的值覆盖系统初值;
            real = cin.nextDouble();
            imag = cin.nextDouble();
            array[i] = new Complex(real,imag);
        }
        for(int i = 0; i < array.length; i++)
            array[i].show();
    }
}

多维数组

没有多维数组!
不管多少维数组都只有最后一维才能存真正的数组元素!

从内存的角度来说,Java也好,C/C++也好,这些高级语言都不存在多维数组的概念.所谓多维数组,就是数组里存数组.二维数组就是数组每一个元素都是一个新的数组,三维数组同理,四维数组也一样.

在Java里,多维数组就是使用一个引用变量,指向一片内存空间首地址,而这片内存空间存的是一系列的引用变量指向其它的多片空间首地址,我们来探讨它们在内存的存在关系,先给出结论:

  • 指向多维数组第一维的引用变量(即多维数组名),存在栈内存,其余维的引用变量/数组元素,存在堆内存

  • 图例:

MultiDemensionArrays

解释:

  • 多个方法应能通过多维数组名引用数组,方法结束后应该清理这些中间名,因此多维数组名存在栈内存
  • 作为数组的一部分,哪怕是引用变量也不能用完立即销毁(否则找不到数组剩余部分),因此其它维的引用变量/数组元素在堆内存

多维数组的初始化

可以把多维数组看成是一维数组进行初始化,这样其余未初始化的维数将自动以null填充,如:

int[][] a = new int[5][];
int[][][] b = new int[5][][];

注意只能使用初始化过的维数,否则将会抛出异常,如下:

int[][][][] a = new int[5][][][];
for(int i = 0; i < a.length; i++)
    System.out.print(a[i][i]+" ");	//异常,第二维没有初始化;

int[][][][] a = new int[5][10][][];
for(int i = 0; i < a.length; i++)
    System.out.print(a[i][i]+" ");	//正常,输出5个null;

除了最后一维,每一维都有自己的length,且长短不一定相同,使用时注意不要越界:

int[][][][] a = new int[5][10][][];
for(int i = 0; i < a[0].length; i++)
    System.out.print(a[i][i]+" ");		//异常,第一维长度只有5;
System.out.println("");

int[][][][] a = new int[10][10][][];
for(int i = 0; i < a[0].length; i++)
    System.out.print(a[i][i]+" ");		//正常,输出10个null;
System.out.println("");

以上只是说明一些注意事项,请实际操作不要使用奇怪的用法,尽量使用 \le 3维的数组


正常的写法应该是,循环赋值,几维数组就使用几层循环嵌套,就像这样:

int[][] a = new int[4][5];
for(int i = 0; i < a.length; i++)
    for(int j = 0; j < a[0].length; j++)
        //input a[i][j];
        
int[][][] b = new int[4][5][6];
for(int i = 0; i < b.length; i++)
    for(int j = 0; j < b[0].length; j++)
        for(int z = 0; z < b[0][0].length; z++)
            //input b[i][j];

知道了多维数组的本质,我们就方便对其进行操作了.Java使用[]的数量来表示维数,一个[]表示一个维数

下面是二/三维数组的使用示例:

import java.util.Scanner;

public class MultiDemensionArrays {
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        int[][] a = new int[2][3];
        int[][][] b = new int[2][2][3];
        for(int i = 0; i < a.length; i++)
            for(int j = 0; j < a[0].length; j++)
                a[i][j] = cin.nextInt();
        for(int i = 0; i < b.length; i++)
            for(int j = 0; j < b[0].length; j++)
                for(int z = 0; z < b[0][0].length; z++)
                b[i][j][z] = cin.nextInt();
        System.out.println("");
        for(int i = 0; i < a.length; i++) {
            for(int j = 0; j < a[0].length; j++)
                System.out.print(a[i][j]+" ");
            System.out.println("");
        }
        System.out.println("");
        for(int i = 0; i < b.length; i++) {
            for(int j = 0; j < b[0].length; j++) {
                for(int z = 0; z < b[0][0].length; z++)
                    System.out.print(b[i][j][z]+" ");
                System.out.println("");
            }
        }
    }
}

Arrays类

Java提供了工具包Arrays,在java.util.Arrays下面可以找到.

Arrays类的常见方法及用途如下表:

方法 返回类型 用途
binarySearch(type[] a,type key) int 二分查找,返回key的索引,不存在返回负数
binarySearch(type[] a,int st,int ed,type key) int 返回[st,ed)内key的索引,否则返回负数
copyOf(type[] a,int len) type[] 返回a长度为len的复制数组,不够长后补初值
copyOfRange(type[] a,int st,int ed) type[] 返回a在[st,ed)的复制数组,不够长后补初值
equals(type[] a,type[] b) boolean 数组a与b完全相等(长度和元素值),返回true
fill(type[] a,type val) void 将a所有元素赋值为val
fill(type[] a,int st,int ed,type val) void 将a中[st,ed)内的所有元素赋值为val
sort(type[] a) void 将a进行升序排序
sort(type[] a,int st,int ed) void 将a的[st,ed)区间进行升序排序
toString(type[] a) String 将a转换为字符串,每个元素用’,‘或’ '隔开

Java里的二分查找需要序列已经是升序排序后的序列

这里的方法都是静态static方法,使用时需要用类名调用,如Arrays.sort(args);

Arrays的一些方法使用示例如下:

import java.util.Arrays;
import java.util.Scanner;

public class ArraysTest {
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        int[] array = new int[10];
        for(int i = 0; i < array.length; i++)
            array[i] = cin.nextInt();
        Arrays.sort(array); //升序排序;
        int[] temp = Arrays.copyOfRange(array,0,15);    //前十个元素来自array,后五个填充0;
        for(int x : temp)
            System.out.print(x+" ");
        System.out.println("");
        if(Arrays.equals(array,temp))   //比较两个数组;
            System.out.println("Yes");
        else
            System.out.println("No");
        int t;
        do {
            t = cin.nextInt();
        } while(Arrays.binarySearch(array,t) < 0 && Arrays.binarySearch(temp,10,15,t) < 0);
        Arrays.fill(temp,1000);
        Arrays.fill(temp,10,15,20);
        for(int x : temp)
            System.out.print(x+" ");
        System.out.println("");
    }
}

猜你喜欢

转载自blog.csdn.net/AAMahone/article/details/86625357