程序是什么?程序就是告诉计算机要操作的数据和执行的指令序列。
但是数据在计算机内部是用二进制表示的,不方便操作,为了方便操作于是就有了数据类型和变量的概念。
数据类型和变量
数据类型用于对数据的归类,以便于理解和操作。
java是一种面向对象的语言,除了基本数据类型以外其他都是对象类型。对象是什么?对象就是由基本数据类型、数组和其他对象组合而成的一个东西。
日期在java里面也是一个对象,但是在java内部表示为整形long。
有了数据后我们还要操作数据并且将数据存放进内存。内存就是一块有地址编号的连续的空间。为了方便找到和操作这个数据我们需要给这一块内存取个名字,就是变量。
我们声明了一个变量,其实就是在内存中分配了一块空间。变量就是给数据取名字,方便找到不同的数据,它的值可以变但是这它的含义不应该变。
有了变量之后,就是在内存中分配一块空间,但是这个空间里面的内容是未知的。赋值就是给这块未知的空间设定一个确定的值。
java里面有如下基本数据类型:
整数类型:有4种整数类型byte/short/int/long,分别
有不同的取值范围;
类型名 | 取值范围 |
---|---|
byte | -2^7~2^(7-1) |
short | -2^15~2^(15-1) |
int | -2^31~2^(31-1) |
long | -2^31~2^(31-1) |
赋值其实很简单,直接把熟悉的数字常量形式赋值给变量就行了,对应的内存空间的值就从未知变成了确定的常量。但是常量不能超过对应类型的表示范围。例如:
byte b = 23;
short s = 333;
int i = 999;
long l = 2222;
- 1
- 2
- 3
- 4
但是在给long赋值的时候我们需要注意一点,如果我们所赋的值超过了int的表示范围,我们就需要在值后面加一个大写或小写字母的L。例如:
long l = 33333333333333L;
- 1
小数类型:有两种类型float/double,有不同的取值范围和精度;
类型名 | 取值范围 |
---|---|
float | 1.4E-45~3.4E+38 -3.4E+38~-1.4E-45 |
double | -3.4E+38~1.4E-45 -1.7E+308~-4.9E-324 |
对于double,直接把熟悉的小数赋值就行了。例如:
double d = 0.666;
- 1
对于float,需要在数字后面加大写字母F或小写的字母f,例如:
float f = 2.3f;
- 1
字符类型:char,表示单个的字符;
char用于表示一个字符,这个字符可以是中文字符,也可以是英文字符,赋值是把常量字符用单引号括起来,不要使用双引号。例如:
char c = 'C';
char a ='的';
- 1
- 2
真假类型:boolean,表示真假;
boolean类型的值很简单,直接使用true或false赋值,分别表示真和假。例如:
boolean b = true;
boolean a = false;
- 1
- 2
数组类型
基本类型的数组有三种赋值形式,如下:
//1、
int[] arr = {1,2,3};
//2、
int[] arr = new int[]{1,2,3};
//3、
int[] arr = new int[3];
arr[0] = 1; arr[1] = 2; arr[2] = 3;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
第一种和第二种是先知道数组的内容,第三种是先分配长度,然后再给每一个元素赋值。每一个元素都有一个默认值,默认值和数组的类型有关。数组的长度也可以动态确定,如下:
int length = …;//根据条件动态计算
int arr = new int[length];
数组长度虽然可以动态确定,但是定了之后就不可以改变。数组有一个length属性,但这个属性是只读的。还有在给数组赋值的时候不能同时给定数组长度。因为给了初始值就已经决定了数组的长度,再给一个长度,如果不一致就会报错。
数组类型和基本数据类型是有很大区别的,一个基本类型的变量,内存中只会有一块对应的存储空间。但是数组有两块内存:一块用来存储数组内容本身,另一块是用来存储内容对应的位置。
那么数组为什么要用两块空间呢?我们看下面的代码。
int[] arrA = {1,2};
int[] arrB = {1,2,3};
arrA = arrB;
- 1
- 2
- 3
这段代码里面,arrA初始的长度是2,arrB的长度是3,然后我将arrB的值赋给了arrA。如果arrA对应的内存空间是直接存放数组内容的,那么arrA就没有足够的空间去容纳arrB的所有元素,因为arrA的长度比arrB的长度小。如果用了两块存储空间那就简单了。arrA存储的值就变成了arrB一样的。以后访问arrA和arrB就是一样的了。而arrA{1,2}的内存空间就会因为没有引用了就会被垃圾回收机制回收了。
如下图所示:
由上图可以看出给数组变量赋值和给数组中元素赋值是不相同的,数组中元素赋值是改变数组内容,而给数组变量赋值会让变量指向一个不同的内存空间。虽然上面说数组长度不可以改变,但是不可以改变的是数组得内容空间,经过分配后长度就不能再变了,但是我们可以改变数组变量的值,让它指向一个长度不同的空间。
基本运算
算术运算:加(+)减(-)乘(*)除(、),取模运算(%),自增(++),自减(–)
注意事项:
运算时要注意结果的范围,使用恰当的数据类型;
整数相除不是四舍五入,而是直接舍去小数位;
小数计算有时候结果不精确;
自增(++)/自减(–)放在变量后(a++)是先用原来的值进行操作,然后再对自身修改,而放在变量前(++a)是先对自己进行修改然后进行其它操作。
比较运算:比较大小
比较两个值得关系,结果是一个boolean类型的值。
比较操作符有:
大于(>)、大于等于(>=)、小于(<)、小于等于(<=)、等于(==)、不等于(!=)
逻辑运算:针对布尔值进行运算
逻辑运算根据数据的逻辑关系,生成一个布尔值true或是false。逻辑运算只可以用于boolean类型的数据。
逻辑运算符如下:
与(&):两个都为true才为true,只要有一个是false就是false;
或(|):只要有一个为true就是true,都是false才是false;
非(!):针对一个变量,true会变成false,false会变成true;
异或(^):两个相同为false,两个不相同为true;
短路与(&&):和&类似;
短路或(||):和|类似;
&&和&的区别:
&:不管前面的条件是否正确,后面都执行;&&:前面条件正确时,才执行后面,不正确时,就不执行,就效率而言,这个更好。同理可以得出||和|的区别。
条件执行
流程控制中最基本的就是条件执行,一些操作只能在某些条件满足的情况下才能进行。表达条件执行的语法如下:
if(判断条件){
//代码块
}
//或
if(true) //代码块;
表达的含义很简单,只有在条件语句为真的情况下才执行后面的代码,为假就不执行了。
if的陷阱:有时会忘记在if后面的代码块中,有时希望执行多条语句而没有加括号,结果就只会执行第一条语句,建议所有的if后面都加括号。
也可以根据条件做分支,就是满足条件执行一种操作,不满足的时候执行另一种操作。语法如下:
if(判断条件){
//代码块
} else{
//代码块
}
还有一种和if/else很像的一个条件运算,叫三元运算符,语法如下:
判断条件 ? 表达式1 : 表达式2
当判断条件为真的时候返回表达式1的值,否则就返回表达式2的值。
如果有多个判断条件就可以使用if/else if/else,语法如下:
if(判断条件){
//代码块
} else if(判断条件){
//代码块
} …
else{
//代码块
}
if/else if/else的陷阱:在if/else if/else中,判断的顺序是很重要的,后面的判断只有在前面的判断为false的情况下才会执行。
当条件太多了,用if/else if/else比较繁琐,这种情况就可以使用switch了。语法如下:
switch(表达式){
case 值1:
代码1;
break;
case 值2:
代码2;
break;
….
case 值n:
代码n;
break;
default :
代码n+1;
}
根据表达式的值找到匹配的case,执行后面的代码,碰到break结束,如果没有找到匹配的值则执行default语句。
表达式的数据类型只能是byte、short、int、char、枚举、和String(java7以后)。
break是指跳出switch语句,每条case语句后面都应该跟break语句,否则会继续执行后面case中的代码直到碰到break语句或switch结束。
条件执行的实现原理
程序在计算机看来就是一条条的指令,CPU有一个指令指示器,指令一般都是具体的操作或运算,在执行完一个操作后,指令指示器会自动指向挨着的另一条指令。但是也有一些特殊的指令,称为跳转指令,这些指令会修改指令指示器的值,让CPU跳转到一个指定的地方执行。跳转有两种:无条件跳转(直接跳转)和条件跳转(检查某个条件,满足就进行跳转)。
例如下面的代码:
int a = 10; //1
if(a%2==0) //2
{ //3
System.out.println("偶数");//4
} //5
//其它代码 //6
- 1
- 2
- 3
- 4
- 5
- 6
以上代码转换成指令可能是:
int a = 10;
条件跳转:如果a%2==0,跳转到第四行执行输出语句
无条件跳转:跳转到第6行执行其它代码
以上代码转换成指令也可能是:
int a = 10;
条件跳转:如果a%2!=0,跳转到第六行执行其它代码
if、if/else、if/else if/else、三元运算符都会转换为条件跳转和无条件跳转,但是switch不太一样。
switch如果分支比较少就会转换成跳转指令,如果分支比较多,使用条件跳转会进行很多次的比较运算,效率比较低。它会转换成一个跳转表。跳转表是一个映射表,存储了可能的值以及要跳转的地址。如下表格所示:
条件值 | 跳转地址 | 条件值 | 跳转地址 |
---|---|---|---|
值1 | 代码块1的地址 | … | … |
值2 | 代码块2的地址 | 值n | 代码块n的地址 |
跳转表为什么更高效?因为其值必须为整数,且按大小顺序排序,然后就会使用二分法查找。如果值是连续的,则跳转表还会进行特殊的优化,优化为一个数组,找都不用找了,值就是数组的下标,直接根据值就可以找到跳转的地址,没有的就直接指向default分支。
之前不是说switch的值只能是byte、short、int、char、枚举、和String(java7以后)。其中byte、short、int本来就是整数类型,char本质上也是一个整数,枚举类型也会有对应的整数,String通过hashCode()方法转换为整数,但是不同的String的hashCode也可能相同,跳转后会再次根据String的内容进行比较判断。
循环
循环有四种形式分别是:
while:
语法如下:
while(条件语句){
代码块;
}
或:
while(条件语句) 代码;
只要条件语句为真,就一直执行后面的代码,为假就停止不做了。
do/while:
语法如下:
do{
代码块;
}while(条件语句)
不管条件语句是什么,代码块至少执行一次。先执行代码块,然后判断条件语句,如果为true就继续循环,否则退出循环。
for:
语法如下:
for(初始化语句; 循环条件; 步进操作){
代码块;
}
一般用于循环次数已知的情况。括号里有两个分号,分隔三条语句。除了循环条件外必须返回一个boolean类型外,其它语句没有什么要求。
for循环的执行流程如下:
1、执行初始化命令
2、检查条件是否为true,如果是false,则跳转到第6步
3、循环条件为真,执行循环体
4、执行步进操作
5、步进操作执行完成跳转到第2步,即继续检查循环条件
6、for循环后面的语句
在for中每条语句都可以是空的,如下所示:
for(;;){}
- 1
这是一个死循环,在for中可以省略某些条件,但是分号不能省。
foreach:
示例如下:
int[] arr = {1,2,3};
for(int element : arr){
System.out.println(element);
}
- 1
- 2
- 3
- 4
foreach使用:,冒号前面是循环中的每个元素,包括数据类型和变量名称,冒号后面是要遍历的数组或集合,每次循环element都会自动更新,不需要使用索引变量,在只有遍历的情况下,foreach语法更加简单。
循环控制
在循环的时候,我们可以根据循环的条件控制是否结束循环,但有时候需要根据其它条件提前结束循环,这时我们就可以使用break或continue了。
break:
用于提前结束循环,跳出循环后执行循环后面的语句。
continue:
会跳过循环体中剩下的代码,然后执行步进操作。
循环的实现原理
循环和if一样,都是通过条件跳转指令和无条件跳转指令实现的。不同的是在if里面,跳转至会往后面跳,但是在循环中可能会往前面跳。
虽然循环看起来是重复执行类似的操作,但它其实是计算机解决问题的一种很基本的方式。
方法(函数)的用法
计算机使用方法来减少重复代码和分解复杂操作。
方法的语法结构:
修饰符 返回值类型 函数名字(参数类型 参数名字,….){
操作
}
方法主要由以下几个部分组成:
1、方法名字:表示方法的功能,不可或缺;
2、参数:可以没有,可以有多个,每一个参数由参数类型和参数名组成;定义方法的时候声明参数实际上就是定义变量,只是这个变量的值是未知的,调用方法时传递的参数就是给方法中的变量赋值。
3、操作:方法的具体操作代码;
4、返回值:方法可以没有返回值,当没有返回类型时写成void,如果有则在方法代码中必须使用return返回一个值,这个值得类型需要和声明的返回值类型一样。
**修饰符:**java中有很多修饰符,分别表示不同的目的,根据目的的不同使用不同的修饰符。
在java中有一个方法叫main,它是整个java程序的入口语法如下:
public static void main(String[] args){
}
- 1
- 2
在java中,方法在程序代码中的位置和实际执行的顺序是没有关系的。
在调用方法的时候可能需要传递参数,参数有两种特殊类型的参数分别是数组和可变长度的参数。
数组:在方法内修改数组的元素会修改调用者中的数组内容。
可变长度参数如下所示:
public int max(int min,int...a){
}
- 1
- 2
可变长度参数的语法就是在数据类型后面加 三个点”…”。我们要注意的是可变长度参数必须是参数列表中的最后一个并且一个方法内只能有一个可变长度参数。可变长度参数传过来后实际上会转换成一个数组参数。
方法里面还有返回用到return语句。但是return不是必需的。在没有return的情况下,方法在执行到结尾自动返回,return用于显示结束方法的执行。return可以用于方法内的任意地方,可以提前结束方法的执行。方法的返回值只能有一个。
方法重载:
同一个类里面,方法可以重名,但是参数不能完全一样(参数类型不同或参数个数不同),这叫做方法的重载。
方法在被调用的时候,调用者传递的数据需要与方法声明的参数类型是匹配的,但不要完全一样,因为java编译器会自动将其进行类型转换,并寻找最匹配的类型方法。在有方法重载的情况下,系统会自动调用最匹配的那个方法。
递归方法:其实方法也可以自己调用它自己,这种方法叫递归方法或者递归函数。
总的来说,方法是计算机程序的一个重要结构,通过方法来减少重复代码、分解复杂操作是计算机程序的一种重要方式。
方法调用的原理
上面说程序执行的基本原理就是基于CPU的指令指示器,指向下一条要执行的指令,要么顺序执行,要么就跳转(条件跳转或无条件跳转)。
程序从main方法进去,方法的调用就可以看做是一个无条件跳转,当碰到return或者执行到方法结尾的时候又执行一次无条件跳转。但是在这里面会有几个问题:
1、参数是怎样传递的?
2、方法怎么知道返回到哪个地方?
3、方法的结果怎样传给调用方?
解决的方法就是使用内存存储这些数据,这一块内存叫做栈。栈有特别的使用约定,一般是先进后出,往栈里面存放东西叫做入栈,最下面的叫栈底,最上面的叫栈顶,从栈顶拿出数据叫出栈。栈是从高位地址向低位地址扩展,也就是栈底的内存地址是 最高的,栈顶的内存地址是最低的。
栈用来存放方法调用过程中的数据,包括参数、返回地址和方法内定义的局部变量。返回值不相同,返回值可能放在栈中,有的系统使用CPU内的一个存储器存返回值,可以简单的理解为一个专门存返回值的一个存储器。每调用一次方法数据就会入栈,调用结束数据就会出栈。
我们看以下代码:
public class sum{ //1
public static int sum(int a,int b){ //2
int c = a+b; //3
return c; //4
} //5
public static void main(String[] args){ //6
int d = Sum.sum(1,2); //7
System.out.println(d); //8
} //9
} //10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
在这段代码里面main方法调用了sum方法,计算a变量和b变量的和,然后输出。那么我们从栈的角度来看看。
在mian方法调用sum方法之前,栈的内存如下图所示:
在main方法调用sum方法之后,栈的内存如下图所示:
为什么栈的内存会变成这样呢?现在分析一下。首先我们调用sum方法,参数a和b会入栈,然后将返回的地址入栈,然后就跳转到sum方法的内部为局部变量c分配一个空间然后入栈,而参数a和b直接对应入栈的数据1和2,在返回之前,返回值保存到了专门存储返回值得存储器中。在调用return后,程序又会跳转到栈中保存的返回地址,就是main的下一条指令地址,sum方法相关的数据就会出栈,就会变回地第一幅图的样子。
从上面的图我们可以看出方法的参数和方法内的局部变量到在栈中,这些变量都只有方法被调用的时候分配并且在调用结束后就被释放了。但是这主要针对的是基本数据类型,接下来看看数组是怎样分配内存空间的。
同样我们先看下面的代码:
public calss ArrayMax{
public static int max(int min,int[] arr){
int max = min;
for(int a : arr){
if(a>max){
max = a;
}
}
return max;
}
public static void main(String[] args){
int[] arr = new int[]{2,3,4};
int ret = max(0,arr);
System.out.println(ret);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
这段代码的作用是main方法新建了一个数组,然后调用了max方法计算0和数组中元素的最大值,在程序执行到max方法的return语句之前,内存中栈和堆的情况如下图所示:
对于数组arr,在栈中其实是存放的实际内容的地址0x1000,存放地址的栈空间会随着入栈分配,出栈释放,但不会影响存放实际内容的堆空间。但是堆空间完全不受影响也是不正确的,因为堆空间里面的值如果没有变量指向它的时候,java系统会自动进行垃圾回收释放着块空间。
上面讲了普通方法调用的原理,那递归调用的原理是什么呢?我们先看下面的代码:
punlic static int fac(int n){
if(n==0){
return 1;
} else{
return n*fac(n-1);
}
}
public static void main(String[] args){
int ret = fac(4);
System.out.println(ret);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
在fac第一次被调用的时候,n是4,在执行到n*fac(n-1),也就是4*fac(3)之前的时候,栈的情况如下图所示:
注意返回值存储器是没有值的
在调用fac(3)之后,栈的情况如下图所示:
栈的深度增加了,返回值存储器还是为空,就这样每递归一次,栈的深度就增加一层,每次调用都会分配对应的参数和返回地址,在调用到n等于0的时候,栈的情况就如下图所示:
这个时候有返回值了,fac(0)的返回值为1,fac(0)返回到fac(1),fac(1)执行1*fac(0),结果也是1;然后返回到fac(2),fac(2)执行2*fac(1),结果是2;接着返回到fac(3),fac(3)执行3*fac(2),结果是6;然后返回到f(4),执行4*fac(3),结果是24。这就是递归方法的一个执行过程了,在执行的时候,每调用一次,就会有一次入栈,生成一份不同的参数、局部变量和返回地址。
方法的调用主要是通过栈来存储相关的数据,系统就方法调用者和方法如何使用栈做了约定,返回值可以简单的理解为是通过一个专门的返回存储器存储的。
我们还需要注意的是,如果递归的次数比较多,就需要更多的内存,栈的内存不是无限的。如果栈空间太深了,系统就会抛出
栈溢出异常(java.lang.StackOverflowError)