java编程基础(一)
认识变量
对java而言,有如下基本数据类型
- 整数:有4种整形byte/short/int/long;分别占1/2/4/8字节
- 小数:有两种类型float/double
- 字符类型:char,表示单个字符
- 真假类型:boolean,表示真假。
基本数据类型都有对应的数组类型,数组表示固定长度的同种数据类型的多条记录,比如:一个字符可以用一个char类型数据表示,一段文字可以用一个char数组表示。
java是面向对象的语言,除了基本数据类型,其他都是对象类型。世界万物都是由元素周期表组成的,基本数据类型就相当于化学中的基本元素,而对象就相当于世间万物。
为了操作数据,需要把数据存放到内存中。所谓内存在程序看来就像是一块有地址编号的连续空间,数据放到内存中的某个位置后,为了方便地找到和操作这个数据,需要给这个位置起一个名字。编程语言通过变量这个概念来表示这个过程。
声明一个变量,比如int a
,其实就是在内存中分配了一块空间,这块空间存放int数据类型,a指向这块内存空间所在的位置,通过对a操作即可操作a指向的内存空间。之所以叫“变”量,是因为它表示的是内存中的位置,这个位置存放的值是可以变的。
虽然变量的值可以变化,但变量的名字是不变的,这个名字应该表示程序员心目中这块内存空间所存储内容的意义,这个意义是不变的。
赋值
整型
声明变量之后,就在内存分配了一块位置,但这个位置的内容是位置的,赋值就是把这块位置的内容设为一个确定的值。
赋值形式很简单,直接把熟悉的数字常量形式赋值给变量即可,对应的内存空间的值就从未知变成了确定的常量。
byte b = 23;
short s = 3333;
int i = 9999;
long l = 32323;
但,在给long类型赋值时,如果常量超过了int的表示范围,需要在常量后面加大写或小写L,例如:
long a = 323234343433L;
小数类型
对于double,直接把小数表示赋值给变量就可
double d = 333.33;
但对于float,需要在数字后面加大写字母F或小写字母f,例如:
float f = 333.33f;
这是由于小数常量默认是double类型。
除了小数,也可以把整数直接赋值给double或float;这很好理解:整数是特殊的小数。
真假类型
boolean b = true;
b = false;
字符类型
字符类型char用于表示一个字符,char占内存空间两个字节。赋值时把常量字符用单引号括起来。
char c = 'A';
数组类型
基本类型的数组有3种赋值形式
int[] arr = {1,2,3};//当变量赋值
int[] arr = new int[] {1,2,3};//当对象赋值
int[] arr = new int[3];//先定好长度,后赋值。
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
第一种和第二种都是预先知道数组的内容,而第三种是先分配长度,然后再给每个元素赋值。第三种形式中,即使没有给每个元素赋值,每个元素也都有一个默认值,这个默认值跟数组类型有关,数值类型的值为0,boolean为false,char为空字符。
数组的长度可以动态确定
int length = ...;//根据一些条件动态计算
int arr = new int[length];
数组长度虽然可以动态确定,但定了之后就不可以变。数组有一个length属性,但只能读,不能更改。还有一个小细节,不能在给定初始值的同时给定长度,即如下格式是不允许的:
int[] arr = new int[3]{1,2,3};
可以这么理解,因为初始值已经决定了长度,再给个长度,如果还不一致,计算机将无所适从。
数组类型和基本类型有明显不同的,一个基本类型变量,内存中只会有一块对应的内存空间。但数组有两块:一块用于存储数组内容本身,另一块用于存储内容的位置。用一个例子来说明,有一个int变量a,以及一个int数组变量arr,其代码、变量对应的内存地址和内存内容如下所示:

基本类型a的内存地址是1000,这个位置存储的就是它的值100,数组类型arr的内存地址是2000,这个位置存储的值是一个位置3000,3000开始的位置存储的才是实际的数据“1,2,3”。
为什么数组要用两块要用两块空间?
int[] arrA = {1,2,3};
int[] arrB = {4,5,6,7};
arrA = arrB;
这段代码中,arrA初始长度是3X4个字节,arrB的初始长度是4X4个字节,后来将arrB的值赋给了arrA。如果arrA对应的内存空间是直接存储的数组内容,那么它将没有足够的空间去容纳arrB的所有元素。
用两块空间存储就简单得多,arrA存储的值就变成了和arrB的一样,存储的都是数组内容{4,5,6,7}的地址,此后访问arrA就和arrB是一样的了,而arrA{1,2,3}的内存空间由于不再被引用会进行垃圾回收。
由上也可以看出,给数组变量赋值和给数组中元素赋值是两回事,给数组中元素赋值是改变数组内容,而给数组变量赋值则会让变量指向一个不同的位置。
上面说数组的长度是不可以变的,不可变指的是数组的内容空间,一经分配,长度就不能再变了,但可以改变数组变量的值,它它指向一个长度不同的空间。
基本运算
java中基本类型数据的主要运算有:
- 算术运算:日常的加减乘除。
- 比较运算:日常的大小比较。
- 逻辑运算:针对布尔值进行运算。
算数运算符有加、减、乘、除,符号分别是+、-、*、/,另外还有取模运算符%,以及自增(++)和自减(--)运算符。取模运算适用于整数和字符类型,其他算术运算适用于所有数值类型和字符类型。大部分运算都符合我们的数学常识。但,字符怎么也可以进行算术运算?这个后面会讲。
- 减号用于两个数相减,也可以放在一个数前面用来表示负数。
- 取模(%)就是数学中的求余数,例如,5%3是2,10%9是1。
- 自增(++)和自减(--),是一种快捷方式,是对自己进行加1或减1操作。
加减乘除大部分情况和数学运算是一样的,都很容易理解,但有一些需要注意的地方,而自增、自减稍微复杂一些。
运算时要注意结果的范围,使用恰当的数据类型。两个整数都可以用int表示,但相乘的结果可能就会超出:
int a = 2147483647*2; //2147483647是int能表示的最大值
a的结果是-2。要避免这样的情况,我们的结果类型应使用long,但只改为long也是不够的,因为运算还是默认按照int类型进行,需要将至少一个数据表示为long形式,即在后面加L或l。
long a = 2147483647*2L;
另外,需要注意的是,整数相除不是四舍五入,而是直接舍去小数位
double d = 10/4;
结果是2而不是2.5,如果要按小数进行运算,需要将至少一个数表示为小数形式,或者使用强制类型转换,即在数字前面加(double),表示将数字看作double类型
double d = 10/4.0;
double d = 10/(double)4;
无论使用float还是double,进行运算时都会出现一些非常令人困惑的现象,比如:
float f = 0.1f*0.1f;
System.out.println(f);
这个结果看上去应该是0.01,但实际上,屏幕输出却是0.010000001,后面多了个1。换用double看看:
double d = 0.1*0.1;
System.out.println(d);
屏幕输出0.010000000000000002,一连串的0后多了个2。这是为什么呢?会在后面学习!
自增/自减
自增/自减是对自己做加1减1操作,但每个都有两种形式,一种是放在变量后,例如a++、a--,另一种是放在变量前,例如++a、--a。
如果只是对自己操作,这两种形式没差,区别在于还有其他操作的时候。放在变量后(a++)是先用原来的值进行其他操作,再对自己进行修改,而放在变量前(++a)是先对自己修改,再用修改的值做其他操作。
快捷运算 | 等同运算 |
---|---|
b=a++-1 | b=a-1 ; a=a+1 |
c=++a-1 | a=a+1; c=a |
arrA[i++]=arrB[++j] | j=j+1;arrA[i]=arrB[j];i=i+1 |
比较运算
比较运算就是计算两个值之前的关系,结果是一个布尔类型的值。比较运算适用于所有数值类型和字符类型。
比较运算有大于(>)、大于等于(>=)、小于(<)、小于等于(<=)、等于(==)、不等于(!=)。
需要注意的是,对于数组,==判断的是两个变量指向的是不是同一个数组,而不是两个数组的元素内容是否一样,即使两个数组的内容一样,但如果两个不同的数组,==依然会返回false
int[] a = new int[] {1,2,3};
int[] b = new int[] {1,2,3};
//a==b 的结果是false
如果需要比较数组的内容是否一样,需要逐个比较里面存储的每个元素。
逻辑运算
- 与(&): 两个都为true才是true,只要有一个是false就是false。
- 或(|):只要有一个为true就是true,都是false才是false。
- 非(!): 针对一个变量,true会变成false,false会变成true。
- 异或(^): 两个相同为false,两个不同为true;
- 短路与(&&): 和&类似
- 短路或(||): 与|类似
逻辑运算的大部分都是比较直观的,需要注意的是&和&&,以及|和||的区别。如果只是进行逻辑运算,它们也都是相同的,区别在于同时有其他操作的情况下
boolean a = true;
int b = 0;
boolean flag = a | b++ >0;
因为a为true,所以flag也为true,但b的结果为1,因为|后面的式子也会进行运算,即使只看a已经知道flag的结果,还是会进行后面的运算。而||则不同,如果最后一句的代码是:
boolean flag = a || b++ >0;
则b的值还是0, 因为||会“短路”,即再看到||前面部分就可以判定结果的情况下会忽略后面的运算。
那如果我们把a的值换成false呢?结果是false 0;还是false 1 ?
这个问题很有意思,结果是false 1;
boolean a = false;
int b = 0;
boolean flag = a || b++ >0;
显然,当a不是false,就不会忽略后面的运算了;可后面不是++b,而是b++;即:先进行其他操作,再自增,所以结果是false 1。
boolean a = false;
int b = 0;
boolean flag = a || ++b >0;
那么,这个返回什么呢?
答案:ture 、1。
因为这次是++b,先自增后进行其他操作。
条件执行
流程控制中最基本的就是条件执行,一些操作只能在某些条件满足的情况下才执行,在一些条件下执行某些操作,在另一些条件下执行另外的操作。
java中表达条件执行的基本语法是if语句,它的语法是:
if(条件语句){
代码块
}
或
if(条件语句) 代码;
表达的含义也非常简单,只有在条件语句为真的情况下,才执行后面的代码,为假就不执行了。具体来说,条件语句必须为布尔值,可以是一个直接的布尔变量,也可以是变量运算后的结果。如果条件语句为true,则执行括号{}中的代码,如果后面没有括号,则执行后面第一个分号;前的代码。
比如,只在变量为偶数的情况下输出:
int a = 10;
if(a%2 == 0){
System.out.println("偶数")
}
或者:
int a = 10;
if(a%2==0) System.out.println("偶数");
if实现的是条件满足的时候做什么操作,如果需要根据条件做分支,即满足的时候执行某种逻辑,而不满足的时候执行另一种逻辑,则可以用if/else
if(判断条件){
代码块1
}else{
代码块2
}
三元运算符的语法为:
判断条件 ? 表达式1 : 表达式2
三元运算符会得到一个结果,判断条件为真的时候就返回表达式1的值,否则就返回表达式2的值。三元运算符经常用于对某个变量赋值,例如求两个数的最大值:
int max = x > y ? x:y;
三元运算符完全可以用if/else代替,但三元运算符的书写方式更简洁。
如果有多个判断条件,而且需要根据这些判断条件的组合执行某些操作,则可使用if / else if / else 语法如下:
if(条件1){
代码块1
}else if(条件2){
代码块2
}...
else if(条件n){
代码块n
}else{
代码块n+1
}
if / else if / else: 需要注意的是,判断顺序很重要,后面的判断只有在前面的条件为false的时候才会执行。
如下面这个代码:
if(score>60){
return "及格";
}else if(score >80){
return "良好";
}else{
return "优秀";
}
这段代码乍一看,似乎没什么问题;如果score是90,期望返回“优秀”,但实际只会返回“及格”。
在if /else if /else中,如果判断的条件基于的是同一个变量,只是根据变量值的不同而有不同的分支,如果值比较多,比如根据星期几进行判断,有7种可能性,或者根据英文字母进行判断,有26种可能性,使用if/ else if /else 比较繁琐,这种情况就可以使用switch:
switch(表达式){
case 值1:
代码1; break;
case 值2:
代码2; break;
...
case 值n:
代码n; break;
default: 代码n+1
}
switch也比较简单,根据表达式的值执行不同的分支,具体来说,根据表达式的值找匹配的case,找到后执行后面的语句。**表达式值得数据类型只能是byte、short、int、char、枚举和String。**枚举和String后面会介绍。关键词break是指跳出switch语句,执行switch后面得语句。
从逻辑上讲,if/else 、 if/else if/else、三元运算符、switch都可以只用if代替,但使用不同得语句表达更简洁,在条件比较多的时候,switch从性能上看也更高。
条件执行的实现原理
条件执行具体是怎么实现的呢?程序最终都是一条条的指令,CPU有一个指令指示器,指向下一条要执行的指令,CPU根据指示器的指示加载指令并且执行。指令大部分是具体的操作和运算,在执行这些操作时,执行完一个操作后,指令指示器会自动指向挨着的下一条指令。
但有一些特殊的指令,称为跳转指令,这些指令会修改指令指示器的值,让CPU跳到一个指定的地方指向。跳转有两种:一种是条件跳转;一种是无条件跳转。条件跳转检查某个条件,满足则进行跳转,无条件跳转则是直接进行跳转。
if/else实际上会转换为这些跳转指令,比如:
int a = 10;
if (a%2 == 0)
{
System.out.println("偶数");
}
//其他代码
抓换到的转移指令可能是:
int a = 10;
条件跳转:如果a%2==0,跳转到第四行
无条件跳转:跳转到第七行
{
System.out.println("偶数");
}
//其他代码
你可能会奇怪第3行的无条件跳转指令,没有它不行吗?不行,没有这条指令,它会顺序执行接下来的指令,导致不管什么条件,括号中的代码都会执行。不过对应的跳转指令也可能是:
int a = 10;
条件跳转:如果a%2!= 0,跳转到第六行
{
System.out.ptintln("偶数");
}
//其他代码
这里就没有无条件跳转指令,具体怎么对应和编译器实现有关。在单一if的情况下可能不用无条件跳转指令,但稍微复杂一些的情况都需要。if、if/else、if/else if/else、三元运算都会转换为条件跳转和无条件跳转,但swich不太一样。
switch的转换和具体系统系统实现有关,如果分支比较少,可能会转换为跳转指令。如果分支比较多,使用条件跳转会进行很多次的比较运算,效率比较低,可能会使用一种更为高效的方式,叫跳转表。跳转表是一个映射表,存储了可能的值以及要跳转到的地址。
条件值 | 跳转地址 | 条件值 | 跳转地址 |
---|---|---|---|
值1 | 代码块1的地址 | ... | ... |
值2 | 代码块2的地址 | 值n | 代码块n的地址 |
跳转表之所以高效,是因为其中的值必须为整数,且按大小顺序排序。按大小排序的整数可以使用高效的二分查找。如果值是连续的,则跳转表还会进行特殊优化,优化为一个数组,连找都不用找了,值就是数组的下标索引,直接根据值就可以找到跳转的地址。即使值不是连续的,但数字比较密集,差的不多,编译器也可能会优化为一个数组型的跳转表,没有的值指向default分支。
程序源代码中的casr值排列不要求是排序的,编译器会自动排序。之前说switch值的类型可以是byte、short、int、char、枚举和string。其中byte/short/int本来就是整数,char本质上也是整数,而枚举类型也有对应的整数(这个后面会学),String用于switch时也会转换为整数。不可以使用long,因为跳转表值得存储空间为32位,容不下long。简单说明下String,String是通过hashCode方法转换为整数的,但不同String的hashCode可能相同,跳转后会再次根据String的内容进行比较判断。
循环
在java中,循环有四种形式,分别是while、do/while、for和foreach,下面分别介绍
while的语句
while(条件语句){
代码块
}
while和if的语法很像,只是把if换成了while,它表达的含义也非常简单,只要条件语句为真,就一直执行后面的代码,为假就停止不做了。比如
Scanner reader = new Scanner(System.in);
System.out.println("please input password");
int num = reader.nextInt();
int password = 6789;
while(num!=password){
System.out.println("please input password");
}
System.out.println("correct");
reader.close();
以上代码中,我们使用类型为Scanner的reader变量从屏幕控制台接收数字,reader.nextInt()从屏幕接收一个数字,如果数字不是6789,就一直提示输入,否则跳出循环。
while循环中,代码块中会有影响循环中断或退出的条件,但经常不知道什么时候循环会中断或退出。
do/while
不管条件语句是什么,代码块都会至少执行一次
do{
代码块;
}while(条件语句)
for
实际中应用最为广泛的循环语法可能是for了,尤其是在循环次数已知的情况。语法为:
for(初始化语句;循环条件;步进操作){
循环体
}
for后面的括号中有两个分号;分隔了三条语句。除了循环条件必须返回一个boolean类型外,其他语句没有什么要求,但通常情况下第一条语句用于初始化,尤其是循环的索引变量,第三条语句修改循环变量,一般是步进,即递增或递减索引变量,循环体是在循环中执行的语句。
它的执行流程如下:
- 执行初始化指令;
- 检查循环条件是否为true,如果为false,则跳转到第六步;
- 循环条件为真,执行循环体;
- 执行步进操作;
- 步进操作执行完后,跳转到第二步,即继续检查循环条件;
- for循环后面的语句;
下面是一个简单的for循环
int[] arr = {1,2,3,4};
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}
顺序打印数组中的每个元素,初始化语句初始化索引i为0,循环条件为索引雄安与数组长度,步进操作为递增索引i,循环体打印数组元素。
在for中,每条语句都是可以为空的,也就是
for( ; ; ){}
是有效的,这是一个死循环。注意:可以省略语句,但分号不能省。
foreach
我们来看一个foreach的例子
int[] arr = {1,2,3,4};
for(int element : arr){
System.out.println(element);
}
foreach不是一个关键字,它使用冒号:,冒号前面是循环中的每个元素,包括数据类型和变量名称,冒号后面是要遍历的数组或集合,每次循环element都会自动更新。对于不需要使用索引变量,只是简单遍历的情况,foreach语法上会更简洁。
在循环中,会以循环条件作为是否结束的依据,但有时可能会需要根据别的条件提前结束循环或跳过一些代码,这时候可以使用break或continue关键字对循环进行控制。
break
break用于提前结束循环。比如,在一个数组中查找某个元素时,循环条件可能是到数组结束,但如果找到了元素,可能就会想提前结束循环,这时候就可以使用break。
int[] arr = ...; //在该数组中查找元素
int toSearch = 100; //要查找的元素
int i = 0;
for(;i<arr.length;i++){
if(arr[i]==toSearch){
break;
}
}
if(i!=arr.length){
System.out.println("found");
}else{
System.out.println("not found");
}
continue
int[] arr = ... //在该数组中查找元素
int toSearch = 2; //要查找的元素
int count = 0;
for(int i=0; i<arr.length; i++){
if(arr[i] != toSearch){
continue;
}
count++;
}
System.out.println("found count"+count);
上面代码统计数组中值等于toSearch的元素个数,如果值不等于toSearch,则跳过剩下的循环代码,执行i++。以上代码也可以不用continue,使用相反的if判断也可以得到相同的结果。
函数
函数这个概念,我们学数学的时候都接触过,其基本格式是y=f(x),表示的是x到y的对应关系,给定输入x,经过函数变换f,输出y。程序中的函数概念与其类似,也由输入、操作和输出组成,但它表示的是一段子程序,这个子程序有一个名字,表示它的目的(类比f),有零个或多个参数(类比于x),有可能返回一个结果。下面是一个函数的例子:
public static int sum(int a, int b){
int sum = a + b;
return sum;
}
public static void print3Lines(){
for(int i=0 ; i < 3 ; i++){
System.out.println();
}
}
第一个函数名叫sum,它的目的是对输入的两个数求和。有两个输入参数,分别是int整数a和b,它的操作是对两个数求和,求和结果放在变量sum中。然后使用return语句将结果返回,最开始的public static是函数修饰符,后续介绍。
第二个函数名字叫print3Lines,它的目的是在屏幕上输出三个空行,它没有输入参数,操作是使用一个循环输出三个空行,它没有返回值。
函数的基本结构如下:
修饰符 返回值类型 函数名字(参数类型 参数名字, ...){
操作;
return 返回值;
}
- 函数名字:名字是不可或缺的,表示函数的功能。
- 参数:参数有0个到多个,每个参数由参数的数据类型和参数名字组成。
- 操作:函数的具体操作代码。
- 返回值: 函数可以没有返回值,如果没有返回值则类型写成void,如果有则在函数代码中必须使用return语句返回一个值,这个值的类型需要和声明的返回值类型一致。
- 修饰符:暂时不介绍,后面介绍
以上就是函数的语法,定义函数就是定义了一段有着明确功能的子程序,但定义函数本身不会执行任何代码,函数要被执行,需要被调用。
java中,任何函数都需要放在一个类中。类还没有介绍,我们暂时可以把类看作函数的一个容器。即函数放在类中,类中包含多个函数(方法)。类里面可以定义一个叫做main的方法,形式如下:
public static void main(String[] args){
...
}
这个函数有特殊的含义,表示程序的入口,String[] args表示从控制台接收到的参数,我们暂时可以忽略它。Java运行一个程序的时候,需要指定一个定义了main函数的类,java会寻找main函数,并从main函数开始执行。
关于参数传递,简单总结一下,定义函数时声明参数,无论是形参,还是返回值,实际上就是在定义变量,只是这些变量的值是未知的。调用函数时传递参数,实际上就是给函数中的变量赋值。
我们知道,程序执行基本只有顺序执行、条件执行和循环执行,但更完整的描述应该包括函数的调用过程。程序从main函数开始执行,碰到函数调用的时候,会跳转进函数内部,返回到main函数并且main函数没有要执行的语句后程序结束。
参数传递有两类特殊类型的参数:数组和可变长度的参数。
(1)数组
数组作为参数与基本类型是不一样的,基本类型不会对调用者中的变量造成任何影响,但数组不是,在函数内修改数组中的元素会修改调用者中的数组内容。我们看这个例子:
public static void reset(int[] arr){
for(int i=0; i<arr.length; i++){
arr[i] = i;
}
}
public static void main(String[] args){
int[] arr = {10,20,30,40};
reset(arr);
for(int i=0; i < arr.length ; i++){
System.out.println(arr[i])
}
}
在reset函数内给参数数组元素赋值,在main函数中数组arr的值也会变。
这个其实也容易理解,一个数组变量有两块空间,一块用于存储数组内容本身,一块用于存储内容的位置,给数组变量赋值不会影响原有的数组内容,而只会让数组变量指向一个不同的数组内容空间。
在上例中,函数参数中的数组变量arr和main函数中的数组变量arr存储的都是相同的位置,而数组内容本身只有一份数据,所以,在reset中修改数组元素内容和在main中修改是完全一样的。
前面介绍的函数,参数个数都是固定的,但有时候可能希望个数不是固定的,比如求若干个数的最大值,可能是两个,也可能是多个。java支持可变长度参数如下:
public static int max(int min, int ... a){
int max = min;
for (int i=0; i< a.length; i++){
if(max<a[i]){
max = a[i];
}
}
return max;
}
public static void main(String[] args){
System.out.println(max(0));
System.out.println(max(0,2));
System.out.println(max(0,2,4));
System.out.println(max(0,2,4,5));
}
这个max函数接受一个最小值,以及可变长度的若干参数,返回其中的最大值。可变长度参数的语法是在数据类型后面加三个点"...",在函数内,可变长度参数可以看作是数组。可变长度参数必须是参数列表中的最后一个,一个函数也只能有一个可变长度的参数。
可变长度参数实际上会转换为数组参数,也就是说,函数声明max(int min,int... a)实际上会转换为max(int min, int[] a)。
理解返回
return用于显式结束函数执行,返回调用方。return可以用于函数内的任意地方,可以在函数结尾,也可以在中间,可以在if语句内,可以在for循环内,用于提前结束函数执行,返回调用方。
函数返回值类型为void也可以使用return,即"return;",不用带值,含义是返回调用方,只是没有返回值而已。
函数的返回值最多只能有一个,那如果实际情况需要多个返回值呢?比如,计算一个整数数组中的最大的前三个数,需要返回三个结果。这个可以用数组作为返回值,在函数内创建一个包含三个元素的数组,然后将前三个结果赋给对应的数组元素。
如果实际情况需要的返回值是一种复合结果呢?比如,查找一个字符数组中所有重复出现的字符以及重复出现的次数,这个可以用对象作为返回值。总之,虽然返回值最多只能有一个,但其实一个就够了。
每个函数都有一个名字,这个名字表示这个函数的意义。名字可以重复吗?在不同类里,答案是肯定的,在同一类里,要看情况。
同一个类里,函数可以重名,但是参数不能完全一样,即要么参数个数不同,要么参数个数相同但至少有一个参数类型不一样。
我问:在java中,在同一类中的两个同名函数仅返回值类型不同,形参类型、个数都相同;这种属于符合语法规则的重载函数吗?
AI回答:在Java中,函数的重载(Overloading)是指在同一类中可以有多个同名的方法,但是这些方法的参数列表必须有所不同(即参数的数量、类型或顺序不同)。根据这个定义,如果两个方法仅仅是返回值类型不同,而参数列表完全相同的话,那么这并不符合Java中方法重载的规则。
具体来说,方法的重载与返回类型无关,只取决于参数列表的不同。因此,如果你在同一类中有两个同名方法,它们的参数列表完全一样,只是返回类型不同,编译器会报错,因为它无法区分这两个方法,这不符合Java的方法重载语法规则。
同一个类中函数名相同但参数不同的现象,一般称为函数重载。
比如,在JAVA的Math库中就定义了4个函数,如下所示:
public static double max(double a, double b)
public static float max(float a, float b)
public static int max(int a, int b)
public static long max(long a, long b)
在之前介绍函数调用的时候,我们没有特别说明参数的类型。这里说明一下,参数传递实际上是给参数赋值,调用者传递的数据需要与函数声明的参数类型是匹配的,但不要求完全一样。什么意思呢? java编译器会自动进行类型转换,并寻找到最匹配的函数,比如:
char a = 'a';
char b = 'b';
System.out.println(Math.max(a,b));
参数是字符类型的,但Math并没有定义针对字符类型的max函数,这是因为char其实是一个整数,java会自动将char转换为int,然后调用。
如果Math中没有定义针对int类型的max函数呢?调用也会成功,会调用long类型的max函数。如果long也没有呢?会调用float型的max函数。如果float也没有,会调用double型的。
递归函数
函数大部分情况下都是被别的函数调用的,但其实函数也可以调用它自己,调用自己的函数叫递归函数。我们来看一个例子,求一个数的阶乘,数学中一个数n的阶乘,表示为n!,它的值定义是这样的:
0!=1
n!=(n-1)!xn
这个定义是一个递归的定义,为求n的值,需先求n-1的值,直到0,然后依次往回退。
public static long factorial(int n){
if(n==0){
return 1;
}else{
return n*factorial(n-1)
}
}
看上去比较容易,和数学定义类型。递归函数形式上往往比较简单,但递归函数有开销的,而且使用不当,可能会出现意外的情况。
System.out.println(factorial(100000));
系统不会给出任何结果,而会抛出异常。
递归不可行的情况下怎么办呢? 递归函数经常可以转换为非递归函数的形式,通过循环实现。比如,求阶乘的例子,其非递归形式的定义是:
n! = 1x2x3x4x5x6...xn
这个可以用循环来实现
public static long factorial(int n){
long result = 1;
for(int i=1 ; i <= n ; i++){
result =result*i;
}
return result;
}