1. 数组基本用法
1.1 什么是数组
数组本质上就是让我们能 “批量” 创建相同类型的变量.
例如:
如果需要表示两个数据, 那么直接创建两个变量即可 int a; int b
如果需要表示五个数据, 那么可以创建五个变量 int a1; int a2; int a3; int a4; int a5;
但是如果需要表示一万个数据, 那么就不能创建一万个变量了. 这时候就需要使用数组, 帮我们批量创建.
注意事项: 在 Java 中, 数组中包含的变量必须是 相同类型.
1.2 创建数组
基本语法
// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] {
初始化数据 };
//初始化给定长度的数组
int[] arr = new arr[arr.length];
// 静态初始化
数据类型[] 数组名称 = {
初始化数据 };
代码示例
int[] arr = new int[]{
1, 2, 3};
int[] arr = {
1, 2, 3};
注意事项: 静态初始化的时候, 数组元素个数和初始化数据的格式是一致的.
1.3 数组的使用
代码示例: 获取长度 & 访问元素
int[] arr = {
1, 2, 3};
// 获取数组长度
System.out.println("length: " + arr.length); // 执行结果: 3
// 访问数组中的元素
System.out.println(arr[1]); // 执行结果: 2
System.out.println(arr[0]); // 执行结果: 1
arr[2] = 100;
System.out.println(arr[2]); // 执行结果: 100
注意事项
- 使用 arr.length 能够获取到数组的长度. . 这个操作为成员访问操作符. 后面在面向对象中会经常用到.
- 使用 [ ] 按下标取数组元素. 需要注意, 下标从 0 开始计数
- 使用 [ ] 操作既能读取数据, 也能修改数据.
- 下标访问操作不能超出有效范围 [0, length - 1] , 如果超出有效范围, 会出现下标越界异常
代码示例: 遍历数组
所谓 “遍历” 是指将数组中的所有元素都访问一遍, 不重不漏. 通常需要搭配循环语句.
int[] arr = {
1, 2, 3};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// 执行结果
1
2
3
代码示例: 使用 for-each 遍历数组
int[] arr = {
1, 2, 3};
for (int x : arr) {
System.out.println(x);
}
// 执行结果
1
2
3
for-each 是 for 循环的另外一种使用方式. 能够更方便的完成对数组的遍历. 可以避免循环条件和更新语句写错.
注意:for循环 和 for each循环的区别?
- for循环是可以拿到下标的;
- for each是拿不到下标的,只拿元素(多用于后面会学到的集合中);
2. 数组作为方法的参数
2.1 基本用法
代码示例: 打印数组内容
public static void main(String[] args) {
int[] arr = {
1, 2, 3};
printArray(arr);
}
public static void printArray(int[] a) {
for (int x : a) {
System.out.println(x);
}
}
// 执行结果
1
2
3
在这个代码中
- int[] a 是函数的形参, int[] arr 是函数实参.
- 如果需要获取到数组长度, 同样可以使用 a.length
2.2 理解引用类型(重点/难点)
我们尝试以下代码
代码示例1 参数传内置类型
public static void main(String[] args) {
int num = 0;
func(num);
System.out.println("num = " + num);
}
public static void func(int x) {
x = 10;
System.out.println("x = " + x);
}
// 执行结果
x = 10
num = 0
我们发现, 修改形参 x 的值, 不影响实参的 num 值.
代码示例2 参数传数组类型
public static void main(String[] args) {
int[] arr = {
1, 2, 3};
func(arr);
System.out.println("arr[0] = " + arr[0]);
}
public static void func(int[] a) {
a[0] = 10;
System.out.println("a[0] = " + a[0]);
}
// 执行结果
a[0] = 10
arr[0] = 10
我们发现, 在函数内部修改数组内容, 函数外部也发生改变.
此时数组名 arr 是一个 “引用” . 当传参的时候, 是按照引用传参.
如何理解内存?
内存就是指我们熟悉的 “内存”. 内存可以直观的理解成一个宿舍楼. 有一个长长的大走廊, 上面有很多房间.
每个房间的大小是 1 Byte (如果计算机有 8G 内存, 则相当于有 80亿 个这样的房间).
每个房间上面又有一个门牌号, 这个门牌号就称为 地址
什么是引用?
引用相当于一个 “别名”, 也可以理解成一个指针.
创建一个引用只是相当于创建了一个很小的变量, 这个变量保存了一个整数, 这个整数表示内存中的一个地址.
针对 int[] arr = new int[]{1, 2, 3}
这样的代码, 内存布局如图:
a) 当我们创建 new int[]{1, 2, 3}
的时候, 相当于创建了一块内存空间保存三个 int
b) 接下来执行 int[] arr = new int[]{1, 2, 3}
相当于又创建了一个 int[] 变量, 这个变量是一个引用类型, 里面只保存了一个整数(数组的起始内存地址)
c) 接下来我们进行传参相当于 int[] a = arr
, 内存布局如图
d) 接下来我们修改 a[0] , 此时是根据 0x100
这样的地址找到对应的内存位置, 将值改成 100
此时已经将 0x100
地址的数据改成了 100 . 那么根据实参 arr 来获取数组内容 arr[0]
, 本质上也是获取 0x100
地址上的数据, 也是 100.
总结: 所谓的 “引用” 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).
容易犯错误的知识点:
1.实参和形参
func1 结果为 :
1,2,3,4,5,6
1,2,3,4,5,6
原因分析:实参指向对象,形参指向另一个对象,打印出的实参结果不变。
func2 结果为 :
1,2,3,4,5,6
899,2,3,4,5,6
原因分析:实参指向对象,形参也指向对象。
2.一个引用不能同时指向多个对象,一个引用只能保存一个对象的地址。
3.引用一定是在栈上吗?不一定:一个变量在不在栈上,是变量的性质决定的。如果是一个局部变量,就在栈上。如果是实例成员变量,那就不一定再栈上了。
一个例子:https://www.zhihu.com/question/300519939/answer/1617705726
扩展:
1.类的成员变量的引用是在栈上还是在堆上?
答:堆上,随着对象实例放在堆当中。
2.类的静态方法在main方法中被引用是在栈上还是堆上?
答:栈上,所有方法都是在栈内存中执行。
2.3 认识 null
null 在 Java 中表示 “空引用” , 也就是一个无效的引用
int[] arr = null;
System.out.println(arr[0]);
// 执行结果
Exception in thread "main" java.lang.NullPointerException
at Test.main(Test.java:6)
null 的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操作. 一旦尝试读写, 就会抛出 NullPointerException.
注意: Java 中并没有约定 null 和 0 号地址的内存有任何关联.
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 开始, 运行时常量池在堆上)
Native 方法:
JVM 是一个基于 C++ 实现的程序. 在 Java 程序执行过程中, 本质上也需要调用 C++ 提供的一些函数进行和操作系统底层进行一些交互. 因此在 Java 开发中也会调用到一些 C++ 实现的函数.这里的 Native 方法就是指这些 C++ 实现的, 再由 Java 来调用的函数.
我们发现, 在上面的图中, 程序计数器, 虚拟机栈, 本地方法栈被很多个原谅色的, 名叫 Thread(线程) 的方框圈起来了,并且存在很多份. 而 堆, 方法区, 运行时常量池, 只有一份. (关于线程, 这是我们后面重点讲解的内容).关于上面的划分方式, 我们随着后面的学习慢慢理解. 此处我们重点理解 虚拟机栈 和 堆
- 局部变量和引用保存在栈上, new 出的对象保存在堆上.
- 堆的空间非常大, 栈的空间比较小.
- 堆是整个 JVM 共享一个, 而栈每个线程具有一份(一个 Java 程序中可能存在多个栈).
线程共享区:Java堆 、方法区
线程私有内存区: 虚拟机栈、本地方法栈、程序计数器
3. 数组作为方法的返回值
代码示例: 写一个方法, 将数组中的每个元素都 * 2
// 直接修改原数组
class Test {
public static void main(String[] args) {
int[] arr = {
1, 2, 3};
transform(arr);
printArray(arr);
}
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
public static void transform(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i] * 2;
}
}
}
这个代码固然可行, 但是破坏了原有数组. 有时候我们不希望破坏原数组, 就需要在方法内部创建一个新的数组, 并由方法返回出来.
// 返回一个新的数组
class Test {
public static void main(String[] args) {
int[] arr = {
1, 2, 3};
int[] output = transform(arr);
printArray(output);
}
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
public static int[] transform(int[] arr) {
int[] ret = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
ret[i] = arr[i] * 2;
}
return ret;
}
}
这样的话就不会破坏原有数组了.
另外由于数组是引用类型, 返回的时候只是将这个数组的首地址返回给函数调用者, 没有拷贝数组内容, 从而比较高效.
4. 数组练习
4.1 数组转字符串
代码示例
import java.util.Arrays
int[] arr = {
1,2,3,4,5,6};
String newArr = Arrays.toString(arr);
System.out.println(newArr);
// 执行结果
[1, 2, 3, 4, 5, 6]
使用这个方法后续打印数组就更方便一些.
Java 中提供了 java.util.Arrays
包, 其中包含了一些操作数组的常用方法.
4.2 数组拷贝
一共有五种方法
代码示例
第一种:利用for循环,此种方法创建了新对象,为深拷贝。
public static int[] copyArray(int[]a){
int[] ret = new int[a.length];
for(int i = 0; i<ret.length; i++){
ret[i]=a[i];
}//利用for循环
return ret;//返回数组
}
第二种:Arrays.copyOf(arr, arr.length)
(默认浅拷贝)
第三种:Arrays.copyOfRange(arr, 2, 4)
(默认浅拷贝)
import java.util.Arrays
int[] arr = {
1,2,3,4,5,6};
int[] newArr = Arrays.copyOf(arr, arr.length);
System.out.println("newArr: " + Arrays.toString(newArr));
arr[0] = 10;
System.out.println("arr: " + Arrays.toString(arr));
System.out.println("newArr: " + Arrays.toString(newArr));
// 拷贝某个范围.
int[] newArr = Arrays.copyOfRange(arr, 2, 4);
System.out.println("newArr2: " + Arrays.toString(newArr2));
第四种:System.arraycopy
是native方法,使用c/c++写的内置库。(默认浅拷贝)
int[] copy = new int[array.length];
System.arraycopy(array,0,copy,0,array.length);
System.out.println(Arrays.toString(copy));
第五种:array.clone()
(默认是浅拷贝,深拷贝需要重写)
int[] array = {
1,2,3,4,5,6};
int[] copy = array.clone();
System.out.println(Arrays.toString(copy));
注意事项:
- 相比于
newArr = arr
这样的赋值, copyOf 是将数组进行了 深拷贝, 即又创建了一个数组对象,拷贝原有数组中的所有元素到新数组中. 因此, 修改原数组, 不会影响到新数组.
深拷贝:拷贝对象 B复制A,A变,B不变
浅拷贝:拷贝地址 B复制A,A变,B变
深拷贝和浅拷贝是一种概念,具体需要看代码是怎样拷贝的。
看拷贝的是基本类型,还是引用类型,是否创建新对象。
深拷贝和浅拷贝的知识链接:
Java深入理解深拷贝和浅拷贝区别,超链接点击查看 - int[] newArr = Arrays.copyOfRange(arr, 2, 4); //此处实际上拷贝到下表2到3,最后一位下标4所在元素不拷贝.
4.3 找数组中的最大元素
给定一个整型数组, 找到其中的最大元素 (找最小元素同理)
4.4 求数组中元素的平均值
给定一个整型数组, 求平均值
4.5 查找数组中指定元素(顺序查找)
给定一个数组, 再给定一个元素, 找出该元素在数组中的位置.
4.6 查找数组中指定元素(二分查找)
针对有序数组, 可以使用更高效的二分查找.
以升序数组为例, 二分查找的思路是先取中间位置的元素, 看要找的值比中间元素大还是小. 如果小, 就去左边找; 否则就去右边找.
代码示例
public static void main(String[] args) {
int[] arr = {
1,2,3,4,5,6};
System.out.println(binarySearch(arr, 6));
}
public static int binarySearch(int[] arr, int toFind) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (toFind < arr[mid]) {
// 去左侧区间找
right = mid - 1;
} else if (toFind > arr[mid]) {
// 去右侧区间找
left = mid + 1;
} else {
// 相等, 说明找到了
return mid;
}
}
// 循环结束, 说明没找到
return -1;
}
// 执行结果
5
4.7 检查数组的有序性
给定一个整型数组, 判断是否该数组是有序的(升序)
public static void main(String[] args) {
int[] arr = {
1,2,3,10,5,6};
System.out.println(isSorted(arr));
}
public static boolean isSorted(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) {
return false;
}
}
return true;
}
4.8 数组排序(冒泡排序)
给定一个数组, 让数组升序 (降序) 排序.
算法思路
每次尝试找到当前待排序区间中最小(或最大)的元素, 放到数组最前面(或最后面).
代码示例1
// [bound, length) 构成了一个前闭后开区间, 表示已排序区间
// [0, bound)构成了一个前闭后开区间, 表示待排序区间
// 每循环一次, 就找到一个合适大小的元素, 已排序区间就增大1.
public static void main(String[] args) {
int[] arr = {
9, 5, 2, 7};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void bubbleSort(int[] arr) {
for (int bound = arr.length; bound > 0; bound--) {
for (int cur = 0; cur < bound - 1; cur++) {
if (arr[cur] > arr[cur + 1]) {
int tmp = arr[cur];
arr[cur] = arr[cur + 1];
arr[cur + 1] = tmp;
}
}
}
}
// 执行结果
[2, 5, 7, 9]
代码示例2
思路和上面一样,只是边界Bound从左向右开始循环。
其中,如何循环到i次恰好没有互换元素,就说明已经有序了,就直接break即可(使用Boolean判断是否互换过元素)
private static void bubbleSort(int[] arr) {
for (int bound = arr.length; bound > 0; bound--) {
boolean flg = false;
for (int cur = 0; cur < bound - 1; cur++) {
if (arr[cur] > arr[cur + 1]) {
int tmp = arr[cur];
arr[cur] = arr[cur + 1];
arr[cur + 1] = tmp;
flg = true;
}
}
if (flg == false) {
break;
}
}
}
代码示例3
思路同上
private static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
boolean flg = false;
for (int cur = 0; cur < arr.length - 1 - i; cur++) {
if (arr[cur] > arr[cur + 1]) {
int tmp = arr[cur];
arr[cur] = arr[cur + 1];
arr[cur + 1] = tmp;
flg = true;
}
}
if (flg == false) {
break;
}
}
}
冒泡排序性能较低. Java 中内置了更高效的排序算法
public static void main(String[] args) {
int[] arr = {
9, 5, 2, 7};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
关于 Arrays.sort 的具体实现算法, 我们在后面的排序算法课上再详细介绍. 到时候我们会介绍很多种常见排序算法.
4.9 数组逆序
给定一个数组, 将里面的元素逆序排列.
思路
设定两个下标, 分别指向第一个元素和最后一个元素. 交换两个位置的元素.然后让前一个下标自增, 后一个下标自减, 循环继续即可.
代码示例
public static void main(String[] args) {
int[] arr = {
1, 2, 3, 4};
reverse(arr);
System.out.println(Arrays.toString(arr));
}
public static void reverse(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
4.10 数组数字排列
给定一个整型数组, 将所有的偶数放在前半部分, 将所有的奇数放在数组后半部分
例如
{
1, 2, 3, 4}
调整后得到
{
4, 2, 3, 1}
基本思路
设定两个下标分别指向第一个元素和最后一个元素.
用前一个下标从左往右找到第一个奇数, 用后一个下标从右往左找到第一个偶数, 然后交换两个位置的元素.依次循环即可.
代码示例
public static void main(String[] args) {
int[] arr = {
1, 2, 3, 4, 5, 6};
transform(arr);
System.out.println(Arrays.toString(arr));
}
public static void transform(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
// 该循环结束, left 就指向了一个奇数
while (left < right && arr[left] % 2 == 0) {
left++;
}
// 该循环结束, right 就指向了一个偶数
while (left < right && arr[right] % 2 != 0) {
right--;
}
// 交换两个位置的元素
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
}
}
5. 二维数组
二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组.
基本语法
数据类型[][] 数组名称 = new 数据类型 [行数][列数] {
初始化数据 };
代码示例
方法一:for循环打印二维数组
int[][] arr = {
{
1, 2, 3, 4},
{
5, 6, 7, 8},
{
9, 10, 11, 12}
};
for (int row = 0; row < arr.length; row++) {
for (int col = 0; col < arr[row].length; col++) {
System.out.printf("%d\t", arr[row][col]);
}
System.out.println("");
}
// 执行结果
1 2 3 4
5 6 7 8
9 10 11 12
方法二:使用for each
int[][] array = {
{
1,2,3},
{
4,5,6}
};
for (int[] ret : array) {
for (int x :ret) {
System.out.print(x +" ");
}
System.out.println();
}
方法三:使用Arrays.deepToString(array)
System.out.println(Arrays.deepToString(array));
注意:
Java中的二维数组列数可以省略,行数不可以int[][] array2 = new int[2][];
不指定列数,则为null,列数需要手动指定。
不规则的二维数组:
int[][] array2 = new int[2][];
array2[0] = new int[3];
array2[1] = new int[2];
练习题:
import java.util.Scanner;
public class works {
//8. 编写程序数一下 1到 100 的所有整数中出现多少个数字9。
public static void main11(String[] args) {
int count = 0;
for (int i = 1;i <= 100;i++){
if(i % 10 == 9){
count++;
}
if (i / 10 == 9){
count++;
}
}
System.out.printf("1~100中有%d个数字9",count);
}
// public static void main(String[] args) { 这个程序错了
// int count = 0,a,b;
// for (int i = 1;i <= 100;i++){
// a = i;
// while(a != 0){
// b = i % 10;
// if (b == 9){
// count++;
// }
// a /= 10;
// }
// }
// System.out.printf("1~100中有%d个数字9",count);
// }
//9. 求出0~999之间的所有“水仙花数”并输出。(“水仙花数”是指一个三位数,其各位数字的立方和确好等于该数本
//身,如;153=1+5+3?,则153是一个“水仙花数”。)
public static void main77(String[] args) {
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
for (int i = 0;i <= num;i++){
int tmp = i;
int count = 0;
while (tmp != 0){
count++;
tmp /= 10;
}
tmp = i;
int sum = 0;
while(tmp != 0){
sum += Math.pow(tmp % 10,count);
tmp /= 10;
}
if (sum == i){
System.out.printf("水仙花数%d\n",i);
}
}
}
//10. 编写代码模拟三次密码输入的场景。 最多能输入三次密码,密码正确,提示“登录成功”,密码错误, 可以重新输
//入,最多输入三次。三次均错,则提示退出程序
public static void main3(String[] args) {
String passWord ="ysb4856939";
Scanner sc = new Scanner(System.in);
for (int i = 1;i <= 3;i++){
String myGuess = sc.nextLine();
if(passWord.equals(myGuess)){
System.out.println("登陆成功");
break;
}else if(i != 3){
System.out.println("登陆失败");
}else{
System.out.println("三次均错,退出程序");
break;
}
}
}
//11. 写一个函数返回参数二进制中 1 的个数 比如: 15 0000 1111 4 个 1 以下为参考答案:
public static void main45(String[] args) {
// System.out.println(Integer.bitCount(21));
Scanner sc = new Scanner(System.in);
int m = sc.nextInt();
int count =0;
while(m != 0){
count++;
m = m & (m-1);
}
System.out.println(count);
}
//另一种方法实现 函数返回参数二进制中 1 的个数
public static void main66(String[] args) {
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
int count = 0;
while (num != 0) {
if ((num & 1) == 1) {
//每次循环都和1相与,只要结果为1就count++
count++;
}
num = num >>> 1; //然后,数字num右移一位
}
}
//12. 获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。 以下为参考答案:
public static void main6(String[] args) {
Scanner sc=new Scanner(System.in);
int num=sc.nextInt();
System.out.print("odd sequence:");
for(int i=30;i>=0;i-=2){
//奇数位
System.out.print((num>>i)&1);
}
System.out.print(" even sequence:"); //偶数位
for(int i=31;i>=1;i-=2){
System.out.print((num>>i)&1);
}
sc.close();
}
//13. 输出一个整数的每一位.
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int num = sc.nextInt();
int a;
while (num != 0){
a = num % 10;
System.out.println(a);
num /= 10;
}
}
}