文章目录
- 流程控制
- 代码块
- 选择结构
- 循环结构
- 跳转控制关键字
- 方法
- 方法的概述
- 方法的重载
- Junit单元测试初识
- 全限定类名
- Debug 小技巧
- 数组
- 数组的基本概念
- 数组的基本使用
- 数组的声明
- 数组的初始化
- JVM内存模型
- 什么是引用数据类型
- 基本数据类型和引用数据类型的区别
- 堆和栈中内容的区别
- 数组异常
- 长度为0的数组 和 数组是null
- 遍历数组多种操作
- 可变参数
- 方法的传参问题
- 二维数组
- 二维数组的基本概念
- 二维数组的基本使用
- 二维数组的进阶操作
- 递归
- 递归注意事项
- 异常
流程控制
代码块
在深入学习选择结构和循环结构前,我们还需要了解块(block)的概念。
什么是代码块?代码块的定义是:由若干条Java语句组成,并且用一对大括号括起来的结构,叫做代码块。
很显然,我们写的main方法就是一个代码块。
那么代码块有什么用处呢?主要是:
- 代码块决定了块中的变量的作用域,也就是块中的变量只在当前块中生效。这意味着同一个代码块中,不可能有同名的变量。
- 定义在块中的变量,被块限制了作用域,称之为局部变量。这是一个非常重要的概念
最后谈两点注意事项:
- 代码块是可以嵌套定义的。
- 关于代码块,我们后面会专门讲解,这里先暂且不谈。
选择结构
其实和c++写的差不多,个人感觉上没有区别:
选择结构有两种实现方式,if 和 switch:
- if的使用场景
- 针对结果是布尔类型的判断。
- 多分支if结构,可以使用多个判断条件。
- if的判断条件可以是一个连续的取值范围
- switch的使用场景
- 针对结果是固定类型、固定值的判断,尤其注意不能是boolean类型。
- switch始终只能对一个条件进行选择
- 每个分支的取值,只能是固定的且离散的。这是switch和if最本质的区别。
- 如果碰到if和switch都可以的情况,建议选择if,因为if语法简单不容易出错。而如果是针对离散值的判断,那就选择switch,因为它更加就简洁。实际开发中,99%以上的情况都在使用if而不是switch。
还有就是不管if后面有几条语句,都需要进行大括号括起来(规范)。
循环结构
看着语法来说,也和C++一样,这边就不细说了。
while、for和do…while之间的异同:
- while和for都要先判断条件是否成立,再决定是否执行循环体语句。
- do…while先执行循环体语句,再执行判断条件。所以无论如何,do…while都要执行一次循环体语句。
- while、for是先判断再干,do…while是干了再说。
跳转控制关键字
还是和C++一致,break,continue,return,至于goto(爬)
提到C/C++当中的循环控制关键字,还有一个非常著名的绕不过去,那就是goto
goto关键字,可以帮助我们在循环的过程中跳转到循环任何地方,极大增强了循环的灵活性。但是goto的缺点也是显而易见的,它破坏了程序的结构,使得循环的逻辑被打破,循环的结果容易变得不可控。
到了现在,虽然C++中依然保留goto,但实际上也是不推荐使用的。Java语法更直接摒弃了它,没有将它作为一个关键字,而是作为一个没有任何意义的保留字。
作为保留字,也是提醒程序员:这东西没用,你也不要定义一个东西叫goto。
方法
方法的概述
对于方法来说,实际上和C++的函数相类似,只是java当中每一个东西都被当作是一个类,所以我们需要加上public static 作为前面的标识,实际上就是,原本的方法的意识,这边给出样例:
[修饰符列表] 返回值类型 方法名 (形式参数列表){//方法体
}
例子:
public static void main(String[] args) {// Result of 'NewDemo.sum()' is ignored// 方法既然有返回值,那么建议去接收或者使用这个返回值int sumValue = sum(10, 20);System.out.println(sumValue);int num1 = 100;int num2 = 200;// 操作方法调用就是操作方法的返回值System.out.println(sum(num1, num2));System.out.println(sum(num1, num2) + 100);
}// 定义一个方法,来完成求两个int类型数值的和
public static int sum(int num1, int num2) {return num1 + num2;
}
然后就是概念向的:
- 我们把[修饰符列表] 返回值类型 方法名 (形式参数列表)称作方法头,或者方法的声明
- 我们把方法名 (形式参数列表)称作方法的签名,而对于方法的签名来说,在一个类当中必须是唯一的。
其他的细节:
- Java中的一个类中的方法,是平行的关系,位置不同,不会影响方法调用的结果
- 同类中的static和static修饰的方法之间可以互相调用,直接用方法名调用即可
以及和C++当中一样实参与返回值实际上会进行自动类型转换(由小变大)。
小技巧:
-
提取方法的快捷键 Ctrl+Alt+M.
-
快速生成方法:直接写方法调用,编译报错后使用 alt+enter 快速生成方法
方法的重载
语法要求:
一个类中的多个方法,可以具有相同的方法名,但是它们的形参列表必须不同。
形参列表不同意味着:
- 形参数量不同
- 形参数量相同时,形参的数据类型不同
- 形参数量和数据类型都相同时,形参的数据类型的顺序不同
而具体使用什么函数则和C++一样满足的是就近原则,但此时就有人会这么做:
// 方法1
public static void test(int a,double b){}
// 方法2
public static void test(double a,int b){}
调用:
test(10, 10);
那么此时调用的是啥呢?
显然不好确定,无论是1还是2都需要类型转换才能匹配,既然都转换,并且都是int—>double,那么到底谁"近"呢?实际上这个方法的调用,是一个模糊的调用,会编译报错。这一点在开发中,多个方法组成方法重载时,要格外注意。
Junit单元测试初识
Junit就是用来做单元测试的,而这一部分存在于编译器当中,也属于第三方库,导入过程就需要alt + Enter进行操作
通俗点说,junit可以实现在一个类中实现多个main方法的效果
Junit的使用:
- 首先要创建一个类,而且这个类最好不要叫Test
- 然后在这个类的类体中,方法外面,写注解@Test
注解: annotation,它是一种和class同等级别的数据类型,是一种引用数据类型
"@Test"当中,其中Test是这个注解的名字,相当于类名
"@"加上这个注解名,表示创建这个注解的对象(这是一种比较独特的创建对象方式)
"@Test"注解是Junit的一个基本注解,它表示创建一个测试方法,这个测试方法类似于main方法 - 直接写注解肯定会报错,原因和直接在代码中写Scanner类似,是因为缺少导包的步骤,需要导包,但是注解@Test和Scanner导包还有所不同,Scanner是JDK中原本就存在的类,可以直接导包,注解@Test不是JDK中已经存在的注解,而是第三方开发出来的工具当中的注解。 这时需要一个将三方工具包导入当前工程的过程,称之为"导入依赖" 正常情况下,依赖导入需要通过依赖管理工具(maven)去导入,或者手动下载,手动导入,但是Junit这个第三方依赖很特殊,它的依赖包已经存在于IDEA本地文件当中,可以直接通过"alt+回车"导入依赖,并完成导包。
- 写完注解后,还需要写测试方法,格式如下:
public void 方法名(){// 方法体}
注意事项:
- Junit单元测试的格式上:
- public void 是固定格式,不可修改,修改会报错。
- 并且测试方法正常情况下,不能添加形参,必须是空参方法。
- 做单元测试的类中,不要写main方法,没有太大意义。
具体的导入依赖的操作:
在对应标红色的位置
我这边由于自己已经导入了,所以就不再次进行尝试了,这边就讲个操作方式。
全限定类名
为什么Junit的使用最好不要在一个Test类中呢?
因为由于Junit的测试方法需要使用注解@Test,其中Test是注解的名字,相当于类名,而注解和类是同等级别的数据类型,所以如果直接在Test类中,写注解@Test,会优先选择使用自身类Test作为一个注解,但是Test类本身就不是注解,而且也不是我们想要的那个注解Test,这肯定是不可行的!这也是一种就近原则.
如何解决?如果非要在Test类中使用注解Test咋办呢?
因为我要使用的注解Test是org.junit包下的Test注解,而不是自身的类Test,所以在写注解时,就不能简单写注解的名字了,要带上包名,由于在Java中,同包下绝不可能存在同名类(注解),所以包名+类名(注解名)是可以唯一的确定一个类(注解)的,我们把直接写"类名"的形式,称之为"简单类名",而把"包名+类名"的形式,称之为"全限定类名",全限定类名的作用是唯一确定一个类 。
例子:
public class Test {// 这里不能使用简单类名,要使用全限定类名@org.junit.Testpublic void test() {System.out.println("hello world!");}
}
Debug 小技巧
- 如果想要进入查看源代码(类或者方法等)时,可以按快捷键: CTRL + 左键。
- 查看代码时:从A—> B —> C —> D,我把D看完了,想继续看C的代码,怎么办呢?使用快捷键 ctrl + alt + 左右键 回到上一次鼠标的位置(左键) 去到下一次鼠标的位置(右键)
数组
数组的基本概念
数组最显著的特征是支持随机访问。
随机访问:指的是在访问某个元素时,无需经过其它元素,直接定位访问它
非随机访问:指的是访向某个元素时,需要先访问其它元素。
显然随机访问的效率很高,时间复杂度是常数级别的O(1)。
而数组的随机访问实现方式是:根据数组的首地址和下标,通过寻址公式直接计算出对应的内存地址,最终找出数据。要想使用这种方式实现随机访问,显然数组对数据结构和数组中的元素都是有要求的:
- 存储结构必须是连续的(有序),这样才能连续计算。
- 存储的元素必须数据类型相同,这样每个存储单元的地址偏移量都是相同的。
综上,数组是用一段连续的内存空间,来存储一组具有相同类型的数据的结构。
数组的基本使用
数组的声明
要想使用数组,首先要声明(declaration)数组,类似于变量的声明。
声明数组的两种语法格式:
- 格式一
数据类型[] 数组名;
- 格式二
数据类型 数组名[];
- 第一种格式具有更好的可读性,可以直观的看到这个数组是一个什么数据类型的数组。
- 第二种格式,是Java沿袭自C语言的一种声明方式。我们都知道,早期很多Java开发者都是C转过来的,所以很多开发者在开发Java时,一时改不了使用习惯。我们在Java早期的源代码中,可以发现很多格式二的使用案例。Java为了代码的兼容性考虑,不太可能会取消这一声明格式。(但是像C#这种和Java同源的设计语言,已经取消了数组的声明格式二)
- 但是,我们毕竟不是开发者,而几乎所以Java规范中都禁止使用格式二定义数组。规范的Java代码应该永远采用格式一。
数组的初始化
- 静态初始化
由程序员显式的,指定数组中每个元素的初始值,数组的长度由系统决定(实际上也是由程序员给出的)
// 和数组的声明写在一起,语法格式就是:
数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3...};// 静态初始化有简写的形式,可以省略new关键字,但省略使用必须和声明一起写,而带new的,实际上可以自己放在外面进行使用,自己本身就已经是一个数组了。
数据类型[] 数组名 = {元素1,元素2,元素3...};
- 动态初始化
动态初始化指的是:程序员只是指定数组的长度,数组中每个元素的初始值由系统(实际上是JVM)决定。
数据类型[] 数组名 = new 数据类型[数组长度];
注意事项:
- 动态初始化没有给出具体元素的赋值,但仍然能够初始化完成,这是因为数组中的元素具有默认值。
- 数组的长度必须是一个int范围内的非负数。
- 动态初始化数组相对更灵活,是更常用的方式。
- 数组的初始化一旦完成,它的长度就不能改变了!一般来说都会使用集合类中ArrayList(ArrayList底层实现仍然是数组,它有下标,能够随机访问,具有数组的优点,但是它没有长度的限制。它是通过将元素放入更长的数组中实现扩容的。这点我们后面会学习。)
还有就是小的tips:
int a = 10;
int[] arr = new int[3];// 直接输出数组名/*首先arr是数组名,同时它更是一个局部变量我们可以输出数组名,看一下这个局部变量中到底存放了什么输出的结果是:[I@6d6f6e28解释:[ 一个左中括号表示这是一个一维数组。([[就表示二维数组)I 表示这个一维数组是int类型的@后面跟的是一个十六进制的数,这个十六进制的数表示的是地址所以arr中存储的是地址
*/
System.out.println(arr);
输出:
[I@6d6f6e28
JVM内存模型
JVM的五大区域:
-
JVM栈(以后简称栈,stack):描述的是Java的(普通)方法执行时的所占内存的内存模型。程序运行时调用方法的代价是:方法中有局部变量需要开辟空间存储,方法的执行过程会产生中间变量,方法执行完毕还需要存储返回地址等等。JVM栈正是Java的(普通)方法执行时所占用的内存空间,局部变量会直接存储在栈帧中。
于是,方法的执行流程,在JVM内存中,就变成下面这样:
- 每当Java程序执行一个方法,都会在栈上分配一块只属于该方法的内存区域,称之为栈帧
- 每当Java程序执行一个方法,都会将一个存储该方法信息的栈帧压入栈中,称之为方法进栈
- 方法进栈的同时局部变量开辟内存空间存储值,局部变量生效。
- 当方法执行完毕后,该方法的栈帧随之销毁,称之为
方法的出栈
- 方法栈帧被销毁的同时,局部变量也被销毁,局部变量失效。
注:栈中只有处于栈顶的栈帧才会生效,表示正在执行的方法。称之为当前栈帧,当前方法。
-
堆(heap):堆是JVM内存中最大的一块,new出来的东西(称之为
对象
或者实例)都在堆上。所以new关键字的语义就是:在堆上开辟一片空间给相应的对象。而这片空间(对象)是有内存地址的,这个内存地址是留给外界访问用的。注:引用数据用比较运算符比较的地址就是这个地址,即比较对象的内存地址。
-
方法区(method area):面向对象详细讲。
-
本地方法栈:和JVM栈类似,区别是本地方法栈是给本地(native)方法使用的,而不是普通方法。
-
程序计数器:JVM执行代码解释执行的,即是一行一行执行字节码的,程序计数器用来记录当前执行的行数。
很明显,在JVM内存模型中,相对比较重要的,和程序的执行联系更紧密的是:堆和JVM栈。堆内存用来存储对象,由于Java是面向对象语言,Java面向对象程序中将会有非常多的对象,所以 堆内存主要决定了Java程序的数据如何存储的问题。而JVM栈用来表示方法的执行流程, 它决定了程序如何执行,或者说如何处理数据。
或者举个例子,当我们在写了一个函数:
public static void test()
{int a = 1;double[] b = new Double[]{0.1,0.2,0.3};String[] arr3 = {"hello", "abc", "666"};
}
对于上面的东西,我们进行分析,首先明白test函数是放在栈区的,然后栈区当中的这个函数内部又会存储局部变量a 的值,以及b的引用(由于b是数组属于引用数据类型,所以实际上存储了一个b的地址,引用),然后这些0.1啊什么的是存储在堆区当中,相同的arr3这些也是一样的arr3也是把引用存在栈区当中,当函数的具体调用的时候,会拿着这个引用(地址)来去堆区拿数据来使用。
而需要注意的是,我们讨论中,在堆区创建的数据(也就是在main下创建的数据)是有默认值的,比如我们可以直接在main下int[] a = new Double[3],这让内部的值都为0,但是在局部变量当中这是没有默认值的 ,这需要十分的注意。
什么是引用数据类型
引用数据类型是Java的两大数据类型之一,通过数组初始化的内存分配过程来一窥引用数据类型的特点。
引用数据类型的创建分为两部分:
- 首先是在栈上分配一片空间给引用数据类型的引用,简称引用,它是一个局部变量,直接存储在栈帧中。
- 在堆上开辟一片空间,用于存放引用数据类型的实际信息,称之为对象或者实例
虽然有两个部分,但对象才是引用数据类型的实质,栈上的引用通过存储对象的地址,指向了堆上对象,这样就可以通过引用间接访问堆上的对象。总结来说就是:对象是实质,但我们不能直接访问堆上的对象,而是通过栈上的引用间接访问。
基本数据类型和引用数据类型的区别
基本数据类型的变量必然都是局部变量,你可能会疑惑,数组的元素也可以是基本数据类型,那它们不是局部变量啊。实际上我们不应该这么去思考,数组中的元素其实已经是(数组)对象的一部分了,它不应该单独拎出来看。所以它们的区别在于:
- 存储位置(本质区别)
- 基本数据类型不存在
引用
的概念,数据都是直接存储在栈上的栈帧里; - 引用数据类型在栈帧中存储引用,引用作为一个局部变量,存储的只是该引用类型在堆上对象的内存地址。 存储在堆上的对象存储具体信息,才是引用数据类型的实质。引申出,打印变量名区别:
- 基本数据类型,打印变量名就是该变量具体的数值
- 引用数据类型,没有办法直接访问对象,打印变量名(引用)会显示该引用存储的堆上的对象的内存地址。
- 基本数据类型不存在
堆和栈中内容的区别
我们从以下三个角度来分析这个问题:
- 从存储的类型来看:
- 堆上存储的是new出来的东西,是引用数据类型的实质——对象。
- 栈上存储的是局部变量(基本数据类型和引用类型的引用)
- 从默认值来看:
-
堆上的变量具有默认值
- 整形(byte、short、int、long)默认值为0
- 浮点类型(float、double)默认值为0.0
- 字符类型(char)默认值是’\u0000’ 表示编码值为0的字符,一个绝对空字符。
- 布尔类型(boolean)默认值是false
- 引用数据类型默认值是null ,null既不是对象也不是任何一种数据类型,它仅是一个特殊的值,任何引用数据类型的引用都可以指向null,指向null并不意味着没有初始化,可以认为引用指向了虚无,反正没有指向任何一个对象。
- 对象才是引用数据类型的实质,没有指向对象的引用实际上没有任何意义,指向null的引用是无法正常使用的
- 基本数据类型不能等于null
-
栈上的局部变量没有默认值,声明局部变量后必须显式的初始化,否则无法使用。
-
- 生命周期来看:
- 堆上的对象使用完毕后,随着方法的出栈,对象的引用就会被销毁。这个时候对象就没有引用指向它,而是孤零零的单独存在于堆上,这种对象意味着我们就无法再次使用它了,这种对象没有意义了。在Java中,我们把这种对象称之为垃圾或者垃圾对象,它们会等待垃圾回收器进行内存回收。
- 关于Java的垃圾回收机制(Garbage Collection简称GC):
堆上的对象变成垃圾后,并不是立刻就会被回收,而是需要GC通过一系列的算法来决定它是否被回收。Java的GC机制是全自动的,程序员几乎无法干涉和主动回收垃圾。这一方面为Java程序员的开发节省了大量的精力(无需花费大量精力来管理堆内存),相比于C++的全手动回收垃圾对象,Java在GC机制上的创新是Java能够如此流行的重要原因之一。但另一方面,一旦GC这种机制出现问题,对Java而言将会是非常难以解决的问题。垃圾回收是Java和C++之间的一道围墙,墙外的人想进来,墙内的人却想出去。 - 栈上的局部变量的生命周期和栈帧保持一致。方法栈帧进栈后,局部变量开辟空间生效了,方法出栈后,局部变量就被销毁了。
数组异常
在数组当中比较常见的异常名称:
- 数组下标越界异常:ArrayIndexOutOfBoundsException
- 空指针异常:NullPointerException
长度为0的数组 和 数组是null
对于下面的需要进行相关的理解:
- 数组未初始化: 这个数组完全是不可用的,没有初始化的数组毫无意义,一旦使用会编译报错。
- 数组长度为0和数组为null都是可以使用的,可以认为是经过初始化的,但它们都不是正常数组:
- 长度为0的数组,只在内存中存在结构但没有存储单元,不能存储任何数据。它的操作中:
- 直接打印数组名可以获取数组对象的地址。
- 不能访问任何数组下标,否则都会抛出数组下标越界异常。
- 输出数组的长度为0
- 数组为null,表示数组的引用指向了null,数组(对象)无法进行任何操作。
- 直接打印数组名得到一个null字符串。
- 不能访问任何数组下标,也不能打印数组长度,都会报空指针异常。
- 长度为0的数组,只在内存中存在结构但没有存储单元,不能存储任何数据。它的操作中:
一个长度为0的数组的经典用途:
假如方法的返回值是一个数组,而又有无返回数据的需求,这个时候普遍有两种做法 :
- 返回一个长度为0的数组
- 返回一个为null的数组
以上两种方式皆可,但是建议优先选装返回长度为0的数组,避免空指针异常
遍历数组多种操作
Arrays.toString(数组)
数组遍历并输出值,是非常常见的操作。所以如果你仅仅是想看一下数组里的元素长啥样,完全不需要自己手写实现。而是直接使用下面的方式
范围for 或者叫 增强for操作:
for(数据类型 变量名 : 要遍历的数组或者集合){System.out.println(变量名);
}
这边补充一下,可以直接快捷键,输入iter,直接输出,而java当中的引用采用的都是值引用,不论是函数传参还是什么别的方法,采用的都是值引用。
可变参数
这个… 实际上就是数组,我们实际上按照数组来进行使用即可。 区别在于: 原先的数组是程序员自己创建的,可变参数的数组是编译器帮助创建的,像这种实现原理不变,但是简化了程序员的操作,让代码更灵活的语言特性就是可变参数。
public static int sum(int... var) {// 遍历数组求和int sumValue = 0;for (int ele : var) {sumValue += ele;}return sumValue;}
注意事项:
- 调用方法时,如果有一个固定参数的方法匹配的同时,也可以与可变参数的方法匹配,则选择固定参数的方法。
- 调用方法时,如果出现两个可变参数的方法都能匹配,则报错,这两个方法都无法调用了
- 一个方法只能有一个可变长参数,并且这个可变长参数必须是该方法的最后一个参数。
- 可变参数的书写格式,推荐"数据类型… 变量名"
- 带有可变参数的方法本质上是带有数组形参的一个方法,所以它也是可以和其他方法构成方法重载的!
- 尤其是当可变参数和固定参数的方法组成方法重载时,优先匹配固定参数的方法
- 当两个可变参数的方法构成方法重载时,要千万小心
- 很容易导致两个方法同时无法调用的情况
方法的传参问题
在java当中的所有的传参所传递的都是值传递,java当中没有引用传递这种说法,(值传递,传递值,或者说传递拷贝,引用传递,传递的是别名,也即自己的值的别名)
常见误解:
认为当传递数组的时候,是引用传递:
public static void doubleElementValue(int[] arr) {for (int i = 0; i < arr.length; i++) {arr[i] *= 2;}}
当我们在main当中调用这个函数,传递一个arr给他,我们会发现arr内部的数据是会进行修改的,但此时我们把其当作是引用传递,这是很有问题的,事实上值传递,也即传递引用数据类型的地址,我们也能根据这个地址去改变堆中数据的大小的。
看下面的例子进行分辨:
public static void swapReference(int[] arr1, int[] arr2) {int[] temp = arr1;arr1 = arr2;arr2 = temp;System.out.println("交换的方法中arr1"+Arrays.toString(arr1));System.out.println("交换的方法中arr2"+Arrays.toString(arr2));}
当调用这个函数的时候,我们会发现在main函数中arr1和arr2并没有发生变化,怎么理解呢?我们先假设如果是引用传递会发生什么?
- 当发生引用传递的时候:我们传入的是main当中数组的别名,也即两个对应的是同一个东西,所以最后在arr1和arr2会发生变化,arr1和arr2相互交换。
- 当发生值传递的时候:传入的是main的值,也即拷贝,所以我们交换的只是jvm栈当中的arr1和arr2,外面的并没有进行调换。
总结,由于java值传递的特性,可以得到以下结论:
- 不能修改基本数据类型的实参的值,也不能修改引用数据类型实参引用的取值,一个方法不能修改其它方法中局部变量的取值。一旦违反上述原则,那么局部变量就不"局部"了。
- 可以改变引用数据类型中对象里的数据(称之为改变对象的状态,改变对象的属性值)
- 换句话来说,就是没办法改变引用数据类型的值,但可以改变引用数据类型内部数据的值。
二维数组
二维数组的基本概念
二维数组的实质,仍然是一维数组。二维数组的数据类型由其中一维数组的类型决定,只能存储相同类型的一维数组,但值得注意的是和c/c++不一样的是,这个一维数组的大小可以不同,也即二维数组中的每一个一维数组的大小可以不一样,所以java就不存在和c/c++一样的连续存储的情况,他的每一个一维数组的存储都是随机的。
二维数组的基本使用
二维数组的声明:
数据类型[][] 二维数组名;
二维数组的初始化有三种格式:
- 静态初始化
二维数组名 = new 数据类型[][]{{元素1,元素2,元素..},{元素1..}...};//可以简写为下面格式数据类型[][] 二维数组名 = {{元素1,元素2,元素..},{元素1..}...};
- 动态初始化格式一
二维数组名 = new 数据类型[m][n];
其中:
- m表示二维数组的长度,代表二维数组当中,一维数组的个数。
- n代表二维数组当中,每个一维数组的长度,即一维数组能存储元素的个数。
注:
- 动态初始化格式一创建的二维数组,里面的每个一维数组的长度都相同。
- 其中的每一个一维数组相当于动态初始化,里面的元素都具有默认值, 可以直接使用。内存图如下:
- 动态初始化格式二
二维数组名 = new 数据类型[m][];
其中:
m表示二维数组的长度,代表二维数组当中,一维数组的个数。
注:
这种方式创建的二维数组,实际上只有默认值null,不能直接使用。内存图如下:
除了上述三种三种初始化方式,其它任何方式都是错误的,比如下面:
三种初始化方式中,静态初始化很固定地为元素赋值。动态初始化格式一创建完毕后,一维数组的长度都是一样的,但可以直接使用。而格式二直接使用会空指针异常,还需要手动初始化二维数组中的每一个一维数组,每个一维数组的长度可以自己给出,相对灵活。
二维数组的进阶操作
便利打印:
Arrays.deepToString(某个二维数组)
由于他的每一层的数组大小不一样,所以他的遍历有点不一样,理解一下,下面的即可:
import java.util.Arrays;public class Main {public static void main(String[] args) {int[][] a = {{1, 2, 3, 4}, {2, 3, 4}, {3, 4}};System.out.println(Arrays.deepToString(a));print(a);}private static void print(int[][] a) {System.out.print("[");for (int i = 0; i < a.length; i++) {for (int j = 0; j < a[i].length; j++) {if (j==0){System.out.print("["+a[i][j]+",");} else if (j==a[i].length-1) {System.out.print(a[i][j]+"],");}else {System.out.print(a[i][j]+",");}}}System.out.print("\b]");}
}
输出:
[[1, 2, 3, 4], [2, 3, 4], [3, 4]]
[[1,2,3,4],[2,3,4],[3,4]]
递归
递归注意事项
使用的递归的注意事项:
- 首先需要找到方法体中,自身调用自身的代码,这称之为递归体,没有递归体肯定不是递归。
- 光有递归体的递归肯定是不行的,方法只进栈不出栈一定会栈溢出,所以需要给出一个条件让方法结束调用出栈,这称之为"递归的出口", 对于一个正常的递归而言,递归的出口必不可少。
- 思考: 一个有递归的出口就一定不会出错吗?一定不会栈溢出?一定安全吗?不一定,递归即便有出口,也需要考虑递归的次数,称之为"递归的深度",递归的深度不能超过栈的空间大小,否则仍然会栈溢出。
总之: 递归是很危险的,使用递归之前一定要严格考虑递归的出口和递归的深度.递归虽然很好,但是能不用尽量不用
public static int sum(int num) {// 递归需要出口,否则一定栈溢出if (num == 0) {return 0;}return num + sum(num - 1);}
异常
递归栈溢出错误:
递归是方法自我调用的过程,但是“只递不归”的套娃
会导致程序崩溃,报错递归栈溢出错误(StackOverflowError),是一个错误(Error),是比Exception要更加的严重的错误。
产生栈溢出错误的原因在于:
- Java程序运行时,调用方法是有代价的:要占用栈(stack)中的内存空间
- 方法执行结束后,方法出栈,释放内存,所以一般情况下,栈内存不会溢出,始终够用
- 无限制的递归调用方法,会导致方法只进栈不出栈,很快栈内存空间就不够用了
- 这种情况就是"栈溢出错误",对程序而言是致命错误,程序必须停止执行。