组成一门语言,最本质需要些什么?我认为不外乎,数字,操作符,这两者表达了,干什么。流程控制来表达怎么干。流程控制无非为两种:顺序的,跳跃的,跳跃的你可以往前挑可以往后调,可以在有条件的时候跳到不同的指定的位置上。
前辈们抽象出如此简单的能力,然后将这种最简单的能力组装成为复杂大逻辑。很厉害。
上一篇解决了干什么的问题,这部分就来描述怎么做的问题。而怎么做,就是所谓的流程控制,所谓流程控制就是解决代码跳转问题。因此,在流程控制内又抽象出几种特定的跳转模式。
顺序结构
顺序结构是最符合人的思考方式的。无非就是一步接着一步执行。排在前面的代码会先去执行,自始至终都是从上往下走。
分支结构
世界上的事情可以很复杂,而代码就要对复杂事件做抽象。前辈们针对事件的进行创造了顺序流与分支流的做法,这是一个很重要的指导思想——系统性思考。而系统性思考的前提就是做拆解。将复杂事件推进转化为单步执行,但是某一步执行会依赖之前执行的结果。这就变成分支。计算机底层使用的是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