系列文章目录
文章目录
前言
在编程中,某段功能的代码可能频繁使用到,如果在每个位置都重新实现一遍,会:
- 使程序变得繁琐
- 开发效率低下,做了大量重复性的工作
- 不利于维护,需要改动时,所有用到该段代码的位置都需要修改
- 不利于复用
因此,在编程中,我们也可以将频繁使用的代码封装成"帖子”(方法),需要时直接拿来链接(即方法名–方法的入口地址)使用即可,避免了一遍一遍的累赘。
一、Java中的方法
方法就是一个代码片段. 类似于 C 语言中的 “函数”。相信学过C的同学们都知道函数是组成代码的重要组成部分,所以方法的地位亦是如此。
1.1方法的定义
//方法定义
修饰符 返回值类型 方法名称 (形式参数列表){
方法体代码
}
上面就是Java方法的语法格式,大家在使用时要严格按照语法,和英语中的语法一样,错了就无法使用了。
示例:实现一个方法,检测一个年份是否为闰年
public static boolean isLeapYear(int year){
if((0 == year % 4 && 0 != year % 100) || 0 == year % 400){
return true;
}else {
return false;
}
}
下面,就要说说定义方法时的注意事项:
- 返回值类型:如果方法有返回值,返回值类型必须要与返回的实体类型一致,如果没有返回值,必须写成 void
- 方法名字:采用小驼峰命名(首字母小写,后面每个单词的首字母大写)
- 参数列表:如果方法没有参数,()中什么都不写,如果有参数,需指定参数类型,多个参数之间使用逗号隔开
- 方法体:方法内部要执行的语句
- 在java当中,方法必须写在类当中
- 在java当中,方法不能嵌套定义
- 在java当中,没有方法声明一说
)
1.2方法调用的执行过程
示例:计算1!+2!+3!+…+n!
public static int fac(int n){
int ret = 1;
for (int i = 1; i <= n ; i++) {
ret *= i;
}
System.out.println("计算每一次的阶乘 = " + n);
return ret;
}
public static void main(String[] args) {
int sum = 0;
System.out.println("请输入你想要求的数:");
Scanner scanner = new Scanner(System.in);
int x = scanner.nextInt();
for (int i = 1; i <= x; i++) {
sum += fac(i);
}
System.out.println("sum = " + sum);
}
方法调用的过程:调用方法—>传递参数—>找到方法地址—>执行被调方法的方法体—>被调方法结束返回—>回到主调方法继续往下执行
根据上面的示例可以知道
- 定义方法的时候,不会执行方法的代码,只有在调用的时候才会执行。
- 一个方法可以被多次调用
在Java虚拟机栈中,每调用一次方法会为其开辟一块空间,用于存储方法中的变量,随着方法的调用结束,这块空间也会被编译器回收掉。
1.3实参和形参的关系(重点)
方法的形参相当于数学函数中的自变量,比如:公式sum(n) = n x (n + 1)
Java中方法的形参就相当于sum函数中的自变量n,用来接收sum函数在调用时传递的值的。形参的名字可以随意取名,对方法都没有任何影响,形参只是方法在定义时需要借助的一个变量,用来保存方法在调用时传递过来的值。
public static int add(int a,int b){
return a + b;
}
public static void main(String[] args) {
add(2,3); //2和3是实参,在调用时传给形参a和b
}
注意:在Java中,实参的值永远都是赋值到形参中,形参和实参本质是两个实体。
- 示例:交换两个整型变量
public static void swap(int x,int y){
int tmp = x;
x = y;
y = tmp;
System.out.println("swap:x = " + x + "y = " + y);
}
public static void main(String[] args) {
int a = 10;
int b = 20;
swap(a,b);
System.out.println("main:a = " + a + "b = " + b);
}
可以看到,在swap函数交换之后,形参x和y的值发生了改变,但是main方法中a和b还是交换之前的值,即没有交换成功。
实参a和b是main方法中的两个变量,其空间在main方法的栈(一块特殊的内存空间)中,而形参x和y是swap方法中的两个变量,x和y的空间在swap方法运行时的栈中,因此:实参a和b 与 形参x和y是两个没有任何关联性的变量,在swap方法调用时,只是将实参a和b中的值拷贝了一份传递给了形参x和y,因此对形参x和y操作不会对实参a和b产生任何影响。对于基础类型来说,形参相当于实参的临时拷贝,即传值调用。
解决办法:传引用类型参数(例如数组)
public static void swap(int[] arr){
int tmp = arr[0];
arr[0] = arr[1];
arr[1] = tmp;
}
2.方法的重载(重点)
在自然语言中,经常出现“一词多义”的现象,一个词语如果有多重含义,那么就说该词语被重载了,具体代表什么含义需要结合具体的场景。在Java中方法也是可以重载的。
重载的条件:
- 方法名必须相同的
- 参数列表必须不同(参数的个数不同、参数的类型不同、类型的次序不同)
- 与返回值类型是否相同无关
示例:
public static int add(int x,int y){
return x + y;
}
public static double add(double x, double y){
return x + y;
}
public static int add(int x,int y, int z){
return x + y + z;
}
public static void main(String[] args) {
System.out.println(add(3, 4));
System.out.println(add(1.5, 2.5));
System.out.println(add(1, 2, 3));
}
从上面的示例中可以看出,add这个方法构成了重载,编译器在编译代码时,会对实参类型进行推演,根据推演的结果来确定调用哪个方法,这样就解决了取名字这一让人头疼的事情。
3.递归
一个方法在执行过程中调用自身,称为“递归”。递归相当于数学上的“数学归纳法”,有一个起始条件,然后有一个递推公式。自身中又包含了自己,该种思想在数学和编程中非常有用,因为有些时候,我们遇到的问题直接并不好解决,但是发现将原问题拆分成其子问题之后,子问题与原问题有相同的解法,等子问题解决之后,原问题就迎刃而解了。
示例:递归求n!
public static int factor(int n){
if(1 == n){
return 1;
}
return n * factor(n - 1);
}
根据递归求4!,画出上图来分析递归的执行过程,递归求解的难点在于找出那个递推公式。
示例:求斐波那契数列的第n项
斐波那契数列:1,1,2,3,5,8,13,21,34,55,89…
public static int fib(int n){
if(n == 1 || n == 2){
return 1;
}
return fib(n - 1) + fib(n - 2);
}
根据递归的思想可以很快写出代码来,后两个数等于前两位数的和。但是,我们也可以很明显地看出其中的问题,当要求的n很大时,程序执行速度极慢,究其原因是进行了大量的重复运算。所以,在真正解决这个问题时就不应该用递归的方式,而是要用循环的方式来求解斐波那契数列问题,避免出现冗余运算。
public static int fib(int n){
int f1 = 1;
int f2 = 1;
int f3 = 0;
for (int i = 3; i <= n ; i++) {
f3 = f1 + f2;
f1 = f2;
f2 = f3;
}
return f3;
}
此时程序的执行效率就大大提高了。
二、Java中的数组
数组:可以看成是相同类型元素的一个集合。在内存中是一段连续的空间。和C语言的数组基本类似。
1、数组的创建和初始化
1.1、数组的创建
T[] 数组名 = new T[N];
T:表示数组中存放元素的类型
T[]:表示数组的类型
N:表示数组的长度
int[] array1 = new int[10]; // 创建一个可以容纳10个int类型元素的数组
double[] array2 = new double[5]; // 创建一个可以容纳5个double类型元素的数组
String[] array3 = new String[3]; // 创建一个可以容纳3个字符串元素的数组
1.2、数组的初始化
数组的初始化只要分为动态初始化和静态初始化
- 动态初始化:在创建数组时,直接指定数组中元素的个数。
int[] array = new int[10];
- 静态初始化:在创建数组时不直接指定数据元素个数,而直接将具体的数据内容进行指定。
int[] array1 = new int[]{1,2,3,4,5,6,7,8,9,10};
int[] array2 = {1,2,3,4,5,6,7,8,9,10};
Attention:
1、静态初始化虽然没有指定数组的长度,但是编译器在编译时会根据{ }中的元素个数来确定数组的长度。
2、静态初始化时,{ }中数据类型必须与[ ]前数据类型一致。
3、静态初始化可以将后面的new T[ ]省略进行简写。
- 静态和动态初始化也可以分为两步进行
int[] array1;
array1 = new int[5];
int[] array2;
array2 = new int[]{1,2,3,4,5};
int array3;
array3 = {1,2,3,4,5};
//编译失败,省略格式是不可以进行拆分的
- 如果对数组进行动态初始化,且没有对数组内容进行指定,那么数组中的元素就是其数组类型的默认值。
byte—>0 、short—>0、int—>0、long—>0、float—>0.0f、double—>0.0、char—>/u0000
boolean—>false、引用类型—>null
2、数组的简单使用
2.1、访问数组中元素
数组在内存中是一段连续的空间(因此支持随机访问),空间的编号都是从0开始依次递增,该编号称为数组的下标,数组可以通过下标快速访问其任意位置的元素。
int[] array = new int[]{10,20,30,40,50};
System.out.println(array[0]);
System.out.println(array[1]);
System.out.println(array[2]);
System.out.println(array[3]);
System.out.println(array[4]);
注意:下标从0开始,介于[0,N)之间不包含N(N为元素个数),不能越界,否则会抛出下标越界异常 java.lang.ArraylndexOutOfBoundsException 所以,使用数组一定要谨防下标越界。
2.2、遍历数组
“遍历”是指将数组中的所有元素都访问一遍。
- JAVA和C一样,可以通过for循环来对数组进行遍历。
int[] array = new int[]{1,2,3,4,5};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
注意:在Java中,可以通过 数组对象.length 来获取数组的长度。
- for-each遍历数组
for (int x:array) {
System.out.print(x + " ");
}
for-each 是for循环的另外一种使用方式,能够更方便地完成对数组的遍历,可以避免循环条件和更新条件写错。
3、数组是引用类型
3.1、基本类型变量与引用类型变量的区别
基本数据类型创建的变量,称为基本变量,该变量空间中直接存放的是其所对应的值;
而引用数据类型创建的变量,一般称为对象的引用,其空间中存储的是对象所在空间的地址。
示例:
public static void func(){
int a = 10;
int[] array = new int[]{1,2,3,4};
}
在上述代码示例中,a、array,都是函数内部的变量,因此其空间都在func方法对应的栈帧中分配。
a是内置类型的变量,因此其空间中保存的就是给该变量初始化的值。
array是数组类型的引用变量,其内部保存的内容可以简单理解成是数组在堆空间中的首地址。
array这个引用指向了数组对象,这样,引用变量便可以去操作对象。
3.2、引用变量
下面,我们再进一步更加深入地来理解一下引用变量。
示例:
public static void func(){
int[] array1 = new int[]{10,20,30};
int[] array2 = new int[]{1,2,3,4,5};
array2[0] = 100;
array2[1] = 200;
array1 = array2;
array1[2] = 300;
array1[3] = 400;
array1[4] = 500;
for (int i = 0; i < array2.length; i++) {
System.out.print(array2[i] + " ");
}
}
大家可以先看看上面的示例代码,看看能不能一眼看出最后的输出结果
你做对了吗?那么,为什么会是这个结果呢,我们来画图深刻理解一下。
首先,在栈中创建了array1和array2两个局部变量,因为它们也同样都是引用变量,所以在堆中创建了两个对象。array2这个引用指向了数组对象后,通过下标将前两个数组元素的值进行了修改。“array1 = array2”这条语句表示array1这个引用也指向了array2这个引用所指向的对象。最后通过array1这个引用把数组对象当中的值进行了修改。
- 认识null
int[] arr = null;
System.out.println(arr[0]);
// 执行结果
Exception in thread "main" java.lang.NullPointerException
at Test.main(Test.java:6)
null 在 Java 中表示 “空引用” , 也就是一个不指向对象的引用。
null 的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操作。 一旦尝试读写, 就会抛出 NullPointerException。
4、二维数组
二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组。
数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };
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
- 不规则的二维数组
Java可以允许二维数组中每行中的元素个数不同。
int[][] array = {{1,2,3},{4,5,6,7,8}};
System.out.println(Arrays.deepToString(array));
打印二维数组就行上面一样,Arrays.deepToString(数组名)。
5、数组的一些操作
Java 中提供了 java.util.Arrays 包, 其中包含了一些操作数组的常用方法。
- 数组转字符串
import java.util.Arrays;
int[] array = {1,2,3,4,5,6,7,8};
String newArr = Arrays.toString(array);
System.out.println(newArr);
//System.out.println(Arrays.toString(array));
//执行结果
[1, 2, 3, 4, 5, 6, 7, 8]
使用 toString 这个方法后,打印数组就更方便了。
- 拷贝字符串
int[] arr = {1,2,3,4,5,6};
int[] copy1 = new int[6];
copy1 = Arrays.copyOf(arr, arr.length);
System.out.println("arr: " + Arrays.toString(arr));
System.out.println("copy1: " + Arrays.toString(copy1));
int[] copy2 = new int[3];
copy2 = Arrays.copyOfRange(arr,1,4);
System.out.println("copy2: " + Arrays.toString(copy2));
//执行结果
arr: [1, 2, 3, 4, 5, 6]
copy1: [1, 2, 3, 4, 5, 6]
copy2: [2, 3, 4]
使用 copyOf 这个方法来拷贝指定的数组。
使用 copyOfRange 这个方法来拷贝指定的数组的指定范围。
- 排序
int[] array = {1,12,32,2,34,43,21};
Arrays.sort(array);
System.out.println(Arrays.toString(array));
//执行结果
[1, 2, 12, 21, 32, 34, 43]
使用 sort 方法可以直接将数组进行升序排序。
- 二分查找
针对有序数组, 可以使用更高效的二分查找。
int[] array = {1,2,3,4,5,6,7};
System.out.println(Arrays.binarySearch(array,5));
//执行结果
4
所以,在对一个无序的数组查找元素的时候,我们可以先对它进行 sort 排序处理,再使用 binarySearch 进行二分查找元素。
下面是二分查找的代码具体实现:
private static int binarySearch0(int[] a, int fromIndex, int toIndex, int key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
int midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
- 比较两个数组对应位置数据是否一样
int[] arr1 = {1,3,5,7,9};
int[] arr2 = {1,3,5,7,9};
System.out.println(Arrays.equals(arr1, arr2));
//执行结果
true
equals 方法,如果两个指定的int数组彼此 相等 ,则返回 true。
=
- 填充数据
int[] arr = new int[4];
Arrays.fill(arr,1);
System.out.println(Arrays.toString(arr));
//执行结果
[1, 1, 1, 1]
fill 方法,将指定的int值分配给指定的int数组的每个元素。
总结
这一节,我们学习了Java中的方法和数组,虽然和C有些许不同,但大多数地方还是非常相似的,主要解决以下问题:
怎样定义方法?
方法的执行过程是什么?
实参和形参存在怎样的关系?
什么是方法的重载?
递归是怎么实现的?
怎么初始化数组?
数组为什么称作引用变量?
数组在空间中是怎么指向的?
数组的常用方法有哪些?