前言❤️❤️
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
上篇博客包括数组的一些基础知识和内存存储的解析,因此在看这篇博客前,建议铁汁们先把上篇博客过一下(链接就放在下面),这篇博客的内容包括数组常见的各种操作,引用,元素交换,数组复制,数组元素的查找和排序,帮助大家快速入门💪💪💪Java,内容比较多,铁汁们可以分两次来看🐵
🎇上篇博客:数组基础
🎇个人主页:洋不写bug的博客
🎇所属专栏:Java学习之旅,从入门到进阶
🎇铁汁们对于Java的各种常用核心语法(不太常用的也有😆),都可以在上面的Java专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~
1,认识null(空引用)
null这个单词要读对,如果发音不对的话,后面面试时可能面试官听不懂我们在讲什么,这个拼音的话读的是nao.

在Java中,空引用定义为“无效的引用”,至于里面的地址是啥,其实是不知道的,这个取决的JVM的具体实现。
下面这段代码就会报错,提示空指针异常,但是Java中把指针这个概念淡化掉了,因此我们一般称这个为空引用异常。
public class exercise {
public static void main(String[] args) {
int []arr = null;
System.out.println(arr[0]);
}
}

可以这样理解“空引用(null)”,那就是一个不指向任何对象的引用
上篇数组基础博客中的第5部分有这部分内容的解析,这部分想再了解一下可以看下:
数组基础
null(空引用)其实与C语言中的空指针比较类似,都表示一个无效的内存位置,不能对这个内存位置进行读取,修改等操作,一旦尝试操作,就会报出“空引用异常”的错误
2,方法中数组内部引用修改的解析
1,修改引用
铁汁们现在可以先猜一下这段代码的执行结果,认为会打印出1,2,3的在评论区扣1,其他的在评论区扣2
import java.util.Arrays;
public class exercise {
public static void main(String[] args) {
int []arr = {1,2,3};
func(arr);
System.out.println(Arrays.toString(arr));
}
public static void func(int[] a){
a = new int[]{100,2,3};
}
}
上篇博客提到,形参是实参的一份拷贝,那么刚执行到方法时,形参就在栈上拷贝了一份,存储的地址和实参存储的地址相同。

接下来执行到 a = new int[ ]{100,2,3}; 这一步是重点,这里的new int[]其实是新在堆上创建了一片空间,然后用形参a指向这一块新的空间。

这里一定要注意的一点,那就是形参在栈中存储的地址更新了,指向了新创建的这部分空间,相当于跟原来的实参就没有任何关系了。
我们前面说过,只要是new,都是在堆内存中创建了一块空间,a = new int[ ]{100,2,3}
代码大家不要从左往右看,那样容易误解,其实是从右往左看的,先在堆中创建了一个空间,再让a这个引用指向这个空间。
所以,最后的结果就是没有改变

2,修改本体
铁汁们再看一下面这段代码,猜一下这个的打印结果。
import java.util.Arrays;
public class exercise {
public static void main(String[] args) {
int []arr = {1,2,3};
func(arr);
System.out.println(Arrays.toString(arr));
}
public static void func(int[] a){
a[0] = 100;
}
}
这里再画一个内存图,这里的修改,其实是直接修改到了本体,因为并没有重新在堆内存中重新new出一片空间:

运行结果如下:

总结:
方法内部对引用的修改,也不总是能影响到外面的,关键是看对引用进行的修改是哪种操作
- 如果修改的是指向对象(堆内存中)的本体,那么这里的修改是能被方法外感知到的。
- 如果修改的引用本身,那么方法中的引用只是栈内存中的一份拷贝,这时的修改方法外面是感知不到的。
其实这里大家不需要死记硬背,只需要结合内存图,多写几次代码,理解一下即可
3,交换
1,常量的交换
如果这样交换的话,能否进行有效交换?
public class exercise {
public static void main(String[] args) {
int x = 20;
int y = 10;
swap(x,y);
System.out.println("x = " + x + " y = " + y);
}
public static void swap(int a,int b){
int temp = a;
a = b;
b = temp;
}
}
其实是不能的,因为形参只是一份拷贝,交换后并不会影响原来的实参

虽然形参这里的拷贝交换了,但是它只是一份复制,方法调用完后就会自动销毁,因此就不会影响原来的。

那我们怎么进行常量的交换呢,其实说来也简单,那就是把变量搞在数组中,数组存在堆内存中,这样是比较好交换的
2,数组的交换
这个就是直接修改堆内存中的数据,能够修改成功(也就是前面提到的在方法中本体的修改)
import java.util.Arrays;
public class exercise {
public static void main(String[] args) {
int [] arr = {1,2};
swap(arr,0,1);
System.out.println(Arrays.toString(arr));
}
public static void swap(int []arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

4,C和Java数组内存存储和释放机制
铁汁们可以回忆一下,在C语言中这个代码写的有没有问题
int *func(){
int [] a = {1,2,3};
return a;
}
其实是有问题的
a这个数组是局部变量,当函数执行完以后,这个局部变量就释放内存了,当函数外面拿着这个内存再去使用(比如说读取什么的),就是访问非法的内存空间。
(就好比出去旅游住酒店,已经把房退了,还拿着私自配的房卡,去开这个门)
在Java中写这样一个代码,运行后并不会报错。
import java.util.Arrays;
public class exercise {
public static void main(String[] args) {
int [] arr = func4();
System.out.println(Arrays.toString(arr));
}
public static int[] func4(){
int[] a = {1,2,3};
return a;
}
}

原因就是:
在C语言中,数组是存到栈中的,一出方法就会被释放掉。
在Java中,数组的核心数据是存储在堆中的,堆上的内存,释放实际上并不是跟当前的代码相关的。
那么Java中这些new出来的在堆上的内存到底什么时候才会释放呢?
当我们的代码中没有任何地方使用这些对象/数组时,就会被JVM自动释放掉,这个释放内存的过程是我们感知不到的,称为GC(garbage collection 垃圾回收机制),堆上的内存,都是通过GC来自动管理的。
而在C++中,它们堆上的内存是需要手动去释放的,要是忘记释放,或者代码出现bug没有运行到,那么就会就会引起一个大问题———内存泄漏。
内存泄露是C++程序员职业生涯的最大杀手,一旦爆发,系统就会崩溃,卡顿,但是我们学习的Java中,通过垃圾回收,就不用考虑这一问题
当然,垃圾回收也需要一定的代价,比如消耗更多的系统资源,使程序在运行中需要更多的时间等。
5,模拟实现toString方法
1,目的
上篇博客在数组的打印部分提到了用Arrays类的toString方法来打印,代码如下:
import java.util.Arrays;
public class exercise {
public static void main(String[] args) {
int[] arr = {1,2,3};
System.out.println(Arrays.toString(arr));
}
}

那既然Arrays工具类为我们提供了这个方法,为什么还要自己再写一个呢
这样的练习,一方面能增强我们对这个方法的理解,另外也能增强我们的写代码的能力
有时候面试也会考让我们自己实现一个工具类中存在的方法,就是为了考验我们的理解能力(这里注意,我们只是练习一下,后序在打印时直接用工具类Arrays的就行)
2,字符串知识补充和代码实现
这里要用到字符串(String),但是字符串是在后面博客中才会提到的,这里先补充一些。
字符串是可以拼接字符串的,代码如下,打印出来就是helloworld
public class exercise2 {
public static void main(String[] args) {
String s = "hello";
String a = "world";
System.out.println(s + a);
}
}
这里是我们自己写的toString方法,用的不是Arrays这个工具类中的方法,而是自己写的,因此就不用写Arrays.toString,直接写toString就可以了。
这里就是让这个toString字符串不断的拼接新的字符串
每次拼接完一个元素,需要判断是不是数组中最后一个元素,进而决定加不加逗号,如果是数组中的最后一个元素,就不需要加逗号了
public class exercise {
public static void main(String[] args) {
int[] arr = {1,2,3};
System.out.println(toString(arr));
}
public static String toString(int [] arr) {
String result = "[";
for (int i = 0; i < arr.length; i++) {
result += arr[i];
if (i < arr.length - 1) {
result += ",";
}
}
result += "]";
return result;
}
}
打印的结果跟Arrays类中的toString方法效果是相同的

6,Arrays工具类中的数组复制方法
1,区间复制
这个Arrays工具类copyOfRange方法就是用于数组的区间复制
前面是要复制的数组名,后面是复制的区间,是前闭后开的,也就是[1,3),实际上复制的就是arr数组的第一项和第二项,刚好是2和3。
import java.util.Arrays;
public class exercise2 {
public static void main(String[] args) {
int [] arr = {1,2,3,4,5};
int [] arr2 = Arrays.copyOfRange(arr,1,3);
System.out.println(Arrays.toString(arr2));
}
}

这里的copyOfRange中的“Range”就是区间的意思,在IDEA界面会看到这里有个from to

在编程中,只要说到区间,大部分都是前闭后开(极少数情况是前闭后闭)
这样的设定是为了能够立刻算清楚这个区间中有几个元素,比如说[1,3),我们直接用后一项减去前一项,就能算出区间中元素的个数为2
2,长度复制
长度复制,就是直接写出新复制数组的长度,然后从前往后进行复制,长度用完就停止复制了。
import java.util.Arrays;
public class exercise2 {
public static void main(String[] args) {
int [] arr = {1,2,3,4,5};
int [] arr2 = Arrays.copyOf(arr,5);
System.out.println(Arrays.toString(arr2));
}
}

方法后面的参数改成4,就是复制4个元素,是从前往后复制的。
import java.util.Arrays;
public class exercise2 {
public static void main(String[] args) {
int [] arr = {1,2,3,4,5};
int [] arr2 = Arrays.copyOf(arr,4);
System.out.println(Arrays.toString(arr2));
}
}

那如果写的长度超过了原数组的长度,不会报错,多出的部分就是补零。
import java.util.Arrays;
public class exercise2 {
public static void main(String[] args) {
int [] arr = {1,2,3,4,5};
int [] arr2 = Arrays.copyOf(arr,6);
System.out.println(Arrays.toString(arr2));
}
}

3,模拟实现长度复制方法
如果这样写代码实现长度复制,有没有bug?
import java.util.Arrays;
public class exercise2 {
public static void main(String[] args) {
int [] arr = {1,2,3,4,5};
int [] arr2 = copyOf(arr,3);
System.out.println(Arrays.toString(arr2));
}
public static int[] copyOf(int [] arr,int newLength){
//按照指定长度创建新数组
int [] result = new int [newLength];
for(int i = 0;i < arr.length;i++){
result[i] = arr[i];
}
return result;
}
}
这里使用的是静态初始化,根据需要创建,上来会把result里面的元素都初始化为0
但是arr.length是5,result创建的大小为3,后面再赋值时就会出现数组越界异常

在方法中这样先判断一下,就可以了
如果arr.length大,说明复制的数组长度比较小,就使用newLength来赋值,避免result出现数组越界
如果newLength大,说明复制的数组长度比较大,就使用arr.lenth来赋值,避免arr出现数组越界异常
相等的话,那就是一比一复制,用哪个来复制都可以
public static int[] copyOf(int [] arr,int newLength){
//按照指定长度创建新数组
int [] result = new int [newLength];
int len = arr.length > newLength ? newLength : arr.length;
for(int i = 0;i < len;i++){
result[i] = arr[i];
}
return result;
}
IDEA这里会提醒有更好的写法,我们把鼠标放上去,alt + enter一下

会发现替换成了这样,这个就是使用Math类中的求最值的方法
那以后就没有必要再去使用三元表达式了,直接用这个函数就可以了

7,Java标准库
前面使用的Arrays.toString()方法,还有Arrays.copy方法,包括刚刚IDEA帮我们自动补充道的Math.min,都是Java标准库中的方法。
在C中也有标准库,但是C的标准库近似于没有,功能非常少。
而Java的标准库就牛逼许多了,那些开发Java的大佬已经把各种常用功能准备好了,我们只需调用即可,
除了Java官方公司(Oracle)搞得标准库之外,还有很多第三方库,是其他非官方的人搞的库,这些库的数量,超级超级多
8,编写方法查找数组中指定的元素
1,顺序查找
这是最简单的一种查找,找到的话返回所在下标,找不到就返回-1
public static int find(int[] arr,int num){
int f = 1;
for(int i = 0;i < arr.length;i++){
if (arr[i] == num)
return i;
}
return -1;
}
2,二分查找(效率高)
二分查找是有要求的,只能用于有序的数组
public class exercise {
public static void main(String[] args) {
int []arr = {1,2,3,4,5,6,7,8,9,10};
int index = binarySearch(arr,7);
if(index == -1){
System.out.println("数组中没有这个元素");
}else{
System.out.println("这个元素在数组中的下标为:" + index);
}
}
//二分查找函数(返回查找到的数的下表)
public static int binarySearch(int [] arr,int target){
int left = 0;
int right = arr.length - 1;
while(left <= right){
int mid = (left + right) / 2;
if(target == arr[mid]) {
return mid;
}
else if(target < arr[mid]){//target在mid的左边
right = mid - 1;
}
else if(target > arr[mid]){//target在mid的右边
left = mid + 1;
}
}
return -1;
}
}
二分查找的思路:
①用left表示最左侧元素的下标,用right表示右侧元素的下标。
②找出数组的中间元素,如果有两个中间元素的话因为这里的int的舍弃小数,因此我们取得是前一个元素。
③对比中间元素跟比较值的大小关系,再根据这个不断的缩小比较范围。
④那这时候就需要在最外层搞一个while循环,判断什么情况是没有查找失败,可以继续查找,这里就是left <= right,那么为什么这样写的,分析如下:
①首先确定能不能写等号,这里的区间是前闭后闭,[left,right]区间,这个区间中还是存在一个数的,因此还可以再比较一次
②当最后这个数还是不相等,无论是比目标值大还是小,只有两种情况
要不就是执行right = mid - 1;right跑到left左边
要不就是执行left = mid + 1;left跑到right右边
那这时候我们就知道没有判断的必要了,left > right时,不符合while循环的条件,会直接跳出来,也就是没有找到对应元素
大家可以自己在本子上画一下left和right移动的过程,有不懂的地方也可以在评论留言或者发私信
9,改进版冒泡排序(bubbleSort)
冒泡排序是数组非常经典的一种排序方式,在C语言中已经学过,下面就是改进版的冒泡排序的代码:
import java.util.Arrays;
public class exercise {
public static void main(String[] args) {
int [] arr = {9,5,2,7,3,6};
bubblesort(arr);
System.out.println(Arrays.toString(arr));
}
//冒泡排序(升序)
public static void bubblesort(int [] arr){
for(int i = 0;i < arr.length - 1;i++){
boolean f = true;
for(int j = 0;j < arr.length - 1 - i;j++){
if(arr[j] > arr[j + 1])
{
f = false;
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if(f)
break;
}
}
}
①外面的for循环是每循环一次,就让最大的一个数到最后,一个数组有n个数,只需要循环n - 1次即可
因为还剩两个数时,只需要把较大的数放后面,那么这两个数就都是有序的的了
②内层循环就是判断相邻的两个数谁大,把大的放在后面,这样循环一轮,就会把最大的一个数放在最后
那么如何看到底需要循环几次呢,
可以看下面这个图,两两循环,有n个数,是循环n - 1次,那么每次循环完以后,需要排序的数都会减一个,第一次相对于所有数少1个,第二次相对于所有数少两个,因此再减去i就可以了。因此就是循环n - 1 - i次

③代码优化: 当数组是比较有序的时候,可能外部for循环搞个几次就有序了,那么这时候再循环到底,不断地判断,就有点麻烦了
当完全有序的时候,内部的for循环里面if里的内容就不会执行;
因此,如果内部for循环走了一圈还没有触发if的话,那这个数组就已经有序了,就可以直接跳出排序了。
这里引入了布尔类型的变量f,用来判断内层每一轮for循环结束后,是否数组已经有序了
10,标准库排序(有手就能排)
import java.util.Arrays;
public class exercise {
public static void main(String[] args) {
int [] arr = {9,5,2,7,3,6};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
就是这么简单,而且标准库排序是大佬们研究的高度优化的排序,比我们自己写的排序效率是要高的。
那既然有这么牛逼的排序,我们为什么还要自己写各种排序呢,原因就是面试,笔试的时候会用到😄,用来考察我们的思维和代码能力
11,数组逆序(双下标法)
要求:把一个数组里面的元素进行逆序处理,原来是[1,2,3],逆序处理后就是[3,2,1]
一些铁汁可能会想到再创建一个新的数组,老数组逆序赋值给新数组,再用新数组把老数组给替换掉,但是这样就需要额外的一块存储数组的空间
这里我们使用“双指针法”,由于Java中弱化了指针的概念,因此称之为双下标法,双下标法是无需额外的数组空间的
最左侧元素的下标left,最右侧元素的下标right,交换left和right的元素,并将left和right都往中间移动,移动一次,交换一次,直到left和right重合,或者left在right右边,right在left左边。
import java.util.Arrays;
public class exercise {
public static void main(String[] args) {
int [] arr = {1,2,3,4,5,6,7};
reverseArray(arr);
System.out.println(Arrays.toString(arr));
}
// //数组倒序函数
public static void reverseArray(int [] arr){
int left = 0;
int right = arr.length - 1;
while(left <= right){
swap(arr,left,right);
left++;
right--;
}
}
public static void swap(int []arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
当数组元素个数是奇数时,最后left和right会移到中间
这时候自己跟自己交换也没什么问题,因此写不写等号无所谓
下一步right向左移,left右移,这时候已经全部交换完了,就不用再交换了,left > right,跳出循环。

当个数是偶数个时,这时候最后一次交换对应数是在中间两个数中,接下来left就跑到了right的右边,交换完成,不符合while条件,直接退出。

注:上面只是简单的演示一下,实际的数字交换博主并没有做。
12,二维数组
二维数组,本质上是一个一维数组,只不过每个数组的元素都是一个一维数组
三维数组,本质上也是一个一维数组,只不过每个数组的元素都是一个二维数组
维数的升高,就是在套娃😄
下面这段代码可以理解为定义了一个长度为3的一维数组,每个一维数组里有4个元素
public class exercise {
public static void main(String[] args) {
//定义一个3行4列的arr数组
int[][] arr = new int[3][4];
}
}
这段代码,打印的结果是2,并不是4,在Java中,是不要求所有的行数,列数一致的
上篇博客中提到,在C中,数组的类型不是int[ ],而是int[4],因此数组要求每个元素的类型一致,二维数组中的每个元素都得是int[4],这是不科学的。
public class exercise {
public static void main(String[] args) {
//定义一个3行4列的arr数组
int[][] arr = {{1,2,3,4},{1,2,3,4},{1,2}};
System.out.println(arr[2].length);
}
}

结语💕💕
这篇文章中代码量很大,强烈建议铁汁们有空动手敲一下,刚开始学习感觉绕是很正常的,,经过不断的深入学习,几个月后再看这些东西就会感觉超级简单👍👍👍
🥳🥳大家都学废了吗?完结撒花~ 🎉🎉🎉



被折叠的 条评论
为什么被折叠?



