003_流程控制与数组

组成一门语言,最本质需要些什么?我认为不外乎,数字,操作符,这两者表达了,干什么。流程控制来表达怎么干。流程控制无非为两种:顺序的,跳跃的,跳跃的你可以往前挑可以往后调,可以在有条件的时候跳到不同的指定的位置上。

前辈们抽象出如此简单的能力,然后将这种最简单的能力组装成为复杂大逻辑。很厉害。

上一篇解决了干什么的问题,这部分就来描述怎么做的问题。而怎么做,就是所谓的流程控制,所谓流程控制就是解决代码跳转问题。因此,在流程控制内又抽象出几种特定的跳转模式。

顺序结构

顺序结构是最符合人的思考方式的。无非就是一步接着一步执行。排在前面的代码会先去执行,自始至终都是从上往下走。

分支结构

世界上的事情可以很复杂,而代码就要对复杂事件做抽象。前辈们针对事件的进行创造了顺序流与分支流的做法,这是一个很重要的指导思想——系统性思考。而系统性思考的前提就是做拆解。将复杂事件推进转化为单步执行,但是某一步执行会依赖之前执行的结果。这就变成分支。计算机底层使用的是jump语句,进行跳转,这个机制可以处理一切分支性的事件扭转。对应到java,就类似于goto。但是,问题就来了,goto是一个关键字,但是是没什么用的,这个关键字只做备用。为什么不提供goto出来玩儿呢,因为跳来跳去太乱了。虽然我们抽象了一个万金油式的分支控制指令,但是太灵活,导致后期这个代码实在没法看。怎么办呢?继续抽象,直到人类可以很方便理解代码本身。是更高维度的语义理解。

if

java提供了if关键字,通过布尔来控制分支。其中语法层面会有这样的写法:

第一种:

if(logic_expression){
   statement;
}

第二种:

if(logic_expression){
   statement;
} else {
   statement;
}

第三种:

if(logic_expression){
   statement;
} else if{
   statement;
} 。。。// 可以跟上很多else if

这三种写法很容易理解,如果只有一种可能性,走第一种。如果有十种可能性,就走第三种,如果10中可能性,大部分都是走一样的逻辑。那就使用第二种,将相同逻辑合并为一个分支处理逻辑。

如果statement只有简单一句话,花括号可以不写,但是太丑了,不光丑,还难理解,不要干这种不道德的事情。

注意:if语句块儿,最理想的情况是覆盖所有情况,不然很有可能会出现bug。我们平时说的逻辑严谨也是可以从这种小事儿上看出来的。

switch

上面的if语句里面提到了,如果有十种可能性,每种可能性都有对应的处理,意味着,我们的if else 会很多。前辈们一看,这不行啊,太难看了,能不能想个招把那么丑陋的代码给搞的漂亮点呢?

switch就这么诞生了。怀揣美好愿景来到世上,为程序员带来了福音,说是这么说,我基本不用,这玩意儿和我的世界观不匹配。如果我发现我需要一个switch才能表达我的逻辑,我会怀疑我的逻辑出了问题。一句话,我嫌弃它。

switch的语句语法格式如下:

switch(expression){
  case condition1:{
    statement1;
    break;
  }
  case condition2:{
    statement2;
    break;
  }
  ... //可以放上无数个分支块儿
  default:{
    statementN;
  }
}

语法的含义是:先对expression进行求值,然后依次匹配condition1,condition2…遇到合适的就执行,都没遇上就执行default块儿。

这里的expression只能是byte,short,char,int,没有boolean值

由于语法设计,可以很清楚看到分支的初始与结束,因此花括号全部拿掉也不成问题。多分支情况下,拿掉反而清晰。

你会看到每个分支内的部分都会有break;  这个其实很重要,在switch执行过程中,除非遇上break;不然就会一直去进行匹配,至死方休。

循环结构

顺序结构,表达一条一条往下执行。分支结构表达岔路,然后往下执行。问题来了,我的逻辑的确需要回退——就是说往回跳,这里不一样了,之前都是来控制往下走,这个时候我需要往上走。

goto是个好办法,但是我们之前说了,这个是个万恶之源,不能用,控制不好就是大问题。

我们继续思考,我们什么时候会往回跳?就是说,什么场景下,干什么事儿需要再回过头去干。前辈们抽象来抽象去,发现,往回跳干活也就只有一种情况:重复干一个事情,这会导致代码跳转到执行块儿的初始点。有了这个抽象,自然而然就出现了,循环的概念。

while循环

while循环的语法结构如下:

[init_statement]
while(test_expression){
	statement;
	[iteration_statement]
}
  • init_statement 表达一条或者多条语句完成初始化工作,必须先于while循环。
  • test_expression 一个布尔表达式,决定是否执行循环体。
  • iteration_statement 表达在一次循环体执行结束之后,对循环条件求值之前执行,控制循环体内的变量,使循环体在合适的时候停止。

注意:

while()之后虽然可以加上;变成 while();,但是这么一来就导致循环体事实上是空的,那么就会引发无限循环

while循环的{}外面可以不放;,也会被认为是一个正常的循环体

例如:

public class WhileTest {
    public static void main(String[] args) {
        int i = 0;
        while (i < 3){
            System.out.println(i);
            i = i + 1;
        }
    }
}

将会打印:

0
1
2

这里最经常搞错的是循环的停止时候,反过来,如果希望打印0-9,test_expression应该写成什么?乍一看,9,10都是处于边界值。面对简单问题,可以做下逆向推演,打印出9,意味着,跳出循环就是10了,10不符合检测条件,那么就应该是 i < 10 或者 i <= 9,这样的条件可以把10给卡住,但是稍微逻辑一复杂就蒙圈。一懵圈,没关系,尝试一下边界值代入,再加上debug,一般问题不大。

do while循环

do while可以说是while的变种,他们俩的区别体现在判断逻辑与执行块儿的顺序上:

while循环是先判断循条件,如果符合预期,则执行循环块儿代码

do while是先执行循环体,然后判断循环条件,如果循环条件符合预期,则执行下一次循环

例如:

public class DoWhileTest {
    public static void main(String[] args) {
        int i = 0;
        do{
            System.out.println(i);
            i = i + 1;
        } while (i < 3);
    }
}

将会打印:

0
1
2

for循环

for循环是最强大的循环结构,可以等价实现替换while与do while,只不过有些场合while更简洁而已

for循环的语法如下:

for([init_statements];[test_expression];[iteration_statement]){
		statements;
}

for语句的执行顺序是:先执行init_statements,初始化语句只会在循环开始前执行一次,每次执行循环体之前先计算test_express的循环条件值,如果是true则自行循环体部分。循环体执行结束后执行循环迭代语句。因此循环条件总是要比循环体多执行一次,最后一次执行循环条件获得false才能跳出循环体。

for循环的循环迭代语句没有和循环体放在一起,如果执行循环体的时候遇到continue语句则介乎本次循环,但是循环迭代语句一样会执行一遍。

例子1:最简单的一个for循环

public class For1Test {
    public static void main(String[] args) {
        for(int i = 0; i<3; i++){
            System.out.println(i);
        }
    }
}

打印结果:

0
1
2

例子2:初始化语句可以有多个

public class For2Test {
    public static void main(String[] args) {
        for(int i = 0, j=1, z=2; i<3; i++){
            System.out.println("i:"+i+"|j:"+j+"|z:"+z);
            j = j + 1;
            z = z + 1;
        }
    }
}
// 打印结果
i:0|j:1|z:2
i:1|j:2|z:3
i:2|j:3|z:4

在循环控制体内也可以去更改初始化值的数据,虽然可以改(大多数场景都是为了处理复杂算法问题),但是建议不要这么玩儿,在循环体内更改循环初始化值的数据,会导致代码难以理解,不小心就会出错。

括号内只有两个;是必须的:

例子3:如果只保留;而不增加任何条件的话,判定条件默认为true,意味着这个循环是个死循环,将会不断打印操作

public class For3Test {
    public static void main(String[] args) {
        for(; ;){
            System.out.println("操作");
        }
    }
}

例子4:如果只保留判定条件,而将初始化语句,迭代循环语句放到其他地方也是可以的

public class For4Test {
    public static void main(String[] args) {
        int i = 0;
        for(; i < 3 ;){
            System.out.println(i);
            i = i + 1;
        }
    }
}

嵌套循环

嵌套循环解决了更为复杂的循环结构,大循环内置小循环。当程序遇到嵌套循环之后,将会先执行外层循环,内存循环将被外层循环的循环体执行——意味着,内存循环需要反复被执行。只有当内层循环结束了,外层循环的条件判定为false,才算是真正的将嵌套循环解决掉。

public class ForFor1Test {
    public static void main(String[] args) {
        for(int i = 0; i<2; i++){
            for(int j = 0; j<3; j++){
                System.out.println(i+":"+j);
            }
        }
    }
}

打印结果为:

0:0
0:1
0:2
1:0
1:1
1:2

控制循环

嵌套循环解决了复杂循环的问题,但是随之而来产生了新的问题,我怎么去控制这么复杂的多级嵌套呢?这就涉及到循环控制的问题,前辈们想来想去,觉得多重嵌套的循环控制无非分为2种:直接中止当前循环,结束当前本次循环

对应的分别为 break , continue

break控制

break可以在程序满足某一条件的时候,强制跳出当前的

public class ControlLoopBreakTest {
    public static void main(String[] args) {
        for(int i = 0; i < 4;i++){
            System.out.println("当前i值为"+);
            if(i == 2){
                break;
            }
        }
    }
}

打印出来是:

当前i值为0
当前i值为1
当前i值为2

你会发现,循环体被整个跳出。无论多少层嵌套。

continue控制

continue与break是相似的,区别在于,continue是结束掉当前的一次循环,然后继续跑

public class ControlLoopContinueTest {
    public static void main(String[] args) {
        for(int i = 0; i < 4;i++){
            if(i == 2){
                continue;
            }
            System.out.println("当前i值为"+i);
        }
    }
}

输出为:

当前i值为0
当前i值为1
当前i值为3

return控制

return 事实上是控制方法的,当你在方法内部书写循环,你可以在任意位置写上return,那么意味着直接结束当前的方法,那自然而然就是整个结束掉当前的循环了。

控制标签

通过break和continue可以解决复杂流程,一般够用了。但是前辈们还是觉得控制的不够精确。比方说有10级嵌套循环,我在内层循环里,使用break,就直接出去了,但是很可能我只是想结束内层的几个嵌套,不是全部跳出。continue也是同样的道理。想来想去,还是觉得类似于goto一样的东西很香,想用,但是要挡住诱惑,不能开这个口子。

然后呢,前辈们就想了个招,使用标签一样的东西来帮助他们更精确地控制循环。

public class LoopMarkTest {
    public static void main(String[] args) {
        
        for(int i = 0; i < 3 ; i++){
            outer:
            for(int j = 0; j < 3 ; j++){
                for(int z = 0; z < 3 ; z++){
                    System.out.println("i:"+i+"|j:"+j+"|z:"+z);
                    if(z == 1){
                        break outer;
                    }
                }
            }
        }
    }
}

获得输出:

i:0|j:0|z:0
i:0|j:0|z:1
i:1|j:0|z:0
i:1|j:0|z:1
i:2|j:0|z:0
i:2|j:0|z:1

标签使用标志符:的格式作为一个标识,只有在循环语句之前才有作用。continue也有类似的用法,直接跳转到对应的标签上进行新的一轮迭代循环。break/continue+标签的用法平时很少见到,但在Java的源码中有时候会看到,ThreadPoolExecutor类中的addWorker方法就使用了"continue 标签"。第一次看的时候我还纳闷,java怎么会有这样的不伦不类的语法。当然,话说回来,这也是Java程序设计的一种技巧,其存在必有其存在的意义,在一些极端复杂的时候可能会有帮助。

数组类型

从上面的循环可以看到,我们经常性地需要定义一个初始变量,例如 int i = 0,然后自增,然后使用i干各种事情。

问题就来了,i是一个顺序的值,那也只是一个值而已。比方说我想去遍历一个班的同学,但是我手上只有i,那么自然而然我们就想到,能不能先制造一个映射,比如 i=1表示张三,i=2表示李四呢?看来问题变成了,怎么制造一个好的映射,而且是一个很容易理解的映射。

很容易我们就想到了,能不能让全班同学按照顺序先排队排好了,坐在顺序排列的位子上,例如当i=0,意味着在叫位置0上的同学起立。

排队排好的同学们的位子,用文字来形象表达一番:|张三|李四|王五|xx1|xx2|,张三在第一个坑位,当i=0,就把张三拿出来,是不是就很人性化?

数组定义

前面我们推演出,应该有个东西,可以天生很自然的表达出顺序关系,每个顺位上放着你想要的东西。java里面就有,叫做数组。

这个时候,我们正儿八经地赋予"数组"的语法:

Type[] arrayName;

Type arrayName[]

上面两种定义数组的写法都是可行的,但是回过头一想,数组本身是一种数据结构,是一种引用类型。因此Type[]更像是一种新的类型,容易理解,建议使用第一种形式。

所有的语言格式都是认为设计出来的,你或许有更好的表达方案,就像世界上有那么多种语言,但是在表达具体东西的时候,语言本身是不确定的,唯一确定的是其语言背后所表达的含义本身。

这里的type可以放基础类型,也可以存放引用类型,我们说数组也是引用类型,那么数组里面放数组可以吗?可以,这样就变成:

Type[][] arrayName;

逻辑上,可以理解为二维数组,但是计算机是没有二维,只不过在一维数组内存放了引用,这个引用指向另外一个数组而已。

内存上来说,一维数组的数据本身是放在堆内存上的,而引用本身是放在栈内存里面的,栈内存指向堆内存。

堆啊,栈啊,是归属JVM的知识体系部分,属于java运行时,咱们之后在说。

数组初始化及其使用

java中的数组必须先进行初始化,才可以使用。初始化的含义是为数组的数组元素分配内存空间,并且给每个元素赋予初始值。

数组的初始化方式分为两种:

  • 静态初始化:程序员显式指定每个元素的值,数组长度由计算机自己计算。
    语法:arrayName = new type[]{e1,e2,e3};
  • 动态初始化:程序员显式执行数组长度,计算机分配初始值。
    语法:arrayName = new type[长度];
    该场景下,看type是什么,如果是基本类型,则使用type(基本类型)的默认值,如果是引用类型,则每个元素默认为null

接下去我们就需要去使用它了,这里会有两种重要的方式操作数组:

  • 获得数组长度:int length = arrayName.length;
  • 获得数组的特定位置上的值:value = arrayName[index];
  • 数组的特定位置上进行赋值:arrayName[index] = value;

我们接下来使用上面说到的所有元素进行一次for循环:

public class ArrayLoop1Test {
    public static void main(String[] args) {
        String[] arrayName = new String[]{"早上吃饭","中午吃饭","晚上吃饭"};
        for(int i = 0; i < arrayName.length; i++){
            System.out.println(arrayName[i]);
        }
    }
}
// 输出
// 早上吃饭
// 中午吃饭
// 晚上吃饭

有了数组的加持,我们的for循环能干的事儿就更多了。

Jdk1.5之后又引入了foreach循环的语法糖来帮助开发者开发出更漂亮的代码:

public class ArrayLoop2Test {
    public static void main(String[] args) {
        String[] arrayName = new String[]{"早上吃饭","中午吃饭","晚上吃饭"};
        for(String str : arrayName){
            System.out.println(str);
        }
    }
}

你会发现,指向数组成分的index值已经省略了,for-each会将数组从头开始遍历到尾。当然这是语法糖,只是语法糖而已,编译里面有专门的一个环节叫解语法糖,而这部分和字节码关系密切。字节码指令是稳定的,想在语法层面做手脚的话,无论如何都要在编译器里面改把漂亮的语法糖转换为最原始的样子。

java.util.Arrays 数组工具类

Arrays是操作数组对象的工具类。

数组和循环控制可以说任何语言都需要具备,而这一块儿也是算法集结之地。数组占用空间算是小的,而且获得数组数据的方式很快速,这两个东西在算法领域是很好的材料。而针对数组是有些通用的功能的,这些功能如果每次让人手写那会让人崩溃,因此JDK也自带了一些专门操作数组的工具类。虽说JDK已经有部分api可供使用,但是往往人们不满足,经常会在三方包内增加新的api。网络上的工具包大行其道,到底是JDK真的不太行还是人类天生就是贪婪,藐视他人代码的生物呢?时常对自己写的代码自信满满,而对他人的代码则嗤之以鼻。

Arrays里面有很多static的方法提供调用。

排序相关

// 普通排序
Arrays.sort(int[] a)
Arrays.sort(int[] a, int fromIndex, int toIndex)
// 并行排序:JDK1.8新增
Arrays.parallelSort(int[] a)
Arrays.parallelSort(int[] a, int fromIndex, int toIndex)

其他非boolean基础数据类型的数组对象以及实现Comparable接口的类的数组对象均有此方法。

二分法查找:前提是该数组已经进行了排序:

Arrays.binarySearch(int[] a, int key)
Arrays.binarySearch(int[] a, int fromIndex, int toIndex, int key)

其他非boolean基础数据类型的数组对象以及实现Comparable接口的类的数组对象均有此方法。

判断两个数组是否相等:

Arrays.equals(int[] a, int[] a2)

其他基础数据类型的数组对象以及Object数组对象均有此方法,Object调用的是equels()方法。

对数组进行填充:

Arrays.fill(int[] a, int val)
Arrays.fill(int[] a, int fromIndex, int toIndex, int val)

其他基础数据类型的数组对象以及Object数组对象均有此方法。

复制数组:

Arrays.copyOf(int[] original, int newLength),返回赋值后的数组,数组长度为newLength。
Arrays.copyOfRange(int[] original, int from, int to)

其他基础数据类型的数组对象以及Object数组对象均有此方法。Object数组为浅复制,即复制的是引用。

并行计算:JDK1.8新增,支持函数式编程,根据传入的方法进行一次计算。

Arrays.parallelPrefix(int[] array, IntBinaryOperator op)
Arrays.parallelPrefix(int[] array, int fromIndex, int toIndex, IntBinaryOperator op)

其他非boolean基础数据类型的数组对象以及实现Comparable接口的类的数组对象均有此方法。

打印值

// 将元素用","隔开,包裹在"[ ]"内。
Arrays.toString(int[] a)
// 方法内部调用了a.toString()
Arrays.deepToString(Object[] a)

更改元素值:JDK1.8新增,支持函数式编程

setAll(int[] array, IntUnaryOperator generator)
setAll(long[] array, IntToLongFunction generator)
setAll(double[] array, IntToDoubleFunction generator)
setAll(T[] array, IntFunction<? extends T> generator)

该类方法支持这四种类型。每种类型均有对应的并行设置方法parallelSetAll()

数组转集合:

// 返回List<T>
Arrays.asList(T... a)

生成并行遍历的Spliterator,JDK1.8新增

Arrays.spliterator(int[] array)
Arrays.spliterator(int[] array, int startInclusive, int endExclusive)

生成Stream类,JDK1.8新增

Arrays.stream(int[] a)
Arrays.stream(int[] array, int startInclusive, int endExclusive

除了上述描述的方法之外还有很多方法,你可以到源码分析专栏查看其详细。s

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值