Java快速入门
Java简介
- Java介于编译型语言和解释型语言之间。编译型语言如C、C++,代码是直接编译成机器码执行,解释型语言如Python可以由解释器直接加载源码然后运行,代价就是运行效率太低。而Java是将代码编译成一种“字节码”,它类似于抽象的CPU指令,然后,针对不同平台编写虚拟机,不同平台的虚拟机负责加载字节码并执行,这样就实现了“一次编写,到处运行”的效果。
- java的三个不同版本
- java的学习路线
- 安装配置好java环境以后,可以在cmd命令行当中输入 java - version,如果出现下面情况,则表明一切正常
- 第一个java程序
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
在一个java程序当中总能找到一个类似于:
public class Hello {
...
}
上面这个定义被称之为class类,这里的类名就是Hello,大小写敏感,class用来定义一个类,public表示这个类是公开的,public、class都是Java的关键字,必须小写,Hello是类的名字,按照习惯,首字母H要大写。而花括号{}中间则是类的定义。
在类的定义中,我们定义了一个名为main的方法:
public static void main(String[] args) {
...
}
方法是可执行的代码块,一个方法除了方法名main,还有用()括起来的方法参数,这里的main方法有一个参数,参数类型是String[],参数名是args,public、static用来修饰方法,这里表示它是一个公开的静态方法,void是方法的返回类型,而花括号{}中间的就是方法的代码。
方法中的代码的每一行用**;**结束,这里只有一行代码就是:
System.out.println("Hello, world!");
Java规定,某个类定义的public static void main(String[] args)是Java程序的固定入口方法,因此,Java程序总是从main方法开始执行。
- 如何运行Java程序
注意点: - IDE是集成开发环境:Integrated Development Environment的缩写。
Java程序基础
java程序基本结构
- 书写一个完整的java程序,基本结构如下所示:
/**
* 可以用来自动创建文档的注释
*/
public class Hello {
public static void main(String[] args) {
// 向屏幕输出文本:
System.out.println("Hello, world!");
/* 多行注释开始
注释内容
注释结束 */
}
} // class定义结束
因为Java是面向对象的语言,一个程序的基本单位就是class,class是关键字,这里定义的class名字就是Hello:
public class Hello { // 类名是Hello
// ...
} // class定义结束
类名要求:① 类名必须以英文字母开头,后接字母,数字和下划线的组合 ② 习惯以大写字母开头
几个要点:① public是访问修饰符,表示class是公开的 ② 不写public也可以正常编译, 但是这个类没有办法从命令行执行 ③ 在class内部,可以定义若干方法
public class Hello {
public static void main(String[] args) { // 方法名是main
// 方法代码...
} // 方法定义结束
}
- 在方法的内部,语句才是真正的执行代码,Java的每一行语句必须以分号结束;
- java的三种注释
① 第一种是单行注释,以双斜线开头,直到这一行的结尾结束:
// 这是注释...
② 多行注释以/星号开头,以/结束,可以有多行:
/*
这是注释
blablabla...
这也是注释
*/
③ 还有一种特殊的多行注释,以/*开头,以/结束,如果有多行,每行通常以星号开头:
/**
* 可以用来自动创建文档的注释
*
* @auther liaoxuefeng
*/
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
- 这种特殊的多行注释需要写在类和方法的定义处,可以用于自动创建文档。
- Java程序对格式没有明确的要求,多几个空格或者回车不影响程序的正确性,但是我们要养成良好的编程习惯,注意遵守Java社区约定的编码格式。
- Eclipse IDE提供了快捷键Ctrl+Shift+F(macOS是⌘+⇧+F)帮助我们快速格式化代码的功能,
变量和数据类型
- 什么是变量?
变量就是初中数学的代数的概念,例如一个简单的方程,x,y都是变量:
y = x + 1 - java中变量分为两种:基本类型的变量和引用类型的变量。
基本类型变量
- 变量必须先定义后使用,在定义变量的时候,可以给它一个初始值。例如:
int x = 1;
解释:上述语句定义了一个整型int类型的变量,名称为x,初始值为1。
- 不写初始值,就相当于给它指定了默认值。默认值总是0。
- 完整的定义变量,打印变量值的实例:
public class Main {
public static void main(String[] args) {
int x = 100; // 定义int类型变量x,并赋予初始值100
System.out.println(x); // 打印该变量的值
}
}
- !!!变量还有一个很重要的特点就是:可以重新赋值,对于变量x, 先赋值100,再赋值200,观察两次打印的结果。
public class Main {
public static void main(String[] args) {
int x = 100; // 定义int类型变量x,并赋予初始值100
System.out.println(x); // 打印该变量的值,观察是否为100
x = 200; // 重新赋值为200
System.out.println(x); // 打印该变量的值,观察是否为200
}
}
注意:第一次定义变量x的时候,需要指定变量类型int,因此使用语句int x = 100;。而第二次重新赋值的时候,变量x已经存在了,不能再重复定义,因此不能指定变量类型int,必须使用语句x = 200;。
- 变量不但可以重新赋值,还可以赋值给其它变量,实例:
// 变量之间的赋值
public class Main {
public static void main(String[] args) {
int n = 100; // 定义变量n,同时赋值为100
System.out.println("n = " + n); // 打印n的值
n = 200; // 变量n赋值为200
System.out.println("n = " + n); // 打印n的值
int x = n; // 变量x赋值为n(n的值为200,因此赋值后x的值也是200)
System.out.println("x = " + x); // 打印x的值
x = x + 100; // 变量x赋值为x+100(x的值为200,因此赋值后x的值是200+100=300)
System.out.println("x = " + x); // 打印x的值
System.out.println("n = " + n); // 再次打印n的值,n应该是200还是300?
}
}
- 变量可以反复赋值,注意:等号 = 是赋值语句,不是数学意义上的相等,否则无法解释 x = x + 100
基本数据类型
基本数据类型是CPU可以直接进行运算的类型。Java定义了以下几种基本数据类型:
- 整数类型:byte,short,int,long
- 浮点数类型:float,double
- 字符类型:char
- 布尔类型:boolean
Java定义的这些基本数据类型有没有什么区别?
想要知道区别,必须了解一下计算机内存的基本结构。计算机内存的最小存储单元是字节(byte),一个字节就是一个8位二进制数,即8个bit。它的二进制表示范围从0000000011111111,换算成十进制是0255,换算成十六进制是00~ff。
依次介绍几种数据类型
整型
对于整型类型,Java只定义了带符号的整型,因此,最高位的bit表示符号位(0表示正数,1表示负数)。各种整型能表示的最大范围如下:
- byte:-128 ~ 127
- short: -32768 ~ 32767
- int: -2147483648 ~ 2147483647
- long: -9223372036854775808 ~ 9223372036854775807
定义整型
public class Main {
public static void main(String[] args) {
int i = 2147483647;
int i2 = -2147483648;
int i3 = 2_000_000_000; // 加下划线更容易识别
int i4 = 0xff0000; // 十六进制表示的16711680
int i5 = 0b1000000000; // 二进制表示的512
long l = 9000000000000000000L; // long型的结尾需要加L
}
}
注意:同一个数的不同进制的表示是完全相同的,例如15=0xf=0b1111。
浮点型
浮点类型的数就是小数,因为小数用科学计数法表示的时候,小数点是可以“浮动”的,如1234.5可以表示成12.345x102,也可以表示成1.2345x103,所以称为浮点数。
- 定义浮点数的例子:
float f1 = 3.14f;
float f2 = 3.14e38f; // 科学计数法表示的3.14x10^38
double d = 1.79e308;
double d2 = -1.79e308;
double d3 = 4.9e-324; // 科学计数法表示的4.9x10^-324
注意点:
- 对于float类型,需要加上f后缀。
- 浮点数可表示的范围非常大,float类型可最大表示3.4x10^38,
- double类型可最大表示1.79x10^308。
布尔类型
布尔类型boolean只有true和false两个值,布尔类型总是关系运算的计算结果:
boolean b1 = true;
boolean b2 = false;
boolean isGreater = 5 > 3; // 计算结果为true
int age = 12;
boolean isAdult = age >= 18; // 计算结果为false
- ava语言对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要1 bit,但是通常JVM内部会把boolean表示为4字节整数。
字符类型
字符类型char表示一个字符。Java的char类型除了可表示标准的ASCII外,还可以表示一个Unicode字符:
public class Main {
public static void main(String[] args) {
char a = 'A';
char zh = '中';
System.out.println(a);
System.out.println(zh);
}
}
- char类型使用单引号’,且仅有一个字符,要和双引号"的字符串类型区分开。
引用类型
除了上述基本类型的变量,剩下的都是引用类型。例如,引用类型最常用的就是String字符串:
String s = "hello";
- 引用类型的变量类似于C语言的指针,它内部存储一个“地址”,指向某个对象在内存的位置
常量
定义变量的时候,如果加上final修饰符,这个变量就变成了常量:
final double PI = 3.14; // PI是一个常量
double r = 5.0;
double area = PI * r * r;
PI = 300; // compile error!
- 常量在定义时进行初始化后就不可再次赋值,再次赋值会导致编译错误。
- 常量的作用:有意义的变量名来避免魔术数字(Magic number),例如,不要在代码中到处写3.14,而是定义一个常量。如果将来需要提高计算精度,我们只需要在常量的定义处修改,例如,改成3.1416,而不必在所有地方替换3.14。
- 根据书写习惯,常量名通常全部大写。
var关键字
使用var定义变量,仅仅是少写了变量类型而已。
有些时候,类型的名字太长,写起来比较麻烦,例如:
StringBuilder sb = new StringBuilder();
这个时候如果想要省略变量类型,可以使用var关键字:
var sb = new StringBuilder();
编译器会根据赋值语句自动推断出变量sb的类型是StringBuilder。对编译器来说,语句:
var sb = new StringBuilder();
实际上会自动变成:
StringBuilder sb = new StringBuilder();
- 所以说, 使用var定义变量,仅仅是少写了变量类型而已。
变量的作用范围
在Java中,多行语句用{ }括起来。很多控制语句,例如条件判断和循环,都以{ }作为它们自身的范围,例如:
if (...) { // if开始
...
while (...) { // while 开始
...
if (...) { // if开始
...
} // if结束
...
} // while结束
...
} // if结束
要正确地嵌套这些{ },编译器就能识别出语句块的开始和结束。而在语句块中定义的变量,它有一个作用域,就是从定义处开始,到语句块结束。超出了作用域引用这些变量,编译器会报错。举个例子:
{
...
int i = 0; // 变量i从这里开始定义
...
{
...
int x = 1; // 变量x从这里开始定义
...
{
...
String s = "hello"; // 变量s从这里开始定义
...
} // 变量s作用域到此结束
...
// 注意,这是一个新的变量s,它和上面的变量同名,
// 但是因为作用域不同,它们是两个不同的变量:
String s = "hi";
...
} // 变量x和s作用域到此结束
...
} // 变量i作用域到此结束
- 定义变量时,要遵循作用域最小化原则,尽量将变量定义在尽可能小的作用域,并且,不要重复使用变量名。
变量和数据类型小结
- Java提供了两种变量类型:基本类型和引用类型
- 基本类型包括整型,浮点型,布尔型,字符型。
- 变量可重新赋值,等号是赋值语句,不是数学意义的等号。
- 常量在初始化后不可重新赋值,使用常量便于理解程序意图。
整数运算
Java的整数运算遵循四则运算规则,可以使用任意嵌套的小括号。四则运算规则和初等数学一致。例如:
public class Main {
public static void main(String[] args) {
int i = (100 + 200) * (99 - 88); // 3300
int n = 7 * (5 + (i - 9)); // 23072
System.out.println(i);
System.out.println(n);
}
}
- 整数的数值表示不但是精确的,而且整数运算永远是精确的,即使是除法也是精确的,因为两个整数相除只能得到结果的整数部分:
int x = 12345 / 67; // 184
- 求余运算使用%:
int y = 12345 % 67; // 12345÷67的余数是17
- 整数的除法对于除数为0时运行时将报错,但编译不会报错。(细节)
溢出
整数由于存在范围限制,如果计算结果超出了范围,就会产生溢出,而溢出不会出错,却会得到一个奇怪的结果:
public class Main {
public static void main(String[] args) {
int x = 2147483640;
int y = 15;
int sum = x + y;
System.out.println(sum); // -2147483641
}
}
解释:换成二进制做加法!
解决上述问题:可以把int换成long类型,由于long可表示的整型范围更大,所以结果就不会溢出:
long x = 2147483640;
long y = 15;
long sum = x + y;
System.out.println(sum); // 2147483655
- 简写运算符:即+=,-=,*=,/=,它们的使用方法如下:
n += 100; // 3409, 相当于 n = n + 100;
n -= 100; // 3309, 相当于 n = n - 100;
自增/自减
Java还提供了++运算和–运算,它们可以对一个整数进行加1和减1的操作:
public class Main {
public static void main(String[] args) {
int n = 3300;
n++; // 3301, 相当于 n = n + 1;
n--; // 3300, 相当于 n = n - 1;
int y = 100 + (++n); // 不要这么写
System.out.println(y); // 3401
}
}
注意:++写在前面和后面计算结果是不同的,++n表示先加1再引用n,n++表示先引用n再加1。不建议把++运算混入到常规运算中,容易自己把自己搞懵了。
移位运算
在计算机中,整数总是以二进制的形式表示。例如,int类型的整数7使用4字节表示的二进制如下:(1字节 = 8比特)
00000000 0000000 0000000 00000111
可以对整数进行移位运算。对整数7左移1位将得到整数14,左移两位将得到整数28:
int n = 7; // 00000000 00000000 00000000 00000111 = 7
int a = n << 1; // 00000000 00000000 00000000 00001110 = 14
int b = n << 2; // 00000000 00000000 00000000 00011100 = 28
int c = n << 28; // 01110000 00000000 00000000 00000000 = 1879048192
int d = n << 29; // 11100000 00000000 00000000 00000000 = -536870912
- 左移的话是将最后一位进行左移操作!!!
- 左移29位时,由于最高位变成1,因此结果变成了负数。
类似的,对整数7进行右移,结果如下:
nt n = 7; // 00000000 00000000 00000000 00000111 = 7
int a = n >> 1; // 00000000 00000000 00000000 00000011 = 3
int b = n >> 2; // 00000000 00000000 00000000 00000001 = 1
int c = n >> 3; // 00000000 00000000 00000000 00000000 = 0
- 还有一种无符号的右移运算,使用>>>,它的特点是不管符号位,右移后高位总是补0,因此,对一个负数进行>>>右移,它会变成正数,原因是最高位的1变成了0:(作为简单了解)
int n = -536870912;
int a = n >>> 1; // 01110000 00000000 00000000 00000000 = 1879048192
int b = n >>> 2; // 00111000 00000000 00000000 00000000 = 939524096
int c = n >>> 29; // 00000000 00000000 00000000 00000111 = 7
int d = n >>> 31; // 00000000 00000000 00000000 00000001 = 1
- 对byte和short类型进行移位时,会首先转换为int再进行位移。
- 仔细观察可发现,左移实际上就是不断地×2,右移实际上就是不断地÷2。
位运算
位运算是按位进行与、或、非和异或的运算。
- 与运算的规则是,必须两个数同时为1,结果才为1:
n = 0 & 0; // 0
n = 0 & 1; // 0
n = 1 & 0; // 0
n = 1 & 1; // 1
- 或运算的规则是,只要任意一个为1,结果就为1:
n = 0 | 0; // 0
n = 0 | 1; // 1
n = 1 | 0; // 1
n = 1 | 1; // 1
- 非运算的规则是,0和1互换:
n = ~0; // 1
n = ~1; // 0
- 异或运算的规则是,如果两个数不同,结果为1,否则为0:
n = 0 ^ 0; // 0
n = 0 ^ 1; // 1
n = 1 ^ 0; // 1
n = 1 ^ 1; // 0
- 对两个整数进行位运算,实际上就是按位对齐,然后依次对每一位进行运算。例如:
public class Main {
public static void main(String[] args) {
int i = 167776589; // 00001010 00000000 00010001 01001101
int n = 167776512; // 00001010 00000000 00010001 00000000
System.out.println(i & n); // 167776512
}
}
在计算机网络当中,上述按位与运算实际上可以看作两个整数表示的IP地址10.0.17.77和10.0.17.0,通过与运算,可以快速判断一个IP是否在给定的网段内。
运算优先级
!!! 记不住,只需要在运算的时候加上括号就可以保证运算的优先级正确。
类型自动提升与强制类型转型
在运算过程中,如果参与运算的两个数类型不一致,那么计算结果为较大类型的整型。例如,short和int计算,结果总是int,原因是short首先自动被转型为int:
public class Main {
public static void main(String[] args) {
short s = 1234;
int i = 123456;
int x = s + i; // s自动转型为int
short y = s + i; // 编译错误!
}
}
为了防止报错也可以使用强制类型转换, 例如,将int强制转型为short:
int i = 12345;
short s = (short) i; // 12345
要注意,超出范围的强制转型会得到错误的结果,原因是转型时,int的两个高位字节直接被扔掉,仅保留了低位的两个字节:
public class Main {
public static void main(String[] args) {
int i1 = 1234567;
short s1 = (short) i1; // -10617
System.out.println(s1);
int i2 = 12345678;
short s2 = (short) i2; // 24910
System.out.println(s2);
}
}
INTERNAL_SERVER_ERROR
!!! 所以有时候强制类型转换可能是错的
练习:
小总结:
- 整数运算的结果永远是精确的
- 运算结果会自动提升;
- 可以强制转型,但超出范围的强制转型会得到错误的结果;
- 应该选择合适范围的整型(int或long),没有必要为了节省内存而使用byte和short进行整数运算。
浮点数运算
- 浮点数运算和整数运算相比,只能进行加减乘除这些数值计算,不能做位运算和移位运算。
- 在计算机中,浮点数虽然表示的范围大,但是,浮点数有个非常重要的特点,就是浮点数常常无法精确表示。
- 举个实际例子:浮点数0.1在计算机中就无法精确表示,因为十进制的0.1换算成二进制是一个无限循环小数,很显然,无论使用float还是double,都只能存储一个0.1的近似值。但是,0.5这个浮点数又可以精确地表示。
- 因为浮点数常常无法精确表示,因此,浮点数运算会产生误差:
浮点数运算误差
public class Main {
public static void main(String[] args) {
double x = 1.0 / 10;
double y = 1 - 9.0 / 10;
// 观察x和y是否相等:
System.out.println(x);
System.out.println(y);
}
}
0.1
0.09999999999999998
- 由于浮点数存在运算误差,所以比较两个浮点数是否相等常常会出现错误的结果。正确的比较方法是判断两个浮点数之差的绝对值是否小于一个很小的数:
// 比较x和y是否相等,先计算其差的绝对值:
double r = Math.abs(x - y);
// 再判断绝对值是否足够小:
if (r < 0.00001) {
// 可以认为相等
} else {
// 不相等
}
- 浮点数在内存的表示方法和整数比更加复杂。Java的浮点数完全遵循IEEE-754标准,这也是绝大多数计算机平台都支持的浮点数标准表示方法。
类型提升
- 如果参与运算的两个数其中一个是整型,那么整型可以自动提升到浮点型:
public class Main {
public static void main(String[] args) {
int n = 5;
double d = 1.2 + 24.0 / n; // 6.0
System.out.println(d);
}
}
- 特别注意,在一个复杂的四则运算中,两个整数的运算不会出现自动提升的情况。例如:
double d = 1.2 + 24 / 5; // 5.2
解释:计算结果为5.2,原因是编译器计算24 / 5这个子表达式时,按两个整数进行运算,结果仍为整数4。
溢出
整数运算在除数为0时会报错,而浮点数运算在除数为0时,不会报错,但会返回几个特殊值
- NaN表示Not a Number
- Infinity表示无穷大
- -Infinity表示负无穷大
举例:
double d1 = 0.0 / 0; // NaN
double d2 = 1.0 / 0; // Infinity
double d3 = -1.0 / 0; // -Infinity
这三种特殊值在实际运算中很少碰到,我们只需要了解即可。
强制转型
int n1 = (int) 12.3; // 12
int n2 = (int) 12.7; // 12
int n2 = (int) -12.7; // -12
int n3 = (int) (12.7 + 0.5); // 13
int n4 = (int) 1.2e20; // 2147483647
注意:int n2 = (int) -12.7; // -12 这一个
- 如果要进行四舍五入,可以对浮点数加上0.5再强制转型:
public class Main {
public static void main(String[] args) {
double d = 2.6;
int n = (int) (d + 0.5);
System.out.println(n);
}
}
练习:
小总结:
- 浮点数常常无法精确表示,并且浮点数的运算结果可能有误差;
- 比较两个浮点数通常比较它们的差的绝对值是否小于一个特定值;
- 整型和浮点型运算时,整型会自动提升为浮点型;
- 可以将浮点型强制转为整型,但超出范围后将始终返回整型的最大值。
布尔运算
对于布尔类型boolean,永远只有true和false两个值。
布尔运算是一种关系运算,包括以下几类:
- 比较运算符:>,>=,<,<=,==,!=
- 与运算 &&
- 或运算 ||
- 非运算 !
示例:
boolean isGreater = 5 > 3; // true
int age = 12;
boolean isZero = age == 0; // false
boolean isNonZero = !isZero; // true
boolean isAdult = age >= 18; // false
boolean isTeenager = age >6 && age <18; // true
关系运算符的优先级从高到低是:
短路运算
- 布尔运算的一个重要特点是短路运算。如果一个布尔运算的表达式能提前确定结果,则后续的计算不再执行,直接返回结果。
- false && x的结果总是false,无论x是true还是false,因此,与运算在确定第一个值为false后,不再继续计算,而是直接返回false。
public class Main {
public static void main(String[] args) {
boolean b = 5 < 3;
boolean result = b && (5 / 0 > 0);
System.out.println(result);
}
}
false
- 如果没有短路运算,&&后面的表达式会由于除数为0而报错,但实际上该语句并未报错,原因在于与运算是短路运算符,提前计算出了结果false。
- 如果变量b的值为true,则表达式变为true && (5 / 0 > 0)。因为无法进行短路运算,该表达式必定会由于除数为0而报错,可以自行测试。
- 类似的,对于||运算,只要能确定第一个值为true,后续计算也不再进行,而是直接返回true:
boolean result = true || (5 / 0 > 0); // true
三元运算符
Java还提供一个三元运算符b ? x : y,它根据第一个布尔表达式的结果,分别返回后续两个表达式之一的计算结果。示例:
public class Main {
public static void main(String[] args) {
int n = -100;
int x = n >= 0 ? n : -n;
System.out.println(x);
}
}
解释:
- 上述语句的意思是,判断n >= 0是否成立,如果为true,则返回n,否则返回-n。这实际上是一个求绝对值的表达式。
- 注意到三元运算b ? x : y会首先计算b,如果b为true,则只计算x,否则,只计算y。此外,x和y的类型必须相同,因为返回值不是boolean,而是x和y之一。
练习
判断指定年龄是否是小学生(6~12岁):
小总结:
- 与运算和或运算是短路运算;
- 三元运算b ? x : y后面的类型必须相同,三元运算也是“短路运算”,只计算x或y。
字符和字符串
在Java中,字符和字符串是两个不同的类型。
字符类型
字符类型char是基本数据类型,它是character的缩写。一个char保存一个Unicode字符:
char c1 = 'A';
char c2 = '中';
因为Java在内存中总是使用Unicode表示字符,所以,一个英文字符和一个中文字符都用一个char类型表示,它们都占用两个字节。要显示一个字符的Unicode编码,只需将char类型直接赋值给int类型即可:
int n1 = 'A'; // 字母“A”的Unicodde编码是65
int n2 = '中'; // 汉字“中”的Unicode编码是20013
还可以直接用转义字符\u+Unicode编码来表示一个字符:
// 注意是十六进制:
char c3 = '\u0041'; // 'A',因为十六进制0041 = 十进制65
char c4 = '\u4e2d'; // '中',因为十六进制4e2d = 十进制20013
字符串类型
和char类型不同,字符串类型String是引用类型,我们用双引号"…"表示字符串。一个字符串可以存储0个到任意个字符:
String s = ""; // 空字符串,包含0个字符
String s1 = "A"; // 包含一个字符
String s2 = "ABC"; // 包含3个字符
String s3 = "中文 ABC"; // 包含6个字符,其中有一个空格
因为字符串使用双引号"…"表示开始和结束,那如果字符串本身恰好包含一个"字符怎么表示?例如,“abc"xyz”,编译器就无法判断中间的引号究竟是字符串的一部分还是表示字符串结束。这个时候,我们需要借助转义字符\:
String s = "abc\"xyz"; // 包含7个字符: a, b, c, ", x, y, z
- 因为\是转义字符,所以,两个\表示一个\字符:
String s = "abc\\xyz"; // 包含7个字符: a, b, c, \, x, y, z
常见的转义字符
例如:
String s = "ABC\n\u4e2d\u6587"; // 包含6个字符: A, B, C, 换行符, 中, 文
字符串连接
Java的编译器对字符串做了特殊照顾,可以使用+连接任意字符串和其他数据类型,这样极大地方便了字符串的处理。例如:
// 字符串连接
public class Main {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "world";
String s = s1 + " " + s2 + "!";
System.out.println(s);
}
}
Hello world!
- 如果用+连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接:
public class Main {
public static void main(String[] args) {
int age = 25;
String s = "age is " + age;
System.out.println(s);
}
}
多行字符串
- 如果我们要表示多行字符串,使用+号连接会非常不方便:
String s = "first line \n"
+ "second line \n"
+ "end";
- 从Java 13开始,字符串可以用"""…"""表示多行字符串(Text Blocks)了。举个例子:
// 多行字符串
public class Main {
public static void main(String[] args) {
String s = """
SELECT * FROM
users
WHERE id > 100
ORDER BY name DESC
""";
System.out.println(s);
}
}
不可变特性
Java的字符串除了是一个引用类型外,还有个重要特点,就是字符串不可变。考察以下代码:
public class Main {
public static void main(String[] args) {
String s = "hello";
System.out.println(s); // 显示 hello
s = "world";
System.out.println(s); // 显示 world
}
}
解释:
- 理解了引用类型的“指向”后,试解释下面的代码输出:
public class Main {
public static void main(String[] args) {
String s = "hello";
String t = s;
s = "world";
System.out.println(t); // t是"hello"还是"world"?
}
}
hello
空值null
引用类型的变量可以指向一个空值null,它表示不存在,即该变量不指向任何对象。例如:
String s1 = null; // s1是null
String s2; // 没有赋初值值,s2也是null
String s3 = s1; // s3也是null
String s4 = ""; // s4指向空字符串,不是null
注意:注意要区分空值null和空字符串"",空字符串是一个有效的字符串对象,它不等于null。
练习:
请将一组int值视为字符的Unicode编码,然后将它们拼成一个字符串:
方法一:
public class Test {
public static void main(String[] args){
// 方法一:将一组int值视为字符Unicode编码,然后拼成字符串
int a = 72;
int b = 105;
int c = 65281;
String s = "" + (char)a + (char)b + (char)c;
System.out.println(s);
// 强制类型转换
// double a = (int)9;
// System.out.println(a); // 9.0
}
}
Hi!
注意: + 更多强调的是字符串拼接!
方法二:
小总结:
- Java的字符类型char是基本类型,字符串类型String是引用类型;
- 基本类型的变量是“持有”某个数值,引用类型的变量是“指向”某个对象;
- 引用类型的变量可以是空值null;
- 要区分空值null和空字符串""。
数组类型
- 如果我们有一组类型相同的变量,例如,5位同学的成绩,可以这么写:
public class Main {
public static void main(String[] args) {
// 5位同学的成绩:
int n1 = 68;
int n2 = 79;
int n3 = 91;
int n4 = 85;
int n5 = 62;
}
}
- 但其实没有必要定义5个int变量。可以使用数组来表示“一组”int类型。代码如下:
public class Main {
public static void main(String[] args) {
// 5位同学的成绩:
int[] ns = new int[5];
ns[0] = 68;
ns[1] = 79;
ns[2] = 91;
ns[3] = 85;
ns[4] = 62;
}
}
- 定义一个数组类型的变量,使用数组类型“类型[]”,例如,int[]。和单个基本类型变量不同,数组变量初始化必须使用new int[5]表示创建一个可容纳5个int元素的数组。
- Java数组的几个特点:① 数组所有元素初始化为默认值,整型都是0,浮点型是0.0,布尔型是false;② 数组一旦创建后,大小就不可改变。
- 要访问数组中的某一个元素,需要使用索引。数组索引从0开始,例如,5个元素的数组,索引范围是0~4。
- 可以修改数组中的某一个元素,使用赋值语句,例如,ns[1] = 79;
- 可以用数组变量.length获取数组大小:
public class Main {
public static void main(String[] args) {
// 5位同学的成绩:
int[] ns = new int[5];
System.out.println(ns.length); // 5
}
}
- 数组是引用类型,在使用索引访问数组元素时,如果索引超出范围,运行时将报错:
public class Main {
public static void main(String[] args) {
// 5位同学的成绩:
int[] ns = new int[5];
int n = 5;
System.out.println(ns[n]); // 索引n不能超出范围
}
}
报错:Index 5 out of bounds for length 5
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at Main.main(Main.java:7)
- 也可以在定义数组时直接指定初始化的元素,这样就不必写出数组大小,而是由编译器自动推算数组大小。例如:
public class Main {
public static void main(String[] args) {
// 5位同学的成绩:
int[] ns = new int[] { 68, 79, 91, 85, 62 };
System.out.println(ns.length); // 编译器自动推算数组大小为5
}
}
- 初始化数组的时候进行简写:
int[] ns = { 68, 79, 91, 85, 62 };
- 数组是引用数据类型,并且数组大小不可变,
public class Main {
public static void main(String[] args) {
// 5位同学的成绩:
int[] ns;
ns = new int[] { 68, 79, 91, 85, 62 };
System.out.println(ns.length); // 5
ns = new int[] { 1, 2, 3 };
System.out.println(ns.length); // 3
}
}
5
3
解释:
字符串数组
- 如果数组元素不是基本类型,而是一个引用类型,那么,修改数组元素会有哪些不同?
- 字符串是引用类型,因此我们先定义一个字符串数组:
String[] names = {
"ABC", "XYZ", "zoo"
};
- 对于引用类型的指向理解:
- 对“指向”有了更深入的理解后,看:
public class Main {
public static void main(String[] args) {
String[] names = {"ABC", "XYZ", "zoo"};
String s = names[1];
names[1] = "cat";
System.out.println(s); // s是"XYZ"还是"cat"?
}
}
"cat"
小总结:
- 数组是同一数据类型的集合,数组一旦创建后,大小就不可变;
- 可以通过索引访问数组元素,但索引超出范围将报错;
- 数组元素可以是值类型(如int)或引用类型(如String),但数组本身是引用类型;
流程控制
输入和输出
输出
- 前面的代码中,我们总是使用System.out.println()来向屏幕输出一些内容。
- println是print line的缩写,表示输出并换行。因此,如果输出后不想换行,可以用print():
// 输出
public class Main {
public static void main(String[] args) {
System.out.print("A,");
System.out.print("B,");
System.out.print("C.");
System.out.println();
System.out.println("END");
}
}
格式化输出
- Java还提供了格式化输出的功能。**为什么要格式化输出?**因为计算机表示的数据不一定适合人来阅读:
// 格式化输出
public class Main {
public static void main(String[] args) {
double d = 12900000;
System.out.println(d); // 1.29E7
}
}
- 如果要把数据显示成我们期望的格式,就需要使用格式化输出的功能。格式化输出使用System.out.printf(),通过使用占位符%?,**printf()**可以把后面的参数格式化成指定格式:
// 格式化输出
public class Main {
public static void main(String[] args) {
double d = 3.1415926;
System.out.printf("%.2f\n", d); // 显示两位小数3.14
System.out.printf("%.4f\n", d); // 显示4位小数3.1416
}
}
Java的格式化功能提供了多种占位符,可以把各种数据类型“格式化”成指定的字符串:
占位符 | 说明 |
%d | 格式化输出整数 |
%x | 格式化输出十六进制整数 |
%f | 格式化输出浮点数 |
%e | 格式化输出科学计数法表示的浮点数 |
%s | 格式化字符串 |
- 注意,由于%表示占位符,因此,连续两个%%表示一个%字符本身。
- 占位符本身还可以有更详细的格式化参数。下面的例子把一个整数格式化成十六进制,并用0补足8位:
// 格式化输出
public class Main {
public static void main(String[] args) {
int n = 12345000;
System.out.printf("n=%d, hex=%08x", n, n); // 注意,两个%占位符必须传入两个数
}
}
n=12345000, hex=00bc5ea8
输入
- 和输出相比,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代表标准输入流。直接使用System.in读取用户输入虽然是可以的,但需要更复杂的代码,而通过Scanner就可以简化后续的代码。
- 有了Scanner对象后,要读取用户输入的字符串,使用scanner.nextLine(),要读取用户输入的整数,使用scanner.nextInt()。Scanner会自动转换数据类型,因此不必手动转换。
- 要测试输入,我们不能在线运行它,因为输入必须从命令行读取,因此,需要走编译、执行的流程:
$ javac Main.java
- 这个程序编译时如果有警告,可以暂时忽略它,在后面学习IO的时候再详细解释。编译成功后,执行:
$ java Main
Input your name: Bob
Input your age: 12
Hi, Bob, you are 12
根据提示分别输入一个字符串和整数后,我们得到了格式化的输出。
练习:
请帮小明同学设计一个程序,输入上次考试成绩(int)和本次考试成绩(int),然后输出成绩提高的百分比,保留两位小数位(例如,21.75%)。
代码实现:
理解一:
package LiaoxuefenTest;
/**
* @author Fcun
* @data 2021/12/11 --- 16:27
*/
import java.util.Scanner;
public class scannerTest {
public static void main(String[] args) {
// 初始化一个数值表示成绩提高的百分比
double res = 0;
Scanner sc = new Scanner(System.in);
System.out.println("Please input your last score: ");
int prevScore = sc.nextInt();
System.out.println("Please input your current score: ");
int currScore = sc.nextInt();
// 保证代码逻辑的全面性,成绩提高的百分比:直接进行相除即可
if (prevScore < currScore) {
res = (double)currScore / (double)prevScore;
System.out.println("成绩提高了喔");
}else if (prevScore > currScore){
res = (double)prevScore / (double)currScore;
System.out.println("成绩下降了喔");
}else{
System.out.println("成绩没有变化的喔");
}
res = res * 100.0;
System.out.printf("%.2f%%", res);
}
}
// 运行
Please input your last score:
80
Please input your current score:
90
成绩提高了喔
112.50%
理解二:
import java.util.Scanner;
/**
* @author Fcun
* @data 2021/12/11 --- 18:51
*/
public class chengjitigao2 {
public static void main(String[] args) {
// 初始化一个数值表示成绩提高的百分比
double res = 0;
Scanner sc = new Scanner(System.in);
System.out.println("Please input your last score: ");
int prevScore = sc.nextInt();
System.out.println("Please input your current score: ");
int currScore = sc.nextInt();
// 保证代码逻辑的全面性,成绩提高的百分比:直接进行相除即可
if (prevScore < currScore) {
res = (double)(currScore - prevScore) / prevScore;
System.out.println("成绩提高了喔");
}else if (prevScore > currScore){
// res = (double)prevScore / (double)currScore;
System.out.println("成绩下降了喔");
}else{
System.out.println("成绩没有变化的喔");
}
res = res * 100.0;
System.out.printf("%.2f%%", res);
sc.close();
}
}
// 运行
Please input your last score:
80
Please input your current score:
90
成绩提高了喔
12.50%
小总结:
- Java提供的输出包括:System.out.println() / print() / printf(),其中printf()可以格式化输出;
- Java提供Scanner对象来方便输入,读取对应的类型可以使用:scanner.nextLine() / nextInt() / nextDouble() / …
if判断
在Java程序中,如果要根据条件来决定是否执行某一段代码,就需要if语句。
- if语句的基本语法是:
if (条件) {
// 条件满足时执行
}
- 根据if的计算结果(true还是false),JVM决定是否执行if语句块(即花括号{}包含的所有语句)。
查看示例:
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 60) {
System.out.println("及格了");
}
System.out.println("END");
}
}
解释:当条件n >= 60计算结果为true时,if语句块被执行,将打印"及格了",否则,if语句块将被跳过。修改n的值可以看到执行效果。
- 注意到if语句包含的块可以包含多条语句:
// 条件判断
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 60) {
System.out.println("及格了");
System.out.println("恭喜你");
}
System.out.println("END");
}
}
- 当if语句块只有一行语句时,可以省略花括号{}:
// 条件判断
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 60)
System.out.println("及格了");
System.out.println("END");
}
}
- 但是,省略花括号并不总是一个好主意。假设某个时候,突然想给if语句块增加一条语句时:
// 条件判断
public class Main {
public static void main(String[] args) {
int n = 50;
if (n >= 60)
System.out.println("及格了");
System.out.println("恭喜你"); // 注意这条语句不是if语句块的一部分
System.out.println("END");
}
}
由于使用缩进格式,很容易把两行语句都看成if语句的执行块,但实际上只有第一行语句是if的执行块。在使用git这些版本控制系统自动合并时更容易出问题,所以不推荐忽略花括号的写法。
else
- if语句还可以编写一个else { … },当条件判断为false时,将执行else的语句块:
// 条件判断
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 60) {
System.out.println("及格了");
} else {
System.out.println("挂科了");
}
System.out.println("END");
}
}
- 修改上述代码n的值,观察if条件为true或false时,程序执行的语句块。
需要注意的是:else不是必须的,还可以用多个if … else if …串联。例如:
// 条件判断
public class Main {
public static void main(String[] args) {
int n = 70;
if (n >= 90) {
System.out.println("优秀");
} else if (n >= 60) {
System.out.println("及格了");
} else {
System.out.println("挂科了");
}
System.out.println("END");
}
}
上面的代码效果其实等价于:
if (n >= 90) {
// n >= 90为true:
System.out.println("优秀");
} else {
// n >= 90为false:
if (n >= 60) {
// n >= 60为true:
System.out.println("及格了");
} else {
// n >= 60为false:
System.out.println("挂科了");
}
}
- 在串联使用多个if时,要特别注意判断顺序。观察下面的代码:
// 条件判断
public class Main {
public static void main(String[] args) {
int n = 100;
if (n >= 60) {
System.out.println("及格了");
} else if (n >= 90) {
System.out.println("优秀");
} else {
System.out.println("挂科了");
}
}
}
及格了
执行发现,n = 100时,满足条件n >= 90,但输出的不是"优秀",而是"及格了",原因是if语句从上到下执行时,先判断n >= 60成功后,后续else不再执行,因此,if (n >= 90)没有机会执行了。
- 正确的方式是按照判断范围从大到小依次判断:
// 从大到小依次判断:
if (n >= 90) {
// ...
} else if (n >= 60) {
// ...
} else {
// ...
}
- 或者改成从小到大依次判断:
// 从小到大依次判断:
if (n < 60) {
// ...
} else if (n < 90) {
// ...
} else {
// ...
}
- 使用if的时候,还需要特别注意边界问题:例如
public class Main {
public static void main(String[] args) {
int n = 90;
if (n > 90) {
System.out.println("优秀");
} else if (n >= 60) {
System.out.println("及格了");
} else {
System.out.println("挂科了");
}
}
}
// 及格了
假设我们期望90分或更高为“优秀”,上述代码输出的却是“及格”,原因是>和>=效果是不同的。
- 前面讲过了浮点数在计算机中常常无法精确表示,并且计算可能出现误差,因此,判断浮点数相等用==判断不靠谱:
public class Main {
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.println("x is NOT 0.1");
}
}
}
x is NOT 0.1
- 正确的方法是利用差值小于某个临界值来判断:
// 条件判断
public class Main {
public static void main(String[] args) {
double x = 1 - 9.0 / 10;
if (Math.abs(x - 0.1) < 0.00001) {
System.out.println("x is 0.1");
} else {
System.out.println("x is NOT 0.1");
}
}
}
x is 0.1
判断引用类型相等
在Java中,判断值类型的变量是否相等,可以使用==运算符。但是,判断引用类型的变量是否相等,**表示“引用是否相等”,或者说,是否指向同一个对象。**例如,下面的两个String类型,它们的内容是相同的,但是,分别指向不同的对象,用判断,结果为false:
public class Main {
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");
}
}
}
hello
hello
s1 != s2
- 要判断引用类型的变量内容是否相等,必须使用equals()方法:
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1);
System.out.println(s2);
if (s1.equals(s2)) {
System.out.println("s1 equals s2");
} else {
System.out.println("s1 not equals s2");
}
}
}
hello
hello
s1 equals s2
- 注意:执行语句s1.equals(s2)时,如果变量s1为null,会报NullPointerException:
public class Main {
public static void main(String[] args) {
String s1 = null;
if (s1.equals("hello")) {
System.out.println("hello");
}
}
}
报错
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "<local1>" is null
at Main.main(Main.java:5)
- 要避免NullPointerException错误,可以利用短路运算符&&
public class Main {
public static void main(String[] args) {
String s1 = null;
if (s1 != null && s1.equals("hello")) {
System.out.println("hello");
}
}
}
- 还可以把一定不是null的对象"hello"放到前面:例如:if (“hello”.equals(s)) { … }。
练习:
请用if … else编写一个程序,用于计算体质指数BMI,并打印结果。
BMI = 体重(kg)除以身高(m)的平方
BMI结果:
过轻:低于18.5
正常:18.5-25
过重:25-28
肥胖:28-32
非常肥胖:高于32
代码实现
/**
- @author Fcun
- @data 2021/12/12 --- 16:02
*/
import java.util.Scanner;
public class BMI {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("please input your weight:");
double weight = sc.nextDouble();
System.out.println("please input your height:");
double height = sc.nextDouble();
// 可以先设置一个初始值
// BMI = 体重(kg)除以身高(m)的平方
double bmi = 0;
bmi = weight / (height * height);
if (bmi < 18.5){
System.out.println("体重过轻");
}else if(18.5 <= bmi && bmi < 25){
System.out.println("体重正常");
}else if(25 <= bmi && bmi < 28){
System.out.println("体重过重");
}else if(28 <= bmi && bmi < 32){
System.out.println("体重肥胖");
}else{
System.out.println("非常肥胖");
}
}
}
// 测试
please input your weight:
175
please input your height:
65
体重过轻
小总结:
- if … else可以做条件判断,else是可选的;
- 不推荐省略花括号{};
- 多个if … else串联要特别注意判断顺序
- 要注意if的边界条件;
- 要注意浮点数判断相等不能直接用==运算符;
- 引用类型判断内容相等要使用equals(),注意避免NullPointerException。
switch多重选择
- 除了if语句外,还有一种条件判断,是根据某个表达式的结果,分别去执行不同的分支。
- 例如,在游戏中,让用户选择选项:
1、单人模式
2、多人模式
3、退出游戏
这时,switch语句就派上用场了。 - switch语句根据switch (表达式)计算的结果,跳转到匹配的case结果,然后继续执行后续语句,直到遇到break结束执行。
举一个例子:
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;
}
}
}
- 修改option的值分别为1、2、3,观察执行结果。
- 如果option的值没有匹配到任何case,例如option = 99,那么,switch语句不会执行任何语句。这时,可以给switch语句加一个default,当没有匹配到任何case时,执行default:
public class Main {
public static void main(String[] args) {
int option = 99;
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("Not selected");
break;
}
}
}
// 测试
Not selected
- 如果把switch语句翻译成if语句,那么上述的代码相当于:
- 其实是可以等同于if else语句的,这是很关键的
if (option == 1) {
System.out.println("Selected 1");
} else if (option == 2) {
System.out.println("Selected 2");
} else if (option == 3) {
System.out.println("Selected 3");
} else {
System.out.println("Not selected");
}
- 对于多个==判断的情况,使用switch结构更加清晰。
- 同时注意,上述“翻译”只有在switch语句中对每个case正确编写了break语句才能对应得上。
- 使用switch时,注意case语句并没有花括号{},而且,case语句具有“穿透性”,漏写break将导致意想不到的结果:
public class Main {
public static void main(String[] args) {
int option = 2;
switch (option) {
case 1:
System.out.println("Selected 1");
case 2:
System.out.println("Selected 2");
case 3:
System.out.println("Selected 3");
default:
System.out.println("Not selected");
}
}
}
// 测试
Selected 2
Selected 3
Not selected
- 千万不要漏写break, 解释:当option = 2时,将依次输出"Selected 2"、“Selected 3”、“Not selected”,原因是从匹配到case 2开始,后续语句将全部执行,直到遇到break语句。因此,任何时候都不要忘记写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语句时,只要保证有break,case的顺序不影响程序逻辑:
switch (option) {
case 3:
...
break;
case 2:
...
break;
case 1:
...
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;
}
}
}
// 测试
Selected apple
编译检查
- 使用IDE时,可以自动检查是否漏写了break语句和default语句,方法是打开IDE的编译检查。
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");
}
}
}
// 测试
Selected apple
注意新语法使用->,如果有多条语句,需要用{}括起来。不要写break语句,因为新语法只会执行匹配的语句,没有穿透效应。
很多时候,我们还可能用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);
}
}
// 测试
opt = 1
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);
}
}
// 测试
opt = -1008851410
练习:
使用switch实现一个简单的石头、剪子、布游戏。
package LiaoxuefenTest;
/**
* @author Fcun
* @data 2021/12/13 --- 22:28
*/
import java.util.Scanner;
import java.util.Random;
public class SwitchTest {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("please choose:");
System.out.println("1: Rock");
System.out.println("2: Scissors");
System.out.println("3: Paper");
int choice = sc.nextInt();
// 既然是游戏,在进行石头剪刀布肯定需要和电脑进行比较
Random rand = new Random();
// rand.nextInt(3) 里面仅仅只是随机0-2即可
int pc = rand.nextInt(3) + 1;
// 标准化输出的时候用的是printf
System.out.printf("PC的选择是:%d", pc);
switch (choice){
case 1 -> System.out.println(pc == 1? "平局": pc == 2 ?"胜利":"失败");
case 2 -> System.out.println(pc == 1? "失败": pc == 2 ?"平局":"胜利");
case 3 -> System.out.println(pc == 1? "胜利": pc == 2 ?"失败":"平局");
default -> System.out.println("请输入正确选项:");
}
}
}
小总结:
- switch语句可以做多重选择,然后执行匹配的case语句后续代码;
- switch的计算结果必须是整型、字符串或枚举类型;
- 注意千万不要漏写break,建议打开fall-through警告;
- 总是写上default,建议打开missing default警告;
- 从Java 14开始,switch语句正式升级为表达式,不再需要break,并且允许使用yield返回值。
while循环
- 循环语句就是让计算机根据条件做循环计算,在条件满足时继续循环,条件不满足时退出循环。
例如,计算从1到100的和:
1 + 2 + 3 + 4 + … + 100 = ?
- 除了用数列公式外,完全可以让计算机做100次循环累加。因为计算机的特点是计算速度非常快,我们让计算机循环一亿次也用不到1秒,所以很多计算的任务,人去算是算不了的,但是计算机算,使用循环这种简单粗暴的方法就可以快速得到结果。
- 我们先看Java提供的while条件循环。它的基本用法是:
while (条件表达式) {
循环语句
}
// 继续执行后续代码
- while循环在每次循环开始前,首先判断条件是否成立。如果计算结果为true,就把循环体内的语句执行一遍,如果计算结果为false,那就直接跳到while循环的末尾,继续往下执行。
public class Main {
public static void main(String[] args) {
int sum = 0; // 累加的和,初始化为0
int n = 1;
while (n <= 100) { // 循环条件是n <= 100
sum = sum + n; // 把n累加到sum中
n ++; // n自身加1
}
System.out.println(sum); // 5050
}
}
- 注意到while循环是先判断循环条件,再循环,因此,有可能一次循环都不做。
public class Main {
public static void main(String[] args) {
int sum = 0;
int n = 0;
while (n <= 100) {
n ++;
sum = sum + n;
}
System.out.println(sum);
}
}
- 如果循环条件永远满足,那这个循环就变成了死循环。死循环将导致100%的CPU占用,用户会感觉电脑运行缓慢,所以要避免编写死循环代码。
- 如果循环条件的逻辑写得有问题,也会造成意料之外的结果:
public class Main {
public static void main(String[] args) {
int sum = 0;
int n = 1;
while (n > 0) {
sum = sum + n;
n ++;
}
System.out.println(n); // -2147483648
System.out.println(sum);
}
}
解释:表面上看,上面的while循环是一个死循环,但是,Java的int类型有最大值,达到最大值后,再加1会变成负数,结果,意外退出了while循环。
练习:
使用while计算从m到n的和:
小总结:
- while循环先判断循环条件是否满足,再执行循环语句;
- while循环可能一次都不执行;
- 编写循环时要注意循环条件,并避免死循环。
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);
}
}
// 测试
5050
使用do while循环时,同样要注意循环条件的判断。
练习:
使用do while循环计算从m到n的和。
小总结:
- do while循环先执行循环,再判断条件;
- do while循环会至少执行一次。
for循环
除了while和do while循环,Java使用最广泛的是for循环。
for循环的功能非常强大,它使用计数器实现循环。for循环会先初始化计数器,然后,在每次循环前检测循环条件,在每次循环后更新计数器。计数器变量通常命名为i。
我们把1到100求和用for循环改写一下:
public class Main {
public static void main(String[] args) {
int sum = 0;
for (int i=1; i<=100; i++) {
sum = sum + i;
}
System.out.println(sum);
}
}
// 测试
5050
在for循环执行前,会先执行初始化语句int i=1,它定义了计数器变量i并赋初始值为1,然后,循环前先检查循环条件i<=100,循环后自动执行i++,因此,和while循环相比,for循环把更新计数器的代码统一放到了一起。在for循环的循环体内部,不需要去更新变量i。
因此,for循环的用法是:
for (初始条件; 循环检测条件; 循环后更新计数器) {
// 执行语句
}
如果我们要对一个整型数组的所有元素求和,可以用for循环实现:
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
int sum = 0;
for (int i=0; i<ns.length; i++) {
System.out.println("i = " + i + ", ns[i] = " + ns[i]);
sum = sum + ns[i];
}
System.out.println("sum = " + sum);
}
}
解释:上面代码的循环条件是i<ns.length。因为ns数组的长度是5,因此,当循环5次后,i的值被更新为5,就不满足循环条件,因此for循环结束。
- 注意for循环的初始化计数器总是会被执行,并且for循环也可能循环0次。
- 使用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++) {
System.out.println(ns[i]);
i = i + 1;
}
}
}
// 测试
1
9
25
- 虽然不会报错,但是,数组元素只打印了一半,原因是循环内部的i = i + 1导致了计数器变量每次循环实际上加了2(因为for循环还会自动执行i++)。因此,在for循环中,不要修改计数器的值。计数器的初始化、判断条件、每次循环后的更新条件统一放到for()语句中可以一目了然。
- 如果希望只访问索引为奇数的数组元素,应该把for循环改写为:
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i<ns.length; i=i+2) {
System.out.println(ns[i]);
}
- 通过更新计数器的语句i=i+2就达到了这个效果,从而避免了在循环体内去修改变量i。即计数器的更新不一定只有 i++,可以有多种形式,比如说 : i = i + 2 等等
- 使用for循环时,计数器变量i要尽量定义在for循环中:
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i<ns.length; i++) {
System.out.println(ns[i]);
}
// 无法访问i
int n = i; // compile error!
- 如果变量i定义在for循环外:
int[] ns = { 1, 4, 9, 16, 25 };
int i;
for (i=0; i<ns.length; i++) {
System.out.println(ns[i]);
}
// 仍然可以使用i
int n = i;
那么,退出for循环后,变量i仍然可以被访问,这就破坏了变量应该把访问范围缩到最小的原则。
灵活使用for循环
for循环还可以缺少初始化语句、循环条件和每次循环更新语句,例如:
// 不设置结束条件:
for (int i=0; ; i++) {
...
}
// 不设置结束条件和更新语句:
for (int i=0; ;) {
...
}
// 什么都不设置:
for (;;) {
...
}
通常不推荐这样写,但是,某些情况下,是可以省略for循环的某些语句的。
for each 循环
for循环经常用来遍历数组,因为通过计数器可以根据索引来访问数组的每个元素:
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i<ns.length; i++) {
System.out.println(ns[i]);
}
但是,很多时候,我们实际上真正想要访问的是数组每个元素的值。Java还提供了另一种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);
}
}
}
// 测试
1
4
9
16
25
和for循环相比,for each循环的变量n不再是计数器,而是直接对应到数组的每个元素。for each循环的写法也更简洁。但是,for each循环无法指定遍历顺序,也无法获取数组的索引。
除了数组外,for each循环能够遍历所有“可迭代”的数据类型,包括后面会介绍的List、Map等。
练习 1:
给定一个数组,请用for循环倒序输出每一个元素:
练习 2:
利用for each循环对数组每个元素求和:
练习 3:
注意点:Math中的pow方法是用来返回指定数字的指定次幂、
小总结:
- for循环通过计数器可以实现复杂循环;
- for each循环可以直接遍历数组的每个元素;
- 最佳实践:计数器变量定义在for循环内部,循环体内部不修改计数器;
break 和 continue
- 无论是while循环还是for循环,有两个特别的语句可以使用,就是break语句和continue语句。
break:
在循环过程中,可以使用break语句跳出当前循环。我们来看一个例子:
public class Main {
public static void main(String[] args) {
int sum = 0;
for (int i=1; ; i++) {
sum = sum + i;
if (i == 100) {
break;
}
}
System.out.println(sum);
}
}
// 测试:
5050
- 使用for循环计算从1到100时,我们并没有在for()中设置循环退出的检测条件。但是,在循环内部,我们用if判断,如果i==100,就通过break退出循环。
- 因此,break语句通常都是配合if语句使用。要特别注意,break语句总是跳出自己所在的那一层循环。例如:
public class Main {
public static void main(String[] args) {
for (int i=1; i<=10; i++) {
System.out.println("i = " + i);
for (int j=1; j<=10; j++) {
System.out.println("j = " + j);
if (j >= i) {
break;
}
}
// break跳到这里
System.out.println("breaked");
}
}
}
// 测试
i = 1
j = 1
breaked
i = 2
j = 1
j = 2
breaked
i = 3
j = 1
j = 2
j = 3
breaked
i = 4
j = 1
j = 2
j = 3
j = 4
breaked
i = 5
j = 1
j = 2
j = 3
j = 4
j = 5
breaked
i = 6
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
breaked
i = 7
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
j = 7
breaked
i = 8
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
j = 7
j = 8
breaked
i = 9
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
j = 7
j = 8
j = 9
breaked
i = 10
j = 1
j = 2
j = 3
j = 4
j = 5
j = 6
j = 7
j = 8
j = 9
j = 10
breaked
- 上面的代码是两个for循环嵌套。因为break语句位于内层的for循环,因此,它会跳出内层for循环,但不会跳出外层for循环。
continue
break会跳出当前循环,也就是整个循环都不会执行了。而continue则是提前结束本次循环,直接继续执行下次循环。我们看一个例子:
public class Main {
public static void main(String[] args) {
int sum = 0;
for (int i=1; i<=10; i++) {
System.out.println("begin i = " + i);
if (i % 2 == 0) {
continue; // continue语句会结束本次循环,后面的代码都不执行
}
sum = sum + i;
System.out.println("end i = " + i);
}
System.out.println(sum); // 25
}
}
// 测试
begin i = 1
end i = 1
begin i = 2
begin i = 3
end i = 3
begin i = 4
begin i = 5
end i = 5
begin i = 6
begin i = 7
end i = 7
begin i = 8
begin i = 9
end i = 9
begin i = 10
25
注意观察continue语句的效果。当i为奇数时,完整地执行了整个循环,因此,会打印begin i=1和end i=1。在i为偶数时,continue语句会提前结束本次循环,后面的代码都不执行,重新开始新的一轮循环,因此,会打印begin i=2但不会打印end i = 2。
- 在多层嵌套的循环中,continue语句同样是结束本次自己所在的循环。
小总结:
- break语句可以跳出当前循环;
- break语句通常配合if,在满足条件时提前结束整个循环;
- break语句总是跳出最近的一层循环;
- continue语句可以提前结束本次循环;
- continue语句通常配合if,在满足条件时提前结束本次循环
数组操作
遍历数组
- 我们在Java程序基础里介绍了数组这种数据类型。有了数组,我们还需要来操作它。而数组最常见的一个操作就是遍历。
- 通过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);
}
}
}
// 测试
1
4
9
16
25
为了实现for循环遍历,初始条件为i=0,因为索引总是从0开始,继续循环的条件为i<ns.length,因为当i=ns.length时,i已经超出了索引范围(索引范围是0 ~ ns.length-1),每次循环后,i++。
- 第二种方式是使用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循环,取决于我们的需要。
打印数组内容
- 直接打印数组变量,得到的是数组在JVM中的引用地址:
int[] ns = { 1, 1, 2, 3, 5, 8 };
System.out.println(ns); // 类似 [I@7852e922
- 这并没有什么意义,因为我们希望打印的数组的元素内容。因此,使用for each循环来打印它:
int[] ns = { 1, 1, 2, 3, 5, 8 };
for (int n : ns) {
System.out.print(n + ", ");
}
- 使用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));
}
}
// 测试
[1, 1, 2, 3, 5, 8]
练习:
请按倒序遍历数组并打印每个元素:
小总结:
- 遍历数组可以使用for循环,for循环可以访问数组索引,for each循环直接迭代每个数组元素,但无法获取索引;
- 使用Arrays.toString()可以快速获取数组内容。
数组排序
- 对数组进行排序是程序中非常基本的需求。常用的排序算法有冒泡排序、插入排序和快速排序等。
- 我们来看一下如何使用冒泡排序算法对一个整型数组从小到大进行排序:
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));
}
}
- 冒泡排序的特点是,每一轮循环后,最大的一个数被交换到末尾,因此,下一轮循环就可以“刨除”最后的数,每一轮循环都比上一轮循环的结束位置靠前一位。冒泡排序可以从前往后也可以从后往前
- 另外,注意到交换两个变量的值必须借助一个临时变量。像这么写是错误的:
int x = 1;
int y = 2;
x = y; // x现在是2
y = x; // y现在还是2
正确的写法:
int x = 1;
int y = 2;
int t = x; // 把x的值保存在临时变量t中, t现在是1
x = y; // x现在是2
y = t; // y现在是t的值1
- 实际上,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));
}
}
// 测试
[8, 12, 18, 28, 36, 50, 65, 73, 89, 96]
对于数组排序的解释:
- 必须注意,对数组排序实际上修改了数组本身,例如:排序前的数组是:
int[] ns = { 9, 3, 6, 5 };
练习:
请思考如何实现对数组进行降序排序:
方式一:
- Arrays.toString(ns) 只是简单的将某一个数组全部打印出来,sort() 方法才是用来排序的
方式二:
小总结:
- 常用的排序算法有冒泡排序、插入排序和快速排序等;
- 冒泡排序使用两层for循环实现排序;
- 交换两个变量的值需要借助一个临时变量。
- 可以直接使用Java标准库提供的Arrays.sort()进行排序;
- 对数组排序会直接修改数组本身。修改了指向,
多维数组
二维数组
- 二维数组就是数组的数组。定义一个二维数组如下:
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 看的是行
}
}
// java中的二维数组用的是{{}...}
二维数组的内存结构
- 如果我们定义一个普通数组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); // [I@71a794e5 地址
System.out.println(arr0.length); // 4
}
}
解释:实际上arr0就获取了ns数组的第0个元素。因为ns数组的每个元素也是一个数组,因此,arr0指向的数组就是{ 1, 2, 3, 4 }。在内存中,结构如下:
- 访问二维数组的某个元素需要使用array[row][col],直接具体到行和列例如:
而且行和列都是从0开始的
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));
}
}
// [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
三维数组
三维数组就是二维数组的数组。可以这么定义一个三维数组:
int[][][] ns = {
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
},
{
{10, 11},
{12, 13}
},
{
{14, 15, 16},
{17, 18}
}
};
- 三维数组的内存结构图:
- 如果我们要访问三维数组的某个元素,例如,ns[2][0][1],只需要顺着定位找到对应的最终元素15即可。
- 理论上,我们可以定义任意的N维数组。但在实际应用中,除了二维数组在某些时候还能用得上,更高维度的数组很少使用。
练习
使用二维数组可以表示一组学生的各科成绩,请计算所有学生的平均分:
方式一循环:
方式二循环:
小总结:
- 二维数组就是数组的数组,三维数组就是二维数组的数组;
- 多维数组的每个数组元素长度都不要求相同;
- 打印多维数组可以使用Arrays.deepToString();
- 最常见的多维数组是二维数组,访问二维数组的一个元素使用array[row][col]
命令行参数
- 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;
}
}
}
}
小总结:
- 命令行参数类型是String[]数组;
- 命令行参数由JVM接收用户输入并传给main方法;
- 如何解析命令行参数需要由程序自己实现。
面向对象编程
面向对象基础
方法
- 一个class可以包含多个field,例如,我们给Person类就定义了两个field:
class Person {
public String name;
public int age;
}
- 但是,直接把field用public暴露给外部可能会破坏封装性。比如,代码可以这样写:
Person ming = new Person();
ming.name = "Xiao Ming";
ming.age = -99; // age设置为负数
- 显然,直接操作field,容易造成逻辑混乱。为了避免外部代码直接去访问field,我们可以用private修饰field,拒绝外部访问
class Person {
private String name;
private int age;
}
- 试试private修饰的field有什么效果:
public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.name = "Xiao Ming"; // 对字段name赋值
ming.age = 12; // 对字段age赋值
}
}
class Person {
private String name;
private int age;
}
// 编译以后就出错了
Main.java:5: error: name has private access in Person
ming.name = "Xiao Ming"; // 对字段name赋值
^
Main.java:6: error: age has private access in Person
ming.age = 12; // 对字段age赋值
^
2 errors
error: compilation failed
- 是不是编译报错?把访问field的赋值语句去了就可以正常编译了。
- 把field从public改成private,外部代码不能访问这些field,那我们定义这些field有什么用?怎么才能给它赋值?怎么才能读取它的值?
- 所以我们需要使用方法(method)来让外部代码可以间接修改field:
即使用set以及get方法来进行实现
public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setName("Xiao Ming"); // 设置name
ming.setAge(12); // 设置age
System.out.println(ming.getName() + ", " + ming.getAge());
}
}
class Person {
private String name;
private int age;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
if (age < 0 || age > 100) {
throw new IllegalArgumentException("invalid age value");
}
this.age = age;
}
}
// 测试
Xiao Ming, 12
- 虽然外部代码不能直接修改private字段,但是,外部代码可以调用方法setName()和setAge()来间接修改private字段。在方法内部,我们就有机会检查参数对不对。比如,setAge()就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把age设置成不合理的值。
- 对setName()方法同样可以做检查,例如,不允许传入null和空字符串:
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("invalid name");
}
this.name = name.strip(); // 去掉首尾空格
}
- 同样,外部代码不能直接读取private字段,但可以通过getName()和getAge()间接获取private字段的值。、
- 所以,一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。
- 调用方法的语法是实例变量.方法名(参数);。一个方法调用就是一个语句,所以不要忘了在末尾加;。例如:ming.setName(“Xiao Ming”);。
定义方法
从上面的代码可以看出,定义方法的语法是:
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
- 方法返回值通过return语句实现,如果没有返回值,返回类型设置为void,可以省略return。
private方法
有public方法,自然就有private方法。和private字段一样,private方法不允许外部调用,那我们定义private方法有什么用?
答:
定义private方法的理由是内部方法是可以调用private方法的。例如:
public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setBirth(2008);
System.out.println(ming.getAge());
}
}
class Person {
private String name;
private int birth;
public void setBirth(int birth) {
this.birth = birth;
}
public int getAge() {
return calcAge(2019); // 调用private方法
}
// private方法:
private int calcAge(int currentYear) {
return currentYear - this.birth;
}
}
// 测试
11
- 观察上述代码,calcAge()是一个private方法,外部代码无法调用,但是,内部方法getAge()可以调用它。
- 此外,我们还注意到,这个Person类只定义了birth字段,没有定义age字段,获取age时,通过方法getAge()返回的是一个实时计算的值,并非存储在某个字段的值。这说明方法可以封装一个类的对外接口,调用方不需要知道也不关心Person实例在内部到底有没有age字段。
this变量
- 在方法内部,可以使用一个隐含的变量this,**它始终指向当前实例。**因此,通过this.field就可以访问当前实例的字段。
- 如果没有命名冲突,可以省略this。例如:
class Person {
private String name;
public String getName() {
return name; // 相当于this.name,里面的某一个方法
}
}
- 但是,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this:
class Person {
private String name;
public void setName(String name) {
this.name = name; // 前面的this不可少,少了就变成局部变量name了
}
}
方法参数
- 方法可以包含0个或任意个参数。方法参数用于接收传递给方法的变量值。调用方法时,必须严格按照参数的定义一一传递。例如:
class Person {
...
public void setNameAndAge(String name, int age) {
...
}
}
- 调用这个setNameAndAge()方法时,必须有两个参数,且第一个参数必须为String,第二个参数必须为int:
Person ming = new Person();
ming.setNameAndAge("Xiao Ming"); // 编译错误:参数个数不对
ming.setNameAndAge(12, "Xiao Ming"); // 编译错误:参数类型不对
可变参数
- 可变参数用类型…定义,可变参数相当于数组类型:
class Group {
private String[] names;
public void setNames(String... names) {
this.names = names;
}
}
- 上面的setNames()就定义了一个可变参数。调用时,可以这么写:
Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames("Xiao Ming"); // 传入1个String
g.setNames(); // 传入0个String
- 完全可以把可变参数改写为String[]类型:
class Group {
private String[] names;
public void setNames(String[] names) {
this.names = names;
}
}
- 但是,调用方需要自己先构造String[],比较麻烦。例如:
Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]
- 另一个问题是,调用方可以传入null:
Group g = new Group();
g.setNames(null);
- 而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null。
参数绑定
- 调用方把参数传递给实例方法时,调用时传递的值会按参数位置一一绑定。
- 那什么是参数绑定?
- 我们先观察一个基本类型参数的传递:
public class Main {
public static void main(String[] args) {
Person p = new Person();
int n = 15; // n的值为15
p.setAge(n); // 传入n的值
System.out.println(p.getAge()); // 15
n = 20; // n的值改为20
System.out.println(p.getAge()); // 15还是20?
}
}
class Person {
private int age;
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
// 测试
15
15
- 运行代码,从结果可知,修改外部的局部变量n,不影响实例p的age字段,原因是setAge()方法获得的参数,复制了n的值,因此,p.age和局部变量n互不影响。
- 结论:基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
- 我们再看一个传递引用参数的例子:
public class Main {
public static void main(String[] args) {
Person p = new Person();
String[] fullname = new String[] { "Homer", "Simpson" };
p.setName(fullname); // 传入fullname数组
System.out.println(p.getName()); // "Homer Simpson"
fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart"
System.out.println(p.getName()); // "Homer Simpson"还是"Bart Simpson"?
}
}
class Person {
private String[] name;
public String getName() {
return this.name[0] + " " + this.name[1];
}
public void setName(String[] name) {
this.name = name;
}
}
// 测试
Homer Simpson
Bart Simpson
- 注意到setName()的参数现在是一个数组。一开始,把fullname数组传进去,然后,修改fullname数组的内容,结果发现,实例p的字段p.name也被修改了!
- 结论:引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。
- 有了上面的结论, 我们再看一个例子:
public class Main {
public static void main(String[] args) {
Person p = new Person();
String bob = "Bob";
p.setName(bob); // 传入bob变量
System.out.println(p.getName()); // "Bob"
bob = "Alice"; // bob改名为Alice
System.out.println(p.getName()); // "Bob"还是"Alice"?
}
}
class Person {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
// 测试
Bob
Bob
- 不要怀疑引用参数绑定的机制,试解释为什么上面的代码两次输出都是"Bob"。
练习
小总结:
- 方法可以让外部代码安全地访问实例字段;
- 方法是一组执行语句,并且可以执行任意逻辑;
- 方法内部遇到return时返回,void表示不返回任何值(注意和返回null不同);
- 外部代码通过public方法操作实例,内部代码可以调用private方法;
- 理解方法的参数绑定。
构造方法
- 创建实例的时候,我们经常需要同时初始化这个实例的字段,例如:
Person ming = new Person();
ming.setName("小明");
ming.setAge(12);
- 初始化对象实例需要3行代码,而且,如果忘了调用setName()或者setAge(),这个实例内部的状态就是不正确的。
- 能否在创建对象实例时就把内部字段全部初始化为合适的值?
完全可以,这时候我们就需要使用构造方法。 - 创建实例的时候,实际上是通过构造方法来初始化实例的。我们先来定义一个构造方法,能在创建Person实例的时候,一次性传入name和age,完成初始化:
public class Main {
public static void main(String[] args) {
Person p = new Person("Xiao Ming", 15);
System.out.println(p.getName());
System.out.println(p.getAge());
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
// 测试
Xiao Ming
15
- 由于构造方法是如此特殊,所以构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。
默认构造方法
- 是不是任何class都有构造方法?是的。
- 那前面我们并没有为Person类编写构造方法,为什么可以调用new Person()?
答:原因是如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似这样:
class Person {
public Person() {
}
}
- 要特别注意的是,如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法:
public class Main {
public static void main(String[] args) {
Person p = new Person(); // 编译错误:找不到这个构造方法
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
// 测试:报错
Main.java:4: error: constructor Person in class Person cannot be applied to given types;
Person p = new Person(); // 编译错误:找不到这个构造方法
^
required: String,int
found: no arguments
reason: actual and formal argument lists differ in length
1 error
error: compilation failed
- 如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来:
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Xiao Ming", 15); // 既可以调用带参数的构造方法
Person p2 = new Person(); // 也可以调用无参数构造方法
}
}
class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
- 没有在构造方法中初始化字段时,引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false:
class Person {
private String name; // 默认初始化为null
private int age; // 默认初始化为0
public Person() {
}
}
- 也可以对字段直接进行初始化:
class Person {
private String name = "Unamed";
private int age = 10;
}
- 那么问题来了:既对字段进行初始化,又在构造方法中对字段进行初始化:
class Person {
private String name = "Unamed";
private int age = 10;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
- 当我们创建对象的时候,new Person(“Xiao Ming”, 12)得到的对象实例,字段的初始值是啥?
- 在Java中,创建对象实例的时候,按照如下顺序进行初始化:
① 先初始化字段,例如,int age = 10;表示字段初始化为10,double salary;表示字段默认初始化为0,String name;表示引用类型字段默认初始化为null;
② 执行构造方法的代码进行初始化。 - 因此,构造方法的代码由于后运行,所以,new Person(“Xiao Ming”, 12)的字段值最终由构造方法的代码确定。
多构造方法
- 可以定义多个构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this.name = name;
this.age = 12;
}
public Person() {
}
}
解释:
- 如果调用new Person(“Xiao Ming”, 20);,会自动匹配到构造方法public Person(String, int)。
- 如果调用new Person(“Xiao Ming”);,会自动匹配到构造方法public Person(String)。
- 如果调用new Person();,会自动匹配到构造方法public Person()。
- 一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…):,
调用其他构造方法使用的语法是this(…)
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
}
public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String)
}
}
练习
请给Person类增加(String, int)的构造方法:
小总结:
- get 方法照常书写即可
- 实例在创建时通过new操作符会调用其对应的构造方法,构造方法用于初始化实例;
- 没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法;
- 可以定义多个构造方法,编译器根据参数自动判断;
- 可以在一个构造方法内部调用另一个构造方法,便于代码复用。
方法重载
在一个类中,我们可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。例如,在Hello类中,定义多个hello()方法:
class Hello {
public void hello() {
System.out.println("Hello, world!");
}
public void hello(String name) {
System.out.println("Hello, " + name + "!");
}
public void hello(String name, int age) {
if (age < 18) {
System.out.println("Hi, " + name + "!");
} else {
System.out.println("Hello, " + name + "!");
}
}
}
- 这种方法名相同,但各自的参数不同,称为方法重载(Overload)。
- 注意:方法重载的返回值类型通常都是相同的。
- 方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。
举一个稍微具体一点的例子, String类提供了多个重载方法indexOf(),可以查找子串:
- int indexOf(int ch):根据字符的Unicode码查找;
- int indexOf(int ch):根据字符的Unicode码查找;
- int indexOf(int ch, int fromIndex):根据字符查找,但指定起始位置;
- int indexOf(String str, int fromIndex)根据字符串查找,但指定起始位置。
public class Main {
public static void main(String[] args) {
String s = "Test string";
int n1 = s.indexOf('t');
int n2 = s.indexOf("st");
int n3 = s.indexOf("st", 4);
System.out.println(n1);
System.out.println(n2);
System.out.println(n3);
}
}
// 测试
3
2
5
练习:
小总结:
- 方法重载是指多个方法的方法名相同,但各自的参数不同;
- 重载方法应该完成类似的功能,参考String的indexOf();
- 重载方法返回值类型应该相同。
集合
Java集合简介
- 什么是集合(Collection)?集合就是“由若干个确定的元素所构成的整体”。例如,5只小兔构成的集合:
- 在数学中,我们经常遇到集合的概念。例如:
- 有限集合:① 一个班所有的同学构成的集合;② 一个网站所有的商品构成的集合; ③ …
- 无限集合:① 全体自然数集合:1,2,3,…… ② 有理数集合;
- 为什么要在计算机中引入集合呢?这是为了便于处理一组类似的数据,例如:
① 计算所有同学的总成绩和平均成绩; ② 列举所有的商品名称和价格等 - 在Java中,如果一个Java对象可以在内部持有若干其他Java对象,并对外提供访问接口,我们把这种Java对象称为集合。很显然,Java的数组可以看作是一种集合:
String[] ss = new String[10]; // 可以持有10个String对象
ss[0] = "Hello"; // 可以放入String对象
String first = ss[0]; // 可以获取String对象
- 既然Java提供了数组这种数据类型,可以充当集合,那么,我们为什么还需要其他集合类?这是因为数组有如下限制:
① 数组初始化后大小不可变;② 数组只能按索引顺序存取。 - 因此,我们需要各种不同类型的集合类来处理不同的数据,例如:
① 可变大小的顺序链表;② 保证无重复元素的集合;
Collection
Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口。Java的java.util包主要提供了以下三种类型的集合:
- List:一种有序列表的集合,例如,按索引排列的Student的List;
- Set:一种保证没有重复元素的集合,例如,所有无重复名称的Student的Set;
- Map:一种通过键值(key-value)查找的映射表集合,例如,根据Student的name查找对应Student的Map。
Java集合的设计有几个特点:
- 一是实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类有ArrayList,
LinkedList等, - 二是支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素,例如:
List<String> list = new ArrayList<>(); // 只能放入String类型
- 最后,Java访问集合总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。
- 由于Java的集合设计非常久远,中间经历过大规模改进,我们要注意到有一小部分集合类是遗留类,不应该继续使用:
① Hashtable:一种线程安全的Map实现 ② Vector:一种线程安全的List实现; ③ Stack:基于Vector实现的LIFO的栈。还有一小部分接口是遗留接口,也不应该继续使用:Enumeration:已被Iterator取代。 – 以上就是有点印象即可
小总结:
- Java的集合类定义在java.util包中,支持泛型,主要提供了3种集合类,包括List,Set和Map。Java集合使用统一的Iterator遍历,尽量不要使用遗留接口。
使用List
- 在集合类中,List是最基础的一种集合:它是一种有序列表。
- List的行为和数组几乎完全相同:List内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置,List的索引和数组一样,从0开始。
- 数组和List类似,也是有序结构,如果我们使用数组,在添加和删除元素的时候,会非常不方便。例如,从一个已有的数组{‘A’, ‘B’, ‘C’, ‘D’, ‘E’}中删除索引为2的元素:
- 这个“删除”操作实际上是把’C’后面的元素依次往前挪一个位置,而“添加”操作实际上是把指定位置以后的元素都依次向后挪一个位置,腾出来的位置给新加的元素。这两种操作,用数组实现非常麻烦。
- 因此,在实际应用中,需要增删元素的有序列表,我们使用最多的是ArrayList。实际上,ArrayList在内部使用了数组来存储所有元素。例如,一个ArrayList拥有5个元素,实际数组大小为6(即有一个空位):
我们考察List接口,可以看到几个主要的接口方法: - 在末尾添加一个元素:boolean add(E e)
- 在指定索引添加一个元素:boolean add(int index, E e)
- 删除指定索引的元素:E remove(int index)
- 删除某个元素:boolean remove(Object e)
- 获取指定索引的元素:E get(int index)
- 获取链表大小(包含元素的个数):int size()
但是,实现List接口并非只能通过数组(即ArrayList的实现方式)来实现,另一种LinkedList通过“链表”也实现了List接口。在LinkedList中,它的内部每个元素都指向下一个元素: – LinkedList 实现的是链表的形态
- 我们来比较一下ArrayList和LinkedList:
- 一般情况下,用ArrayList会比较多一点
List的特点 - 使用List时,我们要关注List接口的规范。List接口允许我们添加重复的元素,即List内部的元素可以重复:
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple"); // size=1
list.add("pear"); // size=2
list.add("apple"); // 允许重复添加元素,size=3
System.out.println(list.size());
}
}
// 测试
3
List还允许添加null:
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple"); // size=1
list.add(null); // size=2
list.add("pear"); // size=3
String second = list.get(1); // null
System.out.println(second);
}
}
// 测试
3
创建List
除了使用ArrayList和LinkedList,我们还可以通过List接口提供的of()方法,根据给定元素快速创建List:
List<Integer> list = List.of(1, 2, 5);
- 但是List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常。
遍历List
- 和数组类型,我们要遍历一个List,完全可以用for循环根据索引配合get(int)方法遍历:
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (int i=0; i<list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
}
}
- 但这种方式并不推荐,一是代码复杂,二是因为get(int)方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢。
- 所以我们要始终坚持使用迭代器Iterator来访问List。Iterator本身也是一个对象,但它是由List的实例调用iterator()方法的时候创建的。Iterator对象知道如何遍历一个List,并且不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率。
- Iterator对象有两个方法:boolean hasNext()判断是否有下一个元素,E next()返回下一个元素。因此,使用Iterator遍历List代码如下
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
}
}
// 测试
apple
pear
banana
- 有童鞋可能觉得使用Iterator访问List的代码比使用索引更复杂。但是,要记住,通过Iterator遍历List永远是最高效的方式。并且,由于Iterator遍历是如此常用,所以,Java的for each循环本身就可以帮我们使用Iterator遍历。把上面的代码再改写如下:
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (String s : list) {
System.out.println(s);
}
}
}
// 测试
apple
pear
banana
- 上述代码就是我们编写遍历List的常见代码。
- 实际上,只要实现了Iterable接口的集合类都可以直接用for each循环来遍历,Java编译器本身并不知道如何遍历集合对象,但它会自动把for each循环变成Iterator的调用,原因就在于Iterable接口定义了一个Iterator iterator()方法,强迫集合类必须返回一个Iterator实例。
List和Array转换
- 把List变为Array有三种方法,第一种是调用toArray()方法直接返回一个Object[]数组:
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
Object[] array = list.toArray();
for (Object s : array) {
System.out.println(s);
}
}
}
// 测试
apple
pear
banana
- 这种方法会丢失类型信息,所以实际应用很少。
- 第二种方式是给toArray(T[])传入一个类型相同的Array,List内部自动把元素复制到传入的Array中:
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Integer[] array = list.toArray(new Integer[3]);
for (Integer n : array) {
System.out.println(n);
}
}
}
// 测试
12
34
56
- 注意到这个toArray(T[])方法的泛型参数并不是List接口定义的泛型参数,所以,我们实际上可以传入其他类型的数组,例如我们传入Number类型的数组,返回的仍然是Number类型:
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Number[] array = list.toArray(new Number[3]);
for (Number n : array) {
System.out.println(n);
}
}
}
- 但是,如果我们传入类型不匹配的数组,例如,String[]类型的数组,由于List的元素是Integer,所以无法放入String数组,这个方法会抛出ArrayStoreException。
- 如果我们传入的数组大小和List实际的元素个数不一致怎么办?根据List接口的文档,我们可以知道:如果传入的数组不够大,那么List内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List元素还要多,那么填充完元素后,剩下的数组元素一律填充null。
- 实际上,最常用的是传入一个“恰好”大小的数组:
Integer[] array = list.toArray(new Integer[list.size()]);
- 最后一种更简洁的写法是通过List接口定义的T[] toArray(IntFunction<T[]> generator)方法:
Integer[] array = list.toArray(Integer[]::new);
- 这种函数式写法我们会在后续讲到。
- 反过来,把Array变为List就简单多了,通过List.of(T…)方法最简单:
Integer[] array = { 1, 2, 3 };
List<Integer> list = List.of(array);
- 对于JDK 11之前的版本,可以使用Arrays.asList(T…)方法把数组转换成List。
- 要注意的是,返回的List不一定就是ArrayList或者LinkedList,因为List只是一个接口,如果我们调用List.of(),它返回的是一个只读List:
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
list.add(999); // UnsupportedOperationException
}
}
- 对只读List调用add()、remove()方法会抛出UnsupportedOperationException。