Bootstrap

相信这里囊括了你初学Java时见过的所有细节易错点(6万字建议收藏)

前言:

文章有点长~    按  Ctrl + F 即可搜索你想要的内容  

本文包含Java基础部分的一些重要知识及基础易错题总结(太简单的内容未包含在内,为的就是干货满满),内容为自己复盘自己学习到的Java基础知识时整理,相信大家读后必能有收获,但由于其中有些问题带着个人对东西的理解难免会有一些错误,希望大家能够指正。若有哪里写的有让你不明白的地方,也欢迎提问哦。

文章经过多天熬夜整理而来,创作不易,怎能忍心不点赞收藏一波!!

Java语言的重要特性:

1.跨平台   (平台: 操作系统) 

跨平台: 一次编译,到处运行。

其跨平台特性的关键就在与Java虚拟机(JVM)。Java源码通过编译产生class文件(即字节码文件),再通过JVM解释class文件的信息,然后发出命令给相应的(操作系统)去执行相应的操作,从而达到了一次编译(编译成class文件),到处运行。

2.面向对象

3.Java语言健壮。其强制类型机制、异常处理、垃圾回收是其健壮的重要保证。

4.Java语言是解释性的语言(还有PHP\Javascript等也是),解释型语言的编译后的代码不能直接由机器执行。(编译型语言则其编译后的代码可以直接执行。如:c\c++)

一张图了解jdk、jre、jvm三者间的关系:

源文件(后缀名为.java)在经过Java开发工具java.exe程序编译后生成字节码文件(后缀名为.class)而此时就需要Java虚拟机(JVM)将其解析,然后将相应的命令发给操作系统去执行。

若只是想运行Java程序,则只需安装JRE即可。

关于Open JDK与Oracle JDK的区别:

1.Open JDK三月发布一次,而Oracle JDK是三年发布一次的。

2.Open JDK 是一个参考模型且是完全开源的,而Oracle JDK是Open JDK的一个实现,并不是完全开源的。

3.Oracle JDK 比 Open JDK 更稳定。

4.在响应性和JVM性能方面,Oracle JDK与Open JDK相比提供了更好的性能。

5.Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本。

6.Oracle JDK是根据二进制代码许可协议获得许可,而Open JDK根据GPL v2许可获得许可。 

关于安装jdk后配置环境变量的原因:

我们在安装完后大家都知道要配置环境变量,但是却不知道为什么要这么做,那究竟是为什么呢?

对于这个问题,首先我们得知道在cmd窗口执行一个可执行文件的过程是怎么样的:在控制台窗口,如图:

若输入javac这个编译java源码的exe文件(javac.exe),则控制台会在c盘这个路径中寻找是否存在该可执行文件,如果存在,则会立即执行改文件,但若找不到该文件,则会去搜索系统中环境变量Path所保存的路径底下是否存在该文件,若存在则执行,若不存在则会报错。

如果不添加环境变量,则只有在控制台中切换到javac所在的文件中才能执行这一文件,而此时,若要编译的java文件不存在javac所在的文件中,则会导致javac这一exe文件无法编译该java文件。所以最好的就是用在path环境变量中添加bin路径,这样java文件无论存在哪就都可以用javac文件编译了,也就是让控制台无论在哪个路径底下都能找到java开发工具

那为什么添加JAVA_HOME这一环境变量再把其变量值改为bin所在的路径然后在Path环境变量中写%JAVA_HOME%\bin呢?

其中JAVA_HOME只是其的一个环境变量名,叫什么并不重要,而%JAVA_HOME%中两百分号表示引用,所以这里表示引用JAVA_HOME这一环境变量,而该环境变量的变量值又是bin所在的路径,所以%JAVA_HOME%\bin就也一样表示bin文件夹中javac和java等exe文件所存储的位置了。

那为啥明明可以一步解决直接在Path环境变量中加入完整的bin中文件的路径却还要这么做呢?

其实对bin文件的位置做了修改,则Path环境变量里的值存的那个路径还得修改,但Path环境变量中还有很多其他的东西,对Path路径修改如果不小心改了其他的东西则可能会影响系统其他东西的运行,而自己创建的JAVA_HOME路径专门用于存放bin文件夹的路径则方便以后的修改,防止误改,起到了避免去修改Path环境变量信息的作用。其实简单点说就是把bin中文件的路径单独拎出来,更方便日后的修改。

Java编译运行时要注意的:

一个java源文件可以有在不同类中有多个main方法,但这些类(可以多个类)中只能有一个public类,且在编译时,javac  文件名.java中的的文件名必须与public类类名一致,如果不一致就会报出:类…是公共的,应在…名为… .java的文件中声明。当有一个java源文件中如果各类都有main方法,则在运行时只需,要运行某个类中的main只需   java main所在类类名   即可。

Java编写代码时需注意的:

1.源文件要使用utf-8编码

2.每行宽度不要超过80字符

3.编写时有次行风格和行尾风格,但最好使用行尾风格。

4.写注释。

标识符注意事项:

1、组成元素只能是字母、下划线、数字、美元符$。

2、不能以数字开头。

3、严格区分大小写(Java里不只是标识符,所有的东西都区分大小写)。

4、注意让人能够见名思意。

6、标识符不能是关键字、保留字,但可以含关键字或保留字。

标识符命名规范:

1、类名、接口名:所有单词的首字母大写,其他小写。如:MyDog

2、变量名、函数名:首单词全部小写,其他单词的首字母大写。如:pigWeight

3、包名:全部小写               格式:com.公司名.项目名.业务模块名

4、常量名:全部大写且单词与单词间用下划线分割。如:MY_NAME

关键字:

在Java中有特殊含义的标识符成为关键字,一般用于表示一个程序的结构或者数据类型。变量名、方法名、类名、包名 都不能是关键字。

关于注释:

1.作为一名合格的程序员必须有良好的注释习惯,其作用是:增强代码可读性,方便后期维护。

2.注释包括三种:单行注释,多行注释,文档注释。

其中文档注释在此多提几句:其注释的内容是可以通过java工具javadoc.exe所解析的,操作方式:javadoc -d 目录 -标签(如:version,author等) …… -标签  文件名.java(.java也是属于文件名的部分,但由于有人没明白在此处和上面一些部分就分开了…)   在执行完这句后就可以在该指定的目录中找到index.html查看了(注意若执行的代码中的那个目录还不存在,则会自动创建)

关于注释中要注意的:

1、单行注释可以嵌套使用,但多行注释不行,如下图中可以看到后面的那个e未注释掉,是因为多行注释是从第一个/*开始到找到一个*/之间的内容看成注释部分,因此中间那个/*被视为了注释内容。

常量中的易错点:

1、字符常量:单个字符用单引号引起的常量,诸如:'12'、'ab' 这些都是错误的。

2、字符串常量:用双引号引起来的内容,如:"a"也是

数据类型:

1.String不是基本数据类型

2.存储long类型数据应该在数据后加L(小写l也行,但不建议,因为容易让人看错)

3.存储float类型数据应该在数据后加f(大写F也行) 

4.boolean类型占一个字节

5.浮点数可以只有小数位,如 double num = .123;

6.关于浮点数精度问题:浮点数运算时得到的数会不准确,得到的值是一个非常接近推断的值的数(也有时就是推断的数),如:double num1= 8.1/3;此时得到的就不是2.7而是一个非常接近于2.7的数,当一个经过运算的得到的小数与另一个小数比较是否相等时,我们会判断他们的差的绝对值是否小于某个数(这个数根据具体的需求确定),如:double num2 = 2.7;  如果直接num1 == num2 则此处返回的为false,Math.abs(num1-num2) < 0.01; 则返回的为true,此处若业务逻辑是金额,则只需两个差的绝对值小于0.01则可以判断两个金额是相等的。

数据类型转化需要注意的:

1、凡是byte short char类型的数据在运算过程中都会自动转化成int类型的数据进行运算。                   如:byte a = 1; byte b = 2; byte c = a+b; 会报错                                                                               如:System.out.println('a'+1);输出结果为98

2、两个不同数据类型的数据运算时,结果的数据类型取决于大的那个数据的类型。                              如:int a = 1;  long b = 2L; int sum = a + b;也会报错。                                                                      如:System.out.println('a'+1);输出结果为98

3、byte b = 3;b += 2; 此处 b += 2; 等于 b = b + 2;但是在此处却不会报错,因为该复合运算底层有一个强制类型转换(b++;也不会报错的原因也是底层有一个强制类型转换)

一个整数在默认情况下为整型,但看到下面几句代码,是否会有一些疑问呢?

int i = 5; byte b = i;编译时会出现报错的情况,

 但  byte b = 5; 编译时却没有问题。

既然5默认是一个整型,而i也是一个整型,那究竟是为什么只有上面的代码报错而下面的代码却没有报错呢?原因其实是:java编译器在编译时能够读取一个常量的值,而却不能读取一个变量的值。在byte b = 5;这里5是一个常量,编译器能够读取到其值,检测到其值能够被byte类型存下是则能通过编译,而byte b = i;时此时i是一个变量,编译器只能检测到它是一个int类型的值而不会知道值具体是多少(在这我们还应知道变量在编译时其实是不会开辟内存空间的,只有在java虚拟机运行到该语句时才会开辟内存空间存储那个值),就会在编译时报错显示精度可能会丢失。

基本数据类型与String类型的转换:

int i = 1;①String str = i + ""; ②String str1 = Integer.toString(i);③String str2 = String.valueOf(i);

但如果②方法中如果i本身是是包装类的类型则是:Integer i = 1; String str2 = i.toString();即可

字符串类型转基本数据类型,用基本数据类型包装类的parseXXX方法,如:int i = Integer.parseInt("54");但如果要String类装包装类则Integer integer = Integer.parseInt("54");或者是 Integer integer = new Integer(str);其中str要为数字的字符串

顺便多说一个,提取字符串中某个字符的方法:                                                                               字符串(或字符串变量名).charAt(要提取字符所在位置);   如:"hawa".charAt(0); //h  

转义字符需注意的:

在Windows操作系统下只有\r\n一起用(且顺序也要先\r后\n)才能实现回车换行,其他操作系统只需\n即可。

算术运算符中要注意的例子:

int i = 0;i = i++; System.out.println(i);输出结果为0。

要知道此题答案需要知道jvm虚拟机的后自增原理。i++;这一句在jvm虚拟机中会先创建一个临时变量,int temp = i; 然后再i = i +1;最后如果有赋值语句时再把temp给出去这里则是 i = temp;              虽然这波操作看起来有点奇怪,但是其实这才符合算术运算符的优先顺序,而以往刚开始学习说的先使用后++反而有点问题,因为++的优先级是高于=的,如果按大多数的地方理解后++就是先使用后++的话 这里就不太符合优先级了。     

同理,后置--也会有同样的现象。       

复合赋值运算符需注意:   

 byte b = 1; b = b+1;则会报错                                                                                                           而 byte b = 1; b += 1;则是不会报错的,因为java编译器会自动强制类型转换

其他复合赋值运算符也有相同现象。

位操作符中一条重要规律: 

如果一个操作数异或另一个数两次,那结果还是原来那个数据。(可用于加密文件)

位操作符有关的两道经典题:

不创建临时变量交换两个变量的值。int a = 1; int b = 2;                                                                    解法一:a = a + b; b = a - b; a = a - b;                                                                                            缺点: 两个int 类型的值相加放在int中可能会溢出 

解法二:a = a ^ b; b = a ^ b; a = a ^ b; (用到了上述重要规律) 经过a = 1 ^ 2;后第二句相当于b=1 ^ 2 ^ 2  则b换成了1,而第三句相当于 a = 1 ^ 2 ^ 1 则a换成了2                                              缺点:逻辑不清晰                                                                                  

取出一个数二进制位的后六位

如:   00000000 00000000 00000000 00011011                                                                              只需 &00000000 00000000 00000000 00111111                                                                            取某个数二进制的后a位只需与上一个后a位全是1,其他位全是0的二进制

移位操作符及相应经典面试题

<<     左移:对应二进制位向左移动,末位补0

      注:正数向左移动n位相当于乘以2的n次方(除了首位去掉的是1或却掉后是1的情况)

>>     右移:对应二进制位向右移动,首位补充符号位

      注:正数向右移动n位相当于除以2的n次方

>>>    无符号右移(逻辑右移):对应二进制位向右移动,首位补0

移位操作符的特点:效率高

以最高效率算出2乘以8的值:答案2<<3

一个负数得到其绝对值的方法:

1.负数补码,-1,所有位都取反,得到其绝对值,2.负数,所有位取反,+1,得到其绝对值

例如:得到-1的绝对值:int a = -1;  (~(a)) + 1 的结果即是-1的绝对值了。在有些地方也可以看到(a^( a>>31)) + a>>>31; 也是可以的,其中a>>>31位,a是负数的话结果就是1,而a>>31位得到的就是32个1的二进制序列,而一个数^一个全为1的二进制序列得到的结果就是相当于 所有位都取反。但后面这种的好处就是它无论是对正数还是负数都能得到其绝对值,但前面那种方法只能对负数取其绝对值。

关于==号

1、如果==号两边是基本数据类型,则比较的是两个内容是否相等

2、如果==号两边是引用数据类型时,则比较的是两个数据的地址

关于switch语句中需要注意的:

1.switch语句适用的数据类型:byte、short、char、int、String、枚举类型

2.case后只能接常量或者常量表达式(如:'a' + 1)(如果switch中的类型是枚举类型,那么此处写枚举值即可  注意:不需枚举类类名.枚举值,只需写枚举值即可)

3.switch表达式中的类型必须与case常量的类型一致,或者其中一个是可以自动转化为另一个才可以。

//基本格式
switch (expression) { 

    case value1: 

         break; 

    case value2: 

         break;

    ... case valueN: 

         break; 

    default: 

}

关于打印乘法口诀表时怎么对齐:

我知道的有两种,若还有其他的,欢迎评论区留言:                                                                        System.out.printf("%d*%d=%-2d ",i,j,i*j);              //%-2d表示输出2列左对齐
System.out.print(i + "*" + j + "=" + i*j + "\t");   //\t是水平制表符,用于对齐

关于方法调用过程中内存的变化:

在方法调用时,就会在栈区开辟一片空间(栈空间),当方法执行完毕时该方法的栈空间就会销毁,main方法是最先开辟,最晚销毁的。

关于方法需要注意的:

1.方法里面不能定义方法(方法不能嵌套定义)。

2.若方法中传递过来的是引用类型,要注意方法中判断该引用类型的数据是否为null,否则会导致空指针异常,当传入的是数组这种引用类型时最好还判断 arr.length > 0;  不然下面  arr[索引值]  时会导致越界访问。 以下举个例子方便大家记忆:

//未进行任何优化得到一个数组中的最大值
public int max(int[] arr) {
    //如果arr.length = 0 则arr[0]会出现越界访问
    //如果arr为null则arr[0]及arr.length会出现空指针异常
    int max = arr[0];
	for(int i = 0; i < arr.length; i++) {
		if(max < arr[i]) {
			max = arr[i];
		}
	}
	return max;
}
//返回类型用Integer使得如果arr为null或arr.length为0时可以返回null
public Integer max(int[] arr) {
    //避免出现越界访问及空指针异常
	if(arr != null && arr.length > 0) {
		int max = arr[0];
		for(int i = 0; i < arr.length; i++) {
			if(max < arr[i]) {
				max = arr[i];
			}
		}
		return max;
	} else {
		return null;
	}
}

关于方法重载:

1.方法名一样

2.方法的参数列表不一样

3.方法的返回类型及访问修饰符任意

注意:只有返回类型不同不能算重载,要的是参数列表不同,返回类型同不同无所谓

全局变量与局部变量的作用域:

局部变量:即除属性以外的变量,作用域为定义该变量的语句所在的代码块。

全局变量:即属性,作用域为所在类体。若在其他类中创建了该全局变量所在类的实例化对象,则可以通过该对象在其他类中使用。

关于局部变量和全局变量要注意的:

1.局部变量没有默认值,未赋值不可以直接使用。但全局变量(属性)有默认值,可以直接使用。

2.局部变量不能添加访问修饰符,但全局变量可以。

for循环需注意的:

for(int i = 0; 条件判断部分; i++) {}                                                                                                      1.若在for循环的括号内定义i  ,则出了for循环体i则不能使用。                                                          2.for循环括号内的内容除了条件判断的部分和分号以外都可以写在外面(上面的 int i = 0 与 i++ )可以是 int i = 0; for(;条件判断部分;){ i++; }                                                                                          3.如果条件判断部分不写,则是表示死循环。

break与continue在嵌套循环中如何作用于不是那个默认的当前循环:

只需在循环前(在循环的上一行也行)添加标签名(命名只要符合标识符的命名即可)加上冒号,然后在break/continue的后面加上标签名即可作用于想要作用的循环。(平常尽量不要使用标签,因为可读性会降低)

以下举一个简单的例子,执行后发现,只会打印一个“略略略”:

outer:for(int i = 1; i<=9; i++) {
			inner:for(int j = 1; j<=i; j++) {
				      System.out.println("略略略"); 
				      break outer;
			      }
      }
//或者像下面这样也一样
outer:
for(int i = 1; i<=9; i++) {
    inner:
    for(int j = 1; j<=i; j++) {
        System.out.println("略略略"); 
        break outer;
    }
}

break和continue后面不能直接跟语句:

因为如果直接跟语句,即break/continue与其后面的语句的执行条件是一样的,其后的语句是在break/continue后面执行,但是break/continue以后后面的语句是没有机会执行的,故其后紧接着的语句相当于废的语句。下面列举一个简单例子,其在编译时是会出错的。

for(int i = 0;i<2;i++) {
			break;
			System.out.println(i);
		}

创建一维数组相关重要知识点:

动态初始化:

1.声明数组的同时创建数组:int[] arr = new int[3];(也可以 int arr[] = new int[3];),但建议写前一个,因为arr的数据类型就是int[],写在前面就跟声明其他数据类型一样写法(数据类型 变量名)    左边int表示只能存储int类型的数据,[]表示是一个数组,arr是变量名                                              右边new是创建一个对象的关键字,int表示该数组对象只能存放int类型,[]表示数组类型,3表示数组能存放的元素个数                                                                                                                          2.先声明数组再创建数组:int[] arr;(也可以 int arr[];)  arr = new int[3];    这其实就是将上面那种声明的同时创建数组的方法拆开成了两步,效果是一样的。但在这里还要知道,在创建这个数组前是不会给这个数组分配空间的。

要真正了解数组还需知道数组在内存中是怎么存的,以下这张图能帮助我们很好的理解。            其中数组名arr存放在栈中,而new关键字创建的数组对象则存放在堆中。

 静态初始化:int[] arr = {元素};(同样也可以int arr[] = {元素};)这种初始化方法在内存中产生的效果是一样的。但注意不可以  int[] arr; arr = {元素};

两种初始化方法看需求选择即可(一般刚开始就确定了数据则使用静态初始化)。

数组中要注意的:

1.如果创建数组后没有赋值,它本身是有默认初始值的:基本数据类型byte  0, short  0, int  0, float  0.0,  double 0.0, char  '0', boolean  false, 引用数据类型 如String,Object等的数组默认值都为null

2.注意索引值不要越界。

3.注意还有一种不常见的创建数组的方式:int[] arr = new int[]{1, 2, 3};   且要注意此时不能指定个数如:int[] arr = new int[2]{1, 2};是错误的。

如何给原始数组是静态初始化的数组扩容?

创建一个大的数组,并将原来数组的内容拷贝到该数组,最后使原来数组的引用的指向新的数组。

几种排序的方式:

1.直接排序(选择排序)

public static void main(String[] args) {
    	int[] arr = {1, 3, 9, 6, 2, 7, 5};
    	selectSort(arr);
    	for(int i = 0; i < arr.length; i++) {
    		System.out.print(arr[i] + " ");
    	}
    }

    public static void selectSort(int[] arr) {
    	int temp = arr[0];
    	for(int i = 0; i < arr.length - 1; i++) {
    		for(int j = i + 1; j < arr.length; j++) {
    			if(arr[j] > arr[i]) {
    				temp = arr[i];
    			    arr[i] = arr[j];
    			    arr[j] = temp;
    			}
    		}
    	}
    }

2.已进行过优化的冒泡排序

	public static void main顺序冒泡(String[] args) {
	 	int[] arr = {6, 9, 4, 2, 5, 7, 8, 3, 1};
	 	for(int j = 0; j < arr.length-1; j++) {
	 		boolean flag = true;//搞个标记以实现优化(看在下趟排序时是否进行了交换,如果没有则说明不用再排了)
	 		for(int i = 0; i < arr.length-1-j; i++) {
		 		if(arr[i] > arr[i+1]) {
		 			int temp = arr[i];
		 			arr[i] = arr[i+1];
		 			arr[i+1] = temp;
		 			//如果在一次冒泡排序过程中经过了交换
		 			flag = false;
		 		}
	 		}
	 		//判断是否需要继续排序
	 		if(flag) {
	 			break;
	 		}
	 	}
	 	for(int i : arr) {
	 		System.out.print(i + " ");
	 	}
	 }

上述第一种方法是从大到小,第二种方法从小到大排序,但这两种方法各种都可以完成从大到小或从小到大排序,此处就不赘述了。

在有序数组中插入一个数,使插入后任然有序

public static void main(String[] args) {
	 	int[] arr = {10, 12, 23, 90};
	 	//需要添加的元素
	 	int addNum = 23;
	 	//搞个新数组以实现扩容
	 	int[] arrNew = new int[arr.length+1];
	 	boolean flag = true;//用于判断是否已经添加
	 	for(int i = 0, j = 0; i < arrNew.length; i++) {
	 		if(flag) {
	 			//使比添加的元素更小的元素在前面
	 			if(arr[j] < addNum) {
	 				arrNew[i] = arr[j];
	 				j++;//如果在for循环中j++的话,当走下面else添加addNum时j也会++,下面再提 
                                                                                    
	 			} else {
	 				arrNew[i] = addNum;
	 				flag = false;
	 				//如果这里也有j++的话,那么就会漏掉此时的arr[j]
	 			}
	 		} else {
	 			arrNew[i] = arr[j];
	 			j++;
	 		}
	 	}
	 	arr = arrNew;
	 	for(int i : arr) {
	 		System.out.print(i + " ");
	 	}
	 }

或者下面这样:

public static void main(String[] args) {
	    int[] arr = {12, 45, 67, 89};
	  	int addNum = 34;
	  	int i = 0;
	  	int[] arrNew = new int[arr.length+1];
	  	for(i = 0; i < arrNew.length; i++) {
	  		if(arr[i] < addNum) {
	  			arrNew[i] = arr[i];
	  		} else {
	  			break;
	  		}
	  	}
	  	arrNew[i] = addNum;
	  	for(int j = arrNew.length-1; j > i; j--) {
	  		arrNew[j] = arr[j-1];
	  	}
	  	arr = arrNew;
	 	for(int num : arr) {
	 		System.out.print(num + " ");
	 	}
	}

二分(折半)查找法:

import java.util.*;
class Demo {
    public static void main(String[] args) {
    	int[] arr = {1,2,3,4,5,6,7,8,9};
    	Scanner scanner = new Scanner(System.in);
    	int num = scanner.nextInt();
    	int a = findNum(arr,num);
    	if(a != -1) {
    		System.out.println("找到了,索引值为" + a);
    	}else {
    		System.out.println("没找到");
    	}
    }

    public static int findNum(int[] arr,int a) {
    	int left = 0;
    	int right = arr.length-1;
    	int mid = (left+right)/2;
    	while(left<=right) {
    		if(a>arr[mid]) {
    		    left = mid+1;
    		    mid = (left+right)/2;
    	    }else if(a<arr[mid]) {
    		    right = mid-1;
    		    mid = (left+right)/2;
    	    }else {
    		    return mid;
    	    }
    	}
    	return -1;
    }
}

二维数组:

二维数组就是一个存放数组的数组。可以把二维数组看成一个一维数组里每个元素是一个一维数组。其中每一个一维数组的元素个数可以不同。二维数组跟一维数组一样,可以先声明再创建。

int[][] arr = new int[2][3];创建的是一个二维数组,但实际上相当于创建了一个含两个元素的数组,其中2就是这个二维数组的元素个数(用arr.length可得到),这两个元素又分别是一个含有三个元素的数组(二维数组中每个一维数组的元素可以用arr[i].length得到)。

上面的int[][] arr = new int[2][3];(写成int arr[][] = new int[2][3];也是一样的,还可以写成int[] arr[] = new int[2][3];   但自己写最好还是别这么写)如果其二维数组中的一维数组的元素个数不确定或不同,则不写, 如:int[][] arr = new int[2][];此时这句的意思是创建二维数组,并给定了二维数组中一维数组的元素,每一个一维数组通过arr[i] 来指向,但此时并没有给里面的一维数组开辟空间,arr[i]的内容为null。如果要对其初始化可以arr[i] = new arr[元素个数],但注意不可以arr[i] = {元素};,这里与一维数组的先声明再创建类似,一位数组不可以先声明,再静态初始化。

注意:可以 int[][] arr = { {1, 2}, {1, 2, 3},{100}}; 但不可以int[][] arr = { {1, 2}, {1, 2, 3},100};   大括号不能省,省了类型

;