一.数组
数组是最为常见的一种数据结构,是相同类型的、用一个标识符封装到一起的基本类型数据序列或对象序列。
数组是具有相同数据类型的一组数据的集合,根据维数不同可以分为一维数组、二维数组和多维数组。大家可以将一维看作直线,二维看作平面、三维看成立体空间。
一、一维数组
1. 创建一维数组
数组作为对象允许使用new关键字进行内存分配。在使用数组之前,必须首先定义数组变量所属的类型,即声明数组。
声明数组有两种形式,在之前的篇章中也有所提及,语法格式如下:
数组元素类型 数组名[];
数据元素类型[] 数组名;
程序员在编写代码时习惯使用第二种方法,需要注意的声明时是[]中不能添加任何数据。下面是一个声明数组的例子,两种声明方式都正确,不同数据要声明不同类型的数组:
int arr[]; //声明int型数组,数组中每个元素都是int型数值
String[] str; //声明String数组,数组中每个元素都是String数组
声明数组后还不能访问它的任何元素,要想真正使用数组还要为其分配内存空间,且分配内存空间时必须指明数组的长度。语法格式如下:
数组名 = new 数组元素类型[数组元素个数];
下面举一个例子,为数组分配一个大小为5的内存空间:
arr = new int[5];
一维数组arr的存储状态如下图:
arr[0]
arr[1]
arr[2]
arr[3]
arr[4]
括号中的0、1、2、3、4表示数组的下标。需要注意的是,下标从0开始,到数组长度-1为止。
当然也可以直接声明并分配内存,如下:
int[] week = new int[7];
上述代码创建了一个一维数组week,并指定了数组长度为7。
还有一点需要注意的是,使用new关键字为数组分配内存时,数组中各个元素的初始化值都为0。比如上述代码使用new关键字创建了长度为7的week数组,那么数组中的元素可以表示为[0, 0, 0, 0, 0, 0, 0],这就是一个一维数组,数组中的每一个元素都初始化为0。
2. 初始化一维数组
前面说的初始化是使用new关键字自动初始化,数组也可以与基本数据类型一样由程序员进行初始化操作,可以分别初始化数组中的每个元素。
数组初始化的方式有两种,下面用一个例子来说明:
int arr1[] = new int[]{1,2,3,4,5,6}; //第一种
int arr2[] = {10,11,12,13,14}; //第二种
数组初始化的方式是:把数据包括在大括号之内,中间用逗号分开数组中的元素的值,系统自动为数组分配一定的空间。上述第一种创建了6个元素的数组,其值依次为1、2、3、4、5、6,第二种创建了5个元素的数组,其值依次为10、11、12、13、14。
下面我们结合上篇的流程控制来举一个一维数组数组的例子。
eg:求一维数组中各元素的和
1 public classSumNum {2
3 public static voidmain(String[] args) {4 int[] num = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; //创建并初始化数组
5 int sum = 0; //存储累加和
6
7 for(int i=0; i
9 System.out.print(num[i] + "="); //输出等号
10 } else{11 System.out.print(num[i] + "+"); //输出加号
12 }13 sum +=num[i];14 }15
16 System.out.print(sum);17 }18
19 }
其中,num是数组名,这个数组总长是10,但一般在编程中,可能会出现需要修改数组长度的时候,此时假设for循环中写的是i<10,那么我们在修改代码时可能会忽略这个地方导致出现意想不到的错误,所以我们在编写代码时,要习惯性使用数组名.length来获取数组的长度,如上述代码中的num.length其实就是10。而下一句中的num.length-1是因为数组下标是从0开始的,当我们获取最后一个元素时,数组下标为总长减一,故num.length-1就是最后一个元素10的下标为9。最后的运行结果为:
二、多维数组
1. 二维数组
如果一维数组中的各个元素仍是一维数组,那么它就是一个二维数组。二维数组常用于表示表,表中的信息以行和列的形式组织,第一个下标代表元素所在的行,第二个下标代码元素所在的列。或用线性代数的知识来说,二维数组就是一个矩阵,第一个下标代表矩阵的行,第二个下标代表矩阵的列。
二维数组的声明也有两种方式,语法格式如下:
数组元素类型 数组名[][];
数组元素类型[][] 数组名;
与一维数组一样,如果声明时没有分配内存空间,同样也要使用关键字new来分配内存。
二维数组可以看作由多个一维数组组成,在给二维数组分配内存时,可以为这些一维数组同时分配相同的内存。第一个中括号中的数组是一维数组的个数,第二个中括号中是一维数组的长度。
int arr = new int[2][4];
上述代码就是一个典型的声明一个二维数组arr并为其分配内存空间,分配后arr拥有两个长度为4的一维数组,内存分配如下图:
arr[0][0]
arr[0][1]
arr[0][2]
arr[0][3]
arr[1][0]
arr[1][1]
arr[1][2]
arr[1][3]
我们也可以为每一维单独分配内存。如下:
int[][] a = new int[2][];
a[0] = new int[2];
a[1] = new int[3];
这样我们就可以得到一个二维数组,第一个一维数组长度为2,第二个一维数组长度为3。
二维数组的初始化与一维数组类似,同样可以使用大括号完成二维数组的初始化。如下:
int arr[][] = {{3,0},{5,7},{6,9}};
这就是一个arr[3][2]的数组,有三个一维数组,每个一维数组的长度都为2。但要明确下标是从开始的,比如arr[1][0]=5,指的是第二个一维数组的第一个元素为5。故我们也可以直接给arr[x][y]赋值,如给arr[1]的第二个元素赋值:
arr[1][1] = 50;
那么上述数组就变成了如下形式:
int arr[][] = {{3,0},{5,50},{6,9}};
2. 三维数组
对于三维数组,想必各位已经能推算出来了,一维用一个括号,二维用两个括号,那么三维就用三个括号。
int arr[][][] = new int[][][]{
{{1,2,3},{4,5,6}},
{{7,8,9},{10,11,12}},
{{13,14,15},{16,17,18}}
};
三、数组的基本操作
1. 遍历数组
遍历数组就是获取数组中的每个元素,通常遍历数组都是用for循环实现的。
遍历一维数组只需用一个for循环即可实现,如下例:
int[] week = {1,2,3,4,5,6,7}for(int i=0; i<7; i++) {
System.out.println(week[i]);
}
还有另一种遍历方法就是使用foreach语句,这个在上一篇最后已经提到了,这里就不再过多赘述了,直接上代码:
for(intarr : week) {
System.out.println(arr);
}
遍历二维数组比一维稍微麻烦一点,需使用双层循环,如下例:
int a[][] = {{1,2},{3,4,5},{6,7,8,9}};for(int i=0; i
System.out.print(a[i][j]+ " "); //输出
}
System.out.println();//换行
}
需要注意的是,第一个for循环是遍历行的,所以是长度是a.length,而第二个for循环是遍历列的,长度是a[i].length。
再强调一次print和println的区别,print是输出但不换行,println是输出并换行,所以输出结果如下:
2. 对数组进行排序
数组可以通过使用java.util包中的Arrays类来实现排序,sort()方法提供了许多重载形式,可对任意类型数组进行升序排序。语法格式为 Arrays.sort(object);
eg:创建一数组,排序后输出。
1 public classTaxis {2
3 public static voidmain(String[] args) {4 int[] arr = new int[] {17, 21, 6, 59, 31, 13, 3};5
6 System.out.println("原始数组:");7 for(int i=0; i
11 Arrays.sort(arr); //按字典顺序排序
12
13 System.out.println("\n排序后的数组:");14 for(int i=0 ; i
19 }
Java语言中的String类型数组排序算法是根据字典编排顺序排序的,因此数字排在字母前面,大写字母排在小写字母前面。运行结果如下:
3. 其它操作
数组还有许多操作,比如填充替换数组元素的fill方法、复制数组的copyOf方法等等,包括Arrays类也还提供了其它操作数组的方法,大家可以通过查阅资料或API来学习它们的使用方法。
还有一个很重要的知识点就是排序算法,上面已经出现了多重循环,那么就可以由此引出诸如冒泡排序、选择排序等等的许多排序算法,如果以后有时间我可能会总结一下各类排序算法,常见的有冒泡、选择、直接插入、快速、归并、希尔、堆排序等等
二。String类
常量池:
静态常量池:指的是编译时常量池,我们不关注
字符串常量池:在java8之后放在了堆里
运行时常量池:放在了元空间里,元空间放在了主存里(不在堆里了,不占有jvm内存空间)
使用字符串常量池(也就是当我们用类似String a = “xiaoming”;)。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串!
但当我们使用new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后放池里一份(所以new一般是new两个对象),然后返回堆中的地址。
举个例子来理解String字符串:
String a = “xiaoming”;
final String m = “xiao”;
final String n = “ming”;
String b = “xiao”;
String c = “ming”;
String d = “xiao” + “ming”;
String e= b + c;
String f = m+n;
String g = new String(“xiaoming”);
System.out.println((a == d));
System.out.println((a == e));
System.out.println((a == f));
System.out.println((a == g));
输出结果:
true
false
true
false
首先,==比的都是地址,第一种情况下,由于编译期会优化,优化后的 d = “xiaoming”,由于字符串常量池的原因,d会指向已经创建好的a,所以地址不变返回true。
第二种情况,JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,所以会新建一个变量,a和e的地址就不相同了,返回true。
第三种情况,被final修饰后的变量不可再赋值,所以会返回true。
第四种情况,最常见,直接新建一个变量在堆里,二者地址显然不同,返回false。
String、StringBuffer、StringBuilder的区别
(1)可变与不可变:String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变)。
(2)是否多线程安全:String中的对象是不可变的,也就可以理解为常量,显然线程安全。StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer 中的方法大都采用了synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是非线程安全的。
(3)String、StringBuilder、StringBuffer三者的执行效率:
StringBuilder > StringBuffer > String 当然这个是相对的,不一定在所有情况下都是这样。比如String str = “hello”+ "world"的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:
当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;
当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。