Java自学指南
一、Java程序基础
十分推荐去看廖雪峰老师的这个Java教程 廖雪峰Java教程(Java 20),资源开源的,代码全在gitee,语言精炼质朴,通俗易懂,别去看CSDN乱七八糟的博客,也别去买书了,配合B站教程食用即可,获得最佳白嫖 体验。
本篇博客是精简后,用于自我学习记录的,是博主自己抓的重点,遗忘点,和易错点,可能并不适用于基础者(博主先学了C++/C、Python),所以大多数情况下,本篇博客是写的Java和C++的不同之处。
1.1 Java程序基础结构
public class Hello{
public static void main(String[] args){
System.out.println("Hello, world");
}
}
1.1 变量和数据类型
略
1.2 运算
略
1.3 字符和字符串
略
1.4 数组类型
和c++、python不同的是,Java创建数组必须先new
:
public class Hello{
public static void main(String[] args){
int[] StudentGrade = new int[5];
//创建一个int类型的数组,数组可以存储五个变量
StudentGrade[0] = 100;
StudentGrade[1] = 100;
StudentGrade[2] = 100;
StudentGrade[3] = 100;
StudentGrade[4] = 100;
//第二种写法,在定义时直接写明数组内容,编译器会自动计算数组个数
int[] StudentGrade1 = new int[] {54, 34, 98, 83, 77};
//进一步简化为:
int[] StudentGrade2 = {54, 34, 98, 83, 77};
System.out.println(StudentGrade[0]);
//可以用 数组变量.length 获取数组大小。
System.out.println(StudentGrade.length);
}
}
Java的数组有几个特点:
- 数组所有元素初始化为默认值,整型都是
0
,浮点型是0.0
,布尔型是false
; - 数组一旦创建后,大小就不可改变。
要访问数组中的某一个元素,需要使用索引。数组索引从0
开始,例如,5个元素的数组,索引范围是0~4
。
可以修改数组中的某一个元素,使用赋值语句,例如,StudentGrade[1] = 79;
。
可以用 数组变量.length
获取数组大小。
注意数组是引用类型,并且数组大小不可变。
1.4.1 字符串数组
public class Hello{
public static void main(String[] args){
String[] names = {
"ABC", "XYZ", "zoo"
};
String s = names[1];
names[1] = "cat";
System.out.println(s); //输出为XYZ
System.out.println(names[1]); //输出为cat
}
}
二、流程控制
2.1 输出和输入
现在都省略了class结构噢,就是省略了这个:
public class Hello{
public static void main(String[] args){
// 把下面的内容写在这里
// 放下面的代码片
}
}
输出
输出有两种,分别是换行和不换行:
System.out.println("println是换行的哟~");
System.out.print("print是不换行的哟~");
如果要把数据显示成我们期望的格式,就需要使用格式化输出的功能。格式化输出使用System.out.printf()
,通过使用占位符 %?
, printf()
可以把后面的参数格式化成指定格式:
double d = 3.1415926;
System.out.printf("%.2f\n", d); //显示3.14
System.out.printf("%.4f\n", d); //显示3.1415
Java的格式化功能提供了多种占位符,可以把各种数据类型“格式化”成指定的字符串:
占位符 | 说明 |
---|---|
%d | 格式化输出整数 |
%x | 格式化输出十六进制整数 |
%f | 格式化输出浮点数 |
%e | 格式化输出科学计数法表示的浮点数 |
%s | 格式化字符串 |
注意,由于%表示占位符,因此,连续两个%%表示一个%字符本身。
比如,下面这个例子是把十进制转换为十六进制,并且补足八位(在前面补零):
public class Hello{
public static void main(String[] args){
int n = 12345000;
System.out.printf("n = %d, hex = %08x", n, n);
//这里有两个输出,一个是%d,一个是%08x
}
}
输出结果是这个:
输入
和输出相比,Java的输入就要复杂得多。
我们先看一个从控制台读取一个字符串和一个整数的例子:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 创建Scanner对象
System.out.print("Input your name: "); // 打印提示
String name = scanner.nextLine(); // 读取一行输入并获取字符串
System.out.print("Input your age: "); // 打印提示
int age = scanner.nextInt(); // 读取一行输入并获取整数
System.out.printf("Hi, %s, you are %d\n", name, age); // 格式化输出
}
}
首先,我们通过import
语句导入java.util.Scanner
,import是导入某个类的语句,必须放到Java源代码的开头,后面我们在Java的package中会详细讲解如何使用import。
然后,创建Scanner
对象并传入System.in
。System.out
代表标准输出流,而System.in
代表标准输入流。
有了Scanner对象后,要读取用户输入的字符串,使用scanner.nextLine()
,要读取用户输入的整数,使用scanner.nextInt()
。Scanner会自动转换数据类型,因此不必手动转换。
2.2 if判断
单个if的时候,语法结构:
if (n < 60) {
// ...
}
if … else if… else … 串联判断
也可以使用多个if串联,但是注意程序执行是从上到下,不会返回来执行开头的if判断,所以写程序时注意顺序:
if (n >= 90) {
// n >= 90为true:
System.out.println("优秀");
} else if (n >= 60) {
// n >= 60为true,这里判断的其实是60~90分的人
System.out.println("及格了");
} else {
// 这里判断的是<60分的人
System.out.println("挂科了");
}
在串联使用多个if时,要特别注意判断顺序,正确的方式是按照判断范围从大到小依次判断,或者改写成从小到大依次判断,不能乱七八糟的。
浮点数判断相等
浮点数在计算机中常常无法精确表示,并且计算可能出现误差,因此,判断浮点数相等用==
判断不靠谱,一般是判断小于某个区间即可。
例如这样,计算得出的结果往往是两者不相等:
public class Hello{
public static void main(String[] args){
double x = 1 - 9.0 / 10;
if ( x == 0.1){
System.out.println("x is 0.1");
}else {
System.out.printf("x is NOT 0.1, x is %f", x );
}
}
}
但是看运行结果,似乎x又是0.1的样子:
比较浮点数是否相等,正确的方法是利用差值小于某个临界值来判断:
public class Hello{
public static void main(String[] args){
double x = 1 - 9.0 / 10;
if (Math.abs(x-0.1) < 0.0000001){
//这里是一个调用的数学函数,表示取x-0.1的绝对值,判断x和0.1的差值有多少
//如果差值小于0.0000001,就证明x=0.1
System.out.println("x is 0.1");
}else {
System.out.printf("x is NOT 0.1, x is %f", x );
}
}
}
运行结果:
判断引用类型是否相等
在Java中,判断值类型的变量是否相等,可以使用==
运算符。
但是,判断引用类型的变量是否相等,==
表示 引用是否相等 ,或者说,是否指向同一个对象。
例如,下面的两个String类型,它们的内容是相同的,但是,分别指向不同的对象,用==
判断,结果为false
:
public class Hello{
public static void main(String[] args){
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1);
System.out.println(s2);
if(s1 == s2){
System.out.println("s1==s2");
}else {
System.out.println("s1!=s2");
}
}
}
这个时候要用equils()
来判断内容是否相等,而不是==
,==
相当于判断指向的地址是不是一个地址,而不是判断地址里面的内容。
if(s1.equals(s2) ){
System.out.println("s1==s2");
}else {
System.out.println("s1!=s2");
}
null错误
执行语句s1.equals(s2)
时,如果变量s1为null
,会报NullPointerException
,这时我们可以利用&&
这样写:
public class Hello{
public static void main(String[] args){
String s1 = null;
String s2 = "HELLO";
if(s1!=null && s1.equals(s2) ){
System.out.println("s1---1");
}else if(s1 == null){
System.out.println("s1---3");
}
}
}
输出结果:
小结
if ... else
可以做条件判断,else
是可选的;
不推荐省略花括号{ }
;
多个if ... else
串联要特别注意判断顺序;
要注意if的边界条件;
要注意浮点数判断相等不能直接用==
运算符;
引用类型判断内容相等要使用equals(),注意避免NullPointerException
。
2.3 switch多重选择
switch语句根据switch (表达式)计算的结果,跳转到匹配的case结果,然后继续执行后续语句,直到遇到break结束执行。
使用switch时,注意case语句并没有花括号{},而且,case语句具有“穿透性”,一定要等到break才结束,而C++里面是可以用花括号的,这一点和c++不同,要注意。
所以,一定记得要用break结束每一个case!!!
public class Main {
public static void main(String[] args) {
int option = 1;
switch (option) {
case 1:
System.out.println("Selected 1");
break;
case 2:
System.out.println("Selected 2");
break;
case 3:
System.out.println("Selected 3");
break;
default:
System.out.println("你没有选择给定的数字");
break;
}
}
}
如果有几个case语句执行的是同一组语句块,可以这么写:
public class Main {
public static void main(String[] args) {
int option = 2;
switch (option) {
case 1:
System.out.println("Selected 1");
break;
case 2:
case 3:
System.out.println("Selected 2, 3");
break;
default:
System.out.println("Not selected");
break;
}
}
}
switch匹配字符串
switch语句还可以匹配字符串。字符串匹配时,是比较“内容相等”。例如:
public class Main {
public static void main(String[] args) {
String fruit = "apple";
switch (fruit) {
case "apple":
System.out.println("Selected apple");
break;
case "pear":
System.out.println("Selected pear");
break;
case "mango":
System.out.println("Selected mango");
break;
default:
System.out.println("No fruit selected");
break;
}
}
}
switch模式匹配方法
使用switch
时,如果遗漏了break
,就会造成严重的逻辑错误,而且不易在源代码中发现错误。从Java 12开始,switch
语句升级为更简洁的表达式语法,使用类似**模式匹配(Pattern Matching)**的方法,保证只有一种路径会被执行,并且不需要break
语句:
public class Main {
public static void main(String[] args) {
String fruit = "apple";
switch (fruit) {
case "apple" -> System.out.println("Selected apple");
case "pear" -> System.out.println("Selected pear");
case "mango" -> {
System.out.println("Selected mango");
System.out.println("Good choice!");
}
default -> System.out.println("No fruit selected");
}
}
}
而且使用模式匹配的方法,也可以使用花括号{ }
了。
注意新语法使用->,如果有多条语句,需要用{}括起来。不要写break
语句,因为新语法只会执行匹配的语句,没有穿透效应。
用switch给变量赋值
很多时候,我们还可能用switch
语句给某个变量赋值。例如:
int opt;
switch (fruit) {
case "apple":
opt = 1;
break;
case "pear":
case "mango":
opt = 2;
break;
default:
opt = 0;
break;
}
使用新的switch语法,不但不需要break,还可以直接返回值。把上面的代码改写如下:
public class Main {
public static void main(String[] args) {
String fruit = "apple";
int opt = switch (fruit) {
case "apple" -> 1;
case "pear", "mango" -> 2;
default -> 0;
}; // 注意赋值语句要以;结束
System.out.println("opt = " + opt);
}
}
yield
大多数时候,在switch
表达式内部,我们会返回简单的值。
但是,如果需要复杂的语句,我们也可以写很多语句,放到{…}里,然后,用yield
返回一个值作为switch
语句的返回值:
public class Main {
public static void main(String[] args) {
String fruit = "orange";
int opt = switch (fruit) {
case "apple" -> 1;
case "pear", "mango" -> 2;
default -> {
int code = fruit.hashCode();
yield code; // switch语句返回值
}
};
System.out.println("opt = " + opt);
}
}
2.4 while循环
while循环是先判断循环条件,再循环,因此,有可能一次循环都不做。
while (条件表达式) {
循环语句
}
// 继续执行后续代码
while循环在每次循环开始前,首先判断条件是否成立。如果计算结果为true,就把循环体内的语句执行一遍,如果计算结果为false,那就直接跳到while循环的末尾,继续往下执行。
如果循环条件永远满足,那这个循环就变成了死循环。死循环将导致100%的CPU占用,用户会感觉电脑运行缓慢,所以要避免编写死循环代码。
2.5 do while循环
在Java中,while循环是先判断循环条件,再执行循环。而另一种do while循环则是先执行循环,再判断条件,条件满足时继续循环,条件不满足时退出。它的用法是:
do {
执行循环语句
} while (条件表达式);
可见,do while
循环会至少循环一次。
我们把对1到100的求和用do while循环改写一下:
public class Main {
public static void main(String[] args) {
int sum = 0;
int n = 1;
do {
sum = sum + n;
n ++;
} while (n <= 100);
System.out.println(sum);
}
}
2.6 for循环
和c++一样,懒得写了
2.7 break和continue
break会跳出当前循环,也就是整个循环都不会执行了。而continue则是提前结束本次循环,直接继续执行下次循环。
小结
break语句可以跳出当前循环;
break语句通常配合if,在满足条件时提前结束整个循环;
break语句总是跳出最近的一层循环;
continue语句可以提前结束本次循环;
continue语句通常配合if,在满足条件时提前结束本次循环。
三、数组操作
3.1 遍历数组
循环遍历
通过for循环就可以遍历数组。因为数组的每个元素都可以通过索引来访问,因此,使用标准的for循环可以完成一个数组的遍历:
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i<ns.length; i++) {
int n = ns[i];
System.out.println(n);
}
}
}
for each 遍历数组
第二种方式是使用for each循环,直接迭代数组的每个元素:
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
for (int n : ns) {
System.out.println(n);
}
}
}
注意:在for (int n : ns)循环中,变量n直接拿到ns数组的元素内容,而不是索引。
显然for each循环更加简洁。但是,for each循环无法拿到数组的索引,因此,到底用哪一种for循环,取决于我们的需要。
Arrays.toString()直接打印
使用for each循环打印也很麻烦。幸好Java标准库提供了Arrays.toString(),可以快速打印数组内容:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 1, 2, 3, 5, 8 };
System.out.println(Arrays.toString(ns));
}
}
小结
遍历数组可以使用for循环,for循环可以访问数组索引,for each循环直接迭代每个数组元素,但无法获取索引;
使用Arrays.toString()
可以快速获取数组内容。
3.2 数组排序
对数组进行排序是程序中非常基本的需求。常用的排序算法有冒泡排序、插入排序和快速排序等。
我们来看一下如何使用冒泡排序算法对一个整型数组从小到大进行排序:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] ns = { 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 };
// 排序前:
System.out.println(Arrays.toString(ns));
for (int i = 0; i < ns.length - 1; i++) {
for (int j = 0; j < ns.length - i - 1; j++) {
if (ns[j] > ns[j+1]) {
// 交换ns[j]和ns[j+1]:
int tmp = ns[j];
ns[j] = ns[j+1];
ns[j+1] = tmp;
}
}
}
// 排序后:
System.out.println(Arrays.toString(ns));
}
}
冒泡排序的特点是,每一轮循环后,最大的一个数被交换到末尾,因此,下一轮循环就可以“刨除”最后的数,每一轮循环都比上一轮循环的结束位置靠前一位。
另外,注意到交换两个变量的值必须借助一个临时变量。
实际上,Java的标准库已经内置了排序功能,我们只需要调用JDK提供的Arrays.sort()
就可以排序:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] ns = { 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 };
Arrays.sort(ns);
System.out.println(Arrays.toString(ns));
}
}
必须注意,对数组排序实际上修改了数组元素本身。
原来的3个字符串在内存中均没有任何变化,但是指向字符串内容的ns数组的每个元素变化了。
如果对一个字符串数组进行排序,例如:
String[] ns = { "banana", "apple", "pear" };
排序前,这个数组在内存中表示如下:
┌──────────────────────────────────┐
┌───┼──────────────────────┐ │
│ │ ▼ ▼
┌───┬─┴─┬─┴─┬───┬────────┬───┬───────┬───┬──────┬───┐
ns ─────▶│░░░│░░░│░░░│ │"banana"│ │"apple"│ │"pear"│ │
└─┬─┴───┴───┴───┴────────┴───┴───────┴───┴──────┴───┘
│ ▲
└─────────────────┘
调用Arrays.sort(ns)
;排序后,这个数组在内存中表示如下:
┌──────────────────────────────────┐
┌───┼──────────┐ │
│ │ ▼ ▼
┌───┬─┴─┬─┴─┬───┬────────┬───┬───────┬───┬──────┬───┐
ns ─────▶│░░░│░░░│░░░│ │"banana"│ │"apple"│ │"pear"│ │
└─┬─┴───┴───┴───┴────────┴───┴───────┴───┴──────┴───┘
│ ▲
└──────────────────────────────┘
原来的3个字符串在内存中均没有任何变化,但是ns数组的每个元素指向变化了。
这里的意思就是,初始化数组内容的时候,数组内容就被固定死了,但是ns数组指向这些内容的元素,类似于一个指针的东西发生了改变。
也就是说,排序后,内存中的内容没变,变的是ns的指向。
(可能有人问,了解这个东西有什么作用呢,但是,说不定什么时候就坑你一下呢?就譬如前面的==
和equils
,也是这个元素和地址的概念哟呵~)
3.3 多维数组
二维数组
二维数组就是数组的数组。定义一个二维数组如下:
public class Main {
public static void main(String[] args) {
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
System.out.println(ns.length); // 3
}
}
因为ns包含3个数组,因此,ns.length
为3。实际上ns在内存中的结构如下:
┌───┬───┬───┬───┐
┌───┐ ┌──▶│ 1 │ 2 │ 3 │ 4 │
ns ─────▶│░░░│──┘ └───┴───┴───┴───┘
├───┤ ┌───┬───┬───┬───┐
│░░░│─────▶│ 5 │ 6 │ 7 │ 8 │
├───┤ └───┴───┴───┴───┘
│░░░│──┐ ┌───┬───┬───┬───┐
└───┘ └──▶│ 9 │10 │11 │12 │
└───┴───┴───┴───┘
如果我们定义一个普通数组arr0,然后把ns[0]赋值给它:
public class Main {
public static void main(String[] args) {
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
int[] arr0 = ns[0];
System.out.println(arr0.length); // 4
}
}
实际上arr0就获取了ns数组的第0个元素。因为ns数组的每个元素也是一个数组,因此,arr0指向的数组就是{ 1, 2, 3, 4 }。在内存中,结构如下:
arr0 ─────┐
▼
┌───┬───┬───┬───┐
┌───┐ ┌──▶│ 1 │ 2 │ 3 │ 4 │
ns ─────▶│░░░│──┘ └───┴───┴───┴───┘
├───┤ ┌───┬───┬───┬───┐
│░░░│─────▶│ 5 │ 6 │ 7 │ 8 │
├───┤ └───┴───┴───┴───┘
│░░░│──┐ ┌───┬───┬───┬───┐
└───┘ └──▶│ 9 │10 │11 │12 │
└───┴───┴───┴───┘
访问二维数组的某个元素需要使用array[row][col]
,例如:
System.out.println(ns[1][2]); // 7
二维数组的每个数组元素的长度并不要求相同,例如,可以这么定义ns数组:
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6 },
{ 7, 8, 9 }
};
要打印一个二维数组,可以使用两层嵌套的for循环:
for (int[] arr : ns) {
for (int n : arr) {
System.out.print(n);
System.out.print(', ');
}
System.out.println();
}
或者使用Java标准库的Arrays.deepToString()
:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
System.out.println(Arrays.deepToString(ns));
}
}
三维数组
三维数组就是二维数组的数组。
如果我们要访问三维数组的某个元素,例如,ns[2][0][1],只需要顺着定位找到对应的最终元素15即可。
理论上,我们可以定义任意的N维数组。但在实际应用中,除了二维数组在某些时候还能用得上,更高维度的数组很少使用。
3.4 命令行参数
(解决了我多年的疑惑,一直不知道程序入口的String[] args
是干嘛的,原来是为了控制台传参数的时候,调参数的)
Java程序的入口是main
方法,而main
方法可以接受一个命令行参数,它是一个String[]
数组。
这个命令行参数由JVM接收用户输入并传给main
方法:
public class Main {
public static void main(String[] args) {
for (String arg : args) {
System.out.println(arg);
}
}
}
我们可以利用接收到的命令行参数,根据不同的参数执行不同的代码。例如,实现一个-version参数,打印程序版本号:
public class Main {
public static void main(String[] args) {
for (String arg : args) {
if ("-version".equals(arg)) {
System.out.println("v 1.0");
break;
}
}
}
}
上面这个程序必须在命令行执行,我们先编译它:
$ javac Main.java
然后,执行的时候,给它传递一个-version
参数:
$ java Main -version
v 1.0
这样,程序就可以根据传入的命令行参数,作出不同的响应。
小结
命令行参数类型是String[]
数组;
命令行参数由JVM接收用户输入并传给main
方法;
如何解析命令行参数需要由程序自己实现。
后续部分的内容,见开头的跳转链接,去到下一个博客了~