文章目录
1️⃣输入和输出
1.输出到控制台
基本语法
这里有三种基本输出方式:
System.out.println(msg); // 输出一个字符串, 带换行
System.out.print(msg); // 输出一个字符串, 不带换行
System.out.printf(format, msg); // 格式化输出
println
输出的内容自带\n
,print
不带\n
printf
的格式化输出方式和 C 语言的printf
是基本一致的.
不需要刻意去记住,使用的时候再查询也ok
代码示例
System.out.println("hello world");
int x = 10;
System.out.printf("x = %d\n", x)
2.从键盘输入
输入整型、字符串、浮点型
import java.util.Scanner; // 需要导入 util 包
Scanner sc = new Scanner(System.in);
System.out.println("请输入你的姓名:");
//读取字符串
String name = sc.nextLine();
System.out.println("请输入你的年龄:");
//读取整型
int age = sc.nextInt();
System.out.println("请输入你的工资:");
//读取浮点型
float salary = sc.nextDouble();
System.out.println("你的信息如下:");
System.out.println("姓名: "+name+"\n"+"年龄:"+age+"\n"+"工资:"+salary);
sc.close(); // 注意, 要记得调用关闭方法
// 执行结果
请输入你的姓名:
张三
请输入你的年龄:
18
请输入你的工资:
1000
你的信息如下:
姓名: 张三
年龄:18
工资:1000.0
输入字符
先创建一个Scanner
对象,调用Scanner
对象的next()
方法获取控制台输入的字符串,返回的是一个String
类型,因为没有nextChar()
方法,所以调用String
的charAt(0)
方法获取第一个字符,这样一来,我们就输入了一个字符串:
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
char c = scanner.next().charAt(0);
next() 和 nextLine()的区别
两者都可以从键盘读取输入,但是又有点差别:
next()方法
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入字符串:");
System.out.println("输入的字符串为:" + sc.next());
System.out.println("结束了");
}
}
输出结果:
nextLine()方法
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入字符串:");
System.out.println("输入的字符串为:" + sc.nextLine());
System.out.println("结束了");
}
}
输出结果:
从两者的比较中可以清楚的看到区别,next()
方法在键盘中读取的时候,空格和回车都可以作为字符串输入结束,而nextLine()
不会将空格作为输入结束,而是将其读取保存到字符串中。
使用 Scanner 循环连续读取
hasnext()方法
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入字符串:");
while(sc.hasNext()){
System.out.println("输入的字符串为:" + sc.next());
}
System.out.println("结束了");
}
}
输出结果:
hasNextLine()方法
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入字符串:");
while(sc.hasNextLine()){
System.out.println("输入的字符串为:" + sc.nextLine());
}
System.out.println("结束了");
}
}
输出结果:
从比较中可以看出,两个方法都可以从键盘连续读取,直到使用Ctrl + D
退出连续读取。此时循环读取的时候,next()
方法依旧将空格看作一个字符串输入完毕,而nextLine()
方法则不会。
循环输入的注意事项
通过在网上搜索,获取到这两种方法的区别:
在检查输入流时:
hasNext()
方法会判断接下来是否有非空字符.如果有,则返回true
,否则返回false
hasNextLine()
方法会根据行匹配模式去判断接下来是否有一行(包括空行),如果有,则返回true
,否则返回false
比如当前我们有如下测试用例:
7 15 9 5
这个测试用例在牛客网上是以文件的形式进行存储的.
而在 linux 系统中文件的结尾会有一个换行符\n
,也就是说从System.in
输入流中真正读取到的数据流是这样的:
7 15 9 5\n
程序在处理完5之后,输入流中就只剩下一个换行符\n
了,在处理完5之后while
再去进行循环判断,此时hasNext()
方法和hasNextLine()
方法去判断得到的结果就产生了差异.
hasNext()
方法会认为之后再没有非空字符,会返回一个false
hasNextLine()
方法会认为换行符\n是一个空行,符合行的匹配模式,则会返回一个true
,但实际上由于之后再没有数据了,所以会在读取输入流的时候发生异常,从而导致整个运行报错.
建议方案:
采用hasNextXxxx()
的话,后面也要用nextXxxx()
:
- 比如前面用
hasNextLine()
,那么后面要用nextLine()
来处理输入; - 后面用
nextInt()
方法的话,那么前面要使用hasNextInt()
方法去判断.
执行过程
可以这样进行理解:
方法解释:如果此扫描器的输入(缓冲区)中有另一个token(输入的字符串),则返回true。what? 根本没有提到什么时候返回false。其实执行过程是这样的。重点:当执行到hasNext()时,它会先扫描缓冲区中是否有字符,有则返回true,继续扫描。直到扫描为空,这时并不返回false,而是将方法阻塞,等待你输入内容然后继续扫描。
解决方法
除了使用Ctrl + D
,还可以使用
带有参数的重载方法,当扫描到的字符与参数值匹配时返回true
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入字符串:");
while(!sc.hasNext("#")){//匹配#返回true,然后取非运算。即以#为结束符号
System.out.println("输入的字符串为:" + sc.next());
}
System.out.println("结束了");
}
}
运行结果:
3.关闭输入的注意事项
如果创建了两个Scanner
的对象,在第一个对象的输入使用完毕后,使用close()
进行关闭输入后,再用第二个Scanner
的对象从键盘读取输入的时候,此时会报错。
这是因为虽然是两个独立的对象,但是用的是同一个输入流,在调用close()
实际上相当System.in.close()
,对于后面的Scanner
对象来说,System.in
已经被关闭了。
解决办法:
在所有其他的类中不使用使用close()
方法,最后调用close()
.
不管有几个Scanner
的对象,close()
只能调用一次.
2️⃣方法的使用
方法就是一个代码片段. 类似于 C 语言中的 “函数”.
方法存在的意义:
- 是能够模块化的组织代码(当代码规模比较复杂的时候)。
- 该代码片段可被重复使用,并且可以在多个位置调用。
- 让代码更好理解更简单,可以让代码更好地实现解耦。
- 直接调用现有方法开发, 不必重复造轮子。
1.方法的基本用法
基本语法
// 方法定义
public static 方法返回值 方法名称([参数类型 形参 ...]){
方法体代码;
[return 返回值];
}
// 方法调用
返回值变量 = 方法名称(实参...);
用法示例
实现一个方法实现两个整数相加
class Test {
public static void main(String[] args) {
int a = 10;
int b = 20;
// 方法的调用
int ret = add(a, b);
System.out.println("ret = " + ret);
}
// 方法的定义
public static int add(int x, int y) {
return x + y;
}
}
// 执行结果
ret = 30
注意事项
- public 和 static 两个关键字后面讲解.
- 方法定义时, 参数可以没有. 每个参数要指定类型
- 方法定义时, 返回值也可以没有, 如果没有返回值, 则返回值类型应写成 void
- 方法定义时的参数称为 “形参”, 方法调用时的参数称为 “实参”.
- 方法的定义必须在类之中, 代码书写在调用位置的上方或者下方均可.
在c语言中,函数的实现必须在使用之前,要么在使用前声明,否则编译报错。 - 然而Java 中没有 “函数声明” 这样的概念.
一旦定义了方法就必须写它的实现.
2.方法调用的执行过程
基本规则
- 定义方法的时候, 不会执行方法的代码. 只有调用的时候才会执行.
- 当方法被调用的时候, 会将实参赋值给形参.
- 参数传递完毕后, 就会执行到方法体代码.
- 当方法执行完毕之后(遇到 return 语句), 就执行完毕, 回到方法调用位置继续往下执行.
- 一个方法可以被多次调用.
在IDEA中快速创建方法的快捷键:Alt + Enter
比如要实现两个整数相加的方法:
直接写出调用函数,写明函数名和参数以及返回值,再按下快捷键,选择第一项就可以在后面直接创建一个方法。
3.实参和形参的关系(重要)
代码示例:
class Test {
public static void main(String[] args) {
int a = 10;
int b = 20;
swap(a, b);
System.out.println("a = " + a + " b = " + b);
}
public static void swap(int x, int y) {
int tmp = x;
x = y;
y = tmp;
}
}
// 运行结果
a = 10 b = 20
结果分析
刚才的代码, 没有完成数据的交换.
对于基础类型来说, 形参相当于实参的拷贝.
原因是Java方法的参数传递只有值传递,只是将实参的值复制给形参而已。
变量的地址对于用户全部是不可见的,不能像c语言中将变量的地址当做形参传递。
原因是JVM将地址全部藏起来了。
解决办法:传引用类型参数(用数组可以解决上述问题)
class Test {
public static void main(String[] args) {
int[] arr = {10, 20};
swap(arr);
System.out.println("a = " + arr[0] + " b = " + arr[1]);
}
public static void swap(int[] arr) {
int tmp = arr[0];
arr[0] = arr[1];
arr[1] = tmp;
}
}
// 运行结果
a = 20 b = 10
4.方法重载
使用重载
如果想要实现两个整数相加,只需要一个方法就可以了,但是如果还要实现两个甚至更多浮点型相加,并且不想改变方法名的话,就需要用到方法重载。
例如:
class Test {
public static void main(String[] args) {
int a = 10;
int b = 20;
int ret = add(a, b);
System.out.println("ret = " + ret);
double a2 = 10.5;
double b2 = 20.5;
double ret2 = add(a2, b2);
System.out.println("ret2 = " + ret2);
double a3 = 10.5;
double b3 = 10.5;
double c3 = 20.5;
double ret3 = add(a3, b3, c3);
System.out.println("ret3 = " + ret3);
}
public static int add(int x, int y) {
return x + y;
}
public static double add(double x, double y) {
return x + y;
}
public static double add(double x, double y, double z) {
return x + y + z;
}
}
还有控制输出的println()
方法,之所以什么都可以往里面仍进行输出,就因为该方法是重载的,能针对各种传递的参数。
重载的规则
针对同一个类:
- 方法名相同
- 方法的参数不同(参数个数或者参数类型)
- 方法的返回值类型不影响重载.
- 当两个方法名字相同,参数也相同时,但是返回值不相同的时候,不构成重载
5.方法递归
递归在之前c语言的时候就已经写过了,这里不给出具体阐述了,其思想和实现都是差不多的,只是语法上有些差别。
感兴趣的可以看看之前写的关于c语言函数相关的博客,递归的内容在文章后面
传送门🌀已为您打开
然后就是一道困扰我很久的一道递归题(很经典),写了一篇很详细的博客去讲解它,感兴趣的可以戳戳看:
3️⃣数组的定义和使用
1.数组的基本用法
创建数组
// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };
// 静态初始化
数据类型[] 数组名称 = { 初始化数据 };
代码示例:
int[] arr = new int[3];//创建一个长度为3的整型数组,未初始化内部元素都是默认值,这里是0
int[] arr = new int[]{1, 2, 3};
int[] arr = {1, 2, 3};//静态数组编译后,会变成上面那种方式,即动态初始化
int[] arr = new int[2]{1,1};//错误的使用方式
其实数组也可以写成
int arr[] = {1, 2, 3};
这样就和 C 语言更相似了. 但是我们还是更推荐写成 int[] arr 的形式. int和 [] 是一个整体.
数组的使用
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
能够获取到数组的长度..
这个操作为成员访问操作符. 这里不像 c 语言求数组长度要使用sizeof
关键字。 - 下标访问操作不能超出有效范围
[0, length - 1]
, 如果超出有效范围, 会出现下标越界异常
抛出java.lang.ArrayIndexOutOfBoundsException
异常,使用数组一定要下标谨防越界.
遍历数组
除了常见的定义变量充当下标使用外,还可以使用for-each
遍历数组,能够更方便的完成对数组的遍历.,可以避免循环条件和更新语句写错.
int[] arr = {1, 2, 3};
for (int x : arr) {
System.out.println(x);
}
// 执行结果
1
2
3
相当于是创建临时变量,变量依次从数组中拿出值,将值复制给变量而已
2.数组作为方法的参数
基本用法
当参数传内置类型时,对形参进行修改,观察其输出值变化
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 值,原因是形参和实参是两块内存:
当传入引用类型数组的时候,修改值观察值变化:
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
为什么数组作方法的参数后就能修改呢?
先了解什么是引用?
引用相当于一个 “别名”, 也可以理解成一个指针.
创建一个引用只是相当于创建了一个很小的变量, 这个变量保存了一个整数, 这个整数表示内存中的一个地址.
针对 int[] arr = new int[]{1, 2, 3}
这样的代码, 内存布局如图:
- 当我们创建
new int[]{1, 2, 3}
的时候, 相当于创建了一块内存空间保存三个 int - 接下来执行
int[] arr = new int[]{1, 2, 3}
相当于又创建了一个 int[] 变量, 这个变量是一个引用类型, 里面只保存了一个整数(数组的起始内存地址)
- 接下来我们进行传参相当于
int[] a = arr
, 内存布局如图
- 接下来我们修改
a[0]
, 此时是根据0x100
这样的地址找到对应的内存位置, 将值改成100
此时已经将 0x100
地址的数据改成了 100
. 那么根据实参 arr
来获取数组内容 arr[0]
, 本质上也是获取 0x100
地址上的数据, 也是 100
.
总结: 所谓的 “引用” 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).
Java中的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
.
注意:在 C 语言中,NULL和 0 号地址是有关系的,即(void *) 0
,通过强转将0号地址规定成空指针。
但是在Java 中并没有约定 null 和 0 号地址的内存有任何关联.
初识 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 来调用的函数.
虚拟机栈 和 堆:
- 局部变量和引用保存在栈上, new 出的对象保存在堆上.
- 堆的空间非常大, 栈的空间比较小.
- 堆是整个 JVM 共享一个, 而栈每个线程具有一份(一个 Java 程序中可能存在多个栈).
3.数组作为方法的返回值
此处没有什么好讲的,看下代码了解如何使用:
// 写一个方法, 将数组中的每个元素都 * 2
class Test {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
int[] output = transform(arr);
}
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.数组的实用方法
JDK内置了数组的工具包,即java.util.Arrays
包, 其中包含了一些操作数组的常用方法.
数组转字符串
Arrays.toString()方法
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]
数组拷贝
Arrays.copyOfRange()方法
Arrays.copyOf(原数组名称,拷贝后的新数组长度)
//从数组的第一个元素开始拷贝
Arrays.copyOf()方法
Arrays.copyOfRange(原数组名称,原数组起始位置,原数组结束位置)
//拷贝某个范围
//左闭右开
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));
注意事项: 相比于 newArr = arr 这样的赋值, copyOf 是将数组进行了 深拷贝, 即又创建了一个数组对象, 拷贝原有数组中的所有元素到新数组中. 因此, 修改原数组, 不会影响到新数组.
数组排序
Arrays.sort()方法
//默认升序
//JDK默认的双轴快排
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
数组二分查找
针对有序数组, 可以使用更高效的二分查找.
啥叫有序数组?
有序分为 “升序” 和 “降序”
如 1 2 3 4 , 依次递增即为升序.
如 4 3 2 1 , 依次递减即为降序.
以升序数组为例, 二分查找的思路是先取中间位置的元素, 看要找的值比中间元素大还是小. 如果小, 就去左边找; 否则就去右边找.
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
5.二维数组
二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组.
基本语法
数据类型[][] 数组名称 = 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
获取二维数组长度
//定义一个整型数组:3行4列
int a[][] = new int[3][4];
//获取行数---3行
int row = a.length;
//获取列数---4列
int col = a[0].length;
在二维数组内部存放的也是一位数组,arr.length
获取的是二维数组的长度,即有多少个一维数组;a[0].length
获取的是二维数组中一维数组的长度。