2. 流程控制|方法|数组|二维数组|递归

流程控制

代码块

在深入学习选择结构和循环结构前,我们还需要了解块(block)的概念。

什么是代码块?代码块的定义是:由若干条Java语句组成,并且用一对大括号括起来的结构,叫做代码块。

很显然,我们写的main方法就是一个代码块。

那么代码块有什么用处呢?主要是:

  1. 代码块决定了块中的变量的作用域,也就是块中的变量只在当前块中生效。这意味着同一个代码块中,不可能有同名的变量。
  2. 定义在块中的变量,被块限制了作用域,称之为局部变量。这是一个非常重要的概念

最后谈两点注意事项:

  1. 代码块是可以嵌套定义的。
  2. 关于代码块,我们后面会专门讲解,这里先暂且不谈。

选择结构

其实和c++写的差不多,个人感觉上没有区别:

选择结构有两种实现方式,if 和 switch:

  1. if的使用场景
    1. 针对结果是布尔类型的判断。
    2. 多分支if结构,可以使用多个判断条件。
    3. if的判断条件可以是一个连续的取值范围
  2. switch的使用场景
    1. 针对结果是固定类型、固定值的判断,尤其注意不能是boolean类型。
    2. switch始终只能对一个条件进行选择
    3. 每个分支的取值,只能是固定的且离散的。这是switch和if最本质的区别。
  3. 如果碰到if和switch都可以的情况,建议选择if,因为if语法简单不容易出错。而如果是针对离散值的判断,那就选择switch,因为它更加就简洁。实际开发中,99%以上的情况都在使用if而不是switch。

还有就是不管if后面有几条语句,都需要进行大括号括起来(规范)。

循环结构

看着语法来说,也和C++一样,这边就不细说了。

while、for和do…while之间的异同:

  1. while和for都要先判断条件是否成立,再决定是否执行循环体语句。
  2. do…while先执行循环体语句,再执行判断条件。所以无论如何,do…while都要执行一次循环体语句。
  3. 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;
}

然后就是概念向的:

  1. 我们把[修饰符列表] 返回值类型 方法名 (形式参数列表)称作方法头,或者方法的声明
  2. 我们把方法名 (形式参数列表)称作方法的签名,而对于方法的签名来说,在一个类当中必须是唯一的。

其他的细节:

  1. Java中的一个类中的方法,是平行的关系,位置不同,不会影响方法调用的结果
  2. 同类中的static和static修饰的方法之间可以互相调用,直接用方法名调用即可

以及和C++当中一样实参与返回值实际上会进行自动类型转换(由小变大)。

小技巧:

  1. 提取方法的快捷键 Ctrl+Alt+M.
    在这里插入图片描述
    在这里插入图片描述

  2. 快速生成方法:直接写方法调用,编译报错后使用 alt+enter 快速生成方法
    在这里插入图片描述
    在这里插入图片描述

方法的重载

语法要求:
一个类中的多个方法,可以具有相同的方法名,但是它们的形参列表必须不同。

形参列表不同意味着:

  1. 形参数量不同
  2. 形参数量相同时,形参的数据类型不同
  3. 形参数量和数据类型都相同时,形参的数据类型的顺序不同

而具体使用什么函数则和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的使用:

  1. 首先要创建一个类,而且这个类最好不要叫Test
  2. 然后在这个类的类体中,方法外面,写注解@Test
    注解: annotation,它是一种和class同等级别的数据类型,是一种引用数据类型
    "@Test"当中,其中Test是这个注解的名字,相当于类名
    "@"加上这个注解名,表示创建这个注解的对象(这是一种比较独特的创建对象方式)
    "@Test"注解是Junit的一个基本注解,它表示创建一个测试方法,这个测试方法类似于main方法
  3. 直接写注解肯定会报错,原因和直接在代码中写Scanner类似,是因为缺少导包的步骤,需要导包,但是注解@Test和Scanner导包还有所不同,Scanner是JDK中原本就存在的类,可以直接导包,注解@Test不是JDK中已经存在的注解,而是第三方开发出来的工具当中的注解。 这时需要一个将三方工具包导入当前工程的过程,称之为"导入依赖" 正常情况下,依赖导入需要通过依赖管理工具(maven)去导入,或者手动下载,手动导入,但是Junit这个第三方依赖很特殊,它的依赖包已经存在于IDEA本地文件当中,可以直接通过"alt+回车"导入依赖,并完成导包。
  4. 写完注解后,还需要写测试方法,格式如下:
 public void 方法名(){
 	// 方法体
 }

注意事项:

  1. Junit单元测试的格式上:
    1. public void 是固定格式,不可修改,修改会报错。
    2. 并且测试方法正常情况下,不能添加形参,必须是空参方法。
  2. 做单元测试的类中,不要写main方法,没有太大意义。

具体的导入依赖的操作:
在对应标红色的位置
在这里插入图片描述
我这边由于自己已经导入了,所以就不再次进行尝试了,这边就讲个操作方式。

全限定类名

为什么Junit的使用最好不要在一个Test类中呢?

因为由于Junit的测试方法需要使用注解@Test,其中Test是注解的名字,相当于类名,而注解和类是同等级别的数据类型,所以如果直接在Test类中,写注解@Test,会优先选择使用自身类Test作为一个注解,但是Test类本身就不是注解,而且也不是我们想要的那个注解Test,这肯定是不可行的!这也是一种就近原则.

如何解决?如果非要在Test类中使用注解Test咋办呢?
因为我要使用的注解Test是org.junit包下的Test注解,而不是自身的类Test,所以在写注解时,就不能简单写注解的名字了,要带上包名,由于在Java中,同包下绝不可能存在同名类(注解),所以包名+类名(注解名)是可以唯一的确定一个类(注解)的,我们把直接写"类名"的形式,称之为"简单类名",而把"包名+类名"的形式,称之为"全限定类名",全限定类名的作用是唯一确定一个类 。

例子:

public class Test {
    // 这里不能使用简单类名,要使用全限定类名
    @org.junit.Test
    public void test() {
        System.out.println("hello world!");
    }
}

Debug 小技巧

  1. 如果想要进入查看源代码(类或者方法等)时,可以按快捷键: CTRL + 左键。
  2. 查看代码时:从A—> B —> C —> D,我把D看完了,想继续看C的代码,怎么办呢?使用快捷键 ctrl + alt + 左右键 回到上一次鼠标的位置(左键) 去到下一次鼠标的位置(右键)

数组

数组的基本概念

数组最显著的特征是支持随机访问

随机访问:指的是在访问某个元素时,无需经过其它元素,直接定位访问它

非随机访问:指的是访向某个元素时,需要先访问其它元素。

显然随机访问的效率很高,时间复杂度是常数级别的O(1)。

而数组的随机访问实现方式是:根据数组的首地址和下标,通过寻址公式直接计算出对应的内存地址,最终找出数据。要想使用这种方式实现随机访问,显然数组对数据结构和数组中的元素都是有要求的:

  1. 存储结构必须是连续的(有序),这样才能连续计算。
  2. 存储的元素必须数据类型相同,这样每个存储单元的地址偏移量都是相同的。

综上,数组是用一段连续的内存空间,来存储一组具有相同类型的数据的结构。

数组的基本使用

数组的声明

要想使用数组,首先要声明(declaration)数组,类似于变量的声明。

声明数组的两种语法格式:

  1. 格式一
数据类型[] 数组名;
  1. 格式二
数据类型 数组名[];
  1. 第一种格式具有更好的可读性,可以直观的看到这个数组是一个什么数据类型的数组。
  2. 第二种格式,是Java沿袭自C语言的一种声明方式。我们都知道,早期很多Java开发者都是C转过来的,所以很多开发者在开发Java时,一时改不了使用习惯。我们在Java早期的源代码中,可以发现很多格式二的使用案例。Java为了代码的兼容性考虑,不太可能会取消这一声明格式。(但是像C#这种和Java同源的设计语言,已经取消了数组的声明格式二)
  3. 但是,我们毕竟不是开发者,而几乎所以Java规范中都禁止使用格式二定义数组。规范的Java代码应该永远采用格式一。

数组的初始化

  1. 静态初始化
    由程序员显式的,指定数组中每个元素的初始值,数组的长度由系统决定(实际上也是由程序员给出的)
// 和数组的声明写在一起,语法格式就是:
数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3...};

// 静态初始化有简写的形式,可以省略new关键字,但省略使用必须和声明一起写,而带new的,实际上可以自己放在外面进行使用,自己本身就已经是一个数组了。
数据类型[] 数组名 = {元素1,元素2,元素3...};

  1. 动态初始化
    动态初始化指的是:程序员只是指定数组的长度,数组中每个元素的初始值由系统(实际上是JVM)决定。
数据类型[] 数组名 = new 数据类型[数组长度];

注意事项:

  1. 动态初始化没有给出具体元素的赋值,但仍然能够初始化完成,这是因为数组中的元素具有默认值。
  2. 数组的长度必须是一个int范围内的非负数。
  3. 动态初始化数组相对更灵活,是更常用的方式。
  4. 数组的初始化一旦完成,它的长度就不能改变了!一般来说都会使用集合类中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的五大区域:

  1. JVM栈(以后简称栈,stack):描述的是Java的(普通)方法执行时的所占内存的内存模型。程序运行时调用方法的代价是:方法中有局部变量需要开辟空间存储,方法的执行过程会产生中间变量,方法执行完毕还需要存储返回地址等等。JVM栈正是Java的(普通)方法执行时所占用的内存空间,局部变量会直接存储在栈帧中。

    于是,方法的执行流程,在JVM内存中,就变成下面这样:

    1. 每当Java程序执行一个方法,都会在栈上分配一块只属于该方法的内存区域,称之为栈帧
    2. 每当Java程序执行一个方法,都会将一个存储该方法信息的栈帧压入栈中,称之为方法进栈
    3. 方法进栈的同时局部变量开辟内存空间存储值,局部变量生效。
    4. 当方法执行完毕后,该方法的栈帧随之销毁,称之为方法的出栈
    5. 方法栈帧被销毁的同时,局部变量也被销毁,局部变量失效。

    注:栈中只有处于栈顶的栈帧才会生效,表示正在执行的方法。称之为当前栈帧,当前方法。

  2. 堆(heap):堆是JVM内存中最大的一块,new出来的东西(称之为对象或者实例)都在堆上。所以new关键字的语义就是:在堆上开辟一片空间给相应的对象。而这片空间(对象)是有内存地址的,这个内存地址是留给外界访问用的。

    注:引用数据用比较运算符比较的地址就是这个地址,即比较对象的内存地址。

  3. 方法区(method area):面向对象详细讲。

  4. 本地方法栈:和JVM栈类似,区别是本地方法栈是给本地(native)方法使用的,而不是普通方法。

  5. 程序计数器: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的两大数据类型之一,通过数组初始化的内存分配过程来一窥引用数据类型的特点。

引用数据类型的创建分为两部分:

  1. 首先是在栈上分配一片空间给引用数据类型的引用,简称引用,它是一个局部变量,直接存储在栈帧中。
  2. 在堆上开辟一片空间,用于存放引用数据类型的实际信息,称之为对象或者实例

虽然有两个部分,但对象才是引用数据类型的实质,栈上的引用通过存储对象的地址,指向了堆上对象,这样就可以通过引用间接访问堆上的对象。总结来说就是:对象是实质,但我们不能直接访问堆上的对象,而是通过栈上的引用间接访问。

基本数据类型和引用数据类型的区别

基本数据类型的变量必然都是局部变量,你可能会疑惑,数组的元素也可以是基本数据类型,那它们不是局部变量啊。实际上我们不应该这么去思考,数组中的元素其实已经是(数组)对象的一部分了,它不应该单独拎出来看。所以它们的区别在于:

  1. 存储位置(本质区别)
    1. 基本数据类型不存在引用的概念,数据都是直接存储在栈上的栈帧里;
    2. 引用数据类型在栈帧中存储引用,引用作为一个局部变量,存储的只是该引用类型在堆上对象的内存地址。 存储在堆上的对象存储具体信息,才是引用数据类型的实质。引申出,打印变量名区别:
      1. 基本数据类型,打印变量名就是该变量具体的数值
      2. 引用数据类型,没有办法直接访问对象,打印变量名(引用)会显示该引用存储的堆上的对象的内存地址。

堆和栈中内容的区别

我们从以下三个角度来分析这个问题:

  1. 从存储的类型来看:
    1. 堆上存储的是new出来的东西,是引用数据类型的实质——对象。
    2. 栈上存储的是局部变量(基本数据类型和引用类型的引用)
  2. 从默认值来看:
    1. 堆上的变量具有默认值

      1. 整形(byte、short、int、long)默认值为0
      2. 浮点类型(float、double)默认值为0.0
      3. 字符类型(char)默认值是’\u0000’ 表示编码值为0的字符,一个绝对空字符。
      4. 布尔类型(boolean)默认值是false
      5. 引用数据类型默认值是null ,null既不是对象也不是任何一种数据类型,它仅是一个特殊的值,任何引用数据类型的引用都可以指向null,指向null并不意味着没有初始化,可以认为引用指向了虚无,反正没有指向任何一个对象。
      6. 对象才是引用数据类型的实质,没有指向对象的引用实际上没有任何意义,指向null的引用是无法正常使用的
      7. 基本数据类型不能等于null
    2. 栈上的局部变量没有默认值,声明局部变量后必须显式的初始化,否则无法使用。

  3. 生命周期来看:
    1. 堆上的对象使用完毕后,随着方法的出栈,对象的引用就会被销毁。这个时候对象就没有引用指向它,而是孤零零的单独存在于堆上,这种对象意味着我们就无法再次使用它了,这种对象没有意义了。在Java中,我们把这种对象称之为垃圾或者垃圾对象,它们会等待垃圾回收器进行内存回收。
    2. 关于Java的垃圾回收机制(Garbage Collection简称GC):
      堆上的对象变成垃圾后,并不是立刻就会被回收,而是需要GC通过一系列的算法来决定它是否被回收。Java的GC机制是全自动的,程序员几乎无法干涉和主动回收垃圾。这一方面为Java程序员的开发节省了大量的精力(无需花费大量精力来管理堆内存),相比于C++的全手动回收垃圾对象,Java在GC机制上的创新是Java能够如此流行的重要原因之一。但另一方面,一旦GC这种机制出现问题,对Java而言将会是非常难以解决的问题。垃圾回收是Java和C++之间的一道围墙,墙外的人想进来,墙内的人却想出去。
    3. 栈上的局部变量的生命周期和栈帧保持一致。方法栈帧进栈后,局部变量开辟空间生效了,方法出栈后,局部变量就被销毁了。

数组异常

在数组当中比较常见的异常名称:

  1. 数组下标越界异常:ArrayIndexOutOfBoundsException
  2. 空指针异常:NullPointerException

长度为0的数组 和 数组是null

对于下面的需要进行相关的理解:

  1. 数组未初始化: 这个数组完全是不可用的,没有初始化的数组毫无意义,一旦使用会编译报错。
  2. 数组长度为0和数组为null都是可以使用的,可以认为是经过初始化的,但它们都不是正常数组:
    1. 长度为0的数组,只在内存中存在结构但没有存储单元,不能存储任何数据。它的操作中:
      • 直接打印数组名可以获取数组对象的地址。
      • 不能访问任何数组下标,否则都会抛出数组下标越界异常。
      • 输出数组的长度为0
    2. 数组为null,表示数组的引用指向了null,数组(对象)无法进行任何操作。
      • 直接打印数组名得到一个null字符串。
      • 不能访问任何数组下标,也不能打印数组长度,都会报空指针异常。

一个长度为0的数组的经典用途:

假如方法的返回值是一个数组,而又有无返回数据的需求,这个时候普遍有两种做法 :

  1. 返回一个长度为0的数组
  2. 返回一个为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;
    }

注意事项:

  1. 调用方法时,如果有一个固定参数的方法匹配的同时,也可以与可变参数的方法匹配,则选择固定参数的方法。
  2. 调用方法时,如果出现两个可变参数的方法都能匹配,则报错,这两个方法都无法调用了
  3. 一个方法只能有一个可变长参数,并且这个可变长参数必须是该方法的最后一个参数。
  4. 可变参数的书写格式,推荐"数据类型… 变量名"
  5. 带有可变参数的方法本质上是带有数组形参的一个方法,所以它也是可以和其他方法构成方法重载的!
    1. 尤其是当可变参数和固定参数的方法组成方法重载时,优先匹配固定参数的方法
    2. 当两个可变参数的方法构成方法重载时,要千万小心
    3. 很容易导致两个方法同时无法调用的情况

方法的传参问题

在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并没有发生变化,怎么理解呢?我们先假设如果是引用传递会发生什么?

  1. 当发生引用传递的时候:我们传入的是main当中数组的别名,也即两个对应的是同一个东西,所以最后在arr1和arr2会发生变化,arr1和arr2相互交换。
  2. 当发生值传递的时候:传入的是main的值,也即拷贝,所以我们交换的只是jvm栈当中的arr1和arr2,外面的并没有进行调换。

总结,由于java值传递的特性,可以得到以下结论:

  1. 不能修改基本数据类型的实参的值,也不能修改引用数据类型实参引用的取值,一个方法不能修改其它方法中局部变量的取值。一旦违反上述原则,那么局部变量就不"局部"了。
  2. 可以改变引用数据类型中对象里的数据(称之为改变对象的状态,改变对象的属性值)
  3. 换句话来说,就是没办法改变引用数据类型的值,但可以改变引用数据类型内部数据的值

二维数组

二维数组的基本概念

在这里插入图片描述
二维数组的实质,仍然是一维数组。二维数组的数据类型由其中一维数组的类型决定,只能存储相同类型的一维数组,但值得注意的是和c/c++不一样的是,这个一维数组的大小可以不同,也即二维数组中的每一个一维数组的大小可以不一样,所以java就不存在和c/c++一样的连续存储的情况,他的每一个一维数组的存储都是随机的。

二维数组的基本使用

二维数组的声明:

数据类型[][] 二维数组名;

二维数组的初始化有三种格式:

  1. 静态初始化
   二维数组名 = new 数据类型[][]{{元素1,元素2,元素..},{元素1..}...};
   //可以简写为下面格式
   数据类型[][] 二维数组名 = {{元素1,元素2,元素..},{元素1..}...};
  1. 动态初始化格式一
   二维数组名 = new 数据类型[m][n];

其中:

  1. m表示二维数组的长度,代表二维数组当中,一维数组的个数。
  2. n代表二维数组当中,每个一维数组的长度,即一维数组能存储元素的个数。

注:

  1. 动态初始化格式一创建的二维数组,里面的每个一维数组的长度都相同。
  2. 其中的每一个一维数组相当于动态初始化,里面的元素都具有默认值, 可以直接使用。内存图如下:

在这里插入图片描述

  1. 动态初始化格式二
   二维数组名 = 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]]

递归

递归注意事项

使用的递归的注意事项:

  1. 首先需要找到方法体中,自身调用自身的代码,这称之为递归体,没有递归体肯定不是递归。
  2. 光有递归体的递归肯定是不行的,方法只进栈不出栈一定会栈溢出,所以需要给出一个条件让方法结束调用出栈,这称之为"递归的出口", 对于一个正常的递归而言,递归的出口必不可少。
  3. 思考: 一个有递归的出口就一定不会出错吗?一定不会栈溢出?一定安全吗?不一定,递归即便有出口,也需要考虑递归的次数,称之为"递归的深度",递归的深度不能超过栈的空间大小,否则仍然会栈溢出。

总之: 递归是很危险的,使用递归之前一定要严格考虑递归的出口和递归的深度.递归虽然很好,但是能不用尽量不用

    public static int sum(int num) {
        // 递归需要出口,否则一定栈溢出
        if (num == 0) {
            return 0;
        }
        return num + sum(num - 1);
    }

异常

递归栈溢出错误:
递归是方法自我调用的过程,但是“只递不归”的套娃会导致程序崩溃,报错递归栈溢出错误(StackOverflowError),是一个错误(Error),是比Exception要更加的严重的错误。

产生栈溢出错误的原因在于:

  1. Java程序运行时,调用方法是有代价的:要占用栈(stack)中的内存空间
  2. 方法执行结束后,方法出栈,释放内存,所以一般情况下,栈内存不会溢出,始终够用
  3. 无限制的递归调用方法,会导致方法只进栈不出栈,很快栈内存空间就不够用了
  4. 这种情况就是"栈溢出错误",对程序而言是致命错误,程序必须停止执行。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值