一、数组基本用法
1.什么是数组
数组就是一组相同类型的元素的集合
当我们需要定义多个变量时
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int e = 50;
如果定义几十甚至几百个变量,那么就不能这样创建变量了,这个时候我们就需要用到数组了
int[] array = {10,20,30,40,50};
这就是创建了一个简单的整形数组
2.数组的创建
C语言的创建数组方式
int array[] = {1,2,3,4,5};
java的创建数组方式
int[] array = {1,2,3,4,5};
其实java中也可以使用C语言的创建数组方式,但是我们还是更推荐写成 int[] arr 的形式. int和 [] 是一个整体。这也是是一种语法规范吧!
3.定义数组的方式
定义数组共有三种方式
第一种:静态初始化
int[] array = {1,2,3,4,5};
第二种:动态初始化
int[] array = new int[]{1,2,3,4,5};
第三种
int[] array = new int[5];
注意:
1.静态初始化的时候, 数组元素个数和初始化数据的格式是一致的。
2.第一种和第二种定义,等号右边的 [ ] 的里面不可以出现数字。
3.第一种和第二种数组的大小就是里面数据的个数。
4.数组的使用
4.1 获取数组的长度
通过 数组名.length 就可以获取数组的长度
注意:数组的下标是从0开始的,使用 [ ] 就可以访问数组的元素,技能读取数据也能修改数据。
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5};
array[0] = 10;
System.out.println(array[0]);
}
}
运行结果
4.2 遍历数组的两种方式
1.for循环遍历
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
}
}
2.foreach遍历
for (int x:array) {
System.out.print(x+" ");
}
4.3 for循环和foreach(增强for循环)的区别
for循环是可以拿到下标的
foreach是拿不到下标的
所以以后要看情况使用for循环和foreach
foreach的遍历原理:遍历array数组里面的每一个元素,把每一个元素取出来,然后赋值给x,最后打印x,直到array数组全部遍历完。
两种遍历的运行结果
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+" ");
}
System.out.println();
for (int x:array) {
System.out.print(x+" ");
}
}
}
4.4 以字符串打印数组
数组的工具类:import java.util.Arrays
Arrays.toString(数组名)
把你需要打印的数组放到toString函数里面,那么就会把当前的数组转变为字符串进行输出!所以调用这个函数后,它的返回值就是一个字符串。
来看一个例子
运行结果:可以看到数组被连同方括号都被转换成字符串了
4.5 数组下标越界访问异常
当你创建的数组长度只有5,你非要访问5下标(数值的下标是从0开始) 。那就会出现下标越界访问异常。
就比如这里数组只有5个元素最大下标是4,而而要去访问下标5就是出现数组越界访问异常。
5.数组在内存中的存储
在JVM(JAVA虚拟机)中共有5块内存,而我们平常所说的栈就是JAVA虚拟栈。局部变量和数组名都是在栈上开辟内存的。
数组变量是一个引用
引用一般指向一个对象(当前你可以理解为,等号右边的就是数组对象)
来看一段代码
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5};
int[] array2 = new int[5];
System.out.println(array);
System.out.println(array2);
}
}
运行结果
运行后打印出两个地址
注意:这两个地址不是真正的地址,他是通过正式的地址hash得到的
但是你可以把它当做一个真实的地址,这两个地址是唯一的(安全性)
二、数组作为方法的参数
1.基本用法
代码如下(示例):
public class Test{
public static void prints(int[] arr){
for (int x:arr) {
System.out.print(x+" ");
}
}
public static void main(String[] args){
int[] array = {1,2,3,4,5};
prints(array);
}
}
运行结果
我这里用的是foreach遍历整个数组
- int[ ] arr 是函数的形参, int[ ] array 是函数实参.
- 如果需要获取到数组长度, 同样可以使用 a.length
2.理解引用类型
2.1 传内置类型
代码示例如下(传内置类型):
public class Test{
public static void exchange(int n){
n = 200;
System.out.println("n= "+n);
}
public static void main(String[] args){
int a = 100;
exchange(a);
System.out.println("a= "+a);
}
}
运行结果:
我们发现修改形参n的值,不影响实参a的值
2.2 传引用类型
代码示例(传数组):
public class Test{
public static void exchange(int[] arr){
int tmp = arr[0];
arr[0] = arr[1];
arr[1] = tmp;
}
public static void main(String[] args){
int[] array = {10,20,30,40};
System.out.println("交换前");
System.out.println("array[0] = "+array[0]+" array[1] = "+array[1]);
exchange(array);
System.out.println("交换后");
System.out.println("array[0] = "+array[0]+" array[1] = "+array[1]);
}
}
运行结果:
我们发现数组组为参数传参的话就能修改变量的值,我们前面说过array存储的是地址,也就是说我们传的是引用。
什么是引用?
- 引用相当于一个 “别名”, 也可以理解成一个指针.
- 创建一个引用只是相当于创建了一个很小的变量, 这个变量保存了一个整数,
这个整数表示内存中的一个地址
我们再通过图来了解一下为社么传引用就能修改实参呢?
传引用实际上就是传的地址,通过地址找到同一块空间。修改同一块地址上的数据,自然就修改了。
总结:
所谓的 “引用” 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参,
其实只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).
2.3 认识 null
null在java中表示 ”空引用“ ,表示不指向任何对象,也就是一个无效的引用。
引用类型的0值就是null
来看一个代码:
public class Test{
public static void exchange(int[] arr){
int tmp = arr[0];
arr[0] = arr[1];
arr[1] = tmp;
}
public static void main(String[] args){
int[] array = {10,20,30,40};
exchange(null);
}
}
运行结果:
如果在传引用时传的时null就会报出异常, NullPointerException。null 的作用类似于 C 语言中的 NULL
(空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操作。
那如果我们把数组赋一个 null
public class Test{
public static void main(String[] args){
int[] array = null;
System.out.println(array);
}
}
运行结果
我们发现并没有打印0,而是打印null
那null的数组能不能求长度呢?
public class Test{
public static void main(String[] args){
int[] array = null;
System.out.println(array.length);
}
}
运行结果
我们发现这里也是一个空引用异常,从这我们可以知道,把null 赋给了 array,表示array没有指向任何对象,所以它没在堆上开辟空间,报出空引用异常。所以null.任何东西都会报出异常
总结:
当我们以后遇到 NullPointerException 都是空引用异常
2.4 初识 JVM 内存区域划分
直接来看一幅图简单了解一下JVM的内存区域划分
- 程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址.
- 虚拟机栈(JVM Stack): 重点是存储局部变量表(当然也有其他信息). 我们刚才创建的 int[] arr 这样的存储地址的引用就是在这里保存.
- 本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量.
在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的. - 堆(Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2,3})
- 方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据.方法编译出的的字节码就是保存在这个区域.
- 运行时常量池(Runtime Constant Pool): 是方法区的一部分, 存放字面量(字符串常量)与符号引用. (注意 从JDK1.7 开始, 运行时常量池在堆上)
总结:
1.JVM是多线程的,多线程就意味着高效率。
2.局部变量和引用保存在栈上, new 出的对象保存在堆上.
3.堆的空间非常大, 栈的空间比较小.
4.堆是整个 JVM 共享一个, 而栈每个线程具有一份(一个 Java 程序中可能存在多个栈)
三、 数组作为方法的返回值
假设我们要实现一个方法,把数组里的每个元素都加上1
import java.util.Arrays;
public class Test{
public static void addOne(int[] arr){
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i] + 1;
}
}
public static void main(String[] args){
int[] array = {1,2,3,4,5};
addOne(array);
System.out.println(Arrays.toString(array));
}
}
这种方法当然可以,但这会破坏原有的数组,有时候我们不希望破坏原数组, 就需要在方法内部创建一个新的数组, 并由方法返回出来.
来看一下代码:
import java.util.Arrays;
public class Test{
public static int[] addOne(int[] arr){
int[] array2 = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
array2[i] = arr[i] + 1;
}
return array2;
}
public static void main(String[] args){
int[] array = {1,2,3,4,5};
int[] ret = addOne(array);
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(ret));
}
}
运行结果:
这样的话就不会破坏原有数组了.
另外由于数组是引用类型, 返回的时候只是将这个数组的首地址返回给函数调用者, 没有拷贝数组内容, 从而比较高效。
四、数组的拷贝
1.什么是包
Java 中提供了 java.util.Arrays 包, 其中包含了一些操作数组的常用方法.
那什么是包呢?简单简单点说,假设你煮面吃是不是要自己调配料,而方便面直接就有调料包,拿起来就用,不用像自己煮面一样还要想怎么放才好吃。
像我们很多程序写的过程中不必把所有的细节都自己实现, 已经有大量的标准库(JDK提供好的代码)和海量的第三方库(其他机构组织提供的代码)供我们直接使用. 这些代码就放在一个一个的 “包” 之中.
有了这个包,我们只需要 Arrays. 就能使用JAVA提供的很多和数组相关的方法。
1. for循环拷贝
新创建一个数组,再利用for循环把以前的数组拷贝到新数组里。
注意:新开辟的数组大小一定要大于或者等于原数组的大小。
import java.util.Arrays;
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5};
int[] arr = new int[array.length];
for (int i = 0; i < array.length; i++) {
arr[i] = array[i];
}
System.out.println(Arrays.toString(arr));
}
}
2. copyof拷贝
这是JAVA提供给我们的一种拷贝数组的方法,先来看一下这个方法的源代码
这个方法需要两个参数,一个要拷贝的数组和新数组的长度。
代码示例:
import java.util.Arrays;
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5};
int[] arr = Arrays.copyOf(array,10);
System.out.println(Arrays.toString(arr));//以字符串方式打印数组
}
}
运行结果:
我们发现如果新数组的长度大于要拷贝数组的长度,多余的位置默认为0.
3. copyOfRange拷贝
同样这也是Arrays提供的一种拷贝数的方法,源代码如图
再来看一个例子
import java.util.Arrays;
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5};
int[] arr = Arrays.copyOfRange(array,1,4);
System.out.println(Arrays.toString(arr));
}
}
运行结果
因为拷贝范围是左闭右开的,所以并没有拷贝到5.
注意:如果超出拷贝数组的范围则超出的元素默认为0,且范围是不能为负数的。
4. arraycopy拷贝
来看一下源代码
使用例子
import java.util.Arrays;
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5,6};
int[] arr = new int[array.length];
System.arraycopy(array,0,arr,0,6);
System.out.println(Arrays.toString(arr));
}
}
运行结果
注意:这里的拷贝和上面的两种方式是不一样的,这种拷贝方式是不能越界的!如果你的数组要拷贝的长度从你的起始下标起大于了数组的长度(无论是起始数组还是目的地数组都是不能越界的),就会出现数组下标越界异常。
4.1 native:本地方法
还有一个重点不知道大家注意没,arraycopy这个方法的源代码是没有具体实现过程的。而且这个方法中有 native 这么一个关键字。这属于本地方法
那么 native 这样的方法有什么特点呢?
1.运行在本地方法栈上
2.底层是由C/C++写的
3.这样的方法速度是非常快的
所以我们以后拷贝数组优先使用的是这种方法,因为它速度快。
5. clone拷贝
数组名.clone直接产生一个数组的副本
import java.util.Arrays;
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5};
int[] arr = array.clone();
System.out.println(Arrays.toString(arr));
}
}
6.深拷贝和浅拷贝
深拷贝:修改拷贝之后的数组不会影响原数组
浅拷贝:修改拷贝之后的数组会影响原数组
如何达到深拷贝:对对象本身进行拷贝
我们刚刚的例子都属于深拷贝
因为JAVA以后是面向对象,数组里一般放的都是对象很少放整形。如果要细说,那么就分基本类型和引用类型。
如果数组里放的是基本数据类型那就是深拷贝,如果数组里放的是引用类型那就是浅拷贝。
五、数组的练习(经典题型)
1.二分查找(折半查找)
二分查找只能用于一组有序数组(升序或者降序)
以升序数组为例, 二分查找的思路是先取中间位置的元素, 看要找的值比中间元素大还是小. 如果小, 就去左边找; 否则就去右边找.
import java.util.Scanner;
public class Test{
public static int binarySearch(int[] array,int n){
if(array == null) return -1;
int left = 0;
int right = array.length-1;
while(left<=right){
int mid = left+(right-left)/2;
if(array[mid]>n){//当要找的数比中间那个数小
right = mid-1;
}else if(array[mid]<n){//当要找的数比中间那个数大
left = mid+1;
}else{
return mid;//找到了返回下标
}
}
return -1;//没找到时
}
public static void main(String[] args){
int[] array = {1,2,3,4,5,6,7,8,9};
System.out.println("请输入年哟查找的数字");
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int ret = binarySearch(array,n);
if(ret == -1){
System.out.println("没找到");
}else{
System.out.println("找到了下标为"+ret);
}
}
}
JAVA还提供了一个二分查找的方法binarySearch
源代码就不看了,和上面的实现方法类似
同样是两个参数,一个是要查找的数组,一个则是要查找的数字。
如果找到了就放回该数字的下标,没找到就放回一个负数。
来看一下代码
import java.util.Arrays;
import java.util.Scanner;
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5,6,7,8,9};
System.out.println("请输入年哟查找的数字");
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int ret = Arrays.binarySearch(array,n);//调用方法
if(ret < 0){
System.out.println("没找到");
}else{
System.out.println("找到了下标为"+ret);
}
}
}
2.数组排序(冒泡排序)
给定一个数组把它排列成升序或者降序
思路:两两相比较交换位置,把最大或者最小的数字放到最后。
import java.util.Arrays;
import java.util.Scanner;
public class Test{
public static void bubbleSort(int[] array){
if(array == null) return;
boolean flag = false;//定义个flag判断数组是否有序
for (int i = 0; i < array.length-1; i++) {
for (int j = 0; j < array.length-1-i; j++) {
if(array[j] > array[j+1]){
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flag = true;
}
}
if(flag != true){//当数组已经是有序时直接跳出循环
break;
}
}
}
public static void main(String[] args){
int[] array = {2,3,1,5,6,4,3,2,1};
System.out.println(Arrays.toString(array));
bubbleSort(array);
System.out.println(Arrays.toString(array));
}
}
3.判断两个数组是否相等
JAVA中提供了一个方法 equals 用来判断两个数组是否相等
可以看到equals的返回类型是布尔类型,相等返回true不相等返回false
代码示例:
import java.util.Arrays;
public class Test{
public static void main(String[] args){
int[] array = {1,2,3,4,5};
int[] arr = {1,2,3,4,5};
boolean bool = Arrays.equals(array,arr);
System.out.println(bool);
}
}
4.数组填充
JAVA中还提供整体填充的方法 fillc ,把数组里的元素替换成指定元素。可适用各种类型。
import java.util.Arrays;
public class Test{
public static void main(String[] args){
int[] arr = {1,2,3,4,5};
Arrays.fill(arr,6);
System.out.println(Arrays.toString(arr));
}
}
运行结果:
该函数还能指定下标替换,记住下标是不能越界的。
运行结果:
六、二维数组
1.定义二维数组
在JAVA中常见的定义数组方式,JAVA中二维数组可以省略列,不能省略行。
int[][] array = {{1,2},{3,4},{5,6}};
int[][] array2 = new int[3][2];
int[][] array3 = new int[][]{{1,2},{3,4},{5,6}};
2.二维数组在内存中的存储
我们知道二维数组其实就是一个特殊的一维数组,那么它在内存中到底是怎么存储的呢?
每一行构成了一个一维数组,数组里的每一个元素存的都是指向每一列一维数组的地址。
每一列的一维数组存的则是指向数据的地址。
3.二维数组的打印方式
3.1 for循环打印
我们知道数组名.length求的是数组的长度,而二维数组名.length求的则是拿到得是每一列的地址,那么求每一列的长度则是 数组名[i].length。
public class Test{
public static void main(String[] args){
int[][] array = {{1,2,3},{3,4,5},{5,6,7}};
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j]+" ");
}
System.out.println();
}
}
}
3.2foreach打印
第一层foreach是遍历整个array数组,因为二维数组的数组名都是得地址,拿到每一列得地址也就是相当于拿到了一个一维数组,通过第二次foreach循环就可以拿到每一个元素
public class Test{
public static void main(String[] args){
int[][] array = {{1,2,3},{3,4,5},{5,6,7}};
for (int[] arr:array) {
for (int x:arr) {
System.out.print(x+" ");
}
System.out.println();
}
}
}
3.3 deepToString(深度打印)
改方法是用来把二维数组转换为字符串的,它的返回类型为String
import java.util.Arrays;
public class Test{
public static void main(String[] args){
int[][] array = {{1,2,3},{3,4,5},{5,6,7}};
System.out.println(Arrays.deepToString(array));
}
}
运行结果:
4.不规则的二维数组
我们前面说过二维数组的列是可以省略的,但如果省略那这个二维数组能打印吗?
public class Test{
public static void main(String[] args){
int[][] array = new int[3][];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
System.out.println(array[i][j]);
}
System.out.println();
}
}
}
运行结果
看到了我们熟悉的空引用异常,那这是什么原因呢?
我们知道二维数组名.length拿到的是地址,而现在数组没有指定列,所以里面存的是null,当我们array[ i ].length 的时候就出现了异常,前面也说过 null.任何东西都会出现异常。
1.不规则二维数组的创建
因为二维数组一般都是行和列都已经写好了,而不规则的二维数组在定义时把列给省略掉了,所以没一行的列数可以有自己定。
public class Test{
public static void main(String[] args){
int[][] array = new int[3][];
array[0] = new int[1];
array[1] = new int[2];
array[2] = new int[4];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j]+" ");
}
System.out.println();
}
}
}
运行结果
public class Test{
public static void main(String[] args){
int[][] array = new int[3][];
array[0] = new int[1];
array[1] = new int[]{2,2,2};
array[2] = new int[]{1,2,3,4};
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j]+" ");
}
System.out.println();
}
}
}
运行结果:
2.不规则的二维数组在数组中的存储
不规则的二维数组和普通的二维数组没什么区别
数组详解就到这里,记得点个赞再走!